PyQt5+深度学习:课堂学生专注度分析系统实战解析

发布时间:2026/10/5 3:32:17

PyQt5+深度学习:课堂学生专注度分析系统实战解析 简介这套智慧课堂项目资源以PyQt5和深度学习为核心面向线下课堂的学生专注度分析场景适合计算机、人工智能、数据科学等专业的学生用于毕业设计、课程设计或初期项目演示。压缩包共218个文件主要包含Python源码与编译文件、Qt界面文件、模型资源、图像素材以及xml/txt/csv配置数据整体大小约17.02MB其中py文件承载主程序与算法逻辑ui文件对应界面布局jpg/png/ico等用于界面和展示素材各类配置数据则便于理解输入输出与参数设置结构清晰。内容涵盖可运行的工程代码、界面设计、模型文件与设计文档能体现从数据读取、界面交互到模型推理部署的完整链路。已有151人学习下载代码经验证可稳定运行适合有一定Python和深度学习基础、希望快速搭建或二次开发智慧课堂相关项目的学习者使用。1. 专注度分析的真问题不是识别脸而是识别“没在听”摄像头拉一个教室的全景画面用深度学习模型把每个人都框出来这件事做到今天已经不难了。真正麻烦的是一个学生端端正正坐在那儿眼睛也睁着既不玩手机也不趴桌但他可能早就神游天外了。你要做的这套基于 PyQt5 深度学习的线下课堂学生专注度分析系统难点从来不在“检测到人”而在“判断这个人有没有在接收信息”。我当时第一次拿到类似需求时以为把迁移学习模型训练好就够了结果发现真正决定项目能用不能用的是后面那套数据链路和桌面端交互PyQt5 负责摄像头预览、结果展示和最终报告导出深度学习模型负责把人脸框出来、把头部姿态算出来再换算成一个 0 到 1 的专注度分数。适合什么人去看这篇文章学校老师想做课堂录播的辅助分析或者学生在做毕业设计、课设时拿 PyQt5 写一个能现场演示的桌面系统。后者往往更关心“怎么把模型接进界面还不卡死”前者则关心“导出的一节课报告靠不靠谱”。下面按我自己的落地顺序来讲先把系统拆开再把模型链路跑通然后接 PyQt5 界面最后补上那些不跑一遍根本发现不了的坑。2. 系统拆解与选型PyQt5 在课堂专注度分析里到底负责哪一块2.1 数据流全景摄像头、模型、界面之间的一帧要经过哪几站先别急着写界面把整条数据链路在纸上画出来。一套最常见的线下课堂专注度分析系统一帧画面从摄像头到界面大致要经过六站视频源USB 摄像头、RTSP 网络摄像头或者一段课堂录播 MP4。取帧OpenCV 的 VideoCapture 读出一张 BGR 格式的 numpy 数组。人脸检测对整帧做一次目标检测输出每个人脸的矩形框和置信度。头部姿态估计截取每个人脸区域送入另一个模型输出 yaw、pitch、roll 三个角度。专注度计算结合头姿角度、眼睑开合程度和时间窗口算出 0 到 1 的注意力评分。界面渲染PyQt5 主窗口把原图画上检测框同时更新右侧列表和底部曲线。这套链路里深度学习模型不是只有一个人脸检测就完了。很多刚接触的人以为用一个“专注度分类模型”就能端到端判断实际上公开数据集里几乎没有带课堂注意力标注的够大数据你很难训练出一个能直接用的分类网络。更稳妥的做法是拆成“人脸检测 头部姿态估计 启发式规则”因为这几个环节各自的预训练模型都比较成熟规则部分又能根据现场情况随时调。做好功课之后目录结构必须是模块化的否则你换一个检测模型界面代码就要跟着大改。文件夹拆分是后面几分钟的事。我们从需求上可以先明确摄像头采集只管拿帧推理模块只管出结果PyQt5 只管显示三者之间用类或脚本隔离。后续接报告导出、接新的模型都不至于推倒重来。2.2 为什么选 PyQt5桌面端实时视频场景下的 GUI 选型理由你可能会问现在 Web 端技术这么方便为什么偏要选 PyQt5 做桌面端我自己做过对比核心原因是实时视频这类场景下桌面端省掉了浏览器和服务器之间的编解码传输环节延迟能低一大截。摄像头画面在 PyQt5 里就是内存中的 numpy 数组画上检测框后直接交给 QLabel 显示没有 WebRTC 那套信令和推流链路。如果做成 Web 页面要么浏览器端用自己的 getUserMedia要么把视频流推到服务端再转发回来中间的延迟和掉帧在多人教室场景下很难受。PyQt5 在桌面端还有一个优势就是线程模型和信号槽机制非常适合“一边采集、一边推理、一边显示”这种多任务结构。视频采集线程、模型推理线程、UI 主线程三者可以互不阻塞推理模型在 CPU 上跑 200 毫秒也不会让界面卡住。按钮点击、摄像头开关、导出报告这些事件都通过信号槽分发代码结构清楚。PyQt5 的 QChart、QTableWidget 这类控件也能直接拿来画专注度曲线和展示学生列表不需要再引一套前端图表库。缺点也不是没有。PyQt5 的界面样式偏原生想做得漂亮得靠 QSS 慢慢调打包成 exe 体积也比较大一个带 torch 的推理环境经常 200MB 以上。但考虑到这类专注度分析工具通常是教师机离线使用不是公网产品PyQt5 是投入产出比最高的选择。至少我见过的课程设计、实验室项目大部分都是这条路。2.3 源码、设计文档、模型在项目目录里怎么放决定了后续调试效率标题里那个压缩包叫“python源码设计文档模型下载”它其实暗示了一个好习惯源码、模型权重、文档必须分层放不能把 .pt 或 .onnx 堆在代码根目录。一个我常用的稳定目录结构是这样classroom-attention/ ├── src/ │ ├── main.py # PyQt5 程序入口 │ ├── camera_worker.py # 摄像头采集线程 │ ├── inference_worker.py # 深度学习推理线程 │ ├── attention.py # 专注度评分规则 │ └── ui/ │ ├── main_window.py # 主窗口布局与信号槽 │ └── widgets.py # 自定义控件 ├── models/ │ ├── face_det.onnx # 人脸检测模型 │ └── head_pose.onnx # 头部姿态模型 ├── data/ │ └── classroom_demo.mp4 # 测试用课堂视频 ├── docs/ │ └── design.md # 设计文档 └── requirements.txt这样拆分有几个具体好处。第一camera_worker.py 和 inference_worker.py 互相不认识摄像头线程只需要把帧发出去推理线程拿到帧出结果谁崩了都不影响另一个。第二models 目录单独放权重文件是因为模型文件动辄几十到几百 MB不该提交进代码版本管理也不该在代码里写死绝对路径。第三设计文档单独放 docs里面记录数据流、模型输入输出尺寸、评分阈值设定这不是给老师看的应付材料而是几个月后你自己回来改代码时的救命笔记。requirements.txt 里一定要锁版本尤其是 opencv-python、PyQt5、numpy 这三件套。OpenCV 不同版本之间的 dnn 接口基本稳定但 PyQt5 从 5.15 之后不再有新特性numpy 版本太新可能和 PyQt5 的数组转换有兼容问题。我吃过这个亏之后每次新建环境都会先固定这三个版本再往上加。3. 专注度模型先离线验证再进界面别把推理链路放最后调3.1 专注度建模头部姿态、眼睑状态、时间窗口比端到端分类更稳给“专注度”下定义是这套系统里最容易被轻视的环节。直接把模型输出一个“专注/不专注”二分类现场场景根本不买账学生只是低头写字模型就给判成“走神”这种误报会让系统毫无可信度。所以我更倾向把专注度拆成三个可测量的维度再按权重合成一个分数。第一维度是头部姿态。正常听课看黑板时头基本朝前yaw 左右偏转和 pitch 上下俯仰都在一个小范围里低头玩手机时 pitch 会突然变大扭头看同桌时 yaw 会明显偏移。这个维度用头部姿态估计模型直接拿角度就好不需要额外标注数据。第二维度是眼睑开合程度。用面部关键点算出 EAR 值眼睛睁开程度变化能判断疲劳状态。课堂后半段瞌睡场景下头没怎么动但眼睛闭上的时间越来越长单靠头姿抓不到。第三维度是时间稳定度。单帧角度抖动得很厉害一个学生只是弯腰捡笔pitch 瞬间变大但只持续半秒这时直接判“不专注”太冤。常见做法是取一个 2 到 3 秒的滑动窗口窗口内持续低头或持续闭眼才标记成异常状态。合成时我给了一个简洁的加权公式具体权重需要你用测试视频去标定attention_score 0.4 * head_forward 0.3 * eyes_open 0.3 * stabilityhead_forward 是头部朝向接近正前方的程度eyes_open 是这一帧的 EAR 归一化值stability 是最近 N 帧得分波动的反向指标。注意这个权重不是拍脑袋定的我会先用几段不同场景的视频跑一遍看误报集中在哪个维度再回头调权重。这套方案最大的优点是每个维度都能单独解释老师看报告时知道某个学生分数低是因为“低头 8 秒”而不是模型玄学。3.2 模型选型人脸检测与头部姿态估计各用什么输入输出如何对齐模型选型要分两个模型讲。人脸检测我见过几种方案OpenCV 自带的 res10 SSD 检测器MTCNNRetinaFace还有 YOLO 系列的人脸变体。单人近景时 res10 SSD 就够用CPU 上大概 20 到 30 毫秒一帧部署零成本教室全景多人时我会优先考虑 MTCNN 或 YOLOv8 的 face 模型因为 res10 在密集小目标上容易漏检。压缩包里的“模型下载”大概率就是这类人脸检测权重加一个头部姿态权重拿到手先别急着跑用 Netron 打开看一眼输入尺寸和输出维度这一步能省后面半天排查时间。头部姿态估计的选择更多。传统做法是用 68 点或者 106 点关键点检测器再用 solvePnP 把 2D 关键点和 3D 人脸模型对应起来解出三个旋转角缺点是代码量大、OpenCV 的坐标系要小心。更省事的方案是找一个直接输出 yaw/pitch/roll 的 ONNX 模型比如 SixDRepNet、PFLD 这类输入一张对齐后的人脸图输出就是三个角度。我用 ONNX 的理由也很简单OpenCV 的 dnn 模块直接能加载不需要额外装深度学习框架PyQt5 打包时也能少带一大坨依赖。输入输出对齐是最容易翻车的点。人脸检测输出的是原图坐标框头部姿态模型要的是裁剪后的人脸图尺寸和归一化方式必须和模型训练时一致。我见过一个人头姿态模型要求输入 224x224 的 RGB 图mean128结果接代码时用了 SSResNet 的归一化输出角度全部偏了十几度整个专注度评分全乱。项目目录里的设计文档这部分一定要单独写清楚模型输入尺寸、通道顺序、归一化参数、输出角度单位是弧度还是角度。3.3 先离线验证模型用一段测试视频跑通前向推理链路带上模型后第一步不是接界面而是写一个纯控制台脚本用录好的课堂视频把“取帧 → 人脸检测 → 姿态估计 → 角度输出”跑通。这一步没有界面没有信号槽问题定位会快很多。import cv2 import numpy as np # 人脸检测OpenCV DNN 加载 res10 SSD face_net cv2.dnn.readNetFromCaffe( models/deploy.prototxt, models/res10_300x300_ssd_iter_140000.caffemodel ) # 头部姿态模型ONNX输出 yaw/pitch/roll这里用 Netron 确认过输入为 224x224 RGB pose_net cv2.dnn.readNetFromONNX(models/head_pose.onnx) cap cv2.VideoCapture(data/classroom_demo.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break h, w frame.shape[:2] blob cv2.dnn.blobFromImage( frame, 1.0, (300, 300), (104.0, 177.0, 123.0), swapRBFalse ) face_net.setInput(blob) dets face_net.forward() for i in range(dets.shape[2]): conf dets[0, 0, i, 2] if conf 0.5: continue x1 int(dets[0, 0, i, 3] * w) y1 int(dets[0, 0, i, 4] * h) x2 int(dets[0, 0, i, 5] * w) y2 int(dets[0, 0, i, 6] * h) face frame[y1:y2, x1:x2] if face.size 0: continue # 按姿态模型的输入要求做缩放和归一化mean 要和训练时一致 face_blob cv2.dnn.blobFromImage( face, 1.0 / 255.0, (224, 224), mean(128, 128, 128), swapRBTrue ) pose_net.setInput(face_blob) angles pose_net.forward().flatten() # [yaw, pitch, roll]单位弧度 yaw, pitch, roll np.degrees(angles) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText( frame, fyaw{yaw:.1f} pitch{pitch:.1f}, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2 ) cv2.imshow(offline test, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()代码逻辑不复杂但参数值得盯着看。第一段 blobFromImage 里的 300x300 是 res10 SSD 输入尺寸后面的 (104, 177, 123) 是它训练时用的均值不能随手删置信度阈值 0.5 对单人近景够用教室全景遮挡多时降到 0.3 能少漏人但同时会增加误检要现场调。第二段 blobFromImage 里 224x224 和 mean128 必须和 head_pose.onnx 模型匹配swapRBTrue 是因为模型训练用的是 RGBOpenCV 读进来是 BGR。输出角度单位是弧度我手动转成角度方便调试。第一次跑的时候别追求完美只看两件事人脸框有没有跟住人角度数值在低头、抬头、转头时有没有明显变化。有些模型的 yaw 正负号定义和你想的相反如果发现低头时 pitch 变小而不是变大就把符号取反或者直接打印连续几十帧观察变化趋势。这段脚本跑通了后面接 PyQt5 就只剩线程和数据传递问题不会和模型问题混在一起。4. PyQt5 核心实现线程、信号槽、界面三板斧4.1 QThread 线程分工摄像头采集和深度学习推理必须分开我见过太多 PyQt5 工程最后死在“界面卡死”上原因基本都是图省事把摄像头读取和模型推理直接写进了按钮点击回调。OpenCV 的 cap.read() 本身就要几十毫秒模型推理在 CPU 上可能几百毫秒这两个操作任何一个放进 GUI 线程整个窗口都会变成“未响应”。正确的做法是用两个 QThreadCameraWorker 负责取帧和推送预览InferenceWorker 负责从帧队列里拿图做推理。UI 线程只做一件事接收信号刷新控件。有人问为什么不让 CameraWorker 和 InferenceWorker 合成一个线程省得传帧。理论上可以但视频画面的实时性和模型的推理速度不匹配合在一起会让视频预览跟着推理速度走——推理一卡画面就卡。分开后即便模型推理只能跑 5 FPS预览画面依然保持 25 FPS使用者体验完全不一样。线程怎么停止也要提前想清楚。QThread 里不能用 terminate那样线程资源来不及释放摄像头句柄可能被占住。我在 CameraWorker 里用一个_running标志位配合wait()安全退出下面是具体的实现import cv2 import numpy as np from PyQt5.QtCore import QThread, pyqtSignal class CameraWorker(QThread): # ndarray 在 Qt 信号里不是一个能直接识别的类型所以用 object frame_ready pyqtSignal(object, int) def __init__(self, src0): super().__init__() self.src src self._running True def run(self): cap cv2.VideoCapture(self.src) if not cap.isOpened(): self.frame_ready.emit(None, -1) return idx 0 while self._running and cap.isOpened(): ok, frame cap.read() if not ok: break self.frame_ready.emit(frame, idx) idx 1 # 限制最大采集帧率避免视频预览吃掉太多 CPU self.msleep(30) cap.release() def stop(self): self._running False self.wait()这段代码里有两个关键点。frame_ready 信号第一个参数声明成 object是因为 numpy 数组不是 Qt 内部类型但通过 object 信号可以完整传递数组引用这是 PyQt5 里跨线程传 OpenCV 帧最常见的方式。第二个是 msleep(30)它把采集频率限制在 33 FPS 左右纯预览场景下不需要更高省下来的 CPU 可以给推理线程用。stop 方法里先置_running False再 wait()确保循环退出、摄像头资源释放完了才返回避免下次启动时设备被上一轮占用。4.2 跨线程传帧信号槽与 OpenCV 画面的配合信号槽是 PyQt5 里线程间通信的核心。CameraWorker 在子线程里 emit frame_ready主窗口如果有一个槽函数连接到这个信号PyQt 会保证跨线程时采用队列连接也就是 emit 后立即返回槽函数在 UI 线程的事件循环里异步执行不会阻塞摄像头线程。这里需要注意的是QImage 和 QPixmap 这些 GUI 对象只能在主线程操作所以我在槽函数里才做 BGR 到 RGB 的转换和 QImage 封装。from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtWidgets import QMainWindow, QLabel class MainWindow(QMainWindow): def __init__(self): super().__init__() self.video_label QLabel() self.setCentralWidget(self.video_label) self.video_label.setScaledContents(True) self.camera CameraWorker(src0) self.camera.frame_ready.connect(self.update_frame) def update_frame(self, frame, idx): if frame is None: self.statusBar().showMessage(摄像头打开失败) return # OpenCV 读进来是 BGRQImage 要 RGB顺序不转颜色会发蓝发红 rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, c rgb.shape # 用 frame 的内存直接构造 QImage避免一次完整拷贝 qimg QImage(rgb.data, w, h, c * w, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(qimg))表面上看只是显示一帧里面有几个约定俗成的规矩。第一QImage 构造函数里的 bytesPerLine 要填c * w不能漏否则图像在宽度不是四的倍数时会出现错位。第二rgb.data指向的数组必须活得比 QImage 久好在 frame 是在槽函数局部变量里rgb 也在这里Qt 绘制 pixmap 时会在这段作用域内完成不会出悬空引用。第三这里是定时刷新而不是等到每次推理完成所以界面即使在模型算得慢时也不会静止。如果想把预览和处理彻底分离你可以在 CameraWorker 里加一个信号专门发原图在 MainWindow 里把收到的帧再转交给 InferenceWorker。这样界面显示的是最新一帧推理线程拿到的可能是略早的帧但没有任何阻塞。处理延时小现场感反而更流畅。4.3 界面布局实时视频、学生列表、专注度曲线的三板斧这一步是 PyQt5 界面设计最常见的落地结构。主窗口里我用 QHBoxLayout 把画面和右侧信息区左右排开左侧视频区占三份宽度右侧信息区占两份。右侧从上到下是两个控件QTableWidget 放每个学生的检测记录和专注度分数QChartView 放整节课的专注度瞬时曲线。底部留一个状态栏显示当前 FPS、检测人数、模型状态这个状态栏在真机上非常有用不然你都不知道是摄像头断了还是模型卡了。我把视频部分做成 QLabel 而不是 QVideoWidget原因是我们的视频流不是直接播放源而是经过 OpenCV 处理、带检测框的每帧图像QLabel 配合 QPixmap 是最直接的方式。setScaledContents(True)可以让画面自适应窗口大小但记得分辨率和窗口比例不一致时画面会拉伸变形我一般会让 QLabel 保持 16:9 固定比例或者 setScaledContents 时把窗口等比缩放。表格列我建议设四列学号或座位号、专注度分数、状态标记、最后更新时间。默认按分数排序让老师一眼看到哪些座位需要留意。曲线部分用 QChart 的 QLineSeries横轴是时间纵轴是全班平均专注度或者单个学生分数。这里不用引 matplotlib虽然 matplotlib 也能嵌入 PyQt5但 QChart 在实时刷新时的效率高很多画布更新不会抢走 UI 线程太多资源。4.4 把推理结果变成界面可读状态缓存与更新机制推理线程的结果信号可以这样定义class InferenceWorker(QThread): result_ready pyqtSignal(int, int, float, float, float, float) # 帧号人数yawpitchroll平均专注度信号里传太多参数容易乱更常见的做法是定义一个小类或字典。我倾向在信号里传一个 list of dict每个学生一条记录UI 层直接遍历更新表格不用去猜信号参数含义。推理线程内部要注意的就是丢帧策略CameraWorker 发帧太快时推理线程的队列会越积越多如果不处理关闭程序时队列里还剩几百帧线程退不干净。我会判断一下队列长度超过两帧就丢旧帧只处理最新帧。更新的交互逻辑也要克制。表格没必要每帧都刷新QTableWidget 刷新太频繁会闪烁我一般把刷新频率压到每秒 2 到 3 次相当于只在推理线程出结果时才更新一次。曲线上的历史数据保存到内存每节 45 分钟课记录 2700 个点就够不用画每帧。界面刷新这层做完你就有了一套能看的 PyQt5 实时分析系统。但真正跑一节课之前下面这五个坑我劝你先排查一遍每一个都是我实际踩过的。5. PyQt5 深度学习集成时避坑5 条高频问题排查记录5.1 摄像头打不开或黑屏到底是谁占用了设备现象程序启动后视频区域一直黑屏状态栏报“摄像头打开失败”但单独写一个cv2.VideoCapture(0)的小脚本却能正常出画面。更气人的是在 PyCharm 里跑没问题打包成 exe 后反而打不开。原因最常见的是摄像头设备被上一个没有释放干净残留的进程占用。PyQt5 程序崩溃过一次后子线程里的 cap 没有 release摄像头就被系统标记为占用。另一类原因是某些笔记本摄像头需要指定 API 后端Windows 下 OpenCV 默认用 MSMF有些设备兼容性差。打包后打不开则大多是因为缺少 OpenCV 的视频后端 dll。解决每次启动前先尝试释放一次cv2.VideoCapture(0).release()然后枚举设备序号从 0 到 5 逐一尝试isOpened()找一个真正能打开的。Windows 下显式指定后端可以试试cv2.VideoCapture(0, cv2.CAP_DSHOW)。代码里加一条“摄像头自检”日志把后端、分辨率、帧率打出来而不是只弹一句“失败”定位会快很多。5.2 界面卡死、转圈耗时操作跑进了 GUI 线程现象点击“开始分析”按钮后整个窗口立刻卡住标题栏出现“未响应”拖动窗口时画面像幻灯片。过了几秒又恢复反复如此。原因这是最典型的错误把cap.read()或者model.forward()直接写在了按钮的 clicked 槽函数里。PyQt5 的 GUI 线程同时负责 Qt 的事件循环和绘图任何一个耗时超过几十毫秒的操作都会让它没法处理鼠标事件操作系统就会判定为未响应。解决耗时操作全部挪到 QThread主线程和子线程之间只走信号。按我前面写的 CameraWorker 和 InferenceWorker 分开跑采集线程里只管取帧和发信号推理线程里只管读模型出结果。如果之前代码已经写成了单线程可以先快速验证在按钮点击处理函数里加一行 print 看打印时间如果打印间隔达到几百毫秒说明这里就有耗时操作。5.3 画面颜色反了BGR 与 RGB 的顺序问题现象摄像头画面里原本是蓝色的窗帘变成了橙色人脸的肤色发青整个画面像是反色滤镜。这个错在第一次接 PyQt5 时很容易犯。原因OpenCV 读出来的图像通道顺序是 BGR而 QImage 的Format_RGB888是按 RGB 顺序解释数据的。直接把 BGR 的 numpy 数组塞给 QImageQt 会把第一个通道当红色最后一个当蓝色颜色全部错位。解决在构造 QImage 之前先cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)。如果你完全不想做转换也可以用QImage.Format_BGR888这个格式 Qt 也有但性能没有明显差别我一般还是按 RGB 显式转语义更清楚。顺手检查一下送入深度学习模型的人脸图是不是也用了正确的 swapRB很多头部姿态模型训练时用的是 RGB但 OpenCV 读出来是 BGR忘记转就会导致姿态角度整体偏移。5.4 模型加载要好几秒首帧延迟的削减方法现象程序启动后窗口能出来但画面黑屏 3 到 5 秒然后突然跳出一帧带框的画面。第一次启动尤其明显之后再开就快一点。原因深度学习模型初始化本身就慢ONNX 模型第一次 forward 前会做算子选择和内存池初始化几百 MB 的权重从磁盘读进内存也要时间。如果还用了杀毒软件实时监控每次加载都会被拦截扫描时间直接翻倍。解决把模型加载放进子线程不要在__init__里同步加载。加载期间主窗口正常显示状态栏显示“模型加载中…”加载完成后再 enable 分析按钮。模型路径尽量用相对路径并处理成os.path.join避免打包后路径对不上。如果模型文件实在太大可以做一个二次加载启动时只加载人脸检测模型先看到画面头部姿态模型等分析按钮按下时再加载把首帧延迟从用户可感知变成基本无感。5.5 教室多人时帧率塌方检测频率与输入分辨率的取舍现象单人近景测试时推理能跑到 12 FPS一到教室全景画面人数超过十个帧率直接跌到 3 FPS曲线图变成了一条断断续续的虚线。原因人数越多人脸检测和姿态估计的耗时越高。教室全景画面大、人脸小检测器要处理的分辨率更高同时每张脸都要再过一遍姿态模型总耗时接近线性增长。CPU 推理在多人场景下几乎撑不住。解决两个层面同时做。第一降低检测频率每 3 帧取一帧做推理中间两帧继续沿用上一次的检测框画面看起来还是连续的但推理负载降到三分之一。第二把检测输入尺寸从 640x640 降到 320x320教室全景下小目标会漏掉一些但 FPS 能回涨一倍。如果还要兼顾多人最有效还是 GPU 推理ONNX Runtime 用 CUDA 加速姿态模型从每个 50ms 降到 5ms这个差距是数量级的。第三个技巧是只对检测框内区域做姿态估计绝不把整张大图喂给姿态模型这能省下大量无效计算。6. 最终验证与交付把一段课堂视频变成可交付的专注度报告6.1 用录播课堂视频做端到端验证先算准再谈快系统写完后先别拿着摄像头对着自己工位测找一段真实课堂录播视频跑一轮离线验证。我是这样操作的把视频按 1 秒 1 帧抽样共抽 100 个时间点人工肉眼判断每个时间点上画面里的学生大致是什么状态再和系统输出的专注度分数做对照。如果 95% 以上能对上这套阈值就能用对不上的集中在哪些场景就调对应的阈值。比如低头写字被误判成走神就把 pitch 的阈值放宽几度或者把持续时间窗口拉长到 3 秒。唯一真报表建议单独测一下老师站在讲台上这个位置头部始终朝前如果系统因为光照、角度问题误判成低头那整个报告就失去意义。验证通过后再测性能看单帧推理耗时、检测人数波动、是否出现漏检记住一个原则稳定跑完一节课比马上多跑几个 FPS 重要得多。6.2 导出课程专注度报告CSV 表格与趋势图教室端的数据分析完之后要输出给老师一个能看的报告。我用的是 CSV 加一张趋势图CSV 方便后续 Excel 统计趋势图直接嵌进 PyQt5 窗口里导出 PNG。CSV 的格式是时间,座位号,专注度,状态 00:01:02,01,0.85,专注 00:01:02,02,0.32,低头 00:01:08,02,0.41,瞌睡写入时要特别注意编码。用 Python 标准库 csv 写中文如果只指定utf-8Excel 打开会乱码要指定utf-8-sig。另外状态列不要存“1”和“0”直接用“专注 / 低头 / 瞌睡”这类可读文本老师拿到报告不需要查代码才知道什么意思。6.3 我自己的交付习惯从我个人经验看这类项目的交付重点不在代码量而在“别人能不能不看设计文档就上手跑起来”。我习惯把 requirements.txt 里的关键依赖版本固定好写一个检查脚本启动时自动检测摄像头和模型文件是否存在缺什么就在状态栏提示什么。还有一次项目验证结束时发现摄像头跑了一个多小时自动断开后来我在 CameraWorker 里加了断线重连逻辑断线后自动重新初始化并续写报告不让前面四十多分钟的数据白费。这些细节做齐系统才算真正可以从课堂录播转化成一份有价值的报告。希望这些经验帮你在自己的 PyQt5 专注度分析项目里少走一些弯路。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/5 3:27:17

Java与C++多态访问成员变量和方法差异全解析

我最近在带新人时出了一道经典测验题:父类引用指向子类对象,Java和C分别访问成员变量和调用成员方法,各自会输出什么?结果答得五花八门,有人把"成员变量也可以被重写"写在注释里,有人分不清C的vi…

2026/10/5 3:27:17

110张熊猫数据集VOC与YOLO格式要点及YOLOv8训练全流程解析

简介:面向目标检测学习与研究的熊猫数据集,同时提供Pascal VOC和YOLO两种标注格式,适配SSD、YOLO系列等常见框架,可直接划分训练验证集使用。包内含110张jpg图片,以及一一对应的110个xml、110个txt标注文件&#xff0c…

2026/10/5 3:27:17

Python双目标优化:混合配电系统规划与可靠性评估实战

干配电网规划的朋友应该都有同一个体会:改动一个数据,重跑一轮规划,可能就是一个通宵。尤其是现在把分布式光伏、风电、储能都塞进配电网之后,系统从原先“单电源、单方向”的被动网络,变成了“多电源、多运行方式、多…

2026/10/5 6:12:23

PX4无人机坐标系转换实战:FRD与FLU的坑及MAVROS目标点发布

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 6:12:23

SpringBoot应用迁移到BES 9.5.5信创中间件完整改造指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 6:12:23

MRAM与PIC18F86K90实战:高可靠SPI存储方案设计与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 6:12:23

DeepSeek本地领域数据注入训练实战:从PDF/Excel/Word到LoRA微调

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 6:12:23

手持激光测距仪硬件设计全解析:从芯片选型到调试实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 6:07:23

InDuDoNet复现指南:双域展开网络低剂量CT重建的PyTorch实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 0:01:02

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/4 0:01:02

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/4 1:01:05

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑