Nodes/ComfyUI CV/CV Draw Flow Grid
ComfyUI Node

CV Draw Flow Grid

The quiver plot for dense optical flow

By bmad4ever·Created 4 months ago·Updated 14 days ago· 1
CV Draw Flow Grid
  • flow
  • source
  • nparray
  • count
◄step16►
◄scale1.0►
◄thickness1►
◄colormapCOLORMAP_VIRIDIS►
◄tip_length0.30►
◄min_magnitude0.00►
◄max_magnitude0.00►

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 - HxWx2 float, e.g. the flow output of CV 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 -> Image or Preview CV Array before anything that expects a ComfyUI image.
  • count - arrows drawn after the min_magnitude filter. 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_magnitude explicitly.
  • Output wired to a Save Image node and it complains. nparray is an NPARRAY; convert it first.
  • Arrows too short to see. Raise scale. Don't raise max_magnitude expecting length - that changes the colour mapping, not the arrow length.
  • Contrib build. This pack needs the contrib wheel; a plain opencv-python over it empties the contrib submodules and takes the contrib nodes with it. tools/repair_opencv_contrib.py --check / --apply.
Categoryimage/CV/features

Inputs (9)

NameTypeDefaultDescription
flowNPARRAYHxWx2 float (dx, dy) per pixel - the dense flow field from 'CV Optical Flow (Farneback)'.
stepINT162–256Grid spacing in pixels: one arrow is sampled every 'step' pixels. Smaller = denser, busier.
scaleFLOAT1.00.1–50Arrow-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.
thicknessINT11–32Arrow line width in pixels.
colormapCOMBOCOLORMAP_VIRIDISMaps 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.
sourceoptNPARRAY,IMAGE,MASKOptional 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_lengthoptFLOAT0.300–2Arrowhead length as a fraction of the shaft (cv2.arrowedLine tipLength).
min_magnitudeoptFLOAT0.000–1000000Skip arrows shorter than this many pixels - hides the static background. 0 draws every grid cell.
max_magnitudeoptFLOAT0.000–1000000Magnitude 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)

NameTypeDescription
nparrayNPARRAYBGR uint8 quiver rendering.
countINTHow many arrows were drawn (after the min_magnitude filter) - branch on it with if/else for nothing-moved.