基于YOLOv5+SlowFast+PyQt5的人体动作识别系统实战

发布时间:2026/9/13 19:55:02
基于YOLOv5+SlowFast+PyQt5的人体动作识别系统实战 简介基于YOLOv5与SlowFast双模型融合搭配PyQt5构建的人体动作识别完整项目面向高校毕业设计、课程设计与期末大作业场景。项目从目标检测、姿态估计到动作分类形成完整技术链路代码含详细注释并附使用文档说明即使新手也能按文档完成环境配置与本地部署快速还原可运行系统。压缩包共150个文件涵盖55个Python源码、PyQt界面文件.ui及图标素材另含演示视频mp4、预训练权重.pt与配置文件整体约153.4MB目录结构清晰便于按模块学习与二次开发。目前已有326人学习参考项目经过严格调试可稳定运行功能完善、界面美观管理便捷适合作为高评分毕业设计或课程设计作品直接使用。1. 从“能检测”到“能理解”动作识别项目到底难在哪做过目标检测的人都有一个体会YOLO 系列把“画面里有什么、在哪”这件事做得越来越顺手但当你把模型部署到实际场景发现只回答“有什么”远远不够。比如监控画面里检测到一个人你还需要知道他是在走路、挥手、还是摔倒在安防、健身、康复训练这类场景里“动作是什么”往往比“人是谁”更有业务价值。这正是“基于YOLOv5 SlowFast PyQt5的人体动作识别项目”要解决的核心问题先用 YOLOv5 完成目标级别的空间定位再用 SlowFast 完成视频级别的时间建模最后用 PyQt5 把整个流程封装成可操作的桌面应用。它本质上是一个“检测 行为理解 交互界面”的三层架构适合用来做毕业设计也适合作为进入视频理解方向的过渡项目。这个项目最容易被低估的地方在于它不是把两个模型串起来就完事。YOLOv5 和 SlowFast 的输入粒度、帧率、类别体系完全不同中间需要一整套数据对齐和推理调度逻辑。PyQt5 要处理的也不只是显示一张图而是视频流、线程、信号槽和模型推理的并发协作。这篇文章按“理论 → 实现 → 实战 → 排错 → 进阶”的顺序把从环境搭建到界面运行的完整路径讲清楚。面向的读者是已经跑通过 YOLOv5 检测、但对视频理解还不熟悉的开发者或者正在选毕业设计题目的在校生。文章里所有命令和代码都基于可复现的常见做法不依赖任何私有数据集。2. 技术选型与核心原理为什么是 YOLOv5 SlowFast PyQt5 的组合2.1 SlowFast 的动作识别原理双分支时间建模SlowFast 是 FAIR 在 2019 年提出的视频动作识别网络核心思想是模拟人类视网膜中的 P 细胞和 M 细胞分工一个分支处理慢速、高分辨率的空间语义另一个分支处理快速、低分辨率的运动变化。慢路径Slow Pathway以较低的帧率采样但通道数更多负责捕捉静态外观和场景信息快路径Fast Pathway以较高的帧率采样但通道数更少负责捕捉运动模式。快路径通过侧向连接lateral connections把运动信息融合进慢路径最终得到一个同时包含“是什么”和“在干什么”的统一表示。在推理阶段SlowFast 接受的是一个视频片段通常是一个 32 帧或 64 帧的 clip而不是单张图片。这意味着你不能把一个摄像头画面直接丢给它而要先做帧序列的采样、缩放、归一化。帧率的选择直接影响计算量和准确率帧率太低快速动作比如挥手会被糊掉帧率太高Fast 路径的计算开销直线上升。常见设置在alpha8时取 32 帧输入即每 8 帧取 1 帧相当于覆盖约 2 秒的视频时长。这个参数在后面实现里会反复用到。2.2 为什么 YOLOv5 只承担定位职责不做分类YOLOv5 是单阶段目标检测器对单张图片做前向推理通常只需要几十毫秒取决于输入分辨率和硬件。但它在动作识别上有一个天然短板没有时间维度。一个人的“举手”和“放下手”在单帧里可能长得一样只有放到时间序列里才能区分。所以在这个项目里YOLOv5 的职责范围是从视频帧中找出所有人物目标输出边界框坐标裁剪出人物区域然后把裁剪后的帧序列交付给 SlowFast。这个分工既减小了 SlowFast 的输入干扰也降低了显存占用——你不需要把整张高清图输入 SlowFast只输入人物区域的序列即可。有人会问能不能直接用 SlowFast 端到端识别省掉 YOLOv5可以但不推荐。SlowFast 的输入是固定尺寸的 clip如果视频里有多个目标整图输入会导致小目标信息被压缩而且 SlowFast 的类别预测是“整个 clip 属于哪个动作”无法区分不同人的动作。YOLOv5 做前置裁剪之后每个人物都是独立的 clip动作归属关系变得清晰。这就是“检测 识别”两级架构在多人场景下更合理的原因。2.3 PyQt5 在项目中的角色不是可选项是交付边界PyQt5 负责把模型推理打包成普通用户能操作的程序。毕业设计评分和实际项目交付都有一个共同点不只看算法指标还要看能否形成闭环。一个只能在终端里输出数字的项目远不如一个能选视频、点按钮、看检测框和动作标签的桌面程序有说服力。PyQt5 通过信号槽机制把 UI 事件和推理流程解耦点击按钮触发QThread启动推理推理过程中的每一帧结果通过信号发回主界面刷新画面这样界面不会因为模型推理而卡死。这个设计模式在后面的实现章节会详细展开。三个技术点的关系可以这样理解YOLOv5 是“眼睛”SlowFast 是“大脑”PyQt5 是“四肢和嘴巴”。没有眼睛大脑看不到画面没有大脑眼睛看到的东西没有意义没有四肢和嘴巴整个系统无法被人使用。3. 环境搭建与最小实现从依赖安装到跑通第一个推理3.1 依赖清单与版本兼容性规划这个项目的依赖分为三块YOLOv5 相关、SlowFast 相关、PyQt5 相关。最容易出问题的地方是 PyTorch 与 CUDA 的版本匹配以及 torchvision 与 PyTorch 的配套关系。以下是一个经过验证的兼容组合# 创建 Python 3.8 虚拟环境 conda create -n action_recognition python3.8 -y conda activate action_recognition # 安装 PyTorch以 CUDA 11.8 为例 pip install torch2.1.0 torchvision0.16.0 torchaudio2.1.0 --index-url https://download.pytorch.org/whl/cu118 # 安装 YOLOv5 依赖 pip install -r requirements.txt # 在 yolov5 目录下执行 # 安装 PyQt5 pip install pyqt55.15.9 # 安装其他工具库 pip install opencv-python av einops fvcore参数说明torch2.1.0对应torchvision0.16.0这个组合在 CUDA 11.8 和 Python 3.8 下稳定性较好。pyqt55.15.9是目前 Windows 和 Linux 下兼容性最好的版本之一如果遇到Qt platform plugin xcb报错通常是缺少libxcb-cursor0或 PyQt5 版本过高导致。av是 PyAV 库SlowFast 的官方数据加载器依赖它来解码视频。提示如果只有 CPU 环境把 PyTorch 安装命令换成pip install torch2.1.0 torchvision0.16.0 torchaudio2.1.0 --index-url https://download.pytorch.org/whl/cpu但推理速度会慢 5 倍以上建议至少准备一张 4GB 显存的 GPU。3.2 准备 YOLOv5 模型与推理逻辑封装YOLOv5 的官方仓库提供了detect.py可以直接运行但在这个项目里我们需要把它封装成可调用的函数因为后面要在 PyQt5 的线程里调用。一个常见做法是加载官方预训练权重yolov5s.ptCOCO 类别包含人这一类然后写一个返回检测框列表的函数import torch import cv2 # 加载 YOLOv5s 模型只做推理 model torch.hub.load(ultralytics/yolov5, yolov5s, pretrainedTrue) model.conf 0.4 # 置信度阈值 model.iou 0.45 # NMS IoU 阈值 model.classes [0] # 只保留 person 类别 def detect_persons(frame): results model(frame) boxes results.xyxy[0].cpu().numpy() persons [] for box in boxes: x1, y1, x2, y2, conf, cls box if cls 0 and conf model.conf: persons.append((int(x1), int(y1), int(x2), int(y2), float(conf))) return persons这段代码里有几个容易被忽略的细节model.classes [0]会过滤掉非人物类别减少前向计算量results.xyxy[0]返回的是 xyxy 格式的边界框左上角和右下角坐标后面裁剪人物区域时直接用这四个坐标。conf阈值设 0.4 是因为动作识别还需要 SlowFast 二次判断检测阶段可以放宽一些避免漏检。3.3 准备 SlowFast 模型与预测函数SlowFast 没有像 YOLOv5 那样开箱即用的torch.hub接口需要从官方仓库下载 checkpoint。常见做法是使用慢路径为R50的SlowFast-R50模型在 Kinetics-400 上预训练。预测函数需要处理单张人物 cropped 序列import torch import torch.nn.functional as F from slowfast.models import build_model from slowfast.config.defaults import get_cfg # 构建配置 cfg get_cfg() cfg.merge_from_file(configs/Kinetics/SLOWFAST_8x8_R50.yaml) cfg.NUM_GPUS 1 cfg.TEST.CHECKPOINT_FILE_PATH checkpoints/SLOWFAST_8x8_R50.pkl model build_model(cfg) checkpoint torch.load(cfg.TEST.CHECKPOINT_FILE_PATH, map_locationcpu) model.load_state_dict(checkpoint[model_state]) model.eval() model.cuda() def predict_action(clip_tensor): # clip_tensor: [1, 3, 32, 224, 224] 或 [1, 3, 64, 224, 224] with torch.no_grad(): preds model(clip_tensor) probs F.softmax(preds, dim1) top1_idx torch.argmax(probs, dim1).item() top1_prob probs[0, top1_idx].item() return top1_idx, top1_probclip_tensor的维度是[batch, channel, frames, height, width]这里 channel 为 3RGBframes 取决于配置8x8 配置对应 32 帧因为 8 倍帧率采样height 和 width 为 224。SlowFast 在内部会把输入拆分成慢路径和快路径两个视图不需要手动预处理。3.4 将 YOLOv5 和 SlowFast 串成完整推理链路这一步是整个项目的衔接点也是最容易写错的地方。直接从视频文件逐帧读取会让 SlowFast 的输入帧乱序或帧数不足。一个可靠的思路是先读取视频段的连续帧用 YOLOv5 在关键帧上做检测然后对每个检测框做时序跟踪关联最后把每个人的帧序列裁剪出来组成 clip。这里提供一个简化但可用的版本假设对每一帧都做检测按位置 IoU 做最简单的关联def build_person_clips(frames, detections, clip_len32): person_clips {} for i, dets in enumerate(detections): for det in dets: x1, y1, x2, y2, conf det # 用中心点作为 key简单跟踪 cx, cy (x1 x2) / 2, (y1 y2) / 2 best_id None best_dist float(inf) for pid, center in person_clips.get(centers, {}).items(): dist (center[0] - cx) ** 2 (center[1] - cy) ** 2 if dist best_dist: best_dist dist best_id pid if best_id is None or best_dist 10000: best_id len(person_clips) # 裁剪人物区域 h, w frames[i].shape[:2] x1c, y1c, x2c, y2c max(0, x1), max(0, y1), min(w, x2), min(h, y2) crop frames[i][y1c:y2c, x1c:x2c] if crop.size 0: continue crop cv2.resize(crop, (224, 224)) person_clips[best_id] person_clips.get(best_id, []) [crop] # 只保留长度达到 clip_len 的片段 valid_clips [] for pid, frames_list in person_clips.items(): if len(frames_list) clip_len: clip torch.stack([torch.from_numpy(f.transpose(2,0,1)) for f in frames_list[:clip_len]]) valid_clips.append(clip) return valid_clips这个实现有一个明显不足中心点距离阈值 10000 是经验值人物快速移动时会跟丢或串号。更稳妥的方案是引入 IoU 匹配或 ByteTrack 这样的跟踪器但这会超出本文范围。对于毕业设计如果场景是固定摄像头下的单人动作中心点跟踪已经足够。4. 用 PyQt5 搭建可视化操作界面线程、信号与实时显示4.1 界面布局与交互流程设计桌面应用的交互流程不要设计得太复杂三个核心操作足够选择视频、开始识别、停止。显示区域分为两块——左侧显示原视频带 YOLOv5 检测框右侧显示当前动作识别结果动作标签和置信度。底部放置一个进度条和日志文本框日志用来输出每步的处理状态比如“正在读取视频帧”“正在检测人物”“识别结果walking 0.87”。class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(人体动作识别系统) self.setMinimumSize(1200, 600) # 左侧视频显示 self.video_label QLabel() self.video_label.setMinimumSize(640, 360) self.video_label.setStyleSheet(background-color: black;) # 右侧识别结果显示 self.result_label QLabel(等待识别...) self.result_label.setAlignment(Qt.AlignCenter) self.result_label.setStyleSheet(font-size: 20px; font-weight: bold;) # 按钮 self.open_btn QPushButton(选择视频) self.start_btn QPushButton(开始识别) self.stop_btn QPushButton(停止) # 日志区 self.log_text QTextEdit() self.log_text.setReadOnly(True)界面设计有几个细节QLabel用来显示视频帧需要定期用setPixmap()更新按钮在推理过程中要禁用“开始”按钮防止重复启动线程日志区的append()方法会自动滚动到末尾适合做实时输出。4.2 用 QThread 把推理移出主线程避免界面假死PyQt5 的界面事件循环不能执行耗时操作否则窗口会变成“未响应”状态。标准做法是创建一个继承自QThread的推理线程把视频处理、YOLOv5 检测、SlowFast 识别的全部逻辑放在run()方法里通过信号把画面帧和识别结果发回主线程。class InferenceThread(QThread): frame_signal pyqtSignal(QImage) result_signal pyqtSignal(str, float) log_signal pyqtSignal(str) finished_signal pyqtSignal() def __init__(self, video_path): super().__init__() self.video_path video_path self._is_running True def stop(self): self._is_running False def run(self): cap cv2.VideoCapture(self.video_path) frames_buffer [] detections_buffer [] while self._is_running: ret, frame cap.read() if not ret: break # YOLOv5 检测 persons detect_persons(frame) frames_buffer.append(frame) detections_buffer.append(persons) # 发帧到界面显示 rgb_image cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb_image.shape bytes_per_line ch * w qt_image QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888) self.frame_signal.emit(qt_image) # 攒够 32 帧后做动作识别 if len(frames_buffer) 32: clips build_person_clips(frames_buffer, detections_buffer) for clip in clips: clip_tensor clip.float().div(255.0).sub(0.45).div(0.225).unsqueeze(0) action_idx, prob predict_action(clip_tensor) action_name idx_to_action(action_idx) self.result_signal.emit(action_name, prob) frames_buffer.clear() detections_buffer.clear() cap.release() self.finished_signal.emit()这段代码里有一个关键点clip.float().div(255.0)是归一化操作sub(0.45).div(0.225)是 ImageNet 的均值和方差必须和 SlowFast 训练时的预处理一致否则识别精度会明显下降。idx_to_action需要从 Kinetics-400 的标签文件kinetics400_label.txt中加载映射表。frame_signal.emit发出的QImage在界面端直接setPixmap即可但要注意QImage的数据必须保持有效因此不要在原frame上做后续修改。4.3 信号槽连接与界面刷新逻辑主窗口里写好信号连接后整个应用的运行方式就完整了self.open_btn.clicked.connect(self.open_video) self.start_btn.clicked.connect(self.start_recognition) self.stop_btn.clicked.connect(self.stop_recognition) def start_recognition(self): if not hasattr(self, video_path): self.log_text.append(请先选择视频文件) return self.thread InferenceThread(self.video_path) self.thread.frame_signal.connect(self.update_frame) self.thread.result_signal.connect(self.update_result) self.thread.log_signal.connect(self.log_text.append) self.thread.finished_signal.connect(self.on_finished) self.thread.start() self.start_btn.setEnabled(False) self.stop_btn.setEnabled(True) def update_frame(self, qt_image): self.video_label.setPixmap(QPixmap.fromImage(qt_image).scaled( self.video_label.size(), Qt.KeepAspectRatio)) def update_result(self, action_name, prob): self.result_label.setText(f{action_name}\n{prob:.2f})update_frame中的scaled()可以防止视频尺寸和界面尺寸不一致时出现变形KeepAspectRatio参数保证画面比例不变但会留出黑边这是视觉上可以接受的取舍。4.4 界面显示额外信息检测框、帧率和置信度如果只想显示原始视频界面的信息量不够。一个更好的做法是先在 OpenCV 里画好检测框再转成 QImagefor person in persons: x1, y1, x2, y2, conf person cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, fperson {conf:.2f}, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) # 在左上角显示当前帧率 cv2.putText(frame, fFPS: {fps:.1f}, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255, 255, 0), 2)YOLOv5 的检测框应该在emit之前就绘制到图像上这样界面端收到的图像自带框线不需要再做一次坐标映射。帧率的计算方式是用1 / (当前帧处理时间 - 上一帧处理时间)这个指标在判断系统是否实时运行时很直观。5. 训练自己的动作数据集与模型优化参数调整和踩坑记录5.1 数据采集与预处理从视频到可训练的多模态数据公开的 Kinetics-400 数据集有 400 个动作类别但如果没有现成的预训练权重或者需要识别自定义动作比如“举重”“踢腿”“摔倒”就要自己准备数据。一个可行的数据组织方式是每个动作一个文件夹里面放多个短视频时长建议 2 到 4 秒分辨率无所谓但帧率要统一。然后你用 YOLOv5 检测出每个人物并裁剪成序列存成 JPEG 序列或者打包成.mp4片段SlowFast 的训练数据管线会自动读取。预处理环节有三个容易被忽略的点。第一是类别不平衡动作 A 有 500 个视频、动作 B 只有 30 个时训练会严重偏向 A。缓解办法是采样时对少数类做重复采样或者对少数类视频做水平翻转等数据增强。第二是时间对齐所有 clip 必须统一帧数不够的做循环填充多余的均匀丢弃。第三是空间归一化SlowFast 采用中心裁剪和短边缩放先把人物裁剪区域 resize 到短边为 256再中心裁剪 224这样训练和推理时不会因人物大小差异导致性能下降。5.2 YOLOv5 的自定义训练参数与超参数调节如果动作数据集中的人物场景比较特殊比如俯视视角的监控画面YOLOv5 预训练模型可能漏检率高。这时需要在自定义数据集上微调 YOLOv5。常见截止方式是把数据集组织为images和labels两个目录使用 LabelImg 标注后训练python train.py --img 640 --batch 16 --epochs 100 \ --data custom_dataset.yaml --weights yolov5s.pt \ --hyp hyp.scratch-low.yaml --cache参数说明--img 640是输入图片尺寸太小会丢失小目标信息太大会增加显存开销--batch 16在 8GB 显存下比较安全如果显存不够就降到 8--hyp hyp.scratch-low.yaml是耗时最少的超参数配置训练动作识别场景的人物检测时不需要过强的数据增强因为人物目标本身尺度变化不大。训练完成后用export.py导出为 TorchScript 格式推理速度可以提升 20% 左右python export.py --weights runs/train/exp/weights/best.pt --include torchscript导出后推理代码要修改加载方式model torch.jit.load(best.torchscript) model.eval() results model(frame)5.3 SlowFast 微调的注意事项与硬件限制在自定义数据集上微调 SlowFast 是整个项目里最吃显存的部分。SLOWFAST_8x8_R50这个配置在 8 帧采样下输入 clip 的等效时间长度是 8 帧 × 8 64 个原始帧一个 batch 大小为 8 时大约需要 16GB 显存。如果只有 8GB 显存有两种降级方案。方案一是减小DATA.TRAIN_CROP_SIZE从 224 到 182或者设置DATA.NUM_FRAMES 32让 clip 变短。方案二是开启混合精度训练SOLVER.FP16.ENABLED: True这会省下一半左右显存代价是精度略微下降。微调时的另一个关键参数是学习率预训练模型在 Kinetics-400 上已经收敛自定义数据集通常是同类动作的子集所以学习率不要设置得太高一般从0.000125开始使用余弦退火策略衰减即可。下面是常见配置项的说明参数名推荐值作用与注意事项DATA.NUM_FRAMES32单个 clip 的帧数影响时间感受野DATA.SAMPLING_RATE8慢路径采样间隔8 表示每 8 帧取 1 帧BN.NUM_SYNC_DEVICES1多卡同步 BN 会影响精度单卡设为 1SOLVER.BASE_LR0.000125微调时不宜过大否则破坏预训练特征TEST.ENABLETrue训练时开启验证便于观察每个 epoch 的指标DATA.TEST_CROP_SIZE224测试时中心裁剪尺寸需与训练一致5.4 推理加速与模型轻量化TensorRT 和帧级缓存训练完成后回到推理阶段你可能会发现端到端的处理速度只有 5 到 10 FPS这在交互式应用里体验很差。第一个可以做的优化是引入帧级缓存YOLOv5 不是每一帧都要做可以让检测的间隔为 3 帧中间 2 帧复用上一次的检测框。SlowFast 也不需要对每个滑动窗口都推理可以每 8 帧做一次动作识别然后把中间帧的动作标签沿用上一次结果。下面是间隔采样对速度的影响frame_step 3 # 每隔 3 帧做一次 YOLOv5 检测 action_step 8 # 每隔 8 帧做一次 SlowFast 识别 while cap.isOpened(): ret, frame cap.read() if not ret: break frame_count 1 # 每隔 frame_step 帧检测一次 if frame_count % frame_step 0: persons detect_persons(frame) last_persons persons # 每隔 action_step 帧识别一次 if frame_count % action_step 0 and len(clip_buffer) 32: clip_tensor torch.stack(clip_buffer[-32:]).unsqueeze(0) action_name, prob predict_action(clip_tensor)第二个优化是使用 TensorRT 加速 YOLOv5 的推理。NVIDIA Jetson 平台上能用--include engine导出引擎文件x86 平台上需要先安装tensorrtPython 包然后用torch2trt或者TensorRT的 Python API 做离线转换。由于 TensorRT 版本与显卡驱动强绑定如果你的环境不能保证版本一致建议用 ONNX Runtime 替代虽然加速效果不如 TensorRT 明显但兼容性更强。提示如果你在训练 SlowFast 时遇到RuntimeError: Sizes of tensors must match这样的张量维度不匹配报错首先要检查DATA.NUM_FRAMES和DATA.SAMPLING_RATE的乘积是否与预训练权重要求的输入帧数一致。8x8 配置必须是 64 帧原始输入32x2 配置必须是 64 帧这个乘积是固定搭配。6. 从 demo 到可演示系统顺手就能用的进阶技巧到这一步你的项目已经能运行了PyQt5 界面、视频选择、人物检测、动作标签都能工作。但从“能跑”到“能在答辩现场稳定演示”之间还有几个值得打磨的细节。首先是界面里加一个置信度阈值滑块。这个滑块实时调整 YOLOv5 的conf参数让演示者根据场景动态调节检测的严格程度。默认 0.4 在光线充足的室内够用但在暗光环境下目标置信度普遍下降可以滑到 0.3。这在 PyQt5 里就是加一个QSlider把valueChanged信号连接到修改模型置信度的函数。其次是视频循环播放。演示时视频播完就停住现场气氛会比较尴尬。在run()方法里检测到cap.read()返回 False 时用cap.set(cv2.CAP_PROP_POS_FRAMES, 0)回到第一帧继续播放同时加一个循环次数上限防止死循环。再有一个实用的小技巧把识别结果落盘成 CSV 文件。每次识别完一个视频把时间戳、动作标签、置信度、人物 ID 写入文件。这个功能在毕业设计里算“数据可视化”的加分项在真实项目里更是刚需。你可以在InferenceThread.run()结束时把self.results_history列表写入文件self.results_history.append({ time: frame_count / 30.0, action: action_name, probability: prob, person_id: pid }) # 识别结束后 import csv with open(results.csv, w, newline) as f: writer csv.DictWriter(f, fieldnames[time, action, probability, person_id]) writer.writeheader() writer.writerows(self.results_history)最后是两个容易让界面卡死的大坑。第一个是predict_action在 GPU 上推理时如果显存不足PyQt5 界面不会报错但整个程序会直接退出所以要在run()方法开头加一个torch.cuda.empty_cache()释放上一轮推理的缓存。第二个是 PyQt5 关闭窗口时后台 QThread 还在跑程序会显示“正在终止”但卡住。处理方式是在窗口关闭事件里先调用线程的stop()再wait()等待线程完全退出def closeEvent(self, event): if hasattr(self, thread) and self.thread.isRunning(): self.thread.stop() self.thread.wait(2000) event.accept()到这里基于 YOLOv5 检测、SlowFast 动作分类、PyQt5 界面展示的三层系统已经可以完整演示。下一步想深挖的话优先把人物跟踪从中心点换成 ByteTrack再去研究 SlowFast 的 Attention 分支结构这些都是这个项目自然的延伸方向。本文还有配套的精品资源点击获取