Nodes/genereti/ꘇ perception.measures
ComfyUI Node

ꘇ perception.measures

Perception.measures

By languel·Created 19 days ago·Updated a day ago· 1
ꘇ perception.measures
  • image
  • measures
  • observations
  • landmarks
◄streambody►
◄featureshands.left.thumb_tip hands.left.index_tip►
◄selection_streammeasures►
◄min_confidence0.40►
◄max_age_ms500.00►
◄definitions{"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}]}►

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.

CategoryGenereti / Perception

Inputs (7)

NameTypeDefaultDescription
streamSTRINGbody—
featuresSTRINGhands.left.thumb_tip hands.left.index_tip—
selection_streamSTRINGmeasures—
min_confidenceFLOAT0.400–1—
max_age_msFLOAT500.0050–10000—
definitionsSTRING{"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}]}—
imageoptIMAGE—

Outputs (3)

NameTypeDescription
measuresGENERETI_CHOP—
observationsGENERETI_DAT—
landmarksIMAGE—