Visual Crop (Studio Leiel)
Drag a crop box instead of typing coordinates
- image
- mask
- image
- x
- y
- width
- height
- mask
Every ComfyUI user has been there: you want to upscale just a face, or re-render one region of an image, and you're sat there eyeballing pixel coordinates into a Crop Image node like it's 1999. Visual Crop (Studio Leiel) kills that whole ritual. It shows the source image right on the node body, you drag a box over the part you care about, and the crop coordinates write themselves into the widgets. Run once, drag, run again. That's the entire workflow, and it's shockingly pleasant in practice.
The name is a lie in the best way - no API call, no key, no model download. It's a handshake between a few lines of Python and a JavaScript overlay, and everything happens inside your own ComfyUI instance.
How it works
The backend (visual_crop.py) is refreshingly small. When the node executes, it grabs the input tensor, saves a copy downscaled to 1024px into ComfyUI's temp/ folder, and returns that as the node's preview image. The bundled web/visual_crop.js then paints that preview inside the node and turns it into an interactive surface: drag on empty space to create a box, drag inside it to move it, drag a corner handle to resize it. Every move writes back into the x, y, width and height widgets, so the values you see in the widget bar are exactly the values that run.
Here's the clever bit: the preview is only for your eyes. The crop itself is always computed against the full-resolution source tensor (image[:, cy:cy+ch, cx:cx+cw, :] in the source), and the node re-snaps and re-clamps everything on every run. Coordinates snap to the snap unit - default 8, which keeps everything tidy for tile-based pipelines - and if you drag the box half off the image, it gets pulled back inside instead of erroring. That clamping matters for a couple of the "gotchas" below.
The inputs and outputs that matter
You'll barely touch most of these, because dragging fills them in - but the three worth knowing:
enabled- flip this OFF and the original image passes through uncropped. That's the README's tip for final renders: leave the node in place but switch it off instead of deleting it from the graph.snap- coordinate snapping unit, default 8. Lower it for finer control; 1 means no snapping.lock_aspect- one offree,1:1,4:5,5:4,3:2,2:3,16:9. Handy for keeping composition-friendly ratios while you resize.
The node outputs the cropped image plus four integers - x, y, width, height - reporting the values that were actually applied after snapping and clamping. Those loose INTs are the real power move. The README's intended pipeline is Image Loader → this node → Ultimate SD Upscale: crop a 1024×1024 region, upscale just that, then composite back onto the source with ImageCompositeMasked, multiplying the coordinates by your upscale factor (x*4, y*4 for a 4x). It's the fastest way to test whether a region actually has recoverable detail before committing to a full-image upscale - and since a small crop only runs a tile or two of Ultimate SD Upscale, it's also a cheap way to eyeball render conditions, exactly the tiled-upscale workflow our KB notes for arbitrary output sizes on limited VRAM.
One wiring note: unlike core's PrimitiveBoundingBox, this node emits four separate INTs rather than a BOUNDING_BOX, so if a downstream region node expects that structure you'll want to bundle the ints through a bounding-box node first.
Install
Through ComfyUI Manager, search "Visual Crop" and hit Install. Manually:
cd ComfyUI/custom_nodes
git clone https://github.com/StudioLeiel/comfyui_visual_crop
Then restart ComfyUI. That's it - there's no requirements.txt to wrestle with and no models to download; the pack is pure torch/numpy/PIL plus one JS file. About as dependency-free as a custom node gets.
Gotchas
- Preview vs. reality: the preview is capped at 1024px, but the crop always uses full source resolution. A "small-looking" box is still cropping real pixels.
- Stale JS: if the drag overlay misbehaves after an update, hard-refresh the browser (Ctrl+Shift+R). The JS is served from the
web/folder and browser caches love holding onto old copies - the README calls this out explicitly. - Clamping surprise: if the box goes outside the image bounds it's pulled back in automatically, which is great - but it means the applied coordinates can differ from what you dragged. That's exactly why the node reports the applied values as outputs; read those, not the widgets, when you're compositing.
It's a young, zero-impression pack from a single author, so keep expectations proportional: one small utility, not a framework. But for crop-then-upscale iteration it's genuinely the node I'd reach for over hand-typed numbers.
Inputs (9)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — | |
| enabled | BOOLEAN | true | — |
| x | INT | 00–16384 | — |
| y | INT | 00–16384 | — |
| width | INT | 10248–16384 | — |
| height | INT | 10248–16384 | — |
| snap | INT | 81–64 | — |
| lock_aspect | COMBO | 1:1 | 7 options: free, 1:1, 4:5, 5:4, 3:2, 2:3, +1 |
| maskopt | MASK | — |
Outputs (6)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |
| x | INT | — |
| y | INT | — |
| width | INT | — |
| height | INT | — |
| mask | MASK | — |