YOLOv3小目标检测实战:危化品车辆标志牌识别与部署 简介围绕危化品车辆货物类型识别这一课题这份PDF文献面向交通管理、道路安全监控与计算机视觉方向的学习者和工程人员重点解决高速公路实时场景下危险货物类型自动判别的问题。文中以YOLOv3单阶段目标检测模型为主线结合基于机器视觉的车辆抓拍系统、红外补光采集方案与包含23类标注的危化品车辆数据集讨论数据标注、模型训练、损失函数优化及不同光照和天气条件下的识别表现并给出实验设置与结果分析。资源包共1个PDF文件大小约2.41MB适合按章节查阅研究背景、方法设计与实验结论。目前已有107人学习可作为了解YOLO在交通场景落地、撰写相关课程报告或方案论证时的参考材料。1. 一块贴在车尾的菱形牌暴露了通用检测模型的软肋在 4096×2160 的卡口原图里一块危化品车辆货物类型标志牌通常只有 170×190 像素左右把它等比压到 YOLOv3 常用的 416×416 输入这块牌子就只剩 17×19 像素比模型最细那一层的感受野还小。所以危化品车辆货物类型识别这类目标检测任务真正难的地方并不在看出这是辆车而在于四个数量级的尺度压缩之后模型还得分辨出牌子上画的到底是易燃液体、氧化性物质还是腐蚀品。YOLOv3 在 2020 年前后被大量卡口项目选中不是因为它在公开榜单上最漂亮而是因为它的三尺度输出、多标签损失和可嵌入式部署的组合恰好卡在这个场景的约束里。搞懂这套组合为什么能对上比记住一个模型名字有用得多。2. 从两阶段到单阶段卡口实时识别为什么最终选择 YOLOv32.1 两阶段检测器的时间预算R-CNN 系列的成本在哪以 R-CNN 系列为代表的两阶段检测器结构上可以拆成三块第一块生成基于类别的候选区域这些候选区域定义了后续检测器可用的候选检测集第二块是深度卷积网络负责从每个候选区域里抽出一个固定长度的特征向量第三块是同类型的线性 SVM 分类器对特征向量做判别。Faster R-CNN 之后候选区域生成从外部算法选择性搜索换成了网络内部的 RPN候选框和特征共享卷积计算速度上了一个台阶但先找候选、再逐个精修的两段式流程没变。这个流程的代价是显性的每张图要处理几百个候选区域每个候选区域都要过一次 ROI 池化和分类头单帧耗时跟候选数量近似线性相关。候选框数量又要靠提高召回率来保证——设少了会漏掉小目标设多了延迟直接爆炸。对于一块 17×19 像素的标志牌两阶段方法的高召回优势其实被大幅稀释了候选框的尺寸先验来自聚类如果聚类时没覆盖到这么小的比例第一阶段就把牌子筛掉了第二阶段再准也没用。2.2 单阶段把检测写成一次回归网格、锚框与端到端输出单阶段检测器走的是另一条路直接把目标检测当回归问题解用预定义的锚点密集覆盖整张图像的空间位置不再额外挂一个区域建议网络。YOLOv3 的做法是把特征图划分成 S×S 的网格每个网格生成 B 个边界框做预测总共得到 S×S×B 个预测框然后对这些框直接执行边界回归与类别决策一次前向就出结果。同期还有两个常被拿来对比的模型。SSD 在多个不同深度的特征图上分别预测浅层特征图分辨率高、适合小目标但浅层卷积的语义太弱容易把背景纹理误判成目标RetinaNet 提出 Focal Loss 来解决单阶段检测器正负样本极度悬殊的问题——一张 416×416 的图里几万个先验框真正匹配到目标的可能只有几个普通交叉熵会被海量负样本淹没。YOLOv3 的取舍是用类似 FPN 的上采样加通道拼接把深层语义和浅层分辨率融合起来在三个尺度上分别预测绕开了浅层语义弱的问题同时用忽略阈值加部分损失权重来控制负样本的影响没有引入 Focal Loss 那套额外超参。2.3 约束反推卡口场景的三个硬条件选型得从场景倒推。第一个约束是时延高速公路卡口过车时一辆车在画面里停留的时间很短识别得在前端边缘计算设备上完成算力预算大概是几十毫秒一帧这个量级两阶段模型在这个预算下很难跑满帧率。第二个约束是尺度跨度同一条车道上近处货车的标志牌能有 300 像素宽远处只剩 80 像素同一类目标跨了将近一个数量级模型必须多尺度同时在线否则就是近的认得出、远的全漏。第三个约束是标签结构危化品标志本身是多标签的一块牌可能同时表示易燃和有毒输出头不能强制互斥。这三个条件叠起来YOLOv3 的三个特性正好对上三尺度特征图分别是 13×13、26×26、52×52把大小目标分派给不同层置信度和分类都用独立的二分类交叉熵天然支持一块牌多个属性;模型主干是纯卷积、无特殊算子容易量化后塞进嵌入式盒子。反过来说如果这个任务的目标是单张图的极致精度、允许离线批量跑两阶段模型加上更大的输入分辨率依然是更稳的选择。选型从来不是哪个模型更强而是哪个模型的短板不在我的关键路径上。3. YOLOv3 的输出张量S×S×B 到底在预测什么3.1 三个尺度、九组锚框与标签分配规则以 416×416 输入为例三个尺度的特征图分别是 13×13、26×26、52×52每个网格预测 3 个框每个框输出 4 个位置量、1 个置信度和 C 个类别分数。三个尺度各配 3 组先验框一共 9 组规则是尺度越大特征图越粗配越大的先验框13×13 那层配 (116,90)、(156,198)、(373,326)负责大目标52×52 那层配 (10,13)、(16,30)、(33,23)负责小目标。标签分配这一步最容易被忽略却直接决定训练能不能收敛。YOLOv3 的规则是每个真值框只分配给与它 IoU 最大的那个先验框这个框负责正样本其余先验框里IoU 超过忽略阈值常见 0.5的不计入损失低于阈值的才算负样本。这个中间地带直接忽略的设计很关键——一个 170×190 的标志牌跟好几个先验框的 IoU 都在 0.3 到 0.6 之间如果强行把它们都当负样本模型会被训练成看到类似大小的物体就压制小目标的召回会一路往下掉。顺带一个常被忽视的事实COCO 那套先验框是按通用物体聚出来的宽高比偏向竖直的人形和细长的车形。危化品标志牌在图像里多是接近正方形的菱形或者接近 2:1 的矩形跟 COCO 先验的分布并不吻合。要在自家数据集上跑好用 k-means 对自家真值框重新聚一次先验收益通常比调学习率明显。3.2 解码公式为什么中心点要过 sigmoid、宽高要取指数网络输出的四个位置量是偏移量不是绝对坐标。还原的时候要经过下面这套解码import numpy as np def decode(pred, anchors, stride, img_size416): pred: (B, S, S, 3, 4 1 C) 网络原始输出 anchors: (3, 2) 该尺度负责的三组先验框宽高像素单位 stride: img_size // S13-3226-1652-8 B, S pred.shape[0], pred.shape[1] # 网格左上角坐标形状 (1, S, S, 1, 2) gy, gx np.meshgrid(np.arange(S), np.arange(S), indexingij) grid np.stack([gx, gy], axis-1).reshape(1, S, S, 1, 2) # 中心点偏移压到 0~1保证预测中心落在本网格内 xy 1.0 / (1.0 np.exp(-pred[..., 0:2])) # 宽高按先验框做指数缩放输出恒为正 wh np.exp(pred[..., 2:4]) * anchors.reshape(1, 1, 1, 3, 2) obj 1.0 / (1.0 np.exp(-pred[..., 4:5])) # 置信度 xy (xy grid) * stride / img_size # 还原到整图并归一化 wh wh * stride / img_size return xy, wh, obj中心点过 sigmoid 的动机是稳定性训练初期网络输出接近 0如果不加约束预测框中心可能落在距离真值几百像素之外的位置梯度方向上会产生很大的拉扯早期训练容易发散。压到 0~1 再加网格坐标等于给每个预测框上了一道不许跑到别人格子里的硬约束模型只需要学落在格子里的哪个位置收敛快得多。宽高取指数而不是直接回归是为了保证输出恒为正同时把绝对尺寸变成相对先验框的缩放倍数。这一层转换很重要远处 80 像素的牌子和近处 300 像素的牌子绝对尺寸差了近 4 倍但它们相对于各自匹配的先验框的倍数可能都在 0.9 到 1.2 之间。回归相对量等于把尺度差异从学习目标里消掉了这是多尺度检测能共享一套权重的前提。3.3 四项损失的设计动机置信度和分类为什么用二分类交叉熵原始损失函数写成四项之和中心点误差、宽高误差、置信度误差、分类误差。前两项是定位项后两项是判别项各自带权重系数做平衡。定位项用平方误差因为坐标回归是连续值问题平方误差对大偏差的惩罚更重能推着框快速贴合判别项用二分类交叉熵而不是 softmax 交叉熵这是整套设计里最有意思的一处取舍。softmax 的语义是这些类别里必须选一个且只能选一个。危化品标志不满足这个前提一块牌可以同时标注易燃液体和毒性物质一辆车尾部也可能同时挂主标志牌和次要标志牌。用 softmax 会让两类分数互相压制训练时梯度方向互相打架表现为今天这个类涨、明天那个类掉。改成对每个类别独立做 sigmoid 加二分类交叉熵每个类别的判别就解耦了多标签天然支持。还有一个细节值得说明YOLOv3 的置信度回归目标不是简单的 0/1正样本的置信度目标取预测框与真值框的 IoU。这意味着置信度不只是一个有没有目标的概率它还携带了定位质量的信息——框贴得越准目标值越接近 1框歪了目标值就掉下来。所以推理阶段用置信度阈值过滤实际上同时在过滤定位差的框这就是为什么调整一个阈值能同时改善误检和框质量。4. 数据集与标注体系23 个类别、22174 个框是怎么定义出来的4.1 两级类别体系粗粒度属性 5 类与细粒度品类 18 类这套数据集收了 10000 张真实道路监控场景下的危化品运输车辆背面图像标注出 23 种与危化品运输种类相关的类别、共 22174 个标注框。23 这个数字不是拍脑袋定的它拆成了两层一层是危险化学品的属性分类5 类共 8419 个框另一层是危险化学品的品类分类18 类共 13755 个框。行业标准按危险品的主要特性和运输要求划分大类粗粒度这 5 类大致对应易爆、易燃、毒害、腐蚀、放射性这类直观属性细粒度这 18 类对应到具体品类用于判断该调什么处置剂、该按什么等级隔离。两级体系的价值在应急侧体现得最清楚。发生泄漏时救援人员第一眼需要知道的是这车会不会炸、有没有毒这是粗粒度标签能回答的而且粗粒度类别样本多、特征差异大比如爆炸品的橙色爆炸图形和腐蚀品的黑白分半图形形状差异极大召回率容易做高。细粒度标签回答的是具体是什么物质样本少、类间差异小同为易燃液体不同品名的标志牌可能只差一个数字和一行小字精度天然更低。做成两级等于给系统一个降级路径细分类置信度不够时至少还能输出粗属性而不是干脆什么都不报。工程上这通常落成两个并行的分类头共享同一个 backbone 和同一套候选框只在最后的分类分支上分叉。这样做的额外好处是两级标签互为约束——如果细分类预测出易燃液体但粗属性头给出的却是腐蚀两者矛盾这个框的置信度就该被打折能过滤掉一批明显错的预测。4.2 采集链路与标注文件从 4096×2160 到 416×416采集端是机器视觉抓拍系统监控摄像机拍背面弱光照环境用红外闪光灯补光采集图片像素为 4096×2160识别结果和部分原始图像经专有网络回传到监测中心。选背面而不是正面是因为标志牌按要求安装在车辆后部卡口相机从后方抓拍标志牌正好正对镜头形变最小。标注用 labelimg 完成每张图生成一个与原文件同名的 xml记录标注框的位置和类别annotation filename20200612_0830_back_00417.jpg/filename sizewidth4096/widthheight2160/heightdepth3/depth/size object nameflammable_liquid/name !-- 二级标签易燃液体 -- difficult0/difficult bndbox xmin1988/xminymin1274/ymin xmax2160/xmaxymax1466/ymax !-- 172×192 像素 -- /bndbox /object /annotation这个框对应到原图是 172×192 像素。如果预处理时直接把 4096×2160 硬 resize 到 416×416横向压缩 9.85 倍、纵向压缩 5.19 倍两个方向的缩放比差了一倍多正方形的菱形牌子会被压成扁菱形宽高比从 1.0 变成 0.53 左右前面聚类出来的先验框全部失配。所以这类宽高比悬殊的卡口图必须走 letterbox等比缩放后补灰边保证目标形状不被破坏代价是有效像素变少——4096×2160 等比缩到 416 宽实际高度只有 219 像素上下各补 98 像素灰边标志牌真正占的像素又掉了一截。这个账要在设计阶段就算清楚否则后面调参全是玄学。4.3 训练超参的取舍动量 0.9、权重衰减与分段学习率训练在深度学习服务器上完成动量设为 0.9权重衰减约为 5e-4训练周期 300在总周期的 70% 和 90% 两个节点做学习率衰减衰减系数 0.1cfg dict( epochs300, momentum0.9, # SGD 惯性加速沿一致方向的下降 weight_decay5e-4, # L2 正则1 万张图的小数据集必须压过拟合 lr01e-3, # 初始学习率 lr_decay_epochs[210, 270], # 300 的 70% 与 90% lr_decay_gamma0.1, # 每次衰减到原来的 1/10 img_size416, batch16, ignore_thresh0.5, # IoU 超过此值的非最优框不计损失 )动量 0.9 是 SGD 的常规取值作用是让参数更新带上历史方向穿过损失平面上那些窄谷权重衰减压到 5e-4 这个量级是因为一万张图对这个参数量的网络来说偏小不压正则很容易在训练后期把置信度推到极端值表现为验证集上该报的不报、不该报的乱报。分段衰减的用意是把训练切成三个阶段前 210 轮用大学习率把特征学粗中间 60 轮收窄搜索范围最后 30 轮用极小学习率精修定位。这个节奏比余弦退火更好控因为每个阶段的验证结果都能单独观察出问题容易定位是哪一段崩的。5. 排查清单小目标、夜间补光与类别长尾的五个坑5.1 数据与标注阶段的坑坑一白天指标漂亮夜间几乎全漏。现象是同一批车白天召回能到八成以上晚上掉到两成。原因是弱光照下靠红外闪光灯补光红外照明把图像里的颜色信息基本削掉了而危化品标志牌恰恰是强颜色编码的——橙底黑爆炸图形、红底白字、黑白分半颜色是最强判别特征。色彩一旦丢失模型只能靠形状和文字布局去认而形状在低分辨率下本就模糊。解决办法有两步一是采集端在夜间保留一路可见光曝光参数别完全依赖红外二是训练集里按昼、夜、雨、雾分别统计样本量做条件均衡采样别让夜间样本只占一成。坑二货真价实的小目标被预处理吃掉了一半。现象是远车道车辆的标志牌完全不召回近车道的都正常。原因前面算过172×192 的框在 letterbox 之后只剩 17×19 像素落在 52×52 那层而这一层配的最小先验框是 10×13、16×30、33×23跟 17×19 勉强匹配但有效特征只有两三个像素的差异CNN 很难区分。解决办法是把输入分辨率提上去416 提到 608或者干脆做两级流水线先用一个轻量检测器定位车辆和车牌按车辆框裁 ROI 放大后再送分类等效于把目标尺寸放大三到五倍。坑三标注框贴边太紧缩放后坐标取整把框抖掉。现象是训练 loss 抖动大收敛后框位置总是偏一点。原因是标注时框紧贴标志牌外沿1 到 2 个像素的余量都没有缩放 10 倍后这点余量直接消失加上坐标取整真值框和实际目标错开一到两像素而小目标的总尺寸才十几像素这点偏差占比很大。解决办法是标注规范里明确留 2 到 3 像素外扩别贴死。5.2 训练与上线阶段的坑坑四长尾类别几乎不召回模型退化成只认常见几种。5 类属性共 8419 个框均下来每类 1684 个18 类品类共 13755 个框均下来每类 764 个但实际分布是长尾的——易燃液体、易燃气体这类占了绝大多数放射性、感染性这类可能只有几十个框。负样本数量是正样本的上百倍普通交叉熵下这些稀有类的梯度被完全淹没。解决办法是分类损失按类别频率加权或者对稀有类做复制粘贴式的过采样评估时一定要看每类召回只看总体 mAP 会被常见类拉高掩盖掉稀有类全灭的事实。坑五换个卡口点就掉点anchor 和尺度分布全错位。现象是 A 点调好的模型搬到 B 点精度掉一大截。原因是不同点位的镜头焦距、安装高度、俯仰角不同同一块标志牌在图像里的像素尺寸分布整体偏移原先 k-means 聚出来的先验框和三个尺度各自负责的尺寸区间接不上。解决办法是把新点位的抓拍图先跑一遍统计画出真值框宽高的分布直方图跟先验框对比偏了就重新聚类import numpy as np from sklearn.cluster import KMeans # boxes: (N, 2)每行是一个真值框的宽高相对当前输入尺寸像素 def recluster(boxes, k9, iters300): km KMeans(n_clustersk, max_iteriters, n_init10).fit(boxes) anchors km.cluster_centers_ # 按面积从小到大排序前三组给 52x52 层中间给 26x26最后给 13x13 anchors anchors[np.argsort(anchors[:, 0] * anchors[:, 1])] return anchors.reshape(3, 3, 2) # (尺度, 该尺度 3 组, 宽高)聚类完之后别急着全量重训先冻结主干只调检测头跑几十轮看验证集的框回归损失是否明显下降再决定要不要放开全量微调。6. 设计取舍把识别标志牌当成工程问题而不是刷榜问题在 2020 年前后做这个选型YOLOv3 之所以被选中本质上是它在精度够用、延迟可控、多标签原生支持、部署简单这四个维度上没有明显短板。如果换到今天重做我会先算一笔账再决定把检测和识别拆成两级流水线——第一级只负责找到车辆和标志牌的大致区域第二级在放大后的 ROI 上做细分类。这样小目标问题被前处理解决了大半细分类还可以换成更轻的分类网络整体延迟反而可能低于单模型硬扛 608 输入。方案单帧延迟边缘设备量级小目标召回多标签部署难度两阶段检测器高随候选数线性涨中依赖候选框尺寸先验需改造分类头中算子多YOLOv3 416 单模型低低17×19 像素目标勉强原生支持低纯卷积易量化YOLOv3 608 单模型中中有效像素提升约 2 倍原生支持低检测车辆 ROI 裁切分类低到中两级可流水并行高等效放大三到五倍分类头天然支持中多一个模型要维护验证这套方案有没有真的 work别只看一个总体 mAP。我习惯把验证集按条件切桶昼、夜、雨雾各一份再按类别拆开分别算召回和误检率两级标签分开看混淆矩阵重点看粗属性对、细分类错的比例——这批数据恰恰说明降级输出是有效的粗属性对了就已经能给救援提供方向。阈值也不能一把尺子量到底漏检一辆满载易燃液体的车代价远大于多报几次误检但在隧道口这种需要立刻封路的位置误检带来的处置成本又很实在所以阈值应该按点位风险等级分开配置而不是全局一个数。还有一个容易被跳过的环节是误检的归因。上线初期一定会遇到把普通货车的反光贴认成标志牌这类问题光看指标查不出来得把误检样本按类别和时间聚一下看看是集中在某个点位、某个时段还是某种车型。我踩过最深的一次坑就是拿着一个总体指标看起来还行的模型直接推上线结果夜间的误检率是白天的六倍回头查才发现夜间训练样本只占全部数据的百分之八。从那以后我每次做完数据统计、都要强制走一遍按条件切桶的评估宁可上线晚一周也别让一个只在白天有效的模型跑到卡口上。希望帮到你。本文还有配套的精品资源点击获取