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 (basic 8-bit YUV 444 JPEG with no / GIMP default sRGB color profile). #249

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

Comments

@gergo-salyi
Copy link

I'm relaying this from my issue at the image crate using zune-jpg for JPEG decoding: image-rs/image#2411

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. zune-jpeg does this:

//! 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`)

//! 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`)

This integer approximation is known to have caused problems elsewhere in the past, see: https://en.wikipedia.org/wiki/YCbCr#Approximate_8-bit_matrices_for_BT.601

The JPEG File Interchange Format Version 1.02 spec defines on 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 of the discoloration, but I have this suspicion seeing the sloppy integer math.

Being off by +-1 in a 0-255 ranged 8-bit color intensity (in a non-random / non-dithered way) 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.

Please also see image-rs/image#2411 , the maintainer there posted some statistical measurements about the above images.

@etemesi254
Copy link
Owner

Sorry, for delay in reply.

It is probably the cause, will rewrite it to use integer intrinsics

@etemesi254
Copy link
Owner

If you or anyone has some time tho, you can use the impl from stb for inspiration @https://github.com/nothings/stb/blob/5c205738c191bcb0abc65c4febfa9bd25ff35234/stb_image.h#L3690-L3746

@awxkee
Copy link
Contributor

awxkee commented Feb 26, 2025

He already tried transformation using floating point see

I wrote few hundreds of Yuv conversions. I may do PR with fast high precision transform(probably exact with libjpeg-turbo), better than yours with low precision, or mixed precision as in stb, but probably not the exact same as stb.

However, YUV transforms even in low precision usually do not produce this effect, there is likely issue with DCT coefficients or with something else.

@etemesi254
Copy link
Owner

Nice if you have time that would be appreciated.

libjpeg-turbo matching is better than stb.

@awxkee
Copy link
Contributor

awxkee commented Mar 1, 2025

So after we've made YUV conversion is almost loseless brightness gain decoded correctly and images became a way better.

SSIM:
dssim ./assets/bench.jpg v_old.png
0.00045124 v_old.png

dssim ./assets/bench.jpg v_new.png
0.00036883 v_new.png

libjpeg-turbo
dssim ./assets/bench.jpg vturb.png
0.00001187 vturb.png

The "greenish effect" became less noticeable because the image brightness is at least not changing. However, it is still present. Here are the images I tested.

Archive.zip

@etemesi254
Copy link
Owner

Any other reasons you suspect that the dsism is that high compared to libjpeg-turbo?

@awxkee
Copy link
Contributor

awxkee commented Mar 1, 2025

YUV conversion in 14 bit is almost loseless, so the issue somewhere in other place.

Let's check the statistics.
You could see on statistics that blue channel have significant deviation.
That may mean 2 things:

  • I computed incorrect B coefficients for YUV conversion.
  • Cb(U) component that delivered from DCT already have a mistake

identify -verbose /Users/radzivon/RustroverProjects/moxcms/v_new.png

Channel statistics:
Pixels: 2658007
Red:
min: 0 (0)
max: 255 (1)
mean: 148.481 (0.582279)
median: 163 (0.639216)
standard deviation: 78.4231 (0.307541)
kurtosis: -0.729625
skewness: -0.662664
entropy: 0.910364
Green:
min: 0 (0)
max: 255 (1)
mean: 148.72 (0.583217)
median: 144 (0.564706)
standard deviation: 67.0394 (0.262899)
kurtosis: -0.936945
skewness: -0.189044
entropy: 0.942256
Blue:
min: 0 (0)
max: 255 (1)
mean: 154.854 (0.607269)
median: 152 (0.596078)
standard deviation: 68.9375 (0.270343)
kurtosis: -0.849295
skewness: -0.266997
entropy: 0.933972

identify -verbose /Users/radzivon/RustroverProjects/moxcms/assets/bench.jpg

Channel statistics:
Pixels: 2658007
Red:
min: 0 (0)
max: 255 (1)
mean: 148.95 (0.584118)
median: 163 (0.639216)
standard deviation: 78.7979 (0.309011)
kurtosis: -0.737554
skewness: -0.653646
entropy: 0.912861
Green:
min: 0 (0)
max: 255 (1)
mean: 148.604 (0.582759)
median: 144 (0.564706)
standard deviation: 67.0147 (0.262803)
kurtosis: -0.938889
skewness: -0.185771
entropy: 0.942668
Blue:
min: 0 (0)
max: 255 (1)
mean: 155.545 (0.609979)
median: 153 (0.6)
standard deviation: 69.4637 (0.272407)
kurtosis: -0.854046
skewness: -0.25791
entropy: 0.930841

@awxkee
Copy link
Contributor

awxkee commented Mar 1, 2025

I'd bet that Cb coeffiecient delivered from DCT is incorrect. It also have contribution to Green channel that mean it delivers error in 2 channels at the time.

I'll recheck my coefficients, but I think they're correct.

@awxkee
Copy link
Contributor

awxkee commented Mar 1, 2025

Also there is might also be another option, it may explain why coefficients in libjpeg-turbo looks unnatural and it is not clear how to reproduce them: someone might adjusted Yuv coefficients by hands to compensate errors from DCT, by simplex method, brute forcing or something.

I’m not sure this is real. I’ll try their coefficients

@etemesi254
Copy link
Owner

Another problem might be problems with the upsampling step.

I choose a different algorithm for sampling

@awxkee
Copy link
Contributor

awxkee commented Mar 1, 2025

So, I tried theirs YUV coefficients:

Ours:
dssim ./assets/bench.jpg v_new.png
0.00036883 v_new.png

libjpeg-turbo:
dssim ./assets/bench.jpg v_tb_coeffs.png
0.00037672 v_tb_coeffs.png

That means there is no any magic in libjpeg-turbo coefficients, perhaps I just used more precise software.\

Here is original coefficients that I made with MPFR 150 bits of precision.

Inverse CbCrInverseTransform { y_coef: 1.0000000000000000000000000000000000000000000000, cr_coef: 1.4020000100135803222656250000000000000000000000, cb_coef: 1.7719999998807907104492187500000000000000000000, g_coeff_1: 7.1413627332466104295757259219925233487803202950e-1, g_coeff_2: 3.4413628345745065477397884219925233487803202915e-1 }
Inverse CbCrInverseTransform { y_coef: 1.0, cr_coef: 1.402, cb_coef: 1.772, g_coeff_1: 0.7141363, g_coeff_2: 0.3441363 }
Inverse Numeric CbCrInverseTransform { y_coef: 16384, cr_coef: 22970, cb_coef: 29032, g_coeff_1: 11700, g_coeff_2: 5638 };

We could change it if any doubts, recompute with Wolfram or any other arbitrary precision tools. But atm I think they are correct: https://github.com/libjpeg-turbo/libjpeg-turbo/blob/adbb328159b5558e846690c49f9458deccbb0f43/src/jdcolor.c#L54

@gergo-salyi
Copy link
Author

gergo-salyi commented Mar 1, 2025

etemesi254 thank you for looking into this. I never coded DCT so I couldn't/cannot comment on the DCT coeff part without learning it.

Another problem might be problems with the upsampling step.

My image starting this issue was YUV444 so there should be no upsampling involved as far as I understand the decoding.

@etemesi254
Copy link
Owner

etemesi254 commented Mar 1, 2025 via email

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

3 participants