Control level of sharpening via radius/flat/jagged #108

This commit is contained in:
Lovell Fuller 2014-11-10 16:20:04 +00:00
parent 47927ef47d
commit 86681100b7
6 changed files with 134 additions and 33 deletions

View File

@ -319,9 +319,15 @@ 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".
#### sharpen() #### sharpen([radius], [flat], [jagged])
Perform a mild sharpen of the output image. This typically reduces performance by 10%. When used without parameters, perform a fast, mild sharpen of the output image. This typically reduces performance by 10%.
When a `radius` is provided, perform 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.
* `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.
#### interpolateWith(interpolator) #### interpolateWith(interpolator)

View File

@ -43,7 +43,9 @@ var Sharp = function(input) {
// operations // operations
background: [0, 0, 0, 255], background: [0, 0, 0, 255],
flatten: false, flatten: false,
sharpen: false, sharpenRadius: 0,
sharpenFlat: 1,
sharpenJagged: 2,
gamma: 0, gamma: 0,
greyscale: false, greyscale: false,
// output options // output options
@ -137,16 +139,6 @@ Sharp.prototype.extract = function(topOffset, leftOffset, width, height) {
return this; return this;
}; };
/*
Deprecated embed* methods, to be removed in v0.8.0
*/
Sharp.prototype.embedWhite = util.deprecate(function() {
return this.background('white').embed();
}, "embedWhite() is deprecated, use background('white').embed() instead");
Sharp.prototype.embedBlack = util.deprecate(function() {
return this.background('black').embed();
}, "embedBlack() is deprecated, use background('black').embed() instead");
/* /*
Set the background colour for embed and flatten operations. Set the background colour for embed and flatten operations.
Delegates to the 'Color' module, which can throw an Error Delegates to the 'Color' module, which can throw an Error
@ -215,8 +207,35 @@ Sharp.prototype.withoutEnlargement = function(withoutEnlargement) {
return this; return this;
}; };
Sharp.prototype.sharpen = function(sharpen) { /*
this.options.sharpen = (typeof sharpen === 'boolean') ? sharpen : true; 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
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') {
// No arguments: default to mild sharpen
this.options.sharpenRadius = -1;
} else if (typeof radius === 'boolean') {
// Boolean argument: apply mild sharpen?
this.options.sharpenRadius = radius ? -1 : 0;
} else if (typeof radius === 'number' && !Number.isNaN(radius) && (radius % 1 === 0)) {
// Numeric argument: specific radius
this.options.sharpenRadius = radius;
if (typeof flat === 'number' && !Number.isNaN(flat)) {
// Control over flat areas
this.options.sharpenFlat = flat;
}
if (typeof jagged === 'number' && !Number.isNaN(jagged)) {
// Control over jagged areas
this.options.sharpenJagged = jagged;
}
} else {
throw new Error('Invalid integral sharpen radius ' + radius);
}
return this; return this;
}; };

View File

@ -1,6 +1,6 @@
{ {
"name": "sharp", "name": "sharp",
"version": "0.7.2", "version": "0.8.0",
"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>",
@ -43,7 +43,7 @@
"dependencies": { "dependencies": {
"bluebird": "^2.3.11", "bluebird": "^2.3.11",
"color": "^0.7.1", "color": "^0.7.1",
"nan": "^1.4.0", "nan": "^1.4.1",
"semver": "^4.1.0" "semver": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -50,7 +50,9 @@ struct ResizeBaton {
std::string interpolator; std::string interpolator;
double background[4]; double background[4];
bool flatten; bool flatten;
bool sharpen; int sharpenRadius;
double sharpenFlat;
double sharpenJagged;
double gamma; double gamma;
bool greyscale; bool greyscale;
int angle; int angle;
@ -74,7 +76,9 @@ struct ResizeBaton {
canvas(CROP), canvas(CROP),
gravity(0), gravity(0),
flatten(false), flatten(false),
sharpen(false), sharpenRadius(0),
sharpenFlat(1.0),
sharpenJagged(2.0),
gamma(0.0), gamma(0.0),
greyscale(false), greyscale(false),
flip(false), flip(false),
@ -506,10 +510,12 @@ class ResizeWorker : public NanAsyncWorker {
image = extractedPost; image = extractedPost;
} }
// Mild sharpen // Sharpen
if (baton->sharpen) { if (baton->sharpenRadius != 0) {
VipsImage *sharpened = vips_image_new(); VipsImage *sharpened = vips_image_new();
vips_object_local(hook, sharpened); vips_object_local(hook, sharpened);
if (baton->sharpenRadius == -1) {
// Fast, mild sharpen
VipsImage *sharpen = vips_image_new_matrixv(3, 3, VipsImage *sharpen = vips_image_new_matrixv(3, 3,
-1.0, -1.0, -1.0, -1.0, -1.0, -1.0,
-1.0, 32.0, -1.0, -1.0, 32.0, -1.0,
@ -519,6 +525,12 @@ class ResizeWorker : public NanAsyncWorker {
if (vips_conv(image, &sharpened, sharpen, NULL)) { if (vips_conv(image, &sharpened, sharpen, NULL)) {
return Error(baton, hook); return Error(baton, hook);
} }
} else {
// Slow, accurate sharpen in LAB colour space, with control over flat vs jagged areas
if (vips_sharpen(image, &sharpened, "radius", baton->sharpenRadius, "m1", baton->sharpenFlat, "m2", baton->sharpenJagged, NULL)) {
return Error(baton, hook);
}
}
g_object_unref(image); g_object_unref(image);
image = sharpened; image = sharpened;
} }
@ -838,7 +850,9 @@ 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->sharpen = options->Get(NanNew<String>("sharpen"))->BooleanValue(); baton->sharpenRadius = options->Get(NanNew<String>("sharpenRadius"))->Int32Value();
baton->sharpenFlat = options->Get(NanNew<String>("sharpenFlat"))->NumberValue();
baton->sharpenJagged = options->Get(NanNew<String>("sharpenJagged"))->NumberValue();
baton->gamma = options->Get(NanNew<String>("gamma"))->NumberValue(); baton->gamma = options->Get(NanNew<String>("gamma"))->NumberValue();
baton->greyscale = options->Get(NanNew<String>("greyscale"))->BooleanValue(); baton->greyscale = options->Get(NanNew<String>("greyscale"))->BooleanValue();
baton->angle = options->Get(NanNew<String>("angle"))->Int32Value(); baton->angle = options->Get(NanNew<String>("angle"))->Int32Value();

View File

@ -163,7 +163,7 @@ async.series({
deferred.resolve(); deferred.resolve();
}); });
} }
}).add('sharp-sharpen', { }).add('sharp-sharpen-mild', {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).sharpen().toBuffer(function(err, buffer) { sharp(inputJpgBuffer).resize(width, height).sharpen().toBuffer(function(err, buffer) {
@ -175,6 +175,18 @@ async.series({
} }
}); });
} }
}).add('sharp-sharpen-radius', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).sharpen(3, 1, 3).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-nearest-neighbour', { }).add('sharp-nearest-neighbour', {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {

View File

@ -7,7 +7,57 @@ var fixtures = require('../fixtures');
describe('Sharpen', function() { describe('Sharpen', function() {
it('sharpen image is larger than non-sharpen', function(done) { it('specific radius and levels 0.5, 2.5', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.sharpen(3, 0.5, 2.5)
.toFile(fixtures.path('output.sharpen-3-0.5-2.5.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('specific radius 3 and levels 2, 4', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.sharpen(5, 2, 4)
.toFile(fixtures.path('output.sharpen-5-2-4.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('mild sharpen', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.sharpen()
.toFile(fixtures.path('output.sharpen-mild.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('invalid radius', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpg).sharpen(1.5);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('sharpened image is larger than non-sharpened', function(done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(320, 240) .resize(320, 240)
.sharpen(false) .sharpen(false)
@ -19,7 +69,7 @@ describe('Sharpen', function() {
assert.strictEqual(240, info.height); assert.strictEqual(240, info.height);
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(320, 240) .resize(320, 240)
.sharpen() .sharpen(true)
.toBuffer(function(err, sharpened, info) { .toBuffer(function(err, sharpened, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, sharpened.length > 0); assert.strictEqual(true, sharpened.length > 0);