sharp icon indicating copy to clipboard operation
sharp copied to clipboard

png resize artefacts related to premultiplication rounding?

Open dannerei opened this issue 3 months ago • 10 comments

What are you trying to achieve?

We develop a product that relies heavily on image processing, and in particular on image resizing. We were previously using ImageMagick for this but wanted to transition to Sharp. Using Sharp 0.31.3 (or earlier), resizing (downscaling) png images with transparency works as expected (i.e the results using Sharp are almost indistinguishable from the results using ImageMagick), but using Sharp >= 0.32.0 downscaled pngs get weird color artefacts at the cross over from opaque to transparent. Our first thought was that libvips had changed something between 8.13 and 8.14, but using libvips directly proved that theory wrong. Hence, our best guess is that the change to "Prefer integer (un)premultiply for faster resizing of RGBA images", introduced in Sharp 0.32.0, is to blame for what we are seeing. If so, would it be possible to add a flag to opt out of using the integer premultiply when resizing?

When you searched for similar issues, what did you find that might be related?

https://github.com/lovell/sharp/issues/3658

Please provide a minimal, standalone code sample, without other dependencies, that demonstrates this question

sharp("path to image").resize(20, 20)

Please provide sample image(s) that help explain this question

These images are very small in size and zooming will be required to notice the artefacts I'm referring to. Larger images can be provided if needed.

Original (100x100) sample Resized using Sharp 0.31.3 (20x20) sample-resized-using-sharp-0 31 3 Resized using Sharp 0.33.3 (20x20). Notice the discoloring of the top opaque pixel row. sample-resized-using-sharp-0 33 3

dannerei avatar Apr 02 '24 14:04 dannerei

Hi, you can use pipelineColourspace to force the processing colourspace (and therefore force a bitdepth).

For example, if you want floating-point RGB, try scrgb:

sharp("path to image")
  .pipelineColourspace('scrgb')
  .resize(20, 20)
  ...

lovell avatar Apr 04 '24 20:04 lovell

That seemed to do the trick. Thanks a lot! One thing though. According to the documentation that feature is experimental, so can we rely on it?

dannerei avatar Apr 05 '24 08:04 dannerei

that feature is experimental, so can we rely on it?

Experimental insofar as there are probably still some untested code paths, with the aim to add more test cases as bugs are found and fixed. I don't expect to remove this feature.

lovell avatar Apr 05 '24 13:04 lovell

Hi again, and an update on this:

We have now run a lot of tests using the pipelineColourspace('scrgb') setting, and although the originally reported rounding artefacts are gone, we have noticed that the resulting images look sharpened in a way that is not to our liking. Especially in transitional areas between bright and dark, the edges get unrealistically sharpened. Therefore I ask again, would it be possible to add a flag to opt out of using the integer premultiply when resizing (the 0.31.3 behaviour)?

dannerei avatar Apr 09 '24 11:04 dannerei

we have noticed that the resulting images look sharpened in a way that is not to our liking

Please can you provide sample images and minimal code that allows someone else to reproduce. Please also include more information about expected vs actual output.

lovell avatar Apr 09 '24 17:04 lovell

Aside: I've removed the "experimental" status of pipelineColourspace via commit https://github.com/lovell/sharp/commit/f67228e5ea049db67210f0c5e4b88baa6b93211c

lovell avatar Apr 09 '24 21:04 lovell

Here are a few samples using sharp 0.33.3 with and without pipelineColourspace. We expected the outputs to be very similar, but as can be seen there are major differences in the actual results.

Resize sample 1:

Original image: sample1_orig

Result using sharp("path to image").resize(240, 240) sample1

Result using sharp("path to image").pipelineColourspace('scrgb').resize(240, 240) sample1_scrgb

Resize sample 2:

Original image: sample2_orig

Result using sharp("path to image").resize(240, 240) sample2

Result using sharp("path to image").pipelineColourspace('scrgb').resize(240, 240) sample2_scrgb

dannerei avatar Apr 10 '24 11:04 dannerei

Thanks for the examples, my best guess would be that the use of wide-gamut, linear scRGB is producing more accurate luminance levels in the output compared with non-linear sRGB.

If you would prefer less accurate luminance, perhaps try experimenting with gamma levels e.g. in=2.2, out=1.0.

lovell avatar Apr 10 '24 21:04 lovell

@dannerei Were you able to make any progress with this?

lovell avatar Apr 29 '24 07:04 lovell

Unfortunately we were not able to reach satisfactory results in a consistent way using the proposed solution, so for now we're stuck with ImageMagick. I still hope that we can make the transition to sharp sometime in the future though, since it offers superior performance and a very pleasant API to work with.

dannerei avatar Apr 30 '24 14:04 dannerei