ꘇ top.tochop
Read an image back as control data, on purpose
- image
- channels
- value
ꘇ top.tochop samples an IMAGE into named channels. You give it a small grid size, it resizes the frame, reads those pixels back to the CPU, and hands you r, g, b, a as CHOP channels plus a FLOAT. Now a picture can drive a parameter.
A GPU→CPU readback is the expensive thing in any visual pipeline, so this node is built around containing it: the sampling grid is capped at 64 pixels per axis, the readback rate is configurable, and only one read is allowed in flight. The author's framing is blunt - this is an "explicit image readback boundary", not a lossless round trip. Use it for probes, not for streaming pixels.
What it is
People come to ComfyUI from realtime tools asking for the same thing: the image should influence the rest of the rig. That question shows up constantly in r/comfyui threads about realtime video and TouchDesigner integration - the answer is usually "write a custom node". This is the pack's version of that node, and the honest caveat is that "genereti" as a search term returns zero threads in the reddit corpus, so you're on your own with the docs. They're good docs, though, and the mechanism is small enough to read.
Where it's genuinely handy: sampling a tiny grid of a finished render to check whether the frame is bright or dark and feed that back into a control, pulling one pixel's value into a numeric widget, driving CHOP operators from an image edit you made by hand, and building cheap image→signal feedback loops inside one graph.
How it works
The image is cropped to the selected batch frame, bilinearly resized to sample_width × sample_height, and converted: if the frame has three channels it gains an opaque alpha, so you always end up with four. Then it's moved to CPU and each channel's pixels are flattened in row-major order - the whole first row left to right, then the second row, and so on. Channels are named r, g, b, a, with values in 0..1 because the underlying tensor is normalized.
Because pixel order is row-major and flat, the last sample of r is the bottom-right pixel of your grid. That's what the value output reports, which is why people wire it somewhere and get a confusingly specific number. If you want something meaningful, select a channel and reduce it yourself - ꘇ chop.math can't average a block, but a 1×1 sample grid removes the whole question by giving you exactly one pixel per channel.
On the timing side: live, the node rate-limits its readback with a single in-flight read, so it paces itself rather than letting a fast upstream stall the frame. On Queue, it samples one explicit batch frame and moves on. A batch_index beyond the batch raises "Batch index exceeds IMAGE batch" instead of silently returning the last frame.
The inputs and outputs that matter
- image - the IMAGE to sample.
- sample_width / sample_height - INT 1..64 each, default 16. Total samples are the product, so 64×64 is 4096 positions per channel. Smaller is faster and usually enough.
- sample_rate - FLOAT 0.1..60, default
10, in samples per second. Ten a second is a sane default for control work; the author's own lesson uses 8×8 at 2 Hz for exactly this reason. - batch_index - INT 0..10000, default
0. Which frame of the batch to read when a video-ish source hands you many. - channels out -
r,g,b,a, row-major, 0..1.ꘇ chop.selectnarrows this to what you want. - value out - FLOAT, the last sample of
r(bottom-right of the grid). Useful as a quick "is it bright down there" probe, not as a general-purpose readout.
Install
Every operator here ships in the genereti pack. Manager → search genereti (title "ꘇ Genereti") → install → restart ComfyUI → hard-refresh the browser 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
Use your ComfyUI's Python for the pip step. Requirements are one line - comfyui-frontend-package>=1.53.6 - and this node uses the torch and NumPy ComfyUI already provides, so there's nothing to download and nothing to build. TOP rendering elsewhere in the pack needs a browser with WebGPU; this conversion path runs in Python and doesn't.
Common issues
"Batch index exceeds IMAGE batch." You asked for frame 5 of a one-frame batch. Check batch_index against whatever produced the image.
The numbers won't sit still. Live, this is a moving mouthful of pixels; the readback is rate-limited but the source is whatever the graph last produced. Set sample_rate down, use a tiny grid, and expect stepping.
It's slower than it looks. Deliberate: bounded grid, one in-flight read. If you find yourself pushing 64×64 at 60 a second, you're using the wrong tool - the docs are explicit that these converters are not lossless image round trips.
Alpha is always there. Three-channel images get an alpha of 1 appended, so a is a flat 1.0 unless your image genuinely had four channels. Don't build logic on alpha varying.
Cycles are not supported. Feed a CHOP derived from this back into the image's own upstream and the live graph reports it rather than looping.
Pack is an alpha. README: super alpha, early classroom testing, back up workflows before updating.
Inputs (5)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — | |
| sample_width | INT | 161–64 | — |
| sample_height | INT | 161–64 | — |
| sample_rate | FLOAT | 10.000.1–60 | — |
| batch_index | INT | 00–10000 | — |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| channels | GENERETI_CHOP | — |
| value | FLOAT | — |