CV Draw Flow Vectors
Arrows between two point sets, the honest way to check tracking
- source
- points_from
- points_to
- labels
- image
- count
Why you'd reach for this
Feature tracking and sparse matching both produce the same shape of result: two equal-length, same-order arrays of points, points_a and points_b, where row i is the same feature in two frames. That's a list of motions - sparse optical flow - and again, it's unreadable as numbers.
Two point sets drawn as dots tell you almost nothing. Arrows from each from point to its matching to point tell you everything: whether the field is coherent, where it's coherent, and where a tracker has gone off into the noise. This is the diagnostic to run before you trust CV Track Features (KLT) output, and it doubles as the picture for motion segmentation, since cluster labels colour the arrows.
How it works
It computes to - from per row, draws cv2.arrowedLine for the ones that clear min_magnitude, and colours them one of three ways:
- fixed color - your BGR literal, uniform.
- direction (color wheel) - the arrow's hue is its angle, so all the arrows moving the same way share a colour. This is the standard flow-colour convention. Note the node's own caveat: hue alone, so it's a picture to look at, not a key to read back.
- magnitude (heatmap) - magnitude mapped through
magnitude_colormap, defaultCOLORMAP_VIRIDIS, chosen because it rises monotonically in luminance: dark is slow, bright is fast, and that survives greyscale and colour blindness.max_magnitudeat 0 auto-scales to the longest vector in this frame, which is fine on its own and wrong for comparing frames - set a fixed value when you want to compare.
Then there's the override that makes it more than a debug toy: connect labels and every arrow takes its cluster's colour from the pack's safe categorical palette, and color_by is ignored entirely. Motion clustering falls out of that - segment the arrows by label and you can see "these points are moving with the camera, those are pedestrians."
Hard requirement, stated by the node itself: points_from and points_to must be equal in count and order, or it raises with both counts. And this is the reason the pack's trackers publish pairs rather than a single array.
Inputs and outputs that matter
- points_from / points_to -
Nx1x2, e.g.points_a/points_bfromCV Track Features (KLT)orCV Match Features. - color_by - the mode. Direction for "is this field sane", magnitude for "where is it fast", fixed when the picture is going on a slide.
- min_magnitude - default 0 draws everything, which on KLT tracking output means a field of sub-pixel jitter arrows that hides the real motion. Set it to 0.5–1 and the actual movement appears.
- thickness (default 1) and tip_length (default 0.3, the arrowhead as a fraction of the shaft) - the arrow's appearance. Raise thickness when you're rendering small and the arrows are thready.
- labels (optional) - overrides the colouring with per-cluster colours.
- image - same format as the
sourceinput; and count, the arrows actually drawn after the magnitude filter. Branch on it for the nothing-moved case.
Install
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"
Python ≥ 3.12 and a ComfyUI with the V3 node API, or install ComfyUI CV from ComfyUI Manager. No model needed - this renders whatever points you give it.
Common issues
- Count mismatch error. The arrays aren't a matched pair.
CV Filter Points By Maskor the trackers'statusfiltering is where people drop one array and not the other. - A field of tiny arrows, no signal. KLT jitter at sub-pixel scale with
min_magnitude = 0. Raise it. - Arrows point backwards. You swapped the two inputs, which flips every vector and the hue with it. Direction colouring makes that mistake visible, which is a point in its favour.
- Mask canvas looks empty. On a single-channel canvas a BGR tuple keeps only its blue component. Use a single value, or you'll be staring at black.
- Contrib wheels. Install a non-contrib OpenCV over the contrib build and the shared
site-packages/cv2loses its contrib submodules; contrib-backed nodes disappear.tools/repair_opencv_contrib.py --checkthen--apply.
Inputs (11)
| Name | Type | Default | Description |
|---|---|---|---|
| source | COMFY_MATCHTYPE_V3 | Image or mask to draw on (a copy is made). 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. | |
| points_from | NPARRAY | Nx1x2 arrow tails - the points in the FIRST frame (e.g. 'Track Features (KLT)' points_a). | |
| points_to | NPARRAY | Nx1x2 arrow heads - the SAME points in the second frame (points_b); must match points_from in count and order. | |
| thickness | INT | 11–64 | Arrow line width in pixels. |
| color | STRING | (0, 255, 0) | Color as a single value (broadcast to all channels) or BGR tuple, e.g. '255' or '(0, 255, 0)'. Shorter tuples are zero-padded; longer tuples are truncated. Used only in 'fixed color' mode (and ignored when labels are connected). |
| color_by | COMBO | direction (color wheel) | How to color each arrow: a single fixed color; by motion DIRECTION (hue = angle, the standard flow wheel - hue alone, so it is a picture to look at, not a key to read); or by MAGNITUDE, through 'magnitude_colormap' (dark = slow -> bright = fast, readable as brightness alone). Connecting labels overrides this with per-cluster colors. |
| tip_lengthopt | FLOAT | 0.300–2 | Arrowhead length as a fraction of the shaft (cv2.arrowedLine tipLength). |
| min_magnitudeopt | FLOAT | 0.000–1000000 | Skip arrows shorter than this many pixels - hides sub-pixel tracking jitter. 0 draws everything. |
| max_magnitudeopt | FLOAT | 0.000–1000000 | Magnitude mapped to the hot end of the heatmap; larger clip. 0 = auto (this frame's longest vector) - set it to compare frames on one scale. |
| labelsopt | NPARRAY | (N,) integer labels: one color per cluster (e.g. motion segmentation), overriding color_by. | |
| magnitude_colormapopt | COMBO | COLORMAP_VIRIDIS | Ramp for the 'magnitude (heatmap)' mode. The default is perceptually uniform and rises monotonically in brightness, so slow-vs-fast survives colour blindness and greyscale; COLORMAP_JET is the familiar blue->red but does neither. |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| image | COMFY_MATCHTYPE_V3 | Same format as the image input. |
| count | INT | How many arrows were drawn (after the min_magnitude filter) - branch on it with if/else for the nothing-moved case. |