How Do You Ship Industrial Defect Detection With Only 'Good' Samples? An Anomalib Field Guide
Key Takeaways — Executive & AI Summary
- Anomalib is the OpenVINO team's open-source industrial anomaly-detection library (now under the open-edge-platform GitHub organization, v2.x): PaDiM, PatchCore, EfficientAd and other models train on normal samples only — we counted 209–391 'good' images per scene in the standard benchmark — because the paradigm learns what healthy looks like and scores everything that deviates.
- What we actually stood up on the workstation (2025-11-19): a conda environment, a 5-option quick_start.sh that checks GPU/env/mirror/dataset then runs train or predict, a train_all_scenes.py batch runner covering MVTecAD's 15 scene categories with per-scene timing and error logs, and three working docs (deployment guide, SOP, results interpretation).
- The ledger, completed 2026-08-30: all 15 MVTec categories now have trained checkpoints and measured metrics — mean image-AUROC 0.981 (range 0.911-1.000) at one-epoch defaults, per-scene durations 29-169 s on a 16GB A4000, with three texture classes showing brittle pixel-level AUROC under untuned defaults. Completing it took fixing three real bugs first: the shipped batch script never worked (v2 CLI rejects its batch flags), the HF mirror variable is mandatory or runs stall on backbone checks, and CUDA's fastest-first ordering lands single-GPU jobs on the busiest card unless you pin by UUID.
Industrial quality inspection has a data paradox: defects are rare, defect labels are rarer, and yet most supervised vision needs both. Anomalib inverts the problem — learn only what healthy product looks like, then flag whatever deviates. This dispatch is the field-guide version of standing that up on a real workstation: what the library is, what it does, the exact path we ran, and where it bit us — and it opens this site’s Industrial AI line, where the rubric stays the same for every tool: what it is → what it does → how to run it → what went wrong. Written for engineers meeting it for the first time; every command below is the one we actually used.
What it is
Anomalib is the OpenVINO team’s open-source library for visual anomaly and defect detection — currently v2.x under the open-edge-platform GitHub organization — bundling the mainstream algorithms (PaDiM, PatchCore, EfficientAd, plus FastFlow, STFpm, CFlow) behind one CLI. The unifying idea: train on normal samples only. No defect taxonomy, no balanced datasets, no labeling campaign. PatchCore, the default workhorse, memorizes patch-level features of good product into a coreset memory and scores test images by nearest-memory distance — which is why it runs one-epoch training and still performs.
The library grew up around MVTec AD, the industry-standard benchmark of 15 industrial scene categories (bottle, cable, capsule, carpet, grid, hazelnut, leather, metal_nut, pill, screw, tile, toothbrush, transistor, wood, zipper) — textures and objects photographed as a production camera would see them.
What that means in sample counts
The “few normal samples” claim, measured on our own disk: bottle trains from 209 good images, cable 224, screw 320, hazelnut 391. That is the whole training set per scene — defect images exist only in the test split, for scoring. If your line can photograph a few hundred good parts, you have a training set.
How we ran it
The rig: a two-GPU workstation (RTX 4090D 24GB — the card recorded in our environment snapshot, 24564 MiB as nvidia-smi reports it — plus an A4000 16GB), Ubuntu, conda environment anomalib_env on Python 3.10, an anomalib checkout at 2.1.0.dev0. Training runs were single-GPU (--trainer.devices 1).
One script to start. quick_start.sh checks the GPU, activates the conda env, sets HF_ENDPOINT=https://hf-mirror.com (mainland-network reality: pretrained backbones download through the mirror or not at all), verifies the dataset tree, then offers a 5-option menu — 1-epoch smoke train on bottle, full 5-epoch train, inference test against the saved checkpoint, batch-train everything, or exit.
One script to batch. train_all_scenes.py loops all 15 categories with Patchcore, defaulting to 1 epoch and batch 32 per scene, timing each run, writing per-scene success/error logs, and finishing with a summary that verifies each model.ckpt path under results/Patchcore/MVTecAD/<category>/v0/weights/lightning/.
Three docs to hand over. The work distilled into a deployment guide (environment from zero through training and troubleshooting), an SOP (the universal one-line template, custom-dataset wiring for both Folder and CSV formats, per-model and multi-GPU DDP variants), and a results-interpretation note (the v0/v1 versioning and the latest symlink — deploy from latest/weights/lightning/model.ckpt unless you are pinning a known-good version).
The core command, the one line everything else wraps:
anomalib train \
--model Patchcore \
--data anomalib.data.MVTecAD \
--data.category bottle \
--data.root ./data \
--trainer.max_epochs 1 \
--trainer.accelerator gpu \
--trainer.devices 1
The ledger — completed (2026-08-30 sweep)
The original honest gap — 3 of 15 scenes trained, durations and metrics unretained — is now closed. On 2026-08-30 a sweep on the idle A4000 trained the remaining 12 categories and read-only-evaluated the three November checkpoints (the same byte-identical files the Mac mini deployment carries — untouched, evaluated in place). All 15 categories now have checkpoints and measured metrics:
| Category | image AUROC | image F1 | pixel AUROC | Source |
|---|---|---|---|---|
| bottle | 1.000 | 1.000 | 0.986 | Nov ckpt, read-only eval |
| cable | 0.986 | 0.967 | 0.985 | Nov ckpt, read-only eval |
| screw | 0.968 | 0.947 | 0.989 | Nov ckpt, read-only eval |
| capsule | 0.993 | 0.982 | 0.990 | 2026-08-30 train |
| carpet | 0.986 | 0.972 | 0.991 | 2026-08-30 train |
| grid | 0.986 | 0.957 | 0.982 | 2026-08-30 train |
| hazelnut | 0.993 | 0.988 | 0.632 | retry at eval_batch 8 |
| leather | 0.995 | 0.992 | 0.439 | 2026-08-30 train |
| metal_nut | 0.998 | 0.984 | 0.987 | 2026-08-30 train |
| pill | 0.945 | 0.950 | 0.981 | 2026-08-30 train |
| tile | 0.994 | 0.956 | 0.619 | 2026-08-30 train |
| toothbrush | 0.911 | 0.935 | 0.989 | 2026-08-30 train |
| transistor | 0.993 | 0.950 | 0.973 | 2026-08-30 train |
| wood | 0.987 | 0.959 | 0.932 | 2026-08-30 train |
| zipper | 0.976 | 0.979 | 0.981 | 2026-08-30 train |
Mean image-AUROC 0.981 (range 0.911–1.000) at one-epoch defaults — no coreset tuning, no threshold calibration. Per-scene training time on the A4000: 29 s (toothbrush) to 169 s (carpet), plus 28–38 s for each read-only evaluation. The November checkpoints remain the per-scene coreset artifacts first published — 231,212,587 bytes (bottle), 240,649,771 (cable) — unchanged by the sweep and now joined by twelve siblings of their own kind. The honest asterisks: three texture classes (leather 0.439, tile 0.619, hazelnut 0.632) show brittle pixel-level AUROC under these defaults — image-level detection stays healthy while localization degrades; and hazelnut’s row exists only because its first attempt died of CUDA OOM at the default evaluation batch on 16 GB (it passed at eval_batch_size 8). For tuned reference numbers, the PatchCore paper’s 99.1% image AUROC remains the authors’ figure, not ours.
What the sweep cost to actually run — three bugs, each verified by its own failure first. One: the shipped train_all_scenes.py has never successfully trained anything — the v2 CLI rejects its top-level --train_batch_size/--eval_batch_size flags, which is why November’s run stopped at 3 scenes (those came from quick_start.sh’s flag-free commands); the working invocation drops the batch flags. Two: without HF_ENDPOINT=https://hf-mirror.com, training completes and then the run stalls to death on backbone HEAD checks to huggingface.co — the mirror is mandatory even for cached weights. Three: CUDA enumerates devices fastest-first, so --trainer.devices 1 lands on the 24 GB card that was already running a live service, not the idle 16 GB card beside it — pin with CUDA_VISIBLE_DEVICES=<UUID>. And the disk footprint stands as first published: 4.8 GB of MVTecAD under datasets/ (5.0 GB working copy under data/), 804 MB of training artifacts — now grown by twelve scenes.
The pitfalls (lived, not hypothetical)
Version churn is the tax. Our checkout is 2.1.0.dev0; between the tutorials we followed, the CLI’s data-module syntax changed — older style --data anomalib.data.MVTecAD --data.category bottle (what our runner uses) versus v2 style --data anomalib.data.datamodules.image.mvtecad.MVTecAD --data.init_args.category bottle (what our SOP’s universal template uses). Our two documents codify both syntaxes because both worked at different moments of the checkout’s life. That is the “fast iteration, lagging docs” experience: search-engine answers from six months ago quietly stop applying. Pin your version; read the CLI help of the version you pinned.
Results directories version silently. Each retrain creates v0, v1, v2… with a latest symlink. Convenient — until a script grabs latest after an accidental retrain and your deployed model changes underneath you. Pin explicit version paths in production.
Wrapped runs hide progress. Our batch runner captures subprocess output to log after completion — during a long sweep the terminal sits silent. For interactive use, run the raw command; for sweeps, trust the logs.
The mirror is not optional (here). Without HF_ENDPOINT=https://hf-mirror.com, first-run backbone downloads stall. One environment variable; both our scripts set it; worth knowing it’s the failure mode when someone reports “it hangs at startup.”
What we’d tell a first-timer: expect the friction in versions and paths, not in GPUs or math — the recorded effort went into reconciling tutorial syntax with the installed version and understanding the results tree, not into wrangling compute.
Who this is for
Engineers doing industrial visual QC with few or zero labeled defect samples; teams evaluating whether open-source detection is credible before buying a vision system; anyone who wants anomaly detection running locally this afternoon rather than after a labeling campaign. The pros, honestly earned: out-of-the-box models that train in one epoch on a few hundred normals, one CLI across paradigms, heatmap visualizations per test image (under results/.../v0/images/), and an OpenVINO export path when it’s time to deploy on Intel edge hardware. The cons, equally honest: a fast-moving 2.x API surface, docs that trail it, and an ecosystem where the benchmark answers your search before your version does.
Replication appendix
# Environment
conda create -n anomalib_env python=3.10 -y && conda activate anomalib_env
# (install anomalib per the repo's current instructions; ours was a 2.1.0.dev0 checkout)
# Dataset: MVTec AD, 15 categories, extracted so that ./data/<category>/train/good exists
# Mirror for mainland networks
export HF_ENDPOINT=https://hf-mirror.com
# Smoke train (one scene, one epoch)
anomalib train --model Patchcore --data anomalib.data.MVTecAD \
--data.category bottle --data.root ./data \
--trainer.max_epochs 1 --trainer.accelerator gpu --trainer.devices 1
# Inference against the checkpoint
anomalib predict --model Patchcore --data anomalib.data.MVTecAD \
--data.category bottle --data.root ./data \
--ckpt_path results/Patchcore/MVTecAD/bottle/v0/weights/lightning/model.ckpt \
--return_predictions true
# Batch: python train_all_scenes.py (15 scenes; or --category screw for one)
# ⚠ as shipped the batch script fails on v2 CLIs — use the direct command above per scene
# Read-only evaluation of an existing checkpoint (no retraining, weights untouched):
anomalib test --model Patchcore --data anomalib.data.MVTecAD \
--data.category bottle --data.root ./data \
--ckpt_path results/Patchcore/MVTecAD/bottle/v0/weights/lightning/model.ckpt
# 16GB-card scenes that OOM at default eval batch (hazelnut here):
# add --data.init_args.eval_batch_size 8
Artifacts produced and kept on the workstation: quick_start.sh, train_all_scenes.py, ANOMALIB_DEPLOYMENT_GUIDE.md, sop.md, results_explanation.md (plus a qa.md), checkpoints for 3 categories, per-run config.yaml and visualization folders. The scripts, docs and the 15-category metrics table are now open-sourced at github.com/xiong1984/anomalib-mvtec-field-kit — including the v2-CLI-fixed batch trainer and the three gotchas above as its README. Numbers from this deployment are in the /data/ ledger; this article is the narrative around them.
Sources and method
First-party: the workstation’s scripts, docs, dataset tree and result artifacts (dated 2025-11-19), inspected directly for this dispatch. Third-party, checked: the anomalib repository (organization move, model roster), Intel’s anomalib defect-detection tutorial, the PatchCore paper (arXiv:2106.08265), and the MVTec AD dataset page. Drafted with AI assistance under human editorial direction; the AUROC figure cited is the paper authors’ claim, explicitly not our measurement. Provenance note for the provenance-minded: the deployment work itself was originally set up with AI assistance on the workstation — this site discloses that where it matters.
FAQ — Direct Answers
- What is anomalib and who maintains it?
- An open-source deep-learning library for visual anomaly and defect detection, released by Intel's OpenVINO team and now hosted at the open-edge-platform GitHub organization (current line is v2.x). It collects state-of-the-art algorithms — PaDiM, PatchCore, EfficientAd, FastFlow, STFPM, CFlow and more — under one CLI and Python API, with export paths to OpenVINO (and TensorRT) for deployment. It grew out of the MVTec AD benchmark ecosystem and wins recurrently at industrial-inspection challenges.
- How can it train with only normal samples?
- The paradigm shift: instead of learning what defects look like (which requires labeled defect images you rarely have), the models learn a dense statistical or memory-based description of 'healthy' from good-only images, then score deviations at inference. In the standard MVTecAD benchmark, a scene's entire training set is a few hundred good images — we counted 209 for bottle, 224 for cable, 320 for screw, 391 for hazelnut. Defect images appear only at test time, for evaluation.
- How do I train all scenes in one go?
- Not with the shipped batch script — as delivered, train_all_scenes.py has never worked: the v2 CLI rejects its top-level batch flags, which is exactly why the November run stopped at 3 of 15 scenes. The working invocation is the quick_start-style command per category: `anomalib train --model Patchcore --data anomalib.data.MVTecAD --data.category <name> --data.root ./data --trainer.max_epochs 1 --trainer.accelerator gpu --trainer.devices 1` — with `HF_ENDPOINT=https://hf-mirror.com` exported and the GPU pinned by UUID (see the ledger section for why each of those is mandatory).
- How do I use my own production-line data?
- Match the folder convention the SOP documents: train/good (normals only), test/good plus test/<defect_type>, optional ground_truth/<defect_type> masks — then point the Folder datamodule at it (`--data anomalib.data.datamodules.image.folder.FolderDataModule --data.init_args.normal_dir="train/good" ...`). A CSV annotation format also exists. Swapping a real line's data in is a folder-layout exercise, not a code exercise.
- Does it work from networks that can't reach HuggingFace?
- Yes, with one environment variable: `export HF_ENDPOINT=https://hf-mirror.com` — the pretrained backbones download through the mirror. Both our scripts set it automatically; without it, first-run backbone downloads stall in mainland-China network conditions.
- What are the real pain points?
- Version churn first: our checkout is 2.1.0.dev0, and between the tutorials we followed the CLI's data-module syntax changed (`--data anomalib.data.MVTecAD --data.category X` versus `--data.init_args.category X`) — our own two docs codify both syntaxes, which is exactly what 'documentation lags the library' looks like in practice. Also expect: results directories versioning as v0/v1 with a `latest` symlink you must understand before deployment; progress invisible if a wrapper captures output (our runner logs after completion, not streaming); and docs that reference renamed modules until the next release catches up.