Ir al contenido

← Template matching con búsqueda de rotación en OpenCV

Inspección visual (visión artificial)Raspberry Pi + cámara USBUSB/V4L2Visión artificial

Template matching con búsqueda de rotación en OpenCV — ejemplo completo

Template matching tolerante a rotación para inspección pass/fail en OpenCV. Rota la captura de -15 a +15 grados y puntúa con TM_CCOEFF_NORMED.

Programa completo y ejecutable para el Raspberry Pi + cámara USB (rotational-template-matching.py): incluye cabecera de conexionado, requisitos y notas de integración.

Descarga el pack completo del proyecto — gratisEste ejemplo + los relacionados + lista de materiales

Vista de solo lectura.

# -*- coding: utf-8 -*-
"""
COMPLETE EXAMPLE — Template matching with rotational search

Hardware:  Raspberry Pi + USB camera
Based on:  visual inspection project in production (vision.py)

Requirements:
    pip install opencv-python numpy

What it does:
    Compares the binary image of a part with its reference trying
    rotations from -15 to +15 degrees in steps of 2 (TM_CCOEFF_NORMED).
    The part arrives from the conveyor with variable orientation: without
    the rotational search, a good part rotated 8 degrees would give a low
    score and a false reject. Score >= 0.85 -> PASS.

    The script generates a reference and a synthetic "live capture"
    (the same silkscreen rotated and with noise) so it runs without
    hardware, and prints the score for every angle tried.

Integration with the catalog:
    - The input (binary image of the part) is produced by the pipeline
      (ejemplos/inspeccion-visual/opencv-silkscreen-pipeline.py).
    - The reference is saved/loaded as a processed PNG from references/
      (ejemplos/inspeccion-visual/json-config-references.py).
    - The OK/NOK verdict is shown in the operation tab of the GUI
      (ejemplos/inspeccion-visual/gui-pyside6-camera-thread.py).
"""

import cv2
import numpy as np

# ---------------------------------------------------------------------------
# Search parameters (in production they are read from config.json)
# ---------------------------------------------------------------------------
ANGLE_MIN = -15       # degrees
ANGLE_MAX = 15
STEP = 2              # precision / cycle-time trade-off on the Pi
OK_THRESHOLD = 0.85   # minimum TM_CCOEFF_NORMED score to return PASS


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


def rotational_match(live, ref_img):
    """
    Searches for the reference inside the capture trying rotations.
    Returns (best_score, best_angle, best_position).
    """
    # Padding around the capture: no corners are lost when rotating and
    # matchTemplate requires the image to be larger than the template.
    pad = max(ref_img.shape) // 2
    live_padded = cv2.copyMakeBorder(live, pad, pad, pad, pad,
                                     cv2.BORDER_CONSTANT, value=0)
    h, w = live_padded.shape
    center = (w // 2, h // 2)
    size = (w, h)

    best_match, best_angle, best_loc = -1.0, 0, (0, 0)
    for angle in range(ANGLE_MIN, ANGLE_MAX + 1, STEP):
        # We rotate the capture (not the reference): the processed
        # reference is the "good" pattern and stays untouched.
        M = cv2.getRotationMatrix2D(center, angle, 1.0)
        rotated = cv2.warpAffine(live_padded, M, size)

        # TM_CCOEFF_NORMED: invariant to global brightness changes
        res = cv2.matchTemplate(rotated, ref_img, cv2.TM_CCOEFF_NORMED)
        _, max_val, _, max_loc = cv2.minMaxLoc(res)
        print(f"  angle {angle:+3d} degrees -> score {max_val:.3f}")

        if max_val > best_match:
            best_match, best_angle, best_loc = max_val, angle, max_loc

    return best_match, best_angle, best_loc


def main():
    # "Good pattern" reference and simulated capture: the same silkscreen
    # rotated 8 degrees with noise, as it would arrive from the conveyor.
    ref_img = binary_silkscreen(angle=0)
    live = binary_silkscreen(angle=8, noise=True)

    print("Searching for the reference with rotations from "
          f"{ANGLE_MIN} to {ANGLE_MAX} degrees (step {STEP}):")
    score, angle, loc = rotational_match(live, ref_img)

    verdict = "PASS" if score >= OK_THRESHOLD else "DEFECTIVE"
    print(f"\nBest score: {score:.3f} at {angle:+d} degrees "
          f"(threshold {OK_THRESHOLD}) -> {verdict}")

    # Visualization: reference and capture side by side
    cv2.imshow("Reference (pattern)", ref_img)
    cv2.imshow(f"Live capture -> {verdict}", live)
    cv2.waitKey(0)
    cv2.destroyAllWindows()


if __name__ == "__main__":
    main()
Descarga el pack completo del proyecto — gratisEste ejemplo + los relacionados + lista de materiales