cv2.LUT
The whole tone curve, for the price of one table lookup
- src
- lut
- result
The naming is the whole story, and it's the one thing beginners get wrong: cv2.LUT is not "apply a colour look". It's look-up table. You give it an image of 8-bit (or 16-bit) integers and a table, and every pixel is replaced by the table entry at its own value. That's it. It is the fastest per-pixel mapping a computer can do - one array index instead of an exponential.
Which makes it the correct tool for the thing the KB keeps telling people to stop using img2img for: a tone curve. A curve is a function from input intensity to output intensity, and a LUT is that function, precomputed. Gamma without a pow(), an invert, a posterize, a per-channel colour balance, a calibration curve you measured off a chart - all of them are LUTs, and all of them cost about nothing.
How it works
dst(p) = lut[src(p)], per channel. Two constraints fall straight out of that: src must have integer depth (8-bit needs a 256-entry table, 16-bit needs 65536), and the table's channel count has to either match the source or be single-channel, in which case the same curve is applied to every channel. The node's lut input accepts an IMAGE/MASK or an NPARRAY, which means a table can arrive as a 1×256 picture, as a literal array from CV Numbers To Array (columns: 1, dtype: uint8), or as an array computed upstream.
src is the interesting socket: it echoes format, so IMAGE in → IMAGE out and MASK in → MASK out. An IMAGE linked straight in arrives at cv2 as uint8 BGR 0–255, which is exactly the 8-bit integer depth a LUT wants - no conversion, no 0–1 scaling.
What you actually set
Only two sockets, no optional widgets at all. src, and lut.
For the table itself, the in-graph options are: build it with CV Numbers To Array from a list of values; generate a ramp somewhere and modify it; or - the sneaky one - feed the same image in as the table and get an identity map, which is a fine way to prove your wiring before you make it interesting. Once you've built a curve as float data (cv2.pow with power: 0.4545 is the classic gamma 2.2 encode), CV Cast Array saturates it into a uint8 table so the indexing works.
The output is one socket, echoing src's format. That echo is why LUT is so pleasant to use in a real graph: you can drop it in the middle of an IMAGE chain and nothing downstream notices.
Installing the pack
This node ships in ComfyUI CV (bmad4ever/comfyui_cv) - about 470 auto-generated cv2.* wrappers plus curated nodes. Search "comfyui_cv" in ComfyUI Manager, or:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"
Restart ComfyUI. The pack wants Python ≥ 3.12 and a ComfyUI built on the V3 node API, and its single dependency is that pinned contrib wheel. cv2.LUT itself is core OpenCV - no models, no compute worth measuring.
Where people get burned
"LUT" made you expect a .cube file. There's no file loader here: this node takes a table, not a 3D LUT, and it isn't reading an industry LUT off disk. If that's what you wanted, this isn't it.
Float input goes wrong. The source has to be 8-bit or 16-bit integers. If you convert to float32 first - which is the right move for Laplacian-style math - the LUT path stops being the thing you want; either cast back with CV Cast Array or do the curve in float.
The table is the wrong length. 256 entries for 8-bit input, 65536 for 16-bit. Nothing here checks politely; you get an OpenCV error or a wrapped-around mess. When you build tables with CV Numbers To Array, float32 is the default dtype - set uint8 explicitly.
A 3-channel image and a 3-channel table isn't "one curve per channel" by default. A single-channel table broadcasts to every channel, which is what you want for a luminance curve; for per-channel work the table needs the same channel count as the source, and the order is BGR like every other colour input in this pack.
Contrib nodes vanished from the menu. Unrelated to LUT specifically, but the classic pack-wide injury: all four opencv-* wheels share one site-packages/cv2, so installing a non-contrib wheel over the contrib one empties the contrib submodules and those nodes just stop registering. python tools/repair_opencv_contrib.py --check diagnoses it, --apply repairs it.
Inputs (2)
| Name | Type | Default | Description |
|---|---|---|---|
| src | COMFY_MATCHTYPE_V3 | input array of 8-bit or 16-bit integer elements. The image output(s) echo this input's format. Accepts a ComfyUI IMAGE/MASK directly (frame 0 of a batch) or an NPARRAY. Arithmetic ops (add, multiply, etc.) process the full IMAGE batch when both inputs have the same batch size. | |
| lut | NPARRAY,IMAGE,MASK | look-up table of 256 elements (if src has depth CV_8U or CV_8S) or 65536 elements(if src has depth CV_16U or CV_16S); in case of multi-channel input array, the table should either have a single channel (in this case the same table is used for all channels) or the same number of channels as in the input array. Accepts a ComfyUI IMAGE/MASK directly (frame 0 of a batch) or an NPARRAY. Arithmetic ops (add, multiply, etc.) process the full IMAGE batch when both inputs have the same batch size. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| result | COMFY_MATCHTYPE_V3 | Echoes the 'src' input's format: an IMAGE link comes back as IMAGE, MASK as MASK, NPARRAY stays NPARRAY. |