Segment Image Using Watershed
SUMMARY
Segment Image Using Watershed grows marker seeds outward across an image until they meet at watershed lines, separating touching or overlapping objects.
It treats image as a topographic surface (brighter = higher) and floods it outward from each labeled seed region in markers, growing every region until they meet — the meeting lines become the segment boundaries. This needs good markers to work well: a few pixels per object labeled as seeds, plus one region marked as background/unknown, typically built from thresholding and morphological cleanup. It should also run on an image gradient rather than the raw image, so the watershed lines fall on real edges instead of arbitrary raw-intensity contours. Useful for separating touching or overlapping objects that a single global threshold would merge into one blob (e.g. touching coins or cells).
Use this Skill when you want to separate touching or overlapping objects that a single global threshold would merge into one blob.
The Skill
from telekinesis import cornea
segmented_image = cornea.segment_image_using_watershed(
image=gradient_image,
markers=markers,
connectivity=1,
)Example
Input Image

Original image for watershed segmentation
Output Image

Separated objects using watershed algorithm
The Code
"""
Demonstrates watershed segmentation.
"""
import cv2
import numpy as np
from loguru import logger
import rerun as rr
from telekinesis import cornea, datatypes, pupil
def segment_image_using_watershed_example():
"""Segments an image using the watershed algorithm."""
# ===================== Load Image ==========================================
image_url = "https://assets.telekinesis.ai/examples/v1/images/water_coins.jpg"
image = datatypes.Image.from_url(url=image_url)
image_np = image.to_numpy()
# ===================== Run Skill ==========================================
markers = datatypes.Image(_build_watershed_markers(image_np.copy()))
gray = cv2.cvtColor(image_np, cv2.COLOR_RGB2GRAY)
gray_image = datatypes.Image(gray)
gradient_y = pupil.filter_image_using_sobel(image=gray_image, dx=0, dy=1).to_numpy()
gradient_x = pupil.filter_image_using_sobel(image=gray_image, dx=1, dy=0).to_numpy()
gradient = np.sqrt(gradient_x**2 + gradient_y**2)
gradient_normalized = (
(gradient - gradient.min()) / (gradient.max() - gradient.min() + 1e-12) * 255
).astype(np.uint8)
gradient_image = datatypes.Image(gradient_normalized)
segmented_image = cornea.segment_image_using_watershed(
image=gradient_image, markers=markers, connectivity=1
)
# ===================== Log ================================================
logger.success(f"Segmented {image} using the watershed algorithm.")
logger.success(f"Results: {segmented_image}")
logger.info(f"Segmented image label codes: {segmented_image.label_codes}")
logger.info(f"Segmented image number of labels: {segmented_image.number_of_labels}")
logger.info(f"Segmented image shape: {segmented_image.shape}")
logger.info(f"Segmented image dtype: {segmented_image.dtype}")
# ===================== Visualization (Optional) ======================
rr.init("segment_image_using_watershed_example", spawn=True)
datatypes.visualize(image, entity_path="/input_image")
datatypes.visualize(segmented_image, entity_path="/segmented_image")
def _build_watershed_markers(rgb_image_np, kernel_size=3, opening_iterations=2,
dilate_iterations=3, dist_fg_ratio=0.7):
"""Builds watershed markers from an RGB image using morphological operations."""
if rgb_image_np.ndim == 2:
gray = rgb_image_np
else:
gray = cv2.cvtColor(rgb_image_np, cv2.COLOR_RGB2GRAY)
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
kernel = np.ones((kernel_size, kernel_size), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=opening_iterations)
sure_bg = cv2.dilate(opening, kernel, iterations=dilate_iterations)
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
_, sure_fg = cv2.threshold(dist_transform, dist_fg_ratio * dist_transform.max(), 255, 0)
sure_fg_u8 = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg, sure_fg_u8)
num_labels, markers = cv2.connectedComponents(sure_fg_u8)
markers = markers + 1
markers[unknown == 255] = 0
return markers.astype(np.int32)
if __name__ == "__main__":
segment_image_using_watershed_example()Runnable examples are available in the Telekinesis examples repository.
Follow the README in that repository to set up the environment, run this specific example with:
cd telekinesis-examples
python examples/segmentation/segment_image_using_watershed.pyParameter Configuration
These parameters supply the surface to flood, the seeds to grow it from, and the neighborhood rule used while growing.
| Key | Type | Default | Description |
|---|---|---|---|
image | datatypes.Image | np.ndarray | required | The (typically gradient or intensity) image to flood-fill from the markers, shape (H, W) or (H, W, 3) |
markers | datatypes.Image | np.ndarray | required | Seed labels to grow, the same (H, W) size as image. 0 marks unknown/to-be-filled pixels, and each positive integer marks a different seed region (e.g. 1 for background, 2+ for each foreground object) |
connectivity | datatypes.Int | int | 1 | How neighboring pixels are considered connected while growing regions: 1 for 4-connectivity (up/down/left/right), 2 for 8-connectivity (also diagonals) |
Returns
| Type | Description |
|---|---|
datatypes.SegmentationImage | A per-pixel label map, shape (H, W), where each pixel is assigned the label of the seed region it was grown from. Use .data for the raw label array, .label_codes for the sorted array of unique ids present, .number_of_labels for how many regions were found, and .shape/.dtype for its size and label dtype. |
Raises
| Exception | Condition |
|---|---|
TypeError | A parameter's value does not match its expected type (see the Parameter Configuration table above) |
ConfigurationError | The TELEKINESIS_API_KEY environment variable is not set |
SerializationError | The request input failed to serialize, or the response failed to deserialize |
RequestTimeoutError | The request to the Cornea service timed out |
TransportError | A network failure occurred before a response was received |
ClientError | The Cornea service rejected the request due to invalid input, invalid data, or another unexpected 4xx response |
AuthenticationError | The API key was rejected as invalid or expired |
AuthenticationServiceError | The authentication service was unavailable |
ServerError | The Cornea service returned a 5xx or otherwise unexpected error response |
How to Tune the Parameters
markers
- Controls: The seed regions that watershed grows outward from; every output pixel inherits the label of whichever seed's flood reaches it first.
- Units: Label image (integer ids), same size as
image - Default: required — no default
- Build markers from thresholding plus morphological cleanup. The reference example's
_build_watershed_markersdoes this by Otsu-thresholding the image, applying morphological opening to remove speckle noise, running a distance transform on the cleaned mask, and labeling the resulting "sure foreground" blobs into distinct seed ids with connected-component labeling — leaving0for the unknown region between "sure foreground" and "sure background" - Place at least one seed per object you want as its own region, plus one region for the background — markers that are too few or badly placed will merge or split objects incorrectly no matter how good the rest of the pipeline is
connectivity
- Controls: Whether region growth considers only up/down/left/right neighbors (
1) or also diagonal neighbors (2). - Units: Integer,
1or2 - Default:
1 - Increase to
2→ grows more readily across diagonal gaps, at the cost of potentially merging regions that1would keep separate - Keep at
1when objects are close together and you want to preserve fine separations between them - Typical range:
1or2— no other values are valid for a 2D image
TIP
Build markers from the same image you're segmenting — threshold it, clean the mask with morphological opening to remove speckle noise, then use a distance transform plus connected-component labeling to turn each "sure foreground" blob into its own seed id (see _build_watershed_markers in the reference example) — and run watershed itself on that image's gradient, not its raw pixel values, so the resulting boundaries fall on real edges.
Where to Use the Skill
Common pipelines include:
- Separating touching objects – splitting coins, parts, or cells that a single threshold would merge into one blob
- Cell/particle counting – segmenting individual cells or particles in microscopy or industrial inspection images
- Object counting on a line or tray – counting touching parts before further per-part processing
Alternative Skills
| Skill | vs. Segment Image Using Watershed |
|---|---|
| segment_image_using_flood_fill | Flood fill grows a single region from one seed point by color similarity. Use flood fill for one connected region; use watershed when you need several regions separated at once, each grown from its own marker. |
When Not to Use the Skill
Do not use Segment Image Using Watershed when:
- You don't have (or can't build) good markers — a few seed pixels per object plus a background/unknown region are required; without them the algorithm has nothing to grow from
- Objects are already well-separated — a simpler global or adaptive threshold segments them without the overhead of building markers
- You're tempted to run it directly on the raw image — run watershed on an image gradient instead (e.g. via
pupil.filter_image_using_sobel), so the resulting watershed lines fall on real object edges rather than arbitrary raw-intensity contours

