Skip to content

Commit

Permalink
Expand linear operation to allow use of per-channel arrays #3303
Browse files Browse the repository at this point in the history
  • Loading branch information
antonmarsden authored and lovell committed Aug 20, 2022
1 parent b9261c2 commit 74e3f73
Show file tree
Hide file tree
Showing 10 changed files with 104 additions and 34 deletions.
26 changes: 22 additions & 4 deletions docs/api-operation.md
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/search-index.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions lib/constructor.js
Expand Up @@ -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);
Expand Down
43 changes: 36 additions & 7 deletions lib/operation.js
Expand Up @@ -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;
}
Expand Down
10 changes: 7 additions & 3 deletions src/operations.cc
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/operations.h
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions src/pipeline.cc
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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");
Expand Down
8 changes: 4 additions & 4 deletions src/pipeline.h
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down
Binary file added test/fixtures/expected/linear-per-channel.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 28 additions & 9 deletions test/unit/linear.js
Expand Up @@ -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/
);
});
});

0 comments on commit 74e3f73

Please sign in to comment.