Skip to content
Back to skills

Color Correction

ASecurity

Diagnose and fix video/image color OBJECTIVELY with the get_image (action:"analyze_color") tool (scopes/stats such as black/white points, contrast, saturation, clipping, cast) instead of eyeballing a contact sheet. Covers the "washed out" signature, why reference color-match (mkl/ColorMatch/ColorMatchAdobe) CAN'T add contrast a flat source lacks, the levels/contrast-stretch fix (core AdjustContrast / CurveEditor), the measure→fix→re-measure loop, the side-by-side sandbox pattern, and where to...

  • 778 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added August 31, 2026
ai-agentspythongobashnode

Works with

  • cli
  • mcp

Security analysis

A100/100

Scanned August 31, 2026

npx -y skills add artokun/comfyui-mcp --skill color-correction --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Color Correction?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Color Correction
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/artokun-color-correction/badge)](https://www.skillsdirectory.com/skills/artokun-color-correction)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: color-correction
description: Diagnose and fix video/image color OBJECTIVELY with the get_image (action:"analyze_color") tool (scopes/stats such as black/white points, contrast, saturation, clipping, cast) instead of eyeballing a contact sheet. Covers the "washed out" signature, why reference color-match (mkl/ColorMatch/ColorMatchAdobe) CAN'T add contrast a flat source lacks, the levels/contrast-stretch fix (core AdjustContrast / CurveEditor), the measure→fix→re-measure loop, the side-by-side sandbox pattern, and where to place the fix in a render graph (after decode, before save). Use when a render looks washed out / flat / dull / over-saturated / color-cast, or when deciding between a color-match and a contrast/levels fix.
globs:
  - "**/*.json"
  - "**/packs/**"
---

# Color Correction (measure, don't eyeball)

## The core principle

You cannot reliably judge color from a storyboard or contact sheet. "Is it washed
out?" flip-flops by eye, especially on AI-gen video. Make color measurable with the
`get_image (action:"analyze_color")` MCP tool, read the numbers like a colorist reads scopes, pick the
fix the data points to, then re-measure to confirm. The whole skill is this loop:

```
extract a frame ─► get_image (action:"analyze_color") ─► read black/white points + contrast + saturation
       ▲                                          │
       │                                          ▼
   re-measure  ◄──── apply fix (levels / contrast / match) ◄── diagnose from the numbers
```

> Origin: on a WAN-Animate render we argued for many turns over whether the clip was
> "washed out." The instant we measured it, the answer was unambiguous and the *correct*
> fix (a contrast stretch, not the color-match nodes we'd been adding) fell straight out.

---

## The `get_image (action:"analyze_color")` tool

Read-only. Source = `asset_id`, a ComfyUI output ref (`filename`/`subfolder`/`type`), or
an image `path` (absolute, or under the output dir). It returns per-image stats, heuristic
flags, a one-line verdict, and optionally an overlaid R/G/B/luma histogram PNG.

```
get_image({ action: "analyze_color", filename: "render_00007_.png" })       # absolute numbers
get_image({ action: "analyze_color", path: "frame.png", reference_path: "src.jpg" })  # + shot-match deltas
get_image({ action: "analyze_color", filename: "x.png", histogram: true })  # + histogram image
```

For videos, `get_image (action:"analyze_color")` is image-only (no ffmpeg dep). Extract a frame first with the
ComfyUI venv's cv2:

```bash
"<comfy-venv>/python" -c "import cv2; c=cv2.VideoCapture(r'IN.mp4'); n=int(c.get(7)); \
  c.set(1, n//2); _,f=c.read(); cv2.imwrite(r'frame.png', f)"
```

Grab the middle frame, or frame 0. For window-drift checks grab a frame from each window.

### What the numbers mean (8-bit)

| Field | Reads like a scope | Healthy-ish |
|---|---|---|
| `luma.blackPoint` (1st pct) | where shadows bottom out | ~0–16 (lifted if >16) |
| `luma.whitePoint` (99th pct) | where highlights top out | ~240–255 (dim if <235) |
| `luma.contrast` (std) | overall punch | ~45+ (flat if <45) |
| `luma.dynamicRange` | white−black | wide is good |
| `saturation.meanSaturation` (HSV S) | vectorscope spread | ~0.25+ (dull if <0.22) |
| `channels.{r,g,b}Mean` + `castHint` | RGB parade / white balance | spread <~12 = neutral |
| `luma.clippedHighPct/LowPct` | blown / crushed pixels | keep low (<~2%) |

Flags: `washedOut, lowContrast, liftedBlacks, dimHighlights, lowSaturation, colorCast`.

---

## The "washed out" signature

Washed out means compressed tonal range, and it has an exact fingerprint:

- `whitePoint` well below 255 (e.g. 191), so highlights never reach white
- `blackPoint` lifted off 0 (e.g. 45), giving milky shadows
- `contrast` low (std < 45)
- often *normal* saturation. Washout is usually not a saturation problem.

If you see that, the fix is a levels / contrast stretch, not a color match. In a real case,
a WAN-Animate frame measured blackPoint 45 / whitePoint 191 / contrast 43, clearly a range
problem; saturation 0.25 was fine.

---

## The load-bearing insight: a reference-match can't add contrast the source lacks

The instinct is to "match the render to the input photo" with a color-match node
(`ColorMatchV2` mkl/hm, `ImageColorMatchAdobe+`, easy `imageColorMatch`). Measure the
reference first. If the reference is itself flat (e.g. a casual phone selfie with
blackPoint 40, contrast 44), matching to it cannot produce punch; you'll match your way
to the *same* flat numbers. In the real case, mkl and Adobe matches both left the frame
flagged `washedOut` (whitePoint only crept from 191 to ~218).

So:

- Use a reference-match (`ColorMatchV2`, `ImageColorMatchAdobe+`) when you want to *match
  a known-good graded frame or shot-match across clips*, and the reference is actually good.
- Use a levels / contrast stretch when the defect is compressed range (the washout case).
  It targets full range *regardless* of the reference. This is usually the real fix.

---

## The fix: contrast / levels stretch (core nodes, no install)

`AdjustContrast` (core `comfy_extras.nodes_dataset`, category *image/adjustments*) has one
`factor` (1.0 = none, >1 = more). It pivots around mid-gray, so it pushes the white point up
and the black point down together. Tune it by measurement, not feel. A real measured sweep
on the washout frame:

| factor | blackPoint | whitePoint | contrast | sat | clippedHigh | verdict |
|---|---|---|---|---|---|---|
| 1.3 | 29 | 239 | 56 | 0.33 | **0%** | ✅ not washed (slightly soft) |
| ~1.4 | ~20 | ~247 | ~60 | ~0.38 | ~1–2% | ✅ **sweet spot** |
| 1.6 | 7 | 254 | 67 | 0.44 | **7.2%** ⚠️ | punchy but blows highlights |

Pick the factor that lands whitePoint ~248 to 255 with `clippedHighPct` < ~2%. Going too far
(1.6 here) blows highlights *and* over-warms, because per-channel contrast drops blue more than red and
`castHint` worsens. The sweet spot was ~1.4.

Other levers when contrast alone isn't enough:
- `CurveEditor` (core, *utilities*) feeds a CURVE for precise black/white-point and gamma
  control (a true levels curve) when you need more than a single contrast pivot.
- `AdjustContrast` plus a small saturation/brightness adjust (same *image/adjustments* family)
  to fine-tune after the stretch.
- A mild reference-match *after* the stretch, only if you need to shot-match.

---

## The sandbox pattern (test corrections side by side, then measure)

Don't tune blind on the full pipeline. Build a tiny separate workflow (`panel_new_workflow`)
and let one run produce several candidates you then measure:

```
LoadImage (the washed frame, staged into input/)
LoadImage (the reference, if shot-matching)
        │
        ├─► AdjustContrast factor 1.3 ─► SaveImage "contrast13"
        ├─► AdjustContrast factor 1.6 ─► SaveImage "contrast16"
        ├─► ColorMatchV2 (mkl, ref)   ─► SaveImage "balance_mkl"
        └─► ImageColorMatchAdobe+(LAB)─► SaveImage "balance_adobe"
```

Run once, then `get_image (action:"analyze_color")` every output, plus the reference and the untouched frame, and
compare blackPoint / whitePoint / contrast / saturation / clippedHigh. Whichever lands the
numbers in range wins; interpolate the factor (1.3 vs 1.6 gives 1.4) and confirm with one more
pass. Stage the frame into the ComfyUI input dir first (cp the extracted PNG there) so
`LoadImage` can see it. Input and output dirs may be custom, so don't guess paths.

---

## Porting the fix into a render graph

Place the chosen correction after decode, before the save. For a WAN/video graph that's
right after `WanVideoDecode` (or the per-chunk color-match) and feeding the
`VHS_VideoCombine`/save node. One `AdjustContrast` node is usually the whole fix. Keep it as a
single inline node (or a small bypassable group) so it's easy to toggle and re-tune. Re-render
a clip, extract a frame, `get_image (action:"analyze_color")` it, and nudge the factor to hit whitePoint ~250.

---

## Video-specific gotchas

- **Per-window drift (WAN long video).** WAN-Animate/long clips render in temporal windows and
  can desaturate or dim across them. Measure a frame from the *first* window and a *late* window.
  If they differ, that's drift, not a global grade issue; use the embeds' between-window
  `colormatch`, not a final stretch.
- **Relight LoRA is not levels.** A WanAnimate relight LoRA changes *lighting*, not black/white
  points. It won't fix a compressed-range washout. Measure before and after to prove it.
- **Don't over-stretch.** Watch `clippedHighPct`; blown highlights are unrecoverable. Prefer
  the lower factor that still clears `dimHighlights`.
- **Cast is often inherited.** If the reference has the same warm/cool cast (festival/sunset
  light), a matching cast on the render is *correct*. Don't "fix" it.

---

## Quick reference — the decision tree

```
get_image (action:"analyze_color") on the frame
  ├─ washedOut / lowContrast / dimHighlights ........ contrast/levels stretch (AdjustContrast ~1.4 → measure)
  ├─ lowSaturation only ............................. saturation boost (small)
  ├─ colorCast (and reference is neutral) ........... white-balance / neutralization, or reference-match
  ├─ want to MATCH a known-good graded frame ........ ColorMatchV2 / ImageColorMatchAdobe+ (ref must be good)
  └─ blackPoint/whitePoint already 0/255, sat ok .... color is healthy — stop touching it
```

Always re-measure after the fix. If `get_image (action:"analyze_color")` still flags it, the fix was wrong.
Adjust and measure again. Numbers over vibes.

## Sources

- **Official:** comfyui-mcp `get_image` action:"analyze_color" product behavior (this repo).
- **Empirical:** washed-out signature and levels-stretch recipes from observed renders.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.