Explainable classification of electronic components — every prediction traceable to a readable decision path.
A Random Forest over 33 hand-engineered computer-vision features that classifies discrete electronic components — resistors, capacitors, diodes, inductors, transistors and ICs — from a single image, and can explain why it reached each decision.
No GPU, no pretrained backbone, ~50 ms per image on a laptop CPU.
Five components across different types — resistor, IC, capacitor, diode, inductor — each with its full six-panel explanation: input, class probabilities, feature importance, structural cues, colour signature and the literal decision path.
Trained on 1,195 labelled images across 6 classes. Evaluated twice: on a stratified 25% test split, and on a 120-image holdout set that was never part of training or tuning.
| Evaluation | Accuracy |
|---|---|
| Test split (299 samples) | 92.98% |
| Holdout set (120 images) | 93.33% (112/120) |
The two numbers agreeing to within half a point is the point worth making: the model is not tuned to its own test split.
| Class | Precision | Recall | F1 | Support |
|---|---|---|---|---|
| Resistors | 0.93 | 1.00 | 0.96 | 154 |
| Transistors | 1.00 | 0.90 | 0.95 | 21 |
| ICs | 0.97 | 0.88 | 0.92 | 32 |
| Capacitors | 0.90 | 0.92 | 0.91 | 59 |
| Inductors | 0.88 | 0.78 | 0.82 | 18 |
| Diodes | 1.00 | 0.60 | 0.75 | 15 |
| Weighted avg | 0.93 | 0.93 | 0.93 | 299 |
Diodes are the honest weak spot — perfect precision but 0.60 recall, on only 15 test samples. They are visually close to small capacitors, and the class is under-represented. That is the first thing the next iteration should fix.
Accuracy alone does not tell an inspection process what to do. The more useful property is whether the model's confidence is informative — and here it is:
| Threshold | Accepted | Accuracy on accepted | Flagged for review | Errors caught |
|---|---|---|---|---|
| none | 100% | 93.3% | 0 | 0 / 8 |
| 0.50 | 88% | 98.1% | 14 | 6 / 8 |
| 0.60 | 79% | 98.9% | 25 | 7 / 8 |
| 0.70 | 66% | 98.7% | 41 | 7 / 8 |
Mean confidence is 0.798 when correct and 0.452 when wrong. Accepting only predictions above 0.60 leaves 98.9% accuracy on the 79% it handles automatically, and routes 7 of the 8 errors into the fifth it hands to a human. That is a usable operating point, not just a headline number.
Regenerate with:
python scripts/confidence_report.py --predictions results/holdout_predictions.csv| Rank | Feature | Importance |
|---|---|---|
| 1 | color_ratio_r |
0.077 |
| 2 | center_value |
0.055 |
| 3 | r_mean |
0.053 |
| 4 | center_saturation |
0.052 |
| 5 | g_std |
0.052 |
Colour dominates, which matches how a human sorts a parts bin: beige cylinders are resistors, silver cans are electrolytics, black slabs are ICs. Importance is spread fairly evenly across the 33 features rather than concentrated — no single feature carries the model.
Every prediction can be rendered as a six-panel report: what the model saw, what it concluded, which features drove it, and the literal comparisons the tree made.
resistors_101.png — correct at 99.0%. Four leads detected, no printed markings, colour and solidity place it firmly in the resistor region.
And the same reasoning on a different component type:
inductors_53.png — correct at 74.0%. Five leads and electrodes at both ends, printed markings present, a brown body — the structural cues and colour together place it in the inductor region, with capacitors the distant runner-up.
Generate these for any image:
python scripts/explain.py --model results/model.joblib --image data/holdout/ICs_105.pngThe same trace is available as text:
$ python scripts/predict.py --model results/model.joblib --image data/holdout/resistors_10.png --explain
Image: resistors_10.png
Prediction: resistors
Confidence: 93.96%
Probability distribution:
resistors 93.96% |############################ |
ICs 3.16% | |
inductors 2.34% | |
capacitors 0.39% | |
transistors 0.11% | |
diodes 0.04% | |
Key features:
colour : gray
is circle : no
electrodes at ends: no
lead count : 4
has text : no
Decision path (tree #0):
Node 0: solidity <= 0.816 (value: 0.763)
Node 1: g_std > 36.622 (value: 61.379)
Node 23: r_mean <= 67.140 (value: 48.647)
Node 24: color_ratio_r <= 0.345 (value: 0.335)
Node 25: gradient_std <= 128.178 (value: 62.977)
Node 26: color_ratio_r > 0.311 (value: 0.335)
Node 28: contour_area > 340.000 (value: 2569.000)
Node 30: aspect_ratio <= 3.346 (value: 2.477)This is the reason for choosing a forest over a small CNN. In an inspection setting, "rejected" is not a useful output on its own — "rejected because the body colour ratio and the absence of printed markings place it outside the resistor region" is.
Each extractor fuses several independent strategies rather than trusting one heuristic:
| Extractor | Produces | Approach |
|---|---|---|
| Colour | color_code |
Weighted vote across 7 sampling regions using 11 expanded HSV ranges; LAB fallback when the top two candidates are within 10% |
| Shape | is_circle, circularity, aspect_ratio, solidity |
Hough circles with a fill-ratio check, backed by contour circularity |
| Electrodes | electrodes_both_ends |
Four scored strategies — dark-pixel histogram, edge density, vertical Hough lines, texture uniformity — requiring 3 of 4 on both ends |
| Leads | pin_count, three_legs |
Contour candidates reconciled against grouped vertical lines, then filtered by spacing regularity |
| Markings | has_text, written_chars, text_density |
Three merged thresholds over the central 60%, filtered on shape, then grouped by spacing uniformity and baseline alignment |
| Statistics | 18 features | Sobel gradients, local-variance texture, RGB statistics, histogram moments, edge density |
The multi-strategy design is what does the work. A single HSV range confuses beige resistors with cream ICs under warm light; the weighted regional vote does not. A single contour pass either merges adjacent leads or splits one in two; reconciling contours against line groups and enforcing spacing regularity recovers the true count.
Full details in docs/architecture.md.
Tree #0 of 100 — 153 nodes, 77 leaf rules, rendered to depth 4. The root splits on solidity; the paths below it are the rules the model applies to real images.
git clone https://github.com/ParsaVictor/pcb-component-classifier.git
cd pcb-component-classifier
pip install -r requirements.txtTrain from scratch (~2 minutes on a laptop CPU):
python scripts/train.py --data data/train --out resultsClassify a single image, with the decision path:
python scripts/predict.py --model results/model.joblib --image path/to/component.png --explainClassify a folder and score it against ground truth:
python scripts/predict.py --model results/model.joblib --folder data/holdoutAs a library:
import sys; sys.path.insert(0, "src")
from pcb_classifier import extract_features_advanced, load_model, predict_one, decision_path_trace
model, feature_cols = load_model("results/model.joblib")
features = extract_features_advanced("component.png")
label, probabilities, classes = predict_one(model, features, feature_cols)
print(f"{label} ({max(probabilities):.1%})")
for step in decision_path_trace(model, features, feature_cols, max_steps=5):
print(step)The full dataset ships with the repository — no download step, no external dependency.
| Split | Images | Purpose |
|---|---|---|
data/train/ |
1,195 | Training and the stratified test split |
data/holdout/ |
120 | Never seen during training or tuning |
Six classes: resistors, capacitors, diodes, inductors, transistors, ICs. Labels are derived from filenames (see infer_category).
pcb-component-classifier/
├── src/pcb_classifier/ # the library
│ ├── preprocessing.py # letterbox normalisation
│ ├── color.py # multi-region colour voting
│ ├── shape.py # circles and contour descriptors
│ ├── detectors.py # electrodes and lead counting
│ ├── text.py # printed marking detection
│ ├── features.py # the 33-feature vector
│ └── model.py # training, evaluation, explanation
├── scripts/
│ ├── train.py # train and write all artefacts
│ ├── predict.py # classify an image or a folder
│ ├── explain.py # six-panel explanation figures
│ ├── confidence_report.py # the calibration analysis above
│ └── make_architecture_diagram.py # regenerates the pipeline diagram
├── notebooks/ # the full exploratory notebook, with outputs
├── tests/ # 32 tests, no dataset required
├── data/ # train/ (1,195) and holdout/ (120)
├── results/ # metrics, plots, explanations, demo.gif, model
├── configs/default.yaml
└── docs/ # architecture.md + architecture.png
Every figure in this README is reproducible from the scripts above — none of them are hand-drawn.
pip install -r requirements-dev.txt
pytest tests/ -v32 tests covering normalisation, every detector, the feature contract, label inference, training, persistence and the explanation path. They run on synthetic images, so no dataset is needed and CI stays fast.
- Fix diode recall — the clearest weakness at 0.60
- Rebalance the classes (resistors are 51% of the training set)
- Replace filename-derived labels with reviewed annotations
- Combine with the YOLOv8 detector: locate components on a board, then classify each crop with a traceable justification
- Interactive demo (Gradio), exposing the confidence threshold as a live control
pcb-component-detection-yolov8 — the detection half of the problem: locating components on a populated board. The two are designed to be combined.
MIT — see LICENSE.
Mohammad Parsa Karkooti — AI Engineer (Computer Vision, Machine Learning, Deep Learning)



