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`)
|
- `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
|
- Throws **[Error][5]** Invalid parameters
|
||||||
|
|
||||||
Returns **Sharp**
|
Returns **Sharp**
|
||||||
|
@ -25,6 +25,10 @@ Requires libvips v8.7.0.
|
|||||||
[#1475](https://github.com/lovell/sharp/pull/1475)
|
[#1475](https://github.com/lovell/sharp/pull/1475)
|
||||||
[@jaubourg](https://github.com/jaubourg)
|
[@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
|
#### v0.21.0 - 4<sup>th</sup> October 2018
|
||||||
|
|
||||||
* Deprecate the following resize-related functions:
|
* 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)
|
* [Axel Eirola](https://github.com/aeirola)
|
||||||
* [Freezy](https://github.com/freezy)
|
* [Freezy](https://github.com/freezy)
|
||||||
* [Julian Aubourg](https://github.com/jaubourg)
|
* [Julian Aubourg](https://github.com/jaubourg)
|
||||||
|
* [Keith Belovay](https://github.com/fromkeith)
|
||||||
|
|
||||||
Thank you!
|
Thank you!
|
||||||
|
|
||||||
|
@ -378,6 +378,43 @@ function linear (a, b) {
|
|||||||
return this;
|
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.
|
* Decorate the Sharp prototype with operation-related functions.
|
||||||
* @private
|
* @private
|
||||||
@ -398,6 +435,7 @@ module.exports = function (Sharp) {
|
|||||||
convolve,
|
convolve,
|
||||||
threshold,
|
threshold,
|
||||||
boolean,
|
boolean,
|
||||||
linear
|
linear,
|
||||||
|
recomb
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -57,7 +57,8 @@
|
|||||||
"Axel Eirola <axel.eirola@iki.fi>",
|
"Axel Eirola <axel.eirola@iki.fi>",
|
||||||
"Freezy <freezy@xbmc.org>",
|
"Freezy <freezy@xbmc.org>",
|
||||||
"Daiz <taneli.vatanen@gmail.com>",
|
"Daiz <taneli.vatanen@gmail.com>",
|
||||||
"Julian Aubourg <j@ubourg.net>"
|
"Julian Aubourg <j@ubourg.net>",
|
||||||
|
"Keith Belovay <keith@picthrive.com>"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)",
|
"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);
|
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.
|
* 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);
|
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
|
} // namespace sharp
|
||||||
|
|
||||||
#endif // SRC_OPERATIONS_H_
|
#endif // SRC_OPERATIONS_H_
|
||||||
|
@ -525,6 +525,11 @@ class PipelineWorker : public Nan::AsyncWorker {
|
|||||||
baton->convKernel);
|
baton->convKernel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recomb
|
||||||
|
if (baton->recombMatrix != NULL) {
|
||||||
|
image = sharp::Recomb(image, baton->recombMatrix);
|
||||||
|
}
|
||||||
|
|
||||||
// Sharpen
|
// Sharpen
|
||||||
if (shouldSharpen) {
|
if (shouldSharpen) {
|
||||||
image = sharp::Sharpen(image, baton->sharpenSigma, baton->sharpenFlat, baton->sharpenJagged);
|
image = sharp::Sharpen(image, baton->sharpenSigma, baton->sharpenFlat, baton->sharpenJagged);
|
||||||
@ -1234,6 +1239,13 @@ NAN_METHOD(pipeline) {
|
|||||||
baton->convKernel[i] = AttrTo<double>(kdata, i);
|
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"));
|
baton->colourspace = sharp::GetInterpretation(AttrAsStr(options, "colourspace"));
|
||||||
if (baton->colourspace == VIPS_INTERPRETATION_ERROR) {
|
if (baton->colourspace == VIPS_INTERPRETATION_ERROR) {
|
||||||
baton->colourspace = VIPS_INTERPRETATION_sRGB;
|
baton->colourspace = VIPS_INTERPRETATION_sRGB;
|
||||||
|
@ -146,6 +146,7 @@ struct PipelineBaton {
|
|||||||
std::string tileFormat;
|
std::string tileFormat;
|
||||||
int tileAngle;
|
int tileAngle;
|
||||||
VipsForeignDzDepth tileDepth;
|
VipsForeignDzDepth tileDepth;
|
||||||
|
std::unique_ptr<double[]> recombMatrix;
|
||||||
|
|
||||||
PipelineBaton():
|
PipelineBaton():
|
||||||
input(nullptr),
|
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