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%.
* `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.

View File

@ -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*"

View File

@ -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;
};

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) {
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)
);
}
}

View File

@ -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

View File

@ -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<bool>(options, "flatten");
baton->negate = attrAs<bool>(options, "negate");
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->sharpenJagged = attrAs<double>(options, "sharpenJagged");
baton->threshold = attrAs<int32_t>(options, "threshold");

View File

@ -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),

View File

@ -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);
});
});