Anomalib for industrial quality inspection

Written by our experts Benedikt Neyses and Andreas Thore.

What you will learn in this tutorial

How you as manufacturing engineer or applied AI practitioner can build a working visual quality inspection pipeline even when you only have very few defect examples, using Anomalib’s ready-made data modules, training loop integration, and modern anomaly detection models.

Introduction

In many manufacturing settings, visual inspection has an awkward asymmetry: you often have lots of images of acceptable parts, but only a small number of defect examples. That is exactly why anomaly detection is so attractive for industrial computer vision. Instead of needing many labelled examples of every defect type, you train on normal data and ask the model to learn what “good” looks like. Anomalib is a practical library built around this workflow, with ready-made anomaly models, datamodules, and evaluation utilities for public and private datasets. Links to Anomalib:

The goal of this post is to show how to go from a clean notebook example to a structure that you and your team can reproduce, understand, and later adapt to its own cameras, parts, and inspection constraints.

Why anomaly detection is a good starting point

If you are inspecting painted sheet metal, cast components, machined parts, or coated surfaces, you have probably noticed that you can produce thousands of normal images every day, but only a handful of relevant examples of scratches, dents, or other surface irregularities. In that setting, a standard supervised defect detection model is often not the best first move, because it assumes you already have enough labelled examples of each defect class. Anomaly detection changes the problem: train on normal data, then during deployment you only try to detect when an image deviates from what we see as normal or acceptable. 

That does not mean anomaly detection solves everything. It is strongest when you need screening and localisation of unusual regions. It is weaker when you need precise defect taxonomy, root-cause attribution, or a defect-class code that must line up with an ERP or quality database. But as a first production-facing step, it is often the fastest way to get a useful visual inspection system off the ground.

Anomalib and MVTec AD2

Anomalib is useful because it simplifies much of the repetitive work around anomaly-detection experiments: data loading, model integration, training, evaluation, and deployment helpers. For this tutorial, we use MVTec AD 2 and specifically the sheet_metal category. The dataset is a benchmark for unsupervised anomaly detection on challenging industrial inspection use cases, with more than 8,000 high-resolution images in total. You can download the dataset here: https://www.mvtec.com/research-teaching/datasets/mvtec-ad-2

What we will build in this tutorial

At a high level, the pipeline is simple:

  • Load MVTec AD 2 sheet_metal with anomalib.
  • Inspect the normal and anomalous samples.
  • Train a baseline anomaly model.
  • Run inference and inspect anomaly maps.
  • Evaluate on the test split.
  • Show how to adapt the same pipeline to a company dataset.

We use the PatchCore architecture as the example model. It is a well-known industrial anomaly baseline built around a memory bank of patch features from a pretrained neural network backbone. In anomalib’s implementation, PatchCore extracts patch features from normal images during training and during deployment it compares the collected image patches against that memory bank.

1. Environment setup and installing Anomalib

Bash
python -m venv .venv
source .venv/bin/activate

pip install anomalib

If you already manage environments with uv, conda, or containers, keep using whatever your team standard is. In the anomalib documentation you can find more options, e.g. specifying a backend using PyTorch.

2. Load MVTec AD2 dataset

Anomalib provides a dedicated MVTec AD2 datamodule, and the we choose sheet_metal as the category.

Python
from anomalib.data import MVTecAD2

datamodule = MVTecAD2(
    root="./datasets/MVTec_AD_2",
    category="sheet_metal",
    train_batch_size=16,
    eval_batch_size=16,
    num_workers=8,
    test_type="public",   # local evaluation with ground-truth masks
)

datamodule.setup()

train_batch = next(iter(datamodule.train_dataloader()))
test_batch = next(iter(datamodule.test_dataloader()))

print("Train keys:", train_batch.keys())
print("Test keys:", test_batch.keys())
print("Train image shape:", train_batch["image"].shape)

The exact keys may vary a little across versions. Anomalib’s image datamodules expose the standard image, label, mask, and path fields needed for anomaly workflows. Below are examples of a good image and a bad image with the defect mask. We don’t need bad images or the defect mask for training. 

Good image:

Good image (underexposed):

Good image (lighting shift):

Bad image:

Defect mask:

3. Train a first anomaly model (PatchCore)

The anomalib documentation shows a clean API pattern using a model class plus the Engine wrapper for training and testing. The Engine wraps PyTorch Lightning functionality with anomaly-specific behaviour and uses default_root_dir to control where training outputs are written. The default deep learning backbone of Patchcore is a pretrained ResNet50 variant, but this can be easily replaced with more modern and advanced models e.g., a Transformer for more tricky use cases.

Python
from anomalib.models import Patchcore
from anomalib.engine import Engine

model = Patchcore()

engine = Engine(
    default_root_dir="results/patchcore_sheet_metal",
    accelerator="gpu",   # use "cpu" if no GPU is available
    devices=1,
)

engine.fit(
    model=model,
    datamodule=datamodule,
)

engine.test(
    model=model,
    datamodule=datamodule,
)

print("Best checkpoint:", engine.best_model_path)

One nice detail from anomalib is that models can inject sensible default image transforms if you do not manually define them. Default preprocessing includes resizing, centre cropping, and normalization. That is useful because it removes some boilerplate coding while still following the model’s expected preprocessing pipeline. Check out: https://anomalib.readthedocs.io/en/v1.1.1/markdown/guides/how_to/data/transforms.html

Below we have some pseudo code to show what training is doing conceptually:

for each normal training image:
    extract patch-level features with pretrained backbone
    store representative patch features in memory bank
for each test image:
    extract patch-level features
    compare each patch to nearest normal features in memory bank
    convert distances into anomaly map and image-level anomaly score

That is a simplified description, but it captures the core intuition: the model is not learning “scratch” or “dent” labels directly. It is learning the feature distribution of normal surfaces and flagging deviations from that distribution.

4. Evaluate and inspect anomaly maps

Metrics matter, but in industrial vision, visual inspection of failure cases matters just as much. A model can look good in aggregate and still fail in exactly the situations a plant cares about. A useful first evaluation loop is:

Python
predictions = engine.predict(
    model=model,
    datamodule=datamodule,
)

print(f"Number of prediction batches: {len(predictions)}")

Pseudocode for a visualisation loop:

for sample in selected_test_samples:
    image = load_image(sample)
    gt_mask = load_mask_if_available(sample)
    pred = run_model(sample)
    show_panel(
        image=image,
        ground_truth=gt_mask,
        anomaly_map=pred.anomaly_map,
        score=pred.pred_score,
        predicted_mask=pred.pred_mask,
    )

5. Training PatchCore on your own data

anomalib’s Folder datamodule is designed for custom datasets with normal and abnormal images, with optional masks. The most straightforward way is to apply a simple structure with normal_dir, abnormal_dir, optional normal_test_dir, and optional mask_dir. Below is the recommended dataset structure:

factory_surface_data/
  train/
    good/
      part_0001.png
      part_0002.png
      ...
  test/
    good/
      part_1001.png
      ...
    scratch/
      part_2001.png
      ...
    dent/
      part_3001.png
      ...
  ground_truth/
    scratch/
      part_2001.png
      ...
    dent/
      part_3001.png
      ...
  • train/good contains normal images which are used to learn what normal means.
  • test/good contains normal images to test the trained PatchCore model.
  • test/<defect_type>: anomalous images to test if the model detects them as such.
  • ground_truth/<defect_type> contains optional masks for pixel-level evaluation.

To reiterate, masks are optional. If you don’t have them, you can still run image-level anomaly detection. Ground truth masks become more relevant in further steps, when you want to build a real defect detection or multi-class classification pipeline.

Code snippet to use anomalib on your own custom dataset:

Python
from anomalib.data import Folder
from anomalib.models import Patchcore
from anomalib.engine import Engine

datamodule = Folder(
    name="factory_surface_data",
    root="./factory_surface_data",
    normal_dir="train/good",
    abnormal_dir="test/scratch",      # or a list of defect folders
    normal_test_dir="test/good",
    mask_dir="ground_truth/scratch",  # optional
    train_batch_size=16,
    eval_batch_size=16,
    num_workers=8,
)

model = Patchcore()

engine = Engine(
    default_root_dir="results/factory_patchcore",
    accelerator="gpu",
    devices=1,
)

engine.fit(model=model, datamodule=datamodule)
engine.test(model=model, datamodule=datamodule)

The exact folder-to-argument mapping may depend on how you organize multi-defect folders, but the essential point is that you do not need to reproduce the full benchmark dataset structure. All you need to get started is a clean separation between normal training images, abnormal test images, optional normal test images, and optional masks.

Practical advice for using your own data:

  • Keep the imaging setup as stable as possible at first.
  • Avoid near-duplicate frames leaking across train and test. So, be careful with automated train-test splits. Ideally you will have a real test set, i.e., images collected at a different time than for the training set, maybe another from another production batch.
  • If production conditions vary by line, shift, material batch, or lighting setup, store that metadata, but to start with, keep these factors as constant as possible. Start with one narrow use case instead of mixing many unrelated products into one first experiment.

6. A short note on running this pipeline on a GPU cluster

Most single anomaly experiments are not huge training jobs (but they become heavier with using larger and more advanced backbone models. The real value of a cluster for anomaly detection is usually parallel experimentation: multiple categories, multiple resolutions, or multiple preprocessing choices. A few practical guidelines:

  • Keep datasets on a shared project storage.
  • Write logs and checkpoints to a project output directory
  • Run one experiment per job
  • Use job arrays when sweeping over categories or settings

Jobs are often submitted through SLURM (open-source job scheduler that is used by many supercomputers and compute clusters) rather than manually starting processes.

Pseudo code for a simple SLURM job

#!/bin/bash
#SBATCH --job-name=anomalib_sheet_metal
#SBATCH --gres=gpu:1
#SBATCH --cpus-per-task=8
#SBATCH --mem=32G
#SBATCH --time=02:00:00

source .venv/bin/activate

python train_anomalib.py

Where to go next

Once you have managed to train your first anomaly detection model, there are several natural next steps to explore:

Summary

You do not need a fully labelled defect taxonomy to start building useful visual inspection. If you can collect representative normal images, hold out realistic test data, and structure the dataset cleanly, you can already build an industrial anomaly pipeline that detects and localizes unusual surface regions. anomalib and MVTec AD 2 make that workflow accessible, and the same structure can be carried over to a company’s own data with relatively little friction.