cv2.watershed
Separating blobs that are touching
- image
- markers
- nparray
Thresholding a photo of coins gives you one blob, not twelve coins. That's the problem watershed solves: objects that touch, that a global threshold fuses into a single connected region. You supply markers - seeds that say "this pixel belongs to object A, this one to object B, this one is unknown" - and watershed grows the regions outward until they meet, drawing a ridge line wherever two basins collide.
It's a classical segmentation algorithm from the 1970s that still earns its keep, because it's deterministic, fast, needs no model, and on high-contrast scenes with roughly blob-shaped objects it's hard to beat. On anything photographic and ambiguous, it will happily flood the background. Which is worth saying up front: for "cut this person out," a segmentation model is the tool, and the pack has curated nodes for the modern side of that too. For "these rocks/cells/coins/washers are touching and I need them apart," watershed is exactly right.
The mechanics that matter
Two things define the behaviour, and neither is the picture:
- Seed convention. Markers start as an int32 map where
0means "unknown, decide for me" and any positive label means "this is object N". The image itself supplies only the gradients watershed floods along - it does not define the regions. If you pass an image and an all-zero marker map, you get one region, or nothing useful. - The output is a marker map with walls. The function writes
-1along the boundaries where basins met. So the "segmentation" you get back is the same label map, now with contour lines in it. Everything downstream (per-region masks, area stats, fills) starts by dealing with those-1s.
Also worth knowing: cv2 refuses to run with the wrong marker type, and the error is one line - (-215:Assertion failed) src.type() == CV_8UC3 && dst.type() == CV_32SC1. The image is 8-bit 3-channel and the markers must be 32-bit signed single-channel. Feed a uint8 marker map and it dies instantly; feed a float mask and likewise.
Building markers (the actual work)
The seed map is where the effort goes. The pack's 22_watershed_playground example workflow is the classic recipe end to end:
Load Image → Image → CV Array (GRAY, uint8)
→ cv2.threshold (THRESH_BINARY_INV | THRESH_OTSU)
→ cv2.getStructuringElement + cv2.morphologyEx (MORPH_OPEN) → cv2.dilate
→ cv2.distanceTransform (DIST_L2, CV_32F)
→ cv2.normalize → cv2.threshold (≈150) → subtract
→ cv2.connectedComponents (CV_32S) ← the sure-foreground seeds
→ CV Cast Array (int32) ← the line cv2 requires
→ cv2.watershed
Then it compares the result against -1 (a CV Scalar literal plus cv2_compare, CMP_EQ) to actually see the walls, and the pack's CV Labels to Masks (full size) turns a finished label map into one MASK per region - which is what you'd hand to a detailer, an inpainter, or anything else in a modern graph.
Inputs and output
image- the 8-bit 3-channel picture.NPARRAY,IMAGEorMASK; frame 0 behaviour, and the op is per-frame batch safe if you feed anIMAGEbatch.markers- the int32 seed map, same size as the image.NPARRAY,IMAGEorMASK.
One NPARRAY out: the marker map with -1 at the boundaries. The wrapper copies arrays on the way in, so your seed map upstream isn't mutated - you get a new value.
Installing it
Ships inside ComfyUI CV. Manager → search ComfyUI CV, or:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"
# restart ComfyUI
Python ≥3.12, recent V3-API ComfyUI. This node has no models and no extra dependencies - it's core cv2.
What goes wrong
- The type assertion above. Cast your markers to int32 with
CV Cast Array. It's the single most common failure with this node. - Everything is one region. Your seeds were all one label (or all zero). The distance-transform + threshold step exists to produce distinct peaks - if the threshold is too generous you get one big blob and watershed has nothing to separate.
- The walls break downstream nodes.
-1is not a valid label anywhere else in the graph. Offset it (+1), mask it out, or convert to masks withCV Labels to Masks (full size)before you use the label map as labels. - Oversegmentation. Too many seeds, from a noisy distance map, gives you a mosaic. Blur or clean the seed image before thresholding.
- Wrong image size.
markersmust matchimagein width and height, or cv2 throws an assertion at you.
Inputs (2)
| Name | Type | Default | Description |
|---|---|---|---|
| image | NPARRAY,IMAGE,MASK | Input 8-bit 3-channel image. 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. | |
| markers | NPARRAY,IMAGE,MASK | Input/output 32-bit single-channel image (map) of markers. It should have the same size as image . 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 |
|---|---|---|
| nparray | NPARRAY | — |