news 2026/9/23 22:08:32

PyQt5与深度学习实战:智慧课堂专注度分析系统构建全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 pyqt5==5.15.10 opencv-python numpy # 如果模型需要用 PyTorch 加载,再装 CPU 版 pip install torch torchvision

参数说明:PyQt5 5.15 是 5 系列的最后一个大版本,大量课程设计和开源项目都基于它编写。Python 版本如果高于 3.10,pip 很可能找不到匹配的 wheel,直接报「Could not find a version」。这不是命令写错了,而是版本墙。opencv-python 是 OpenCV 的官方预编译包,一定要装这个,不要装成 opencv-contrib-python 之外的山寨包。

装完先做一次快速验证,别急着跑整个工程。这条验证命令能区分「PyQt5 没装好」和「你的代码有 bug」两种问题,调试时很省时间。

python -c "from PyQt5.QtWidgets import QApplication; app=QApplication([]); 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), swapRB=True) 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_size=45, distract_ratio=0.6): # window_size: 参与统计的帧数 # distract_ratio: 非专注帧占比阈值 self.window = deque(maxlen=window_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_size=45 配合 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_path="focus.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: can't 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 线程里,答辩演示当场卡死,后来才明白界面与推理必须分离。这套系统真正的价值不在于准确率有多高,而在于你能不能把摄像头到数据库这条链路完整走通,并且每一步都能说清为什么这么做。希望这份拆解和踩坑记录能帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 22:03:58

Python大语言模型评测框架设计:可复现、可归因、可审计

简介&#xff1a;本资源是一套面向AI算法工程师、大模型研究者及高校科研人员的大语言模型效果评测工具代码&#xff0c;聚焦主观题与客观题双维度性能评估&#xff0c;解决模型输出质量量化难、评测流程不统一等实际问题。压缩包共142个文件&#xff0c;含102个CSV用于记录多轮…

作者头像 李华
网站建设 2026/9/23 22:01:55

CSP-J初赛真题解析与自动化备考方法

简介&#xff1a;本资源是面向CSP-J组初赛备考学生的专项训练资料&#xff0c;聚焦计算机基础与编程能力认证核心考点&#xff0c;适用于初中阶段信息学竞赛入门者及教师教学参考。文件为单个Word文档&#xff08;.doc格式&#xff0c;17KB&#xff09;&#xff0c;完整收录202…

作者头像 李华
网站建设 2026/9/23 22:01:13

ESP32 SPI 驱动 W5500 以太网通信实战:从协议原理到 ESP-IDF 代码逐行解析

1. 为什么我劝你把 ESP32 的 SPI 彻底搞懂很多人玩 ESP32&#xff0c;一开始都是连 WiFi、点个灯、读个传感器&#xff0c;日子过得挺舒服。可一旦项目里出现“有线网络”这四个字&#xff0c;尤其是要接 W5500 这种硬件 TCP/IP 芯片的时候&#xff0c;问题就来了——代码跑不起…

作者头像 李华
网站建设 2026/9/23 22:01:05

解码场景GEMM优化实战:从访存瓶颈到硬件环境排查

搞了半年多decoding相关的GEMM优化经验&#xff0c;我最大的感受是&#xff1a;真正的瓶颈往往不在数学本身&#xff0c;而在你如何认识这个算子的真实形态、如何伺候好底层硬件与运行环境。把一条自回归解码链路里反复执行的矩阵乘法拆开看&#xff0c;它的形状、访存模式、硬…

作者头像 李华