Skip to content

Commit

Permalink
Enhancement: add support to recomb for 4x4 recombination matrices
Browse files Browse the repository at this point in the history
  • Loading branch information
ton11797 committed Jul 2, 2024
1 parent eab7dc1 commit c66b118
Show file tree
Hide file tree
Showing 12 changed files with 80 additions and 30 deletions.
2 changes: 2 additions & 0 deletions lib/constructor.js
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,8 @@ const Sharp = function (input, options) {
timeoutSeconds: 0,
linearA: [],
linearB: [],
recombMatrixSize: 3,

// Function to notify of libvips warnings
debuglog: warning => {
this.emit('warning', warning);
Expand Down
5 changes: 3 additions & 2 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,11 +571,11 @@ declare namespace sharp {

/**
* 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
* @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.
Expand Down Expand Up @@ -1730,6 +1730,7 @@ declare namespace sharp {

type Matrix2x2 = [[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;
28 changes: 16 additions & 12 deletions lib/operation.js
Original file line number Diff line number Diff line change
Expand Up @@ -787,24 +787,28 @@ function linear (a, b) {
* // With this example input, a sepia filter has been applied
* });
*
* @param {Array<Array<number>>} inputMatrix - 3x3 Recombination matrix
* @param {Array<Array<number>>} inputMatrix - 3x3 Recombination matrix or 4x4 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
if (!Array.isArray(inputMatrix)) {
throw new Error('Invalid recombination matrix');
}

const length = inputMatrix.length;
if (length !== 3 && length !== 4) {
throw new Error('Invalid recombination 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);

for (const row of inputMatrix) {
if (row.length !== length) {
throw new Error('Invalid recombination matrix');
}
}

this.options.recombMatrix = inputMatrix.flat().map(Number);
this.options.recombMatrixSize = length;
return this;
}

Expand Down
27 changes: 16 additions & 11 deletions src/operations.cc
Original file line number Diff line number Diff line change
Expand Up @@ -183,19 +183,24 @@ namespace sharp {
* 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) {
VImage Recomb(VImage image, std::unique_ptr<double[]> const &matrix, int recombMatrixSize) {
double *m = matrix.get();
image = image.colourspace(VIPS_INTERPRETATION_sRGB);
return image
.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));
if (recombMatrixSize == 3) {
return image
.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));
} else {
return image
.recomb(VImage::new_from_memory(m, 16 * sizeof(double), 4, 4, 1, VIPS_FORMAT_DOUBLE));
}
}

VImage Modulate(VImage image, double const brightness, double const saturation,
Expand Down
2 changes: 1 addition & 1 deletion src/operations.h
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ namespace sharp {
* 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);
VImage Recomb(VImage image, std::unique_ptr<double[]> const &matrix, int recombMatrixSize);

/*
* Modulate brightness, saturation, hue and lightness
Expand Down
14 changes: 10 additions & 4 deletions src/pipeline.cc
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ class PipelineWorker : public Napi::AsyncWorker {

// Recomb
if (baton->recombMatrix != NULL) {
image = sharp::Recomb(image, baton->recombMatrix);
image = sharp::Recomb(image, baton->recombMatrix, baton->recombMatrixSize);
}

// Modulate
Expand Down Expand Up @@ -1613,10 +1613,16 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
}
}
if (options.Has("recombMatrix")) {
baton->recombMatrix = std::unique_ptr<double[]>(new double[9]);
baton->recombMatrixSize = sharp::AttrAsInt32(options, "recombMatrixSize");
if (baton->recombMatrixSize == 3) {
baton->recombMatrix = std::unique_ptr<double[]>(new double[9]);
} else {
baton->recombMatrix = std::unique_ptr<double[]>(new double[16]);
}
Napi::Array recombMatrix = options.Get("recombMatrix").As<Napi::Array>();
for (unsigned int i = 0; i < 9; i++) {
baton->recombMatrix[i] = sharp::AttrAsDouble(recombMatrix, i);
unsigned int matrixElements = baton->recombMatrixSize * baton->recombMatrixSize;
for (unsigned int i = 0; i < matrixElements; i++) {
baton->recombMatrix[i] = sharp::AttrAsDouble(recombMatrix, i);
}
}
baton->colourspacePipeline = sharp::AttrAsEnum<VipsInterpretation>(
Expand Down
1 change: 1 addition & 0 deletions src/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ struct PipelineBaton {
std::string tileId;
std::string tileBasename;
std::unique_ptr<double[]> recombMatrix;
int recombMatrixSize;

PipelineBaton():
input(nullptr),
Expand Down
Binary file added test/fixtures/d.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/d-opacity-30.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions test/fixtures/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ module.exports = {

testPattern: getPath('test-pattern.png'),

inputPngWithTransparent: getPath('d.png'),
// Path for tests requiring human inspection
path: getPath,

Expand Down
7 changes: 7 additions & 0 deletions test/types/sharp.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,13 @@ sharp('input.gif')
[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({ hue: 180 })
.modulate({ lightness: 10 })
Expand Down
23 changes: 23 additions & 0 deletions test/unit/recomb.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
it('missing', function () {
assert.throws(function () {
Expand Down

0 comments on commit c66b118

Please sign in to comment.