Skip to content

Separate touching objects

CookbookRegions and masks

Use peaks in the foreground distance map as seeds for watershed segmentation.

Try the recipe ↓

The pipeline

VISUAL WALKTHROUGHRegions and masks
Follow the images, then read what passes to the next algorithm.
STARTING IMAGEOriginal sample448 × 320
The unchanged source image. All steps use this same example.

The unchanged source image. All steps use this same example.

STEP 01 · 1 / 1Foreground mask448 × 320
Foreground mask: The white foreground contains the objects to separate.

The white foreground contains the objects to separate.

STEP 01 / 03

Threshold the foreground

Explore the algorithm →
threshold

Build a binary object mask.

Receives
Bright touching objects against a darker background.
Passes to the next step
A binary foreground mask that may contain several touching objects in one component.

Why this step? Thresholding limits the problem to a foreground silhouette. Its boundary is crucial: the distance transform will measure distance to this boundary, so a missing edge or merged background region changes where object centres appear.

Actual OpenCV 5.0.0 results on the illustrated sample, using the lab’s default algorithm settings. Masks, overlays and normalized fields are labelled previews; the data contracts above describe what the algorithms really exchange. Try this chain with your images ↓

Why this chain works

Touching objects share a connected silhouette, so counting connected components cannot separate them. Their interiors can still have distinct centres. Use those centres as competing seeds and grow separate regions from them.

  1. 01

    Threshold the foreground

    threshold

    Thresholding limits the problem to a foreground silhouette. Its boundary is crucial: the distance transform will measure distance to this boundary, so a missing edge or merged background region changes where object centres appear.

    Receives
    Bright touching objects against a darker background.
    Passes on
    A binary foreground mask that may contain several touching objects in one component.
  2. 02

    Find interior seeds

    distanceTransformdilatecompareconnectedComponents

    Distance increases toward the interior. Comparing the distance map with its local dilation finds local maxima; a height threshold rejects shallow peaks. Connected components turn surviving peak areas into separate marker IDs. Pixels outside the mask become a known background marker, while the rest remain unassigned.

    Receives
    The foreground mask.
    Passes on
    A signed 32-bit marker image with distinct foreground seeds, background and unknown pixels.
  3. 03

    Flood from the seeds

    watershed

    Marker-controlled watershed expands competing regions using image differences. Where competing basins meet, it marks a boundary. This can split a connected silhouette because its seeds already encode multiple candidate objects.

    Receives
    The colour image and marker labels.
    Passes on
    A label image with -1 at watershed boundaries, overlaid as red lines on the original image.

Tune and diagnose

Choose the parameters

Lower the minimum peak height if smaller objects have no seeds. Increase the suppression radius if one object receives many nearby seeds, but keep it smaller than the spacing between genuine centres.

Read the result

Inspect Foreground seeds before the final boundary. Aim for one seed per intended object. If strong overlap leaves only one interior peak, watershed has no evidence that there should be two objects. Correct the mask or provide better markers.

Try it with your images

Choose your own image or start with the built-in sample. Run the recipe, then use the stage buttons to inspect intermediate results without rerunning it.

YOUR IMAGE · REAL OPENCV

Experiment at pixel level

Open full lab ↗

Use peaks in the foreground distance map as seeds for watershed segmentation.

The engine loads on your first run. Your images stay in this browser.

Input448 × 320
OutputWaiting for a result

Scroll over either image to zoom at the pointer. Use the scrollbars to pan both views over the same relative area. Zoom is relative to the input; pixel coordinates belong to each image. Warps can change scene correspondence.

Pixel inspector RGBA · native values · matched scale · 9 × 9 output pixels
Hover to inspect. Click to pin a pixel.
Input
Select a pixel

Output
Select a pixel

Sample models and licenses

Assumptions and limits

Seed quality controls the split. Very strong overlap can leave only one peak; noise can create too many seeds. Lower the peak height for smaller objects and adjust the suppression radius to their spacing.

TypeScript core chain

Initialize the shared engine once with await initOpenCV(), then use these named imports. In a bundled browser app, pass the WASM URL as shown in the quickstart. image is an 8-bit BGR Mat from that same engine; paired recipes receive an equally sized nextImage. Region recipes use an in-bounds pixel rect. Read outputs before the using scope ends. See matrix ownership.

import {
  CMP_GE,
  COLOR_BGR2GRAY,
  CV_8U,
  DIST_L2,
  MORPH_ELLIPSE,
  Mat,
  THRESH_BINARY,
  bitwise_and,
  compare,
  connectedComponents,
  cvtColor,
  dilate,
  distanceTransform,
  getStructuringElement,
  minMaxLoc,
  threshold,
  watershed
} from '@banou/opencv-wasm'

// The engine is already initialized; image is an 8-bit BGR Mat.
// "using" releases native handles at scope exit; inspect or copy outputs before then.

// 1. Allocate brightness, foreground mask and distance-to-background images.
using gray = new Mat(), mask = new Mat(), distance = new Mat()
// Allocate candidate seeds, integer watershed labels, and local-maximum scratch images.
using seeds = new Mat(), markers = new Mat(), dilated = new Mat(), peaks = new Mat()
// Convert the colour image to brightness for foreground thresholding.
cvtColor(image, gray, COLOR_BGR2GRAY)
// White pixels (brightness >127) define the touching-object silhouette.
threshold(gray, mask, 127, 255, THRESH_BINARY)
// 2. Measure approximate Euclidean distance to the nearest zero/background pixel.
// DIST_L2 uses Euclidean distance; the 5x5 mask controls this approximation.
distanceTransform(mask, distance, DIST_L2, 5)
// Keep interior pixels farther than 35% of the global maximum distance.
// This height cutoff prevents very shallow bumps from becoming seeds.
threshold(distance, seeds, minMaxLoc(distance).maxVal * 0.35, 255, THRESH_BINARY)
// Convert the float threshold result into an 8-bit candidate mask.
seeds.convertTo(seeds, CV_8U)
// A 31x31 ellipse searches roughly 15 pixels around each point for stronger peaks.
using neighborhood = getStructuringElement(MORPH_ELLIPSE, { width: 31, height: 31 })
// At each pixel, dilation records the largest distance in that neighbourhood.
dilate(distance, dilated, neighborhood)
// Select pixels equal to the neighbourhood maximum: candidate object centres.
compare(distance, dilated, peaks, CMP_GE)
// Keep only local maxima that also pass the interior-distance cutoff.
bitwise_and(peaks, seeds, seeds)
// 3. Give each connected seed area its own signed 32-bit integer label.
connectedComponents(seeds, markers)
// Encode watershed markers: 1 is known background, 2+ are foreground seeds,
// and 0 is unknown foreground that must be assigned during flooding.
for (let i = 0; i < markers.data32S.length; i++) {
  // Read the seed-component ID for this pixel (0 if it was not a seed).
  const label = markers.data32S[i]
  // Use the original mask to distinguish true background from unseeded foreground.
  markers.data32S[i] = mask.data[i] === 0 ? 1 : label ? label + 1 : 0
}
// 4. Grow competing regions over the colour image. markers is overwritten
// with region IDs; -1 marks a boundary where regions meet. Inspect seeds first:
// watershed cannot split two objects if they were given only one foreground seed.
watershed(image, markers)

The snippets isolate the core operations. The complete runnable recipes also include validation, filtering, overlays, intermediate previews and resource cleanup.