mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 10:30:15 +02:00
Add most dominant colour to image stats #640
This commit is contained in:
parent
dcc42f8514
commit
c42de19d2a
@ -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]>**
|
||||
|
@ -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
|
||||
|
@ -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<Object>}
|
||||
|
@ -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<double> alpha;
|
||||
alpha.push_back(sharp::MaximumImageAlpha(image.interpretation()));
|
||||
image = image.bandjoin_const(alpha);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
} // namespace sharp
|
||||
|
10
src/common.h
10
src/common.h
@ -271,6 +271,16 @@ namespace sharp {
|
||||
*/
|
||||
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
|
||||
|
||||
#endif // SRC_COMMON_H_
|
||||
|
@ -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<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
|
||||
*/
|
||||
|
@ -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
|
||||
*/
|
||||
|
17
src/stats.cc
17
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<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) {
|
||||
(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() });
|
||||
|
@ -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)
|
||||
{}
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user