Loads any Adobe .cube 3D LUT, HALD CLUT PNG, or 1D tone curve and applies it to a NumPy image in
one call — pure NumPy, no ffmpeg process, no OpenColorIO, no GPU.
Why a 3D lookup table
A global knob — saturation, an S-curve, a black point — moves every hue in an image the same way.
A real film stock’s look is hue-selective: it does one thing to skin and a different thing to
foliage, which needs a full three-dimensional lookup. Video tools reach .cube files through
ffmpeg’s lut3d filter; for a single still image already in memory, spawning an encoder just to
grade it is the wrong tool. pycube-lut is the still-image equivalent — a small NumPy module that
parses the table and trilinearly interpolates the pixels through it.
One call, three input shapes
| You have | load_cube gives you |
|---|---|
Adobe .cube with LUT_3D_SIZE | the table as-is |
| HALD CLUT PNG (a square grid image) | the same table, read from pixels |
.cube with LUT_1D_SIZE (three tone curves) | an equivalent separable 3D cube |
apply_cube(image, cube, strength) takes it from there. strength is opacity in [0, 1], and
the output dtype always matches the input — a 16-bit master stays 16-bit out, so a look laid over
it does not throw away its headroom.
Honest limits
Trilinear interpolation only — Resolve, Photoshop and ffmpeg’s own lut3d default to tetrahedral,
so expect small deviations on steep LUTs. 8-bit and 16-bit integer images only, no float and no
colour management: a LUT authored for log footage needs log input, and nothing here checks that
for you.
Requirements
Python 3.11+. NumPy only for .cube and 1D-curve files; reading a HALD CLUT PNG additionally
needs Pillow (the package’s hald extra pulls it in).
Documentation
pycube-lut’s documentation lives in its repository, next to the code it describes — the README is the manual.