
简介这套基于深度学习与PyQt5的行人识别检测计数系统是一份面向毕业设计场景的完整源码项目适合计算机相关专业学生在毕设、课程设计或期末大作业中直接参考与二次开发。系统采用CenterNet等目标检测方法以PyQt5搭建交互界面从数据准备、模型预测到计数统计均有对应Python脚本实现并已提供训练好的模型权重可直接运行。压缩包中总计34个文件以19个Python脚本为主体覆盖UI、模型结构、训练与预测等环节另有h5模型文件、csv标注数据、ttf字体和配置文档等辅助内容整体包体约684MB结构清晰便于按模块查阅。该设计评审分99分经导师把关代码完整性高目前已有110人学习下载附带说明文档和数据样例能帮助快速复现并理解整套行人计数流程对需要高分毕设参考的学习者很有价值。1. 这项目到底解决什么问题界面、检测和计数怎么合成一个能演示的系统答辩前一个星期模型在终端里已经能框出人导师要看的是能操作、能计数的界面于是很多人开始找“基于深度学习PyQt5的行人识别检测计数系统”这类源码。这道题的本质不是让你从零发明算法而是把三件事串成一条能演示的链路用深度学习模型最常选 YOLO 系列在每帧里检出行人用跟踪和计数逻辑统计人数再用 PyQt5 把画面、检测框、统计数字放进同一个桌面窗口。它对毕业设计、课程设计很友好也适合想快速搭一个可视化 Demo 的工程入门者。难点不在某个环节而在它们之间的衔接。2. 核心链路深度学习行人检测 PyQt5 界面架构怎么串起来我拿到这类需求时第一步不是写代码而是先把数据流画出来摄像头或视频文件 - OpenCV 解码成帧 - 模型前向推理 - 得到检测框 - 跟踪器补 ID - 计数逻辑更新统计 - 最后把结果帧交给 PyQt5 显示。这样拆完之后你自然会发现主线程和推理线程必须分开否则界面会卡到像死机。下面把模型选型和线程模型一起讲清楚。2.1 行人检测选型为什么是 YOLOv8n 而不是自己训 Faster R-CNN很多人的第一反应是“深度学习嘛那我自己训一个模型才显得有工作量”。但在毕业设计这个时间盒里自己从零训练目标检测模型意味着要处理数据标注、类别不平衡、训练收敛、验证集划分一大堆问题。更合理的常见做法是直接用已经在 COCO 上训练好的公开权重再针对实际场景做少量微调。COCO 数据集的 80 个类别里就有 person 这一类YOLO 系列权重天然知道“人长什么样”。在 YOLO 系列里我一般会选 YOLOv8n。这里的 n 是 nano最轻量的一档。它和 YOLOv8s/m 相比精度稍微低一点但前向速度明显更快模型文件也小。在 PyQt5 桌面应用里你要的是“用户双击打开就能看到实时画面”不是跑 benchmark。如果显卡只是入门级用 YOLOv8n 能把 FPS 拉上去界面体验会好很多。你可能会搜到“yolov5训练自己的模型”“yolov11训练自己的模型”这些词它们解决的是训练侧的问题而“行人识别检测计数系统”这类标题一般更侧重于部署和界面侧。选型时还要注意一组容易被忽略的参数输入尺寸和置信度。常见配置是 imgsz640conf0.5iou0.45。imgsz 越大小目标检测效果越好但推理时间增长不是线性的640 是性价比最高的起点。conf 是保留检测框的最低置信度0.5 对行人这类目标很稳如果镜头远处的人经常被漏检可以降到 0.3但误检会变多。iou 是 NMS 阈值默认 0.45控制两个重叠框是否合并行人之间一旦发生遮挡iou 调低到 0.4 能减少框互相吞掉的情况。再说“自己训 Faster R-CNN”为什么通常不划算。Faster R-CNN 精度上限不差但它分成 RPN 和 RCNN 两段前向计算比单阶段的 YOLO 重很多。在 PyQt5 里接实时摄像头时FPS 一旦掉到个位数演示效果会很难看。除非题目点名要求两阶段方法否则我建议选 YOLOv8n。如果确实需要自己训练可以走“yolov5训练自己的模型”那条路线标注数据、准备 yaml、跑训练脚本最后导出 best.pt。这个“训练好模型”的权重文件就是整个系统最关键的输入。2.2 PyQt5 界面与推理线程分离QThread 加信号槽的标准做法PyQt5 教程里反复强调一件事不要在 UI 主线程里做耗时操作。模型推理一次少说几十毫秒在 CPU 上甚至要几百毫秒。如果你在按钮的 clicked 槽里直接调用 model.predictQt 的事件循环会被卡住窗口拖不动、按钮点不了看起来就像死机。准确地说不是死机是主线程在等推理返回。常见做法是把检测放到 QThread 里通过信号把结果传回主线程。PyQt5 的信号槽是线程安全的信号在子线程里 emit主线程的槽函数会在事件循环里被调用这就绕开了手动加锁传递数据的麻烦。我一般会写一个专门的 Worker 类import cv2 from PyQt5.QtCore import QThread, pyqtSignal from ultralytics import YOLO class DetectWorker(QThread): frame_ready pyqtSignal(object) result_ready pyqtSignal(object) def __init__(self, model_path, source, parentNone): super().__init__(parent) self.model YOLO(model_path) self.source source self._running True def run(self): cap cv2.VideoCapture(self.source) while self._running and cap.isOpened(): ok, frame cap.read() if not ok: break self.frame_ready.emit(frame.copy()) # 不复制会踩到上一帧的内存 results self.model.predict( frame, imgsz640, conf0.5, verboseFalse ) self.result_ready.emit(results) cap.release() def stop(self): self._running False这段代码的逻辑很直白run() 是 QThread 启动后子线程里执行的入口VideoCapture 打开 sourcesource 可以是摄像头索引 0也可以是视频文件路径每一帧先发一个 frame_ready 信号让界面先显示原始画面再调用模型推理把 results 通过 result_ready 发回去。frame.copy() 值得专门说因为 OpenCV 的 read 可能会复用同一块内存不复制就发出去等主线程显示的时候这块内存可能已经被下一帧覆盖了画面会闪或花屏。参数上source 为 0 时表示默认摄像头笔记本多摄像头可能要试 1、2。model_path 指向 best.pt 这类训练好的权重。pyqtSignal(object) 里用 object 是因为要跨线程传 OpenCV 图像和 YOLO 的 results 对象它们不是 PyQt 内置类型object 是最省事的泛型槽。stop() 里把 _running 置为 False循环会尽快退出关闭窗口时一定要调用 stop() 再 wait()不然线程还在跑程序退出时可能报 “QThread: Destroyed while thread is still running”。这里有一个取舍我让每一帧都做推理是图省事。实际项目里如果推理速度跟不上视频帧率QThread 里会不断积压待处理帧内存上涨界面延迟变大。更稳的做法是只在“新帧准备好且上一帧推理已完成”时才处理下一帧或者用队列控制最多缓存两帧。这个 Worker 是串行消费模型不是并行流水线但对毕业设计演示已经足够。2.3 模型加载与推理从 best.pt 到一帧检测结果的最小代码当你拿到一个“源码训练好模型”的压缩包时优先确认的东西只有几个权重文件在哪、类别序号是不是 person0、推理代码在哪个文件。很多系统的入口文件叫 main.py 或 main_window.py但代码结构无非就是这一步用一个 YOLO 对象加载权重然后反复调用 predict 或 track。下面是最小可运行的一段from ultralytics import YOLO model YOLO(rweights/best.pt) results model.predict( frame, imgsz640, # 输入分辨率越大越慢 conf0.5, # 置信度阈值低于此值的框被丢弃 iou0.45, # NMS 的 IoU 阈值 device0, # 0 表示第一块 GPUcpu 表示 CPU halfTrue, # 半精度 FP16 推理仅 GPU 可用 verboseFalse ) boxes results[0].boxes if boxes is not None and len(boxes) 0: for box in boxes: cls int(box.cls[0]) conf float(box.conf[0]) x1, y1, x2, y2 [int(v) for v in box.xyxy[0]] if cls 0: pass # 这里就是你要的行人框这段代码先把 best.pt 加载成 YOLO 对象。predict 接收一帧 BGR 图像返回一个 Results 列表。列表里第一个元素的 boxes 字段包含了检测框、置信度、类别。注意 boxes.xyxy 默认是像素坐标直接可以画如果使用 results[0].boxes.xyxyn 则是归一化坐标画之前要乘回图像宽高。device0 表示使用第一块 GPU不写或者写 devicecpu 就是纯 CPU 推理。halfTrue 打开 FP16 半精度只有 GPU 才支持在 CPU 上开 half程序会直接报错。这是常见翻车点我一般在代码里加一个判断只有 torch.cuda.is_available() 时才传 halfTrue。imgsz640 和 conf0.5 与 2.1 节对应。如果显存不够把 imgsz 降到 512 或 416 能明显减负代价是小目标召回率下降。提示拿到 best.pt 后不要马上接界面先用一张行人图片单独跑一次推理打印出 cls 和 conf确认权重没问题再接 UI。这个过程能过滤掉一半以上的“检测不到人”问题。3. 让“计数”可用检测 跟踪 计数逻辑的三层落地检测解决“人在哪”计数解决“有多少人 / 过了多少人”。这两者的差别很容易被忽略如果只用每帧检测数量当统计数字同一人走出画面再回来就会被重复计算。真正的计数系统要分三步先做目标检测再做目标跟踪最后用跟踪 ID 去重并给出统计值。这套逻辑在商场客流统计、教室人数统计里都是同一个套路。3.1 画面内计数与越线计数两种需求对应两种实现最简单的“画面内计数”就是当前帧里检测框的数量用来回答“现在画面里有几个人”。这个不用跟踪器person_count sum(1 for box in boxes if int(box.cls) 0)一行代码就能算出来。但这种统计有明显弱点检测一波动计数就跟着抖。如果需求是“报告当前人数”通常还需要一个时间窗口平滑比如取最近 5 帧的中位数。而“越线计数”关心的是流量比如一天有多少人从某个门口经过这时必须建立跨帧的轨迹。越线计数的经典做法是设定一条虚拟线例如画面高度 480 像素处然后判断跟踪目标的重心是否跨越了这条线。我会保存每个跟踪 ID 的上一帧重心坐标当上一帧在线的上方、当前帧到了下方就认为完成一次方向向下的穿越反向类似。为了不重复计数要维护一个 crossed_ids 集合同一 ID 只计一次只有当目标离开视线再重新出现时才允许下一次计数。实现如下def count_line_cross(tracks, line_y, crossed_ids, directiondown): cross_count 0 for track in tracks: track_id track[id] prev_y track[prev_center][1] curr_y track[center][1] if track_id in crossed_ids: continue # 判断重心是否从线的一侧到了另一侧 if direction down and prev_y line_y and curr_y line_y: crossed_ids.add(track_id) cross_count 1 elif direction up and prev_y line_y and curr_y line_y: crossed_ids.add(track_id) cross_count 1 return cross_count这段代码我在不同项目里写过很多遍需要解释三个参数line_y 是线的 y 坐标可以固定写在配置里更进阶的做法是在 PyQt5 窗口上用鼠标拖动一条横线把位置保存下来。direction 区分计数方向单人单向通行时只统计“进”双向场景可以计两个数。crossed_ids 是 Python 的 set去重的核心依据是跟踪 ID不是检测框。如果没有跟踪器同一人跨线的 10 帧会触发 10 次计数这也是很多人说“计数系统不准”的最大原因。还有一个细节用重心判断跨线其实不是最准的。人走路时身体上下晃动重心可能会在线上来回抖动造成反复穿越。更稳的做法是用脚底点框的下边沿 y2或者要求连续 3 帧都满足穿越条件后再计数。很多开源代码用重心是为了实现简单我自己的习惯是加一个连续帧阈值把偶然抖动滤掉。3.2 跟踪器接入ByteTrack 与 DeepSort 的参数取舍跟踪器的作用是给每个检测框分一个 ID让同一人在连续帧里保持同一个 ID。不需要自己实现跟踪器ultralytics 的 track 方法已经内置了用起来比单独调库省事很多。常见的两种是 ByteTrack 和 DeepSort。ByteTrack 基于检测框的 IoU 关联不需要额外的行人外观特征速度快对普通视频够用DeepSort 额外引入 ReID 特征在严重遮挡、目标长期消失再出现的场景里更鲁棒但要额外下载一个特征权重推理时间也更高。在“行人识别检测计数系统”里我会优先用 ByteTrack。原因是摄像头场景下行人目标相对大遮挡虽然存在但 ByteTrack 的 track_buffer 参数已经把短暂遮挡的情况考虑进去了。接入代码比 predict 只多两个参数results model.track( frame, trackerbytetrack.yaml, persistTrue, # 跨帧保持跟踪状态不能漏 conf0.3, # 跟踪场景下 conf 可以调低 iou0.5, imgsz640, verboseFalse, ) if results[0].boxes.id is not None: ids results[0].boxes.id.cpu().tolist() xyxy results[0].boxes.xyxy.cpu().tolist() clss results[0].boxes.cls.cpu().tolist()这个代码块的意思track 会在每帧检测后执行跟踪给有把握的框分配 ID。persistTrue 让跟踪状态在视频帧之间保持绝对不能漏。返回值里 boxes.id 就是每个框的轨迹 IDboxes.id 可能是 None表示这一帧没有成功关联的目标所以使用前必须判空。conf 调到 0.3 是为了让跟踪器能拿到更多低分检测框ByteTrack 的特点是它会把低分框也纳入匹配因此这里不能把 conf 卡得太死。bytetrack.yaml 里真正影响行为的参数有三个track_thresh、track_buffer、match_thresh。track_thresh 是激活跟踪的分数阈值默认 0.5track_buffer 是目标丢失后保留轨迹的帧数默认 30如果画面中有目标被完全遮挡几秒可以调大到 60match_thresh 是匹配时用的相似度门槛默认 0.8调小会让匹配更宽松但容易串 ID。这三个参数没有绝对正确我通常先按默认跑一遍看计数结果再针对性调 track_buffer。跟踪器的坑多数不在理论而在 ID 跳变这是计数误差的直接来源。如果坚持用 DeepSort需要额外指定一个 ReID 权重路径例如 trackerdeepsort.yaml其中 model 字段指向 ReID 模型。DeepSort 对 CPU 更不友好GPU 资源不够时会明显掉 FPS所以除非场景里人挨着人、遮挡非常严重否则 ByteTrack 是性价比更高的选择。3.3 计数结果可视化在 PyQt5 画面上叠加统计面板检测和计数做完以后要让评委一眼看懂界面至少要有三样东西实时画面、每帧检测框、当前计数或累计计数。画框可以在拿到 results 后用 OpenCV 直接画在帧上再把帧转成 QImage 显示。另一路是单独放一个 QLabel 显示“当前人数: 5 / 累计通过: 23”用 setText 更新即可。我一般会在 Worker 里维护一个计数器对象每次检测完把最新统计放回 results 对象里界面只管读。把 OpenCV 图像显示到 QLabel 有一段固定代码容易写错的是颜色通道和 bytesPerLinedef frame_to_qimage(frame): rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # OpenCV 是 BGR h, w, ch rgb.shape bytes_per_line ch * w # 必须等于 3 * width return QImage(rgb.data, w, h, bytes_per_line, QImage.Format_RGB888)OpenCV 默认通道顺序是 BGRQt 是 RGB不转换的话画面会偏蓝。QImage 的第四个参数叫 bytesPerLine必须等于通道数乘宽度也就是 3 * width。如果只写 width图像会按错误的步长解析出现斜切或颜色错乱。第二种显示方式是把 OpenCV 的 Mat 直接转成 QPixmap但转换前还是绕不开 BGR 到 RGB。最省心的是先转 RGB 再构造 QImage再交给 QLabel.setPixmap(QPixmap.fromImage(qt_image))。绘制检测框时可以使用 OpenCV 的 rectangle 和 putText颜色用 (0, 255, 0) 绿色线宽 2。如果要显示人流方向和计数可以在画面顶部分别画一上一下两个统计数字。文字建议用 FONT_HERSHEY_SIMPLEX字体大小 1.0颜色用白底红字或黑底白字不然在复杂背景里看不清。这里补一个血泪经验不要把计数逻辑写在 PyQt5 的槽函数里槽函数只负责把收到的 results 数据取出来显示。逻辑全放 Worker 侧界面侧只做 copy、转换和 setPixmap否则槽函数里一旦跑重活界面又会卡。4. 避坑与常见问题从 PyQt5 安装到检测不到人的 5 个典型翻车点我在帮人排查这类项目时出现频率最高的问题其实和模型关系不大大多集中在环境和线程上。下面 5 条基本覆盖了“装不上、黑屏、花屏、卡死、计数乱跳”这几个症状。每一条我都按“现象 - 原因 - 解决”的顺序写方便你直接对着排查。4.1 PyQt5 装不上或装错版本先看 Python 版本和 pip 源现象pip install PyQt5 报错或者装完 import 失败提示找不到 QtCore。原因PyQt5 的某些版本对 Python 3.11/3.12 支持不好或者是 pip 源下载速度太慢导致超时。解决建议用 Python 3.8 到 3.10 搭配 PyQt5 5.15.9 或 5.15.10这是最常见的稳定组合。安装命令用pip install PyQt55.15.9 -i https://pypi.tuna.tsinghua.edu.cn/simple装完一定要跑一下python -c from PyQt5.QtCore import QThread; print(ok)确认 import 没问题再做界面开发。这里还容易踩一个坑机器上同时装了 PyQt5 和 PySide6两个库的 Qt 插件会冲突报 “could not find or load the Qt platform plugin”。建议只保留一个绑定库卸载另一个。4.2 摄像头画面黑屏或界面卡死OpenCV 与 Qt 事件循环冲突现象程序启动后窗口出来了但画面区黑屏或者拖动窗口时界面无响应。原因摄像头读取加模型推理都在主线程VideoCapture.read() 会阻塞等待下一帧Qt 事件循环无法刷新。解决按 2.2 那样把读帧和推理放进 QThread主线程只接收信号。另外注意 VideoCapture 的摄像头索引有的笔记本把摄像头识别成 1不是 0建议枚举一下可用索引。还有一个容易忽略的点如果代码里还调用了 cv2.waitKey()它会抢 Qt 的事件循环画面一样会假死。OpenCV 的 waitKey 在 PyQt5 程序里基本用不到删掉即可。4.3 画面颜色偏蓝或偏绿RGB 和 BGR 通道顺序现象画面颜色明显不对人的脸发蓝环境色偏青。原因OpenCV 读出来的帧是 BGRQt 的 QImage 默认按 RGB 解释通道错位。解决QImage 构造前先 cv2.cvtColor 成 RGB或者 QImage 构造时用 Format_BGR888但要注意 Qt 版本支持情况。我一般用转换后再构造兼容性最好。另一个相关问题是 QImage 的数据生命周期如果 rgb 变量在函数结束后被回收QImage 持有的是悬空指针画面会闪或者直接崩溃。解决方法是把 rgb 存成成员变量或者在 QImage 上调用 copy() 持有像素数据。4.4 GPU 显存不足或推理很慢模型尺寸、半精度和设备选择现象第一次推理报 CUDA out of memory或者在 CPU 笔记本上只有两三帧。原因模型输入分辨率太大默认 batch 可能大于 1且没开 half。解决predict 时把 imgsz 降到 640甚至 512确认 device 是 0 且 halfTrue如果是 CPU 设备换回 imgsz416 并把 conf 调高到 0.6减少后处理负担。不要同时开多个模型实例PyQt5 界面里只保留一个 YOLO 对象。还有一个容易被忽略的点torch 的 CUDA 版本和显卡驱动不匹配时torch.cuda.is_available() 返回 False代码会悄悄走 CPU 路径。先跑一句 python -c import torch; print(torch.cuda.is_available()) 验证环境再谈性能。4.5 计数跳变或重复计数没有跟踪器或跨线判定太敏感现象一个人过线被计了三次或者计数一会儿多一会儿少。原因用了每帧检测数当累计值或者跨线判定用了重心和单帧判断。解决先用 track() 拿到 ID再说计不计数。跨线判定加连续几帧的确认或用脚底点代替重心。修计数逻辑时先拿一段固定视频反复跑不要一边调摄像头一边改代码。如果 ID 还是频繁跳变回到 bytetrack.yaml 调大 track_buffer同时确认 conf 没有高到让行人在中途丢失。5. 验证与验收用 mAP、FPS 和计数误差证明“高分”不是吹的界面做完、计数能跑还差最后一步怎么向导师或评委证明结果可靠。我从三个维度做验收这也是答辩时最容易被追问的点。5.1 用验证集跑 mAP确认模型没有过拟合如果是拿别人训练好的 best.pt也需要在验证集上跑一遍指标。常见命令是yolo val modelweights/best.pt datacoco.yaml imgsz640重点看 mAP50 和 mAP50-95。mAP50 高但 mAP50-95 偏低说明模型能框出目标但框不够精确这是正常现象。答辩时说清楚验证集来源和指标含义比报一个数字可信得多。5.2 实测 FPS区分 GPU 和 CPU 的验收标准在 PyQt5 界面上测 FPS 不能只看模型前向时间还要算上解码和显示。做法是统计 100 帧耗时取平均start time.perf_counter() results model.predict(frame, imgsz640, conf0.5, verboseFalse) total time.perf_counter() - start fps frames / totalCPU 上 YOLOv8n 能到 10 FPS 就算能演示GPU 上期望 30 FPS 以上。如果界面显示和推理不同步要说明瓶颈在显示复制还是模型。5.3 计数误差与人工标注对比取 3 段不同时长、不同人数的视频人工数出通过人数再和系统计数对比。记录误差率测试视频人工计数系统计数误差率视频1约1分钟---视频2约3分钟---视频3约5分钟---误差率控制在 5% 以内基本能支撑“系统可用”的结论。如果误差率偏高回到 4.5 检查跟踪 ID 和跨线逻辑。我自己的习惯是先把界面按钮用假数据跑通再接入模型这样 PyQt5 的槽和线程问题不会和模型问题混在一起。有次我为了省事直接在 UI 线程里调 predict拖动窗口时直接假死被当成事故记到现在。先跑通 10 帧再上摄像头能省掉一半排查时间。希望帮到你。本文还有配套的精品资源点击获取