Industrial anomaly detection

September 5, 2026 · View on GitHub

Indad operator console with dataset gallery, readiness checks, and sample review

Build and inspect industrial anomaly datasets with a workflow designed for operators and automation: create → import → check → review → run a baseline. The focus is persistent datasets, actionable quality checks, and a small set of shared model abstractions.

Start the operator workspace:

python -m pip install -e ".[web]"
indad-web

Open http://127.0.0.1:8000.

Create datasets, save healthy and defective images, review masks and dataset health, then train a baseline. Export the same checks and sample inventory as JSON for agents:

indad-data inspect datasets/your_dataset --require inspection > manifest.json

See the operator and agent dataset workflow for creation, imports, readiness rules, and the Python API.

Three core anomaly detection baselines support this workflow:

  1. SPADE (Cohen et al. 2021) - knn in z-space and distance to feature maps spade schematic
  2. PaDiM* (Defard et al. 2020) - distance to multivariate Gaussian of feature maps padim schematic
  3. PatchCore (Roth et al. 2021) - knn distance to avgpooled feature maps patchcore schematic

* actually does not have any knn mechanism, but shares many things implementation-wise.

Current version: 0.4.0. Releases follow Semantic Versioning; see the changelog for release notes.


Install

python -m pip install -e .

Install optional UI and export support with python -m pip install -e ".[web,export]". Python 3.10 or newer is required.

Usage

CLI:

indad METHOD [--dataset DATASET]
# or: python run.py METHOD [--dataset DATASET]

Results can be found under ./results/.

Code example:

from indad import MVTecDataset, SPADE

model = SPADE(k=5, backbone_name="resnet18", device="cpu")

# get some training data
class_name = "bottle"
train_ds, test_ds = MVTecDataset(class_name).get_dataloaders()

model.fit(train_ds)  # dataloaders currently use batch_size=1

# evaluate
image_rocauc, pixel_rocauc = model.evaluate(test_ds)
print(image_rocauc, pixel_rocauc)

Fitted memory banks are included in the model state, so normal PyTorch persistence works. Recreate the model with the same constructor options before loading:

import torch

torch.save(model.state_dict(), "spade.pt")
restored = SPADE(k=5, backbone_name="resnet18", pretrained=False)
state = torch.load("spade.pt", map_location="cpu", weights_only=True)
restored.load_state_dict(state)

Custom datasets

Use the dataset workspace or indad-data create and indad-data import to build a persistent MVTec-compatible collection. See the dataset workflow. Visual inspection supports datasets without masks; CLI benchmark evaluation requires pixel-level ground truth.


Results

📝 = paper, 👇 = this repo

Image-level

classSPADE 📝SPADE 👇PaDiM 📝PaDiM 👇PatchCore 📝PatchCore 👇
bottle-98.899.8100.0100.0
cable-76.593.399.596.2
capsule-84.688.398.195.3
carpet-84.399.498.798.7
grid-37.198.298.293.0
hazelnut-88.783.7100.0100.0
leather-97.199.9100.0100.0
metal_nut-74.699.4100.098.3
pill-72.689.096.692.8
screw-53.183.098.196.7
tile-97.898.698.799.0
toothbrush-89.497.2100.098.1
transistor-89.296.8100.099.7
wood-98.398.999.298.8
zipper-96.789.599.498.4
averages85.582.695.3*94.399.197.7
  • PaDiM average referencing PaDiM-WR50-Rd550

Pixel-level

classSPADE 📝SPADE 👇PaDiM 📝PaDiM 👇PatchCore 📝PatchCore 👇
bottle97.597.798.397.898.697.8
cable93.794.396.796.198.597.4
capsule97.698.698.598.398.998.3
carpet87.499.099.198.699.198.3
grid88.596.197.397.298.796.7
hazelnut98.498.198.297.598.798.1
leather97.299.299.298.799.398.4
metal_nut99.096.197.296.598.496.2
pill99.193.595.793.297.698.7
screw98.198.998.597.899.498.4
tile96.593.394.194.895.994.0
toothbrush98.998.998.898.398.798.1
transistor97.996.397.597.296.497.5
wood94.194.494.793.695.191.9
zipper96.598.298.597.498.997.6
averages96.996.897.596.998.197.2

PatchCore-10 was used.

Hyperparams

The following parameters were used to calculate the results. They more or less correspond to the parameters used in the papers.

spade:
  backbone: wide_resnet50_2
  k: 50
padim:
  backbone: wide_resnet50_2
  d_reduced: 350
  epsilon: 0.04
patchcore:
  backbone: wide_resnet50_2
  f_coreset: 0.1
  n_reweight: 3

Direction

Prioritize dataset creation, review, quality checks and reproducibility for operators and agents. Keep SPADE, PaDiM and PatchCore as the core models. The next dataset milestones are capture-group splits, versioned snapshots, and saved inspection runs with threshold review. See the workflow and roadmap.

Design considerations

  • Data is processed in single images to avoid batch-statistics interference. Models validate this contract and accept arbitrary spatial input sizes.
  • I decided to implement greedy kcenter from scratch and there is room for improvement.
  • torch.nn.AdaptiveAvgPool2d for feature map resizing, torch.nn.functional.interpolate for score map resizing.
  • GPU is used for backbones and, when available, coreset selection. Pass device="cpu" or --device cpu for deterministic CPU execution.
  • Historical coreset-selection performance:
    • 400-500 it/s @ float32 (RTX3080)
    • 1000+ it/s @ float16 (RTX3080)

Operator console

Install the web extra and run indad-web. Open http://127.0.0.1:8000. The responsive browser interface includes dataset creation, imports, a searchable sample gallery, mask review, label correction, reversible sample exclusion, quality findings, and background baseline inspection. Use indad-web --datasets /path/to/datasets --port 8000 to configure local storage.

The console is plain HTML/CSS/JavaScript served by FastAPI, with no frontend build step. Agents can use the same HTTP API; interactive documentation is at /docs and the OpenAPI schema at /openapi.json. See the HTTP API guide.


Development

python -m pip install -e ".[dev,web,export]"
make lint
make test

The default tests use a small deterministic backbone and do not download model weights. ONNX coverage is marked export; run it with pytest -m export when the export dependencies are installed.


Acknowledgements

References

SPADE:

@misc{cohen2021subimage,
      title={Sub-Image Anomaly Detection with Deep Pyramid Correspondences},
      author={Niv Cohen and Yedid Hoshen},
      year={2021},
      eprint={2005.02357},
      archivePrefix={arXiv},
      primaryClass={cs.CV}
}

PaDiM:

@misc{defard2020padim,
      title={PaDiM: a Patch Distribution Modeling Framework for Anomaly Detection and Localization},
      author={Thomas Defard and Aleksandr Setkov and Angelique Loesch and Romaric Audigier},
      year={2020},
      eprint={2011.08785},
      archivePrefix={arXiv},
      primaryClass={cs.CV}
}

PatchCore:

@misc{roth2021total,
      title={Towards Total Recall in Industrial Anomaly Detection},
      author={Karsten Roth and Latha Pemula and Joaquin Zepeda and Bernhard Schölkopf and Thomas Brox and Peter Gehler},
      year={2021},
      eprint={2106.08265},
      archivePrefix={arXiv},
      primaryClass={cs.CV}
}

MVTec dataset:

@article{Bergmann2021,
 author = {Paul Bergmann and Kilian Batzner and Michael Fauser and David Sattlegger and Carsten Steger},
 title = {The MVTec Anomaly Detection Dataset: A Comprehensive Real-World Dataset for Unsupervised Anomaly Detection},
 journal = {International Journal of Computer Vision},
 year = {2021},
 volume = {129},
 number = {4},
 pages = {1038-1059},
 doi = {10.1007/s11263-020-01400-4}
}
@inproceedings{Bergmann2019,
 author = {Paul Bergmann and Michael Fauser and David Sattlegger and Carsten Steger},
 title = {MVTec AD — A Comprehensive Real-World Dataset for Unsupervised Anomaly Detection},
 booktitle = {IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)},
 year = {2019},
 pages = {9584-9592},
 doi = {10.1109/CVPR.2019.00982}
}