ꘇ perception.measures
Perception.measures
- image
- measures
- observations
- landmarks
Pose detection in ComfyUI usually means one thing: a preprocessor chewing a frame into an OpenPose skeleton so a ControlNet can copy it. DWPose finds the joints, conditioning does the rest, and the output is a picture - image in, image out.
ꘇ perception.measures is the other thing you can do with a skeleton. It reads landmarks and gives you numbers: how far apart two fingertips are, where a nose sits, whether a hand is inside a box, whether two segments crossed. Numbers are a different currency - they drive sliders, gate queues, feed synth parameters. If you've ever wanted a gesture to do something rather than appear in a render, this is the node.
Where the landmarks come from
Not from here, and that's the important part: this node runs no inference at all. Somewhere in your graph is a ꘇ mediapipe.stream provider running MediaPipe in your browser tab (WASM, CPU), publishing its result under a name that every reader node shares. One provider, many readers, no duplicated work.
MediaPipe is Google's on-device ML library, and in this ecosystem it's usually the licence-friendly substitute for InsightFace's non-commercial face models - what people swap in when a project needs to be sellable. Here it does pose, hands and face in the browser: models download once, on request, into models/genereti/browser-perception, and the docs are blunt about the limits - CPU only, 15 inferences a second by default, 1024 px maximum per side.
So perception.measures is a consumer. It reads the newest result off a named stream, discards anything older than max_age_ms, drops points under min_confidence, computes your measures and republishes them under its own stream name.
The definitions document is the whole node
Everything interesting lives in one versioned JSON widget, and the default is a working example worth reading before you touch anything:
{"version":1,
"locators":[{"name":"handL","points":["hands.left.thumb_tip"]},
{"name":"fingerL","points":["hands.left.index_tip"]}],
"measures":[{"name":"nose","type":"point","a":"pose.nose"},
{"name":"pinch","type":"distance","a":"$handL","b":"$fingerL"}],
"events":[],
"contacts":[{"name":"pinch","feature":"pinch.distance","close":0.04,"open":0.07,
"hold_ms":40,"tap_ms":350,"click_ms":700,"cooldown_ms":200}]}
Locators give landmarks a short name you can reference with $. Measures are the geometry: point position, group centroid, aspect-corrected 2D distance in image-height units, inside/outside a box, finite segment crossing, hand geometry and counts, mouth and eye aperture, iris-in-eye.
The contacts block is the bit people skip and then wonder why their pinch flickers. close: 0.04 and open: 0.07 are hysteresis thresholds - the pinch starts under 0.04 and only releases above 0.07, and that dead band is the difference between one clean trigger and forty chattering ones. hold_ms, tap_ms, click_ms and cooldown_ms then turn a sustained touch into taps and clicks, with a minimum hold and a re-arm delay: button semantics you'd otherwise write by hand.
Inputs and outputs
stream is the provider's name and must match exactly (body). features takes space-separated landmark paths or prefixes; the default watches both left-hand fingertips, and subject-side names like hands.left.thumb_tip are what you want - the numeric hands.0.index_tip form is a per-frame detection index, not a stable identity. selection_stream is the name it publishes under. min_confidence (0.4) and max_age_ms (500) are the tolerance dials. The optional image underlays the preview.
Three outputs. measures is a GENERETI_CHOP - the channels you wire into the pack's modulation and audio nodes. observations is a GENERETI_DAT, the structured dump you inspect when a number looks wrong. landmarks is an IMAGE of the selected points over the source, so you can see what the node is measuring.
Installing
Manager → search Genereti → install → restart → refresh the tab. Manual:
cd ComfyUI
git clone https://github.com/languel/genereti.git custom_nodes/genereti
./.venv/bin/python -m pip install -r custom_nodes/genereti/requirements.txt
The only substantive pip line is comfyui-frontend-package>=1.53.6, because the nodes import comfy_api.latest and a stale install fails at import. No Python MediaPipe package is installed, no cloud service is called, nothing is uploaded - the browser fetches pinned models from Google and jsDelivr only when you ask.
Where people get burned
You queue it and it errors. perception.measures raises by design; its input is browser Live state, so a headless run has nothing to read. Start the provider (↓ download, ↑ load, ▶ start), then read outputs live.
Nothing is detected and every channel reads 0. Check observations before blaming the node. A misspelled stream name, a provider that never started, or a result older than max_age_ms all produce emptiness - and emptiness is not "hand at 0,0". Stale and low-confidence points come back unavailable, never as zero.
The camera started by itself? No. Subscribing to a stream never starts capture in this pack; camera and screen sources are separate nodes you start explicitly. That's why you can build these graphs before granting permissions.
Hands swap identity mid-take. Per-frame hand indices aren't track IDs - the pack says so itself. Build on subject-side names and geometry, not on "hand 0".
It works on your 640×360 webcam and not on a 4K source. Distances are in image-height units, and the provider caps input at 1024 px, asking for an upstream resize rather than mangling a bigger frame. Measure against consistent framing.
The pack labels itself super alpha, interfaces moving - back up workflows before updating. For a node holding a hand-authored measure document, that's not boilerplate.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| stream | STRING | body | — |
| features | STRING | hands.left.thumb_tip hands.left.index_tip | — |
| selection_stream | STRING | measures | — |
| min_confidence | FLOAT | 0.400–1 | — |
| max_age_ms | FLOAT | 500.0050–10000 | — |
| definitions | STRING | {"version":1,"locators":[{"name":"handL","points":["hands.left.thumb_tip"]},{"name":"fingerL","points":["hands.left.index_tip"]}],"measures":[{"name":"nose","type":"point","a":"pose.nose"},{"name":"pinch","type":"distance","a":"$handL","b":"$fingerL"}],"events":[],"contacts":[{"name":"pinch","feature":"pinch.distance","close":0.04,"open":0.07,"hold_ms":40,"tap_ms":350,"click_ms":700,"cooldown_ms":200}]} | — |
| imageopt | IMAGE | — |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| measures | GENERETI_CHOP | — |
| observations | GENERETI_DAT | — |
| landmarks | IMAGE | — |