Files
RecipesCam/docker/frontend/shared
3dtours 9164bf3228 web: read DEHAZE off the dark channel, and let it run both ways
DEHAZE read its haze estimate out of the frame's own bilateral reference — the
patch AVERAGE — where the Dark Channel Prior asks for the patch MINIMUM. That
one word is the whole prior: `dark = min(min(r,g,b)/A)` over a neighbourhood
reads 0 for any patch that holds a shadow or a black frame line, so the
transmission stays at 1 and the patch is left alone, while the average of a
patch that holds a dark pixel is still bright, so every patch looked hazy. The
positive end therefore ground the frame down instead of taking haze out of it:
at +9 the mask moved its own middle band -0.2127 and the frame-wide row moved
the whole frame -0.2311, and the local contrast went the WRONG way (dhp -0.0060
on the mask, -0.0056 frame-wide) — a haze remover that lowers contrast is a haze
remover that is lowering everything.

The pass reads the dark channel from the image it is correcting, five by five
taps at DEHAZE_PATCH_STEP (0.625% of the frame's width per tap, a 2.5%-wide
patch — the DCP's own 15 pixels on a 600px frame, and the same fraction of a
4000px export) in DEHAZE_SKSL and in gradientMask's block, so the mask and the
frame-wide row are the same neighbourhood at every render size. Five by five
rather than fifteen by fifteen because 225 child reads per pixel is what
CLARITY_BLUR_SKSL already refused for a reference the prior does not need to be
that wide. The bilateral reference is now only what CLARITY compares against, so
DEHAZE no longer takes a second child at all.

DEHAZE is signed, which it was not: the knob was 0..10 and the export engine
skipped the pass unless the amount was above zero, so a negative value was a
slider the UI would not even offer. It is -10..+10 now, and the transmission
carries the sign — positive pushes t below 1 and `J = (I - A)/t + A` takes the
scattered light out, negative pushes it above 1 and the same expression scatters
light back in. That is the direction a photo shot through mist wants, and it
needs no second formula: one expression, both signs, the ceiling at
1 + DEHAZE_MAX_OMEGA.

CLARITY's negative side was the last place where a knob meant two different
things depending on where it was read: the frame-wide row softened with a mist
blur of its own radius (MakeBlur, sigma |c|/10*4) while a mask mixed toward the
bilateral reference the positive side reads — two neighbourhoods, two strengths,
one name. CLARITY_BLEND_SKSL now carries both directions of the one move (above
zero the doc's unsharp, below it the mix back toward the same reference, gain
1), so the frame-wide row and a mask's CLARITY are the same reference at the
same strength, and the frame-wide mist blur is gone.

Measured in one harness, one photo, one session, knob at +-9, before -> after,
mask phase and frame phase in the same run (the box is the mask's own middle
box for the mask, the stage's own box for the frame-wide row):

  - FRAME DEHAZE +9: dmean -0.1680 -> -0.0751, dhp -0.0056 -> +0.0036, white
    band -0.2156 -> -0.0522 — it darkens the haze and raises the contrast
    instead of lowering both.
  - FRAME DEHAZE -9: dmean +0.0469 (was not offered), dhp -0.0010 — the same
    knob on the other side, and the frame gets hazier.
  - MASK DEHAZE +9: dmean -0.1490 -> -0.0513, dhp -0.0060 -> +0.0039, white band
    -0.1234 -> -0.0274, dark band -0.0595 -> -0.0075 — a mask's DEHAZE is now
    the frame-wide move on the mask's own pixels (dhp +0.0039 against the
    frame's +0.0036).
  - MASK DEHAZE -9: dmean +0.0319, dhp -0.0013.
  - FRAME CLARITY -9: dhp -0.0200 -> -0.0094, white band -0.1112 -> -0.0203, so
    the frame-wide row no longer pays for its soften by flattening every white
    in the frame; MASK CLARITY -9 is the same move (dhp -0.0150, white band
    -0.0103) and the two now agree in direction, sign and rough magnitude at
    -9. CLARITY +9 is untouched on both sides (+0.0335 mask, +0.0307 frame) and
    every other knob's numbers are unchanged to within +-0.0005, which is the
    run-to-run noise of the same harness.

`step` was the uniform's first name and SkSL refused the shader with it (a
builtin), which is how a whole DEHAZE row came back with all-zero deltas in the
first measurement after the change; `stepPx` is what compiles. `npm run
typecheck` and `npm run build` are clean, and the stage draws with no page error
(the only console error is the dev server's own `/api/events` 404).

Not ported: nothing. The phone's renderer has no gradient mask and no
atmospheric-light estimate to mirror; `shared/utils/toneShader.ts` and
`shared/utils/gradientMask.ts` are the web engine's own files.

Probes: measure-parity (both phases in one run, one photo, before and after —
the same harness the previous commit was scored with), measure-dehaze2 (the same
script with only DEHAZE in both phases, plus a console listener, which is how
the `step` uniform was caught), sim-dehaze-dcp (the offline simulation that
picked the min-patch over the average: clear frame +9, contrast 0.0248 -> 0.0292
against the average's 0.0248 -> 0.0235).
2026-09-26 20:16:13 +07:00
..