Skip to content

Qwen3VL: apply the sRGB tone curve in the image preprocess path#411

Merged
davidkoski merged 1 commit into
ml-explore:mainfrom
spokvulcan:fix/qwen3vl-image-srgb-tone-curve
Jul 13, 2026
Merged

Qwen3VL: apply the sRGB tone curve in the image preprocess path#411
davidkoski merged 1 commit into
ml-explore:mainfrom
spokvulcan:fix/qwen3vl-image-srgb-tone-curve

Conversation

@spokvulcan

Copy link
Copy Markdown
Contributor

Fixes #410.

The Qwen3VL image path hands the model CoreImage's linear-light working-space values instead of the gamma-encoded sRGB values the HF reference preprocesses: CoreImage filters run in linear light and MediaProcessing.asMLXArray renders with a nil colorSpace. The sibling Qwen25VL/Qwen2VL image paths and this file's own video path all apply the linear→sRGB tone-curve step; the Qwen3VL port dropped it in the image path.

Bright content survives the error; dark content does not — near-black text (luminance 23/255 on a 6/255 background) reaches the ViT with ~12× less contrast than reference and is unreadable. Measured on the incident screenshot (text-vs-background signal after mean/std-0.5 normalization): 0.011 without the step, 0.126 with it, HF reference 0.133. Before/after screenshots in #410.

The change is the one-line inSRGBToneCurveSpace wrap at the resample site, plus a regression test (Qwen3VLToneCurveTests) that runs a synthetic faint dark-theme fixture through preprocess and pins the model-side extrema to the gamma-encoded reference values — the linear-space failure mode lands far outside the tolerances.

Note: textually adjacent to #398 (both touch the resample site); whichever lands second is a trivial rebase.

CoreImage filters run in a linear-light working space and asMLXArray
renders with a nil colorSpace, so the image path fed the model
linearized values instead of the gamma-encoded sRGB bytes the HF
reference preprocesses. Bright content survives that error; dark
content does not — near-black text (luminance 23/255 on a 6/255
background) reached the ViT with ~12x less contrast than reference
and was unreadable. The sibling Qwen25VL/Qwen2VL image paths and this
file's own video path already apply the tone-curve step; the port
dropped it in the image path.

Measured on the incident screenshot: faint-band signal vs background
was 0.011 without the step, 0.126 with it (reference: 0.133).

Adds a regression test pinning the model-side extrema of a faint
dark-theme fixture to the gamma-encoded reference values.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
let resampled = processed.map {
MediaProcessing.resampleBicubic(
MediaProcessing.inSRGBToneCurveSpace($0), to: targetSize)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if other models have the same issue? From a quick scan it looks like Idefics3, MistralV3, and Paligemma might.

Qwen25VL, Gemma3 and Gemma4 all do the resample in gamma space (incorrect, but probably matches the python code) so they won't have this issue.

@davidkoski davidkoski left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change looks good. See comment on other models. Thank you.

@davidkoski davidkoski merged commit 2118561 into ml-explore:main Jul 13, 2026
2 checks passed
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.

Qwen3VL image preprocessing skips the sRGB tone curve — model receives linear-light values, dark content becomes unreadable

2 participants