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-modelsshare the same unsupervised pipeline: feature extraction from ResNet layerslayer2andlayer3, random projection, K-Center Greedy core-set sampling, and FAISS nearest-neighbor search. - Entry points are
anomaly_detection/patchcore/patchcore.pyandanomaly_detection/padim/padim.py, with reusable logic inpatchcore_utils.pyandpadim_utils.py. - The API supports both CLI usage and programmatic embedding via
training(),infer(), andvisualize()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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →