ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于YOLO和质心跟踪的车辆计数与速度估计系统实现

基于YOLO和质心跟踪的车辆计数与速度估计系统实现 简介这套交通监控与分析系统源码基于计算机视觉和深度学习实现面向人工智能、计算机视觉方向的开发者与学生可用于高速公路车辆检测、计数和平均测速等场景。资源共32个文件以源代码、编译缓存、视频样本、模型配置与说明文档为主压缩包约137MB目录包含检测主程序、跟踪模块、模型参数和测试视频结构清晰。已有145人浏览学习。系统内部采用YOLO目标检测算法与质心跟踪算法能实时统计车流量并估算车速内置多种模型配置文件方便基于COCO或PASCAL类别进行扩展。项目还配套多个真实高速公路视频样本和预训练模型下载链接可直接运行主程序观察效果适合作为计算机视觉课程设计、毕业设计或目标检测跟踪项目的参考。1. 交通监控系统为什么需要 YOLO 加质心跟踪的组合做交通监控与分析系统最直接的车流量统计是背景差分或帧间差分把“变化的像素”当车辆。可一旦遇到阴影、夜间车灯、雨天反光前景区域经常连成一片车辆在画面中一停留就会被当成两辆甚至三辆车计数完全不可信。这个项目换成另一种思路由 YOLO 在单帧图像中输出车辆边界框再用质心跟踪算法把连续帧中的同一个目标关联起来是一套典型的计算机视觉加深度学习链路。检测解决“车在哪”的感知问题跟踪解决“是不是同一辆”的关联问题组合之后计数和测速才有可解释性。系统最终输出路段车辆总数和平均速度估计可用于广告牌车流定价、入城车辆统计等宏观场景同时也是课程设计里能完整跑通检测、跟踪、统计三段流程的样本工程。2. 目标检测模块YOLO 权重加载与车辆目标过滤2.1 项目文件布局与权重放置解压之后的目录结构并不复杂核心入口是 main.py跟踪逻辑集中在 tracking.py检测封装在 TrafficSurveillance/ObjectDetection.py 中。model_data 下放的是 YOLO 运行时的配置文件yolo_anchors.txt 保存候选框的预设尺寸coco_classes.txt 是 COCO 数据集的 80 类标签pascal_classes.txt 是 PASCAL VOC 的 20 类标签。预训练权重文件因为体积比较大没有和源码放在一起需要单独下载后放进 model_data 目录否则 main.py 会在加载模型阶段直接退出。这里有一个容易踩的坑同一个权重文件必须和 classes 文件、anchors 文件配套使用。混用 coco_classes 和 pascal 权重检测输出会完全错位车框全部落在行人或红绿灯上。下表是各文件的职责排错时对照着看能省不少时间。文件/目录职责运行时要点main.py程序入口读取视频并串联检测、跟踪、统计流程输入视频路径、模型权重路径都在这里改tracking.py质心跟踪器与 TrackableObject 定义update() 接收检测质心返回新旧 IDTrafficSurveillance/ObjectDetection.pyYOLO 检测封装加载模型并执行单帧推理predict 返回 boxes、scores、classesmodel_data/yolo_anchors.txtYOLO 解码层需要的 anchor 尺寸换权重文件时必须同步替换model_data/coco_classes.txtCOCO 类别名按索引对应模型输出车辆类集中在索引 2、3、5、7VideoDataSets/测试用 mp4/avi 视频集建议先跑 4.avi 这类短片段权重文件命名和路径变更时注意 main.py 里的模型路径、类别路径、anchor 路径要同时改。项目里这几个参数通常集中在文件顶部的配置区放在同一个字典里维护不要只改某一个路径否则加载阶段会报 shape 不匹配或者直接找不到文件。2.2 检测器的初始化与单帧推理YOLO 的调用通常被封装成一个 Detector 类初始化时加载网络结构、权重、类别和两个关键阈值。下面这段以通用命名写具体类名以项目里的 ObjectDetection.py 为准。from TrafficSurveillance.ObjectDetection import Detector # 初始化检测器weights 文件放在 model_data 目录 # 假设权重文件名为 yolo_weights.h5实际名称以你下载到的为准 detector Detector( yolo_weights_pathmodel_data/yolo_weights.h5, anchors_pathmodel_data/yolo_anchors.txt, classes_pathmodel_data/coco_classes.txt, score_threshold0.5, iou_threshold0.45, ) frame capture.read() # 拿到一帧 BGR 图像 boxes, scores, classes detector.detect(frame)score_threshold 是置信度下限低于这个值的框会被直接丢弃。固定机位高速场景里远处车辆在图像中往往只有 20 到 40 像素大小模型给出的置信度天然偏低我一般会把它放到 0.4漏检对后续计数的影响比误检更大。iou_threshold 用于非极大值抑制同类别的多个重叠框只保留得分最高的一个0.45 是目标检测任务里的常见取值如果想减少紧邻车辆的重复框可以提高一点但并排车辆更容易被合并成一个框。detect() 返回的 boxes 通常是 (x1, y1, x2, y2) 绝对坐标classes 是类别索引而不是类别名后面过滤车辆类时要用索引去和 classes.txt 对应。如果本机同时跑多个模型或者显存比较小TensorFlow 默认会在进程启动时预占全部显存Detector 初始化时容易显存不足。常见做法是允许显存按需增长在导入模型之前配置import tensorflow as tf gpus tf.config.experimental.list_physical_devices(GPU) if gpus: tf.config.experimental.set_memory_growth(gpus[0], True)这段配置要放在 Detector 实例化之前否则 TensorFlow 已经申请了显存设置不会再生效。CPU 环境下不需要这段代码但推理速度会明显慢于 GPU视频分辨率如果超过 1080p建议先把帧缩放到 416x416 或 608x608 再送入网络。2.3 把检测结果过滤成车辆类别COCO 的 80 类中与道路上机动车相关的只有 car、motorcycle、bus、truck 四类索引分别是 2、3、5、7从 0 数起。如果不做类别过滤行人、红绿灯、路牌都会被送进跟踪器车辆计数值会被明显放大。过滤逻辑很直白# 只保留车辆类索引与 coco_classes.txt 一一对应 vehicle_class_ids {2, 3, 5, 7} valid_boxes, valid_scores, valid_classes [], [], [] for box, score, cls in zip(boxes, scores, classes): if cls in vehicle_class_ids: valid_boxes.append(box) valid_scores.append(score) valid_classes.append(cls)如果从 PASCAL VOC 权重换过来类别索引和名称就变了VOC 只有 20 类car、motorbike、bus、truck 的索引分别是 6、3、5、7。这也是项目里同时放两份 classes.txt 的原因。过滤完的 boxes 可以直接提取质心x int((x1 x2) / 2)y int((y1 y2) / 2)。这里的 y 坐标取的是框中心但做测速时建议改取框底边中点因为车底与地面接触点更稳定不会随车辆高度而变化。后面讲速度估计时还会回到这个细节。3. 质心跟踪跨帧 ID 分配与去重逻辑3.1 用质心而不是完整框做目标状态YOLO 输出的是一组带宽高的矩形框但跨帧关联只需要判断“这个点在上一帧对应哪个点”。取框中心作为质心原因是固定机位下车辆运动近似平移框的形变主要来自远近变化质心位置对尺度变化不敏感。tracking.py 里维护两个集合上一帧所有车辆的质心集合和当前帧检测到的质心集合。为了让新一帧能继续跟踪每个被跟踪对象还需要保存历史信息典型状态可以简化成下面这个结构from collections import deque class TrackableObject: def __init__(self, centroid): self.centroid centroid # 当前质心 self.centroids deque(maxlen16) # 历史质心用于计算速度 self.centroids.append(centroid) self.track_id None # 由跟踪器分配 self.counted False # 是否已计入总数 self.disappeared 0 # 连续丢失帧数 self.speed_avg 0.0 # 滑动平均速度像素/秒历史质心用 deque(maxlen16) 而不是 list是因为速度计算只需要最近一小段轨迹旧点自动被挤出内存开销稳定。这个对象在跟踪器注册新 ID 时创建在车辆离开画面或持续跟踪失败时销毁。关键阈值参数见下表。参数建议值说明max_distance50~80当前帧质心与上一帧质心的最大匹配距离像素max_disappeared20~30连续未匹配帧数超过后删除对象score_threshold0.4~0.5YOLO 置信度阈值小目标场景取下限iou_threshold0.45NMS 阈值决定重叠框是否合并3.2 最近邻匹配算法每来一帧跟踪器先把上一帧已有的质心集合记为 existing把当前帧检测到的质心集合记为 input。对 input 里的每个点计算它与所有 existing 点的欧氏距离选出距离最小的那一个只要最小距离小于预设的 max_distance就认为二者是同一辆车更新对应质心否则认为出现了新车。跟踪器的核心逻辑可以用下面这个类表达import numpy as np class CentroidTracker: def __init__(self, max_distance50, max_disappeared20): self.next_id 1 self.max_distance max_distance # 匹配距离阈值像素 self.max_disappeared max_disappeared # 车辆连续丢失多少帧后删除 self.objects {} # track_id - TrackableObject def _register(self, centroid): obj TrackableObject(centroid) obj.track_id self.next_id self.objects[self.next_id] obj self.next_id 1 def update(self, input_centroids): if input_centroids is None or len(input_centroids) 0: return self.objects # 当前没有任何车辆全部注册为新 ID if len(self.objects) 0: for pt in input_centroids: self._register(pt) return self.objects existing_ids list(self.objects.keys()) existing_pts np.array([self.objects[i].centroid for i in existing_ids]) input_pts np.array(input_centroids, dtypefloat) # 距离矩阵行是当前帧目标列是上一帧目标 dists np.linalg.norm(input_pts[:, None, :] - existing_pts[None, :, :], axis2) # 贪心匹配按距离从小到大取出合法配对 matched_pairs [] for i in range(len(input_pts)): j int(np.argmin(dists[i])) if dists[i, j] self.max_distance: matched_pairs.append((i, j)) self.objects[existing_ids[j]].centroid input_pts[i] self.objects[existing_ids[j]].disappeared 0 self.objects[existing_ids[j]].centroids.append(input_pts[i]) matched_existing {j for i, j in matched_pairs} matched_input {i for i, j in matched_pairs} # 未匹配上的 input 点注册为新车辆 for i in range(len(input_pts)): if i not in matched_input: self._register(input_pts[i]) # 上一帧有、当前帧没匹配上的对象消失计数加一 for track_id, obj in list(self.objects.items()): if track_id in matched_existing: continue obj.disappeared 1 if obj.disappeared self.max_disappeared: del self.objects[track_id] return self.objects这个简化版没有做“一个 existing 点只能被匹配一次”的互斥处理实际项目里通常会用行和列的匹配状态做双向去重但骨架已经足够说明质心跟踪的原理。np.linalg.norm 的维度展开可能对不熟悉 NumPy 广播的人有点绕实际效果就是造了一个 len(input) 行、len(existing) 列的距离矩阵再逐行取最小距离。max_distance 是很敏感的参数。25fps 固定机位下一辆 100km/h 的车在两帧之间大约移动 40 到 60 像素取决于视角和焦距阈值取 50 能覆盖大多数情况。如果视频帧率更高或者车速更低可以放宽到 80车辆密集且相互穿插时阈值越小越不容易串 ID。距离矩阵法在车辆数量超过几十辆时会有些吃力每帧都要做 N×M 次距离计算复杂度是 O(NM)。高速拥堵画面里同时出现 40 辆以上车时贪心匹配还会因为两车距离过近而交换 ID。如果要做这种极端场景建议把匹配距离阈值调小再结合历史速度预测下一帧质心位置在距离计算前先排除明显不可能的候选。3.3 计数时刻与去重跟踪器自身不负责计数main.py 在拿到 update() 返回结果后遍历 objects凡是发现一个 track_id 不在已统计集合中就把 counter 加一并把它标记为已统计。这个去重很关键否则一辆车停留在画面里 200 帧每帧都会被当成新车。新车产生只有一个入口某帧检测到一个质心但和当前所有现存质心的距离都大于 max_distance。这个逻辑同时决定了计数误差来源只要有一辆车漏检一帧质心跳变超过阈值就会被重新注册成新 ID导致重复计数。漏检比误检对计数的破坏性更大所以检测阶段宁可多几个误检框也不要让置信度阈值卡得太高。把 counted 状态放在主循环而不是跟踪器内部也是一种刻意设计。如果跟踪器自己维护 counted 状态之后想复用跟踪器去做多车道分线统计、按方向分类统计就要改跟踪器的实现现在计数逻辑和跟踪逻辑解耦换一个业务场景时只改 main.py 里的统计部分就够了。4. 车辆计数与平均速度估计的工程实现4.1 主循环怎么把三个模块串起来main.py 的整体流程是打开视频 → 逐帧送入 detector.detect() → 过滤车辆类提取质心 → 交给 tracker.update() → 遍历返回对象完成计数和速度累积 → 画框画文本写回输出视频。下面这个循环基本是项目主流程的骨架capture cv2.VideoCapture(VideoDataSets/4.avi) tracker CentroidTracker(max_distance50, max_disappeared30) fps capture.get(cv2.CAP_PROP_FPS) # 用于时间差换算 scale 0.05 # 像素到米的标定系数需要实测 counted_ids set() total_vehicles 0 while True: ok, frame capture.read() if not ok: break boxes, _, classes detector.detect(frame) centroids [] for box, cls in zip(boxes, classes): if cls in vehicle_class_ids: # 只保留车辆 x1, y1, x2, y2 box # 用底边中点做质心接近车辆与地面的接触点 centroids.append(((float(x1) float(x2)) / 2, float(y2))) objects tracker.update(centroids) for track_id, obj in objects.items(): if track_id not in counted_ids: total_vehicles 1 counted_ids.add(track_id)这里用底边中点而不是框中心原因是车辆高度不同框中心会随车型上下浮动底边与轮胎边缘基本贴地投影位置稳定得多。centroids 的数据类型要注意tracker 内部用 np.linalg.norm传 list of tuple 会被自动转成 float 数组如果传入的是整数坐标距离计算会损失精度建议在派生质心时直接用 float。如果 VideoDataSets 里有多个视频最好把每段视频的输出结果单独写成一个文件主循环外面按文件名做一次外循环即可。videocapture 的 get 方法在部分 AVI 编码下可能返回 0.0这种情况下 fps 要用 25 兜底否则后面速度计算会出现除零或者里程计爆炸。4.2 平均速度估计从像素位移到 km/h速度计算需要两个信息相邻两帧间的车辆位移以及这段时间的真实时长。位移直接用车底质心在帧间的欧氏距离时长用 1/fps。但这个瞬时速度抖动非常大检测框哪怕只抖 3 个像素在 30fps 下就会造成 90 像素/秒的误差。项目对每个 TrackableObject 维护一段历史轨迹取最近 N 帧的位移平均再把像素每秒换算成实际速度if obj.centroid is not None and len(obj.centroids) 5: # 取最近 5 个质心首尾相减算平均位移 start np.array(obj.centroids[-5]) end np.array(obj.centroids[-1]) pixel_per_frame np.linalg.norm(end - start) / 4.0 speed_mps pixel_per_frame * fps * scale # 米/秒 obj.speed_avg speed_mps * 3.6 # km/hscale 是每个像素对应的实际米数fps 是视频帧率。取首尾相减而不是累计每帧位移可以抵消部分检测框随机抖动。如果车辆在画面中走的是斜线建议用“沿道路方向的分量”而不是全量欧氏距离否则弯道和变道车辆的速度会被明显高估。判断道路方向的方法不复杂取车辆第一次出现和最近一次出现的质心连线作为道路主轴再把当前帧位移投影到这个主轴上。4.3 标定系数怎么来标定是速度误差的最大来源scale 并不是固定的它由摄像头安装高度、俯仰角、焦距共同决定。项目把标定系数留给使用者自己调比较保守但可靠。如果你没有标定数据一个可接受的初始做法是利用车道线我国高速公路标准车道宽 3.75 米在画面上找到一段完整的同向车道量出它的横向像素宽度scale 3.75 / 横向像素宽。例如画面中一个车道宽 100 像素scale 约等于 0.0375 米/像素这样得到的纵向速度也基本可用因为车道宽度对横向透视不敏感。标定方法已知条件操作车道宽度法标准车道 3.75m画面中量一个车道横向像素宽scale 3.75 / px路面标线法车道虚线白块 6m量连续两个虚线块的像素距离已知测距法两个路牌或立交间距量两点像素距离直接算 scale需要注意透视误差距离摄像头近的车在图像中移动快远处的移动慢使用全局 scale 计算出的平均速度必然存在系统偏差。想要更准确需要把画面按深度分成多个带状区域每段单独标定这就是逆透视映射的思路。对广告牌定价、入城车流量这类宏观统计全局系数已经够用。还要注意标定系数只在固定机位下有效摄像头一旦被风吹偏角度或重新调整之前的 scale 必须重新标定。很多交通分析系统用久了测速偏差变大都是因为摄像头角度已经变了而 scale 没有更新。4.4 输出与可视化最后把计数和速度画在帧上供人工核验。车辆 ID、计数结果、平均速度三个信息分别用 cv2.rectangle 和 cv2.putText 输出。多个车辆同时出现时ID 文本放在框左上角速度文本放在框左下角避免相互覆盖。速度建议在累计 5 帧之后再显示前几帧轨迹太短数值没有意义。统计结果要落盘的话用 csv 模块逐帧写 track_id、frame_no、speed_kmh、timestamp比打印到控制台更便于事后分析。逐帧写 CSV 的数据量不小1 分钟视频大约产生 1500 到 1800 行记录建议只写两帧间隔较大的“有效行”或者按车辆 ID 聚合后输出每辆车一行包含出现帧区间、消失帧、平均速度、最大速度。5. 运行调试权重匹配、标定收敛与实时流扩展5.1 起不来的三个典型原因模型在加载时报错第一件事检查权重文件是否在 model_data 目录且文件名与 main.py 中配置完全一致。第二件事看权重对应的类别文件如果用的是 COCO 权重但 class_path 指向 pascal_classes.txt输出索引会全部错位车辆框会出现在行人和车辆之间跳来跳去。第三件事确认 anchor 文件没有被改动YOLO 的解码层依赖 anchor 尺寸生成候选框anchor 和权重不匹配时召回率明显下跌表现就是小汽车经常漏检而且置信度集中在 0.3 以下。前两个问题按序检查即可第三个问题只能通过换回配套的 anchor 文件解决。5.2 让速度估计收敛到可接受范围用全局 scale 测速时建议按底边 y 坐标分带例如把画面分成远、近两个区域各自给一个独立 scale。标定完先跑 30 秒视频把计算速度与视频中肉眼可判断的车速对比根据偏差方向调整系数。不要用固定 fps 逐帧累积遇到掉帧视频cap.read() 返回的两帧间隔并不均匀更稳妥的做法是读取时记录 cv2.CAP_PROP_POS_MSEC用两帧的真实毫秒差代替 1/fps。判断收敛程度时取同一 ID 在画面中间连续 10 帧的速度曲线正常应是一条低幅波动曲线如果出现周期性尖峰多半是检测框抖动或 ID 切换可以把滑动平均窗口从 5 帧扩到 10 帧。5.3 从离线视频到实时流的改造如果要把 VideoDataSets 的视频换成摄像头核心只改视频源这一行把VideoDataSets/4.avi换成 0默认摄像头或 RTSP 地址。改动之后要留意两件事一是连续帧读取不能再阻塞把 cap.grab() 和 cap.retrieve() 拆开grab 负责拿压缩帧retrieve 负责解码二是检测耗时若超过帧间隔视频会越来越卡此时可以把输入分辨率降到 416x416或者在每 2 帧中只处理 1 帧未检测帧直接沿用上一帧质心坐标做递推。最后一个实用细节实时模式下调大 max_disappeared因为摄像头画面里车辆从出现到离开通常不到 40 帧偶发漏检如果阈值太小车还没走出画面就被删掉ID 会频繁切换计数立刻失真。本文还有配套的精品资源点击获取
返回列表