基于YOLOv8与DeepSORT的实时人流量检测系统实战

发布时间:2026/10/11 12:20:10
基于YOLOv8与DeepSORT的实时人流量检测系统实战 简介这份资源面向计算机相关专业的毕业设计、课程设计与期末综合作业场景提供一套基于深度学习的实时人流量检测系统完整方案与Python源码。项目已通过导师审核并获优秀评价涵盖数据收集与预处理、模型训练与评估、实时视频流检测计数等环节后端可依托TensorFlow或PyTorch构建神经网络核心run.py负责整合模型加载、视频流处理与结果输出并配有部署说明与使用指南初学者也能快速上手。压缩包共1482个文件约61.53MB以382个html、194个js、208个png等前端与界面资源为主另有76个py源码、32个pyc、8个md文档及若干json、yaml配置兼顾代码、文档与展示素材。目前已有57人学习下载适合需要完整可运行项目、排错思路与目录结构参考的学生与开发者。1. 实时人流量检测系统从 YOLO 到 DeepSORT 的工程化落地商场入口的摄像头画面里人一多就糊成一片靠人工盯屏统计进出人数十分钟就眼花。基于深度学习的实时人流量检测系统要解决的就是这件事用目标检测模型把人框出来再用多目标跟踪算法给每个人分配稳定 ID最后按越线规则计数。整套流程用 Python 就能跑通从模型推理到业务逻辑不超过 500 行核心代码。适合有 Python 基础、想做一个能落地的深度学习实战项目的开发者也适合需要快速验证客流统计方案的工程团队。源码解析的重点不在模型结构本身而在检测与跟踪的衔接、ID 跳变处理和计数逻辑的鲁棒性。2. 检测与跟踪的选型为什么是 YOLOv8 DeepSORT2.1 检测模型选型精度、速度与部署成本的三角权衡实时人流量检测的第一环是行人检测。常见做法有三类两阶段检测器Faster R-CNN 系列、单阶段检测器YOLO 系列、SSD、以及基于 Transformer 的 DETR 系列。两阶段精度高但推理慢DETR 对小目标友好但训练成本高YOLO 系列在速度和精度之间取得了最好的平衡。YOLOv8 是目前工程落地中最常见的选择原因有三一是 Ultralytics 封装的 Python API 极其简洁三行代码就能完成推理二是 n/s/m/l/x 五个尺寸覆盖了从边缘设备到服务器的全部场景三是社区生态成熟导出 ONNX、TensorRT 的路径清晰。对于人流量检测这种只需要检测“人”一个类别的任务YOLOv8n 或 YOLOv8s 就足够了在 1080p 画面上用 GPU 推理可以轻松跑到 60 FPS 以上。如果部署在边缘设备如 Jetson Nano、树莓派可以考虑 YOLOv8n 导出 NCNN 或 ONNX Runtime牺牲少量精度换取实时性。我一般会先用 YOLOv8s 在目标硬件上测一轮 FPS如果低于 15 FPS 再降级到 n。2.2 跟踪算法选型DeepSORT 的工程优势与边界检测只给出每一帧的人框帧与帧之间没有关联。跟踪算法要解决的是第 1 帧里的张三和第 10 帧里的张三怎么判定是同一个人。常见方案有 SORT、DeepSORT、ByteTrack、OC-SORT。DeepSORT 在 SORT 的基础上增加了外观特征提取网络ReID通过卡尔曼滤波预测轨迹、匈牙利算法做匹配、级联匹配处理遮挡。它的优势是 ID 切换率低适合人流量检测这种需要稳定计数的场景。ByteTrack 速度更快但依赖检测质量遮挡严重时 ID 跳变明显。OC-SORT 在非线性运动场景下表现更好但工程资料相对少。我选 DeepSORT 的核心理由是它的 ReID 特征在人群密集、短暂遮挡的场景下能显著降低 ID 跳变而人流量计数最怕的就是同一个人被重复计数。代价是每帧要多跑一次 ReID 网络FPS 会下降 20% 到 30%。如果硬件吃紧可以换 ByteTrack但计数逻辑要加去重策略。2.3 最小可跑通环境搭建先确认 Python 版本。YOLOv8 要求 Python 3.8 以上DeepSORT 的依赖对 3.11 兼容性一般建议用 3.9 或 3.10。安装命令如下# 创建虚拟环境避免污染全局包 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装核心依赖 pip install ultralytics8.1.0 pip install deep-sort-realtime1.3.2 pip install opencv-python4.9.0.80 pip install numpy1.24.3这里选deep-sort-realtime而不是原版deep_sort因为前者封装更干净、依赖更少且支持直接传入检测框和置信度。ultralytics的版本锁定在 8.1.0 是因为后续版本 API 有变动源码解析以这个版本为准。验证安装是否成功import ultralytics from deep_sort_realtime.deepsort_tracker import DeepSort import cv2 print(ultralytics.__version__) print(cv2.__version__)如果ultralytics导入报错大概率是 torch 版本不匹配。先pip install torch torchvision再重试。CPU 环境下 torch 安装包约 200MBGPU 版本需要根据 CUDA 版本去 PyTorch 官网找对应命令。3. 从视频流到计数检测、跟踪、越线判定的完整链路3.1 用 YOLOv8 做行人检测的最小代码先跑通单帧检测确认模型能正确框出人。YOLOv8 预训练权重在 COCO 数据集上训练过其中 class 0 就是 person不需要自己训练就能用。from ultralytics import YOLO import cv2 # 加载预训练模型首次运行会自动下载权重 model YOLO(yolov8s.pt) # 读取一帧测试 frame cv2.imread(test.jpg) results model(frame, classes[0], conf0.5, iou0.45) # 解析检测结果 for r in results: boxes r.boxes for box in boxes: x1, y1, x2, y2 box.xyxy[0].tolist() conf box.conf[0].item() print(fperson: ({x1:.0f},{y1:.0f})-({x2:.0f},{y2:.0f}) conf{conf:.2f})classes[0]只保留 person 类减少后处理开销。conf0.5是置信度阈值低于这个值的框直接丢弃人流量场景建议设 0.4 到 0.6太低会引入误检太高会漏掉远处的小目标。iou0.45控制 NMS 的合并阈值人群密集时可以调到 0.5 到 0.6避免相邻的人被合并成一个框。3.2 DeepSORT 跟踪器初始化与参数调优DeepSORT 的核心参数有三个max_age、n_init、max_iou_distance。它们直接决定 ID 稳定性和计数准确性。from deep_sort_realtime.deepsort_tracker import DeepSort tracker DeepSort( max_age30, # 轨迹丢失后保留的帧数 n_init3, # 连续命中多少帧才确认新轨迹 max_iou_distance0.7, # IOU 匹配阈值 embeddermobilenet, # ReID 特征提取网络 embedder_gpuTrue, # 有 GPU 就开 halfTrue # 半精度推理省显存 )max_age30意味着一个人被遮挡后轨迹保留 30 帧约 1 秒等待重新匹配。设太小会导致遮挡后 ID 跳变设太大会让离开画面的人被错误匹配到新出现的人。n_init3要求连续 3 帧检测到才确认轨迹能过滤掉单帧误检。max_iou_distance0.7是 IOU 匹配阈值人群密集时可以降到 0.5 到 0.6减少错误匹配。3.3 检测结果转 DeepSORT 输入格式YOLO 输出的框格式是[x1, y1, x2, y2]DeepSORT 需要的是[left, top, width, height]加置信度。这个转换看起来简单但坐标格式写错是新手最常见的翻车点。def yolo_to_deepsort(results, conf_threshold0.5): 将 YOLO 检测结果转为 DeepSORT 输入格式 detections [] for r in results: for box in r.boxes: conf box.conf[0].item() if conf conf_threshold: continue x1, y1, x2, y2 box.xyxy[0].tolist() # DeepSORT 需要 [left, top, w, h] w x2 - x1 h y2 - y1 detections.append(([x1, y1, w, h], conf, person)) return detections注意box.xyxy[0]返回的是 tensor必须.tolist()转成 Python 列表否则 DeepSORT 内部做数值计算时会报类型错误。person这个类别标签在 DeepSORT 里只是透传不影响跟踪逻辑但方便后续按类别过滤。3.4 越线计数用线段交叉判断进出方向计数逻辑是整个系统最容易出玄学问题的地方。常见做法是画一条虚拟线判断人的中心点是否从线的一侧移动到另一侧。但直接用中心点位置判断会有抖动问题人在线上来回走会导致反复计数。更稳的做法是用轨迹的历史点做线段交叉判断def check_crossing(track_history, line_start, line_end): 判断轨迹是否跨越计数线 if len(track_history) 2: return 0 prev track_history[-2] curr track_history[-1] # 用叉积判断两点是否在线的两侧 def cross_product(p, a, b): return (b[0]-a[0])*(p[1]-a[1]) - (b[1]-a[1])*(p[0]-a[0]) d1 cross_product(prev, line_start, line_end) d2 cross_product(curr, line_start, line_end) if d1 * d2 0: # 异号说明跨越了 # 再用方向判断是进还是出 return 1 if d1 0 else -1 return 0track_history保存每个 ID 最近若干帧的中心点。叉积异号说明两点在线的两侧即发生了跨越。方向由d1的符号决定正负分别对应进和出。这个逻辑比单纯判断中心点位置稳定得多因为要求连续两帧都在线的不同侧才触发。4. 避坑与排查ID 跳变、漏检、计数翻倍的实战记录4.1 同一个人被分配多个 ID现象画面里一个人走过去计数结果加了 3 次。原因n_init设得太小比如 1单帧误检也会创建新轨迹或者max_age太小遮挡几帧后轨迹被删除重新出现时分配了新 ID。解决n_init至少设 3max_age设 25 到 30。如果 ReID 特征质量差换embeddermobilenet为torchreid但速度会慢一些。另外检查检测框是否抖动严重可以加一个简单的中心点平滑滤波。4.2 远处的人检测不到现象画面远端的人漏检计数偏少。原因YOLOv8 默认输入尺寸是 640远处的人在这个分辨率下只有十几个像素特征不足以触发检测。解决把推理尺寸调到 1280 或 1536model(frame, imgsz1280)。代价是 FPS 下降约 40%。另一个办法是用切片推理SAHI把大图切成小块分别检测再合并但工程复杂度高。我一般先试imgsz1280不够再考虑切片。4.3 计数线附近来回走导致反复计数现象有人在计数线附近徘徊计数结果反复加减。原因中心点抖动导致叉积符号反复变化。解决加一个冷却时间同一个 ID 在 2 秒内只允许计数一次。或者要求轨迹连续 3 帧都在线的同一侧才确认跨越。代码里加一个last_count_time字典按 ID 记录上次计数时间。4.4 GPU 显存溢出现象跑一段时间后报CUDA out of memory。原因DeepSORT 的 ReID 特征缓存没有及时释放或者 YOLO 推理的中间张量累积。解决在 DeepSORT 初始化时设halfTrue用半精度每处理 1000 帧手动torch.cuda.empty_cache()如果还不行把max_age降到 20 减少轨迹缓存。CPU 环境下不会溢出但速度会慢很多建议至少用 GTX 1660 以上的卡。4.5 视频流读取延迟累积现象处理速度跟不上视频帧率延迟越来越大。原因用cv2.VideoCapture逐帧读取时如果处理时间超过帧间隔缓冲区会累积。解决开一个独立线程专门读帧只保留最新一帧丢弃旧帧。或者用cap.grab()跳帧每处理一帧就 grab 掉几帧。实时场景下宁可丢帧也不要累积延迟。5. 进阶技巧用轨迹平滑和区域过滤提升计数准确率5.1 卡尔曼滤波平滑轨迹中心点DeepSORT 内部已经用了卡尔曼滤波做轨迹预测但输出的to_tlbr()仍然是原始检测框。如果检测框抖动严重可以在外层再加一层简单的移动平均from collections import deque class TrackSmoother: def __init__(self, window5): self.history {} # track_id - deque of centers self.window window def update(self, track_id, center): if track_id not in self.history: self.history[track_id] deque(maxlenself.window) self.history[track_id].append(center) # 返回平滑后的中心点 xs [c[0] for c in self.history[track_id]] ys [c[1] for c in self.history[track_id]] return (sum(xs)/len(xs), sum(ys)/len(ys))window5表示用最近 5 帧的平均值。窗口太大会导致响应滞后太小起不到平滑效果。5 到 7 是比较平衡的值。这个平滑只用于计数判断不影响 DeepSORT 内部的匹配逻辑。5.2 用多边形区域过滤无效检测实际场景中画面边缘可能有玻璃反光、广告牌上的人像等干扰。用多边形区域限定检测范围只统计区域内的目标import cv2 import numpy as np def filter_by_region(detections, polygon): 只保留中心点在多边形内的检测框 valid [] for det in detections: bbox, conf, cls det cx bbox[0] bbox[2] / 2 cy bbox[1] bbox[3] / 2 if cv2.pointPolygonTest(polygon, (cx, cy), False) 0: valid.append(det) return valid # 定义有效区域顺时针顶点 region np.array([[100, 200], [1800, 200], [1800, 1000], [100, 1000]], np.int32)pointPolygonTest返回正值表示点在多边形内。区域顶点按顺时针或逆时针排列都可以但不能自交叉。这个过滤放在检测之后、跟踪之前能显著减少误检带来的虚假轨迹。5.3 用计数结果做异常告警人流量数据本身就有业务价值。比如商场入口每分钟超过 50 人时触发拥堵告警或者夜间检测到人时触发安防告警。实现方式很简单维护一个滑动窗口计数器每 10 秒统计一次import time from collections import deque class FlowMonitor: def __init__(self, window_sec10, threshold50): self.events deque() # (timestamp, direction) self.window window_sec self.threshold threshold def add_event(self, direction): self.events.append((time.time(), direction)) def get_flow_rate(self): now time.time() # 清理窗口外的数据 while self.events and now - self.events[0][0] self.window: self.events.popleft() return len(self.events) / self.window * 60 # 每分钟人数 def check_alert(self): rate self.get_flow_rate() return rate self.threshold, ratewindow_sec10表示用最近 10 秒的数据估算每分钟流量。窗口太短波动大太长响应慢。10 到 30 秒是比较实用的范围。告警阈值根据场景调整商场入口和地铁闸机的合理值完全不同。5.4 验证计数准确率的土办法没有标注数据的情况下怎么知道计数准不准我的习惯是录一段 5 分钟的视频人工数一遍进出人数然后跑系统对比。误差在 5% 以内算合格超过 10% 就要查 ID 跳变和漏检。重点看计数线附近有没有反复加减以及遮挡区域有没有 ID 切换。这个土办法比任何指标都直接因为最终要的是业务数字对得上。踩过的坑里最深的永远是参数硬编码。max_age、conf、imgsz这些值在不同场景下最优解完全不同我现在的习惯是把它们全部提到配置文件里换一个摄像头就重新调一轮。希望帮到你。本文还有配套的精品资源点击获取