DeMuS: Decoupled Mutual Scoring for Batch Zero-Shot Industrial Anomaly Detection

June 30, 2026 · View on GitHub

Official implementation of DeMuS (Decoupled Mutual Scoring), a two-stage learning framework for Batch Zero-Shot Industrial Anomaly Detection (IAD).

DeMuS: Learning Decoupled Matching and Scoring for Batch Zero-Shot Industrial Anomaly Detection Zhengyang Zhao, Hailong Sun, Binhang Qi, Hongrui Yu, Zhongchi Wang, Hang Xu

DeMuS targets a realistic deployment setting where products are inspected in small batches (e.g. 4–8 images) with no target-domain training data and where intra-batch samples exhibit significant pose variation (rotation, translation). Applying conventional mutual-scoring directly in this setting suffers from two failure modes — the anomaly-to-anomaly shortcut during matching, and a lack of anomaly-aware semantics during scoring. DeMuS resolves both by decoupling the problem into two lightweight, separately trained adapters on top of a frozen backbone:

  • a Matching Adapter (g_m, where-to-match) that learns pose-invariant part-level correspondence, and
  • a Scoring Adapter (g_s, how-to-score) that learns discriminative anomaly semantics.

At inference, a single top-k nearest-neighbor matching step produces soft correspondences, and anomaly scores are aggregated only over aligned parts, preserving the efficiency of baseline mutual scoring.

Results

Image / pixel level, mean over 5 random seeds (from the paper). The released weights reproduce the MVTec AD and VisA numbers below.

DatasetImage AUROCImage APPixel AUROCPixel AUPRO
MVTec AD94.1796.8996.1593.50
VisA90.1691.7096.9492.23

Following the cross-dataset auxiliary-training protocol, the model evaluated on MVTec AD uses VisA as the auxiliary set, and vice versa.

Installation

git clone https://github.com/evoLonation/DeMuS.git
cd DeMuS
pip install -r requirements.txt

The released weight files are tracked with Git LFS. Install LFS (git lfs install) before cloning so the .pt files are fetched, or run git lfs pull afterwards.

DINOv3 backbone

DeMuS uses a frozen DINOv3 ViT-L/16 backbone. Obtain the DINOv3 code and pretrained weights from the official release and accept their license:

Then point DeMuS at your local copies via environment variables:

export DINOV3_REPO_DIR=/path/to/dinov3            # the cloned DINOv3 repo
export DINOV3_WEIGHTS_DIR=/path/to/dinov3_weights # dir with the *.pth checkpoints

The ViT-L/16 weight file expected by default is dinov3_vitl16_pretrain_lvd1689m-8aa4cbdd.pth.

Datasets

Download MVTec AD, VisA, and/or Real-IAD from their official sources and place them under a single root, then point DeMuS at it:

export ANOMALY_DATA_ROOT=/path/to/datasets
# expected subdirectories, e.g.:
#   $ANOMALY_DATA_ROOT/mvtec_anomaly_detection
#   $ANOMALY_DATA_ROOT/VisA_pytorch/1cls
#   $ANOMALY_DATA_ROOT/Real-IAD

You can also pass an explicit path= to any dataset constructor instead of relying on the environment variable.

Reproducing the paper results

Two released weight files reproduce the reported numbers:

FileTarget datasetAuxiliary dataset
demus_aux_visa_for_mvtec_ad.ptMVTec ADVisA
demus_aux_mvtec_for_visa.ptVisAMVTec AD

Each file holds the matching- and scoring-branch adapter state_dicts plus a reported_metrics summary; the frozen DINOv3 backbone is loaded separately (see above).

Build a detector and run inference

import torch
from demus.release import load_detector

# DINOV3_REPO_DIR / DINOV3_WEIGHTS_DIR must be set in the environment.
detector = load_detector("weights/demus_aux_visa_for_mvtec_ad.pt", topk=3)

# A batch of intra-batch images, resized/cropped to 512x512.
images = torch.randn(4, 3, 512, 512, device="cuda")
result = detector.model(images)
print(result.scores.shape)        # image-level scores:  (B,)
print(result.scores_pixel.shape)  # pixel-level maps:     (B, H, W)

Evaluate on a benchmark

detector is a standard detector that plugs into the evaluation loop. With ANOMALY_DATA_ROOT configured:

from pathlib import Path
from data import MVTecAD
from evaluator.evaluation import evaluation_detection
from evaluator.sampler import random_sampler_getter

evaluation_detection(
    Path("results/demus_mvtec_ad"),
    detector,
    MVTecAD(),
    batch_size=4,
    sampler_getter=random_sampler_getter,
    save_anomaly_score=True,
)

This writes per-category metrics (AUROC / AP / pixel AUROC / AUPRO) under the output directory. Use demus_aux_mvtec_for_visa.pt with VisA() to reproduce the VisA row. DeMuS uses batch size 4 for MVTec AD and VisA (8 for Real-IAD), input resolution 512×512, and topk=3.

Method at a glance

  • Frozen backbone + multi-scale adapters. Multi-layer patch tokens from a frozen DINOv3 are refined by an Inception-style trainable adapter (parallel 1×1 / 3×3 / 5×5 convolutions with a residual connection), applied independently for the matching and scoring branches.
  • Stage 1 — Matching Adapter. Trained with explicit "same-part" correspondence supervision (derived from an alignment oracle on auxiliary data) plus distractor pairs, via MSE regression. This removes the anomaly-to-anomaly shortcut and makes matching robust to pose.
  • Stage 2 — Scoring Adapter. Trained to pull normal–normal pairs together and push anomalous pairs apart, again via MSE regression on the same-part candidate set, yielding anomaly-aware features.
  • Inference. For each target patch, retrieve top-k correspondences in each reference via the matching features, softmax-weight them, and aggregate scoring-feature distances over those correspondences. Scores are fused across references by taking the most "normal" explanation.

Citation

@inproceedings{zhao2026demus,
  title     = {DeMuS: Learning Decoupled Matching and Scoring for Batch Zero-Shot Industrial Anomaly Detection},
  author    = {Zhao, Zhengyang and Sun, Hailong and Qi, Binhang and Yu, Hongrui and Wang, Zhongchi and Xu, Hang},
  year      = {2026}
}

License

This code is released under the Apache-2.0 License. The DINOv3 backbone, datasets, and any other third-party assets are subject to their own licenses; obtain and use them accordingly.