cv2.copyTo
One image, one mask, hard edges, no blending
- src
- mask
- result
cv2.copyTo does one thing: copy the pixels of an image where a mask says yes, and leave everything else black. It's the binary-cutout primitive - no feathering, no alpha maths, no second image to blend against. In the ComfyUI CV pack it's the node cv2.copyTo, category image/CV/low-level/cv2 C.
That sounds trivial until you're staring at a segmentation mask and want everything but the subject gone, or you want to isolate one region before a blur so the surrounding pixels can't bleed into it. This is the deterministic answer, and it's the kind of millisecond-scale, $0 operation you should reach for instead of an img2img pass.
How it works
Under the hood it's src.copyTo(dst, mask). cv2 walks the two arrays together: where a mask element is non-zero, the corresponding source pixel is written to the destination; where the mask is zero, it isn't. The destination here is freshly allocated by the node, and OpenCV's docs are specific about what that means - when the create() call reallocates the output, the new matrix is initialized to zeros before copying. So unmasked pixels come out black, not as whatever was there before. There is no "background" hiding behind this node; if you want a composite, you build the other half and add it.
Leaving mask unconnected is not an error: the pack passes None through, which cv2 reads as "the whole image", and you get a plain copy. (The author's table of optional buffer parameters lists copyTo's mask specifically because cv2 accepts None there - same reasoning as the photo module's region masks.)
The inputs and outputs that matter
src is the required input and it accepts IMAGE, MASK or NPARRAY; the single output result echoes whatever src was, so an IMAGE link comes back as IMAGE. That echo is why this node composes cleanly - no bridge nodes.
mask is the optional second input, same three socket types. A ComfyUI MASK arrives as float 0–1 and the pack converts it to uint8 0–255 for the cv2 call, so the selector behaves the way you expect from any mask in ComfyUI - whatever produced it, from a segmentation model to a hand-drawn brush. It must be the same width and height as src.
Where it earns its place
The masked copy is the piece you compose with. Cut the subject out with cv2.bitwise_not on the mask for the other side, cv2.copyTo twice, then cv2.add the two results - that's a full-frame composite built from primitives. Same trick isolates a region for a local operation: mask → copyTo → blur/filter → add back, so the filter only ever sees the pixels you meant it to see.
If you need soft edges, this is the wrong node and no amount of fiddling will fix it. Feathering needs weighted blending - cv2.blendLinear in this same pack, or an alpha composite - because copyTo is a hard switch at the mask boundary. Also worth knowing before you spend an hour on it: for pasting one crop into another image, the curated CV Paste by BBox / CV Crop by Masks nodes do the geometry work that copyTo doesn't.
Installing it
Manager → search the pack title (ComfyUI CV) → install → restart. Or:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"
Python ≥ 3.12 and a ComfyUI recent enough to have the V3 node API. The contrib wheel matters: if you ever install plain opencv-python on top of it, the shared site-packages/cv2 loses its contrib submodules and those nodes disappear. tools/repair_opencv_contrib.py --check / --apply exists for that accident.
Common issues and troubleshooting
"Sizes of input arguments do not match." The mask and the source aren't the same resolution - common when the mask came from a different branch of the graph that got resized or cropped.
Black background, not my background image. Correct behaviour, wrong expectation: the node only takes one image. Composite the two masked halves yourself with cv2.add.
The mask edge is jagged even though the mask looked smooth. A soft mask becomes a hard cut, because anything non-zero copies the pixel and anything zero doesn't. Rebuild the mask with a threshold at the level you actually want, then decide whether that boundary belongs in this node or a blending one.
A different node broke after installing this pack. The pinned wheel pulls numpy 2.x, and that's the collision everyone hits - insightface wants numpy 1.x, opencv wants 2.x, and people lose days to it. If FaceID/IPAdapter nodes died at the same time, that's the cause, not copyTo.
Inputs (2)
| Name | Type | Default | Description |
|---|---|---|---|
| src | 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. | |
| maskopt | NPARRAY,IMAGE,MASK | - - - Optional - leave unconnected for the OpenCV default (None). 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. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| result | COMFY_MATCHTYPE_V3 | Echoes the 'src' input's format: an IMAGE link comes back as IMAGE, MASK as MASK, NPARRAY stays NPARRAY. |