You have a grayscale frame, a person, and a wall covered in clutter. The adaptive threshold that isolated the person cleanly on a plain background now returns fragments: pieces of the body plus every shadow, poster edge and chair leg in the scene. The cause is usually not the threshold values — it is the order of the operations around them, plus an area filter tuned for a different image size. This guide rewrites the pipeline step by step in the OpenCV Java bindings you already use, sizes the contour area filter from the frame resolution, and states when to stop tracing edges and use a person detector instead.
Why the contour falls apart
Every function in the chain has an input contract, and yours violates two of them.
-
adaptiveThresholdwithADAPTIVE_THRESH_GAUSSIAN_CandTHRESH_BINARY_INValready returns a finished binary mask: each pixel is 0 or 255, and the person is a filled white shape. -
Cannythresholds gradients, so it wants a grayscale image with gradual intensity changes. Fed a binary mask, the only gradients left are single-pixel steps, and it answers with one-pixel-wide outlines that break at every gap. -
GaussianBlurafter Canny smears those outlines into soft gray ramps.findContourssegments a binary image; soft ramps give it disconnected curves. -
findContoursis the one step that wants exactly what the threshold produced. Hand it the mask directly.
The same failure — Canny between the threshold and the contour search shredding the shape — is what leaves open contours after Canny: the fix there is also to threshold the mask and skip the edge pass. The ordering rule generalizes: filter the noise out before the threshold, never between the threshold and the contour search.
What you need
- OpenCV with the Java bindings — the
Imgproccalls already in your code. - A camera on a fixed mount. A moving viewpoint turns both adaptive thresholds and background subtraction into moving targets.
- A known frame size. Every constant below scales with it; the worked example uses 4000 × 3000.
- Some control over the wall behind the person. The lighting section shows how much that buys.
If the thresholding concepts are new, edge detection without a vision library covers what a binary mask is and why contour analysis starts there. For the camera itself, browse area scan cameras or the camera finder.
The corrected pipeline, step by step
- Convert the frame to grayscale, as you already do.
- Blur first.
Imgproc.GaussianBlur(gray, gray, new Size(5, 5), 0)— the 5 × 5 kernel suppresses pixel-scale noise before any threshold decision. On grainy frames step up to 7 × 7; beyond that the person's outline softens. - Threshold second.
Imgproc.adaptiveThreshold(gray, mask, 255, Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C, Imgproc.THRESH_BINARY_INV, 7, 3)— blockSize 7 with C 3 is a sound start, andTHRESH_BINARY_INVmakes the darker person the white foreground. Keep blockSize odd; raise it toward 21 when the background texture is coarser than the person's outline. - Close the mask. A morphological close — dilate, then erode, with the same 5 × 5 kernel — fills the pinholes between arm and torso and through hair. Threshold then morphology walks through this pairing for part detection; the order is the same here: binarize, then clean the binary shape.
- Search contours on the mask.
Imgproc.findContours(mask, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE)—RETR_EXTERNALkeeps the outer boundary only, one contour per connected white shape, which is the silhouette you want to draw. - Filter by area band, then draw:
// Illustrative variable names; the Imgproc calls are the documented OpenCV Java API.
Mat gray = new Mat(); // grayscale input frame
Mat mask = new Mat(); // binary silhouette mask
Mat hMat = new Mat(); // colour frame you draw on
Mat hierarchy = new Mat();
ArrayList contours = new ArrayList(); // the same raw list your code already uses
Imgproc.GaussianBlur(gray, gray, new Size(5, 5), 0);
Imgproc.adaptiveThreshold(gray, mask, 255,
Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C,
Imgproc.THRESH_BINARY_INV, 7, 3);
Imgproc.findContours(mask, contours, hierarchy,
Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE);
double areaFloor = 60000; // from the worked example below
double areaCeil = 4800000; // from the worked example below
for (int i = 0; i < contours.size(); i++) {
double cont_area = Imgproc.contourArea(contours.get(i));
if (cont_area >= areaFloor && cont_area <= areaCeil) {
Imgproc.drawContours(hMat, contours, i, new Scalar(130, 0, 15), 4);
}
}
- Inspect every intermediate image while tuning: gray, blurred, mask, contours on the original. Parameters chosen blind are rarely right.
The test that proves the reorder worked: on a frame with one person clear of the wall texture, this pipeline returns exactly one external contour with an area inside the band computed next. Your original order returns dozens of fragments on the same frame.
Worked example: one person on a 4000 × 3000 frame
Inputs, each with its source:
- Frame resolution 4000 × 3000 — the data-sheet value of the worked camera below.
- Person height 1.7 m — illustrative typical adult.
- Framing: the person fills 80% of the frame height — illustrative station layout.
- Shoulder width 0.45 m — illustrative.
- A standing figure fills about 40% of its own bounding box — an engineering estimate covering head, torso, legs and arms.
The rule in words: convert the person's height to pixels, derive the pixel scale, size the bounding box, apply the fill fraction, then set an acceptance band around that number.
The arithmetic:
- Height: 3000 px × 0.80 = 2400 px.
- Scale: 1700 mm ÷ 2400 px = 0.71 mm per px.
- Width: 450 mm ÷ 0.71 mm per px ≈ 634 px.
- Bounding box: 2400 px × 634 px = 1,521,600 px.
- Silhouette: 1,521,600 px × 0.40 = 608,640 ≈ 610,000 px.
- Frame total: 4000 × 3000 = 12,000,000 px, so the silhouette covers 610,000 ÷ 12,000,000 = 5.1% of the frame.
- Floor at 0.5% of the frame: 12,000,000 × 0.005 = 60,000 px — a 10× margin under the silhouette.
- Ceiling at 40% of the frame, to reject wall-scale blobs and merged shadows: 12,000,000 × 0.40 = 4,800,000 px.
- Distance margin: a person twice as far covers half the linear size and a quarter of the area — 610,000 ÷ 4 = 152,500 px, still 2.5× above the floor.
The original filter, cont_area > 1000, accepts a 1000 px blob — one twelve-thousandth of this frame (12,000,000 ÷ 1000 = 12,000). Background texture survives the 5 × 5 blur in patches far larger than that, which is the confetti you are drawing.
Decision for this station: accept contours with 60,000 px ≤ area ≤ 4,800,000 px, and expect exactly one per person. The camera behind the numbers:
- Sensor format
- 1/1.7"
- Interface
- USB3
- Resolution
- 12 MP
- Shutter
- Rolling
Its data sheet prints 4000 × 3000 resolution at 30 fps, so the band holds frame after frame and 30 consecutive frames equal one second for the stability check later. Mind the exposure: at a brisk 1.5 m/s the subject moves 1.5 mm per millisecond, which is 1.5 ÷ 0.71 = 2.1 px of smear per ms, so cap the exposure near 2 ms to hold blur around 4 px.
If your station frames the person smaller — longer throw, wider shot — the pixels across the body decide everything above, so size the sensor first:
Fix the scene before you fix the filter
The pipeline worked on a monochrome background; give it some of that back and the parameters stop mattering.
- Put a plain, matte surface behind the walk line and move posters, rails and cable runs out of the field of view. The cheapest image processing is a clean wall.
- Light that surface evenly. On a uniformly lit wall a single global Otsu threshold is stabler than any adaptive one, because there is nothing local left to adapt to. Where illumination still varies, the sequence that holds up is: close the image to flatten brightness, then apply one global Otsu threshold.
- Control the ambient contribution with shutters, shrouds or scheduling. The same fight in pick-and-place cells, and the ways to win it, are covered in controlling ambient light variation in vision.
At doorway and counter distances, a diffused ring light around the lens flattens the background brightness for you.
- Size
- 100-200 mm
- Type
- Diffused
- Size
- 100-200 mm
- Type
- Diffused
The LS-RNG-0143 is a 100 mm ring light with a diffuser at 30°, and the LS-RNG-0517 is a 120 mm unit at 0°; both run on 24 V. Ring lights also come in 850 nm and 940 nm versions for stations that must work in the dark.
A ring light is a close-range tool: at several metres, full-body work calls for broad area lighting from the lighting finder, while ring lights cover the close stations.
When contour tracing is the wrong tool
A contour is a shape, not a person: nothing in findContours knows that a fragment belongs to a body. In uncontrolled scenes the honest options are:
- Background subtraction, fixed camera. Store an empty-scene frame, subtract it from each live frame, threshold the difference, then run the same findContours with the same area band. The mask now holds only what changed, usually the person. Two guides cover the traps: aligning images before background subtraction and reducing false defects from image subtraction.
- HOG pedestrian detector. OpenCV ships a pre-trained HOG model for pedestrians. It slides a window and returns boxes rather than exact outlines, and suppressing its false positives takes tuning — but it recognizes people, which no threshold does.
- A CNN detector with segmentation. Modern single-shot models return a person mask directly. Heaviest to run, and the right choice when the camera moves or the background cannot be controlled at all.
| Scene | Route |
|---|---|
| Fixed camera, wall you control | This guide: blur, threshold, findContours |
| Fixed camera, static clutter | Background subtraction plus the same area band |
| Fixed camera, moving clutter | HOG detector, contour on the box afterwards |
| Moving camera, crowds | CNN detector with segmentation |
Once the mask is clean, measuring it is standard blob analysis — area, centroid, bounding box — and the geometry checks in detecting a circle in a binary image apply to any shape you fit to a contour. For stations that must run without a PC, vision sensors carry the detection onboard and drive their own outputs:
- Interface
- GigE
- Resolution
- Up to 3 MP
The LS-SMC-0063 is a 1.6 MP smart camera at 60 fps with a 16 mm lens and its own white light source — a doorway or machine-cell presence station in one housing. The rule-based-versus-model decision is covered in AI versus rule-based smart camera inspection, and the rest of the line is in smart cameras.
Verify the fix
- Display the mask beside the source frame. The person must be one solid white shape before findContours gets blamed for anything.
- Run 30 consecutive frames — one second at 30 fps — and count contours inside the band: exactly one per person, every frame. Two misses in thirty point at the threshold, not the code.
- Reject contours whose bounding box touches the frame border; a clipped person yields an open shape that the next frame's logic will chase.
- Log cont_area for a day. A bimodal histogram means two lighting states, not one filter problem.
Common mistakes
- Thresholding into Canny: a gradient operator fed a binary image, the largest single cause of fragmented contours.
- Blurring after the threshold: it destroys the binary mask findContours needs.
- Copying an area filter between frame sizes: 1000 px is one twelve-thousandth of a 12 MP frame.
- blockSize smaller than the background texture it must average out.
- Tuning thresholds forever in a scene that needs background subtraction or a detector.
Rebuild the pipeline in the order above first; when the station needs hardware sized with it, start at the camera step of the configurator, and read the rest of the systems topic. Questions about a specific scene go to the contact page.
Questions
Why does Canny after adaptive threshold break the contour?
Canny thresholds gradients, and a thresholded image has none left except single-pixel steps, so it returns one-pixel-wide outlines that break at every gap. findContours then traces those fragments instead of one filled shape. Canny has no role between a threshold and a contour search; if you need edges, run Canny on the grayscale image and keep it out of the mask path.
What blockSize and C values work for the adaptive threshold?
Start at blockSize 7 with C 3. blockSize must stay odd; raise it toward 21 when the background texture is coarser than the person's outline, because a larger window averages that texture away. C shifts the local threshold: raise it when the white mask swells past the person's edge, lower it toward 0 when the silhouette breaks up. Judge every change on the mask image, not on the drawing.
How large should the contour area filter be?
Scale it to the frame, not to a constant. A full-person silhouette framed to 80% of the frame height covers roughly 5% of the pixels — about 610,000 px on a 4000 × 3000 frame — so a floor at 0.5% of the frame, 60,000 px, keeps a 10× margin and still rejects every background patch. Area falls with the square of distance: a person twice as far covers a quarter.
Is a silhouette contour enough to detect people reliably?
Only with a controlled background. findContours knows shapes, not people, so in clutter nothing distinguishes a person from a coat stand except your area band. With a fixed camera, background subtraction plus the same band handles static clutter. For moving clutter or a moving camera, use the HOG pedestrian detector that ships with OpenCV, or a CNN segmentation model that returns a person mask directly.
Can this run on a smart camera instead of a PC?
Yes. Vision sensors run detection onboard and switch their own outputs, which suits presence and counting stations. A model like the LS-SMC-0063 — 1.6 MP at 60 fps with a 16 mm lens and built-in white light — fits a doorway or machine-cell check. Full-body silhouettes at several metres want a larger sensor and separate lighting; size that with the configurator.



