简介:围绕危化品车辆货物类型识别这一课题,这份PDF文献面向交通管理、道路安全监控与计算机视觉方向的学习者和工程人员,重点解决高速公路实时场景下危险货物类型自动判别的问题。文中以YOLOv3单阶段目标检测模型为主线,结合基于机器视觉的车辆抓拍系统、红外补光采集方案与包含23类标注的危化品车辆数据集,讨论数据标注、模型训练、损失函数优化及不同光照和天气条件下的识别表现,并给出实验设置与结果分析。资源包共1个PDF文件,大小约2.41MB,适合按章节查阅研究背景、方法设计与实验结论。目前已有107人学习,可作为了解YOLO在交通场景落地、撰写相关课程报告或方案论证时的参考材料。
1. 一块贴在车尾的菱形牌,暴露了通用检测模型的软肋
在 4096×2160 的卡口原图里,一块危化品车辆货物类型标志牌通常只有 170×190 像素左右;把它等比压到 YOLOv3 常用的 416×416 输入,这块牌子就只剩 17×19 像素,比模型最细那一层的感受野还小。所以危化品车辆货物类型识别这类目标检测任务,真正难的地方并不在"看出这是辆车",而在于四个数量级的尺度压缩之后,模型还得分辨出牌子上画的到底是易燃液体、氧化性物质还是腐蚀品。YOLOv3 在 2020 年前后被大量卡口项目选中,不是因为它在公开榜单上最漂亮,而是因为它的三尺度输出、多标签损失和可嵌入式部署的组合,恰好卡在这个场景的约束里。搞懂这套组合为什么能对上,比记住一个模型名字有用得多。
2. 从两阶段到单阶段:卡口实时识别为什么最终选择 YOLOv3
2.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_size=416): """ pred: (B, S, S, 3, 4 + 1 + C) 网络原始输出 anchors: (3, 2) 该尺度负责的三组先验框宽高(像素单位) stride: img_size // S,13->32,26->16,52->8 """ B, S = pred.shape[0], pred.shape[1] # 网格左上角坐标,形状 (1, S, S, 1, 2) gy, gx = np.meshgrid(np.arange(S), np.arange(S), indexing='ij') 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> <filename>20200612_0830_back_00417.jpg</filename> <size><width>4096</width><height>2160</height><depth>3</depth></size> <object> <name>flammable_liquid</name> <!-- 二级标签:易燃液体 --> <difficult>0</difficult> <bndbox> <xmin>1988</xmin><ymin>1274</ymin> <xmax>2160</xmax><ymax>1466</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.1:
cfg = dict( epochs=300, momentum=0.9, # SGD 惯性,加速沿一致方向的下降 weight_decay=5e-4, # L2 正则,1 万张图的小数据集必须压过拟合 lr0=1e-3, # 初始学习率 lr_decay_epochs=[210, 270], # 300 的 70% 与 90% lr_decay_gamma=0.1, # 每次衰减到原来的 1/10 img_size=416, batch=16, ignore_thresh=0.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, k=9, iters=300): km = KMeans(n_clusters=k, max_iter=iters, n_init=10).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。我习惯把验证集按条件切桶:昼、夜、雨雾各一份,再按类别拆开,分别算召回和误检率;两级标签分开看混淆矩阵,重点看粗属性对、细分类错的比例——这批数据恰恰说明降级输出是有效的,粗属性对了就已经能给救援提供方向。阈值也不能一把尺子量到底:漏检一辆满载易燃液体的车,代价远大于多报几次误检,但在隧道口这种需要立刻封路的位置,误检带来的处置成本又很实在,所以阈值应该按点位风险等级分开配置,而不是全局一个数。
还有一个容易被跳过的环节是误检的归因。上线初期一定会遇到"把普通货车的反光贴认成标志牌"这类问题,光看指标查不出来,得把误检样本按类别和时间聚一下,看看是集中在某个点位、某个时段还是某种车型。我踩过最深的一次坑,就是拿着一个总体指标看起来还行的模型直接推上线,结果夜间的误检率是白天的六倍,回头查才发现夜间训练样本只占全部数据的百分之八。从那以后我每次做完数据统计、都要强制走一遍按条件切桶的评估,宁可上线晚一周,也别让一个只在白天有效的模型跑到卡口上。希望帮到你。
本文还有配套的精品资源,点击获取