Enhanced Image Colour Transfer
Steal any photo's color grade, no LUT required
- source_image
- target_image
- IMAGE
What it actually does
You feed it two images: a source (the look you want - a film still, a photo with a mood) and a target (your render). The output is your render recolored to match the source's overall color statistics - brightness, contrast, color cast, saturation. This is the classic Reinhard colour transfer from the 2001 paper, the same algorithm behind every "make my image look like this movie's palette" script that predates modern AI. No LUT, no training, no model: it's pure statistics on the two images you give it.
The mechanism, plain
Reinhard's trick is doing the match in a clever color space. The source code is explicit that this uses the Lαβ space derived from log-LMS, not CIELAB. The point of that space: luminance (L) is fully separated from the two chroma channels (α, β), and those channels are decorrelated - so you can match the target's mean and standard deviation to the source's per channel, and brightness, contrast, and color each follow independently without dragging the others along. Match means and standard deviations, and the target inherits the source's overall grade while keeping its own content.
The "enhanced" part is everything layered on top of that vanilla mean/std match: iterative reshaping of the chroma distribution (which handles sources with nonlinear color stats that plain Reinhard mangles), cross-covariance limiting so α and β don't get tangled, a saturation pass in HSV, an optional shading pass, and tint plus scale-vs-clip handling at the end. It's an aggressively knobby node - nine controls, none of them documented with tooltips.
The inputs you'll actually touch
Two are non-negotiable, and getting them backwards is the classic mistake:
- source_image - the reference, the look to steal.
- target_image - the image being recolored. The output is always the target wearing the source's statistics.
After that, honestly, one knob matters for most people: modified_val (default 0.5) blends the result back toward the original target, so it's your "how aggressive is this transfer" dial - 0 keeps your image untouched, 1 is full transfer. shader_val (default 0.5) controls how much of the source's brightness bleeds in; if the output comes out too dark or too washed out, that's the knob. The rest - cross_covariance_limit, reshaping_iteration, extra_shading, scale_vs_clip, tint_val, saturation_val - exist for fine control, and the defaults (0.5, 1, on, 1, 1, auto) work for a first pass. One trap: saturation_val defaults to -1, which isn't "no saturation," it's "auto" - the node computes a saturation factor from the images instead of applying a fixed value. Read it as a plain slider and you'll be confused why nothing happens.
Where people get burned
The big conceptual one: this is a global match. It changes the grade and mood of the whole frame, not the actual hue of specific objects. If you want "make this red car blue," this is the wrong tool - that's IP-Adapter or ControlNet territory. It's a grading tool, and a great pairing is to run it first, then finish with the film-grain node from the same pack, because palette alone doesn't give you the captured-on-film look.
Then there's the tuning problem: nine parameters, zero tooltips, and a pack this small has no community consensus to copy from. Change one thing at a time, preview against your target, and lean on modified_val and shader_val before touching anything else. The output is always an IMAGE at the target's resolution, so wire it into a Save Image or into your next post-processing step.
Install
Same pack as the grain node - it ships in the same ComfyUI-ImageBag repo:
cd ComfyUI/custom_nodes
git clone https://github.com/NHLStenden/ComfyUI-ImageBag.git
Restart ComfyUI. Or search "ImageBag" in ComfyUI Manager if it's in the catalog. Dependencies are torch and numpy, both already in every ComfyUI environment, and there are no model downloads. It's a pure function of the two input images, runs under torch.no_grad(), and works fine on CPU if you're testing - so the only real cost is your patience with the parameter list.
Inputs (10)
| Name | Type | Default | Description |
|---|---|---|---|
| source_image | IMAGE | — | |
| target_image | IMAGE | — | |
| cross_covariance_limit | FLOAT | 0.500–1 | — |
| reshaping_iteration | INT | 11–10 | — |
| modified_val | FLOAT | 0.500–1.5 | — |
| extra_shading | BOOLEAN | true | — |
| shader_val | FLOAT | 0.500–1.5 | — |
| scale_vs_clip | FLOAT | 1.000–1 | — |
| tint_val | FLOAT | 1.000–1.5 | — |
| saturation_val | FLOAT | -1.00-1–2 | — |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| IMAGE | IMAGE | — |