简介:这是一套基于PyQt5与深度学习的智慧课堂专注度分析系统源码包,面向计算机相关专业在校学生、教师及技术人员,主要用于线下课堂学生专注度的自动分析与评估,适用于毕业设计、课程设计、大作业或初期项目演示等场景。压缩包共218个文件,整体大小约17.02MB,主要包含Python源码文件(py)、界面设计文件(ui)、编译缓存(pyc)、配置与说明文档(xml、md、txt),以及演示图片、动图等辅助素材,目录结构清晰,便于按模块查阅。这套源码已有151人学习/下载,其中代码经过完整验证,可稳定运行,配套设计文档与模型文件有助于理解深度学习模型部署及PyQt5界面交互流程。此外,内容还涉及视频源配置、NMS加速实现、演示动图等,既适合新手入门进阶,也可基于此进行二次开发,扩展其他智慧课堂功能。
1. 智慧课堂专注度分析系统:摄像头画面如何变成每个学生的专注度分数
教室后排学生在低头写笔记还是刷手机,单靠老师站在讲台上扫一眼很难判断。专注度分析这类智慧课堂项目,就是用一路普通USB摄像头拍摄整间教室,靠深度学习模型实时识别人脸位置、头部姿态和视线方向,再综合玩手机、趴桌这类行为,给每个学生算出一个专注度分数,最后通过Pyqt5桌面界面呈现出来。它不是论文里的实验demo,而是一套能落地的桌面应用,交付物里包含python源码、设计文档和训练好的模型权重。适合三类人:正在做课设或毕设的学生、想快速搭课堂分析原型的开发者,以及第一次把Pyqt5和深度学习模型装进同一个工程、想看清完整链路的人。拿到压缩包只是开始,把模型加载起来、看到摄像头画面里出现带分数的检测框,才算真正跑通。
2. 专注度计算的技术链路:人脸检测、姿态估计与分数合成的选型思路
2.1 专注度从哪里来:四个可计算的信号维度
先要明确一个前提:深度学习没有办法直接“读”出专注度。所有模型输出的都是客观信号,比如人脸框、关键点、头部角度、画面里有没有手机,专注度分数是这些信号按规则合成的结果。常见做法是把课堂场景拆成四个可计算的信号维度。
第一是人脸检测,拿到每个学生的人脸框,判断人在不在画面里、坐在哪个位置。第二是头部姿态估计,输出yaw(左右转头)、pitch(低头抬头)、roll(歪头)三个欧拉角,低头超过阈值就视为不在看讲台。第三是视线方向,通过眼睛区域估计视线朝向,判断学生是否看着讲台或屏幕,这一路模型最重、受距离和光线影响最大。第四是课堂行为识别,包括玩手机(额外检测手机目标)、趴桌(姿态加人脸消失判断)、交头接耳(相邻两框的距离和相对朝向做规则判断)。
这四个维度不需要全部上齐,常见做法是“人脸检测+头部姿态+行为检测”三件套。视线估计因为摄像头的安装高度和教室距离很难稳定,通常作为可选优化而不是默认主路。专注度可以理解成一个加权打分:分数等于姿态正常权重加视线朝前权重加无手机行为权重,再扣掉趴桌等异常行为权重。具体权重在配置文件里,我一般会把姿态和手机行为权重调高,视线相关权重调低甚至先去掉——小分辨率摄像头下视线估计的可靠性最差。下面这张表是不同信号维度的模型选择参考:
| 信号维度 | 常用模型 | 输出 | 可靠度 | 备注 |
|---|---|---|---|---|
| 人脸检测 | YOLOv5n / SCRFD | 人脸框 | 高 | 距离远、遮挡时漏检 |
| 头部姿态 | 6DRepNet / Hopenet | yaw/pitch/roll | 中高 | 低光照影响较大 |
| 视线估计 | L2CS-Net | 视线向量 | 低 | 需要近距离大图 |
| 行为识别 | 轻量CNN或规则 | 行为类别 | 中 | 建议用检测器辅助判断 |
这段拆解的意义是让你在改这套系统之前先想清楚:模型只是输出原始信号,专注度分数的“解释权”在合成规则里。调好四个权重,比换一个大模型对结果的影响更直接。
2.2 模型怎么选:检测、姿态估计与行为分类的搭配逻辑
系统推理链路是级联的:先检测人脸,再从每张人脸图算姿态,同时在全图上检测手机和异常行为。这样选型是因为专注度本质是“一图多任务”,用单一分类模型直接端到端输出专注度,训练数据很难收集,泛化也差。级联的好处是每一级都能单独替换、单独调参、单独排查。
目标检测层优先选YOLO系。教室场景有大量小目标,后排学生人脸可能只有三四十像素,所以选模型时要把输入分辨率放在第一位。我一般用YOLOv5n或v8n这类轻量版本,640输入分辨率,一张图能框出几十个人脸,普通显卡上可以跑到30帧以上。如果只想做演示、不想引入Torch的重量级依赖,MTCNN也能用,但密集小场景下漏检明显,不建议作为正式方案。
头部姿态估计层常用Hopenet和6DRepNet。6DRepNet精度更高、模型更小,ONNX格式只有几十MB。输入是人脸框裁剪图,输出三个角度,用pitch大于30度判断低头,yaw大于45度判断侧头。这里的坑是角度阈值不能照抄论文,摄像头装在讲台和装在教室侧面,同一姿态的角度差异很大,后面避坑章会专门讲。
行为检测层,玩手机检测最可靠的做法是让YOLO同时检测手机类别,新增一个class,用手机公开数据集或自录几十张教室照片微调。趴桌不需要单独模型,人脸检测不到且上一帧存在,结合位置稳定性就能判断。交头接耳则是对相邻两框计算中心距离和头部朝向,用规则判断。规则判断在课堂场景下比训练交互行为分类器更可控,你不会希望模型把两个相邻学生低头写字误判成聊天。
推理框架层面,首选ONNX Runtime,其次OpenVINO,最后才考虑直接用PyTorch的eval推理。原因很现实:PyTorch的GPU推理换机器要配CUDA版本,在别人的电脑上跑源码,最容易翻车的就是torch和CUDA版本匹配;导成ONNX后CPU能跑、显卡能跑,部署麻烦少一半。源码里如果带模型导出脚本,建议先导一遍再继续。
2.3 系统三层结构:采集、推理、界面怎么分工
拿到源码先别急着跑,对应设计文档里的架构图,找到三个角色。采集层负责从USB摄像头或视频文件读帧,这一层用OpenCV,不参与深度学习推理,任务是把帧按固定帧率送到推理层,同时深拷贝一份给界面预览。推理层是核心,消费采集层送来的帧,按“检测-姿态-行为-分数”顺序计算,把结果打包成结构化数据,包括人脸框列表、角度列表、分数列表。推理层必须做丢帧处理,计算不过来就直接丢,而不是排队积压,积压的后果是延迟越来越大,学生都翻书了画面还停留在上一秒。
UI层用Pyqt5做三块内容:实时视频画面叠加检测框和分数、右侧学生状态列表、底部班级专注度曲线。UI层不碰模型,只从推理层拿结果刷新控件。三层对应到代码里通常是camera_thread、infer_thread、main_window三个模块。跑代码时你会看到线程启动顺序:先开采集,再开推理,最后显示窗口。如果源码里推理和UI写在同一个类里,后面卡顿几乎必然发生,建议用第5节的方案改造成线程分离再继续。
提示:读设计文档时重点对照模块划分和时序图,文档里写的模块名和代码里的类名不一致,是这类项目最常见的情况。后续定位问题要以代码实际结构为准,不要被文档带偏。
3. 让源码跑起来的最小路径:环境配置、依赖安装与模型加载
3.1 环境准备:Python版本与依赖搭配
拿到压缩包,第一步永远是读依赖清单。这类项目多半带requirements.txt或environment.yml。深度学习项目对版本敏感,不建议一条条pip装,因为torch和Pyqt5的组合很容易出现“装完了导入报错”。我的习惯是先建一个虚拟环境,Python版本固定在3.8或3.9,这两个版本对Pyqt5、torch、opencv的兼容性最稳,3.10以上部分旧版wheel缺失,没必要拿学习项目去赌。
下面这份requirements是专注度分析这类项目的典型内容,以你手里的源码清单为准:
# requirements.txt 典型内容 python==3.8 # 3.8/3.9 二选一 pyqt5==5.15.* opencv-python==4.5.* numpy==1.24.* torch==1.13.* torchvision==0.14.* onnxruntime-gpu==1.14.* pyyaml安装命令按顺序执行,建议用国内镜像源:
conda create -n focus python=3.8 conda activate focus pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple版本搭配的逻辑说一下:torch选1.13系列,是因为这个版本同时兼顾GPU和CPU环境,很多源码的训练阶段就是在1.13上完成的,直接装新版torch读旧权重容易遇到模型参数与代码不匹配。onnxruntime-gpu如果不想配CUDA,可以降级成纯CPU版,课堂演示级项目CPU跑15帧也能接受。Pyqt5要注意,5.15.2之后官方没有继续发新版wheel,装到5.15系列即可。numpy锁在1.24,新版本在python3.8下会报警告,部分旧代码的np.float写法会直接报错。
注意:看到torch安装失败,八成是python版本过高或pip源超时。先把python降到3.9以内再重试,不要系统里混装多套python硬刚。
3.2 模型文件放对位置:权重目录、yaml与加载检查
深度学习项目的模型文件通常比较大,从几十MB到两百MB不等,源码包里往往只带路径占位或一份小权重,完整模型需要从网盘下载后放进指定目录。解压后第一步不是双击main.py,而是确认模型目录完整。这类项目的典型目录结构如下:
focus-classroom/ ├── main.py # 程序入口 ├── configs/ │ └── config.yaml # 摄像头索引、置信度、模型路径 ├── models/ │ ├── yolov5n.pt # 人脸/手机检测权重 │ ├── repnet.onnx # 头部姿态权重 │ └── class_names.txt # 类别标签 ├── ui/ │ └── main_window.py └── utils/ ├── camera.py ├── inference.py └── scorer.py模型加载代码通常在utils/inference.py里,常见写法是这样的:
import onnxruntime as ort import torch # 加载头部姿态ONNX模型,CPU执行 sess = ort.InferenceSession( "models/repnet.onnx", providers=["CPUExecutionProvider"] ) def load_yolo_weights(path, conf_thres): # 从本地路径加载YOLO权重,不从网络拉取 model = torch.hub.load("ultralytics/yolov5", "custom", path=path, local_only=True) model.conf = conf_thres # 检测置信度阈值 model.iou = 0.45 # NMS的IoU阈值 return modelonnxruntime的InferenceSession负责加载ONNX模型,providers参数决定执行设备,CPU写“CPUExecutionProvider”,显卡写“CUDAExecutionProvider”。YOLO加载用torch.hub的custom模式,表示读取本地权重而不是下载预训练模型。conf和iou两个参数直接挂在模型上,作为本次运行期的全局阈值。重点是conf:教室场景建议0.3到0.45之间,低于0.25检测框会疯狂抖动,高于0.5会漏检后排小脸。
路径和配置的坑集中在三处:代码写死相对路径,必须从项目根目录启动,否则找不到模型;中文路径会让ONNX和torch出现莫名其妙的加载失败,项目路径、图片路径、模型路径全部改成英文;权重和yaml不匹配时检测层报错提示类数量对不上,多半是用了不同版本的YOLO权重,换回源码配套版本即可。
3.3 启动系统:从入口文件到摄像头自检
跑通的最小命令很朴素。先确认配置文件里的摄像头索引,然后运行入口文件:
# 在项目根目录执行,不要用IDE的临时目录 python main.py --camera 0 --config configs/config.yamlmain.py里大概做了三件事:读配置、创建采集与推理线程、显示主窗口。入口骨架常见写法:
import sys from PyQt5.QtWidgets import QApplication from ui.main_window import MainWindow app = QApplication(sys.argv) # 摄像头索引从命令行参数读取,默认0 camera_id = int(sys.argv[1]) if len(sys.argv) > 1 else 0 # 主窗口内部负责创建采集线程和推理线程 win = MainWindow(camera_id=camera_id, config_path="configs/config.yaml") win.show() sys.exit(app.exec_())QApplication是所有Pyqt5程序必经的起点,一个进程里只能有一个。MainWindow初始化时要启动两个线程,而不是直接打开摄像头——把摄像头初始化放在线程的run方法里,可以避免主窗口启动时长时间无响应。camera_id的判定逻辑是:笔记本自带摄像头往往占0,外接USB摄像头可能是1或2。具体是哪个,用下面这个脚本探测:
import cv2 for i in range(4): cap = cv2.VideoCapture(i) ok, frame = cap.read() print(f"camera {i}: {ok}") cap.release()这个探测脚本建议在启动系统之前先跑一遍。很多人打开报错其实只是camera_id不对,换一个索引就好了。启动之后的首屏自检有四个信号:控制台打印模型加载完成、窗口出现视频预览、检测框稳定出现、专注度分数随时间变化。如果检测框出现但分数不动,说明scorer模块没有从推理结果里取到有效的角度数据,往下看第5节的排查方法。
3.4 读懂源码模块:文件与职责对照
拿到一套不熟悉的源码,最快的方式是做模块映射,而不是从main.py第一行往下读。下面这张表是这类项目的典型映射,可以直接对照:
| 文件/目录 | 职责 | 读代码时的关注点 |
|---|---|---|
| main.py | 程序入口、参数解析 | 线程启动顺序 |
| ui/main_window.py | 主窗口、视频控件、图表 | 信号槽连接 |
| utils/camera.py | 摄像头采集、帧率控制 | 是否做丢帧处理 |
| utils/inference.py | 模型加载、推理管线 | providers、阈值 |
| utils/scorer.py | 专注度分数计算 | 权重与规则 |
| configs/config.yaml | 全局参数 | 路径、索引、阈值 |
| docs/设计文档 | 方案设计与说明 | 与源码是否一致 |
读的时候重点对齐一件事:配置文件里写的模型路径,和utils/inference.py里实际加载的路径是否一致。两处不一致是这类源码最常见的“黑匣子”问题——代码看起来没报错,实际加载的却是另一个模型。把配置文件的路径统一成相对路径后跑一遍,能少掉一半玄学问题。
4. 自己训练专注度模型:数据集整理、迁移学习与关键训练参数
4.1 先判断要不要自己训
用现成权重跑通之后,很多人会想自己训一套让效果更好。先泼冷水:人脸检测和头部姿态这两个模型,自己从零训练的成本远高于收益,问题出在标注。人脸框标注还好,头部姿态要求的yaw/pitch/roll标注需要工具配合,标错一个角度模型就学歪了。常见做法是用现成开源权重做迁移学习,只针对自己教室的摄像头角度做少量微调。
专注度系统里真正值得自己训的,是行为分类那一小块,比如“玩手机vs写字”。公开数据集的手机样本和教室实拍差异很大,教室窗口反光、书本封面、笔袋都可能被误判成手机。所以数据集整理的精力要集中在行为分类和场景适配,不要一上来就训检测和姿态。这个判断能帮你省下至少两周时间。
4.2 数据集从哪来、标注成什么格式
行为分类的数据集有三种来源:公开的手机检测相关数据集,自录的教室视频抽帧,以及数据增强后的样本。第二种最贴合场景,找一个正常上课的教室架一台手机录40分钟,抽帧后标注,每类200到300张就足够微调。第三种是把已有数据做亮度、模糊、透视变换,模拟教室不同座位距离。三种来源混合使用最稳。
标注格式方面,走YOLO检测路线时数据要转成YOLO的txt格式,每行是“类别 中心x 中心y 宽 高”,坐标是归一化后的0到1小数。走分类路线则按文件夹组织,train和val两类目录,每个类别一个子文件夹。下面这个脚本把VOC格式的xml转成YOLO格式,是训练前最常写的工具:
import os import xml.etree.ElementTree as ET def voc2yolo(xml_path, out_dir, classes): root = ET.parse(xml_path).getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text cls_id = classes.index(name) # 类别名转id box = obj.find("bndbox") x1 = float(box.find("xmin").text); y1 = float(box.find("ymin").text) x2 = float(box.find("xmax").text); y2 = float(box.find("ymax").text) # 绝对坐标转归一化中心点与宽高 cx = (x1 + x2) / 2 / img_w cy = (y1 + y2) / 2 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") out_path = os.path.join(out_dir, os.path.splitext(os.path.basename(xml_path))[0] + ".txt") with open(out_path, "w") as f: f.write("\n".join(lines))xml里的bndbox是左上角和右下角的绝对坐标,YOLO要的是归一化后的中心点坐标和宽高,所以每个值都要除以图片尺寸。类别id取决于classes列表的顺序,这个顺序在训练配置和推理时完全一致,否则会出现“检测出手机但显示成书本”这类错位。转换时注意边界:标注工具偶尔会让x2大于图片宽度,转换脚本里最好加一个min-max裁剪。数据整理阶段最容易被忽略的是图片和xml名字一一对应,转换前先做文件名对齐检查。
之后是数据集划分,常见比例是8:2或9:1。下面这段代码把图片和标签成对划分,避免图片进了train而标签进了val:
import os, random, shutil random.seed(42) img_dir, label_dir = "images_all", "labels_all" train_ratio = 0.8 imgs = [f for f in os.listdir(img_dir) if f.endswith(".jpg")] random.shuffle(imgs) split = int(len(imgs) * train_ratio) for i, img in enumerate(imgs): name = os.path.splitext(img)[0] dest = "train" if i < split else "val" os.makedirs(f"{dest}/images", exist_ok=True) os.makedirs(f"{dest}/labels", exist_ok=True) shutil.copy(f"{img_dir}/{img}", f"{dest}/images/{img}") shutil.copy(f"{label_dir}/{name}.txt", f"{dest}/labels/{name}.txt")固定随机种子让每次划分结果一致,复现实验时不会被数据划分差异干扰。另一个细节是训练和验证按“视频片段”划分而不是按单帧划分:同一个视频相邻帧高度相似,如果同段视频的帧同时进了train和val,验证集指标会虚高,训练时自我感觉良好,上真实课堂就露馅。
4.3 关键训练参数:batch、lr、epoch与显存不足
训练阶段如果基于YOLO微调,使用源码自带的train.py即可,核心参数如下:
python train.py --data classroom.yaml --weights yolov5n.pt --img 640 \ --batch 16 --epochs 100 --device 0 --patience 20--weights指定预训练权重,从yolov5n.pt开始微调而不是从空权重训练,收敛速度和最终精度都好很多。--batch是单次喂入的图片数,16是安全值,显存报错就降到8或4,同时把--img从640降到480。--epochs 100对迁移学习足够,再多边际收益很低。--patience是早停参数,验证集loss连续20轮不下降就停止,能省时间。
如果是训练行为分类的小CNN,PyTorch训练循环里最需要关注的是学习率调度:
# 分类模型训练的核心循环片段 import torch from torchvision.models import mobilenet_v3_small model = mobilenet_v3_small(num_classes=2, pretrained=True) criterion = torch.nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) # 每8个epoch学习率降到1/10,避免后期震荡 scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=8, gamma=0.1) for epoch in range(epochs): for imgs, labels in train_loader: out = model(imgs) loss = criterion(out, labels) optimizer.zero_grad() loss.backward() optimizer.step() val_acc = evaluate(model, val_loader) scheduler.step()迁移学习场景下1e-3作为初始学习率比较合适,1e-2容易把预训练权重冲坏,1e-4收敛太慢。StepLR每8个epoch把学习率乘0.1,让损失在训练后半段平稳下降。还有个实际经验:专注度行为分类的类别往往极不平衡,“正常学习”帧占九成,“玩手机”只占一成。面对这个现象,给分类器加权重CrossEntropyLoss或对少数类做过采样,比换模型更有效。训练过程中如果val_acc一直不涨,先检查数据是否成对放对,再检查学习率,实战中问题大多出在这两步。
训练完得到自己的best.pt,替换源码里的检测权重,配置文件指向新路径,按第3章的步骤启动看效果。
5. 避坑指南:Pyqt5与深度学习集成的5个高频踩坑记录
5.1 界面卡死:把推理从主线程里挪出来
现象:窗口能打开,但拖动卡顿,视频画面几十秒不刷新,点击关闭按钮无响应,控制台显示GPU占用很高。
原因:Pyqt5的界面事件循环和推理放在了同一个线程。深度学习推理是耗时操作,一次检测可能占几十到几百毫秒,主线程卡在这里,Qt就没机会处理重绘和鼠标事件。
解决:把推理放进QThread,用信号把结果传回主线程。最小改造如下:
from PyQt5.QtCore import QThread, pyqtSignal class InferenceThread(QThread): # 参数是帧和检测结果,主线程里刷新UI用 frame_ready = pyqtSignal(object, object) def run(self): while not self.isInterruptionRequested(): frame = self.queue.get() results = predict(frame) # 推理只在这个线程发生 self.frame_ready.emit(frame, results)改造思路:把原来在窗口更新方法里做的检测工作整体移入run,主线程只负责信号槽函数更新控件。队列用queue.Queue做缓冲,队列长度限制为2,推理慢时直接丢旧帧,避免积压。改完判断成功的标准很简单:拖动窗口顺畅、帧率数字稳定、关闭按钮能秒退。
5.2 摄像头黑屏或打不开:先索引后权限,最后查分辨率
现象:VideoCapture返回False,或read返回的frame是None;笔记本自带摄像头能开但外接USB摄像头黑屏。
原因:第一是camera_id不对,有些设备自带摄像头占索引0,USB摄像头是1或2。第二是摄像头被会议软件或调试器占用,OpenCV拿不到句柄。第三是权限问题,尤其在linux系统下,python进程没有/dev/video设备的访问权限。
解决:用第3节的探测脚本逐个索引试;关闭所有可能占用摄像头的软件;linux下把当前用户加入video组后再试。分辨率参数要设成摄像头支持的值,把1280x720改成640x480再试——很多廉价USB摄像头对高分辨率支持不完整,会直接黑屏。最后,不用时记得cap.release(),否则第二次启动会被残留句柄卡住。
5.3 检测框乱跳:置信度阈值与目标跟踪
现象:人脸框忽大忽小,同一个学生两帧之间位置跳变,座位上的人偶尔消失,偶尔把书本误检成手机。
原因:检测置信度阈值设得太低,模型把噪点也框了出来;没有做跟踪,每一帧独立检测,模型抖动被直接呈现;另外教室光线变化会让同一目标在不同帧的置信度波动。
解决:把conf从默认的0.25提高到0.35到0.45,观察误检率变化。再加一个基于IoU的简易跟踪,或引入ByteTrack这类轻量跟踪,让每个学生有稳定ID。分数计算必须基于“跟踪后的ID”而不是“每一帧的检测框”,否则按学生聚合统计时数据会崩坏。顺带检查一下class_names.txt的类别顺序,检测结果显示错位时,这个文件往往是元凶。
5.4 模型加载报错KeyError或shape mismatch
现象:加载权重后推理报错,错误信息里出现KeyError或shape mismatch;YOLO权重换版本后报class数量不对。
原因:权重文件与代码里的模型定义不一致,或者ONNX的输入尺寸与预处理尺寸不一致。也有人用新版本torch加载旧权重,遇到序列化格式不兼容的报错。
解决:如果还在调试阶段,换回源码配套的预训练权重,不要一开始就混搭。如果坚持用自己训练的权重,必须确认训练时的类别顺序、输入尺寸与推理代码一致。用ONNX的InferenceSession加载时报输入名字不同,打印session.get_inputs()[0].name,把预处理代码里的输入名改成一致即可。改动之前先把原始权重备份一份,这是你的后悔药。
5.5 分数不动或恒为零:先查信号来源
现象:检测框正常,但专注度分数一直不变,或者全部学生都是0分。
原因:scorer模块拿不到姿态角度,通常是姿态估计的输入裁剪出了问题。人脸框坐标是原图坐标系,姿态模型输入是缩放后的裁剪图,坐标变换没对齐;或者角度输出范围理解错了,有的模型输出弧度,代码当角度用,算出来的分数自然异常。
解决:在推理管线里加一段打印,单独输出某张人脸图的pitch、yaw、roll原始值和score中间结果。更直接的方式是对一张已知角度的测试图跑一次姿态模型,确认输出范围。如果输出在-1到1之间,大概率是弧度,需要乘以57.3转成角度。这条链路是整系统里最容易变成黑匣子的地方,逐级拆分排查比整体猜测有效得多。
提示:遇到这类问题,先把推理层的中间输出打印全。死磕UI代码之前,先确认模型输出了什么。
6. 从能跑到跑好:课堂统计报表、离线回放与模型加速的三个进阶技巧
系统能实时跑起来只完成了一半。要进课堂或答辩,得让数据能沉淀、效果可展示、速度可交代。
6.1 统计报表:让专注度数据沉淀下来
把每帧的每个学生ID、专注度分数、时间戳写入CSV,课后按时间段聚合。以5分钟为窗口计算班级平均分曲线,一眼就能看出哪些时段学生疲劳。关键点是CSV写入要在推理线程里异步处理,不要阻塞UI;文件按日期命名,避免长时间运行导致文件过大。所有课堂分析系统最后看的都是报表而不是实时画面,实时画面是过程,报表才是结论。
6.2 离线回放:演示更稳、调试更快
接一路实时摄像头做演示,在场地、灯光、网络不稳定的场合容易翻车。替代方案是把课堂视频先录下来,给camera.py增加一个video模式,把摄像头源替换成视频文件路径。演示时可以反复暂停、放大某个区域,效果远比实时流稳定。调试也更省力,任何效果问题都能对着同一段视频复现,而不是靠运气复现现场。实现时只需要给采集层加一个is_video参数,推理链路完全不用改。
6.3 模型加速:普通笔记本也能流畅跑
优先做两件事:把模型统一导出成ONNX,再做int8量化。量化后精度会掉一点,但专注度分析输出的是分数区间,完全可用。另一个有效的改动是推理分辨率分级:检测用640输入,姿态估计用128或160输入。检测框的精细度对专注度分数影响不大,但速度能提升接近一倍。合理做法是分模块设置输入分辨率,而不是所有模型共用一套缩放。
最后说一个我的教训:整个项目做完回看,最耽误时间的不是模型训练,而是摄像头角度阈值拍脑袋。当时在教室现场花了一晚上调pitch阈值,后来才意识到不同安装高度的摄像头,同样的低头动作角度数值差异很大。正确做法是先录5分钟现场视频,统计正常听课状态的角度分布,再定阈值,而不是拿论文里的经验值硬套。希望这个教训能帮你省下一晚上的调参时间,也希望这篇笔记能让你手里的源码真正跑起来、跑得稳。希望帮到你。
本文还有配套的精品资源,点击获取