mirror of
https://github.com/lovell/sharp.git
synced 2025-07-15 05:00:14 +02:00
Expose libvips recombination matrix operation #1477
This commit is contained in:
parent
94945cf6ac
commit
541e7104fd
@ -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**
|
||||
|
@ -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:
|
||||
|
@ -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!
|
||||
|
||||
|
@ -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
|
||||
});
|
||||
};
|
||||
|
@ -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)",
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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_
|
||||
|
@ -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;
|
||||
|
@ -146,6 +146,7 @@ struct PipelineBaton {
|
||||
std::string tileFormat;
|
||||
int tileAngle;
|
||||
VipsForeignDzDepth tileDepth;
|
||||
std::unique_ptr<double[]> recombMatrix;
|
||||
|
||||
PipelineBaton():
|
||||
input(nullptr),
|
||||
|
BIN
test/fixtures/expected/Landscape_1-recomb-saturation.jpg
vendored
Normal file
BIN
test/fixtures/expected/Landscape_1-recomb-saturation.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 82 KiB |
BIN
test/fixtures/expected/Landscape_1-recomb-sepia.jpg
vendored
Normal file
BIN
test/fixtures/expected/Landscape_1-recomb-sepia.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 77 KiB |
BIN
test/fixtures/expected/Landscape_1-recomb-sepia2.jpg
vendored
Normal file
BIN
test/fixtures/expected/Landscape_1-recomb-sepia2.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 85 KiB |
BIN
test/fixtures/expected/alpha-recomb-sepia.png
vendored
Normal file
BIN
test/fixtures/expected/alpha-recomb-sepia.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 209 KiB |
131
test/unit/recomb.js
Normal file
131
test/unit/recomb.js
Normal 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]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user