CV Draw Flow Grid
The quiver plot for dense optical flow
- flow
- source
- nparray
- count
Why you'd reach for this
Dense optical flow is an HxWx2 field of (dx, dy) vectors - one per pixel, tens of thousands of numbers, and completely unreadable as numbers. Two ways to look at it. The per-pixel direction wheel (this pack's CV Flow To Color) gives you a picture of the whole field, which is great for showing off and bad for comparing magnitudes. The classic alternative is the quiver plot: sample the field on a regular grid, draw an arrow per sample, colour it by how fast that spot moved.
That's this node. It's the one to reach for when the question is "is this flow field usable?" rather than "does this flow field look pretty?", because a grid makes bad flow obvious - arrows pointing in every direction in a flat region means the estimator is hallucinating texture.
How it works
It takes samples on a grid every step pixels (offset to the cell centre), reads the flow vector at each sample, and draws it with cv2.arrowedLine. scale multiplies the arrow's length for visibility only - the colour still encodes the true, unscaled magnitude. That separation is deliberate and it's a nice detail, because sub-pixel motion produces arrows too short to see, and it's tempting to fix that by lying about the magnitude. You can't; the colour is computed from the real numbers.
Colour comes from cv2.applyColorMap through a named colormap, COLORMAP_VIRIDIS by default. That default is chosen rather than defaulted: VIRIDIS rises monotonically in luminance, so slow-versus-fast reads correctly in greyscale and for a colour-blind viewer. COLORMAP_JET is the familiar blue-to-red and does neither - it invents structure that isn't in the data. The author is consistent about this across the pack's ramp-colouring nodes, and he's right.
max_magnitude is the frame-comparison control. At 0 it auto-scales to the fastest arrow in this field, which is fine for a single frame and useless for comparing two frames - the second frame will rescale and a slower move can still show as hot. Set a fixed max and frames share a scale. min_magnitude hides the static background, which is often most of the frame.
Optional source gives you a real photograph to draw over instead of a black canvas; if the sizes differ it's resized with area interpolation to match the flow.
Inputs and outputs that matter
- flow -
HxWx2float, e.g. theflowoutput ofCV Optical Flow (Farneback). Any other shape raises with the shape printed, and the message names the node you probably meant. - step - grid spacing, default 16. Smaller is busier; on a 1080p frame, 16 gives you a readable density and 4 gives you a texture.
- scale, thickness, tip_length - arrow appearance.
- min_magnitude / max_magnitude - the two numbers that turn this from a picture into a measurement.
- nparray - a BGR uint8 rendering, not an IMAGE. Run it through
CV Array -> ImageorPreview CV Arraybefore anything that expects a ComfyUI image. - count - arrows drawn after the
min_magnitudefilter. Zero means nothing moved above your threshold, which is a real answer for a locked-off shot, and a good thing to branch on.
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, current ComfyUI (V3 node API), or ComfyUI CV via Manager. No model involved - flow comes from an upstream cv2 node, and this one just renders it on the CPU.
Common issues
- Blank output. An all-zero flow field draws nothing and returns
count = 0. Check the upstream estimator got a real pair of frames, in the right order (params order changes the sign of the vectors). - Everything looks slow/the same speed. Auto-scaling. Set
max_magnitudeexplicitly. - Output wired to a Save Image node and it complains.
nparrayis an NPARRAY; convert it first. - Arrows too short to see. Raise
scale. Don't raisemax_magnitudeexpecting length - that changes the colour mapping, not the arrow length. - Contrib build. This pack needs the contrib wheel; a plain
opencv-pythonover it empties the contrib submodules and takes the contrib nodes with it.tools/repair_opencv_contrib.py --check/--apply.
Inputs (9)
| Name | Type | Default | Description |
|---|---|---|---|
| flow | NPARRAY | HxWx2 float (dx, dy) per pixel - the dense flow field from 'CV Optical Flow (Farneback)'. | |
| step | INT | 162–256 | Grid spacing in pixels: one arrow is sampled every 'step' pixels. Smaller = denser, busier. |
| scale | FLOAT | 1.00.1–50 | Arrow-length multiplier for visibility only (the COLOR still encodes the true, unscaled magnitude). Raise it when sub-pixel flow makes the arrows too short to see. |
| thickness | INT | 11–32 | Arrow line width in pixels. |
| colormap | COMBO | COLORMAP_VIRIDIS | Maps magnitude to color. The slowest grid arrow takes the cold end, the fastest the hot end. The default is perceptually uniform and rises monotonically in brightness, so slow-vs-fast reads correctly for a colour-blind viewer and in greyscale; COLORMAP_JET is the familiar blue->red but does neither. |
| sourceopt | NPARRAY,IMAGE,MASK | Optional canvas (IMAGE or MASK) to draw the arrows over (resized to the flow if the sizes differ); a black canvas is used if omitted. 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. | |
| 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 the static background. 0 draws every grid cell. |
| max_magnitudeopt | FLOAT | 0.000–1000000 | Magnitude mapped to the hot end of the colormap; larger values clip. 0 = auto (this field's fastest grid arrow) - set it to compare frames on one scale. |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| nparray | NPARRAY | BGR uint8 quiver rendering. |
| count | INT | How many arrows were drawn (after the min_magnitude filter) - branch on it with if/else for nothing-moved. |