diff --git a/docs/api.md b/docs/api.md index d5d36ad8..519953bc 100644 --- a/docs/api.md +++ b/docs/api.md @@ -374,15 +374,15 @@ When used without parameters, performs a fast, mild blur of the output image. Th When a `sigma` is provided, performs a slower, more accurate Gaussian blur. This typically reduces performance by 25%. -* `sigma`, if present, is a Number between 0.3 and 1000 representing the approximate blur radius in pixels. +* `sigma`, if present, is a Number between 0.3 and 1000 representing the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`. -#### sharpen([radius], [flat], [jagged]) +#### sharpen([sigma], [flat], [jagged]) When used without parameters, performs a fast, mild sharpen of the output image. This typically reduces performance by 10%. -When a `radius` 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. This typically reduces performance by 50%. +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. This typically reduces performance by 50%. -* `radius`, if present, is an integral Number representing the sharpen mask radius in pixels. +* `sigma`, if present, is a Number representing the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`. * `flat`, if present, is a Number representing the level of sharpening to apply to "flat" areas, defaulting to a value of 1.0. * `jagged`, if present, is a Number representing the level of sharpening to apply to "jagged" areas, defaulting to a value of 2.0. diff --git a/docs/changelog.md b/docs/changelog.md index 72e4dc03..d53e6be9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,7 @@ * Take advantage of libvips 8.3 features. Use shrink-on-load for WebP input. + Break existing sharpen API to accept sigma and improve precision. [#369](https://github.com/lovell/sharp/issues/369) ### v0.14 - "*needle*" diff --git a/index.js b/index.js index 94e8a7e7..36d0fabe 100644 --- a/index.js +++ b/index.js @@ -80,7 +80,7 @@ var Sharp = function(input, options) { flatten: false, negate: false, blurSigma: 0, - sharpenRadius: 0, + sharpenSigma: 0, sharpenFlat: 1, sharpenJagged: 2, threshold: 0, @@ -154,14 +154,20 @@ var isDefined = function(val) { var isObject = function(val) { return typeof val === 'object'; }; +var isBoolean = function(val) { + return typeof val === 'boolean'; +}; var isBuffer = function(val) { return typeof val === 'object' && val instanceof Buffer; }; var isString = function(val) { return typeof val === 'string' && val.length > 0; }; +var isNumber = function(val) { + return typeof val === 'number' && !Number.isNaN(val); +}; var isInteger = function(val) { - return typeof val === 'number' && !Number.isNaN(val) && val % 1 === 0; + return isNumber(val) && val % 1 === 0; }; var inRange = function(val, min, max) { return val >= min && val <= max; @@ -406,17 +412,17 @@ Sharp.prototype.withoutEnlargement = function(withoutEnlargement) { Call with a sigma to use a slower, more accurate Gaussian blur. */ Sharp.prototype.blur = function(sigma) { - if (typeof sigma === 'undefined') { + if (!isDefined(sigma)) { // No arguments: default to mild blur this.options.blurSigma = -1; - } else if (typeof sigma === 'boolean') { + } else if (isBoolean(sigma)) { // Boolean argument: apply mild blur? this.options.blurSigma = sigma ? -1 : 0; - } else if (typeof sigma === 'number' && !Number.isNaN(sigma) && sigma >= 0.3 && sigma <= 1000) { + } else if (isNumber(sigma) && inRange(sigma, 0.3, 1000)) { // Numeric argument: specific sigma this.options.blurSigma = sigma; } else { - throw new Error('Invalid blur sigma (0.3 to 1000.0) ' + sigma); + throw new Error('Invalid blur sigma (0.3 - 1000.0) ' + sigma); } return this; }; @@ -425,38 +431,38 @@ Sharp.prototype.blur = function(sigma) { Sharpen the output image. Call without a radius to use a fast, mild sharpen. Call with a radius to use a slow, accurate sharpen using the L of LAB colour space. - radius - size of mask in pixels, must be integer + sigma - sigma of mask flat - level of "flat" area sharpen, default 1 jagged - level of "jagged" area sharpen, default 2 */ -Sharp.prototype.sharpen = function(radius, flat, jagged) { - if (typeof radius === 'undefined') { +Sharp.prototype.sharpen = function(sigma, flat, jagged) { + if (!isDefined(sigma)) { // No arguments: default to mild sharpen - this.options.sharpenRadius = -1; - } else if (typeof radius === 'boolean') { + this.options.sharpenSigma = -1; + } else if (isBoolean(sigma)) { // Boolean argument: apply mild sharpen? - this.options.sharpenRadius = radius ? -1 : 0; - } else if (typeof radius === 'number' && !Number.isNaN(radius) && (radius % 1 === 0) && radius >= 1) { - // Numeric argument: specific radius - this.options.sharpenRadius = radius; + this.options.sharpenSigma = sigma ? -1 : 0; + } else if (isNumber(sigma) && inRange(sigma, 0.01, 10000)) { + // Numeric argument: specific sigma + this.options.sharpenSigma = sigma; // Control over flat areas - if (typeof flat !== 'undefined' && flat !== null) { - if (typeof flat === 'number' && !Number.isNaN(flat) && flat >= 0) { + if (isDefined(flat)) { + if (isNumber(flat) && inRange(flat, 0, 10000)) { this.options.sharpenFlat = flat; } else { - throw new Error('Invalid sharpen level for flat areas ' + flat + ' (expected >= 0)'); + throw new Error('Invalid sharpen level for flat areas (0 - 10000) ' + flat); } } // Control over jagged areas - if (typeof jagged !== 'undefined' && jagged !== null) { - if (typeof jagged === 'number' && !Number.isNaN(jagged) && jagged >= 0) { + if (isDefined(jagged)) { + if (isNumber(jagged) && inRange(jagged, 0, 10000)) { this.options.sharpenJagged = jagged; } else { - throw new Error('Invalid sharpen level for jagged areas ' + jagged + ' (expected >= 0)'); + throw new Error('Invalid sharpen level for jagged areas (0 - 10000) ' + jagged); } } } else { - throw new Error('Invalid sharpen radius ' + radius + ' (expected integer >= 1)'); + throw new Error('Invalid sharpen sigma (0.01 - 10000) ' + sigma); } return this; }; diff --git a/src/operations.cc b/src/operations.cc index 555f183d..ca5d41ba 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -135,10 +135,10 @@ namespace sharp { } /* - * Gaussian blur (use sigma <0 for fast blur) + * Gaussian blur. Use sigma of -1.0 for fast blur. */ VImage Blur(VImage image, double const sigma) { - if (sigma < 0.0) { + if (sigma == -1.0) { // Fast, mild blur - averages neighbouring pixels VImage blur = VImage::new_matrixv(3, 3, 1.0, 1.0, 1.0, @@ -153,10 +153,10 @@ namespace sharp { } /* - * Sharpen flat and jagged areas. Use radius of -1 for fast sharpen. + * Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen. */ - VImage Sharpen(VImage image, int const radius, double const flat, double const jagged) { - if (radius == -1) { + VImage Sharpen(VImage image, double const sigma, double const flat, double const jagged) { + if (sigma == -1.0) { // Fast, mild sharpen VImage sharpen = VImage::new_matrixv(3, 3, -1.0, -1.0, -1.0, @@ -167,7 +167,7 @@ namespace sharp { } else { // Slow, accurate sharpen in LAB colour space, with control over flat vs jagged areas return image.sharpen( - VImage::option()->set("radius", radius)->set("m1", flat)->set("m2", jagged) + VImage::option()->set("sigma", sigma)->set("m1", flat)->set("m2", jagged) ); } } diff --git a/src/operations.h b/src/operations.h index 48b14834..7820cd2e 100644 --- a/src/operations.h +++ b/src/operations.h @@ -25,14 +25,14 @@ namespace sharp { VImage Gamma(VImage image, double const exponent); /* - * Gaussian blur. Use sigma of -1 for fast blur. + * Gaussian blur. Use sigma of -1.0 for fast blur. */ VImage Blur(VImage image, double const sigma); /* - * Sharpen flat and jagged areas. Use radius of -1 for fast sharpen. + * Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen. */ - VImage Sharpen(VImage image, int const radius, double const flat, double const jagged); + VImage Sharpen(VImage image, double const sigma, double const flat, double const jagged); /* Calculate crop area based on image entropy diff --git a/src/pipeline.cc b/src/pipeline.cc index 2fd4fa37..209a2e6d 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -444,7 +444,7 @@ class PipelineWorker : public AsyncWorker { bool shouldAffineTransform = xresidual != 1.0 || yresidual != 1.0; bool shouldBlur = baton->blurSigma != 0.0; - bool shouldSharpen = baton->sharpenRadius != 0; + bool shouldSharpen = baton->sharpenSigma != 0.0; bool shouldThreshold = baton->threshold != 0; bool shouldPremultiplyAlpha = HasAlpha(image) && (shouldAffineTransform || shouldBlur || shouldSharpen || hasOverlay); @@ -598,7 +598,7 @@ class PipelineWorker : public AsyncWorker { // Sharpen if (shouldSharpen) { - image = Sharpen(image, baton->sharpenRadius, baton->sharpenFlat, baton->sharpenJagged); + image = Sharpen(image, baton->sharpenSigma, baton->sharpenFlat, baton->sharpenJagged); } // Composite with overlay, if present @@ -1053,7 +1053,7 @@ NAN_METHOD(pipeline) { baton->flatten = attrAs(options, "flatten"); baton->negate = attrAs(options, "negate"); baton->blurSigma = attrAs(options, "blurSigma"); - baton->sharpenRadius = attrAs(options, "sharpenRadius"); + baton->sharpenSigma = attrAs(options, "sharpenSigma"); baton->sharpenFlat = attrAs(options, "sharpenFlat"); baton->sharpenJagged = attrAs(options, "sharpenJagged"); baton->threshold = attrAs(options, "threshold"); diff --git a/src/pipeline.h b/src/pipeline.h index d7ff6c03..49ca6d39 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -51,7 +51,7 @@ struct PipelineBaton { bool flatten; bool negate; double blurSigma; - int sharpenRadius; + double sharpenSigma; double sharpenFlat; double sharpenJagged; int threshold; @@ -104,7 +104,7 @@ struct PipelineBaton { flatten(false), negate(false), blurSigma(0.0), - sharpenRadius(0), + sharpenSigma(0.0), sharpenFlat(1.0), sharpenJagged(2.0), threshold(0), diff --git a/test/unit/sharpen.js b/test/unit/sharpen.js index 22c75e24..244059de 100644 --- a/test/unit/sharpen.js +++ b/test/unit/sharpen.js @@ -7,10 +7,10 @@ var fixtures = require('../fixtures'); describe('Sharpen', function() { - it('specific radius 10', function(done) { + it('specific radius 10 (sigma 6)', function(done) { sharp(fixtures.inputJpg) .resize(320, 240) - .sharpen(10) + .sharpen(6) .toBuffer(function(err, data, info) { assert.strictEqual('jpeg', info.format); assert.strictEqual(320, info.width); @@ -19,10 +19,10 @@ describe('Sharpen', function() { }); }); - it('specific radius 3 and levels 0.5, 2.5', function(done) { + it('specific radius 3 (sigma 1.5) and levels 0.5, 2.5', function(done) { sharp(fixtures.inputJpg) .resize(320, 240) - .sharpen(3, 0.5, 2.5) + .sharpen(1.5, 0.5, 2.5) .toBuffer(function(err, data, info) { assert.strictEqual('jpeg', info.format); assert.strictEqual(320, info.width); @@ -31,10 +31,10 @@ describe('Sharpen', function() { }); }); - it('specific radius 5 and levels 2, 4', function(done) { + it('specific radius 5 (sigma 3.5) and levels 2, 4', function(done) { sharp(fixtures.inputJpg) .resize(320, 240) - .sharpen(5, 2, 4) + .sharpen(3.5, 2, 4) .toBuffer(function(err, data, info) { assert.strictEqual('jpeg', info.format); assert.strictEqual(320, info.width); @@ -55,9 +55,9 @@ describe('Sharpen', function() { }); }); - it('invalid radius', function() { + it('invalid sigma', function() { assert.throws(function() { - sharp(fixtures.inputJpg).sharpen(1.5); + sharp(fixtures.inputJpg).sharpen(-1.5); }); });