Break existing sharpen API to accept sigma and improve precision

This commit is contained in:
Lovell Fuller 2016-03-31 22:00:33 +01:00
parent ee21d2991c
commit b7a098fb28
8 changed files with 55 additions and 48 deletions

View File

@ -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%. 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 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. * `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. * `jagged`, if present, is a Number representing the level of sharpening to apply to "jagged" areas, defaulting to a value of 2.0.

View File

@ -4,6 +4,7 @@
* Take advantage of libvips 8.3 features. * Take advantage of libvips 8.3 features.
Use shrink-on-load for WebP input. 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) [#369](https://github.com/lovell/sharp/issues/369)
### v0.14 - "*needle*" ### v0.14 - "*needle*"

View File

@ -80,7 +80,7 @@ var Sharp = function(input, options) {
flatten: false, flatten: false,
negate: false, negate: false,
blurSigma: 0, blurSigma: 0,
sharpenRadius: 0, sharpenSigma: 0,
sharpenFlat: 1, sharpenFlat: 1,
sharpenJagged: 2, sharpenJagged: 2,
threshold: 0, threshold: 0,
@ -154,14 +154,20 @@ var isDefined = function(val) {
var isObject = function(val) { var isObject = function(val) {
return typeof val === 'object'; return typeof val === 'object';
}; };
var isBoolean = function(val) {
return typeof val === 'boolean';
};
var isBuffer = function(val) { var isBuffer = function(val) {
return typeof val === 'object' && val instanceof Buffer; return typeof val === 'object' && val instanceof Buffer;
}; };
var isString = function(val) { var isString = function(val) {
return typeof val === 'string' && val.length > 0; return typeof val === 'string' && val.length > 0;
}; };
var isNumber = function(val) {
return typeof val === 'number' && !Number.isNaN(val);
};
var isInteger = function(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) { var inRange = function(val, min, max) {
return val >= min && val <= 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. Call with a sigma to use a slower, more accurate Gaussian blur.
*/ */
Sharp.prototype.blur = function(sigma) { Sharp.prototype.blur = function(sigma) {
if (typeof sigma === 'undefined') { if (!isDefined(sigma)) {
// No arguments: default to mild blur // No arguments: default to mild blur
this.options.blurSigma = -1; this.options.blurSigma = -1;
} else if (typeof sigma === 'boolean') { } else if (isBoolean(sigma)) {
// Boolean argument: apply mild blur? // Boolean argument: apply mild blur?
this.options.blurSigma = sigma ? -1 : 0; 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 // Numeric argument: specific sigma
this.options.blurSigma = sigma; this.options.blurSigma = sigma;
} else { } 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; return this;
}; };
@ -425,38 +431,38 @@ Sharp.prototype.blur = function(sigma) {
Sharpen the output image. Sharpen the output image.
Call without a radius to use a fast, mild sharpen. 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. 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 flat - level of "flat" area sharpen, default 1
jagged - level of "jagged" area sharpen, default 2 jagged - level of "jagged" area sharpen, default 2
*/ */
Sharp.prototype.sharpen = function(radius, flat, jagged) { Sharp.prototype.sharpen = function(sigma, flat, jagged) {
if (typeof radius === 'undefined') { if (!isDefined(sigma)) {
// No arguments: default to mild sharpen // No arguments: default to mild sharpen
this.options.sharpenRadius = -1; this.options.sharpenSigma = -1;
} else if (typeof radius === 'boolean') { } else if (isBoolean(sigma)) {
// Boolean argument: apply mild sharpen? // Boolean argument: apply mild sharpen?
this.options.sharpenRadius = radius ? -1 : 0; this.options.sharpenSigma = sigma ? -1 : 0;
} else if (typeof radius === 'number' && !Number.isNaN(radius) && (radius % 1 === 0) && radius >= 1) { } else if (isNumber(sigma) && inRange(sigma, 0.01, 10000)) {
// Numeric argument: specific radius // Numeric argument: specific sigma
this.options.sharpenRadius = radius; this.options.sharpenSigma = sigma;
// Control over flat areas // Control over flat areas
if (typeof flat !== 'undefined' && flat !== null) { if (isDefined(flat)) {
if (typeof flat === 'number' && !Number.isNaN(flat) && flat >= 0) { if (isNumber(flat) && inRange(flat, 0, 10000)) {
this.options.sharpenFlat = flat; this.options.sharpenFlat = flat;
} else { } 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 // Control over jagged areas
if (typeof jagged !== 'undefined' && jagged !== null) { if (isDefined(jagged)) {
if (typeof jagged === 'number' && !Number.isNaN(jagged) && jagged >= 0) { if (isNumber(jagged) && inRange(jagged, 0, 10000)) {
this.options.sharpenJagged = jagged; this.options.sharpenJagged = jagged;
} else { } 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 { } else {
throw new Error('Invalid sharpen radius ' + radius + ' (expected integer >= 1)'); throw new Error('Invalid sharpen sigma (0.01 - 10000) ' + sigma);
} }
return this; return this;
}; };

View File

@ -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) { VImage Blur(VImage image, double const sigma) {
if (sigma < 0.0) { if (sigma == -1.0) {
// Fast, mild blur - averages neighbouring pixels // Fast, mild blur - averages neighbouring pixels
VImage blur = VImage::new_matrixv(3, 3, VImage blur = VImage::new_matrixv(3, 3,
1.0, 1.0, 1.0, 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) { VImage Sharpen(VImage image, double const sigma, double const flat, double const jagged) {
if (radius == -1) { if (sigma == -1.0) {
// Fast, mild sharpen // Fast, mild sharpen
VImage sharpen = VImage::new_matrixv(3, 3, VImage sharpen = VImage::new_matrixv(3, 3,
-1.0, -1.0, -1.0, -1.0, -1.0, -1.0,
@ -167,7 +167,7 @@ namespace sharp {
} else { } else {
// Slow, accurate sharpen in LAB colour space, with control over flat vs jagged areas // Slow, accurate sharpen in LAB colour space, with control over flat vs jagged areas
return image.sharpen( 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)
); );
} }
} }

View File

@ -25,14 +25,14 @@ namespace sharp {
VImage Gamma(VImage image, double const exponent); 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); 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 Calculate crop area based on image entropy

View File

@ -444,7 +444,7 @@ class PipelineWorker : public AsyncWorker {
bool shouldAffineTransform = xresidual != 1.0 || yresidual != 1.0; bool shouldAffineTransform = xresidual != 1.0 || yresidual != 1.0;
bool shouldBlur = baton->blurSigma != 0.0; bool shouldBlur = baton->blurSigma != 0.0;
bool shouldSharpen = baton->sharpenRadius != 0; bool shouldSharpen = baton->sharpenSigma != 0.0;
bool shouldThreshold = baton->threshold != 0; bool shouldThreshold = baton->threshold != 0;
bool shouldPremultiplyAlpha = HasAlpha(image) && bool shouldPremultiplyAlpha = HasAlpha(image) &&
(shouldAffineTransform || shouldBlur || shouldSharpen || hasOverlay); (shouldAffineTransform || shouldBlur || shouldSharpen || hasOverlay);
@ -598,7 +598,7 @@ class PipelineWorker : public AsyncWorker {
// Sharpen // Sharpen
if (shouldSharpen) { 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 // Composite with overlay, if present
@ -1053,7 +1053,7 @@ NAN_METHOD(pipeline) {
baton->flatten = attrAs<bool>(options, "flatten"); baton->flatten = attrAs<bool>(options, "flatten");
baton->negate = attrAs<bool>(options, "negate"); baton->negate = attrAs<bool>(options, "negate");
baton->blurSigma = attrAs<double>(options, "blurSigma"); baton->blurSigma = attrAs<double>(options, "blurSigma");
baton->sharpenRadius = attrAs<int32_t>(options, "sharpenRadius"); baton->sharpenSigma = attrAs<double>(options, "sharpenSigma");
baton->sharpenFlat = attrAs<double>(options, "sharpenFlat"); baton->sharpenFlat = attrAs<double>(options, "sharpenFlat");
baton->sharpenJagged = attrAs<double>(options, "sharpenJagged"); baton->sharpenJagged = attrAs<double>(options, "sharpenJagged");
baton->threshold = attrAs<int32_t>(options, "threshold"); baton->threshold = attrAs<int32_t>(options, "threshold");

View File

@ -51,7 +51,7 @@ struct PipelineBaton {
bool flatten; bool flatten;
bool negate; bool negate;
double blurSigma; double blurSigma;
int sharpenRadius; double sharpenSigma;
double sharpenFlat; double sharpenFlat;
double sharpenJagged; double sharpenJagged;
int threshold; int threshold;
@ -104,7 +104,7 @@ struct PipelineBaton {
flatten(false), flatten(false),
negate(false), negate(false),
blurSigma(0.0), blurSigma(0.0),
sharpenRadius(0), sharpenSigma(0.0),
sharpenFlat(1.0), sharpenFlat(1.0),
sharpenJagged(2.0), sharpenJagged(2.0),
threshold(0), threshold(0),

View File

@ -7,10 +7,10 @@ var fixtures = require('../fixtures');
describe('Sharpen', function() { describe('Sharpen', function() {
it('specific radius 10', function(done) { it('specific radius 10 (sigma 6)', function(done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(320, 240) .resize(320, 240)
.sharpen(10) .sharpen(6)
.toBuffer(function(err, data, info) { .toBuffer(function(err, data, info) {
assert.strictEqual('jpeg', info.format); assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width); 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) sharp(fixtures.inputJpg)
.resize(320, 240) .resize(320, 240)
.sharpen(3, 0.5, 2.5) .sharpen(1.5, 0.5, 2.5)
.toBuffer(function(err, data, info) { .toBuffer(function(err, data, info) {
assert.strictEqual('jpeg', info.format); assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width); 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) sharp(fixtures.inputJpg)
.resize(320, 240) .resize(320, 240)
.sharpen(5, 2, 4) .sharpen(3.5, 2, 4)
.toBuffer(function(err, data, info) { .toBuffer(function(err, data, info) {
assert.strictEqual('jpeg', info.format); assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width); assert.strictEqual(320, info.width);
@ -55,9 +55,9 @@ describe('Sharpen', function() {
}); });
}); });
it('invalid radius', function() { it('invalid sigma', function() {
assert.throws(function() { assert.throws(function() {
sharp(fixtures.inputJpg).sharpen(1.5); sharp(fixtures.inputJpg).sharpen(-1.5);
}); });
}); });