DanBloomberg / leptonica

Leptonica is an open source library containing software that is broadly useful for image processing and image analysis applications. The official github repository for Leptonica is: danbloomberg/leptonica. See leptonica.org for more documentation.
Other
1.77k stars 389 forks source link

pixWriteJp2k: unexpected strong color artifacts on featureless image #730

Closed atykhyy closed 8 months ago

atykhyy commented 8 months ago

I am using Leptonica's pixWriteJp2k to compress background images, many of which are rather featureless (typical example below). With an identical input bitmap, the output of Leptonica's pixWriteJp2k is both 4x larger in byte size and has much more pronounced color and speckling artifacts compared to the output of ImageMagick despite using the same JPEG2000 quality (SNR) setting. Other compression settings are Leptonica / ImageMagick defaults. I looked at source code to find out what the default settings are, and they seem quite similar. Both use nlevels = 5 and one layer on top of opj_set_default_encoder_parameters(). What other differences am I missing? Is it possible to prevent compression from generating these color artifacts with non-default settings? Is there a better place to ask this question?

The example image was compressed on the same machine using Leptonica 1.83.1 (essentially pixWriteJp2k ("test-lp.jp2", pixRead ("test.png", 0), 42, 0, L_JP2_CODEC, 0, 0)) and ImageMagick 7.1.1-27 (convert test.png -quality 42 test-im.jp2). Both link to OpenJPEG 2.5.0. I tried multiple quality values with similar results. I cannot use other image compression algorithms for unrelated technical reasons.

Sample input image Leptonica output (2831 bytes) ImageMagick output (748 bytes)

DanBloomberg commented 8 months ago

Thank you for finding and reporting this problem. It's fairly clear that we are doing something wrong with the interface for writing jp2 files.

I did a few experiments to test the influence of the nlevels argument. It has a huge effect. If you set it to 3 instead of 5, the file is ten times bigger: 25.6KB. If you set it to 2, it is 73.2KB. I haven't figured out how to modify this (or even if it is possible) in the imagemagick convert program to test the use of nlevels there.

atykhyy commented 8 months ago

I tracked the major difference down to ImageMagick enabling MCT transform (whatever that is) on multi-channel images:

parameters->tcp_mct=channels == 3 ? 1 : 0;

In addition, ImageMagick selects its default nlevels value based on horizontal and vertical dimensions of the input image, and for images larger than 64 pixels in both dimensions it ends up equal to 6 and not 5 as I erroneously stated above. This has a minor effect compared to tcp_mct, however. With both these modifications the JP2 code stream becomes bitwise identical to ImageMagick.

PS: would you accept a PR to use openjpeg's callback interface to implement pix{Read,Write}MemJp2k() directly instead of relying on open_memstream (which falls back to temp file on Windows)?

DanBloomberg commented 8 months ago

Thank you for the additional information! The jp2k interface is now fixed, with commit https://github.com/DanBloomberg/leptonica/commit/ad5206391cdea44441d2b9c1be74111e359f89b Leptonica and ImageMagick convert image outputs are identical and the codestreams only differ in the comment (file creator).

It would be nice to use openjpeg's callback interface for the reason you give, as long as we don't end up with essentially a duplication of the large read stream and write stream functions in jp2kio.c. Note that the I/O files have stub files (e.g., jp2kiostub.c) that must be changed when there is an interface change.

atykhyy commented 8 months ago

The jp2k interface is now fixed, with commit https://github.com/DanBloomberg/leptonica/commit/ad5206391cdea44441d2b9c1be74111e359f89b

Thanks!

Note that the I/O files have stub files (e.g., jp2kiostub.c) that must be changed when there is an interface change.

I wouldn't dream of changing the public interface! (ETA: although I may have to add readResolutionMemJp2k() to it.) I will make a separate PR and close this issue.

DanBloomberg commented 8 months ago

Anton, if you would like to be acknowledged by name in the source code for your contribution, I'll be happy to do it.

Dan

atykhyy commented 8 months ago

Thank you, but I don't feel a contribution this tiny merits that :)

DanBloomberg commented 7 months ago

I disagree, and you can always put in a PR to remove it ;-) Thanks again, Anton.