Quality & Process Control

Ajouter l'inspection visuelle IA à vos tests

Ajoutez une inspection visuelle par IA à vos tests : capturez chaque soudure, signalez les suspectes avec un modèle et faites-les confirmer.

JJulien Buteau
intermediate10 min de lecture27 octobre 2026

Une caméra et un modèle de vision peuvent examiner chaque soudure de chaque unité, ce qu'un inspecteur ne peut pas tenir à la cadence de production. Ce guide montre comment capturer chaque soudure en pièce jointe, laisser un modèle signaler les soudures suspectes, et faire confirmer ou rejeter chaque signalement par l'opérateur depuis une invite opérateur TofuPilot, en gardant l'IA comme aide au signalement à côté des critères IPC-A-610.

Ce qu'un modèle de vision peut juger, et ce qu'il ne peut pas

Une caméra voit l'extérieur de la soudure. Tout ce qu'un modèle conclut vient de cette surface.

DéfautVisible par une caméraRemarques
Pont de soudure entre pastillesOuiContraste net, bonne cible pour un modèle
Soudure absente ou insuffisanteOuiForme et couverture du ménisque
Brins égarés hors de la soudureOuiDemande une résolution suffisante à la sortie du fil
Isolant brûlé ou fonduOuiChangement de couleur
Surface terne ou granuleuse (soudure froide possible)En partieDépend de l'éclairage, de l'alliage et des résidus de flux
Fissure sous le ménisqueNonUtiliser une mesure électrique sous flexion
Mauvais mouillage des brins dans la soudureNonUtiliser une chute de tension 4 fils
Vides dans la soudureNonRayons X ou micro-section

Les méthodes électriques pour les défauts cachés sont traitées dans Contrôler les soudures sans les détruire et Détecter les contacts intermittents en production.

Choisir le type de modèle

ApprocheEntraîné surSortieForceFaiblesse
Classifieur superviséImages étiquetées de bonnes soudures et de chaque type de défautUne classe par soudureNomme le défautDemande de nombreux exemples étiquetés de chaque défaut, rares sur une bonne ligne
Détecteur d'anomaliesImages de bonnes soudures uniquementUn score : l'écart à la « normale »Démarre avec des unités saines seulementSignale tout changement, y compris anodin (éclairage, résidus de flux, couleur du fil)

La détection d'anomalies est le point de départ habituel, car les défauts sont rares au début. Les confirmations recueillies par la procédure ci-dessous deviennent plus tard le jeu étiqueté d'un classifieur.

Trois limites s'appliquent aux deux approches :

  • Faux positifs. Un seuil assez bas pour détecter les vrais défauts signale aussi de bonnes soudures. L'étape de revue par l'opérateur les absorbe.
  • Faux négatifs. Un défaut que le modèle n'a jamais vu, ou qui paraît normal en surface, passe. L'IA ne remplace pas l'inspection humaine selon les critères IPC-A-610 ; elle oriente l'attention.
  • Sensibilité au montage. Une caméra déplacée, un éclairage modifié ou une nouvelle révision de carte décalent tous les scores. Figez le montage optique et revalidez le modèle après tout changement.

Prérequis

  • Le CLI TofuPilot installé sur le poste de test
  • Une caméra sur un support fixe, une source de lumière fixe (anneau lumineux ou dôme diffus) et un gabarit qui place chaque carte dans la même position
  • Un modèle d'anomalies exporté en ONNX qui prend une vignette de soudure et renvoie un score ; le prétraitement de ce guide (RGB, 224 × 224, valeurs de 0 à 1) est un exemple et doit correspondre à l'entraînement de votre modèle
  • Les positions des soudures dans l'image, sous forme de rectangles mesurés une fois sur une carte de référence
requirements.txt
opencv-python-headless>=4.8onnxruntime>=1.17numpy>=1.24

Étape 1 : écrire le plug caméra

Les méthodes d'un plug échangent des valeurs sérialisables en JSON avec la phase, le plug écrit donc l'image sur disque et renvoie son chemin, pas le tableau de pixels.

plugs/camera.py
26 lines
import osimport cv2class Camera:    def __init__(self, index=0, width=3840, height=2160):        self._cap = cv2.VideoCapture(index)        self._cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)        self._cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)        if not self._cap.isOpened():            raise RuntimeError(f"Camera {index} could not be opened")    def capture(self, out_dir, name):        # Vide les images mises en tampon par le pilote avant la mise en place de la carte.        for _ in range(5):            self._cap.read()        ok, frame = self._cap.read()        if not ok:            raise RuntimeError("Camera returned no frame")        path = os.path.join(out_dir, name)        cv2.imwrite(path, frame)        return path    def __del__(self):        self._cap.release()

Étape 2 : écrire le plug d'inspection

L'inspecteur découpe chaque soudure, lui attribue un score, enregistre la vignette et renvoie une entrée par soudure.

plugs/inspector.py
42 lines
import osimport cv2import numpy as npimport onnxruntime as ortclass JointInspector:    def __init__(self, model_path, threshold, rois, input_size=224):        # Résout un chemin relatif depuis le répertoire de la procédure, pas le répertoire courant.        if not os.path.isabs(model_path):            model_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", model_path)        self._session = ort.InferenceSession(model_path)        self._input_name = self._session.get_inputs()[0].name        self._threshold = threshold        self._rois = rois        self._size = input_size    def inspect(self, image_path, out_dir):        image = cv2.imread(image_path)        results = []        for roi in self._rois:            x, y, w, h = roi["x"], roi["y"], roi["w"], roi["h"]            crop = image[y : y + h, x : x + w]            crop_path = os.path.join(out_dir, f"{roi['joint']}.png")            cv2.imwrite(crop_path, crop)            # Doit correspondre au prétraitement utilisé à l'entraînement.            rgb = cv2.cvtColor(crop, cv2.COLOR_BGR2RGB)            tensor = cv2.resize(rgb, (self._size, self._size)).astype(np.float32) / 255.0            tensor = np.transpose(tensor, (2, 0, 1))[np.newaxis, ...]            score = float(self._session.run(None, {self._input_name: tensor})[0].ravel()[0])            results.append(                {                    "joint": roi["joint"],                    "score": score,                    "flagged": score >= self._threshold,                    "crop_path": crop_path,                }            )        return results

Étape 3 : capturer et noter dans une phase

phases/inspect.py
22 lines
import base64import tempfileimport cv2def capture_and_score(camera, inspector, measurements, attach, log):    work_dir = tempfile.mkdtemp(prefix="joint-inspect-")    image_path = camera.capture(work_dir, "board.png")    attach.file(image_path, "board.png")    results = inspector.inspect(image_path, work_dir)    flagged = [r for r in results if r["flagged"]]    measurements.max_anomaly_score = max(r["score"] for r in results)    measurements.flagged_count = len(flagged)    measurements.flagged = {        "joints": [r["joint"] for r in flagged],        "crops": {r["joint"]: r["crop_path"] for r in flagged},        "scores": {r["joint"]: round(r["score"], 4) for r in flagged},    }    log.info(f"{len(flagged)} of {len(results)} joints flagged")

L'image complète de la carte est jointe à chaque run. Le dictionnaire flagged est une mesure JSON, que la phase suivante lit par les résultats précédents.

Étape 4 : faire confirmer chaque signalement par l'opérateur

Ajoutez une seconde fonction dans le même fichier. Elle affiche côte à côte les vignettes signalées et demande lesquelles sont de vrais défauts. Les options de la checklist sont remplies à l'exécution avec ui.set, et ui.wait() renvoie la réponse de l'opérateur.

phases/inspect.py
23 lines
def confirm_flags(capture_and_score, phase, ui, measurements, attach):    flagged = capture_and_score.flagged    if not flagged["joints"]:        phase.skip("No joint flagged by the model")    tiles = []    for joint in flagged["joints"]:        tile = cv2.resize(cv2.imread(flagged["crops"][joint]), (320, 320))        cv2.putText(tile, joint, (8, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2)        tiles.append(tile)    ok, png = cv2.imencode(".png", cv2.hconcat(tiles))    ui.flagged_image = "data:image/png;base64," + base64.b64encode(png.tobytes()).decode()    ui.set("confirmed", options=[{"value": j, "label": j} for j in flagged["joints"]])    answer = ui.wait()    confirmed = answer["confirmed"]    # Nomme chaque vignette avec le verdict : les pièces jointes forment un jeu étiqueté.    for joint in flagged["joints"]:        verdict = "defect" if joint in confirmed else "false_positive"        attach.file(flagged["crops"][joint], f"{joint}_{verdict}.png")    measurements.confirmed_defects = len(confirmed)

Étape 5 : déclarer la procédure

procedure.yaml
59 lines
name: ESC Joint Visual Inspectionversion: 1.0.0execution:  workers: 1plugs:  - name: Inspection Camera    key: camera    python: plugs.camera:Camera    scope: station    with:      index: 0  - name: Joint Inspector    key: inspector    python: plugs.inspector:JointInspector    scope: station    with:      model_path: "models/joint_anomaly.onnx"      threshold: 0.6      rois:        - { joint: bat_pos, x: 410, y: 220, w: 300, h: 300 }        - { joint: bat_neg, x: 760, y: 220, w: 300, h: 300 }        - { joint: m1_a, x: 1210, y: 640, w: 260, h: 260 }main:  - name: Capture and Score    key: capture_and_score    python: phases.inspect:capture_and_score    measurements:      - name: Max Anomaly Score        key: max_anomaly_score      - name: Flagged Count        key: flagged_count      - name: Flagged Joints        key: flagged  - name: Confirm Flagged Joints    key: confirm_flags    python: phases.inspect:confirm_flags    depends_on: [capture_and_score]    ui:      components:        - key: flagged_image          type: image          label: "Joints flagged by the model"          width: 100%          fit: contain        - key: confirmed          type: checklist          label: "Select the joints that are real defects per IPC-A-610"          description: "Leave all unselected if every flag is a false positive"          required: false    measurements:      - name: Confirmed Defects        key: confirmed_defects        validators:          - operator: "=="            expected_value: 0

Choix de conception de ce fichier :

  • Seul un défaut confirmé fait échouer l'unité. Le signalement du modèle est enregistré mais n'a pas de validateur. L'opérateur, formé à l'IPC-A-610, prend la décision d'acceptation ou de rejet.
  • Le seuil est un exemple. Fixez-le à partir des scores mesurés sur des unités saines et sur les défauts dont vous disposez, puis revoyez-le à mesure que les confirmations s'accumulent.
  • Quand rien n'est signalé, la phase de confirmation se saute elle-même avant d'enregistrer quoi que ce soit, elle ne compte donc pas comme un échec. Son écran s'ouvre au début de la phase et se ferme au moment du saut.
  • scope: station garde la caméra ouverte et le modèle chargé d'une unité à l'autre.

Étape 6 : mesurer la performance du modèle

Chaque run enregistre désormais combien de soudures le modèle a signalées et combien l'opérateur en a confirmé. Dans le dashboard TofuPilot :

QuestionOù la lire
Le taux de faux positifs est-il acceptableNombre de signalements comparé aux défauts confirmés sur l'ensemble des runs
Le modèle dérive-t-il (éclairage, caméra, révision de carte)Carte de contrôle du score d'anomalie maximal
Quelle soudure est le plus souvent confirmée défectueusePareto des défaillances sur les défauts confirmés, et pièces jointes de chaque run
Un opérateur rejette-t-il des signalements que d'autres confirmentDéfauts confirmés filtrés par opérateur

Une hausse du score d'anomalie sur toutes les unités à la fois vient généralement du montage, pas du brasage. Une hausse sur une seule position de soudure est un signal de procédé, à analyser avec Pourquoi les soudures d'ESC cèdent en vol.

L'accord entre opérateurs compte autant que la précision du modèle. Si deux inspecteurs rendent des verdicts différents sur la même vignette, l'étiquette n'est pas fiable. L'accord entre inspecteurs est une question de système de mesure, et les principes de Qu'est-ce que la R&R et la MSA s'y appliquent. Conservez l'enregistrement d'inspection comme décrit dans Suivi des inspections IPC-A-610.

Pièges courants

Utiliser le modèle comme décision bon/mauvais. Un modèle a un taux de faux négatifs, et personne ne revoit ce qu'il laisse passer. Gardez le verdict humain comme décision et le modèle comme signalement.

Changer l'éclairage ou la caméra sans revalider. Tous les scores se décalent. Figez le montage optique et refaites la validation sur des unités saines et défectueuses connues après tout changement.

Renvoyer des images depuis un plug. Les tableaux de pixels ne sont pas sérialisables en JSON. Renvoyez un chemin de fichier.

Un prétraitement différent de l'entraînement. BGR au lieu de RGB, une autre taille d'entrée ou une autre normalisation donnent des scores plausibles qui ne signifient rien.

Attendre de la caméra qu'elle trouve les défauts cachés. Les fissures sous le ménisque, le mauvais mouillage interne et les vides sont invisibles. Associez l'inspection à une mesure 4 fils.

Jeter les verdicts de l'opérateur. Les vignettes nommées sont les images de défauts étiquetées dont le prochain modèle a besoin.

Points clés

  • Une caméra et un modèle peuvent examiner chaque soudure, mais seulement sa surface visible.
  • Commencez par un détecteur d'anomalies entraîné sur de bonnes soudures ; recueillez des défauts étiquetés par les confirmations de l'opérateur.
  • Capturez la carte en pièce jointe, notez chaque vignette de soudure dans un plug, et enregistrez les signalements comme des mesures.
  • Laissez l'opérateur confirmer chaque signalement avec ui.wait() ; seuls les défauts confirmés font échouer l'unité.
  • Suivez les faux positifs et la dérive des scores dans le dashboard, et revalidez après tout changement du montage optique.
  • Conservez l'inspection IPC-A-610 et la mesure électrique ; l'IA oriente l'attention, elle ne les remplace pas.

Plus de guides

Mettez ce guide en pratique