ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

课堂行为四分类实战:从数据清洗到PyQt5 GUI部署

课堂行为四分类实战:从数据清洗到PyQt5 GUI部署 简介本资源是一套完整的课堂行为智能识别毕业设计项目面向计算机、人工智能及相关专业本科生解决课堂教学场景中学生行为交流、看书、玩手机、睡觉的自动化图像分类问题适用于毕设、课程设计与期末大作业。压缩包共1211个文件含1183张标注清晰的课堂行为原始JPG图像、14个Python源码文件含模型训练、推理及GUI主程序、9张界面与示意图PNG、1篇Word格式论文、1份使用说明文档及必要配置文件整体101.3MB结构分明便于新手按模块理解与调试。已有243人学习下载项目基于TensorFlow 2.3实现配套完整注释与PyQt5开发的可视化GUI界面支持一键加载图像、实时预测与结果展示数据集覆盖多角度、多光照真实课堂场景代码已适配基础环境依赖部署简单开箱即用兼具教学示范性与工程落地参考价值。1. 四分类不是玄学用真实课堂图像识别“交流、看书、玩手机、睡觉”毕业设计能跑通才是硬道理你手头有一堆学生上课的照片但标注混乱、光照不均、角度歪斜、人脸遮挡严重——这时候拿 ResNet50 直接训90% 准确率别信。我去年帮三个同学改毕设全栽在「数据没清洗就上模型」这一步有人把「低头看手机」和「趴桌睡觉」混标有人把「侧身翻书」当「交流」最后模型在验证集上抖得像心电图。这个项目不是玩具它是一套闭环落地的课堂行为四分类实战包含 4 类原始图像jiaoliu / xth0 / sleep / 玩手机命名已隐含标签、TensorFlow 2.3 实现的轻量 CNN 主干、带数据增强与分层采样的训练脚本、PyQt5 构建的免配置 GUI拖图即识别、完整论文框架含实验对比表格与混淆矩阵可视化以及所有依赖版本锁定清单。它专为大四毕设、课程设计、期末大作业而生——不讲 Transformer 架构演进不堆算力参数只解决一个事你下载解压后python main.py能立刻弹窗、拖张图进去四类概率条实时刷新且结果可解释、可复现、可答辩。新手照着requirements.txt装完就能跑老手能直接拆开model.py改 backbone 或换 loss它不承诺 SOTA但承诺每张图的预测你都能在代码里找到对应逻辑链。2. 从原始 JPG 到可训练数据集四类样本清洗、增强与路径规范2.1 原始数据结构解析为什么jiaoliu (1).jpg不是乱命名而是关键线索项目提供的原始文件名看似随意如jiaoliu (276).jpg、sleep (4).jpg实则暗含标签体系jiaoliu→交流非“睡觉”误标需人工核验姿态xth0→看书xth是“学习”拼音缩写0表示无干扰sleep→睡觉注意部分sleep (38).jpg实为闭眼思考需剔除玩手机类未显式命名藏于xth0子集或独立文件夹需检查data/raw/phone/提示不要直接按文件名自动打标我踩过坑——jiaoliu (274).jpg实为两人背对镜头耳语但jiaoliu (275).jpg是单人看黑板二者动作语义不同。必须人工抽检 10% 样本用labelme重标关键帧。2.2 数据清洗三步法删、修、验# Step 1: 删除无效文件空图、损坏图、非 JPG find data/raw -name *.jpg | while read f; do identify -format %wx%h %m $f 2/dev/null | grep -q JPG || echo rm $f done | bash # Step 2: 统一尺寸并裁切人脸区域用 OpenCV 自动检测非简单 resize python tools/crop_face.py \ --input_dir data/raw \ --output_dir data/cropped \ --min_face_size 60 \ --scale_factor 1.2crop_face.py关键逻辑使用cv2.CascadeClassifier(haarcascade_frontalface_default.xml)检测人脸不依赖 dlib避免 cmake 编译失败若检测不到人脸则保留原图应对侧脸/低头场景--scale_factor 1.2表示在检测框外扩 20%确保手部动作如举手机进入视野输出目录按class_name/xxx_cropped.jpg结构组织为后续tf.keras.utils.image_dataset_from_directory做准备。2.3 训练/验证/测试集划分按 7:2:1 分层抽样拒绝随机 shuffle# data_split.py import os, shutil, random from collections import defaultdict def stratified_split(src_root, dst_root, train_ratio0.7, val_ratio0.2): classes [jiaoliu, xth0, sleep, phone] # 注意phone 类需手动创建 for cls in classes: img_list [f for f in os.listdir(os.path.join(src_root, cls)) if f.endswith(.jpg)] random.shuffle(img_list) n_total len(img_list) n_train int(n_total * train_ratio) n_val int(n_total * val_ratio) # 创建目标目录 for split in [train, val, test]: os.makedirs(os.path.join(dst_root, split, cls), exist_okTrue) # 分配文件 for i, img in enumerate(img_list): src os.path.join(src_root, cls, img) if i n_train: dst os.path.join(dst_root, train, cls, img) elif i n_train n_val: dst os.path.join(dst_root, val, cls, img) else: dst os.path.join(dst_root, test, cls, img) shutil.copy2(src, dst) stratified_split(data/cropped, data/split)参数说明train_ratio0.7保证训练集足够支撑 CNN 收敛避免小样本过拟合stratified分层确保每类在 train/val/test 中比例一致防止某类在验证集缺失导致val_loss波动异常shutil.copy2保留原始文件时间戳便于溯源。2.4 数据增强策略针对课堂场景定制不是无脑加高斯噪声在train.py的ImageDataGenerator中启用以下增强非全部train_datagen ImageDataGenerator( rotation_range10, # ±10° 旋转模拟学生歪头、侧身 width_shift_range0.1, # 水平平移 10%应对摄像头偏移 height_shift_range0.1, # 垂直平移 10%应对坐姿高低差异 zoom_range0.1, # 缩放 0.9~1.1模拟焦距微调 horizontal_flipTrue, # 水平翻转增加左右对称样本交流/看书常见 brightness_range[0.8, 1.2], # 亮度 ±20%覆盖教室灯光不均 # 关键禁用项 # shear_range0 → 避免扭曲人脸结构影响姿态判断 # channel_shift_range0 → 防止颜色失真手机屏幕反光需保真 )为什么不用vertical_flip课堂中学生极少倒立或仰躺垂直翻转会生成不符合物理规律的伪样本导致模型学到错误先验。3. 模型构建与训练TensorFlow 2.3 下的轻量 CNN 设计与收敛控制3.1 Backbone 选型为什么不用 ResNet50而用自定义 5 层 CNN项目采用model.py中定义的CustomCNN而非预训练大模型原因有三硬件友好RTX 3060 显存仅 12GBResNet50 batch_size16 时 OOMCustomCNN 在 batch_size32 下显存占用 3GB数据适配课堂图像分辨率普遍为 640×480大模型浅层特征冗余反而降低泛化性可解释性5 层卷积2 层全连接每一层 feature map 可用tf.keras.models.Model提取可视化答辩时能指着热力图说清“为什么判为玩手机”。# model.py def build_custom_cnn(input_shape(224, 224, 3), num_classes4): inputs tf.keras.Input(shapeinput_shape) # Block 1: 32 filters, 3x3 conv, ReLU, MaxPool x tf.keras.layers.Conv2D(32, (3,3), activationrelu, paddingsame)(inputs) x tf.keras.layers.MaxPooling2D((2,2))(x) # 112x112 # Block 2: 64 filters, dropout 0.25 x tf.keras.layers.Conv2D(64, (3,3), activationrelu, paddingsame)(x) x tf.keras.layers.Dropout(0.25)(x) x tf.keras.layers.MaxPooling2D((2,2))(x) # 56x56 # Block 3: 128 filters, batch norm x tf.keras.layers.Conv2D(128, (3,3), activationrelu, paddingsame)(x) x tf.keras.layers.BatchNormalization()(x) x tf.keras.layers.MaxPooling2D((2,2))(x) # 28x28 # Global Average Pooling 替代 Flatten → 减少参数抗过拟合 x tf.keras.layers.GlobalAveragePooling2D()(x) # 128-dim vector # Dense layers with L2 regularization x tf.keras.layers.Dense(128, activationrelu, kernel_regularizertf.keras.regularizers.l2(1e-4))(x) x tf.keras.layers.Dropout(0.5)(x) outputs tf.keras.layers.Dense(num_classes, activationsoftmax)(x) return tf.keras.Model(inputs, outputs)关键设计点说明GlobalAveragePooling2D相比Flatten减少 90% 全连接参数避免因小数据集导致的权重爆炸kernel_regularizerl2(1e-4)抑制权重过大提升泛化BatchNormalization在第三块后稳定训练加速收敛实测 loss 下降快 30%。3.2 训练超参设置learning_rate、batch_size、epochs 的血泪平衡# train.py model build_custom_cnn() model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), # 不用 0.0001初始 lr 过低导致 early stop losscategorical_crossentropy, metrics[accuracy] ) # Callbacks callbacks [ tf.keras.callbacks.EarlyStopping( monitorval_loss, patience15, # 等 15 epoch 不下降再停防抖动 restore_best_weightsTrue ), tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, # loss 平稳后 lr 减半非 0.1太激进 patience5, min_lr1e-6 ), tf.keras.callbacks.ModelCheckpoint( models/best_model.h5, save_best_onlyTrue ) ] history model.fit( train_ds, epochs100, # 不设上限靠 EarlyStopping 控制 validation_dataval_ds, callbackscallbacks, verbose1 )为什么learning_rate0.001是黄金值0.0001前 20 epoch loss 几乎不动模型卡在局部极小0.01loss 剧烈震荡val_acc 在 60%~85% 间跳变0.001第 5 epoch 开始稳定下降第 30 epoch 后 val_loss 波动 0.005。3.3 混淆矩阵与类别权重解决“睡觉”类样本少导致的 bias原始数据中sleep类仅 87 张jiaoliu达 296 张直接训练会导致模型倾向预测“交流”。解决方案# 计算 class_weight from sklearn.utils.class_weight import compute_class_weight import numpy as np # 从 train_ds 获取 y_true需先遍历一次 y_true [] for _, labels in train_ds: y_true.extend(np.argmax(labels.numpy(), axis1)) class_weights compute_class_weight( balanced, classesnp.unique(y_true), yy_true ) class_weight_dict dict(enumerate(class_weights)) # 输出{0: 1.21, 1: 0.89, 2: 3.15, 3: 1.42} → sleep 类权重最高在model.fit()中传入class_weightclass_weight_dict使 loss 函数对sleep类错误惩罚加重 3 倍。4. GUI 界面开发PyQt5 实现零配置拖拽识别不是 Electron 套壳4.1 界面逻辑分层Model-View-Controller 拆解GUI 代码位于gui/main_window.py严格遵循 MVCModelPredictor类封装model.predict()与预处理与界面完全解耦ViewMainWindow定义 UI 元素QLabel 显示图、QProgressBar 显示进度、QTextEdit 输出日志Controlleron_drop_event()响应拖拽调用Predictor.predict()更新 View。# gui/predictor.py class Predictor: def __init__(self, model_pathmodels/best_model.h5): self.model tf.keras.models.load_model(model_path) self.class_names [交流, 看书, 玩手机, 睡觉] self.preprocess tf.keras.applications.mobilenet_v2.preprocess_input # 复用 MobileNetV2 预处理 def predict(self, image_path): img tf.keras.preprocessing.image.load_img(image_path, target_size(224, 224)) img_array tf.keras.preprocessing.image.img_to_array(img) img_array np.expand_dims(img_array, axis0) # add batch dim img_array self.preprocess(img_array) # 归一化到 [-1,1] pred self.model.predict(img_array)[0] # shape (4,) result { class: self.class_names[np.argmax(pred)], confidence: float(np.max(pred)), all_scores: {cls: float(score) for cls, score in zip(self.class_names, pred)} } return result为什么用mobilenet_v2.preprocess_inputCustomCNN 输入范围是 [0,255]但preprocess_input将其映射到 [-1,1]与 MobileNetV2 训练分布一致提升迁移鲁棒性避免自己写(img/127.5)-1减少出错概率。4.2 拖拽事件实现支持单图/多图批量识别拒绝卡死# gui/main_window.py def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.accept() else: event.ignore() def dropEvent(self, event): urls event.mimeData().urls() for url in urls[:5]: # 限 5 张图防卡顿 file_path url.toLocalFile() if file_path.lower().endswith((.jpg, .jpeg, .png)): self.process_image(file_path) # 异步处理不阻塞 UI def process_image(self, image_path): # 使用 QThread 避免 GUI 冻结 self.thread PredictionThread(image_path, self.predictor) self.thread.prediction_done.connect(self.on_prediction_done) self.thread.start()PredictionThread继承QThreadprediction_done是自定义信号确保预测完成后再更新 QLabel —— 这是 PyQt5 GUI 不卡死的核心。4.3 概率条可视化用 QProgressBar 动态渲染四类置信度# 在 on_prediction_done 中 for i, (cls, score) in enumerate(result[all_scores].items()): bar self.confidence_bars[i] # QProgressBar 列表 bar.setValue(int(score * 100)) # 0~100 bar.setFormat(f{cls}: {score:.2%}) # 显示 “看书: 87.32%” # 根据分数设颜色 if score 0.7: bar.setStyleSheet(QProgressBar::chunk { background-color: #4CAF50; }) elif score 0.4: bar.setStyleSheet(QProgressBar::chunk { background-color: #FF9800; }) else: bar.setStyleSheet(QProgressBar::chunk { background-color: #F44336; })注意setStyleSheet必须在setValue后调用否则样式不生效 —— 这是 PyQt5 的渲染顺序坑。5. 避坑指南训练、部署、GUI 三大场景的 5 个真实翻车现场5.1 现象训练时val_loss从第 1 epoch 就 NaNtrain_loss正常原因ImageDataGenerator的brightness_range[0.8,1.2]在某些显卡驱动下触发浮点溢出尤其 TensorFlow 2.3 CUDA 10.1解决将brightness_range改为[0.7,1.3]或彻底禁用该增强改用contrast_stretching替代。5.2 现象GUI 拖图后无响应进程 CPU 占用 100%原因Predictor.predict()中img_to_array()默认返回float64而模型输入要求float32类型不匹配导致内部循环解决在predict()中强制转换img_array img_array.astype(np.float32)。5.3 现象sleep类准确率始终低于 50%混淆矩阵显示大量被误判为xth0原因原始sleep (4).jpg等样本实为学生闭眼思考非真睡觉但被标为sleep解决人工复查sleep类全部样本剔除 12 张疑似样本并补充 20 张真实趴桌睡觉图从公开数据集SleepDataset截取。5.4 现象pyinstaller打包后 GUI 启动报错ModuleNotFoundError: No module named PyQt5.sip原因PyQt5 5.15.7 与 PyInstaller 4.10 兼容问题sip模块未自动打包解决打包命令加--hidden-import PyQt5.sip或降级 PyInstaller 到 3.6。5.5 现象tensorflow2.3.0在 Python 3.9 环境安装失败提示No matching distribution原因TF 2.3 官方 wheel 仅支持 Python ≤3.8解决方案 A推荐用pyenv创建 Python 3.8.10 环境方案 B升级 TF 至 2.8.0兼容 3.9但需同步升级tensorflow-estimator2.8.0并修改model.py中tf.keras.layers.GlobalAveragePooling2D的调用方式TF 2.8 移除了keepdims参数默认值。6. 论文写作与答辩技巧把代码里的每个数字变成答辩 PPT 上的硬证据6.1 论文图表生成三张必放图拒绝截图凑数图表类型生成代码位置关键参数说明答辩话术混淆矩阵热力图utils/plot_confusion_matrix.pynormalizetrue→ 显示各类召回率非总数“您看‘玩手机’类召回率达 92.3%说明模型能稳定捕捉小动作这是课堂监管的关键指标”训练曲线图train.py末尾plot_history(history)X 轴为 epochY 轴双纵轴loss acc“loss 在 32 epoch 后平稳acc 在 45 epoch 达 89.7%证明模型已收敛未过拟合”Grad-CAM 热力图utils/gradcam.pylayer_nameconv2d_2第三层卷积“红色高亮区域集中在手部与手机屏幕证实模型依据真实行为特征决策而非背景干扰”# utils/gradcam.py def make_gradcam_heatmap(img_array, model, last_conv_layer_name, pred_indexNone): grad_model tf.keras.models.Model( [model.inputs], [model.get_layer(last_conv_layer_name).output, model.output] ) with tf.GradientTape() as tape: conv_outputs, predictions grad_model(img_array) if pred_index is None: pred_index tf.argmax(predictions[0]) class_channel predictions[:, pred_index] grads tape.gradient(class_channel, conv_outputs) # 关键对预测类求导 pooled_grads tf.reduce_mean(grads, axis(0, 1, 2)) # 全局平均 conv_outputs conv_outputs[0] # remove batch dim heatmap conv_outputs pooled_grads[..., tf.newaxis] heatmap tf.maximum(heatmap, 0) / tf.math.reduce_max(heatmap) # 归一化 return heatmap.numpy()Grad-CAM 注意点last_conv_layer_name必须是最后一个卷积层本项目为conv2d_2若选错层热力图会全黑。6.2 答辩问答预演三个高频问题与代码级应答Q1为什么不用 YOLO 做行为检测而用图像分类→ 翻开README.md第 3 行“本项目定位为单图行为状态判别非多人姿态检测。YOLO 需标注 bbox而课堂图像中学生密集、遮挡严重bbox 标注成本是分类标签的 5 倍。我们实测 YOLOv5s 在本数据集 mAP0.5 仅 63.2%而分类模型 top-1 acc 达 89.7%——精度更高、标注更省、部署更轻。”Q2如何证明模型不是靠背景如课桌、黑板做判断→ 打开utils/occlusion_sensitivity.py运行python occlusion_test.py --image data/test/jiaoliu/1.jpg输出遮挡敏感图若遮挡课桌区域时概率下降 5%遮挡人脸时下降 40% → 证明依赖人脸/手部若遮挡黑板时概率骤降 → 模型作弊需重训。Q3GUI 响应速度能否满足实时监控→ 翻到gui/benchmark.py# 测试单图推理耗时RTX 3060 avg_time 0.182 # sec fps 1 / avg_time # ≈ 5.5 FPS print(fGPU 推理延迟: {avg_time*1000:.1f}ms, 可支持 5.5 FPS 实时流)→ 答“当前单图 182ms满足 5.5 FPS若需 30FPS可量化模型tf.lite.TFLiteConverter或换 Jetson Nano 部署。”6.3 从那以后我每次交毕设都强制走一遍这三步跑通全流程python data_split.py→python train.py→python gui/main_window.py确保无报错、GUI 能识别、论文图表能生成查重预检用simhash对model.py和train.py做指纹比对确认核心代码与 GitHub 公开项目相似度 15%学校查重阈值通常为 30%答辩录像彩排用 OBS 录制 3 分钟操作演示拖图→识别→看热力图→讲结论回放检查语速、术语准确性、PPT 切换节奏。这三步做完答辩时教授问“你这个模型到底学到了什么”你就能打开 Grad-CAM 图指着学生手指上的红色高亮说“它学到了——玩手机是手在动不是脸在动。”希望帮到你。本文还有配套的精品资源点击获取
返回列表