ꘇ top.blobs
Give every bright blob an ID your visuals can follow
- image
- image
- blobs
Stick a bright shape on a dark background and most ComfyUI nodes can't tell you anything about it. top.blobs can: it thresholds the image into a mask and then finds the connected components in it, reporting each one's centroid, bounding box, area, and - in Live - a persistent ID plus a velocity per second. That's what lets a visual follow the same dot from frame to frame instead of teleporting to whichever shape happened to be biggest.
It's a utility TOP in ꘇ Genereti (languel/genereti, by Liuboto, MIT, super-alpha), the realtime creative-coding layer for ComfyUI. Worth setting expectations about what class of thing it is: this is classical computer vision, 8-connected component labelling, not a detector. The KB's detection essay draws the distinction that matters - a detector finds things; everything downstream is a loop over what it found. Here, you supply the "what" via a threshold and the node supplies the geometry.
Mechanism
The pipeline is short and legible:
- Threshold. With
channelset toluma, the test is luminance multiplied by alpha versusthreshold; withalpha, it's just alpha.invertflips it. The IMAGE output is that mask - opaque, three channels of the same value plus alpha 1 - so you can see what the node is working on, which is a genuinely good design choice. - Downsample for analysis. The mask is resized to
analysis_width(default 256, range 16–512, longest axis) before labelling. That's the cost control, and it's also the accuracy control: smaller analysis means fewer spurious components and coarser centroids. - Label. CPU-side 8-connected component labelling (scipy's
ndimage.labelwith a full 3×3 structure, which is why diagonal touching blobs count as one). - Filter and sort. Components smaller than
min_area(a fraction of the analysis frame, default 0.001) are dropped; the rest are sorted by area descending and cut tomax_blobs(default 32). - Track, in Live only. One-to-one nearest-centroid matching within
track_distance(a normalized distance, default 0.1) assigns stable IDs across frames; unmatched tracks stay matchable forhold_frames(default 3) before expiring. Velocity is reported per second.
Inputs and outputs
You'll set channel, threshold (0.5) and analysis_width most often; min_area is the one that cleans up noise; max_blobs caps your downstream work. track_distance and hold_frames only matter Live.
Outputs are image (the threshold mask as a normal IMAGE socket) and blobs (a GENERETI_DAT). The DAT is a table with the header batch, id, x, y, width, height, area, vx, vy, age, plus a JSON blob of the same records. Coordinates are normalized. From there it's DAT land: ꘇ dat.inspect to read it, dat.select / dat.expression / dat.tochop to get numbers into channels. That DAT-to-CHOP bridge is how a blob centroid becomes a translate parameter.
Live versus Queue - the important bit
This is the node where Genereti's live/queue split bites hardest, and the docs are unusually clear about it: tracking advances only on source frames, resets on rewire, settings change or seek, and Queue "processes each batch independently without temporal state." So a queued run labels each image in the batch from scratch and the id column is just ordering by area, not identity. Velocity is zero. If you build something that depends on IDs being stable, it works Live and lies on Queue. The author also notes the honest failure mode: merges, splits and crossings can change IDs even Live.
Also: this is not recognition or semantic tracking. Two identical dots are two blobs with no idea which is which.
Install
Manager → search "Genereti" → install ꘇ Genereti → restart ComfyUI → refresh the browser page. Manual:
git clone https://github.com/languel/genereti.git custom_nodes/genereti
./.venv/bin/python -m pip install -r custom_nodes/genereti/requirements.txt
The live path is WebGPU in the browser; the queued path is PyTorch plus an 8-connected labelling step that uses scipy (which ships with a normal ComfyUI install - a stripped environment is where that import would bite). Needs ComfyUI 0.38.2+, Python 3.11+. The pack's own benchmark for a 512² mask plus 256² readback and detection was about 0.8 ms in a browser fixture, with the usual caveat that it isn't full graph FPS.
Gotchas
Feed it a clean image. The threshold is doing all the detection work, so contrast is everything. If you're pulling blobs out of a photo, threshold the luma of a blurred or edge-detected copy, not the raw frame.
min_area is a fraction, not pixels. People set 0.01 expecting "10 pixels" and get "1% of the analysis frame," which at 256-wide is a big area. When everything vanishes, this is usually the culprit.
Downsampling moves centroids. Analysis happens at analysis_width, so blob geometry is quantized to that grid. Fine for interaction, not fine for measurement.
Inputs (9)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — | |
| channel | COMBO | 2 options: luma, alpha | |
| threshold | FLOAT | 0.500–1 | — |
| invert | BOOLEAN | false | — |
| analysis_width | INT | 25616–512 | — |
| min_area | FLOAT | 0.000–1 | — |
| max_blobs | INT | 321–64 | — |
| track_distance | FLOAT | 0.100–1 | — |
| hold_frames | INT | 30–30 | — |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |
| blobs | GENERETI_DAT | — |