
简介这份资源面向计算机视觉与智能交通方向的个人学习者提供一套基于YOLOv8与ByteTrack融合的车辆实时检测、追踪与流量统计完整实现。系统通过YOLOv8完成视频帧中车辆定位与车型分类再借助ByteTrack进行跨帧身份关联最终基于轨迹分析实现多车道车辆计数、速度记录与轨迹绘制并具备违章变道、违规停车等异常行为识别能力适合作为课程设计、毕业设计或算法练手项目。资源包共12个文件约74MB以Python源码与依赖配置为主另含演示视频、效果截图、说明文档及若干备份文件便于直接运行与二次修改。目前已有88人学习下载。读者可从中获取从检测、追踪到统计的完整工程代码与运行素材理解多目标追踪中的数据关联与状态估计思路并参考可视化结果快速验证算法效果。1. 从一段路口视频说起YOLOv8ByteTrack 到底在解决什么手里有一段路口监控视频想统计一小时内经过了多少辆车、每辆车往哪个方向走、哪个车道压力最大。人工数不现实纯检测模型逐帧框车也不行——同一辆车在连续 300 帧里会被框 300 次计数直接爆炸。这就是多目标车辆实时检测与流量统计要解决的核心问题检测负责看见跟踪负责认人统计负责记账。YOLOv8 负责每帧把车、卡车、公交车、摩托车框出来ByteTrack 负责把这些框在时间轴上串成一条条轨迹每条轨迹给一个稳定 ID最后在虚拟线圈或越线判定上做加减计数。整套方案适合做交通流量统计、停车场进出管理、卡口过车记录这类场景硬件从 GTX1660Ti 到 RK3588、Orin 都能跑关键看你把检测和跟踪的算力预算怎么分配。下面按先跑通、再调参、最后避坑的顺序讲清楚。2. 检测与跟踪的分工为什么是 YOLOv8 配 ByteTrack2.1 YOLOv8 在流量统计里的角色边界很多人一上来就想让 YOLOv8 直接输出第几辆车这是方向性错误。YOLOv8 是单帧检测器它的输出是当前帧的边界框、类别和置信度帧与帧之间没有任何记忆。你给它两张相邻帧它不知道框里是不是同一辆车。所以检测模型在系统里的职责被严格限定为在每一帧里尽可能准、尽可能快地给出车辆的位置和类别。选 YOLOv8 而不是更早的版本实际工程里主要看三点。第一是 anchor-free 的检测头省掉了聚簇调 anchor 的环节换数据集时不用重新跑 k-means对交通场景这种目标尺度跨度大的任务更省心。第二是模型尺寸梯度完整n/s/m/l/x 五档GTX1660Ti 这种 6G 显存卡跑 s 档能到实时RK3588 板端跑 n 档量化后也能撑住 15fps 以上。第三是训练和导出链路成熟yolov8n.pt这类预训练权重直接能拿来微调model.export(formatonnx)一行出 ONNX部署侧不用自己写转换脚本。需要提醒的是YOLOv8 官方预训练权重是在 COCO 上训的车辆类别只有 car、bus、truck、motorcycle 这几个粗类。如果你要区分轿车/SUV/面包车或者要识别车牌必须自己标数据微调别指望预训练权重直接给你细分类。2.2 ByteTrack 的匹配逻辑低分框为什么不能直接扔ByteTrack 的核心贡献就一句话把检测框按置信度分成高低两档高分框先匹配低分框再拿去补匹配那些没跟上的轨迹。传统 SORT 类方法会把置信度低于阈值的框直接丢掉但车辆在遮挡、远距离、运动模糊时检测分数经常掉到 0.3 以下这些框虽然分数低位置却是对的扔掉就等于让轨迹断掉ID 一断计数就错。ByteTrack 的匹配分两步走。第一步用高分框比如 conf0.5和现有轨迹做 IoU 匹配用的是匈牙利算法代价矩阵是 IoU 距离。匹配上的轨迹更新状态没匹配上的轨迹进入第二步。第二步把低分框0.1conf0.5拿出来和第一步剩下的未匹配轨迹再匹配一次这次只算 IoU不看分数。这样遮挡期间轨迹能靠低分框续上ID 不会跳。# ByteTrack 匹配逻辑的简化示意帮助理解高低分两阶段 def bytetrack_associate(detections, tracks, high_th0.5, low_th0.1): # 按置信度切分检测框 high_dets [d for d in detections if d.score high_th] low_dets [d for d in detections if low_th d.score high_th] # 第一阶段高分框与轨迹做 IoU 匹配 matched, unmatched_tracks, unmatched_high iou_match(high_dets, tracks) for t, d in matched: t.update(d) # 匹配上的轨迹用检测框更新位置 # 第二阶段低分框与剩余轨迹再匹配只算 IoU 不看分数 matched2, unmatched_tracks2, _ iou_match(low_dets, unmatched_tracks) for t, d in matched2: t.update(d) # 遮挡恢复时靠这一步续上 ID # 仍未匹配的轨迹进入丢失缓存超过 max_age 帧才删除 for t in unmatched_tracks2: t.mark_lost() return tracks这段逻辑里三个参数最关键。high_th决定哪些框算可信交通场景一般设 0.5夜间或雨雾可以降到 0.4。low_th是低分框下限设 0.1 是经验值再低会把背景噪声引进来。max_age是轨迹丢失后保留多少帧路口场景车被大车挡住通常 2~3 秒按 25fps 算就是 50~75 帧设太小 ID 会断设太大容易把已经离开的车和新车搞混。2.3 检测跟踪串起来的最小可跑代码把 YOLOv8 和 ByteTrack 接起来最省事的方式是用 Ultralytics 自带的model.track()它内部已经集成了 ByteTrack 和 BoT-SORT。下面这段是本地跑通的最小骨架。from ultralytics import YOLO import cv2 # 加载 YOLOv8 检测权重n 档适合先验证流程 model YOLO(yolov8n.pt) # 打开视频源0 是摄像头也可以传视频文件路径 cap cv2.VideoCapture(road.mp4) # 只保留车辆相关类别COCO 里 2car 3motorcycle 5bus 7truck VEHICLE_CLASSES [2, 3, 5, 7] while cap.isOpened(): ret, frame cap.read() if not ret: break # persistTrue 是关键告诉跟踪器这是连续帧不要每帧重置 results model.track( frame, persistTrue, trackerbytetrack.yaml, # 指定 ByteTrack 配置 classesVEHICLE_CLASSES, # 只检测车辆减少误检 conf0.3, # 检测置信度阈值 iou0.5, # NMS 的 IoU 阈值 verboseFalse ) # 取出带 ID 的跟踪结果 if results[0].boxes.id is not None: boxes results[0].boxes.xyxy.cpu().numpy() ids results[0].boxes.id.cpu().numpy().astype(int) for box, tid in zip(boxes, ids): x1, y1, x2, y2 map(int, box) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, fID {tid}, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(track, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()persistTrue是最容易漏的参数不加的话每帧都会当成新序列ID 每帧都在变跟踪等于没做。classes过滤掉行人、红绿灯等无关类别既提速又减少误检。conf0.3比默认 0.25 略高是因为跟踪阶段低分框由 ByteTrack 自己处理检测阶段不用放太松。trackerbytetrack.yaml指向 Ultralytics 内置配置里面就是track_high_thresh、track_low_thresh、track_buffer这几个参数需要调的时候直接改这个 yaml 或复制一份改。3. 流量统计怎么落地虚拟线圈、越线判定与计数去重3.1 虚拟线圈和越线判定的实现检测和跟踪跑通后统计就是在这条轨迹上做几何判断。最常见两种方案虚拟线圈和越线判定。虚拟线圈是在画面上画一个矩形区域轨迹中心点从区域外进入区域内算一次进入从内到外算一次离开进出配对得到车流量。越线判定是画一条线轨迹中心点从线的一侧穿到另一侧算一次通过配合方向向量还能区分上下行。# 越线计数判断轨迹中心点是否穿过一条水平线 class LineCounter: def __init__(self, line_y, directiondown): self.line_y line_y # 线的 y 坐标 self.direction direction # 允许通过的方向 self.prev_side {} # 记录每个 ID 上一帧在线的哪一侧 self.counted set() # 已计数的 ID防止来回抖动重复计 def update(self, track_id, cy): side below if cy self.line_y else above prev self.prev_side.get(track_id) # 首次出现只记录位置不计数 if prev is None: self.prev_side[track_id] side return False # 发生跨线且方向符合且该 ID 没计过 crossed (prev ! side) if crossed and track_id not in self.counted: if self.direction down and prev above and side below: self.counted.add(track_id) self.prev_side[track_id] side return True if self.direction up and prev below and side above: self.counted.add(track_id) self.prev_side[track_id] side return True self.prev_side[track_id] side return Falsecounted这个集合是防重复计数的后悔药。车辆在线上来回抖动、或者跟踪 ID 短暂跳变又跳回来都会导致同一辆车被计两次。用 ID 做去重键配合方向判断能挡掉大部分误计。prev_side字典要定期清理轨迹消失后对应条目留着会占内存一般配合跟踪器的max_age一起清。3.2 计数去重与 ID 跳变的处理ID 跳变是流量统计里最头疼的问题。车被遮挡几帧ByteTrack 续不上重新出现时给了新 ID同一辆车就被计了两次。工程上有几种缓解手段。一是调大track_buffer让轨迹丢失后保留更久给低分框更多机会续上。二是用外观特征做二次匹配ByteTrack 本身不带 ReID可以外挂一个轻量 ReID 模型在 ID 跳变时用特征相似度把新旧轨迹关联起来。三是统计层面做去重同一位置短时间内出现的新 ID如果运动方向一致、时间间隔小于阈值判定为同一辆车。# 基于时空约束的 ID 去重短时间内同方向的新 ID 视为同一辆车 class IDDeduplicator: def __init__(self, time_window1.5, dist_thresh80): self.time_window time_window # 秒 self.dist_thresh dist_thresh # 像素 self.recent [] # (timestamp, cx, cy, direction) def is_duplicate(self, ts, cx, cy, direction): self.recent [r for r in self.recent if ts - r[0] self.time_window] for r_ts, r_cx, r_cy, r_dir in self.recent: dist ((cx - r_cx) ** 2 (cy - r_cy) ** 2) ** 0.5 if dist self.dist_thresh and r_dir direction: return True self.recent.append((ts, cx, cy, direction)) return Falsetime_window设 1.5 秒是经验值路口车速下 1.5 秒车移动距离有限超过这个窗口的新 ID 更可能是真的新车。dist_thresh按画面分辨率调1080p 下 80 像素大概对应半个车身宽度。这套去重是统计层的补丁不能替代跟踪层的稳定根子上还是要把 ByteTrack 参数调好。3.3 分车道、分方向的流量统计表单一线圈只能给总数实际项目往往要分车道、分方向。做法是给每个车道画独立的线圈或线每条轨迹根据中心点落在哪个车道区域归属到对应车道再叠加方向判断最后按分钟或小时聚合。统计维度实现方式关键参数输出示例总流量单线圈进出配对线圈坐标1200 辆/小时分车道每车道独立线圈车道多边形左道 400中道 500右道 300分方向越线方向向量方向阈值上行 600下行 600分车型检测类别映射类别 ID轿车 900卡车 200公交 100分时段时间窗口聚合窗口长度每分钟 20 辆聚合时注意时间窗口的边界跨窗口的轨迹要归到越线那一刻所在的窗口不能按轨迹开始时间算否则高峰期数据会错位。4. 避坑与排查那些让计数对不上的细节4.1 现象同一辆车被计了多次原因通常是 ID 跳变或轨迹抖动。车被遮挡后 ByteTrack 给了新 ID或者车在线上来回移动导致多次跨线判定。解决分两层跟踪层调大track_buffer到 50~75降低track_high_thresh到 0.4 让更多框参与匹配统计层用 ID 去重集合加时空去重双保险。如果还不行检查视频帧率是否稳定掉帧会让跟踪器的时间假设失效。4.2 现象夜间或逆光下漏检严重YOLOv8 在低照度下召回率下降是常态检测漏了跟踪自然断。解决不是硬调检测阈值而是先做图像预处理CLAHE 增强对比度或者用 Gamma 校正提亮暗部。如果场景固定最彻底的办法是采集夜间数据微调模型加几百张夜间标注图mAP 能明显回升。临时方案是把conf降到 0.2靠 ByteTrack 的低分框机制兜底但误检会增多要配合类别过滤。4.3 现象计数比实际少轨迹频繁断除了遮挡常见原因是检测框抖动导致 IoU 匹配失败。车静止或慢速时相邻帧检测框位置几乎不变IoU 很高没问题但车快速运动时相邻帧位移大IoU 可能低于匹配阈值。解决是调低 ByteTrack 的 IoU 匹配阈值或者改用中心点距离做代价矩阵。另外检查max_age是不是设太小车被挡 2 秒就删轨迹出来就是新 ID。4.4 现象GPU 利用率低但帧率上不去瓶颈往往不在 GPU 而在前后处理。视频解码用 CPU 软解会拖慢整体换成cv2.CAP_FFMPEG或硬件解码能提速。另外model.track()每帧都做 NMS 和跟踪匹配如果画面里车不多可以把检测间隔拉大比如每 2 帧检测一次中间帧靠跟踪器预测位置帧率能翻倍代价是快速运动时框会滞后。4.5 现象RK3588 板端跑不动板端部署和 PC 是两套逻辑。YOLOv8 要导出 ONNX 再转 RKNN量化到 int8n 档模型在 RK3588 上能到 15~20fps。ByteTrack 是纯 CPU 逻辑反而成了瓶颈要控制轨迹数量及时清理丢失轨迹。常见翻车点是量化后精度掉太多解决是用混合量化检测头部分保留 fp16。板端内存有限视频缓冲别开太大否则容易 OOM。5. 把统计做准的进阶技巧从能跑到可信系统能跑起来只是第一步统计结果能不能信取决于你怎么处理边界情况。我一般会加一个轨迹质量分综合轨迹长度、平均检测置信度、ID 切换次数给每条轨迹打分低于阈值的轨迹不参与计数。这样能挡掉那些一闪而过的误检和碎片轨迹。# 轨迹质量分长度、置信度、ID 稳定性加权 def track_quality(track): length_score min(track.hits / 30.0, 1.0) # 至少 30 帧才算完整 conf_score track.avg_score # 平均检测置信度 id_score 1.0 / (1 track.id_switches) # ID 切换越多分越低 return 0.4 * length_score 0.4 * conf_score 0.2 * id_scorehits是轨迹被成功匹配的帧数30 帧在 25fps 下约 1.2 秒低于这个的轨迹大概率是噪声。avg_score是轨迹生命周期内检测框的平均置信度低于 0.4 的轨迹要警惕。id_switches记录这条轨迹关联过几个不同 ID切换多说明跟踪不稳。三个权重按场景调路口场景长度和置信度更重要权重可以给到 0.4/0.4。验证统计准不准最土也最有效的办法是人工抽帧核对。抽 10 段各 1 分钟的视频人工数一遍和系统输出对比误差超过 5% 就回去查参数。别只看总数要分车道、分方向对总数对得上但分车道错位的情况很常见那是线圈画歪了。还有一个容易被忽略的点时间同步。如果视频有丢帧或者处理速度跟不上导致跳帧基于帧序号的逻辑都会错。我习惯在处理循环里记录真实时间戳统计窗口按真实时间切不按帧号切。这样即使帧率波动每分钟的流量统计依然准。最后说个习惯每次调完参数别只看一段视频就下结论。交通流量有早晚高峰、有随机波动至少跑满一个完整周期比如早 7 点到晚 7 点再看统计曲线是否合理。我踩过最深的坑就是拿一段平峰视频调好参数上线遇到高峰直接崩ID 跳变率翻了三倍。参数要按最坏场景调不是按最好场景调。希望帮到你。本文还有配套的精品资源点击获取