Reject non-finite numbers and empty LUT input ranges in .cube import (#998)
filmcraft-color
Colour science for FilmCraft (layer L0, no workspace dependencies): Y'CbCr matrices, transfer functions, camera log curves, RGB gamuts, colour-managed input/output transforms with tone and gamut mapping, and LUT files.
| Module | Contents |
|---|---|
lib.rs |
Matrix/Transfer/Primaries/Range/ColorInfo (stream metadata), Y'CbCr↔R'G'B', sRGB, PQ (ST 2084), HLG (ARIB STD-B67), HSL, hex colours |
log |
camera log curves (encode/decode), their signal domains |
spaces |
gamuts and 3×3 matrices (SMPTE RP 177), ColorSpace (Interpret Footage choices), WorkingSpace, ColorPipeline |
transform |
InputTransform (source → working), OutputTransform (working → monitor / export), BT.2390 tone mapping, gamut compression |
lut |
.cube (1D, 3D, shaper + 3D) and .3dl parse/write, tetrahedral interpolation |
Working units
The compositor works in linear light with 1.0 = reference white: SDR white for SDR media, and 203 cd/m² for HDR (ITU-R BT.2408, "HDR Reference White"). SDR media keeps its values in an HDR sequence (the BT.2408 SDR-in-HDR mapping), HDR highlights are values above 1.0, and log media decodes to scene-linear reflectance (18 % grey = 0.18). Working primaries are BT.709 for a Rec. 709 sequence and BT.2020 for Rec. 2100 PQ/HLG sequences or when "wide gamut" is on.
Camera log curves
Each curve is implemented from the manufacturer's published document. "Domain" is the normalisation
the document uses: CV = 10-bit code value / 1023 (full scale), IRE = video-range normalised
(code 64 → 0, code 940 → 1). log::signal_from_normalized converts the decoder's normalised value
(which already accounts for the file's range flag) into the curve domain, so the same formulas work
for limited- and full-range files.
| Curve | Gamut | Source | Domain | Decode (signal y → linear x) |
|---|---|---|---|---|
| Sony S-Log3 | S-Gamut3.Cine / S-Gamut3 | Sony, Technical Summary for S-Gamut3.Cine/S-Log3 and S-Gamut3/S-Log3 (2014) | CV | y ≥ 171.2102946929/1023: x = 10^((1023y − 420)/261.5)·0.19 − 0.01; else x = (1023y − 95)·0.01125/(171.2102946929 − 95) |
| Panasonic V-Log | V-Gamut | Panasonic, V-Log/V-Gamut Reference Manual (2014) | CV | y < 0.181: x = (y − 0.125)/5.6; else x = 10^((y − d)/c) − b, b = 0.00873, c = 0.241514, d = 0.598206 |
| Canon Log 2 | Cinema Gamut | Canon, Canon Log Gamma Curves white paper (2018) | IRE | x' = ±(10^(±(y − 0.092864125)/0.24136077) − 1)/87.09937546 (sign by y vs 0.092864125); x = 0.9·x' (Canon's x is reflectance / 0.9) |
| Canon Log 3 | Cinema Gamut | same | IRE | y < 0.097465473: x' = −(10^((0.12783901 − y)/0.36726845) − 1)/14.98325; y ≤ 0.15277891: x' = (y − 0.12512219)/1.9754798; else x' = (10^((y − 0.12240537)/0.36726845) − 1)/14.98325; x = 0.9·x' |
| ARRI LogC3 (EI 800) | ARRI Wide Gamut 3 | ARRI, ALEXA Log C Curve — Usage in VFX (2017), EI 800 row | CV | y > e·cut + f: x = (10^((y − d)/c) − b)/a; else x = (y − f)/e; cut 0.010591, a 5.555556, b 0.052272, c 0.247190, d 0.385537, e 5.367655, f 0.092809 |
| ARRI LogC4 | ARRI Wide Gamut 4 | ARRI, ARRI LogC4 Logarithmic Color Space — Specification (2022) | CV | a = (2¹⁸ − 16)/117.45, b = 928/1023, c = 95/1023, s = 7·ln2·2^(7 − 14c/b)/(a·b), t = (2^(6 − 14c/b) − 64)/a; y ≥ 0: x = (2^(14(y − c)/b + 6) − 64)/a; else x = y·s + t |
| Apple Log | BT.2020 | Apple, Apple Log Profile white paper (2023) | IRE | R0 = −0.05641088, Rt = 0.01, c = 47.28711236, β = 0.00964052, γ = 0.08550479, δ = 0.69336945; P ≥ c(Rt − R0)²: x = 2^((P − δ)/γ) − β; else x = √(P/c) + R0 |
| DJI D-Log | D-Gamut | DJI, White Paper on D-Log and D-Gamut of DJI Cinema Color System (2017) | CV | y ≤ 0.14: x = (y − 0.0929)/6.025; else x = (10^(3.89616y − 2.27752) − 0.0108)/0.9892 |
The encoders are the exact inverses given in the same documents. DJI D-Log M is not implemented: DJI has not published its formula (only conversion LUTs), and we do not derive curves from third-party LUTs. (Checked again in October 2026: DJI's published white papers cover D-Log / D-Gamut only; D-Log M remains unsupported, so D-Log M footage should be interpreted with a LUT.)
Gamut primaries (all D65 white, x/y):
| Gamut | R | G | B | Source |
|---|---|---|---|---|
| BT.709 | 0.640, 0.330 | 0.300, 0.600 | 0.150, 0.060 | ITU-R BT.709-6 |
| BT.2020 | 0.708, 0.292 | 0.170, 0.797 | 0.131, 0.046 | ITU-R BT.2020-2 |
| P3 D65 | 0.680, 0.320 | 0.265, 0.690 | 0.150, 0.060 | SMPTE EG 432-1 |
| S-Gamut3.Cine | 0.766, 0.275 | 0.225, 0.800 | 0.089, −0.087 | Sony technical summary |
| S-Gamut3 | 0.730, 0.280 | 0.140, 0.855 | 0.100, −0.050 | Sony technical summary |
| V-Gamut | 0.730, 0.280 | 0.165, 0.840 | 0.100, −0.030 | Panasonic reference manual |
| Cinema Gamut | 0.740, 0.270 | 0.170, 1.140 | 0.080, −0.100 | Canon white paper |
| ARRI Wide Gamut 3 | 0.6840, 0.3130 | 0.2210, 0.8480 | 0.0861, −0.1020 | ARRI Log C document |
| ARRI Wide Gamut 4 | 0.7347, 0.2653 | 0.1424, 0.8576 | 0.0991, −0.0308 | ARRI LogC4 specification |
| D-Gamut | 0.71, 0.31 | 0.21, 0.88 | 0.09, −0.08 | DJI white paper |
Matrices are derived from the chromaticities (SMPTE RP 177 normalised primary matrix); tests check the result against the published BT.709 RGB→XYZ and BT.2087 BT.709→BT.2020 matrices.
HDR transfer functions
- PQ (SMPTE ST 2084 / BT.2100): decoded to absolute cd/m², then divided by 203.
- HLG (ARIB STD-B67 / BT.2100): inverse OETF to scene light, then the BT.2100 OOTF
Fd = Lw·Ys^(γ−1)·Ewith Lw = 1000 cd/m², γ = 1.2 and Ys from BT.2020 luma, then ÷ 203. A 75 % HLG signal lands at reference white (BT.2408). HLG export applies the inverse OOTF and the OETF.
Tone mapping (HDR/log → SDR)
When an HDR or log source enters a Rec. 709 working space with Auto Tone Map Media on (the default), and on the monitors of an HDR sequence, FilmCraft applies the ITU-R BT.2390 EETF:
- m = max(R, G, B) in working units, converted to cd/m² (×203) and PQ-encoded, normalised by the PQ value of the source peak;
- below the knee KS = 1.5·maxLum − 0.5 (maxLum = PQ(target peak)/PQ(source peak)) the value is unchanged; above it a Hermite spline maps the rest of the range onto [KS, maxLum];
- RGB is scaled by m'/m, which keeps hue and saturation.
Source peak: the content's mastering peak for PQ when known (default 1000 cd/m²), 1000 cd/m² for HLG, and for log media the curve's own peak capped at 4000 cd/m² (so the knee starts above mid grey). Target peak: 203 cd/m² = SDR white. HDR reference white then lands at ≈ 0.79 linear (≈ 90 % on the sRGB curve).
Grading signal (grade)
Lumetri's controls work on a 0…1 signal. GradeSpace picks it from the working space:
| Working space | Signal of working-linear c (1.0 = 203 cd/m² in HDR) |
1.0 means |
|---|---|---|
| Rec. 709 | sRGB curve, clamped back to 0…1 | SDR white |
| Rec. 2100 PQ | PQ⁻¹(c·203/10000) / PQ⁻¹(W/10000) (SMPTE ST 2084) |
HDR White W cd/m² |
| Rec. 2100 HLG | F = c·203/W, Y = BT.2020 luma(F), E = F·Y^((1−γ)/γ), signal = HLG OETF(E), γ = 1.2 + 0.42·log10(W/1000) (BT.2100 inverse OOTF, note 5f) |
display peak W |
W is Lumetri's HDR White (Basic Correction) or HDR Range (Curves), default 1000 cd/m². HDR
values above W are signals above 1 and are kept (PQ to 10 000 cd/m², HLG to signal 1.5). Checked
values: PQ 100 / 203 / 1000 cd/m² = 0.508 / 0.581 / 0.752 of the plain PQ signal (BT.2408); HLG on
a 1000 cd/m² display: 203 cd/m² = 0.75, 26 cd/m² (18 % grey) ≈ 0.38 (BT.2408).
HDR static metadata
HdrMetadata (mastering display max / min luminance, MaxCLL, MaxFALL; 0 = unknown) gives the
tone-mapping source peak of PQ media: MaxCLL when known (the brightest pixel of the content),
else the mastering peak, clamped to 203…10 000 cd/m²; without metadata 1000 cd/m² is assumed.
Gamut mapping
- Input (camera gamuts wider than the working gamut): per-channel distance compression towards the achromatic axis, after the ACES Reference Gamut Compression: for each channel d = (max − c)/max; distances below 0.8 are unchanged, distances from 0.8 to 1.25 are compressed into 0.8–1.0 with a power-1.2 curve. Colours within 80 % of the boundary are untouched.
- Display-referred wide-gamut input (BT.2020 / P3 / PQ / HLG media into a narrower working gamut) and
- Output (BT.2020 working → BT.709 monitor/export): colours with a negative component are
desaturated towards their BT.709 luminance just enough to fit (
desaturate_into_gamut); in-gamut colours, such as BT.709 media in a wide-gamut sequence, come back exactly.
LUTs
.cube: the published Cube LUT 1.0 text format —TITLE,LUT_1D_SIZE(2–65536),LUT_3D_SIZE(2–256),DOMAIN_MIN/DOMAIN_MAX, legacyLUT_1D_INPUT_RANGE/LUT_3D_INPUT_RANGE, comments, CRLF. A file with both sizes is a 1D shaper followed by a cube. Red changes fastest..3dl: optional3DMESH/Mesh <in> <out>header, the input mesh line (non-uniform meshes become a 1D shaper),n³integer rows with blue fastest; output depth from the header or the largest value. We write 33-point, 10-bit-mesh, 12-bit-output files.- 3D interpolation is tetrahedral. Tests compare it with a brute-force reference (barycentric coordinates in each of the six Kuhn tetrahedra, solved with a 3×3 inverse) on random points for sizes 2–33, check exactness on affine LUTs and at grid nodes, and round-trip both formats.
FilmCraft ships no LUT files. Its built-in looks and camera conversions are generated from code
(filmcraft-render), and are written to disk only when the user exports them.
Tests
cargo test -p filmcraft-color: encode∘decode round trips for every curve (−1 % to 8× and over
the full signal range in both file ranges), published reference points (S-Log3 95/420/598, V-Log
128/433/602, LogC3 18 % → 0.391, LogC4 18 % → 0.2784, Canon Log 2/3 18 % → 39.2 %/34.3 % IRE, Apple
Log 18 % → 0.488, D-Log 0 → 0.0929), PQ/HLG reference white, EETF continuity/monotonicity and peak,
hue preservation, gamut compression, BT.709/BT.2087 matrices, LUT interpolation and file round
trips.