Nodes/ComfyUI-SeedVR2-TilingWrapper/Advanced Color Match (OKLAB MKL)
ComfyUI Node

Advanced Color Match (OKLAB MKL)

Color matching that doesn't re-light the whole picture

By turinastudio·Created 6 months ago·Updated 6 months ago· 1
Advanced Color Match (OKLAB MKL)
  • target
  • source
  • image
strength1.00
preserve_lumafalse

Most color-matching nodes in ComfyUI do the same cheap thing: convert to some color space, match the per-channel mean and standard deviation, convert back. It works until it doesn't - hue shifts, washed-out mids, and a result that matches the average color of your reference but not its actual character. This node does the version that actually holds up: it operates in OKLAB, a perceptually uniform color space, and transfers the full color distribution using Monge-Kantorovich linear optimal transport. Fancy name, real result: the target picks up the source's palette without the hue drift.

It ships in the SeedVR2 Tiling pack, and there's a reason. SeedVR2's known flaw is a contrast lift that makes upscaled output read "more AI" than the input. Matching the upscale's color back toward the original input is the clean fix for that palette drift - and because this node is fully standalone, it's equally useful for style transfer, matching a generation to a reference photo, or correcting a tinted render. It doesn't need SeedVR2 at all.

How it works

Two ideas stacked. First, both images are converted from sRGB to OKLAB, a color space built so that equal numeric distances mean equal perceived differences. Matching in OKLAB is what stops the hue shifts you get doing the same math in RGB or HSV. Second, instead of per-channel statistics, it does MKL optimal transport: it computes the covariance matrix of each image and uses SVD to derive a linear mapping from the target's distribution to the source's. You're not just matching the average - you're matching how colors vary together, which is what "the palette" actually means.

preserve_luma is the control that makes this useful instead of destructive. Off (default), it matches all three OKLAB channels, so the target is re-lit by the source. On, it locks the target's L (lightness) channel and only moves the a and b chrominance channels - the lighting and structural depth of your image survive untouched, and only the colors move.

The inputs

Two images, two settings. That's the whole node:

  • target - the image you want to change.
  • source - the image whose palette you're stealing. If you have more target frames than source images, the last source image is reused.
  • strength (default 1.0) - 1.0 is full transfer, 0.5 is halfway, 0 is a no-op. The interpolation happens in OKLAB space, so mid-values still look natural rather than muddy.
  • preserve_luma (default off) - keep the target's lighting, match color only.

Output is image, same size and alpha as the target.

What I'd actually set

For the "fix SeedVR2's contrast drift" use case, enable preserve_luma. You want the upscale's newly-restored shading to stay - you're only pulling the chroma back into the original's palette. Full-strength matching without preserve_luma re-lights the whole image to the source, which is the aggressive version; it's great for a deliberate style pass and wrong for restoration.

Installing

It's the same one-step install as the rest of the pack - no requirements.txt, pure torch under the hood. ComfyUI Manager → search "SeedVR2 TilingWrapper", or:

cd ComfyUI/custom_nodes
git clone https://github.com/turinastudio/ComfyUI-SeedVR2-TilingWrapper.git

Restart, find it under SeedVR2_Tiling. Heads-up: the README's clone URL still names shikasensei-dev/..., but the repo is actually at turinastudio/... - use the one above.

Common issues

  • It re-lights everything and you didn't want that. That's preserve_luma off, doing its job. Flip it on and the effect gets far gentler.
  • The match is too strong. Dial strength to 0.5–0.8 instead of trying to undo it afterward.
  • It's slow on huge images. It flattens each image to do the statistics, so a 4K target takes a moment - normal, not broken.
CategorySeedVR2_Tiling

Inputs (4)

NameTypeDefaultDescription
targetIMAGE
sourceIMAGE
strengthFLOAT1.000–1
preserve_lumaBOOLEANfalse

Outputs (1)

NameTypeDescription
imageIMAGE