ARTICLE DETAIL

资讯详情

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

基于YOLO与ByteTrack的人流量检测系统:Python毕业设计全流程实战

基于YOLO与ByteTrack的人流量检测系统:Python毕业设计全流程实战 简介这份资源是面向高校学生与深度学习初学者的毕业设计完整项目包主题为基于深度学习的人流量检测系统使用Python开发适合用作课程设计、期末大作业或计算机视觉方向的实践参考。压缩包共1482个文件约61.64MB涵盖76个Python源码文件、382个html页面、208张png图片、194个js脚本及css、json、md等配套资源其中person_detection模块承载行人检测算法实现doc与pfd_web目录提供项目文档和Web界面设计文件run.py作为系统入口负责调用模型与处理数据。项目围绕数据收集、模型训练、算法部署与系统实现展开采用CNN等网络结构完成视频帧中行人的特征学习与计数并关注准确性与实时性。已有75人学习下载读者可借此理解深度学习在智能监控场景中的落地流程积累从模型训练到应用部署的完整经验。1. 从零搭一套人流量检测系统毕业设计选题里最容易被低估的工程活宿舍楼道口装一个摄像头画面里人来人往你想知道今天到底过了多少人——这件事听起来像是调个 API 就能搞定但真动手做毕业设计时你会发现从「画面里有人」到「今天下午三点到四点进楼 47 人」之间隔着检测、跟踪、计数逻辑、去重、可视化整整一条链路。基于深度学习的人流量检测系统设计与实现核心就是用 Python 把这条链路串起来让模型输出的框变成有业务含义的客流数字。它适合计算机、电子信息、物联网方向的本科毕业生也适合刚入门深度学习、想找一个能跑通全流程项目的开发者。选这个题的好处是数据好找、模型成熟、效果肉眼可见答辩时能现场演示难点在于计数逻辑的边界处理这才是拉开分数的地方。2. 人流量检测的技术选型为什么是 YOLO 加跟踪而不是纯分类2.1 检测、跟踪、计数三层架构的分工一套完整的人流量检测系统拆开来看是三个独立又串联的模块。第一层是目标检测负责在每一帧画面里找出所有人的位置输出边界框坐标和置信度第二层是目标跟踪给每个检测框分配一个稳定的 ID让同一个人在不同帧里保持同一个编号第三层是计数逻辑根据跟踪 ID 的运动轨迹判断这个人是否越过了预设的计数线越线才计数没越线不算。为什么不能只用检测因为视频是连续的同一帧里检测到 5 个人下一帧还是这 5 个人如果每帧都计数数字会爆炸。跟踪的作用就是去重把「帧级检测」变成「目标级事件」。为什么不用纯分类模型分类只能告诉你「这张图里有没有人」给不出位置更给不出运动方向没法判断进出。所以检测加跟踪是目前最稳妥的方案也是工业界客流统计的常见做法。选型上检测模型我一般推荐 YOLOv8n 或 YOLOv5s理由是模型小、推理快、Python 生态好本科毕设的算力条件完全够用。跟踪算法用 ByteTrack 或 DeepSORTByteTrack 更轻量不需要额外的外观特征模型适合实时场景。计数逻辑自己写核心就是一个线段相交判断不需要额外依赖。2.2 用 Python 搭出检测加跟踪的最小可跑框架先把环境跑通再谈优化。下面这段代码用 ultralytics 的 YOLOv8 做检测用 ByteTrack 做跟踪读本地视频文件逐帧处理并打印每个人当前的跟踪 ID 和位置。这是整个系统的骨架后面所有功能都在这上面加。# minimal_flow.py # 最小可跑框架YOLOv8 检测 ByteTrack 跟踪 from ultralytics import YOLO import cv2 # 加载预训练的 YOLOv8n 模型首次运行会自动下载权重 model YOLO(yolov8n.pt) # 打开视频文件也可以换成 0 使用摄像头 cap cv2.VideoCapture(test_video.mp4) # 用 ByteTrack 做跟踪persistTrue 表示帧间保持跟踪状态 while cap.isOpened(): ret, frame cap.read() if not ret: break # classes[0] 只检测 person 类避免其他类别干扰 results model.track(frame, persistTrue, classes[0], verboseFalse) # 取出当前帧的跟踪结果 if results[0].boxes.id is not None: boxes results[0].boxes.xyxy.cpu().numpy() # 边界框坐标 ids results[0].boxes.id.cpu().numpy() # 跟踪 ID for box, tid in zip(boxes, ids): x1, y1, x2, y2 box # 计算框中心点后续计数用中心点轨迹 cx, cy (x1 x2) / 2, (y1 y2) / 2 print(fID{int(tid)} center({cx:.0f},{cy:.0f})) cv2.imshow(frame, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码的关键参数有三个。classes[0]限定只检测人COCO 数据集里 person 的类别编号就是 0不加这个参数会把车、包、椅子都框出来。persistTrue让跟踪器在帧与帧之间保持状态否则每帧都会重新分配 ID跟踪就失效了。verboseFalse只是关掉冗余日志不影响功能。运行前确认test_video.mp4存在或者把路径换成0直接用摄像头测试。跑通之后你会看到控制台不断输出 ID 和中心点坐标。这时候先别急着加计数观察一下同一个人的 ID 是否稳定如果 ID 频繁跳变说明跟踪参数需要调或者视频帧率太低导致目标位移过大。这一步是整个系统的地基地基不稳后面全白搭。2.3 计数线怎么画越线判断的逻辑与参数计数线的本质是一条虚拟线段人从线的一侧移动到另一侧时触发计数。实现上我一般用「中心点前后两帧相对于线段的位置变化」来判断。具体做法是记录每个跟踪 ID 上一帧的中心点当前帧中心点与上一帧中心点分别对计数线做叉积如果符号相反说明越线了。# counter.py # 越线计数逻辑基于中心点轨迹与计数线的位置关系 import numpy as np # 计数线由两个端点定义这里画一条水平线 LINE [(0, 400), (1920, 400)] def cross_line(p1, p2, line): 判断 p1 到 p2 的移动是否穿过 line x1, y1 line[0] x2, y2 line[1] def side(px, py): # 叉积判断点在线的哪一侧 return (x2 - x1) * (py - y1) - (y2 - y1) * (px - x1) s1 side(*p1) s2 side(*p2) # 符号相反说明跨越了线段所在直线 return s1 * s2 0 # track_history 保存每个 ID 上一帧的中心点 track_history {} count_in 0 count_out 0 def update_count(tid, center): global count_in, count_out if tid in track_history: prev track_history[tid] if cross_line(prev, center, LINE): # 根据移动方向判断是进还是出 if prev[1] LINE[0][1] center[1]: count_in 1 elif prev[1] LINE[0][1] center[1]: count_out 1 track_history[tid] centercross_line函数用叉积符号判断点在线段的哪一侧这是计算几何里最常用的方法比算斜率再比较更稳定不用担心垂直线段除零的问题。track_history字典以跟踪 ID 为键保存上一帧中心点每个 ID 独立维护轨迹。进出方向的判断依据是中心点 y 坐标相对于计数线 y 坐标的变化方向prev[1] LINE[0][1] center[1]表示从上往下穿线记为进入。这里有个容易翻车的点计数线不要画在画面最边缘。如果线贴着画面顶部或底部人刚进入画面就被计数或者还没完全离开就触发会导致重复计数。我一般把线放在画面高度 40% 到 60% 的位置给跟踪器留出足够的稳定帧数。另外如果画面里有人徘徊在计数线附近来回走动可能被反复计数解决办法是给每个 ID 加一个冷却时间比如 2 秒内不重复触发。3. 把检测结果变成可用数据存储、可视化与 Web 展示3.1 用 SQLite 存每一笔客流记录检测和计数跑通之后数据要落盘不然刷新页面就没了。毕设场景下 SQLite 足够用不需要装 MySQL一个文件搞定。表结构设计两张表一张记录每次越线事件一张按小时聚合用于展示。# db.py # SQLite 存储客流记录 import sqlite3 from datetime import datetime conn sqlite3.connect(flow.db) cur conn.cursor() # 事件表每一条越线记录 cur.execute( CREATE TABLE IF NOT EXISTS flow_event ( id INTEGER PRIMARY KEY AUTOINCREMENT, track_id INTEGER, direction TEXT, -- in 或 out event_time TEXT, -- ISO 格式时间戳 frame_no INTEGER ) ) # 小时聚合表用于前端快速查询 cur.execute( CREATE TABLE IF NOT EXISTS flow_hourly ( hour TEXT PRIMARY KEY, in_count INTEGER DEFAULT 0, out_count INTEGER DEFAULT 0 ) ) conn.commit() def log_event(tid, direction, frame_no): now datetime.now().isoformat() cur.execute( INSERT INTO flow_event (track_id, direction, event_time, frame_no) VALUES (?,?,?,?), (tid, direction, now, frame_no) ) hour now[:13] # 取到小时如 2025-01-15T14 col in_count if direction in else out_count cur.execute(f INSERT INTO flow_hourly (hour, {col}) VALUES (?, 1) ON CONFLICT(hour) DO UPDATE SET {col} {col} 1 , (hour,)) conn.commit()flow_event表保留原始事件方便回溯和导出flow_hourly表做预聚合前端查图表时不用扫全表。ON CONFLICT是 SQLite 的 upsert 语法小时记录不存在就插入存在就累加。注意hour now[:13]这个截取方式依赖 ISO 格式的固定长度如果换成其他时间格式要相应调整。3.2 用 Flask 把计数结果推到网页上毕设答辩需要一个能看的界面Flask 是最轻的选择。两个路由一个返回当前统计数据一个渲染页面。前端用 Chart.js 画折线图不需要前端框架。# app.py # Flask 后端提供客流数据接口 from flask import Flask, jsonify, render_template import sqlite3 app Flask(__name__) def query_db(sql): conn sqlite3.connect(flow.db) conn.row_factory sqlite3.Row rows conn.execute(sql).fetchall() conn.close() return [dict(r) for r in rows] app.route(/api/hourly) def hourly(): # 返回最近 24 小时的分时客流 data query_db( SELECT hour, in_count, out_count FROM flow_hourly ORDER BY hour DESC LIMIT 24 ) return jsonify(data[::-1]) # 反转成时间正序 app.route(/) def index(): return render_template(index.html) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)query_db封装了连接和关闭避免每次写重复代码。row_factory sqlite3.Row让查询结果可以按列名访问转成字典后 jsonify 直接可用。data[::-1]把倒序查询结果反转前端图表从左到右就是时间正序。host0.0.0.0让同一局域网内的手机也能访问答辩时可以用手机演示。前端index.html里用 fetch 定时拉/api/hourly拿到数据后更新 Chart.js 实例。轮询间隔设 5 秒就够太频繁没必要客流统计不需要秒级实时。3.3 视频流处理的帧率控制与跳帧策略实际跑的时候你会发现YOLOv8n 在 CPU 上大概每秒处理 5 到 10 帧如果视频本身是 30 帧每秒处理速度跟不上播放速度画面会卡顿或者延迟越来越大。解决办法有两个一是跳帧每 3 帧取 1 帧做检测中间帧复用上一帧的跟踪结果二是降低输入分辨率把 1080p 缩到 640 宽再送进模型。# 跳帧处理每 3 帧检测一次 frame_idx 0 DETECT_INTERVAL 3 while cap.isOpened(): ret, frame cap.read() if not ret: break if frame_idx % DETECT_INTERVAL 0: results model.track(frame, persistTrue, classes[0], verboseFalse) last_results results # 缓存检测结果 else: results last_results # 非检测帧复用缓存 # 后续计数逻辑照常执行 frame_idx 1DETECT_INTERVAL 3表示每 3 帧做一次检测检测帧的结果缓存到last_results非检测帧直接复用。这样整体处理速度能提升约 3 倍代价是跟踪精度略微下降。如果画面里人移动很快间隔要调小如果人移动慢可以调到 5。这个参数没有标准答案要根据你的视频内容和硬件性能实测。注意跳帧后frame_no要传原始帧号而不是检测帧号否则数据库里的帧号会对不上视频时间轴。4. 人流量检测系统避坑排查那些答辩前一夜才发现的翻车点4.1 跟踪 ID 频繁跳变导致重复计数现象同一个人走过画面控制台打印出三四个不同的 ID计数结果比实际人数多了一倍。原因通常是两个一是检测置信度阈值设得太高某些帧漏检导致跟踪器重新分配 ID二是视频帧率低或跳帧间隔大目标在两帧之间位移超过跟踪器的匹配范围。解决办法是把model.track的conf参数从默认 0.25 降到 0.15 到 0.2减少漏检同时把DETECT_INTERVAL降到 2 或 1让跟踪器有更多帧做匹配。如果还不行换 DeepSORT 并开启外观特征匹配代价是速度慢一些。4.2 计数线位置不当造成边界重复触发现象有人在计数线附近停下来聊天计数数字来回跳动。原因是中心点在线的两侧反复穿越每次都触发计数。解决办法是加冷却时间同一个 ID 在 2 秒内只允许触发一次计数。实现上用一个字典记录每个 ID 上次触发的时间戳触发前检查时间差。import time last_trigger {} def update_count_with_cooldown(tid, center): now time.time() if tid in last_trigger and now - last_trigger[tid] 2.0: return # 冷却期内不重复计数 # ... 越线判断逻辑 ... last_trigger[tid] now4.3 模型只认 COCO 里的人特殊场景漏检严重现象正常行走的人能检测到但蹲着的人、被遮挡一半的人、穿深色衣服在暗光下的人经常漏检。原因是 YOLOv8n 在 COCO 上训练COCO 里的人物姿态以站立行走为主特殊姿态样本少。解决办法有两个方向一是换更大的模型YOLOv8m 或 YOLOv8l 的召回率明显更高代价是速度下降二是用自己的场景数据做微调标注几百张漏检严重的帧用 ultralytics 的 train 接口跑 50 个 epoch效果提升很明显。毕设如果有时间微调这一步是加分项。4.4 视频读取速度与处理速度不匹配导致内存暴涨现象程序跑了几分钟后越来越卡最后内存溢出崩溃。原因是cap.read()按视频原始帧率读取但处理速度跟不上帧数据在缓冲区堆积。解决办法是在循环里加一个帧队列长度检查或者直接用cap.grab()跳过多余帧。# 只处理最新帧丢弃积压 while cap.isOpened(): cap.grab() # 快速取帧但不解码 ret, frame cap.retrieve() # 解码当前帧 if not ret: break # 处理 framegrab()只取帧不解码retrieve()解码当前帧这样缓冲区不会堆积。代价是会丢帧但对于客流统计来说丢几帧不影响总数。4.5 答辩演示时摄像头权限或路径问题翻车现象本地跑得好好的换到答辩教室的电脑上打不开摄像头或读不到视频文件。原因是cv2.VideoCapture(0)依赖系统摄像头驱动不同电脑的摄像头索引可能不是 0视频文件路径用了相对路径换目录就找不到。解决办法是提前准备一个视频文件作为备选路径用os.path.join(os.path.dirname(__file__), test_video.mp4)拼绝对路径。摄像头索引可以先枚举 0 到 3 试一遍找到能打开的那个。答辩前一定要在演示电脑上完整跑一遍别等到现场再调。5. 让计数更准的两个进阶技巧区域过滤与多线统计5.1 用多边形区域排除画面干扰实际场景里画面边缘可能有玻璃反光、树影晃动、其他屏幕里播放的人像这些都会被检测成 person。解决办法是定义一个感兴趣区域只对区域内的检测框做计数。实现上用cv2.pointPolygonTest判断中心点是否在多边形内。import cv2 import numpy as np # 定义感兴趣区域多边形按实际画面调整 ROI np.array([[200, 100], [1700, 100], [1700, 900], [200, 900]]) def in_roi(center): return cv2.pointPolygonTest(ROI, center, False) 0 # 在计数逻辑里加一层过滤 if in_roi((cx, cy)): update_count(tid, (cx, cy))ROI的四个顶点按顺时针或逆时针排列pointPolygonTest返回正值表示点在多边形内。这个过滤能挡掉大部分画面边缘的误检尤其是玻璃反光和屏幕人像。ROI 的坐标要在实际视频上量不同摄像头角度差别很大没有通用值。5.2 多计数线实现分区域客流统计如果场景是「大厅有多个出入口」单条计数线不够用需要多条线分别统计。做法是把计数线定义成列表每条线独立维护计数器和轨迹历史。LINES { entrance_a: [(0, 400), (960, 400)], entrance_b: [(960, 400), (1920, 400)], } counters {name: {in: 0, out: 0} for name in LINES} histories {name: {} for name in LINES} def update_multi(tid, center): for name, line in LINES.items(): hist histories[name] if tid in hist: prev hist[tid] if cross_line(prev, center, line): if prev[1] line[0][1] center[1]: counters[name][in] 1 elif prev[1] line[0][1] center[1]: counters[name][out] 1 hist[tid] center每条线有独立的histories和counters互不干扰。这个结构可以直接扩展到任意数量的计数线前端按区域名展示就行。注意LINES里的线段端点不要重叠否则同一个人可能被两条线同时计数。5.3 验证计数准确率的土办法毕设答辩时老师一定会问「你这个准不准」。最实在的验证方法是人工数一遍视频和系统结果对比。具体操作选一段 5 分钟的视频人工记录进出人数然后跑系统算误差率。误差在 10% 以内算合格5% 以内算优秀。如果误差大逐帧回放找出误计或漏计的片段针对性调参数。我一般会准备一个ground_truth.txt每行记录一个人越线的时间戳和方向和系统日志做 diff很快就能定位问题。# 人工标注与系统结果对比 # ground_truth.txt 格式时间戳 方向 # 系统日志从 flow_event 表导出 sqlite3 flow.db SELECT event_time, direction FROM flow_event ORDER BY event_time system_log.txt diff ground_truth.txt system_log.txt | head -50diff能快速看出哪些事件对不上head -50只看前 50 行差异避免输出太多。这个方法土但有效比任何指标都直观。5.4 我踩过的最大一个坑最后说一个我自己的血泪教训。第一次做这个系统时我把计数线画在了画面正中间测试视频里效果很好误差不到 3%。结果换了一个摄像头角度画面里人从左上角斜着走进来中心点轨迹和计数线的交点位置完全变了计数直接翻倍。后来我改成用「目标框底部中心点」而不是「框中心点」来判断越线因为底部中心点更接近人的脚受上半身摆动影响小换角度后稳定性明显提升。这个改动只有一行代码但效果立竿见影。如果你也在做类似的项目建议一开始就用底部中心点能省掉很多调参时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表