Add support to recomb operation for 4x4 matrices

This commit is contained in:
Denice 2024-07-02 17:06:26 +07:00 committed by Lovell Fuller
parent eab7dc1b49
commit 60c5c5083d
14 changed files with 74 additions and 34 deletions

View File

@ -580,7 +580,7 @@ Recombine the image with the specified matrix.
| Param | Type | Description | | Param | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| inputMatrix | <code>Array.&lt;Array.&lt;number&gt;&gt;</code> | 3x3 Recombination matrix | | inputMatrix | <code>Array.&lt;Array.&lt;number&gt;&gt;</code> | 3x3 or 4x4 Recombination matrix |
**Example** **Example**
```js ```js

View File

@ -16,6 +16,10 @@ Requires libvips v8.15.2
* Ensure `sharp.format.heif` includes only AVIF when using prebuilt binaries. * Ensure `sharp.format.heif` includes only AVIF when using prebuilt binaries.
[#4132](https://github.com/lovell/sharp/issues/4132) [#4132](https://github.com/lovell/sharp/issues/4132)
* Add support to recomb operation for 4x4 matrices.
[#4147](https://github.com/lovell/sharp/pull/4147)
[@ton11797](https://github.com/ton11797)
### v0.33.4 - 16th May 2024 ### v0.33.4 - 16th May 2024
* Remove experimental status from `pipelineColourspace`. * Remove experimental status from `pipelineColourspace`.

View File

@ -296,3 +296,6 @@ GitHub: https://github.com/adriaanmeuris
Name: Richard Hillmann Name: Richard Hillmann
GitHub: https://github.com/project0 GitHub: https://github.com/project0
Name: Pongsatorn Manusopit
GitHub: https://github.com/ton11797

5
lib/index.d.ts vendored
View File

@ -571,11 +571,11 @@ declare namespace sharp {
/** /**
* Recomb the image with the specified matrix. * Recomb the image with the specified matrix.
* @param inputMatrix 3x3 Recombination matrix * @param inputMatrix 3x3 Recombination matrix or 4x4 Recombination matrix
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
* @returns A sharp instance that can be used to chain operations * @returns A sharp instance that can be used to chain operations
*/ */
recomb(inputMatrix: Matrix3x3): Sharp; recomb(inputMatrix: Matrix3x3 | Matrix4x4): Sharp;
/** /**
* Transforms the image using brightness, saturation, hue rotation and lightness. * Transforms the image using brightness, saturation, hue rotation and lightness.
@ -1730,6 +1730,7 @@ declare namespace sharp {
type Matrix2x2 = [[number, number], [number, number]]; type Matrix2x2 = [[number, number], [number, number]];
type Matrix3x3 = [[number, number, number], [number, number, number], [number, number, number]]; type Matrix3x3 = [[number, number, number], [number, number, number], [number, number, number]];
type Matrix4x4 = [[number, number, number, number], [number, number, number, number], [number, number, number, number], [number, number, number, number]];
} }
export = sharp; export = sharp;

View File

@ -787,24 +787,22 @@ function linear (a, b) {
* // With this example input, a sepia filter has been applied * // With this example input, a sepia filter has been applied
* }); * });
* *
* @param {Array<Array<number>>} inputMatrix - 3x3 Recombination matrix * @param {Array<Array<number>>} inputMatrix - 3x3 or 4x4 Recombination matrix
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
*/ */
function recomb (inputMatrix) { function recomb (inputMatrix) {
if (!Array.isArray(inputMatrix) || inputMatrix.length !== 3 || if (!Array.isArray(inputMatrix)) {
inputMatrix[0].length !== 3 || throw is.invalidParameterError('inputMatrix', 'array', inputMatrix);
inputMatrix[1].length !== 3 ||
inputMatrix[2].length !== 3
) {
// must pass in a kernel
throw new Error('Invalid recombination matrix');
} }
this.options.recombMatrix = [ if (inputMatrix.length !== 3 && inputMatrix.length !== 4) {
inputMatrix[0][0], inputMatrix[0][1], inputMatrix[0][2], throw is.invalidParameterError('inputMatrix', '3x3 or 4x4 array', inputMatrix.length);
inputMatrix[1][0], inputMatrix[1][1], inputMatrix[1][2], }
inputMatrix[2][0], inputMatrix[2][1], inputMatrix[2][2] const recombMatrix = inputMatrix.flat().map(Number);
].map(Number); if (recombMatrix.length !== 9 && recombMatrix.length !== 16) {
throw is.invalidParameterError('inputMatrix', 'cardinality of 9 or 16', recombMatrix.length);
}
this.options.recombMatrix = recombMatrix;
return this; return this;
} }

View File

@ -183,19 +183,21 @@ namespace sharp {
* Recomb with a Matrix of the given bands/channel size. * Recomb with a Matrix of the given bands/channel size.
* Eg. RGB will be a 3x3 matrix. * Eg. RGB will be a 3x3 matrix.
*/ */
VImage Recomb(VImage image, std::unique_ptr<double[]> const &matrix) { VImage Recomb(VImage image, std::vector<double> const& matrix) {
double *m = matrix.get(); double* m = const_cast<double*>(matrix.data());
image = image.colourspace(VIPS_INTERPRETATION_sRGB); image = image.colourspace(VIPS_INTERPRETATION_sRGB);
if (matrix.size() == 9) {
return image return image
.recomb(image.bands() == 3 .recomb(image.bands() == 3
? VImage::new_from_memory( ? VImage::new_matrix(3, 3, m, 9)
m, 9 * sizeof(double), 3, 3, 1, VIPS_FORMAT_DOUBLE
)
: VImage::new_matrixv(4, 4, : VImage::new_matrixv(4, 4,
m[0], m[1], m[2], 0.0, m[0], m[1], m[2], 0.0,
m[3], m[4], m[5], 0.0, m[3], m[4], m[5], 0.0,
m[6], m[7], m[8], 0.0, m[6], m[7], m[8], 0.0,
0.0, 0.0, 0.0, 1.0)); 0.0, 0.0, 0.0, 1.0));
} else {
return image.recomb(VImage::new_matrix(4, 4, m, 16));
}
} }
VImage Modulate(VImage image, double const brightness, double const saturation, VImage Modulate(VImage image, double const brightness, double const saturation,

View File

@ -95,7 +95,7 @@ namespace sharp {
* Recomb with a Matrix of the given bands/channel size. * Recomb with a Matrix of the given bands/channel size.
* Eg. RGB will be a 3x3 matrix. * Eg. RGB will be a 3x3 matrix.
*/ */
VImage Recomb(VImage image, std::unique_ptr<double[]> const &matrix); VImage Recomb(VImage image, std::vector<double> const &matrix);
/* /*
* Modulate brightness, saturation, hue and lightness * Modulate brightness, saturation, hue and lightness

View File

@ -609,7 +609,7 @@ class PipelineWorker : public Napi::AsyncWorker {
} }
// Recomb // Recomb
if (baton->recombMatrix != NULL) { if (!baton->recombMatrix.empty()) {
image = sharp::Recomb(image, baton->recombMatrix); image = sharp::Recomb(image, baton->recombMatrix);
} }
@ -1613,9 +1613,10 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
} }
} }
if (options.Has("recombMatrix")) { if (options.Has("recombMatrix")) {
baton->recombMatrix = std::unique_ptr<double[]>(new double[9]);
Napi::Array recombMatrix = options.Get("recombMatrix").As<Napi::Array>(); Napi::Array recombMatrix = options.Get("recombMatrix").As<Napi::Array>();
for (unsigned int i = 0; i < 9; i++) { unsigned int matrixElements = recombMatrix.Length();
baton->recombMatrix.resize(matrixElements);
for (unsigned int i = 0; i < matrixElements; i++) {
baton->recombMatrix[i] = sharp::AttrAsDouble(recombMatrix, i); baton->recombMatrix[i] = sharp::AttrAsDouble(recombMatrix, i);
} }
} }

View File

@ -223,7 +223,7 @@ struct PipelineBaton {
VipsForeignDzDepth tileDepth; VipsForeignDzDepth tileDepth;
std::string tileId; std::string tileId;
std::string tileBasename; std::string tileBasename;
std::unique_ptr<double[]> recombMatrix; std::vector<double> recombMatrix;
PipelineBaton(): PipelineBaton():
input(nullptr), input(nullptr),

BIN
test/fixtures/d.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
test/fixtures/expected/d-opacity-30.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -139,6 +139,7 @@ module.exports = {
testPattern: getPath('test-pattern.png'), testPattern: getPath('test-pattern.png'),
inputPngWithTransparent: getPath('d.png'),
// Path for tests requiring human inspection // Path for tests requiring human inspection
path: getPath, path: getPath,

View File

@ -295,6 +295,13 @@ sharp('input.gif')
[0.2392, 0.4696, 0.0912], [0.2392, 0.4696, 0.0912],
]) ])
.recomb([
[1,0,0,0],
[0,1,0,0],
[0,0,1,0],
[0,0,0,1],
])
.modulate({ brightness: 2 }) .modulate({ brightness: 2 })
.modulate({ hue: 180 }) .modulate({ hue: 180 })
.modulate({ lightness: 10 }) .modulate({ lightness: 10 })

View File

@ -121,6 +121,29 @@ describe('Recomb', function () {
}); });
}); });
it('applies opacity 30% to the image', function (done) {
const output = fixtures.path('output.recomb-opacity.png');
sharp(fixtures.inputPngWithTransparent)
.recomb([
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 0.3]
])
.toFile(output, function (err, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(48, info.width);
assert.strictEqual(48, info.height);
fixtures.assertMaxColourDistance(
output,
fixtures.expected('d-opacity-30.png'),
17
);
done();
});
});
describe('invalid matrix specification', function () { describe('invalid matrix specification', function () {
it('missing', function () { it('missing', function () {
assert.throws(function () { assert.throws(function () {