做工业视觉这几年,我越来越觉得,很多质检问题不是画个框就能解决的。腰果质量分级就是一个典型。腰果表面上的裂纹、霉斑、黑点,形状千奇百怪,有的细得像一条线,有的是一整片不规则暗区。你要判断一个果仁到底该归为A级还是B级,光知道“框里有个目标”没有任何意义,你得知道缺陷长什么样、有多大面积、边界在哪里。这也是我们最终选择基于mask-rcnn_hrnetv2p-w32-1x_coco这套配置来做实例分割,而不是简单跑一个目标检测模型的原因。
这篇文章不打算照着文档复述,而是想把我从数据采集、标注、训练、评估到上线的完整过程整理一遍,把踩过的坑和试错后的经验写出来。适合正在做食品外观质检、农产品分级,或者准备用实例分割落地细分缺陷检测场景的朋友参考。哪怕你没碰过腰果,里面的选型逻辑、标注方法、训练调参和部署注意事项,也可以直接迁移到类似的视觉项目上。
1. 项目概览与需求拆解
1.1 腰果质量分级究竟难在哪
腰果加工线跟很多食品产线一样,原料进来之后要先经过外观筛选。外壳脱掉之后,果仁本身会出现各种情况:完整的、半瓣碎的、表面带黑斑的、有裂纹的、放久了发霉的,甚至被虫蛀出小孔的。不同客户对等级要求不一样,有的按缺陷数量,有的按缺陷面积占比,有的按是否允许碎片。
传统的方案就是人工目检。传送带旁边坐着工人,眼睛盯着往下过的腰果,看到坏的挑出来。但腰果这种物料表面是浅黄色带纹理的,裂纹和自然皱皮经常混在一起,靠人眼长时间盯,疲劳之后漏检率会明显上升。而且质检标准很容易带主观性,同一个果仁,上午的班次可能判成B级,下午的班次可能就变成C级了。
工业上要解决的是三件事:第一,能不能稳定找出每一颗腰果的缺陷,并说清楚缺陷类型;第二,能不能根据缺陷面积或数量自动给出等级判断,把“人说了算”变成“规则说了算”;第三,能不能跟上产线节拍,在几毫秒到几十毫秒内完成判断,带动执行机构把不合格品剔除掉。
要在算法层面满足这三点,只给目标框是远远不够的。
1.2 为什么实例分割比目标检测更适合腰果质检
先看一个实际问题。某一次测试里,我们先用一个普通的目标检测模型去识别腰果上的霉斑,检测框画得也挺准,但做分级的时候发现完全没法用。原因很简单:霉斑是长条状或不规则片状,检测框给的是一个矩形范围,框里既有霉斑也有正常果仁表面。如果按这个框的面积去算霉变占比,结果会严重偏大,一批明明只有一点点霉斑的A级腰果会被误判成不合格。
腰果分级最重要的指标之一就是缺陷面积占比。矩形框无法准确表达缺陷的真实边界,只有像素级的轮廓才能算准。Mask R-CNN这类实例分割模型,能把每个缺陷实例的轮廓分割出来,再统计轮廓内的像素数量,这个面积才是真实的损伤面积。
另外还有一个场景在食品物料上非常常见:腰果在传送带上往往是紧挨着、甚至有部分重叠的。目标检测模型可以画出两个框,但两个框叠在一起,哪个缺陷属于哪个果仁很难判断。语义分割虽然做了像素分类,但它不会区分实例,重叠在一起的两个果仁会连成一片,也没法按单个果仁去计算结果。
Mask R-CNN的好处在于它天然是“检测+分割”双输出。它先用RPN区域提议网络把可能的目标位置找出来,再对每个候选框同时做分类、框回归和掩码分割。每一个腰果就是一个独立的实例,不会被旁边的腰果“合并”掉。对我们后续做单个果仁缺陷统计来说,这个设计非常关键。
1.3 HRNetV2p-W32作为主干网络的取舍
刚看到这个标题里的HRNetV2p-W32,可能有人会问:为什么不干脆用ResNet50?在COCO上这两者都能跑,但换到腰果这种细纹理场景,差异就出来了。
ResNet是编码-解码思路,图片经过多次降采样,高分辨率信息会逐步丢失。到深层特征图里,一个小裂缝可能只剩下几个响应点,不容易和背景区分开。HRNet的思路不一样,它从头到尾一直保留了一条高分辨率分支,其他分支负责生成更低分辨率的特征,然后不同分辨率之间不断做信息融合。这样一来,细节信息没有被牺牲掉,对小目标、细长裂缝、浅色霉斑这类特征的感知会好很多。
V2p这个版本,是在HRNetV2的基础上对高分辨率表示做了可学习的池化处理,后续接检测头时表达能力更强。W32指的是主干网络中等宽度分支的通道数,W48通道更宽、能力更强,但计算量和显存开销也更高。从我实际体感来说,W32配合1x训练策略,在腰果这种类型相对单一的任务上,性价比已经很高,训练速度快,部署时也更容易满足实时性要求。
1x在这里指的是训练计划按12个epoch来做,学习率在第8和第11个epoch下调,而不是那种动辄几十上百个epoch的长训练。用它做基线,一晚到两晚能跑完一轮,方便快速验证数据和标注质量。先跑通流程,再考虑更好的模型,这是我一直推荐的节奏。
2. 数据集构建与标注实践
2.1 采集图像时会忽略的坑
很多人一开始就想上最好的工业相机,我觉得没必要。第一版采集直接用了普通的高分辨率摄像头,配合固定光源和固定俯拍高度,先把数据跑起来,证明方案可行之后再升级硬件,这个路径省钱也省时间。
采集阶段最关键的是变化性。如果所有图片都在同一台设备、同一角度、同一光线下拍的,模型很容易把背景环境特征当成腰果特征去学,一换场景就崩。常规做法是分多批采集,上午下午各采一批,适当调整光源角度,把腰果随机翻几个面。腰果是三维物料,侧面有沟、边缘有棱,不同姿态下缺陷的形态差异很大。如果只拍正面,模型遇到侧面裂纹就基本无法识别。
类别体系也要在采集前定好。常见的腰果外观缺陷包括裂口、黑斑、霉变、破损,再加上一个正常类,五个类别起步就够用。这里要特别注意类别粒度和分级规则的对应关系。如果你的分级规则里要求区分“轻微黑斑”和“严重黑斑”,那就要分两类;如果不区分,就不要强行拆,类别越多,标注成本越高,模型也越容易混淆。
样本数量方面,我个人的经验是一个类别至少要有500到1000个独立实例,才能看到一个相对收敛的效果。腰果表面缺陷比较密集时,单张图可能同时有多个缺陷实例,所以实际需要的图片数可以比实例数少一些。如果预算有限、类别特别多,还可以先只分“正常”和“异常”两类,把模型跑通后再细分。
2.2 标注腰果缺陷时的细节规范
标注是实例分割项目里最耗时间、也最决定上限的环节。一个模型能学到什么程度,很大程度上取决于标注边界是否干净。
腰果主体的标注相对简单,沿着果仁轮廓描多边形就行。麻烦的是裂缝。裂缝又细又长,有的只有2到3个像素宽,稍不小心就标成一条锯齿线。我们统一的规则是:肉眼能看出的裂缝都标,标注时沿裂缝中心线画窄多边形,宽度尽量控制在3像素左右。这样做不是为了像素级完美,而是为了保证不同标注员手底下尺度一致。如果一个人标3像素宽,另一个人标10像素宽,模型看到相同裂缝却对应完全不同的目标,训练时很容易乱掉。
黑斑、霉变这类区域面积大,边界相对清晰,但经常有灰度过渡。标注的时候不要纠结于过渡区的每一个像素,建议按“视觉上明显异常的范围内边界”为准。这块最好在标注规范里写清楚,包括最小缺陷尺寸、哪些自然纹理不算缺陷、边缘和阴影要不要标等。
多人并行标注时,至少留出5%到10%的图做交叉复核,由同一个人复查,统计不同标注员之间的结果差异。这个机制能提前暴露出规范没说清的地方,非常值得做。
2.3 切到COCO格式的细节和校验方法
实例分割数据集的通用格式是COCO格式。结构上并不复杂,就是几个JSON数组:images里放每张图片的id、文件名、宽高;annotations里放每个实例的category_id、bbox、polygon分割点、area和iscrowd;categories里放类别映射。
很多人在生成COCO格式时会栽在两个地方。一是area字段直接用图像宽高相乘之类的占位值,没有按多边形真实面积算。这在某些框架训练时不算大问题,但会影响部分评估代码的计算,最好在建数据时直接把数据集计算出来的真实像素面积写进去。二是多边形坐标要和图像尺寸严格对应,一旦图片做过缩放或裁剪,坐标也要同步转换,否则模型训练时polygon坐标全错位。
数据准备好之后,一定要做可视化校验。简单的方法是写一个小脚本,把标注的多边形和框叠加在原图上,人工翻几百张图看看有没有错位或漏标。再写一个统计脚本,输出每个类别的实例数量、每张图的实例数分布、腰果面积的尺寸范围。这个统计能帮我们发现类别严重不均衡的问题,也能帮后续调anchor的时候判断初始尺度设置。
数据划分上还有一个容易被忽略的地方:同一个果仁如果拍了多张不同角度的照片,不能让它们同时出现在训练集和验证集里。否则模型可能不是真的学会识别腰果缺陷,而是记住了这个果仁的特征,验证指标会虚高。更好的做法是按采集批次来划分训练集和验证集,模拟不同批次的真实分布变化。
3. 模型训练与配置调优
3.1 环境准备与预训练权重
现在做深度学习项目基本都绕不开PyTorch。起步阶段,环境版本不需要追新,我用的是比较稳定的版本组合。训练框架方面,很多开源检测工具箱都内置了mask-rcnn_hrnetv2p-w32-1x_coco这套配置,直接在此基础上改就行,不用从零手写训练循环。
安装依赖时最容易踩坑的是版本不匹配。CUDA、cuDNN和PyTorch版本要对应好,装完之后一定要先跑一下官方自带的demo图片,确认前向推理正常,再开始训练。这一步能省下后面排查环境问题的大把时间。
预训练权重我直接用COCO数据集预训练好的模型。虽然腰果和COCO里的日常物体差异很大,但底层纹理、边缘、颜色特征仍然可以复用。修改模型输出类别数之后,需要注意新加的分类层和mask分支是随机初始化,其他层加载预训练权重。如果显存允许,我建议先冻结主干网络最前面的两到三个stage,用较低的学习率单独训练新增分支,等loss稳定后再解冻主干,对最终精度提升有帮助。
3.2 核心配置文件改动记录
在开源检测框架里,模型文件、训练计划、数据集路径都是围绕配置文件组织的。mask-rcnn_hrnetv2p-w32-1x_coco这个配置,名字已经把关键信息都写出来了:mask rcnn是模型结构,hrnetv2p-w32是主干网络,1x是训练策略,coco是预训练数据集。
实际使用时,最少要改几个地方。第一是类别数,把默认的80类改成自己的类别数量,比如5类或7类。第二是数据集的路径和标注文件路径。第三是训练超参数,尤其是学习率和batch size。
配置文件里和训练计划相关的部分通常长这样,我用简化示例说明:
optimizer = dict(type='SGD', lr=0.02, momentum=0.9, weight_decay=1e-4) lr_config = dict( policy='step', warmup='linear', warmup_iters=500, warmup_ratio=0.001, step=[8, 11]) runner = dict(type='EpochBasedRunner', max_epochs=12)这里默认的lr=0.02对应的是使用8张卡、总batch size为16的前提。如果你用单卡训练,batch size只有2或4,学习率直接沿用0.02大概率会发散。常见的做法是按batch size线性缩放:从0.02除以16再乘以当前batch size,比如batch size=2时,lr建议设为0.0025到0.005。这个缩放规则很粗糙,但比不缩放要稳得多。
锚框参数我也调过。腰果是长条形,默认长宽比0.5到2.0基本够用,但如果你想更贴合,可以加一个4.0的选项。锚框的base scale默认8,对腰果这种几十像素到几百像素的目标是合适的,不太需要动。
输入尺寸方面,默认的短边800、长边1333对大多数工业相机拍出来的图片来说没问题。如果图片里腰果非常小,比如2000万像素拍几十颗腰果,那就需要把输入分辨率提高或者做裁剪,否则小裂缝很容易在降采样时直接消失。
3.3 训练过程怎么监控
训练一启动,终端会不断打印当前的iter、loss、学习率等信息。刚开始接触实例分割训练的人会有点懵,因为同时有好几个loss并列显示,比如rpn_cls_loss、rpn_bbox_loss、cls_loss、bbox_loss和mask_loss。
这里简单拆一下。RPN相关的两个loss负责候选框的生成是否准确,区分前景背景;cls_loss和bbox_loss负责对最终检测框做分类和位置修正;mask_loss专门负责像素级分割,也就是每个像素的掩码预测是否对。Mask R-CNN对每个roi会预测多个类别的mask,训练时只选择该roi真实类别对应的那一路计算损失。
理想情况下,整个loss曲线在前几百个iter快速下降,然后慢慢平稳。如果一个epoch之后loss还在高位横盘,优先检查学习率是否设置过高或过低,其次检查标注数据是不是有问题,比如上下颠倒、类别映射错位。
warmup的作用不可小看。开始训练时学习率从很低的初始值逐步升到设定值,可以避免一开始梯度更新幅度过大,参数在头几步就被推飞。warmup_iters一般设500到1000,warmup_ratio通常0.001。
单卡训练1x schedule,如果输入尺寸是800到1333,batch size取2,24G显存的显卡大概需要运行十几个小时。如果训练中显存溢出,可以先降低batch size,并同步降低学习率。实在想提速,可以考虑自动混合精度训练,显存占用能明显下降,速度也能提升。
4. 模型评估与迭代优化
4.1 评估指标不是越高越好
训练完成后,常规评估会输出一组指标:mAP、mAP_50、mAP_75、AR,以及mask分支对应的mask AP、mask mAP_50等。mAP是COCO的标准指标,计算IoU从0.5到0.95的多个档位再取平均。
但工业质检场景里,我对mAP_50和mAP_75更敏感。实际产线判断缺陷是否存在时,不需要框的位置精确到像素级,只要大致能对上就能触发剔除,所以mAP_50往往更有参考价值。而mask AP直接反映了分割轮廓的质量,这决定面积统计准不准。因此在项目汇报里,至少要看三组数字:mAP、mAP_75、mask AP,才能比较全面地了解模型状态。
特别要留意每个类别的单独AP,而不是只看平均。腰果实际运行中,黑斑和霉变的检测效果通常还行,裂缝类的AP往往偏低。原因很简单,裂缝在IoU计算规则里很吃亏:一个细长的目标,预测结果稍微偏移几个像素,IoU就会大幅下降。如果你只看平均AP,可能看不出裂缝已经被拖后腿了。
单看AP还不够,还要换算成业务指标。生产线上最关心的是误检率和漏检率。把验证集里所有预测结果跟人工标注比较,设置不同的置信度阈值,观察误检和漏检怎么变化。最终阈值不一定选0.5,要根据不同缺陷的经济代价去调。比如漏过一个发霉的果仁被客户发现,损失远大于把几个好果仁误判成坏的,那阈值就可以适当降低。
4.2 把预测结果可视化后你才能看到问题
跑完评估只看指标表远远不够,我会把预测结果以图像形式批量输出,然后人工翻看。这个过程能直观地发现很多指标反映不了的问题。
第一类问题是漏检。很多时候裂缝在图上确实细到几乎看不见,模型没能识别出来。翻到这种图,我们会回到标注数据里去看:这类样本数量是不是太少?是不是很多人标注时都漏掉了?如果是,先从数据补齐开始。
第二类问题是误检。腰果表面有天然的沟和皱皮,纹理很容易被当成裂纹。翻图一看,啊,冒出一堆细长mask在正常果仁上,就知道是数据标注规范里没有把“皱皮不算缺陷”这条说清楚,模型学歪了。解决办法不是骂标注员,而是把大量带皱皮且标注为正常的样本加进训练集,让模型学会区分。
第三类问题是mask边界粗糙。预测的mask在腰果边缘处有锯齿、超出轮廓几个像素。这种问题多数可以通过提升输入分辨率、增加训练epoch或者换更宽的backbone来缓解。如果只在及格线附近徘徊,我一般先不急着换模型,而是先把最大的一类错误样本补掉。
4.3 调优主线和几个实用路子
模型调优有个优先级习惯:先数据、后模型、再训练策略。每次优化只改一个变量,不然你根本说不清是哪个改动带来了提升。
数据层面,针对低AP的类别单独补数据是最直接的。比如裂缝样本少,就专门采一批带裂缝的腰果,拍完后做随机旋转、镜像、亮度扰动再进训练。数据增强上我常开随机翻转和多尺度训练,尺度抖动对腰果这种大小差异大的目标很有帮助。
模型层面,如果数据已经比较充分,可以把W32换成W48,通道变宽后对大位点特征和边缘细节的拟合能力会更强,显存开销也会上升。另外也可以给特征金字塔的顶层加一个输出,或者把NMS阈值从默认往下调一点,让互相靠近的腰果不容易漏检。要注意的是HRNet本身参数量就不小,换W48前先确认部署侧的推理耗时能不能接受。
训练策略层面,从1x升级到2x甚至3x,多训练几个epoch往往能带来稳定提升,代价只是时间。EMA指数移动平均在一些开源工具箱里也能一键开启,对最终精度有正面效果。推理时做多尺度测试翻图,会把每个尺度下的预测结果综合起来,能涨一点mAP,但推理时间成倍增加,产线部署时要权衡。
还有一个非常容易被人忽略的调优点:后处理里的置信度阈值和小图尺寸。同样一个模型,如果把测试短边从800放大到1000,小裂缝的检出率会有肉眼可见的提升。前提是部署端能接受这个耗时。
5. 部署落地与工程化经验
5.1 模型导出与推理加速
训练用的模型权重并不能直接拿到产线上跑,通常要做推理优化。比较常见的链路是先把PyTorch训练模型导出成ONNX,再通过推理引擎转成加速版本。
导出时要注意固定输入尺寸。训练时代码里经常允许动态输入,但部署端固定成短边800或960可以显著提高推理性能。另外,NMS这类后处理在不同框架里支持程度不一样,通常建议单独实现或使用引擎自带的插件,不要把整套模型原样导出。
我自己在部署时踩过的一个坑是:导出ONNX时没有把原始图像的预处理步骤对齐。训练时的BGR通道顺序、归一化系数、缩放方式,在部署代码里必须保持一致。有一版模型导出后精度掉了很多,最后发现是推理代码里用了RGB顺序做预处理,跟训练时不一致。
加速之后的速度差异非常大。一张1080P的腰果图,纯PyTorch推理可能要两百多毫秒,优化后能压到二十到三十毫秒,基本够用。这里具体数字会因显存、TensorRT版本和输入尺寸而不同,但趋势是一致的:部署优化是必不可少的一步。
5.2 产线集成与分拣联动的关键控制点
谈部署不能只谈模型,还要考虑相机怎么装、光源怎么打、信号怎么联动。
相机方面,普通工业相机加固定镜头的方案就够用。腰果在传送带上是动态的,快门速度要足够快,避免运动模糊。光源建议用低角度的环形光或条形光,这样可以减少腰果表面反光和阴影对缺陷识别的影响。整个成像区域要加遮光罩,避免车间自然光变化造成图片亮度漂移。
触发方式一般用光电传感器。腰果通过传感器后,相机在固定延时下拍照,确保腰果正好在画面中央。这样拍的图片位置稳定,模型处理起来更省心。
检测完的结果还要传给后端执行机构。如果只是报警,可以把结果写到PLC或工控机,由产线控制剔除气嘴。如果要做自动分级,就要把每个检测框的类别、分数、mask面积比例一起传出去。这里的通信协议和交互时序要提前约定好,尤其是多相机并联时,数据同步很容易出乱子。
产线侧判定规则通常和模型阈值层分开。模型只负责输出“这个区域有多大概率属于某类缺陷”,最终是否判不合格、判到什么等级,由规则层根据缺陷面积占比、数量、种类去决定。这样做有一个明显的好处:客户改了标准,只需要改规则配置,不用重新训模型。
5.3 持续迭代的数据闭环
模型上线只是开始不是结束。算法在真实产线运行,肯定会遇到新的情况:新的原料批次颜色更深、烘焙工艺调整后霉斑颜色变浅、不同季节的腰果纹理不一样。
数据回流机制比换一个更强的主干网络更有价值。我建议在产线边上部署一个自动截图服务,凡是模型置信度在0.4到0.7之间的模糊判断结果,以及现场确认的误检漏检图片,自动保存到待标注队列。每周或每两周做一次标注补录,和原始训练数据混合后再增量训练,这个闭环跑起来,模型会越来越适应真实场景。
模型迭代时要做好版本管理,每个版本的权重文件对应哪份训练数据、哪个标注规范版本、哪套超参数,都要记录清楚。否则过两个月想回滚到之前那个效果好的模型,可能根本找不回来。
6. 常见问题速查与避坑清单
6.1 数据与标注阶段的典型问题
表格里的问题都是我在项目里真实遇到过的,按频次排序。
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 验证集AP很高但产线效果差 | 数据划分没有按批次分隔,模型记住了特征 | 按采集批次重新划分训练/验证集 |
| 裂缝类几乎检不出来 | 标注宽度差异太大,模型学不到一致模式 | 统一裂缝标注宽度,补标一批裂缝样本 |
| 模型把腰果皱皮当成裂纹 | 正常样本缺少自然纹理变化 | 大量增加带皱皮的正常类别样本 |
| 一张图里多个腰果挤在一起,mask粘连 | 堆叠情况标注不完整,难以学习实例边界 | 单独采集一批单颗腰果图,辅助模型学习边界 |
| 同样的标注标准,两个标注员风格差异大 | 没有明确标注规范 | 写书面规则,明确最小缺陷尺寸、裂缝宽度、不算缺陷的情况 |
6.2 训练阶段常见问题
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| loss开头就直接飞掉 | 学习率太高,或warmup设置过短 | 降低学习率,确认warmup_iters数值 |
| 训练loss持续不下降 | 学习率太低,或数据标注存在大量错标 | 小幅调高学习率并检查标注可视化 |
| 显存不足无法训练 | batch size太大,输入分辨率太高 | 减小batch size并按比例降低学习率,开启混合精度 |
| 训练完mask轮廓粗糙、边缘外扩 | 训练epoch不足,或输入尺寸偏小 | 增加训练轮数,适度提高测试输入分辨率 |
训练阶段还有一个让我印象深刻的问题:用默认的lr=0.02在单卡训练,前面200个iter loss疯狂上升,当时以为是代码写错了,折腾了一晚,最后发现就是学习率没按batch size缩放。后面我把这个规则写在配置注释里,再也没犯过。
6.3 部署阶段常见问题
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 导出的ONNX推理结果和训练不一致 | 预处理步骤不一致,BGR/RGB顺序不同 | 对比训练代码和部署代码的归一化与通道顺序 |
| TensorRT转换报错 | 网络结构里存在不支持的算子 | 检查模型结构,处理掉特殊算子或更换兼容层 |
| 相机画面清晰但模型检测率突然下降 | 光源老化或环境光变化 | 检查光源状态、重新采集对照图,尝试自动亮度补偿 |
| 产线节拍跟不上 | 单张图推理时间太长 | 固定输入尺寸、启用半精度推理、批量处理叠加 |
最后分享一点体会
回头再看整个项目,mask-rcnn_hrnetv2p-w32-1x_coco这套配置本身并不算新,但它证明了在垂直检测场景中,一个稳定、可解释、能快速迭代的基线方案,往往比追逐最新网络结构更靠谱。我体会最深的一点是:项目成败很多时候不取决于模型性能涨了多少个点,而是标注是否统一、指标是否贴合真实质检规则、部署后反馈闭环能不能跑起来。这套流程放在腰果上成立,换成果仁、坚果、水果甚至五金件表面缺陷检测,思路都是一样的。如果你也准备启动类似的项目,建议先用这套配置把端到端流程打通,再根据收集到的问题逐步迭代,你会少走很多弯路。