CV Phase Correlate (Translation)
Sub-pixel shift in one FFT — the laziest good registration there is
- image
- shifted
- dx
- dy
- response
- translation
- found
Registering two images normally means either detecting features and matching them (fails on a featureless frame) or running an iterative transform fit (slow, and you get to babysit convergence). Phase correlation does neither. It takes one FFT pair, finds the dominant peak in the cross-power spectrum, and reports the shift it implies - sub-pixel accurate, robust to uniform brightness changes, and it cannot fail to converge because it never iterates.
That's the sales pitch, and it's the honest one: no keypoints to fail on, nothing to tune, milliseconds instead of seconds. For video stabilisation, burst alignment, scan-line registration, and frame-to-frame jitter measurement, this is the first thing to try.
The limits are real too. It measures a global translation - one shift for the whole frame. Pan and tilt, fine. Rotation, scale, perspective, or a subject moving inside the frame: it will hand you the dominant shift and quietly be wrong about everything else. When motion is more than a shift you want an iterative estimator or a feature-based one instead; when the frame is featureless (sky, water, blur, a blank scan) this is the only one of the three with anything to lock onto.
What goes in, what comes out
image is the reference, shifted is the moved image, and they must be the same size - phase correlation compares spectra, and spectra of different-sized frames aren't comparable. Both are converted to grayscale float32 internally, so color and dtype differences don't matter. Since an IMAGE drops in directly (frame 0 of a batch) or you can wire an NPARRAY, the usual wiring is a video loader's frame batch indexed twice.
min_response is the reject-the-nonsense knob: found is only true when the correlation peak is at least this sharp. The response falls off both as the overlap between the frames shrinks and as the motion stops being a pure shift, so on a stabilisation pass this is what stops a motion-blurred frame from producing a confident-looking garbage offset. 0.0 accepts anything and is the default - fine for interactive use, not what you want in a pipeline.
hann_window (on, advanced) applies a Hann window before correlating. Leave it on. Without it, the image borders act like a huge step edge and can dominate the real peak. The only reason to turn it off is already-windowed or periodic data.
Outputs: dx and dy in pixels (sub-pixel - this is the useful part), response (peak sharpness in [0,1]; 0 when not found), translation (the same shift as a 2x3 float32 affine matrix [[1,0,dx],[0,1,dy]], identity when not found so a downstream cv2.warpAffine is a no-op), and found.
That identity-on-failure matrix is a nice piece of design: wire translation straight into a warp and a bad frame passes through unshifted instead of flying off the canvas.
Install
Manager → search ComfyUI CV, or:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
cd comfyui_cv && pip install "opencv-contrib-python-headless~=5.0.0.93"
Restart. Python ≥ 3.12, V3 node API. Phase correlation is core OpenCV; the pack's pinned contrib headless wheel is for the pack as a whole.
Common issues
foundis false on good-looking frames - checkmin_responseisn't too high, and check the two frames really are the same size. Mismatched or degenerate input returns zeros withfound=falserather than throwing.- A consistent half-frame offset - you're correlating frames that don't overlap enough, or the actual motion is more than a translation. The response value tells you which; watch it.
- The shift is right for the background and wrong for everything else - expected. One number describes the whole frame.
- Wiring it in a loop is slow or the graph re-executes oddly - remember ComfyUI caches aggressively; a node reading a changing frame batch needs its inputs to actually change, or the cache will hand you the previous result.
cv2DLL errors on Windows portable - the mixed-wheel problem that breaks every cv2 node in the install, reported by ComfyUI users across many packs. Environment, not graph.
Pack caveat as ever: LLM-assisted, personal, no production guarantee. For a Fourier-domain shift estimate wrapped faithfully over cv2.phaseCorrelate, that's a risk I'd take.
Inputs (4)
| Name | Type | Default | Description |
|---|---|---|---|
| image | NPARRAY,IMAGE | Reference image. Converted to grayscale float32 internally. 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. | |
| shifted | NPARRAY,IMAGE | The moved image. Must be the SAME size as 'image' - phase correlation compares spectra, so it cannot align frames of different sizes. 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. | |
| min_response | FLOAT | 0.000–1 | Minimum peak response for 'found' to be true. The response falls off as the overlap shrinks and as the motion stops being a pure shift, so this is the reject-the-nonsense knob. 0.0 accepts anything. |
| hann_windowopt | BOOLEAN | true | Apply the Hann window before correlating. Leave it on: without it the image borders act like a huge edge and can dominate the real peak. Turn it off only for already-windowed or periodic data. |
Outputs (5)
| Name | Type | Description |
|---|---|---|
| dx | FLOAT | Horizontal shift in pixels, sub-pixel accurate: how far 'shifted' moved RIGHT relative to 'image'. Warp it back with a translation matrix (2x3) into cv2.warpAffine. |
| dy | FLOAT | Vertical shift in pixels (positive = downward). |
| response | FLOAT | Peak sharpness in [0,1] - how confident the estimate is. 0.0 when not found. |
| translation | NPARRAY | The same shift as a 2x3 float32 affine matrix [[1,0,dx],[0,1,dy]], ready for cv2.warpAffine (identity when not found, so warping is a no-op). |
| found | BOOLEAN | — |