Skip to content

Count separated objects

CookbookRegions and masks

Separate bright objects from a dark background, clean the mask and count connected regions that pass an area filter.

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 / 1Threshold mask448 × 320
Threshold mask: Bright foreground after smoothing and thresholding.

Bright foreground after smoothing and thresholding.

STEP 01 / 03

Create a foreground mask

Explore the algorithm →
GaussianBlurthreshold

Smooth grayscale intensity and apply a threshold.

Receives
An image with brighter objects on a darker background.
Passes to the next step
A binary foreground mask.

Why this step? Blur reduces small fluctuations, and the threshold decides which intensities belong to foreground. That decision determines what an object means for every following step; the component counter has no knowledge of the original appearance.

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

Counting starts with a definition of foreground. Here that definition is brightness. Once each object is a separate white island, connected-component labelling turns the counting problem into counting islands.

  1. 01

    Create a foreground mask

    GaussianBlurthreshold

    Blur reduces small fluctuations, and the threshold decides which intensities belong to foreground. That decision determines what an object means for every following step; the component counter has no knowledge of the original appearance.

    Receives
    An image with brighter objects on a darker background.
    Passes on
    A binary foreground mask.
  2. 02

    Remove small noise

    morphologyExMORPH_OPEN

    Opening erodes and then dilates the mask, removing bright specks too small to hold the structuring element. It can also break narrow connections, but it can erase real thin objects, so its size must reflect the objects you want to count.

    Receives
    The foreground mask.
    Passes on
    A cleaner set of foreground islands.
  3. 03

    Count components

    connectedComponentsWithStats

    Labelling gives each connected island an integer ID. Statistics provide pixel area, bounding box and centroid; label zero is background. Only foreground labels meeting the minimum-area threshold contribute to the reported count.

    Receives
    The cleaned binary mask.
    Passes on
    Object count, numbered boxes and inspectable component IDs.

Tune and diagnose

Choose the parameters

First choose a threshold that separates objects from their surroundings, then use the smallest opening kernel that removes noise. Finally set the area cutoff below the smallest legitimate object. For dark objects, invert the threshold in your own chain.

Read the result

Compare the mask with the image: one island should correspond to one intended object. Touching objects count as one; use the watershed recipe when they need splitting. Uneven lighting may call for adaptive thresholding or colour selection.

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 ↗

Separate bright objects from a dark background, clean the mask and count connected regions that pass an area filter.

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

Touching objects become one component. Uneven lighting may need local thresholding or colour segmentation.

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 {
  CC_STAT_AREA,
  COLOR_BGR2GRAY,
  GaussianBlur,
  MORPH_ELLIPSE,
  MORPH_OPEN,
  Mat,
  THRESH_BINARY,
  connectedComponentsWithStats,
  cvtColor,
  getStructuringElement,
  morphologyEx,
  threshold
} 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 a grayscale view and a binary foreground mask.
using gray = new Mat(), mask = new Mat()
// Reduce colour to brightness because foreground is defined by intensity here.
cvtColor(image, gray, COLOR_BGR2GRAY)
// A 5x5 blur with sigma 1 suppresses small fluctuations before thresholding.
GaussianBlur(gray, gray, { width: 5, height: 5 }, 1)
// Pixels brighter than 127 become white (255); darker pixels become background (0).
threshold(gray, mask, 127, 255, THRESH_BINARY)
// Choose a 3x3 elliptical neighbourhood, smaller than objects you want to retain.
using kernel = getStructuringElement(MORPH_ELLIPSE, { width: 3, height: 3 })
// 2. Opening erodes then dilates: tiny white specks disappear while larger islands remain.
morphologyEx(mask, mask, MORPH_OPEN, kernel)
// Allocate per-pixel labels, per-label statistics, and x,y centroids.
using labels = new Mat(), stats = new Mat(), centroids = new Mat()
// 3. Label connected foreground islands. The returned count includes background label 0.
const count = connectedComponentsWithStats(mask, labels, stats, centroids)
// Accumulate only components large enough to count as intended objects.
let retained = 0
// Start at 1 to skip background. Each stats row has five signed 32-bit integers.
for (let label = 1; label < count; label++) {
  // CC_STAT_AREA selects the pixel-count column; require at least 80 pixels.
  // Touching objects share a label, so this counts connected islands, not semantic objects.
  if (stats.data32S[label * 5 + CC_STAT_AREA] >= 80) retained++
}
// Report the retained island count, which depends on threshold and cleanup choices.
console.log('Objects:', retained)

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