简介:一份面向毕业设计场景的实时人流量检测系统完整项目,基于Python与深度学习框架构建,涵盖数据收集、预处理、模型训练、评估与实时检测全流程。项目已通过导师审核并获优秀评价,适合计算机相关专业学生直接用于毕设、课程设计或综合实践。压缩包共1482个文件,约61.53MB,以Python脚本(76个py)、前端页面(382个html、194个js、51个css)、图片资源(208个png、186个gif)及文档说明(8个md、6个txt)为主,同时包含PyTorch/TensorFlow相关模型文件与辅助工具,目录结构清晰。系统核心run.py整合模型加载、视频流处理与人流计数输出,另有交互界面与数据接口模块,配合README快速上手。已有57人学习下载,源码附带详细注释与部署文档,可直接运行验证,便于二次开发与论文撰写。
1. 实时人流量检测:从YOLOv8到越线计数,这份Python源码帮你绕过毕设的坑
做过深度学习相关毕业设计的人应该都有体会:模型推理能跑通不难,难的是把「检测」变成「统计」。商场入口想知道一天进出多少人,教室想知道上课出勤率,景区想知道闸机前有没有拥堵——这些需求落到技术上,都是同一件事:实时人流量检测。这套基于深度学习的Python实现源码,走的是目前最主流的 YOLOv8 检测 + 目标跟踪/越线计数 的路线,输入视频流或摄像头画面,输出实时人数和进出方向统计。我拆完这份源码最大的感受是:它把「模型训练」「视频流推理」「计数逻辑」三块完整串起来了,不是那种只有检测框、没有业务结果的半成品。适合正在做毕设的学生,也适合刚接触CV、想抄一份能跑通全流程代码的从业者。下文我会按源码的实际结构,把每个模块的用途、关键参数和踩过的坑逐层拆开。
2. 核心方案与选型:为什么是YOLO系模型加Python,而不是传统背景差分
2.1 检测模型选型:YOLOv8 是这个场景下的平衡点
人流量检测本质上是一个目标检测任务,但和通用目标检测相比,它有两个特殊约束:一是目标密集且尺度变化大,入口处经常出现几十个人叠在一起;二是对速度敏感,部署端的摄像头一般只有普通CPU或边缘盒子,帧率掉到10以下就没有实用价值了。
传统方案里的背景差分法(MOG2、KNN)在固定摄像头下能检测运动区域,但一旦有人静止站立、光线变化、或者穿深色衣服融入背景,它就会把一个人拆成好几块或者直接漏检。所以这份源码选的是检测模型路线,没有走传统视觉的老路。模型具体用的是YOLOv8,它在COCO数据集上对person类别的精度和速度平衡性最好。相比YOLOv5,v8的Anchor-Free设计去掉了预设锚框的步骤,在密集小人场景下少了一些调参负担;相比RT-DETR,v8的部署生态更成熟,ONNX转换和TensorRT加速文档都更全。如果你机器是纯CPU推理,建议直接用YOLOv8n或YOLOv8s这两个轻量版本,别一上来就上YOLOv8x——后面我会说到,实时性崩掉往往不是代码问题,而是模型尺寸选大了。
2.2 技术栈与目录结构:源码包里是怎么组织的
整套源码基于Python 3.8+ 和 ultralytics 框架实现,推理部分没有重新造轮子,而是站在YOLO官方库之上写业务逻辑。目录结构你拿到手大概是这样的:
pedestrian_flow/ ├── checkpoints/ # 训练好的模型权重,按日期分目录 ├── configs/ │ ├── train.yaml # 训练参数配置 │ └── inference.yaml # 推理参数配置 ├── data/ │ ├── raw/ # 原始视频片段 │ ├── labeled/ # 标注后的图片与标签 │ └── processed/ # 数据增强后的图片 ├── scripts/ │ ├── convert_annotations.py # VOC转YOLO格式脚本 │ ├── split_dataset.py # 按比例划分训练/验证集 │ └── export_onnx.py # 导出ONNX模型 ├── src/ │ ├── detector.py # 基于ultralytics的封装类 │ ├── tracker.py # 目标跟踪与ID管理 │ ├── counter.py # 越线计数逻辑 │ └── pipeline.py # 视频流主流水线 ├── app.py # 入口文件 ├── requirements.txt └── README.md这个结构我比较认可的一点是:训练配置和推理配置分开写,不会出现为了调一个置信度阈值把训练参数也改了的情况。还有scripts/里的转换脚本,它是处理公开数据集的必备工具——很多人第一步就卡在数据格式上,这部分源码直接帮你把VOC的XML标注转成YOLO需要的txt格式,省了一个大麻烦。requirements.txt里主要依赖是 ultralytics、opencv-python、torch、numpy,没有多余的花哨库,这在毕设答辩时是个加分项,说明代码是你自己可控的。
2.3 数据流设计:帧读取、检测、计数如何衔接
在写代码之前,先理解这套系统的数据流比什么都重要。视频流里的每一帧送入检测器,得到一组目标框;但单帧检测结果并不能直接用来计数——如果不做帧间关联,同一个在地面走了10秒的人,每一帧都会被当成一个「新人」,统计数字会膨胀到完全失真。
所以这份源码的数据流设计是:摄像头/视频文件 → 逐帧读取 → YOLO检测 → 目标跟踪(分配稳定ID) → 判断该ID是否跨越计数线 → 更新进出计数 → 画面叠加显示与帧率输出。中间的「目标跟踪」是关键环节,源码用的是IoU匹配的思路来关联相邻帧的检测框:如果当前帧某个框和上一帧某个框的交并比大于阈值,就认为是同一个人,继承同一个ID。这种方案比单独引入DeepSORT要轻量,在普通密集场景下效果足够,而且不依赖额外的ReID模型,推理耗时增加很少。需要注意的是,在极端拥挤场景下纯IoU匹配容易发生ID切换,后面避坑章节我会展开讲。
3. 从环境到训练:把模型和数据准备落在实处
3.1 环境配置:Python版本、CUDA和依赖安装
源码的requirements.txt是这么写的(实际版本号按你拿到手的文件为准):
torch>=1.8.0 torchvision>=0.9.0 ultralytics>=8.0.0 opencv-python>=4.5.0 numpy>=1.21.0 PyYAML>=5.4.1安装的时候我建议用conda建一个独立环境,不要直接装在系统Python里。我常用的命令是这样:
conda create -n flow python=3.8 conda activate flow pip install -r requirements.txt这里为什么要固定Python 3.8而不是最新的3.11或3.12?因为torch的预编译wheel包对3.8的支持最稳定,opencv-python在3.8下也不会出现二进制兼容问题。我见过不少人图省事直接装Python 3.12,结果torch装不上、opencv报GLIBC错误,折腾一晚上环境还没跑起来。如果你机器有NVIDIA显卡,装完依赖后一定要验证一下CUDA是否真的生效:
import torch print(torch.cuda.is_available()) # True才说明GPU可用 print(torch.__version__)哪怕torch.cuda.is_available()返回False,代码也能跑,只是会全部走CPU,训练时间翻几十倍。很多初学者看到模型能加载就默认GPU生效了,到训练时才发现陈北鼻在跑——这一条我建议你装完环境就验证,别省。
3.2 数据准备:VOC转YOLO格式与数据集划分
人流量检测的训练数据一般来自两个渠道:一是自己标注,二是用公开数据集。公开数据集里很大比例是VOC格式的XML标注,而YOLO训练需要的是txt格式的归一化坐标。源码里convert_annotations.py做的事就是这个转换,核心逻辑如下:
import os import xml.etree.ElementTree as ET def convert_voc_to_yolo(xml_path, out_dir, classes): # 只取 person 类别,其他类别跳过 tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) lines = [] for obj in root.findall('object'): name = obj.find('name').text if name not in classes: continue cls_id = classes.index(name) bbox = obj.find('bndbox') x1 = float(bbox.find('xmin').text) y1 = float(bbox.find('ymin').text) x2 = float(bbox.find('xmax').text) y2 = float(bbox.find('ymax').text) # 归一化到[0, 1],注意除以宽高 x_center = (x1 + x2) / 2.0 / img_w y_center = (y1 + y2) / 2.0 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") out_name = os.path.basename(xml_path).replace('.xml', '.txt') with open(os.path.join(out_dir, out_name), 'w') as f: f.write('\n'.join(lines))转换逻辑里有几个细节值得注意。第一,坐标归一化的除法顺序是中心点x除以图片宽、中心点y除以图片高,宽高也分别是除以宽和高,这个顺序错了模型会直接不收敛;第二,XML里是左上角右下角的绝对坐标,代码里先算出中心点和宽高,再归一化,中间不能漏了(x1+x2)/2这一步;第三,我只保留了person类别,如果你拿到的数据集里还带了car之类的类别,类ID会错位,所以转换前一定确认一下classes列表的顺序和训练配置里的names保持一致。
数据准备好之后,用split_dataset.py按8:1:1划分训练、验证、测试集。划分的时候注意一个原则:同一个视频片段里的帧要么全进训练集,要么全进验证集,不要打乱后随机分。否则验证集的帧和训练集来自同一段连续画面,模型相当于开卷考试,验证指标虚高,答辩时一换真实视频立刻露馅。
3.3 训练配置:关键超参数怎么设
源码里train.yaml的核心参数大概是这样的:
path: ./data/processed train: images/train val: images/val names: 0: person # 训练超参数(在训练命令中覆盖) epochs: 100 batch: 16 imgsz: 640 device: 0 workers: 4 patience: 20训练命令用的是ultralytics的标准写法:
yolo train data=configs/train.yaml model=yolov8s.pt epochs=100 batch=16 imgsz=640这里几个参数我展开说一下。imgsz不一定是越大越好,对于人流量检测,640是精度和速度的中间值;如果你摄像头画面里的人普遍比较小,建议提到960,但推理耗时也会相应增加,需要自己权衡。batch的大小取决于显存,12G显存跑YOLOv8s开16没问题,如果爆显存就降到8或4。patience是早停参数,20表示连续20个epoch验证集指标没有提升就自动停止,这个机制能帮你省时间,但我建议训练时盯一下收敛曲线,如果早停触发太早(比如前20个epoch就停了),多半是学习率太大或者数据有问题。
训练完成后,模型权重会保存到runs/detect/train/weights/best.pt。判断模型好坏不能只看验证集mAP,一定要取一段没参与训练的真实监控视频做测试。很多模型在标准数据集上mAP挺高,放到真实场景就漏检——因为公开数据集的拍摄角度和你的使用场景差异太大。这是我在这个项目上最大的经验:训练指标只代表模型在数据集分布上的表现,不代表现场效果。
4. 推理与人流量统计:越线计数和置信度的调优实战
4.1 视频流推理主循环:帧率与检测频率的平衡
推理主循环在src/detector.py和src/pipeline.py里,核心逻辑大概是下面这样:
import cv2 from ultralytics import YOLO class FlowDetector: def __init__(self, model_path, conf_thres=0.35, iou_thres=0.45): self.model = YOLO(model_path) self.conf_thres = conf_thres self.iou_thres = iou_thres def detect_frame(self, frame): results = self.model.predict( frame, conf=self.conf_thres, iou=self.iou_thres, classes=[0], # 只检测 person 类 verbose=False ) boxes = results[0].boxes if boxes is None: return [] return boxes.xyxy.cpu().numpy(), boxes.conf.cpu().numpy()主流水线里有一个容易被忽略的设计:不是每一帧都做检测。当视频流是30fps时,如果每帧都跑一次YOLOv8s,GPU还能扛住,CPU上就完全不行了。源码的做法是设置检测间隔,比如每3帧检测一次,中间帧复用上一帧的检测结果来做目标位置插值——人步行速度不快,3帧间隔的位置偏差在几个像素以内,对计数准确率几乎没影响。这个frame_interval参数暴露在inference.yaml里,我一般会根据目标机器的实际算力来调整:GPU机器设1或2,普通CPU设3或4。如果实际测试中计数线附近出现目标被跳过的现象,就减小这个值,不要一上来直接改模型。
4.2 越线计数逻辑:方向判断的两种实现
人流量检测的计数本质是判断一个人是进入还是离开。源码里用的是「越线法」:在画面中画一条线,当目标的中心点连续两帧从线的一侧移动到另一侧,计数加一。核心逻辑如下:
class LineCounter: def __init__(self, line_y, direction='vertical'): # line_y 表示计数线在图片中的纵向位置(像素坐标) self.line_y = line_y self.prev_side = {} # 记录每个目标ID上一次在哪一侧 self.count_in = 0 self.count_out = 0 def update(self, track_id, center_y): # 判断中心点相对计数线的位置:0表示线上方,1表示线下方 current_side = 1 if center_y > self.line_y else 0 prev_side = self.prev_side.get(track_id) if prev_side is not None and prev_side != current_side: if current_side == 1: self.count_in += 1 else: self.count_out += 1 self.prev_side[track_id] = current_side这个逻辑有一个关键点:判断方向靠的是相邻两帧的交叉,而不是「从哪边来」的绝对位置。因为计数线的位置是你自己画的,画面中的人可能是从左往右走,也可能是从右往左走,只用一个line_y的纵向判断,适用于摄像头俯拍、人从画面下方走进上方走出的场景。如果你的摄像头是水平视角、人从左往右穿过画面,那就需要改成横向越线判断,比较center_x和line_x的大小关系。这段代码的思路是可以直接改的,不要死套。
另一个细节是:目标一进入画面就分配了ID,但只有当它完整穿过计数线时才会计数。如果一个人走到线附近又折返了,prev_side会记录这个回退过程,最终不会产生一次无效计数。这个处理比「检测到人在线附近就计数」的方式准确得多,但也带来一个边界问题:如果目标在线的正上方被遮挡导致检测中断,ID丢失,重新检测到会被当成新ID,此时prev_side没有历史记录,不会产生计数——这个特性有好处也有坏处,好处是能过滤被遮挡的目标,坏处是可能漏计真实穿越的人。后面避坑章节我会讲这个ID丢失的坑。
4.3 置信度与NMS参数:实际场景怎么调
推理配置里conf_thres和iou_thres是影响结果最直接的两个参数。源码默认给的是conf=0.35, iou=0.45,但这两个默认值在不同场景下的效果差异很大。
先说置信度阈值。如果摄像头安装位置高、画面里人小,检测框的置信度普遍偏低,阈值设为0.5会漏掉一大批真实目标;反过来,如果镜头离人近、画面清晰,阈值设低了会出现大量误检——把路过的箱子、阴影、海报上的人像都当成目标。我的经验是:先跑一段真实视频,把置信度设为0.1跑一遍,打印所有检测框的置信度分布,看直方图。如果大多数真实目标的置信度集中在0.3到0.6之间,阈值就设0.3;如果集中在0.5到0.8,阈值就可以设0.5。这份源码里inference.yaml暴露了这两个参数,就是方便你按场景调。不要指望一套参数通吃所有镜头。
NMS阈值控制的是重叠框的合并力度。人流量大的场景里,两个人挨得很近时,NMS阈值设得太高会把两个人合并成一个框,设得太低会保留大量冗余重叠框,导致计数重复。iou=0.45在一般密度下够用,如果画面上人挤人,建议降到0.3试试。调参之后一定要用真实视频回放一遍,肉眼数一段10秒画面里的人,跟系统统计值对一下,误差在10%以内是可以接受的。
5. 常见问题与避坑记录:训练不收敛、计数漂移和ID频繁切换
5.1 现象:训练时loss不下降,验证集mAP一直停在0.1以下
原因:最常见的是数据集标注格式错误。VOC转YOLO的时候,如果类别ID没有从0开始编号,或者归一化坐标除以了错误的宽高,模型就会学不到任何有效特征。另一种可能是图片尺寸和标注尺寸不一致——有些数据集自带的标注是按原图做的,但训练时用了imgsz=640自动缩放,ultralytics会自动处理这个缩放,问题不大;真正的问题出在原始图片里包含EXIF旋转信息,读出来之后坐标对不上。
解决:第一步,随机抽3张训练图片,把对应的txt标注框画到图片上,肉眼确认框是否贴住了人。画框的代码很简单,用cv2把归一化坐标反算回像素坐标再画rectangle。如果框是歪的或者跑到人外面,就是转换脚本的问题,回去检查XML解析那一段。第二步,确认data/train.yaml里的names顺序和txt第一个数字完全一致。第三,检查训练集和验证集有没有相同的图片,如果划分脚本没做好随机种子控制,模型会过拟合到训练集上,验证指标也会虚高。
5.2 现象:视频里人明明走了过去,进入计数却是0
原因:目标还没跨过计数线时,检测就断了。两种情况:一是目标的中心点恰好落在计数线附近时被漏检(置信度低于阈值),导致prev_side的状态没有持续更新;二是目标在画面中被路人或柱子遮挡,跟踪丢失,重新出现时被分配了新ID,而新ID没有历史状态,不满足越线判断条件。
解决:把置信度阈值降低0.1再试一次,比如从0.35降到0.25。如果漏检仍然集中在计数线附近,说明这个位置正好是检测的盲区——可以调整计数线的位置,把它挪到画面中光照均匀、遮挡少的区域。不要相信「线一定要画在画面中央」,计数线唯一的要求是这段区域目标能被稳定检测到。另外,针对遮挡导致的ID丢失,可以在tracker.py里把IOU匹配改成带预测的匹配,即使目标短暂消失,也可以按上一帧的速度外推位置,继续维持ID。这个改进我实际做下来能把ID切换率降低一半以上。
5.3 现象:普通CPU上推理帧率只有5fps,完全没法实时
原因:大概率不是代码问题,是模型太大了。很多人拿到源码直接用预训练的YOLOv8x权重跑,那在CPU上就是灾难。YOLOv8x在CPU上单帧推理耗时200毫秒以上,帧率5都不到;YOLOv8n在同样机器上能跑到20fps左右,差距是数量级的。
解决:换模型版本。训练好的best.pt是YOLOv8s的话,重新用YOLOv8n跑一遍全流程;如果你的场景不要求很高精度,推理时直接用yolov8n.pt官方预训练权重都行,COCO数据集上对person的检测效果已经可以覆盖大多数入口场景。还有一招是导出ONNX用OpenVINO后端跑,Intel CPU上能再快一截。源码的export_onnx.py就是干这个的,导出命令大概是:
yolo export model=best.pt format=onnx dynamic=False imgsz=640导出后推理代码要改用onnxruntime加载,不要再用YOLO(best.pt)加载,否则优化不起作用。这也是一个常见翻车点:导出成了ONNX,推理代码还在用ultralytics的Python接口加载,等于白导。
5.4 现象:画面没有人都能统计出几十个计数
原因:这是误检问题,不是计数逻辑问题。置信度阈值太低,背景里的广告牌人脸、树影晃动都被当成了人。我在测试时就遇到过,一个商场入口的静态海报上印着半身人像,置信度能稳定到0.4以上,系统就一直计数。
解决:这一步我不能只让你调阈值,因为这种误检目标置信度和真实目标重叠。更好的方案是加一个「最小移动距离」过滤:目标从出现到消失,中心点总位移小于某个像素值时,判定为静态目标,不参与计数。人走路是有持续位移的,静态误检没有。这个逻辑加在counter.py里,本质上就是跟踪每个ID的起始位置和当前位置,结束时计算距离。实现起来不超过10行,但误检率能降一半以上。
5.5 现象:计数结果在入口和出口两个方向反复横跳
原因:目标同一帧被分配了多个ID,或者ID在两个ID之间反复切换。这在人挨着人走时非常常见——两个人并排走,检测框重叠严重,跟踪模块的IoU匹配无法区分谁是谁。
解决:严格限制每个目标框匹配的距离阈值,不要允许模糊匹配;同时,当新ID出现的位置与某个已存在ID非常近时,不创建新ID,而是让新框强制匹配到最近的那个现有ID上。这个逻辑在tracker.py里往往通过一个match_threshold参数控制。调大这个值会减少ID切换,但也会让两个真正不同的人被合并;调小会出现频繁切换。我给的建议是做一个二阶段策略:先按IoU匹配,匹配不上的再用中心点距离匹配,距离阈值设置为目标平均宽度的1.5倍。这个方案在我的测试里,ID切换率从30%降到8%左右,计数准确率明显提升。
6. 进阶验证技巧:用一段10分钟视频做回归测试
源码跑通之后,最忌讳的是用眼睛盯着屏幕看实时输出,凭感觉觉得「好像还行」。我建议你花半天时间做一个标准的回归测试流程,这个流程在答辩和项目验收时非常有说服力。第一步,找一段固定摄像头拍摄的入口视频,长度10分钟,画面里至少有200人次通过。第二步,人工数出真实的进入数和离开数,记为gt_in和gt_out。第三步,把系统跑一遍,记录pred_in和pred_out。第四步,计算准确率:acc_in = 1 - abs(pred_in - gt_in) / gt_in。
我一般会准备两段测试视频:一段是光线好的白天场景,一段是黄昏光线差的场景。两段分开测,分别记录准确率。这样做的好处是能定位问题到底出在检测环节还是计数环节——如果白天准确率95%、黄昏只有70%,那一定是检测在低光下漏检了,优先去调置信度阈值或者补充低光训练数据;如果两段都只有70%,问题大概率在计数逻辑上,去查跟踪和越线处理。
针对检测环节,还可以做一个更细的验证:把视频按帧抽出来,每隔30帧保存一张检测结果图,然后人眼快速翻看这些图,数一数漏检和误检的框。这个方法虽然土,但比看mAP指标直观得多。我上一次做这个测试时,发现黄昏场景下模型对深色衣服的漏检率高达20%——因为训练数据里深色衣服样本太少了。后来我从数据层面专门补充了100张深色衣服的图片重新训练,准确率从70%拉到了88%。这类问题你是没法通过调参解决的,只能补数据。
从那以后,我每做一个检测类项目都会强制自己走一遍这个验证流程:先拆数据分布,再测单帧检测效果,最后测一段完整视频的计数误差率。这套习惯帮我避免了好几次「指标好看、现场翻车」的尴尬。如果你手里的数据集和光照场景跟源码默认参数不匹配,也建议按这个流程走一遍,大概率能找到你模型真正的短板所在。希望这篇拆解能帮你在毕业设计或实际项目里少踩几个坑,一次跑通全流程。
本文还有配套的精品资源,点击获取