mirror of
https://github.com/lovell/sharp.git
synced 2025-07-13 12:20:13 +02:00
Expand linear operation to allow use of per-channel arrays #3303
This commit is contained in:
parent
b9261c243c
commit
74e3f73934
@ -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
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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.
|
||||||
|
@ -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");
|
||||||
|
@ -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),
|
||||||
|
BIN
test/fixtures/expected/linear-per-channel.jpg
vendored
Normal file
BIN
test/fixtures/expected/linear-per-channel.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 161 KiB |
@ -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/
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user