简介:这是一份面向高校学生与深度学习入门者的毕业设计参考资源,围绕YOLOv8与PyQt5构建密集人群计数检测系统,适合需要完成目标检测类课题、希望快速搭建可视化演示界面的开发者。系统支持单张图片、视频文件与摄像头实时流三种检测方式,可实时输出人群数量与检测框,兼顾算法性能与交互体验。压缩包共184个文件,约59.54MB,包含20个py源码文件、3个pt权重文件、2个ui界面文件,以及大量jpg测试图片、png图标、xml标注与pyc缓存等,覆盖从模型推理到界面展示的完整工程结构。目前已有146人学习,读者可参考其YOLOv8检测流程、PyQt5界面布局与多源输入处理逻辑,快速理解人群计数项目的实现思路,并在此基础上完成自己的毕业设计或课程实践。
1. 密集人群计数系统拆包:YOLOv8 加 PyQt5 到底能跑出什么效果
密集人群计数这个方向,每年毕业设计季都会被翻出来做一遍。原因很直接:场景好理解,地铁口、景区入口、商场中庭,一眼就能看出需求;技术栈也成熟,YOLOv8 做检测头,PyQt5 做界面壳,两三天能搭出能跑的原型。但真正上手就会发现,能跑和能用之间隔着一堆细节——检测框抖动导致计数跳变、密集遮挡下漏检成片、视频流和界面刷新不同步直接卡死。
这份资源包给的是一个完整可运行的参考实现,核心是 YOLOv8 目标检测加 PyQt5 图形界面,支持图片、视频文件、摄像头实时流三种输入方式,检测结果直接在界面上叠加显示并输出人数统计。它适合两类人:一是正在做毕业设计、需要一套能演示能答辩的完整代码;二是刚接触 YOLOv8 部署、想找一个带 GUI 的端到端案例来练手。包里带了若干测试图片(40030016.jpg、40030170.jpg 等),拿到手不用自己找数据就能先跑通流程。下面按实际拆包顺序,从环境配置到界面逻辑再到踩坑记录,把这份资源怎么用、哪里容易翻车讲清楚。
2. 环境配置与依赖安装:把 YOLOv8 和 PyQt5 装进同一个 Python 环境
2.1 版本选型:为什么 Python 3.8 到 3.10 是安全区
YOLOv8 依赖 ultralytics 包,PyQt5 依赖 Qt5 运行时,两者对 Python 版本的要求有重叠但不完全一致。实测下来,Python 3.8 到 3.10 是最稳的区间。3.11 以上部分 ultralytics 版本会报torch编译兼容问题,3.7 以下 PyQt5 的新版本又装不上。如果你用的是 Ubuntu 20.04,系统自带的 Python 3.8 刚好在安全区内,不用额外折腾。
显卡方面,这个项目在 CPU 上也能跑,只是帧率会掉到个位数。如果有 GTX 1660 Ti 或更高规格的显卡,装 CUDA 版的 PyTorch 后检测速度会有明显提升。没有独显也不影响功能验证,先跑通再考虑加速。
2.2 依赖安装:一条命令背后的版本约束
资源包里没有显式的 requirements.txt,但根据代码引用可以反推出核心依赖。我一般会先建虚拟环境,再按下面的顺序装:
# 创建虚拟环境,Python 版本建议 3.8-3.10 python -m venv crowd_env source crowd_env/bin/activate # Windows 用 crowd_env\Scripts\activate # 先装 PyTorch,CPU 版够用,有显卡换对应 CUDA 版本 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 再装 ultralytics 和 PyQt5 pip install ultralytics pyqt5 opencv-python numpy # 验证关键包版本 python -c "import ultralytics; print(ultralytics.__version__)" python -c "from PyQt5.QtCore import QT_VERSION_STR; print(QT_VERSION_STR)"这里有个顺序问题:一定要先装 PyTorch 再装 ultralytics。ultralytics 安装时会检查 torch 是否存在,如果先装 ultralytics,pip 会自动拉一个可能不匹配的 torch 版本,后面再改就麻烦了。opencv-python用于视频帧读取和图像预处理,numpy 是隐式依赖但建议显式装上,避免版本冲突。
装完后跑一下yolo predict model=yolov8n.pt source=40030016.jpg,如果能输出检测结果图,说明 YOLOv8 环境没问题。PyQt5 的验证更简单,python -c "from PyQt5.QtWidgets import QApplication; print('ok')"不报错就行。
2.3 模型权重放置:yolov8n.pt 该放哪
YOLOv8 的预训练权重文件(yolov8n.pt、yolov8s.pt 等)不会自动出现在项目目录里。ultralytics 首次运行时会从官方源下载,但如果你在离线环境或者下载速度慢,就需要手动放置。常见做法是在项目根目录建一个weights文件夹,把下载好的 .pt 文件放进去,然后在代码里用相对路径引用。
资源包里没有附带权重文件,这是正常的——权重文件动辄几十兆,不适合打包进 zip。你需要自己下载 yolov8n.pt(约 6MB)或 yolov8s.pt(约 22MB)。n 版速度最快精度最低,s 版平衡一些,密集人群场景建议先用 n 版跑通流程,再根据漏检情况决定是否换更大的模型。
注意:权重文件的路径在代码里通常是硬编码的,拿到包后先全局搜一下
.pt关键字,确认它期望的路径和你实际放置的位置一致,否则会报FileNotFoundError。
3. 检测核心逻辑:YOLOv8 推理、计数与界面数据绑定
3.1 推理入口:从一帧图像到检测框列表
YOLOv8 的推理接口非常简洁,ultralytics 把预处理、前向计算、NMS 后处理都封装在了一次调用里。核心代码大概长这样:
from ultralytics import YOLO import cv2 # 加载模型,首次运行会自动下载权重 model = YOLO('yolov8n.pt') def detect_frame(frame): """ 输入:BGR 格式的 numpy 图像数组 输出:带标注的图像、检测框列表、人数 """ results = model(frame, conf=0.35, iou=0.45, classes=[0]) # classes=[0] 只检测人 annotated = results[0].plot() # 直接在原图上画框 boxes = results[0].boxes count = len(boxes) if boxes is not None else 0 return annotated, boxes, countconf=0.35是置信度阈值,低于这个值的检测框会被丢弃。密集人群场景下这个值不能设太高,否则远处的小目标全被过滤掉;也不能太低,否则背景误检会让人数虚高。0.3 到 0.4 是常见区间,具体看你的画面质量。iou=0.45控制 NMS 的合并力度,人群密集时框与框重叠严重,这个值可以适当调高到 0.5 左右,减少误合并。classes=[0]是关键参数,COCO 数据集里人的类别 ID 是 0,不加这个限制模型会把背包、手机都检出来。
3.2 计数逻辑:为什么直接 len(boxes) 会跳变
最直觉的计数方式就是数检测框数量,但实际跑起来会发现人数在相邻帧之间频繁跳动。原因有三个:一是视频压缩导致的帧间差异让模型对同一目标的置信度波动;二是遮挡导致目标时隐时现;三是 NMS 阈值在临界点附近不稳定。
常见做法是加一个简单的滑动窗口平滑:
from collections import deque class CountSmoother: def __init__(self, window_size=5): self.window = deque(maxlen=window_size) def update(self, count): self.window.append(count) # 取中位数比均值更抗离群值 sorted_w = sorted(self.window) return sorted_w[len(sorted_w) // 2]窗口大小 5 意味着用最近 5 帧的中位数作为当前显示值。中位数比均值好的地方在于,偶尔一帧误检出几十个人,均值会被拉高,中位数则不受影响。这个类在视频和摄像头模式下都要用,图片模式不需要。
3.3 PyQt5 界面与检测线程的绑定
PyQt5 的界面刷新必须在主线程,而 YOLOv8 推理是计算密集型任务,放在主线程会直接卡死界面。标准做法是用 QThread 把检测循环跑在子线程里,通过信号槽把结果传回主线程更新 UI。
from PyQt5.QtCore import QThread, pyqtSignal import cv2 class DetectThread(QThread): frame_ready = pyqtSignal(object, int) # 图像, 人数 def __init__(self, source=0): super().__init__() self.source = source self.running = True def run(self): cap = cv2.VideoCapture(self.source) smoother = CountSmoother(window_size=5) while self.running and cap.isOpened(): ret, frame = cap.read() if not ret: break annotated, _, raw_count = detect_frame(frame) smooth_count = smoother.update(raw_count) self.frame_ready.emit(annotated, smooth_count) cap.release() def stop(self): self.running = False self.wait()frame_ready信号携带两个参数:标注后的图像和当前人数。主线程收到信号后,把图像转成 QImage 再转成 QPixmap 显示在 QLabel 上,人数更新到 QLCDNumber 或 QLabel。这里有个性能细节:图像格式转换(BGR → RGB → QImage)比较耗时,如果帧率要求高,可以在子线程里就转好再发信号。
source=0表示默认摄像头,传视频文件路径就是视频模式,传图片路径就是单张模式。资源包里的 40030016.jpg 等图片可以直接作为 source 传入测试。
4. 三种输入模式的切换与参数调优
4.1 图片模式:单帧检测的边界处理
图片模式最简单,读一帧、检测、显示、结束。但有两个边界情况要注意:一是图片尺寸过大时,YOLOv8 内部会缩放到 640×640 再推理,检测框坐标需要映射回原图尺寸;二是图片格式不是标准 BGR 时(比如 PNG 带透明通道),cv2.imread 读进来是四通道,直接传给模型会报错。
def detect_image(image_path): frame = cv2.imread(image_path) if frame is None: raise ValueError(f"无法读取图片: {image_path}") # 确保是三通道 if len(frame.shape) == 3 and frame.shape[2] == 4: frame = cv2.cvtColor(frame, cv2.COLOR_BGRA2BGR) annotated, boxes, count = detect_frame(frame) return annotated, countresults[0].plot()返回的图像已经自动处理了坐标映射,不需要手动缩放。但如果你要自己画框或者导出检测坐标,就要注意boxes.xyxy返回的是原图坐标系下的值,可以直接用。
4.2 视频文件模式:帧率控制与跳帧策略
视频文件模式比摄像头多了一个问题:处理速度可能快于或慢于视频原始帧率。如果每帧都检测,CPU 上可能只能跑到 5 FPS,视频看起来像幻灯片;如果跳帧,又可能漏掉关键画面。
我一般会加一个帧间隔参数:
def process_video(video_path, frame_skip=2): cap = cv2.VideoCapture(video_path) fps = cap.get(cv2.CAP_PROP_FPS) frame_idx = 0 smoother = CountSmoother(window_size=5) while cap.isOpened(): ret, frame = cap.read() if not ret: break if frame_idx % frame_skip == 0: annotated, _, raw_count = detect_frame(frame) smooth_count = smoother.update(raw_count) # 显示 annotated 和 smooth_count frame_idx += 1 cap.release()frame_skip=2表示每两帧检测一次,中间帧复用上一次的检测结果。这样在 CPU 上能把有效帧率翻倍,代价是检测结果的时间分辨率降低。对于人群计数这种变化缓慢的场景,跳帧带来的误差可以接受。
4.3 摄像头实时流:延迟累积与缓冲区清理
摄像头模式最容易被忽略的问题是 OpenCV 的缓冲区。cv2.VideoCapture默认会缓存若干帧,如果你的检测速度跟不上摄像头采集速度,缓冲区会越积越多,界面显示的永远是几秒前的画面。
解决办法是设置缓冲区大小为 1:
cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)这样每次cap.read()拿到的都是最新帧。另外,摄像头模式下建议把frame_skip设为 1,因为实时性比流畅度更重要。如果检测一帧需要 200ms,那就接受 5 FPS 的刷新率,至少画面是实时的。
提示:不同摄像头驱动对
CAP_PROP_BUFFERSIZE的支持程度不一样,有些摄像头设了也不生效。如果发现延迟仍然很大,可以在每次 read 之前连续调用两次cap.grab()丢弃旧帧。
5. 避坑与排查:密集人群场景下最容易翻车的五个点
5.1 人数跳变严重,相邻帧差出十几个
现象:视频模式下人数显示在 20 到 35 之间反复横跳,肉眼观察画面里人数并没有明显变化。
原因:YOLOv8 对遮挡目标的置信度本身就在阈值附近波动,加上 NMS 的随机性,导致某些帧多检出几个、某些帧少检出几个。没有做任何平滑处理时,原始计数直接反映到了界面上。
解决:加滑动窗口中位数平滑,窗口大小 5 到 10 之间。如果跳变仍然明显,把conf从 0.35 降到 0.3,让检测更稳定,同时用iou调高到 0.5 减少框合并的随机性。
5.2 密集区域漏检成片,远处的人完全检不出来
现象:画面前排的人能检出,后排密集区域只检出零星几个,实际人数远大于显示人数。
原因:YOLOv8 默认输入尺寸是 640×640,密集小目标在缩放后像素太少,特征提取不充分。另外conf阈值设太高也会过滤掉这些小目标的低置信度检测。
解决:把推理尺寸调大,model(frame, imgsz=1280),代价是速度下降约一半。同时把conf降到 0.25 左右。如果还不行,考虑换 yolov8s 或 yolov8m 模型,小模型的特征提取能力有限。
5.3 PyQt5 界面卡死,点击按钮无响应
现象:启动检测后界面直接白屏,按钮点不动,窗口拖拽也卡顿。
原因:检测循环写在了主线程里,YOLOv8 推理阻塞了 Qt 的事件循环。
解决:把检测逻辑移到 QThread 子线程,通过信号槽更新 UI。注意子线程里不能直接操作 UI 组件,所有界面更新必须通过信号触发主线程执行。
5.4 摄像头打开失败或读到黑屏
现象:cv2.VideoCapture(0)返回 True 但read()一直返回 False,或者画面全黑。
原因:摄像头被其他程序占用,或者 Linux 下权限不足,或者摄像头索引不是 0。
解决:先确认没有其他程序在用摄像头。Linux 下检查/dev/video*权限,必要时把用户加入 video 组。索引从 0 试到 3,有些笔记本的内置摄像头是 1 而不是 0。
5.5 模型权重下载慢或下载失败
现象:首次运行时报Downloading yolov8n.pt然后卡住或超时。
原因:ultralytics 默认从 GitHub 下载权重,网络不稳定时容易失败。
解决:手动下载 yolov8n.pt 放到项目目录,然后在代码里把YOLO('yolov8n.pt')改成YOLO('./yolov8n.pt')指定本地路径。或者设置环境变量YOLO_WEIGHTS_DIR指向本地权重目录。
6. 从能跑到好用:三个提升检测质量的实操技巧
6.1 用跟踪算法稳住 ID,减少重复计数
单纯靠检测框计数,同一个人在不同帧里被反复计数是常态。加一个轻量级跟踪器(比如 ByteTrack)可以给每个检测框分配稳定 ID,计数时只统计当前帧中 ID 的数量,而不是框的数量。ultralytics 内置了跟踪接口:
from ultralytics import YOLO model = YOLO('yolov8n.pt') results = model.track(frame, persist=True, classes=[0], conf=0.35) if results[0].boxes.id is not None: count = len(set(results[0].boxes.id.int().tolist()))persist=True让跟踪器在帧间保持状态。boxes.id是每个检测框的跟踪 ID,用 set 去重后就是当前画面中的独立人数。这个方式比纯计数稳定得多,代价是增加了跟踪的计算开销,CPU 上帧率会再降一些。
6.2 区域计数:只统计画面中特定区域的人数
实际场景中往往只关心某个区域,比如地铁口的闸机通道。可以在画面上画一个多边形区域,只统计中心点落在区域内的检测框:
import cv2 import numpy as np def count_in_region(boxes, region_points): """ boxes: YOLOv8 的 boxes 对象 region_points: 多边形顶点列表 [(x1,y1), (x2,y2), ...] """ region = np.array(region_points, dtype=np.int32) count = 0 for box in boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() cx, cy = (x1 + x2) / 2, (y1 + y2) / 2 if cv2.pointPolygonTest(region, (cx, cy), False) >= 0: count += 1 return countpointPolygonTest返回正值表示点在多边形内部。区域顶点可以在界面上让用户手动标定,也可以硬编码。这个功能在答辩时很加分,因为它体现了对实际场景的理解。
6.3 导出检测结果:把每一帧的人数存成 CSV
答辩或写报告时需要数据支撑,把每帧的人数导出成 CSV 是最直接的方式:
import csv def export_counts(counts, output_path='counts.csv'): with open(output_path, 'w', newline='') as f: writer = csv.writer(f) writer.writerow(['frame', 'count']) for i, c in enumerate(counts): writer.writerow([i, c])counts是每帧平滑后的人数列表。导出的 CSV 可以直接丢进 Excel 画曲线图,也可以用来计算平均人数、峰值人数等统计指标。如果用了跟踪算法,还可以额外导出每个 ID 的出现帧范围,分析每个人的停留时间。
6.4 一个我踩过的坑:模型切换后忘记改类别过滤
有次我把 yolov8n 换成 yolov8s 做对比测试,发现人数突然多了好几倍。排查了半天才想起来,新模型的classes参数没传,它把画面里的背包、椅子、手机全检出来了。从那以后我每次换模型或者改推理参数,都强制走一遍单张图片的验证流程,确认输出框的数量和位置合理,再跑视频。这个习惯帮我省了很多次重新排查的时间。
希望这份拆解能帮你把这份资源顺利跑起来,少走一些我走过的弯路。
本文还有配套的精品资源,点击获取