cv2.circle
The cheapest way to put a ring on a frame (and build a mask)
- source
- center
- result
What it actually is
cv2.circle is OpenCV's "draw a circle on this array" call, exposed as a ComfyUI node with the same arguments. Nothing clever is happening: no model, no GPU, one function call, microseconds. That's the pitch. The deterministic primitive layer is exactly where you should stop burning a diffusion pass - a ring, a vignette, a marker on a detected blob is a two-line graphics operation, not a sampling problem (post-processing.md is the long version of that argument).
It ships in ComfyUI CV (bmad4ever/comfyui_cv), a pack of ~470 auto-generated raw cv2.* wrappers plus a few hundred hand-written curated nodes. This one is in the raw bucket, under image/CV/low-level/cv2 C, which is your cue that the author generated it from the OpenCV type stubs and did not sugar-coat it.
How it works
You hand it a canvas, a centre, a radius and a colour, and it rasterises the ring into a private copy of that array. Circle rasterisation in OpenCV walks the outline, so the cost is proportional to the circumference, not the area - drawing a filled circle is cheap, and drawing 200 thin ones is still cheap.
The canvas socket is polymorphic: a ComfyUI IMAGE, a MASK, or an NPARRAY all go in, and the output echoes whatever you plugged in. Feed it an IMAGE and you get an IMAGE back; feed it a MASK and you get a MASK back, drawn onto. So the two real uses are (a) annotate/visualise, and (b) synthesise geometry - draw a filled disc onto a black canvas and hand the result to an inpaint node as a mask. Circle is on the pack's per-frame-safe list, too, so a 4-frame IMAGE batch comes back as a 4-frame batch with every frame drawn on, not just frame 0.
Inputs and outputs that matter
- source - the canvas. IMAGE / MASK / NPARRAY.
- center - a
CV_TUPLE, i.e. one composite(x, y)value. It travels as a whole, so you either type the two numbers into the node or wire it from CV Tuple; you cannot connect just the x. Read the author's tooltip as a hint: build the point once, share it. - radius - plain INT, pixels.
- color - a literal string,
"(0, 255, 0)". BGR, not RGB - that green is green. A bare number broadcasts:"255"means white. Note the default is(0, 0, 0, 0), which is black, and on a black background that is a very convincing "the node is broken". - thickness (optional, 1) - negative means filled (
-1). - lineType (optional,
LINE_AA) - antialiased by default. Switch toLINE_8if you want a hard, aliased 1px stroke. - shift (optional, 0) - fractional bits in the centre/radius.
shift=1means your(100, 100)is actually(50, 50). Leave it alone unless you are deliberately working in sub-pixel space. - result - the drawn canvas, same type as
source. Wire it to a preview, or on tocv2.bitwise_and/ a mask consumer.
Optional inputs are advanced - collapsed until you hit "show advanced inputs" on the node.
Installing the pack
ComfyUI Manager → search ComfyUI CV. Manually:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
Restart ComfyUI. That's the whole install - the single dependency is the OpenCV wheel (opencv-contrib-python-headless~=5.0.0.93, per requirements.txt). The two hard gates are worth knowing up front: Python ≥ 3.12, and a ComfyUI recent enough to have the V3 node API. Older builds will just fail to load the pack.
Where people get burned
- Black circle, black background. Covered above, still the #1 report.
- "It needs contrib, not plain opencv." The
cv2namespace is shared by all four OpenCV wheels, so if something else pip-installsopencv-pythonover your contrib build, the contrib submodules go quiet and a chunk of this pack's nodes disappear. The pack shipstools/repair_opencv_contrib.pywith a--checkand an--applyfor exactly this. - Old ComfyUI portable installs with a broken
cv2.ImportError: DLL load failed while importing cv2is a real, well-documented failure around OpenCV-based node packs, and it takes every pack that touches cv2 down with it. If the whole pack fails to load rather than just this node, that is the class of problem you are in - not a bug incircle. - Set your expectations on support. The README is unusually blunt: LLM-assisted development, self-declared test overfitting risk, "not recommended for production", updates not planned. And the pack has essentially no Reddit footprint yet - searching the corpus turns up nothing about it. Read the node, test on your own images, don't file an issue expecting a fix.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| source | COMFY_MATCHTYPE_V3 | The image output(s) echo this input's format. Accepts a ComfyUI IMAGE/MASK directly (frame 0 of a batch) or an NPARRAY. Arithmetic ops (add, multiply, etc.) process the full IMAGE batch when both inputs have the same batch size. | |
| center | CV_TUPLE | 0,0 | Center of the circle. One value with 2 components (x, y) - it travels as a whole, so it cannot arrive half-connected. Wire it from 'CV Tuple' or type the components in place. |
| radius | INT | 0-2147483648–2147483647 | Radius of the circle. |
| color | STRING | (0, 0, 0, 0) | Circle color. cv2 Scalar as a literal, e.g. "(0, 255, 0)" (BGR) or "(0, 255, 0, 64)" (BGRA). A bare number broadcasts to every component, so "255" means (255, 255, 255, 255). Components past the target's channel count are ignored by OpenCV. |
| thicknessopt | INT | 1-2147483648–2147483647 | Thickness of the circle outline, if positive. Negative values, like #FILLED, mean that a filled circle is to be drawn. Preset to the OpenCV default (1). |
| lineTypeopt | COMBO | LINE_AA | Type of the circle boundary. See #LineTypes |
| shiftopt | INT | 0-2147483648–2147483647 | Number of fractional bits in the coordinates of the center and in the radius value. Preset to the OpenCV default (0). |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| result | COMFY_MATCHTYPE_V3 | Echoes the 'source' input's format: an IMAGE link comes back as IMAGE, MASK as MASK, NPARRAY stays NPARRAY. |