Nodes/radiance/ACES 2.0 Gamut Compress
ComfyUI Node

ACES 2.0 Gamut Compress

Stop your neon from turning into flat pink

By FXTD-Studios·Created 8 months ago·Updated about 18 hours ago· 246
ACES 2.0 Gamut Compress
  • image
  • image
  • compress_info
◄strength1.00►
◄limit_cyan1.15►
◄limit_magenta1.26►
◄limit_yellow1.31►
◄threshold_cyan0.81►
◄threshold_magenta0.80►
◄threshold_yellow0.88►

The problem this solves

Take a scene-linear image in ACEScg with something genuinely saturated in it - a neon sign, an LED, a laser, a sunset-red tail light. Those colours live outside every display gamut you can actually buy. Convert naively to sRGB and the channels clip at different points, and your vivid magenta becomes a flat, dead patch with a hard edge where the clip starts. Everyone has seen this. It looks like a bug, and it is one, in the pipeline.

The ACES 2.0 fix is gamut compression: instead of clipping, you smoothly squeeze out-of-gamut colours back inside the boundary, per channel, so hues bend a little instead of dying. This node is that operation, standalone.

How it works

It's the ACES 2.0 reach-based method. Each channel has a reach limit - the largest well-defined boundary for that axis - and a threshold, the distance from neutral where compression starts. Values between threshold and limit get compressed smoothly rather than cut. The defaults are the Academy's own numbers from the ACES Gamut Compression Guide v1.0.1, and the node maps them the way the guide does:

  • cyan (the R channel): limit 1.147, threshold 0.815
  • magenta (the G channel): limit 1.264, threshold 0.803
  • yellow (the B channel): limit 1.312, threshold 0.880

The naming is a bit counter-intuitive - the cyan limit applies to red, because a cyan colour is deficient in red relative to the gamut boundary. Read the tooltips, they spell this out per channel.

strength scales the whole thing. 0 is a bypass, 1.0 is the standard ACES 2.0 amount, and above 1 pushes harder, which the tooltip describes as "for difficult footage". There's a subtlety worth knowing: below strength 1, the thresholds are also lowered slightly, not just the compression. So it's not a linear fade to nothing - it's a gentler compression curve.

Inputs and outputs

Required: image (scene-linear ACEScg batch) and strength. Optional: the six numbers above. In practice you leave the six alone unless you're matching a reference grade or specifically fighting one colour family - for a problematic red, adjusting limit_cyan / threshold_cyan is the surgical move.

Outputs are image and compress_info. The info string is more useful than most: it reports the percentage of out-of-gamut pixels before and after, which is how you know whether you moved anything at all. If it says 2.40% → 0.00%, you did the job. If it says 0.00% → 0.00%, your image was never out of gamut and this node is doing nothing.

When to reach for it

In the ACES world, the answer is "always, as part of an output transform" - which is why the Full output transform node runs it internally, with gamut_compress_strength exposed as a widget. You'd use the standalone node when you want compression without an output transform: compressing in a working space so a downstream node doesn't clip, or checking how much of your image is actually out of gamut before you decide anything else. It's also the quickest A/B for "is my saturation problem a gamut problem or a grade problem" - run it and look at the number.

For generated images specifically: models produce punchy, over-saturated highlights more often than physical capture does, and anything you've pushed into HDR from an SDR source tends to land outside display gamut. This is one of the few "colour management" nodes with an obvious use for a hobbyist workflow, because it fixes something you can see.

Install

ComfyUI 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

Use the bundled python_embeded\python.exe for pip on Windows portable. This node needs nothing beyond torch and numpy - the OCIO/OpenEXR stack in requirements.txt is for the file and colour-management nodes elsewhere in the pack. The pack does enable OpenCV's EXR codec at startup (OPENCV_IO_ENABLE_OPENEXR=1) and picks up $OCIO if you have one.

Gotchas

  • Feed it ACEScg, not sRGB. The limits are AP1 boundaries. On an sRGB-encoded image the numbers mean nothing and you'll compress the wrong things.
  • Its output is still scene-linear. Good - you can keep grading, and you should apply an output transform after it. Don't confuse compression with encoding.
  • Doing it twice. If you're using the Full output transform node, its gamut stage is already running. Standalone compression in front of it just adds up.
  • Expecting saturation to be preserved exactly. It isn't. The point is to trade a little saturation for the absence of a hard clip. If a colour came out desaturated when you wanted it screaming, that's strength above 1 territory, not a bug.
CategoryFXTD STUDIOS/Radiance/HDR

Inputs (8)

NameTypeDefaultDescription
imageIMAGEScene-linear ACEScg (AP1) image batch.
strengthFLOAT1.000–1.5Compression strength. 0 = bypass. 1.0 = standard ACES 2.0. >1 = more aggressive squeeze for difficult footage.
limit_cyanoptFLOAT1.151–2Reach gamut limit for cyan channel (R).
limit_magentaoptFLOAT1.261–2Reach gamut limit for magenta channel (G).
limit_yellowoptFLOAT1.311–2Reach gamut limit for yellow channel (B).
threshold_cyanoptFLOAT0.810–1Distance from neutral (on R) where cyan compression starts. Lower protects fewer colours; below strength 1 it is also lowered slightly.
threshold_magentaoptFLOAT0.800–1Distance from neutral (on G) where magenta compression starts. Lower protects fewer colours; below strength 1 it is also lowered slightly.
threshold_yellowoptFLOAT0.880–1Distance from neutral (on B) where yellow compression starts. Lower protects fewer colours; below strength 1 it is also lowered slightly.

Outputs (2)

NameTypeDescription
imageIMAGE—
compress_infoSTRING—