Improve tint luminance with weighting function (#3859)
Co-authored-by: John Cupitt <jcupitt@gmail.com>
@ -1,7 +1,7 @@
|
|||||||
## tint
|
## tint
|
||||||
> tint(rgb) ⇒ <code>Sharp</code>
|
> tint(tint) ⇒ <code>Sharp</code>
|
||||||
|
|
||||||
Tint the image using the provided chroma while preserving the image luminance.
|
Tint the image using the provided colour.
|
||||||
An alpha channel may be present and will be unchanged by the operation.
|
An alpha channel may be present and will be unchanged by the operation.
|
||||||
|
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ An alpha channel may be present and will be unchanged by the operation.
|
|||||||
|
|
||||||
| Param | Type | Description |
|
| Param | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| rgb | <code>string</code> \| <code>Object</code> | parsed by the [color](https://www.npmjs.org/package/color) module to extract chroma values. |
|
| tint | <code>String</code> \| <code>Object</code> | Parsed by the [color](https://www.npmjs.org/package/color) module. |
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
```js
|
```js
|
||||||
|
@ -20,6 +20,10 @@ Requires libvips v8.15.0
|
|||||||
* Options for `trim` operation must be an Object, add new `lineArt` option.
|
* Options for `trim` operation must be an Object, add new `lineArt` option.
|
||||||
[#2363](https://github.com/lovell/sharp/issues/2363)
|
[#2363](https://github.com/lovell/sharp/issues/2363)
|
||||||
|
|
||||||
|
* Improve luminance of `tint` operation with weighting function.
|
||||||
|
[#3338](https://github.com/lovell/sharp/issues/3338)
|
||||||
|
[@jcupitt](https://github.com/jcupitt)
|
||||||
|
|
||||||
* Ensure all `Error` objects contain a `stack` property.
|
* Ensure all `Error` objects contain a `stack` property.
|
||||||
[#3653](https://github.com/lovell/sharp/issues/3653)
|
[#3653](https://github.com/lovell/sharp/issues/3653)
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ const colourspace = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tint the image using the provided chroma while preserving the image luminance.
|
* Tint the image using the provided colour.
|
||||||
* An alpha channel may be present and will be unchanged by the operation.
|
* An alpha channel may be present and will be unchanged by the operation.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
@ -27,14 +27,12 @@ const colourspace = {
|
|||||||
* .tint({ r: 255, g: 240, b: 16 })
|
* .tint({ r: 255, g: 240, b: 16 })
|
||||||
* .toBuffer();
|
* .toBuffer();
|
||||||
*
|
*
|
||||||
* @param {string|Object} rgb - parsed by the [color](https://www.npmjs.org/package/color) module to extract chroma values.
|
* @param {string|Object} tint - Parsed by the [color](https://www.npmjs.org/package/color) module.
|
||||||
* @returns {Sharp}
|
* @returns {Sharp}
|
||||||
* @throws {Error} Invalid parameter
|
* @throws {Error} Invalid parameter
|
||||||
*/
|
*/
|
||||||
function tint (rgb) {
|
function tint (tint) {
|
||||||
const colour = color(rgb);
|
this._setBackgroundColourOption('tint', tint);
|
||||||
this.options.tintA = colour.a();
|
|
||||||
this.options.tintB = colour.b();
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,8 +212,7 @@ const Sharp = function (input, options) {
|
|||||||
kernel: 'lanczos3',
|
kernel: 'lanczos3',
|
||||||
fastShrinkOnLoad: true,
|
fastShrinkOnLoad: true,
|
||||||
// operations
|
// operations
|
||||||
tintA: 128,
|
tint: [-1, 0, 0, 0],
|
||||||
tintB: 128,
|
|
||||||
flatten: false,
|
flatten: false,
|
||||||
flattenBackground: [0, 0, 0],
|
flattenBackground: [0, 0, 0],
|
||||||
unflatten: false,
|
unflatten: false,
|
||||||
|
6
lib/index.d.ts
vendored
@ -239,12 +239,12 @@ declare namespace sharp {
|
|||||||
//#region Color functions
|
//#region Color functions
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tint the image using the provided chroma while preserving the image luminance.
|
* Tint the image using the provided colour.
|
||||||
* An alpha channel may be present and will be unchanged by the operation.
|
* An alpha channel may be present and will be unchanged by the operation.
|
||||||
* @param rgb Parsed by the color module to extract chroma values.
|
* @param tint Parsed by the color module.
|
||||||
* @returns A sharp instance that can be used to chain operations
|
* @returns A sharp instance that can be used to chain operations
|
||||||
*/
|
*/
|
||||||
tint(rgb: Color): Sharp;
|
tint(tint: Color): Sharp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert to 8-bit greyscale; 256 shades of grey.
|
* Convert to 8-bit greyscale; 256 shades of grey.
|
||||||
|
@ -16,30 +16,44 @@ using vips::VError;
|
|||||||
|
|
||||||
namespace sharp {
|
namespace sharp {
|
||||||
/*
|
/*
|
||||||
* Tint an image using the specified chroma, preserving the original image luminance
|
* Tint an image using the provided RGB.
|
||||||
*/
|
*/
|
||||||
VImage Tint(VImage image, double const a, double const b) {
|
VImage Tint(VImage image, std::vector<double> const tint) {
|
||||||
// Get original colourspace
|
std::vector<double> const tintLab = (VImage::black(1, 1) + tint)
|
||||||
|
.colourspace(VIPS_INTERPRETATION_LAB, VImage::option()->set("source_space", VIPS_INTERPRETATION_sRGB))
|
||||||
|
.getpoint(0, 0);
|
||||||
|
// LAB identity function
|
||||||
|
VImage identityLab = VImage::identity(VImage::option()->set("bands", 3))
|
||||||
|
.colourspace(VIPS_INTERPRETATION_LAB, VImage::option()->set("source_space", VIPS_INTERPRETATION_sRGB));
|
||||||
|
// Scale luminance range, 0.0 to 1.0
|
||||||
|
VImage l = identityLab[0] / 100;
|
||||||
|
// Weighting functions
|
||||||
|
VImage weightL = 1.0 - 4.0 * ((l - 0.5) * (l - 0.5));
|
||||||
|
VImage weightAB = (weightL * tintLab).extract_band(1, VImage::option()->set("n", 2));
|
||||||
|
identityLab = identityLab[0].bandjoin(weightAB);
|
||||||
|
// Convert lookup table to sRGB
|
||||||
|
VImage lut = identityLab.colourspace(VIPS_INTERPRETATION_sRGB,
|
||||||
|
VImage::option()->set("source_space", VIPS_INTERPRETATION_LAB));
|
||||||
|
// Original colourspace
|
||||||
VipsInterpretation typeBeforeTint = image.interpretation();
|
VipsInterpretation typeBeforeTint = image.interpretation();
|
||||||
if (typeBeforeTint == VIPS_INTERPRETATION_RGB) {
|
if (typeBeforeTint == VIPS_INTERPRETATION_RGB) {
|
||||||
typeBeforeTint = VIPS_INTERPRETATION_sRGB;
|
typeBeforeTint = VIPS_INTERPRETATION_sRGB;
|
||||||
}
|
}
|
||||||
// Extract luminance
|
// Apply lookup table
|
||||||
VImage luminance = image.colourspace(VIPS_INTERPRETATION_LAB)[0];
|
|
||||||
// Create the tinted version by combining the L from the original and the chroma from the tint
|
|
||||||
std::vector<double> chroma {a, b};
|
|
||||||
VImage tinted = luminance
|
|
||||||
.bandjoin(chroma)
|
|
||||||
.copy(VImage::option()->set("interpretation", VIPS_INTERPRETATION_LAB))
|
|
||||||
.colourspace(typeBeforeTint);
|
|
||||||
// Attach original alpha channel, if any
|
|
||||||
if (HasAlpha(image)) {
|
if (HasAlpha(image)) {
|
||||||
// Extract original alpha channel
|
|
||||||
VImage alpha = image[image.bands() - 1];
|
VImage alpha = image[image.bands() - 1];
|
||||||
// Join alpha channel to normalised image
|
image = RemoveAlpha(image)
|
||||||
tinted = tinted.bandjoin(alpha);
|
.colourspace(VIPS_INTERPRETATION_B_W)
|
||||||
|
.maplut(lut)
|
||||||
|
.colourspace(typeBeforeTint)
|
||||||
|
.bandjoin(alpha);
|
||||||
|
} else {
|
||||||
|
image = image
|
||||||
|
.colourspace(VIPS_INTERPRETATION_B_W)
|
||||||
|
.maplut(lut)
|
||||||
|
.colourspace(typeBeforeTint);
|
||||||
}
|
}
|
||||||
return tinted;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -15,9 +15,9 @@ using vips::VImage;
|
|||||||
namespace sharp {
|
namespace sharp {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Tint an image using the specified chroma, preserving the original image luminance
|
* Tint an image using the provided RGB.
|
||||||
*/
|
*/
|
||||||
VImage Tint(VImage image, double const a, double const b);
|
VImage Tint(VImage image, std::vector<double> const tint);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Stretch luminance to cover full dynamic range.
|
* Stretch luminance to cover full dynamic range.
|
||||||
|
@ -736,8 +736,8 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tint the image
|
// Tint the image
|
||||||
if (baton->tintA < 128.0 || baton->tintB < 128.0) {
|
if (baton->tint[0] >= 0.0) {
|
||||||
image = sharp::Tint(image, baton->tintA, baton->tintB);
|
image = sharp::Tint(image, baton->tint);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove alpha channel, if any
|
// Remove alpha channel, if any
|
||||||
@ -1527,8 +1527,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
|
|||||||
baton->normalise = sharp::AttrAsBool(options, "normalise");
|
baton->normalise = sharp::AttrAsBool(options, "normalise");
|
||||||
baton->normaliseLower = sharp::AttrAsUint32(options, "normaliseLower");
|
baton->normaliseLower = sharp::AttrAsUint32(options, "normaliseLower");
|
||||||
baton->normaliseUpper = sharp::AttrAsUint32(options, "normaliseUpper");
|
baton->normaliseUpper = sharp::AttrAsUint32(options, "normaliseUpper");
|
||||||
baton->tintA = sharp::AttrAsDouble(options, "tintA");
|
baton->tint = sharp::AttrAsVectorOfDouble(options, "tint");
|
||||||
baton->tintB = sharp::AttrAsDouble(options, "tintB");
|
|
||||||
baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth");
|
baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth");
|
||||||
baton->claheHeight = sharp::AttrAsUint32(options, "claheHeight");
|
baton->claheHeight = sharp::AttrAsUint32(options, "claheHeight");
|
||||||
baton->claheMaxSlope = sharp::AttrAsUint32(options, "claheMaxSlope");
|
baton->claheMaxSlope = sharp::AttrAsUint32(options, "claheMaxSlope");
|
||||||
|
@ -69,8 +69,7 @@ struct PipelineBaton {
|
|||||||
bool premultiplied;
|
bool premultiplied;
|
||||||
bool tileCentre;
|
bool tileCentre;
|
||||||
bool fastShrinkOnLoad;
|
bool fastShrinkOnLoad;
|
||||||
double tintA;
|
std::vector<double> tint;
|
||||||
double tintB;
|
|
||||||
bool flatten;
|
bool flatten;
|
||||||
std::vector<double> flattenBackground;
|
std::vector<double> flattenBackground;
|
||||||
bool unflatten;
|
bool unflatten;
|
||||||
@ -239,8 +238,7 @@ struct PipelineBaton {
|
|||||||
attentionX(0),
|
attentionX(0),
|
||||||
attentionY(0),
|
attentionY(0),
|
||||||
premultiplied(false),
|
premultiplied(false),
|
||||||
tintA(128.0),
|
tint{ -1.0, 0.0, 0.0, 0.0 },
|
||||||
tintB(128.0),
|
|
||||||
flatten(false),
|
flatten(false),
|
||||||
flattenBackground{ 0.0, 0.0, 0.0 },
|
flattenBackground{ 0.0, 0.0, 0.0 },
|
||||||
unflatten(false),
|
unflatten(false),
|
||||||
|
BIN
test/fixtures/expected/tint-alpha.png
vendored
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 135 KiB |
BIN
test/fixtures/expected/tint-blue.jpg
vendored
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
BIN
test/fixtures/expected/tint-cmyk.jpg
vendored
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 30 KiB |
BIN
test/fixtures/expected/tint-green.jpg
vendored
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
BIN
test/fixtures/expected/tint-red.jpg
vendored
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 15 KiB |
BIN
test/fixtures/expected/tint-sepia.jpg
vendored
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 15 KiB |
@ -8,16 +8,19 @@ const assert = require('assert');
|
|||||||
const sharp = require('../../');
|
const sharp = require('../../');
|
||||||
const fixtures = require('../fixtures');
|
const fixtures = require('../fixtures');
|
||||||
|
|
||||||
|
// Allow for small rounding differences between platforms
|
||||||
|
const maxDistance = 6;
|
||||||
|
|
||||||
describe('Tint', function () {
|
describe('Tint', function () {
|
||||||
it('tints rgb image red', function (done) {
|
it('tints rgb image red', function (done) {
|
||||||
const output = fixtures.path('output.tint-red.jpg');
|
const output = fixtures.path('output.tint-red.jpg');
|
||||||
sharp(fixtures.inputJpg)
|
sharp(fixtures.inputJpg)
|
||||||
.resize(320, 240, { fastShrinkOnLoad: false })
|
.resize(320, 240)
|
||||||
.tint('#FF0000')
|
.tint('#FF0000')
|
||||||
.toFile(output, function (err, info) {
|
.toFile(output, function (err, info) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
assert.strictEqual(true, info.size > 0);
|
assert.strictEqual(true, info.size > 0);
|
||||||
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-red.jpg'), 18);
|
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-red.jpg'), maxDistance);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -25,12 +28,12 @@ describe('Tint', function () {
|
|||||||
it('tints rgb image green', function (done) {
|
it('tints rgb image green', function (done) {
|
||||||
const output = fixtures.path('output.tint-green.jpg');
|
const output = fixtures.path('output.tint-green.jpg');
|
||||||
sharp(fixtures.inputJpg)
|
sharp(fixtures.inputJpg)
|
||||||
.resize(320, 240, { fastShrinkOnLoad: false })
|
.resize(320, 240)
|
||||||
.tint('#00FF00')
|
.tint('#00FF00')
|
||||||
.toFile(output, function (err, info) {
|
.toFile(output, function (err, info) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
assert.strictEqual(true, info.size > 0);
|
assert.strictEqual(true, info.size > 0);
|
||||||
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-green.jpg'), 27);
|
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-green.jpg'), maxDistance);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -38,12 +41,12 @@ describe('Tint', function () {
|
|||||||
it('tints rgb image blue', function (done) {
|
it('tints rgb image blue', function (done) {
|
||||||
const output = fixtures.path('output.tint-blue.jpg');
|
const output = fixtures.path('output.tint-blue.jpg');
|
||||||
sharp(fixtures.inputJpg)
|
sharp(fixtures.inputJpg)
|
||||||
.resize(320, 240, { fastShrinkOnLoad: false })
|
.resize(320, 240)
|
||||||
.tint('#0000FF')
|
.tint('#0000FF')
|
||||||
.toFile(output, function (err, info) {
|
.toFile(output, function (err, info) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
assert.strictEqual(true, info.size > 0);
|
assert.strictEqual(true, info.size > 0);
|
||||||
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-blue.jpg'), 14);
|
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-blue.jpg'), maxDistance);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -51,13 +54,13 @@ describe('Tint', function () {
|
|||||||
it('tints rgb image with sepia tone', function (done) {
|
it('tints rgb image with sepia tone', function (done) {
|
||||||
const output = fixtures.path('output.tint-sepia-hex.jpg');
|
const output = fixtures.path('output.tint-sepia-hex.jpg');
|
||||||
sharp(fixtures.inputJpg)
|
sharp(fixtures.inputJpg)
|
||||||
.resize(320, 240, { fastShrinkOnLoad: false })
|
.resize(320, 240)
|
||||||
.tint('#704214')
|
.tint('#704214')
|
||||||
.toFile(output, function (err, info) {
|
.toFile(output, function (err, info) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
assert.strictEqual(320, info.width);
|
assert.strictEqual(320, info.width);
|
||||||
assert.strictEqual(240, info.height);
|
assert.strictEqual(240, info.height);
|
||||||
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-sepia.jpg'), 10);
|
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-sepia.jpg'), maxDistance);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -65,13 +68,13 @@ describe('Tint', function () {
|
|||||||
it('tints rgb image with sepia tone with rgb colour', function (done) {
|
it('tints rgb image with sepia tone with rgb colour', function (done) {
|
||||||
const output = fixtures.path('output.tint-sepia-rgb.jpg');
|
const output = fixtures.path('output.tint-sepia-rgb.jpg');
|
||||||
sharp(fixtures.inputJpg)
|
sharp(fixtures.inputJpg)
|
||||||
.resize(320, 240, { fastShrinkOnLoad: false })
|
.resize(320, 240)
|
||||||
.tint([112, 66, 20])
|
.tint([112, 66, 20])
|
||||||
.toFile(output, function (err, info) {
|
.toFile(output, function (err, info) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
assert.strictEqual(320, info.width);
|
assert.strictEqual(320, info.width);
|
||||||
assert.strictEqual(240, info.height);
|
assert.strictEqual(240, info.height);
|
||||||
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-sepia.jpg'), 10);
|
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-sepia.jpg'), maxDistance);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -79,13 +82,13 @@ describe('Tint', function () {
|
|||||||
it('tints rgb image with alpha channel', function (done) {
|
it('tints rgb image with alpha channel', function (done) {
|
||||||
const output = fixtures.path('output.tint-alpha.png');
|
const output = fixtures.path('output.tint-alpha.png');
|
||||||
sharp(fixtures.inputPngRGBWithAlpha)
|
sharp(fixtures.inputPngRGBWithAlpha)
|
||||||
.resize(320, 240, { fastShrinkOnLoad: false })
|
.resize(320, 240)
|
||||||
.tint('#704214')
|
.tint('#704214')
|
||||||
.toFile(output, function (err, info) {
|
.toFile(output, function (err, info) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
assert.strictEqual(320, info.width);
|
assert.strictEqual(320, info.width);
|
||||||
assert.strictEqual(240, info.height);
|
assert.strictEqual(240, info.height);
|
||||||
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-alpha.png'), 10);
|
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-alpha.png'), maxDistance);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -93,12 +96,12 @@ describe('Tint', function () {
|
|||||||
it('tints cmyk image red', function (done) {
|
it('tints cmyk image red', function (done) {
|
||||||
const output = fixtures.path('output.tint-cmyk.jpg');
|
const output = fixtures.path('output.tint-cmyk.jpg');
|
||||||
sharp(fixtures.inputJpgWithCmykProfile)
|
sharp(fixtures.inputJpgWithCmykProfile)
|
||||||
.resize(320, 240, { fastShrinkOnLoad: false })
|
.resize(320, 240)
|
||||||
.tint('#FF0000')
|
.tint('#FF0000')
|
||||||
.toFile(output, function (err, info) {
|
.toFile(output, function (err, info) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
assert.strictEqual(true, info.size > 0);
|
assert.strictEqual(true, info.size > 0);
|
||||||
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-cmyk.jpg'), 15);
|
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-cmyk.jpg'), maxDistance);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|