Add most dominant colour to image stats #640

This commit is contained in:
Lovell Fuller 2020-07-15 19:58:54 +01:00
parent dcc42f8514
commit c42de19d2a
10 changed files with 130 additions and 36 deletions

View File

@ -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. - `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) - `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) - `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 ### Parameters
@ -89,7 +90,8 @@ image
``` ```
```javascript ```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]>** Returns **[Promise][5]<[Object][6]>**

View File

@ -12,6 +12,9 @@ Requires libvips v8.10.0
* JPEG output `quality` >= 90 no longer automatically sets `chromaSubsampling` to `4:4:4`. * 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* ## v0.25 - *yield*
Requires libvips v8.9.1 Requires libvips v8.9.1

View File

@ -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. * - `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) * - `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) * - `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 * @example
* const image = sharp(inputJpg); * const image = sharp(inputJpg);
@ -310,7 +311,8 @@ function metadata (callback) {
* }); * });
* *
* @example * @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)` * @param {Function} [callback] - called with the arguments `(err, stats)`
* @returns {Promise<Object>} * @returns {Promise<Object>}

View File

@ -724,4 +724,25 @@ namespace sharp {
return std::make_tuple(image, alphaColour); 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<double> alpha;
alpha.push_back(sharp::MaximumImageAlpha(image.interpretation()));
image = image.bandjoin_const(alpha);
}
return image;
}
} // namespace sharp } // namespace sharp

View File

@ -271,6 +271,16 @@ namespace sharp {
*/ */
std::tuple<VImage, std::vector<double>> ApplyAlpha(VImage image, std::vector<double> colour); std::tuple<VImage, std::vector<double>> ApplyAlpha(VImage image, std::vector<double> colour);
/*
Removes alpha channel, if any.
*/
VImage RemoveAlpha(VImage image);
/*
Ensures alpha channel, if missing.
*/
VImage EnsureAlpha(VImage image);
} // namespace sharp } // namespace sharp
#endif // SRC_COMMON_H_ #endif // SRC_COMMON_H_

View File

@ -27,29 +27,6 @@ using vips::VImage;
using vips::VError; using vips::VError;
namespace sharp { 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<double> 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 * Tint an image using the specified chroma, preserving the original image luminance
*/ */

View File

@ -25,16 +25,6 @@ using vips::VImage;
namespace sharp { 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 * Tint an image using the specified chroma, preserving the original image luminance
*/ */

View File

@ -86,6 +86,18 @@ class StatsWorker : public Napi::AsyncWorker {
0.0, 1.0, 0.0); 0.0, 1.0, 0.0);
laplacian.set("scale", 9.0); laplacian.set("scale", 9.0);
baton->sharpness = greyscale.conv(laplacian).deviate(); 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<double> maxpos = hist.maxpos();
int const dx = static_cast<int>(std::real(maxpos));
int const dy = static_cast<int>(std::imag(maxpos));
std::vector<double> 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) { } catch (vips::VError const &err) {
(baton->err).append(err.what()); (baton->err).append(err.what());
} }
@ -133,6 +145,11 @@ class StatsWorker : public Napi::AsyncWorker {
info.Set("isOpaque", baton->isOpaque); info.Set("isOpaque", baton->isOpaque);
info.Set("entropy", baton->entropy); info.Set("entropy", baton->entropy);
info.Set("sharpness", baton->sharpness); 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 }); Callback().MakeCallback(Receiver().Value(), { env.Null(), info });
} else { } else {
Callback().MakeCallback(Receiver().Value(), { Napi::Error::New(env, baton->err).Value() }); Callback().MakeCallback(Receiver().Value(), { Napi::Error::New(env, baton->err).Value() });

View File

@ -48,6 +48,9 @@ struct StatsBaton {
bool isOpaque; bool isOpaque;
double entropy; double entropy;
double sharpness; double sharpness;
int dominantRed;
int dominantGreen;
int dominantBlue;
std::string err; std::string err;
@ -55,7 +58,10 @@ struct StatsBaton {
input(nullptr), input(nullptr),
isOpaque(true), isOpaque(true),
entropy(0.0), entropy(0.0),
sharpness(0.0) sharpness(0.0),
dominantRed(0),
dominantGreen(0),
dominantBlue(0)
{} {}
}; };

View File

@ -27,6 +27,11 @@ describe('Image Stats', function () {
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541)); assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541));
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 0.7883011147075762)); 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 // red channel
assert.strictEqual(0, stats.channels[0].min); assert.strictEqual(0, stats.channels[0].min);
assert.strictEqual(255, stats.channels[0].max); 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.entropy, 0.3409031108021736));
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 9.111356137722868)); 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 // red channel
assert.strictEqual(0, stats.channels[0].min); assert.strictEqual(0, stats.channels[0].min);
assert.strictEqual(255, stats.channels[0].max); 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.entropy, 0.06778064835816622));
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 2.522916068931278)); 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 // red channel
assert.strictEqual(0, stats.channels[0].min); assert.strictEqual(0, stats.channels[0].min);
assert.strictEqual(255, stats.channels[0].max); 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.entropy, 0));
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 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 // alpha channel
assert.strictEqual(0, stats.channels[3].min); assert.strictEqual(0, stats.channels[3].min);
assert.strictEqual(0, stats.channels[3].max); 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.entropy, 0.3851250782608986));
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 10.312521863719589)); 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 // red channel
assert.strictEqual(0, stats.channels[0].min); assert.strictEqual(0, stats.channels[0].min);
assert.strictEqual(255, stats.channels[0].max); 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.entropy, 7.51758075132966));
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 9.959951636662941)); 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 // red channel
assert.strictEqual(0, stats.channels[0].min); assert.strictEqual(0, stats.channels[0].min);
assert.strictEqual(true, isInRange(stats.channels[0].max, 254, 255)); 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.entropy, 6.087309412541799));
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 2.9250574456255682)); 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 // red channel
assert.strictEqual(35, stats.channels[0].min); assert.strictEqual(35, stats.channels[0].min);
assert.strictEqual(254, stats.channels[0].max); 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.entropy, 1));
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 15.870619016486861)); 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 // gray channel
assert.strictEqual(0, stats.channels[0].min); assert.strictEqual(0, stats.channels[0].min);
assert.strictEqual(101, stats.channels[0].max); 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) { it('Stream in, Callback out', function (done) {
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) {
@ -420,6 +471,11 @@ describe('Image Stats', function () {
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541)); assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541));
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 0.788301114707569)); 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 // red channel
assert.strictEqual(0, stats.channels[0].min); assert.strictEqual(0, stats.channels[0].min);
assert.strictEqual(255, stats.channels[0].max); 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.entropy, 7.319914765248541));
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 0.788301114707569)); 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 // red channel
assert.strictEqual(0, stats.channels[0].min); assert.strictEqual(0, stats.channels[0].min);
assert.strictEqual(255, stats.channels[0].max); 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.entropy, 7.319914765248541));
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 0.788301114707569)); 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 // red channel
assert.strictEqual(0, stats.channels[0].min); assert.strictEqual(0, stats.channels[0].min);
assert.strictEqual(255, stats.channels[0].max); assert.strictEqual(255, stats.channels[0].max);