System designHow-to

Count the pixel area of a color region with OpenCV in C++

Threshold a color region in HSV, count its pixel area in C++ with OpenCV, and convert pixels to mm² from your lens field of view and sensor resolution.

LumaSpec Vision8 min read Share on LinkedIn

On this page 8 sections
  1. 01What does the area of a color region mean in code?
  2. 02Why does exact BGR matching fail on real parts?
  3. 03How do you count red pixels in C++?
  4. 04How do you convert the pixel count to square millimeters?
  5. 05Where does the converted area go wrong?
  6. 06Which camera and lighting setup keeps the count stable?
  7. 07What do you check before trusting the number?
  8. 08Questions

You have one fixed color — red in this example — and you want one number back: how much of the image that color covers. The pipeline is short. Threshold the frame into a binary mask, count the white pixels with cv::countNonZero, and, when you need real units, multiply by the area of one pixel from your field of view and sensor resolution. This guide gives the exact C++ steps with OpenCV, a synthetic frame that proves the count, the mm² conversion with the arithmetic shown, and the optics errors that quietly move the converted number. The code runs unchanged on a still image or on live camera frames.

What does the area of a color region mean in code?

In code the area is a pixel count: threshold the image into a mask where the color is white and everything else is black, and cv::countNonZero returns the sum as an int — 12,000 white pixels report an area of 12,000 px.

No geometry is needed for the count itself; the unit is square pixels, because one pixel is one square of the sensor. On a 640 × 480 frame of 307,200 pixels, a 12,000-px region covers 3.9 % of the frame: 12,000 / 307,200 = 0.0391. Splitting the mask into connected blobs turns the one total into per-object areas; measuring blob area by pixel counting shows that variant and its pitfalls.

Why does exact BGR matching fail on real parts?

Exact BGR matching fails on real parts because (0, 0, 255) is pure display red, while a painted or anodized part scatters light across a band of hues — and red straddles the 0° wrap, so one narrow window always clips part of it.

OpenCV stores 8-bit hue on a 0-179 scale, so red appears at both ends of the range. A dull red part typically spans hue 0-10 and 170-179, with saturation and value dropping wherever shading, glare or shadow sit. The fix is two cv::inRange windows OR-ed together, plus floors on the other channels: S ≥ 80 drops the gray background, V ≥ 80 drops shadows and dark reflections. Those floors are starting values for a mid-red part; sample your part and adjust. How a saturated background distorts the same thresholds is the subject of the HSV segmentation guide.

How do you count red pixels in C++?

Five steps turn any BGR frame into the count, and a painted 100 × 120 rectangle proves the pipeline: it must report exactly 12,000 px, because 100 × 120 = 12,000.

  1. Load the frame with cv::imread and reject an empty result.
  2. Convert BGR to HSV with cv::cvtColor and the cv::COLOR_BGR2HSV flag.
  3. Threshold the low red window, hue 0-10, with cv::inRange.
  4. Threshold the high window, hue 170-179, and merge both masks with cv::bitwise_or.
  5. Report cv::countNonZero(mask) as the pixel area.
#include <opencv2/imgproc.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>

int main()
{
    // Starting values for a mid-red part; sample your part and adjust.
    const int hueLow1 = 0,   hueHigh1 = 10;
    const int hueLow2 = 170, hueHigh2 = 179;
    const int satFloor = 80, valFloor = 80;

    cv::Mat bgr = cv::imread("part.png", cv::IMREAD_COLOR);
    if (bgr.empty()) { std::cerr << "image read failed\n"; return 1; }

    cv::Mat hsv;
    cv::cvtColor(bgr, hsv, cv::COLOR_BGR2HSV);

    cv::Mat low, high, mask;
    cv::inRange(hsv, cv::Scalar(hueLow1, satFloor, valFloor),
                     cv::Scalar(hueHigh1, 255, 255), low);
    cv::inRange(hsv, cv::Scalar(hueLow2, satFloor, valFloor),
                     cv::Scalar(hueHigh2, 255, 255), high);
    cv::bitwise_or(low, high, mask);

    const int redPixels = cv::countNonZero(mask);   // area in pixels
    std::cout << redPixels << " px\n";
    return 0;
}

Prove the pipeline on synthetic data before pointing it at parts:

cv::Mat test = cv::Mat::zeros(480, 640, CV_8UC3);
cv::rectangle(test, cv::Rect(40, 40, 100, 120), cv::Scalar(0, 0, 255), -1);

Pure red BGR (0, 0, 255) converts to hue 0, S 255, V 255, which lands inside the first window. The rectangle holds 100 × 120 = 12,000 px, so countNonZero must print exactly 12000. Once it does, a wrong count on a real frame is a threshold problem, not a code problem.

How do you convert the pixel count to square millimeters?

Multiply the pixel count by the area of one pixel, which comes from your field of view: 100 mm across 640 px is 0.15625 mm per pixel, one pixel covers 0.024414 mm², and 12,000 px is 292.97 mm² — call it 293 mm².

mm per pixel = field of view / sensor width in pixels (per direction) area per pixel = mm per pixel × mm per pixel (square pixels) real area = mask pixel count × area per pixel

Line by line for the example:

  • 100 mm / 640 px = 0.15625 mm per pixel, the same vertically with square pixels.
  • 0.15625 mm/px × 0.15625 mm/px = 0.024414 mm² per pixel.
  • 12,000 px × 0.024414 mm²/px = 292.97 mm².

Resolution moves the result as much as the lens does: the same 100 mm field over a 4000-px-wide sensor gives 100 / 4000 = 0.025 mm/px, so the same 12,000-px region is 12,000 × 0.025² = 7.5 mm². Where the field of view itself comes from — sensor width, focal length, working distance — is the subject of the lens field of view guide; the calculator below steps from working distance to the focal length that delivers your field. The same conversion drives crack width, length and area measurement after segmentation.

Lens focal length, field of view and working distance
Units
Solve for
Focal length
– mm
Field of view
– mm
Working distance
– mm
Magnification
– x
Angle of view
– °
Pixel on the part
– mm

Thin-lens model: m = sensor width / field of view, f = WD × m / (1 + m). Accurate to a few percent above about 150 mm; check the lens's minimum object distance.

Find lenses with this focal length Open the full calculator

Where does the converted area go wrong?

The pixel count is exact; the mm² number inherits every scale error squared — a 5 % error in field of view becomes a 10.25 % error in area, turning 293 mm² into anything between 264 and 323 mm².

Area scales with the square of the linear scale (1.05² = 1.1025), so small optics errors get loud. Three bite hardest:

  • Working distance drift. Pixel scale is proportional to distance, so a part sitting 10 % closer to the lens reads about 21 % larger (1.1² − 1 = 0.21). Fix the stand-off, or re-measure the scale at every setup.
  • Camera tilt. Reported area scales with cos θ: a 10° tilt costs about 1.5 % (cos 10° = 0.985), a 30° tilt about 13 % (cos 30° = 0.866). Square the camera to the part.
  • Lens distortion at the frame edges. Conventional lenses bend straight lines most at the corners, where a few percent of error is typical; keep the part in the middle third of the frame or calibrate the lens.

A calibrated target in the part plane absorbs all three at once, provided focus and stand-off stay where you calibrated them.

Which camera and lighting setup keeps the count stable?

Stable counts come from a stable image: lock exposure and white balance, block ambient light, and use a global-shutter camera so every frame sees the same geometry — the thresholds then hold from frame to frame instead of drifting with the shop lights.

For a count running in your own process, any GigE or USB3 area scan camera feeds it; the LS-CMR-0166 hands the pipeline 4000 × 3000 frames at 30 fps.

Area scan cameras12 MP USB3 area scan camera, IMX226, 1/1.7" rolling shutter, 30 fps, 4000 × 3000 resolution (LS-CMR-0166)LS-CMR-0166
Sensor format
1/1.7"
Interface
USB3
Resolution
12 MP
Shutter
Rolling
$560.00View partBuild a system with it

When the PC should disappear, smart cameras threshold and count on the device. The LS-SMC-0063 integrates a 16 mm lens with white light, and its polarized option tames glare on shiny surfaces.

Smart cameras & vision sensors1.6 MP smart camera, 60 fps, 16 mm lens, white light (polarized, transparent light) (LS-SMC-0063)LS-SMC-0063
Interface
GigE
Resolution
Up to 3 MP
$1,110.00View part

White light keeps every hue in its natural band, which is what fixed color thresholds assume. The LED colour contrast guide ranks which color separates your part from its background best.

What do you check before trusting the number?

Four checks turn a plausible number into a defensible one: preview the mask, check the scale against a known target, check repeatability within about 2 %, and confirm the camera is still square to the part.

  • Mask preview. Display the mask itself; the region should read solid white on black. Big holes mean a threshold problem, not a measurement problem, and the live binary preview guide shows the fastest set-up.
  • Scale against a known target. A 50.0 mm circle spanning 320 px must back out to 0.15625 mm/px. If it does not, the nominal field of view is wrong, and every mm² after it.
  • Repeatability. Ten frames of a still part should agree within about 2 % of the count. A wider spread points at exposure or ambient drift, not at the code.
  • Geometry. Re-check the square-on alignment whenever anyone touches the stand; the tilt numbers above price a loose mount at 13 % for 30°.

Pick the camera that feeds your counter in the camera finder or step through the camera configurator; we will check your thresholds against sample images through the contact page. When the count has to drive a machine instead of a console, the write-up on sending an OpenCV color count to a PLC over SNAP7 picks up exactly there.

Questions

Does cv::contourArea give the same area as cv::countNonZero?

No. cv::countNonZero counts every mask pixel and is the true pixel area. cv::contourArea applies Green's theorem to a contour's outline points, so it estimates the enclosed geometric area and reads slightly low, most visibly on small or thin regions. Use contourArea for fast per-object estimates from cv::findContours, and countNonZero when the pixel total itself is the deliverable.

How do I pick HSV thresholds for a new part color?

Capture a frame under the production light and read HSV values directly on the part: print a handful of pixels or inspect them in a debug view. Center the hue window on the median hue and span about ±10, then raise the saturation and value floors until background and shadows drop out, typically between 60 and 100. Confirm the mask visually before counting.

Why does OpenCV hue stop at 179 instead of 359?

8-bit HSV stores hue in one byte, so cvtColor halves the 0-359 degree scale to 0-179 while saturation and value keep the full 0-255 range. Take any hue from a 360° color wheel, halve it, and round before using it in cv::inRange. The two-window trick for red exists because 0° and 360° both halve to the ends of the byte.

How do I count each object separately instead of the total?

Run cv::connectedComponentsWithStats on the merged mask: each label is one blob, and its stats output carries that blob's pixel count, bounding box and centroid, so fragments can be rejected by area and per-part values reported. Clean the mask first with cv::morphologyEx — a 3 × 3 MORPH_OPEN removes speckles and MORPH_CLOSE seals pinholes — or noise fragments inflate the list.