Rotation-tolerant template matching for pass/fail inspection
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
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
Tuning step size against cycle time
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 imgThe 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.