experiment(render): revert sat/gamma/blur to baseline; blue-channel ×0.95
CI / test (push) Has been cancelled
CI / test (push) Has been cancelled
Experiment #3 (sat 115, gamma 1.2, blur 0.6) was a net loss — greens desaturated, no help on sky→face blue bleed. Reverting those three to baseline (130, 1.0, 0.0). New experiment #4: multiply the source's blue channel by 0.95 before dither. Real sky stays well above the BLUE-vs-WHITE boundary, but borderline-bluish skin (sky cast on outdoor faces) drops below it and maps to YELLOW / WHITE / RED instead of feeding the dither's BLUE attractor. Adds public BLUE_CHANNEL_MUL constant; render-compare's baseline struct gets blue_mul=1.0 so half-A is still frozen at the original. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,7 @@ final class RenderCompareCommand extends Command
|
||||
'gamma' => 1.0,
|
||||
'sharpen' => 0.8,
|
||||
'blur' => 0.0,
|
||||
'blue_mul' => 1.0,
|
||||
'dither' => \Imagick::DITHERMETHOD_FLOYDSTEINBERG,
|
||||
];
|
||||
|
||||
@@ -94,6 +95,7 @@ final class RenderCompareCommand extends Command
|
||||
'gamma' => RenderImageMessageHandler::GAMMA,
|
||||
'sharpen' => RenderImageMessageHandler::SHARPEN_SIGMA,
|
||||
'blur' => RenderImageMessageHandler::BLUR_SIGMA,
|
||||
'blue_mul' => RenderImageMessageHandler::BLUE_CHANNEL_MUL,
|
||||
'dither' => RenderImageMessageHandler::DITHER_METHOD,
|
||||
];
|
||||
|
||||
@@ -140,8 +142,8 @@ final class RenderCompareCommand extends Command
|
||||
? 'FloydSteinberg'
|
||||
: ($p['dither'] === \Imagick::DITHERMETHOD_RIEMERSMA ? 'Riemersma' : 'No');
|
||||
return sprintf(
|
||||
'sat=%d gamma=%.2f sharpen=%.2f blur=%.2f dither=%s',
|
||||
$p['saturation'], $p['gamma'], $p['sharpen'], $p['blur'], $dither,
|
||||
'sat=%d gamma=%.2f sharpen=%.2f blur=%.2f blue_mul=%.2f dither=%s',
|
||||
$p['saturation'], $p['gamma'], $p['sharpen'], $p['blur'], $p['blue_mul'], $dither,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -164,6 +166,9 @@ final class RenderCompareCommand extends Command
|
||||
if ($params['blur'] > 0) {
|
||||
$im->blurImage(0, $params['blur']);
|
||||
}
|
||||
if ($params['blue_mul'] !== 1.0) {
|
||||
$im->evaluateImage(\Imagick::EVALUATE_MULTIPLY, $params['blue_mul'], \Imagick::CHANNEL_BLUE);
|
||||
}
|
||||
|
||||
if ($im->getImageWidth() !== self::W || $im->getImageHeight() !== self::FULL_H) {
|
||||
$canvas = new \Imagick();
|
||||
|
||||
@@ -37,29 +37,28 @@ final class RenderImageMessageHandler
|
||||
|
||||
/** modulateImage(brightness=100, SATURATION, hue=100). 100 = no change.
|
||||
* Baseline: 130 (+30%, prevents desaturated photos collapsing to dither
|
||||
* noise). Experiment #1: 115 (less risk of garish faces). */
|
||||
public const SATURATION_PCT = 115;
|
||||
* noise; greens stay vibrant). */
|
||||
public const SATURATION_PCT = 130;
|
||||
|
||||
/** 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). */
|
||||
public const GAMMA = 1.2;
|
||||
/** gammaImage(GAMMA). 1.0 = no change; >1 lifts midtones, <1 crushes. */
|
||||
public const GAMMA = 1.0;
|
||||
|
||||
/** sharpenImage(radius=0, SHARPEN_SIGMA). Baseline: 0.8 (light). */
|
||||
public const SHARPEN_SIGMA = 0.8;
|
||||
|
||||
/** blurImage(radius=0, BLUR_SIGMA) applied *before* dither. 0 = no blur.
|
||||
* Baseline: 0.0. Experiment #3: 0.6 — softens sharp blue-sky → skin
|
||||
* transitions so Floyd-Steinberg has less per-pixel error to carry
|
||||
* forward, reducing the blue "bleed" into faces below the sky. */
|
||||
public const BLUR_SIGMA = 0.6;
|
||||
/** blurImage(radius=0, BLUR_SIGMA) applied *before* dither. 0 = no blur. */
|
||||
public const BLUR_SIGMA = 0.0;
|
||||
|
||||
/** Imagick dither method.
|
||||
* FLOYDSTEINBERG (baseline) — error diffuses down-right in row order,
|
||||
* risks blue bleed into faces below sky.
|
||||
* RIEMERSMA — Hilbert-curve scan, error stays local but produces
|
||||
* visible "ink-spill" streaks in low-contrast regions.
|
||||
* Rejected experiment #2 — much worse than the bleed.
|
||||
* Sticking with Floyd-Steinberg, attacking the bleed via blur. */
|
||||
/** evaluateImage(MULTIPLY, BLUE_CHANNEL_MUL, CHANNEL_BLUE) — scales the
|
||||
* source's blue channel before dither. 1.0 = no change.
|
||||
* Experiment #4: 0.95 (knock 5% off blue). Real sky stays well above
|
||||
* the BLUE-vs-WHITE boundary, but borderline-bluish skin pixels (sky
|
||||
* cast on faces in outdoor photos) drop below it and map to YELLOW /
|
||||
* WHITE / RED instead of contaminating the face with BLUE dither. */
|
||||
public const BLUE_CHANNEL_MUL = 0.95;
|
||||
|
||||
/** Imagick dither method. Sticking with Floyd-Steinberg — Riemersma
|
||||
* produced visible Hilbert-curve "ink-spill" streaks in skin/sky. */
|
||||
public const DITHER_METHOD = \Imagick::DITHERMETHOD_FLOYDSTEINBERG;
|
||||
|
||||
public function __construct(
|
||||
@@ -166,15 +165,20 @@ final class RenderImageMessageHandler
|
||||
// Light sharpen so edges survive the dithering scatter.
|
||||
$imagick->sharpenImage(0, self::SHARPEN_SIGMA);
|
||||
|
||||
// Optional pre-dither blur to soften sharp colour transitions —
|
||||
// Floyd-Steinberg propagates each pixel's error to its neighbours,
|
||||
// and a hard sky→skin boundary leaks blue into faces below. Even
|
||||
// a sub-1px Gaussian smooths the boundary enough that the carried
|
||||
// error is small. Sharpening above keeps edge crispness.
|
||||
// Optional pre-dither blur to soften sharp colour transitions.
|
||||
if (self::BLUR_SIGMA > 0) {
|
||||
$imagick->blurImage(0, self::BLUR_SIGMA);
|
||||
}
|
||||
|
||||
// Optional blue-channel knock-down. Spectra-6 has only 6 inks; skin
|
||||
// tones in outdoor photos pick up a sky cast that the dither would
|
||||
// otherwise map to BLUE. Scaling the blue channel down a few % pulls
|
||||
// borderline-bluish-skin pixels off the BLUE attractor while leaving
|
||||
// real sky safely above it.
|
||||
if (self::BLUE_CHANNEL_MUL !== 1.0) {
|
||||
$imagick->evaluateImage(\Imagick::EVALUATE_MULTIPLY, self::BLUE_CHANNEL_MUL, \Imagick::CHANNEL_BLUE);
|
||||
}
|
||||
|
||||
// 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.
|
||||
if ($imagick->getImageWidth() !== $width || $imagick->getImageHeight() !== $height) {
|
||||
|
||||
Reference in New Issue
Block a user