experiment(render): extract tunables + gamma 1.2, saturation 115%
CI / test (push) Has been cancelled

Make the render-pipeline knobs Matt and I are about to iterate on
visible as class constants on RenderImageMessageHandler — single source
of truth, easy diff per change.

First experiment (vs baseline git tag render-baseline-2026-05-14):
  SATURATION_PCT  130 → 115   (less risk of ruddy faces / synthetic skies)
  GAMMA           1.0 → 1.2   (gentle midtone lift; faces + shadows
                                climb out of the BLACK cluster after dither)

Sharpening (0.8) and Floyd-Steinberg dithering unchanged this round.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 13:22:06 -04:00
parent 82a42011d8
commit 1ebc9b615d
@@ -28,6 +28,27 @@ final class RenderImageMessageHandler
0x6 => [16, 160, 64 ], // GREEN 0x6 => [16, 160, 64 ], // GREEN
]; ];
// ── Render tunables ──────────────────────────────────────────────────────
// Tweak these to change how photos look on the panel; each value has the
// baseline it shipped with as a comment so we can rollback per-knob.
// git tag render-baseline-2026-05-14 captures the full known-working set.
/** modulateImage(brightness=100, SATURATION, hue=100). 100 = no change.
* Baseline: 130 (+30% saturation, prevents desaturated photos collapsing
* to dither noise). Experiment #1: 115 (+15%, less risk of garish faces). */
private const SATURATION_PCT = 115;
/** gammaImage(GAMMA). 1.0 = no change; >1 lifts midtones, <1 crushes them.
* Baseline: 1.0 (no gamma pass). Experiment #1: 1.2 (gentle midtone lift —
* faces and shadows climb out of the BLACK cluster). */
private const GAMMA = 1.2;
/** sharpenImage(radius=0, SHARPEN_SIGMA). Baseline: 0.8 (light). */
private const SHARPEN_SIGMA = 0.8;
/** Imagick dither method. Baseline: DITHERMETHOD_FLOYDSTEINBERG. */
private const DITHER_METHOD = \Imagick::DITHERMETHOD_FLOYDSTEINBERG;
public function __construct( public function __construct(
private readonly ImageRepository $imageRepo, private readonly ImageRepository $imageRepo,
private readonly RenderedAssetRepository $assetRepo, private readonly RenderedAssetRepository $assetRepo,
@@ -116,12 +137,21 @@ final class RenderImageMessageHandler
// histogram percentiles and produces gentle, correct stretching. // histogram percentiles and produces gentle, correct stretching.
$imagick->normalizeImage(); $imagick->normalizeImage();
// Boost saturation 130%. Dark desaturated photos otherwise map almost // Midtone lift before the saturation + dither passes. Spectra-6's
// entirely to BLACK with scattered noise dots from error diffusion. // tonal range is squeezed (effectively ~3 bits per channel after
$imagick->modulateImage(100, 130, 100); // dither); without a gamma push, faces and shadow detail get
// crushed into BLACK and the photo reads as too dark on the panel.
if (self::GAMMA !== 1.0) {
$imagick->gammaImage(self::GAMMA);
}
// Saturation boost. Dark desaturated photos otherwise map almost
// entirely to BLACK with scattered noise dots from error diffusion;
// too much saturation and skin tones go ruddy / sky goes synthetic.
$imagick->modulateImage(100, self::SATURATION_PCT, 100);
// Light sharpen so edges survive the dithering scatter. // Light sharpen so edges survive the dithering scatter.
$imagick->sharpenImage(0, 0.8); $imagick->sharpenImage(0, self::SHARPEN_SIGMA);
// Now composite onto a white canvas of exact target dims so that any // Now composite onto a white canvas of exact target dims so that any
// aspect mismatch shows up as letterbox bars, not as a sliced crop. // aspect mismatch shows up as letterbox bars, not as a sliced crop.
@@ -161,7 +191,7 @@ final class RenderImageMessageHandler
$palImagick->resetIterator(); $palImagick->resetIterator();
$palStrip = $palImagick->appendImages(false); $palStrip = $palImagick->appendImages(false);
$imagick->remapImage($palStrip, \Imagick::DITHERMETHOD_FLOYDSTEINBERG); $imagick->remapImage($palStrip, self::DITHER_METHOD);
$palStrip->destroy(); $palStrip->destroy();
$palImagick->destroy(); $palImagick->destroy();