Expose libvips recombination matrix operation #1477

This commit is contained in:
Keith 2018-11-26 16:26:00 -05:00 committed by Lovell Fuller
parent 94945cf6ac
commit 541e7104fd
14 changed files with 244 additions and 2 deletions

View File

@ -254,6 +254,35 @@ Apply the linear formula a \* input + b to the image (levels adjustment)
- `b` **[Number][1]** offset (optional, default `0.0`)
- Throws **[Error][5]** Invalid parameters
Returns **Sharp**
## recomb
Recomb the image with the specified matrix.
### Parameters
- `inputMatrix`
- `3x3` **[Array][7]<[Array][7]<[Number][1]>>** Recombination matrix
### Examples
```javascript
sharp(input)
.recomb([
[0.3588, 0.7044, 0.1368],
[0.2990, 0.5870, 0.1140],
[0.2392, 0.4696, 0.0912],
])
.raw()
.toBuffer(function(err, data, info) {
// data contains the raw pixel data after applying the recomb
// With this example input, a sepia filter has been applied
});
```
- Throws **[Error][5]** Invalid parameters
Returns **Sharp**

View File

@ -25,6 +25,10 @@ Requires libvips v8.7.0.
[#1475](https://github.com/lovell/sharp/pull/1475)
[@jaubourg](https://github.com/jaubourg)
* Expose libvips' recombination matrix operation.
[#1477](https://github.com/lovell/sharp/pull/1477)
[@fromkeith](https://github.com/fromkeith)
#### v0.21.0 - 4<sup>th</sup> October 2018
* Deprecate the following resize-related functions:

View File

@ -120,6 +120,7 @@ the help and code contributions of the following people:
* [Axel Eirola](https://github.com/aeirola)
* [Freezy](https://github.com/freezy)
* [Julian Aubourg](https://github.com/jaubourg)
* [Keith Belovay](https://github.com/fromkeith)
Thank you!

View File

@ -378,6 +378,43 @@ function linear (a, b) {
return this;
}
/**
* Recomb the image with the specified matrix.
*
* @example
* sharp(input)
* .recomb([
* [0.3588, 0.7044, 0.1368],
* [0.2990, 0.5870, 0.1140],
* [0.2392, 0.4696, 0.0912],
* ])
* .raw()
* .toBuffer(function(err, data, info) {
* // data contains the raw pixel data after applying the recomb
* // With this example input, a sepia filter has been applied
* });
*
* @param {Array<Array<Number>>} 3x3 Recombination matrix
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
function recomb (inputMatrix) {
if (!Array.isArray(inputMatrix) || inputMatrix.length !== 3 ||
inputMatrix[0].length !== 3 ||
inputMatrix[1].length !== 3 ||
inputMatrix[2].length !== 3
) {
// must pass in a kernel
throw new Error('Invalid Recomb Matrix');
}
this.options.recombMatrix = [
inputMatrix[0][0], inputMatrix[0][1], inputMatrix[0][2],
inputMatrix[1][0], inputMatrix[1][1], inputMatrix[1][2],
inputMatrix[2][0], inputMatrix[2][1], inputMatrix[2][2]
].map(Number);
return this;
}
/**
* Decorate the Sharp prototype with operation-related functions.
* @private
@ -398,6 +435,7 @@ module.exports = function (Sharp) {
convolve,
threshold,
boolean,
linear
linear,
recomb
});
};

View File

@ -57,7 +57,8 @@
"Axel Eirola <axel.eirola@iki.fi>",
"Freezy <freezy@xbmc.org>",
"Daiz <taneli.vatanen@gmail.com>",
"Julian Aubourg <j@ubourg.net>"
"Julian Aubourg <j@ubourg.net>",
"Keith Belovay <keith@picthrive.com>"
],
"scripts": {
"install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)",

View File

@ -278,6 +278,25 @@ namespace sharp {
return image.conv(kernel);
}
/*
* Recomb with a Matrix of the given bands/channel size.
* Eg. RGB will be a 3x3 matrix.
*/
VImage Recomb(VImage image, std::unique_ptr<double[]> const &matrix) {
double *m = matrix.get();
return image
.colourspace(VIPS_INTERPRETATION_sRGB)
.recomb(image.bands() == 3
? VImage::new_from_memory(
m, 9 * sizeof(double), 3, 3, 1, VIPS_FORMAT_DOUBLE
)
: VImage::new_matrixv(4, 4,
m[0], m[1], m[2], 0.0,
m[3], m[4], m[5], 0.0,
m[6], m[7], m[8], 0.0,
0.0, 0.0, 0.0, 1.0));
}
/*
* Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen.
*/

View File

@ -107,6 +107,12 @@ namespace sharp {
*/
VImage Linear(VImage image, double const a, double const b);
/*
* Recomb with a Matrix of the given bands/channel size.
* Eg. RGB will be a 3x3 matrix.
*/
VImage Recomb(VImage image, std::unique_ptr<double[]> const &matrix);
} // namespace sharp
#endif // SRC_OPERATIONS_H_

View File

@ -525,6 +525,11 @@ class PipelineWorker : public Nan::AsyncWorker {
baton->convKernel);
}
// Recomb
if (baton->recombMatrix != NULL) {
image = sharp::Recomb(image, baton->recombMatrix);
}
// Sharpen
if (shouldSharpen) {
image = sharp::Sharpen(image, baton->sharpenSigma, baton->sharpenFlat, baton->sharpenJagged);
@ -1234,6 +1239,13 @@ NAN_METHOD(pipeline) {
baton->convKernel[i] = AttrTo<double>(kdata, i);
}
}
if (HasAttr(options, "recombMatrix")) {
baton->recombMatrix = std::unique_ptr<double[]>(new double[9]);
v8::Local<v8::Array> recombMatrix = AttrAs<v8::Array>(options, "recombMatrix");
for (unsigned int i = 0; i < 9; i++) {
baton->recombMatrix[i] = AttrTo<double>(recombMatrix, i);
}
}
baton->colourspace = sharp::GetInterpretation(AttrAsStr(options, "colourspace"));
if (baton->colourspace == VIPS_INTERPRETATION_ERROR) {
baton->colourspace = VIPS_INTERPRETATION_sRGB;

View File

@ -146,6 +146,7 @@ struct PipelineBaton {
std::string tileFormat;
int tileAngle;
VipsForeignDzDepth tileDepth;
std::unique_ptr<double[]> recombMatrix;
PipelineBaton():
input(nullptr),

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

131
test/unit/recomb.js Normal file
View File

@ -0,0 +1,131 @@
'use strict';
const assert = require('assert');
const sharp = require('../../');
const fixtures = require('../fixtures');
describe('Recomb', function () {
it('applies a sepia filter using recomb', function (done) {
const output = fixtures.path('output.recomb-sepia.jpg');
sharp(fixtures.inputJpgWithLandscapeExif1)
.recomb([
[0.3588, 0.7044, 0.1368],
[0.299, 0.587, 0.114],
[0.2392, 0.4696, 0.0912]
])
.toFile(output, function (err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(600, info.width);
assert.strictEqual(450, info.height);
fixtures.assertMaxColourDistance(
output,
fixtures.expected('Landscape_1-recomb-sepia.jpg')
);
done();
});
});
it('applies a sepia filter using recomb to an PNG with Alpha', function (done) {
const output = fixtures.path('output.recomb-sepia.png');
sharp(fixtures.inputPngAlphaPremultiplicationSmall)
.recomb([
[0.3588, 0.7044, 0.1368],
[0.299, 0.587, 0.114],
[0.2392, 0.4696, 0.0912]
])
.toFile(output, function (err, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(1024, info.width);
assert.strictEqual(768, info.height);
fixtures.assertMaxColourDistance(
output,
fixtures.expected('alpha-recomb-sepia.png')
);
done();
});
});
it('applies a different sepia filter using recomb', function (done) {
const output = fixtures.path('output.recomb-sepia2.jpg');
sharp(fixtures.inputJpgWithLandscapeExif1)
.recomb([
[0.393, 0.769, 0.189],
[0.349, 0.686, 0.168],
[0.272, 0.534, 0.131]
])
.toFile(output, function (err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(600, info.width);
assert.strictEqual(450, info.height);
fixtures.assertMaxColourDistance(
output,
fixtures.expected('Landscape_1-recomb-sepia2.jpg')
);
done();
});
});
it('increases the saturation of the image', function (done) {
const saturationLevel = 1;
const output = fixtures.path('output.recomb-saturation.jpg');
sharp(fixtures.inputJpgWithLandscapeExif1)
.recomb([
[
saturationLevel + 1 - 0.2989,
-0.587 * saturationLevel,
-0.114 * saturationLevel
],
[
-0.2989 * saturationLevel,
saturationLevel + 1 - 0.587,
-0.114 * saturationLevel
],
[
-0.2989 * saturationLevel,
-0.587 * saturationLevel,
saturationLevel + 1 - 0.114
]
])
.toFile(output, function (err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(600, info.width);
assert.strictEqual(450, info.height);
fixtures.assertMaxColourDistance(
output,
fixtures.expected('Landscape_1-recomb-saturation.jpg')
);
done();
});
});
describe('invalid matrix specification', function () {
it('missing', function () {
assert.throws(function () {
sharp(fixtures.inputJpg).recomb();
});
});
it('incorrect flat data', function () {
assert.throws(function () {
sharp(fixtures.inputJpg).recomb([1, 2, 3, 4, 5, 6, 7, 8, 9]);
});
});
it('incorrect sub size', function () {
assert.throws(function () {
sharp(fixtures.inputJpg).recomb([
[1, 2, 3, 4],
[5, 6, 7, 8],
[1, 2, 9, 6]
]);
});
});
it('incorrect top size', function () {
assert.throws(function () {
sharp(fixtures.inputJpg).recomb([[1, 2, 3, 4], [5, 6, 7, 8]]);
});
});
});
});