简介:基于YOLOV8的进出口人流量统计识别方案,整合了Python源码与配套文档,适合毕业设计、期末大作业及课程设计场景,也面向具备一定深度学习基础、希望快速上手目标检测与计数项目的学习者。压缩包共72个文件,以Python脚本和YAML配置为主,辅以模型运行生成的pyc缓存、PyTorch相关模块、图片示例及Markdown说明,便于理解工程结构和二次开发;整体大小约2.13MB,轻量易部署。资源已获得128人次学习/下载,代码经过调试可直接运行,并附带详细文档说明,覆盖从模型调用、视频预测到界面展示的完整流程。内含的代码注释清晰,新手也能看懂;同时提供文档说明(docx)及图像素材,便于论文撰写和效果展示。对于希望快速完成课设、毕设或提升工程实践能力的读者,是一份完整且可直接复用的参考项目。
1. 用YOLOv8做进出口人流统计,难点根本不在检测
很多人看到“进出口人流量统计”这个标题,第一反应是“用YOLOv8框住人不就行了”,实际上框住人只是整条链路里最简单的一环。一套能用的进出计数系统,至少要回答三个问题:这个人是不是上一帧那个人?他现在往哪个方向走?他一旦被计过数,怎么保证不会反复计?检测、跟踪、方向判断、去重,这四个环节任何一个出错,统计结果都会失真。
这套方案的适用场景很明确:门店客流分析、办公楼宇进出管理、展厅展位的人流热区统计。它解决的是“有多少人进来、多少人出去、差值是多少”的业务问题,而不是单纯的目标检测演示。标题里的“Python源码+文档说明”,意味着这是一个可以交付的完整项目形态,而不是随手跑的脚本。接下来按任务拆解、代码结构、环境与训练、落地调优四个层面把这条链路讲透,适合自己动手搭过检测模型、但没完整做过计数项目的工程师。
2. 进出口计数的任务拆解与方案选型,先立理论再写代码
2.1 一条计数记录的四段链路:检测、跟踪、越线、计数
进出口人流统计的本质是“对出现在画面里的人做轨迹分析”,一条完整的计数流程必须经过四个阶段:
- 检测:用YOLOv8在每一帧图像中定位人的位置,输出边界框和置信度分数。这是整条链路的基础,检测漏掉人或者把背景当成人,后面全都会错。
- 跟踪:给每个检测到的人分配一个全局唯一的ID,并在连续帧中维持这个ID。检测只回答“这一帧哪里有个人”,跟踪回答“这个人是刚才那个人”。没有跟踪,就无法判断方向,也无法去重。
- 越线判断:在画面中定义一条虚拟参考线(例如门口中线),当某个人的轨迹从线的一侧跨越到另一侧时,才触发一次通行事件。
- 计数去重:每条通行事件按ID和方向分类累加,同一ID在同一方向上只计一次,防止人在门口来回走动时产生重复计数。
检测阶段关注的指标是mAP和召回率,跟踪阶段关注的核心是ID Switch率,越线阶段关心的是参考线位置,计数阶段关心的是去重逻辑。很多项目死在第二步:检测精度很高,但ID频繁跳变,导致越线次数虚高。
进出计数的技术选型通常有两套路线。第一套是端到端方案,用类似FairMOT或CenterTrack的方法把检测和跟踪在同一个模型里完成,训练复杂、数据要求高。第二套是解耦方案——检测和跟踪独立运行,这也是大多数Python落地项目采用的路线。YOLOv8负责检测,跟踪器用ByteTrack或DeepSORT,方向判断和计数用纯几何方法处理。解耦方案的好处是每个环节可以单独替换和调优,检测模型更新了不用动跟踪代码。
2.2 跟踪器选型:ByteTrack与DeepSORT怎么选
跟踪器的选择直接影响进出计数的准确度。DeepSORT在YOLOv5时代是绝对主流,核心贡献是引入了外观特征(ReID)向量,即便目标短暂遮挡或丢失,也能靠外观相似度把ID续上。但DeepSORT的代价是需要额外训练一个ReID特征提取网络,前后两个模型叠加,推理性能和部署复杂度都在上升。YOLOv8时代,ByteTrack的普及度明显更高,它的思路更直接:把所有检测框都参与关联,高置信度框做一次匹配,低置信度框再利用IoU做二次匹配。低分框往往对应遮挡或模糊的目标,这个二次匹配策略大幅度降低了漏跟踪率。
从进出计数的实际场景看,ByteTrack通常是最优选择。出入口场景的特征是画面纵深变化大、人互相遮挡频繁,但不要求长时间保持同一个ID。ByteTrack不依赖ReID特征,轻量且推理快,在密集人群下的ID Switch率比DeepSORT更可控。下表是两者的核心差异:
| 对比维度 | DeepSORT | ByteTrack |
|---|---|---|
| 依赖模型 | 检测模型 + ReID特征模型 | 仅检测模型 |
| 匹配依据 | 运动特征 + 外观特征 | 检测框IoU + 置信度分数 |
| 低分框处理 | 直接丢弃 | 二次关联充分利用 |
| 推理开销 | 较高,额外特征提取耗时 | 较低 |
| 密集人群表现 | 遮挡时依靠ReID恢复 | 低分框二次匹配,ID更稳定 |
| 实现复杂度 | 需要训练ReID网络 | 无需额外训练 |
2.3 进出方向的判断模型:中心点、轨迹向量与参考线
方向判断不能只看一帧。人在画面中的移动是一个连续过程,最可靠的方式是维护每个人的轨迹队列,记录其最近若干帧的中心点坐标,然后用轨迹变化来决定方向。工程上最常见的做法是“参考线 + 向量投影”:
- 在画面中定义一条线段作为虚拟门线,并确定一个正方向,例如“从左到右为进,从右到左为出”。
- 对每个跟踪ID,保存其当前帧和上一帧的中心点。
- 构造方向向量“当前点减去上一帧点”,再构造“参考线法向量”,用点积判断方向。
这里有一个关键细节:图像坐标系中y轴向下为正。如果参考线是水平布置的,判断上下时需要额外留意y值的增长方向,否则进出会反向统计。若参考线竖直布置,则只需比较相邻帧中心点的x坐标大小——x变小为向左通过,x变大向右通过,逻辑最直观。为了避免人在线上来回抖动造成误判,通常会设定一个越线缓冲区域:只有轨迹从线的一侧移动到了另一侧一定距离(比如边界框宽度的一半),才记录一次事件。
3. 源码整体结构怎么读:检测模型、跟踪器和计数逻辑三类文件
3.1 合理的项目文件组织,文档说明覆盖哪些内容
一个可交付的“Python源码+文档说明”,目录结构通常如下:
pedestrian_counting/ ├── main.py # 主入口,视频流读取与可视化 ├── config.yaml # 模型路径、置信度、参考线坐标等参数 ├── trackers/ │ ├── byte_tracker.py # ByteTrack的封装版本 │ └── track.py # 轨迹管理和方向判断逻辑 ├── utils/ │ ├── detector.py # YOLOv8模型封装 │ ├── count_zone.py # 参考线与越线计算 │ └── visualizer.py # 画框、画线、画计数统计 ├── weights/ │ └── yolov8n.pt # 模型权重 └── docs/ ├── 环境搭建.md # Python环境、依赖安装步骤 ├── 数据准备.md # 视频样本、标注文件格式 └── 参数调优.md # 阈值调节建议源码按“检测器、跟踪器、计数计算、可视化”四个模块分离,彼此之间通过数据结构解耦。detector只负责输出“检测框列表”,tracker只负责“ID和轨迹”,count_zone只负责“方向判断和累加计数”,main.py负责把这些模块串起来。文档说明的重点是环境搭建和参数调优,这两块不写清楚,源码很难被真正用起来。
3.2 检测模型封装:YOLOv8推理接口与结果解析
YOLOv8的推理接口非常简洁,官方ultralytics包提供了开箱即用的API。封装检测器时需要注意一件事:虽然库本身有默认参数,但项目集成时建议把置信度阈值、IoU阈值和检测类别都在配置中显式定义,以免默认参数和业务预期不一致。下面是一份典型的detector封装代码:
from ultralytics import YOLO import numpy as np class YOLODetector: def __init__(self, model_path: str, conf_thres: float = 0.25, iou_thres: float = 0.5): self.model = YOLO(model_path) self.conf_thres = conf_thres self.iou_thres = iou_thres def detect(self, frame: np.ndarray): results = self.model.predict( source=frame, conf=self.conf_thres, iou=self.iou_thres, classes=[0], # COCO数据集里类别0为person verbose=False ) dets = results[0].boxes boxes_xyxy = dets.xyxy.cpu().numpy() if dets is not None else np.empty((0, 4)) scores = dets.conf.cpu().numpy() if dets is not None else np.empty((0,)) return boxes_xyxy, scores代码里classes=[0]限定了只检测person类别,这个参数在进出口场景中值得反复确认:如果模型是COCO权重,类别0就是人;如果用了自定义数据集,类别编号可能不同,写错会导致检测结果全部为空。conf参数控制的是“多像才算人”,阈值设得低召回率高但误报多,设得高误报少但漏检也多。现实项目的常见做法是先把conf压到0.2左右跑一遍视频,观察哪些目标被错误丢弃,再逐步上调。
3.3 跟踪与越线计数:参考线、方向判断和去重逻辑的核心代码
方向判断与计数是整个代码库中最容易出错的部分,也是项目价值的核心。下面给出参考线判断计数核心逻辑的简化实现:
class CountZone: def __init__(self, line_start, line_end, direction="right"): self.line_start = line_start self.line_end = line_end self.direction = direction self.in_count = 0 self.out_count = 0 self.counted_ids = set() # 已计数ID,防止重复累加 self.prev_centers = {} # 维护每个ID的上一帧中心点 def update(self, track_id, center_xy): cx, cy = center_xy prev = self.prev_centers.get(track_id) # 场景1:无历史帧,先记录坐标,不判断方向 if prev is None: self.prev_centers[track_id] = (cx, cy) return # 场景2:判断是否跨越竖直参考线,比较x坐标 px, _ = prev # 方向判定:向右越过参考线记为“进”,向左记为“出” if self.direction == "right": if px < self.line_start[0] and cx >= self.line_start[0]: if track_id not in self.counted_ids: self.in_count += 1 self.counted_ids.add(track_id) elif px > self.line_end[0] and cx <= self.line_end[0]: if track_id not in self.counted_ids: self.out_count += 1 self.counted_ids.add(track_id) self.prev_centers[track_id] = (cx, cy) def clear_old_ids(self, active_ids): for tid in list(self.prev_centers.keys()): if tid not in active_ids: del self.prev_centers[tid]代码关注三个要点:其一是prev_centers用来暂存每个ID的上一帧中心点,必须用active_ids定期清理已丢失的ID,否则内存会随时间持续增长;其二是counted_ids负责去重,但“永远不去重”和“一直去重”都不对——需要设定一个时间窗口,比如超过一定时长未再次出现的ID,可以从counted_ids中移除,允许它重新计数;其三是clear_old_ids的调用频率影响内存占用量,一般每处理50帧清理一次即可。
去重逻辑的细节值得展开。进出口场景中人们常常在门口徘徊,一个人跨了线又退回去,如果不做去重,系统就计了两次。最简单的做法是ID级别的永不去重,但这会漏掉同一个人二进二出。更稳妥的做法是维护“最近N秒内已计数的ID集合”,用存活时间控制窗口:
def _expire_counted(self, expire_seconds=30, fps=25): expire_frames = expire_seconds * fps for tid, frame_num in list(self.counted_time.items()): if frame_num + expire_frames < self.current_frame: self.counted_ids.discard(tid) del self.counted_time[tid]3.4 主流程串联与实时可视化,任务间通过帧号同步
主流程main.py负责读视频、逐帧调用各模块、最后把结果画到画面上。一个容易忽视的点是检测器输出的目标框需要从像素坐标转换为跟踪器需要的中心点坐标,然后才能进行方向判断。可视化通常包括:画出目标框和ID、画出参考线、在画面左上角显示进出累计数。实时视频流的处理速度依赖推理延迟,主流程中不要加入过于复杂的图像绘制操作,否则即使检测很快,画面也会卡顿。
4. 环境搭建、数据集准备与YOLOv8训练的关键参数
4.1 从零搭建YOLOv8运行环境,Python版本和CUDA匹配是最大坑
YOLOv8的安装门槛较低,但环境匹配问题确实影响很多人的进度。一个常见的现象是:代码本身没问题,项目却因为Python版本与PyTorch版本错配而无法正常加载模型。基于YOLOv8的Python项目,推荐的环境组合如下:
# 用conda创建独立环境,避免系统Python被污染 conda create -n yolov8_env python=3.9 conda activate yolov8_env # 安装PyTorch,注意先确认CUDA版本 # CUDA 11.8对应安装命令: pip install torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cu118 # 安装ultralytics及其依赖 pip install ultralytics==8.0.0 # 验证安装是否成功 python -c "from ultralytics import YOLO; model = YOLO('yolov8n.pt'); print('YOLOv8环境正常')"安装过程中最容易碰到的三个坑:一是PyTorch与CUDA版本不匹配,模型直接运行在CPU上,速度慢到不可用;二是numpy版本冲突,ultralytics新版本要求numpy>=1.23,如果项目里其他依赖装了旧版,需要手动升级;三是GTX 1660 Ti这类GPU算力相对有限,显存在6GB以下时不要用yolov8x或yolov8l,yolov8n或yolov8s是更合理的选择。
4.2 训练自己的数据集,命令参数和控制精度的核心指标
如果直接使用COCO预训练权重,只能识别“通用的人”;部署到特定出入口、特定摄像头视角下时,通常需要用自己的数据微调。YOLOv8支持自定义数据集,数据组织格式如下:
dataset/ ├── images/ │ ├── train/ # 训练图片 │ └── val/ # 验证图片 ├── labels/ │ ├── train/ # 对应的标注txt文件 │ └── val/ └── data.yaml # 数据集配置文件yaml文件内容示例:
path: D:/projects/dataset train: images/train val: images/val names: 0: person使用YOLOv8官方命令行训练:
yolo detect train \ data=D:/projects/dataset/data.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ lr0=0.01几个参数的含义需要说清楚。epochs控制训练轮数,数据量小(几百张)时100轮足以,过大容易过拟合;imgsz是训练时的图像分辨率,640是速度与精度的平衡点,如果画面中行人密集且偏小,可以提到960;batch则受显存限制,1660 Ti的6GB显存配合yolov8s建议batch=8到16,显存不够时优先减小batch而不是imgsz,否则模型性能会明显下降。
训练结束后重点看两个指标:一是mAP50,它衡量的是检测框和真实标注框的重叠程度,进出口场景建议达到0.85以上;二是confusion matrix里的误检情况,在仅有人的单类别任务里,主要观察背景类是否被误判为人。这两个指标达标后再导出用于推理的权重,格式用torchscript或onnx都可以。
4.3 推理速度和精度的平衡,部署端的模型选择建议
在进出口计费场景中,推理延迟直接决定后续跟踪和计数是否能跟上人流速度。实时视频流的推理时间一般在20ms到60ms之间,对应的帧率约16到50FPS。模型参数量与推理时间的关系大致如下:
| 模型版本 | 参数规模 | 1660 Ti推理耗时(约) | 6GB显存可用性 | 适用建议 |
|---|---|---|---|---|
| yolov8n | 约300万 | 10-15ms | 充足 | 极速部署,遮挡严重时不推荐 |
| yolov8s | 约1100万 | 20-30ms | 适中 | 性价比最高,推荐首选 |
| yolov8m | 约2500万 | 40-55ms | 紧张 | 精度优先的离线视频分析 |
出入口环境复杂时,nano模型的漏检率往往偏高。常见做法是直接用yolov8s作为起始版本,推理性能不足时再考虑剪枝或者换更小的模型,不要一开始就选择最大模型,否则后续调优空间很小。
5. 画损失函数曲线和调优技巧,让计数结果更可信
5.1 从训练日志里画损失函数曲线,判断训练状态而不是盲调参数
模型训练完成之后有个常见的坑:metrics看着正常但测试视频里计数结果非常差。大部分情况下,问题出在过拟合或者欠拟合还没有暴露之前就停了训练。画损失函数曲线是判断训练状态的直接手段,但很多人只用训练日志里最后一行的数字,看不到曲线趋势。YOLOv8默认会在训练过程中输出train/box_loss、val/box_loss等日志文件,可以用下面这段脚本把它们画出来:
import pandas as pd import matplotlib.pyplot as plt # 训练结果保存在runs/detect/train目录下 df = pd.read_csv("runs/detect/train/results.csv") # 去掉列名首尾空格后选择损失列 df.columns = [c.strip() for c in df.columns] plt.figure(figsize=(12, 5)) plt.subplot(1, 2, 1) plt.plot(df["epoch"], df["train/box_loss"], label="train_box_loss") plt.plot(df["epoch"], df["val/box_loss"], label="val_box_loss") plt.title("Box Loss Curve") plt.legend() plt.subplot(1, 2, 2) plt.plot(df["epoch"], df["metrics/mAP50(B)"], label="mAP50") plt.xlabel("epoch") plt.title("mAP50 Curve") plt.legend() plt.tight_layout() plt.savefig("loss_curve.png", dpi=150) print("损失曲线已保存")这个脚本对部署项目的意义在于判断是否发生过拟合:如果val_loss早早反弹上升、train_loss却持续下探,说明模型在记忆训练集而不是泛化目标,继续训练只会越训越差。此时需要提前停止、增强数据增强(如mosaic、平移旋转)或者减小模型复杂度,而不是盲目增加epochs。如果train_loss和val_loss同步下降且mAP还在爬升,那训练过程基本健康。
实际项目里有个实用经验:人流统计场景的数据往往是现场采集当天拍的视频截帧,场景单一、光线固定,这样的数据训练出来的模型换一个时段就会明显退化。如果项目刚验证完想尽快落地,直接用COCO预训练权重跑推理,数据采集成本最低;如果追求更稳定的效果,至少要采集三个时段(白天、晚上、黄昏)的样本再微调。
5.2 三个容易见效的参数级调优:置信度、去重窗口、参考线位置
第一个参数是检测置信度阈值。很多人认为置信度越低越好,但实际场景中一旦出现误检,误检目标也会生成轨迹、跨线、计数,最终导致进出数虚高。一个可复现的经验是:在人员密集的进出口场景,conf设在0.3到0.4之间,通过跟踪器过滤掉轨迹过短的噪声,效果往往优于阈值压到0.1。
第二个参数是去重窗口。配置文件中通常会有一个类似count_interval=300的参数,含义是同一个ID在300帧之内不会二次计数。这个值需要根据人流速度定:正常步行通过镜头的速度大约为0.8到1.2米/秒,视频帧率25FPS时,一个人从进画面到离开画面大约经历60到100帧,因此去重窗口设在150帧以上比较安全。窗口设太短会重复计数,设太长会漏掉同一个人短时间内二次回归的情况。
第三个参数是参考线的摆放。参考线应尽量垂直于人的主要移动方向,且放在画面中相对“窄”的位置。参考线越贴近画面边缘,人进入时刚露出半个身位就越线,容易漏计;参考线放在画面中央偏后的位置,通行方向几乎完全垂直于镜头光轴时,判断误差最小。放在入口处时建议线离画面底部保留一个人身体宽度的缓冲距离,轨迹抖动造成的边界回穿会有明显减少。
最后的验证环节,用一段自录的、带有明确进出标注的短视频做灰度测试,对比人工计数与系统计数的差异。达到95%以上的统计准确率,这个项目才算真正可交付,而不只是能跑通。整个链路里YOLOv8解决的是“看得到人”的问题,而跟踪、去重和参考线这三段工程逻辑,才是决定最终人流统计数字是否可信的关键。
本文还有配套的精品资源,点击获取