CV Cylinder Map
The pre-warp that makes panoramas line up
- map_x
- map_y
- map_x
- map_y
What it's for
Panorama stitching fails in a specific, instructive way. You shoot by rotating the camera - pure rotation, no translation - and then you feed the frames to a stitcher expecting a plane, which tries to explain the overlap with a homography. It can't, because the geometry isn't planar. The camera rotated about its vertical axis; that's a cylindrical motion, and the correct model for it is a cylinder.
Curving each photo onto a cylinder first is the classic pre-warp that makes the rest of the problem tractable: rotation becomes a pure horizontal translation, which a matcher can find without fighting the geometry. It's a step in OpenCV's own stitching_detailed sample pipeline, whose stage order and parameter set this pack's advanced stitcher follows - so if you're building panoramas by hand around CV Stitch and CV Stitch Advanced, this node is the first stage you'd otherwise be skipping.
It's also the tool for the reverse: unwrapping an already-cylindrical image back to flat, which you want when you've been doing detail work at the compressed edges and want to see what you actually produced.
How it works - and the one thing that confuses everyone
This node does not produce an image. It produces coordinate maps: map_x and map_y, the source-coordinate lookup tables you then feed to a remap node to actually move pixels. That trips up nearly everyone the first time. The map-chain design is deliberate - every remap node in the pack (CV Pinch Stretch Map, CV Wave Map, CV Radial Lens Map, CV Polynomial Radial Map, distortion, displacement maps) takes maps in and gives maps out, so several can be composed in a row before a single remap does the work. One resample instead of five, which is the difference between a sharp panorama and a smeared one.
The chain starts at CV Identity Map - that's the "no distortion yet" starting point - and each subsequent map node bends it further.
- direction -
wrap around the cylinder (pano pre-warp)curves a flat image onto a cylinder;unwrap the cylinder (inverse)flattens an already-cylindrical image. Wrapping compresses the side edges, unwrapping stretches them. - field_of_view (10–160°, default 90) - sets the effective focal length via
f = (extent / 2) / tan(fov / 2), the pinhole model. Wider FOV, stronger curvature. This has to roughly match the lens the photos were taken with, or the curvature is wrong and the stitching still won't agree - this is the node's most consequential number and its least guessable. - axis -
vertical axiscurves the left and right edges (the pano case: camera rotated about the vertical);horizontal axiscurves the top and bottom edges instead. - map_x, map_y - the map inputs, which must share a shape because they came out of the same chain.
Outputs are the same pair, ready for the next map node or a remap.
Install
ComfyUI Manager, search ComfyUI CV. By hand:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
Then restart. Needs Python ≥ 3.12, a ComfyUI recent enough for the V3 node API, and the contrib OpenCV wheel:
pip install "opencv-contrib-python-headless~=5.0.0.93"
Where people get burned
"I ran it and nothing happened." It emitted two arrays, not a picture. Wire map_x / map_y into a remap node (CV Remap, or the raw cv2_remap wrapper) and you'll see the warp. Start from CV Identity Map if you're not sure the chain is even connected.
The two maps must come from the same chain. Same shape, same branch. Mixing a map_x from one node with a map_y from another gives you a warp neither node described - usually a diagonal smear that looks like a data problem and is actually a wiring problem.
FOV is the parameter people leave at 90. A phone's main camera and a 24 mm lens are not the same projection, and the wrong curvature means the sides still won't overlap cleanly. If your stitcher keeps throwing away matches at the frame edges, this number is the first suspect.
Wrap, then stitch - not the other way round. Once you've warped, the frames are cylindrical, and they need to stay that way through matching and blending. Unwrapping mid-pipeline undoes the very alignment you just bought.
Detail work on a wrapped edge is detail work on resampled pixels. The compression at the sides is real: edges lose resolution there, so per-region cropping and refining on the curved frame is fighting the geometry. Do the flat detail work first, warp for panorama assembly second.
The repo's 43_remap_playground.json and the panorama workflow are the reference builds; both are faster to read than to rebuild.
Inputs (5)
| Name | Type | Default | Description |
|---|---|---|---|
| map_x | NPARRAY | Incoming source-x coordinate map to distort. Start the chain with 'CV Identity Map'; further remap nodes compose onto it. | |
| map_y | NPARRAY | Incoming source-y coordinate map (must share map_x's shape - both come from the same chain). | |
| field_of_view | FLOAT | 9010–160 | Horizontal (or vertical, see axis) field of view in degrees; wider = stronger curvature. |
| direction | COMBO | wrap around the cylinder (pano pre-warp) | wrap curves a flat image onto a cylinder (panorama pre-warp); unwrap is the inverse (flattens it back). |
| axisopt | COMBO | vertical axis (curves the left/right edges) | Cylinder orientation: vertical curves the left/right edges, horizontal curves the top/bottom edges. |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| map_x | NPARRAY | — |
| map_y | NPARRAY | — |