Skip to content

fix chroma resampling - #114

Open
adam-ce wants to merge 5 commits into
CESNET:masterfrom
AlpineMapsOrgDependencies:feature/chroma-resampling
Open

adam-ce wants to merge 5 commits into
CESNET:masterfrom
AlpineMapsOrgDependencies:feature/chroma-resampling

Conversation

@adam-ce

@adam-ce adam-ce commented Nov 3, 2026

Copy link
Copy Markdown

when we tried to adopt GPU jpeg for our project, I realised that it has slightly worse encoding quality compared to jpeg turbo. so I tasked Claude to look for and fix the issue. codex conducted a review.

I also have some other fixes, but I don't know what your LLM policy is, so I'm starting with this one. I'm happy to iterate on it, you can take it or close it :)

this is the llm summary:

Summary

Improve JPEG colour fidelity by filtering chroma during encoding and decoding.

  • Encoder: replace point sampling with averaging over the subsampling window.
  • Decoder: replace sample replication with triangle interpolation for 4:2:0,
    4:2:2 and 4:4:0 chroma when producing full-resolution output.
  • Enable both filters by default, with independent options to restore the
    previous behaviour.

Evidence

Measured on four real 4096×4096 RGB orthophoto tiles at quality 95, 4:2:0,
using an RTX 4090. RGB PSNR is measured against source pixels.

  • Full GPUJPEG encode/decode improves by 0.64–1.39 dB, averaging 1.01 dB
    across the four tiles—equivalent to 14–27% lower mean squared error.
  • The quality deficit relative to libjpeg-turbo shrinks from 0.67–1.44 dB
    to 0.03–0.06 dB.
  • Encoder-only improvement, using libjpeg-turbo for decoding: 0.17–0.49 dB.
  • Decoder-only improvement, using identical libjpeg-turbo JPEGs: 0.32–0.70 dB.
  • Encoded files are approximately 0.4–0.6% smaller.
  • Added kernel time is approximately 0.01 ms each for preprocessing and
    postprocessing per 4096×4096 image.

CTest passes all three suites. CUDA memcheck reports no errors or leaks,
and targeted chroma-kernel racecheck reports no hazards. Tests cover
sampling layouts, odd dimensions, padded input, default-on behaviour,
and toggling the filters between calls.

The preprocessor stored the chroma of the top-left pixel of each
subsampling window (point sampling), which aliases and shifts chroma by half
a pixel from the centered siting that JFIF (and decoders) assume. Average
the window instead if the input has full chroma resolution and Cb and Cr
have the same sampling.

On four 4096x4096 orthophotos (q95, 4:2:0, decoded by libjpeg-turbo with
fancy upsampling) this improves PSNR by 0.17-0.49 dB, the JPEG is
0.4-0.6 % smaller and the preprocessor kernel takes ~0.01 ms longer.

The previous behavior can be selected by GPUJPEG_ENC_OPT_CHROMA_FILTER_BOOL.
The postprocessor replicated 4:2:0, 4:2:2 and 4:4:0 chroma. For
full-resolution output, interpolate it with the triangle filter that
libjpeg uses by default ("fancy upsampling", same weights and rounding).
Other layouts (dynamic kernels) and subsampled output formats are
unchanged. Unlike libjpeg, no exception is made for chroma width <= 2.

Decoding libjpeg-turbo q95 4:2:0 JPEGs of four 4096x4096 orthophotos, PSNR
improves by 0.32-0.70 dB (now equal to libjpeg-turbo), the postprocessor
kernel takes ~0.01 ms longer.

The previous behavior can be selected by GPUJPEG_DEC_OPT_CHROMA_FILTER_BOOL.
Compares the encoder's chroma planes and the decoder's full-resolution
output with a CPU reference for 4:2:0, 4:2:2, 4:4:0 and 4:1:1 with the
filters on and off, and the option change without reinitialization.
Check also the default (filter on) and the option change on the same
coders for each layout (instead of only 4:2:0); express the 4:2:0
reference with the plain 9/3/3/1 weights.
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

Successfully merging this pull request may close these issues.

1 participant