Gwenfred Tham

Personal project · 2026

DeepPCB-AOI

Bare-board PCB inspection that finds copper defects with a learned template-aware detector and returns an auditable pass, rework or scrap decision.

RepoDemo video coming

pcb_inspectionAnimated walkthrough of the DeepPCB-AOI console: template, test image with boxes, difference map, then a SCRAP decision.
Recording from the repo: one DeepPCB template/test pair going through the console.

The problem

Bare printed circuit boards are inspected by comparing each board with a known-good template. Finding differences is only half of the job. The line also needs a decision (pass, rework or scrap) that follows written limits, can be audited later, and never lets a fatal defect such as an open or a short through, even if an AI component in the loop disagrees.

What it does

Results

Scores on the official DeepPCB test split (500 board pairs, 3,140 labelled defects), class-aware matching at IoU 0.33, the threshold used by the dataset's authors. Numbers are copied from evaluation_results.json in the repo.

DetectorInputPrecisionRecallF1
A · Difference baseline (hand-built ONNX graph)template + test0.0120.0910.021
B · YOLOv8n, test image onlytest0.8700.9720.919
C · YOLOv8n, 3-channel composite[template, test, absdiff]0.9010.9830.940

At IoU 0.50 the 3-channel model holds at 0.939 F1. Giving the model the template mattered most on the two classes that look like normal copper without it: F1 on Short rose from 0.858 to 0.927 and on Pin-hole from 0.851 to 0.963, where pin-hole precision went from 74% to 93% because normal drilled pads stopped being flagged. In the evaluation run the 3-channel model averaged about 10 ms per board pair on a GPU.

How it works

  1. 1 · DetectTwo swappable backends. The learned one stacks template, test and their absolute difference into a 3-channel image and runs a YOLOv8n model exported to ONNX, with per-class NMS. The default baseline subtracts the images in a 696-byte ONNX graph and labels blobs by polarity and shape.core/vision · DETECTOR_BACKEND
  2. 2 · Fast exitNo boxes means PASS. Nothing else runs.core/vision
  3. 3 · RulesEach box is compared with the area table, and the batch counter for its class goes up.core/tools
  4. 4 · Optional triageA pluggable model may suggest a decision. Its output must validate as InspectionDecision; it gets up to three attempts, then the rule engine decides. None is configured by default.core/agent
  5. 5 · GuardrailsOpen and Short are forced to SCRAP / CRITICAL, an over-limit box cannot pass, and the third repeat of a class in a batch halts the line.core/agent/guardrails.py

Served by api/ (FastAPI) with the console at web/index.html.

Detection results: five regions with class, area and disposition; the board is marked SCRAP.
Per-region results for one board. Open and Short are not allowed to pass, so the board is SCRAP.
Decision flowchart for one detected box: Open or Short goes to SCRAP, over the area limit goes to REWORK, a third repeat in the batch halts the line, otherwise PASS.
The decision path one detected box takes, from the repo's flowchart.
Single-image model output: many normal round pads boxed in red as pin-holes.
Failure case, model B (test image only): normal round pads are flagged as pin-holes. Green = ground truth, red = prediction.
3-channel model output on one of its hardest boards: all labelled defects found, plus nine extra boxes.
One of model C's ten worst test boards: every labelled defect found, plus 9 extra boxes.

Hard parts and decisions

Geometry lives in one place
Boxes, scores and areas are computed in core/vision. Later stages only read them, and the optional model cannot edit box coordinates.
Guardrails run last, in plain Python
Safety overrides are applied after any model triage. The tests include a stub model that tries to PASS a short; the override catches it and records guardrail_overridden.
Cheap path first
An empty detection list returns straight away, and the area check and batch counter run before any model call, so a clean board never costs a model request.
Model output is a contract
Anything that does not validate against the Pydantic schema is retried twice and then replaced by the rule engine's decision.
Measure the baseline first
The hand-built difference graph scored 0.02 F1 with about 47 false alarms per board, mostly from slight misalignment along sharp copper edges. Putting a number on that is what justified training a model.
An ablation, not one number
The same YOLOv8n was trained twice, on the test image alone and on the 3-channel composite, with the same split and seed. Colour augmentation is switched off for the composite so each channel keeps its meaning.
The rules still have the last word
Swapping in a learned detector changes only stage 1. Area limits, batch counts and the Open / Short overrides run exactly as before.

Stack

View the repo

More work