mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 18:40:16 +02:00
Add convolve operation for kernel-based convolution (#479)
This commit is contained in:
parent
ba5a8b44ed
commit
b70a7d9a3b
15
docs/api.md
15
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`.
|
* `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%.
|
||||||
|
37
index.js
37
index.js
@ -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.
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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(
|
||||||
|
@ -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
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
BIN
test/fixtures/expected/conv-2.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 806 B |
3
test/fixtures/index.js
vendored
3
test/fixtures/index.js
vendored
@ -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
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
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
82
test/unit/convolve.js
Normal 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]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user