Ir al contenido

← Pipeline de inspección de calidad OpenCV en Raspberry Pi

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

Pipeline de inspección de calidad OpenCV en Raspberry Pi — ejemplo completo

Pipeline de preprocesado OpenCV para inspección de impresión en Raspberry Pi: umbralización, morfología, CLAHE y umbral adaptativo, bien explicados.

Programa completo y ejecutable para el Raspberry Pi + cámara USB (opencv-silkscreen-pipeline.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 — OpenCV vision pipeline for silkscreen inspection

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

Requirements:
    pip install opencv-python numpy

What it does:
    Processes ONE image end to end with the same pipeline used on the
    production line to isolate the silkscreen print of a part:

        gray -> Gaussian blur -> background threshold -> open/close morphology
             -> largest contour -> crop -> CLAHE -> medianBlur
             -> adaptive threshold

    If no image path is given, it generates a synthetic part so the
    script runs without hardware. It displays every stage on screen.

Integration with the catalog:
    - The resulting binary image is the input of the rotational template
      matching module (ejemplos/inspeccion-visual/rotational-template-matching.py).
    - The 6 adjustable parameters are loaded from config.json
      (ejemplos/inspeccion-visual/json-config-references.py).
    - The live-tuning sliders are in the PySide6 GUI
      (ejemplos/inspeccion-visual/gui-pyside6-camera-thread.py).
"""

import sys
import cv2
import numpy as np

# ---------------------------------------------------------------------------
# Pipeline parameters (the same ones the GUI exposes through sliders).
# In production they are read from config.json; here they are inline for clarity.
# ---------------------------------------------------------------------------
CFG = {
    "bg_threshold": 60,    # fixed threshold to separate part from dark background
    "morph_kernel": 5,     # morphology kernel size (odd)
    "clahe_clip": 2.0,     # CLAHE contrast limit
    "clahe_grid": 8,       # CLAHE grid size
    "block_size": 31,      # adaptive threshold neighborhood (odd)
    "C": 7,                # constant subtracted in the adaptive threshold
}


def synthetic_image():
    """Generates a light 'part' on a dark background with a silkscreen print."""
    img = np.full((480, 640, 3), 25, np.uint8)            # dark background
    cv2.rectangle(img, (180, 120), (460, 360), (190, 190, 190), -1)  # part
    cv2.putText(img, "IS-42", (230, 260), cv2.FONT_HERSHEY_SIMPLEX,
                1.6, (40, 40, 40), 4)                      # silkscreen
    noise = np.random.randint(0, 18, img.shape, np.uint8)  # camera noise
    return cv2.add(img, noise)


def process(frame, cfg):
    """Full pipeline. Returns (gray_crop, binary) or None on failure."""
    # 1) Gray + Gaussian blur: removes high-frequency camera noise
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    blurred = cv2.GaussianBlur(gray, (5, 5), 0)

    # 2) Background threshold: the part is light on a dark background
    _, bg_mask = cv2.threshold(blurred, cfg["bg_threshold"], 255,
                               cv2.THRESH_BINARY)

    # 3) Open morphology (removes specks) + close (fills holes in the part)
    k = cfg["morph_kernel"]
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (k, k))
    bg_mask = cv2.morphologyEx(bg_mask, cv2.MORPH_OPEN, kernel)
    bg_mask = cv2.morphologyEx(bg_mask, cv2.MORPH_CLOSE, kernel)

    # 4) Largest contour = the part. If there are no contours, there is no part.
    contours, _ = cv2.findContours(bg_mask, cv2.RETR_EXTERNAL,
                                   cv2.CHAIN_APPROX_SIMPLE)
    if not contours:
        return None
    c = max(contours, key=cv2.contourArea)
    x, y, w, h = cv2.boundingRect(c)

    # 5) Crop of the region of interest (only the part, background out)
    cropped = gray[y:y + h, x:x + w]

    # 6) CLAHE: local equalization -> robust against uneven lighting
    clahe = cv2.createCLAHE(clipLimit=cfg["clahe_clip"],
                            tileGridSize=(cfg["clahe_grid"],
                                          cfg["clahe_grid"]))
    cropped = clahe.apply(cropped)

    # 7) medianBlur: smooths without destroying the silkscreen edges
    cropped = cv2.medianBlur(cropped, 3)

    # 8) Adaptive threshold: binarizes the silkscreen regardless of the
    #    global lighting level of the scene
    binary = cv2.adaptiveThreshold(cropped, 255,
                                   cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                                   cv2.THRESH_BINARY_INV,
                                   cfg["block_size"], cfg["C"])
    return cropped, binary


def main():
    # Real image via argument, or synthetic one to test without a camera
    if len(sys.argv) > 1:
        frame = cv2.imread(sys.argv[1])
        if frame is None:
            sys.exit(f"Could not read the image: {sys.argv[1]}")
    else:
        print("No argument: using synthetic demo image.")
        frame = synthetic_image()

    result = process(frame, CFG)
    if result is None:
        sys.exit("No part detected in the image.")

    cropped, binary = result
    print(f"Part detected. Crop: {cropped.shape[1]}x{cropped.shape[0]} px")

    # Stage visualization (press a key to close)
    cv2.imshow("1 - Input", frame)
    cv2.imshow("2 - Crop + CLAHE", cropped)
    cv2.imshow("3 - Final binary (matching input)", binary)
    cv2.waitKey(0)
    cv2.destroyAllWindows()


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