Skip to content

Commit

Permalink
Ensure correct Gaussian blur before affine #121
Browse files Browse the repository at this point in the history
Use double sigma instead of int radius for blur
  • Loading branch information
lovell committed Nov 20, 2014
1 parent 177a4f5 commit b7c7fc2
Show file tree
Hide file tree
Showing 6 changed files with 57 additions and 38 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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".

#### blur([radius])
#### blur([sigma])

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])

Expand Down
24 changes: 12 additions & 12 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ var Sharp = function(input) {
// operations
background: [0, 0, 0, 255],
flatten: false,
blurRadius: 0,
blurSigma: 0,
sharpenRadius: 0,
sharpenFlat: 1,
sharpenJagged: 2,
Expand Down Expand Up @@ -208,21 +208,21 @@ Sharp.prototype.withoutEnlargement = function(withoutEnlargement) {

/*
Blur the output image.
Call without a radius to use a fast, mild blur.
Call with a radius to use a slower, more accurate Gaussian blur.
Call without a sigma to use a fast, mild blur.
Call with a sigma to use a slower, more accurate Gaussian blur.
*/
Sharp.prototype.blur = function(radius) {
if (typeof radius === 'undefined') {
Sharp.prototype.blur = function(sigma) {
if (typeof sigma === 'undefined') {
// No arguments: default to mild blur
this.options.blurRadius = -1;
} else if (typeof radius === 'boolean') {
this.options.blurSigma = -1;
} else if (typeof sigma === 'boolean') {
// Boolean argument: apply mild blur?
this.options.blurRadius = radius ? -1 : 0;
} else if (typeof radius === 'number' && !Number.isNaN(radius) && (radius % 1 === 0) && radius >= 1) {
// Numeric argument: specific radius
this.options.blurRadius = radius;
this.options.blurSigma = sigma ? -1 : 0;
} else if (typeof sigma === 'number' && !Number.isNaN(sigma) && sigma >= 0.3 && sigma <= 1000) {
// Numeric argument: specific sigma
this.options.blurSigma = sigma;
} 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;
};
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sharp",
"version": "0.8.0",
"version": "0.8.1",
"author": "Lovell Fuller <npm@lovell.info>",
"contributors": [
"Pierre Inglebert <pierre.inglebert@gmail.com>",
Expand Down
51 changes: 35 additions & 16 deletions src/resize.cc
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ struct ResizeBaton {
std::string interpolator;
double background[4];
bool flatten;
int blurRadius;
double blurSigma;
int sharpenRadius;
double sharpenFlat;
double sharpenJagged;
Expand All @@ -78,7 +78,7 @@ struct ResizeBaton {
canvas(Canvas::CROP),
gravity(0),
flatten(false),
blurRadius(0),
blurSigma(0.0),
sharpenRadius(0),
sharpenFlat(1.0),
sharpenJagged(2.0),
Expand Down Expand Up @@ -364,22 +364,33 @@ class ResizeWorker : public NanAsyncWorker {
}

// Use vips_affine with the remaining float part
if (residual != 0) {
// Apply variable blur radius of floor(residual) before large affine reductions
if (residual >= 1) {
VipsImage *blurred;
if (vips_gaussblur(image, &blurred, floor(residual), NULL)) {
return Error(baton, hook);
if (residual != 0.0) {
// Apply Gaussian blur before large affine reductions
if (residual < 1.0) {
// Calculate standard deviation
double sigma = ((1.0 / residual) - 0.5) / 1.5;
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"
VipsInterpolate *interpolator = vips_interpolate_new(baton->interpolator.c_str());
vips_object_local(hook, interpolator);
// Perform affine transformation
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);
}
vips_object_local(hook, affined);
Expand Down Expand Up @@ -502,10 +513,10 @@ class ResizeWorker : public NanAsyncWorker {
}

// Blur
if (baton->blurRadius != 0) {
if (baton->blurSigma != 0.0) {
VipsImage *blurred;
if (baton->blurRadius == -1) {
// Fast, mild blur
if (baton->blurSigma < 0.0) {
// Fast, mild blur - averages neighbouring pixels
VipsImage *blur = vips_image_new_matrixv(3, 3,
1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
Expand All @@ -517,7 +528,15 @@ class ResizeWorker : public NanAsyncWorker {
}
} else {
// 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);
}
}
Expand Down Expand Up @@ -847,7 +866,7 @@ NAN_METHOD(resize) {
baton->interpolator = *String::Utf8Value(options->Get(NanNew<String>("interpolator"))->ToString());
// Operators
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->sharpenFlat = options->Get(NanNew<String>("sharpenFlat"))->NumberValue();
baton->sharpenJagged = options->Get(NanNew<String>("sharpenJagged"))->NumberValue();
Expand Down
8 changes: 4 additions & 4 deletions test/unit/blur.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ describe('Blur', function() {
});
});

it('specific radius 100', function(done) {
it('specific radius 0.3', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.blur(100)
.toFile(fixtures.path('output.blur-100.jpg'), function(err, info) {
.blur(0.3)
.toFile(fixtures.path('output.blur-0.3.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
Expand All @@ -64,7 +64,7 @@ describe('Blur', function() {
it('invalid radius', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpg).blur(1.5);
sharp(fixtures.inputJpg).blur(0.1);
} catch (err) {
isValid = false;
}
Expand Down
4 changes: 2 additions & 2 deletions test/unit/io.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ describe('Input/output', function() {
});

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
sharp(fixtures.inputPng)
.resize(320, 240)
Expand Down Expand Up @@ -401,7 +401,7 @@ describe('Input/output', function() {
});

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);
sharp(inputTiffBuffer)
.resize(320, 240)
Expand Down

0 comments on commit b7c7fc2

Please sign in to comment.