Skip to content

Add ChromaSubsamplingMethod option for box-average downsampling#33

Open
tylerneylon wants to merge 2 commits into
vstroebel:mainfrom
tylerneylon:box-average-chroma-subsampling
Open

Add ChromaSubsamplingMethod option for box-average downsampling#33
tylerneylon wants to merge 2 commits into
vstroebel:mainfrom
tylerneylon:box-average-chroma-subsampling

Conversation

@tylerneylon
Copy link
Copy Markdown

Adds an opt-in ChromaSubsamplingMethod::Average that box-averages each chroma block when subsampling, matching libjpeg / libjpeg-turbo's default downsampler. The existing point-sampling behavior stays the default (Nearest), so there is no change for current users.

Motivation

When chroma is subsampled (e.g. 4:2:0), each output Cb/Cr sample represents a 2×2 block of source pixels. Today jpeg-encoder takes the top-left pixel of each block and discards the other three. libjpeg instead averages all four (see jcsample.c::h2v2_downsample).

On natural photographs the difference is negligible. On images with sharp colored edges — UI screenshots, rendered text, anti-aliased line art — point-sampling can stamp an entire 2×2 block with one outlier pixel's chroma, producing visible color fringing that box-averaging avoids.

On a mixed set of ~1,500 images (photographs, charts, UI screenshots, rendered text) encoded at quality 75, mean per-pixel-channel |Δ| against libjpeg-turbo's output drops from 0.26 with Nearest to 0.003 with Average.

What's in this change

  • ChromaSubsamplingMethod { Nearest, Average } enum and Encoder::set_chroma_subsampling_method.
  • A get_block_averaged path alongside the existing get_block, wired into both the interleaved and progressive encode paths. It's generic over stride, so 4:2:2, 4:1:1, etc. get the same treatment.
  • The rounding bias alternates per output column (1,2,1,2,... for 4:2:0), matching libjpeg's dither so rounding error averages to zero across each row rather than accumulating.
  • Four new tests: two unit tests on the averaging arithmetic and two encode→decode round-trips (interleaved and progressive).

Compatibility

Default remains Nearest. No change to output for any existing caller.

Related

Tangentially related to image-rs/image#2202 (JPEG encode quality), since image now uses this crate for JPEG encoding.

Adds an opt-in ChromaSubsamplingMethod::Average that box-averages chroma
samples when subsampling, matching libjpeg/libjpeg-turbo's default
h2v2_downsample behavior — including its per-column dither of the rounding
bias so that rounding error averages to zero across each row.

The existing point-sampling behavior remains the default (Nearest), so this
is fully backward-compatible.

Box-averaging produces noticeably better quality on images with sharp
colored edges (screenshots, rendered/antialiased text), where point-sampling
can stamp an entire 2x2 block with one outlier pixel's chroma.
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