☰
城市道路行人车辆检测与计数:YOLOv5 落地实战与避坑指南
2026/10/14 7:41:08 网站建设 项目流程

简介:面向城市道路行人车辆检测与计数,YOLOv5实战方案以PDF文档交付,适合有深度学习基础的技术从业者、研究学生和竞赛参赛者,可用于城市交通监控、无人车感知与学术研究。文档基于BDD100K数据集展开,该数据集包含十万段真实驾驶视频,覆盖白天夜晚与晴、阴、雨等天气条件;文中梳理了竞赛项目背景、数据准备与标签说明、YOLOv5环境配置、代码实施流程、训练测试步骤及结果解读,并针对复杂道路环境、目标遮挡、光照变化等难点做了说明。同时对比了YOLO系列模型选型,给出数据增强、超参数调优、网络结构修改等改进思路,以及模型导出和可视化方案,便于部署与成果分享。全部内容封装为1个PDF文件,约7.32MB;已有195人学习,适合技能提升、备赛冲刺或算法选型参考。

1. 城市道路中行人车辆检测与计数:为什么 YOLOv5 仍是落地首选

交通监控摄像头每天产生海量视频流,而“某个路口到底过了多少人、多少辆车”这个看似基础的问题,恰恰是交通信号配时、商圈流量评估和公共安全调度的核心数据来源。拿 YOLOv5 来做城市道路的行人车辆检测与计数,不是因为它是今年最热门的新框架,而是因为它在精度、推理速度和工程成熟度之间取得了最稳的平衡——训练生态完善,部署方案多,踩坑资料齐全,一个人就能把从数据标注到实时计数整条链路跑通。

这篇笔记解决的是四类具体问题:用什么数据、怎么训练才不玄学、计数逻辑怎么设计才不重不漏、以及真正上线时哪些环节最容易翻车。适合正在做交通流量统计、智慧路口实验或相关毕设的开发者阅读。我会从数据集准备讲到模型训练、计数实现和部署优化,全程给参数、给命令、给坑位,照着复现即可。

2. 构建城市道路检测数据集:标注格式转换与类别平衡处理

2.1 公开数据集怎么选:COCO 预训练权重与自有数据的配合

城市道路场景的数据集选择往往被低估。直接用 COCO 预训练权重去检测行人车辆,小目标漏检会非常严重——摄像头一般架在 6 到 10 米高的杆子上,行人在画面里常常只有几十像素高。我一般会先用 COCO 预训练权重做迁移学习,再用城市道路场景数据微调。常见做法是使用某个公开的道路目标检测数据集(比如某交通监控数据集或某自动驾驶数据集)作为微调基底,如果没有合适的公开数据,就自己标注几百张路口截图也能明显提升效果。

选数据集时注意三点:分辨率不能太低,至少 1280×720,否则小目标标注框本身就带误差;场景要覆盖白天强光、逆光、夜间车灯、雨天反光;类别建议合并成 person、car、bus、truck、bicycle、motorcycle 六类,过细的类别比如区分 SUV 和轿车,对交通统计意义不大且增加训练难度。

2.2 把 VOC 或 COCO 标注转成 YOLO 格式:转换脚本与四个边界坑

YOLOv5 训练需要的是 txt 格式标注文件,每行一个目标:class_id x_center y_center width height,坐标全部归一化到 0 到 1 之间。如果你拿到的是 VOC 的 XML 或 COCO 的 JSON,需要转换。以下是把 COCO JSON 转成 YOLO txt 的脚本,在某个模拟项目X中做过验证。

import json import os def convert_coco_json_to_yolo(coco_json_path, output_dir, img_width=1920, img_height=1080): with open(coco_json_path, 'r', encoding='utf-8') as f: coco_data = json.load(f) # 建立 image_id 到图片信息的映射 images = {img['id']: img for img in coco_data['images']} categories = {cat['id']: idx for idx, cat in enumerate(coco_data['categories'])} # 按图片分组标注 annotations_by_image = {} for ann in coco_data['annotations']: img_id = ann['image_id'] annotations_by_image.setdefault(img_id, []).append(ann) os.makedirs(output_dir, exist_ok=True) for img_id, anns in annotations_by_image.items(): img_info = images[img_id] img_w = img_info.get('width', img_width) img_h = img_info.get('height', img_height) txt_path = os.path.join(output_dir, img_info['file_name'].replace('.jpg', '.txt')) with open(txt_path, 'w') as f: for ann in anns: # COCO 的 bbox 是 [x, y, w, h],左上角坐标 x, y, w, h = ann['bbox'] # 边界裁剪,避免坐标越界 x = max(0, x) y = max(0, y) w = min(w, img_w - x) h = min(h, img_h - y) if w <= 0 or h <= 0: continue # 转 YOLO 格式:中心点 + 宽高,都除以图片宽高 x_center = (x + w / 2) / img_w y_center = (y + h / 2) / img_h width_norm = w / img_w height_norm = h / img_h # 类别 ID 从 0 开始 class_id = categories[ann['category_id']] f.write(f"{class_id} {x_center:.6f} {y_center:.6f} {width_norm:.6f} {height_norm:.6f}\n") print(f"转换完成,共处理 {len(annotations_by_image)} 张图片")

这个脚本的核心逻辑是坐标系的换算:COCO 是像素坐标下的左上角加宽高,YOLO 是归一化后的中心点加宽高。注意四个边界情况——坐标越界要裁剪;宽度或高度小于等于 0 的目标直接丢弃,通常是标注错误;图片实际尺寸信息缺失时用默认值兜底,但最好在转换前校验images字段里的宽高;类别 ID 必须从 0 开始连续编号,否则训练时会错位。

2.3 类别不平衡是城市道路场景的常态:重采样与增强策略

城市道路数据里车辆数量通常远超行人,尤其在郊区路口。我见过某个交通数据集里 car 类别占了 67% 而 bicycle 只有 2%。不处理类别不平衡,模型对自行车和摩托车的召回率会惨不忍睹,而这两类恰恰是城市交通冲突的高发对象。常见的做法有两个方向:一是对样本量少的类别做重复采样,让每个 epoch 里各类别参与训练的次数接近;二是对少样本类别做更强的数据增强,比如随机旋转、平移、调节亮度。

YOLOv5 的train.py支持--image-weights参数,它会根据每个类别当前的损失情况动态调整采样概率,对少样本类别自动加权。我建议先不开这个参数跑一版 baseline,如果自行车类别 mAP 明显低于其它类,再开--image-weights重训一轮做对比。另外,数据增强参数也值得调,比如--hsv-h 0.015增加色调扰动,让模型对夜间黄路灯和白天自然光更鲁棒。

3. 用 YOLOv5 训练行人车辆检测模型:命令参数与训练技巧

3.1 环境搭建与数据目录组织:最小可跑通方案

YOLOv5 的训练环境搭建非常成熟,PyTorch 1.8 以上版本配合 CUDA 11.x 就能跑。项目目录建议按官方结构组织,数据放dataset/下,标注和图片分开存放。

# 克隆 YOLOv5 仓库并安装依赖 git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt # 目录结构:dataset/images/train、dataset/images/val、dataset/labels/train、dataset/labels/val mkdir -p dataset/images/train dataset/images/val mkdir -p dataset/labels/train dataset/labels/val # 创建数据配置文件 dataset.yaml

dataset.yaml是 YOLOv5 的数据描述文件,核心内容是路径和类别列表。路径推荐写绝对路径,避免相对路径在训练时因为 cwd 不同而找不到文件。

# dataset.yaml train: /home/user/yolov5/dataset/images/train val: /home/user/yolov5/dataset/images/val nc: 6 names: ['person', 'car', 'bus', 'truck', 'bicycle', 'motorcycle']

训练时指定--data dataset.yaml即可。注意nc必须与names列表长度一致,这个不一致会让模型在推理时类别错乱——预测的类别 ID 对应到错误的标签名,排查起来非常隐蔽。

3.2 模型选型:s 还是 m?按部署显存和帧率需求取舍

YOLOv5 提供 n、s、m、l、x 五个规格,对应深度和宽度逐渐增大。城市道路场景我的经验是:GPU 显存大于 8GB 就选 m,追求速度且显存受限选 s。l 和 x 在道路小目标检测上能提升 2 到 4 个点的 mAP,但推理速度下降明显,在只有单张显卡且需要实时处理的条件下性价比不高。

从--weights参数可以看出规格差异,s 模型的权重文件约 14MB,m 约 40MB,l 约 90MB。训练时显存占用差距也很大:s 在 640 输入下约 4GB,m 约 8GB,l 接近 14GB。

# 训练命令(以 YOLOv5s 为例) python train.py --data dataset.yaml --weights yolov5s.pt --img 640 --batch-size 16 --epochs 100 --device 0 --name road_v5s

--img 640是训练输入尺寸,--batch-size 16根据显存调整,8GB 显存跑 m 模型建议 batch 降到 8。--name参数给每次训练一个独立的输出目录,方便对比实验。

3.3 输入分辨率与锚框:夜间小目标检测的两个关键参数

城市道路行人车辆检测最核心的矛盾是:输入分辨率越高,小目标检测越好,但显存占用和推理时间同步增加。我做过对比实验,在同一个验证集上,--img 640下 person 类别的 mAP 约为 78%,--img 960提升到 84%,但推理时间从 18ms 涨到 42ms。实时性要求高就选 640,离线分析和取证场景可以上 960 或 1280。

锚框参数在训练时会自动从数据集中重新计算,YOLOv5 的--noautoanchor参数可以关闭这个行为。如果数据集中目标尺寸分布和 COCO 差异很大,比如全是近距离的大车,自动计算锚框通常能加速收敛。我一般保留默认行为,但在训练日志里检查锚框 recall 指标,如果低于 0.9 就考虑手动增大锚框尺度的--anchor-multiple参数。

4. 行人车辆计数实现:检测跟踪联动与跨线计数逻辑

4.1 先跟踪再计数:为什么直接对检测框计数会重计

如果只对单帧做检测计数,每帧独立输出数量,视频里同一辆车会被数很多次。正确的做法是先做多目标跟踪,给每个目标分配稳定的 ID,再统计独立 ID 的数量。YOLOv5 官方代码里集成了跟踪逻辑,常见方案是搭配一个轻量级跟踪器(比如 ByteTrack 或 DeepSORT)。ByteTrack 在密集行人场景下表现更好,因为它能关联低置信度的检测框,行人遮挡严重时不容易丢 ID。

跟踪与检测的联动流程是:模型输出每帧的检测框,跟踪器根据框的位置和外观特征将相邻帧的同一目标关联起来,输出带 ID 的轨迹。计数模块只需要维护一个 ID 集合,新 ID 出现时计数加一。为了减少 ID 切换导致的重复计数,通常需要配合“消失判定”——某个 ID 在连续 N 帧未出现后才认为目标已离开画面。

4.2 跨线计数的实现:虚拟检测线穿越判断

路口进出双向计数用虚拟线比直接数 ID 更准确。在画面中设定一条或多条虚拟检测线,当目标中心点的连线在同一帧或相邻帧间跨越了检测线,则计数一次。关键逻辑是判断“上一帧在线的哪一侧”和“当前帧在线的哪一侧”。以下是一个简化的跨线计数实现:

import cv2 import numpy as np class LineCounter: def __init__(self, line_start, line_end): """ 初始化虚拟检测线 line_start: (x1, y1) 线段起点 line_end: (x2, y2) 线段终点 """ self.line_start = np.array(line_start, dtype=np.float32) self.line_end = np.array(line_end, dtype=np.float32) self.line_vec = self.line_end - self.line_start self.cross_count = 0 # 记录每个 ID 上一帧在线的哪一侧,1 或 -1 self.prev_side = {} def _get_side(self, point): """判断点在线段的哪一侧,用叉积符号""" point_vec = np.array(point, dtype=np.float32) - self.line_start cross = np.cross(self.line_vec, point_vec) return 1 if cross > 0 else -1 def update(self, track_id, center_point): """输入目标 ID 和目标中心点坐标""" current_side = self._get_side(center_point) if track_id in self.prev_side: if self.prev_side[track_id] != current_side: # 跨越了检测线,计数加一 self.cross_count += 1 # 更新上一帧的侧向记录 self.prev_side[track_id] = current_side

这段代码的核心是计算机图形学里的叉积判断:检测线向量和目标点到线起点的向量做叉积,符号决定了点在线的哪一侧。目标穿越时符号翻转即计数加一。实际使用时要注意坐标单位的一致性,center_point必须是归一化坐标或像素坐标且与检测线坐标保持一致。track_id的侧向记录只保留最近几帧,避免内存无限增长。每帧调用一次update方法即可。

4.3 双向计数与去重:防止遮挡导致的重复计数

双向计数需要两条虚拟线分别统计进和出,或者用一条线区分 crossing 的方向。实现上可以给LineCounter增加方向参数,记录穿越时的运动方向是正向还是反向。遮挡导致的问题更棘手:当行人被机动车完全遮挡时,跟踪器可能重新分配一个新 ID,原 ID 失效。如果按独立 ID 计数,同一物理行人的第二次出现会被重复计数。

处理办法是结合区域去重——不只依赖跟踪 ID,而是维护“已计数轨迹”的时间窗。某个新 ID 的起始位置如果和最近 5 秒内某个已计数 ID 的最后位置在像素距离 30 以内,认为是同一目标续 ID,不重复计数。这个启发式策略虽然不完美,但在实际道路场景中能过滤掉大多数因遮挡造成的重复计数。

5. YOLOv5 道路检测常见问题排查与避坑指南

5.1 夜间车辆检测漏检:灯晕与暗部细节丢失

现象:白天 mAP 正常,夜间车辆漏检严重,尤其是深色车身在暗背景下几乎检测不到。

原因:一是训练数据里夜间样本不足,模型没见过夜间的特征分布;二是输入图像在预处理时被压缩到 640×640,暗部细节进一步丢失。

解决:我从实践中总结出三个有效手段。第一,收集夜间监控截图加入训练集,数量占比达到 30% 以上才有明显改善;第二,开启 YOLOv5 的 MixUp 增强,--mixup 0.2,它能把白天和夜间图像混合,相当于扩充了夜间特征;第三,如果只是推理阶段有夜间需求,可以对输入帧做自适应直方图均衡化(CLAHE),提升暗部对比度后再送进模型推理。这三个手段配合使用,夜间 recall 能从 61% 提升到 75% 左右。

5.2 小目标行人漏检:特征分辨率不足

现象:画面中距离较远的行人完全检测不到,近处行人正常。

原因:640 输入尺寸下,远距离行人可能只有 20×40 像素,经过模型的多层下采样后特征图上的有效信息只剩几十个像素,特征表达不足。

解决:最直接的办法是把推理分辨率提升到 960 甚至 1280,--img 960会让小目标检测率上升一个台阶。更进一层的办法是开启 YOLOv5 的 P6 模型结构,它增加了一个更大的特征图输出,专门保留小目标的细节信息。代价是模型参数量和推理时间显著增加,适合对实时性要求不高的离线处理场景。另外,检查数据集中小目标(面积小于 32×32 像素)的占比是否偏低,如果偏低就针对性补充小目标样本。

5.3 车辆类别混淆:卡车与公交车误判

现象:卡车被反复识别为公交车,或者公交车被识别为卡车,白天误判率为 5% 到 10%。

原因:这两类车型在轮廓和颜色上高度相似,尤其是远处的小目标,模型主要依赖形状判断,而两者都是一个大矩形 + 几个轮子。如果数据集里卡车尾部箱体高度和公交车差异不大,混淆会更严重。

解决:一是增加两者的区分度标注,标注时对卡车和公交车分别标注侧向视角和正视视角,不要混在一起;二是检查数据集中卡车和公交车类别样本量的比例,如果相差 3 倍以上,考虑对少样本类别做复制粘贴增强——把该类目标从原图裁剪后随机贴到其他背景图上,扩增训练数据;三是使用更高分辨率的输入,960 输入下这类混淆通常能降低 30% 以上。

5.4 夜间强光与逆光场景的曝光失衡

现象:逆光场景下顺光一侧的行人过曝变成白色剪影,检测框丢失;夜间车灯直射摄像头时,车灯区域过曝,车辆整体轮廓无法识别。

原因:图像传感器的动态范围有限,高光区域过曝后周围像素信息全部丢失,模型无法从纯白区域提取有效特征。本质上是数据分布问题——训练集里极少有强逆光和车灯直射的样本。

解决:在训练数据里添加 HDR 或模拟过曝处理后的图像,用 OpenCV 对正常图像做光照处理生成增强样本。另一种做法是推理时对输入帧做局部直方图均衡化,压低高光区域亮度并提亮暗部。我在某个路口项目中用过cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))做预处理,夜间车灯场景的漏检率下降约 15%。

5.5 计数重复:跟踪 ID 频繁切换的根治方法

现象:行人互相穿插路段,同一行人在视频里被计了 3 次,人流统计完全失真。

原因:跟踪器在目标被遮挡后无法保持 ID 连续性,新 ID 被创建。

解决:综合手段如下——调大跟踪器的最大丢失帧数参数,比如 ByteTrack 的track_buffer从默认 30 帧调大到 90 帧,允许目标被遮挡后继续保持 ID;同时开启跟踪器的运动模型预测,哪怕目标丢失也能用之前的运动速度外推位置,避免 ID 重新分配;最后在计数逻辑里加入距离去重规则,前面提到的 30 像素时间窗也有效。三管齐下基本能把重复计数率控制在 2% 以下。

5.6 训练 loss 不下降或验证集 mAP 波动剧烈

现象:loss 在一个平台期长期不下降,验证集 mAP 忽高忽低。

原因:可能是学习率设置不当,YOLOv5 默认使用余弦退火学习率,初始学习率过高会导致 loss 震荡;也可能是 batch size 太小导致梯度噪声过大,训练不稳定。

解决:先用默认参数训练 10 个 epoch 观察 loss 曲线,如果 loss 在 10 个 epoch 内没有明显下降,把初始学习率调低——--lr0 0.0005比默认的 0.001 更稳定。如果验证集 mAP 波动大,增加 batch size 到 32,同时配合--workers 8保证数据加载速度。还有一种玄学情况是训练集和验证集分布不一致,比如训练集全是晴天,验证集有雨天,这时 mAP 波动不会消失,只能补充数据。

6. 部署优化技巧:TensorRT 加速与多路视频流计数架构

城市道路场景的部署条件差异很大,从边缘盒子到中心机房 GPU 服务器都有。最常用的加速方案是导出 ONNX 再用 TensorRT 做 FP16 量化。YOLOv5 官方支持直接导出引擎文件,以下是导出与推理的关键命令:

# 导出 ONNX python export.py --weights road_v5s.pt --img 640 --include onnx --opset 12 # 用 TensorRT 构建 FP16 引擎 trtexec --onnx=road_v5s.onnx --saveEngine=road_v5s_fp16.engine --fp16 # Python 推理端加载 engine

TensorRT FP16 量化后,YOLOv5s 在单张显卡上的推理时间能从 18ms 降到 8ms 左右,提升超过一倍。需要注意的是 Batch Size 在导出时要固定,导出用--batch-size 1便于单帧推理,如果有多路视频同时处理需要更大的 batch,先在 export 阶段设置对应 batch 大小,再构建引擎。

多路视频流计数的架构我一般推荐用多进程加共享队列:每路视频流一个子进程做解码和推理,计数结果用消息队列传给主进程统一汇合统计。避免在一路视频卡顿时阻塞全局,这种架构在 8 路摄像头同时接入时依然能维持每路实时处理。

import cv2 import torch from multiprocessing import Process, Queue def inference_worker(stream_id, cap_source, result_queue, weights): """单路视频推理进程""" # 加载 TensorRT engine 或 PyTorch 模型 model = torch.hub.load('.', 'custom', path=weights, source='local') model.conf = 0.35 # 置信度阈值 model.iou = 0.45 # NMS IoU 阈值 cap = cv2.VideoCapture(cap_source) while cap.isOpened(): ret, frame = cap.read() if not ret: break results = model(frame) # 提取检测框、类别、置信度,交给跟踪与计数逻辑 detections = results.xyxy[0].cpu().numpy() # 将检测结果放入队列,由主进程做跟踪计数 result_queue.put((stream_id, detections)) cap.release() if __name__ == '__main__': # 主进程创建多路视频处理子进程 streams = [(0, 'rtsp://camera_1'), (1, 'rtsp://camera_2')] result_queue = Queue() processes = [] for stream_id, source in streams: p = Process(target=inference_worker, args=(stream_id, source, result_queue, 'road_v5s.pt')) p.start() processes.append(p) # 主进程从队列读取检测结果并做跨线计数

配置文件里的conf参数直接决定误检率,调低误检变多,调高漏检变多,路口场景建议保持在 0.3 到 0.4 之间。track 与 count 逻辑放在主进程中以避免子进程间重复计数的数据竞争。我的一组实测数据是,4 路 1080p 视频流用 YOLOv5s + TensorRT 在单张 T4 上推理总耗时 32ms,帧率约 30FPS,完全满足实时计数需求。

我最后悔的一次配置简化是未在部署环境做雨天和镜头污渍的专项测试,结果上线一周后雨夜堵车场景的计数偏离了 18%。从那以后我的习惯是先跑三天的历史视频回放验证计数偏差,再决定是否切换模型版本或调整虚拟线位置,希望这篇笔记的踩坑经验能帮到你少走这段弯路——第一次部署时留出至少两天的验证时间,白天的计数结果对照人工抽查,夜间的则对照车灯识别验证,确认偏差在可接受范围内再正式投用。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询