From 25bd2cea3eb2bbdd348857e45991c97edce79e88 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Mon, 6 Aug 2018 15:41:27 +0100 Subject: [PATCH] Add experimental entropy field to stats response --- docs/changelog.md | 2 ++ lib/input.js | 1 + src/stats.cc | 19 +++++++++++-------- src/stats.h | 4 +++- test/unit/stats.js | 15 +++++++++++++++ 5 files changed, 32 insertions(+), 9 deletions(-) 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']);