简介这套基于 Python 深度学习的课堂行为图像识别分类项目源码面向高校学生的毕业设计、课程大作业以及图像分类入门实践。项目围绕真实课堂场景图像实现对举手、阅读、睡觉、交流等典型行为的自动识别与分类可用于教学督导、在线课堂质量分析等场景。资源共包含 1206 个文件其中以 1183 张 JPG 标注样本为主体按行为类别分目录存放配合 14 个 Python 源码文件、模型说明文档与配置文件压缩包整体约 100.29MB结构清晰便于直接运行和二次开发。源码经过严格调试评审分达 95 分以上环境与依赖说明齐全解压后即可体验数据加载、模型训练到分类评估的完整流程。已有 571 人学习下载适合需要快速搭建课堂行为识别基线项目、参考数据处理与模型训练完整代码的开发者使用。1. 课堂行为识别项目到底是什么从一段教室录像到行为分类结果如果你手头有一份“基于python的深度学习的课堂行为图像识别分类项目源码.zip”大概率你正在做这几件事中的一件毕业设计要交差、学校要求做一个“AI教育”的落地 demo、或者你接了个人脸考勤/课堂分析相关的横向项目。先说结论这个项目的本质不是“识别学生”而是对教室场景里单帧图像中的人物姿态和行为状态做分类——抬头听课、低头写字、举手发言、趴桌睡觉、玩手机、站立这是最常见的六类课堂行为。输入是一张教室监控截图输出是一个带类别标签和置信度的结果。它和你想象中的人脸识别完全是两回事。人脸识别要解决“这个人是谁”课堂行为识别要解决“这群人在干什么”。目标不同技术路线也不同前者是度量学习后者是图像分类或目标检测加行为分类。这个项目能解决的实际问题很明确——把人工巡课、看录像抽查这种低效方式换成模型自动给每节课输出行为分布统计比如“这节课抬头率只有62%后排趴桌率明显偏高”。适合的人群也清晰有 Python 基础、跑过简单深度学习教程、但还没完整走过一个图像分类项目全流程的开发者以及需要快速交付课设或 demo 的在校生。接下来我按自己做这类项目的完整路径从数据组织到训练调参到避坑逐步拆给你看。2. 方案选型与数据集准备为什么选 PyTorch ResNet以及如何组织你自己的课堂数据2.1 为什么不是 TensorFlow也不是 YOLO拿到这个标题里的“深度学习图像识别分类”首先要定技术栈。我一般默认 PyTorch原因很实在课堂行为识别属于典型的小样本、多类别、场景相对固定的分类任务PyTorch 生态里对图像分类的支持最顺滑预训练模型下载、fine-tune、断点续训、TensorBoard 可视化这些操作对新手来说容错率最高。TensorFlow 不是不行但 Keras 的抽象层次高出问题时排查链路长对于这种需要反复调参的项目PyTorch 的“显式张量流转”反而更好理解。再回答一个常被问的问题“为什么不用 YOLO 直接检测”如果你只做分类YOLO 是杀鸡用牛刀——它做的是定位加分类输出 bounding box而课堂行为识别的大部分场景只需要知道“画面里这帧整体是什么行为状态”或者“某个区域里主要行为是什么”。用检测模型意味着你得额外处理锚框、NMS、mAP 这些指标而用分类模型一个 ResNet 就能解决。当然如果你的需求细化到“统计每个学生的个体行为”那确实得走检测分类的级联路线这个我们放到最后一章讲。2.2 数据从哪来自带数据集、公开数据集与自采数据的三种路径这个 zip 解压后通常能看到data/或dataset/目录。先说结论无论 zip 里有没有带数据集你都要重新审视数据质量而不是直接开训。课堂行为识别最常见的翻车不是模型结构不对而是数据组织混乱。数据来源有三种路径按优先级排公开数据集课堂行为方向常用的是 SCBStudent Classroom Behavior数据集包含 6 类课堂行为大约 5 万多张图像训练集和测试集已有官方划分。还有 PEOD学生的课堂参与度数据集按参与度打分。如果你做课设直接用官方划分就够了。从项目自带数据里拆分如果 zip 里已经带了图片和标注先看目录结构。规范的做法是train/下每个类别一个子文件夹val/和test/同理。如果不规范需要自己写脚本重排。自采数据用教室监控截图或手机拍摄的课堂照片按类别归档。这类数据最贴近你的真实场景但标注工作量极大且要处理隐私合规问题。我强烈建议你先把官方数据集跑通再考虑自采数据。原因后面避坑章节会详细说。2.3 从零组织数据集目录结构与划分脚本先说最终的目标目录结构这是全项目的地基classroom_behavior/ ├── data/ │ ├── train/ │ │ ├── raise_hand/ │ │ ├── reading/ │ │ ├── writing/ │ │ ├── sitting/ │ │ ├── sleeping/ │ │ └── bowing_head/ │ ├── val/ │ │ └── ...同上每个类别文件夹 │ └── test/ │ └── ...同上 ├── checkpoints/ ├── models/ ├── utils/ ├── train.py ├── predict.py └── requirements.txt如果你手上的数据不是这种结构比如所有图片堆在一个文件夹里、标注在 CSV 里用下面这个脚本归类。这里以 CSV 里有两列filename,label为例import os import shutil import pandas as pd from sklearn.model_selection import train_test_split # 读取标注文件 df pd.read_csv(labels.csv) # 按 7:2:1 划分训练/验证/测试集 train_df, temp_df train_test_split(df, test_size0.3, stratifydf[label], random_state42) val_df, test_df train_test_split(temp_df, test_size0.33, stratifytemp_df[label], random_state42) # 类别名映射按你自己的实际类别调整 class_names [raise_hand, reading, writing, sitting, sleeping, bowing_head] for split_name, split_df in [(train, train_df), (val, val_df), (test, test_df)]: for _, row in split_df.iterrows(): src_path os.path.join(all_images, row[filename]) dst_dir os.path.join(data, split_name, row[label]) os.makedirs(dst_dir, exist_okTrue) shutil.copy(src_path, os.path.join(dst_dir, row[filename])) print(数据划分完成)这段脚本做了三件关键事用stratify按类别比例抽样保证每个类别在训练/验证/测试集中的分布和原始数据一致——这一点在类别不平衡时会直接影响验证集的可信度固定random_state42保证可复现按 7:2:1 划分而不是常见的 8:2因为课堂行为数据通常有噪声验证集留多一些能更早发现过拟合。注意如果你使用 SCB 这类公开数据集不要自己重新划分直接用官方划分这样你的结果可以和论文对比。3. 源码结构与核心模块拆解从数据加载到模型训练的完整链路3.1 用 PyTorch 的 ImageFolder 加载数据三行代码解决数据管道数据组织好之后加载就是标准操作。PyTorch 的torchvision.datasets.ImageFolder可以直接按文件夹结构生成数据集配合DataLoader做批量化。这是整个项目里最不应该出错、却经常出错的地方。from torchvision import datasets, transforms from torch.utils.data import DataLoader # 定义数据增强与归一化 train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), # 随机水平翻转增加多样性 transforms.RandomRotation(10), # 随机旋转 ±10 度 transforms.ColorJitter(brightness0.2, contrast0.2), # 亮度对比度扰动 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset datasets.ImageFolder(rootdata/train, transformtrain_transform) val_dataset datasets.ImageFolder(rootdata/val, transformval_transform) test_dataset datasets.ImageFolder(rootdata/test, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) test_loader DataLoader(test_dataset, batch_size32, shuffleFalse, num_workers4)这段代码里的几个参数值得细说。Resize((224, 224))是配合 ResNet 输入尺寸的标准值如果你换用 EfficientNet 可能要用 240 或 300但 ResNet 系列统一 224 即可。num_workers4表示用 4 个子进程预取数据Windows 上如果报错就改成 0Linux/Mac 上 4 是合理值。pin_memoryTrue在 GPU 训练时能减少主机到设备的数据拷贝时间CPU 训练时无效。数据增强部分我只用了轻量级的翻转和旋转没有用 CutMix 或 MixUp——课堂行为数据的类别间差异够大不需要那么强的增强增强过猛反而会让模型学到错误的 invariance。注意ImageFolder的类别顺序是按文件夹名的字母序排列的不是你在class_names里定义的顺序。训练完之后预测时一定要用train_dataset.classes来获取类别索引映射不要自己硬编码。这是新手最容易踩的索引错位坑。3.2 构建模型ResNet50 做 backbone替换全连接层模型结构的选择上我默认用 ResNet50 而不是 ResNet18 或更深的 ResNet101。理由有三条课堂行为特征不算细粒度不需要 101 层那种超大感受野50 层的参数量在单卡 GTX 1660 或 3060 上跑起来不吃力torchvision.models.resnet50有完善的 ImageNet 预训练权重迁移学习效果好。你手上如果只有 CPU那就降到 ResNet18训练时间能缩短一半以上精度差距在 2 个百分点以内。import torch import torch.nn as nn from torchvision import models def build_model(num_classes6, pretrainedTrue): # 加载预训练模型 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2 if pretrained else None) # 获取全连接层的输入维度 in_features model.fc.in_features # 替换最后的全连接层 model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(256, num_classes) ) return model这里要把model.fc替换成自己的分类头原因很简单ImageNet 预训练的最后一层是 1000 类输出和我们的 6 类不匹配。我在中间加了两层全连接和 Dropout而不是直接换成nn.Linear(in_features, num_classes)。为什么因为课堂行为数据里“低头写字”和“低头看书”这种类间差异很小需要分类头有更强的非线性表达能力。Dropout 是我调试后确认有效的比例 0.3 比 0.5 更适合小数据集——Dropout 太强会抑制特征表达。训练时还有个关键的 trick冻结 backbone 的前几层只训练后面的层。ResNet 的前几层学到的是边缘、纹理等通用特征这些特征在 ImageNet 上和课堂图像上是通用的没必要重新学。常见做法是冻结layer1和layer2只训练layer3、layer4和分类头。def freeze_partial(model, freeze_layers[layer1, layer2]): for name, param in model.named_parameters(): if any(name.startswith(layer) for layer in freeze_layers): param.requires_grad False # 在 train.py 里这样调用 model build_model(num_classeslen(train_dataset.classes)) freeze_partial(model)冻结后你会有个直观感受显存占用降了、训练速度快了而且验证集精度通常比全量微调高 1-3 个百分点。原因是课堂数据量远少于 ImageNet全量微调容易在低层特征上发生过拟合。3.3 训练循环交叉熵损失 AdamW 余弦退火训练循环是核心中的核心。这里给出一个完整的训练脚本框架你可以直接照着写。损失函数用交叉熵优化器用 AdamW 而不是 SGD学习率调度用余弦退火而不是 StepLR——这三个选择都是在多次实验后验证过的稳定组合。import torch import torch.nn as nn import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max30, eta_min1e-6) best_val_acc 0.0 num_epochs 30 for epoch in range(num_epochs): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) epoch_loss running_loss / len(train_dataset) # 验证 model.eval() val_correct 0 val_total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) val_correct (predicted labels).sum().item() val_total labels.size(0) val_acc val_correct / val_total print(fEpoch {epoch1}/{num_epochs}, Loss: {epoch_loss:.4f}, Val Acc: {val_acc:.4f}) # 保存最优模型 if val_acc best_val_acc: best_val_acc val_acc torch.save({ epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), best_val_acc: best_val_acc, class_to_idx: train_dataset.class_to_idx }, checkpoints/best_model.pth) scheduler.step()几个参数说明lr1e-4是迁移学习的标准初始学习率如果从零训练可以设 1e-3但课堂行为项目几乎都用迁移学习不要调到 1e-3否则 loss 会震荡。weight_decay1e-4做 L2 正则化防止过拟合。T_max30要和num_epochs一致这样余弦退火的周期刚好覆盖整个训练过程学习率从 1e-4 平滑下降到 1e-6。保存模型时我把class_to_idx也存进去了这是为了推理时能准确映射类别名字避免前面说的索引错位问题。3.4 推理与评估单张图片预测与混淆矩阵分析训练完成后写一个独立的预测脚本。这个脚本读入单张图片输出类别和置信度同时可以批量跑测试集出混淆矩阵。import torch from PIL import Image from torchvision import transforms def predict_image(model, image_path, class_names, device): transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) image Image.open(image_path).convert(RGB) input_tensor transform(image).unsqueeze(0).to(device) model.eval() with torch.no_grad(): outputs model(input_tensor) probabilities torch.softmax(outputs, dim1) confidence, predicted torch.max(probabilities, 1) class_name class_names[predicted.item()] return class_name, confidence.item() # 使用时加载保存的权重 checkpoint torch.load(checkpoints/best_model.pth) model build_model(num_classes6) model.load_state_dict(checkpoint[model_state_dict]) model model.to(device) # 从 checkpoint 恢复类别映射 class_to_idx checkpoint[class_to_idx] idx_to_class {v: k for k, v in class_to_idx.items()} class_names [idx_to_class[i] for i in range(len(idx_to_class))] result, conf predict_image(model, test_images/sample1.jpg, class_names, device) print(f预测结果: {result}, 置信度: {conf:.4f})注意Image.open后要加.convert(RGB)强制转三通道因为有些手机或监控截图是 RGBA 或灰度图直接喂给模型会报维度错误。置信度输出用的是torch.softmax而不是直接取outputs的最大值因为我们需要概率值来人工判断结果是否可信。如果置信度低于 0.6我一般会标记为“无法判断”而不是强行给一个类别——这在后面避坑章节会细说。4. 训练与推理的避坑指南5 个让新手翻车的典型问题4.1 踩坑一类别不平衡导致准确率虚高现象训练集里“ sitting”坐着听讲有 2 万张“sleeping”趴桌睡觉只有 2000 张。训练完成后总准确率 85%但看混淆矩阵发现 sleeping 的召回率只有 30%大部分 sleeping 都被分成了 sitting。原因交叉熵损失对类别数量敏感多数类主导了梯度更新模型学会了“无脑猜 sitting 也能拿高准确率”的捷径。准确率指标被多数类绑架少数类完全没学好。解决第一个手段是加权损失函数给少数类更高的权重。用 PyTorch 实现很简单from sklearn.utils.class_weight import compute_class_weight import numpy as np # 统计各类别样本数 labels [train_dataset.targets[i] for i in range(len(train_dataset))] class_weights compute_class_weight(balanced, classesnp.unique(labels), ylabels) class_weights torch.tensor(class_weights, dtypetorch.float32).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)compute_class_weight会自动计算每个类别的权重样本数少的类权重高样本数多的类权重低。设置weight参数后损失函数会对少数类的错误分类施以更大的惩罚。第二个手段是数据增强时对少数类做更激进的增强比如对 sleeping 类额外做随机裁剪和颜色抖动。第三个手段是用 Focal Loss 替代交叉熵——它在交叉熵基础上引入了调制因子让模型更关注难分的样本但实现复杂一些新手先用加权交叉熵就够了。改完后 sleeping 的召回率通常能提升到 60% 以上总准确率会略有下降但整体 F1 分数更好看也更真实。4.2 踩坑二数据集划分不合理导致的“作弊”问题现象验证集准确率 92%测试集准确率却只有 70%差距巨大。或者更隐蔽的模型在验证集上表现很好但投到真实场景完全不能用。原因课堂行为数据往往是从连续视频里抽帧得到的同一个学生的相邻帧几乎一模一样。如果你在划分数据时随机打乱同一个学生的画面会同时出现在训练集和验证集里模型相当于“见过”验证集的图像——这就是数据泄露data leakage。测试集表现差是正常的因为它是真正没见过的数据。解决按“视频来源”划分数据而不是按“帧”划分。如果你的数据来自多个视频文件确保同一个视频的所有帧只出现在一个集合里。代码层面这样做# 假设你的数据标注文件里有 video_id 字段 from sklearn.model_selection import GroupShuffleSplit gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(gss.split(df, groupsdf[video_id])) train_df df.iloc[train_idx] val_df df.iloc[val_idx]如果 zip 里的数据没有video_id你只能通过文件名前缀来识别来源并在划分时按前缀分组。这个坑是课堂行为识别项目里最容易被忽略、影响最致命的——我见过很多人论文里的精度很高但实际系统上线后完全不可用原因就是数据泄露。如果你要拿这个项目交作业或者发表这个步骤一定要做对。4.3 踩坑三训练 loss 为 NaN现象训练到第 5 个 epochloss 突然变成nan验证集准确率瞬间掉到 0。原因最常见的原因是学习率过大导致梯度爆炸ResNet 的 BatchNorm 层对学习率很敏感。其次是输入数据里有 NaN 值可能是图片读取失败或者标注文件里有空值。解决首先把学习率从 1e-4 降到 3e-5 试试如果不再出现 NaN 就说明是学习率问题。其次检查数据加载部分在训练循环里加一个断言for images, labels in train_loader: assert not torch.isnan(images).any(), 输入包含 NaN assert not torch.isnan(labels).any(), 标签包含 NaN如果断言触发说明是数据问题。检查图片文件是否完整——有些 zip 包解压不完整会损坏图片ImageFolder不会主动报错但读取出来的张量可能是空的。最后如果用了混合精度训练AMP检查scaler.scale(loss)和scaler.step(optimizer)的顺序是否正确。新手不建议开 AMP等基础流程跑通后再优化也不迟。4.4 踩坑四GPU 显存不足 OOM现象CUDA out of memory报错训练中断。尤其是用 ResNet50 加 batch_size32 时在 8GB 显存的卡上很容易爆。原因ResNet50 的中间特征图占用显存很高batch_size32 加上 224x224 输入再算上梯度和优化器状态8GB 显存确实吃紧。解决最直接的办法是把batch_size降到 16 或 8。但 batch_size 变小会影响 BatchNorm 的统计量估计也可能让训练不稳定。更好的办法是开梯度累积accumulation_steps 4 # 等效 batch_size 32 / 4 8 optimizer.zero_grad() for i, (images, labels) in enumerate(train_loader): images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) / accumulation_steps # 归一化 loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()这里把损失除以accumulation_steps再反向传播这样累积 4 次梯度后再更新一次参数等效于 batch_size 乘以 4但显存占用不变。注意 BatchNorm 层在这种模式下仍然按每个小批次计算统计量所以效果不完全等同于大 batch但对这个项目来说够用了。另外一个技巧是设置torch.backends.cudnn.benchmark True这能让 cuDNN 自动选择最优卷积算法略微降低显存占用并加速训练。4.5 踩坑五模型保存和加载时的设备不一致现象训练时用 GPU保存模型后在只有 CPU 的机器上加载报错RuntimeError: Attempting to deserialize object on a CUDA device。原因torch.save(model.state_dict(), model.pth)保存的张量是绑定了 CUDA 设备的。加载时如果目标机器没有 GPU需要显式映射到 CPU。解决加载时加map_location参数checkpoint torch.load(checkpoints/best_model.pth, map_locationtorch.device(cpu))或者更通用的写法device torch.device(cuda if torch.cuda.is_available() else cpu) checkpoint torch.load(checkpoints/best_model.pth, map_locationdevice)这个坑看起来小但在你部署到服务器或换机器训练时总会踩到。另外保存 checkpoint 时建议把整个状态字典打包包括 epoch、优化器状态、类别映射而不是只保存model.state_dict()——否则后续想恢复断点续训就没法恢复了。5. 让模型真正可用的进阶技巧置信度校准、模型导出与边缘端部署5.1 置信度阈值不要无条件信任模型的输出模型训练完准确率 90% 以上看起来能用了。但真实课堂画面里总会遇到模型没见过的场景——学生戴着口罩、摄像头角度异常、画面里有老师走动。这时候模型的输出极不稳定可能出现“对着空教室给出 95% 的 sitting 置信度”这种离谱结果。解决办法是置信度阈值。在实际推理时不要直接取argmax而是先看最大概率值低于阈值就返回“无法判断”def predict_with_threshold(model, image_path, class_names, device, threshold0.6): class_name, confidence predict_image(model, image_path, class_names, device) if confidence threshold: return f无法判断置信度 {confidence:.2f} 低于阈值 {threshold} return class_name阈值怎么定我一般用验证集来标定统计所有验证样本的置信度分布找到能让“错误分类样本的置信度”和“正确分类样本的置信度”分开得最明显的那个值。画一张置信度直方图就一目了然。如果两类分布高度重叠说明模型本身校准不好可以考虑温度缩放temperature scaling——在 softmax 前除以一个温度系数 TT1 时输出分布更平滑T1 时更尖锐。用小量验证集数据学习 T 值能显著提升置信度的可靠性。5.2 模型导出为 ONNX摆脱 PyTorch 依赖训练好的模型不能一直活在 Python 进程里。实际部署时你可能要用 C 服务、用 OpenCV DNN 模块、或者部署到 Jetson 边缘设备。这时候把 PyTorch 模型导出为 ONNX 格式是标准做法。import torch model.eval() dummy_input torch.randn(1, 3, 224, 224).to(device) torch.onnx.export( model, dummy_input, classroom_behavior.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}, opset_version11 )关键参数说明dynamic_axes声明 batch 维度是动态的这样导出的模型可以一次处理任意数量的图片而不只是 1 张。opset_version建议 11 以上太低会缺失一些算子支持。导出后可以用onnxruntime验证一致性import onnxruntime as ort import numpy as np ort_session ort.InferenceSession(classroom_behavior.onnx) ort_inputs {ort_session.get_inputs()[0].name: np.random.randn(1, 3, 224, 224).astype(np.float32)} ort_outputs ort_session.run(None, ort_inputs) print(fONNX 推理输出形状: {ort_outputs[0].shape})5.3 从“分类单帧”到“统计整节课”输出层之后的业务逻辑这个项目最终的价值不在于“单帧分类准不准”而在于“整节课的行为报告有没有参考价值”。我一般会在分类模型之上再套一层统计分析逻辑。实现不复杂每 5 秒抽一帧做推理一节课 45 分钟就是 540 帧统计每个类别的占比画成时间序列折线图标出“趴桌率持续超过 30% 的时间段”。这里有个细节单帧误差经过统计平均后会大幅降低所以不需要过度追求单帧精度。与其花时间调模型到 99%不如接受 90% 的单帧精度用时间维度的平滑来提升整体系统的鲁棒性。这也是我做了几个类似项目后的经验之谈——很多新手把精力全花在把训练集准确率从 95% 拼到 96%但实际部署时多一个合理的统计逻辑比多一个点精度有用得多。5.4 如果要升级到“个体行为分析”怎么扩展最后回答一个方向性的问题如果课堂上需要统计“每个学生个体”的行为而不是整体画面怎么办两条路按预算和需求取舍。第一条路检测 分类级联。先用 YOLOv8 或 Faster R-CNN 检测出画面里的每个人裁剪出单人图像块再送到 ResNet 分类器里判断行为。这是最稳妥的方案复用已有的分类模型但要额外训练检测模型且要处理检测框抖动问题——同一个学生相邻帧的检测框不重合分类结果会不稳定。第二条路姿态估计 规则分类。用 OpenPose 或 MediaPipe 提取每个人体关键点然后根据关键点坐标写规则判断行为低头角度大于 45 度判为“低头”手部关键点在头部附近判为“举手”。这条路不需要大量标注数据但规则写起来繁琐且对遮挡和侧身情况鲁棒性差。我的建议是先做整体画面分类跑通后如果你的业务确实需要个体行为统计再往检测 分类方向演进。不要一开始就上个体分析否则你会被数据标注量拖垮。这个方向我踩过的最大一个坑就是在一开始追求“一步到位”的个体行为识别结果数据标注花了两个月模型效果还因为标注不一致而起伏不定。后来回到“整体分类 帧间平滑 阈值控制”这个最朴素的方案一周就交付了可用版本。做深度学习落地项目永远是先跑通最简方案再迭代复杂度。希望这篇拆解能帮你在拿到这份源码时少走这些弯路把时间花在真正影响交付效果的地方。本文还有配套的精品资源点击获取
