fix: replace contrastStretchImage with normalizeImage for auto-levels
CI / test (push) Has been cancelled

contrastStretchImage's parameters are quantum-range intensity thresholds
in IM7, not pixel counts as the original code assumed. Empirically any
arg >= 1 collapsed low-tonal-range photos to pure white — verified by
probing image 16's mid-photo pixel which goes from (80,69,59) to
(255,255,255) with cs(100,100), cs(1382,1382), or cs(3840,3840) alike.

normalizeImage() uses the image's actual histogram percentiles (default
2% black/white clip) and produces gentle, correct stretching: same
pixel goes (80,69,59) to (89,72,59) — a small contrast bump that
preserves the photo's tonal information instead of obliterating it.

Existing on-disk bins were rendered with the broken stretch; running
app:rerender-assets after this deploy regenerates them with the new
pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 16:43:44 -04:00
parent c633601d90
commit e0bad975ec
@@ -107,12 +107,14 @@ final class RenderImageMessageHandler
// Per-pixel adjustments run on the PHOTO before the white canvas is
// composited under it — otherwise the bars dominate the histogram and
// the contrast stretch over-clips the photo content to white.
// would skew any auto-leveling toward white.
// Auto-levels: stretch the tonal range, clipping 1% at each end.
// Fixes underexposed/dark photos so the full palette range is used.
$photoPixels = $imagick->getImageWidth() * $imagick->getImageHeight();
$imagick->contrastStretchImage((int) ($photoPixels * 0.01), (int) ($photoPixels * 0.01));
// Auto-levels via normalizeImage (default 2%/2% clip). Empirically
// contrastStretchImage's args are quantum-range intensity thresholds,
// not pixel counts, and even small ints (>= ~1) collapse low-tonal-
// range photos to pure white. normalizeImage uses the image's actual
// histogram percentiles and produces gentle, correct stretching.
$imagick->normalizeImage();
// Boost saturation 130%. Dark desaturated photos otherwise map almost
// entirely to BLACK with scattered noise dots from error diffusion.