ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

人流量检测系统实战:YOLO跟踪计数与部署避坑指南

人流量检测系统实战:YOLO跟踪计数与部署避坑指南 简介这是一份基于深度学习的人流量检测系统毕业设计项目包含 Python 源码与项目说明文档面向计算机相关专业正在准备毕业设计的学生也适合需要项目实战练习的学习者可用于课程设计或期末大作业。项目经严格调试后可稳定运行作为导师认可的高分毕业设计从模型搭建、训练到前端检测展示均有完整代码目录结构清晰便于对照说明理解整套人流量检测流程。资源包共 1235 个文件约 61.54MB其中 76 个 Python 文件构成核心检测逻辑配合 382 个 HTML 页面、194 个 JS 脚本、51 个 CSS 样式搭建可视化界面208 张 PNG 图片与 186 张 GIF 动图用于界面元素和过程演示另有 ipynb 笔记、配置文件与模型资源方便复现训练与推理过程。该资源已有 264 人学习使用适合在已有项目上快速上手、二次开发也可用于学习深度学习检测项目的工程组织方式。1. 人流量检测别急着训模型拿到这个标题你第一反应多半是找个人体检测模型数画面里有几个人。但我做下来的感受是人流量检测真正难的不是检测而是数得稳——跨摄像头、跨时段、有遮挡、有透视畸变时同一个人在不同帧里既可能被漏检也可能被重复计数准确率浮动非常大。这个毕业设计项目的核心并不在于用深度学习框出人而在于把检测结果、追踪逻辑和业务口径什么才算通过一次拼成一套能自洽的计数系统。适合正在做毕设或课设、想拿一个完整可演示项目的同学也适合刚入门目标检测、想看看工程约束怎么倒逼模型选型的开发者。下面我把整套方案的选型、数据准备、源码结构和踩坑点按实际开发顺序拆给你。2. 检测 vs 计数两条技术路线与选型判断2.1 为什么单张图检测不够用计数口径才是系统的灵魂人流量检测系统如果只做每帧输出若干框站在答辩或验收视角是站不住的——演示时随便有人交错走过人数就乱了。真正的流量指标是单位时间内有多少人穿过某个断面这要求系统能区分这是刚才那个人走过去了还是新来一个人。常见做法是引入跟踪Track来给检测框分配 ID再配合一条虚拟计数线或区域判断来完成计数。也就是说模型只是上游Tracking 和计数逻辑才是中游和下游三者缺一不可。我一般会把技术选型分成两类一类是检测类方案即 YOLO 系模型检出人框再配 IoU 匹配做跟踪另一类是密度回归类方案即 CSRNet 这类网络直接回归人群密度图再积分求人数。前者对个体定位清晰、可解释性强、单人场景精度高但遮挡严重时漏检率会陡升后者在密集人群场景更有优势但无法给出每个人在哪的框而且训练密度图标注成本极高。2.2 毕设场景选 YOLO 系模型的四个理由如果你的场景是教室门口、走廊、超市出入口这类半密集场景我会优先选 YOLO 系检测模型理由很直接预训练权重成熟公开数据集多换到自己场景只需要微调推理速度快普通 GPU 上轻松跑实时可视化效果好画框标 ID 一眼就能看懂答辩时演示性强生态完善从标注格式转换到部署都有大量现成代码可参考。相比之下CSRNet 这类密度回归模型在 50 人以上的密集场景确实更强但标注密度图是逐点标注人头的工作量比画框还大而且视觉反馈弱演示效果不容易讨喜。选型还要考虑运行环境。大部分毕设项目会在自己的笔记本上演示因此我建议优先考虑 YOLOv5s 或 YOLOv8n 这类小体量版本而不是 YOLOv8x 这种重模型。同样的训练设备和推理设备n/s 级别模型能保证在 CPU 上也有可接受的推理帧率这对演示现场没有 GPU 的情况非常关键。如果你的机器配置好再考虑大模型。2.3 完整系统的最小功能骨架一个能交付的人流量检测系统至少要包含五个模块目标检测模块负责从画面中检出人跟踪模块负责分配并维护 ID计数逻辑模块负责定义穿越规则并累计结果可视化模块负责实时画框、画计数线和显示人数曲线数据记录模块负责把计数结果落到 CSV 或数据库里供后续分析或答辩展示。标题里源码项目说明正常情况下就应包含这些模块对应的代码文件和说明文档。项目结构我一般会这样组织people_flow/ ├── checkpoints/ # 模型权重存放目录 ├── configs/ # 配置文件路径、阈值、计数线位置等 ├── data/ │ ├── dataset/ # 标注数据与图片 │ ├── processed/ # 转换后的 YOLO 格式标注 │ └── videos/ # 测试视频 ├── scripts/ │ ├── convert_labels.py # 标注格式转换脚本 │ └── train.py # 训练入口 ├── src/ │ ├── detector.py # 检测封装 │ ├── tracker.py # 跟踪封装 │ ├── counter.py # 计数逻辑 │ ├── visualizer.py # 可视化 │ └── main.py # 主入口 └── requirements.txt为什么要拆这么细因为毕设答辩会问你每个文件是干什么的如果全部逻辑堆在一个 main.py 里提问时很难讲清楚模块划分。按功能拆出独立模块既能体现工程能力也方便单独测试某一个环节比如单独跑 detector 看检测效果单独跑 counter 看计数逻辑。3. 训练数据准备从 VOC 到 YOLO 格式的转换脚本与四个边界坑3.1 优先用公开数据集做迁移学习自定义数据只做微调流量检测的数据集准备有一个原则不要从零标注几千张图。公开数据集里COCO 的 person 类别、CrowdHuman、MOT Challenge 系列都可以直接用来做预训练或微调。其中 CrowdHuman 对人群遮挡场景标注比较充分MOT16/17 带跟踪标注适合顺带做跟踪评估。毕设项目一般不需要自己从零采集大量数据常见做法是先用公开数据训练一个可用的检测器再采集一小部分目标场景数据比如自己教室门口、实验室走廊的视频做微调让模型适应当前摄像头的视角和光照。这样能明显减少标注工作量训练数据量在几百张级别就够了。需要提醒的是即便用了迁移学习标注格式依然是最容易被绊倒的环节。假如你找到的公开数据集是 COCO 格式JSON而训练代码用的是 YOLO 格式TXT中间必须写转换脚本。很多学生在网上找的代码只支持一种格式数据喂进去报错半天才发现是格式问题这个我深有体会。因此第一步先确认你的训练代码吃的是什么格式再写转换脚本而不是先转完再回头看代码。3.2 标注格式转换脚本XML/TXT 与 JSON 的互相转换这里给出一个从 COCO JSON 转 YOLO TXT 的脚本。它是整套数据流程中最常被复用的工具后续你换数据集、换场景大概率还要改它来适配。import json import os # COCO 数据集的 person 类别 id 通常是 1需按实际数据集确认 PERSON_CATEGORY_ID 1 def convert_coco_json_to_yolo_txt(coco_json_path, output_dir): with open(coco_json_path, r, encodingutf-8) as f: coco json.load(f) # 建立 image_id - 图片文件名的映射 images {img[id]: img for img in coco[images]} os.makedirs(output_dir, exist_okTrue) for ann in coco[annotations]: if ann[category_id] ! PERSON_CATEGORY_ID: continue image_info images[ann[image_id]] img_w, img_h image_info[width], image_info[height] # COCO 的 bbox 是 [x, y, width, height]YOLO 需要的是归一化中心点和宽高 x, y, w, h ann[bbox] cx (x w / 2) / img_w cy (y h / 2) / img_h w_norm w / img_w h_norm h / img_h # 一个图片对应一个 txt 文件多个标注就追加多行 txt_path os.path.join( output_dir, os.path.splitext(image_info[file_name])[0] .txt ) with open(txt_path, a, encodingutf-8) as out: out.write(f0 {cx:.6f} {cy:.6f} {w_norm:.6f} {h_norm:.6f}\n) print(f转换完成标注文件输出到: {output_dir})这段脚本的逻辑不复杂对每一条 category 为 person 的标注取出 bbox将左上角坐标格式转成归一化的中心点加宽高格式写入同名 TXT。注意它是按行追加写入的所以同一个图片有多个人时会产生多行标注。如果你拿到的数据本身就是 VOC 的 XML 格式也是同样的逻辑只是解析标签从 JSON 字段换成 XML 的 object/bndbox 节点。3.3 四个边界坑坐标裁剪、空标注、类别编号、数据集划分第一个坑是 bbox 出界。摄像头画面边缘的人往往只露出一半身体标注框的坐标可能落在图像边界之外。YOLO 训练时若不对坐标做 clip裁剪会算出负的宽高或超出 1 的归一化值轻则警告重则训练 loss 变成 NaN。因此转换时务必加一步x max(0, x)、x w min(img_w, x w)之类的边界处理。第二个坑是空标注文件。如果某张图只标了 person 类而实际画面有大量人没被标注转换后会生成空 TXT。把这些空文件直接丢给训练脚本有些框架会报no labels错。我的建议是转换脚本里统计每个 TXT 的行数自动列出空文件让你人工确认是漏标还是确实无人。第三个坑是类别编号一致性。YOLO 训练配置里 class 编号从 0 开始很多人从网上下载的标注里 person 编号是 0但换一个数据集可能变成 1 或 15训练时如果不改配置文件模型会把人学成背景精度一塌糊涂。第四个坑是数据集划分。训练、验证、测试三个集合之间不能有图片重叠而且同一个人在不同帧里的画面不要被分到训练和验证两个集合里否则验证指标虚高。这一点在按帧抽帧做人流数据时特别容易犯——视频连续帧高度相似随机划分会导致验证集泄露。提示转换脚本里建议把类别编号做成可配置参数不要写死。你在换数据集时只需改一个变量的值而不是去代码里到处找数字。3.4 数据增强怎么做才不偏离流量场景数据增强方面人流场景我最常用的组合是HSV 色域扰动、随机平移与缩放、马赛克增强。HSV 扰动可以提高不同光照下的泛化能力因为你训练的可能是白天采集的视频而实际部署可能要面对傍晚光线马赛克增强则把四张图拼成一张提升了模型在人群遮挡场景下的鲁棒性。需要谨慎的是大角度旋转和上下翻转。上下翻转在人流场景是禁忌因为监控摄像头一般是固定的不可能出现倒立的人大角度旋转也会产生大量现实中不存在的姿态。合理的增强策略应该是模拟摄像头可能遇到的真实变化而不是无脑堆叠所有增强手段。4. 源码拆解与二次开发模型定义、推理主循环和评估指标4.1 模型结构选择与参数设定从 YOLOv5s 到 YOLOv8n 的取舍到了源码层面首先面对的是模型怎么定义。如果你拿到的项目说明基于 YOLOv5那它大概率用 yaml 文件描述网络结构如果是 YOLOv8结构定义则内聚在 Python 类中。两种写法本质一样都包含 Backbone、Neck、Head 三部分。毕设项目里我建议优先读 YOLOv8n 的源码因为它的结构更清晰、接口更简洁而且文档和示例更丰富。# yolo_v8_n.yaml 的核心结构说明用实际文件由 ultralytics 包自动加载 backbone: - [-1, 1, Conv, [64, 3, 2]] # 下采样输出 1/2 分辨率特征图 - [-1, 1, Conv, [128, 3, 2]] # 下采样输出 1/4 分辨率特征图 - [-1, 3, C2f, [128, True]] # C2f 模块特征提取 - [-1, 1, Conv, [256, 3, 2]] # 下采样输出 1/8 分辨率特征图 - [-1, 6, C2f, [256, True]] # ... 后续为 1/16、1/32 分辨率层级 head: - [-1, 1, Detect, [nc]] # nc 是类别数这里固定为 1person这段描述里每一行的[-1, 1, Conv, [64, 3, 2]]含义是从上一层输入-1 表示上一层、1 个该模块、Conv 卷积模块、输出通道 64、卷积核 3x3、步长 2。C2f 是 YOLOv8 的核心残差模块兼顾速度和精度。修改结构时你只需要关注两个参数depth_multiple 和 width_multiple分别控制网络深度和宽度调小它们就能得到更轻量的模型调大则更准但更慢。4.2 推理主循环帧读取、检测、跟踪、计数如何串起来在线推理主循环是系统运行的核心。下面给出一个简化但完整的推理循环骨架展示了检测与跟踪是如何衔接的import cv2 import torch from ultralytics import YOLO model YOLO(checkpoints/yolov8n_person.pt) tracker cv2.legacy.TrackerKCF_create() # 用 KCF 做简单兜底跟踪 count_line_y 240 # 计数线位置需按实际画面标定 up_count 0 down_count 0 tracked_positions {} cap cv2.VideoCapture(data/videos/test.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break # 检测conf 阈值控制误检与漏检的平衡 results model(frame, conf0.4, iou0.5, classes[0]) boxes results[0].boxes.xyxy.cpu().numpy() for box in boxes: x1, y1, x2, y2 box[:4].astype(int) cx, cy (x1 x2) // 2, (y1 y2) // 2 # 框底部中心点更稳定 # 判断是否穿越计数线 if cy count_line_y: down_count 1 else: up_count 1 # 可视化 frame cv2.line(frame, (0, count_line_y), (frame.shape[1], count_line_y), (0, 255, 0), 2) cv2.imshow(people_flow, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码的逻辑是每读一帧送进 YOLO 检测人框取框的底边中心点作为人的落脚点当落脚点跨过计数线时上/下行计数加一。为什么用底边中心而不是框中心因为框中心会随人体姿态变化而上下浮动在弯腰、坐下等动作下容易误判跨越方向而底边中心贴近地面几何上更稳定。注意上面这个循环为了展示骨架省略了跟踪模块。实际项目中直接用逐帧检测做计数会产生严重重复计数——同一个停留的人会被反复累加。至少需要加一层简单的目标 ID 去重判断当前检测框是否已关联到某个已有 ID如果关联到则只更新位置、不重复计数。4.3 项目说明通常包含哪些内容评估指标与可视化输出项目说明在毕设项目里通常不只是代码注释它要包含数据集介绍、模型结构说明、训练参数表、评估指标和运行步骤。评估指标方面人流检测系统建议至少报告三个检测精度的 mAP0.5计数精度的 MAE平均绝对误差和 RMSE均方根误差。MAE 的计算很简单逐帧把预测人数与人工标注人数做差的绝对值再取平均。在答辩时给出 mAP 只能证明你检测得准给出 MAE 才能证明你数得准后者才是人流量检测系统区别于普通目标检测项目的核心。def compute_mae(pred_counts, gt_counts): assert len(pred_counts) len(gt_counts) errors [abs(p - g) for p, g in zip(pred_counts, gt_counts)] return sum(errors) / len(errors) pred [3, 4, 5, 6] # 模型每帧计数 gt [3, 5, 4, 6] # 人工标注 print(compute_mae(pred, gt)) # 输出 0.75这个函数看起来简单但在毕设项目中几乎是必不可少的。因为人流量检测的项目说明文档里需要你给出本系统计数精度达到 MAE0.75这样的结论性数据而不是只写模型效果不错。我建议你在项目说明中附上逐帧的预测值和真实值对比表格再画出变化曲线图这种量化的呈现方式在答辩时非常加分。5. 部署避坑白天黑夜漂移卡顿和这些常见问题5.1 模型在白天效果好、傍晚效果差的玄学多时段数据微调我做流量检测遇到的第一个大坑就是白天能用傍晚翻车。原因不复杂训练数据基本来自白天光线充足的场景模型学到的是亮环境下的行人外观。到了傍晚暗光加路灯的混合光源下人脸和衣服的颜色细节都变了检测置信度直线下降漏检率飙升。解决思路不是加多少数据增强就能搞定的最有效的做法是采集目标场景下多个时段早、中、晚各 10~20 分钟视频抽帧后做半自动标注再加入训练集微调。注意微调时不要只用傍晚数据要混合原有数据否则模型会对傍晚过拟合、白天精度反而下降。5.2 视频里人一多就掉帧推理速度优化三件套人流检测最容易在演示时暴露的问题是卡顿——人一多帧率降到 15 以下画面肉眼可见地不流畅。性能瓶颈通常在检测模型而不在跟踪和计数逻辑。优化手段按性价比排序第一换小模型YOLOv8x 换成 YOLOv8n精度可能只降 2~3 个点速度翻倍第二输入分辨率从 1280 降到 640这是最直接的加速方式第三打开 TensorRT 或 ONNX 导出做推理加速用 GPU 的话能再快不少。在这三招之后才考虑改跟踪策略、跳帧检测这类工程技巧因为跳帧检测会牺牲计数实时性很容易被答辩老师看穿。# 用半精度推理加速GPU 可用时能明显提升帧率 model YOLO(checkpoints/yolov8n_person.pt) results model(frame, conf0.4, imgsz640, halfTrue)这段代码的关键参数有两个imgsz640直接降低输入分辨率halfTrue启用 FP16 半精度推理。如果目标机器是 GPU这两个参数叠加通常能把 1080p 视频的推理帧率拉到可用水平。CPU 机器上则不建议开 half部分 CPU 对 FP16 支持不好反而更慢。5.3 重复计数没有跟踪模块时的经典翻车现场重复计数是人流量检测里最经典也最丢分的 bug。现象是一个人站在镜头前不动计数数字却不断上涨。原因前面提过——逐帧检测没有去重。解决思路是引入跟踪给每个检测框分配稳定的 ID。实现方案很多OpenCV 自带 sort 或 BoT-SORT 都是可以选的前者轻量、适合毕设后者精度更稳、但依赖更多。# 一个极简的基于位置去重方案当前框的中心点 # 若与上一帧某个已跟踪目标距离小于阈值则视为同一人 import math tracked_targets {} # id - (cx, cy) NEXT_ID 0 DIST_THRESH 50 # 单位像素需按实际画面尺寸调整 for box in boxes: x1, y1, x2, y2 box[:4].astype(int) cx, cy (x1 x2) // 2, (y1 y2) // 2 matched_id None for tid, (tx, ty) in tracked_targets.items(): if math.hypot(cx - tx, cy - ty) DIST_THRESH: matched_id tid break if matched_id is None: tracked_targets[NEXT_ID] (cx, cy) NEXT_ID 1 else: tracked_targets[matched_id] (cx, cy)这个方案只能算最简单的距离匹配真实项目中它扛不住目标交错和短暂遮挡。但作为毕设项目的起步它能立刻解决重复计数问题而且代码量极小、容易讲清楚。DIST_THRESH 这个参数需要按画面里人的移动速度调如果人走得快、两帧之间位移大阈值小了就会把同一人判成不同人如果画面高糊、噪声多阈值大了又容易把两个人合并成一个 ID。5.4 计数线位置和方向设定几何标定的细节计数线的位置看起来是随手画一条线实际调参时很讲究。如果线画得太靠近画面上边缘人在远处尺寸很小、检测框不稳定容易来回横跳导致重复计数如果画得太靠下人已经走到镜头正下方框的底边中点受遮挡影响大。常见做法是选择画面中行人走动的必经断面且在检测框高度约为整幅画面高度 1/4 的位置。线方向的选择也影响左右计数映射要明确好画面上方向下走对应的是进入还是离开一旦映射反了统计出的进出人数就是反的。这个问题在评估时很容易被忽略等看数据才发现进的人数比出的还少。5.5 模型权重文件的坑训练了一个类别的模型部署时类别过滤最后一条常见的坑是类别编号在部署时不匹配。训练时候如果你改成了nc1只有 person 一类那么模型的输出类别编号就是 0推理时直接全部框都可以接受。但如果你的权重是从 YOLOv8 官方 COCO 预训练权重继续微调的它的类别编号 0 仍然是 person那就没问题。最怕的是从别人项目里拿的权重文件类别列表可能被改过部署时没有检查 classes 参数结果模型把猫、狗、车都当成目标框出来计数直接崩。解决方案只有一个拿到任何权重文件第一件事是用一张包含行人和非行人的测试图跑一次推理查看输出的类别 ID 和置信度确认类别含义再投入使用。6. 让准确率再进一步透视校正、区域计数与卡尔曼滤波的配合6.1 为什么要做透视校正等大的人在画面远处更小流量检测系统到了能用阶段你还是会发现一个视觉上很明显、数据上也很明显的问题同一段真实人流在画面远处检测到的框小、漏检多近处的框大、误检也多。这是因为摄像头是透视成像远处的人所占像素面积本来就小小目标检测能力就成了瓶颈。一个务实的做法是引入透视校正在画面中选几个参考点标出实际距离的比例关系将远处目标按比例放大后再送进检测器。这样可以让整幅画面中人的尺寸大致与真实物理尺寸成比例提升远处小目标的召回率。import numpy as np # 在画面中选择 4 个点对应于地面一个矩形的 4 个角 # src 是画面中的像素坐标dst 是物理平面上的坐标映射 src np.array([[200, 300], [600, 300], [900, 450], [100, 450]], dtypenp.float32) dst np.array([[0, 0], [400, 0], [400, 300], [0, 300]], dtypenp.float32) matrix cv2.getPerspectiveTransform(src, dst) warped cv2.warpPerspective(frame, matrix, (400, 300))透视校正的本质是把斜视角的监控画面摊平成俯视视角。应用这个变换后再对 warped 图像做检测远处小目标的像素尺寸会被放大检测置信度会明显提升。这个方案有两个限制一是只能校正地面平面人站立时的高度信息无法恢复二是变换后画面四周会产生黑边或拉伸失真需要裁剪。因此实际部署中我一般只对计数线附近区域做透视校正而不是整幅画面。6.2 区域计数进入/离开判定比线计数更稳用一条虚拟计数线做穿越判定虽然直观但在多人并肩行走、来回走动频繁的场景里误差很大。更稳的做法是用区域计数划定两个相邻的区域比如 A 区和 B 区当某个目标的轨迹从 A 区中心进入 B 区中心时才判定为一次穿越。区域计数天然能过滤掉只在计数线附近来回试探的情况因为你需要的是完整的跨区轨迹而不是某一帧的跨越。实现上区域计数的核心是一个状态机对每个跟踪 ID记录它上一次所在的区域当检测到区域从 A 变为 B就认为发生了一次 A→B 的穿越。这样对同一个人往返走也能正确记录为两次穿越。相比单线计数区域计数的优点是对检测框抖动不那么敏感缺点是逻辑稍复杂、需要维护每个 ID 的区域状态。6.3 卡尔曼滤波让轨迹平滑计数更准确最后一个提升准确率的常用技巧是卡尔曼滤波。它解决的是检测框抖动问题YOLO 在连续帧中对同一个人的框会有几个像素的无规律抖动这种抖动叠加到计数线上就会造成重复跨越。卡尔曼滤波的基本思想是用上一帧的预测位置和当前帧的观测位置做一个加权融合让轨迹更平滑。OpenCV 中可以用cv2.KalmanFilter实现。kf cv2.KalmanFilter(4, 2) # 状态 [x, y, vx, vy]观测 [x, y] kf.measurementMatrix np.array([[1, 0, 0, 0], [0, 1, 0, 0]], dtypenp.float32) kf.transitionMatrix np.array([[1, 0, 1, 0], [0, 1, 0, 1], [0, 0, 1, 0], [0, 0, 0, 1]], dtypenp.float32) def filter_update(cx, cy): prediction kf.predict() measured np.array([[np.float32(cx)], [np.float32(cy)]]) filtered kf.correct(measured) return filtered[0, 0], filtered[1, 0]卡尔曼滤波在这里不是必须的如果用了区域计数通常不需要它。但如果你做的是单线计数且画面有轻微抖动加上它会让计数曲线平滑很多。一个多年调参的教训是卡尔曼滤波的噪声参数measurementNoiseCov、processNoiseCov对结果影响极大噪声协方差设得太大滤波结果基本等于观测值失去滤波意义设得太小轨迹会变得非常钝、跟不上快速移动的人。我通常从measurementNoiseCov1e-1、processNoiseCov1e-3开始调再根据画面中人的移动速度做微调。有时候现场演示时画面上有树叶摇晃、灯光闪烁这些干扰滤波器反而可以帮你把误检带来的跳变吸收掉。这个技巧在你做项目效果对比时很有用——同一段视频加不加滤波计数曲线会明显差一个级别拿来作为优化前后对比展示在项目说明里说服力很强。希望这些踩过的坑和调过的参数能帮你在毕设或项目里少走一段弯路。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表