简介:针对图像缺陷检测任务打造的完整Python实现包,面向本科与硕士阶段进行计算机视觉、工业质检相关教研学习的学生。内容以Defect Eye缺陷检测为主线,覆盖数据示例、模型推理、评估验证等环节。压缩包共227个文件,以py源码为核心,配合jpg样例图像、md说明文档,以及少量c/cpp/pyx扩展模块和ckpt预训练权重,整体约49.66MB,目录结构便于按检测、评估、工具脚本分类查阅。其中包含mask、bbox、nms等检测相关实现和pycocoDemo等COCO风格评估示例,可直接用于理解缺陷区域标注、候选框处理与结果评估流程。已有657人学习,适合需要动手复现缺陷检测项目、参考完整代码组织方式的研究者。
1. Defect Eye 缺陷检测:一套把“框住缺陷”升级成“抠出缺陷轮廓”的 Python 工程
做工业缺陷检测的朋友大多经历过这种尴尬:拿公开的 YOLO 模型去检测钢材表面划痕,框是框住了,但轮廓歪得没法用,因为缺陷边界本身就是不规则的。Defect Eye 是一套基于 Mask R-CNN + ResNet-101 的缺陷检测 Python 代码包,自带预训练权重 resnet_v1_101.ckpt,外加 pycocoDemo / pycocoEvalDemo 两个验证入口,解决的是“既要框住缺陷、又要像素级分割缺陷轮廓”这一类任务。适合刚入缺陷检测方向的硕博生,也适合工厂里想快速验证 Mask R-CNN 方案能否落地的算法工程师。我先说一个反直觉结论:这套资源常被归到 matlab 图像处理分享里,解压后却全是 .c、.ckpt、.ipynb 文件,是标准的 Python 工程,按 Python 环境去搭才能跑通。
2. 选型与结构:Mask R-CNN + ResNet-101 + COCO 评测,这份代码到底由什么组成
2.1 为什么缺陷检测场景首选 Mask R-CNN,而不是 YOLO 或 FCN
工业缺陷检测的麻烦在于“不知道边界在哪”。表面划痕、麻点、凹坑、脏污,形态不规则,很多缺陷只有几百个像素,放到 512×512 的图像里就是几个点。YOLO 系列检测器虽然部署友好,但输出只有矩形框;框住缺陷很容易,框住“缺陷的实际占据区域”却做不到。后续要做缺陷面积统计、方向判断、等级分类时,矩形框给出的面积误差能到 30%~50%,对焊接气孔、硅片划痕、轴承滚道凹坑这类缺陷来说,这个误差不可接受。
Mask R-CNN 在 Faster R-CNN 的检测分支基础上加了一条并行的掩膜分支:先通过 RoIAlign 把每个候选框内的特征对齐到固定尺寸,再输出 14×14 或 28×28 的二值掩膜,上采样到原图尺寸后与真值掩膜做损失。训练时整体是一个多任务损失,公式大致是 L = L_cls + L_box + L_mask。L_cls 负责判断框内缺陷类别,L_box 用 smooth-L1 回归修正框位置,L_mask 用逐像素二分类损失保证缺陷轮廓准确。这套设计天然适合缺陷检测:框负责召回区域,掩膜负责精确表达边界。
另一个原因和“实例数量”有关。像电子元件引脚表面,十几个瑕疵可能挤在一个连通区域内,语义分割(FCN、U-Net)会把它们当成一个整体,无法区分每个独立缺陷;Mask R-CNN 的掩膜是 instance-level 的,能输出每个缺陷的独立掩膜,方便后续按单个缺陷编号统计。这正好命中缺陷检测最常被问的一个问题:同一区域内多个缺陷怎么分组,怎么按缺陷逐个做面积和周长统计。
那为什么用 ResNet-101 而不是 ResNet-50?代码包里的 resnet_v1_101.ckpt 基本说明了一切。更深网络配合 FPN 做多尺度特征融合,对小目标召回一般比 ResNet-50 好;代价是显存和推理时间。产线相机分辨率高时,1280×1024 的输入在 ResNet-101 下显存压力非常大。我的习惯是验证阶段用 ResNet-101 出精度,落地阶段换 ResNet-50 做裁剪和量化,两套权重分开维护。
2.2 代码包文件地图:从 maskApi 到 resnet_v1_101.ckpt 各管什么
解压后的文件列表很有代表性,基本就是 Mask R-CNN 官方工程和 COCO PythonAPI 的常见组合。我整理了一张文件职责表:
| 文件 | 类型 | 在缺陷检测里负责什么 |
|---|---|---|
| resnet_v1_101.ckpt | 预训练权重 | ResNet-101 骨干初始权重,在 ImageNet 上预训练,迁移到缺陷特征 |
| pycocoDemo.ipynb | 演示 Notebook | 加载训练好的模型,对单张图像做推理并可视化 mask 结果 |
| pycocoEvalDemo.ipynb | 演示 Notebook | 按 COCO 指标算 mAP,评估模型在验证集上的效果 |
| maskApi.c / maskApi.h | C 源码 | 掩膜的 RLE(Run-Length Encoding)编解码,压缩存储缺陷掩膜 |
| _mask.c | Cython 扩展 | 把 maskApi 封装成 Python 接口,训练时每个 step 都要调用 |
| bbox.c | C 源码 | COCO 格式包围框的计算与转换 |
| nms.c | C 源码 | 非极大值抑制的 CPU 实现,抑制同一缺陷上的重复框 |
| gason.cpp / gason.h | JSON 解析器 | 读取 COCO JSON 标注文件,解析类别、多边形、bbox 字段 |
先说结论:这几个 C 文件不是给人肉看的,是为了性能存在的。一次训练里一个 batch 几十张图,每张图几百个候选框,每个候选框的 mask 都要做 RLE 编解码;如果用纯 Python 处理,单个 epoch 会多出几十分钟训练时间。maskApi.c 的作用就是把二值掩膜压缩成 RLE 字符串存进 JSON,训练时再解压回 mask。bbox.c 和 nms.c 同理,都是频繁路径上的性能优化。
gason.cpp 是一个轻量 JSON 解析器,COCO 格式的标注文件常常到几十 MB,解析速度直接决定数据加载耗时。gason.c 的代码量很小,读起来不费劲,但它不是某个隐藏框架,只是 PythonAPI 自带组件。这份工程在数据处理流上很清楚:COCO JSON 标注 → gason 解析标注 → maskApi 解码 RLE 掩膜 → 喂给 Mask R-CNN 模型训练。我实际调试时就是按这个链路排查问题,基本能定位到具体环节。
提示:pycocoDemo 和 pycocoEvalDemo 都是基于 COCO 数据集的经典演示入口,很多缺陷检测项目把它们保留下来当验收脚本。不是每个文件都需要二次开发,先跑通再改。
2.3 环境兼容矩阵:Python / TensorFlow / Cython 的版本配对
这套工程不是“最新版本安装即用”的现代项目,而是 TensorFlow 1.x 时代的产物,环境版本卡得比较死。以下是我在复现时确认可行的组合:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| Python | 3.7.9 | 3.8+ 编译 pycocotools 时坑多,3.10+ 容易直接编译失败 |
| TensorFlow | 1.15.0 GPU 版 | resnet_v1_101.ckpt 是 TF1 checkpoint 格式 |
| Keras | 2.3.1 | Defect Eye 的模型层依赖这个版本 |
| Cython | 0.29.24 | 新版 Cython 对 .pyx 语法放宽,反而容易出兼容问题 |
| numpy | 1.19.5 | 1.24 以上删了部分 dtype API |
| pycocotools | 2.0.2 | pip 装通用版本即可,源码目录里有 build_ext 也可以 |
我的建议是直接用 conda,单独建一个环境,别在系统环境里硬塞,否则大概率在导入阶段纠缠半天:
conda create -n defect python=3.7.9 -y conda activate defect pip install tensorflow-gpu==1.15.0 keras==2.3.1 cython==0.29.24 numpy==1.19.5 pip install opencv-python pillow matplotlib pycocotools==2.0.2这里每一行都有用意:TensorFlow 1.15 对应 CUDA 10.0 / cuDNN 7.6,装新版本 CUDA 反而跑不起来;Keras 必须 2.3.1,因为模型代码用到了当时旧 Keras 的 API,新 Keras 会直接报 cannot import name;Cython 锁 0.29 是保险,更高版本在 Python 3.7 下也能编,但没必要冒险。opencv 是后面做数据增强、画掩膜轮廓要用的,图像处理项目绕不开这一点。pycocotools 直接走 pip,如果 pip 装出来的包和你手头的 PythonAPI 源码不符,再用本地目录编译。
还有个我差点漏掉的细节:pycocoDemo.ipynb 打开后很可能选不中刚建好的 conda 环境,需要补一个 kernel 注册:
pip install ipykernel python -m ipykernel install --user --name defect --display-name "defect-tf1"这一步不是可选项。很多人解压后在 jupyter 里打开两个 ipynb,Kernel 菜单里根本没有刚建的环境,就是少了这个注册步骤。两个 Notebook 都是按 jupyter kernel 名匹配环境的,名字对不上就会用默认 kernel,加载 TensorFlow 1.15 直接失败。
3. 把 Defect Eye 跑起来:数据、训练、推理三步走
3.1 数据准备:从 labelme 标注到 COCO JSON 格式转换
Mask R-CNN 训练需要的不只是框,还有每个缺陷的多边形或掩膜。最常用的标注工具是 labelme,多边形标注后保存为独立 JSON;但模型框架吃的是 COCO 格式数据集目录,所以第一步是转换。COCO 标注文件有三个顶层字段:images 记录每张图的 id、宽高、文件名;categories 记录缺陷类别名称和 id;annotations 记录每个缺陷实例,包含 image_id、category_id、多边形 segmentation、bbox、area。
下面这个转换脚本是我常用的核心逻辑,负责把 labelme 的多边形转成 COCO 需要的 segmentation 和 bbox:
import json from pycocotools import mask as mask_utils def polygon_to_coco_item(image_id, annotation, height, width): # annotation["points"] 是 labelme 里的多边形坐标 poly = [float(x) for pt in annotation["points"] for x in pt] # frPyObjects 输入要求坐标展平,顺序是 [x0, y0, x1, y1, ...] rle = mask_utils.frPyObjects([poly], height, width) rle_merged = mask_utils.merge(rle) # 统一编码成 COCO 推荐的 RLE segmentation seg = mask_utils.encode(mask_utils.decode(rle_merged)) bbox = mask_utils.toBbox(seg).tolist() return { "image_id": image_id, "category_id": annotation["category_id"], "segmentation": seg, "bbox": bbox, "area": float(mask_utils.area(seg)) }这段代码里 frPyObjects 的输入要求是“多边形坐标展平后的列表”,每个点必须按 [x0, y0, x1, y1, ...] 顺序排列,漏掉 float() 转换在部分 pycocotools 版本下会报 cannot convert to RLE。merge 是把同一个缺陷的多段多边形合并成一块,避免一个断开的标注被当成多个实例。bbox 用 mask_utils.toBbox 从掩膜计算,比用多边形顶点极值更准确,因为掩膜已经在图像坐标系里对齐过了。
数据目录建议按官方工程惯例组织:
datasets/ defect_train/ train/ image_001.jpg image_002.jpg val/ image_003.jpg train.json val.jsontrain.json 与 val.json 里 categories 要统一,比如 {1: "scratch", 2: "pit"}。一个小坑:COCO 的 image id 必须全局唯一,如果 train 和 val 各自从 0 开始编号,后面 pycocoEvalDemo 出的 mAP 会串数据。我的习惯是所有图像 id 用文件名 hash 生成,宁可多占几个字节也不怕重。
3.2 训练配置与权重加载:预训练权重、anchor 参数、batch size 怎么设
工程里需要一个继承 Config 的类,核心参数如下:
class DefectConfig(Config): NAME = "defect" NUM_CLASSES = 1 + 2 # 背景 + 划痕 + 凹坑 IMAGE_MIN_DIM = 512 IMAGE_MAX_DIM = 1024 BATCH_SIZE = 2 # 8GB 显存上限,16GB 可以到 4 STEPS_PER_EPOCH = 500 VALIDATION_STEPS = 50 RPN_ANCHOR_SCALES = (16, 32, 64, 128, 256) LEARNING_RATE = 0.001NUM_CLASSES 必须是 1 + 缺陷类别数,这个 1 是背景类,忘掉就等着模型把所有像素都当缺陷。IMAGE_MIN_DIM 和 IMAGE_MAX_DIM 决定图像缩放上限,工业相机 1280×1024 的图会被压到最大 1024,但小缺陷可能因此缩没了;所以批次里应保留原图分辨率,并在验证时不要缩小。RPN_ANCHOR_SCALES 默认对齐 COCO 的大目标统计,缺陷场景常需要把最小锚点降到 8,甚至 4,否则小划痕在特征图上只有几个像素,正样本都采不到。
权重加载分两步。第一步加载预训练骨干:
model = MaskRCNN(mode="training", config=config, model_dir="./logs") model.load_weights("resnet_v1_101.ckpt", by_name=True)第二步训练时注意冻结策略:
model.train(train_dataset, val_dataset, learning_rate=config.LEARNING_RATE, epochs=40, layers="heads") # 先只训练 RPN 和检测头 model.train(train_dataset, val_dataset, learning_rate=config.LEARNING_RATE / 10, epochs=100, layers="all") # 解冻全部 backboneby_name=True 是关键:ckpt 里只有骨干变量,不匹配 head 变量是正常的,它会按变量名把 ResNet 卷积和 BN 层填进来。layers="heads" 阶段学习率保持 0.001,等 head 收敛后再以 0.0001 解冻 backbone,否则 backbone 老权重被大步长打乱,loss 会先涨一波。显存只有 8GB 时,BATCH_SIZE 建议直接设为 1,STEPS_PER_EPOCH 相应加到 800,不然训练后期基本必 OOM。
训练中间也可以加一点随机增强,比如左右翻转、旋转 90 度、随机亮度扰动。对于钢材表面划痕这类方向性较强的缺陷,翻转要谨慎,横着和竖着的划痕可能不是同一类;而螺栓表面缺陷这类旋转不变性强的样本,增强空间可以大一点。这个差异会在验证集 AP 上表现得非常明显。
3.3 训练与推理:从加载权重到 pycocoDemo 的完整链路
训练日志要盯三个量:总 loss、rpn_bbox_loss、mrcnn_mask_loss。总 loss 稳但 mask loss 不动,说明掩膜分支没学到东西,多半是标注里 segmentation 为空或全部是背景。交互式 Notebook 里,模型加载与推理代码是这样的:
from mrcnn.model import MaskRCNN model = MaskRCNN(mode="inference", config=config, model_dir="./logs") model.load_weights("mask_rcnn_defect_0100.h5", by_name=True) results = model.detect([image], verbose=1)[0] for i, score in enumerate(results["scores"]): if score > 0.5: mask = results["masks"][:, :, i] # 布尔掩膜 class_id = results["class_ids"][i] roi = results["rois"][i] # [y1, x1, y2, x2]detect 返回的 rois 顺序是 y1, x1, y2, x2,不是 x, y, w, h,很多人画框时把 x 和 y 换错,看到 mask 和框错位才回头查。masks 的第三个维度是实例索引,每张图最多输出的实例数就是这个维度长度。score 默认阈值 0.7,工业场景我一般降到 0.5 以保召回。调试阶段建议顺手把 mask 叠加在图像上用 opencv 画出来看看,肉眼比指标更容易发现方向性问题:
import cv2 vis = image.copy() for i, score in enumerate(results["scores"]): if score < 0.5: continue m = results["masks"][:, :, i].astype("uint8") * 255 contours, _ = cv2.findContours(m, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cv2.drawContours(vis, contours, -1, (0, 255, 0), 2)cv2.findContours 把布尔掩膜转成轮廓,再用 drawContours 画出轮廓线,这是 opencv 图像处理项目里很标准的可视化套路。如果画出来的轮廓和标注图对不上,先检查数据转换,再怀疑训练。推理阶段全部就绪后,下一步就是用 pycocoEvalDemo 算指标。
4. Defect Eye 避坑实录:5 个我在复现时踩过的坑
4.1 坑一:resnet_v1_101.ckpt 加载失败,提示 Unknown opcode
现象:代码执行到 model.load_weights("resnet_v1_101.ckpt", by_name=True) 时直接抛 ValueError,错误类似 Unknown opcode 或 No variables to save;也有人遇到 checkpoint 读取后所有变量列表为空。
原因:resnet_v1_101.ckpt 是 TensorFlow 1.x 保存的 checkpoint 文件,内部记录的计算图 opcode 与 TensorFlow 2.x 不兼容。TF 2 用 compat.v1 模块读取也可能因为变量作用域命名不一致而找不到匹配项。还有一个隐含原因:checkpoint 只有数据没有图结构,必须等模型定义完成后才能 load,而模型定义依赖 config,config 里 NUM_CLASSES 改错也会导致变量名不匹配。
解决:先确认环境是 TF 1.15 而不是 TF 2.x。如果必须留在 TF 2,则要先把 ckpt 转成 h5,转换思路是遍历 checkpoint 变量名,逐个赋给 Keras 模型,不同工程的导出姿势略有差异。实际操作中我绕过转换,直接把整个训练流程都放在 TF 1.15 环境里,反正工业项目也不需要依赖 TF 2 的新特性。还有一个排查偏方:先用脚本列出 checkpoint 里所有变量名,检查 backbone 变量的前缀是不是 resnet_v1_101/conv1/...,如果前缀不对,说明这个 ckpt 是从别的 ResNet 变体重命名的,加载时就要做变量名映射。
python -c "from tensorflow.python.training import checkpoint_utils; print(len(checkpoint_utils.list_variables('resnet_v1_101.ckpt')[0]))"4.2 坑二:编译 maskApi.c 时报错找不到 gason.h 或头文件冲突
现象:在 PythonAPI 目录下跑 python setup.py build_ext --inplace 报错 fatal error: gason.h: No such file or directory,或者编译通过但 import pycocotools 失败,提示 undefined symbol。
原因:cocoapi 官方源码里 gason.cpp 与 gason.h 在 common/ 子目录,而 setup.py 的 include 路径没有加 common/;旧版 cocoapi 对 Python 3.7 以上版本兼容性也不好。更常见的是下载到的压缩包本身缺了 gason.h,只保留了 gason.cpp,自然编译不过。
解决:先确认源码完好,从 cocoapi 源码包把 common/gason.h 补进对应目录,然后统一执行编译:
cd coco/PythonAPI export CFLAGS="-I./common" python setup.py build_ext --inplace编译完成后一定要在 Python 里做一次冒烟测试, import pycocotools 并执行一次 RLE 编解码,确认扩展库真的生效。还有内存对齐问题:新版 gcc 默认优化级别在某些平台下会导致 nms.c 的行为变怪,表现是同一输入两次运行结果不同,这时候把优化级别降到 -O0 再编一次。Windows 下没有完整 Visual Studio 工具链的直接用 WSL 或者 MinGW,别在第三方源里碰“预编译 wheel”,版本签名对不上反而多耗几个小时。
4.3 坑三:训练 loss 卡在 1.8 附近不降,mAP 全为 0
现象:训练 50 个 epoch,总 loss 一直在 1.8~2.0 之间震荡,验证集 mAP 始终是 0,检测结果全是空白框,或者只输出背景。
原因:最常见的是正样本太少。缺陷尺寸相对整图极小,默认 RPN_ANCHOR_SCALES 从 32 起跳,小缺陷的正样本在锚点上几乎没有命中。第二个原因是类别不平衡严重,划痕类有 1000 个实例、凹坑只有 50 个,模型退化成把缺陷都往划痕类上贴。第三个原因是被我耽误最久的:标注里 segmentation 字段为空,只在 bbox 里给了框,掩膜分支没有真值可学,mrcnn_mask_loss 永远得不到下降信号。
解决:第一个动作先做可视化核查,直接把标注掩膜画在图上:
import cv2, json from pycocotools import mask as mask_utils anns = json.load(open("data/train.json"))["annotations"] for ann in anns: m = mask_utils.decode(ann["segmentation"]) cv2.imwrite(f"mask_{ann['image_id']}_{ann['id']}.png", m * 255)确认掩膜不是全黑也不是全白。如果掩膜区域太小,说明 anchor 尺度要往下调:把 RPN_ANCHOR_SCALES 改为 (8, 16, 32, 64, 128),并在训练数据里做随机裁剪,让缺陷占比变大。类别不平衡则给少数类加权,实践做法是在损失加权项里把 pit 类的 loss 权重乘 2~5。做了这些还没起色,就检查是不是 backbone 解冻太早导致特征被破坏,把 layers="all" 之后的学习率从 0.001 直接降到 0.0001,再多跑 20 个 epoch。mAP 全 0 还有一种常见路径:验证集和训练集的类 ID 没有对齐,COCO 的 category_id 从 1 开始,而某些脚本里用的是 0-index,两个数据集类编号对不齐,评估时所有预测被标成未知类。
4.4 坑四:检测结果同一缺陷出现十几个重叠框,NMS 等于没做
现象:推理结果里,一个真实缺陷被输出十几个不同大小的方框和掩膜,置信度从 0.9 到 0.4 都有。肉眼看着是同一块区域,但模型就是一股脑全给出来。
原因:NMS 没有正常执行。常见有两层:一是 nms.c 编译失败,模型代码里 import 不到自定义 NMS,就退回一个极弱的后处理;二是 NMS 的 IoU 阈值默认 0.3 太严,工业图像背景干净、缺陷框之间重叠度极高,0.3 会把同一缺陷的多个 proposal 全部保留,等于没有抑制。第三种是被忽略的场景:mask 分支叠加在多个 proposal 上,每个 proposal 都有自己的 mask,最终结果是多份掩膜叠加,而不是去重后的唯一输出。
解决:先确认 NMS 是否真的生效,在推理脚本里加一个调试点,打印 proposals 数量和经过 NMS 后的数量:
from mrcnn.utils import apply_nms keep = apply_nms(proposals, scores, 0.5) print(f"before: {len(scores)}, after: {len(keep)}")如果 before 和 after 完全一样,说明 NMS 函数没有接入正路径。然后按实际重叠度调阈值:缺陷本身有粘连时,把 NMS IoU 从 0.3 放宽到 0.5~0.6,能保留不同缺陷但过滤同一缺陷。还有一个容易忽略的参数是 DETECTION_MIN_CONFIDENCE,默认 0.7;如果我降到 0.3 去追召回,NMS 就要面对大量低分框。经验是先 NMS 后阈值:要么先把置信度压到 0.5 再进 NMS,要么让 NMS 按 confidence 排序后直接截断数量。COCO 评测里的 maxDets=100 也是 NMS 之后最多保留 100 个结果,别指望它替你清理重叠框。
4.5 坑五:CUDA OOM,batch size 调到 1 仍然显存不足
现象:训练启动后报 CUDA_ERROR_OUT_OF_MEMORY,或者运行到某一步突然卡死,控制台输出 Resource exhausted: OOM when allocating tensor with shape。有人把 BATCH_SIZE 改成 1 还是不够。
原因:ResNet-101 特征图大,Mask R-CNN 的 FPN 又保留多层特征图,显存占用是 YOLO 的好几倍。一个容易被忽视的显存黑洞是 IMAGE_MAX_DIM:设成 1024 时,FPN 多层特征图加上 RPN proposal 的 ROI 特征,一张图就可能吃掉 3~4 GB,batch=1 也不能兜底。
解决:显存预算要按特征图尺寸算,而不是只调 batch size。先把图片上限降下来:
IMAGE_MIN_DIM = 480 IMAGE_MAX_DIM = 640 RPN_ANCHOR_SCALES = (8, 16, 32, 64, 128)这种配置下缺陷占比变大,小目标锚点也够用,整体显存能压到 2GB 以内。训练脚本入口处加显存自适应的代码,能兜住不同显卡:
import tensorflow as tf from tensorflow.compat.v1 import ConfigProto config = ConfigProto() config.gpu_options.allow_growth = True sess = tf.Session(config=config)allow_growth 让 TensorFlow 按需吃显存,不再一次性把整张卡占满。TF 2 则换 tf.config.experimental.set_memory_growth(gpus[0], True)。再不行就按通道裁剪 FPN 深度,从 256 降到 128,mAP 会掉一点但训练能跑起来。撞过这面墙之后,我的习惯是:新数据集第一次训练,闭眼先用 640 上限把链路跑通,再逐步加到 1024,绝不一上来就挑战最大分辨率。
5. 进阶:用 pycocoEvalDemo 验证 mAP、按阈值反推改参,再做 ROI 裁剪推理
5.1 pycocoEvalDemo 的四个关键参数:iouType、maxDets、areaRng、置信度
pycocoEvalDemo 的评估入口是 COCOeval,初始化时最容易调错的是 iouType。iouType="segm" 按掩膜 IoU 评估,对缺陷轮廓边界的轻微偏移非常敏感;iouType="bbox" 只看包围框,评估结果更乐观。我的习惯是两个都跑,中间差太多就说明轮廓精度不够。maxDets 一般设 [1, 10, 100],缺陷密度高时 10 和 100 的差距能直观反映漏检。areaRng 控制按面积范围过滤,针对微小缺陷要单独看小面积区间的 AR,否则被大缺陷的平均值掩盖。
from pycocotools.coco import COCO from pycocotools.cocoeval import COCOeval coco_gt = COCO("data/val.json") coco_dt = coco_gt.loadRes(predictions_file) coco_eval = COCOeval(coco_gt, coco_dt, "segm") coco_eval.params.maxDets = [1, 10, 100] coco_eval.evaluate() coco_eval.accumulate() coco_eval.summarize()loadRes 需要 predictions 文件,每项包含 image_id、category_id、score、segmentation、bbox。如果预测里少了 bbox,在 bbox 模式下会直接 KeyError。这一步别偷懒,把 mask 用 mask_utils.encode 转成 RLE 再放进去,否则 loadRes 报 segmentation 无法解析。
5.2 按 mAP 反推阈值,决定要不要砍掉 Mask 分支
mAP 在 0.5 以下很正常,先看 PR 曲线:AP@0.5 正常但 AP@0.75 直接腰斩,说明掩膜轮廓偏离真值在 25% IoU 附近徘徊,问题在掩膜分支而不在框分支;所有阈值都低,就从 NMS 阈值和 DETECTION_MIN_CONFIDENCE 查起,先提置信度到 0.8 压掉低分误报,再放开 NMS 到 0.6 让粘连缺陷得以区分。这里有一个“砍分支”的决策可做:如果业务只要缺陷框不要轮廓,就在推理阶段跳过 mask 头,只保留 RPN 和检测头,推理速度大约快 30%。
5.3 工业落地:ROI 裁剪后再推理,把单张耗时压下来
测试阶段最实际的经验是别让 Mask R-CNN 直接吃整张高分辨率图。工业相机动不动 500 万像素,全图推理单张一秒钟以上。我的做法是按正方形滑动窗口裁图,重叠率 25%,先在小窗口上推理,再把 mask 按坐标偏移贴回原图:
def crop_infer(model, image, crop_size=512, stride=384): h, w = image.shape[:2] masks = np.zeros((h, w), dtype="uint8") for y in range(0, h - crop_size + 1, stride): for x in range(0, w - crop_size + 1, stride): crop = image[y:y+crop_size, x:x+crop_size] r = model.detect([crop], verbose=0)[0] for i in range(len(r["masks"][0])): m = r["masks"][:, :, i] masks[y:y+crop_size, x:x+crop_size][m] = 255 return masksstride 比 crop_size 小,让相邻窗口有重叠。重叠区同一缺陷可能被重复检测,合并时以分数最高的实例为准,mask 重叠 IoU 超过 0.5 就取并集;边界处面积占比低于 30% 的碎片掩膜直接丢弃。从那以后,我每次复现一个视觉检测项目,第一件事就是按“ckpt 加载 → 扩展编译 → NMS 生效 → 显存预估 → 小窗验证”的顺序强制走一遍自检流程,这也是我把 5 个坑沉淀成固定动作的习惯。希望帮到你。
本文还有配套的精品资源,点击获取