experiment(render): extract tunables + gamma 1.2, saturation 115%
CI / test (push) Has been cancelled
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:
@@ -28,6 +28,27 @@ final class RenderImageMessageHandler
|
||||
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(
|
||||
private readonly ImageRepository $imageRepo,
|
||||
private readonly RenderedAssetRepository $assetRepo,
|
||||
@@ -116,12 +137,21 @@ final class RenderImageMessageHandler
|
||||
// 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.
|
||||
$imagick->modulateImage(100, 130, 100);
|
||||
// Midtone lift before the saturation + dither passes. Spectra-6's
|
||||
// tonal range is squeezed (effectively ~3 bits per channel after
|
||||
// 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.
|
||||
$imagick->sharpenImage(0, 0.8);
|
||||
$imagick->sharpenImage(0, self::SHARPEN_SIGMA);
|
||||
|
||||
// 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.
|
||||
@@ -161,7 +191,7 @@ final class RenderImageMessageHandler
|
||||
$palImagick->resetIterator();
|
||||
$palStrip = $palImagick->appendImages(false);
|
||||
|
||||
$imagick->remapImage($palStrip, \Imagick::DITHERMETHOD_FLOYDSTEINBERG);
|
||||
$imagick->remapImage($palStrip, self::DITHER_METHOD);
|
||||
$palStrip->destroy();
|
||||
$palImagick->destroy();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user