Leica Look
Overview
A camera's black-and-white rendering, measured from reference frames and rebuilt for print.
Details
At a glance
- Project type
- Image pipeline and browser darkroom
- My role
- Measured the look, fitted it, wrote both renderers and the darkroom around them.
- Category
- Products
- Status
- Prototype2026
What I did
- Fitted the rendering from matched pairs of reference and source frames
- Wrote the exact reference renderer and the fast browser one, and the harness that holds them together
- Designed the darkroom: two looks, eight adjustments, print size, grain and the reading tools
- Wrote down what the data could not support, next to the values it could
Built with
- Python: The exact reference renderer and the fitting pipeline
- NumPy: The colour and tone mathematics, in double precision
- WebGL2: The browser renderer the user actually touches
- ONNX Runtime: Face detection for the optional local masks
- pytest: Colour round-trips, curve monotonicity and recovery of known parameters
Next project
Wisr
The need
A Leica's monochrome rendering does not look like a phone's. The difference is not a filter over the top of the picture. It is a set of decisions about how colour becomes grey, where the tones sit, and how much local contrast is allowed to build, and those decisions are consistent enough that a photographer recognises them.
The question was whether that rendering could be measured rather than imitated, and then applied to an ordinary phone photograph well enough to print at forty-five centimetres.
Measuring a look
The rendering was fitted from matched pairs: the same scene as the camera rendered it and as it came out of the phone. Two looks came out of that work, a softer Classic fitted from thirteen pairs and a harder Contrast fitted from nine.
What the fit produces is a small readable file rather than a black box: how much of the red, green and blue channel becomes grey, a tone curve on a display-referred axis, and a local-contrast term. Raising the red weight lightens skin and darkens a blue sky, and you can see that in the number before you see it in the picture.
Decision
Write down what the data could not support
- Context
- A fitted look can be published as a single confident recipe. Three parts of this one are not well supported: the vignette, because the reference frames are centre-weighted portraits and the measured falloff is really subject placement; the grain, because the two pools sit at different sensor resolutions and a fitted amplitude would encode that difference as style; and the blue channel weight, where ten frames want one value and three want another.
- What I decided
- Every fitted field carries a confidence entry saying how well the data supports it. The three that are not supported are left at identity and exposed as controls set by eye, and the disagreement in the blue weight is written down as an unresolved question rather than averaged away.
- Trade-off
- The product ships with three values the user has to judge instead of inherit, and a README that says plainly where it is uncertain. A tool that quietly presented a guess as a measurement would be easier to sell and worth less to anyone doing serious work with it.
The darkroom
Around the rendering sits a place to work: two looks, eight adjustments, and the reading tools a print needs. A histogram with blown and blocked percentages. A clipping view. A zone map that replaces tone with false colour so each band of brightness can be judged on its own.
The adjustments are built to a rule. Each one is an exact identity at its default and moves in one direction across its whole range, so nothing can reorder tones or quietly alter a batch you did not mean to touch. Shadows reach mid grey; blacks reach only a quarter tone, so you can place true black without washing out the middle.

Sized for paper, not for a screen
Grain is the part most tools get wrong. It is usually sized in pixels, which means the same setting is coarse on a postcard and invisible on a poster. Here it is sized in microns at the print size, so it lands on the paper the way it was set.
The print panel follows the same logic. Choosing a paper fits the picture inside the sheet and reports the size it will actually print at, rather than the sheet's dimension: a three-by-four frame on A4 prints at twenty-eight centimetres, not twenty-nine point seven, because its short edge meets the paper first. And when the source is smaller than the requested output, the tool upscales and says so, because no detail is added by upscaling.
Decision
Two renderers, held to one pixel of each other
- Context
- The browser has to be fast enough to feel live under a slider. Print work has to be exact. One engine cannot be both, so there are two: an exact one in double precision, and a fast half-float one in WebGL that is what the user actually touches.
- What I decided
- A harness renders the same lossless file through both and compares them pixel by pixel. Classic agrees to within one level out of 255 at the worst pixel; Contrast to within two. The gate is on the worst pixel rather than the average, because a mean threshold taken from one look would fail another for doing nothing wrong.
- Trade-off
- Every change to the mathematics has to be made twice and re-measured, or the two drift apart and neither can be trusted. The error that does remain is traceable to one known choice: the fast engine subsamples the local-contrast filter, deliberately, so that a phone can keep up.
Where it stands
Two looks ship. The command line converts a folder to sixteen-bit files in a wide colour space with the profile embedded, and the browser darkroom does the same work live on one photograph. The tests cover colour round-trips, the monotonicity of the curve under arbitrary input, recovery of known parameters from synthetic transforms, and grain sizing across print resolutions.
It is a personal tool rather than a product, and the interesting part was not the code. It was deciding what could honestly be claimed from thirteen pairs of photographs, and then building an interface that tells the truth about the rest.