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".
#### 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)

View File

@ -43,7 +43,9 @@ var Sharp = function(input) {
// operations
background: [0, 0, 0, 255],
flatten: false,
sharpen: false,
sharpenRadius: 0,
sharpenFlat: 1,
sharpenJagged: 2,
gamma: 0,
greyscale: false,
// output options
@ -137,16 +139,6 @@ Sharp.prototype.extract = function(topOffset, leftOffset, width, height) {
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.
Delegates to the 'Color' module, which can throw an Error
@ -215,8 +207,35 @@ Sharp.prototype.withoutEnlargement = function(withoutEnlargement) {
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;
};

View File

@ -1,6 +1,6 @@
{
"name": "sharp",
"version": "0.7.2",
"version": "0.8.0",
"author": "Lovell Fuller <npm@lovell.info>",
"contributors": [
"Pierre Inglebert <pierre.inglebert@gmail.com>",
@ -43,7 +43,7 @@
"dependencies": {
"bluebird": "^2.3.11",
"color": "^0.7.1",
"nan": "^1.4.0",
"nan": "^1.4.1",
"semver": "^4.1.0"
},
"devDependencies": {

View File

@ -50,7 +50,9 @@ struct ResizeBaton {
std::string interpolator;
double background[4];
bool flatten;
bool sharpen;
int sharpenRadius;
double sharpenFlat;
double sharpenJagged;
double gamma;
bool greyscale;
int angle;
@ -74,7 +76,9 @@ struct ResizeBaton {
canvas(CROP),
gravity(0),
flatten(false),
sharpen(false),
sharpenRadius(0),
sharpenFlat(1.0),
sharpenJagged(2.0),
gamma(0.0),
greyscale(false),
flip(false),
@ -506,10 +510,12 @@ class ResizeWorker : public NanAsyncWorker {
image = extractedPost;
}
// Mild sharpen
if (baton->sharpen) {
// Sharpen
if (baton->sharpenRadius != 0) {
VipsImage *sharpened = vips_image_new();
vips_object_local(hook, sharpened);
if (baton->sharpenRadius == -1) {
// Fast, mild sharpen
VipsImage *sharpen = vips_image_new_matrixv(3, 3,
-1.0, -1.0, -1.0,
-1.0, 32.0, -1.0,
@ -519,6 +525,12 @@ class ResizeWorker : public NanAsyncWorker {
if (vips_conv(image, &sharpened, sharpen, NULL)) {
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);
image = sharpened;
}
@ -838,7 +850,9 @@ NAN_METHOD(resize) {
baton->interpolator = *String::Utf8Value(options->Get(NanNew<String>("interpolator"))->ToString());
// Operators
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->greyscale = options->Get(NanNew<String>("greyscale"))->BooleanValue();
baton->angle = options->Get(NanNew<String>("angle"))->Int32Value();

View File

@ -163,7 +163,7 @@ async.series({
deferred.resolve();
});
}
}).add('sharp-sharpen', {
}).add('sharp-sharpen-mild', {
defer: true,
fn: function(deferred) {
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', {
defer: true,
fn: function(deferred) {

View File

@ -7,7 +7,57 @@ var fixtures = require('../fixtures');
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)
.resize(320, 240)
.sharpen(false)
@ -19,7 +69,7 @@ describe('Sharpen', function() {
assert.strictEqual(240, info.height);
sharp(fixtures.inputJpg)
.resize(320, 240)
.sharpen()
.sharpen(true)
.toBuffer(function(err, sharpened, info) {
if (err) throw err;
assert.strictEqual(true, sharpened.length > 0);