简介:面向智慧交通与目标检测应用场景,这份基于YOLOv9实现的车辆行人识别检测计数系统,提供完整Python源码、训练好的模型权重、评估指标曲线与详细运行教程。资源适合计算机视觉、人工智能等相关专业学生作为毕业设计参考,也适合开发者快速搭建道路目标检测与计数功能。压缩包共182个文件,63.03MB,涵盖83个Python脚本、30个YAML配置文件、3个pt模型文件以及jpg/png示例图片、csv评估结果、tfevents训练日志等,代码结构清晰,便于二次开发与调试。目前已有526人学习,项目经过测试运行成功,包含环境配置、数据集准备、训练参数调整和检测脚本使用等完整说明,可帮助使用者避开常见踩坑问题。对于需要完成课设或演示系统的读者,这份资源能有效缩短从环境搭建到模型推理的周期,并支持自行扩展训练自定义数据集。
1. 智慧交通车辆行人识别检测计数系统:这不是一个普通的目标检测Demo
模型训练好只是第一步,真正让智慧交通项目落地的关键,是检测、跟踪、计数这三条链路能不能稳定地串起来。很多人在YOLOv9上跑通了单张图片的车辆行人识别检测,一到视频流就露馅:漏检、重复计数、目标ID跳变、夜间翻车。这套系统的价值就在于,它不只是贴了一个“基于YOLOv9”的标签,而是把车辆行人检测、目标跟踪、双向计数、评估指标曲线完整地打包成了一套可运行、可扩展的Python方案。
拿到压缩包里的源码、训练好的模型和评估指标曲线,你不是拿到一个黑匣子,而是拿到了一条可以自己复现、改参数、换数据的完整链路。这篇内容我会从架构拆解讲到核心代码实现,再到训练评估和踩坑记录,最后补一个ROI区域计数的进阶玩法。适合正在做毕设、智慧交通项目预研、或者想把YOLOv9真正落地到视频分析场景的开发者。
2. 系统架构与技术选型:为什么YOLOv9适合道路车辆行人识别检测计数
2.1 从模型选型看这套系统的设计思路
YOLOv9在目标检测领域的核心优势,是它在保证实时性的同时,把检测精度又往上推了一截。它提出的可编程梯度信息(PGI)和广义高效层聚合网络(GELAN),解决了深度网络在浅层特征传播中的信息丢失问题。放在道路车辆行人识别检测计数的场景里,这个特性直接决定了小目标——比如远处横穿马路的行人、排在红灯路口末尾的摩托车——能不能被稳定地框出来。
对于智慧交通这种摄像头角度固定、目标类别固定(无非是car、bus、truck、person、bicycle、motorcycle这几类)、背景相对稳定的场景,YOLOv9的性价比很高。常见做法是用COCO预训练权重做迁移学习,然后在自己的交通数据集上微调。这套系统里自带的训练好的模型,就是从COCO权重出发、在道路场景数据上跑出来的结果。你拿到手里的best.pt,直接加载就能跑推理。
2.2 系统分层:检测、跟踪、计数各司其职
整套系统按数据流可以拆成五个模块,每个模块的边界必须清晰,不然出了问题你都不知道该查谁。
输入层负责读取视频文件、摄像头流或图片序列。OpenCV的VideoCapture是标配,支持RTSP流的时候要注意缓冲区设置,这个后面在踩坑章节细说。
检测层是YOLOv9推理核心,加载best.pt,对每一帧做前向推理,输出边界框、类别ID和置信度。这里有一个关键设计:计数系统的准确性上限由检测层决定,检测漏一次,后面跟踪和计数再怎么补都补不回来。
跟踪层把检测框关联成运动轨迹,这是车辆行人识别检测计数和单纯目标检测的本质区别。常见方案是ByteTrack或DeepSort,这套系统用的是ByteTrack的思路——先按高置信度框做关联,再用低置信度框补漏,对遮挡场景更友好。
计数层负责统计车辆和行人。两种主流方式:虚拟线计数(跨线触发计数)和区域计数(进入/离开ROI区域触发计数)。道路场景通常用虚拟线,因为车辆轨迹方向明确,跨线计数的逻辑简单稳定。
输出层负责可视化——在帧上画框、画轨迹、更新计数面板,同时写视频文件或推流。
2.3 为什么用ByteTrack而不是DeepSort
很多人在做道路车辆行人识别检测计数时直接在DeepSort和ByteTrack之间纠结。DeepSort依赖ReID特征做外观匹配,在行人密集、遮挡严重的场景里表现好,但ReID模型本身要额外训练,推理耗时也上去了。ByteTrack的思路更轻:它发现检测器漏检时产生的低置信度框里藏着大量被遮挡的目标,与其丢弃不如拿来辅助匹配。
ByteTrack在交通场景下的优势是明显的:车辆外观相似度高(同款白色轿车在相邻车道出现很正常),ReID特征区分度有限,而ByteTrack主要靠位置预测和IoU关联,反而更稳。这套系统的跟踪层推荐保持ByteTrack默认配置,唯一需要调的参数是检测阈值,后面会给出具体建议。
3. 核心代码实现:从模型加载到车辆行人识别检测计数的完整链路
3.1 模型加载与推理配置
拿到源码包后,第一步是确认环境。该项目基于Python 3.8+和PyTorch 1.8+开发,YOLOv9官方仓库的v9-c模型配置对应你手里那份训练好的模型。新建虚拟环境后,按以下步骤安装依赖:
# 创建虚拟环境并激活 python -m venv venv source venv/bin/activate # Windows下执行 venv\Scripts\activate # 安装PyTorch(以CUDA 11.8为例,无GPU可去掉+cu118后缀装CPU版) pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装YOLOv9项目依赖 pip install -r requirements.txt # 验证模型能否正常加载 python -c "from models.experimental import attempt_load; m = attempt_load('weights/best.pt', device='cpu'); print('模型加载成功')"attempt_load是YOLOv9仓库里加载模型的统一入口,它兼容不同版本的权重文件格式。这里如果报错,九成是PyTorch版本和权重文件序列化版本不匹配导致的。比如用PyTorch 2.x加载1.x训练出来的权重,偶尔会报UnpicklingError,解决办法是降低PyTorch版本或在加载时指定map_location。
3.2 推理链路:预处理、前向、后处理
import cv2 import torch import numpy as np from models.experimental import attempt_load from utils.general import non_max_suppression, scale_coords class YOLOv9Detector: def __init__(self, weights_path, device='0', conf_thres=0.25, iou_thres=0.45): self.device = torch.device(f'cuda:{device}' if torch.cuda.is_available() and device != 'cpu' else 'cpu') self.model = attempt_load(weights_path, device=self.device) self.model.eval() self.conf_thres = conf_thres self.iou_thres = iou_thres self.stride = self.model.stride # 模型下采样倍数,YOLOv9通常为32 def preprocess(self, img): """将OpenCV读取的BGR图缩放到模型输入尺寸,并做letterbox填充""" h, w = img.shape[:2] new_size = (640, 640) # letterbox:保持宽高比缩放,剩余部分用灰色填充 ratio = min(new_size[0] / h, new_size[1] / w) new_h, new_w = int(h * ratio), int(w * ratio) resized = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LINEAR) canvas = np.full((new_size[0], new_size[1], 3), 114, dtype=np.uint8) canvas[:new_h, :new_w] = resized # BGR转RGB,HWC转CHW,归一化到[0,1] tensor = canvas[:, :, ::-1].transpose(2, 0, 1).copy() tensor = torch.from_numpy(tensor).float().div(255.0).unsqueeze(0) return tensor, ratio, (new_w, new_h) def predict(self, frame): tensor, ratio, _ = self.preprocess(frame) tensor = tensor.to(self.device) with torch.no_grad(): pred = self.model(tensor)[0] # NMS后处理,过滤低置信度框和重叠框 det = non_max_suppression(pred, self.conf_thres, self.iou_thres)[0] results = [] if det is not None and len(det): # 将框坐标映射回原图尺寸 det[:, :4] = scale_coords(tensor.shape[2:], det[:, :4], frame.shape[:2]) for *xyxy, conf, cls in det: x1, y1, x2, y2 = [int(v.item()) for v in xyxy] results.append({ 'bbox': (x1, y1, x2, y2), 'conf': conf.item(), 'cls': int(cls.item()) }) return results这段代码是整个车辆行人识别检测系统的地基。letterbox预处理为什么重要?因为YOLOv9的训练阶段用了同样的填充策略,推理时如果不做letterbox而是直接resize到640x640,画面比例失真会直接拉低小目标召回率。
scale_coords把预测框从640x640空间映射回原始分辨率空间,这个步骤最容易踩坑——忘记映射的话,你在1080p视频上画的框位置会整体偏移。置信度阈值conf_thres设为0.25是通用推荐值,道路场景下如果发现漏检多,可以降到0.15试试,代价是误检会增多。
3.3 车辆行人识别:按类别ID过滤目标
# COCO数据集的类别定义,YOLOv9预训练权重对应这80类 COCO_CLASSES = [ 'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', 'fire hydrant', ... ] # 道路车辆行人识别检测计数系统只关心车辆和行人,其余类别一律过滤 TRAFFIC_CLASSES = { 'person': 0, # 行人 'bicycle': 1, # 自行车 'car': 2, # 小轿车 'motorcycle': 3, # 摩托车 'bus': 5, # 公交车 'truck': 7, # 卡车 } def filter_traffic_objects(detections): filtered = [] for det in detections: cls_id = det['cls'] # 通过类别ID反向查找类别名 cls_name = COCO_CLASSES[cls_id] if cls_name in TRAFFIC_CLASSES: det['cls_name'] = cls_name filtered.append(det) return filtered这一步就是“车辆行人识别”在代码层面落地的关键。COCO的80类里包含了traffic light、stop sign这些交通标志类,但智慧交通系统通常只统计车辆和行人,不统计信号灯。为什么要单独写过滤器而不是直接改模型输出头?因为训练好的模型是全类别输出的,改输出头意味着重新训练,成本高且没必要,运行时过滤是最常见的做法。
3.4 车辆行人计数:基于跟踪轨迹的虚拟线计数
from collections import defaultdict class LineCounter: def __init__(self, line_start, line_end): """ line_start, line_end: 虚拟线的两个端点 (x, y) 计数规则:目标中心点跨过虚拟线时,根据跨线方向增减计数 """ self.line_start = line_start self.line_end = line_end self.counts = {'car': 0, 'person': 0, 'bicycle': 0, 'motorcycle': 0, 'bus': 0, 'truck': 0} self.last_positions = {} # 轨迹ID -> 上一次的中心点坐标 def update(self, track_id, bbox, cls_name, current_frame, total_frames): x1, y1, x2, y2 = bbox cx, cy = (x1 + x2) / 2, (y1 + y2) / 2 # 跨线判断:计算目标中心点到虚拟线的有向距离 # 如果符号发生变化(正变负或负变正),说明发生了跨线 line_vec_x = self.line_end[0] - self.line_start[0] line_vec_y = self.line_end[1] - self.line_start[1] point_vec_x = cx - self.line_start[0] point_vec_y = cy - self.line_start[1] cross = (line_vec_x * point_vec_y - line_vec_y * point_vec_x) # 首帧只记录位置,不做计数判断 if track_id not in self.last_positions: self.last_positions[track_id] = cross return prev_cross = self.last_positions[track_id] # 跨线:有向距离正负号翻转 if prev_cross * cross < 0: self.counts[cls_name] += 1 # 打印日志,方便核对读数 print(f"[FRAME {current_frame}/{total_frames}] {cls_name} 跨线," f"累计 {self.counts[cls_name]} 辆/人") self.last_positions[track_id] = cross def get_counts(self): return dict(self.counts)虚拟线计数的核心是“有向距离符号翻转”这个几何判据。用叉积判断点在线的哪一侧,相邻两帧符号不同就说明跨线了。这里有一个细节:同一辆车的同一个track_id,一帧内可能同时跨越两条虚拟线(比如双车道场景),所以每帧最多触发一次计数,这个逻辑靠last_positions字典天然保证。
如果要统计双向流量(比如区分进城和出城),只需要再往LineCounter里加一个方向参数:cross > 0记为A方向,cross < 0记为B方向,分别维护两个计数字段即可。
3.5 可视化与结果输出
def draw_results(frame, detections, tracks, counter): """在帧上绘制检测框、轨迹ID、类别标签和实时计数面板""" for det in detections: x1, y1, x2, y2 = det['bbox'] cls_name = det['cls_name'] conf = det['conf'] # 不同类别不同颜色:行人黄色,车辆蓝色 color = (0, 255, 255) if cls_name == 'person' else (255, 0, 0) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) label = f"{cls_name} {conf:.2f}" cv2.putText(frame, label, (x1, y1 - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) # 绘制虚拟线和计数面板 cv2.line(frame, counter.line_start, counter.line_end, (0, 255, 0), 3) y_offset = 30 for cls_name, count in counter.get_counts().items(): if count > 0: text = f"{cls_name}: {count}" cv2.putText(frame, text, (20, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) y_offset += 25 return frame检测框的颜色区分、计数面板的实时更新、虚拟线的可视化,这些都是现场调试时的“后悔药”——没有可视化输出,你根本不知道系统把哪辆车数重复了。
整个主循环常见做法是用cv2.VideoCapture逐帧读取,检测-跟踪-计数-绘制串行处理,最后用cv2.VideoWriter写结果视频。1080p分辨率下,如果GPU是RTX 3060级别,单帧处理时间在15~25毫秒之间,基本能跑实时。
4. 训练与评估:从数据集准备到评估指标曲线的完整解读
4.1 数据集格式:道路车辆行人识别检测场景下的数据准备
这套系统自带的评估指标曲线是基于训练好的模型在验证集上跑出来的。如果你想在自己的场景上重新训练,数据集准备工作是避不开的第一步。道路车辆行人识别检测常用的公开数据集有UA-DETRAC(车辆为主)和CityPersons(行人为主),但更常见的做法是从开源数据集里筛选出交通相关的子集。
YOLOv9训练需要YOLO格式的标注,每张图片对应一个同名txt文件,每行格式为cls_id x_center y_center width height,坐标值都是归一化到0~1的。如果你手里的数据是VOC格式的XML标注,需要先转换:
import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, output_dir, class_list): """将VOC格式XML标注转换为YOLO格式txt标注""" tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) yolo_lines = [] for obj in root.findall('object'): cls_name = obj.find('name').text if cls_name not in class_list: continue cls_id = class_list.index(cls_name) bbox = obj.find('bndbox') x1 = float(bbox.find('xmin').text) y1 = float(bbox.find('ymin').text) x2 = float(bbox.find('xmax').text) y2 = float(bbox.find('ymax').text) # VOC坐标转YOLO:中心点+宽高,归一化 x_center = (x1 + x2) / 2 / img_w y_center = (y1 + y2) / 2 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h yolo_lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") txt_name = os.path.splitext(os.path.basename(xml_path))[0] + '.txt' with open(os.path.join(output_dir, txt_name), 'w') as f: f.write('\n'.join(yolo_lines))转换脚本里有三个容易翻车的边界:一是类别名必须和class_list完全一致,大小写不同都会被当作新类别;二是xmin/ymin可能大于xmax/ymax(有些标注工具会反向画框),转换前要做归一化校验;三是width/height不能为0,否则训练时loss直接爆炸。
4.2 训练配置与关键参数
在YOLOv9仓库中,训练入口是train.py,核心参数如下:
python train.py \ --data traffic.yaml \ # 数据集配置文件,含train/val路径和类别列表 --weights weights/yolov9-c.pt \ # 预训练权重,迁移学习起点 --epochs 100 \ --batch-size 16 \ # 根据显存调整,RTX 3090可到32 --imgsz 640 \ --device 0 \ --project runs/train \ --name traffic_yolov9 \ --hyp data/hyps/hyp.scratch-high.yamltraffic.yaml的内容大致如下:
train: data/traffic/images/train val: data/traffic/images/val nc: 6 # 类别数:person, bicycle, car, motorcycle, bus, truck names: ['person', 'bicycle', 'car', 'motorcycle', 'bus', 'truck']这里最影响训练效果的是hyp.scratch-high.yaml里的数据增强参数。道路场景下,我一般会把mosaic保留(它对小目标有奇效),但会把hsv_h从默认值调低一点——交通摄像头画面色彩普遍稳定,过大的色调扰动反而让模型学到错误的颜色不变性。
训练过程中如果显存吃紧,常见做法是把batch-size减半,同时降低imgsz到480。低显存运行模型的另一个有效手段是开启梯度累积:把batch_size降到4,同时设置--accumulate 4,效果等价于batch_size=16。
4.3 评估指标曲线:到底是看mAP还是看PR曲线
源码包里附带评估指标曲线,通常包括PR_curve.png、F1_curve.png、confusion_matrix.png和各类别的val_loss曲线。很多人只看mAP@0.5这个单点数值,但这在智慧交通场景里远远不够。
| 指标 | 文件 | 怎么看 |
|---|---|---|
| mAP@0.5 | results.png | 粗粒度衡量,>0.8算合格,但掩盖小目标问题 |
| mAP@0.5:0.95 | results.png | 严格指标,框的定位精度也参与评判 |
| PR曲线 | PR_curve.png | 看曲线膝盖位置,判断置信度阈值应该设多少 |
| F1曲线 | F1_curve.png | 找F1最大值对应的置信度,这就是最优阈值 |
| 混淆矩阵 | confusion_matrix.png | 看哪些类别互相误检,比如卡车和公交车 |
在道路场景里,最值得关注的是PR曲线膝盖点的位置。如果膝盖偏右上(召回率0.8以上才掉精度),说明模型在低置信度下还有很强的召回能力,推理时可以把conf_thres调低到0.15来捡漏。反之如果膝盖偏左下,说明低置信度框几乎全是误检,阈值应该往高了调。
另一个常见误区是只盯着损失曲线是否收敛。YOLOv9的box_loss和cls_loss在前30个epoch会快速下降,之后进入平台期,这是正常的。真正要看的是val损失是否出现反弹——如果val/cls_loss在第70个epoch左右开始回升,说明过拟合了,解决方法是把--hyp里的cls_pw调高,或者对数据做更强的增强。
5. 车辆行人识别检测计数的4个实战避坑:现象、原因、解决
5.1 模型加载报错:KeyError: 'model.0.conv1.weight'
拿到训练好的模型后,加载时报字典KeyError,或者推理时输出张量的shape和预期不符。这个坑我见过太多次,九成是权重文件和模型定义不匹配——比如用yolov9-c.pt的权重去加载yolov9-e.pt的模型结构,或者PyTorch版本差异导致的序列化不兼容。
解决思路是检查weights文件名是否和models/yolo.py里的模型类对应。你手里的训练好的模型基于yolov9-c配置,那么加载时也要用yolov9-c.yaml初始化模型。如果确认配置没问题,可以尝试在加载后对状态字典做一次过滤:
# 权重键名前缀过滤,处理DataParallel训练的权重 from collections import OrderedDict raw_state_dict = torch.load('weights/best.pt', map_location='cpu') new_state_dict = OrderedDict() for k, v in raw_state_dict['model'].items(): name = k[7:] if k.startswith('module.') else k new_state_dict[name] = v model.load_state_dict(new_state_dict, strict=False)strict=False允许缺失部分键,这能让你在结构微调后依然加载大部分预训练权重。但要记住,这只适用于轻微改动(比如改了类别数),如果层的顺序变了,不加过滤直接加载才是对的。
5.2 同一辆车被重复计数,尤其在被遮挡后出现
场景是拥堵路段,一辆公交车被大货车挡住几帧,恢复可见后被当成新车,计数重复。原因是跟踪器在目标丢失期间没有保留轨迹,重新检测到后分配了新的track_id,虚拟线计数逻辑把它当成新目标处理。
ByteTrack默认会在目标连续丢失30帧后删除轨迹,这个参数在tracks_removed逻辑里。解决办法是增加跟踪器的记忆能力:把track_buffer从默认的30帧提高到60帧,同时降低检测阈值到0.2,让遮挡期间的低置信度框能持续关联上原来的轨迹。
# 在tracker配置中调整参数 python run_tracking.py --conf-thres 0.2 --track-buffer 60还有一个隐藏因素:虚拟线的位置如果离画面边缘太近,车辆尚未被跟踪稳定就被计数了。常见做法是把虚拟线放在画面中间偏下的位置,保证目标至少被跟踪了10帧以上再参与计数判断。
5.3 夜间场景检测效果断崖式下跌
白天mAP 0.85,到了夜间直接掉到0.5,车辆尾部轮廓和暗色行人几乎检测不出来。原因是训练集里夜间样本太少,模型没有见过足够多的低光照特征。这不完全是模型的问题,是数据分布的问题。
解决优先级从高到低排列:先给摄像头画面做预处理,用自适应直方图均衡化提升暗部细节:
def enhance_night_frame(frame): # CLAHE:限制对比度自适应直方图均衡,适合夜间车牌和行人轮廓 lab = cv2.cvtColor(frame, cv2.COLOR_BGR2LAB) l_chan, a_chan, b_chan = cv2.split(lab) clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8)) l_chan = clahe.apply(l_chan) enhanced = cv2.cvtColor(cv2.merge([l_chan, a_chan, b_chan]), cv2.COLOR_LAB2BGR) return enhanced做个对比实验:夜间视频分别用原始帧和增强帧跑模型,你会发现召回率提升5~10个百分点。但这只是临时补救,根本解法还是收集夜间数据做微调,哪怕只有几百张,效果也比任何图像增强算法强。这类问题在项目里被说成“翻车”,实际上是数据集构建时没有把光照分布这一维考虑进去。
5.4 评估指标很高,但实际视频里计数对不上
模型在验证集上mAP@0.5高达0.9,拿到真实路口视频跑计数,和人工数的结果差了15%以上。这类黑匣子问题最容易消耗排查时间。先别怀疑模型,先怀疑计数逻辑本身:验证集的mAP衡量的是单帧检测能力,而计数是时序累积结果,单帧2%的漏检率在1000帧里就会被放大成几十次的漏计数。
排查顺序应该是:先关闭跟踪器,只在输出视频里画检测框,逐帧看是否有漏检或重复框;再单开跟踪器看ID是否稳定;最后才查虚拟线计数逻辑。每一步的输出都保存成带日志的视频文件——手动数200帧的检测结果,对比计数读数,就能锁定是谁的问题。
另一个常见做法是保留“后悔药”:在计数面板里加一个撤销按钮(记录最近10次计数事件的目标轨迹ID),回看视频时发现某辆车被数了两次,手动撤销并更新统计。这在现场调试阶段能省大量时间。
6. 进阶:ROI区域计数与按车道统计的改造方案
如果你做完基础版车辆行人识别检测计数,想把系统推给甲方或作为正式交付,ROI区域计数是必然要面对的需求。虚拟线计数适合统计车流过路口的流量,但智慧交通项目经常要回答另一个问题:某个区域(比如人行横道禁停区)当前有多少辆车、多少个人,或者某个车道每分钟通过多少辆车。
ROI区域计数的核心逻辑,是把虚拟线的“跨线触发”改成“进入/离开区域触发”:
判断目标中心点是否在ROI多边形内 之前不在且现在在 -> 区域内数量 += 1 之前不在且现在不在 -> 忽略 之前在且现在还在 -> 维持不变OpenCV的cv2.pointPolygonTest可以判断点是否在多边形内,接口很简单。关键参数是ROI的扩张缓冲:因为跟踪框有抖动,目标中心点贴着边界时,判定结果会来回跳,导致计数反复增减。我一般给ROI膨胀5~10个像素的缓冲带,只有中心点完全进入缓冲带内侧才判定为“进入”。
按车道统计的场景,常见做法是在画面上手动标出车道分割线,然后根据目标框底部中心点所在的x坐标,映射到对应的车道编号,再分别维护计数。这个方案对摄像头安装角度很敏感:如果摄像头不是正对车道,而是有倾斜角,车道分割线在画面里就不是垂直的,需要用透视变换拉正画面后再做车道映射。
验证建议:拿一段5分钟的实拍视频,人工数出地面真值(车流量、行人流量),然后跑一遍系统,对比误差。误差在5%以内是交付线,5%~15%需要微调跟踪和计数参数,15%以上先不要调参数,回头检查漏检率和ID切换率。
最后提一个长期有效的小技巧:把所有调试过的视频帧、每帧的检测框、track_id、计数事件都记录成CSV日志。这个日志是排查问题的最好工具——我在调试这套系统的过程中,90%的计数错误都能通过回放CSV日志而不是重跑视频来定位。希望这篇内容能帮你把YOLOv9的车辆行人识别检测计数系统真正跑通,少走一些我走过的弯路。
本文还有配套的精品资源,点击获取