Improve tint luminance with weighting function (#3859)

Co-authored-by: John Cupitt <jcupitt@gmail.com>
This commit is contained in:
Lovell Fuller 2023-11-19 13:19:34 +00:00 committed by GitHub
parent 139e4b9f65
commit 3f7313d031
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 70 additions and 55 deletions

View File

@ -1,7 +1,7 @@
## 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.
@ -12,7 +12,7 @@ An alpha channel may be present and will be unchanged by the operation.
| 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**
```js

View File

@ -20,6 +20,10 @@ Requires libvips v8.15.0
* Options for `trim` operation must be an Object, add new `lineArt` option.
[#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.
[#3653](https://github.com/lovell/sharp/issues/3653)

File diff suppressed because one or more lines are too long

View File

@ -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.
*
* @example
@ -27,14 +27,12 @@ const colourspace = {
* .tint({ r: 255, g: 240, b: 16 })
* .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}
* @throws {Error} Invalid parameter
*/
function tint (rgb) {
const colour = color(rgb);
this.options.tintA = colour.a();
this.options.tintB = colour.b();
function tint (tint) {
this._setBackgroundColourOption('tint', tint);
return this;
}

View File

@ -212,8 +212,7 @@ const Sharp = function (input, options) {
kernel: 'lanczos3',
fastShrinkOnLoad: true,
// operations
tintA: 128,
tintB: 128,
tint: [-1, 0, 0, 0],
flatten: false,
flattenBackground: [0, 0, 0],
unflatten: false,

6
lib/index.d.ts vendored
View File

@ -239,12 +239,12 @@ declare namespace sharp {
//#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.
* @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
*/
tint(rgb: Color): Sharp;
tint(tint: Color): Sharp;
/**
* Convert to 8-bit greyscale; 256 shades of grey.

View File

@ -16,30 +16,44 @@ using vips::VError;
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) {
// Get original colourspace
VImage Tint(VImage image, std::vector<double> const tint) {
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();
if (typeBeforeTint == VIPS_INTERPRETATION_RGB) {
typeBeforeTint = VIPS_INTERPRETATION_sRGB;
}
// Extract luminance
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
// Apply lookup table
if (HasAlpha(image)) {
// Extract original alpha channel
VImage alpha = image[image.bands() - 1];
// Join alpha channel to normalised image
tinted = tinted.bandjoin(alpha);
image = RemoveAlpha(image)
.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;
}
/*

View File

@ -15,9 +15,9 @@ using vips::VImage;
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.

View File

@ -736,8 +736,8 @@ class PipelineWorker : public Napi::AsyncWorker {
}
// Tint the image
if (baton->tintA < 128.0 || baton->tintB < 128.0) {
image = sharp::Tint(image, baton->tintA, baton->tintB);
if (baton->tint[0] >= 0.0) {
image = sharp::Tint(image, baton->tint);
}
// Remove alpha channel, if any
@ -1527,8 +1527,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->normalise = sharp::AttrAsBool(options, "normalise");
baton->normaliseLower = sharp::AttrAsUint32(options, "normaliseLower");
baton->normaliseUpper = sharp::AttrAsUint32(options, "normaliseUpper");
baton->tintA = sharp::AttrAsDouble(options, "tintA");
baton->tintB = sharp::AttrAsDouble(options, "tintB");
baton->tint = sharp::AttrAsVectorOfDouble(options, "tint");
baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth");
baton->claheHeight = sharp::AttrAsUint32(options, "claheHeight");
baton->claheMaxSlope = sharp::AttrAsUint32(options, "claheMaxSlope");

View File

@ -69,8 +69,7 @@ struct PipelineBaton {
bool premultiplied;
bool tileCentre;
bool fastShrinkOnLoad;
double tintA;
double tintB;
std::vector<double> tint;
bool flatten;
std::vector<double> flattenBackground;
bool unflatten;
@ -239,8 +238,7 @@ struct PipelineBaton {
attentionX(0),
attentionY(0),
premultiplied(false),
tintA(128.0),
tintB(128.0),
tint{ -1.0, 0.0, 0.0, 0.0 },
flatten(false),
flattenBackground{ 0.0, 0.0, 0.0 },
unflatten(false),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -8,16 +8,19 @@ const assert = require('assert');
const sharp = require('../../');
const fixtures = require('../fixtures');
// Allow for small rounding differences between platforms
const maxDistance = 6;
describe('Tint', function () {
it('tints rgb image red', function (done) {
const output = fixtures.path('output.tint-red.jpg');
sharp(fixtures.inputJpg)
.resize(320, 240, { fastShrinkOnLoad: false })
.resize(320, 240)
.tint('#FF0000')
.toFile(output, function (err, info) {
if (err) throw err;
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();
});
});
@ -25,12 +28,12 @@ describe('Tint', function () {
it('tints rgb image green', function (done) {
const output = fixtures.path('output.tint-green.jpg');
sharp(fixtures.inputJpg)
.resize(320, 240, { fastShrinkOnLoad: false })
.resize(320, 240)
.tint('#00FF00')
.toFile(output, function (err, info) {
if (err) throw err;
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();
});
});
@ -38,12 +41,12 @@ describe('Tint', function () {
it('tints rgb image blue', function (done) {
const output = fixtures.path('output.tint-blue.jpg');
sharp(fixtures.inputJpg)
.resize(320, 240, { fastShrinkOnLoad: false })
.resize(320, 240)
.tint('#0000FF')
.toFile(output, function (err, info) {
if (err) throw err;
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();
});
});
@ -51,13 +54,13 @@ describe('Tint', function () {
it('tints rgb image with sepia tone', function (done) {
const output = fixtures.path('output.tint-sepia-hex.jpg');
sharp(fixtures.inputJpg)
.resize(320, 240, { fastShrinkOnLoad: false })
.resize(320, 240)
.tint('#704214')
.toFile(output, function (err, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-sepia.jpg'), 10);
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-sepia.jpg'), maxDistance);
done();
});
});
@ -65,13 +68,13 @@ describe('Tint', function () {
it('tints rgb image with sepia tone with rgb colour', function (done) {
const output = fixtures.path('output.tint-sepia-rgb.jpg');
sharp(fixtures.inputJpg)
.resize(320, 240, { fastShrinkOnLoad: false })
.resize(320, 240)
.tint([112, 66, 20])
.toFile(output, function (err, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-sepia.jpg'), 10);
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-sepia.jpg'), maxDistance);
done();
});
});
@ -79,13 +82,13 @@ describe('Tint', function () {
it('tints rgb image with alpha channel', function (done) {
const output = fixtures.path('output.tint-alpha.png');
sharp(fixtures.inputPngRGBWithAlpha)
.resize(320, 240, { fastShrinkOnLoad: false })
.resize(320, 240)
.tint('#704214')
.toFile(output, function (err, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-alpha.png'), 10);
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-alpha.png'), maxDistance);
done();
});
});
@ -93,12 +96,12 @@ describe('Tint', function () {
it('tints cmyk image red', function (done) {
const output = fixtures.path('output.tint-cmyk.jpg');
sharp(fixtures.inputJpgWithCmykProfile)
.resize(320, 240, { fastShrinkOnLoad: false })
.resize(320, 240)
.tint('#FF0000')
.toFile(output, function (err, info) {
if (err) throw err;
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();
});
});