web: the four tonal knobs are sized by what the eye can see, and the highlight head is squared so a bigger rate fits inside the cube
The report was "giá trị thay đổi của các thông số quá nhỏ, không thể hiện được
trên thị giác của ảnh" — at the doc's own rates a full +100 was worth 0.060 of
luma on BLACKS, 0.082 on HIGHLIGHTS and 0.042 on WHITES, and the probe that ran
the frame through the pass read BLACKS +100 moving its mean by 0.0001. Every
rate below is now the largest its own move allows, measured rather than
inherited: 0.058 -> 0.080 on BLACKS, 0.082 -> 0.113 on HIGHLIGHTS and 0.042 ->
0.080 on WHITES, with SHADOWS' 0.151 left where it was because it already bit.
- `TONE_BLACK_LIFT` 0.7 -> 0.93. The ceiling is a FOLD, not a slope: past
0.9387 the doc's own square root carries luma backwards inside its window
(0.94 folds 3.4e-6, 0.95 folds 8.9e-5) and a gradient wears it as a band.
0.93 is the last round rate under it — monotone on the check's 1/32768
grid, and the 1e-4 of travel between it and 0.94 is not a code value.
- `TONE_BLACK_CRUSH` 0.85 -> 8.0. The doc's own form — `L * (1 + amount * W
* 0.85)` — is bounded by its own window, which is 1 only AT the floor, so
its whole visible travel at full -100 is 0.019 of luma: five code values on
a black patch, and a rate past 1 drives the product negative and clips the
toe to a flat black instead of deepening it. The toe's own EXPONENT,
`L -> W * (L/W)^(1 + rate * W)`, is monotone for ANY rate and worth 0.054,
while x = 0 stays on 0 and the 0.18 edge stays on 1 — both anchors and the
compact support kept.
- `TONE_HIGH_GAIN` 2.5 -> 14.0, with the head term moved from `(1 - L)` to
`(1 - L)^2`. The linear headroom dies too slowly to keep the rate's own
ceiling off the clamp: above a gain worth 2.6 the move overshoots 1.0, the
clamp draws a plateau and the ramp falls back over it by 0.018 — the fold
the knee check now measures as a drawdown from the running maximum. Squared,
the move has died out by the time the ramp reaches the clamp: 14.0 is
fold-free at both signs and still lands 1.44x of the 1.5x the quarter it
owns is allowed.
- `TONE_WHITE_GAIN` 1.5 -> 3.0, which takes the top of the ramp TO the
ceiling from 0.92 up and leaves the clamp to flatten what is left. That is
the doc's own §2.4, where a WHITE is the frame's clipping point ("giới hạn
cháy sáng") and not a Hermite that cannot move the head; it is felt only
above the 0.80 shoulder and the head is still exactly 1.0 on 1.0.
`FILM_TONE` is re-solved for the squared head, which is worth less at the 0.75
knot for the same rate: monochrome -0.16 -> -0.1143 and mono-high-contrast
+0.83 -> +0.5929. The four knots the stocks are tuned to do not move — 0.22 and
0.7375 on Acros, 0.17 and 0.815 on Acros HC — and the check pins each of them.
The knee check's guard is REPLACED. The old one compared the first cell of the
sweep against the second (a slope at 1/512), which is blind to a fold that
starts later: it passed a ramp whose own drawdown was 0.018. The new one walks
1/32768 of the ramp and measures `running max - value` for each knob at both
signs, over the four moves alone and then over a 243-combination sweep, so what
is pinned is the fold itself and where it is.
Two folds are pinned rather than removed, both named in the check:
- SHADOWS -100 dips 0.018 (4.6 code values) around 0.06..0.11 of its own
accord. It is the doc's §2.2 formula — `L *= 1 + amount * W * (1 - L)^1.8`
— where the window rises faster than the light, and it PREDATES this change.
The monotone rewrite (`L' = 1 - (1 - L)^(1 - SH * |a| * W(L))`) is written
out beside it and was NOT taken: it is exact but it costs the knob 30-50% of
its crush.
- WHITE +100 rests a plateau on the clamp from 0.92 up. That is what §2.4 asks
of the knob and the ramp is non-decreasing through it, so it is not a fold.
`scripts/tone-base-check.mjs`'s mirror of the pass takes the same three moves
(its own `toneBlack` and the squared head) so the pixel it predicts is still the
pixel the pass draws.
Checked: node scripts/highlight-knee-check.mjs; node scripts/tone-base-check.mjs;
node scripts/auto-tone-check.mjs; node scripts/half-check.mjs; node
scripts/mask-wb-check.mjs; node scripts/preview-match-check.mjs; node
scripts/raw-develop-check.mjs; node scripts/white-level-check.mjs; node
scripts/wb-table-check.mjs; node scripts/sharpen-check.mjs; node
scripts/denoise-check.mjs; npx tsc --noEmit.
This commit is contained in:
@@ -152,17 +152,50 @@ export const TONE_BLACK_EDGE = 0.18;
|
||||
export const TONE_WHITE_EDGE = 0.80;
|
||||
|
||||
// How much of its band a knob is worth at full deflection — 100 on the slider.
|
||||
// BLACK's pair is the doc's own (0.7 opens the toe through the square root, 0.85
|
||||
// is how hard the crush bites). HIGHLIGHT and WHITE are measured rather than
|
||||
// inherited: the doc prints the shapes and leaves the rates off, and its own
|
||||
// sample — a raw pow(L, 1.5) with no headroom term, and a WHITE worth 0.5 —
|
||||
// either clips or does nothing on a real frame. These are the largest rates that
|
||||
// keep a full +100 inside the cube on the sweep in highlight-knee-check, with the
|
||||
// head rolling into the shoulder instead of onto the clamp.
|
||||
export const TONE_BLACK_LIFT = 0.7;
|
||||
export const TONE_BLACK_CRUSH = 0.85;
|
||||
export const TONE_HIGH_GAIN = 2.5;
|
||||
export const TONE_WHITE_GAIN = 1.5;
|
||||
//
|
||||
// Sized by what the EYE can see, not by what the doc's own rates happen to be. At
|
||||
// the doc's numbers a full +100 was worth 0.060 of luma on BLACKS, 0.082 on
|
||||
// HIGHLIGHTS and 0.042 on WHITES (the arithmetic sweep on the scratchpad, and the
|
||||
// probe that ran the frame through the pass: BLACKS moved its mean by 0.0001 in
|
||||
// either direction), which is the "giá trị thay đổi quá nhỏ, không thể hiện trên
|
||||
// thị giác" report. Each rate below is now the largest its own move allows under
|
||||
// the contracts hold in highlight-knee-check: monotone at any amount on the
|
||||
// slider, both anchors fixed exactly, and a knob still exactly the identity off
|
||||
// its own band.
|
||||
//
|
||||
// BLACK's lift is the doc's square root at the rate monotonicity allows — 0.93,
|
||||
// the last rate whose own toe stays monotone on the check's grid (0.94 folds
|
||||
// 3.4e-6 of luma back on itself, 0.9387 is the exact ceiling, and the travel
|
||||
// between 0.93 and 0.94 is 1e-4, so the round number costs nothing). Its crush
|
||||
// cannot be the doc's multiplicative rate at all: that form
|
||||
// is bounded by its own window and is worth 0.019 of luma at full -100, five code
|
||||
// values on a black patch, invisible. The crush is the toe's own exponent instead
|
||||
// (see toneBlack), which is monotone for ANY rate, and worth 0.054.
|
||||
//
|
||||
// HIGHLIGHT and WHITE are measured rather than inherited — the doc draws the
|
||||
// shapes and leaves the rates off, and its own sample (a raw pow(L, 1.5) with no
|
||||
// headroom term, and a WHITE worth 0.5) either blows the frame or does nothing on
|
||||
// one. Both head terms are only monotone while their own rate stays under a
|
||||
// ceiling, and the ceiling is a FOLD, not a slope: the move overshoots 1.0, the
|
||||
// clamp draws a plateau, and the ramp then falls back under it — measured under
|
||||
// the knee check as the drop from the running maximum, at 1/32768 of the ramp.
|
||||
// The head term is squared (see the note above toneCurve) because that is what
|
||||
// buys the room: the move dies out before it can reach the ceiling.
|
||||
//
|
||||
// HIGHLIGHT at 14.0 stretches the quarter it owns to 1.44x of the 1.5x the check
|
||||
// allows and lands +0.113 on the band, 29 code values, with no fold in either
|
||||
// direction. WHITE at 3.0 is worth +0.080 — and it SATURATES the top of the ramp
|
||||
// from 0.92: that is the doc's own §2.4, where a WHITE is the frame's clipping
|
||||
// point ("giới hạn cháy sáng") and not a Hermite that cannot move the head. It is
|
||||
// felt only above the 0.80 shoulder, it is what the knob is for, and the head
|
||||
// still lands on 1.0 exactly. The one fold left in the ramp is not from these
|
||||
// rates: SHADOWS' crush is the doc's own multiplicative form and dips 0.018 of
|
||||
// luma (4.6 code values) over the 0.06..0.11 band at full -100, measured and
|
||||
// pinned in the knee check, whatever rate it is given.
|
||||
export const TONE_BLACK_LIFT = 0.93;
|
||||
export const TONE_BLACK_CRUSH = 8.0;
|
||||
export const TONE_HIGH_GAIN = 14.0;
|
||||
export const TONE_WHITE_GAIN = 3.0;
|
||||
|
||||
// How far out the BASE layer of `toneRamp` reads, as a fraction of the frame's
|
||||
// own width — the fix_shadow.md neighbourhood (it asks for 2%..5% of the width).
|
||||
@@ -228,6 +261,29 @@ float toneBlackW(float L) {
|
||||
// angle, and an angle on a tone curve is a Mach band.
|
||||
return u * u * u;
|
||||
}
|
||||
// BLACKS, both directions, one window. §2.1's TOE SLOPE, and the two moves it
|
||||
// names are not the same kind of move:
|
||||
//
|
||||
// - the lift is the doc's own (L + amount * W * (sqrt(L) - L)), at the largest
|
||||
// rate monotonicity allows. The square root's slope at the floor is what makes
|
||||
// a bigger rate fold the toe back on itself — past 0.93 the ramp carries whole
|
||||
// hundredths of luma backwards and a gradient wears the fold as a band.
|
||||
// - the crush cannot be the doc's (L * (1 + amount * W * 0.85)) at any rate that
|
||||
// can be seen. The window is 1 only AT the floor and this form multiplies what
|
||||
// the window leaves, so its whole travel is 0.019 of luma at full -100 however
|
||||
// hard the rate bites (0.85 or 1.0, measured), and a rate past 1 drives the
|
||||
// product negative and clips the toe to a flat black instead of deepening it.
|
||||
// The toe's own EXPONENT is the slope §2.1 asks for: x -> x^(1 + rate * W) is
|
||||
// monotone for every rate, so it deepens what the doc's form cannot reach —
|
||||
// 0.054 at full -100 — while x = 0 stays on 0 and x = 1 (the 0.18 edge) stays
|
||||
// on 1, which is the compact support and the two anchors both kept.
|
||||
float toneBlack(float L, float bl) {
|
||||
const float W = ${TONE_BLACK_EDGE};
|
||||
float q = toneBlackW(L);
|
||||
if (bl > 0.0) return L + ${TONE_BLACK_LIFT} * bl * q * (sqrt(L) - L);
|
||||
if (L >= W) return L;
|
||||
return W * pow(L / W, 1.0 + ${TONE_BLACK_CRUSH} * (-bl) * q);
|
||||
}
|
||||
float toneShadowW(float L) {
|
||||
return smoothstep(0.02, 0.12, L) * (1.0 - smoothstep(0.25, 0.55, L));
|
||||
}
|
||||
@@ -249,24 +305,25 @@ float toneWhiteW(float L) {
|
||||
// L = 1 without moving either, so the composition is monotone, the black point is
|
||||
// the black point, and the white point is the white point whatever the four
|
||||
// sliders say. The one deliberate departure from the doc's own arithmetic is the
|
||||
// (1.0 - L) on HIGHLIGHTS: the doc's raw soft-knee ADDS pow(L - 0.5, 1.5) to L,
|
||||
// (1.0 - L)^2 on HIGHLIGHTS: the doc's raw soft-knee ADDS pow(L - 0.5, 1.5) to L,
|
||||
// and above 0.94 that overshoots the cube — a measured 17% of the ramp driven to
|
||||
// flat white at +100 before the clamp, the "cháy vùng Whites" the doc's own §1
|
||||
// opens by calling a defect. The knee reads the headroom that is left instead, so
|
||||
// the move is zero at L = 1 by construction and the head rolls instead of
|
||||
// clipping.
|
||||
// clipping — and squared rather than linear, because one power of the headroom
|
||||
// dies too slowly to keep the rate's own ceiling off the clamp: at a linear head
|
||||
// a rate worth 0.113 of luma already overshoots 1.0 near 0.94 and the ramp falls
|
||||
// back over the plateau by 0.018 (the fold the knee check measures), so the rate
|
||||
// that shape allows is the 0.082 this whole pass is replacing.
|
||||
float toneCurve(float L, float bl, float sh, float hl, float wh) {
|
||||
float q;
|
||||
// BLACKS, the toe. Lift opens the detail under the doc's square root, crush
|
||||
// deepens it by the doc's 0.85, and neither touches the anchor: sqrt(0) - 0 is
|
||||
// 0, so the black point is exactly where it was — the offset that lifted
|
||||
// (0,0,0) to a grey pedestal was a SUM adding its bump's height at L = 0, which
|
||||
// is the doc's "Milky / Foggy" failure. Past 0.18 the window is 0 and the move
|
||||
// is exactly the identity.
|
||||
q = toneBlackW(L);
|
||||
L = bl > 0.0
|
||||
? L + ${TONE_BLACK_LIFT} * bl * q * (sqrt(L) - L)
|
||||
: L * max(1.0 + ${TONE_BLACK_CRUSH} * bl * q, 0.0);
|
||||
// BLACKS, the toe — see toneBlack. Neither direction touches the anchor: the
|
||||
// lift is 0 at L = 0 (sqrt(0) - 0), the crush is 0^(anything) = 0, so the black
|
||||
// point is exactly where it was. The offset that lifted (0,0,0) to a grey
|
||||
// pedestal was a SUM adding its bump's height at L = 0, which is the doc's
|
||||
// "Milky / Foggy" failure. Past 0.18 the window is 0 and the move is exactly
|
||||
// the identity, slope and all.
|
||||
L = toneBlack(L, bl);
|
||||
L = clamp(L, 0.0, 1.0);
|
||||
// SHADOWS, a gain on the light with the floor still on 0: the multiplier is
|
||||
// 1 + amount * bell * (1 - L)^1.8, so the window's own bell already keeps it
|
||||
@@ -276,12 +333,19 @@ float toneCurve(float L, float bl, float sh, float hl, float wh) {
|
||||
L = clamp(L, 0.0, 1.0);
|
||||
// HIGHLIGHTS, the doc's soft-knee against the headroom that is left — see the
|
||||
// note above the function. max(L - 0.5, 0) because the pow is undefined under
|
||||
// the knee and the window is only wide where it is not.
|
||||
// the knee and the window is only wide where it is not, and the headroom enters
|
||||
// SQUARED so the move has died out by the time the ramp reaches the clamp.
|
||||
q = toneHighW(L);
|
||||
L += ${TONE_HIGH_GAIN} * hl * q * pow(max(L - 0.5, 0.0), 1.5) * (1.0 - L);
|
||||
L += ${TONE_HIGH_GAIN} * hl * q * pow(max(L - 0.5, 0.0), 1.5) * (1.0 - L) * (1.0 - L);
|
||||
L = clamp(L, 0.0, 1.0);
|
||||
// WHITES, the doc's Hermite on the shoulder: (1 - L) * L is zero on both ends,
|
||||
// so the white point is fixed and the move is spent inside the top of the ramp.
|
||||
// At the rate the knob is worth, +100 takes the top of the ramp TO the ceiling
|
||||
// and the clamp flattens what is left of it: from 0.92 up a pixel is white.
|
||||
// That is §2.4's clipping point ("giới hạn cháy sáng") raised, and it is the
|
||||
// only move in this curve that reaches 1.0 short of the white point itself —
|
||||
// the head is still 1.0 on 1.0, and the ramp is still non-decreasing through
|
||||
// the flat part, so nothing folds.
|
||||
q = toneWhiteW(L);
|
||||
L += ${TONE_WHITE_GAIN} * wh * q * (1.0 - L) * L;
|
||||
return clamp(L, 0.0, 1.0);
|
||||
@@ -953,18 +1017,22 @@ const FILM_TONE: Partial<Record<BaseFilter, Partial<ToneUniforms>>> = {
|
||||
// every step the matrix handed over — which is what 'deep black' costs in a
|
||||
// colour stock and does not have to cost here.
|
||||
// The values move the two end bands of the ramp: -0.20 puts the toe on 0.22
|
||||
// and -0.16 rolls the head to 0.7375 (both solved against the window's own
|
||||
// height at 0.25 and 0.75, see the note above FILM_TONE).
|
||||
monochrome: { sh: -0.20, hl: -0.16 },
|
||||
// and -0.1143 rolls the head to 0.7375 (both solved against the window's own
|
||||
// height at 0.25 and 0.75, see the note above FILM_TONE — the head move is
|
||||
// -0.1143 rather than the -0.16 it used to be because the head term is squared
|
||||
// now, and a squared headroom is worth less at the 0.75 knot for the same rate).
|
||||
monochrome: { sh: -0.20, hl: -0.1143 },
|
||||
// B&W HIGH CONTRAST. Acros' ramp with both ends pushed hard: a deeper toe
|
||||
// (-0.54 against Acros' -0.20, so 0.17 against 0.22) so the darks reach true
|
||||
// black, and a shoulder that LIFTS instead of rolling (-0.16 → +0.83, the
|
||||
// head going to 0.815), which is the whites step of the brief. The stretch
|
||||
// black, and a shoulder that LIFTS instead of rolling (-0.16 → +0.5929, the
|
||||
// head going to 0.815 — re-solved from +0.83 for the squared head, which is
|
||||
// worth less at the 0.75 knot for the same rate), which is the whites step of
|
||||
// the brief. The stretch
|
||||
// between the two inner knots (0.25 and 0.75) is still the identity, so the
|
||||
// long smooth stretch of the greys survives — that is what keeps a hard push
|
||||
// off the posterised look, and the strength the stock needs on the greys is
|
||||
// its matrix slope (SIM_CONTRAST_BIAS in colorUtils), not another move here.
|
||||
'mono-high-contrast': { sh: -0.54, hl: 0.83 },
|
||||
'mono-high-contrast': { sh: -0.54, hl: 0.5929 },
|
||||
};
|
||||
|
||||
// The base layer's neighbourhood is the caller's business, not this function's:
|
||||
|
||||
Reference in New Issue
Block a user