Skip to content

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

python
from telekinesis import cornea

segmented_image = cornea.segment_image_using_watershed(
    image=gradient_image,
    markers=markers,
    connectivity=1,
)
API Reference
Full parameter and return type documentation for segment_image_using_watershed.
View Reference →

Example

Input Image

Input image

Original image for watershed segmentation

Output Image

Output image

Separated objects using watershed algorithm

The Code

python
"""
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:

bash
cd telekinesis-examples
python examples/segmentation/segment_image_using_watershed.py

Parameter Configuration

These parameters supply the surface to flood, the seeds to grow it from, and the neighborhood rule used while growing.

KeyTypeDefaultDescription
imagedatatypes.Image | np.ndarrayrequiredThe (typically gradient or intensity) image to flood-fill from the markers, shape (H, W) or (H, W, 3)
markersdatatypes.Image | np.ndarrayrequiredSeed 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)
connectivitydatatypes.Int | int1How neighboring pixels are considered connected while growing regions: 1 for 4-connectivity (up/down/left/right), 2 for 8-connectivity (also diagonals)

Returns

TypeDescription
datatypes.SegmentationImageA 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

ExceptionCondition
TypeErrorA parameter's value does not match its expected type (see the Parameter Configuration table above)
ConfigurationErrorThe TELEKINESIS_API_KEY environment variable is not set
SerializationErrorThe request input failed to serialize, or the response failed to deserialize
RequestTimeoutErrorThe request to the Cornea service timed out
TransportErrorA network failure occurred before a response was received
ClientErrorThe Cornea service rejected the request due to invalid input, invalid data, or another unexpected 4xx response
AuthenticationErrorThe API key was rejected as invalid or expired
AuthenticationServiceErrorThe authentication service was unavailable
ServerErrorThe 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_markers does 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 — leaving 0 for 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, 1 or 2
  • Default: 1
  • Increase to 2 grows more readily across diagonal gaps, at the cost of potentially merging regions that 1 would keep separate
  • Keep at 1 when objects are close together and you want to preserve fine separations between them
  • Typical range: 1 or 2 — 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

Skillvs. Segment Image Using Watershed
segment_image_using_flood_fillFlood 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