ꘇ top.opticalflow
It outputs a motion field, not a warped image
- image
- previous
- image
Everyone wires up an optical flow node expecting a smoothed video out the other side. That's not this node. top.opticalflow computes dense per-pixel motion between the current frame and a previous one and hands you the flow field itself as an image: red = horizontal motion, green = vertical, blue = confidence. You then decide what to do with it - most usefully, feed it to top.displace and let it warp something.
It's a genuinely unusual thing to have inside ComfyUI. The KB's read on the post-processing layer is that these deterministic primitives are old, cheap and badly packaged, and motion vectors are a good example: interpolation and warping tools usually assume they can compute flow internally and never expose it. Here it's just an image you can composite, colour-map, mask with, or record.
How it works
It's a dense 5×5 Lucas–Kanade solve over a bounded analysis resolution. That phrase matters:
- Dense - every pixel gets a vector, not just tracked features.
- 5×5 - the window the local gradient statistics are gathered over. Small window, so it's cheap and it resolves fine detail, but it can't see large displacement.
- Bounded analysis resolution -
analysis_width(default 256, max 512) sets the longest side the pair is downscaled to before solving. The flow field comes back at that reduced resolution. That's deliberate: it caps the cost, and it means flow is inherently soft compared to your source. Upscale it, don't expect detail.
The encoding is the part worth memorising, and the author spells it out: R/G are (channel − 0.5) × 2 × max_motion, in analysis pixels. So mid-grey (0.5) means zero motion, max_motion clamps how far a vector can be encoded, and a channel of 0.9 means the pixel moved by 0.8 × max_motion in that axis. If you displace with this field, you'll need to scale amount in fractions of the source, because the field is in pixels of a downscaled image. B is a confidence value derived from the local structure tensor - high where the window had real gradient structure to solve against, low on flat walls and uniform sky, which is exactly where flow is unreliable. Alpha is 1.
The previous input is optional. Live mode defaults to the previous source frame automatically; wiring something explicit overrides that history. In Queue there is no history, so you must supply previous yourself or the node returns a neutral field (all-zero motion).
regularization (tiny by default) is the damping term in the least-squares solve; raise it if you're getting wild vectors in low-texture regions.
The inputs that matter
analysis_width, max_motion, and previous. Output is one image - the field. The canonical next node is top.displace, with the flow as the displacement map and center 0.5. The field is also a nice thing to visualise directly through top.colormap, and top.channels will pull the B confidence out if you want to mask unreliable regions before you warp with them.
Install
ComfyUI Manager → search ꘇ Genereti → install, or:
git clone https://github.com/languel/genereti.git custom_nodes/genereti
./.venv/bin/python -m pip install -r custom_nodes/genereti/requirements.txt
Restart Comfy, refresh the browser. One frontend pin; no model weights, which is the appeal over a neural flow/interpolation model when you just need motion vectors. It's ꘇ top.opticalflow under Genereti / TOP; search genereti because the leading glyph sorts badly.
Traps
It doesn't warp anything. Output is data. Wire it into top.displace and remember to scale.
No pyramid. The author is explicit: "No pyramid: large motion/occlusion is unreliable." This solves small motion well and falls apart on big jumps, fast cuts and occlusions. For long displacements you need a coarse-to-fine method, which this isn't.
Analysis resolution is a hard ceiling. Asking for 512 gives you a blurry 512-wide field even if your source is 4K. That's the design, not a quality setting you can crank.
Queue needs previous. No history exists in a queued run, so without that wire you get a neutral field and you'll spend ten minutes wondering why the displacement is a no-op.
Live vs Queue. Live chains run on the browser GPU (WebGPU required) with automatic previous-frame history; Queue runs the PyTorch implementation and returns floats where live keeps 8-bit intermediates. Flow values can differ slightly between the two.
Pack status: super-alpha, early classroom testing. Back up your workflows.
Inputs (5)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — | |
| analysis_width | INT | 25616–512 | — |
| max_motion | FLOAT | 8.000.1–64 | — |
| regularization | FLOAT | 0.000.00001–0.1 | — |
| previousopt | IMAGE | — |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |