cv2.motempl.calcGlobalOrientation
Which way is it moving, in degrees
- orientation
- mask
- mhi
- float
This is the payoff of the motion-template chain: an image in, a single number out - the dominant direction of motion in the region you selected, in degrees, 0 to 360.
If you've built anything that reacts to a gesture, reads a wave, or just wants a "left or right" signal without training a model, this is the cheap classical answer. It doesn't know what moved, and it can't tell you how much. It knows which way.
The mechanism, briefly
An MHI stores when each pixel last moved. calcMotionGradient converts that timestamp field into a per-pixel direction plus a mask of pixels where the direction is meaningful. calcGlobalOrientation then histograms those directions - weighted by the MHI so that recent motion counts for more than stale motion (that weighting is the whole reason it needs the MHI, the timestamp and the duration rather than just the angle map) - and returns the weighted average.
It's a mean over the region, and that has a consequence worth stating plainly: if two things in your mask are moving in opposite directions, the average of two opposite angles is nonsense, not a compromise. Mask or crop down to one object. A wall-mounted camera watching a room gets a meaningless number for exactly this reason.
Inputs, and the one that bites
Everything is required, there are no optionals:
orientationandmask- the two outputs ofcv2.motempl.calcMotionGradient, in that order. They arrive unnamed from that node (nparray_0/nparray_1), so get them the right way round: the angle field goes toorientation, the binary one tomask. Previewing both once is faster than guessing. Not the MHI, and not a hand-made mask.mhi- the same motion-history image the gradient was computed from.timestampandduration- the same values you used for the MHI, in the same unit.
That last line is the classic bug: timestamp/duration mismatches are silent. The function re-derives the recentness weight from the MHI using these numbers, so a duration that doesn't match what the history was built with gives you a plausible-looking angle derived from the wrong weighting. If the number jumps around between frames while the scene is steady, check the units before you check the algorithm.
The single output socket is float, a FLOAT holding 0–360 - note it wraps through 360 rather than going to ±180, so "pointing left-ish" is around 180 and "pointing up" is somewhere near 90 or 270 depending on your image's Y direction (which points down). If your control mapping feels mirrored, that's usually it.
Wire it into anything taking a FLOAT: a log, a threshold/branch, or your own mapping. The pack's CV Array To Text / CV Array To Numbers bridges are how you get a group of numbers into a readable view when you're tuning.
Standing up the whole chain
You need all three nodes plus a way to iterate frames, because an MHI only exists across frames:
cv2_absdifftwo frames (or a background-subtractor foreground) →threshold→ the silhouette.cv2_motempl_updateMotionHistory(silhouette, mhi, timestamp, duration)→ the new MHI, fed back in next frame from a loop node.cv2_motempl_calcMotionGradient(mhi, delta1, delta2)→ orientation + valid mask. Empty mask means your deltas are wrong for your timestamp unit; see that node's page.- This node, with those two plus the MHI and the same timestamp/duration.
Install
ComfyUI Manager → search ComfyUI CV → install → restart, or:
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, ComfyUI on the V3 node API. cv2.motempl is contrib-only - the node silently disappears if a non-contrib opencv-python-headless gets installed over the contrib wheel. tools/repair_opencv_contrib.py --check / --apply fixes it. And note that these three are raw wrappers with no curated high-level node on top; the author hand-wrote their tooltips, which is more documentation than most of the generated layer gets.
Inputs (5)
| Name | Type | Default | Description |
|---|---|---|---|
| orientation | NPARRAY,IMAGE,MASK | - - - 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. | |
| mask | NPARRAY,IMAGE,MASK | - - - 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. | |
| mhi | NPARRAY,IMAGE,MASK | - - - 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. | |
| timestamp | FLOAT | 0.0000-1e+38–1e+38 | - - - |
| duration | FLOAT | 0.0000-1e+38–1e+38 | - - - |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| float | FLOAT | — |