mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 18:40:16 +02:00
Add experimental sharpness calc to stats #2251
This commit is contained in:
parent
9431029917
commit
8f5495a446
@ -71,6 +71,7 @@ A `Promise` is returned when `callback` is not provided.
|
|||||||
- `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`: 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)
|
||||||
|
|
||||||
### Parameters
|
### Parameters
|
||||||
|
|
||||||
@ -87,6 +88,10 @@ image
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { entropy, sharpness } = await sharp(input).stats();
|
||||||
|
```
|
||||||
|
|
||||||
Returns **[Promise][5]<[Object][6]>**
|
Returns **[Promise][5]<[Object][6]>**
|
||||||
|
|
||||||
[1]: https://libvips.github.io/libvips/API/current/VipsImage.html#VipsInterpretation
|
[1]: https://libvips.github.io/libvips/API/current/VipsImage.html#VipsInterpretation
|
||||||
|
@ -21,6 +21,9 @@ Requires libvips v8.9.1
|
|||||||
* Add support for named `alpha` channel to `extractChannel` operation.
|
* Add support for named `alpha` channel to `extractChannel` operation.
|
||||||
[#2138](https://github.com/lovell/sharp/issues/2138)
|
[#2138](https://github.com/lovell/sharp/issues/2138)
|
||||||
|
|
||||||
|
* Add experimental `sharpness` calculation to `stats()` response.
|
||||||
|
[#2251](https://github.com/lovell/sharp/issues/2251)
|
||||||
|
|
||||||
### v0.25.3 - 17th May 2020
|
### v0.25.3 - 17th May 2020
|
||||||
|
|
||||||
* Ensure libvips is initialised only once, improves worker thread safety.
|
* Ensure libvips is initialised only once, improves worker thread safety.
|
||||||
|
@ -299,6 +299,7 @@ function metadata (callback) {
|
|||||||
* - `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`: 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)
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const image = sharp(inputJpg);
|
* const image = sharp(inputJpg);
|
||||||
@ -308,6 +309,9 @@ function metadata (callback) {
|
|||||||
* // stats contains the channel-wise statistics array and the isOpaque value
|
* // stats contains the channel-wise statistics array and the isOpaque value
|
||||||
* });
|
* });
|
||||||
*
|
*
|
||||||
|
* @example
|
||||||
|
* const { entropy, sharpness } = await sharp(input).stats();
|
||||||
|
*
|
||||||
* @param {Function} [callback] - called with the arguments `(err, stats)`
|
* @param {Function} [callback] - called with the arguments `(err, stats)`
|
||||||
* @returns {Promise<Object>}
|
* @returns {Promise<Object>}
|
||||||
*/
|
*/
|
||||||
|
12
src/stats.cc
12
src/stats.cc
@ -75,8 +75,17 @@ class StatsWorker : public Napi::AsyncWorker {
|
|||||||
baton->isOpaque = false;
|
baton->isOpaque = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Convert to greyscale
|
||||||
|
vips::VImage greyscale = image.colourspace(VIPS_INTERPRETATION_B_W)[0];
|
||||||
// Estimate entropy via histogram of greyscale value frequency
|
// Estimate entropy via histogram of greyscale value frequency
|
||||||
baton->entropy = std::abs(image.colourspace(VIPS_INTERPRETATION_B_W)[0].hist_find().hist_entropy());
|
baton->entropy = std::abs(greyscale.hist_find().hist_entropy());
|
||||||
|
// Estimate sharpness via standard deviation of greyscale laplacian
|
||||||
|
VImage laplacian = VImage::new_matrixv(3, 3,
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
1.0, -4.0, 1.0,
|
||||||
|
0.0, 1.0, 0.0);
|
||||||
|
laplacian.set("scale", 9.0);
|
||||||
|
baton->sharpness = greyscale.conv(laplacian).deviate();
|
||||||
} catch (vips::VError const &err) {
|
} catch (vips::VError const &err) {
|
||||||
(baton->err).append(err.what());
|
(baton->err).append(err.what());
|
||||||
}
|
}
|
||||||
@ -123,6 +132,7 @@ class StatsWorker : public Napi::AsyncWorker {
|
|||||||
info.Set("channels", channels);
|
info.Set("channels", channels);
|
||||||
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);
|
||||||
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() });
|
||||||
|
@ -47,13 +47,15 @@ struct StatsBaton {
|
|||||||
std::vector<ChannelStats> channelStats;
|
std::vector<ChannelStats> channelStats;
|
||||||
bool isOpaque;
|
bool isOpaque;
|
||||||
double entropy;
|
double entropy;
|
||||||
|
double sharpness;
|
||||||
|
|
||||||
std::string err;
|
std::string err;
|
||||||
|
|
||||||
StatsBaton():
|
StatsBaton():
|
||||||
input(nullptr),
|
input(nullptr),
|
||||||
isOpaque(true),
|
isOpaque(true),
|
||||||
entropy(0.0)
|
entropy(0.0),
|
||||||
|
sharpness(0.0)
|
||||||
{}
|
{}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ describe('Image Stats', function () {
|
|||||||
|
|
||||||
assert.strictEqual(true, stats.isOpaque);
|
assert.strictEqual(true, stats.isOpaque);
|
||||||
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541));
|
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541));
|
||||||
|
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 0.7883011147075762));
|
||||||
|
|
||||||
// red channel
|
// red channel
|
||||||
assert.strictEqual(0, stats.channels[0].min);
|
assert.strictEqual(0, stats.channels[0].min);
|
||||||
@ -84,6 +85,7 @@ describe('Image Stats', function () {
|
|||||||
|
|
||||||
assert.strictEqual(true, stats.isOpaque);
|
assert.strictEqual(true, stats.isOpaque);
|
||||||
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0.3409031108021736));
|
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0.3409031108021736));
|
||||||
|
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 9.111356137722868));
|
||||||
|
|
||||||
// red channel
|
// red channel
|
||||||
assert.strictEqual(0, stats.channels[0].min);
|
assert.strictEqual(0, stats.channels[0].min);
|
||||||
@ -110,6 +112,7 @@ describe('Image Stats', function () {
|
|||||||
|
|
||||||
assert.strictEqual(false, stats.isOpaque);
|
assert.strictEqual(false, stats.isOpaque);
|
||||||
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0.06778064835816622));
|
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0.06778064835816622));
|
||||||
|
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 2.522916068931278));
|
||||||
|
|
||||||
// red channel
|
// red channel
|
||||||
assert.strictEqual(0, stats.channels[0].min);
|
assert.strictEqual(0, stats.channels[0].min);
|
||||||
@ -185,6 +188,7 @@ describe('Image Stats', function () {
|
|||||||
|
|
||||||
assert.strictEqual(false, stats.isOpaque);
|
assert.strictEqual(false, stats.isOpaque);
|
||||||
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0));
|
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0));
|
||||||
|
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 0));
|
||||||
|
|
||||||
// alpha channel
|
// alpha channel
|
||||||
assert.strictEqual(0, stats.channels[3].min);
|
assert.strictEqual(0, stats.channels[3].min);
|
||||||
@ -212,6 +216,7 @@ describe('Image Stats', function () {
|
|||||||
|
|
||||||
assert.strictEqual(true, stats.isOpaque);
|
assert.strictEqual(true, stats.isOpaque);
|
||||||
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0.3851250782608986));
|
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0.3851250782608986));
|
||||||
|
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 10.312521863719589));
|
||||||
|
|
||||||
// red channel
|
// red channel
|
||||||
assert.strictEqual(0, stats.channels[0].min);
|
assert.strictEqual(0, stats.channels[0].min);
|
||||||
@ -239,6 +244,7 @@ describe('Image Stats', function () {
|
|||||||
|
|
||||||
assert.strictEqual(true, stats.isOpaque);
|
assert.strictEqual(true, stats.isOpaque);
|
||||||
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.51758075132966));
|
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.51758075132966));
|
||||||
|
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 9.959951636662941));
|
||||||
|
|
||||||
// red channel
|
// red channel
|
||||||
assert.strictEqual(0, stats.channels[0].min);
|
assert.strictEqual(0, stats.channels[0].min);
|
||||||
@ -298,6 +304,7 @@ describe('Image Stats', function () {
|
|||||||
|
|
||||||
assert.strictEqual(true, stats.isOpaque);
|
assert.strictEqual(true, stats.isOpaque);
|
||||||
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 6.087309412541799));
|
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 6.087309412541799));
|
||||||
|
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 2.9250574456255682));
|
||||||
|
|
||||||
// red channel
|
// red channel
|
||||||
assert.strictEqual(35, stats.channels[0].min);
|
assert.strictEqual(35, stats.channels[0].min);
|
||||||
@ -357,6 +364,7 @@ describe('Image Stats', function () {
|
|||||||
|
|
||||||
assert.strictEqual(false, stats.isOpaque);
|
assert.strictEqual(false, stats.isOpaque);
|
||||||
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 1));
|
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 1));
|
||||||
|
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 15.870619016486861));
|
||||||
|
|
||||||
// gray channel
|
// gray channel
|
||||||
assert.strictEqual(0, stats.channels[0].min);
|
assert.strictEqual(0, stats.channels[0].min);
|
||||||
@ -410,6 +418,7 @@ describe('Image Stats', function () {
|
|||||||
|
|
||||||
assert.strictEqual(true, stats.isOpaque);
|
assert.strictEqual(true, stats.isOpaque);
|
||||||
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541));
|
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541));
|
||||||
|
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 0.788301114707569));
|
||||||
|
|
||||||
// red channel
|
// red channel
|
||||||
assert.strictEqual(0, stats.channels[0].min);
|
assert.strictEqual(0, stats.channels[0].min);
|
||||||
@ -472,6 +481,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));
|
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541));
|
||||||
|
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 0.788301114707569));
|
||||||
|
|
||||||
// red channel
|
// red channel
|
||||||
assert.strictEqual(0, stats.channels[0].min);
|
assert.strictEqual(0, stats.channels[0].min);
|
||||||
@ -529,6 +539,7 @@ describe('Image Stats', 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));
|
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541));
|
||||||
|
assert.strictEqual(true, isInAcceptableRange(stats.sharpness, 0.788301114707569));
|
||||||
|
|
||||||
// red channel
|
// red channel
|
||||||
assert.strictEqual(0, stats.channels[0].min);
|
assert.strictEqual(0, stats.channels[0].min);
|
||||||
@ -582,6 +593,18 @@ describe('Image Stats', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Blurred image has lower sharpness than original', () => {
|
||||||
|
const original = sharp(fixtures.inputJpg).stats();
|
||||||
|
const blurred = sharp(fixtures.inputJpg).blur().toBuffer().then(blur => sharp(blur).stats());
|
||||||
|
|
||||||
|
return Promise
|
||||||
|
.all([original, blurred])
|
||||||
|
.then(([original, blurred]) => {
|
||||||
|
assert.strictEqual(true, isInAcceptableRange(original.sharpness, 0.7883011147075476));
|
||||||
|
assert.strictEqual(true, isInAcceptableRange(blurred.sharpness, 0.4791559805997398));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('File input with corrupt header fails gracefully', function (done) {
|
it('File input with corrupt header fails gracefully', function (done) {
|
||||||
sharp(fixtures.inputJpgWithCorruptHeader)
|
sharp(fixtures.inputJpgWithCorruptHeader)
|
||||||
.stats(function (err) {
|
.stats(function (err) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user