Fix Image.reduce_colors/2 raising, corrupting output above 256 colors, and returning float images - #229
Conversation
|
This really great. Just wondering if, instead of casting back to |
The round + Might be worth mentioning in the docs that if the source image is CMYK, and the user wants sRGB output at the end, it's better to convert to sRGB once before reducing colors, because CMYK degrades on each conversion, e.g.: iex(1)> px = Image.new!(1, 1, color: [200, 40, 0, 30], interpretation: :cmyk); Image.get_pixel(px, 0, 0)
{:ok, [200, 40, 0, 30]}
iex(2)> px = Image.to_colorspace!(px, :srgb); Image.get_pixel(px, 0, 0)
{:ok, [0, 138, 190]}
iex(3)> px = Image.to_colorspace!(px, :cmyk); Image.get_pixel(px, 0, 0)
{:ok, [194, 44, 2, 30]}
iex(4)> px = Image.to_colorspace!(px, :srgb); Image.get_pixel(px, 0, 0)
{:ok, [1, 137, 187]}
iex(5)> px = Image.to_colorspace!(px, :cmyk); Image.get_pixel(px, 0, 0)
{:ok, [194, 43, 3, 34]}For other interpretations it's a one time cost.
|
|
Yes, you're right, clustering in RGB isn't a great strategy for colour fidelity. LAB - or maybe okLAB - would be a better choice by far. |
|
And thinking further about |
|
Updated with conversion back to the image's original colorspace |
Fixes
Clamp :colors to the number of unique colors. The
:colorsoption is now clamped to the number of unique colors in the image, instead of failing when it exceeded the image's pixel count. Matches whatImage.Scholar.k_means/2does for:num_clusters, and both paths now apply the bound throughImage.Scholar.fit/3.Silently wrong output above 256 colors. A cast to
{:u, 8}wrapped every color index above 255 back around (256 became 0, 257 became 1, etc.). Pixels with a color index above 255 were painted with the color reached by the wraparound. The indices now stay in their original{:s, 32}.Before fix (

colors: 512), half the palette was unreachable:After fix (

colors: 512):Raised instead of returning an error tuple.
Image.Scholar.fit/3did not translate theArgumentErrorthatScholar.Cluster.KMeans.fit/2raises for the checks it makes outside its option schema, nor theArithmeticErrorit raises for a single sample (a 1px image).Raised when the image could not be converted to a tensor.
to_nx!/2inside the body has been replaced byto_nx/2in the existingwith.Added
Adds
Image.Scholar.unique_color_count/1. Returns the number of distinct colors, taking either an image or its tensor.Image.Scholar.unique_colors/1was the only way to get that number, and it does a lot of unnecessary work when all you need is the count.Breaking
Returns
{:u, 8}instead of{:f, 32}. K-means produced a float image, and nothing cast it back. The result is now rounded into the band format it clustered. libvips truncated these float values when casting on save, so output values can change by up to 1 per channel after this change.Returns the image in the colorspace it was given. A
:cmykimage returns four bands and a greyscale image returns one, where previously every image came back as 3-band:srgb. Clustering still happens in:srgband the result is converted back, so:cmykoutput is slightly lossy (though not visible in practice), and a 16-bit image comes back 16-bit with its colors drawn from an 8-bit palette.