Allow sharpen options to be provided as an Object

Also exposes x1, y2, y3 parameters #2561 #2935
This commit is contained in:
Lovell Fuller 2022-03-09 19:07:05 +00:00
parent 1de49f3ed8
commit ea599ade10
10 changed files with 214 additions and 46 deletions

View File

@ -129,13 +129,41 @@ When used without parameters, performs a fast, mild sharpen of the output image.
When a `sigma` is provided, performs a slower, more accurate sharpen of the L channel in the LAB colour space. When a `sigma` is provided, performs a slower, more accurate sharpen of the L channel in the LAB colour space.
Separate control over the level of sharpening in "flat" and "jagged" areas is available. Separate control over the level of sharpening in "flat" and "jagged" areas is available.
See [libvips sharpen][8] operation.
### Parameters ### Parameters
* `sigma` **[number][1]?** the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`. * `options` **[Object][2]?** if present, is an Object with optional attributes.
* `flat` **[number][1]** the level of sharpening to apply to "flat" areas. (optional, default `1.0`)
* `jagged` **[number][1]** the level of sharpening to apply to "jagged" areas. (optional, default `2.0`)
<!----> * `options.sigma` **[number][1]?** the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`.
* `options.m1` **[number][1]** the level of sharpening to apply to "flat" areas. (optional, default `1.0`)
* `options.m2` **[number][1]** the level of sharpening to apply to "jagged" areas. (optional, default `2.0`)
* `options.x1` **[number][1]** threshold between "flat" and "jagged" (optional, default `2.0`)
* `options.y2` **[number][1]** maximum amount of brightening. (optional, default `10.0`)
* `options.y3` **[number][1]** maximum amount of darkening. (optional, default `20.0`)
### Examples
```javascript
const data = await sharp(input).sharpen().toBuffer();
```
```javascript
const data = await sharp(input).sharpen({ sigma: 2 }).toBuffer();
```
```javascript
const data = await sharp(input)
.sharpen({
sigma: 2,
m1: 0
m2: 3,
x1: 3,
y2: 15,
y3: 15,
})
.toBuffer();
```
* Throws **[Error][5]** Invalid parameters * Throws **[Error][5]** Invalid parameters
@ -190,7 +218,7 @@ Returns **Sharp**
Merge alpha transparency channel, if any, with a background, then remove the alpha channel. Merge alpha transparency channel, if any, with a background, then remove the alpha channel.
See also [removeAlpha][8]. See also [removeAlpha][9].
### Parameters ### Parameters
@ -264,7 +292,7 @@ Returns **Sharp**
## clahe ## clahe
Perform contrast limiting adaptive histogram equalization Perform contrast limiting adaptive histogram equalization
[CLAHE][9]. [CLAHE][10].
This will, in general, enhance the clarity of the image by bringing out darker details. This will, in general, enhance the clarity of the image by bringing out darker details.
@ -349,7 +377,7 @@ the selected bitwise boolean `operation` between the corresponding pixels of the
### Parameters ### Parameters
* `operand` **([Buffer][10] | [string][3])** Buffer containing image data or string containing the path to an image file. * `operand` **([Buffer][11] | [string][3])** Buffer containing image data or string containing the path to an image file.
* `operator` **[string][3]** one of `and`, `or` or `eor` to perform that bitwise operation, like the C logic operators `&`, `|` and `^` respectively. * `operator` **[string][3]** one of `and`, `or` or `eor` to perform that bitwise operation, like the C logic operators `&`, `|` and `^` respectively.
* `options` **[Object][2]?** * `options` **[Object][2]?**
@ -474,8 +502,10 @@ Returns **Sharp**
[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array [7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[8]: /api-channel#removealpha [8]: https://www.libvips.org/API/current/libvips-convolution.html#vips-sharpen
[9]: https://en.wikipedia.org/wiki/Adaptive_histogram_equalization#Contrast_Limited_AHE [9]: /api-channel#removealpha
[10]: https://nodejs.org/api/buffer.html [10]: https://en.wikipedia.org/wiki/Adaptive_histogram_equalization#Contrast_Limited_AHE
[11]: https://nodejs.org/api/buffer.html

View File

@ -6,6 +6,12 @@ Requires libvips v8.12.2
### v0.30.3 - TBD ### v0.30.3 - TBD
* Allow `sharpen` options to be provided more consistently as an Object.
[#2561](https://github.com/lovell/sharp/issues/2561)
* Expose `x1`, `y2` and `y3` parameters of `sharpen` operation.
[#2935](https://github.com/lovell/sharp/issues/2935)
* Prevent double unpremultiply with some composite blend modes (regression in 0.30.2). * Prevent double unpremultiply with some composite blend modes (regression in 0.30.2).
[#3118](https://github.com/lovell/sharp/issues/3118) [#3118](https://github.com/lovell/sharp/issues/3118)

File diff suppressed because one or more lines are too long

View File

@ -186,8 +186,11 @@ const Sharp = function (input, options) {
medianSize: 0, medianSize: 0,
blurSigma: 0, blurSigma: 0,
sharpenSigma: 0, sharpenSigma: 0,
sharpenFlat: 1, sharpenM1: 1,
sharpenJagged: 2, sharpenM2: 2,
sharpenX1: 2,
sharpenY2: 10,
sharpenY3: 20,
threshold: 0, threshold: 0,
thresholdGrayscale: true, thresholdGrayscale: true,
trimThreshold: 0, trimThreshold: 0,

View File

@ -185,40 +185,105 @@ function affine (matrix, options) {
* When a `sigma` is provided, performs a slower, more accurate sharpen of the L channel in the LAB colour space. * When a `sigma` is provided, performs a slower, more accurate sharpen of the L channel in the LAB colour space.
* Separate control over the level of sharpening in "flat" and "jagged" areas is available. * Separate control over the level of sharpening in "flat" and "jagged" areas is available.
* *
* @param {number} [sigma] - the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`. * See {@link https://www.libvips.org/API/current/libvips-convolution.html#vips-sharpen|libvips sharpen} operation.
* @param {number} [flat=1.0] - the level of sharpening to apply to "flat" areas. *
* @param {number} [jagged=2.0] - the level of sharpening to apply to "jagged" areas. * @example
* const data = await sharp(input).sharpen().toBuffer();
*
* @example
* const data = await sharp(input).sharpen({ sigma: 2 }).toBuffer();
*
* @example
* const data = await sharp(input)
* .sharpen({
* sigma: 2,
* m1: 0
* m2: 3,
* x1: 3,
* y2: 15,
* y3: 15,
* })
* .toBuffer();
*
* @param {Object} [options] - if present, is an Object with optional attributes.
* @param {number} [options.sigma] - the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`.
* @param {number} [options.m1=1.0] - the level of sharpening to apply to "flat" areas.
* @param {number} [options.m2=2.0] - the level of sharpening to apply to "jagged" areas.
* @param {number} [options.x1=2.0] - threshold between "flat" and "jagged"
* @param {number} [options.y2=10.0] - maximum amount of brightening.
* @param {number} [options.y3=20.0] - maximum amount of darkening.
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
*/ */
function sharpen (sigma, flat, jagged) { function sharpen (options) {
if (!is.defined(sigma)) { if (!is.defined(options)) {
// No arguments: default to mild sharpen // No arguments: default to mild sharpen
this.options.sharpenSigma = -1; this.options.sharpenSigma = -1;
} else if (is.bool(sigma)) { } else if (is.bool(options)) {
// Boolean argument: apply mild sharpen? // Deprecated boolean argument: apply mild sharpen?
this.options.sharpenSigma = sigma ? -1 : 0; this.options.sharpenSigma = options ? -1 : 0;
} else if (is.number(sigma) && is.inRange(sigma, 0.01, 10000)) { } else if (is.number(options) && is.inRange(options, 0.01, 10000)) {
// Numeric argument: specific sigma // Deprecated numeric argument: specific sigma
this.options.sharpenSigma = sigma; this.options.sharpenSigma = options;
// Control over flat areas // Deprecated control over flat areas
if (is.defined(flat)) { if (is.defined(arguments[1])) {
if (is.number(flat) && is.inRange(flat, 0, 10000)) { if (is.number(arguments[1]) && is.inRange(arguments[1], 0, 10000)) {
this.options.sharpenFlat = flat; this.options.sharpenM1 = arguments[1];
} else { } else {
throw is.invalidParameterError('flat', 'number between 0 and 10000', flat); throw is.invalidParameterError('flat', 'number between 0 and 10000', arguments[1]);
} }
} }
// Control over jagged areas // Deprecated control over jagged areas
if (is.defined(jagged)) { if (is.defined(arguments[2])) {
if (is.number(jagged) && is.inRange(jagged, 0, 10000)) { if (is.number(arguments[2]) && is.inRange(arguments[2], 0, 10000)) {
this.options.sharpenJagged = jagged; this.options.sharpenM2 = arguments[2];
} else { } else {
throw is.invalidParameterError('jagged', 'number between 0 and 10000', jagged); throw is.invalidParameterError('jagged', 'number between 0 and 10000', arguments[2]);
}
}
} else if (is.plainObject(options)) {
if (is.number(options.sigma) && is.inRange(options.sigma, 0.01, 10000)) {
this.options.sharpenSigma = options.sigma;
} else {
throw is.invalidParameterError('options.sigma', 'number between 0.01 and 10000', options.sigma);
}
if (is.defined(options.m1)) {
if (is.number(options.m1) && is.inRange(options.m1, 0, 10000)) {
this.options.sharpenM1 = options.m1;
} else {
throw is.invalidParameterError('options.m1', 'number between 0 and 10000', options.m1);
}
}
if (is.defined(options.m2)) {
if (is.number(options.m2) && is.inRange(options.m2, 0, 10000)) {
this.options.sharpenM2 = options.m2;
} else {
throw is.invalidParameterError('options.m2', 'number between 0 and 10000', options.m2);
}
}
if (is.defined(options.x1)) {
if (is.number(options.x1) && is.inRange(options.x1, 0, 10000)) {
this.options.sharpenX1 = options.x1;
} else {
throw is.invalidParameterError('options.x1', 'number between 0 and 10000', options.x1);
}
}
if (is.defined(options.y2)) {
if (is.number(options.y2) && is.inRange(options.y2, 0, 10000)) {
this.options.sharpenY2 = options.y2;
} else {
throw is.invalidParameterError('options.y2', 'number between 0 and 10000', options.y2);
}
}
if (is.defined(options.y3)) {
if (is.number(options.y3) && is.inRange(options.y3, 0, 10000)) {
this.options.sharpenY3 = options.y3;
} else {
throw is.invalidParameterError('options.y3', 'number between 0 and 10000', options.y3);
} }
} }
} else { } else {
throw is.invalidParameterError('sigma', 'number between 0.01 and 10000', sigma); throw is.invalidParameterError('sigma', 'number between 0.01 and 10000', options);
} }
return this; return this;
} }

View File

@ -209,7 +209,8 @@ namespace sharp {
/* /*
* Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen. * Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen.
*/ */
VImage Sharpen(VImage image, double const sigma, double const flat, double const jagged) { VImage Sharpen(VImage image, double const sigma, double const m1, double const m2,
double const x1, double const y2, double const y3) {
if (sigma == -1.0) { if (sigma == -1.0) {
// Fast, mild sharpen // Fast, mild sharpen
VImage sharpen = VImage::new_matrixv(3, 3, VImage sharpen = VImage::new_matrixv(3, 3,
@ -224,8 +225,14 @@ namespace sharp {
if (colourspaceBeforeSharpen == VIPS_INTERPRETATION_RGB) { if (colourspaceBeforeSharpen == VIPS_INTERPRETATION_RGB) {
colourspaceBeforeSharpen = VIPS_INTERPRETATION_sRGB; colourspaceBeforeSharpen = VIPS_INTERPRETATION_sRGB;
} }
return image.sharpen( return image
VImage::option()->set("sigma", sigma)->set("m1", flat)->set("m2", jagged)) .sharpen(VImage::option()
->set("sigma", sigma)
->set("m1", m1)
->set("m2", m2)
->set("x1", x1)
->set("y2", y2)
->set("y3", y3))
.colourspace(colourspaceBeforeSharpen); .colourspace(colourspaceBeforeSharpen);
} }
} }

View File

@ -64,7 +64,8 @@ namespace sharp {
/* /*
* Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen. * Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen.
*/ */
VImage Sharpen(VImage image, double const sigma, double const flat, double const jagged); VImage Sharpen(VImage image, double const sigma, double const m1, double const m2,
double const x1, double const y2, double const y3);
/* /*
Threshold an image Threshold an image

View File

@ -577,7 +577,8 @@ class PipelineWorker : public Napi::AsyncWorker {
// Sharpen // Sharpen
if (shouldSharpen) { if (shouldSharpen) {
image = sharp::Sharpen(image, baton->sharpenSigma, baton->sharpenFlat, baton->sharpenJagged); image = sharp::Sharpen(image, baton->sharpenSigma, baton->sharpenM1, baton->sharpenM2,
baton->sharpenX1, baton->sharpenY2, baton->sharpenY3);
} }
// Composite // Composite
@ -1400,8 +1401,11 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->lightness = sharp::AttrAsDouble(options, "lightness"); baton->lightness = sharp::AttrAsDouble(options, "lightness");
baton->medianSize = sharp::AttrAsUint32(options, "medianSize"); baton->medianSize = sharp::AttrAsUint32(options, "medianSize");
baton->sharpenSigma = sharp::AttrAsDouble(options, "sharpenSigma"); baton->sharpenSigma = sharp::AttrAsDouble(options, "sharpenSigma");
baton->sharpenFlat = sharp::AttrAsDouble(options, "sharpenFlat"); baton->sharpenM1 = sharp::AttrAsDouble(options, "sharpenM1");
baton->sharpenJagged = sharp::AttrAsDouble(options, "sharpenJagged"); baton->sharpenM2 = sharp::AttrAsDouble(options, "sharpenM2");
baton->sharpenX1 = sharp::AttrAsDouble(options, "sharpenX1");
baton->sharpenY2 = sharp::AttrAsDouble(options, "sharpenY2");
baton->sharpenY3 = sharp::AttrAsDouble(options, "sharpenY3");
baton->threshold = sharp::AttrAsInt32(options, "threshold"); baton->threshold = sharp::AttrAsInt32(options, "threshold");
baton->thresholdGrayscale = sharp::AttrAsBool(options, "thresholdGrayscale"); baton->thresholdGrayscale = sharp::AttrAsBool(options, "thresholdGrayscale");
baton->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold"); baton->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold");

View File

@ -90,8 +90,11 @@ struct PipelineBaton {
double lightness; double lightness;
int medianSize; int medianSize;
double sharpenSigma; double sharpenSigma;
double sharpenFlat; double sharpenM1;
double sharpenJagged; double sharpenM2;
double sharpenX1;
double sharpenY2;
double sharpenY3;
int threshold; int threshold;
bool thresholdGrayscale; bool thresholdGrayscale;
double trimThreshold; double trimThreshold;
@ -234,8 +237,11 @@ struct PipelineBaton {
lightness(0), lightness(0),
medianSize(0), medianSize(0),
sharpenSigma(0.0), sharpenSigma(0.0),
sharpenFlat(1.0), sharpenM1(1.0),
sharpenJagged(2.0), sharpenM2(2.0),
sharpenX1(2.0),
sharpenY2(10.0),
sharpenY3(20.0),
threshold(0), threshold(0),
thresholdGrayscale(true), thresholdGrayscale(true),
trimThreshold(0.0), trimThreshold(0.0),

View File

@ -45,6 +45,22 @@ describe('Sharpen', function () {
}); });
}); });
it('sigma=3.5, m1=2, m2=4', (done) => {
sharp(fixtures.inputJpg)
.resize(320, 240)
.sharpen({ sigma: 3.5, m1: 2, m2: 4 })
.toBuffer()
.then(data => fixtures.assertSimilar(fixtures.expected('sharpen-5-2-4.jpg'), data, done));
});
it('sigma=3.5, m1=2, m2=4, x1=2, y2=5, y3=25', (done) => {
sharp(fixtures.inputJpg)
.resize(320, 240)
.sharpen({ sigma: 3.5, m1: 2, m2: 4, x1: 2, y2: 5, y3: 25 })
.toBuffer()
.then(data => fixtures.assertSimilar(fixtures.expected('sharpen-5-2-4.jpg'), data, done));
});
if (!process.env.SHARP_TEST_WITHOUT_CACHE) { if (!process.env.SHARP_TEST_WITHOUT_CACHE) {
it('specific radius/levels with alpha channel', function (done) { it('specific radius/levels with alpha channel', function (done) {
sharp(fixtures.inputPngWithTransparency) sharp(fixtures.inputPngWithTransparency)
@ -92,6 +108,36 @@ describe('Sharpen', function () {
}); });
}); });
it('invalid options.sigma', () => assert.throws(
() => sharp().sharpen({ sigma: -1 }),
/Expected number between 0\.01 and 10000 for options\.sigma but received -1 of type number/
));
it('invalid options.m1', () => assert.throws(
() => sharp().sharpen({ sigma: 1, m1: -1 }),
/Expected number between 0 and 10000 for options\.m1 but received -1 of type number/
));
it('invalid options.m2', () => assert.throws(
() => sharp().sharpen({ sigma: 1, m2: -1 }),
/Expected number between 0 and 10000 for options\.m2 but received -1 of type number/
));
it('invalid options.x1', () => assert.throws(
() => sharp().sharpen({ sigma: 1, x1: -1 }),
/Expected number between 0 and 10000 for options\.x1 but received -1 of type number/
));
it('invalid options.y2', () => assert.throws(
() => sharp().sharpen({ sigma: 1, y2: -1 }),
/Expected number between 0 and 10000 for options\.y2 but received -1 of type number/
));
it('invalid options.y3', () => assert.throws(
() => sharp().sharpen({ sigma: 1, y3: -1 }),
/Expected number between 0 and 10000 for options\.y3 but received -1 of type number/
));
it('sharpened image is larger than non-sharpened', function (done) { it('sharpened image is larger than non-sharpened', function (done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(320, 240) .resize(320, 240)