我先看了这个题目很久。标题本身其实已经把方案说得很清楚了——YOLO12-AFPN-P345,检测目标锁定在卡车场景里的底盘车和堆垛机。但真正落地过这种工业视觉项目的人都清楚,模型选型只是最外围的一层壳,里面包裹的数据采集、标注规范、特征层取舍、定位误差控制、夜间光照补偿,每一层都能让项目从“Demo能跑”变成“现场能扛”。
这篇文章我打算完全以项目复盘的方式来写,不讲空泛的概念,把我们在某个物流中转场和仓储园区里从模型选型到训练调参再到定位部署的真实过程拆开。如果你正在做类似的工业检测项目,或者准备把YOLO体系引入到不那么“规整”的户外场景里,这篇应该能帮你少踩几个我踩过的坑。
1. 卡车场景里为什么非要单独检测底盘车和堆垛机
很多做视觉检测的朋友最开始都有一个惯性思维:拿一个在COCO上预训练好的通用检测模型,直接往工业现场一丢,剩下的交给迁移学习。这个思路在比较“干净”的室内场景里通常没问题,但换到卡车出入频繁的物流中转场,马上就会碰壁。
1.1 底盘车和堆垛机到底难在哪
先说清楚这两个目标是什么。底盘车指的是挂车或半挂车去掉驾驶室之后的那一段车体框架,在物流园区、制造厂、港口中转区里,它经常被牵引车拖到指定工位装卸货物,视觉系统需要判断它是否进入指定区域、是否停正、是否处于可作业状态。堆垛机则是在高位货架区或产线边来回移动取放托盘的设备,视觉系统需要识别它的当前位置、货叉是否伸出、运动方向是否与人行通道冲突。
这两个目标跟通用检测数据集里的“卡车”“叉车”完全不是一回事。底盘车在画面里往往只有车轮、大梁、尾灯、集装箱锁扣这些局部特征,而且经常被集装箱、吊具、线缆和旁边停放的车辆遮挡。堆垛机的形态更是随着货叉高度、摄机角度和光照方向剧烈变化,同一个堆垛机从正面看和从侧面看几乎是两个物种。把这两个目标塞进“car”“truck”这种粗糙类别里训练,结果必然是模型不知道你到底要它找什么。
1.2 通用模型与工业需求之间的粒度差距
COCO数据集里有80个类别,模型学到的语义是“这是一辆车”。但我们在项目里需要的不是这个,我们需要的是“这辆底盘车的尾部中心点在地面坐标系里的位置是多少”“这台堆垛机的货叉现在处于高位还是低位”“它是否已经进入禁入区域”。这些信息严格来说已经超出普通目标检测的范畴,属于定位与状态识别。
这也是为什么方案一开始就定下来要做专门的类别和数据体系,而不是先拿预训练权重凑合用。项目组当时做了一轮基线测试:用YOLOv8的COCO预训练权重直接在我们的监控画面上推理,底盘车被识别成“truck”的召回率确实不低,但框的位置极不稳定,经常把旁边堆放的空托盘或者集装箱边缘一起框进去,堆垛机则基本识别不出来。因为COCO里没有“堆垛机”这个类别,模型只能强行把它归类为某种“车”或者直接丢漏。
所以这个项目从一开始就必须重新定义类别:一类是底盘车,一类是堆垛机,再加上一条底线——不依赖通用预训练类别的语义,只把COCO权重当作特征提取的起点。
2. YOLO12-AFPN-P345选型逻辑:从COCO通用模型到工业场景的差距
方案定型前,我们实际上在几个模型组合之间做了一轮横向比较。参与对比的有YOLOv8的PANet结构、YOLO11的C2f结构,以及最终定下来的YOLO12-AFPN-P345组合。下面这张表记录的是在我们自己的标注集上、相同训练轮数和输入分辨率下的测试结果,虽然具体数字换一个场景肯定变化,但相对差距是有参考意义的。
| 模型配置 | mAP50 | mAP50-95 | 夜间低光照漏检率 | 单卡训练耗时 |
|---|---|---|---|---|
| YOLOv8s-PANet | 86.3 | 74.1 | 约13% | 14.2小时 |
| YOLO11s-C2f-PANet | 88.7 | 76.9 | 约9% | 13.5小时 |
| YOLO12s-AFPN-P345 | 91.2 | 80.4 | 约5% | 15.8小时 |
| YOLO12m-AFPN-P345 | 92.6 | 82.3 | 约4% | 26.1小时 |
2.1 YOLO12在特征提取层面做了什么改变
先说结论:这个模型相对于前几代,最大的变化在主干网络的特征提取能力上,尤其是对形态不规则、部分被遮挡的目标,特征表达能力有明显增强。我们不需要在这里复述论文里的全部公式,只需要知道它在主干里引入了更细粒度的特征权重调整机制,相当于让网络学会“哪些局部特征重要,哪些背景纹理可以忽略”。
这句话翻译成现场效果就是:堆垛机货叉在缩回状态下几乎跟立柱重叠,旧模型会漏检,YOLO12却能从立柱缝隙里把货叉的弱特征捞出来。底盘车尾部被集卡遮挡一半的时候,模型依然能通过轮毂、保险杠和大梁的残余特征把整个目标框出来,而不是把框切到只包住可见部分。
2.2 AFPN为什么在这个场景里比PANet稳
AFPN的全称是Asymptotic Feature Pyramid Network,渐进式特征金字塔。它在PyTorch生态里已经不算新东西,但跟YOLO的组合往往被忽视。我之前见过很多项目直接用默认的PANet结构,很少会去思考这个结构在小目标、遮挡目标居多的工业场景里有什么隐患。
PANet的核心路径是自顶向下传递语义信息,再自底向上传递空间信息,两轮融合确实让每一层特征都拿到了全局上下文和高分辨率细节。问题在于这种双向强融合的处理方式比较“暴力”,细节信息和语义信息在多次卷积、上采样、下采样的过程中会互相干扰,具体表现就是目标边缘和背景纹理纠缠在一起。AFPN的“渐进”思路是分阶段、带权地融合相邻层特征,高层语义逐渐往下渗透,每一层都在前一层的基础上做增量融合,信息传递路径更短,衰减更小。
放在我们的场景里,最直观的差异是底盘车和堆垛机轮廓的锐利程度。用PANet训练出来的模型,检测框经常比目标实际轮廓大一圈,尤其是夜间白色灯光下的底盘车,框会“吃”进旁边黑暗的区域;换成AFPN之后,同样一个目标,框的贴合度明显改善,这直接影响后面做定位时取框底边中心点的精度。
2.3 P345取舍:为什么不用P2也不用P6
P3、P4、P5分别对应输入图像640×640条件下80×80、40×40、20×20的特征图,分别主要编码小、中、大尺度的目标。我们最终只在这三层上接检测头,有意放弃P2和P6。
P2是160×160的极高分辨率特征图,对极小目标有奇效,但代价极其昂贵。640输入下光一个P2检测头带来的计算量就超过P5检测头的4倍,而且卡车场景中的小目标(比如远处的行人、堆垛机货叉尖)并没有小到必须依赖P2的程度。P6需要把输入分辨率拉到1280级别才有意义,否则20×20之上再接一个10×10的层,纯粹是给大目标增大感受野,但底盘车和堆垛机在监控画面里通常占100-300像素的高度,P5层级完全够用。
经过实测,去掉P2检测头之后,模型的推理速度提升了大约18%,而mAP只掉0.3个百分点。这个交换非常划算。P6则一开始就没接入,因为训练集中根本没有占比极小、需要全局上下文才能识别的大远景目标。
3. 数据采集与标注规范:决定模型上限的那一半工作量
我一直认为,工业视觉项目的模型性能上限,在数据采集阶段就已经定了。训练只是把这个上限尽量逼近而已。这一个节讲的都是我们自己踩过之后梳理出来的规则,不一定适用所有场景,但很值得参考。
3.1 相机位姿、安装高度和采集时段怎么定
如果要在多个场地复用这套模型,最忌讳的是只在单一视角下采集数据。我们最初拿到的数据来自某中转场的一个球机,高度大约六米,俯视角度很舒服,模型训练出来效果不错。结果换到另一个园区,相机装在两米高的立柱上,近乎平视,模型的稳定性和召回率立刻下降。原因很简单:视角变了,目标的长宽比分布、可见面结构全变了,模型在训练集里没见过这种几何形态。
所以数据采集阶段就规定:每个场地至少覆盖两种典型安装高度和三种水平朝向角,同时覆盖白天强光、逆光、阴影、黄昏、夜间补光、雨天共六种光照条件。不要小看“雨天”——底盘车大梁上的反光在雨天是完全不同的纹理,模型如果没见过雨滴造成的镜面反射,夜间雨天基本就废了。
采集方式上不要只靠定时截图,要用视频抽帧加关键事件触发结合的办法。我们在现场布置了边缘小主机,对视频流做连续抽帧,同时用运动检测模块在底盘车进入画面、堆垛机转向时触发额外抓帧。这样做的好处是能拿到大量连续状态帧,尤其是堆垛机货叉从伸到缩的完整变化过程,对训练状态识别特别有帮助。
3.2 标注除了画框还要做什么
普通数据集的标注只需要类别加边界框,但我们这个项目在标注规范上多做了三件事,当时确实耗了些人力,后期训练省了无数麻烦。
第一件是标注遮挡目标时强制给出完整包围盒。很多标注员习惯只框住目标可见部分,这在训练时会产生一个很坏的影响:模型学到“目标只占这么大”,推理时遇到遮挡就把框缩了一半。我们要求标注员参照前后帧推断被遮挡目标的完整范围,能判断出来的就画完整框,完全判断不出来的才允许标注为“ignore”区域。
第二件是为每一帧记录目标的状态属性。堆垛机标注为“货叉高位”“货叉低位”“货叉伸缩中”三种属性,底盘车标注为“静止”“行进中”两种属性。这些属性不参与检测头输出,但会在训练时作为辅助分类信号加权,让特征提取层更关注与作业状态相关的局部区域。实际效果是货叉缩回状态下的误检率下降了一半以上。
第三件是统一最难类别的判定口径。底盘车和堆垛机相距比较近的时候,底盘车尾部经常和堆垛机下部重叠,这时候到底算一个目标还是两个目标、框之间允许多少像素的IoU重叠,都要在标注规范里写死。我们定的是两个类别允许互相遮挡,但同类别目标之间IoU超过0.5的帧要单独复核,防止两个重叠的同类别框把模型训练晕。
3.3 数据集规模与类别平衡
最终训练集规模大约在底盘车9000张、堆垛机12000张,验证集各1200张。这里面有一个平衡技巧:不要按场地平均分配,而要按“光照难度”分配。夜间和低光照样本占到总样本的45%左右,因为白天样本再多,模型也学不会夜间的灯晕、反光和低对比度。我们在实验里做过对照,把夜间样本比例从30%提到45%,夜间场景的漏检率下降了将近一半,而白天场景几乎不掉点。
类别不平衡问题其实不太严重,因为两个目标在画面里出现的频率差不多。真正需要处理的是难例不均衡:背景里有很多集装箱角、立柱、托盘堆,这些长得像底盘车或者堆垛机局部的物体,如果不专门作为负样本加入训练,模型会把注意力浪费在这些干扰物上。我们在每个场地额外标注了一批“hard negative”帧,只标背景不标目标,直接加入训练集,效果等同于给模型做了几次负样本强化。
4. 训练调参实录:让P345特征层真正干活的细节
到了训练阶段,事情反而变得有趣起来。同一个YOLO12-AFPN结构,不同的训练策略产生的效果差别相当大。这一节完全围绕训练参数调优展开,给出来的经验以我们自己的GPU集群和数据集为准,不同设备上需要适当调整。
4.1 输入分辨率、批大小和训练轮次怎么搭配
我们采用的是640×640输入,batch size 32,在两张卡上以分布式方式训练。选640而不是更大分辨率,核心原因是部署端的推理设备对处理时间有硬性要求,超过50毫秒就会挤压后续业务逻辑的时间预算。如果只追求精度不管速度,搬到768甚至1024会再提升大约1.5个点的mAP50-95,但推理时间会涨到接近一倍,工业现场接受不了。
epoch数定为300轮,配合余弦退火学习率调度。前50轮用warmup把学习率从1e-4慢慢拉到1e-2,之后每100轮衰减一个数量级。300轮训练完大约18个小时,从训练曲线看,mAP50-95在第240轮之后就基本走平,说明这个数据规模下300轮是一个比较合理的投入产出终点。450轮我也试过,提升不到0.5个点,纯粹浪费时间。
4.2 数据增强哪些开哪些关
这是最想重点说的部分。YOLO系列默认开启的Mosaic增强,在本项目里是关掉的,至少强度大幅调低。原因是底盘车是一个长条形的刚性目标,Mosaic增强会将四张图的局部强行拼接,很容易把底盘车的大梁和车轮从中间切断,生成大量“肢体破碎”的训练样本。模型看到过多这种不合理样本之后,学到的空间结构概念会被污染,表现为正常场景里对底盘车整体轮廓的把握变差。
保留并加强的是两类增强:一类是随机平移和缩放,模拟相机安装位置误差;另一类是亮度对比度扰动和通道噪声,模拟不同曝光条件下的画面变化。这两类增强对工业场景特别对症,因为相机参数在不同时段会有细微漂移,算法必须要能容忍这种变化。
Mixup增强我们也保留了,但概率从默认的1.0调到了0.3,只在训练后期开启,并且只混合不同光照条件下的样本,不让它混合同类的两个目标。这样做可以在不破坏目标结构的前提下,让模型对光照变化更迟钝——也就是更鲁棒。
4.3 损失函数和评价指标怎么选
训练时分类损失用BCE,框回归损失用的是CIoU。一开始尝试过用DIoU,发现收敛速度差不多,但在堆垛机这种长宽比极端的目标上,CIoU对框的宽高比约束更强,最终mAP50-95高了约0.8个点。说明对于形状比例异常的目标,宽高比起码跟IoU一样重要。
评价指标上,mAP50和mAP50-95都要盯,但漏检率要单独统计。我自己的习惯是在验证集里专门抽出一个夜间难例子集,统计IoU大于0.5条件下目标被完全漏掉的百分比。这个指标比mAP更能反映现场真实观感,因为工业用户不关心你的框够不够准,关心的是“该报警的时候报没报”。如果夜间难例子集的召回率低于92%,即使总体mAP再漂亮,这个模型我也劝你慎重上线。
5. 从检测框到位置坐标:单目定位与推理部署实践
检测模型输出的只是图像坐标里的边界框,但项目标题里写的是“定位技术”,这意味着我们必须把框转化成地面坐标系里的真实位置。这部分的工程难度其实比训练模型更大,因为涉及到相机标定、测距误差控制和推理引擎优化。
5.1 为什么不用深度相机而选单目+地面假设
很多方案在室内场景会直接用RGB-D相机获取深度,但在卡车出入的户外场景行不通。一方面阳光直射会严重干扰红外结构光或ToF传感器;另一方面现场灰尘和水汽会污染深度图,导致关键区域的深度值大面积空洞。我们最后采用单目相机配合地面平面假设来测距。
具体做法是:先对相机做内参标定和畸变校正,再选取画面中已知高度的参考目标(比如固定立柱上的标尺),标定出地面在世界坐标系中的平面方程。拿到检测框之后,取框底边的中心点作为脚点,通过相机投影模型把像素坐标映射到地面平面上,得到目标相对于相机的水平距离和横向偏移。
5.2 定位误差是怎么控制住的
单目测距的主要误差来源是检测框底边位置不准。如果模型把框下边框多画了两三个像素,距离越远,误差放大越明显。实测下来,在水平距离20米处,检测框上下偏移5个像素就会造成约0.35米的距离误差。控制这个误差有两个办法:第一个是前面提到的AFPN让框贴合度更好;第二个是在取脚点之前加一步“脚点校正”,搜索框底边附近是否存在明显的车轮或大梁阴影边界,把脚点吸附到真实接触面上。
经过校正,距离20米以内的定位误差可以控制在±0.25米以内,横向偏移误差在±0.15米以内。这个量级对于判断底盘车是否停进装卸位、堆垛机是否越界已经够用。如果你需要更精细的厘米级定位,就得考虑用地面二维码或激光雷达辅助,但那已经是另一个量级的成本投入了。
5.3 推理引擎的工程优化
模型训练完成后,我们把它导出为ONNX,再用TensorRT的FP16精度做推理加速。实测下来在消费级推理卡上单帧推理时间稳定在18到22毫秒,输入分辨率640×640,batch size 1。
FP16量化在这个实验中没有出现精度回退,个别类别上的mAP50甚至还有微弱提升,猜测是低精度推理的舍入在某种程度上引入了正则化效果。不过FP16下有一个问题容易踩:某些算子的输出值范围较大,堆叠之后可能溢出,所以导出后必须在目标硬件上用一组现场真实图片做逐层精度对比,不能只在服务器上测一个整体mAP就交付。
后处理NMS选了多类别独立NMS,而不是对所有类别统一做NMS。这样底盘车和堆垛机即使距离很近相互重叠,也能同时保留两个框,不会因为其中一个置信度高就把另一个压掉。
6. 漏检、误检和长尾问题:三个排查真实案例复盘
即便模型整体指标已经不错,现场运行一段时间后还是会出现一些遗留问题。这三个案例是我认为最有代表性的,每一个都经历了从现象到根因到修复的完整过程。
6.1 夜间补光下的灯晕导致底盘车尾部漏检
现象是夜间某些时段底盘车尾部灯组反射特别强烈,模型连续好几分钟检测不到底盘车。
排查链路:白天正常、黄昏正常、夜间刚开始也正常,但某个固定时段开始异常,说明跟特定光照方向有关。我们拉出当时的监控画面,发现问题不是光线太暗,而是补光灯正好照射到尾灯外壳形成高亮光晕,光晕覆盖了框底边的关键特征。模型并不是看不到目标,而是把光晕识别成了更强的“目标特征”,导致底盘车真实轮毂区域的置信度被压制。
修复办法是双管齐下:一是把夜间难例数据里专门增加“强反射光晕”这一类样本,人工标注时保留完整框;二是在推理端加了一条规则——当检测框中心区域亮度方差超过阈值时,强制降低该框置信度,再与相邻正常置信度的检测结果做合并。最终效果是夜间漏检率从约5%降到了3%以下。
6.2 堆垛机货叉完全缩回时被误判为背景
堆垛机的货叉缩回到最紧的状态时,从正面看只剩两片很窄的金属条,宽度在640×640输入下可能只有10个像素左右。模型经常在这一帧上把堆垛机整体给丢了,因为视觉特征的主干——叉体——几乎消失了。
排查链路:我们在验证集上按“货叉缩回”“货叉半伸”“货叉全伸”三个状态分桶统计召回率,发现缩回状态的召回率比全伸状态低了整整11个百分点,问题定位到状态差异,而不是场景差异。
修复办法没有直接去提高检测阈值,而是从业务理解入手:货叉缩回时,真正稳定出现的是堆垛机的门架立柱和底座的轮廓,这些部分的特征比较弱但存在。我们在数据增强阶段加入对底盘区域的裁剪放大,同时让AFPN的P3层对这种弱局部特征产生更敏感的响应。训练两轮之后,缩回状态召回率提升到与正常状态基本一致。
6.3 两类目标远离镜头时相互混淆
当底盘车和堆垛机都出现在距离相机20米以外的地方,二者尺寸下降到很小,轮廓信息不足,模型偶尔会把堆垛机误报成底盘车。
排查链路:这类误报不是单张图里发生的,而是连续帧上出现的,说明不是孤立噪声,而是深层特征层的语义确实混淆了。我们抽出特征图热点图,发现模型在网络深层主要依赖“底部有大片水平横梁”作为底盘车的判据,而堆垛机底座恰恰也有类似结构。
修复办法有两种思路,一种是增加类别区分度的损失权重,另一种是加一个场景上下文约束——底盘车通常出现在车辆通道,堆垛机通常出现在立柱货架附近。我们选择后一种,在推理端叠加了一个基于位置的先验概率图,根据目标所处图像位置微调两类置信度。修改后这类跨类别误报减少了70%以上。
7. 如果重新做一次,我会在哪些地方改变投入顺序
项目收尾之后回看整个过程,最大的认知是技术选型在这种项目里只占两成权重,剩下的八成都耗在数据和场景理解上。如果让我重新做这个项目,会在开工第一天就把夜间和雨天的相机配置、视角覆盖矩阵这些事定死,而不是等到第一批标注完成后才发现训练数据里白天样本占了七成还要返工。
另外一个很值得说的点是把“检测”和“定位”拆开看待。检测模型只需要回答画面上有没有、在哪里;但真正能给业务带来价值的,是把这些框映射到地面坐标之后的那一步。单目定位虽然精度有限,但配合地面平面假设和脚点校正,在20米范围内已经能支撑绝大部分工业调度需求,成本比激光雷达低一个数量级。
最后再分享一个细节:训练和推理不要在同一个PyTorch版本上裸奔,最好把模型导出ONNX后单独固定推理环境。我们在部署现场遇到过一次奇怪的重复结果问题,排查到最后是GPU驱动和TensorRT版本不匹配导致——在开发机上测试完全没问题,换到现场机器就出了偏差,这个坑如果你以后遇到,不用怀疑模型,先检查推理环境的算子实现差异。