Anomalib自定义数据集构建指南:工业异常检测从采集到训练全流程
2026/9/19 8:12:36 网站建设 项目流程

做异常检测这几年,我越来越觉得真正决定模型上限的往往不是模型本身,而是数据集。Anomalib 是 Intel 开源的一套基于 PyTorch 的异常检测工具库,里面集成了 PatchCore、EfficientAD、STFPM、FastFlow 这些主流工业异常检测算法,官方 repo 的 demo 跑起来很容易,但很多朋友试用后反馈:官方示例在公开数据集上效果不错,一换到自己的产品数据就完全不对劲。原因大多不是模型选错了,而是自定义数据集没有按规范构建。

这篇文章我就以 Anomalib 为核心,完整走一遍“从零开始构建自定义异常检测数据集”的流程。内容覆盖数据采集策略、目录结构组织、像素级 mask 标注、训练配置、常见坑点排查。不管你是刚接触工业视觉的新手,还是已经在用 OpenCV 做规则判断、想切到深度学习方案的开发者,这篇都能给你一套可以直接照做的落地方案。

1. 先搞清楚:Anomalib 到底在解决什么问题

1.1 它和普通分类任务的本质区别

大多数计算机视觉任务,比如目标检测、语义分割,核心思路是“教会模型认识每一类东西”。异常检测不一样,它的目标是“教会模型识别没见过的状况”。在工业场景里,缺陷种类往往是无限的:划痕可以有几十种角度、深度、长度,脏污、凹陷、毛刺也千变万化。你不可能把所有缺陷都标注一遍,这既不现实也没必要。

Anomalib 的思路非常聪明,它只让模型见“正常样本”,然后通过重构误差、特征距离、密度估计等手段,把“偏离正常分布”的样本判为异常。这解决了工业落地中的两个核心痛点:

  • 正常样本容易收集,异常样本难得,尤其产线初期良率很高时,缺陷样本屈指可数。
  • 异常类型无法穷举,传统分类模型面对没见过的缺陷形态很容易误判,而异常检测模型天然对“没见过的东西”敏感。

所以我平时和别人聊,都会先确认一件事:你手里是“有大量缺陷样本、需要给缺陷分类”,还是“缺陷样本少、想判断产品是否异常”。前者该去做分类,后者才适合用 Anomalib。

1.2 一个典型的落地场景

假设你在做手机中框外观检测。产线上每天产出几千片,其中绝大多数是良品,偶尔出现划伤、压伤、氧化色差。你不可能让每片都过人工目检,于是想用工业相机抓拍后交给算法判断。

这时候 Anomalib 的用法就很清晰:

  • 用几百张正常中框图像训练,模型记住“正常纹理和轮廓长什么样”。
  • 上线后,每抓拍一张新图,模型计算它和正常分布的偏离程度,超过阈值就报警。
  • 只有报警样本才需要人工复判,甚至可以做二次分类。

这样即使你没积累几天甚至几个月的缺陷图,也能先把“异常报警”这件事跑起来。这也是 Anomalib 这类算法在工业现场最受欢迎的原因:模型不需要见过缺陷,就能把缺陷找出来。

2. 构建自定义数据集之前,先定好训练策略

很多人一上来就拍脑袋拍照、标注,结果数据收集了一周,训练时才发现策略不对。这里我建议你先回答三个问题,再动手。

2.1 你要做图像级检测,还是像素级检测

图像级异常检测,模型只输出“正常/异常”一个结论,告诉你这片产品有没有问题。像素级异常检测(也叫分割级),模型还会额外输出一张热力图,标出哪个区域异常。这个选择直接决定你要不要做 mask 标注,工作量差别很大。

如果只是产线分拣,图像级已经够用。但如果你希望算法帮 QC 定位缺陷位置、辅助复判,或者后续还要接机械臂打磨、剔除,那就必须做像素级。我建议起步阶段先做图像级跑通流程,确认模型能稳定区分异常后,再补标注做像素级,不然项目周期会被标注拖死。

2.2 正常样本多,还是异常样本多

Anomalib 为代表的单类模型(one-class)训练时只用正常样本,异常样本全部留到测试集。这个特性带来一个好处:你可以把大量未标注的产线图像当作“疑似正常”数据直接参与训练,前提是你要确保这些图像里没有重大缺陷。

这里有个分寸要拿捏。如果训练集里混入外观明显的异常图像,模型就会把异常也当成正常,上线后漏报率会很高。我的做法是:第一批训练先用人工严格确认的纯净正常图,等模型能跑了,再从中高置信度判为正常的产线数据里筛一部分增量训练,这样风险小很多。

2.3 你打算在什么硬件上推理

Anomalib 各算法对算力的要求差别很大。PatchCore 效果好,但把训练集所有正常特征都存进 memory bank,显存和内存占用不小,边缘设备部署时要小心。EfficientAD 是专门为工业场景设计的轻量模型,推理速度快,普通工控机也能跑。STFPM 更轻,但几种算法各自适合不同缺陷类型。

我不建议在项目初期纠结最优算法,先用默认配置跑通 PatchCore 拿到一个基线结果,然后再尝试 EfficientAD、STFPM 做对比。数据集如果构建得干净,算法之间差异远比你想象的少,真正决定天花板的是数据质量和标注一致性。

3. 从零动手:数据采集、整理与目录搭建

3.1 拍照环境一致性比数量更重要

很多人在实验室里随便拍了几十张就开始训练,结果模型把“背景反光”当成正常特征,一到产线就崩。工业异常检测对图像采集的一致性要求极高,哪怕你后续用再强的增强也没用,我踩过最大的坑就是光照不一致。

拍照时要注意的几件事:

  • 固定相机位姿、固定产品摆放角度,尽量让每张图的背景保持一致。
  • 光源要稳定,最好用低角度环形光或同轴光,避免环境光干扰。
  • 同一个工位拍照,不要在上午拍一批、下午换了个窗户位置再拍另一批。
  • 产品自身允许有轻微旋转或位移,但不要太大,否则模型会学“位置变化”而不是“纹理正常”。

我曾经试过用网上爬来的同类产品图片补充数据集,结果训练集只有 80 张,但包含五六种不同背景,模型评估时 image-level AUC 只有 0.75,后来重新统一光源拍摄,同样 80 张,AUC 直接到了 0.96。背景、光照的变化对模型影响之巨大,远超很多人的直觉。

3.2 一个 MVTec 风格的自定义数据目录

Anomalib 数据加载器对目录结构有约定。虽然你可以写自定义 Dataset 类,但项目初期最简单的方式是直接套用官方的 MVTec 目录格式,这样配置文件几乎不用改代码,这也是最快能跑通的方式。推荐的目录结构如下:

任意根目录/ └── product_a/ ├── train/ │ └── good/ │ ├── normal_001.png │ ├── normal_002.png │ └── ... ├── test/ │ ├── good/ │ │ ├── good_001.png │ │ └── ... │ └── scratch/ │ ├── scratch_001.png │ └── ... └── ground_truth/ └── scratch/ ├── scratch_001_mask.png └── ...

train 下只有 good 文件夹,这是 Anomalib 约定俗成的“正常样本专用目录”。所有异常样本按缺陷类型分别放在 test 下的不同子文件夹中,比如 scratch、dent、stain。如果你只做图像级检测,ground_truth 目录可以不建;做像素级检测时,则需要在 ground_truth 下建立和 test 一一对应的子目录,每个异常图配一张同名的 mask 图。

这里要特别提醒:不要在 train 目录里搞一个“anomaly”文件夹想混着训练,Anomalib 单类算法不会读它,即便有的教程让你这样放,也只会让训练入口报错。

3.3 各类型数据量该给多少

正常样本越多越好,但不同算法对数量的敏感度不一样。PatchCore 这类基于特征存储的算法,训练样本太少会导致正常类别的特征覆盖不全,测试时大量正常图被误判为异常,也就是假阳性偏高。我的经验值是最少 100 张,200 到 500 张是性价比最高的区间,超过 1000 张边际收益就很有限了。

异常样本则不需要太多。因为异常样本只参与测试和评估,每种缺陷有 20 到 50 张就够计算召回率了。你更应该把精力花在收集不同类型的异常上,而不是堆同一种缺陷的重复样本。很多项目里我反而是花了两三周攒齐缺陷图,正常图一天就能拍完,这个比例很正常。

4. 像素级标注与 mask 数据制作

4.1 mask 的原理与文件要求

像素级异常检测需要 ground truth mask,用来评估模型预测的像素级热力图准不准。mask 是一张和原图尺寸完全相同的单通道 PNG 图,背景为纯黑(像素值 0),异常区域为纯白(像素值 255)。在 Anomalib 的评估流程里,它会把模型输出的热力图和 mask 逐像素比对,然后算 pixel-level AUROC。

我见过不少人在这上面翻车。最常见的问题是用 JPEG 格式保存 mask,导致白色区域边缘出现压缩伪影,像素值不再是严格的 255,变成 237、245、250 这类灰色值。虽然 Anomalib 会做二值化,但边缘不干净会影响评估准确性,所以记住一句话:mask 一律用 PNG,别用 JPEG。

4.2 手工标注工具与操作流程

标注工具有很多,我平时用得比较多的是 LabelMe 和 CVAT。LabelMe 适合小规模数据,单机操作简单,CVAT 更适合团队协作,但搭建要 Docker,略微重。对于只有几百张图的项目,我更推荐先用 LabelMe,因为它能直接导出 JSON,方便写脚本转成 mask。

操作流程大概这样:

  1. 用 LabelMe 打开异常图,沿着缺陷边缘画多边形标注。
  2. 给每个多边形命名,比如叫“defect”,保存 JSON。
  3. 写一个 Python 脚本,读取 JSON 中的多边形点坐标,在空白画布上画一个填充值为 255 的多边形,生成同尺寸 PNG。

这里有个很容易忽略的点:LabelMe 默认保存的坐标是相对于原图的,如果你的脚本里有 resize 步骤,一定要先把坐标缩放,再画 mask,否则 mask 对不上图。我早期犯过这个错,用 resize 后的图片配原图坐标的 mask,模型训练没问题,评估指标惨不忍睹。

4.3 用脚本快速生成 mask 的示例

如果你对代码不陌生,下面这段脚本可以帮你快速把 LabelMe JSON 转成 mask。这里只列核心逻辑,实际用的时候根据文件路径调整一下。

import json import numpy as np import cv2 from pathlib import Path json_path = Path("label/scratch_001.json") img_path = Path("images/scratch_001.png") mask_path = Path("masks/scratch_001_mask.png") # 读取原图尺寸,严格按原图大小生成 mask img = cv2.imread(str(img_path)) h, w = img.shape[:2] mask = np.zeros((h, w), dtype=np.uint8) with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) for shape in data["shapes"]: # shape["points"] 是多边形的顶点列表,每个顶点是 [x, y] pts = np.array(shape["points"], dtype=np.int32).reshape(-1, 2) cv2.fillPoly(mask, [pts], 255) cv2.imwrite(str(mask_path), mask) print(f"mask saved to {mask_path}, size={mask.shape}")

这个脚本适用于大多数常见标注工具导出的几何标注。如果你要标注的是边缘不规则的缺陷,建议多边形点给密一点,但也不要密密麻麻到上万点,否则人工成本太高。实际项目里,我一般建议每个缺陷用 20 到 200 个点包围出来,足够精确即可。

4.4 图像级检测可以跳过 mask

前面提到,如果你只是做“有无异常”的图像级判断,完全不需要标注 mask,test 目录里的异常图和正常图就够训练评估了。Anomalib 在配置里把任务类型设置成 classification 即可,它会自动忽略 ground_truth 目录。

但我要给个建议:哪怕你现阶段不打算做像素级,最好也在异常图像采集时,顺手用画框软件把缺陷位置标一下。这样万一后续模型要升级做定位,数据不用重拍,只需补 mask。别问我为什么这么推,因为我有很多客户都是模型跑通后又回头补标注,这时候原来的图片要重新找出来整理,非常痛苦。

5. 配置文件与训练实战

5.1 Anomalib 版本差异先对齐

Anomalib 更新很快,不同版本的训练命令和配置写法有差异。以目前主流的 1.x 版本为例,训练入口通常是anomalib train命令行,配置文件使用 YAML 格式,指定模型、数据、训练器等参数。如果你还在用 0.x 老版本,入口则是python tools/train.py,配置字段也会略有不同。我建议直接装新版本,旧版很多 API 已经不维护了。

安装这块我就提一句,涉及具体机器环境差异很大。常规做法是创建一个 Python 3.10 的虚拟环境,然后pip install anomalib,它会顺带把 PyTorch、lightning、openvino 等依赖装好。如果你要跑 ONNX 导出或 OpenVINO 推理,再额外装对应组件。装完之后可以用anomalib install补全可选依赖,具体以官方文档为准。

5.2 一个可直接参考的训练配置示例

下面这个 YAML 配置是我在自定义数据集上常用的骨架,以 PatchCore 为例。注意这是示例,具体字段要以你安装的 Anomalib 版本说明为准,但核心思路是一致的。

model: name: patchcore backbone: wide_resnet50_2 layers: - layer2 - layer3 coreset_sampling_ratio: 0.1 normalize: true dataset: name: my_product format: mvtec path: ./datasets/product_a category: product_a image_size: 256 train_batch_size: 16 eval_batch_size: 32 num_workers: 8 task: segmentation trainer: max_epochs: 1 accelerator: auto devices: 1 default_root_dir: ./results

逐个解释一下关键配置的含义:

  • model.name指定算法,PatchCore 是“无训练”的记忆型方法,训练阶段只是提取并存储正常特征,所以max_epochs设为 1 足够。
  • backbone是特征提取网络,wide_resnet50_2精度高但速度偏慢。如果数据简单、希望更快,可以换成resnet18
  • coreset_sampling_ratio是核心采样比例,0.1 表示从全部正常特征中抽出 10% 作为核心集,用来控制内存占用。数据量大或显存不够时,调成 0.05 或 0.02 可以明显减负。
  • dataset.path指向数据集根目录,category指向数据集根目录下的具体类别目录,两者组合后加载器会自动寻找train/goodtest/...等结构。
  • task设成segmentation时会做像素级评估,设成classification则只做图像级评估。如果你没准备 mask,一定记得改这个字段,否则评估阶段会因为找不到 ground truth 报错。

5.3 开始训练与结果解读

配置写好后,执行训练命令:

anomalib train --config config.yaml

训练过程中可以打开 TensorBoard 看 loss 曲线和样本可视化结果。PatchCore 本身没有梯度更新过程,它是在跑特征提取和存储,所以 loss 曲线参考意义不大,重点看 validation 阶段的 anomaly map 可视化输出是否能把真实缺陷区域标亮。

训练结束后,Anomalib 会在./results目录按数据集、模型、时间戳生成结果文件夹,里面有模型权重、指标文件、测试集的可视化图。我最关心的几个指标是:

  • image-level AUROC:衡量“整张图有没有异常”的区分能力,越接近 1 越好,工业场景下 0.95 以上才比较安心。
  • pixel-level AUROC:衡量热力图定位能力,一般会比 image-level 低一些,0.9 左右已经很不错。
  • F1-max:用不同阈值算出来的最高 F1,实际布署时会参考它来确定报警阈值。

实测经验是:如果 image-level AUROC 只有 0.8 左右,先不要急着换模型,先去看检测错误的样本,大概率是正常样本形态变化太大,或者光照不一致,先把数据问题解决,AUC 往往能涨一大截。

5.4 快速验证异常发生的检测效果

训练完之后,最直观的验证方式是把一批新拍的产品图像丢给模型做推理。Anomalib 1.x 的推理命令很简单:

anomalib predict --model patchcore --weight ./results/patchcore/product_a/weights/model.pt --input ./inference_images

模型会对每张输入图输出一个异常分数,同时保存可视化热力图。你要做的是把异常分数阈值定下来:用测试集里的正常图算出分数分布,取一个能保证“正常品不报警”的阈值,再拿异常图验证这个阈值下缺陷能召回多少。这个阈值就是以后产线报警用的,可以在配置文件或后处理里固化下来。

需要提醒一点,很多模型在测试集上指标很好,一到现场就开始乱报。原因往往是现场图像和训练集分布有偏移。所以我强烈建议,训练完成后专门去现场采集一小批“没见过的正常图”做盲测,如果假阳性过高,就把这些图加入训练集做增量训练,这个步骤比调参重要得多。

6. 常见问题与排查技巧实录

6.1 训练很快结束,但评估分数很低

很多第一次用 PatchCore 的人,看到训练几十秒就结束,会觉得不靠谱。其实这是正常的,PatchCore 训练阶段只做特征提取,不做反向传播,几十秒到几分钟都算正常。评估分数低,我看下来主要是两类原因:

  • 正常样本太少了,模型没看过足够多样的正常纹理,导致任何一点正常波动都被当成异常。解决方法:加到 200 张以上,并确保包含产线正常状态下各种允许的波动。
  • 测试集正常图与训练集正常图分布不一致,比如测试集里产品角度、光照发生了变化。解决方法:严格统一采集条件,或者把这些变化纳入训练集。

6.2 所有异常图都报“异常”,但正常图也误报

如果正常图也开始大面积误报,大概率是正常样本覆盖不够。工业产品即使良品,也会存在纹理差异、轻微位置偏移、边缘反光变化,如果训练集里只包含了其中一种形态,模型就会把其他形态判为异常。

我的处理方式是把正常样本按“形态”分组:不同工位、不同批次、不同表面状态分别拍一批,混合进训练集。这样模型学到的“正常”才是一个带容差的范围,而不是一张模板。这是短期内提升效果最有效的手段,比换任何算法都有用。

6.3 mask 评估报错或指标异常

像素级评估时,如果报错说找不到 mask,先检查task是否配成segmentation,以及ground_truth目录下的子目录名是否和test下的异常子目录名完全一致。比如 test 下是scratch,ground_truth 下也得是scratch,mask 文件名要和异常图文件名一模一样。

如果指标异常低,比如 pixel-level AUROC 只有 0.5 到 0.6,大概率是 mask 和原图尺寸不一致,或者 mask 里异常区域用灰色而非纯白色。可以用脚本统计一下 mask 的像素值分布,正常情况应该是 0 和 255 两种值,中间值越少越干净。

6.4 显存或内存不够

PatchCore 对资源的要求偏高,特征存储量会随训练图数量和分辨率增长。如果显存报 OOM,优先降image_size,从 256 降到 224 或者 192 往往就能解决,精度损失通常可以接受。其次是调低coreset_sampling_ratio,比如从0.1降到0.05,memory bank 会明显变小。

还有一个容易被忽略的点:num_workers 太高也会吃内存,训练数据量大时把num_workers降到 4 或 8,可以缓解内存压力。如果还是不够,换 EfficientAD 或 STFPM,它们的内存占用比 PatchCore 小很多。

6.5 疑难问题速查

问题现象大概率原因处理建议
训练找不到数据集path / category 配置不对检查目录是否包含 train/good,路径是否为绝对路径或相对当前工作目录正确
评估时找不到 ground truthtask 配置错误检查 task 是否应为 classification,或 ground_truth 目录是否缺失
正常图误报率高正常样本多样性不足增加不同产线状态下的正常图到训练集
异常图漏报率高异常特征和正常特征太接近提高图像分辨率,或尝试 EfficientAD / FastFlow
mask 错位坐标未随 resize 缩放确保脚本先缩放坐标再填充 mask
部署推理慢backbone 太大换轻量 backbone,或导出 OpenVINO / ONNX 加速

6.6 最后分享一点我的个人习惯

我从接触 Anomalib 到现在,最大的体会有两条。第一条是“数据洁癖”很重要,每张图从采集到入训练集,都要记录拍摄条件、产品型号、缺陷类型,这样后期排查问题才能快速定位。第二条是“第一次跑通流程先不过度优化模型”,用一个默认配置,跑出基线结果,再根据错误样本决定是加数据、调预处理还是换模型,让数据驱动决策,而不是靠感觉。

另外我习惯在训练完成后,把测试集里所有误判样本单独导出到一个文件夹,按正常误报、缺陷漏报分类。每隔一段时间翻一翻,你会发现很多规律,比如某种纹理总是被误报、某种光照下缺陷总是漏检,这些发现比单纯调参带来的收益大得多。Anomalib 的完整工作流还支持 OpenVINO 导出、API 服务化部署,等你自定义数据集跑通之后,可以再往这些方向扩展。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询