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,
|
'gamma' => 1.0,
|
||||||
'sharpen' => 0.8,
|
'sharpen' => 0.8,
|
||||||
'blur' => 0.0,
|
'blur' => 0.0,
|
||||||
|
'blue_mul' => 1.0,
|
||||||
'dither' => \Imagick::DITHERMETHOD_FLOYDSTEINBERG,
|
'dither' => \Imagick::DITHERMETHOD_FLOYDSTEINBERG,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -94,6 +95,7 @@ final class RenderCompareCommand extends Command
|
|||||||
'gamma' => RenderImageMessageHandler::GAMMA,
|
'gamma' => RenderImageMessageHandler::GAMMA,
|
||||||
'sharpen' => RenderImageMessageHandler::SHARPEN_SIGMA,
|
'sharpen' => RenderImageMessageHandler::SHARPEN_SIGMA,
|
||||||
'blur' => RenderImageMessageHandler::BLUR_SIGMA,
|
'blur' => RenderImageMessageHandler::BLUR_SIGMA,
|
||||||
|
'blue_mul' => RenderImageMessageHandler::BLUE_CHANNEL_MUL,
|
||||||
'dither' => RenderImageMessageHandler::DITHER_METHOD,
|
'dither' => RenderImageMessageHandler::DITHER_METHOD,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -140,8 +142,8 @@ final class RenderCompareCommand extends Command
|
|||||||
? 'FloydSteinberg'
|
? 'FloydSteinberg'
|
||||||
: ($p['dither'] === \Imagick::DITHERMETHOD_RIEMERSMA ? 'Riemersma' : 'No');
|
: ($p['dither'] === \Imagick::DITHERMETHOD_RIEMERSMA ? 'Riemersma' : 'No');
|
||||||
return sprintf(
|
return sprintf(
|
||||||
'sat=%d gamma=%.2f sharpen=%.2f blur=%.2f dither=%s',
|
'sat=%d gamma=%.2f sharpen=%.2f blur=%.2f blue_mul=%.2f dither=%s',
|
||||||
$p['saturation'], $p['gamma'], $p['sharpen'], $p['blur'], $dither,
|
$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) {
|
if ($params['blur'] > 0) {
|
||||||
$im->blurImage(0, $params['blur']);
|
$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) {
|
if ($im->getImageWidth() !== self::W || $im->getImageHeight() !== self::FULL_H) {
|
||||||
$canvas = new \Imagick();
|
$canvas = new \Imagick();
|
||||||
|
|||||||
@@ -37,29 +37,28 @@ final class RenderImageMessageHandler
|
|||||||
|
|
||||||
/** modulateImage(brightness=100, SATURATION, hue=100). 100 = no change.
|
/** modulateImage(brightness=100, SATURATION, hue=100). 100 = no change.
|
||||||
* Baseline: 130 (+30%, prevents desaturated photos collapsing to dither
|
* Baseline: 130 (+30%, prevents desaturated photos collapsing to dither
|
||||||
* noise). Experiment #1: 115 (less risk of garish faces). */
|
* noise; greens stay vibrant). */
|
||||||
public const SATURATION_PCT = 115;
|
public const SATURATION_PCT = 130;
|
||||||
|
|
||||||
/** gammaImage(GAMMA). 1.0 = no change; >1 lifts midtones, <1 crushes them.
|
/** gammaImage(GAMMA). 1.0 = no change; >1 lifts midtones, <1 crushes. */
|
||||||
* Baseline: 1.0 (no gamma pass). Experiment #1: 1.2 (gentle midtone lift). */
|
public const GAMMA = 1.0;
|
||||||
public const GAMMA = 1.2;
|
|
||||||
|
|
||||||
/** sharpenImage(radius=0, SHARPEN_SIGMA). Baseline: 0.8 (light). */
|
/** sharpenImage(radius=0, SHARPEN_SIGMA). Baseline: 0.8 (light). */
|
||||||
public const SHARPEN_SIGMA = 0.8;
|
public const SHARPEN_SIGMA = 0.8;
|
||||||
|
|
||||||
/** blurImage(radius=0, BLUR_SIGMA) applied *before* dither. 0 = no blur.
|
/** blurImage(radius=0, BLUR_SIGMA) applied *before* dither. 0 = no blur. */
|
||||||
* Baseline: 0.0. Experiment #3: 0.6 — softens sharp blue-sky → skin
|
public const BLUR_SIGMA = 0.0;
|
||||||
* 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;
|
|
||||||
|
|
||||||
/** Imagick dither method.
|
/** evaluateImage(MULTIPLY, BLUE_CHANNEL_MUL, CHANNEL_BLUE) — scales the
|
||||||
* FLOYDSTEINBERG (baseline) — error diffuses down-right in row order,
|
* source's blue channel before dither. 1.0 = no change.
|
||||||
* risks blue bleed into faces below sky.
|
* Experiment #4: 0.95 (knock 5% off blue). Real sky stays well above
|
||||||
* RIEMERSMA — Hilbert-curve scan, error stays local but produces
|
* the BLUE-vs-WHITE boundary, but borderline-bluish skin pixels (sky
|
||||||
* visible "ink-spill" streaks in low-contrast regions.
|
* cast on faces in outdoor photos) drop below it and map to YELLOW /
|
||||||
* Rejected experiment #2 — much worse than the bleed.
|
* WHITE / RED instead of contaminating the face with BLUE dither. */
|
||||||
* Sticking with Floyd-Steinberg, attacking the bleed via blur. */
|
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 const DITHER_METHOD = \Imagick::DITHERMETHOD_FLOYDSTEINBERG;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -166,15 +165,20 @@ final class RenderImageMessageHandler
|
|||||||
// Light sharpen so edges survive the dithering scatter.
|
// Light sharpen so edges survive the dithering scatter.
|
||||||
$imagick->sharpenImage(0, self::SHARPEN_SIGMA);
|
$imagick->sharpenImage(0, self::SHARPEN_SIGMA);
|
||||||
|
|
||||||
// Optional pre-dither blur to soften sharp colour transitions —
|
// 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.
|
|
||||||
if (self::BLUR_SIGMA > 0) {
|
if (self::BLUR_SIGMA > 0) {
|
||||||
$imagick->blurImage(0, self::BLUR_SIGMA);
|
$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
|
// 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.
|
||||||
if ($imagick->getImageWidth() !== $width || $imagick->getImageHeight() !== $height) {
|
if ($imagick->getImageWidth() !== $width || $imagick->getImageHeight() !== $height) {
|
||||||
|
|||||||
Reference in New Issue
Block a user