The crash lands without a pointer to the real fault: error: (-215) npoints >= 0 && (depth == CV_32F || depth == CV_32S) in function contourArea, raised from shapedescr.cpp, and the process dies. It almost never means your contours are corrupt. It means contourArea never received a contour — in the common case you passed it the loop index itself. This guide reads the assertion clause by clause, shows why contourArea(i) compiles and then throws, gives the one-line fix, then the corrected detection loop from background model to area gate, and finishes with the further defects hiding in the same for-loop and the checks that catch this class of call-signature error before it reaches a production line.
What the assertion is checking
OpenCV prints the failed precondition as error: (-215) followed by the check itself, with the source file and line — shapedescr.cpp, line 314 in the 3.1.0 sources. Two clauses, two questions:
-
npoints >= 0— did the argument contain a readable sequence of points? A real contour gives a count of zero or more; anything else returns -1, and this clause fails first. -
depth == CV_32F || depth == CV_32S— are the points stored as floats (CV_32F) or integers (CV_32S)?findContoursfills avector<vector<Point> >with integer points, so CV_32S is the depth your contours already carry.
When the argument is not a contour at all, the point count reads -1 and the message names both conditions even though the first alone was enough. Nothing in this message points at your thresholding or your camera: the input to contourArea was never a point sequence.
Cause one: the loop index instead of the contour
The failing line is:
detected_area = contourArea(i);
i is the loop counter. contourArea takes an InputArray, and InputArray has constructors for plain scalars, so the compiler converts the integer silently and the build succeeds. At run time the function unwraps that scalar, finds no point sequence in it, and npoints reads -1 — the assertion throws a cv::Exception and the process terminates.
Two facts pin this bug down:
- The crash happens on the first frame that produces a contour — and because
avgstarts atMat::zeros(new_frame.size(), CV_32FC1), the first difference is half the scene brightness, so wherever the scene is brighter than 70 counts the frame clears the 35-count threshold and the crash lands on frame one. Prime the average with the first frame, held as CV_32F, and the same crash simply waits for the first real motion. - The fix is the argument itself:
detected_area = contourArea(contours[i]);
The compiler will not warn about this constructor conversion — the same silence behind the implicit int to float conversion warning: fix or ignore trap. Search your own file for contourArea( and read every argument before trusting the build.
Cause two: an image where a contour belongs
The same assertion fires when the argument is a whole frame — contourArea(thresh) on the binary mask, or Cv2.ContourArea(closing) on a morphology result in C#. A single-channel 8-bit frame contains no point sequence and its depth matches neither CV_32F nor CV_32S, so both clauses fail. The fix is structural, not cosmetic: findContours must run first and the call takes one element of its output.
| What you passed | What contourArea sees | Fix |
|---|---|---|
contourArea(i) |
a scalar, no points | contourArea(contours[i]) |
contourArea(thresh) |
an 8-bit single-channel frame | run findContours, pass contours[i]
|
contourArea(contours[i]) |
integer points, depth CV_32S | nothing — this is the correct call |
The trap is not specific to this function. Shape calls that expect points refuse everything else the same way — boundingRect returns zeros or an error: Mat type and build covers the sibling case.
The corrected detection loop
From the thresholded difference to the area gate, with the declarations the call needs:
std::vector<std::vector<Point> > contours; // filled by findContours
std::vector<Vec4i> hierarchy;
double detected_area = 0.0; // px^2, one contour at a time
threshold(frameDelta, thresh, 35, 255, THRESH_BINARY);
dilate(thresh, thresh, kernel_dilate, Point(-1, -1), 2, BORDER_CONSTANT);
findContours(thresh, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0, 0));
for (size_t i = 0; i < contours.size(); i++) {
detected_area = contourArea(contours[i]);
if (detected_area > min_area) {
out << result;
break; // one saved frame per event is enough for a pass/fail gate
}
}
Two details in that loop matter as much as the one-line fix. findContours needs a filled binary mask; an edge map instead of a filled mask is the other classic breakage — Canny before findContours leaves open contours: threshold the mask shows why. And the braces around the loop body are load-bearing, not decoration — the original loop had none, which is the first of the further defects below.
Test the fix on a clip with motion and one printed line inside the loop:
printf("contour %d area %f px^2\n", (int)i, detected_area);
Each contour prints a non-negative area and nothing throws. That print line is the proof the fix is in.
Set the numbers that decide detection
Three constants carry the decision, and each has a number you can defend:
-
threshold(frameDelta, thresh, 35, 255, THRESH_BINARY): 35 counts in an 8-bit frame is a 14 % brightness step (35 ÷ 255 = 0.137). The 21 × 21GaussianBlurupstream removes pixel noise, so only frame-scale change survives to become contours. -
dilate(thresh, thresh, kernel_dilate, Point(-1, -1), 2, BORDER_CONSTANT)with the 3 × 3MORPH_RECTkernel: each iteration grows a region by 1 px per side, so two iterations close 2 px tears and merge fragments split by a highlight. -
min_area, in square pixels.
Worked example for the gate. A part that thresholds to a 120 × 80 px silhouette carries 9,600 px². Two dilate iterations grow it to 124 × 84 = 10,416 px². Set min_area at half the expected area, 5,000 px²: the part clears the gate while a 10 × 10 px dust speck, grown to 14 × 14 = 196 px², does not. If you would rather count pixels directly on the mask, measure blob area in a binary image by pixel counting is the shorter route.
When the corrected call returns areas of 0, the argument is right and the mask is not: drop the threshold toward 25, raise the dilate to 3 iterations, and confirm the mask is filled rather than outlined. Faint doubled outlines in frameDelta are a lighting and registration problem, not a code problem — image subtraction false defects: reduce contour artifacts and object detection on a moving background with registered subtraction take those further.
accumulateWeighted(new_frame, avg, 0.5) sets how fast the scene is forgotten: the model takes half of every new frame, adapting within a handful of frames — right for parts crossing the view, wrong for parts that dwell in it.
The camera in front of this pipeline
Motion during exposure smears the object across pixels, and the smear reaches frameDelta as a wide, faint blob that a 35-count threshold may never binarize. Size the exposure against the line speed:
- Scene 300 mm wide across a 1920-pixel frame: 300 mm ÷ 1920 px = 0.156 mm/px.
- Part at 250 mm/s with a 2 ms exposure: smear = 250 mm/s × 0.002 s = 0.5 mm, which is 0.5 ÷ 0.156 = 3.2 px.
- Hold the smear at 2 px: exposure ≤ (2 px × 0.156 mm/px) ÷ 250 mm/s = 0.0012 s, so 1.2 ms.
Decision: at 250 mm/s and 0.156 mm/px, run the exposure at 1 ms or below and supply the light that makes 1 ms bright enough. A strobe pulse of a few tens of microseconds freezes the part outright; pulse it on the frame trigger so light and exposure coincide — machine vision trigger timing: sequence camera, light and reject gate sequences the whole chain. Keep strobed light behind the enclosure: pulses at this intensity are unpleasant at best to anyone staring down the line.
A global shutter matters for the same reason: the whole frame is one instant, not a rolling sweep. Filter the area scan cameras by shutter in the camera finder, or start from the camera step of the configurator.
Work out the pixel count your scene needs before you lock the scale:
For fast motion on a line, choose an area scan camera with a global shutter:
- Sensor format
- 1.1"
- Lens mount
- C-mount
- Interface
- USB3
- Resolution
- 12 MP
When a PC pipeline is more than the station needs, a smart camera runs the detection on the device and hands the PLC a result over its network interface — see the smart cameras range:
- Interface
- GigE
- Resolution
- 6-10 MP
- Sensor format
- 1/2.4"
- Shutter
- Rolling
More defects in the same loop
-
The gate sat outside the loop. Without braces, only the
contourArealine belonged to thefor; theif (detected_area > min_area) { out << result; }ran once, after the loop, on the last contour's area. Motion measured on a small contour was discarded. The braces in the corrected loop put the gate back where it decides. -
argv[3]is read when it may not exist. The program rejectsargc < 3but then callsatoi(argv[3]); with exactly three arguments that pointer is null and the read is undefined behaviour. Require four arguments, or defaultmin_areawhen the third is absent. -
Channel order.
cvtColor(new_frame, new_frame, CV_RGB2GRAY)names RGB while camera streams usually arrive BGR-ordered; the swap re-weights the gray values and moves where the 35-count threshold bites. Match the constant to what your source delivers.
Checks before you ship the detector
- Every
contourArea(in the file takescontours[i]or a point vector you built yourself — search and read each argument. - One frame with motion prints one non-negative area per contour, and nothing throws.
-
min_areasits at about half the smallest expected silhouette, in square pixels. - The gate is inside the loop, in braces.
- The argument-count check covers every argument the program reads.
- Exposure is at or under the smear budget — the 1 ms figure holds at 250 mm/s and 0.156 mm/px; recompute it from your own scale.
- One frame is saved per event, and the saved count matches the parts you ran.
If the pipeline still misfires with all of that true, the fault is upstream of the code. Bring the frame, the lens and the light to the contact page and we will work the numbers with you; more build notes like this one sit on the systems topic page.
Questions
What does error -215 mean in an OpenCV message?
It is the status code OpenCV attaches to a failed internal precondition check, and the text after it names the exact condition, the source file and the line. It means an argument violated a check before any pixel data was processed — not memory corruption, not a driver fault. The (-215) number carries no further information; read the condition to see which argument failed.
Can I pass floating-point points to contourArea?
Yes. The assertion accepts depth CV_32F as well as CV_32S, so a vector of float points passes both clauses. findContours returns integer points, which is why CV_32S is what you normally hold. contourArea returns the same area for the same geometry at either depth, so there is no accuracy reason to convert integer points to floats before the call.
Does the same assertion fire in convexHull?
Yes. cv::convexHull reports the sibling message — total >= 0 with the same depth pair — when its argument is not a point sequence, and the fix is identical: pass one element from the contour vector that findContours produced. Treat it as one class of error: every shape call that expects points rejects an index, an image frame or an empty argument the same way.
How do I convert contourArea output to square millimetres?
Multiply the square-pixel area by the square of your scale. At 0.156 mm per pixel, one pixel covers 0.0243 mm², so a 10,416 px² silhouette measures 10,416 × 0.0243 ≈ 253 mm². Cross-check through the bounding box: 124 px × 0.156 mm/px = 19.3 mm, 84 px × 0.156 mm/px = 13.1 mm, and 19.3 × 13.1 ≈ 253 mm² again.
Why does the detector stop firing when a part stops moving?
Because of the 0.5 learning rate: accumulateWeighted takes half of every new frame, so a stationary part's weight in the average halves each frame. Five frames in, its share is 0.5 raised to the fifth, about 3 %; the difference falls below the 35-count threshold and no contour reaches the gate. For parts that dwell in view, drop the rate toward 0.05 or subtract a stored reference frame instead.

