lut

package
v0.0.0-...-d01fc0f Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 7, 2026 License: GPL-3.0 Imports: 14 Imported by: 0

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

View Source
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.

View Source
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

View Source
var (
	ErrTooSmall      = errors.New("LUT size must be at least 2")
	ErrEmpty         = errors.New("empty LUT")
	ErrSizeMismatch  = errors.New("images have different dimensions")
	ErrEmptyInput    = errors.New("no pixels to sample")
	ErrUnknownInterp = errors.New("unknown interpolation")
)

Functions

func SaveFile

func SaveFile(path string, l *LUT, size int, title string) error

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.

func ToCube

func ToCube(l *LUT, size int, title string) cube.Cube

ToCube converts a LUT to a CUBE file, resampling to size if it differs.

func ToHALD

func ToHALD(l *LUT, level int) image.Image

ToHALD renders a LUT as a HALD CLUT image of the given level. The image is 16-bit: an 8-bit HALD quantises a LUT to 256 steps per channel and visibly bands log-to-display conversions.

func ToVLT

func ToVLT(l *LUT, size int) vlt.VLT

ToVLT converts a LUT to a VLT, resampling to size if it differs.

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

func ParseInterp(s string) (Interp, error)

ParseInterp maps a CLI name to an Interp.

type LUT

type LUT struct {
	Title     string
	Size      int
	DomainMin Sample
	DomainMax Sample
	Samples   []Sample
}

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

func Compose(a, b *LUT, size int) *LUT

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

func Extract(src, dst image.Image, size int, smoothing float64) (*LUT, error)

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 FromCube

func FromCube(c cube.Cube) (*LUT, error)

FromCube converts a parsed CUBE file to a LUT.

func FromHALD

func FromHALD(h hald.HALD) (*LUT, error)

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 FromVLT

func FromVLT(v vlt.VLT) (*LUT, error)

FromVLT converts a parsed VLT file to a LUT. VLT samples are 12-bit.

func Invert

func Invert(l *LUT, size int) *LUT

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 LoadFile

func LoadFile(path string) (*LUT, error)

LoadFile reads a LUT from a .cube, .png (HALD) or .vlt file.

func New

func New(size int) *LUT

New returns an identity LUT of the given lattice size.

func (*LUT) Apply

func (l *LUT) Apply(img image.Image, intensity float64, in Interp) *image.NRGBA64

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) Eval

func (l *LUT) Eval(r, g, b float64, in Interp) Sample

Eval samples the LUT with the requested interpolation.

func (*LUT) Resample

func (l *LUT) Resample(size int) *LUT

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

func (l *LUT) Tetrahedral(r, g, b float64) Sample

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.

func (*LUT) Trilinear

func (l *LUT) Trilinear(r, g, b float64) Sample

Trilinear samples the LUT with the classic 8-corner interpolation.

func (*LUT) Valid

func (l *LUT) Valid() error

Valid reports whether the lattice is consistent and usable.

type Sample

type Sample struct {
	R, G, B float64
}

Sample is an RGB triple, nominally in [0, 1].

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL