Expand linear operation to allow use of per-channel arrays #3303

This commit is contained in:
Anton Marsden 2022-07-23 21:33:44 +12:00 committed by Lovell Fuller
parent b9261c243c
commit 74e3f73934
10 changed files with 104 additions and 34 deletions

View File

@ -466,14 +466,32 @@ Returns **Sharp** 
## linear
Apply the linear formula a \* input + b to the image (levels adjustment)
Apply the linear formula `a` \* input + `b` to the image to adjust image levels.
When a single number is provided, it will be used for all image channels.
When an array of numbers is provided, the array length must match the number of channels.
### Parameters
* `a` **[number][1]** multiplier (optional, default `1.0`)
* `b` **[number][1]** offset (optional, default `0.0`)
* `a` **([number][1] | [Array][7]<[number][1]>)** multiplier (optional, default `[]`)
* `b` **([number][1] | [Array][7]<[number][1]>)** offset (optional, default `[]`)
<!---->
### Examples
```javascript
await sharp(input)
.linear(0.5, 2)
.toBuffer();
```
```javascript
await sharp(rgbInput)
.linear(
[0.25, 0.5, 0.75],
[150, 100, 50]
)
.toBuffer();
```
* Throws **[Error][5]** Invalid parameters

File diff suppressed because one or more lines are too long

View File

@ -319,8 +319,8 @@ const Sharp = function (input, options) {
tileId: 'https://example.com/iiif',
tileBasename: '',
timeoutSeconds: 0,
linearA: 1,
linearB: 0,
linearA: [],
linearB: [],
// Function to notify of libvips warnings
debuglog: warning => {
this.emit('warning', warning);

View File

@ -644,26 +644,55 @@ function boolean (operand, operator, options) {
}
/**
* Apply the linear formula a * input + b to the image (levels adjustment)
* @param {number} [a=1.0] multiplier
* @param {number} [b=0.0] offset
* Apply the linear formula `a` * input + `b` to the image to adjust image levels.
*
* When a single number is provided, it will be used for all image channels.
* When an array of numbers is provided, the array length must match the number of channels.
*
* @example
* await sharp(input)
* .linear(0.5, 2)
* .toBuffer();
*
* @example
* await sharp(rgbInput)
* .linear(
* [0.25, 0.5, 0.75],
* [150, 100, 50]
* )
* .toBuffer();
*
* @param {(number|number[])} [a=[]] multiplier
* @param {(number|number[])} [b=[]] offset
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
function linear (a, b) {
if (!is.defined(a) && is.number(b)) {
a = 1.0;
} else if (is.number(a) && !is.defined(b)) {
b = 0.0;
}
if (!is.defined(a)) {
this.options.linearA = 1.0;
this.options.linearA = [];
} else if (is.number(a)) {
this.options.linearA = [a];
} else if (Array.isArray(a) && a.length && a.every(is.number)) {
this.options.linearA = a;
} else {
throw is.invalidParameterError('a', 'numeric', a);
throw is.invalidParameterError('a', 'number or array of numbers', a);
}
if (!is.defined(b)) {
this.options.linearB = 0.0;
this.options.linearB = [];
} else if (is.number(b)) {
this.options.linearB = [b];
} else if (Array.isArray(b) && b.length && b.every(is.number)) {
this.options.linearB = b;
} else {
throw is.invalidParameterError('b', 'numeric', b);
throw is.invalidParameterError('b', 'number or array of numbers', b);
}
if (this.options.linearA.length !== this.options.linearB.length) {
throw new Error('Expected a and b to be arrays of the same length');
}
return this;
}

View File

@ -306,10 +306,14 @@ namespace sharp {
/*
* Calculate (a * in + b)
*/
VImage Linear(VImage image, double const a, double const b) {
if (HasAlpha(image)) {
VImage Linear(VImage image, std::vector<double> const a, std::vector<double> const b) {
size_t const bands = static_cast<size_t>(image.bands());
if (a.size() > bands) {
throw VError("Band expansion using linear is unsupported");
}
if (HasAlpha(image) && a.size() != bands && (a.size() == 1 || a.size() == bands - 1 || bands - 1 == 1)) {
// Separate alpha channel
VImage alpha = image[image.bands() - 1];
VImage alpha = image[bands - 1];
return RemoveAlpha(image).linear(a, b).bandjoin(alpha);
} else {
return image.linear(a, b);

View File

@ -90,7 +90,7 @@ namespace sharp {
/*
* Linear adjustment (a * in + b)
*/
VImage Linear(VImage image, double const a, double const b);
VImage Linear(VImage image, std::vector<double> const a, std::vector<double> const b);
/*
* Recomb with a Matrix of the given bands/channel size.

View File

@ -688,7 +688,7 @@ class PipelineWorker : public Napi::AsyncWorker {
}
// Linear adjustment (a * in + b)
if (baton->linearA != 1.0 || baton->linearB != 0.0) {
if (!baton->linearA.empty()) {
image = sharp::Linear(image, baton->linearA, baton->linearB);
}
@ -1454,8 +1454,8 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold");
baton->gamma = sharp::AttrAsDouble(options, "gamma");
baton->gammaOut = sharp::AttrAsDouble(options, "gammaOut");
baton->linearA = sharp::AttrAsDouble(options, "linearA");
baton->linearB = sharp::AttrAsDouble(options, "linearB");
baton->linearA = sharp::AttrAsVectorOfDouble(options, "linearA");
baton->linearB = sharp::AttrAsVectorOfDouble(options, "linearB");
baton->greyscale = sharp::AttrAsBool(options, "greyscale");
baton->normalise = sharp::AttrAsBool(options, "normalise");
baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth");

View File

@ -100,8 +100,8 @@ struct PipelineBaton {
double trimThreshold;
int trimOffsetLeft;
int trimOffsetTop;
double linearA;
double linearB;
std::vector<double> linearA;
std::vector<double> linearB;
double gamma;
double gammaOut;
bool greyscale;
@ -251,8 +251,8 @@ struct PipelineBaton {
trimThreshold(0.0),
trimOffsetLeft(0),
trimOffsetTop(0),
linearA(1.0),
linearB(0.0),
linearA{},
linearB{},
gamma(0.0),
greyscale(false),
normalise(false),

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

View File

@ -65,15 +65,34 @@ describe('Linear adjustment', function () {
});
});
it('Invalid linear arguments', function () {
assert.throws(function () {
sharp(fixtures.inputPngOverlayLayer1)
.linear('foo');
it('per channel level adjustment', function (done) {
sharp(fixtures.inputWebP)
.linear([0.25, 0.5, 0.75], [150, 100, 50]).toBuffer(function (err, data, info) {
if (err) throw err;
fixtures.assertSimilar(fixtures.expected('linear-per-channel.jpg'), data, done);
});
});
assert.throws(function () {
sharp(fixtures.inputPngOverlayLayer1)
.linear(undefined, { bar: 'baz' });
});
it('Invalid linear arguments', function () {
assert.throws(
() => sharp().linear('foo'),
/Expected number or array of numbers for a but received foo of type string/
);
assert.throws(
() => sharp().linear(undefined, { bar: 'baz' }),
/Expected number or array of numbers for b but received \[object Object\] of type object/
);
assert.throws(
() => sharp().linear([], [1]),
/Expected number or array of numbers for a but received {2}of type object/
);
assert.throws(
() => sharp().linear([1, 2], [1]),
/Expected a and b to be arrays of the same length/
);
assert.throws(
() => sharp().linear([1]),
/Expected a and b to be arrays of the same length/
);
});
});