简介:基于YOLOv5与BDD100K数据集实现城市道路行人车辆检测与计数的完整指导文档,面向具备一定深度学习基础的技术从业者、研究学生及智能交通领域开发者,适用于城市交通监控、无人驾驶车辆感知等场景。文档从竞赛项目背景与难点展开,系统介绍数据集特点与下载方式、YOLOv5环境配置与代码实施流程、训练测试步骤与结果解读,并针对夜间、遮挡、多目标等复杂路况提出数据增强、超参数调优、网络架构改进等优化策略,同时提供模型导出与可视化方法,便于成果分享和工程落地。资源为单个PDF文件,压缩包大小7.32MB,内容紧凑且结构清晰,可直接作为项目实践或学习笔记。已有195人浏览学习,适合作为智能交通目标检测项目的参考材料或技能提升工具。
1. 城市道路行人车辆检测与计数:这个标题到底在解决什么问题
在路况监控项目里,我经常被问到同一个诉求:能不能把一段路口视频丢进去,自动数出这个小时经过了多少行人和车辆。听起来简单,实际落地时却总在三个环节掉链子——白天检测还行、晚上漏检严重,小目标行人时有时无,最头疼的是同一辆车被两个摄像头帧重复计数。城市道路中行人车辆检测与计数YOLOv5实现这套方案,本质就是围绕这三件事展开:用YOLOv5同时完成行人和车辆的检测分类,再把单帧检测结果跨帧串联成计数。它适合正在做智慧交通、路口流量分析、或想用目标检测替代人工巡检的开发者,不做科研刷点,直接奔着可运行、可交付去。本文会从模型为什么选YOLOv5讲起,一路拆到训练参数、常见翻车点,最后给出计数逻辑的实现。
2. YOLOv5为什么适合城市道路场景:架构优势与最小推理跑通
2.1 Anchor与CSPDarknet:YOLOv5能在车流密集时保持稳定输出的核心原因
城市道路监控的典型画面是俯拍或斜俯拍,行人和车辆尺度差异极大——一辆公交车可能占满半个画面,一个远处行人只有几十个像素。这种场景对检测器的多尺度能力要求很高,YOLOv5恰好是这类需求里综合表现最稳的模型之一。
YOLOv5的骨干网络用了CSPDarknet结构,设计思路是把特征提取拆成两条路径,一条走主干卷积,一条走残差跳跃,最后在通道维度拼接。这个设计带来的直接好处是梯度传递更顺畅,训练时深层网络不容易出现梯度消失,在夜间或弱光数据上收敛更稳定。颈部网络用了PANet结构,它的特点是有一条自顶向下的通路加一条自底向上的通路,让浅层的细节信息和深层的语义信息双向融合。这对小目标非常关键,远处行人之所以能被检测出来,靠的就是浅层特征保留了足够的轮廓和纹理。
另一个不能忽略的设计是三尺度检测头。YOLOv5会分别在8倍、16倍、32倍下采样的特征图上做预测,分别对应小、中、大目标。输入分辨率设为640时,8倍下采样的特征图尺寸是80x80,也就是说画面被划分成6400个网格,每个网格负责预测中心点落在自己内部的目标。小目标行人主要靠这一层检出。Anchor先验框也针对这个场景做了适配,YOLOv5默认的锚框是从COCO数据集统计出来的,其中包含针对小物体的anchor尺寸,但如果你发现自己的场景里行人特别小,后面我会讲怎么重算anchor。
2.2 用YOLOv5s加载权重做一次最小推理,先跑通再说
在碰数据集之前,我建议先用官方权重跑通一次推理,确认环境没问题、理解检测输出的格式。下面是最小验证的启动方式。
# 进入YOLOv5项目目录后,用官方COCO权重检测一张路口图 python detect.py --source street_view.jpg --weights yolov5s.pt --conf-thres 0.4 --iou-thres 0.45 --imgsz 640这段命令的逻辑是把一张本地图片street_view.jpg送入模型,置信度低于0.4的框直接丢弃,两个框的IoU大于0.45时做NMS去重,推理时把图像缩放到640边长。首次运行会自动下载模型权重文件。看到run/detect/exp目录下生成带框的标注图,就说明环境没问题。
参数说明里最需要关注的是conf-thres和iou-thres。conf-thres设低了会出现大量误检框,设高了会漏掉远距离的行人;iou-thres则是控制重叠框保留数量的关键,城市道路里行人密集时,框之间天然有重叠,阈值设太大容易把相邻行人的框合并掉。我一般起步用0.4和0.45,再根据实际场景微调。
2.3 检测头的输出结构:每个框那6个数字到底怎么解读
跑通推理之后,你应该看一下模型输出。YOLOv5的原始预测是一个形状为(1, 25200, 85)的张量,其中25200是三尺度特征图上的anchor总数,85表示4个坐标信息(x_center, y_center, width, height)加上1个objectness置信度再加上80个类别概率。但是detect.py做了后处理,最终你看到的是每行一个检测结果。
从输出txt或运行时打印里,可以看到每行的结构是:类别编号、置信度、框的左上角x坐标、左上角y坐标、右下角x坐标、右下角y坐标。这里要特别说明一个常见误解,YOLOv5在导出或封装后,坐标可能是归一化的也可能缩放到原图尺寸,取决于你是否开启了save-txt和是否做了坐标缩放。新手最容易在这里犯迷糊,把归一化坐标当像素坐标用,画框就偏移。
这个输出结构直接决定了计数逻辑怎么设计。你拿到每一个目标后,需要取它的类别编号(0是person,2是car),取置信度做筛选,再取中心点坐标做跨帧关联。我在做项目时通常先用detect.py验证单帧效果,确认模型能稳定检出远距离的行人,再进入数据集准备阶段。因为如果模型本身对这类目标不适应,后面的训练调参都是在错误基础上打补丁。
3. 准备行人车辆数据集:从类别筛选到标注踩坑
3.1 只取person和car:为什么大刀阔斧砍掉其余类反而更准
城市道路场景里你会遇到公交车、卡车、自行车、摩托车、红绿灯等大量物体。COCO数据集有80个类别,直接全量训练不是不行,但会引入两个问题:一是类别间特征相似造成混淆,比如bus和truck在俯拍视角下从外观上很难分;二是无用类别会挤占模型的表达能力,让主干网络花力气去学习护栏、树木这类背景特征的判别,挤占了对行人细节的学习空间。
我一般会在数据准备阶段就把类别裁剪为person和car两类,必要时把bus、truck并入car。具体做法是建立一个类别映射文件,只保留目标类别的标注记录,其余全部丢弃。如果项目需要区分公交车和卡车,那就保留三类,但需要保证每一类都有充足的样本量,否则宁可不分。这个决策的本质是让模型专注学习两类目标的判别特征,绝大部分路况项目里,行人和车辆的分类就足够支撑业务需求了。
类别映射的一个具体落地方式是写一个脚本处理标注文件。下面的代码把COCO格式转成YOLO格式,同时只保留person和car类。
import json import os def coco_to_yolo(coco_json_path, output_dir, keep_categories): with open(coco_json_path, 'r') as f: coco_data = json.load(f) # 建立category_id到目标类别的映射 cat_mapping = {} for cat in coco_data['categories']: if cat['name'] in keep_categories: cat_mapping[cat['id']] = keep_categories.index(cat['name']) images = {img['id']: img for img in coco_data['images']} anns_by_img = {} for ann in coco_data['annotations']: if ann['category_id'] not in cat_mapping: continue anns_by_img.setdefault(ann['image_id'], []).append(ann) for img_id, anns in anns_by_img.items(): img_info = images[img_id] img_w, img_h = img_info['width'], img_info['height'] txt_path = os.path.splitext(img_info['file_name']) txt_path = os.path.join(output_dir, txt_path[0] + '.txt') os.makedirs(os.path.dirname(txt_path), exist_ok=True) with open(txt_path, 'w') as f: for ann in anns: x, y, w, h = ann['bbox'] # COCO的bbox是左上角+宽高,转成YOLO的归一化中心点+宽高 cx = (x + w / 2) / img_w cy = (y + h / 2) / img_h nw = w / img_w nh = h / img_h f.write(f"{cat_mapping[ann['category_id']]} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}\n") coco_to_yolo('coco_annotations.json', 'labels', ['person', 'car'])代码逻辑分三步:先读COCO的JSON文件建立类别ID映射,然后遍历标注,过滤掉不需要的类别,最终把COCO的左上角加宽高坐标转换成YOLO需要的归一化中心点坐标。参数说明:keep_categories里的顺序决定了输出标注中类别的数字编号,建议固定为['person', 'car'],顺序不要随意调整;COCO的bbox坐标如果是float类型,注意保持精度,否则训练解析时容易出边界框越界的警告。
这里要提醒一个边界坑:如果原图里某个目标的框大部分在画面外,COCO的标注规则是只标可见部分。YOLO训练时对这种情况是可以正常学习的,但如果你转成YOLO格式后发现某些框的中心点特别靠近0或1,解释器会认为这个框不可用。我的处理习惯是直接丢弃中心坐标在0.02以内的框,因为这种边缘截断目标在训练时很容易产生极端梯度,让损失函数震荡。可以加一行过滤逻辑,但你要先评估自己的数据里这种边缘目标占比高不高,如果高就别轻易丢。
3.2 数据增强策略:城市道路的光照与遮挡需要针对性处理
城市道路数据的最大特点是光照条件极端,白天强逆光、夜晚路灯下色温不一、雨天反光,这些都会让模型泛化能力下降。YOLOv5在训练时默认开启了Mosaic增强,四张图随机裁剪拼接成一张新图,这个策略对小目标检测极其有效。模型被迫在拼接图的局部区域寻找目标,相当于变相提高了对局部特征的利用。
针对道路场景,我通常会在hyp.scratch-low.yaml里额外调两组参数。一组是hsv_h、hsv_s、hsv_v,控制色调、饱和度、亮度的随机扰动范围,夜间数据不足时,把hsv_v的随机范围调大能让模型对亮度变化更鲁棒。另一组是degrade,这是YOLOv5较新版本里用于模拟低分辨率输入的增强。城市监控视频经常码率低、画面糊,开启这个增强后模型不会因为测试时图像模糊就失效。
有一种常见误操作是盲目叠加增强策略,把旋转和透视增强开满。城市道路检测的目标朝向是有统计规律的,车辆一般水平或斜向出现在画面里,如果你把图片旋转90度或做大幅透视变换,训练出的模型反而在正常视角下表现变差。增强不是越多越好,要贴合目标在真实场景中的姿态分布。
3.3 数据量不够怎么办:用预训练权重做迁移学习比想象中更省事
很多开发者一上来就用随机初始化的权重训练,效果惨不忍睹,损失函数下不去,mAP也起不来。城市道路场景虽然有特殊性,但行人和车辆的基本视觉结构和COCO里的同类目标是高度一致的。正确做法是加载YOLOv5s的COCO预训练权重,冻结骨干网络的前几层,先训练检测头几轮,再解冻全部层微调。
这么做的理由很直接:预训练骨干已经学会了边缘、纹理、形状等通用特征,你只需要让模型适应道路场景的视角分布和光照特点,收敛速度比从零训练快得多,而且不容易过拟合。如果你的标注数据不足500张,这个策略几乎是必须的。我见过用200张标注图从零训练的情况,验证集mAP只有0.1,但迁移学习能把它拉到0.6以上。
训练时还需要配合早停机制,YOLOv5默认开启patience参数,连续多少轮验证集指标没有提升就会自动停止。下面会讲具体的训练启动方式和参数设置。
4. 训练配置与参数调试:从启动命令到让模型真正可用的关键调优
4.1 训练启动命令与超参数选择,batch和imgsz怎么权衡
数据集准备好之后,训练启动命令是接下来要做的关键步骤。我以一张YOLOv5s为例,给出一个城市道路场景的典型启动配置。
python train.py \ --data data/cityroad.yaml \ --cfg models/yolov5s.yaml \ --weights yolov5s.pt \ --epochs 100 \ --batch-size 16 \ --imgsz 640 \ --device 0 \ --cache \ --hyp data/hyps/hyp.scratch-low.yaml这里data/cityroad.yaml的内容是数据集的路径和类别定义,包含train、val的图片路径和nc=2、names列表。--cfg指定模型结构为yolov5s,--weights使用COCO预训练权重,--epochs给100轮,batch-size按显存决定,--cache把数据提前缓存在内存里减少磁盘IO,--hyp指定超参数文件。
这些参数里,最值得花心思调试的是batch-size与imgsz的匹配关系。imgsz直接影响小目标检测效果,640是默认值,如果你的监控画面分辨率高、远处行人占比大,可以尝试1024甚至1280。但imgsz提高后特征图尺寸平方级增长,显存占用急剧上升,batch-size必须对应调小。比如imgsz从640提到1280,同样的显存下batch-size大约要降到四分之一。我一般先用imgsz 640加batch-size 16跑通流程,确认数据没问题后再尝试高分辨率训练。
一个需要重点确认的细节是data yaml中train和val图片路径不能包含中文,YOLOv5的加载逻辑对非ASCII路径处理得不好,很多翻车都发生在这一步。另外,图片命名最好是纯数字或短英文字符串,避免后续生成缓存文件时出现解析异常。
4.2 训练过程要盯的曲线与判断标准,loss震荡怎么区分正常与异常
训练启动后,你会看到终端里每轮打印的指标:box_loss、cls_loss、obj_loss、P、R、mAP50、mAP50-95。这里的关键判断逻辑是看趋势而不是看绝对值。训练前20轮,loss下降和mAP上升都很快,这是正常且预期内的;30轮以后曲线会进入平台期,缓慢波动,此时不要频繁中断训练去调整参数,要给它足够的迭代轮数。
loss震荡要区分两种情况。一种是训练集loss和验证集loss同步震荡但整体趋势向下,这通常是学习率偏大的表现,但还在可接受范围,可以继续观察;另一种是训练loss持续下降、验证loss反弹上升,这就明显过拟合了,应该在hyp文件里加大数据增强强度或者提前停止。YOLOv5默认的早停机制patience=100的意思是连续100轮没有变好才停,很多人没注意这个参数导致过剩时间,建议设成20到30。
另一个经常被忽略的指标是P和R的相对关系。行人车辆检测场景里,漏检的代价往往比误检高,因为计数任务依赖检测召回率,漏检一个行人会导致整个计数偏低。所以我会在训练完成后检查R值,如果R明显低于P,说明模型偏向保守,此时推理时可以把conf-thres调低一些,牺牲精确率换召回率。
如果训练过程中出现某一类目标的mAP明显低于另一类,大概率是数据不均衡。我遇到过一个项目里车辆样本是行人的5倍,结果行人的mAP只有0.3,车辆却有0.7。解决方法是两种数据重采样策略:一是对行人样本做复制增强,把样本比例拉近到2:1以内;二是在loss层面给行人类别加权重,YOLOv5的类权重参数可以手动设置。复制增强更直接,因为类别数量上去了,模型有更多机会学习行人特征。
4.3 导出与推理部署:ONNX转换和端侧落地要注意什么
训练完成后,模型文件是weights/best.pt,这是PyTorch格式。实际部署时你大概率要转成ONNX或TensorRT格式,这一步有很典型的坑。执行导出命令时,YOLOv5会做一次推理验证,此时如果你的环境里torch版本和opset版本不匹配,导出会报奇怪的算子错误。建议固定ONNX opset版本为12,这个版本在绝大多数推理框架里兼容性最稳。
python export.py --weights best.pt --include onnx --opset 12 --imgsz 640导出ONNX后,用onnxruntime加载推一遍,确认输出结果与PyTorch版本一致。这一步非常关键,因为有一些层在转换时会被折叠或重排,比如YOLOv5的检测头在ONNX里可能有一层Transpose,坐标顺序会变。如果你直接用公式解析输出而不看实际的张量形状,很容易得到完全错误的框坐标。
部署到Jetson这类嵌入式设备时,通常还要做INT8量化。量化最直接的后果是精度下降,尤其是小目标。我的经验是城市道路场景里,INT8量化后mAP可能掉5到10个点,但对计数任务来这种精度损失尚可接受。如果你发现量化后小目标漏掉太多,建议在训练时把imgsz调大一些,给量化后的精度衰减留出余量。
5. 城市道路检测计数的5个典型翻车点与排查方法
5.1 远处行人漏检:看着像闪烁的噪点,实际上是小目标特征丢失
现象:画面远端的路口,行人时有时无,车流量大时行人完全消失。置信度在0.2到0.4之间剧烈波动,刚好卡在阈值边缘。
原因:小目标在骨干网络里经过多层下采样后,特征图上的响应非常弱。加上训练数据里这类小目标样本占比低,模型没有充分学会它们的判别特征。这是YOLOv5的架构局限,不是代码错误。
解决:优先把推理时的imgsz提到1280,这个操作几乎能立刻提升小目标召回;其次在数据准备时统计所有标注框的宽高分布,如果小目标占比确实很大,用YOLOv5的--rect参数或重算anchor来适应你的框尺寸分布。还有一个进阶方案是用SAHI做切片推理,把大图切块再检测再拼接,但推理成本会成倍上升,建议先试imgsz。
5.2 同一辆车被重复计数:单帧检测结果直接累加造成的后果
现象:程序输出的车流量是人工统计的三倍,且数值随着帧率上升,帧率越高数越大。
原因:直接对每帧检测结果的数量做累加,没有做跨帧关联。同一辆车在连续几十帧里都出现,每一帧都被当成新车计数。这是计数逻辑的错误,不是检测模型的错。
解决:增加最简单的目标关联逻辑,记录每个目标的中心点,当前帧中心点与上一帧某个目标中心点距离小于某个阈值(比如车辆宽度的一半),就认为是同一目标,沿用同一个ID,不做重复计数。跨帧关联的实现细节下一章会展开,这里先提一个判断方向:如果检测耗时远大于视频帧间隔,比如每秒只处理5帧,那么目标在相邻帧间的位移会变大,距离阈值要相应放宽。
5.3 夜间和逆光场景整体翻车:训练数据分布不均匀是根源
现象:白天mAP能到0.7,夜间直接跌到0.2,车灯位置出现大片误检框,行人几乎全部漏检。
原因:训练集九成以上是白天数据,夜间样本极少,模型从统计上压根没学过夜间的视觉特征。逆光场景同理,光照条件的变化对模型来说是分布外数据。
解决:最直接的办法是在采集阶段刻意做时段均衡,白天、黄昏、夜间各占三分之一,然后对夜间样本做亮度增强,让模型见到的夜间数据更多样。如果重新采集数据不现实,可以给原始数据集用自适应直方图均衡化做夜间模拟增强,虽然真实性差一些,但至少能让模型学到夜间对比度较低的视觉结构。另一个补救方案是在推理时加图像增强预处理,把暗部提亮后再送入模型。
5.4 训练loss正常但实测效果差:过拟合特定视角还是验证集划分不当
现象:训练和验证的mAP都很高,但拿到一段新视频上实测,效果明显下降,漏检一片。
原因:要么是训练集和验证集来自同一段视频的连续帧,时间相关性太强,模型学到的是该场景的特有特征而非通用的行人和车辆特征;要么是数据全部来自同一机位的固定视角,模型对这个视角产生了过拟合。
解决:数据划分时必须按视频片段来划分,不能随机打乱帧级别的数据。同一个视频的不同帧要全部落在训练集或验证集里,不能两边都沾。验证集最好来自独立的时间段或不同的路口机位,这样才能反映模型真实的泛化能力。我一直沿用这个习惯后,实测效果与验证指标之间的落差明显缩小。
5.5 训练时显存溢出或训练中断:反复OOM背后的资源计算问题
现象:batch-size设16能跑,提到32就OOM,或者训练中途报CUDA out of memory后进程直接退出。
原因:显存占用不仅取决于batch-size,还取决于imgsz和Mosaic增强的临时缓冲区。提高imgsz后峰值显存可能翻三四倍。更隐蔽的是Dataloader的workers数量太多导致的内存碎片,在Windows下尤其常见。
解决:先用nvidia-smi看当前显存余量,再把batch-size调低到8并观察峰值占用。如果还需要更大batch,可以开启梯度累积,等效扩大batch而不增加显存。换一个方向,降低imgsz到512或416也可以显著降低显存压力。如果是Windows环境下训练报错,把workers设为0基本能解决。
6. 把检测结果变成计数输出:跨帧匹配与精度验证的实用技巧
6.1 轻量级帧间匹配计数方案:不引入跟踪模型也能完成计数
很多项目引入DeepSORT来做计数,造成工作量陡增。如果你的场景是固定摄像头、机位稳定,其实可以用一个轻量级的基于中心点距离的匹配方案。核心思路是维护一个目标ID列表,记录每个目标最近一帧的中心点坐标,当前帧每个检测框和已有目标做最近邻匹配,距离小于阈值则视为同一个目标,更新其坐标;没有匹配到的检测框则注册为新ID。计数时设置一条虚拟计数线,目标中心点从线的一侧跨越到另一侧时,计数加一。
class CentroidTracker: def __init__(self, max_dist=50): self.max_dist = max_dist self.next_id = 0 self.targets = {} def update(self, detections): updated = {} # detections: list of (cx, cy, class_id) for cx, cy, cls_id in detections: best_id = None best_dist = float('inf') for tid, (tx, ty, tcls) in self.targets.items(): dist = (cx - tx) ** 2 + (cy - ty) ** 2 if dist < best_dist and tcls == cls_id: # 同类目标之间才做匹配,避免行人和车辆ID串扰 best_dist = dist best_id = tid if best_dist < self.max_dist ** 2 and best_id is not None: updated[best_id] = (cx, cy, cls_id) else: updated[self.next_id] = (cx, cy, cls_id) self.next_id += 1 self.targets = updated return updated代码逻辑是循环每个检测框,和已知目标列表计算欧氏距离的平方,找距离最近且类别一致的目标,如果距离在阈值内就沿用了它的ID,超过阈值则认为是新出现的目标。参数说明里max_dist要按实际画面像素设置,1280分辨率的画面里行人一步可能移动30到60像素,max_dist取80比较合适;但车辆移动快,跨帧位移可能超过150像素,需要按类别用不同的阈值。我一般会把检测框按类别拆分后分别调用这个类,分别设置max_dist。
这个方案对比DeepSORT的好处是零依赖、无额外模型加载、CPU上都能跑得动,适合摄像头数量多的项目。代价是目标被完全遮挡几帧后重新出现会被当作新目标,计数会偏大。所以业务上如果接受这个误差范围,它是最快落地的选择。
6.2 用一段人工标注视频验证计数精度,比争论指标都靠谱
模型训练好了,计数逻辑写完了,接下来最重要的一步是验证。我的做法是找一段约30秒钟的路口视频,帧率25fps,提前用视频标注工具人工数出真实的行人和车辆通过数量并记录。然后跑一遍程序,得到自动计数的结果,对比偏差。
验证时建议记录几个统计量而不是只看总数。比如同一辆车被重复计数的次数、漏计的行人数量、误把行人当车辆计数的情况。这些细粒度信息可以直接帮你定位问题出在检测还是跟踪。如果重复计数占比高,优先调帧间匹配阈值;如果漏计占比高,优先调检测的置信度阈值和imgsz。
另一种验证思路是抽查视频中间某几秒的检测结果,把模型输出的框画成一个独立视频,人工快速浏览。这虽然不精确,但能直观看到模型在什么时段失效,比如傍晚太阳落山那一刻,画面亮度骤降,检测质量会有一个明显的下坠。这类时段性的失效很难通过指标体现,只能靠肉眼观察发现后针对性补数据。
最后分享一个我做这条路线的个人教训:不要在训练阶段花太多时间反复手动调参,多花时间在数据分布的分析上。我早期习惯盯着mAP数字来回调学习率,但最后真正让模型质变的是把夜间样本和数据均衡做好。从模仿阶段就保持这个习惯,能少走很多弯路。如果你正准备在自己的路况数据上做行人车辆计数,希望这篇笔记能帮你把从模型选型到计数落地的坑都提前绕过去,希望帮到你。
本文还有配套的精品资源,点击获取