Nodes/ComfyUI CV/CV Transform (Rotate/Scale/Shift)
ComfyUI Node

CV Transform (Rotate/Scale/Shift)

One warpAffine instead of four stacked nodes

By bmad4ever·Created 4 months ago·Updated 15 days ago· 1
CV Transform (Rotate/Scale/Shift)
  • image
  • IMAGE
◄angle0.0►
◄zoom1.00►
◄shift_x0.00►
◄shift_y0.00►
◄flipnone►
◄interpolationINTER_LINEAR►
◄borderBORDER_CONSTANT►

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 zoom to compensate - roughly 1 / (|cos θ| + |sin θ| · aspect) territory - or accept the crop.
  • The border fill is a widget. BORDER_CONSTANT (the default) gives you black wedges. BORDER_REPLICATE smears 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.0 is neutral, down to 0.01 and up to 16.
  • 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_NEAREST when hard edges matter (masks, pixel art, palettes), INTER_LINEAR/INTER_CUBIC for 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.

Categoryimage/CV

Inputs (8)

NameTypeDefaultDescription
imageIMAGEInput image. A batch is processed frame by frame.
angleFLOAT0.0-3600–3600Rotation in degrees, counter-clockwise.
zoomFLOAT1.000.01–16Uniform scale around the center. 1.0 = no change.
shift_xFLOAT0.00-16384–16384Horizontal shift in pixels.
shift_yFLOAT0.00-16384–16384Vertical shift in pixels.
flipCOMBOnoneOptional mirror applied before rotate/scale: horizontal, vertical, or both.
interpolationCOMBOINTER_LINEARResampling filter for the warp. NEAREST keeps hard edges; LINEAR/CUBIC are smoother for photos.
borderCOMBOBORDER_CONSTANTWhat to fill the uncovered areas with (CONSTANT = black).

Outputs (1)

NameTypeDescription
IMAGEIMAGE—