Skip to Content

← All functionalities

Visual inspection (machine vision)Raspberry Pi + USB cameraUSB/V4L2Machine vision

Rotation-tolerant template matching for pass/fail inspection

Plain OpenCV template matching fails the moment a part arrives slightly rotated — and on a real conveyor, parts always do. In a real production line visual inspection deployment, the fix was simple: rotate the captured image from -15 to +15 degrees in 2-degree steps, run cv2.matchTemplate with TM_CCOEFF_NORMED at each angle, and keep the best score. A score of 0.85 or higher means the printed marking matches the golden reference: the part passes.

Why rotate the capture, not the template

The reference is a carefully captured, preprocessed golden image — you want it untouched so scores stay comparable across parts and shifts. Rotating the live capture (with border padding so corners survive cv2.warpAffine) keeps the template pristine. The search range of ±15° covers how much a part can realistically twist in the fixture; anything beyond that is a placement error worth rejecting anyway.

TM_CCOEFF_NORMED as a pass/fail score

The normalized correlation coefficient is invariant to global brightness changes and returns a clean 0-to-1 score, so one threshold works all day long. In the deployed system 0.85 separates good prints from missing, smudged or wrong markings with margin on both sides. Log the best score and angle per part: a slowly drifting score is your early warning that focus or lighting moved.

Tuning step size against cycle time

Sixteen angles at 2° steps means sixteen matchTemplate calls per part — still well under a second on a Raspberry Pi with the cropped, binarized input from the preprocessing pipeline. If your parts rotate less, narrow the range before shrinking the step: fewer angles buys more speed than finer ones, and 2° resolution is finer than the score curve itself.

A snippet from the implementation

Straight from the example as deployed on the Raspberry Pi + USB camera — copy it freely:

def binary_silkscreen(text="IS-42", angle=0.0, noise=False):
    """Generates a binary silkscreen image, optionally rotated."""
    img = np.zeros((200, 300), np.uint8)
    cv2.putText(img, text, (60, 120), cv2.FONT_HERSHEY_SIMPLEX,
                1.5, 255, 4)
    if angle:
        M = cv2.getRotationMatrix2D((150, 100), angle, 1.0)
        img = cv2.warpAffine(img, M, (300, 200))
    if noise:  # simulates stray pixels left by the adaptive threshold
        salt = np.random.rand(*img.shape) > 0.995
        img[salt] = 255
    return img

The full example is a complete program — wiring header, setup and main loop — ready to adapt to your application.

Frequently asked questions

Why not use feature matching like ORB or SIFT instead?

Binarized prints have few stable keypoints, and feature matching adds tuning complexity. For a fixed camera checking a known marking, exhaustive rotation search over template matching is simpler, deterministic and fast enough.

How was the 0.85 threshold chosen?

Empirically, by recording scores of known-good and known-bad parts. Good parts clustered above 0.90 and defective ones below 0.75, so 0.85 leaves a safety margin in both directions.

What if my parts can arrive at any angle?

Extend the loop to 360 degrees with a coarse pass first, then refine around the best coarse angle. The example keeps ±15° because the fixture constrains orientation on the real line.

Related functionalities