-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ImageMagick-style tint #3338
Comments
Bonjour, this has been discussed previously at #1235 - sharp sets the chroma in the LAB colour space and doesn't scale the luminance, but perhaps should. The suggestion in #1235 (comment) might be worth exploring, and happy to accept a PR, if you're able. |
Much appreciated. I tried the above snippet and ended up with the following, which still isn't right (note how all the highs are gone), but I'll keep looking around. Desired resultVips
|
I think the equivalent in libvips would be // #c31306 as CIELAB triple
const tint = [41.349, 63.353, 53.1];
// let lut = vips.Image.identity() / 255;
let lut = vips.Image.identity().divide(255);
// lut = (1 - (4.0 * ((lut - 0.5) ** 2))) * tint
lut = lut.subtract(0.5).pow(2).multiply(4).multiply(-1).add(1).multiply(tint);
lut = lut.colourspace(vips.Interpretation.srgb/* 'srgb' */, {
source_space: vips.Interpretation.lab // 'lab'
});
// Load an image from file
let im = vips.Image.newFromFile('185454088-9c627d29-c4b4-4d3d-9ce5-0fa4666bb566.png');
if (im.hasAlpha()) {
// Separate alpha channel
const withoutAlpha = im.extractBand(0, { n: im.bands - 1 });
const alpha = im.extractBand(im.bands - 1);
im = withoutAlpha.maplut(lut).bandjoin(alpha);
} else {
im = im.maplut(lut);
}
// Finally, write the result to a blob
const outBuffer = im.writeToBuffer('.png'); However, this would produce a different image than the one from ImageMagick. I probably made a mistake somewhere. 😅 /cc @jcupitt |
I think the weighting function is an upside down parabola: So you need to multiply the tint by the weight, but then use a plain ramp for L (otherwise you'll send white back to black again). I had a stab in python: #!/usr/bin/python3
import sys
import pyvips
# #c31306 as CIELAB triple
tint = [41.349, 63.353, 53.1]
lut = pyvips.Image.identity() / 255
# so max at 0.5, tailing to 0 at black and white
lab = (1 - 4.0 * ((lut - 0.5) * (lut - 0.5))) * tint
# we want L to stay the same, we just take ab from the lab tint
lut = (lut * 100).bandjoin(lab[1:])
# and turn to sRGB
lut = lut.colourspace("srgb", source_space="lab")
image = pyvips.Image.new_from_file(sys.argv[1], access="sequential")
image = image.colourspace("b-w").maplut(lut)
image.write_to_file(sys.argv[2]) To make: It looks a bit dark, but that's because it's in CIELAB and I think IM is working in RGB. Adding a gamma would probably fix it. |
No. fricken. way. It looks fantastic! Thank you all so much, y'all are incredible! 🎉 |
Great! Here's an updated wasm-vips playground link based on the above Python sample.
Removing the monochrome colourspace conversion could also make it a bit brighter. |
Man, you guys are the kindest. Thank you so much again, I could not be more appreciative! 🙏 |
... I thought of a simple way to fix the gamma problem. You build the initial LUT in RGB space, go to LAB, do the tint, then go back to RGB again. Now when you apply the LUT to an RGB image you don't distort the gamma. #!/usr/bin/python3
import sys
import pyvips
if len(sys.argv) != 3:
print(f"usage: {sys.argv[0]} INPUT-IMAGE OUTPUT-IMAGE")
sys.exit(1)
# #c31306 as an rgb triple
tint = [195, 19, 6]
# turn to CIELAB
tint = (pyvips.Image.black(1, 1) + tint).colourspace("lab", source_space="srgb")
tint = [x.avg() for x in tint.bandsplit()]
# start with an RGB greyscale, then go to LAB
lab = pyvips.Image.identity(bands=3).colourspace("lab", source_space="srgb")
# scale to 0-1 and make a weighting function
x = lab[0] / 100
weight = 1 - 4.0 * ((x - 0.5) * (x - 0.5))
# we want L to stay the same, we just weight ab
lab = lab[0].bandjoin((weight * tint)[1:])
# and turn to sRGB
lut = lab.colourspace("srgb", source_space="lab")
image = pyvips.Image.new_from_file(sys.argv[1], access="sequential")
image = image.colourspace("b-w").maplut(lut)
image.write_to_file(sys.argv[2]) I see: From left to right, that's the original, this code, the IM result, and a block of |
Nice! Looking at IM's code, I think they operate directly on RGB, which is probably the reason for this difference. (here's an updated wasm-vips playground link) |
Yes, RGB is really awful for colour work like this, it's very hard to control. |
I'd love to see some progress on this. I tried to implement this with no avail on my side. Are we just waiting on a functional PR? |
I don't know sharp well enough to make a PR, but I reworked that python code into c++, it might help someone else: /* compile with
*
* g++ tint.cc `pkg-config vips-cpp --cflags --libs`
*/
#include <vips/vips8>
using namespace vips;
// make a LUT which applies a tint to a mono image
static VImage
make_tint(std::vector<double> tint)
{
// turn the tint to CIELAB
tint = (VImage::black(1, 1) + tint)
.colourspace(VIPS_INTERPRETATION_LAB,
VImage::option()->set("source_space", VIPS_INTERPRETATION_sRGB))
.getpoint(0, 0);
// start with an RGB greyscale, then go to LAB
auto lab = VImage::identity(VImage::option()->set("bands", 3))
.colourspace(VIPS_INTERPRETATION_LAB,
VImage::option()->set("source_space", VIPS_INTERPRETATION_sRGB));
// scale to 0-1 and make a weighting function
auto l = lab[0] / 100;
auto weight = 1 - 4.0 * ((l - 0.5) * (l - 0.5));
// weight ab
auto ab = (weight * tint)
.extract_band(1, VImage::option()->set("n", 2));
// use weighted ab
lab = lab[0].bandjoin(ab);
// and turn to sRGB
return lab
.colourspace(VIPS_INTERPRETATION_sRGB,
VImage::option()->set("source_space", VIPS_INTERPRETATION_LAB));
}
int
main (int argc, char **argv)
{
// tint with #c31306 as an rgb triple
auto tint = make_tint({195, 19, 6});
auto image = VImage::new_from_file(argv[1], VImage::option()
->set("access", "sequential"));
image = image.colourspace(VIPS_INTERPRETATION_B_W).maplut(tint);
image.write_to_file(argv[2]);
} |
Co-authored-by: John Cupitt <jcupitt@gmail.com>
Co-authored-by: John Cupitt <jcupitt@gmail.com>
Co-authored-by: John Cupitt <jcupitt@gmail.com>
v0.33.0 now available with this improvement, thanks all. |
Question about an existing feature
I'm pretty sure this is just me being stupid, but is there a way to use
tint
to achieve something like the below?ImageMagick
convert "input.png" -fill "#c31306" -tint 100 "output.png"
Sharp
await sharp(image).tint('#c31306')
Note how the highs and lows are also tinted in the Sharp case, but not with ImageMagick.
The ImageMagick source code indicates that they use a weighting function for their tinting:
But I am not sure how to convert this into Sharp terms. Perhaps I could use
recomb
somehow?I have even tried to use
composite
, but none of the blend modes seem to be able to achieve what I want.multiply
gets close, but I think ultimately what I need is something like Cairo'scolor
blend mode, which doesn't exist in Sharp or vips.Any help would be highly appreciated!
What are you trying to achieve?
A tint that preserves hights and lows.
When you searched for similar issues, what did you find that might be related?
I found some tickets regarding tint not behaving as expected, but those issues have been resolved.
Please provide a minimal, standalone code sample, without other dependencies, that demonstrates this question
Please provide sample image(s) that help explain this question
Raw image:
The text was updated successfully, but these errors were encountered: