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 ## 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 ### Parameters
* `a` **[number][1]** multiplier (optional, default `1.0`) * `a` **([number][1] | [Array][7]<[number][1]>)** multiplier (optional, default `[]`)
* `b` **[number][1]** offset (optional, default `0.0`) * `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 * 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', tileId: 'https://example.com/iiif',
tileBasename: '', tileBasename: '',
timeoutSeconds: 0, timeoutSeconds: 0,
linearA: 1, linearA: [],
linearB: 0, linearB: [],
// Function to notify of libvips warnings // Function to notify of libvips warnings
debuglog: warning => { debuglog: warning => {
this.emit('warning', 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) * Apply the linear formula `a` * input + `b` to the image to adjust image levels.
* @param {number} [a=1.0] multiplier *
* @param {number} [b=0.0] offset * 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} * @returns {Sharp}
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
*/ */
function linear (a, b) { 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)) { if (!is.defined(a)) {
this.options.linearA = 1.0; this.options.linearA = [];
} else if (is.number(a)) { } else if (is.number(a)) {
this.options.linearA = [a];
} else if (Array.isArray(a) && a.length && a.every(is.number)) {
this.options.linearA = a; this.options.linearA = a;
} else { } else {
throw is.invalidParameterError('a', 'numeric', a); throw is.invalidParameterError('a', 'number or array of numbers', a);
} }
if (!is.defined(b)) { if (!is.defined(b)) {
this.options.linearB = 0.0; this.options.linearB = [];
} else if (is.number(b)) { } else if (is.number(b)) {
this.options.linearB = [b];
} else if (Array.isArray(b) && b.length && b.every(is.number)) {
this.options.linearB = b; this.options.linearB = b;
} else { } 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; return this;
} }

View File

@ -306,10 +306,14 @@ namespace sharp {
/* /*
* Calculate (a * in + b) * Calculate (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) {
if (HasAlpha(image)) { 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 // Separate alpha channel
VImage alpha = image[image.bands() - 1]; VImage alpha = image[bands - 1];
return RemoveAlpha(image).linear(a, b).bandjoin(alpha); return RemoveAlpha(image).linear(a, b).bandjoin(alpha);
} else { } else {
return image.linear(a, b); return image.linear(a, b);

View File

@ -90,7 +90,7 @@ namespace sharp {
/* /*
* Linear adjustment (a * in + b) * 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. * 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) // 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); 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->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold");
baton->gamma = sharp::AttrAsDouble(options, "gamma"); baton->gamma = sharp::AttrAsDouble(options, "gamma");
baton->gammaOut = sharp::AttrAsDouble(options, "gammaOut"); baton->gammaOut = sharp::AttrAsDouble(options, "gammaOut");
baton->linearA = sharp::AttrAsDouble(options, "linearA"); baton->linearA = sharp::AttrAsVectorOfDouble(options, "linearA");
baton->linearB = sharp::AttrAsDouble(options, "linearB"); baton->linearB = sharp::AttrAsVectorOfDouble(options, "linearB");
baton->greyscale = sharp::AttrAsBool(options, "greyscale"); baton->greyscale = sharp::AttrAsBool(options, "greyscale");
baton->normalise = sharp::AttrAsBool(options, "normalise"); baton->normalise = sharp::AttrAsBool(options, "normalise");
baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth"); baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth");

View File

@ -100,8 +100,8 @@ struct PipelineBaton {
double trimThreshold; double trimThreshold;
int trimOffsetLeft; int trimOffsetLeft;
int trimOffsetTop; int trimOffsetTop;
double linearA; std::vector<double> linearA;
double linearB; std::vector<double> linearB;
double gamma; double gamma;
double gammaOut; double gammaOut;
bool greyscale; bool greyscale;
@ -251,8 +251,8 @@ struct PipelineBaton {
trimThreshold(0.0), trimThreshold(0.0),
trimOffsetLeft(0), trimOffsetLeft(0),
trimOffsetTop(0), trimOffsetTop(0),
linearA(1.0), linearA{},
linearB(0.0), linearB{},
gamma(0.0), gamma(0.0),
greyscale(false), greyscale(false),
normalise(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 () { it('per channel level adjustment', function (done) {
assert.throws(function () { sharp(fixtures.inputWebP)
sharp(fixtures.inputPngOverlayLayer1) .linear([0.25, 0.5, 0.75], [150, 100, 50]).toBuffer(function (err, data, info) {
.linear('foo'); if (err) throw err;
fixtures.assertSimilar(fixtures.expected('linear-per-channel.jpg'), data, done);
});
}); });
assert.throws(function () { it('Invalid linear arguments', function () {
sharp(fixtures.inputPngOverlayLayer1) assert.throws(
.linear(undefined, { bar: 'baz' }); () => 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/
);
}); });
}); });