news 2026/9/30 5:28:15

多尺度YOLOv5交通灯检测实战:小目标召回与调参避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多尺度YOLOv5交通灯检测实战:小目标召回与调参避坑指南

简介:面向自动驾驶与智能交通系统研究者的一份技术文档,系统阐述多尺度YOLOv5交通灯检测算法。针对交通灯尺度小、环境复杂导致检测困难的问题,方案提出复合数据增强、多尺度训练与多尺度特征融合网络相结合的策略,引入远跳链接传递不同级别信息,以提升模型对小目标的检测能力。实验数据表明,改进YOLOv5在采集数据集上检测速度最快可达9.5ms,mAP达99.8%,相比YOLOv5提升17%;在Bosch数据集上mAP增加6.5%,实现实时高精度检测。文档完整覆盖中英文摘要、研究背景、数据集制作、算法原理、消融实验等模块,并附有具体实验参数与结果对比,适合深度学习目标检测方向的学生、算法工程师及自动驾驶从业者作为算法设计与论文写作的参考。资源为1个docx文档,压缩包大小1.6MB,已有194人学习下载。

1. 多尺度YOLOv5交通灯检测:小目标才是真正的坎

多尺度YOLOv5的交通灯检测算法,就是在YOLOv5框架上,把特征层、训练输入、推理尺度都做成多尺度,专门解决交通灯这种小目标、变尺度目标的检测问题。交通灯在画面里往往不到三十个像素,远距离甚至只有十几个像素,而近距离的灯体又能占据上百像素;再加上横排、竖排、圆形灯体三种形态,单尺度模型要么盯着大灯丢掉远方的小灯,要么顾着小目标让近处框体漂移。这个方案的核心价值,是把这类尺度跨度大的目标稳定捡回来,同时保持YOLOv5的实时性。

做辅助驾驶、智能路口巡检、无人配送车过闸口的工程师,都会被这类需求卡住。如果你手里正好有一批路口图像或视频,想在不大改网络结构的前提下快速提升小目标召回,这篇文章可以直接照做。

2. 多尺度机制与YOLOv5结构:交通灯为什么必须吃透P3/P4/P5

2.1 交通灯目标尺寸分布与P3/P4/P5检测头

先看一组让我印象很深的数据:1080p路口图像里,中等距离的交通灯大概只有 15×30 像素,远距离的甚至不到 10×20 像素。如果按 YOLOv5 默认的 640 输入缩放,1920 宽的图像要被压缩到三分之一,这个交通灯在输入图上就只剩 5×10 像素左右。这个尺寸几乎榨干了卷积特征,能保留下来的信息非常有限。

YOLOv5 用三个检测头覆盖不同尺度:P3 在 stride 8 的特征图上做检测,适合 32×32 以下的小目标;P4 在 stride 16 上负责中等目标;P5 在 stride 32 上负责大目标。交通灯这种物体天然跨在 P3 和 P4 之间,远距离的更需要 P3。但 P3 本身来自较浅的层,语义信息不够强,于是“多尺度”不能只是把三个头摆在那里,还要让训练和推理阶段都绕着 P3 转。

检测头特征图尺寸(640 输入)stride适合目标尺寸交通灯场景
P380×8088×8 ~ 32×32远距离小灯、箭头灯
P440×401632×32 ~ 64×64中等距离的灯组
P520×203264×64 以上近距离大灯、满屏灯

不少项目会在 P3 前面再引出一个 P2 检测头(stride 4),专门兜住极端小目标。这个做法本质上是给多尺度再加一档,代价是计算量上升,后面我会讲什么时候值得加。

2.2 锚框聚类:默认锚框为什么让交通灯检测吃亏

YOLOv5 默认锚框是从 COCO 数据集聚类出来的,特征是大而全,形状偏方形。交通灯不一样,横排箭头灯的宽高比能到 3:1 甚至 4:1,竖排灯组接近 1:3,圆形灯才接近 1:1。你拿着 COCO 锚框去匹配交通灯,损失函数从一开始就处于不利位置,小目标漏检几乎是必然结果。

常见做法是拿自己的训练集标注重新聚类锚框。YOLOv5 会自动做这件事,但它是在训练启动时基于整批数据算的,结果不一定是你想要的。我更习惯先跑一段独立聚类,把锚框显式写进模型的 yaml 里面,训练时加--noautoanchor关掉自动计算,保证可复现。

import numpy as np from pathlib import Path from sklearn.cluster import KMeans def auto_anchors(label_dir, img_size=640): # 读取所有 YOLO 格式的 txt,还原成像素宽高 w_list, h_list = [], [] for txt in Path(label_dir).rglob("*.txt"): for line in txt.read_text().strip().splitlines(): parts = line.split() if len(parts) != 5: continue _, _, bw, bh = map(float, parts[1:]) if bw == 0 or bh == 0: continue w_list.append(bw * img_size) h_list.append(bh * img_size) X = np.stack([w_list, h_list], axis=1) km = KMeans(n_clusters=9, random_state=0).fit(X) # 按面积从小到大排列,P3 用小的,P5 用大的 anchors = np.sort(km.cluster_centers_, axis=0) return anchors # 然后在生成的 yaml 里按 [w,h] 对填进 anchors 段

这段代码的核心是让锚框贴合你的真实标注分布。排序后面积小的三个锚框通常落在 P3,负责远距离小灯;面积大的落在 P5。需要注意img_size必须和训练输入尺寸保持一致,你训练用 640 就按 640 聚类,别用原图 1920 的尺寸算完再填进去,那样锚框整体偏大。

2.3 多尺度训练:--multi-scale和 mosaic 怎么配合

YOLOv5 在训练时自带一个多尺度开关:--multi-scale。它会随机把输入图缩放到 0.5 到 1.5 倍之间,相当于人为制造“灯离得近一点”和“灯离得远一点”两种样本,模型被迫学会在不同尺度下输出一致的特征。这个开关配合 mosaic 增强效果非常明显。mosaic 会把四张图拼成一张,每张图本身已经不同程度缩放,再叠加--multi-scale的随机缩放,尺度扰动就足够充分了。

python train.py \ --data traffic_light.yaml \ --cfg yolov5s_tl.yaml \ --weights yolov5s.pt \ --img 640 \ --multi-scale \ --batch 32 \ --epochs 100 \ --cache ram \ --device 0 \ --project runs/traffic_light

这里--multi-scale开启后,每个 batch 的输入尺寸都会变化,PyTorch 这边会自动做 padding,不需要你手动改代码。代价是显存峰值变高,如果你原本 batch 32 能跑,开了多尺度后建议降到 24 或 16,否则容易 OOM。--img 640表示基准输入尺寸,实际训练时会以 640 为中心随机缩放,并不是固定 640。

我一般不建议在数据量很少(比如只有几百张)时直接开--multi-scale。这时候模型还没见过足够多的真实分布,强行加入尺度扰动容易把已经学到的形状特征打散。常见的可靠顺序是:先用固定尺寸跑通,确认 loss 在下降,再开--multi-scale做第二轮微调。

2.4 推理时的多尺度:TTA 在交通灯场景里的取舍

YOLOv5 的--augment是推理阶段的测试时增强,原理是把同一张图缩放到 0.83、1.0、1.2 等倍数分别推理,再把检测结果融合到一起。相当于用三倍推理时间换几个点的 mAP。对离线处理一批图片,这个买卖划算;对路口实时视频流,帧率直接掉一半以上,大多数项目接受不了。

我实际项目里的做法是:训练时开足多尺度,让模型把尺度变化学到权重里;推理时只做普通单尺度推理,必要时对远距离小目标单独切图放大一轮。这样既保住帧率,又不会背上学了个多尺度却在推理时完全用不上的尴尬。TTA 适合在评估验证集、算最终指标的时候开一次,看模型真实上限,然后单尺度跑线上。

3. 用多尺度YOLOv5训练交通灯检测模型:从数据到调参

3.1 数据集准备:大图切块、标注与多尺度增强

交通灯数据最常见的坑就是原图分辨率太高,强行 resize 到 640 后小目标直接消失。比如 4K 路口的图像里,一个 40×40 像素的交通灯缩到 640 宽后就只剩 13×13,再经过网络的 stride 8 下采样,到 P3 特征图上只剩 1-2 个像素。解决这个问题有两个方向:一是训练时用更大的--img 960,二是先把原图切块再做模型输入。

我建议先切块。切块时要把标注框一起处理:目标中心落在哪一块,就归哪一块,并且坐标要做偏移。下面是一个常见的切块逻辑,overlap 控制相邻块之间的重叠比例,避免目标正好卡在边界上。

import cv2 from pathlib import Path def split_image_with_labels(img_path, txt_path, out_dir, tile=640, overlap=0.2): img = cv2.imread(str(img_path)) H, W = img.shape[:2] step = int(tile * (1 - overlap)) lines = txt_path.read_text().strip().splitlines() idx = 0 for y1 in range(0, H, step): for x1 in range(0, W, step): y2 = min(y1 + tile, H) x2 = min(x1 + tile, W) y1c, x1c = y2 - tile, x2 - tile # 保证 tile 尺寸 crop = img[y1c:y2, x1c:x2] new_labels = [] for line in lines: cls, nx, ny, nw, nh = map(float, line.split()) # 还原到原图像素坐标 bx = nx * W by = ny * H bw = nw * W bh = nh * H cx, cy = bx + bw / 2, by + bh / 2 # 只保留中心点落在当前块内的目标 if x1c < cx < x2 and y1c < cy < y2: lx = (cx - x1c) / tile ly = (cy - y1c) / tile lw = bw / tile lh = bh / tile new_labels.append(f"{cls} {lx:.4f} {ly:.4f} {lw:.4f} {lh:.4f}") if new_labels: out_img = out_dir / "images" / f"{Path(img_path).stem}_{idx:04d}.jpg" out_txt = out_dir / "labels" / f"{Path(img_path).stem}_{idx:04d}.txt" cv2.imwrite(str(out_img), crop) out_txt.write_text("\n".join(new_labels)) idx += 1

注意这个脚本里y1c, x1c = y2 - tile, x2 - tile是为了让靠近边缘的块也能取到完整的 tile,避免最后一块尺寸不足。overlap 设 0.2 时,640 的块步长是 512,原图中一个 100×100 的灯最多只会被两个相邻块各切一半,不至于三个块都丢失。切完块之后,我还会对图像做一组离线增强:亮度变化、高斯模糊、过曝模拟。交通灯在夜间和逆光下最容易翻车,离线增强里必须覆盖这两类退化。

3.2 修改模型配置:类别数、锚框和输入尺寸

数据准备好之后,改两个 yaml 文件。先看数据配置traffic_light.yaml:

train: ./dataset/train/images val: ./dataset/val/images nc: 4 names: ["red", "green", "yellow", "off"]

这里nc是类别数。交通灯按颜色分四类是常见做法:红、绿、黄、灭。有些项目把“左转箭头”“直行箭头”也拆开,类别数会变成 6 到 8 个,但初次试验我建议先按颜色分,把检测难度和分类难度分开排查。train和val的路径最好写绝对路径或相对项目根目录的路径,YOLOv5 对相对路径的解析在不同版本里行为不一致,这里踩过的人不少。

再看模型配置yolov5s_tl.yaml:

nc: 4 depth_multiple: 0.33 width_multiple: 0.50 anchors: - [8, 10, 12, 22, 18, 15] - [28, 25, 38, 45, 55, 52] - [70, 68, 110, 95, 160, 145]

anchors是三行,分别对应 P3、P4、P5 检测头,每行三个 [w, h] 对。这里给到的数值是我按常见交通灯标注聚类后的量级,直接抄过去不一定适合你的数据集,建议还是用你自己的数据跑 2.2 的聚类脚本生成。depth_multiple和width_multiple保持默认的 0.33 / 0.50 就是 yolov5s 的体量;如果目标是边缘部署,可以继续用 s,不需要轻易上 m 或 l,交通灯本身类别少、形状单一,s 级模型通常够用。

3.3 训练命令与超参数调优:--multi-scale怎么用最稳

训练命令在 2.3 已经给过,这里重点讲参数怎么调。交通灯数据通常只有几千张,和 COCO 那种百万级数据不一样,训练超参数必须跟着数据量走。

超参数默认值交通灯推荐值说明
lr00.010.005 ~ 0.01数据量小时用 0.005 更稳
lrf0.20.1 ~ 0.2余弦退火的终点比例
momentum0.9370.937保持默认
weight_decay0.00050.0005保持默认
warmup_epochs35小目标数据建议多热身几轮
batch1616 ~ 32开了 multi-scale 就取小值
img640640 或 960以 P3 为主就上 960

warmup_epochs这个参数很容易被忽略。交通灯标注框普遍偏小,如果模型前几轮就用大学习率跑,预训练权重里的特征很快会被小目标的梯度冲乱。把 warmup 从 3 加到 5,让前几个 epoch 慢慢把锚框和特征对齐,后面 loss 会更平滑。

开启--multi-scale之后,batch建议减半。原因很简单:每个 batch 的输入尺寸都不同,PyTorch 要重新分配缓冲,显存峰值比固定尺寸高出不少。如果你的显卡是 8GB 显存,--img 640 --multi-scale时 batch 16 基本是安全线;强行上 32 很容易在训练中途 OOM,前十几个 epoch 白跑。

3.4 后处理与推理参数:conf-thres 和 iou-thres 怎么定

训练完不是直接丢进 detect.py 就完事。交通灯场景的后处理参数和通用目标检测差别很大,默认的conf-thres 0.25在这类小目标上会把大量远距离灯过滤掉。

python detect.py \ --weights runs/traffic_light/exp/weights/best.pt \ --source intersection_4k.mp4 \ --conf-thres 0.2 \ --iou-thres 0.45 \ --max-det 20 \ --save-txt

conf-thres 0.2是我自己在交通灯项目里的常用值。红色和黄色的灯在图像里颜色特征鲜明,模型输出的置信度通常会比较高;但绿色灯在逆光下对比度低,置信度会被压到 0.2 附近,阈值设太高会漏掉。iou-thres 0.45保持默认就行,交通灯目标小、互相重叠少,不需要像行人检测那样拉高到 0.7。max-det 20是限制单张图最多输出 20 个框,路口正常也就 6 到 12 个灯,留点余量防误检。

注意:--augment虽然能提升一点 mAP,但对视频流帧率影响太大。如果只是验证单张图片,可以开;一旦要接实时管线,直接关掉。

4. 多尺度YOLOv5交通灯训练避坑:5个高频翻车现场

4.1 小目标直接漏检

现象:远处的小交通灯完全没有框,近距离的灯一切正常。开--multi-scale之后漏检反而更严重了。

原因:训练时不加区分地把整张图缩放到 0.5 倍,远距离的小灯在输入图上变成 3×5 像素,切到 P3 层后就只剩一个点。多尺度训练确实增加了尺度多样性,但对极端小目标来说,“缩过头”等于让模型被迫学习噪声。

解决:先不要全图缩放,改用 3.1 的切块方案,让远距离小灯在输入图里仍然保持 20 像素以上。调大--img 960,或者给 P3 前面加 P2 检测头。如果验证集里大部分漏检目标小于 10 像素,P2 头比调参更直接。

4.2 红灯绿灯误检混淆

现象:检测框位置都对了,就是颜色判断错,红灯被贴成绿灯。误检主要集中在夜间和路灯、刹车灯附近。

原因:纯视觉检测模型把颜色当作一个隐式特征,没有独立的颜色分支。夜间过曝时红色变成橙白色,训练样本里没覆盖这个分布,模型就会靠形状猜颜色。另外一个原因是标注时有人把红灯的高亮区域标成了灭灯状态,类别边界不一致,模型学出一套奇怪的颜色映射。

解决:标注规范里明确“亮灯”和“灭灯”的判定标准。数据增强里保留 HSV 颜色扰动,但不要做灰度化,那会让颜色信息直接消失。更可靠的做法是在检测头后面加一个颜色分类分支,先定位再分色,这个我会在下一章展开。

4.3 夜间和逆光场景全面退化

现象:白天测试 mAP 有 0.91,夜间直接掉到 0.6 以下。画面里图像噪点多,灯体周围的泛光让检测框明显偏大。

原因:训练集里夜间图占比太少,模型没有见过暗光条件下的灯体边缘特征;交通灯的泛光会形成一个明显的高斯光晕,模型误以为整个光晕都是灯体。

解决:夜间图像要么单独加 20% 到 30% 进训练集,要么用离线增强模拟。我常用的是把亮度降到 30% 并叠加高斯模糊,模拟过曝和光晕。更粗暴但有效的做法是把夜间图的标注框往灯体中心收缩 10%,让模型学习灯芯而不是光晕。

4.4 训练中 loss 震荡不收敛

现象:前 20 个 epoch loss 正常下降,之后开始反复弹跳,mAP 卡在 0.7 上不去。关闭--multi-scale后问题减轻,但 mAP 上限被压住。

原因:--multi-scale导致每个 batch 的输入尺度不同,BN 的统计量一直在波动。数据量小的时候,这种波动会放大,模型在“适应大图”和“适应小图”之间来回切换,权重更新方向不稳定。

解决:先用固定尺寸训练到 loss 平稳,再加载 best.pt 开--multi-scale做第二轮微调。微调时用更小的学习率lr0 0.003,warmup 可以关掉。这套做法相当于把“尺度泛化”作为精调阶段,而不是从零就掺进去。

4.5 推理帧率不达标

现象:模型在 PC 上有 60 FPS,部署到树莓派5这类 ARM 板子上只剩 8 FPS。开多尺度推理后更离谱,直接掉到 3 FPS。

原因:P3/P4/P5 三个检测头在 CPU 上计算量很大,多尺度推理等于把三倍计算量叠上去,ARM 板子扛不住。

解决:部署前把输入尺寸固定到 640,推理阶段关掉 TTA。导出 ONNX 后用 INT8 量化,P3 的特征图占用会明显下降。如果仍然满足不了帧率,考虑切开检测头和主干,把前 6 层共享的计算固化,只在最后两层做多尺度分支。

5. 模型验证与进阶:用误检率和大图切块逼近上线

5.1 用误检率而不是单纯 mAP 来验收

交通灯场景里,漏检和误检的代价不一样:漏掉一个红灯可能意味着安全事故,误检一个红灯则会让车辆无故刹车。所以我评估模型时,除了看 mAP@0.5,还单独统计误检率。做法很简单,准备一组不包含交通灯的负样本路口图,跑一遍模型,记下所有置信度高于 0.2 的误检框数量;误检率超过 5% 就不允许上线。

验证维度指标通过线
检出能力mAP@0.5≥ 0.90
小目标召回AR@[0.01, 0.5]≥ 0.85
负样本误检率FP per image≤ 0.05
颜色准确率分类 F1≥ 0.95

val.py能直接输出 mAP 和混淆矩阵。但我从来不只看自动报告,还会把误检框可视化到图上,人工扫一遍。很多误检集中在路灯、车尾灯、交通标志的反光边缘,这些通过可视化一眼就能看出来。

5.2 大图切块推理:4K 路口的实用技巧

对 4K 路口图像,模型输入固定 640 后必然丢失小目标。我的做法是先切块推理,再把检测框坐标映射回原图。关键有两点:相邻块之间设置 20% 重叠,让跨边界的目标在至少一个块里保持完整;所有块的结果汇总到全图坐标后,再做一次全局 NMS 去重,避免同一个灯被两个块重复检出。

import cv2 import numpy as np import onnxruntime as ort session = ort.InferenceSession("best.onnx") def nms(boxes, iou_thres=0.45): # 按置信度排序,依次抑制与当前框 IoU 过大的框 # 返回保留的索引列表 pass def detect_crop(img_crop): # letterbox 到 640,预处理后交给 session.run # 输出 shape: [1, 25200, 85],解出 cx, cy, w, h, conf, cls # 把坐标映射回 img_crop 的原始尺寸 pass def detect_large_image(img): tile = 640 step = int(tile * 0.8) H, W = img.shape[:2] all_boxes = [] for y1 in range(0, H, step): for x1 in range(0, W, step): y2 = min(y1 + tile, H) x2 = min(x1 + tile, W) crop = img[y1:y2, x1:x2] boxes = detect_crop(crop) for box in boxes: # 局部坐标加回偏移,变成全图坐标 box["x1"] += x1 box["y1"] += y1 all_boxes.append(box) return nms(all_boxes)

这段代码省掉了 letterbox 和 NMS 的具体实现,因为它们和 YOLOv5 后处理逻辑是标准流程。注意step = tile * 0.8就是重叠率 20% 的实现。切块推理得到的框是像素坐标,映射回原图后不需要任何尺度恢复计算,因为它本来就是原图像素。

如果目标设备是树莓派5这类 ARM 边缘板,我建议提前用 ONNX 导出并在板子上试跑一版,别等整个管线做完了才发现算力不够。导出 ONNX 用export.py --weights best.pt --include onnx,板子上用 onnxruntime 加载即可。

我做交通灯检测时踩过最大的坑,就是把注意力全放在网络结构上,却忽略了验证集本身不具备代表性:白天样本多、夜间样本少、负样本缺失。后来我改成“白天验证集 + 夜间验证集 + 纯负样本集”三套验证集,才真正把模型推向可上线状态。这个习惯沿用到了其他小目标项目里,希望你也能少走这段弯路,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 5:28:13

TensorFlow 2.x实战指南:从环境搭建到模型部署与选型

入门那会儿我也没想到&#xff0c;TensorFlow这名字会跟着我走这么多年。从1.x时代写tf.Session()的别扭&#xff0c;到2.x时代Keras一把梭的舒坦&#xff0c;它几乎见证了深度学习框架从“极客玩具”变成“工程标配”的全过程。很多新手一上来就被各种概念劝退&#xff0c;什么…

作者头像 李华
网站建设 2026/9/30 5:28:02

Superio配置空间深度实操:从进入键到寄存器读写的完整指南

做x86平台开发的兄弟&#xff0c;基本都绕不过Superio这颗芯片。它不像CPU、内存那样天天上头条&#xff0c;但真到系统里串口不工作、风扇转速读不出来、GPIO控制不生效的时候&#xff0c;你迟早得跟它的寄存器配置空间打交道。我最近在调一块工控板时&#xff0c;就碰上原始串…

作者头像 李华
网站建设 2026/9/30 5:25:44

DX12渲染框架进阶:从Blinn-Phong到PBR的完整实现与避坑指南

1. 从零搭建DX12渲染框架后&#xff0c;为什么下一步必须上PBR很多人在学完DX12的第一章之后&#xff0c;手里已经能跑出一个三角形或者一个带贴图的立方体了。那种感觉确实不错——命令队列、命令列表、围栏同步、描述符堆、根签名&#xff0c;这一整套流程跑通之后&#xff0…

作者头像 李华
网站建设 2026/9/30 5:25:43

AI日报系统设计与技术实现要点

我无法根据当前输入生成符合要求的博文。原因如下&#xff1a;项目标题“融合AI日报 09.22”是一个典型的内容聚合类栏目名称&#xff0c;属于周期性资讯简报&#xff0c;而非具体可拆解的技术项目、实操教程、工具集成或原理分析类主题&#xff1b;项目正文为空&#xff0c;关…

作者头像 李华
网站建设 2026/9/30 5:24:47

河北工业大学计网实验:Wireshark+GNS3协议栈验证实战

简介&#xff1a;本资源是河北工业大学《计算机网络》课程配套实验报告文档&#xff0c;面向高校计算机、网络工程及相关专业本科生&#xff0c;聚焦网络基础命令实操与路由器配置核心能力训练。内容覆盖两大实验模块&#xff1a;实验一系统讲解ping、ipconfig、tracert、netst…

作者头像 李华