The colour pipeline¶
How the OEM turns a raw C-41 negative scan into a positive. The core is two linked steps (a density conversion and a colour-correction matrix) driven by two data files. This is the piece most film-scanning tools get wrong, because the inversion happens in density (log) space, not linear.
From reverse engineering of the Ansel engine (PakonIMAu.dll) and the OEM
colour-correction data files, March 2026; the exact LUT formula and the
pipeline ordering settled by numeric analysis, July 2026. OEM file paths are
relative to a standard Windows install; this reference documents the derived
facts, not the files.
Engine scope. The OEM's host software has per-model engine builds
(TLB.dll for the F-135/F-135+, TLA.dll for the F-235; see
scanner-family.md), and the
builds differ in the colour-processing stage. The data files described on
this page ship with every build, but which stages a given build actually
applies to colour-negative data, and in what order, is engine-specific.
Analysis of the shipped binaries by the pakon-mac project (Guy Langford-Lee,
https://github.com/gazzdingo/pakon-mac) reports that the LUT→matrix
stage described below is the F-235 engine's (applied through the
PakonIMAu MMX kernel), while the 135-line engine applies a different,
per-unit colour transform to colour-negative data and does not call the
LUT+matrix kernel at all (see Open questions). Readers
working on an F-135/F-135+ should treat this page as documenting the shared
data files and the F-235 path until the 135-line path is documented here.
The inversion LUT, exactly¶
The OEM's negative→positive tone conversion is a single 14-bit (16,384-entry)
1-D lookup table (F-X35 COM SERVER\Config\ColorCorrection\_ClientColNegLut.txt).
Numeric analysis shows it is, exactly:
D(x) = 3500 · log10(16383 / x) x = scanner linear code, 1..16383
with D(0) clamped to 16383. [CONFIRMED] the maximum residual against all
16,383 table entries is 5×10⁻⁵ (float rounding). [July 2026]
What this means precisely:
- The LUT converts scanner-linear transmission code to optical density, scaled 3500 counts per decade of density, anchored so CCD full scale (16383 = maximum transmission) reads density 0.
- It is a pure log/density conversion: there is no film-characteristic shaping and no tone rendering in it. Its output is a scene-log-exposure-like encoding, not a finished positive.
- "Inverts the negative" is true only in the sense that density is inversely related to transmission: a dense part of the negative (a scene highlight) maps to high density. [CONFIRMED by the formula]
12-bit (4096-entry) per-film-product LUT variants exist for the FilmLut
stage. [DOCUMENTED]
The colour matrix¶
After the LUT, a 3×4 affine transform
(Config\ColorCorrection\_ClientColNegMat.txt) is applied in density space:
R' = 1.119·R − 0.101·G − 0.012·B − 82.6
G' = −0.201·R + 1.101·G + 0.117·B − 586.9
B' = −0.117·R + 0.048·G + 1.083·B − 707.8
[DOCUMENTED] the coefficients; [CONFIRMED July 2026] the domain and ordering:
- Order is LUT → matrix. The offsets are density-unit quantities: they are meaningless before the LUT and exact after it.
- The 3×3 is the classical masking-equation correction in density space: it removes C-41 dye unwanted-absorption crosstalk (the orange mask's colour cast). Its row sums are ≈ 1.01, so neutral density maps to neutral density: it corrects colour crosstalk without re-grading neutrals.
- The offset column is Dmin (film-base) subtraction in density units. Dividing by 3500 gives R 0.024 D, G 0.168 D, B 0.202 D, the orange mask's base-density signature (blue densest), measured through this scanner's per-channel exposure calibration. The offsets are therefore entangled with the scanner's exposure/gain calibration; the 3×3 is not.
Pipeline placement¶
The Ansel colour-negative path (AnsColorNegativePath) runs many stages;
relevant to the inversion, the order is: scanner correction (gain/offset,
lens-falloff) on the raw linear data → the density LUT → the crosstalk/Dmin
matrix → output colour-space encoding. The scanner-correction and
output-encoding stages are separate from the two inversion steps above.
[DOCUMENTED]/[INFERRED] stage names are confirmed from the DLL; the exact
execution order within the engine is inferred from dependencies.
Practical restatement for implementers¶
The single invariant: film-base (Dmin) compensation happens exactly once. The OEM performs it inside the matrix stage. Its offset column subtracts the film-base densities, on the assumption that the input is on the OEM's calibrated linear scale (clear base near full scale). [DOCUMENTED]
For input that is not on the OEM's calibrated scale, those baked offsets do not transfer. The film base must instead be established for the actual input (for example, measured from the scan itself) and compensated in one place: either normalise in the linear domain before the LUT, or derive new density-domain offsets for the matrix. Combining linear-domain normalisation with the OEM offset column applies the compensation twice.
The off-diagonal 3x3 terms are scale-independent in a way the offsets are not (a global density scale commutes with the matrix, per the numeric analysis above), so they remain applicable to re-based input. They are the part that pure per-channel processing cannot express: the actual dye-mask correction.
Output-colourspace encoding (sRGB or a rendering profile) is a final, separate step.
One point in this space is verified: measured linear-domain Dmin normalisation, then the LUT, then sRGB, with the matrix omitted entirely, reproduces the OEM positive closely on real scans. [CONFIRMED on hardware, August 2026, in the pakon-macos project by Jorge Rangel: https://github.com/jorshhh/pakon-macos] The full OEM matrix path has not been reproduced outside the OEM software.
Rendering profiles¶
Beyond the faithful inversion, the OEM offers "vibrant" rendering through Kodak
ICC profiles (e.g. an RPD rendering profile) plus scene-balance and highlight
handling, at F-X35 COM SERVER\anselinstalldir\icc\. These are a look applied
on top of the inversion, not part of it. [DOCUMENTED]
Open questions¶
- The full OEM matrix path has not been reproduced outside the OEM software.
- The OEM's film-specific layer (per-film LUTs and profiles, film-class selection, scene balance) is not yet documented here.
- The 135-line colour path. The pakon-mac project's analysis of the
shipped engines (https://github.com/gazzdingo/pakon-mac) reports that
TLB.dll(F-135/F-135+) applies a per-unit second-order polynomial (3×10 coefficients, stored asNegMatrix0-29in the scanner's calibration EEPROM and mirrored to the registry) to colour-negative data, and that the density LUT + 3×4 matrix kernel documented on this page has no call site in that build; the psix project (Oliver Roch, https://github.com/veroc/psix) independently measured the OEM's F-135+ output transfer as per-channel with no post-process 3×3, and the pakon-tlx-macos project (Pablo Navarro, https://github.com/pablonavarrob/pakon-tlx-macos) records the same 3×10 matrix in the F-135's registry. That polynomial's exact form, where the negative→positive inversion then happens on the 135 line, and how the Ansel stages consume the result are not yet documented here. Until they are, the pipeline on this page is the F-235's, and 135-line reimplementations should not assume it.