CV Transform (Rotate/Scale/Shift)
One warpAffine instead of four stacked nodes
- image
- IMAGE
Rotate, scale, shift and flip, in one node, out the other side. That's the entire pitch, and it's a good one - the usual alternative is chaining separate rotate and scale nodes, where each hop resamples the pixels and softens the image a little more.
CV Transform (Rotate/Scale/Shift) builds a single affine matrix and does the whole thing in one cv2.warpAffine.
How it works
The matrix comes from cv2.getRotationMatrix2D around the image centre, scaled by zoom, with shift_x / shift_y added into the translation column. A flip, if you asked for one, is applied to the frame before the warp. Then one warpAffine at your chosen interpolation and border mode.
Two consequences fall straight out of that, and both matter:
- The output is the same size as the input. No canvas expansion. Rotate 30° and the corners of your image get cut off while four empty wedges appear where content used to be. If you want the whole rotated rectangle to fit, drop
zoomto compensate - roughly1 / (|cos θ| + |sin θ| · aspect)territory - or accept the crop. - The border fill is a widget.
BORDER_CONSTANT(the default) gives you black wedges.BORDER_REPLICATEsmears edge pixels outward, which is usually less ugly in a photo and completely useless if you're feeding a mask.
The inputs
- image - a batch is processed frame by frame, so the same transform lands on every frame. Good for augmentation, and note it means you can't vary the angle per frame in one node.
- angle - degrees, counter-clockwise. Range goes to ±3600, so multi-turn rotations are allowed (they're not the same thing when the interpolation filter samples differently).
- zoom - uniform scale about the centre,
1.0is neutral, down to0.01and up to16. - shift_x / shift_y - pixel offsets. This is where you notice the pack is a computer-vision toolkit rather than an image editor: pixel units, not fractions of the frame.
- flip -
none, horizontal, vertical or both, applied before rotating. - interpolation -
INTER_NEARESTwhen hard edges matter (masks, pixel art, palettes),INTER_LINEAR/INTER_CUBICfor photographs. - border - what fills the uncovered area. Constant (black) by default.
Output is a single IMAGE.
When you'd reach for it
The obvious job - straighten a scanned photo, nudge a composition - is the least interesting one. Where this node earns its place:
Data augmentation on a small training set. One node, randomised angle and zoom via converted inputs, applied to the whole batch at once. Cheap, fast, and it doesn't touch a model.
Masks and images must move together. This is a real trap with separate rotate nodes: you rotate the RGB and forget the mask, and now your inpaint region is offset by 15°. Feeding both through identical settings keeps them locked, and INTER_NEAREST on the mask avoids the semi-transparent halo that bilinear interpolation invents along the edge.
Geometry re-registration. If CV analysis told you "the frame needs 2.3° and -8 px", this is the node that applies it. Compare with CV Homography Map when the correction is a genuine perspective warp rather than a rigid one - affine has no perspective term, and forcing a keystone correction through it will look subtly wrong.
Cheap wobble animations. Rotate a still a degree or two, render, rotate back. Prefer the pack's remap chain (CV Identity Map → a wave or displacement map → cv2_remap) if you want it smooth and per-frame.
Installing it
Manager → ComfyUI CV, or:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
Restart. Python ≥ 3.12 and a current ComfyUI on the V3 node API. One runtime dependency: opencv-contrib-python-headless~=5.0.0.93. This node itself needs nothing from contrib - but other nodes in the pack do, and an accidental opencv-python install on top silently strips the contrib submodules out of the shared cv2.
Where people get burned
Black corners. Not a bug, it's BORDER_CONSTANT doing its job. Switch the border mode or scale down.
Blurry rotation. INTER_LINEAR on a 45° rotation is fine; doing it five times in a row is not. Chain your transforms into one matrix instead of five nodes.
Float sizes on the way to cv2. Fine here, since the node handles the matrix internally - but when composite values like sizes travel between raw cv2.* wrappers, they need to be integers. CV Tuple is the node that gets that type right; see its page.
Expecting no resampling. Any angle that isn't a multiple of 90° resamples everything. If you need a lossless quarter-turn, cv2_rotate is the right wrapper and it's in the pack too.
Inputs (8)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | Input image. A batch is processed frame by frame. | |
| angle | FLOAT | 0.0-3600–3600 | Rotation in degrees, counter-clockwise. |
| zoom | FLOAT | 1.000.01–16 | Uniform scale around the center. 1.0 = no change. |
| shift_x | FLOAT | 0.00-16384–16384 | Horizontal shift in pixels. |
| shift_y | FLOAT | 0.00-16384–16384 | Vertical shift in pixels. |
| flip | COMBO | none | Optional mirror applied before rotate/scale: horizontal, vertical, or both. |
| interpolation | COMBO | INTER_LINEAR | Resampling filter for the warp. NEAREST keeps hard edges; LINEAR/CUBIC are smoother for photos. |
| border | COMBO | BORDER_CONSTANT | What to fill the uncovered areas with (CONSTANT = black). |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| IMAGE | IMAGE | — |