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

资讯详情

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

基于深度学习的人流量检测系统:YOLO+ByteTrack从零搭建与避坑指南

基于深度学习的人流量检测系统:YOLO+ByteTrack从零搭建与避坑指南

简介:这份资源是面向高校学生与深度学习初学者的毕业设计完整项目包,主题为基于深度学习的人流量检测系统,使用Python开发,已通过导师指导,可直接用于课程设计、期末大作业或自学实践。压缩包共1482个文件,约61.64MB,涵盖76个Python源码文件、382个html页面、208张png图片及js、css、md、json、yaml等配套资源,其中person_detection模块承载行人检测算法实现,run.py为系统入口,doc与pfd_web目录分别存放项目文档和Web界面设计文件,附赠内容中还可能包含预训练模型与数据集链接。项目完整覆盖数据收集、模型训练、算法部署与系统实现全流程,涉及CNN等网络结构,并关注检测的准确性、实时性与鲁棒性。目前已有75人学习,适合希望将深度学习与计算机视觉理论落地为实际应用、积累项目经验的学习者参考。

1. 从一段监控视频说起:人流量检测系统到底在数什么

商场门口那台摄像头,每天拍下几万帧画面。物业想知道今天进了多少人、哪个时段最挤、要不要加开一部电梯。传统做法是安排一个人盯着屏幕按计数器,或者在地上铺压力感应踏板。前者费人力还容易走神,后者一遇到并排走、拎大箱子、推婴儿车就漏计。基于深度学习的人流量检测系统,要解决的就是把这件事自动化:给一段视频或一路实时流,输出画面里的人数、每个人的位置,甚至进一步统计穿越某条线的累计人次。

这个方向在计算机毕业设计里出现频率极高,原因很实际——它同时踩中了深度学习、Python、源码可复现三个点。你不需要昂贵的采集设备,一段公开的监控视频、一台带显卡的笔记本就能跑起来;算法侧有成熟的开源检测模型可以直接用;工程侧用 Python 把检测、跟踪、计数串成一条流水线,代码量可控,答辩时也讲得清楚。适合谁做?适合已经学过 Python 基础、想找一个能出可视化结果、又能体现算法理解的题目的同学。它不要求你从零训练一个网络,但要求你搞明白检测模型输出的是什么、怎么把逐帧结果变成有意义的人数统计。

我见过太多人卡在同一个地方:模型能框出人,但一帧一帧数出来的数字疯狂跳动,同一个人被重复计数。这不是模型的问题,是系统设计的问题。接下来几章,我把这套系统从选型到跑通、从计数逻辑到踩坑,按我实际做过的顺序讲一遍。

2. 选检测模型还是选跟踪方案:人流量检测系统的技术栈怎么定

动手之前先想清楚一件事:人流量检测系统的核心难点不在“检测”,而在“去重”。检测模型负责回答“这一帧里人在哪”,跟踪或计数逻辑负责回答“这个人和上一帧那个人是不是同一个”。很多人一上来就纠结用 YOLO 还是 Faster R-CNN,其实检测模型选哪个,对最终计数准确率的影响,远小于去重逻辑写得好不好。

2.1 检测模型选型:为什么多数人流量检测系统落在 YOLO 系列

人流量检测对实时性有要求。监控视频常见 25 帧每秒,如果单帧推理要 200 毫秒,整条流水线就跟不上,画面会卡成幻灯片。YOLO 系列属于单阶段检测器,一次前向传播同时输出边界框和类别,速度上有天然优势。Faster R-CNN 这类两阶段检测器精度可能略高,但推理慢,用在离线视频分析还行,做实时系统就吃力。

选型时看三个指标:推理速度(FPS)、小目标召回率、部署难度。监控画面里人往往只占几十个像素,属于小目标,所以模型的输入分辨率不能压得太狠。我一般会把输入尺寸设到 640,再根据显卡显存决定 batch size。部署难度上,YOLO 有成熟的 Python 推理接口,导出成 ONNX 之后还能脱离训练框架跑,这对毕业设计这种“换台电脑也要能演示”的场景很关键。

提示:不要为了追求论文里的高 mAP 去选一个推理极慢的模型。人流量检测系统的验收标准是计数误差和实时性,不是检测榜单排名。

2.2 跟踪与计数:ByteTrack 和虚拟线计数怎么配合

检测只给当前帧,跟踪负责给每个人分配一个稳定的 ID。常用做法是 ByteTrack 这类基于检测的跟踪器:它不依赖外观特征,主要靠运动预测和 IoU 匹配,速度快、对遮挡有一定容忍。跟踪稳定之后,计数就变成“统计有多少个 ID 越过了某条线”。

虚拟线计数的逻辑是:为每个跟踪 ID 记录它上一帧的中心点位置,当中心点从线的这一侧移动到另一侧时,计数加一。这里有个细节——线要有方向,进和出分开统计,否则来回走动的人会被反复计数。下面这段伪代码说明了核心判断:

# tracks: 当前帧所有跟踪目标,每个含 id 和中心点 (cx, cy) # last_positions: 字典,记录每个 id 上一帧的中心点 # line_y: 虚拟线的纵坐标,假设水平线,从上往下穿越算进入 count_in = 0 for t in tracks: tid, cx, cy = t['id'], t['cx'], t['cy'] if tid in last_positions: prev_cy = last_positions[tid][1] # 上一帧在线上方,当前帧在线下方,判定为一次进入 if prev_cy < line_y <= cy: count_in += 1 last_positions[tid] = (cx, cy)

逻辑说明:只对已经出现过的 ID 做穿越判断,新 ID 第一帧只记录位置不计数,避免刚进画面就被误判。参数line_y要根据实际画面里人行走的通道位置来定,不能随便取画面中线。如果摄像头是斜视角,水平线可能不够用,需要改成任意方向的线段,用向量叉积判断穿越方向。

2.3 环境搭建:Python 依赖和推理后端怎么装

环境这块,我建议用 conda 建独立环境,避免和系统里的其他包打架。核心依赖就几个:推理框架、图像处理、数值计算。下面是一份能跑通的最小依赖清单,版本不写死,装当前稳定版即可:

# 创建并激活环境 conda create -n crowd python=3.10 -y conda activate crowd # 安装推理与图像处理依赖 pip install ultralytics opencv-python numpy # ultralytics 提供 YOLO 的 Python 接口,opencv 负责读写视频和画框

逻辑说明:ultralytics这个包把模型加载、推理、后处理都封装好了,几行代码就能跑检测。opencv-python用来读视频帧、画检测框和计数线。装完之后先跑一个最小验证,确认模型能加载、能对一张图出结果,再去接视频。参数上,python=3.10是比较稳的版本,太新的 Python 有时会遇到某些包还没出预编译轮子,装起来费劲。

注意:如果你用的是没有独立显卡的机器,推理会走 CPU,速度可能只有几 FPS。毕业设计演示建议至少用一台带 NVIDIA 显卡的机器,或者把视频抽帧成图片做离线分析,避开实时性要求。

3. 把检测跑起来:从单张图片到视频流的完整链路

环境好了,接下来把检测真正跑通。这一章的目标是:你能拿到一段视频,输出带框的画面,并且框的位置是对的。听起来简单,但视频读写、坐标缩放、帧率控制这几个地方,每个都能让人卡半天。

3.1 用 YOLO 在本地跑通单帧检测的最小命令

先别急着上视频,用一张图确认模型没问题。准备一张有人的图片,跑下面这段:

from ultralytics import YOLO # 加载预训练模型,首次运行会自动下载权重 model = YOLO('yolov8n.pt') # 对单张图片推理,conf 是置信度阈值 results = model('test.jpg', conf=0.4, classes=[0]) # classes=[0] 表示只检测 person 这一类,COCO 数据集里 person 的类别 id 是 0 for r in results: boxes = r.boxes print('检测到人数:', len(boxes)) for box in boxes: # xyxy 是左上角和右下角坐标 print(box.xyxy.tolist(), box.conf.tolist())

逻辑说明:YOLO('yolov8n.pt')里的n是 nano 版本,模型最小、速度最快,适合先跑通流程。conf=0.4是置信度阈值,低于这个值的框会被丢掉;阈值调低会检出更多目标但误检增加,调高则相反。classes=[0]只保留人类,避免把车、椅子也框进来干扰计数。跑通后你会看到打印出人数和每个框的坐标,坐标是相对原图的像素值。

参数怎么调:如果发现漏检(人没被框住),先把conf降到 0.25 试试;如果误检多(把广告牌上的人像也框了),把conf提到 0.5 以上。yolov8n换成yolov8s或yolov8m,精度会提升但速度下降,根据你的显卡权衡。

3.2 视频逐帧推理:读帧、推理、画框、写出的四步循环

单帧没问题后,把循环套上。视频处理的标准结构是:读一帧、推理、画结果、写一帧,直到读完。

import cv2 from ultralytics import YOLO model = YOLO('yolov8n.pt') cap = cv2.VideoCapture('crowd.mp4') # 获取视频参数,用于写出同样规格的结果视频 fps = int(cap.get(cv2.CAP_PROP_FPS)) w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) writer = cv2.VideoWriter('out.mp4', cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h)) while True: ret, frame = cap.read() if not ret: break # 视频读完 results = model(frame, conf=0.4, classes=[0], verbose=False) for r in results: for box in r.boxes: x1, y1, x2, y2 = map(int, box.xyxy[0].tolist()) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) writer.write(frame) cap.release() writer.release()

逻辑说明:cv2.VideoCapture打开视频,read()每次返回一帧和是否成功的标志。verbose=False关掉模型每帧的日志输出,否则终端会被刷屏。画框用cv2.rectangle,颜色(0,255,0)是绿色,线宽 2。写出视频的编码器用mp4v,兼容性好。

参数说明:fps从原视频读取,保证输出视频播放速度正常。如果原视频帧率很高(比如 60),逐帧推理跟不上,可以每隔一帧处理一次,但计数时要相应调整,否则会漏掉快速移动的人。这一步跑完,你应该得到一个带绿框的输出视频,框基本跟得住人。

3.3 接上跟踪与计数:让数字稳定下来

检测框有了,现在把跟踪和虚拟线计数接进去。跟踪器用 ByteTrack,它已经集成在 ultralytics 里,调用时加一个参数就行。

import cv2 from ultralytics import YOLO model = YOLO('yolov8n.pt') cap = cv2.VideoCapture('crowd.mp4') line_y = 300 # 虚拟线纵坐标,按实际画面调整 last_pos = {} # 记录每个 id 上一帧的中心点 count_in = 0 while True: ret, frame = cap.read() if not ret: break # persist=True 让跟踪器在帧之间保持状态 results = model.track(frame, conf=0.4, classes=[0], persist=True, verbose=False) for r in results: if r.boxes.id is None: continue # 这一帧没有跟踪到任何目标 ids = r.boxes.id.int().tolist() boxes = r.boxes.xyxy.tolist() for tid, box in zip(ids, boxes): x1, y1, x2, y2 = box cx, cy = (x1 + x2) / 2, (y1 + y2) / 2 if tid in last_pos: if last_pos[tid] < line_y <= cy: count_in += 1 last_pos[tid] = cy cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.line(frame, (0, line_y), (frame.shape[1], line_y), (0, 0, 255), 2) cv2.putText(frame, f'IN: {count_in}', (20, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) cv2.imshow('crowd', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

逻辑说明:model.track在检测基础上加了跟踪,persist=True是关键,它让跟踪器记住上一帧的状态,否则每帧都当新目标处理,ID 会乱跳。r.boxes.id是跟踪 ID,可能为 None(这一帧没目标)。计数逻辑和前面伪代码一致,只对已存在的 ID 判断穿越。cv2.line画出虚拟线,cv2.putText在左上角显示累计进入人数。

参数说明:line_y是唯一需要你手动调的参数,打开视频看人主要从哪个位置走过,把线放在通道中间。如果画面里人从右往左走,那要改成判断横坐标穿越,逻辑对称。cv2.waitKey(1)里的 1 是等待毫秒数,值越小播放越快,设成 0 会暂停等按键。

4. 人流量检测系统避坑:那些让计数翻车的细节

这一章是我踩过的坑合集。每个都真实发生过,现象、原因、解决按顺序写清楚,你对照自己的系统排查。

4.1 同一个人被反复计数:ID 跳变是元凶

现象:计数数字涨得比实际人数快很多,明明画面里只有五个人,计数器显示二十几。

原因:跟踪 ID 不稳定。当人走到画面边缘、被遮挡、或者检测框抖动时,跟踪器可能给同一个人分配了新 ID。新 ID 第一次出现时,如果它的位置刚好在线的另一侧,就会被误判为一次穿越。

解决:第一,新 ID 的第一帧只记录位置,不参与穿越判断,这一点前面代码已经做了。第二,给跟踪器加一个“丢失后保留”的缓冲,人短暂被遮挡时不立刻删 ID。第三,如果 ID 跳变仍然严重,考虑引入简单的外观特征做二次匹配,但会牺牲速度。我一般先调跟踪器的匹配阈值,把 IoU 匹配放宽一点,让 ID 更稳。

4.2 计数线位置选错:斜视角下水平线失效

现象:人明明从通道走过,计数不增加;或者人在远处走动没经过通道,计数却涨了。

原因:摄像头是斜视角,画面里“通道”在图像上不是水平的,而虚拟线画成了水平线,导致穿越判断和实际行走方向对不上。

解决:把虚拟线改成任意方向的线段,用向量叉积判断点在线段的哪一侧。具体做法是记录每个人上一帧中心点相对线段的方向符号,符号翻转才算穿越。线段两个端点根据画面里实际通道的透视位置来定,不要凭感觉画。

4.3 置信度阈值一刀切:远处的人全漏了

现象:画面近处的人框得很准,远处的人一个都检测不到,计数严重偏低。

原因:远处的人像素少,模型输出的置信度天然偏低。统一的conf=0.4把远处目标全过滤掉了。

解决:不要用单一阈值。可以按检测框面积分档,小框用低阈值、大框用高阈值;或者直接降低全局阈值到 0.25,再靠跟踪的连续性过滤掉偶发误检。另一个办法是提高模型输入分辨率,让远处的人占更多像素,但推理会变慢。我通常先降阈值,观察误检是否可接受,再决定要不要换更大的模型。

4.4 视频读写踩坑:输出文件打不开或没画面

现象:程序跑完没报错,但输出的 mp4 文件打不开,或者打开是黑屏。

原因:编码器不匹配,或者写出的帧尺寸和原视频不一致。cv2.VideoWriter对尺寸很敏感,宽高写反、或者和实际帧尺寸差一个像素,都会导致写入失败。

解决:写之前先打印frame.shape确认实际尺寸,用这个尺寸初始化 writer。编码器mp4v在多数环境可用,如果不行换XVID配合.avi后缀。另外,writer.write的帧必须是 BGR 三通道,如果中间做了灰度转换,写之前要转回来。

4.5 实时演示卡顿:推理和显示抢资源

现象:本地跑视频文件正常,一接摄像头就卡,画面延迟好几秒。

原因:摄像头采集、模型推理、画面显示在同一个循环里串行执行,任何一步慢都会拖累整体。显示窗口的刷新也会占用时间。

解决:把推理和显示解耦,用队列缓冲帧。或者降低处理帧率,每两帧处理一次,显示时复用上一帧的结果。演示场景下,如果只是给人看计数效果,可以关掉实时显示,只输出计数数字,把资源全留给推理。

5. 让系统更像一个“系统”:统计报表与参数调优的进阶做法

前面跑通的是最小可用版本。答辩或者实际交付时,光有一个跳动的数字不够,你需要能回答“今天总共多少人、高峰在几点、误差有多大”。这一章讲怎么把逐帧结果沉淀成可分析的数据,以及几个让准确率再上一档的技巧。

5.1 把逐帧计数落成时间序列:按分钟聚合的统计表

最直接的做法是记录每次计数发生的时间戳,最后按分钟或按小时聚合。下面这段代码在计数发生时追加一条记录,程序结束后汇总:

import time import csv records = [] # 每条记录含时间戳和方向 def on_cross(direction): # direction: 'in' 或 'out' records.append({'ts': time.time(), 'dir': direction}) # 程序结束后,按分钟聚合 from collections import defaultdict buckets = defaultdict(lambda: {'in': 0, 'out': 0}) for r in records: minute = time.strftime('%H:%M', time.localtime(r['ts'])) buckets[minute][r['dir']] += 1 with open('report.csv', 'w', newline='') as f: writer = csv.writer(f) writer.writerow(['时间', '进入', '离开', '净流入']) for minute, v in sorted(buckets.items()): writer.writerow([minute, v['in'], v['out'], v['in'] - v['out']])

逻辑说明:on_cross在每次穿越判断成立时调用,记录时间戳和方向。聚合时用strftime把时间戳格式化成分钟字符串作为 key。输出 CSV 可以直接用 Excel 打开画折线图,答辩时展示高峰时段一目了然。参数上,聚合粒度按视频时长选,几分钟的演示视频按秒聚合,几小时的监控按分钟聚合。

5.2 用抽帧评估法量化你的计数误差

系统准不准,不能靠感觉。找一个已知人数的视频片段,人工数出真实穿越次数,和系统输出对比。更严谨的做法是抽帧评估:每隔一定帧数截一张图,人工标注画面里的人数,和模型检测人数对比,算平均绝对误差。

评估项做法关注指标
检测精度抽帧人工标人数 vs 模型检出人数平均绝对误差、漏检率
计数精度人工数穿越次数 vs 系统计数相对误差百分比
实时性记录处理总帧数和耗时平均 FPS
ID 稳定性观察同一人 ID 切换次数每百帧 ID 切换数

这张表可以直接放进毕业设计论文的实验章节。评估时注意,人工标注本身也有误差,所以不要追求零误差,相对误差控制在 10% 以内就算可用。

5.3 三个让准确率再上一档的调参习惯

第一个习惯:分时段调阈值。白天光线好,检测置信度普遍高;傍晚逆光,置信度整体下降。如果系统要跑全天,固定阈值必然在某个时段翻车。可以按画面亮度动态调整conf,亮度低时降到 0.3,亮度正常时用 0.45。

第二个习惯:给虚拟线加缓冲带。不要用一条零宽度的线判断穿越,而是用两条线组成的窄带。人进入窄带时标记状态,离开窄带时根据离开方向计数。这样能过滤掉在线上来回踱步导致的反复计数。

第三个习惯:定期清理过期 ID。跟踪器维护的 ID 表会越来越大,长时间运行后内存上涨、匹配变慢。每隔一段时间清理掉超过若干帧没有更新的 ID,保持跟踪器轻量。

我做完这套系统最大的教训是:模型选型花了两天,去重逻辑调了一周。真正决定人流量检测系统好不好用的,从来不是检测网络有多深,而是你有没有耐心把跟踪和计数的边界情况一个个堵上。先把最小链路跑通,再拿真实视频反复看计数和实际差多少,差在哪一段,比对着论文调参有用得多。希望帮到你。

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

返回列表