Add convolve operation for kernel-based convolution (#479)

This commit is contained in:
Matt Hirsch 2016-07-04 15:48:00 -04:00 committed by Lovell Fuller
parent ba5a8b44ed
commit b70a7d9a3b
12 changed files with 202 additions and 1 deletions

View File

@ -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`. * `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]) #### sharpen([sigma], [flat], [jagged])
When used without parameters, performs a fast, mild sharpen of the output image. This typically reduces performance by 10%. When used without parameters, performs a fast, mild sharpen of the output image. This typically reduces performance by 10%.

View File

@ -448,6 +448,43 @@ Sharp.prototype.blur = function(sigma) {
return this; 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. Sharpen the output image.
Call without a radius to use a fast, mild sharpen. Call without a radius to use a fast, mild sharpen.

View File

@ -1,5 +1,6 @@
#include <algorithm> #include <algorithm>
#include <tuple> #include <tuple>
#include <memory>
#include <vips/vips8> #include <vips/vips8>
#include "common.h" #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<double[]> &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. * Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen.
*/ */

View File

@ -2,6 +2,7 @@
#define SRC_OPERATIONS_H_ #define SRC_OPERATIONS_H_
#include <tuple> #include <tuple>
#include <memory>
#include <vips/vips8> #include <vips/vips8>
using vips::VImage; using vips::VImage;
@ -34,6 +35,12 @@ namespace sharp {
*/ */
VImage Blur(VImage image, double const sigma); 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<double[]> &kernel_v);
/* /*
* Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen. * Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen.
*/ */

View File

@ -2,6 +2,7 @@
#include <cmath> #include <cmath>
#include <tuple> #include <tuple>
#include <utility> #include <utility>
#include <memory>
#include <vips/vips8> #include <vips/vips8>
@ -49,6 +50,7 @@ using sharp::Cutout;
using sharp::Normalize; using sharp::Normalize;
using sharp::Gamma; using sharp::Gamma;
using sharp::Blur; using sharp::Blur;
using sharp::Convolve;
using sharp::Sharpen; using sharp::Sharpen;
using sharp::EntropyCrop; using sharp::EntropyCrop;
using sharp::TileCache; using sharp::TileCache;
@ -464,11 +466,12 @@ class PipelineWorker : public AsyncWorker {
bool shouldAffineTransform = xresidual != 1.0 || yresidual != 1.0; bool shouldAffineTransform = xresidual != 1.0 || yresidual != 1.0;
bool shouldBlur = baton->blurSigma != 0.0; bool shouldBlur = baton->blurSigma != 0.0;
bool shouldConv = baton->convKernelWidth * baton->convKernelHeight > 0;
bool shouldSharpen = baton->sharpenSigma != 0.0; bool shouldSharpen = baton->sharpenSigma != 0.0;
bool shouldThreshold = baton->threshold != 0; bool shouldThreshold = baton->threshold != 0;
bool shouldCutout = baton->overlayCutout; bool shouldCutout = baton->overlayCutout;
bool shouldPremultiplyAlpha = HasAlpha(image) && bool shouldPremultiplyAlpha = HasAlpha(image) &&
(shouldAffineTransform || shouldBlur || shouldSharpen || (hasOverlay && !shouldCutout)); (shouldAffineTransform || shouldBlur || shouldConv || shouldSharpen || (hasOverlay && !shouldCutout));
// Premultiply image alpha channel before all transformations to avoid // Premultiply image alpha channel before all transformations to avoid
// dark fringing around bright pixels // dark fringing around bright pixels
@ -634,6 +637,14 @@ class PipelineWorker : public AsyncWorker {
image = Blur(image, baton->blurSigma); image = Blur(image, baton->blurSigma);
} }
// Convolve
if (shouldConv) {
image = Convolve(image,
baton->convKernelWidth, baton->convKernelHeight,
baton->convKernelScale, baton->convKernelOffset,
baton->convKernel);
}
// Sharpen // Sharpen
if (shouldSharpen) { if (shouldSharpen) {
image = Sharpen(image, baton->sharpenSigma, baton->sharpenFlat, baton->sharpenJagged); image = Sharpen(image, baton->sharpenSigma, baton->sharpenFlat, baton->sharpenJagged);
@ -1151,6 +1162,22 @@ NAN_METHOD(pipeline) {
} else { } else {
baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_DZ; baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_DZ;
} }
// Convolution Kernel
if(Has(options, New("convKernel").ToLocalChecked()).FromJust()) {
Local<Object> kernel = Get(options, New("convKernel").ToLocalChecked()).ToLocalChecked().As<Object>();
baton->convKernelWidth = attrAs<int32_t>(kernel, "width");
baton->convKernelHeight = attrAs<int32_t>(kernel, "height");
baton->convKernelScale = attrAs<double>(kernel, "scale");
baton->convKernelOffset = attrAs<double>(kernel, "offset");
size_t kernelSize = baton->convKernelWidth * baton->convKernelHeight;
baton->convKernel = std::unique_ptr<double[]>(new double[kernelSize]);
Local<Array> kdata = Get(kernel, New("kernel").ToLocalChecked()).ToLocalChecked().As<Array>();
for(unsigned int i = 0; i < kernelSize; i++) {
baton->convKernel[i] = To<double>(Get(kdata, i).ToLocalChecked()).FromJust();
}
}
// Function to notify of queue length changes // Function to notify of queue length changes
Callback *queueListener = new Callback( Callback *queueListener = new Callback(

View File

@ -1,6 +1,8 @@
#ifndef SRC_PIPELINE_H_ #ifndef SRC_PIPELINE_H_
#define SRC_PIPELINE_H_ #define SRC_PIPELINE_H_
#include <memory>
#include <vips/vips8> #include <vips/vips8>
#include "nan.h" #include "nan.h"
@ -83,6 +85,11 @@ struct PipelineBaton {
std::string err; std::string err;
bool withMetadata; bool withMetadata;
int withMetadataOrientation; int withMetadataOrientation;
std::unique_ptr<double[]> convKernel;
int convKernelWidth;
int convKernelHeight;
double convKernelScale;
double convKernelOffset;
int tileSize; int tileSize;
int tileOverlap; int tileOverlap;
VipsForeignDzContainer tileContainer; VipsForeignDzContainer tileContainer;
@ -136,6 +143,10 @@ struct PipelineBaton {
optimiseScans(false), optimiseScans(false),
withMetadata(false), withMetadata(false),
withMetadataOrientation(-1), withMetadataOrientation(-1),
convKernelWidth(0),
convKernelHeight(0),
convKernelScale(0.0),
convKernelOffset(0.0),
tileSize(256), tileSize(256),
tileOverlap(0), tileOverlap(0),
tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS), tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS),

BIN
test/fixtures/expected/conv-1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

BIN
test/fixtures/expected/conv-2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

View File

@ -91,6 +91,9 @@ module.exports = {
inputJPGBig: getPath('flowers.jpeg'), inputJPGBig: getPath('flowers.jpeg'),
inputPngStripesV: getPath('stripesV.png'),
inputPngStripesH: getPath('stripesH.png'),
outputJpg: getPath('output.jpg'), outputJpg: getPath('output.jpg'),
outputPng: getPath('output.png'), outputPng: getPath('output.png'),
outputWebP: getPath('output.webp'), outputWebP: getPath('output.webp'),

BIN
test/fixtures/stripesH.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

BIN
test/fixtures/stripesV.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

82
test/unit/convolve.js Normal file
View File

@ -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]
});
});
});
});