diff --git a/docs/api.md b/docs/api.md index 5c825332..52731882 100644 --- a/docs/api.md +++ b/docs/api.md @@ -381,6 +381,21 @@ When a `sigma` is provided, performs a slower, more accurate Gaussian blur. This * `sigma`, if present, is a Number between 0.3 and 1000 representing the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`. +#### convolve(kernel) + +Convolve the image with the specified `kernel`. The kernel specification takes the following form: + +* `kernel = ` + `{ 'width': N` + `, 'height': M` + `, 'scale': Z` + `, 'offset': Y` + `, 'kernel':` + ` [ 1, 2, 3,` + ` 4, 5, 6,` + ` 7, 8, 9 ]` + `}` + #### sharpen([sigma], [flat], [jagged]) When used without parameters, performs a fast, mild sharpen of the output image. This typically reduces performance by 10%. diff --git a/index.js b/index.js index a19c9475..34d31735 100644 --- a/index.js +++ b/index.js @@ -448,6 +448,43 @@ Sharp.prototype.blur = function(sigma) { return this; }; +/* + Convolve the image with a kernel. + Call with an object of the following form: + { 'width': N + , 'height': M + , 'scale': Z + , 'offset': Y + , 'kernel': + [ 1, 2, 3, + 4, 5, 6, + 7, 8, 9 ] + } +*/ + +Sharp.prototype.convolve = function(kernel) { + if (!isDefined(kernel) || !isDefined(kernel.kernel) || + !isDefined(kernel.width) || !isDefined(kernel.height) || + !inRange(kernel.width,3,1001) || !inRange(kernel.height,3,1001) || + kernel.height * kernel.width != kernel.kernel.length + ) { + // must pass in a kernel + throw new Error('Invalid convolution kernel'); + } + if(!isDefined(kernel.scale)) { + var sum = 0; + kernel.kernel.forEach(function(e) { + sum += e; + }); + kernel.scale = sum; + } + if(!isDefined(kernel.offset)) { + kernel.offset = 0; + } + this.options.convKernel = kernel; + return this; +}; + /* Sharpen the output image. Call without a radius to use a fast, mild sharpen. diff --git a/src/operations.cc b/src/operations.cc index 2cae8a88..cdbc12cf 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -1,5 +1,6 @@ #include #include +#include #include #include "common.h" @@ -211,6 +212,24 @@ namespace sharp { } } + /* + * Convolution with a kernel. + */ + VImage Convolve(VImage image, int width, int height, double scale, double offset, + const std::unique_ptr &kernel_v) { + VImage kernel = VImage::new_from_memory( + kernel_v.get(), + width * height * sizeof(double), + width, + height, + 1, + VIPS_FORMAT_DOUBLE); + kernel.set("scale", scale); + kernel.set("offset", offset); + + return image.conv(kernel); + } + /* * Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen. */ diff --git a/src/operations.h b/src/operations.h index 044807b4..2b307e82 100644 --- a/src/operations.h +++ b/src/operations.h @@ -2,6 +2,7 @@ #define SRC_OPERATIONS_H_ #include +#include #include using vips::VImage; @@ -34,6 +35,12 @@ namespace sharp { */ VImage Blur(VImage image, double const sigma); + /* + * Convolution with a kernel. + */ + VImage Convolve(VImage image, int width, int height, double scale, double offset, + const std::unique_ptr &kernel_v); + /* * Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen. */ diff --git a/src/pipeline.cc b/src/pipeline.cc index a8c567ce..3c4476b2 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -2,6 +2,7 @@ #include #include #include +#include #include @@ -49,6 +50,7 @@ using sharp::Cutout; using sharp::Normalize; using sharp::Gamma; using sharp::Blur; +using sharp::Convolve; using sharp::Sharpen; using sharp::EntropyCrop; using sharp::TileCache; @@ -464,11 +466,12 @@ class PipelineWorker : public AsyncWorker { bool shouldAffineTransform = xresidual != 1.0 || yresidual != 1.0; bool shouldBlur = baton->blurSigma != 0.0; + bool shouldConv = baton->convKernelWidth * baton->convKernelHeight > 0; bool shouldSharpen = baton->sharpenSigma != 0.0; bool shouldThreshold = baton->threshold != 0; bool shouldCutout = baton->overlayCutout; bool shouldPremultiplyAlpha = HasAlpha(image) && - (shouldAffineTransform || shouldBlur || shouldSharpen || (hasOverlay && !shouldCutout)); + (shouldAffineTransform || shouldBlur || shouldConv || shouldSharpen || (hasOverlay && !shouldCutout)); // Premultiply image alpha channel before all transformations to avoid // dark fringing around bright pixels @@ -634,6 +637,14 @@ class PipelineWorker : public AsyncWorker { image = Blur(image, baton->blurSigma); } + // Convolve + if (shouldConv) { + image = Convolve(image, + baton->convKernelWidth, baton->convKernelHeight, + baton->convKernelScale, baton->convKernelOffset, + baton->convKernel); + } + // Sharpen if (shouldSharpen) { image = Sharpen(image, baton->sharpenSigma, baton->sharpenFlat, baton->sharpenJagged); @@ -1151,6 +1162,22 @@ NAN_METHOD(pipeline) { } else { baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_DZ; } + // Convolution Kernel + if(Has(options, New("convKernel").ToLocalChecked()).FromJust()) { + Local kernel = Get(options, New("convKernel").ToLocalChecked()).ToLocalChecked().As(); + baton->convKernelWidth = attrAs(kernel, "width"); + baton->convKernelHeight = attrAs(kernel, "height"); + baton->convKernelScale = attrAs(kernel, "scale"); + baton->convKernelOffset = attrAs(kernel, "offset"); + + size_t kernelSize = baton->convKernelWidth * baton->convKernelHeight; + + baton->convKernel = std::unique_ptr(new double[kernelSize]); + Local kdata = Get(kernel, New("kernel").ToLocalChecked()).ToLocalChecked().As(); + for(unsigned int i = 0; i < kernelSize; i++) { + baton->convKernel[i] = To(Get(kdata, i).ToLocalChecked()).FromJust(); + } + } // Function to notify of queue length changes Callback *queueListener = new Callback( diff --git a/src/pipeline.h b/src/pipeline.h index 8581eedb..2e50be9c 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -1,6 +1,8 @@ #ifndef SRC_PIPELINE_H_ #define SRC_PIPELINE_H_ +#include + #include #include "nan.h" @@ -83,6 +85,11 @@ struct PipelineBaton { std::string err; bool withMetadata; int withMetadataOrientation; + std::unique_ptr convKernel; + int convKernelWidth; + int convKernelHeight; + double convKernelScale; + double convKernelOffset; int tileSize; int tileOverlap; VipsForeignDzContainer tileContainer; @@ -136,6 +143,10 @@ struct PipelineBaton { optimiseScans(false), withMetadata(false), withMetadataOrientation(-1), + convKernelWidth(0), + convKernelHeight(0), + convKernelScale(0.0), + convKernelOffset(0.0), tileSize(256), tileOverlap(0), tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS), diff --git a/test/fixtures/expected/conv-1.png b/test/fixtures/expected/conv-1.png new file mode 100644 index 00000000..f84a06cb Binary files /dev/null and b/test/fixtures/expected/conv-1.png differ diff --git a/test/fixtures/expected/conv-2.png b/test/fixtures/expected/conv-2.png new file mode 100644 index 00000000..416ec65d Binary files /dev/null and b/test/fixtures/expected/conv-2.png differ diff --git a/test/fixtures/index.js b/test/fixtures/index.js index c8c4e3ed..231c8b3b 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -91,6 +91,9 @@ module.exports = { inputJPGBig: getPath('flowers.jpeg'), + inputPngStripesV: getPath('stripesV.png'), + inputPngStripesH: getPath('stripesH.png'), + outputJpg: getPath('output.jpg'), outputPng: getPath('output.png'), outputWebP: getPath('output.webp'), diff --git a/test/fixtures/stripesH.png b/test/fixtures/stripesH.png new file mode 100644 index 00000000..f0cff675 Binary files /dev/null and b/test/fixtures/stripesH.png differ diff --git a/test/fixtures/stripesV.png b/test/fixtures/stripesV.png new file mode 100644 index 00000000..d4482e95 Binary files /dev/null and b/test/fixtures/stripesV.png differ diff --git a/test/unit/convolve.js b/test/unit/convolve.js new file mode 100644 index 00000000..c366da73 --- /dev/null +++ b/test/unit/convolve.js @@ -0,0 +1,82 @@ +'use strict'; + +var assert = require('assert'); + +var sharp = require('../../index'); +var fixtures = require('../fixtures'); + +describe('Convolve', function() { + + it('specific convolution kernel 1', function(done) { + sharp(fixtures.inputPngStripesV) + .resize(320, 240) + .convolve( + { + 'width': 3, + 'height': 3, + 'scale': 50, + 'offset': 0, + 'kernel': [ 10, 20, 10, + 0, 0, 0, + 10, 20, 10 ] + }) + .toBuffer(function(err, data, info) { + assert.strictEqual('png', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertSimilar(fixtures.expected('conv-1.png'), data, done); + }); + }); + + it('specific convolution kernel 2', function(done) { + sharp(fixtures.inputPngStripesH) + .resize(320, 240) + .convolve( + { + 'width': 3, + 'height': 3, + 'kernel': [ 1, 0, 1, + 2, 0, 2, + 1, 0, 1 ] + }) + .toBuffer(function(err, data, info) { + assert.strictEqual('png', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertSimilar(fixtures.expected('conv-2.png'), data, done); + }); + }); + + it('invalid kernel specification: no data', function() { + assert.throws(function() { + sharp(fixtures.inputJpg).convolve( + { + 'width': 3, + 'height': 3, + 'kernel': [] + }); + }); + }); + + it('invalid kernel specification: bad data format', function() { + assert.throws(function() { + sharp(fixtures.inputJpg).convolve( + { + 'width': 3, + 'height': 3, + 'kernel': [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + }); + }); + }); + + it('invalid kernel specification: wrong width', function() { + assert.throws(function() { + sharp(fixtures.inputJpg).convolve( + { + 'width': 3, + 'height': 4, + 'kernel': [1, 2, 3, 4, 5, 6, 7, 8, 9] + }); + }); + }); +});