A Go library and toolset for working with LUTs (Look-Up Tables) for video and photo color grading.
Prism is a comprehensive LUT manipulation tool designed to fill a gap in Linux tooling for reliable LUT processing. Whether you're working with color grades for video production, photography, or camera profiles (like those for the Panasonic Lumix S9), Prism provides a seamless workflow for converting, applying, and blending LUTs.
- Format Conversion: Convert freely between CUBE, HALD PNG and Panasonic VLT
- LUT Application: Apply one or a chain of LUTs to an image with per-LUT intensity
- Log Workflows: Convert log footage to Rec.709, and rebase log LUTs onto Rec.709 input
- LUT Algebra: Compose LUTs into one, invert a LUT, blend LUTs with weighted interpolation
- LUT Extraction: Recover a LUT from an ungraded/graded image pair
- Tetrahedral Interpolation: Keeps the neutral axis exactly neutral
- 16-bit Output: Images and HALD PNGs are written at 16 bits per channel
- Pure Go: No external dependencies
- CUBE LUT Support: Full read/write support for the CUBE LUT format
- HALD PNG Support: Full read/write support for HALD CLUT images
- VLT Support: Panasonic Varicam/Lumix
.vltLUTs (12-bit integer samples) - LUT Operations:
- Convert between any pair of the three formats
- Apply a chain of LUTs to an image with variable intensity
- Compose LUTs, invert LUTs, blend LUTs with weighted interpolation
- Extract a LUT from a before/after image pair
- Sum, scale, clamp and rescale LUT values
- Tetrahedral and Trilinear Interpolation: Tetrahedral by default
- Pure Go Implementation: No external dependencies
Build the command-line tool:
go buildThe binary will be available as ./prism in the current directory.
The prism command-line tool provides a comprehensive interface for LUT manipulation without requiring code.
Display the general help message:
prism helpGet help for a specific command:
prism help apply
prism help convert
prism help blend| Command | Purpose |
|---|---|
apply |
Apply one or more LUTs to an image |
convert |
Convert between CUBE, HALD PNG and VLT |
compose |
Bake a chain of LUTs into a single LUT |
invert |
Compute the numerical inverse of a LUT |
delog |
Rebase a log-input LUT onto display-referred input |
extract |
Derive a LUT from an ungraded/graded image pair |
blend |
Blend two LUTs together |
identity |
Generate an identity HALD PNG |
Flags may appear anywhere in the command line, before or after the file arguments.
Apply one or more LUTs to an image, in the order given.
prism apply [OPTIONS] LUT[:INTENSITY]... IMAGEOptions:
-o, -out FILE— write output to FILE (default:IMAGE.prism.EXT)-i, -interp MODE—tetra(default) ortri
Examples:
# Convert V-Log footage to Rec.709
prism apply vlog-to-rec709.cube clip.png -o clip709.png
# Convert and grade in one pass, look at 60% strength
prism apply vlog-to-rec709.cube look.cube:0.6 clip.png
# Old-style trilinear interpolation
prism apply -i tri mylut.png photo.jpg -o out.jpgPNG output is written at 16 bits per channel, so a log-to-display conversion does not band.
prism convert [OPTIONS] INPUT OUTPUTAny of .cube, .png (HALD) and .vlt converts to any other; the format is
taken from the file extension.
Options:
-t, -title TITLE— title metadata for CUBE output-s, -size SIZE— output lattice size, or HALD level for.png. The default keeps the source size, falling back to 33 for CUBE and 17 for VLT when the source is a dense HALD.
prism convert mylut.cube mylut.png
prism convert mylut.cube mylut.vlt # for a Lumix S9
prism convert -t "My Grade" mylut.png mylut.cubeBake a chain of LUTs into one. Each LUT is applied to the output of the previous, so the result is equivalent to applying them in order.
prism compose [OPTIONS] LUT... -o OUTPUTUse it when a look outputs log and you want it to output Rec.709:
prism compose look-vlog-out.cube vlog-to-rec709.cube -o look-rec709.cubeCompute the LUT that undoes another LUT.
prism invert [OPTIONS] LUT -o OUTPUTprism invert vlog-to-rec709.cube -o rec709-to-vlog.cubeA LUT that clips or crushes is not invertible in those regions — the inverse settles on the closest preimage there. In practice this means deep sub-black and out-of-gamut colours will not round-trip.
Rebase a LUT that expects log input so that it expects Rec.709 input instead, given the camera's log-to-display conversion LUT.
prism delog -c CONVERSION [OPTIONS] LUT -o OUTPUTprism delog -c vlog-to-rec709.cube look-for-vlog.cube -o look-for-rec709.cubeIt is shorthand for inverting the conversion LUT and composing it in front:
prism invert vlog-to-rec709.cube -o inv.cube
prism compose inv.cube look-for-vlog.cube -o look-for-rec709.cubeDerive the LUT that turns one image into another. Both images must be the same size and the same frame, one ungraded and one graded.
prism extract [OPTIONS] SOURCE GRADED -o OUTPUTOptions:
-s, -size SIZE— output lattice size, HALD level for.png(default 33)-r, -smooth W— smoothing weight, higher is smoother (default 0.05)
The cleanest source is an identity HALD, because it covers the entire colour cube:
prism identity -s 8 -o identity.png
# grade identity.png in any image editor, save it as graded.png
prism extract identity.png graded.png -o look.cubeA photograph works too, but it only constrains the colours it actually
contains; the rest of the cube is filled in smoothly and left near-neutral.
Raise -r for noisy or heavily compressed pairs.
Blend two LUTs of the same format with weighted interpolation. Blending
averages two looks — to stack them instead, use compose.
prism blend [OPTIONS] LUT1[:INTENSITY1] LUT2[:INTENSITY2]Options:
-c, -clamp— clamp output LUT to valid range (default: true)-o, -out FILE— write output to a file (default: stdout)-t, -title TITLE— title metadata for CUBE output
prism blend lut1.cube:0.7 lut2.cube:0.3 -o blended.cubeGenerate an identity HALD PNG, the starting point for extract.
prism identity [-s LEVEL] [-o FILE]-s is the HALD level; the lattice is level² per axis and the image is
level³ pixels square. The default is 12.
The lut package holds a format-agnostic 3D LUT that CUBE, HALD PNG and VLT
all convert to and from losslessly. It is the easiest entry point.
package main
import (
"log"
"github.com/NicoNex/prism/lut"
)
func main() {
conv, err := lut.LoadFile("vlog-to-rec709.cube")
if err != nil {
log.Fatal(err)
}
look, err := lut.LoadFile("look-for-vlog.cube")
if err != nil {
log.Fatal(err)
}
// Rebase the look onto Rec.709 input and save it as a VLT.
rebased := lut.Compose(lut.Invert(conv, 0), look, 0)
if err := lut.SaveFile("look-rec709.vlt", rebased, 0, "Look"); err != nil {
log.Fatal(err)
}
}Applying a LUT to an image:
l, err := lut.LoadFile("mylut.cube")
if err != nil {
log.Fatal(err)
}
f, err := os.Open("photo.png")
if err != nil {
log.Fatal(err)
}
defer f.Close()
src, _, err := image.Decode(f)
if err != nil {
log.Fatal(err)
}
// Full strength, tetrahedral. Returns a 16-bit *image.NRGBA64.
out := l.Apply(src, 1.0, lut.Tetrahedral)
// Or half strength with the classic trilinear interpolation.
out = l.Apply(src, 0.5, lut.Trilinear)Extracting a LUT from an image pair:
before, _, _ := image.Decode(f1)
after, _, _ := image.Decode(f2)
// 0 selects the default lattice size (33) and smoothing.
l, err := lut.Extract(before, after, 0, 0)
if err != nil {
log.Fatal(err)
}
lut.SaveFile("recovered.cube", l, 0, "Recovered")The cube, hald and vlt packages remain available for direct access to
each file format, including the blending operations.
# 1. See the footage in Rec.709
prism apply vlog-to-rec709.cube clip.png -o clip709.png
# 2. Apply the conversion and a look together
prism apply vlog-to-rec709.cube teal-orange.cube:0.7 clip.png -o graded.png
# 3. Bake that whole chain into one LUT for the camera
prism compose vlog-to-rec709.cube teal-orange.cube -o baked.cube
prism convert baked.cube baked.vltA look built for V-Log will not work on footage that is already Rec.709. Rebase it:
prism delog -c vlog-to-rec709.cube look-for-vlog.cube -o look-for-rec709.cube
prism apply look-for-rec709.cube already-graded.pngGrade an identity HALD in whatever editor you like, then extract the LUT:
prism identity -s 8 -o identity.png
# open identity.png in your editor, grade it, save as graded.png
prism extract identity.png graded.png -o mylook.cube
prism apply mylook.cube any-photo.jpgThis also works from a matched pair of real photographs, though a photograph only pins down the colours it contains.
prism convert Panasonic_S9_Profile.cube Panasonic_S9_Profile.pngfor image in *.png; do
prism apply mylut.cube "$image"
done.
├── cube/ # CUBE LUT format: parsing, writing, blending
├── hald/ # HALD CLUT format: parsing, writing, blending
├── vlt/ # Panasonic VLT format: parsing, writing, blending
├── lut/ # Format-agnostic LUT: interpolation, compose, invert, extract
├── main.go # Command-line interface
├── options.go # Flag parsing and help text
└── README.md # This file
apply defaults to tetrahedral interpolation, which splits each lattice
cell into six tetrahedra and interpolates barycentrically within the one
containing the sample. Its useful property is that it reproduces the neutral
axis exactly: if a LUT maps grey to grey, tetrahedral interpolation keeps
greys neutral, whereas trilinear introduces a small colour cast between
lattice points. Trilinear is still available with -i tri.
invert seeds the inverse by pushing the source lattice forward and splatting
it into the output lattice, diffuses that into nodes the forward mapping never
reaches, then refines every node with damped Gauss-Newton
(Levenberg-Marquardt) against a finite-difference Jacobian. Accuracy is
limited only by how invertible the input LUT actually is — a LUT that clips
highlights or crushes blacks has destroyed the information there, and no
inverse can bring it back.
extract treats the problem as regularised least squares: find the lattice
L minimising
‖A·L − graded‖² + smoothing·‖∇(L − identity)‖² + mu·‖L − identity‖²
where A is trilinear interpolation at each source pixel. Because every pixel
touches the 8 corners of a single lattice cell, AᵀA is nonzero only between
nodes at most one step apart, so it fits in a 27-point stencil that one pass
over the image fills in; the system is then solved directly by over-relaxed
Gauss-Seidel. The smoothness term is applied to the difference from identity
rather than to the LUT itself — penalising the LUT's own gradient would be
minimised by a constant LUT, flattening the whole cube to one colour.
CUBE is a text 3D LUT with a title, domain bounds and one RGB triple per lattice node, red varying fastest.
HALD PNG encodes the lattice as an image: a HALD of level L carries an L²-per-axis lattice laid out across an L³ × L³ image. Prism writes them at 16 bits per channel, since 8 bits quantises a LUT to 256 steps per channel and visibly bands log-to-display conversions. Both depths are read.
VLT is Panasonic's Varicam/Lumix format: 12-bit integer samples in
[0, 4095], typically on a 17³ lattice.
Licensed under the GNU General Public License v3.0 - see the LICENSE file for details.
Contributions are welcome! Whether you have bug reports, feature requests, or code improvements, please feel free to open an issue or submit a pull request.