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`.
#### 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%.

View File

@ -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.

View File

@ -1,5 +1,6 @@
#include <algorithm>
#include <tuple>
#include <memory>
#include <vips/vips8>
#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.
*/

View File

@ -2,6 +2,7 @@
#define SRC_OPERATIONS_H_
#include <tuple>
#include <memory>
#include <vips/vips8>
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<double[]> &kernel_v);
/*
* Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen.
*/

View File

@ -2,6 +2,7 @@
#include <cmath>
#include <tuple>
#include <utility>
#include <memory>
#include <vips/vips8>
@ -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<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
Callback *queueListener = new Callback(

View File

@ -1,6 +1,8 @@
#ifndef SRC_PIPELINE_H_
#define SRC_PIPELINE_H_
#include <memory>
#include <vips/vips8>
#include "nan.h"
@ -83,6 +85,11 @@ struct PipelineBaton {
std::string err;
bool withMetadata;
int withMetadataOrientation;
std::unique_ptr<double[]> 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),

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'),
inputPngStripesV: getPath('stripesV.png'),
inputPngStripesH: getPath('stripesH.png'),
outputJpg: getPath('output.jpg'),
outputPng: getPath('output.png'),
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]
});
});
});
});