资讯动态

城市道路行人车辆检测与计数:基于YOLOv5的工程实践

发布时间:2026/10/14 7:41:07 来源:尧图企业网站定制
简介基于YOLOv5与BDD100K数据集实现城市道路行人车辆检测与计数的完整指导文档面向具备一定深度学习基础的技术从业者、研究学生及智能交通领域开发者适用于城市交通监控、无人驾驶车辆感知等场景。文档从竞赛项目背景与难点展开系统介绍数据集特点与下载方式、YOLOv5环境配置与代码实施流程、训练测试步骤与结果解读并针对夜间、遮挡、多目标等复杂路况提出数据增强、超参数调优、网络架构改进等优化策略同时提供模型导出与可视化方法便于成果分享和工程落地。资源为单个PDF文件压缩包大小7.32MB内容紧凑且结构清晰可直接作为项目实践或学习笔记。已有195人浏览学习适合作为智能交通目标检测项目的参考材料或技能提升工具。1. 城市道路行人车辆检测与计数这个标题到底在解决什么问题在路况监控项目里我经常被问到同一个诉求能不能把一段路口视频丢进去自动数出这个小时经过了多少行人和车辆。听起来简单实际落地时却总在三个环节掉链子——白天检测还行、晚上漏检严重小目标行人时有时无最头疼的是同一辆车被两个摄像头帧重复计数。城市道路中行人车辆检测与计数YOLOv5实现这套方案本质就是围绕这三件事展开用YOLOv5同时完成行人和车辆的检测分类再把单帧检测结果跨帧串联成计数。它适合正在做智慧交通、路口流量分析、或想用目标检测替代人工巡检的开发者不做科研刷点直接奔着可运行、可交付去。本文会从模型为什么选YOLOv5讲起一路拆到训练参数、常见翻车点最后给出计数逻辑的实现。2. YOLOv5为什么适合城市道路场景架构优势与最小推理跑通2.1 Anchor与CSPDarknetYOLOv5能在车流密集时保持稳定输出的核心原因城市道路监控的典型画面是俯拍或斜俯拍行人和车辆尺度差异极大——一辆公交车可能占满半个画面一个远处行人只有几十个像素。这种场景对检测器的多尺度能力要求很高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是person2是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_okTrue) 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的图片路径和nc2、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默认的早停机制patience100的意思是连续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_dist50): 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数字来回调学习率但最后真正让模型质变的是把夜间样本和数据均衡做好。从模仿阶段就保持这个习惯能少走很多弯路。如果你正准备在自己的路况数据上做行人车辆计数希望这篇笔记能帮你把从模型选型到计数落地的坑都提前绕过去希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑