Skip to content
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

JPEG decoding yields discolored image, even for basic 8-bit YUV 444 JPEG with no (i.e. GIMP default sRGB) color profile. #2411

Open
gergo-salyi opened this issue Jan 30, 2025 · 6 comments

Comments

@gergo-salyi
Copy link

Tested on Linux with x86_64 CPU (i5-1035G4) 10th gen Intel having AVX2, etc...

# Convert with ImageMagick for reference
magick start.jpg good.png
# Cargo.toml
[dependencies]
image = "0.25.5"
// main.rs
fn main() {
    image::open("start.jpg").unwrap().save("bad.png").unwrap();
}

start.jpg:

Image

good.png:

Image

bad.png:

Image

Lower half of bad.png is visibly more green then start.jpg or good.png. E.g. pixel at index (150, 200) became #131910 instead of #151811 . I expect the the JPEG decoder to yield identical result to ImageMagick.

Not sure about the cause, but I suspect a YCbCr -> RGB conversion problem. What zune-jpeg does is non-compliant:

//! 1. The YCbCr to RGB use integer approximations and not the floating point equivalent.
//! That means we may be +- 2 of pixels generated by libjpeg-turbo jpeg decoding
//! (also libjpeg uses routines like `Y  =  0.29900 * R + 0.33700 * G + 0.11400 * B + 0.25000 * G`)

https://github.com/etemesi254/zune-image/blob/c9f333dd3f725e5fd044e0e6af37f2807485d35e/crates/zune-jpeg/src/color_convert/avx.rs#L16-L18

by their design:

## Non-Goals

- Bit identical results with libjpeg/libjpeg-turbo will never be an aim of this library.
  Jpeg is a lossy format with very few parts specified by the standard
  (i.e it doesn't give a reference upsampling and color conversion algorithm)

https://github.com/etemesi254/zune-image/blob/c9f333dd3f725e5fd044e0e6af37f2807485d35e/crates/zune-jpeg/README.md?plain=1#L46-L50

and known to have caused problems elsewhere in the past: https://en.wikipedia.org/wiki/YCbCr#Approximate_8-bit_matrices_for_BT.601

Being off by +-1 in a 0-255 ranged 8-bit color intensity is a visually significant error, basically JPEG images which are supposed to be visually lossless (encoded with quality 90 in the above example) are completely violated in their usage intent.

@fintelia
Copy link
Contributor

The sections you quote specifically indicate that there isn't any definition of how close the results have to be for something to be a compliant JPEG decoder.

But I actually think this could be unintentional. The PSNR difference between the two outputs is 46.4, which usually would be visually indistinguishable. That's because even if some pixels are different by 1 or 2, other nearby pixels can be off by a similar amount in the opposite direction. But averaging all the pixel values in the two versions shows that the output from zune-jpeg is slightly more red and blue, and a bit less(?) green. In all three cases the difference is about 0.87/255 which is also suspicious.

I'd suggest opening an issue on zune-jpeg to check with them

@gergo-salyi
Copy link
Author

gergo-salyi commented Jan 30, 2025

The sections you quote specifically indicate that there isn't any definition of how close the results have to be for something to be a compliant JPEG decoder.

There are specs published, e.g. see this one: https://www.w3.org/Graphics/JPEG/jfif3.pdf

Quote from page 3:

RGB can be computed directly from YCbCr (256 levels) as follows:
R = Y + 1.402 (Cr-128)
G = Y - 0.34414 (Cb-128) - 0.71414 (Cr-128)
B = Y + 1.772 (Cb-128)

These formulas don't have +-1/255 error.

Again I'm not 100% sure that YCbCr -> RGB conversion is the cause, but I have this suspicion seeing the sloppy integer math.

I'd suggest opening an issue on zune-jpeg to check with them

Ok, gonna do that and link it here. EDIT: etemesi254/zune-image#249

@fintelia
Copy link
Contributor

sloppy

Please be sure to adhere to our code of conduct

@awxkee
Copy link
Contributor

awxkee commented Jan 30, 2025

I also saw slightly greener images even after #2387.
I believe YUV conversion suffers with “greener effect” only on low precision encoding. On low precision decoding images usually are more bright/dark because luma calculation is floating and it is more important than chroma. Thus, I believe there is more likely issue with dct coefficients somewhere. However, the coefficients used for YUV->RGB is very low precise, even libyuv that is using arguable 6 bit coefficients often produce strange outputs, and there is 5 bits of precision in zune-image.

@gergo-salyi
Copy link
Author

I did a short test with zune-jpeg patched to f32 precision YCbCr -> RGB: https://github.com/gergo-salyi/zune-image/tree/exact_ycbcr_to_rgb).

Which for the above test image gives better.png:

Image

Which is annoyingly somewhere in between the result of ImageMagick and current zune-jpeg. I would say perceptually closer to ImageMagick and further from current zune-jpeg to my judgement. (Try putting it on different browser tabs and switching back and forth.)

I don't really know what to expect as a solution. I have been using the image crate as a dep for my wallpaper setter program, so I'm biased wanting no visually significant difference from GIMP and Linux image viewers using libjpeg-turbo.

@kornelski
Copy link
Contributor

The libjpeg-turbo library has been picked as the reference JPEG implementation, so it's right by definition.

OTOH libjpeg(-turbo) by default has 8-bit precision and performs rounding after IDCT before YUV->RGB conversion, so its decoding isn't the most precise, and it's also possible to make a decoder that differs from it by being more precise and less lossy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants