Nodes/ComfyUI CV/cv2.meanShift
ComfyUI Node

cv2.meanShift

A mode-seeking search window, and the rectangle it lands on

By bmad4ever·Created 3 months ago·Updated 14 days ago· 1
cv2.meanShift
  • probImage
  • iterations
  • window
◄window_x0►
◄window_y0►
◄window_w0►
◄window_h0►
◄criteria_typemax count or epsilon (whichever first)►
◄criteria_max_count30►
◄criteria_epsilon0.00►

meanShift is a tracking algorithm with no model, no training and no learned anything. You give it a probability image and a starting window; it slides that window uphill until it sits on the densest nearby region, and returns the window where it stopped. It's the algorithm behind the classic "histogram matching tracker" - pick an object's colours, back-project them over the frame, and meanShift finds the blob.

What makes it a nice fit for this pack is that the pack exposes the rect and termination criteria as flat widgets instead of opaque structs, and returns the result as core ComfyUI data. So the output is a BOUNDING_BOX you can crop with or draw, not an ndarray you have to decode.

How the node is laid out

probImage is the back-projection: the per-pixel probability that a pixel belongs to your target, normally produced with cv2.calcBackProject from a histogram (cv2.calcHist on a reference crop). It accepts NPARRAY, IMAGE/MASK or NPARRAY - and for this node it should be a single-channel 8-bit map, which is what a back-projection of an 8-bit image naturally is.

Where most packs would give you a Rect widget, this one gives four integers: window_x, window_y, window_w, window_h - the starting search window. Note both zero-by-defaults: a window with zero width or height has nothing to search in, so set real values. Then the termination criteria, which this pack also splits into three widgets: criteria_type (stop after the iteration count, when the movement drops below epsilon, or whichever happens first - that last one is the default), criteria_max_count (30 by default), and criteria_epsilon (0.001).

Two outputs. iterations is an INT - how many steps it took before it stopped, which is your signal that the search converged (5 is healthy, 30 means it hit the cap and is probably oscillating). window is a BOUNDING_BOX: {x, y, width, height}, nested one group per frame, and directly consumable by crop-by-bounding-box and draw-bbox nodes, or splittable into the four numbers via CV Split Tuple.

What it's realistically good for

One frame, one mode. This is a single-shot refinement step: "given that the object was roughly here last frame and looks like this colour-wise, where is it now?" It has no memory and no cross-frame state, so it isn't a video tracker by itself - a graph re-runs per frame but nothing carries the previous window forward unless you wire it that way deliberately. Used as a nudge, on a well-separated back-projection, it's quick and surprisingly effective. Used as "track my subject through this clip", you'll be disappointed.

It's also a decent demonstration of why back-projection matters: feed it a raw image instead of a probability map and you're asking "where is the brightest patch", which is a legitimate but different question.

Installing the pack

ComfyUI CV (bmad4ever/comfyui_cv) - ComfyUI Manager, search "comfyui_cv", or:

cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"

Restart ComfyUI. Python ≥ 3.12, a ComfyUI with the V3 node API, that one pinned contrib OpenCV wheel, nothing to download. The histogram nodes you'll want alongside it (cv2.calcHist, cv2.calcBackProject) are in the same pack.

Where people get burned

Zero-size window. The widget defaults are 0,0,0,0, which is not a valid search region - set all four or you get an OpenCV error and no clue which widget did it.

Coordinates in the wrong space. window_x/y are pixels in probImage's own resolution. If you downscaled the frame before back-projecting, the window you get back is in the small space and has to be scaled up before you crop the original.

A 3-channel or float probImage. Back-projections are single-channel 8-bit; anything else is either an error or a meaningless landscape of local maxima.

It didn't move. That's success, usually - it started on the mode. If it moved somewhere stupid, look at the back-projection with Preview CV Array; a flat probability image has no modes, so the first tiny variation wins.

Contrib nodes missing from the menu. Classic pack-wide injury: the four opencv-* wheels share one site-packages/cv2, so a non-contrib wheel installed over the contrib one empties the contrib submodules and those nodes never register. python tools/repair_opencv_contrib.py --check, then --apply.

Categoryimage/CV/low-level/cv2 M

Inputs (8)

NameTypeDefaultDescription
probImageNPARRAY,IMAGE,MASKBack projection of the object histogram. See calcBackProject for details. 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.
window_xINT0-2147483648–2147483647Rectangle top-left corner X in pixels.
window_yINT0-2147483648–2147483647Rectangle top-left corner Y in pixels.
window_wINT00–2147483647Rectangle width in pixels (>= 0).
window_hINT00–2147483647Rectangle height in pixels (>= 0).
criteria_typeCOMBOmax count or epsilon (whichever first)When to stop iterating: after max_count iterations, when the change drops below epsilon, or whichever comes first.
criteria_max_countINT301–2147483647Maximum iterations (ignored when 'epsilon only').
criteria_epsilonFLOAT0.000–1e+38Target accuracy / smallest change worth continuing for (ignored when 'max count only').

Outputs (2)

NameTypeDescription
iterationsINT—
windowBOUNDING_BOX- - - A cv2 Rect as core BOUNDING_BOX data ({x, y, width, height}, nested one group per frame) - feed 'Crop By Bounding Boxes', 'Draw BBoxes', or 'CV Split Tuple' for x/y/w/h.