agentleFS
isaacrowntreeisaacrowntree/color-grade-ai/SKILL.md

color-grade

Skill17 starsChanged 3 months agoMITView on GitHub

Generate .cube 3D LUTs for color correction in DaVinci Resolve and Adobe Premiere Pro. Analyzes a frame or clip, fits the correction by measurement, and emits a ready-to-use LUT. Detects log vs display-referred footage, matches one shot to another, and previews grades live in-browser. Use when the user needs color grading, LUT generation, fixing a colour cast or exposure, matching shots, or frame analysis.

1 other project uses a version of this.

Adopted by

Most recently taken up by

  • Other versions1 project uses a version of this.

    Word for word · 0

    None found.

    Edited · 0

    None found.

    Wrote their own version · 1

    1. dongnguyenvie/bashcut-agent-kit★ 6same name

    How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.

The file

We split this skill into the rules it gives an agent

7 rules. About 0 are common, 6 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈6

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Bypass/reset: all nodes instantly

    line 100
  2. Very specific:

    All LUT types are defined in presets.yml.

    line 110
  3. Distinctive:

    Use 65x for post, 33x for in-camera/monitoring

    line 220
  4. Distinctive:

    DaVinci Resolve: Add a serial node per LUT.

    line 240
  5. Distinctive:

    Every LUT and every target in this repo assumes display-referred Rec.709, applied *after* a camera conversion LUT.

    line 251
  6. Distinctive:

    Desaturating warm tones in HSL produces brown/sepia: Use hue shifting or RGB rebalancing instead.

    line 407
  7. Distinctive:

    Skin tones occupy H=10-35, S=0.08-0.45, L=0.25-0.75: Use all three windows to isolate from light sources.

    line 408

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Color Grade AI

Generate targeted .cube 3D LUTs for color correction.

Point it at a frame or a clip and it measures what is wrong, fits the correction by applying candidates and re-measuring, and hands you a .cube. It states whether the footage is log or display-referred before it does anything, because every target here assumes display-referred Rec.709. Interactive in-browser preview generates LUTs live and exports the chain you are looking at.

Quick Start

# Generate a correction LUT (presets defined in presets.yml)
ruby generate_lut.rb <type> <output_path> [--strength=0.0-1.0]

# Bake multiple presets into a single .cube LUT
ruby generate_chain_lut.rb <output_path> <preset@strength> ...

# Is this log or display-referred Rec.709?
python3 footage_type.py <frame.png>

# Analyse a frame, and optionally fit and emit the correction
python3 auto_grade.py <frame.png> [--emit fix.cube] [--transfer log|display]

# Fit a correction without the analysis report
python3 solve_grade.py <frame.png> --emit fix.cube

# Fit one correction for a whole clip rather than one frame
python3 sample_clip.py <clip.mov> --emit fix.cube [--frames 12]

# Match one shot to another and emit the LUT
python3 match_grade.py <reference.png> <output.png> --emit match.cube

# Colour statistics for one region (skin, wall, costume...)
ruby analyze_frame.rb <image_path> <x1,y1,x2,y2> [label]
python3 analyze_region.py <image_path> <x1,y1,x2,y2> [label]   # JSON only

# Serve the interactive preview
python3 -m http.server 8080
# Open http://localhost:8080/preview.html

Auto-Grade Analysis

auto_grade.py analyzes a Rec.709 frame (post-conversion LUT) and recommends correction values for each node using classical color science:

NodeMethodTarget
Contrast/ExposureLuminance histogram percentilesMedian ~0.45, dynamic range ~0.85
White BalanceShades of Gray (Minkowski p=6) + White Patch blendRGB gains near 1.0
Skin ToneYCbCr skin-locus detection, I-line targetingHue ~20° (peach)
SaturationHasler-Susstrunk colorfulness metricno target — acts only outside 25-95
Black LevelBottom 5% luminance analysis + noise floorBlack point < 0.02
# Full workflow
ffmpeg -y -ss 19 -i video.mp4 -vframes 1 -vf "scale=1280:-1" -update 1 frame_raw.png
ffmpeg -y -i frame_raw.png -vf "lut3d='conversion.cube':interp=tetrahedral" -update 1 frame_709.png
python3 auto_grade.py frame_709.png

Match Grade

match_grade.py fits a correction that moves one frame toward another and emits the .cube. Same closed-loop method as solve_grade.py — apply, re-measure, keep what closes the gap — but the target is a reference image rather than a set of fixed ideals.

python3 match_grade.py <reference.png> <output.png> --emit match.cube

It matches channel balance, exposure, black level and saturation, and refuses to tone-match across transfer curves: matching log against Rec.709 is a missing conversion LUT, not a grading problem.

Interactive Preview (preview.html)

Browser-based node chain previewer using Preact + HTM (~4KB). Correction LUTs are generated live from presets.json at the exact strength requested, not fetched pre-baked — so the slider interpolates parameters, the same operation the CLI performs, and what you see is what generate_lut.rb emits.

Features:

  • Drag-and-drop any frame (raw S-Log3 or converted)
  • Node 1: Conversion LUT — add your own .cube files (not included in repo)
  • Nodes 2-6: Correction chain — dropdown presets + strength sliders
  • Export .cube — bakes the whole visible chain to a file you can drop into Resolve, so the preview is not a separate thing you have to reproduce
  • Creative presets — one-click curated looks (Studio Dance, Studio Clean, Studio Ambient, Studio Gold, Studio Film)
  • S-Gamut3 → Cine compensation toggle for gamut mismatch
  • Bypass/reset all nodes instantly
  • URL parameters — deep-link to a specific state: ?image=frame.png&lut=path/to.cube&preset=studio_gold

The preview generates at a 17-grid (~30ms, keeps up with a slider) and exports at 33. Conversion LUTs are user-provided (gitignored in luts/); the pre-baked correction LUTs in correction_luts/ remain for download-only users who never run the tools.

Config-Driven Presets

All LUT types are defined in presets.yml. Each preset is a pipeline of ordered processing steps. Adding a new LUT type means adding a YAML entry — no Ruby code changes needed.

Available step types: exposure, highlight_protect, black_crush, hue_desat, skin_correction, shadow_sat_boost, skin_highlight, skin_rolloff, global_highlight_desat, rgb_rebalance, global_sat.

Available LUT Types

Correction LUTs (apply after conversion)

TypeWhat it fixes
night_warm_fixAll-in-one for underexposed warm/red scenes. ~1 stop lift + skin hue shift + black crush.
night_purple_fixAll-in-one for underexposed purple/magenta stage lighting. RGB rebalancing + lift + desat + skin fix.
yellow_fixWarm amber/yellow cast from stage lighting. H=10-60, 55% desat.
red_skin_fixRed/flushed skin from warm practicals. Hue shift to peach, skin-only targeting.
pink_cast_fixPink/magenta cast from stage lighting. Gentle RGB rebalance + magenta desat.
overexposure_fixScene-wide ~1 stop reduction with highlight rolloff.
underexposure_fixScene-wide ~1.2 stop lift with shadow recovery.
black_crushCrushes milky/lifted blacks below 12% to true black.
skin_highlight_fixSubtle skin-only highlight rolloff above 70% luminance.
golden_warmStrong golden warm shift via RGB rebalance (R:1.20, G:1.00, B:0.80). For rich warm cinematic looks.
cinema_darkDeep moody contrast with gamma 1.45, shadow lift 0.02, and knee compression (0.72-0.90).

Node Chain Building Blocks

TypeNodeWhat it does
studio_punchContrastSubtle contrast boost for well-lit interiors
film_contrastContrastStronger filmic contrast with deeper shadows
flat_liftContrastLifts shadows for softer, more open look
warm_shiftTemperatureSubtle warm shift via RGB rebalance
cool_shiftTemperatureSubtle cool shift via RGB rebalance
led_green_fixTemperatureFixes green tint from LED/fluorescent lights
sat_boostSaturationGlobal +15% saturation boost
golden_warmTemperatureStrong golden warm shift (R:1.20, G:1.00, B:0.80)
sat_reduceSaturationGlobal -15% saturation reduction
sgamut3_to_cineGamutS-Gamut3 → Cine compensation (desat ~12%)
cinema_darkContrastDeep moody contrast (gamma 1.45, shadow lift, knee compression)
black_liftBlack LevelLifts blacks for vintage/faded look

Creative Presets (node chain combinations)

PresetLookChain
Studio CleanNatural brightstudio_punch(50%) + sat_boost(50%)
Studio BalancedWarm naturalstudio_punch(80%) + warm_shift(30%) + sat_boost(50%) + black_crush(15%)
Studio DanceWarm cinematicstudio_punch(100%) + warm_shift(40%) + sat_boost(60%) + black_crush(25%)
Studio AmbientNo fill lightstudio_punch(80%) + warm_shift(100%) + sat_boost(100%) + black_crush(20%)
Studio GoldWarm cinematiccinema_dark(80%) + golden_warm(75%) + sat_boost(100%) + black_crush(10%)
Studio FilmMoody cinematicfilm_contrast(60%) + warm_shift(20%) + sat_reduce(30%) + black_crush(40%)

Baking Chain LUTs

generate_chain_lut.rb bakes multiple presets at different strengths into a single .cube file. This lets you apply a creative preset as one node in Resolve instead of multiple.

# Bake Studio Balanced into a single LUT
ruby generate_chain_lut.rb correction_luts/studio_balanced_baked.cube \
  studio_punch@0.8 warm_shift@0.3 sat_boost@0.5 black_crush@0.15

# Bake Studio Dance
ruby generate_chain_lut.rb correction_luts/studio_dance_baked.cube \
  studio_punch@1.0 warm_shift@0.4 sat_boost@0.6 black_crush@0.25

Pre-baked LUTs for all creative presets are committed in correction_luts/.

You can also bake a conversion LUT + creative chain into a single .cube for one-node grading in Resolve:

# Bake conversion + creative into a single pass LUT
ruby generate_chain_lut.rb final_grade.cube \
  --conversion=luts/SLog3_to_Rec709.cube \
  cinema_dark@0.8 golden_warm@1.0 sat_boost@1.0 black_crush@0.1

Video Export Workflow

Apply conversion + correction LUTs and encode to H.265 in one pass using ffmpeg.

# Extract a sample frame for analysis
ffmpeg -y -ss 19 -i input.mp4 -vframes 1 -vf "scale=1280:-1" -update 1 frame_raw.png

# Apply conversion LUT to frame for preview
ffmpeg -y -i frame_raw.png -vf "lut3d='conversion.cube':interp=tetrahedral" -update 1 frame_709.png

# Full encode: conversion + correction → H.265 4K
# 50Mbps masters (for re-editing):
ffmpeg -i input.mp4 \
  -vf "lut3d='conversion.cube':interp=tetrahedral,lut3d='correction.cube':interp=tetrahedral,format=nv12" \
  -c:v hevc_videotoolbox -b:v 50M -spatial_aq 1 \
  -c:a aac -b:a 192k -tag:v hvc1 output_master.mp4

# 15Mbps web-optimized (for Instagram/web):
ffmpeg -i input.mp4 \
  -vf "lut3d='conversion.cube':interp=tetrahedral,lut3d='correction.cube':interp=tetrahedral,format=nv12" \
  -c:v hevc_videotoolbox -b:v 15M -spatial_aq 1 \
  -c:a aac -b:a 128k -tag:v hvc1 output_web.mp4

Notes:

  • hevc_videotoolbox uses Apple Silicon hardware encoding (M1/M2/M3)
  • format=nv12 converts to YUV420p for compatibility
  • -tag:v hvc1 ensures QuickTime/browser playback compatibility
  • spatial_aq 1 enables adaptive quantization for better quality

Conversion LUTs (not in repo)

Conversion LUTs (e.g. Sony S-Log3 → Rec.709, ARRI LogC → Rec.709) are camera-specific and often commercially licensed. Store them in luts/ (gitignored).

Phantom LUTs (phantomluts.com) — ARRI Alexa709 emulation for Sony cameras:

  • Location: luts/a7s3-arri-g8/A7s3 Phntm Arri LUTs G8/65x/SLog3/Neutral A7s3_65x.cube
  • Variants: Neutral, IceBlue, Jamaica, Tungsten, Utopia (Standard + Legacy versions)
  • Use 65x for post, 33x for in-camera/monitoring
  • Designed for S-Log3 + S-Gamut3.Cine; enable Cine comp toggle for S-Gamut3 footage

Resolve settings: Colour Science = DaVinci YRGB, Timeline = Rec.709-A (Mac), 3D LUT interpolation = Tetrahedral

Node Order (DaVinci Resolve)

Professional serial node chain:

  1. Conversion LUT (camera log → Rec.709)
  2. Contrast / Exposure
  3. White Balance / Color Temperature
  4. Skin Tone Correction
  5. Color Saturation
  6. Black Levels
  7. (Noise Reduction — not achievable via LUT)
  8. (Vignette — not achievable via LUT)
  9. (Sharpening — not achievable via LUT)

Applying LUTs

DaVinci Resolve: Add a serial node per LUT. Right-click node → LUT → browse to .cube file.

Adobe Premiere Pro: Lumetri Color → Creative → Look dropdown → browse to .cube file.

ffmpeg (batch/preview):

ffmpeg -i input.mp4 -vf "lut3d='conversion.cube':interp=tetrahedral,lut3d='correction.cube':interp=tetrahedral" output.mp4

Log vs Display-Referred

Every LUT and every target in this repo assumes display-referred Rec.709, applied after a camera conversion LUT. Handed log footage, the analyzer sees lifted blacks and flat contrast, calls them defects, and fits a correction for something that is supposed to be there. The result looks plausible and is wrong, which is worse than failing — so the distinction is measured and stated out loud on every run.

python3 footage_type.py frame.png            # just ask what it is
python3 auto_grade.py frame.png --transfer log
python3 solve_grade.py frame.png --transfer display --emit fix.cube

Detection reads the shape of the encoding, not metadata, because by the time a frame reaches the tool it is usually a PNG:

SignalLog looks like
Black pointlifted well above zero (S-Log3 puts black near 0.09)
Highlightsheld back, nothing approaching 1.0
Rangecompressed into a narrow band
Puritydesaturated, pre-conversion
Midpoint positionmedian sits high within the range

Three outcomes, not two. A heavily flattened, lifted, desaturated display-referred grade is genuinely indistinguishable from log by shape alone, so there is an explicit ambiguous verdict. Guessing either way is worse: calling it log blocks a legitimate grade, calling it display produces a confidently wrong one. Ambiguous asks you, which you can always answer and the measurements never can.

The midpoint signal is what separates the two where anything can: rescaling a display-referred image leaves the median's relative position untouched, while a log transfer moves it.

What changes on log or ambiguous footage. Exposure and black level are held back, because a 0.45 median and a 0.02 black point describe a graded image, not a log container. White balance and skin hue still run — a cast is a cast, and skin should read as skin, whatever the transfer curve. --transfer overrides detection entirely.

Closed-Loop Grading

Rather than reporting recommendations for you to transcribe, the tool can fit a correction by measurement and hand you the LUT:

# Frame in, correction LUT out
python3 auto_grade.py frame.png --emit fix.cube

# Same thing without the analysis report
python3 solve_grade.py frame.png --emit fix.cube

# Fit against a whole clip instead of one arbitrary frame
python3 sample_clip.py clip.mov --emit fix.cube --frames 12

How the strengths are chosen. The solver applies a candidate correction, re-measures the frame, and keeps the value that actually minimises the remaining error. Earlier versions guessed with hand-tuned constants (deviation * 10), which had no feedback at all.

Corrections are synthesised, not just selected. The preset library is deliberately gentle — cool_shift is a 4% channel shift at full strength — so no combination of library presets can neutralise a 22% tungsten cast. For white balance, exposure and black level the solver fits the parameters directly from the measurement. Skin correction still uses the tuned red_skin_fix preset, because that is a shape correction rather than a magnitude.

What it measures (grade_metrics.py, reference-free — no pristine reference needed):

MeasurementTargetRuns on log?
White balanceneutral illuminant, skin and saturated props excludedyes
Skin hue20 degreesyes
Exposuremedian luminance 0.45no
Black levelblack point 0.02no
Saturationno target — only acts outside a plausible bandno

Saturation has no target on purpose. Colourfulness is a property of the scene: a grey warehouse is legitimately drab and a fruit market legitimately vivid. The evaluation scene measures 63.8 against auto_grade's old fixed target of 45 — fitting to that number would "correct" a scene that was already right. So the solver stays silent inside a wide band and only acts when saturation has left any plausible range. It is display-only for the same reason the tone stages are: log measures 37.6 against 63.8 for the same scene in Rec.709, because it is desaturated by design.

Skin detection uses the YCbCr skin locus rather than an HSV box. An HSV box that accepts skin also accepts wood, khaki and amber practicals — "warm and mid-bright" is not a description of skin. The locus tolerance is asymmetric: generous toward flushed and sunburnt skin, which is exactly what red_skin_fix exists to correct, and tight toward the wood and amber direction.

Clip analysis samples across the clip, aggregates with the median so one blown frame cannot steer the grade, and warns when the clip varies too much to deserve a single LUT.

Measured on the synthetic evaluation set (eval_scenes.py, eleven known defects), closed-loop grading removes a mean of 81% of the introduced error, worst case 60%.

Interactive LUT Generation

preview.html generates LUTs live in the browser from presets.json rather than fetching pre-baked files, and can export exactly what you are looking at via Export .cube.

This also fixed a real defect: the preview used to interpolate the result of a full-strength LUT against the original, while the CLI interpolates the parameters. For studio_punch at 50% those diverge by up to a quarter of the range on saturated colours — greys agreed, which is why it went unnoticed.

The browser port (pipeline.mjs) is verified against every shipped .cube file in CI, so it cannot silently drift from the Ruby reference.

Tone Model (v2)

Tone operations — exposure, black_crush, highlight_protect, skin_rolloff, skin_highlight — run in linear light, not on gamma-encoded HSL lightness.

The pipeline decodes with the BT.1886 display EOTF (pure 2.4 gamma), applies the curve to Rec.709 luminance, and scales R, G and B by the resulting ratio. Because all three channels are scaled together, hue and saturation are preserved by construction. Where brightening pushes a colour outside the cube, it is desaturated toward its target luminance rather than clipped per channel, which would skew the hue.

Hue and saturation steps (hue_desat, skin_correction, global_sat, shadow_sat_boost, rgb_rebalance) still work in HSL, where perceptual behaviour is what you want.

What changed from v1. v1 applied tone curves to HSL lightness, L = (max + min) / 2, which is not luminance — a saturated red and a grey that look equally bright have very different L, so they were tonemapped by different amounts and colours drifted relative to greys. Rewriting L and converting back also quietly shifted hue and saturation.

Greys are bit-identical between v1 and v2. Only saturated colours move, by up to ~0.37 at the most saturated grid points. 14 of the 27 shipped LUTs changed; the 13 that are pure hue/saturation work are untouched.

To reproduce v1 output exactly:

ruby generate_lut.rb black_crush out.cube --legacy
ruby generate_chain_lut.rb out.cube studio_punch@0.8 sat_boost@0.5 --legacy
ruby regenerate_luts.rb --legacy

reference_checksums_legacy.yml pins the v1 output, and the test suite asserts --legacy still reproduces it byte-for-byte.

Color Science Notes

  • Desaturating warm tones in HSL produces brown/sepia. Use hue shifting or RGB rebalancing instead.
  • Skin tones occupy H=10-35, S=0.08-0.45, L=0.25-0.75. Use all three windows to isolate from light sources.
  • S-Gamut3 vs S-Gamut3.Cine: S-Gamut3 is wider. LUTs calibrated for .Cine may oversaturate S-Gamut3 footage. Use sgamut3_to_cine compensation.
  • 3D LUTs allow cross-channel operations — R output can depend on G and B input.
  • LUTs have no spatial awareness — no noise reduction, sharpening, or vignettes.

Requirements

To generate LUTs — Ruby 2.7+ and nothing else. generate_lut.rb, generate_chain_lut.rb and regenerate_luts.rb have no gems to install.

To analyse frames and fit corrections — Python 3 with:

pip3 install Pillow numpy PyYAML

PyYAML is needed because the solver reads presets.yml when it uses a library preset. Missing it fails only on the skin stage, which is an easy trap.

To sample clips — ffmpeg and ffprobe on PATH, used by sample_clip.py for frame extraction and duration probing.

To run the full test suite — additionally Node 20+, for the browser pipeline parity suite. Not needed to use the tool.

preview.html needs a static server (python3 -m http.server) rather than file://, because it loads presets.json and pipeline.mjs as modules.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.