Quality & InspectionPythonPython 3.8+ · opencv-pythonMIT licenseIntermediate
Presence/absence part check with OpenCV (Python)
"Are all four parts actually in the tray" is the most common inspection question in a shop, and it doesn't need a trained model to answer — a threshold and a contour count get you most of the way. This script converts a photo to grayscale, blurs it slightly to kill sensor noise, thresholds it to black-and-white, counts the blobs above a minimum area, and reports PASS or FAIL against how many you told it to expect. Tested here against a synthetic tray image with a known, deliberately-missing part before this ever needed a real camera.
Before you run it
pip install opencv-python- A reasonably consistent, evenly lit photo — a phone camera on a tripod over a tray works
The code
"""Presence/absence check: does the image contain the expected number of
parts (by contour count), within an area range that filters out noise?
Usage: python presence_check.py photo.jpg --expect 4 --min-area 500
"""
import argparse
import sys
import cv2
def count_parts(image_path, min_area, thresh):
img = cv2.imread(image_path)
if img is None:
sys.exit(f"Could not read {image_path}")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
_, binary = cv2.threshold(blurred, thresh, 255, cv2.THRESH_BINARY_INV)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
parts = [c for c in contours if cv2.contourArea(c) >= min_area]
return parts, img
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("image", help="photo to check")
ap.add_argument("--expect", type=int, required=True, help="expected part count")
ap.add_argument("--min-area", type=float, default=500,
help="contours smaller than this (px^2) are noise, not parts")
ap.add_argument("--thresh", type=int, default=127,
help="binary threshold, 0-255 - tune to your lighting")
ap.add_argument("--annotated", help="save an annotated copy here")
args = ap.parse_args()
parts, img = count_parts(args.image, args.min_area, args.thresh)
found = len(parts)
status = "PASS" if found == args.expect else "FAIL"
print(f"{status}: found {found} part(s), expected {args.expect}")
if args.annotated:
cv2.drawContours(img, parts, -1, (0, 255, 0), 3)
for i, c in enumerate(parts):
x, y, w, h = cv2.boundingRect(c)
cv2.putText(img, str(i + 1), (x, y - 8),
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2)
cv2.imwrite(args.annotated, img)
print(f"Annotated image saved to {args.annotated}")
sys.exit(0 if status == "PASS" else 1)
if __name__ == "__main__":
main()What you get
$ python presence_check.py tray.jpg --expect 4 --annotated checked.jpg
PASS: found 4 part(s), expected 4
Annotated image saved to checked.jpg
$ python presence_check.py tray_missing_one.jpg --expect 4
FAIL: found 3 part(s), expected 4
How it works
GaussianBlurbefore thresholding is the single cheapest improvement over a naive threshold — it smooths out sensor noise and small reflections that would otherwise count as tiny, spurious contours.THRESH_BINARY_INVinverts the threshold so dark parts on a light tray become white blobs — flip back to plainTHRESH_BINARYif your setup is the opposite (light parts on a dark mat).min_areais the whole noise filter: a stray shadow or a scratch on the tray produces a contour too, but almost always a much smaller one than an actual part — filtering by area is simpler and more robust than trying to threshold noise away perfectly.- The script exits with status code
0on PASS and1on FAIL — that's on purpose, so it drops straight into a CI-style pass/fail check or a shell script without any extra parsing.
Gotchas & honest limits
- Lighting is the whole project, same as every vision task: a fixed threshold that works under one light will fail under another. Tune
--threshfor your actual setup, or move to adaptive thresholding (cv2.adaptiveThreshold) if lighting varies. - This counts blobs, not verified part identity — four blobs that happen to be four screws satisfies the count even if one should have been a bolt. Presence/absence is not the same claim as "the right parts."
- Touching or overlapping parts can merge into one contour and undercount — good part separation on the tray matters as much as the code.
--min-areaand--threshare photo-specific; recalibrate both when the camera, distance, or lighting changes, not just when the part changes.
Goes deeper
Want this adapted to your shop — or built into a real tool?
Samples are the free 80%. The last 20% is the part I do for a living.