Nodes/ComfyUI CV/CV Displacement Map
ComfyUI Node

CV Displacement Map

Heat haze, glass, and wobble from any image's brightness

By bmad4ever·Created 4 months ago·Updated 15 days ago· 1
CV Displacement Map
  • map_x
  • map_y
  • displacement
  • map_x
  • map_y
◄strength_x16.0►
◄strength_y16.0►

CV Displacement Map shifts every coordinate in a remap chain by the brightness of a second image. Mid-gray moves nothing, white pulls the sample from +strength pixels away, black from -strength. That's the whole node, and it's a surprisingly useful little tool.

What you'd use it for: heat haze and glass refraction (feed a blurred noise or a soft mask), water or window ripple (feed a wave pattern), subtle organic wobble on a still or a video loop, or lens-distortion-style warping driven by a depth map. It's also the cheapest way to make an otherwise-too-clean render look slightly less digital - a couple of pixels of low-frequency displacement reads as "shot through something".

How it works

This is one node in the pack's remap family, and the family works as a chain over coordinate arrays:

dst(p) = src(map_x(p), map_y(p))

CV Identity Map produces the do-nothing grid for an image's size. Each distortion node rewrites the coordinates it receives and passes them on, so distortions compose with no extra plumbing - a wave map followed by a displacement map gives you both. Crank as many as you like between the identity map and the end of the chain.

The end of the chain is the low-level cv2_remap node, which finally samples the image. Its tooltip advice is worth taking: use borderMode=BORDER_CONSTANT unless you actually want edge reflections, because coordinates pushed outside the source frame are perfectly valid and the border mode decides what shows up there.

The displacement itself is one line of arithmetic: offset = (disp - 0.5) * 2.0, then map_x + offset * strength_x (and the same for Y). So the displacement image's contrast sets how much movement you get, and its mid-tone is the neutral point. That's why a blurred mask works nicely and a hard black-and-white mask produces a hard-edged, unusable split.

Inputs and outputs

Required: map_x and map_y - the incoming coordinate maps, both from the same chain and the same shape, or the node raises rather than guessing. displacement is the image driving the shift: gray or colour (converted to grayscale internally, and resized to the map size automatically, so it doesn't need to match), uint8/uint16 scaled to 0–1 and float used as-is. Then strength_x and strength_y, defaulting to 16 pixels each; negative values invert the direction. Set one to 0 for a purely horizontal or vertical warp.

Outputs are map_x and map_y, ready for another distortion node or for cv2_remap.

Two practical notes from the inputs. First, the auto-resize means you can drive a 1024x1024 remap with a 256x256 noise pattern - which is usually what you want, because high-frequency displacement is jitter, not wobble. Second, since a float displacement image is used as-is, an image whose values run 0–1 gives you the full ±strength swing, while a float image that happens to be in 0–255 range will shove pixels hundreds of pixels sideways. Check the range of whatever you're feeding it.

Install

From comfyui_cv (bmad4ever/comfyui_cv). ComfyUI Manager → search "ComfyUI CV", 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. Python ≥ 3.12 and a recent V3-API ComfyUI. Note what this node is: the remap geometry here is pure Python plus numpy, with no cv2 call at all - the README calls those "genuine exceptions" in a pack that's otherwise wrappers around OpenCV functions. Doesn't change the install, but it does mean the behaviour is deterministic and inspectable in a way that the cv2 class wrappers aren't. Keep the contrib wheel in place regardless; a plain opencv-python from another pack empties the contrib submodules (tools/repair_opencv_contrib.py --check).

Common issues

  • map_x is required - start from 'CV Identity Map'. The exact error you get when you wire a displacement input with no map chain behind it. Every remap chain begins at CV Identity Map; there's no implicit one.
  • map_x and map_y must have the same shape. You're probably mixing maps from two different chains, or from two different image sizes.
  • Nothing happens. Mid-gray displacement and/or strength of 0. Or you fed it a nearly uniform image - a mask that's 0.5 everywhere displaces nothing, by definition.
  • Huge, chaotic smearing. Your displacement image is float in a 0–255 range, so ±strength is being multiplied by ~200. Normalise it first.
  • Black bands at the borders. Coordinates pointed outside the source frame and the border mode filled them with black. Either reduce strength, or set borderMode=BORDER_REFLECT on the cv2_remap node if reflected content is preferable to a void.
  • The noise pattern is visible as jitter. Blur the displacement image on the way in. Low frequency is what reads as a medium; high frequency reads as sensor noise you didn't want.
Categoryimage/CV/remap

Inputs (5)

NameTypeDefaultDescription
map_xNPARRAYIncoming source-x coordinate map to distort. Start the chain with 'CV Identity Map'; further remap nodes compose onto it.
map_yNPARRAYIncoming source-y coordinate map (must share map_x's shape - both come from the same chain).
displacementNPARRAY,IMAGE,MASKGray or color image; uint8/uint16 are scaled to 0-1, float is used as-is. 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.
strength_xFLOAT16.0-10000–10000Maximum horizontal shift in pixels (negative inverts the direction).
strength_yFLOAT16.0-10000–10000Maximum vertical shift in pixels (negative inverts the direction).

Outputs (2)

NameTypeDescription
map_xNPARRAY—
map_yNPARRAY—