# Brightness and relative luminance — model v1

Synthetic neutral-grey images reuse `features/image_core.py` without modification.
The fixtures are **assumed** to be encoded sRGB with R=G=B; this is not a measured
camera transfer function. Decoding then gives linear relative luminance Y, with
white normalized to 1. No output represents physical luminance in cd/m².

Reference: [W3C relative luminance definition](https://www.w3.org/WAI/WCAG21/Understanding/relative-luminance.html).
For a neutral pixel the RGB weights sum to 1. Decode C using C/12.92 when
C ≤ 0.04045, otherwise ((C+0.055)/1.055)^2.4. Encoding uses 12.92Y when
Y ≤ 0.0031308, otherwise 1.055Y^(1/2.4)−0.055. The rounded breakpoints produce
a tiny round-trip discrepancy; zero exposure bypasses decode/re-encode to make
the identity preset exactly identical to its source.

## Fixed processing order

1. Decode the source; multiply linear Y by 2^EV.
2. Clamp to [0,1], then encode to sRGB (identity bypass at EV=0).
3. Encoded gain and offset: z = gain*C + offset. No intermediate clamp here.
4. Encoded contrast about 0.5: z = 0.5 + contrast*(z−0.5).
5. Clamp to [0,1].
6. User gamma: output = z^(1/gamma). Gamma > 1 lifts intermediate values.

Linear exposure and encoded gain are distinct operations. User gamma is an
additional power adjustment, not the sRGB transfer function. All arithmetic uses
floating pixels; display quantization must not feed back into computed metrics.
Clipping at step 2 permanently loses distinctions even if step 3 later darkens
the output. This is an educational bounded-image pipeline, not an HDR editor.

## Measurements

- Clipping counts use strict <0 or >1 immediately before each clamp. Exact
  endpoints are not counted as clipped. Stage counts may overlap; the unique
  count is their pixel-mask union. Final black/white occupancy is separate.
- Histograms have 32 equal bins, left inclusive/right exclusive except the last
  bin includes 1. Encoded and decoded-linear histograms each include all pixels.
- Encoded mean and population standard deviation use floating pixel values.
  Mean relative luminance is the mean of decoded pixels, not decode(mean C).
- Forward differences use gx=C[y,x+1]−C[y,x] and gy=C[y+1,x]−C[y,x] on the common
  (height−1) × (width−1) interior. Magnitude is sqrt(gx²+gy²), in encoded units/pixel.
  No padding or fabricated border gradients; a displayed gradient needs a fixed
  [0,sqrt(2)] scale for fair before/after comparison.

## Browser presentation and reproducibility

The native adapter runs in the existing Pyodide Worker. Images are displayed at
8-bit precision but metrics use the original floating pixels. A/B retains separate
inputs; shared URLs serialize input conditions, not the saved A result or probe.
Gradient view uses a fixed [0,sqrt(2)] scale. Each histogram has a shared count
scale for its before/after pair, with the actual maximum shown on the axis.
The numerical table provides every bin count. Controls are Japanese/English;
other article locales use the established English fallback and localized links.

Native tests cover known sRGB pixels, exposure, operation order, irreversible
clipping, histogram boundaries, mean luminance and finite-difference gradients.
expected.json stores computed metrics for all eight presets. The package reuses
image_core.py unchanged; no camera, detector or tracking accuracy is measured here.
