cv2.warpAffine
Rotate, scale and shear — and yes, you can warp a latent
- src
- M
- dsize
- result
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: anIMAGE, aMASK, a rawNPARRAY, or aLATENT. Whatever you wire in comes back as the same type (the socket is a match-type). If you link anIMAGEbatch, 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 fromParse Matrix,cv2_getRotationMatrix2D,cv2_estimateAffine2D,CV Homography Mapand friends.dsize- output size as oneCV_TUPLEvalue(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 optionalWARP_INVERSE_MAP/WARP_FILL_OUTLIERS, pipe-joined likeINTER_LINEAR | WARP_INVERSE_MAP. In the UI it renders as a dropdown with one toggle per flag, and the curatedCV Warp Flagsnode authors the same string if you'd rather wire it.borderMode/borderValue- what fills the pixels the transform pulled from outside the source.BORDER_REPLICATEstretches the edge (good for slight rotations),BORDER_CONSTANTwith aborderValueliteral like"(0, 255, 0)"(BGR) gives you a coloured fill. Blank means OpenCV's zero default.hint-ALGO_HINT_APPROXopts 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
Mis 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_MAPor you don't; the flag flipsM's meaning, and this is the single most common mix-up with both this node andcv2.warpPerspective. - Black bands at the edges are border fill, not a bug.
BORDER_REPLICATEor a matchingborderValuecleans them up.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| src | COMFY_MATCHTYPE_V3 | input 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. | |
| M | NPARRAY | $2\times 3$ transformation matrix. A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here. | |
| dsize | CV_TUPLE | 0,0 | size 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. |
| flagsopt | STRING | INTER_LINEAR | combination 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. |
| borderModeopt | COMBO | BORDER_DEFAULT | pixel 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. |
| borderValueopt | STRING | value 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. | |
| hintopt | COMBO | ALGO_HINT_DEFAULT | Implementation modification flags. Set #ALGO_HINT_APPROX to use FP16 precision (if available) for linear calculation for faster speed. See #AlgorithmHint. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| result | COMFY_MATCHTYPE_V3 | Echoes the 'src' input's format: an IMAGE link comes back as IMAGE, MASK as MASK, NPARRAY stays NPARRAY. |