Skip to content

Commit

Permalink
Add convolve operation for kernel-based convolution (#479)
Browse files Browse the repository at this point in the history
  • Loading branch information
mhirsch authored and lovell committed Jul 4, 2016
1 parent ba5a8b4 commit b70a7d9
Show file tree
Hide file tree
Showing 12 changed files with 202 additions and 1 deletion.
15 changes: 15 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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%.
Expand Down
37 changes: 37 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
19 changes: 19 additions & 0 deletions src/operations.cc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <algorithm>
#include <tuple>
#include <memory>
#include <vips/vips8>

#include "common.h"
Expand Down Expand Up @@ -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.
*/
Expand Down
7 changes: 7 additions & 0 deletions src/operations.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define SRC_OPERATIONS_H_

#include <tuple>
#include <memory>
#include <vips/vips8>

using vips::VImage;
Expand Down Expand Up @@ -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.
*/
Expand Down
29 changes: 28 additions & 1 deletion src/pipeline.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <cmath>
#include <tuple>
#include <utility>
#include <memory>

#include <vips/vips8>

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down
11 changes: 11 additions & 0 deletions src/pipeline.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#ifndef SRC_PIPELINE_H_
#define SRC_PIPELINE_H_

#include <memory>

#include <vips/vips8>

#include "nan.h"
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down
Binary file added test/fixtures/expected/conv-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/conv-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions test/fixtures/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
Binary file added test/fixtures/stripesH.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/stripesV.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
82 changes: 82 additions & 0 deletions test/unit/convolve.js
Original file line number Diff line number Diff line change
@@ -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]
});
});
});
});

0 comments on commit b70a7d9

Please sign in to comment.