From c42de19d2acc8fb29cff0e18a81421fb05e71e1b Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Wed, 15 Jul 2020 19:58:54 +0100 Subject: [PATCH] Add most dominant colour to image stats #640 --- docs/api-input.md | 4 ++- docs/changelog.md | 3 +++ lib/input.js | 4 ++- src/common.cc | 21 +++++++++++++++ src/common.h | 10 +++++++ src/operations.cc | 23 ---------------- src/operations.h | 10 ------- src/stats.cc | 17 ++++++++++++ src/stats.h | 8 +++++- test/unit/stats.js | 66 ++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 130 insertions(+), 36 deletions(-) diff --git a/docs/api-input.md b/docs/api-input.md index 5fa3f7ac..0b2509d1 100644 --- a/docs/api-input.md +++ b/docs/api-input.md @@ -72,6 +72,7 @@ A `Promise` is returned when `callback` is not provided. - `isOpaque`: Is the image fully opaque? Will be `true` if the image has no alpha channel or if every pixel is fully opaque. - `entropy`: Histogram-based estimation of greyscale entropy, discarding alpha channel if any (experimental) - `sharpness`: Estimation of greyscale sharpness based on the standard deviation of a Laplacian convolution, discarding alpha channel if any (experimental) +- `dominant`: Object containing most dominant sRGB colour based on a 4096-bin 3D histogram (experimental) ### Parameters @@ -89,7 +90,8 @@ image ``` ```javascript -const { entropy, sharpness } = await sharp(input).stats(); +const { entropy, sharpness, dominant } = await sharp(input).stats(); +const { r, g, b } = dominant; ``` Returns **[Promise][5]<[Object][6]>** diff --git a/docs/changelog.md b/docs/changelog.md index 37503ab3..ab668bfe 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -12,6 +12,9 @@ Requires libvips v8.10.0 * JPEG output `quality` >= 90 no longer automatically sets `chromaSubsampling` to `4:4:4`. +* Add most `dominant` colour to image `stats`. + [#640](https://github.com/lovell/sharp/issues/640) + ## v0.25 - *yield* Requires libvips v8.9.1 diff --git a/lib/input.js b/lib/input.js index 2a4bc105..9f2a3f2a 100644 --- a/lib/input.js +++ b/lib/input.js @@ -300,6 +300,7 @@ function metadata (callback) { * - `isOpaque`: Is the image fully opaque? Will be `true` if the image has no alpha channel or if every pixel is fully opaque. * - `entropy`: Histogram-based estimation of greyscale entropy, discarding alpha channel if any (experimental) * - `sharpness`: Estimation of greyscale sharpness based on the standard deviation of a Laplacian convolution, discarding alpha channel if any (experimental) + * - `dominant`: Object containing most dominant sRGB colour based on a 4096-bin 3D histogram (experimental) * * @example * const image = sharp(inputJpg); @@ -310,7 +311,8 @@ function metadata (callback) { * }); * * @example - * const { entropy, sharpness } = await sharp(input).stats(); + * const { entropy, sharpness, dominant } = await sharp(input).stats(); + * const { r, g, b } = dominant; * * @param {Function} [callback] - called with the arguments `(err, stats)` * @returns {Promise} diff --git a/src/common.cc b/src/common.cc index 0f6043a4..11fc9648 100644 --- a/src/common.cc +++ b/src/common.cc @@ -724,4 +724,25 @@ namespace sharp { return std::make_tuple(image, alphaColour); } + /* + Removes alpha channel, if any. + */ + VImage RemoveAlpha(VImage image) { + if (HasAlpha(image)) { + image = image.extract_band(0, VImage::option()->set("n", image.bands() - 1)); + } + return image; + } + + /* + Ensures alpha channel, if missing. + */ + VImage EnsureAlpha(VImage image) { + if (!HasAlpha(image)) { + std::vector alpha; + alpha.push_back(sharp::MaximumImageAlpha(image.interpretation())); + image = image.bandjoin_const(alpha); + } + return image; + } } // namespace sharp diff --git a/src/common.h b/src/common.h index f5e1d965..499066c7 100644 --- a/src/common.h +++ b/src/common.h @@ -271,6 +271,16 @@ namespace sharp { */ std::tuple> ApplyAlpha(VImage image, std::vector colour); + /* + Removes alpha channel, if any. + */ + VImage RemoveAlpha(VImage image); + + /* + Ensures alpha channel, if missing. + */ + VImage EnsureAlpha(VImage image); + } // namespace sharp #endif // SRC_COMMON_H_ diff --git a/src/operations.cc b/src/operations.cc index 95841e6e..bd3dfca4 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -27,29 +27,6 @@ using vips::VImage; using vips::VError; namespace sharp { - - /* - Removes alpha channel, if any. - */ - VImage RemoveAlpha(VImage image) { - if (HasAlpha(image)) { - image = image.extract_band(0, VImage::option()->set("n", image.bands() - 1)); - } - return image; - } - - /* - Ensures alpha channel, if missing. - */ - VImage EnsureAlpha(VImage image) { - if (!HasAlpha(image)) { - std::vector alpha; - alpha.push_back(sharp::MaximumImageAlpha(image.interpretation())); - image = image.bandjoin_const(alpha); - } - return image; - } - /* * Tint an image using the specified chroma, preserving the original image luminance */ diff --git a/src/operations.h b/src/operations.h index b642e247..6659ff8c 100644 --- a/src/operations.h +++ b/src/operations.h @@ -25,16 +25,6 @@ using vips::VImage; namespace sharp { - /* - Removes alpha channel, if any. - */ - VImage RemoveAlpha(VImage image); - - /* - Ensures alpha channel, if missing. - */ - VImage EnsureAlpha(VImage image); - /* * Tint an image using the specified chroma, preserving the original image luminance */ diff --git a/src/stats.cc b/src/stats.cc index 989e5620..bc5ee13e 100644 --- a/src/stats.cc +++ b/src/stats.cc @@ -86,6 +86,18 @@ class StatsWorker : public Napi::AsyncWorker { 0.0, 1.0, 0.0); laplacian.set("scale", 9.0); baton->sharpness = greyscale.conv(laplacian).deviate(); + // Most dominant sRGB colour via 4096-bin 3D histogram + vips::VImage hist = sharp::RemoveAlpha(image) + .colourspace(VIPS_INTERPRETATION_sRGB) + .hist_find_ndim(VImage::option()->set("bins", 16)); + std::complex maxpos = hist.maxpos(); + int const dx = static_cast(std::real(maxpos)); + int const dy = static_cast(std::imag(maxpos)); + std::vector pel = hist(dx, dy); + int const dz = std::distance(pel.begin(), std::find(pel.begin(), pel.end(), hist.max())); + baton->dominantRed = dx * 16 + 8; + baton->dominantGreen = dy * 16 + 8; + baton->dominantBlue = dz * 16 + 8; } catch (vips::VError const &err) { (baton->err).append(err.what()); } @@ -133,6 +145,11 @@ class StatsWorker : public Napi::AsyncWorker { info.Set("isOpaque", baton->isOpaque); info.Set("entropy", baton->entropy); info.Set("sharpness", baton->sharpness); + Napi::Object dominant = Napi::Object::New(env); + dominant.Set("r", baton->dominantRed); + dominant.Set("g", baton->dominantGreen); + dominant.Set("b", baton->dominantBlue); + info.Set("dominant", dominant); Callback().MakeCallback(Receiver().Value(), { env.Null(), info }); } else { Callback().MakeCallback(Receiver().Value(), { Napi::Error::New(env, baton->err).Value() }); diff --git a/src/stats.h b/src/stats.h index eb1f1658..8d9dca5f 100644 --- a/src/stats.h +++ b/src/stats.h @@ -48,6 +48,9 @@ struct StatsBaton { bool isOpaque; double entropy; double sharpness; + int dominantRed; + int dominantGreen; + int dominantBlue; std::string err; @@ -55,7 +58,10 @@ struct StatsBaton { input(nullptr), isOpaque(true), entropy(0.0), - sharpness(0.0) + sharpness(0.0), + dominantRed(0), + dominantGreen(0), + dominantBlue(0) {} }; diff --git a/test/unit/stats.js b/test/unit/stats.js index 8db5abdd..b456ca9c 100644 --- a/test/unit/stats.js +++ b/test/unit/stats.js @@ -27,6 +27,11 @@ describe('Image Stats', function () { assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541)); assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 0.7883011147075762)); + const { r, g, b } = stats.dominant; + assert.strictEqual(40, r); + assert.strictEqual(40, g); + assert.strictEqual(40, b); + // red channel assert.strictEqual(0, stats.channels[0].min); assert.strictEqual(255, stats.channels[0].max); @@ -87,6 +92,11 @@ describe('Image Stats', function () { assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0.3409031108021736)); assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 9.111356137722868)); + const { r, g, b } = stats.dominant; + assert.strictEqual(248, r); + assert.strictEqual(248, g); + assert.strictEqual(248, b); + // red channel assert.strictEqual(0, stats.channels[0].min); assert.strictEqual(255, stats.channels[0].max); @@ -114,6 +124,11 @@ describe('Image Stats', function () { assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0.06778064835816622)); assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 2.522916068931278)); + const { r, g, b } = stats.dominant; + assert.strictEqual(248, r); + assert.strictEqual(248, g); + assert.strictEqual(248, b); + // red channel assert.strictEqual(0, stats.channels[0].min); assert.strictEqual(255, stats.channels[0].max); @@ -190,6 +205,11 @@ describe('Image Stats', function () { assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0)); assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 0)); + const { r, g, b } = stats.dominant; + assert.strictEqual(72, r); + assert.strictEqual(104, g); + assert.strictEqual(72, b); + // alpha channel assert.strictEqual(0, stats.channels[3].min); assert.strictEqual(0, stats.channels[3].max); @@ -218,6 +238,11 @@ describe('Image Stats', function () { assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0.3851250782608986)); assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 10.312521863719589)); + const { r, g, b } = stats.dominant; + assert.strictEqual(248, r); + assert.strictEqual(248, g); + assert.strictEqual(248, b); + // red channel assert.strictEqual(0, stats.channels[0].min); assert.strictEqual(255, stats.channels[0].max); @@ -246,6 +271,11 @@ describe('Image Stats', function () { assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.51758075132966)); assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 9.959951636662941)); + const { r, g, b } = stats.dominant; + assert.strictEqual(40, r); + assert.strictEqual(136, g); + assert.strictEqual(200, b); + // red channel assert.strictEqual(0, stats.channels[0].min); assert.strictEqual(true, isInRange(stats.channels[0].max, 254, 255)); @@ -306,6 +336,11 @@ describe('Image Stats', function () { assert.strictEqual(true, isInAcceptableRange(stats.entropy, 6.087309412541799)); assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 2.9250574456255682)); + const { r, g, b } = stats.dominant; + assert.strictEqual(120, r); + assert.strictEqual(136, g); + assert.strictEqual(88, b); + // red channel assert.strictEqual(35, stats.channels[0].min); assert.strictEqual(254, stats.channels[0].max); @@ -366,6 +401,11 @@ describe('Image Stats', function () { assert.strictEqual(true, isInAcceptableRange(stats.entropy, 1)); assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 15.870619016486861)); + const { r, g, b } = stats.dominant; + assert.strictEqual(8, r); + assert.strictEqual(8, g); + assert.strictEqual(8, b); + // gray channel assert.strictEqual(0, stats.channels[0].min); assert.strictEqual(101, stats.channels[0].max); @@ -411,6 +451,17 @@ describe('Image Stats', function () { }) ); + it('Dominant colour', () => + sharp(fixtures.inputJpgBooleanTest) + .stats() + .then(({ dominant }) => { + const { r, g, b } = dominant; + assert.strictEqual(r, 8); + assert.strictEqual(g, 136); + assert.strictEqual(b, 248); + }) + ); + it('Stream in, Callback out', function (done) { const readable = fs.createReadStream(fixtures.inputJpg); const pipeline = sharp().stats(function (err, stats) { @@ -420,6 +471,11 @@ describe('Image Stats', function () { assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541)); assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 0.788301114707569)); + const { r, g, b } = stats.dominant; + assert.strictEqual(40, r); + assert.strictEqual(40, g); + assert.strictEqual(40, b); + // red channel assert.strictEqual(0, stats.channels[0].min); assert.strictEqual(255, stats.channels[0].max); @@ -483,6 +539,11 @@ describe('Image Stats', function () { assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541)); assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 0.788301114707569)); + const { r, g, b } = stats.dominant; + assert.strictEqual(40, r); + assert.strictEqual(40, g); + assert.strictEqual(40, b); + // red channel assert.strictEqual(0, stats.channels[0].min); assert.strictEqual(255, stats.channels[0].max); @@ -541,6 +602,11 @@ describe('Image Stats', function () { assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541)); assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 0.788301114707569)); + const { r, g, b } = stats.dominant; + assert.strictEqual(40, r); + assert.strictEqual(40, g); + assert.strictEqual(40, b); + // red channel assert.strictEqual(0, stats.channels[0].min); assert.strictEqual(255, stats.channels[0].max);