How to Implement Anomaly Detection with PatchCore or PaDiM in ailia-models

Both PatchCore and PaDiM extract multi-layer features from a pre-trained ResNet, compress them via random projection, build a compact core-set using K-Center Greedy sampling, and detect anomalies by measuring nearest-neighbor distances in a FAISS index.

The ailia-models repository by axinc-ai provides production-ready implementations of these unsupervised visual anomaly detection methods. This guide explains how to implement PatchCore and PaDiM using the repository's modular Python API, covering feature extraction, core-set construction, and inference pipelines.

Understanding the PatchCore and PaDiM Pipeline

Both methods follow the identical unsupervised pipeline implemented in anomaly_detection/patchcore/patchcore_utils.py and anomaly_detection/padim/padim_utils.py.

Feature Extraction Backbone

The implementation uses ONNX models of pre-trained ResNet-18 or Wide-ResNet-50-2 backbones. Features are extracted from the outputs of intermediate layers layer2 and layer3, then concatenated per spatial location to create high-dimensional embedding vectors.

Dimensionality Reduction and Core-Set Sampling

Embeddings are compressed using sklearn.random_projection.SparseRandomProjection to preserve distances via the Johnson-Lindenstrauss lemma. The K-Center Greedy algorithm (k_center_greedy.KCenterGreedy) then selects a representative subset—defaulting to 0.1% of training patches—and stores them in a FAISS index.

Nearest-Neighbor Anomaly Scoring

During inference, each test patch queries the FAISS index (faiss.IndexFlatL2) to retrieve k-nearest neighbors. The distance to the closest neighbor becomes the patch-level anomaly score, which is normalized, blurred with a Gaussian filter, and up-sampled to the original image size.

Project Structure and Key Files

Path Description
anomaly_detection/patchcore/patchcore.py CLI driver for PatchCore
anomaly_detection/patchcore/patchcore_utils.py Core implementation: preprocessing, embedding concatenation, K-Center Greedy, FAISS indexing, inference, and visualization
anomaly_detection/patchcore/README.md Quick-start guide and command-line examples
anomaly_detection/padim/padim.py CLI driver for PaDiM
anomaly_detection/padim/padim_utils.py PaDiM utility functions (identical logic to PatchCore)
anomaly_detection/padim/README.md Usage instructions for PaDiM
util/model_utils.py check_and_download_models() for ONNX weights
util/detector_utils.py load_image() helper
util/arg_utils.py Shared argument parsing

Implementing PatchCore

The following example demonstrates the complete workflow using anomaly_detection/patchcore/patchcore_utils.py:

import ailia
from anomaly_detection.patchcore.patchcore_utils import (
    training, infer, normalize_score_maps, calculate_anomal_scores,
    visualize, get_params,
)
from util.model_utils import check_and_download_models
from util.detector_utils import load_image
import cv2
import pickle

# ── 0. Choose backbone and parameters ──────────────────────────────────────

arch = "wide_resnet50_2"          # or "resnet18"

n_neighbors = 9
weight_path, model_path, params = get_params(arch, n_neighbors)

# ── 1. Download model files if missing ───────────────────────────────────────

REMOTE_PATH = "https://storage.googleapis.com/ailia-models/patchcore/"
check_and_download_models(weight_path, model_path, REMOTE_PATH)

# ── 2. Create the Ailia net ---------------------------------------------------

net = ailia.Net(model_path, weight_path, env_id=0)

# ── 3. Train on normal images (no anomalies) -------------------------------

train_dir = "./anomaly_detection/patchcore/train"   # folder of only “good” samples

embedding_coreset = training(
    net, params,
    size=256, keep_aspect=True,
    batch_size=32,
    train_dir=train_dir,
    aug=False, aug_num=1,
    coreset_sampling_ratio=0.001,
    logger=ailia.utils.logging.getLogger(__name__),
)

# ── 4. (Optional) Save the core‑set for later reuse ───────────────────────────

with open("patchcore_train.pkl", "wb") as f:
    pickle.dump(embedding_coreset, f)

# ── 5. Load a test image ----------------------------------------------------

test_path = "./anomaly_detection/patchcore/bottle_000.png"
img = load_image(test_path)                      # BGR‑A format

img = cv2.cvtColor(img, cv2.COLOR_BGRA2RGB)      # → RGB

# Preprocess must match training

from anomaly_detection.patchcore.patchcore_utils import preprocess
img_pre = preprocess(img, size=256, keep_aspect=True)

# ── 6. Inference ------------------------------------------------------------

score_map = infer(net, params, embedding_coreset, img_pre)

# ── 7. Normalise & visualise -----------------------------------------------

scores = normalize_score_maps([score_map])[0]      # (H, W) in [0, 1]

anomaly_score = calculate_anomal_scores([score_map])[0]
heat, mask, overlay = visualize(
    img,                     # original (H,W,3) uint8

    scores,
    threshold=0.5,           # set manually or compute from GT

)

cv2.imwrite("patchcore_heat.png", heat.astype("uint8"))
cv2.imwrite("patchcore_mask.png", mask.astype("uint8"))
cv2.imwrite("patchcore_overlay.png", overlay)
print(f"Image‑level anomaly score = {anomaly_score:.4f}")

Implementing PaDiM

PaDiM follows the identical API, importing from anomaly_detection/padim/padim_utils.py instead:

import ailia
from anomaly_detection.padim.padim_utils import (
    training, infer, normalize_score_maps,
    calculate_anomal_scores, visualize, get_params,
)
from util.model_utils import check_and_download_models
from util.detector_utils import load_image
import cv2, pickle

# 0. Choose backbone

arch = "resnet18"
n_neighbors = 9
weight_path, model_path, params = get_params(arch, n_neighbors)

# 1. Download model files

REMOTE_PATH = "https://storage.googleapis.com/ailia-models/padim/"
check_and_download_models(weight_path, model_path, REMOTE_PATH)

# 2. Net

net = ailia.Net(model_path, weight_path, env_id=0)

# 3. Training

train_dir = "./anomaly_detection/padim/train"
embedding_coreset = training(
    net, params,
    size=256, keep_aspect=True,
    batch_size=32,
    train_dir=train_dir,
    aug=False, aug_num=1,
    coreset_sampling_ratio=0.001,
    logger=ailia.utils.logging.getLogger(__name__),
)

# 4. Save core‑set (optional)

with open("padim_train.pkl", "wb") as f:
    pickle.dump(embedding_coreset, f)

# 5. Infer on a test image

test_path = "./anomaly_detection/padim/bottle_000.png"
img = load_image(test_path)
img = cv2.cvtColor(img, cv2.COLOR_BGRA2RGB)

from anomaly_detection.padim.padim_utils import preprocess
img_pre = preprocess(img, size=256, keep_aspect=True)

score_map = infer(net, params, embedding_coreset, img_pre)

# 6. Visualise

scores = normalize_score_maps([score_map])[0]
anomaly_score = calculate_anomal_scores([score_map])[0]
heat, mask, overlay = visualize(img, scores, threshold=0.5)

cv2.imwrite("padim_heat.png", heat.astype("uint8"))
cv2.imwrite("padim_mask.png", mask.astype("uint8"))
cv2.imwrite("padim_overlay.png", overlay)
print(f"PaDiM image‑level score = {anomaly_score:.4f}")

Automatic Threshold Selection

When ground-truth masks are available, compute the optimal threshold by maximizing the F1-score using decide_threshold_from_gt_image:

from anomaly_detection.patchcore.patchcore_utils import decide_threshold_from_gt_image

gt_dir = "./anomaly_detection/patchcore/gt_masks"
threshold = decide_threshold_from_gt_image(net, params, embedding_coreset, gt_dir)
print("Optimal threshold =", threshold)

This function uses sklearn.metrics.precision_recall_curve to select the threshold that yields the highest F1-score on the validation set.

Summary

  • PatchCore and PaDiM in ailia-models share the same unsupervised pipeline: feature extraction from ResNet layers layer2 and layer3, random projection, K-Center Greedy core-set sampling, and FAISS nearest-neighbor search.
  • Entry points are anomaly_detection/patchcore/patchcore.py and anomaly_detection/padim/padim.py, with reusable logic in patchcore_utils.py and padim_utils.py.
  • The API supports both CLI usage and programmatic embedding via training(), infer(), and visualize() functions.
  • Automatic threshold selection is available when ground-truth masks are provided, using F1-score optimization.

Frequently Asked Questions

What is the difference between PatchCore and PaDiM in the ailia-models implementation?

Both algorithms follow the identical pipeline in this repository: multi-layer feature extraction, random projection, core-set sampling, and nearest-neighbor distance scoring. The primary distinction lies in the original research focus—PatchCore emphasizes memory-efficient core-set subsampling, while PaDiM focuses on multivariate Gaussian modeling—but the ailia-models implementation unifies both under the same K-Center Greedy and FAISS architecture for practical deployment.

Which backbone architectures are supported for feature extraction?

The implementation supports ResNet-18 and Wide-ResNet-50-2 backbones, provided as ONNX models. Features are extracted from the outputs of intermediate layers layer2 and layer3, then concatenated per spatial location to form the embedding vectors used for core-set construction.

How does the K-Center Greedy algorithm reduce memory usage?

Instead of storing every training patch embedding, the KCenterGreedy class in patchcore_utils.py iteratively selects a subset of patches (default 0.1% of the training set) that maximally cover the embedding space. This core-set is stored in a FAISS index, reducing memory from millions of vectors to thousands while preserving detection accuracy through the Johnson-Lindenstrauss distance-preserving property of random projection.

Can I use custom datasets without ground-truth masks for training?

Yes. Both training() functions require only a directory of normal (non-defective) images. Ground-truth masks are optional and used solely for automatic threshold selection via decide_threshold_from_gt_image(). Without masks, you must manually specify a threshold (typically 0.5) in the visualize() function or determine it empirically on a validation set.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →