Documentation
¶
Overview ¶
Package lut provides a format-agnostic 3D LUT: a single lattice of float64 samples that CUBE, HALD PNG and VLT files all convert to and from losslessly. Interpolation, application and the compose/invert/extract operations are implemented once here instead of once per file format.
Index ¶
- Constants
- Variables
- func SaveFile(path string, l *LUT, size int, title string) error
- func ToCube(l *LUT, size int, title string) cube.Cube
- func ToHALD(l *LUT, level int) image.Image
- func ToVLT(l *LUT, size int) vlt.VLT
- type Interp
- type LUT
- func Compose(a, b *LUT, size int) *LUT
- func Extract(src, dst image.Image, size int, smoothing float64) (*LUT, error)
- func FromCube(c cube.Cube) (*LUT, error)
- func FromHALD(h hald.HALD) (*LUT, error)
- func FromVLT(v vlt.VLT) (*LUT, error)
- func Invert(l *LUT, size int) *LUT
- func LoadFile(path string) (*LUT, error)
- func New(size int) *LUT
- type Sample
Constants ¶
const DefaultSmoothing = 0.05
DefaultSmoothing is the regularisation weight Extract uses when none is given. The data term is normalised per node, so a well-covered node carries a weight near 1; this leaves the measurement clearly in charge where there is data.
const MaxSolveSize = 65
MaxSolveSize caps the lattice Invert and Extract solve on. Both are iterative and cost O(size³) per sweep, and neither gains accuracy from a denser lattice than the data supports. Callers asking for more get a solve at this size that is then resampled up on write.
Variables ¶
Functions ¶
func SaveFile ¶
SaveFile writes a LUT to a .cube, .png (HALD) or .vlt file. size selects the output lattice size (for .png it is the HALD level); 0 picks a sane default.
Types ¶
type Interp ¶
type Interp int
Interp selects the interpolation used when sampling between lattice nodes.
const ( // Tetrahedral splits each lattice cell into 6 tetrahedra. It is the // default: unlike trilinear it reproduces the neutral axis exactly, so // greys stay neutral and log-to-display conversions do not pick up casts. Tetrahedral Interp = iota // Trilinear is the classic 8-corner interpolation. Trilinear )
func ParseInterp ¶
ParseInterp maps a CLI name to an Interp.
type LUT ¶
LUT is a 3D colour lookup table. Samples are indexed r + g*Size + b*Size², i.e. red varies fastest, matching the CUBE and VLT file layouts.
func Compose ¶
Compose returns the LUT equivalent to applying a and then b: out(x) = b(a(x)).
Use it to bake a chain into a single file. Two common cases:
Compose(look, vlogToRec709) // a look that outputs V-Log -> outputs Rec.709 Compose(rec709ToVlog, look) // a look that expects V-Log -> expects Rec.709
size is the output lattice size; 0 keeps the larger of the two inputs.
func Extract ¶
Extract recovers the LUT that best maps src onto dst, given a matched pair of images (the same frame before and after a grade).
A pair of images only constrains the colours it actually contains, so this is a scattered-data fit, not a lookup. It minimises
‖A·L − graded‖² + smoothing·‖∇L‖² + mu·‖L − identity‖²
where A is trilinear interpolation of the lattice at each source pixel. Well covered colours follow the data, sparse ones follow their neighbours, and colours absent from the image stay at identity — "leave unchanged" — rather than drifting to whatever the sparse data extrapolates.
The normal equations are built exactly rather than approximated. Every pixel touches the 8 corners of one lattice cell, so AᵀA is nonzero only between nodes at most one step apart on each axis: it fits in a 27-point stencil that one pass over the pixels fills in. Merely splatting each pixel onto the lattice and dividing by the accumulated weight — the cheap alternative — fits the local *mean* of the graded pixels around each node instead of the value at the node, which visibly biases the result wherever the grade is curved.
smoothing <= 0 uses DefaultSmoothing. Raise it for noisy or compressed sources, lower it when the pair covers the gamut densely and cleanly.
func FromHALD ¶
FromHALD converts a HALD CLUT image to a LUT. A HALD of level L carries an L²-per-axis lattice, so the conversion is exact.
func Invert ¶
Invert returns a numerical inverse of l: Invert(l)(l(x)) ≈ x.
It works in three stages. First the source lattice is pushed forward through l and splatted into the output lattice, which seeds every node that the forward mapping actually reaches. Second, nodes outside the forward gamut are filled by diffusing their neighbours so the whole lattice starts from a plausible guess. Third, every node is refined with damped Gauss-Newton (Levenberg-Marquardt) against a finite-difference Jacobian of l, which is what makes the result accurate rather than merely smooth.
A LUT that is not injective (clipped highlights, crushed blacks) has no true inverse there; those nodes settle on the closest preimage found.
func (*LUT) Apply ¶
Apply maps every pixel of img through the LUT. intensity blends between the original and the mapped colour. Output is 16-bit to keep the precision that tetrahedral interpolation buys; 8-bit output would quantise it away.
func (*LUT) Resample ¶
Resample returns the LUT re-evaluated on a lattice of the given size with a [0, 1] domain. Growing the size never adds detail; shrinking it loses some.
func (*LUT) Tetrahedral ¶
Tetrahedral samples the LUT by splitting the enclosing lattice cell into six tetrahedra and interpolating barycentrically within the one containing the point (Kasson et al.). The six cases below are the barycentric expansions of each tetrahedron, ordered by the ranking of the fractional coordinates.