拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于YOLOv8的进出口人流量统计:检测跟踪越线计数全流程

基于YOLOv8的进出口人流量统计:检测跟踪越线计数全流程

简介:这份资源面向计算机视觉方向的毕业设计、期末大作业与课程设计需求者,提供一套基于YOLOV8的进出口人流量统计识别完整Python项目。项目围绕视频流中人员进出方向的检测与计数展开,代码注释详尽,新手也能读懂,部署后即可运行,可直接作为毕设或课程设计提交。压缩包共72个文件,约2.13MB,其中21个py文件承载模型推理、视频检测与统计逻辑,26个yaml文件用于数据集与模型配置,另有18个pyc编译文件、3张png与1张jpg示意图、2个md说明及1份docx手册,结构清晰便于按模块查阅。目前已有129人学习关注。项目经过严格调试,功能完善、界面美观、操作简单,配套文档对手册与运行流程均有说明,读者可据此快速复现进出口人流统计效果,并在此基础上替换数据集或调整跟踪参数,完成二次开发与答辩展示。

1. 进出口人流量统计:为什么YOLOv8成了当前最稳的落地方案

做进出口人流量统计,最怕的不是模型精度不够,而是场景太碎。商场大门、景区闸机、园区通道、地铁出入口,每个口子的光照、角度、遮挡情况都不一样,传统背景建模或者HOG+SVM那套方案,换个摄像头就得重新调参,维护成本高得离谱。YOLOv8在这类场景里能快速上位,核心原因是它把检测和跟踪的工程链路压缩到了可维护的范围内:单阶段检测器推理快,Python生态成熟,训练和部署的代码量比两阶段方案少一大截。这套「基于YOLOV8的进出口人流量统计识别Python源码+文档说明」本质上解决的就是一件事——给定一段进出口监控视频或RTSP流,自动统计进出方向的人数,输出结构化数据。适合谁用?做智慧园区、连锁门店客流分析、景区人流管控的Python开发者,有基本深度学习环境配置能力,想找一个能跑通、能改、能落地的基线方案。热搜里yolov8训练自己的数据集、yolov8环境配置这些词频繁出现,说明大量人卡在从零搭环境这一步,这篇就按能复现的路径拆开讲。

2. 进出口人流量统计的技术链路:从检测到计数的完整拆解

2.1 为什么选YOLOv8而不是YOLOv5或RT-DETR

YOLOv8相比YOLOv5最大的工程优势在Anchor-Free解耦头。进出口场景里人形目标尺度变化大,近处的人占画面三分之一,远处的人只有几十像素,Anchor-Based方案需要针对数据集重新聚类Anchor尺寸,调参成本高。YOLOv8的Anchor-Free设计直接预测中心点和宽高,省掉了这一步。RT-DETR虽然精度有优势,但推理速度在边缘设备上不如YOLOv8稳定,而且Python部署链路没有Ultralytics这套库成熟。实际选型时我一般看三个指标:mAP@0.5能到0.85以上、单帧推理在目标设备上低于50ms、模型导出ONNX后不出现算子不支持。YOLOv8n在COCO上mAP@0.5约52.9,换到人形检测微调后通常能到0.9以上,这个基线足够支撑计数任务。

2.2 检测+跟踪+越线计数的三段式架构

人流量统计不是单纯的目标检测,完整链路是:逐帧检测人形框 → 多目标跟踪分配ID → 判断ID轨迹是否跨越预设的进出口界线。检测用YOLOv8,跟踪用ByteTrack或BoT-SORT,越线判断用射线法或向量叉积。为什么不用检测框中心点直接计数?因为同一个人在两帧之间可能被漏检,中心点跳变会导致重复计数。跟踪器的作用是把帧间的检测结果关联成轨迹,只有轨迹稳定跨越界线才计数一次。ByteTrack的优势是低分检测框也参与关联,遮挡场景下ID切换少,进出口这种人群密集场景比SORT系列更稳。

2.3 最小可运行环境搭建与依赖安装

环境配置是热搜里出现频率最高的卡点。我一般用conda建独立环境,Python版本锁3.9或3.10,3.11以上有些依赖轮子还没跟上。CUDA版本看显卡,GTX1660Ti用CUDA 11.8就够,RTX40系建议12.1以上。安装命令如下:

conda create -n yolov8_count python=3.10 -y conda activate yolov8_count pip install ultralytics==8.1.0 pip install opencv-python==4.9.0.80 pip install lap==0.4.0 pip install filterpy==1.4.5

ultralytics是YOLOv8的官方库,封装了训练、推理、导出全流程。lap是线性分配库,ByteTrack做匈牙利匹配时依赖它。filterpy用于卡尔曼滤波,跟踪器预测目标下一帧位置时用。版本号不要随意升,ultralytics 8.1.x和8.2.x的API有细微差异,源码里如果写死了某个版本的方法名,升级后会报AttributeError。装完后用yolo checks验证环境,输出里CUDA状态显示可用才算配好。

2.4 用YOLOv8跑通单帧检测的最小代码

先不急着上跟踪,把检测跑通确认模型和环境没问题:

from ultralytics import YOLO import cv2 # 加载预训练模型,首次运行会自动下载yolov8n.pt model = YOLO("yolov8n.pt") # 只检测人这一类,COCO数据集中人的类别ID是0 results = model.predict( source="test_gate.jpg", classes=[0], # 过滤只保留人 conf=0.4, # 置信度阈值,进出口场景建议0.35-0.5 iou=0.5, # NMS的IoU阈值 imgsz=640, # 推理尺寸,和训练尺寸保持一致 device=0 # 0表示第一块GPU,CPU填"cpu" ) 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(f"person at ({x1:.0f},{y1:.0f})-({x2:.0f},{y2:.0f}) conf={conf:.2f}")

classes=[0]是关键参数,不加的话模型会把背包、手提袋也检出来,后续跟踪会引入大量噪声轨迹。conf设0.4是平衡漏检和误检的经验值,进出口逆光场景可以降到0.35,但低于0.3后误检率明显上升。imgsz必须和训练时一致,用640训练的模型推理时改成1280,小目标召回会提升但速度减半,而且框的坐标映射容易出偏差。

2.5 接入ByteTrack实现轨迹ID分配

检测框有了,下一步给每个人分配稳定ID:

from ultralytics import YOLO import cv2 import numpy as np model = YOLO("yolov8n.pt") cap = cv2.VideoCapture("gate_video.mp4") # 用ultralytics内置的跟踪模式,tracker配置选bytetrack.yaml while cap.isOpened(): ret, frame = cap.read() if not ret: break results = model.track( source=frame, classes=[0], conf=0.4, iou=0.5, tracker="bytetrack.yaml", # 内置跟踪配置 persist=True, # 保持帧间跟踪状态 verbose=False ) if results[0].boxes.id is not None: ids = results[0].boxes.id.int().cpu().tolist() boxes = results[0].boxes.xyxy.cpu().numpy() for tid, box in zip(ids, boxes): x1, y1, x2, y2 = box.astype(int) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f"ID:{tid}", (x1, y1-8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow("count", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

persist=True是跟踪模式的核心参数,它让跟踪器在帧之间保持状态,不加的话每帧都重新初始化,ID会乱跳。bytetrack.yaml里有两个关键阈值:track_high_thresh默认0.5,track_low_thresh默认0.1,前者决定哪些检测框进入主关联,后者决定低分框是否用于二次匹配。人群密集时把track_low_thresh调到0.05能减少ID丢失,但计算量会增加约15%。

3. 越线计数逻辑:怎么判断一个人是进还是出

3.1 射线法与向量叉积的选型对比

判断轨迹跨越界线,常见做法有两种。射线法适合任意多边形区域,判断点是否在区域内,但需要记录目标上一帧和当前帧的区域状态,状态翻转时计数。向量叉积法适合直线界线,计算轨迹线段与界线的交点,根据叉积符号判断方向。进出口场景界线通常是一条直线或者窄矩形带,向量叉积法更直接,计算量也小。我一般用叉积法,代码逻辑是:取目标轨迹的前一帧中心点P1和当前帧中心点P2,界线两端点A和B,计算cross(B-A, P1-A)和cross(B-A, P2-A),符号相反说明跨越了界线,再根据cross(B-A, P2-P1)的符号判断进出方向。

3.2 越线计数的完整实现代码

import numpy as np from collections import defaultdict # 界线定义:进出口通道中间画一条线,A和B是线的两个端点 LINE_A = np.array([200, 400]) LINE_B = np.array([800, 400]) # 存储每个轨迹ID的历史中心点 track_history = defaultdict(list) # 计数结果 count_in = 0 count_out = 0 def cross(o, a, b): """向量oa和ob的叉积""" return (a[0]-o[0])*(b[1]-o[1]) - (a[1]-o[1])*(b[0]-o[0]) def check_cross_line(prev_pt, curr_pt): """判断轨迹是否跨越界线,返回1表示进,-1表示出,0表示未跨越""" d1 = cross(LINE_A, LINE_B, prev_pt) d2 = cross(LINE_A, LINE_B, curr_pt) if d1 * d2 >= 0: return 0 # 同侧,未跨越 # 计算轨迹方向与界线法向的关系 d3 = cross(prev_pt, curr_pt, LINE_A) d4 = cross(prev_pt, curr_pt, LINE_B) if d3 * d4 < 0: # 根据叉积符号判断方向,具体正负取决于界线定义方向 return 1 if d1 > d2 else -1 return 0 # 在跟踪循环中调用 for tid, box in zip(ids, boxes): cx = (box[0] + box[2]) / 2 cy = (box[1] + box[3]) / 2 curr_pt = np.array([cx, cy]) if tid in track_history and len(track_history[tid]) > 0: prev_pt = track_history[tid][-1] direction = check_cross_line(prev_pt, curr_pt) if direction == 1: count_in += 1 elif direction == -1: count_out += 1 track_history[tid].append(curr_pt) # 限制历史长度,防止内存泄漏 if len(track_history[tid]) > 30: track_history[tid].pop(0)

track_history用defaultdict存储每个ID的中心点序列,只保留最近30帧,避免长视频内存持续增长。check_cross_line里d1d2>=0表示两点在界线同侧,直接返回0。d3d4<0确保轨迹线段和界线线段真正相交,而不是在延长线上。方向判断的1和-1哪个是进哪个是出,取决于LINE_A和LINE_B的坐标顺序,实际部署时先用已知方向的视频校准一次。

3.3 去重与防抖:同一个人不能被计两次

越线计数最常见的翻车是同一人来回走被重复计数。解决思路有三层:第一层是轨迹长度过滤,ID出现少于5帧的不参与计数,排除误检产生的短轨迹;第二层是越线冷却时间,同一个ID在2秒内不重复计数;第三层是轨迹合并,如果两个ID的轨迹在时空上高度重叠,判定为ID切换,合并计数。冷却时间的实现用一个字典记录每个ID上次计数的时间戳:

import time last_count_time = {} def should_count(tid, cooldown=2.0): now = time.time() if tid in last_count_time: if now - last_count_time[tid] < cooldown: return False last_count_time[tid] = now return True

cooldown设2秒是经验值,人正常步行速度1.2m/s,进出口通道宽度一般2-3米,穿越时间约2秒。设太短防不住抖动,设太长会漏计快速往返的人。

4. 避坑与排查:进出口人流量统计的5个血泪教训

4.1 现象:计数结果比实际人数多出30%以上

原因:YOLOv8在进出口场景把玻璃门反光、广告牌人像、电视屏幕里的人形也检出来了,这些误检产生短轨迹,每条轨迹跨越界线就计一次。解决:提高conf到0.5以上,同时在检测后加一个宽高比过滤,正常人形框的宽高比在0.2到0.6之间,超出范围的框直接丢弃。另外可以在界线附近设一个ROI区域,只有中心点进入ROI的轨迹才参与计数。

4.2 现象:跟踪ID频繁切换,同一个人被分配了多个ID

原因:ByteTrack的track_high_thresh设太高,遮挡时检测框分数掉到阈值以下,跟踪器丢失目标后重新分配新ID。解决:把track_high_thresh从0.5降到0.4,track_low_thresh从0.1降到0.05,让低分框参与二次关联。同时把跟踪器的max_age参数调大,默认30帧,人群密集场景可以调到50帧,给跟踪器更多时间找回目标。

4.3 现象:模型在GPU上推理正常,导出ONNX后精度暴跌

原因:YOLOv8导出ONNX时默认用opset 17,某些显卡的TensorRT版本不兼容,导致算子回退到CPU或者精度损失。解决:导出时指定opset=12,加simplify=True,命令是yolo export model=best.pt format=onnx opset=12 simplify=True。导出后用onnxruntime加载验证,对比PyTorch和ONNX的输出差异,mAP掉超过2个点就要检查预处理是否一致。

4.4 现象:RTSP流读取延迟越来越高,画面卡顿

原因:OpenCV的VideoCapture默认缓冲区会累积帧,如果推理速度跟不上视频帧率,缓冲区越积越多。解决:设置cap.set(cv2.CAP_PROP_BUFFERSIZE, 1),只保留最新一帧。另外把推理和读取放在不同线程,读取线程只负责取帧,推理线程从队列取最新帧处理,丢掉的旧帧不影响计数结果。

4.5 现象:夜间红外模式下模型几乎检不出人

原因:COCO预训练模型没有红外图像,YOLOv8的浅层特征对红外灰度图不敏感。解决:用红外场景的数据微调模型,至少准备500张标注图,训练时冻结backbone前10层,只训练neck和head,学习率设0.001,20个epoch就能看到明显提升。没有标注数据的话,先把输入图像做直方图均衡化,再送进模型,能恢复一部分召回。

5. 从能跑到好用:进出口人流量统计的进阶调优技巧

5.1 用区域掩码替代单条界线

单条界线只能统计进出,没法区分多个通道。实际进出口往往有2-3条并行通道,每条通道单独计数才有意义。做法是给每条通道定义一个多边形ROI,用射线法判断目标中心点落在哪个ROI内,再在ROI内做越线判断。ROI的坐标用标注工具在视频首帧上画,存成JSON配置文件,部署时加载。这样一套模型可以同时统计多个通道,互不干扰。

5.2 模型量化与TensorRT加速

YOLOv8n在RTX3060上FP32推理约8ms,导出TensorRT FP16后降到3ms,INT8量化后能到2ms以下。INT8量化的关键是校准集要覆盖实际场景的光照和角度,我一般从部署现场录10段各1分钟的视频,抽300帧做校准。TensorRT引擎和显卡型号绑定,换设备要重新生成,所以部署包里要保留ONNX和校准集,不能只发engine文件。

精度推理耗时(RTX3060)mAP@0.5变化适用场景
FP328ms基线调试阶段
FP163ms-0.2%生产部署
INT81.8ms-1.5%边缘设备

5.3 计数结果的持久化与可视化

计数结果不能只打印在终端,要落库。我一般用SQLite存每条越线记录,字段包括时间戳、轨迹ID、方向、通道编号、置信度。SQLite零配置,单文件,适合边缘设备。可视化方面,除了画面上的计数叠加,再生成一张按小时聚合的折线图,用matplotlib画,存成PNG供后台展示。折线图的横坐标时间密度问题,用plt.gca().xaxis.set_major_locator(mdates.HourLocator(interval=1))控制,避免标签重叠。

import sqlite3 from datetime import datetime conn = sqlite3.connect("flow_count.db") conn.execute(""" CREATE TABLE IF NOT EXISTS cross_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT NOT NULL, track_id INTEGER, direction TEXT, channel INTEGER, confidence REAL ) """) def log_event(tid, direction, channel, conf): conn.execute( "INSERT INTO cross_events (ts, track_id, direction, channel, confidence) VALUES (?,?,?,?,?)", (datetime.now().isoformat(), tid, direction, channel, conf) ) conn.commit()

这套方案我从去年跑到现在,最大的体会是:模型精度只是及格线,计数逻辑的去重和防抖才是决定能不能上生产的关键。调参时别盯着mAP看,拿一段已知人数的视频跑完整链路,对比计数结果和实际人数的偏差,偏差控制在3%以内才算过关。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表