Ensure correct Gaussian blur before affine #121

Use double sigma instead of int radius for blur
This commit is contained in:
Lovell Fuller 2014-11-20 13:59:39 +00:00
parent 177a4f574c
commit b7c7fc22f3
6 changed files with 57 additions and 38 deletions

View File

@ -319,13 +319,13 @@ Do not enlarge the output image if the input image width *or* height are already
This is equivalent to GraphicsMagick's `>` geometry option: "change the dimensions of the image only if its width or height exceeds the geometry specification". This is equivalent to GraphicsMagick's `>` geometry option: "change the dimensions of the image only if its width or height exceeds the geometry specification".
#### blur([radius]) #### blur([sigma])
When used without parameters, performs a fast, mild blur of the output image. This typically reduces performance by 10%. When used without parameters, performs a fast, mild blur of the output image. This typically reduces performance by 10%.
When a `radius` is provided, performs a slower, more accurate Gaussian blur. This typically reduces performance by 30%. When a `sigma` is provided, performs a slower, more accurate Gaussian blur. This typically reduces performance by 25%.
* `radius`, if present, is an integral Number representing the approximate blur mask radius in pixels. * `sigma`, if present, is a Number between 0.3 and 1000 representing the approximate blur radius in pixels.
#### sharpen([radius], [flat], [jagged]) #### sharpen([radius], [flat], [jagged])

View File

@ -43,7 +43,7 @@ var Sharp = function(input) {
// operations // operations
background: [0, 0, 0, 255], background: [0, 0, 0, 255],
flatten: false, flatten: false,
blurRadius: 0, blurSigma: 0,
sharpenRadius: 0, sharpenRadius: 0,
sharpenFlat: 1, sharpenFlat: 1,
sharpenJagged: 2, sharpenJagged: 2,
@ -208,21 +208,21 @@ Sharp.prototype.withoutEnlargement = function(withoutEnlargement) {
/* /*
Blur the output image. Blur the output image.
Call without a radius to use a fast, mild blur. Call without a sigma to use a fast, mild blur.
Call with a radius to use a slower, more accurate Gaussian blur. Call with a sigma to use a slower, more accurate Gaussian blur.
*/ */
Sharp.prototype.blur = function(radius) { Sharp.prototype.blur = function(sigma) {
if (typeof radius === 'undefined') { if (typeof sigma === 'undefined') {
// No arguments: default to mild blur // No arguments: default to mild blur
this.options.blurRadius = -1; this.options.blurSigma = -1;
} else if (typeof radius === 'boolean') { } else if (typeof sigma === 'boolean') {
// Boolean argument: apply mild blur? // Boolean argument: apply mild blur?
this.options.blurRadius = radius ? -1 : 0; this.options.blurSigma = sigma ? -1 : 0;
} else if (typeof radius === 'number' && !Number.isNaN(radius) && (radius % 1 === 0) && radius >= 1) { } else if (typeof sigma === 'number' && !Number.isNaN(sigma) && sigma >= 0.3 && sigma <= 1000) {
// Numeric argument: specific radius // Numeric argument: specific sigma
this.options.blurRadius = radius; this.options.blurSigma = sigma;
} else { } else {
throw new Error('Invalid blur radius ' + radius + ' (expected integer >= 1)'); throw new Error('Invalid blur sigma (0.3 to 1000.0) ' + sigma);
} }
return this; return this;
}; };

View File

@ -1,6 +1,6 @@
{ {
"name": "sharp", "name": "sharp",
"version": "0.8.0", "version": "0.8.1",
"author": "Lovell Fuller <npm@lovell.info>", "author": "Lovell Fuller <npm@lovell.info>",
"contributors": [ "contributors": [
"Pierre Inglebert <pierre.inglebert@gmail.com>", "Pierre Inglebert <pierre.inglebert@gmail.com>",

View File

@ -51,7 +51,7 @@ struct ResizeBaton {
std::string interpolator; std::string interpolator;
double background[4]; double background[4];
bool flatten; bool flatten;
int blurRadius; double blurSigma;
int sharpenRadius; int sharpenRadius;
double sharpenFlat; double sharpenFlat;
double sharpenJagged; double sharpenJagged;
@ -78,7 +78,7 @@ struct ResizeBaton {
canvas(Canvas::CROP), canvas(Canvas::CROP),
gravity(0), gravity(0),
flatten(false), flatten(false),
blurRadius(0), blurSigma(0.0),
sharpenRadius(0), sharpenRadius(0),
sharpenFlat(1.0), sharpenFlat(1.0),
sharpenJagged(2.0), sharpenJagged(2.0),
@ -364,22 +364,33 @@ class ResizeWorker : public NanAsyncWorker {
} }
// Use vips_affine with the remaining float part // Use vips_affine with the remaining float part
if (residual != 0) { if (residual != 0.0) {
// Apply variable blur radius of floor(residual) before large affine reductions // Apply Gaussian blur before large affine reductions
if (residual >= 1) { if (residual < 1.0) {
VipsImage *blurred; // Calculate standard deviation
if (vips_gaussblur(image, &blurred, floor(residual), NULL)) { double sigma = ((1.0 / residual) - 0.5) / 1.5;
return Error(baton, hook); if (sigma >= 0.3) {
// Create Gaussian function for standard deviation
VipsImage *gaussian;
if (vips_gaussmat(&gaussian, sigma, 0.2, "separable", TRUE, "integer", TRUE, NULL)) {
return Error(baton, hook);
}
vips_object_local(hook, gaussian);
// Apply Gaussian function
VipsImage *blurred;
if (vips_convsep(image, &blurred, gaussian, "precision", VIPS_PRECISION_INTEGER, NULL)) {
return Error(baton, hook);
}
vips_object_local(hook, blurred);
image = blurred;
} }
vips_object_local(hook, blurred);
image = blurred;
} }
// Create interpolator - "bilinear" (default), "bicubic" or "nohalo" // Create interpolator - "bilinear" (default), "bicubic" or "nohalo"
VipsInterpolate *interpolator = vips_interpolate_new(baton->interpolator.c_str()); VipsInterpolate *interpolator = vips_interpolate_new(baton->interpolator.c_str());
vips_object_local(hook, interpolator); vips_object_local(hook, interpolator);
// Perform affine transformation // Perform affine transformation
VipsImage *affined; VipsImage *affined;
if (vips_affine(image, &affined, residual, 0, 0, residual, "interpolate", interpolator, NULL)) { if (vips_affine(image, &affined, residual, 0.0, 0.0, residual, "interpolate", interpolator, NULL)) {
return Error(baton, hook); return Error(baton, hook);
} }
vips_object_local(hook, affined); vips_object_local(hook, affined);
@ -502,10 +513,10 @@ class ResizeWorker : public NanAsyncWorker {
} }
// Blur // Blur
if (baton->blurRadius != 0) { if (baton->blurSigma != 0.0) {
VipsImage *blurred; VipsImage *blurred;
if (baton->blurRadius == -1) { if (baton->blurSigma < 0.0) {
// Fast, mild blur // Fast, mild blur - averages neighbouring pixels
VipsImage *blur = vips_image_new_matrixv(3, 3, VipsImage *blur = vips_image_new_matrixv(3, 3,
1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
@ -517,7 +528,15 @@ class ResizeWorker : public NanAsyncWorker {
} }
} else { } else {
// Slower, accurate Gaussian blur // Slower, accurate Gaussian blur
if (vips_gaussblur(image, &blurred, baton->blurRadius, NULL)) { // Create Gaussian function for standard deviation
VipsImage *gaussian;
if (vips_gaussmat(&gaussian, baton->blurSigma, 0.2, "separable", TRUE, "integer", TRUE, NULL)) {
return Error(baton, hook);
}
vips_object_local(hook, gaussian);
// Apply Gaussian function
VipsImage *blurred;
if (vips_convsep(image, &blurred, gaussian, "precision", VIPS_PRECISION_INTEGER, NULL)) {
return Error(baton, hook); return Error(baton, hook);
} }
} }
@ -847,7 +866,7 @@ NAN_METHOD(resize) {
baton->interpolator = *String::Utf8Value(options->Get(NanNew<String>("interpolator"))->ToString()); baton->interpolator = *String::Utf8Value(options->Get(NanNew<String>("interpolator"))->ToString());
// Operators // Operators
baton->flatten = options->Get(NanNew<String>("flatten"))->BooleanValue(); baton->flatten = options->Get(NanNew<String>("flatten"))->BooleanValue();
baton->blurRadius = options->Get(NanNew<String>("blurRadius"))->Int32Value(); baton->blurSigma = options->Get(NanNew<String>("blurSigma"))->NumberValue();
baton->sharpenRadius = options->Get(NanNew<String>("sharpenRadius"))->Int32Value(); baton->sharpenRadius = options->Get(NanNew<String>("sharpenRadius"))->Int32Value();
baton->sharpenFlat = options->Get(NanNew<String>("sharpenFlat"))->NumberValue(); baton->sharpenFlat = options->Get(NanNew<String>("sharpenFlat"))->NumberValue();
baton->sharpenJagged = options->Get(NanNew<String>("sharpenJagged"))->NumberValue(); baton->sharpenJagged = options->Get(NanNew<String>("sharpenJagged"))->NumberValue();

View File

@ -35,11 +35,11 @@ describe('Blur', function() {
}); });
}); });
it('specific radius 100', function(done) { it('specific radius 0.3', function(done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(320, 240) .resize(320, 240)
.blur(100) .blur(0.3)
.toFile(fixtures.path('output.blur-100.jpg'), function(err, info) { .toFile(fixtures.path('output.blur-0.3.jpg'), function(err, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual('jpeg', info.format); assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width); assert.strictEqual(320, info.width);
@ -64,7 +64,7 @@ describe('Blur', function() {
it('invalid radius', function(done) { it('invalid radius', function(done) {
var isValid = true; var isValid = true;
try { try {
sharp(fixtures.inputJpg).blur(1.5); sharp(fixtures.inputJpg).blur(0.1);
} catch (err) { } catch (err) {
isValid = false; isValid = false;
} }

View File

@ -370,7 +370,7 @@ describe('Input/output', function() {
}); });
if (semver.gte(sharp.libvipsVersion(), '7.41.0')) { if (semver.gte(sharp.libvipsVersion(), '7.41.0')) {
it('withoutAdaptiveFiltering generates smaller file [libvips 7.41.0+]', function(done) { it('withoutAdaptiveFiltering generates smaller file [libvips ' + sharp.libvipsVersion() + '>=7.41.0]', function(done) {
// First generate with adaptive filtering // First generate with adaptive filtering
sharp(fixtures.inputPng) sharp(fixtures.inputPng)
.resize(320, 240) .resize(320, 240)
@ -401,7 +401,7 @@ describe('Input/output', function() {
}); });
if (semver.gte(sharp.libvipsVersion(), '7.40.0')) { if (semver.gte(sharp.libvipsVersion(), '7.40.0')) {
it('Load TIFF from Buffer [libvips 7.40.0+]', function(done) { it('Load TIFF from Buffer [libvips ' + sharp.libvipsVersion() + '>=7.40.0]', function(done) {
var inputTiffBuffer = fs.readFileSync(fixtures.inputTiff); var inputTiffBuffer = fs.readFileSync(fixtures.inputTiff);
sharp(inputTiffBuffer) sharp(inputTiffBuffer)
.resize(320, 240) .resize(320, 240)