PyQt5与深度学习实战:智慧课堂专注度分析系统构建全解析
简介基于PyQt5与深度学习的线下课堂学生专注度分析系统以完整Python源码、设计文档及预训练模型打包形式呈现重点面向计算机相关专业学生的毕业设计、课程设计及项目立项演示。项目代码经功能验证可稳定运行支持在此基础上二次开发扩展其他课堂行为分析功能。资源共含218个文件涵盖88个py核心逻辑源码、9个ui界面文件、66个pyc编译缓存、模型配置与说明文档、图片与图标素材等压缩包整体约17MB结构清晰便于定位修改。当前已有150人浏览学习适合具备一定Python基础、希望快速搭建智慧课堂分析原型或深入学习PyQt5与深度学习结合应用的开发者。压缩目录包含设计文档、界面截图、演示动图及模型文件能够辅助理解专注度判定流程与系统模块划分便于直接运行、调试并作为课设或毕设基础。1. 智慧课堂专注度分析这套 PyQt5 加深度学习的系统到底在解决什么问题教室后排永远有几个学生不在状态低头、趴着、眼神飘窗。老师不可能每时每刻扫到每个角落等发现问题再提醒注意力已经断了。智慧课堂里的专注度分析做的就是让摄像头替老师盯人——用深度学习逐帧判断学生是看黑板、低头写字还是打盹聊天再在 PyQt5 桌面上实时显示并统计成课堂专注度曲线。这套工程把源码、设计文档和模型一起打包适合课程设计、毕业设计和想同时练 GUI、OpenCV、深度学习三块技能的开发者。接下来从架构拆起讲清跑通步骤、判定逻辑和最容易被忽视的坑。2. 先拆架构再动手PyQt5 界面、深度学习模型和数据流三条线怎么串很多人拿到这种 zip 压缩包的第一反应是解压、找 main.py、直接运行结果要么报缺依赖要么界面能开但摄像头黑屏。这里我建议先花半小时把架构看懂再动手跑。理解下面三个问题后面所有调试都有方向桌面端为什么是 PyQt5、推理模型选什么、数据从摄像头到数据库怎么流转。2.1 为什么桌面端选 PyQt5 而不是 Web 前端线下课堂场景有三个硬需求直接读本机摄像头、逐帧显示视频画面、打包成单机程序拿到答辩现场就能演示。如果换成 Web 方案你得自己解决摄像头权限、推流、跨域、刷新延迟这些工作量加在一起比专注度模型本身还大。PyQt5 的优势恰恰集中在这些地方——OpenCV 读出来的 numpy 图像可以直接转成 Qt 的 QImage不需要经过 HTTP 或 WebSocket 中转QTimer 每几十毫秒触发一次读帧与刷新天然匹配视频流打包时用 PyInstaller 出一个 exe演示机器上只要装了摄像头驱动就能跑。从零开始接触 PyQt5 也不用慌这个项目其实只用了四个核心类。QMainWindow 负责主窗口和布局容器QLabel 负责显示一帧一帧的画面QTimer 负责按固定间隔触发读帧QThread 负责把耗时的模型推理从界面线程里挪走。这四个类掌握住整个系统的界面部分就通了。我一般不建议开始就扑到 Qt Designer 里拖控件先把一个带视频画面的白板窗口跑通再回头加按钮和布局这样定位问题快得多。至于 pyqt5 界面设计的具体美化比如用 setStyleSheet 改背景色、给 QLabel 加边框放到最后做它不影响功能正确性。还有一个容易被忽略的细节OpenCV 读出来的是 BGR 顺序而 Qt 显示用的是 RGB。很多新手把 cv2 的帧直接塞进 QImage画面变成蓝蒙蒙一片就是这个通道顺序没处理。常见的做法是cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)转换一次或者直接用QImage.Format_BGR888格式跳过转换。后者少一次像素遍历在高分辨率下能省一点 CPU但可读性差一些我通常选择先 cvtColor代码更直观。2.2 深度学习模型选型CNN 分类还是姿态估计专注度分析在学术界没有标准模型这是整个项目里最像黑匣子的部分。你必须先想清楚一个问题到底要分析单人还是多人摄像头是近景拍一个学生还是全景拍整个教室这两个答案直接决定模型选型也决定数据标注的工作量。常见的做法有三条路线。第一条是轻量 CNN 图像分类把每个学生的头部区域裁出来直接分「专注 / 分心」两类。这条路线最简单数据好标、训练快、推理快课程设计完全够用。第二条是目标检测加分类级联先用 YOLO 在整帧画面里检出所有人头再对每个检测框做状态分类。它适合多人课堂但工程链条长要处理检测框抖动、目标编号、重叠遮挡写起来最费劲。第三条是姿态估计用 MoveNet、OpenPose 这类模型提取头颈关键点计算低头角度、转头角度、趴桌置信度。判定维度最细但关键点模型对图像分辨率要求高教室全景画面里每个学生只占几十像素效果往往不如第二条路线。如果是课设或毕设我给的建议是别一上来碰姿态估计。课堂场景里学生离摄像头远人脸检测经常失败但「头部 肩部」这么一块小图做分类鲁棒性反而高。用 YOLO 先检出人头框再把框略微外扩一点送入 CNN 分类器这个组合是深度学习中比较稳的落地套路。模型格式上OpenCV 的 DNN 模块支持直接加载 ONNX 文件连 PyTorch 环境都不用装这对 PyQt5 桌面程序的分发特别友好。你做 pyqt5 界面设计的同事或队友只要装 opencv-python 就能跑推理少一个深度学习框架的依赖部署时少踩一半的坑。标题里的模型下载解压后第一件事是确认模型文件的扩展名和输入尺寸。ONNX 文件用net cv2.dnn.readNetFromONNX(path)加载PyTorch 的 .pt 文件得用torch.load加载还要知道模型类别定义和训练时的预处理方式。如果压缩包里只有模型没有说明就用print(net.getUnconnectedOutLayers())或直接喂一张测试图看输出维度先确认输出是 2 类还是 3 类。连类别顺序都搞不清楚就开始接代码后面百分百会在专注度分数映射上翻车。2.3 源码包的结构设计与数据流向拿到一个 PyQt5 深度学习项目的源码包目录结构通常长这样。这是一个典型的拆分方式写代码的阶段按这个结构组织后面调试和答辩演示都会轻松很多。smart_classroom/ ├── main.py # 程序入口创建主窗口并启动事件循环 ├── ui/ # PyQt5 界面层 │ ├── main_window.py # 主窗口按钮与布局 │ └── camera_view.py # 摄像头画面显示控件 ├── core/ # 业务逻辑层 │ ├── detector.py # 加载深度学习模型并推理 │ ├── focus_analyzer.py # 专注度判定逻辑 │ └── database.py # 结果写入 SQLite ├── models/ # 模型文件与标签映射 │ └── focus_model.onnx ├── data/ # 运行产生的录像、截图、报表 ├── docs/ # 设计文档、数据库设计、答辩 PPT 素材 └── requirements.txt # 依赖清单为什么入口文件要单独放在最外层因为 PyInstaller 打包时以它作为起点打包配置里的依赖路径都相对它写。如果入口文件嵌在深层目录打包时经常出现资源文件找不到的情况。这属于 PyQt5 打包最经典的坑之一后面避坑章还会提到。ui 和 core 分开是为了防止界面代码和推理代码互相 import 成环。哪怕这个项目只有几百行养成这个习惯后续加功能时你会发现改起来很快。数据流是一条直线摄像头读取原始帧送入 detector 做目标检测和状态分类得到每个学生的单帧专注度focus_analyzer 把单帧结果放进滑动窗口统计判定这段时间是否走神database 把每分钟的统计结果落库界面层只负责展示最终结果和刷新画面。记住这条线出了问题就能快速定位画面卡了怀疑界面线程结果不准怀疑模型和判定逻辑数据没写入怀疑 database 层不要一上来就乱调参数。3. 跑通最小系统pyqt5 安装、摄像头画面与模型推理三步走这一章的目标是让你在本地跑出一个能显示摄像头画面、并且能调用模型输出状态的桌面程序。不要急着把界面做得花里胡哨先保证链路是通的。3.1 pyqt5 安装与版本匹配先解决环境再写代码第一步永远是建虚拟环境。直接往系统 Python 里装依赖两个月后如果另一个项目需要不同版本的 PyQt5你会后悔当初没建 venv。下面这段是项目最常见的初始化命令顺序和版本都经过大量课程设计项目验证。# 建议使用 Python 3.9 或 3.10避免 3.12 的兼容问题 python -m venv venv # Windows venv\Scripts\activate # Linux/macOS source venv/bin/activate pip install pyqt55.15.10 opencv-python numpy # 如果模型需要用 PyTorch 加载再装 CPU 版 pip install torch torchvision参数说明PyQt5 5.15 是 5 系列的最后一个大版本大量课程设计和开源项目都基于它编写。Python 版本如果高于 3.10pip 很可能找不到匹配的 wheel直接报「Could not find a version」。这不是命令写错了而是版本墙。opencv-python 是 OpenCV 的官方预编译包一定要装这个不要装成 opencv-contrib-python 之外的山寨包。装完先做一次快速验证别急着跑整个工程。这条验证命令能区分「PyQt5 没装好」和「你的代码有 bug」两种问题调试时很省时间。python -c from PyQt5.QtWidgets import QApplication; appQApplication([]); print(PyQt5 OK)3.2 在 PyQt5 界面里显示摄像头画面并叠加检测框环境准备好之后先写一个最简的窗口把摄像头画面显示出来。这个代码块是整个系统界面的骨架之后要加检测框、要叠加识别结果都是在 update_frame 里往 frame 上画。import cv2 from PyQt5.QtCore import QTimer, Qt from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtWidgets import QLabel, QMainWindow class CameraWindow(QMainWindow): def __init__(self): super().__init__() self.video_label QLabel(self) self.setCentralWidget(self.video_label) # 0 是默认摄像头笔记本外接摄像头可能改成 1 self.cap cv2.VideoCapture(0) # 降低分辨率能明显提升帧率 self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) self.timer QTimer(self) self.timer.timeout.connect(self.update_frame) self.timer.start(30) # 30ms 约等于 33fps def update_frame(self): ok, frame self.cap.read() if not ok: return # 这里预留模型推理接口后面接入 detector # frame self.detector.inference(frame) rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.video_label.setPixmap( QPixmap.fromImage(qimg).scaled( self.video_label.size(), Qt.KeepAspectRatio))逻辑说明QTimer 每 30 毫秒触发一次 update_frame这个函数里做了三件事——从摄像头读一帧、转成 QImage、渲染到 QLabel。中间注释掉的那一行就是后续把深度学习模型的输出画到 frame 上的接入点。参数说明30ms 是经验值如果电脑性能弱可以改到 50ms画面会略卡但 CPU 占用明显下降640x480 是推理速度和画面清晰度的平衡点用 1080p 喂模型单帧处理时间会翻几倍。注意这里有一个隐患如果在 update_frame 里直接跑模型推理界面会卡成一帧一帧的。原因是推理是同步阻塞操作跑一次几毫秒到几百毫秒UI 事件循环被堵住了。暂时先不管第五章避坑清单里专门讲怎么用 QThread 解决。3.3 把深度学习模型接进来加载、预处理和专注度分数映射画面通了现在把模型接上。下面的 FocusDetector 类封装了一个最常用的推理流程用 OpenCV DNN 加载 ONNX 模型输入一个 BGR 图像块输出类别和专注度分数。import cv2 import numpy as np class FocusDetector: def __init__(self, model_path, input_size(224, 224)): # 用 OpenCV DNN 加载 ONNX省掉 PyTorch 运行环境 self.net cv2.dnn.readNetFromONNX(model_path) self.input_size input_size # 标签顺序必须与训练时一致否则分数映射会错 self.labels [focused, distracted] def predict(self, bgr_patch): # 预处理缩放 归一化 BGR 转 RGB img cv2.resize(bgr_patch, self.input_size) blob cv2.dnn.blobFromImage( img, 1.0 / 255.0, self.input_size, (0, 0, 0), swapRBTrue) self.net.setInput(blob) probs self.net.forward()[0] idx int(np.argmax(probs)) # 单帧专注度专注1.0分心0.0 score 1.0 if idx 0 else 0.0 return self.labels[idx], score, float(probs[idx])逻辑说明模型只接受固定尺寸的输入所以先 resize 到 224x224。blobFromImage 完成归一化和通道转换scale 参数 1/255 把像素值压到 0 到 1。forward 返回的是每个类别的概率argmax 取出最大概率对应的索引。参数说明input_size 必须和模型训练时一致。很多模型文件跑起来结果完全不对就是因为训练用 224推理时改成了 128。缩放的插值方式也有讲究默认的 INTER_LINEAR 在大多数情况下够用但如果输入图像很小可以先放大两倍再缩放减少细节丢失。返回的 probs 也要关注。如果两个类别概率分别是 0.52 和 0.48说明模型很不确定这时候硬取 argmax 会把模糊样本判错。我一般在代码里加一个判断概率差小于 0.1 的样本标记为「模糊」不计入专注度统计。这个细节写在设计文档里答辩时能让老师觉得你想过问题而不只是调包。接入前面 CameraWindow 的注释位置label, score, conf self.detector.predict(frame) cv2.putText(frame, f{label} {conf:.2f}, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2)4. 专注度判定逻辑从单帧分类到课堂时间窗口的可靠统计模型输出的「专注 / 分心」只是单帧快照直接拿它当整堂课的结论你会被自己系统的准确率气到。这一章讲怎么把单帧结果变成课堂维度的可靠统计。4.1 单帧状态不能直接当结论定义你的状态标签课堂上有一种很常见的情况学生低头在写字模型判定为「低头」然后被算成走神。但低头写字和低头睡觉在单帧图像上确实很像这就是为什么单帧分类结果不能直接拿来下结论。你需要先定义一套能自圆其说的状态标签每个标签对应不同的专注度权重。下表是我在类似系统里常用的标签设计你可以按自己的课堂场景调整但一定要保证「判定依据」是可解释的。状态标签判定依据专注度权重看前方头部角度小于 15 度检测框基本不动1.0低头写字头部角度 15 到 45 度框内有手部动作0.8低头静止头部角度大于 45 度且持续超过 5 秒0.0转头侧视水平角度大于 30 度0.2离开座位检测框消失超过 10 秒0.0权重是这套系统里最「玄学」的部分没有标准答案。你要做的是在答辩时能说清楚为什么低头写字给 0.8 而不是 0.5——因为写字是课堂允许的行为但持续时间过长也可能是在开小差。我一般会把权重做成配置文件跑实验时来回调而不是写死在代码里。定义好标签之后再去回看你手上的模型输出。如果模型只有两个输出你可以把「低头静止」和「转头侧视」都映射到分心把「低头写字」映射到中间态。如果模型是三分类输出恭喜你直接对应即可。最怕的是模型输出和标签对不上那说明训练数据本身就没设计好后续再怎么调判定逻辑都白搭。4.2 滑动窗口统计用连续 N 帧决策代替单帧决策单帧分类不稳定一个抖动、一个遮挡就会导致结果跳变。最常见的做法是用一个滑动窗口只统计最近 N 帧里非专注状态的比例超过阈值才判定为走神。这样既抗抖动又能把「瞬间低头」和「持续趴桌」区分开。from collections import deque class FocusTracker: def __init__(self, window_size45, distract_ratio0.6): # window_size: 参与统计的帧数 # distract_ratio: 非专注帧占比阈值 self.window deque(maxlenwindow_size) self.distract_ratio distract_ratio def update(self, focus_score): # focus_score: 0分心, 0.8中性, 1.0专注 self.window.append(focus_score) def is_distracted(self): if len(self.window) self.window.maxlen: return False distracted sum(1 for s in self.window if s 0.5) return distracted / len(self.window) self.distract_ratio def average_focus(self): if not self.window: return 0.0 return sum(self.window) / len(self.window)逻辑说明deque 的 maxlen 参数决定了窗口大小新数据进来时最旧的数据自动弹出始终保持最近 N 帧。is_distracted 计算非专注帧占比average_focus 返回窗口内的平均专注度这个值就是每一分钟报表的数据来源。参数说明window_size45 配合 15fps 的推理帧率正好覆盖 3 秒。窗口太短低头捡个笔都会被判定为走神窗口太长学生趴了半分钟才发现失去了实时提醒的意义。distract_ratio 建议从 0.6 起步误报多就往上调反应慢就往下调。这个模块同时解决另一个问题多人课堂的目标管理。如果画面里有多个学生你需要为每个学生维护一个独立的 FocusTracker 实例用检测框的 ID 做索引。YOLO 会给每个目标分配跟踪 ID但检测框抖动会导致 ID 跳变这是多人追踪里最头疼的事。一个偷懒但有效的办法是不追踪 ID只统计每个人头中心点在帧间的位置变化距离小于阈值就认为是同一个人。4.3 报表输出把专注度数据写进 SQLite 并画曲线专注度统计出来之后一定要落地存储。用 SQLite 而不是 CSV 文件是因为多学生同时写入时 CSV 容易互相覆盖而且 SQLite 查询语法答辩时展示起来更直观。import sqlite3 import time class FocusDB: def __init__(self, db_pathfocus.db): self.conn sqlite3.connect(db_path) # 如果表已存在这条语句不会重复创建 self.conn.execute( CREATE TABLE IF NOT EXISTS focus_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT, timestamp INTEGER, focus_score REAL )) self.conn.commit() def write(self, student_id, focus_score): self.conn.execute( INSERT INTO focus_log (student_id, timestamp, focus_score) VALUES (?, ?, ?), (student_id, int(time.time()), focus_score)) self.conn.commit() def query_by_student(self, student_id): cur self.conn.execute( SELECT timestamp, focus_score FROM focus_log WHERE student_id ? ORDER BY timestamp ASC, (student_id,)) return cur.fetchall()逻辑说明focus_log 表用 timestamp 记录每次写入的时间focus_score 记录该时刻的专注度。每秒钟写一条会数据爆炸我通常让程序每隔 60 秒取一次滑动窗口的平均值写入这样一堂 45 分钟的课每个学生只有 45 条记录画曲线也清爽。参数说明student_id 在单人模式下可以用固定值比如 student_0多人模式下就是检测框跟踪 ID。如果你想做更细的分析加一个 session_id 字段区分不同课堂这样设计文档里「数据统计与分析」章节就有内容可写了。5. 避坑清单PyQt5 与深度学习推理联调的 5 个典型问题这一章全是我自己和身边人实际踩过的坑每条都按「现象、原因、解决」的顺序写希望你在还来得及的时候避开。5.1 pip 装不上 pyqt5连带 labelme 也没法用现象执行 pip install pyqt5 报错「Could not find a version that satisfies the requirement pyqt5」安装 labelme 时也提示依赖无法满足最后什么都装不上。原因Python 3.12 及以上版本没有对应的 PyQt5 稳定 wheel。很多课程项目依赖 PyQt5 5.15.x而这个版本只覆盖到 Python 3.10 左右。新解释器和老库之间横着一条版本墙pip 直接找不到可用的包。解决换 Python 3.9 或 3.10 建虚拟环境然后再装。注意是建虚拟环境不是卸载系统 Python——你可能有其他项目依赖新版解释器。切换之后先跑一遍前面 3.1 节里的验证命令确认 PyQt5 能 import 再继续。如果你只是想用 labelme 标数据也可以直接装 labelme它会自动匹配适合的 PyQt5 版本但最好还是统一到同一个虚拟环境里免得标数据和跑系统用了两套依赖。5.2 摄像头画面卡死一启动就无响应现象点击「开始识别」后窗口开始转圈几秒后系统提示程序未响应。关掉识别按钮画面又恢复正常。原因把模型推理直接写在了 QTimer 的槽函数里。推理是同步阻塞操作一帧几十毫秒到几百毫秒期间事件循环被死死堵住界面自然卡死。推理越慢卡顿越明显如果模型输入是 1080p 原图卡到你以为死机了。解决用 QThread 把读帧和推理丢到工作线程。典型做法是写一个继承 QThread 的工作类run 方法里循环读帧、推理然后发信号把结果传回主线程。界面线程只负责接收信号并刷新 QLabel。如果觉得 QThread 麻烦也可以退而求其次把推理和界面刷新放在同一个 QTimer 里但降频到每 3 帧推理一次。这样代码改动最小但治标不治本CPU 占用还是会高。5.3 模型加载要好几秒第一次推理像死机现象程序能正常打开但第一次点击识别的瞬间画面停顿了 3 到 5 秒然后突然开始流畅输出之后一切正常。原因模型文件几个 MB 到几十 MB从磁盘加载到内存需要时间ONNX 模型首次推理还会触发运行时算子初始化比后续推理慢一个数量级。很多同学把模型加载写在了按钮的点击槽函数里导致每次点击都要重新加载一遍。解决启动程序时就把模型加载好放在主窗口初始化方法里不要等按钮点击再加载。加载完成后用一张全零图或者一张随机噪声图跑一次空推理让运行时的初始化工作提前完成这个操作叫 warm-up。这样真正开始识别时第一次推理的速度已经和后续持平。另外如果发现模型加载确实太慢可以用 onnx-simplifier 简化模型结构再转成 fp16 精度体积能缩小将近一半加载速度也会明显提升。5.4 摄像头黑屏或打不开提示设备被占用现象程序能启动但画面区域一直是黑的控制台输出 CAP_IMAGES: cant find camera 或者读取不到帧。原因Windows 下最常见的原因是摄像头被其他程序占用。视频会议软件、直播工具、甚至前一个没退干净的 Python 进程都会把摄像头独占。Linux 下则是权限问题当前用户不在 video 组里没有权限访问 /dev/video0。还有一种可能摄像头索引不对。解决按顺序排查。第一步确认没有其他软件占用摄像头把视频会议客户端彻底退出检查任务管理器里有没有残留的 python.exe。第二步确认索引笔记本自带摄像头通常是 0外接 USB 摄像头可能是 1把代码里的 VideoCapture(0) 改成 VideoCapture(1) 逐个试。第三步处理权限Linux 执行 sudo usermod -aG video $USER 然后重新登录。Windows 还额外检查系统设置里的相机隐私权限确认桌面应用被允许访问摄像头。5.5 低头就误报走神记笔记被当成睡觉现象系统对低头动作极其敏感学生记笔记时频繁被判为分心课堂专注度曲线惨不忍睹明显不符合真实情况。原因单帧 CNN 只学了个「低头」和「抬头」的表象不区分低头动作的持续时间也不区分低头时手部有没有动作。单帧图像里记笔记和睡觉真的很像。解决把判定逻辑分级不要只依赖模型的单帧输出。第一步用头部角度判断低头程度角度小于 15 度算正常看黑板15 到 45 度算中性大于 45 度标记为「可疑」。第二步结合持续时间可疑状态持续超过 5 秒才升级为「分心」。第三步如果模型输入分辨率允许对检测框的下三分之一区域做手部运动检测——帧间差分就能判断手有没有在动有动作就降级为中性。这三步做完误报率能降下来一大截。这些都是纯规则逻辑不需要重新训练模型改起来成本很低。6. 进阶验证用课堂录像做一次专注度曲线对照实验系统跑通之后你还需要一个能说服别人的验证。单纯拿摄像头对着自己歪头低头只能说明流程通不能说明专注度数据可信。我建议你做一个可复现的对照实验把系统从「能跑」提升到「能证明有效」。具体做法是这样的。录一段 15 到 20 分钟的课堂录像画面里可以只有两三个同学。播放录像时人工每 30 秒记录一次这些同学的真实状态专注记 1不专注记 0得到一份人工标注序列。然后把同一段录像喂给系统让系统也按 30 秒的时间片输出平均专注度二值化之后和人工标注对比算出时间片一致率。import numpy as np # 人工标注与系统输出时间片一一对应 manually [1, 1, 1, 0, 0, 1, 1, 1, 0, 0] system [1, 1, 0, 0, 0, 1, 1, 1, 0, 1] acc np.mean(np.array(manually) np.array(system)) print(f时间片一致率: {acc:.2%})这个指标不需要很高能到 70% 上下就足以说明系统不是随机乱猜把它写进设计文档的「测试与结果」章节比贴一堆模型结构图更有说服力。除了准确率还要看曲线趋势是否一致人工标注里第三分钟到第五分钟集中走神系统曲线也在同一段出现明显下滑这说明系统捕捉到了真实的课堂节奏变化。如果想让实验更有趣可以进一步对比不同视频片段的专注度差异比如课程开始五分钟、中间二十分钟、临近下课五分钟各取一段看看专注度是不是真的呈现「高一低一回升」的形态。这类观察写在论文或报告里很能体现你对问题的理解。同时可以考虑用 QtCharts 把整节课的专注度曲线画在界面上这是答辩时最直观的一个亮点——老师一眼就能看到什么时候课堂注意力在滑坡。我第一次做这个系统时把所有逻辑都堆在 UI 线程里答辩演示当场卡死后来才明白界面与推理必须分离。这套系统真正的价值不在于准确率有多高而在于你能不能把摄像头到数据库这条链路完整走通并且每一步都能说清为什么这么做。希望这份拆解和踩坑记录能帮到你。本文还有配套的精品资源点击获取