Skip to content

Commit

Permalink
Improve tint luminance with weighting function #3338
Browse files Browse the repository at this point in the history
Co-authored-by: John Cupitt <jcupitt@gmail.com>
  • Loading branch information
lovell and jcupitt committed Nov 18, 2023
1 parent 139e4b9 commit 0ef4734
Show file tree
Hide file tree
Showing 17 changed files with 70 additions and 55 deletions.
6 changes: 3 additions & 3 deletions docs/api-colour.md
Original file line number Diff line number Diff line change
@@ -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.


Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

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

Large diffs are not rendered by default.

10 changes: 4 additions & 6 deletions lib/colour.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,20 @@ 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
* const output = await sharp(input)
* .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;
}

Expand Down
3 changes: 1 addition & 2 deletions lib/constructor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
46 changes: 30 additions & 16 deletions src/operations.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/*
Expand Down
4 changes: 2 additions & 2 deletions src/operations.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 3 additions & 4 deletions src/pipeline.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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");
Expand Down
6 changes: 2 additions & 4 deletions src/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down
Binary file modified test/fixtures/expected/tint-alpha.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/fixtures/expected/tint-blue.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/fixtures/expected/tint-cmyk.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/fixtures/expected/tint-green.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/fixtures/expected/tint-red.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/fixtures/expected/tint-sepia.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 17 additions & 14 deletions test/unit/tint.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,97 +8,100 @@ 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();
});
});

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();
});
});

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();
});
});

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();
});
});

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();
});
});

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();
});
});

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();
});
});
Expand Down

0 comments on commit 0ef4734

Please sign in to comment.