← Praruj Thapa
/
Project · Edge AI · 2026

Industrial anomaly detection on the edge

Two detectors that learn only from healthy equipment and flag anything else: one inspects products from photos, the other listens to motor-bearing vibration. Then I shrank both to run on a small CPU, and measured everything on a laptop limited to one thread as a stand-in for an edge device.

161 → 89 ms
per image for visual inspection after int8 and a smaller memory bank (pcb1)
4×
smaller vision model: 11.1 MB → 2.9 MB, image AUROC 0.942 → 0.918
100% → 2.8%
false alarms at an unseen motor load, after re-setting the threshold from 2 s of data
Visual inspection

PatchCore on three categories of the public VisA dataset: a printed circuit board (pcb1), pharmaceutical capsules and cashews. It never sees a defect while learning. The colour shows which patches look unlike anything in the good parts.

pcb1: detected defect heatmap and a good part
pcb1 — left: the highest-scoring defect, heat = patches unlike any good part, cyan = true outline. Right: a good part stays dark.
capsules: detected defect heatmap and a good part
capsules — left: the highest-scoring defect, heat = patches unlike any good part, cyan = true outline. Right: a good part stays dark.
cashew: detected defect heatmap and a good part
cashew — left: the highest-scoring defect, heat = patches unlike any good part, cyan = true outline. Right: a good part stays dark.
What runs on the edge

The network is not the bottleneck. After int8 the ResNet18 backbone takes 38 ms; comparing its 1,024 patches with a 10,000-entry memory bank takes another ~115 ms. A greedy-coreset bank of 2,500 patches keeps most of the accuracy at close to half the time.

Categoryfp32, 10k bankint8, 10kint8, 2.5kint8, 1k
pcb10.942 · 161 ms0.924 · 155 ms0.918 · 89 ms0.885 · 77 ms
capsules0.782 · 161 ms0.749 · 155 ms0.752 · 89 ms0.697 · 78 ms
cashew0.952 · 161 ms0.880 · 154 ms0.875 · 88 ms0.896 · 77 ms

Image AUROC · median latency per image, one CPU thread. 100 defective and 100 good test images per category.

int8 is not free. It makes the model 4× smaller but costs 0.02–0.07 image AUROC, most on cashew. Calibration used good parts only, as it would on a real line.

Vibration monitoring

A 4.5k-parameter autoencoder (11 KB as int8) on 0.17-second windows of bearing vibration from the CWRU dataset. Every model scores AUROC 1.0 here, even with heavy added noise, which says more about the dataset than the models, so I tested what actually breaks in production: a motor load the model never saw.

Vibration monitorFaults caughtFalse alarms (healthy)
Trained on all four motor loads100%6.3%
3 HP load never seen100%100%
Same, threshold re-set from 2 s of healthy data100%2.8%

The ranking stayed perfect, but a fixed alarm threshold flagged every healthy window at the new load. The fix is operational, not a bigger model: recalibrate when a machine changes operating point.

What I learned
  • Profile before you optimise. Quantizing the network made it 4× smaller but barely faster; the nearest-neighbour search was the real cost.
  • Read the data sheet. CWRU's healthy recordings are 48 kHz, not 12 kHz like the fault files; their resonances only line up when read correctly. Read naively, every frequency feature is off by 4×.
  • A perfect score can still be useless. AUROC 1.0 did not stop 100% false alarms at a new operating point. Thresholds need the same care as models.
  • Say where it fails. Small defects barely light up at 256×256, and capsules (0.78 AUROC) is the weakest category. Both are listed in the repository, with what to try next.
Stack
PyTorchONNX Runtime int8PatchCorescikit-learnSciPy signal processingpytest + GitHub Actions