diff --git a/docs/changelog.md b/docs/changelog.md
index e8192c08..8bc85cd9 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -23,6 +23,8 @@ Requires libvips v8.6.1.
* Improve install time error messages for FreeBSD users.
[#1310](https://github.com/lovell/sharp/issues/1310)
+* Add experimental entropy field to stats response.
+
#### v0.20.5 - 27th June 2018
* Expose libjpeg optimize_coding flag.
diff --git a/lib/input.js b/lib/input.js
index f29e24e6..041f7a42 100644
--- a/lib/input.js
+++ b/lib/input.js
@@ -264,6 +264,7 @@ function metadata (callback) {
* - `maxX` (x-coordinate of one of the pixel where the maximum lies)
* - `maxY` (y-coordinate of one of the pixel where the maximum lies)
* - `isOpaque`: Value to identify if the image is opaque or transparent, based on the presence and use of alpha channel
+ * - `entropy`: Histogram-based estimation of greyscale entropy, discarding alpha channel if any (experimental)
*
* @example
* const image = sharp(inputJpg);
diff --git a/src/stats.cc b/src/stats.cc
index f9ef1e1e..2265589d 100644
--- a/src/stats.cc
+++ b/src/stats.cc
@@ -59,7 +59,6 @@ class StatsWorker : public Nan::AsyncWorker {
using sharp::MaximumImageAlpha;
vips::VImage image;
- vips::VImage stats;
sharp::ImageType imageType = sharp::ImageType::UNKNOWN;
try {
@@ -69,9 +68,8 @@ class StatsWorker : public Nan::AsyncWorker {
}
if (imageType != sharp::ImageType::UNKNOWN) {
try {
- stats = image.stats();
- int bands = image.bands();
- double const max = MaximumImageAlpha(image.interpretation());
+ vips::VImage stats = image.stats();
+ int const bands = image.bands();
for (int b = 1; b <= bands; b++) {
ChannelStats cStats(static_cast(stats.getpoint(STAT_MIN_INDEX, b).front()),
static_cast(stats.getpoint(STAT_MAX_INDEX, b).front()),
@@ -83,11 +81,15 @@ class StatsWorker : public Nan::AsyncWorker {
static_cast(stats.getpoint(STAT_MAXY_INDEX, b).front()));
baton->channelStats.push_back(cStats);
}
-
- // alpha layer is there and the last band i.e. alpha has its max value greater than 0)
- if (sharp::HasAlpha(image) && stats.getpoint(STAT_MIN_INDEX, bands).front() != max) {
- baton->isOpaque = false;
+ // Image is not opaque when alpha layer is present and contains a non-mamixa value
+ if (sharp::HasAlpha(image)) {
+ double const minAlpha = static_cast(stats.getpoint(STAT_MIN_INDEX, bands).front());
+ if (minAlpha != MaximumImageAlpha(image.interpretation())) {
+ baton->isOpaque = false;
+ }
}
+ // Estimate entropy via histogram of greyscale value frequency
+ baton->entropy = std::abs(image.colourspace(VIPS_INTERPRETATION_B_W)[0].hist_find().hist_entropy());
} catch (vips::VError const &err) {
(baton->err).append(err.what());
}
@@ -130,6 +132,7 @@ class StatsWorker : public Nan::AsyncWorker {
Set(info, New("channels").ToLocalChecked(), channels);
Set(info, New("isOpaque").ToLocalChecked(), New(baton->isOpaque));
+ Set(info, New("entropy").ToLocalChecked(), New(baton->entropy));
argv[1] = info;
}
diff --git a/src/stats.h b/src/stats.h
index 69f3a371..11fba383 100644
--- a/src/stats.h
+++ b/src/stats.h
@@ -51,12 +51,14 @@ struct StatsBaton {
// Output
std::vector channelStats;
bool isOpaque;
+ double entropy;
std::string err;
StatsBaton():
input(nullptr),
- isOpaque(true)
+ isOpaque(true),
+ entropy(0.0)
{}
};
diff --git a/test/unit/stats.js b/test/unit/stats.js
index de13a9b6..403978de 100644
--- a/test/unit/stats.js
+++ b/test/unit/stats.js
@@ -24,6 +24,7 @@ describe('Image Stats', function () {
if (err) throw err;
assert.strictEqual(true, stats.isOpaque);
+ assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541));
// red channel
assert.strictEqual(0, stats.channels[0]['min']);
@@ -82,6 +83,7 @@ describe('Image Stats', function () {
if (err) throw err;
assert.strictEqual(true, stats.isOpaque);
+ assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0.3409031108021736));
// red channel
assert.strictEqual(0, stats.channels[0]['min']);
@@ -105,7 +107,9 @@ describe('Image Stats', function () {
it('PNG with transparency', function (done) {
sharp(fixtures.inputPngWithTransparency).stats(function (err, stats) {
if (err) throw err;
+
assert.strictEqual(false, stats.isOpaque);
+ assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0.06778064835816622));
// red channel
assert.strictEqual(0, stats.channels[0]['min']);
@@ -180,6 +184,7 @@ describe('Image Stats', function () {
if (err) throw err;
assert.strictEqual(false, stats.isOpaque);
+ assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0));
// alpha channel
assert.strictEqual(0, stats.channels[3]['min']);
@@ -204,7 +209,9 @@ describe('Image Stats', function () {
it('Tiff', function (done) {
sharp(fixtures.inputTiff).stats(function (err, stats) {
if (err) throw err;
+
assert.strictEqual(true, stats.isOpaque);
+ assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0.3851250782608986));
// red channel
assert.strictEqual(0, stats.channels[0]['min']);
@@ -231,6 +238,7 @@ describe('Image Stats', function () {
if (err) throw err;
assert.strictEqual(true, stats.isOpaque);
+ assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.51758075132966));
// red channel
assert.strictEqual(0, stats.channels[0]['min']);
@@ -289,6 +297,7 @@ describe('Image Stats', function () {
if (err) throw err;
assert.strictEqual(true, stats.isOpaque);
+ assert.strictEqual(true, isInAcceptableRange(stats.entropy, 6.087309412541799));
// red channel
assert.strictEqual(35, stats.channels[0]['min']);
@@ -345,7 +354,9 @@ describe('Image Stats', function () {
it('Grayscale GIF with alpha', function (done) {
sharp(fixtures.inputGifGreyPlusAlpha).stats(function (err, stats) {
if (err) throw err;
+
assert.strictEqual(false, stats.isOpaque);
+ assert.strictEqual(true, isInAcceptableRange(stats.entropy, 1));
// gray channel
assert.strictEqual(0, stats.channels[0]['min']);
@@ -387,7 +398,9 @@ describe('Image Stats', function () {
const readable = fs.createReadStream(fixtures.inputJpg);
const pipeline = sharp().stats(function (err, stats) {
if (err) throw err;
+
assert.strictEqual(true, stats.isOpaque);
+ assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541));
// red channel
assert.strictEqual(0, stats.channels[0]['min']);
@@ -449,6 +462,7 @@ describe('Image Stats', function () {
return pipeline.stats().then(function (stats) {
assert.strictEqual(true, stats.isOpaque);
+ assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541));
// red channel
assert.strictEqual(0, stats.channels[0]['min']);
@@ -505,6 +519,7 @@ describe('Image Stats', function () {
it('File in, Promise out', function () {
return sharp(fixtures.inputJpg).stats().then(function (stats) {
assert.strictEqual(true, stats.isOpaque);
+ assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541));
// red channel
assert.strictEqual(0, stats.channels[0]['min']);