Add experimental entropy field to stats response

This commit is contained in:
Lovell Fuller 2018-08-06 15:41:27 +01:00
parent 532de4ecab
commit 25bd2cea3e
5 changed files with 32 additions and 9 deletions

View File

@ -23,6 +23,8 @@ Requires libvips v8.6.1.
* Improve install time error messages for FreeBSD users. * Improve install time error messages for FreeBSD users.
[#1310](https://github.com/lovell/sharp/issues/1310) [#1310](https://github.com/lovell/sharp/issues/1310)
* Add experimental entropy field to stats response.
#### v0.20.5 - 27<sup>th</sup> June 2018 #### v0.20.5 - 27<sup>th</sup> June 2018
* Expose libjpeg optimize_coding flag. * Expose libjpeg optimize_coding flag.

View File

@ -264,6 +264,7 @@ function metadata (callback) {
* - `maxX` (x-coordinate of one of the pixel where the maximum lies) * - `maxX` (x-coordinate of one of the pixel where the maximum lies)
* - `maxY` (y-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 * - `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 * @example
* const image = sharp(inputJpg); * const image = sharp(inputJpg);

View File

@ -59,7 +59,6 @@ class StatsWorker : public Nan::AsyncWorker {
using sharp::MaximumImageAlpha; using sharp::MaximumImageAlpha;
vips::VImage image; vips::VImage image;
vips::VImage stats;
sharp::ImageType imageType = sharp::ImageType::UNKNOWN; sharp::ImageType imageType = sharp::ImageType::UNKNOWN;
try { try {
@ -69,9 +68,8 @@ class StatsWorker : public Nan::AsyncWorker {
} }
if (imageType != sharp::ImageType::UNKNOWN) { if (imageType != sharp::ImageType::UNKNOWN) {
try { try {
stats = image.stats(); vips::VImage stats = image.stats();
int bands = image.bands(); int const bands = image.bands();
double const max = MaximumImageAlpha(image.interpretation());
for (int b = 1; b <= bands; b++) { for (int b = 1; b <= bands; b++) {
ChannelStats cStats(static_cast<int>(stats.getpoint(STAT_MIN_INDEX, b).front()), ChannelStats cStats(static_cast<int>(stats.getpoint(STAT_MIN_INDEX, b).front()),
static_cast<int>(stats.getpoint(STAT_MAX_INDEX, b).front()), static_cast<int>(stats.getpoint(STAT_MAX_INDEX, b).front()),
@ -83,11 +81,15 @@ class StatsWorker : public Nan::AsyncWorker {
static_cast<int>(stats.getpoint(STAT_MAXY_INDEX, b).front())); static_cast<int>(stats.getpoint(STAT_MAXY_INDEX, b).front()));
baton->channelStats.push_back(cStats); baton->channelStats.push_back(cStats);
} }
// Image is not opaque when alpha layer is present and contains a non-mamixa value
// alpha layer is there and the last band i.e. alpha has its max value greater than 0) if (sharp::HasAlpha(image)) {
if (sharp::HasAlpha(image) && stats.getpoint(STAT_MIN_INDEX, bands).front() != max) { double const minAlpha = static_cast<double>(stats.getpoint(STAT_MIN_INDEX, bands).front());
baton->isOpaque = false; 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) { } catch (vips::VError const &err) {
(baton->err).append(err.what()); (baton->err).append(err.what());
} }
@ -130,6 +132,7 @@ class StatsWorker : public Nan::AsyncWorker {
Set(info, New("channels").ToLocalChecked(), channels); Set(info, New("channels").ToLocalChecked(), channels);
Set(info, New("isOpaque").ToLocalChecked(), New<v8::Boolean>(baton->isOpaque)); Set(info, New("isOpaque").ToLocalChecked(), New<v8::Boolean>(baton->isOpaque));
Set(info, New("entropy").ToLocalChecked(), New<v8::Number>(baton->entropy));
argv[1] = info; argv[1] = info;
} }

View File

@ -51,12 +51,14 @@ struct StatsBaton {
// Output // Output
std::vector<ChannelStats> channelStats; std::vector<ChannelStats> channelStats;
bool isOpaque; bool isOpaque;
double entropy;
std::string err; std::string err;
StatsBaton(): StatsBaton():
input(nullptr), input(nullptr),
isOpaque(true) isOpaque(true),
entropy(0.0)
{} {}
}; };

View File

@ -24,6 +24,7 @@ describe('Image Stats', function () {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, stats.isOpaque); assert.strictEqual(true, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541));
// red channel // red channel
assert.strictEqual(0, stats.channels[0]['min']); assert.strictEqual(0, stats.channels[0]['min']);
@ -82,6 +83,7 @@ describe('Image Stats', function () {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, stats.isOpaque); assert.strictEqual(true, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0.3409031108021736));
// red channel // red channel
assert.strictEqual(0, stats.channels[0]['min']); assert.strictEqual(0, stats.channels[0]['min']);
@ -105,7 +107,9 @@ describe('Image Stats', function () {
it('PNG with transparency', function (done) { it('PNG with transparency', function (done) {
sharp(fixtures.inputPngWithTransparency).stats(function (err, stats) { sharp(fixtures.inputPngWithTransparency).stats(function (err, stats) {
if (err) throw err; if (err) throw err;
assert.strictEqual(false, stats.isOpaque); assert.strictEqual(false, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0.06778064835816622));
// red channel // red channel
assert.strictEqual(0, stats.channels[0]['min']); assert.strictEqual(0, stats.channels[0]['min']);
@ -180,6 +184,7 @@ describe('Image Stats', function () {
if (err) throw err; if (err) throw err;
assert.strictEqual(false, stats.isOpaque); assert.strictEqual(false, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0));
// alpha channel // alpha channel
assert.strictEqual(0, stats.channels[3]['min']); assert.strictEqual(0, stats.channels[3]['min']);
@ -204,7 +209,9 @@ describe('Image Stats', function () {
it('Tiff', function (done) { it('Tiff', function (done) {
sharp(fixtures.inputTiff).stats(function (err, stats) { sharp(fixtures.inputTiff).stats(function (err, stats) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, stats.isOpaque); assert.strictEqual(true, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0.3851250782608986));
// red channel // red channel
assert.strictEqual(0, stats.channels[0]['min']); assert.strictEqual(0, stats.channels[0]['min']);
@ -231,6 +238,7 @@ describe('Image Stats', function () {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, stats.isOpaque); assert.strictEqual(true, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.51758075132966));
// red channel // red channel
assert.strictEqual(0, stats.channels[0]['min']); assert.strictEqual(0, stats.channels[0]['min']);
@ -289,6 +297,7 @@ describe('Image Stats', function () {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, stats.isOpaque); assert.strictEqual(true, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 6.087309412541799));
// red channel // red channel
assert.strictEqual(35, stats.channels[0]['min']); assert.strictEqual(35, stats.channels[0]['min']);
@ -345,7 +354,9 @@ describe('Image Stats', function () {
it('Grayscale GIF with alpha', function (done) { it('Grayscale GIF with alpha', function (done) {
sharp(fixtures.inputGifGreyPlusAlpha).stats(function (err, stats) { sharp(fixtures.inputGifGreyPlusAlpha).stats(function (err, stats) {
if (err) throw err; if (err) throw err;
assert.strictEqual(false, stats.isOpaque); assert.strictEqual(false, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 1));
// gray channel // gray channel
assert.strictEqual(0, stats.channels[0]['min']); assert.strictEqual(0, stats.channels[0]['min']);
@ -387,7 +398,9 @@ describe('Image Stats', function () {
const readable = fs.createReadStream(fixtures.inputJpg); const readable = fs.createReadStream(fixtures.inputJpg);
const pipeline = sharp().stats(function (err, stats) { const pipeline = sharp().stats(function (err, stats) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, stats.isOpaque); assert.strictEqual(true, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541));
// red channel // red channel
assert.strictEqual(0, stats.channels[0]['min']); assert.strictEqual(0, stats.channels[0]['min']);
@ -449,6 +462,7 @@ describe('Image Stats', function () {
return pipeline.stats().then(function (stats) { return pipeline.stats().then(function (stats) {
assert.strictEqual(true, stats.isOpaque); assert.strictEqual(true, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541));
// red channel // red channel
assert.strictEqual(0, stats.channels[0]['min']); assert.strictEqual(0, stats.channels[0]['min']);
@ -505,6 +519,7 @@ describe('Image Stats', function () {
it('File in, Promise out', function () { it('File in, Promise out', function () {
return sharp(fixtures.inputJpg).stats().then(function (stats) { return sharp(fixtures.inputJpg).stats().then(function (stats) {
assert.strictEqual(true, stats.isOpaque); assert.strictEqual(true, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541));
// red channel // red channel
assert.strictEqual(0, stats.channels[0]['min']); assert.strictEqual(0, stats.channels[0]['min']);