Nodes/ComfyUI CV/CV Radial Lens Map
ComfyUI Node

CV Radial Lens Map

Fake a lens — or undo one — with four numbers

By bmad4ever·Created 4 months ago·Updated 14 days ago· 1
CV Radial Lens Map
  • map_x
  • map_y
  • map_x
  • map_y
◄a0.00►
◄b0.15►
◄c0.00►
◄formulapolynomial: r * (a r^3 + b r^2 + c r + d)►
◄d0.00►
◄center_x0.50►
◄center_y0.50►

Real lenses bend straight lines. Straight lines are the one thing a synthetic render gets perfectly right, which is a big part of why generated images read as generated. This node is the cheap, deterministic fix in both directions: add the barrel distortion a real wide lens would have, or take it back out of footage that has it.

It's a coordinate map, not a filter

Worth understanding before you wire anything, because it changes what you connect. This node does not touch pixels. It rewrites an HxW pair of float32 coordinate grids - the map_x / map_y that cv2.remap consumes - and hands them back. One cv2.remap at the end of the chain does the actual sampling, which means you can stack five distortions and pay for exactly one resample. That's why the remap nodes in this pack all take map_x, map_y in and give them back out.

Start the chain with CV Identity Map (a grid where map_x[y][x] = x, i.e. the do-nothing map), run it through whatever you want, finish with the raw cv2_remap wrapper.

The math, briefly

It scales each pixel's distance from the centre by a*r³ + b*r² + c*r + d, where r is normalised so r = 1 at the nearest edge of the frame. So b positive bulges the middle outward (barrel, the fisheye look), b negative pinches it in (pincushion), and mixing signs between a and b gives you the mustache distortion that cheap wide lenses actually have. Defaults are b = 0.15 and everything else zero, which is a tasteful amount of barrel.

d is the constant term and it defaults to 0, which means auto - the node substitutes 1 - a - b - c so the r = 1 circle stays put and your frame edges don't crawl. Leave it alone unless you know why you're changing it.

center_x / center_y (default 0.5, 0.5) place the centre of distortion as a fraction of width and height. Those are the advanced inputs, and they're what you reach for when the distortion should be anchored somewhere other than the middle of the frame.

Inverse: the part that makes it a tool rather than a toy

formula has two settings: polynomial: r * (a r^3 + b r^2 + c r + d) applies the distortion, and inverse: undoes the polynomial (same coefficients) removes it. The inverse solves r' * P(r') = r per pixel with a few Newton steps - the same trick cv2.undistortPoints uses - so it's accurate rather than a hand-waved approximation.

The practical upshot: wire the polynomial, then the inverse with the same coefficients, and you round-trip a distortion to sub-pixel accuracy. That's how you check a coefficient set before committing to it, and how you build a "warp in, warp out" pipeline around an edit. If a set of coefficients is extreme enough that the polynomial stops being monotonic - it folds the image onto itself - the inverse clamps at the last invertible radius instead of exploding. It degrades, it doesn't crash.

Install

Part of ComfyUI CV (bmad4ever/comfyui_cv), a GPL-3.0 fork of opencv-comfyui. Manager: search the pack title. By hand:

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 V3-API ComfyUI build. The only runtime dependency is that contrib OpenCV wheel - and contrib is load-bearing here, because the pack also ships cylinder, wave, pinch and homography map nodes alongside this one, and installing a plain opencv-python wheel over a contrib one silently empties the contrib modules those need. The pack ships a repair script (tools/repair_opencv_contrib.py --check) but no install-time guard, so watch your own pip output.

Traps

The big one is that the map chain is order-dependent in a way that reads like it shouldn't be. Wave-then-barrel and barrel-then-wave are different warps, so if you're prototyping, keep the chain short and compare against CV Identity Map swapped in for the whole thing. The pack's workflows/43_remap_playground.json does exactly that on one image, including a composed branch, and it's a much faster way to feel a/b/c than reading this.

Second trap: this is not the calibration path. If your real camera has measured distortion coefficients, you want the undistort nodes that take K and dist_coeffs - this one takes a hand-authored polynomial and centres it by fraction, which is perfect for a deliberate look and wrong for correcting a specific lens by spec. And remember the map is only as big as the image it was built from; change resolution and you rebuild the chain, or the distortion scales with the frame.

Categoryimage/CV/remap

Inputs (9)

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).
aFLOAT0.00-8–8r^3 coefficient.
bFLOAT0.15-8–8r^2 coefficient: + barrel, - pincushion.
cFLOAT0.00-8–8r coefficient.
formulaCOMBOpolynomial: r * (a r^3 + b r^2 + c r + d)polynomial applies the distortion; inverse undoes it with the same coefficients (chain both to round-trip).
doptFLOAT0.00-8–8Constant term; 0 = auto (1 - a - b - c), which keeps the r = 1 circle fixed.
center_xoptFLOAT0.500–1Distortion center as a fraction of the width.
center_yoptFLOAT0.500–1Distortion center as a fraction of the height.

Outputs (2)

NameTypeDescription
map_xNPARRAY—
map_yNPARRAY—