Nodes/ComfyUI CV/cv2.watershed
ComfyUI Node

cv2.watershed

Separating blobs that are touching

By bmad4ever·Created 3 months ago·Updated 14 days ago· 1
cv2.watershed
  • 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 0 means "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 -1 along 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, IMAGE or MASK; frame 0 behaviour, and the op is per-frame batch safe if you feed an IMAGE batch.
  • markers - the int32 seed map, same size as the image. NPARRAY, IMAGE or MASK.

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. -1 is not a valid label anywhere else in the graph. Offset it (+1), mask it out, or convert to masks with CV 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. markers must match image in width and height, or cv2 throws an assertion at you.
Categoryimage/CV/low-level/cv2 W

Inputs (2)

NameTypeDefaultDescription
imageNPARRAY,IMAGE,MASKInput 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.
markersNPARRAY,IMAGE,MASKInput/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)

NameTypeDescription
nparrayNPARRAY—