YOLOv11 在智慧课堂行为识别项目

目录


项目概述

本教程演示如何使用 YOLOv11 在智慧课堂场景下实现 学生行为识别(举手、聊天、饮水、写作、使用手机、空闲等 6 类)。项目结构位于工作区 D:\OpenClaw_Local\.openclaw_state\workspace\yolo11,核心代码包括:

  • train_action.py – 训练入口
  • final.yaml – 数据集配置
  • gpu.pyjiance.py – GPU 管理 & 检测脚本
  • video_Clip.pyvideo_cut.py – 视频切片工具

目标:读者只需按照本文步骤即可完成从数据收集、标注、训练到推理的全流程。


一、环境准备

适用于 Windows 10/11(PowerShell)和 Linux(bash)
推荐使用 Anaconda 创建独立环境,以免与系统库冲突。

# 1. 创建并激活 conda 环境
conda create -n yolo11_env python=3.10 -y
conda activate yolo11_env

# 2. 安装核心依赖(ultralytics 包包含 YOLOv11)
pip install ultralytics==8.2.20  # 兼容 YOLOv11
pip install opencv-python tqdm pandas seaborn matplotlib

# 3. 验证 GPU(可选)
python -c "import torch; print('CUDA available:', torch.cuda.is_available())"

⚠️ 注意:如果机器无 GPU,训练会自动回落到 CPU,速度会显著下降。建议使用至少 6GB 显存 的显卡(如 RTX 3060)。


二、数据准备

数据来源:可参考中的数据集(参考文献),包括课堂录像、行为标注文件 (.txt)、关键点标注 (.json)。
目录结构(参考 final.yaml):

yolo11/
├─ D:/yolo11_test/
│   ├─ train_v2.txt   # 训练图片路径列表
│   ├─ images/        # 所有图片/帧
│   └─ labels/        # YOLO 格式 txt 标注
├─ final.yaml          # 数据集配置文件

2.1 数据集根目录

final.yaml 中已配置:

path: D:/yolo11_test/final_project   # 数据集根目录
train: D:/yolo11_test/train_v2.txt    # 训练集列表(相对路径)
val: images                           # 验证集(暂用与训练相同)
names:
  0: focused
  1: chatting
  2: drinking
  3: writing
  4: phone
  5: null
kpt_shape: [7, 3]                     # 7 个关键点,每点 (x, y, visibility)
skeleton: [[1,2],[0,1],[0,2],[3,1],[4,2],[5,0],[6,0]]

如何生成 train_v2.txt:将所有标注图片的相对路径写入每行,例如:

images/img_00001.jpg
images/img_00002.jpg
...

2.2 标注格式

  • YOLO 检测标注:每行 class_id cx cy w h(相对坐标)。
  • 关键点标注(姿态检测)保存在 labels/*.txt 中,格式:
class_id x1 y1 v1 x2 y2 v2 ... x7 y7 v7

标注工具推荐labelImg(检测) + labelme(关键点),或使用cvat标注平台

2.3 数据预处理脚本(可选)

仓库自带 auto_label.py,用于自动从视频中抽帧并生成 train_v2.txt

python auto_label.py \
    --video D:/raw_videos/classroom1.mp4 \
    --output_dir D:/yolo11_test/images \
    --interval 30   # 每 30 帧抽一帧

三、模型训练

下面演示使用 train_action.py(已封装的训练入口)进行训练。

3.1 关键配置(train_action.py

from ultralytics import YOLO

if __name__ == '__main__':
    # 加载预训练模型(YOLOv11 预训练权重)
    model = YOLO(r"D:\yolo11_test\runs\pose\classroom_action_v1\weights\best.pt")

    results = model.train(
        data="final.yaml",   # 数据集配置文件
        epochs=150,
        imgsz=640,
        batch=8,
        device=0,            # GPU ID,若无 GPU 改为 "cpu"
        name="classroom_v2_final",  # 结果保存目录名
        scale=0.9,           # 数据增强:随机缩放
        mosaic=1.0,
        mixup=0.1,
        optimizer="auto",
        patience=100,        # 早停策略
        cache=False,
    )

3.2 运行训练

# 进入项目根目录
cd D:\OpenClaw_Local\.openclaw_state\workspace\yolo11

# 执行训练脚本
python train_action.py

日志位置runs/pose/classroom_v2_final/ 目录下会生成 weights/best.pt(最佳模型)以及训练日志 results.csvevents.out.tfevents...

3.3 训练期间常用参数解释

参数含义
epochs训练轮数,默认 150。
batch每批样本数,显存足够时可调大。
scale随机缩放比例(0.9 表示 90%~110%)。
mosaicMosaic 增强,拼接 4 张图提升小目标检测。
mixupMixUp 增强,混合两张图。
patience若验证集 mAP 连续 patience 次未提升则提前终止。

四、模型推理

训练完成后,可使用 jiance.py 对单张图片或视频进行行为检测。

4.1 单张图片推理

# jiance.py 示例
from ultralytics import YOLO
import cv2

model = YOLO(r"D:\OpenClaw_Local\.openclaw_state\workspace\yolo11\runs\pose\classroom_v2_final\weights\best.pt")
img = cv2.imread(r"D:\yolo11_test\images\sample1.jpg")
results = model(img, save=True, show=True)  # 保存至 runs/detect/predict/
  • 输出runs/detect/predict/ 中会生成带检测框和关键点的图片 sample1.jpg

4.2 视频推理(实时)

python jiance.py \
    --source D:/raw_videos/classroom_test.mp4 \
    --save True

jiance.py 参数可参考文件头部注释(已在源码中注明)。


五、结果可视化与部署

5.1 结果可视化(Matplotlib)

可以使用 poseggjj.py 绘制关键点热力图。

import matplotlib.pyplot as plt
from poseggjj import draw_keypoints

img = cv2.imread('sample1.jpg')
kp = results[0].keypoints  # (7,3)
draw_keypoints(img, kp)
plt.show()

5.2 pyqt6界面展示

下面给出一个完整的代码,供 CSDN 读者快速部署。

import sys
import os
import cv2
import csv
import numpy as np
import requests
import matplotlib.pyplot as plt
from datetime import datetime
from PyQt6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QPushButton, QLabel, QFileDialog, QComboBox, QMessageBox, QFrame, QTextEdit
)
from PyQt6.QtGui import QImage, QPixmap
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from ultralytics import YOLO

# ==========================================
#                  程序整体架构
#a.AI 逻辑线程:异步调用本地部署的 DeepSeek-R1 模型生成教学建议
#b.视觉处理线程:实时处理视频流并进行行为识别
#c.主程序界面:集成所有功能,提供用户交互界面
#
# ==========================================
# 模块一:AI 逻辑线程 (DeepSeek-R1-1.5B)
# ==========================================
class OllamaThread(QThread):
    """异步调用 Ollama,防止大模型推理时卡住 UI 界面"""
    advice_ready = pyqtSignal(str) # 自定义信号,用于传递AI建议

    def __init__(self, stats):
        super().__init__()
        self.stats = stats # 接收行为统计数据

    def run(self):
        url = "http://localhost:11434/api/generate"
        # 针对 1.5B 模型优化的 Prompt
        prompt = (
            f"当前课堂统计:专注{self.stats['focused']}人,写字{self.stats['writing']}人,"
            f"讨论{self.stats['chatting']}人,玩手机{self.stats['phone']}人。"
            f"请以教育专家的身份,给老师一条 50 字以内的改进建议。"
        )
        try:
            payload = {
                "model": "deepseek-r1:1.5b",  # 更换为适合 3050 的 1.5B 版本
                "prompt": prompt,
                "stream": False,
                "options": {"num_ctx": 512}  # 上下文长度512
            }
            # 发送HTTP POST请求到Ollama本地服务
            response = requests.post(url, json=payload, timeout=30)
            if response.status_code == 200:
                full_text = response.json().get("response", "")
                # 过滤掉思维链 <think> 标签
                clean_text = full_text.split("</think>")[-1].strip() if "</think>" in full_text else full_text
                self.advice_ready.emit(clean_text)
            else:
                self.advice_ready.emit("AI 服务忙碌,请稍后再试。")
        except Exception as e:
            self.advice_ready.emit(f"AI 连接异常: {str(e)}")


# ==========================================
# 模块二:视觉处理线程 (YOLOv11)
# ==========================================
class VideoThread(QThread):
    """高性能视频流处理线程"""
    change_pixmap_signal = pyqtSignal(np.ndarray, dict) # 信号:传递处理后的帧和统计结果

    def __init__(self, source=0, model=None):
        super().__init__()
        self.source = source # 视频源:0-摄像头,或文件路径
        self.running = True # 线程运行控制标志
        self.model = model # YOLO模型实例

    def run(self):
        cap = cv2.VideoCapture(self.source) # 打开视频源
        while self.running:
            ret, frame = cap.read() # 读取一帧
            if not ret: break

            # 执行推理
            annotated_frame, counts = ClassroomApp.run_inference(frame, self.model)
            # 发射信号,传递处理结果
            self.change_pixmap_signal.emit(annotated_frame, counts)
        cap.release() # 释放资源

    def stop(self):
        self.running = False
        self.wait()


# ==========================================
# 模块三:主程序界面与核心逻辑
# ==========================================
class ClassroomApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("智慧课堂行为监测系统 - 毕业设计演示版")
        self.setMinimumSize(1400, 900) # 设置最小窗口尺寸

        #  初始化模型
        self.model = YOLO(r"D:\yolo11_test\runs\pose\classroom_v2_final\weights\best.pt")
        self.last_counts = {"focused": 0, "chatting": 0, "writing": 0, "phone": 0}

        self.init_ui()

    def init_ui(self):
        """UI 布局初始化"""
        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        main_layout = QHBoxLayout(main_widget)

        # --- 左侧:实时视频展示区 ---
        left_layout = QVBoxLayout()
        self.display_label = QLabel("等待接入视频源...")
        self.display_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.display_label.setStyleSheet("background-color: #2c3e50; color: white; border-radius: 8px;")
        left_layout.addWidget(self.display_label, stretch=10)

        # 视频控制工具条
        ctrl_frame = QFrame()
        ctrl_layout = QHBoxLayout(ctrl_frame)
        self.source_select = QComboBox()
        self.source_select.addItems(["本地摄像头 (0)", "视频文件分析..."])
        self.btn_start = QPushButton("📹 开始监测")
        self.btn_stop = QPushButton("🛑 停止")
        self.btn_test_img = QPushButton("🖼️ 图片测试")

        for btn in [self.btn_start, self.btn_stop, self.btn_test_img]:
            ctrl_layout.addWidget(btn)
        ctrl_layout.insertWidget(0, self.source_select)
        left_layout.addWidget(ctrl_frame)

        # --- 右侧:数据看板与 AI 建议区 ---
        right_layout = QVBoxLayout()

        # 实时统计卡片
        right_layout.addWidget(QLabel("📊 行为实时统计"))
        self.stats_label = QLabel("等待数据...")
        self.stats_label.setStyleSheet(
            "font-size: 15px; color: #2c3e50; background: #ecf0f1; padding: 15px; border-radius: 5px;")
        right_layout.addWidget(self.stats_label)

        # AI 建议展示
        right_layout.addSpacing(20)
        right_layout.addWidget(QLabel("🤖 AI 专家实时分析"))
        self.ai_box = QTextEdit()
        self.ai_box.setReadOnly(True)
        self.ai_box.setStyleSheet("font-size: 13px; color: #2c3e50; background: #fdfefe; border: 1px solid #dcdfe6;")
        right_layout.addWidget(self.ai_box)

        # 功能按钮区
        self.btn_get_ai = QPushButton("🧠 获取 AI 分析建议")
        self.btn_get_ai.setStyleSheet("background: #27ae60;")
        self.btn_show_chart = QPushButton("📈 生成统计图表")
        self.btn_show_chart.setStyleSheet("background: #8e44ad;")

        right_layout.addWidget(self.btn_get_ai)
        right_layout.addWidget(self.btn_show_chart)

        main_layout.addLayout(left_layout, stretch=7)
        main_layout.addLayout(right_layout, stretch=3)

        # 信号绑定
        self.btn_start.clicked.connect(self.start_video_logic)
        self.btn_stop.clicked.connect(self.stop_video_logic)
        self.btn_test_img.clicked.connect(self.test_single_image)
        self.btn_get_ai.clicked.connect(self.request_ai_advice)
        self.btn_show_chart.clicked.connect(self.show_analysis_chart)

    # ==========================================
    # 模块四:核心功能函数 (检测/存盘/绘图)
    # ==========================================
    @staticmethod
    def run_inference(frame, model):
        """高精度推理:imgsz=1280 保证人全检出"""
        results = model.predict(
            source=frame,
            imgsz=1280,   # 大尺寸输入,提高小目标检测精度
            conf=0.25,    # 置信度阈值
            iou=0.45,     # NMS的IoU阈值
            verbose=False # 不输出详细信息
        )

        # 统计四类行为数量
        counts = {"focused": 0, "chatting": 0, "writing": 0, "phone": 0}
        for box in results[0].boxes:
            label = model.names[int(box.cls[0])] # 获取类别名称
            if label in counts: counts[label] += 1
        return results[0].plot(), counts # 返回标注后的图像和统计结果

    def save_to_csv(self, stats, advice):
        """自动存档功能:为论文提供原始实验数据"""
        file_path = "classroom_analysis_log.csv"
        file_exists = os.path.isfile(file_path)
        try:
            with open(file_path, "a", newline="", encoding="utf-8-sig") as f:
                writer = csv.writer(f)
                if not file_exists:  # 文件不存在时写入表头
                    writer.writerow(["时间", "总人数", "听课", "写字", "讨论", "手机", "AI建议"])
                now = datetime.now().strftime("%H:%M:%S")
                # 写入一行数据
                writer.writerow([now, sum(stats.values()), stats['focused'],
                                 stats['writing'], stats['chatting'], stats['phone'], advice.replace("\n", " ")])# 替换换行符,避免CSV格式错误
        except Exception as e:
            print(f"存档失败: {e}")

    def show_analysis_chart(self):
        """数据可视化:弹窗展示当前课堂比例"""
        labels = ['专注', '写字', '讨论', '手机']
        sizes = [self.last_counts['focused'], self.last_counts['writing'],
                 self.last_counts['chatting'], self.last_counts['phone']]
        if sum(sizes) == 0: return

        plt.rcParams['font.sans-serif'] = ['SimHei']  # 解决中文乱码设置,SimHei字体解决matplotlib中文显示问题
        plt.figure("课堂行为比例分析", figsize=(6, 5))
        plt.pie(sizes, labels=labels, autopct='%1.1f%%', startangle=140,
                colors=['#3498db', '#2ecc71', '#f1c40f', '#e74c3c'])#蓝色-专注,绿色-写字,黄色-讨论,红色-手机
        plt.title(f"当前课堂状态 (样本人数: {sum(sizes)})")
        plt.show()

    def update_display(self, frame, stats):
        """更新 UI 显示内容"""
        self.last_counts = stats# 保存最新统计

        # OpenCV图像转Qt图像
        h, w, ch = frame.shape
        q_img = QImage(frame.data, w, h, ch * w, QImage.Format.Format_BGR888)

        # 缩放并显示
        self.display_label.setPixmap(
            QPixmap.fromImage(q_img).scaled(
            self.display_label.size(),
                Qt.AspectRatioMode.KeepAspectRatio
            )
        )

        # 更新统计标签
        total = sum(stats.values())
        self.stats_label.setText(
            f"<b>总人数: {total}</b><br>听课: {stats['focused']} | 写字: {stats['writing']}<br>讨论: {stats['chatting']} | 手机: {stats['phone']}")

    def request_ai_advice(self):
        """获取 AI 建议并自动存档"""
        self.ai_box.setText("AI 正在根据实时数据生成建议...")
        self.btn_get_ai.setEnabled(False)
        self.ai_thread = OllamaThread(self.last_counts)
        self.ai_thread.advice_ready.connect(self.on_ai_ready)# AI线程信号连接
        self.ai_thread.start()

    def on_ai_ready(self, advice):
        self.ai_box.setText(advice)
        self.btn_get_ai.setEnabled(True)
        self.save_to_csv(self.last_counts, advice)  # 自动触发存档

    # --- 视频与测试图片切换逻辑 ---
    def start_video_logic(self):
        source = 0
        if self.source_select.currentIndex() == 1:
            fname, _ = QFileDialog.getOpenFileName(self, "选择视频文件")
            if fname:
                source = fname
            else:
                return
        self.video_thread = VideoThread(source, self.model)
        self.video_thread.change_pixmap_signal.connect(self.update_display)# 视频线程信号连接
        self.video_thread.start()
        self.btn_start.setEnabled(False)
        self.btn_stop.setEnabled(True)

    def stop_video_logic(self):
        if hasattr(self, 'video_thread') and self.video_thread.isRunning():
            self.video_thread.stop()
        self.btn_start.setEnabled(True)
        self.btn_stop.setEnabled(False)

    def test_single_image(self):
        fname, _ = QFileDialog.getOpenFileName(self, '选择测试图片', '', 'Images (*.jpg *.png)')
        if fname:
            self.stop_video_logic()
            img = cv2.imread(fname)
            if img is not None:
                res_img, counts = self.run_inference(img, self.model)
                self.update_display(res_img, counts)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    # 全局样式设置
    app.setStyleSheet("QPushButton { height: 35px; border-radius: 5px; color: white; font-weight: bold; } "
                      "QPushButton#btn_start { background: #3498db; }")
    window = ClassroomApp()
    window.show()
    sys.exit(app.exec())

运行python v_ui.py,上传视频或图片即可得到检测结果。


六、常见问题 & FAQ

问题解决方案
显存不足降低 batch(如 4)或 imgsz(如 416),开启 --cache=False
训练卡在某一步检查 final.yaml 中路径是否全部可访问;确认 train_v2.txt 中无空行
关键点不对齐确保标注时使用相同分辨率;kpt_shape 必须保持 7×3
模型推理报错 torch.cuda.OutOfMemoryErrordevice='cpu' 或使用 torch.cuda.empty_cache() 手动释放显存
保存路径为空model.train(..., save=True) 默认保存到 runs/pose/<name>/weights/,可在 results.save_dir 查看

七、参考资源

  • YOLOv11 官方文档: https://github.com/ultralytics/ultralytics
  • aistudio 飞桨:https://aistudio.baidu.com
  • 姿态关键点标注工具:https://app.cvat.ai
  • GitHub 示例仓库: https://github.com/ggjj-hub/yolo-classroom-headup-rate

🎉 完成

按照本指南,你可以快速完成:

  1. 环境搭建✅
  2. 数据准备✅
  3. 模型训练✅
  4. 推理与可视化✅
  5. 简易部署✅

如果还有细节需要补充(例如 数据集的具体标注说明),请直接在本文下方留言,我会进一步完善。祝你实验顺利,项目成功!

更多推荐