ColorLookup
Real curves, typed as JSON, that don't clip your HDR
- image
- image
- grade_info
Brightness is an additive offset - it adds a constant to every pixel, flattens contrast, and clips highlights fast. A curve is a power shape: it moves the midtones while leaving white where it is. When an image looks flat, this is the node you want, not a brightness slider.
What it is
Five curve editors in one node: a master curve applied to R, G and B together, then per-channel red, green and blue curves applied afterwards. That's the classic grading order - shape the whole image's contrast first, then push colour balance per channel.
Except they aren't editors. They're string inputs, and you write them as JSON:
[[0.0,0.0],[0.25,0.25],[0.5,0.5],[0.75,0.75],[1.0,1.0]]
Each pair is [in, out]. The default is the identity diagonal - input equals output at every point, so a fresh node does nothing at all, which is exactly right.
Yes, typing curve points as JSON is less pleasant than dragging a spline. On the upside it's precise, it's copy-pasteable, it diffs in a workflow file, and you can generate it from a script. For a lift-the-shadows curve, an S-curve for contrast, or a cool-the-shadows warm-the-highlights split, it's four lines of arithmetic you do once.
The bit that matters for HDR
Most curve implementations clamp to 0–1 because the tool underneath only understands 8-bit video. This one extrapolates along the last segment beyond the endpoints. So if your curve's final segment has a slope of 1.5 and you feed it a value of 4.0, it comes back as something sensible and large instead of being crushed to 1.0.
That single behaviour is why this node belongs in an HDR chain. Pair it with clamp_output: off downstream and your super-whites survive the grade.
It also means the point positions are in the encoding of whatever arrives. Feed it display-referred sRGB and your points mean 0–1 sRGB values; feed it scene-linear and a point at 0.18 is mid-grey. Same curve, different picture. Decide which space you're grading in and don't move.
Inputs and outputs
Inputs: image, the four curve strings, and strength (0–1) which mixes between the untouched input and the fully graded result - a clean bypass dial for "how much of this grade do I want".
Optional: grade_info_in, which is upstream grade metadata. Read the tooltip carefully: it does not affect the grade. It's recorded in this node's output as the upstream field, forming a chain of what was applied where. Two outputs: image and that grade_info JSON.
Install
Part of Radiance. Manager → search Radiance → install, restart, refresh. Manually:
cd ComfyUI/custom_nodes
git clone https://github.com/fxtd-studios/radiance.git
cd radiance
python -m pip install -r requirements.txt
Windows portable: python_embeded\python.exe for the pip line. No models needed.
Where people get burned
Malformed JSON fails silently-ish. The parser catches exceptions and falls back to the identity curve, so a stray trailing comma gets you a node that runs, reports success, and changes nothing. If your grade "isn't applying", print the string and check it parses.
Curves are absolute, not relative. Applying the same curve twice doubles the shift. That sounds obvious until you've got a master curve and a per-channel curve both lifting the midtones and you're wondering why the skin went milky. Master for luminance, per-channel for balance, and check the total.
And the review trap: these curves act on the values that arrive, so if you grade in linear and preview on an sRGB canvas, the numbers you're setting don't correspond to what your eye is judging. Grade in a display space for look, in linear for technical work - but know which one you're in.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | Image to grade. Curves act on the values as they arrive, so point positions are in that encoding (0 to 1 for display-referred sRGB). | |
| master | STRING | [[0.0,0.0],[0.25,0.25],[0.5,0.5],[0.75,0.75],[1.0,1.0]] | JSON list of [in, out] points applied to R, G and B before the per-channel curves. The default diagonal is identity. |
| red | STRING | [[0.0,0.0],[0.25,0.25],[0.5,0.5],[0.75,0.75],[1.0,1.0]] | JSON list of [in, out] points for the red channel, applied after the master curve. The default diagonal is identity. |
| green | STRING | [[0.0,0.0],[0.25,0.25],[0.5,0.5],[0.75,0.75],[1.0,1.0]] | JSON list of [in, out] points for the green channel, applied after the master curve. The default diagonal is identity. |
| blue | STRING | [[0.0,0.0],[0.25,0.25],[0.5,0.5],[0.75,0.75],[1.0,1.0]] | JSON list of [in, out] points for the blue channel, applied after the master curve. The default diagonal is identity. |
| strength | FLOAT | 1.000–1 | Mix between the input (0) and the fully curved result (1). |
| grade_info_inopt | STRING | Upstream grade metadata. Not used for the grade; it is stored as the 'upstream' field of the grade_info output. |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |
| grade_info | STRING | — |