No jobs
Core 11 min 4 of 5

Everything that is not linear any more

Tone, gamma, local contrast and sharpening run before anything you train sees the image.

What this repairs

Treating the ISP as a colour-space conversion, so tone mapping, local contrast and sharpening read as cosmetic rather than as an irreversible nonlinearity applied before the network sees anything.

The ISP is where a camera stops measuring and starts deciding what the picture should look like. The order in euler-preprocess is fixed and worth memorising, because each stage's effect depends on the space the previous one left behind:

  1. denoise, on the raw-ish signal;
  2. colour matrix, sensor primaries to the working primaries;
  3. tone map, compressing scene range into display range;
  4. gamma, the sRGB encode;
  5. local contrast, a large-radius unsharp on the encoded image;
  6. sharpen, a small-radius unsharp;
  7. saturation, a lerp toward luma.

Stages 1–3 act on light. Stages 5–7 act on codes, because gamma has already happened. That split is the single most useful thing to know about the chain: a sharpening halo is a fixed fraction of a code value, not of a radiance, so its apparent strength depends on where in the tone curve it landed.

Two nonlinearities, stacked

  1. y=x1+σx,y=1(1+σx)2y = \frac{x}{1 + \sigma x}, \qquad y' = \frac{1}{(1 + \sigma x)^{2}}

    The Reinhard tone map and its slope. Monotone, fixes zero, and asymptotes below 1 — which is what "compressing the highlights" means quantitatively. The shipped profile interpolates a 16-entry LUT instead; Reinhard is the one you can differentiate by hand, and it has the same shape.

  2. y(1)y(0)=1(1+σ)2=14(σ=1)\frac{y'(1)}{y'(0)} = \frac{1}{(1 + \sigma)^{2}} = \tfrac{1}{4} \quad (\sigma = 1)

    A difference between two bright surfaces survives at a quarter of its original size, while the same difference in the shadows passes through untouched. Contrast has been moved, not removed.

  3. z=1.055y1/2.40.055,dzdyy0.583z = 1.055\,y^{1/2.4} - 0.055, \qquad \frac{dz}{dy} \propto y^{-0.583}

    Then the gamma encode, whose slope diverges as y goes to zero — so it expands exactly the shadow region the tone map left alone.

  4. dzdx=dzdydydx\frac{dz}{dx} = \frac{dz}{dy}\cdot\frac{dy}{dx}

    The composed response is the product of the two slopes, and the two curves pull in opposite directions. That product is what a histogram of the delivered image is showing you — never the scene.

Saturation closes the chain as a luma-preserving interpolation, c=Y+α(cY)\mathbf{c}' = Y + \alpha(\mathbf{c} - Y) with YY the luminance of the pixel. α<1\alpha < 1 pulls colours toward grey and α>1\alpha > 1 pushes them away, in both cases leaving YY untouched — which is why saturation adjustments do not change a luminance histogram at all.

Two things follow. Pushing α\alpha above one can send a channel outside [0,1][0, 1] on a colour that was in range, and the clip that catches it changes the hue, not just the saturation. And when the scene is already desaturated — fog being the obvious case — an α\alpha below one compounds with the physical desaturation of the veil, so the delivered image is less colourful than the fog model actually predicted.

Check yourself

A faint texture is clearly present in the render and absent from the delivered image. How do you find the stage that removed it?

Show answer

By asking at what level it vanished. If the texture sits in a bright region, suspect the tone map: its slope there may be a quarter of what it is in the shadows, so a difference that was several code values becomes less than one and the quantiser finishes the job. If it disappears at every level, suspect denoise — a smoothing removes low-amplitude high-frequency content regardless of where it sits. And if it survives in a PNG of the same render but not in the delivered JPEG, it was compression, which throws away high-frequency content per 8×8 block and does so most aggressively in the chroma channels.

Euler View - ML Experiment Monitor