ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

多尺度YOLOv5交通灯检测:小目标漏检优化与RK3568部署

多尺度YOLOv5交通灯检测:小目标漏检优化与RK3568部署 简介这份文档面向自动驾驶与计算机视觉方向的学习者和研究者聚焦交通灯尺度小、环境复杂导致的检测难题系统讲解一种多尺度YOLOv5交通灯检测算法。内容涵盖复合数据增强、多尺度训练替代固定尺度训练、多尺度特征融合网络以及远跳链接传递不同层级信息等关键设计并给出在自建数据集与Bosch数据集上的对比实验检测速度最快9.5ms、mAP达99.8%较原YOLOv5提升17%Bosch上mAP增加6.5%可用于理解小目标检测的改进思路与实验验证方法。资源为单个docx文档压缩包约1.6MB结构紧凑适合作为算法复现、论文写作或课程报告的参考材料。目前已有194人学习下载便于快速把握多尺度特征融合与实时检测的实现要点。1. 多尺度YOLOv5做交通灯检测为什么小目标总在远距离翻车城市路口抓拍里交通灯在 1080P 画面中往往只占 20×40 像素远距离红灯甚至不到 15×15。用标准 YOLOv5s 直接训mAP 卡在 0.5 上下红灯漏检集中在 30 米开外——这不是数据不够是 P3 特征图下采样 8 倍后小目标只剩两三个像素的响应。多尺度 YOLOv5 交通灯检测要解决的就是这件事在 neck 部分增加高分辨率浅层特征融合让 15 像素的红灯也能被稳定召回。适合已经跑通过 YOLOv5 训练、但被小目标漏检卡住的算法工程师和嵌入式部署方向的同学。下面从结构改动、数据配置、训练参数到 RK3568 量化部署按我实际调过的路径讲一遍。2. 多尺度特征融合到底改哪里从 PANet 加一条 P2 支路说起2.1 标准 YOLOv5 在交通灯场景的三个短板YOLOv5 默认检测头挂在 P3(80×80)、P4(40×40)、P5(20×20) 上对应 640 输入下的 stride 8/16/32。交通灯属于典型的小目标加极端长宽比竖直灯 1:2 到 1:3标准结构有三个问题第一P3 的 80×80 网格对 15 像素目标只覆盖不到 2 个格子anchor 匹配阶段正样本就少回归梯度稀。第二P5 的 20×20 是为大目标设计的交通灯在 P5 上几乎无有效响应反而引入背景噪声。第三交通灯颜色红/黄/绿是强判别特征但经过多次下采样后颜色信息被稀释P4/P5 上的特征已经偏纹理而非颜色。常见做法是在 neck 里加一条 P2 支路stride 4、160×160 分辨率把浅层颜色和边缘信息直接送进检测头。代价是计算量上升约 30%但对 15~30 像素目标的召回提升明显。2.2 加 P2 检测层的结构改动我一般直接改models/yolov5s.yaml在 backbone 和 head 之间插入 P2 融合节点。核心改动是backbone 第 2 层输出stride 4经 C3 后作为 P2 特征再与上采样后的 P3 做 Concat最后接一个检测头。# models/yolov5s_traffic_light.yaml # 只列改动部分其余与官方 yolov5s.yaml 一致 backbone: # [from, number, module, args] [[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2 [-1, 1, Conv, [128, 3, 2]], # 1-P2/4 [-1, 3, C3, [128]], # 2 - 新增 P2 特征源 [-1, 1, Conv, [256, 3, 2]], # 3-P3/8 [-1, 6, C3, [256]], # 4 [-1, 1, Conv, [512, 3, 2]], # 5-P4/16 [-1, 9, C3, [512]], # 6 [-1, 1, Conv, [1024, 3, 2]], # 7-P5/32 [-1, 3, C3, [1024]], # 8 [-1, 1, SPPF, [1024, 5]]] # 9 head: [[-1, 1, Conv, [512, 1, 1]], # 10 [-1, 1, nn.Upsample, [None, 2, nearest]], [[-1, 6], 1, Concat, [1]], # cat backbone P4 [-1, 3, C3, [512, False]], # 13 [-1, 1, Conv, [256, 1, 1]], # 14 [-1, 1, nn.Upsample, [None, 2, nearest]], [[-1, 4], 1, Concat, [1]], # cat backbone P3 [-1, 3, C3, [256, False]], # 17 [-1, 1, Conv, [128, 1, 1]], # 18 - 新增P3 再上采样到 P2 [-1, 1, nn.Upsample, [None, 2, nearest]], [[-1, 2], 1, Concat, [1]], # cat backbone P2 [-1, 3, C3, [128, False]], # 21 - P2 融合输出 [-1, 1, Conv, [128, 3, 2]], # 22 - 下采样回 P3 [[-1, 18], 1, Concat, [1]], [-1, 3, C3, [256, False]], # 24 [-1, 1, Conv, [256, 3, 2]], # 25 [[-1, 14], 1, Concat, [1]], [-1, 3, C3, [512, False]], # 27 [-1, 1, Conv, [512, 3, 2]], # 28 [[-1, 10], 1, Concat, [1]], [-1, 3, C3, [1024, False]], # 30 [[21, 24, 27, 30], 1, Detect, [nc, anchors]]] # 四个检测头逻辑说明第 2 层 C3 输出 stride 4 的 P2 特征在 head 里从 P3 再上采样一次与之 Concat得到 160×160 的融合特征图作为第 21 层。随后 P2 下采样回 P3、P4、P5形成完整的双向融合。Detect 层从三个头变成四个头anchors 也要相应增加一组。参数说明nc改成你的类别数交通灯一般 4 类红、黄、绿、off。anchors 需要重新聚类P2 层的 anchor 要更小我实测在 640 输入下 P2 用[4,5, 8,10, 13,16]比较稳。C3的False表示不用 shortcut浅层特征通道少加 shortcut 收益不大还占显存。2.3 重新聚类 anchor 的最小脚本P2 头加进去后原来的 anchor 完全不匹配必须用自己数据集的标注框重新 k-means。我一般用下面这个脚本输入是 YOLO 格式的 labels 目录。# tools/cluster_anchors.py import numpy as np from pathlib import Path def load_boxes(label_dir, img_size640): boxes [] for txt in Path(label_dir).glob(*.txt): for line in txt.read_text().strip().splitlines(): _, _, w, h map(float, line.split()) boxes.append([w * img_size, h * img_size]) return np.array(boxes) def iou(box, clusters): # box: (w,h), clusters: (k,2) x np.minimum(box[0], clusters[:, 0]) y np.minimum(box[1], clusters[:, 1]) inter x * y union box[0] * box[1] clusters[:, 0] * clusters[:, 1] - inter return inter / union def kmeans(boxes, k12, iters300): n boxes.shape[0] idx np.random.choice(n, k, replaceFalse) clusters boxes[idx] for _ in range(iters): dist 1 - np.array([iou(b, clusters) for b in boxes]) near dist.argmin(axis1) for i in range(k): if (near i).any(): clusters[i] boxes[near i].mean(axis0) return clusters if __name__ __main__: boxes load_boxes(datasets/traffic_light/labels/train) clusters kmeans(boxes, k12) # 按面积排序前 3 个给 P2中间 3 个给 P3以此类推 clusters clusters[np.argsort(clusters.prod(axis1))] for i, c in enumerate(clusters): print(fanchor {i}: {c[0]:.1f},{c[1]:.1f})逻辑说明用 1-IoU 作为距离度量做 k-means比欧氏距离更贴合检测任务。k12 是因为四个检测头各 3 个 anchor。跑完后按面积从小到大排序前 3 个给 P2依次分配。参数说明img_size要和训练输入一致如果你用 1280 训练就改成 1280。iters300 足够收敛再多收益很小。聚类结果直接替换 yaml 里的 anchors 字段。3. 数据集准备与训练参数从 VOC 转 YOLO 到超参怎么调3.1 交通灯数据集的标注规范与转换交通灯数据集常见来源是 Bosch Small Traffic Lights、LISA、以及自己路口采集。Bosch 是 1280×720 的 PNG标注是 CSVLISA 是 .csv 加 .avi。统一转成 YOLO 格式时有两个坑一是 Bosch 的框有大量小于 8 像素的直接训会拉低 mAP我一般过滤掉宽或高小于 6 像素的二是交通灯有 off 状态要不要单独成类取决于你的业务如果只关心红绿灯把 off 和 unknown 合并成背景。# tools/bosch2yolo.py import csv from pathlib import Path from PIL import Image CLASS_MAP {red: 0, yellow: 1, green: 2, off: 3} def convert(csv_path, img_dir, out_dir): out Path(out_dir) out.mkdir(parentsTrue, exist_okTrue) with open(csv_path) as f: reader csv.DictReader(f) for row in reader: img_path Path(img_dir) / row[path] if not img_path.exists(): continue w, h Image.open(img_path).size x, y, bw, bh map(float, (row[x_min], row[y_min], row[x_max], row[y_max])) bw, bh bw - x, bh - y if bw 6 or bh 6: # 过滤极小框 continue cx, cy (x bw / 2) / w, (y bh / 2) / h cls CLASS_MAP.get(row[label], 3) line f{cls} {cx:.6f} {cy:.6f} {bw/w:.6f} {bh/h:.6f}\n with open(out / (img_path.stem .txt), a) as fo: fo.write(line) if __name__ __main__: convert(datasets/bosch/train.csv, datasets/bosch/train, datasets/traffic_light/labels/train)逻辑说明逐行读 CSV把绝对坐标转成归一化的中心点加宽高。bw 6 or bh 6这行是关键Bosch 里有大量 3~5 像素的框留着会让 P2 头的正样本被噪声主导。参数说明CLASS_MAP按你的类别顺序改YOLO 的类别索引从 0 开始。cx/cy保留 6 位小数足够再多是浪费。3.2 训练超参为什么 lr0 要降到 0.005加了 P2 头后浅层特征梯度比原来大用默认lr00.01容易在前期震荡。我一般降到 0.005配合warmup_epochs5。另外交通灯数据集通常类别极不平衡绿灯样本远多于红灯cls损失权重要调。# 训练命令 python train.py \ --cfg models/yolov5s_traffic_light.yaml \ --data data/traffic_light.yaml \ --weights yolov5s.pt \ --img 640 \ --batch-size 16 \ --epochs 150 \ --lr0 0.005 \ --lrf 0.01 \ --warmup-epochs 5 \ --hyp data/hyps/hyp.traffic_light.yaml \ --cache逻辑说明--cfg指向改过的四头结构--weights用官方预训练权重做迁移浅层卷积仍能复用。--cache把图片缓存到内存交通灯数据集一般几万张16G 内存够用能省 30% 的 IO 时间。参数说明lr0是初始学习率lrf是最终学习率比例余弦退火到lr0*lrf。warmup-epochs5让前 5 个 epoch 学习率从 0 线性升到 lr0避免浅层被大梯度冲垮。hyp.traffic_light.yaml里我把cls0.8默认 0.5box0.05默认 0.05 不变因为类别不平衡时分类损失需要更大权重。3.3 验证时看哪几个指标训练日志里不要只看 mAP0.5。交通灯场景我重点看三个metrics/mAP_0.5:0.95反映定位精度metrics/precision反映误检红灯误报很烦以及按类别拆分的 AP。YOLOv5 默认不输出每类 AP需要改val.py里的ap_per_class调用或者在test.py里加--verbose。如果红灯 AP 明显低于绿灯先查标注里红灯框是不是普遍更小远距离红灯多如果是考虑在数据加载时对红灯样本做额外增强比如mosaic概率调高、copy_paste复制小目标。4. 避坑与排查多尺度交通灯检测的 5 个血泪教训4.1 现象加 P2 后 mAP 反而降了 3 个点原因P2 头引入大量低质量正样本浅层特征语义弱背景被误判为目标。解决在hyp.yaml里把 P2 对应的 anchor 匹配阈值调严或者用--img 1280训练让 P2 分辨率到 320×320语义信息更充分。我实测 1280 输入下 P2 头才真正发挥价值。4.2 现象训练 loss 正常但验证时红灯全漏原因验证集和训练集的交通灯尺寸分布不一致训练集多是近景大灯验证集有远距离小灯。解决划分数据集时按目标尺寸分层采样保证验证集里小目标占比不低于 20%。用labels/*.txt统计框面积按分位数分层。4.3 现象RK3568 量化后精度掉 15 个点原因P2 头输出 160×160×18量化时激活值范围大int8 截断严重。解决量化校准集要包含远距离小目标样本且用KL散度校准而非minmax。另外 P2 头的输出层可以保留 fp16只量化 backbone。4.4 现象推理时同一帧红灯框抖动严重原因多尺度融合后相邻尺度的预测框重叠NMS 的 IoU 阈值 0.45 对交通灯偏大竖直灯框重叠度高。解决把 NMS IoU 降到 0.3或者用--agnostic-nms合并跨类重叠。交通灯类别互斥agnostic NMS 在这里很合适。4.5 现象训练到 80 epoch 后 mAP 突然崩原因余弦退火后期学习率太小P2 头浅层梯度消失BN 统计量漂移。解决把lrf从 0.01 提到 0.05或者在后 30 epoch 冻结 backbone 只训 head。我一般用后者稳定且省时间。5. 部署到 RK3568量化、推理与一个验证技巧RK3568 的 NPU 算力 0.8T跑 640 输入的 YOLOv5s 四头结构大概 25 FPS够路口实时。部署路径是 ONNX → RKNN。关键在量化配置P2 头是精度重灾区。# rknn_convert.py from rknn.api import RKNN rknn RKNN() rknn.config( mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3568, quantized_dtypeasymmetric_quantized-8, quantized_algorithmkl_divergence, # 关键KL 校准 optimization_level3, ) rknn.load_onnx(modelyolov5s_traffic_light.onnx) rknn.build(do_quantizationTrue, datasetcalib.txt) # calib 含小目标样本 rknn.export_rknn(yolov5s_traffic_light.rknn)逻辑说明quantized_algorithmkl_divergence比默认的normal更适合激活分布长尾的检测模型。calib.txt里每行是一张校准图路径我一般放 300 张其中至少 100 张是远距离小目标场景。参数说明mean_values和std_values要和训练时的归一化一致YOLOv5 是 0~1 归一化所以 mean0、std255。optimization_level3会做算子融合但偶尔会融合掉 P2 的 Concat如果精度异常就降到 2。验证量化后精度有个技巧不要只看整体 mAP把验证集按目标像素面积分成三档20²、20²~40²、40²分别算 AP。P2 头的价值全在小目标档如果小目标档 AP 掉超过 5 个点说明校准集不够或 KL 没生效回去补小目标样本。我自己踩过的坑是校准集直接用了训练集随机 300 张结果小目标占比不到 5%量化后小目标 AP 从 0.62 掉到 0.41。后来按面积分层采样小目标档 AP 回到 0.58整体只掉 2 个点。这个分层验证的习惯我一直保留比看整体数字有用得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表