Nodes/ComfyUI CV/cv2.warpAffine
ComfyUI Node

cv2.warpAffine

Rotate, scale and shear — and yes, you can warp a latent

By bmad4ever·Created 4 months ago·Updated 15 days ago· 1
cv2.warpAffine
  • src
  • M
  • dsize
  • result
◄flagsINTER_LINEAR►
◄borderModeBORDER_DEFAULT►
◄borderValue►
◄hintALGO_HINT_DEFAULT►

Affine warps are the workhorse geometry operation: rotate 7 degrees, scale to 62%, shear a little, nudge. In ComfyUI you can usually dodge the matrix maths with a curated node, and this pack has several. But there's one job nothing else in ComfyUI does half as cleanly - rotating a latent - and cv2.warpAffine is the node for it.

The mechanism

You supply a 2×3 matrix M and cv2 computes, for every output pixel, which source pixel maps there, then samples with your chosen interpolation:

dst(x, y) = src(M11·x + M12·y + M13,  M21·x + M22·y + M23)

It's inverse-mapped under the hood, so M is the transform from destination to source - unless you add WARP_INVERSE_MAP to the flags, which tells cv2 your matrix is the forward one instead. That single flag is where most "why is my image mirrored" bugs live.

You rarely write the six numbers by hand. cv2.getRotationMatrix2D builds one from a centre/angle/scale, cv2.estimateAffine2D and cv2.getAffineTransform build one from point correspondences, and the pack's Parse Matrix turns a typed literal into the array if you just want to experiment.

Inputs that matter

  • src - the image. This port is type-flexible: an IMAGE, a MASK, a raw NPARRAY, or a LATENT. Whatever you wire in comes back as the same type (the socket is a match-type). If you link an IMAGE batch, note the wrapper works on frame 0 - this op isn't in the per-frame batch set, so unwind the batch (CV Unstack Batch) if you need every frame.
  • M - the 2×3 matrix, NPARRAY only. No image links, no tuples: it's data, so it comes from Parse Matrix, cv2_getRotationMatrix2D, cv2_estimateAffine2D, CV Homography Map and friends.
  • dsize - output size as one CV_TUPLE value (w, h). The default (0, 0) means "same size as the source", which is what you want nine times out of ten. It travels as a whole value, so you can't wire just the width.
  • flags - interpolation plus optional WARP_INVERSE_MAP / WARP_FILL_OUTLIERS, pipe-joined like INTER_LINEAR | WARP_INVERSE_MAP. In the UI it renders as a dropdown with one toggle per flag, and the curated CV Warp Flags node authors the same string if you'd rather wire it.
  • borderMode / borderValue - what fills the pixels the transform pulled from outside the source. BORDER_REPLICATE stretches the edge (good for slight rotations), BORDER_CONSTANT with a borderValue literal like "(0, 255, 0)" (BGR) gives you a coloured fill. Blank means OpenCV's zero default.
  • hint - ALGO_HINT_APPROX opts into FP16 linear maths for a bit of speed where the build supports it. Leave it default unless you're chasing throughput.

The latent trick

A LATENT linked into src is processed in latent space: frame 0 becomes a float32 [H, W, C] array, values untouched - no uint8 quantisation, no round-trip through the VAE. warpAffine, warpPerspective and warpPolar are the geometry ops the pack whitelists for this. Practically: you can rotate, mirror or subtly shift a latent between sampler passes without decoding it, and it's the recommended route because cv2.rotate and cv2.transpose cap at 4 channels, which excludes most latents anyway. The pack's own 87_latent_cv_playground example does exactly this.

When not to use it

If you want "rotate this image 90°", cv2.rotate is simpler and lossless. If you want "rotate about the centre, scale, shift and flip with sliders", the curated CV Transform (Rotate/Scale/Shift) is one node doing what would take you three here. Affine from correspondences, four-corner homographies and non-rigid warps all have curated nodes too (CV Affine Shape Warp, CV Quad Warp, CV Thin Plate Spline Warp). Reach for the raw wrapper when your matrix comes from somewhere those nodes don't cover - an estimated transform, a script, a latent.

Installing it

One pack, one dependency. In 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, recent V3-API ComfyUI. If you want the example workflows' bundled photos and videos to resolve in Load Image, run workflows/01_install_example_inputs.json once and reload the page - the Load* dropdowns are built at page load.

Troubleshooting

  • "Overload resolution failed" almost always means M is the wrong shape or dtype. cv2 wants a float 2×3, so cast it (CV Cast Array) if you assembled it as integers.
  • Everything's blank or the frame is empty - check dsize. A zero or negative dimension gives you an output that looks like nothing happened, or nothing at all.
  • Wrong direction / mirrored output. You either want WARP_INVERSE_MAP or you don't; the flag flips M's meaning, and this is the single most common mix-up with both this node and cv2.warpPerspective.
  • Black bands at the edges are border fill, not a bug. BORDER_REPLICATE or a matching borderValue cleans them up.
Categoryimage/CV/low-level/cv2 W

Inputs (7)

NameTypeDefaultDescription
srcCOMFY_MATCHTYPE_V3input image. The image output(s) echo this input's format. A LATENT link is processed in latent space: frame 0 becomes a float32 [H,W,C] array (any channel count), values untouched. Arithmetic ops (add, multiply, etc.) also accept a full LATENT batch ({samples: [B,C,H,W]}) — the whole batch flows through when both inputs have the same batch size. 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.
MNPARRAY$2\times 3$ transformation matrix. A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here.
dsizeCV_TUPLE0,0size of the output image. One value with 2 components (w, h) - it travels as a whole, so it cannot arrive half-connected. Wire it from 'CV Tuple' or type the components in place.
flagsoptSTRINGINTER_LINEARcombination of interpolation methods (see #InterpolationFlags) and the optional flag #WARP_INVERSE_MAP that means that M is the inverse transformation ( $\texttt{dst}\rightarrow\texttt{src}$ ). cv2.warpAffine flags: one of INTER_LINEAR, INTER_NEAREST, INTER_CUBIC, INTER_LANCZOS4 plus any of WARP_INVERSE_MAP, WARP_FILL_OUTLIERS, pipe-joined (e.g. "INTER_LINEAR | WARP_INVERSE_MAP"). In the UI this renders as a dropdown with one toggle per flag.
borderModeoptCOMBOBORDER_DEFAULTpixel extrapolation method (see #BorderTypes); when borderMode=#BORDER_TRANSPARENT, it means that the pixels in the destination image corresponding to the "outliers" in the source image are not modified by the function.
borderValueoptSTRINGvalue used in case of a constant border; by default, it is 0. cv2 Scalar as a literal, e.g. "(0, 255, 0)" (BGR) or "(0, 255, 0, 64)" (BGRA). A bare number broadcasts to every component, so "255" means (255, 255, 255, 255). Components past the target's channel count are ignored by OpenCV. Leave blank for the OpenCV default.
hintoptCOMBOALGO_HINT_DEFAULTImplementation modification flags. Set #ALGO_HINT_APPROX to use FP16 precision (if available) for linear calculation for faster speed. See #AlgorithmHint.

Outputs (1)

NameTypeDescription
resultCOMFY_MATCHTYPE_V3Echoes the 'src' input's format: an IMAGE link comes back as IMAGE, MASK as MASK, NPARRAY stays NPARRAY.