首页 / 资讯中心 / 文章详情

基于YOLOv9的车辆行人识别计数系统:从检测到跟踪的完整工程实践

基于YOLOv9的车辆行人识别计数系统:从检测到跟踪的完整工程实践 ★ FEATURED ARTICLE
简介本资源面向计算机、人工智能、自动化等专业在校学生与开发者提供一套基于YOLOv9的智慧交通道路车辆与行人识别检测计数系统完整方案可用于毕业设计、课程项目或算法入门实践。压缩包共182个文件约63.03MB包含83个Python源码文件、30个YAML配置文件、22张JPG示例图、3个PT权重文件及评估指标曲线等覆盖模型训练、推理检测与结果可视化全流程。资源内含详细运行教程从Anaconda与PyCharm环境配置、依赖包安装到数据集准备、YAML参数修改、train_dual.py训练与detect_dual.py测试均给出可操作说明并附训练好的模型与评估曲线便于快速复现与二次开发。目前已有526人学习下载适合希望掌握目标检测落地流程、积累智慧交通项目经验的读者参考使用。1. 智慧交通场景下YOLOv9 车辆行人识别计数到底能解决什么路口摄像头一天产生几十 GB 视频人工盯屏既费眼又漏检这是很多做智慧交通的团队真实遇到的困境。基于 YOLOv9 实现道路车辆行人识别检测计数系统本质是把「检测」和「计数」两件事串起来先用 YOLOv9 在每一帧里框出车、人、非机动车再通过跟踪或越线判定把逐帧结果累计成过车数、过人数。它适合三类人做交通流量统计的算法工程师、需要快速交付 demo 的集成商、以及想拿一个完整 Python 项目练手检测计数链路的学生。标题里给的是一套完整工程——python 源码、运行教程、训练好的模型、评估指标曲线意味着你不需要从零训模型重点在于把推理、跟踪、计数、可视化这几段拼装跑通并看懂评估曲线判断模型能不能用在你的场景。2. YOLOv9 检测链路拆解从权重加载到逐帧推理2.1 为什么选 YOLOv9 而不是 v5/v8YOLOv9 的核心卖点在于可编程梯度信息PGI和 GELAN 结构简单说就是在同等参数量下梯度回传更充分小目标和遮挡目标的召回比早期版本更稳。放到道路场景里远距离的小车、被前车挡住一半的行人是检测最容易翻车的地方。相比 YOLOv5v9 在 COCO 上同量级模型的 mAP 通常有可见提升相比 YOLOv8v9 的 GELAN 主干在浅层特征保留上更友好对密集车流这种目标扎堆的画面更占优。但选型不是唯新是好。如果你手头只有一张入门级显卡或者部署端是算力受限的边缘盒子YOLOv9 的 s/m 版本才是现实选择c/e 版本精度高但推理慢路口实时计数往往吃不消。常见做法是先用 s 版本跑通全链路确认计数逻辑没问题再根据漏检情况决定要不要换 m 或加输入分辨率。2.2 加载模型并跑通单帧推理拿到训练好的权重后第一步永远是单帧验证别急着上视频。下面是最小可运行代码假设权重文件放在weights/目录下。import cv2 import torch from models.experimental import attempt_load # YOLOv9 仓库内的加载函数 from utils.general import non_max_suppression, scale_boxes from utils.augmentations import letterbox # 1. 选择设备有 GPU 优先用 GPU device torch.device(cuda:0 if torch.cuda.is_available() else cpu) # 2. 加载训练好的权重map_location 防止在无 GPU 机器上报错 model attempt_load(weights/best.pt, devicedevice) model.eval() # 推理模式关闭 BN 和 dropout 的随机性 # 3. 读取一帧并做 letterbox 缩放保持长宽比避免形变影响检测 img0 cv2.imread(test.jpg) img letterbox(img0, new_shape640, autoTrue)[0] img img[:, :, ::-1].transpose(2, 0, 1) # BGR 转 RGBHWC 转 CHW img torch.from_numpy(img.copy()).to(device).float() / 255.0 img img.unsqueeze(0) # 增加 batch 维度 # 4. 前向推理 NMS pred model(img, augmentFalse)[0] pred non_max_suppression(pred, conf_thres0.25, iou_thres0.45) # 5. 把框映射回原图尺寸并绘制 for det in pred: if det is not None and len(det): det[:, :4] scale_boxes(img.shape[2:], det[:, :4], img0.shape).round() for *xyxy, conf, cls in det: x1, y1, x2, y2 map(int, xyxy) cv2.rectangle(img0, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(result.jpg, img0)逻辑说明attempt_load负责把权重里的结构和参数还原成可推理模型letterbox是关键它把任意尺寸图片缩放到 640 并补灰边保证长宽比不变否则车辆会被拉扁导致检测框偏移。参数方面conf_thres0.25是置信度阈值低于它的框直接丢弃iou_thres0.45控制 NMS 合并重叠框的力度。这两个值不是固定的——车流密集时把 iou 调到 0.5 以上能减少误合并行人多且重叠时反而要调低到 0.4 左右。2.3 类别过滤与置信度阈值怎么定训练好的模型通常输出多个类别道路场景一般关心 car、bus、truck、person、bicycle、motorbike。计数前必须做类别过滤否则一个「红绿灯」类也会被算进去。做法是在 NMS 之后按 cls 筛选TARGET_CLASSES [0, 1, 2, 3, 5, 7] # 按你训练时的类别索引填写 filtered [] for det in pred: if det is not None and len(det): mask det[:, -1].isin(torch.tensor(TARGET_CLASSES, devicedet.device)) filtered.append(det[mask])阈值这块有个血泪经验不要照搬官方 demo 的 0.25。白天光照好、目标清晰0.25 够用夜间或逆光模型置信度整体下移硬卡 0.25 会大面积漏检这时候要么降到 0.15 配合后处理要么干脆换夜间数据微调。判断标准很简单——拿一段你真实场景的视频跑一遍看漏检和误检哪个更不可接受再回调阈值。3. 车辆行人计数从逐帧检测到跨帧累计3.1 计数为什么不能只靠检测框数量新手最容易犯的错是把每帧检测到的框数量直接当计数结果。视频 30 帧每秒同一辆车在画面里停留 3 秒就会被重复计 90 次。所以计数的本质是「去重」要么给每个目标一个跨帧稳定的 ID跟踪要么划定一条虚拟线只统计目标穿越线的动作越线计数。前者适合统计画面内实时数量后者适合统计一段时间内的过车总量。3.2 用 ByteTrack 做跟踪再计数跟踪方案里ByteTrack 是性价比很高的选择它不依赖外观特征纯靠检测框的运动关联速度快、对遮挡有一定容忍。核心思路是高分框先匹配低分框再补匹配减少因短暂遮挡导致的 ID 跳变。from yolox.tracker.byte_tracker import BYTETracker # 跟踪器参数track_thresh 是进入跟踪的置信度门槛 # track_buffer 是目标丢失后保留 ID 的帧数路口场景建议 30 左右 tracker BYTETracker(track_thresh0.5, track_buffer30, match_thresh0.8) tracks tracker.update(detections, img_size, img_size) # tracks 里每个元素带 track_id用它去重即可得到当前画面目标数 current_count len(set(t.track_id for t in tracks))参数说明track_thresh设太高低置信度的真实目标进不了跟踪ID 会断设太低误检也会被跟踪产生幽灵目标。track_buffer决定目标被遮挡后多久还保留 ID路口车辆互相遮挡频繁这个值太小会导致同一辆车被计两次。match_thresh是匹配阈值越大越严格一般 0.8 起步。3.3 越线计数的判定逻辑如果要统计「过了多少辆车」越线计数更直接。做法是在画面里画一条线水平或垂直记录每个 track_id 上一帧和当前帧的中心点判断连线是否与虚拟线相交。def cross_line(prev_pt, curr_pt, line): # line ((x1, y1), (x2, y2))判断线段 prev-curr 是否跨过 line def side(p, a, b): return (b[0]-a[0])*(p[1]-a[1]) - (b[1]-a[1])*(p[0]-a[0]) s1 side(prev_pt, *line) s2 side(curr_pt, *line) return s1 * s2 0 # 异号说明跨线逻辑说明side计算点在直线哪一侧前后两帧符号相反就说明目标穿过了这条线。注意要维护一个track_id - 上一帧中心点的字典并且对已经计过数的 ID 打标记避免来回抖动重复计数。虚拟线的位置也有讲究——放在画面中部别贴边因为边缘处目标刚进画面中心点不稳定容易误判。4. 评估指标曲线怎么读判断模型能不能上你的场景4.1 mAP、Precision、Recall 各自说明什么训练完会得到一组评估曲线常见的有 mAP0.5、mAP0.5:0.95、Precision-Recall 曲线、F1 曲线。mAP0.5 是 IoU 阈值 0.5 时的平均精度反映「框得大致准不准」mAP0.5:0.95 把 IoU 从 0.5 到 0.95 每隔 0.05 算一次再平均更严格反映「框得精不精」。Precision 是检出的里面有多少是对的Recall 是真实目标里检出了多少。对交通计数来说Recall 往往比 Precision 更重要——漏掉一辆车流量统计就少一辆多检一个框还能靠跟踪和越线逻辑过滤掉。所以看曲线时优先关注 Recall 在高置信度区间是否够高。4.2 从 PR 曲线找最佳工作点PR 曲线上的每一个点对应一个置信度阈值。曲线越靠右上模型整体越好。实际选阈值时找曲线拐点拐点之前降低阈值能明显提升 Recall 而 Precision 掉得不多拐点之后再降阈值 Precision 会断崖式下跌。这个拐点对应的置信度就是你这个场景比较稳的工作点。指标含义交通场景关注度mAP0.5IoU0.5 的平均精度高判断整体可用性mAP0.5:0.95多 IoU 阈值平均中判断框的精细度Precision检出正确率中误检可靠跟踪过滤Recall真实目标检出率高直接决定计数准不准F1精确率与召回率调和中综合参考4.3 用混淆矩阵定位具体类别问题如果整体指标还行但某类总是出问题就看混淆矩阵。常见现象是 truck 被识别成 car或者 motorbike 和 bicycle 互相混。这通常不是模型不行而是训练数据里这两类样本长得太像、标注边界模糊。解决办法是补充难例样本重新微调而不是盲目调推理参数。看曲线时要记住曲线是训练集/验证集上的统计和你的真实路口画面可能有分布差异最终还是要拿真实视频验证。5. 避坑与排查计数系统上线前必须过的几道坎5.1 同一目标被重复计数现象一辆车缓慢通过计数结果多了两三次。原因跟踪 ID 在遮挡或低置信度时断裂重新分配了新 ID越线逻辑把它当成新车。解决调大track_buffer让 ID 保留更久越线判定时对同一空间区域加时间窗去重比如 1 秒内同一位置只计一次。5.2 夜间或逆光大面积漏检现象白天计数正常晚上数量骤降。原因训练数据以白天为主夜间目标对比度低模型置信度整体下移卡在阈值以下。解决先降conf_thres到 0.15 观察召回是否回升若仍不行采集夜间数据做增量微调别指望调参能救回分布外的场景。5.3 虚拟线附近目标抖动导致误计现象车停在线上或缓慢移动计数反复加。原因目标中心点在线的两侧来回跳cross_line反复判定为穿越。解决加方向约束只统计从 A 侧到 B 侧的穿越再加最小位移阈值前后帧位移太小不判定为穿越。5.4 推理速度跟不上视频帧率现象视频播放卡顿计数延迟越来越大。原因输入分辨率过高或用了 c/e 大模型单帧推理超过 33ms。解决换 s 版本权重或把输入从 640 降到 512也可以跳帧推理每 2 帧处理 1 帧计数逻辑用跟踪补帧代价是快速目标可能漏计。5.5 类别索引对不上导致计数为空现象代码跑通但计数始终为 0。原因TARGET_CLASSES里的索引和训练时data.yaml的类别顺序不一致过滤后没有框剩下。解决打开训练配置确认类别顺序打印一次det[:, -1]看实际输出的类别编号再改过滤列表。6. 把计数系统跑稳的几个进阶技巧先说一个容易被忽略的点评估曲线好看不等于你的路口能用。我一般会做一次「影子测试」——把系统接到真实视频流上只记录不报警跑满一整天把计数结果和人工抽样的结果对比算出每个小时的误差率。误差率在早晚高峰是否稳定比单看 mAP 更能说明问题。第二个技巧是分区域计数。一个路口画面里不同车道的流量差异很大如果只画一条总线左转和直行的车会混在一起。做法是把画面切成几个多边形区域每个区域单独维护越线逻辑最后按区域汇总。这样不仅能得到总量还能得到分车道流量对信号配时更有价值。第三个是给跟踪加一个「存活时间」过滤。刚进画面就被遮挡的目标ID 存活很短这类目标往往计数不准。可以设置只有 track 存活超过 N 帧比如 5 帧才纳入计数牺牲一点实时性换取准确率。MIN_TRACK_AGE 5 # 跟踪存活帧数门槛 track_age {} # track_id - 已存活帧数 for t in tracks: track_age[t.track_id] track_age.get(t.track_id, 0) 1 if track_age[t.track_id] MIN_TRACK_AGE: # 只有稳定跟踪的目标才参与越线计数 pass参数MIN_TRACK_AGE怎么定目标在画面里移动越快这个值要越小否则还没计上数就出画面了目标移动慢、遮挡多就调大一点。我一般从 5 起步根据漏计和误计的比例微调。最后说验证方法。别只用一段视频反复调参那样很容易过拟合到这一段。准备三段不同时段、不同天气的视频调参只用第一段另外两段做验证。如果三段误差率都在可接受范围这套参数才算稳。我自己踩过的坑就是拿早高峰视频调到完美结果晚高峰一上就崩后来养成习惯任何计数参数改动必须过三段时间的验证。这套基于 YOLOv9 的车辆行人识别计数系统真正花时间的不是模型本身而是跟踪去重、越线判定和阈值调优这些工程细节。把评估曲线读懂、把避坑清单过一遍再按分区域和存活时间过滤的思路优化基本就能从 demo 走到能用的状态。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站