1、 课程设计题目:

YOLO目标检测、OpenCV视频分析;
主要功能:检测睡觉、玩手机、学生行为统计

2、 课题分析设计思路

2.1 课题背景与目标

课堂大学生行为的自动识别对于教学评估、学生状态监测具有重要意义。传统人工观察效率低且主观性强,无法实现大规模、实时化的课堂行为分析。随着深度学习技术的快速发展,目标检测算法在计算机视觉领域取得了突破性进展,为课堂行为自动识别提供了技术基础。
本项目旨在利用深度学习目标检测技术,构建一个能够自动识别课堂中学生人数统计、玩手机、睡觉三类行为的检测系统。系统最终能够对图片、视频或摄像头实时流进行检测,并在图形界面中直观展示检测结果,帮助教师及时了解课堂状态,提升教学管理效率。

2.2 整体设计流程

系统整体分为三大模块,形成完整的深度学习项目开发流程:

  1. 数据集构建:采集课堂场景图像,使用LabelImg工具手动标注三类目标(人数、玩手机、睡觉),生成YOLO格式的标签文件,并按训练集130张、验证集50张划分数据集。
  2. 模型训练:基于YOLOv8n预训练模型,在自建数据集上进行迁移学习。训练过程中调整图像尺寸、批次大小、数据增强等超参数,并监控损失函数和评估指标变化,确保模型收敛。
  3. 检测与可视化:训练得到最佳模型后,利用PyQt5开发桌面应用程序,实现模型加载、图像/视频/摄像头检测、检测结果表格展示、实时行为统计等功能,提供友好的用户交互界面。

2.3 数据集分析

  • 数据来源:课堂监控视频截图及公开课堂图像,共收集180张原始图像,其中训练集130张,验证集50张,确保训练数据与验证数据的合理比例。
  • 标注类别:共3类,分别为人数(class_id=0)、玩手机(class_id=1)、睡觉(class_id=2)。标注过程中严格遵循目标检测标注规范,确保标注框准确包围目标区域。
  • 标注格式:采用YOLO格式,每张图片对应一个.txt文件,每行包含class_id x_center y_center width height,坐标进行归一化处理,便于模型训练。
  • 数据增强:训练时采用YOLOv8默认增强策略,包括随机翻转、色彩抖动、马赛克(Mosaic)等数据增强方法,有效提升模型的泛化能力,减少过拟合风险。

在这里插入图片描述

在这里插入图片描述

2.4 使用模型

采用YOLOv8n作为基础模型。YOLOv8是Ultralytics公司提出的单阶段目标检测算法,具有速度快、精度高、易部署等优点。n系列为轻量级版本,适合在CPU或低功耗GPU上实时运行。
模型结构包括三个主要部分:Backbone、Neck和Head。Backbone采用CSPDarknet结构,用于提取图像特征;Neck采用FPN+PAN结构,实现多尺度特征融合;Head采用解耦检测头,分别预测类别和边界框,提升检测精度。

2.5 预期效果

能够准确识别图像/视频中的学生个体,并统计课堂总人数,为考勤管理提供数据支持。
能够检测出学生使用手机的行为,并给出警告提示,帮助教师及时发现课堂不专注行为。
能够检测出学生趴桌睡觉的行为,并予以统计,便于教师掌握学生课堂状态。
图形界面友好,支持模型切换、参数调节、结果保存等功能,满足不同使用场景需求。

3、 源程序

本项目源程序由多个Python文件组成,主要包括:
1.py:主程序,实现PyQt5界面、训练线程、检测逻辑,是系统的核心入口文件。
1.py:独立训练脚本,用于命令行模式下的模型训练,支持超参数配置。
data.yaml:数据集配置文件,指定数据集路径、类别数及类别名称,是YOLO训练的必要配置。
由于代码量较大,源程序以附件形式提交(所有.py文件及data.yaml打包为压缩包)。
核心代码片段(模型训练)

from ultralytics import YOLO

model = YOLO('yolov8n.pt')
model.train(
    data='data.yaml',
    epochs=100,
    imgsz=640,
    batch=16,
    device='0',
    workers=0,
    name='classroom_model'
)

核心检测界面类结构(部分)
class ClassroomSystem(QMainWindow):
CLASS_NAMES = {0: ‘人数’, 1: ‘玩手机’, 2: ‘睡觉’}

def load_model(self):
    """加载训练好的YOLO模型"""
    pass

def detect_image(self):
    """图像检测逻辑"""
    pass

def update_statistics(self):
    """更新检测统计信息"""
    pass
源代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
课堂行为检测系统 - 完整单文件版本
包含训练和检测功能
"""

import sys
import os
import cv2
import json
import threading
from datetime import datetime
from pathlib import Path

from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QGroupBox, QLabel, QPushButton, QComboBox, QSlider, QSpinBox,
    QFormLayout, QTableWidget, QTableWidgetItem, QHeaderView,
    QFileDialog, QMessageBox, QStatusBar, QTabWidget, QTextEdit,
    QProgressBar, QLineEdit, QInputDialog, QStyledItemDelegate
)

# 尝试导入 YOLO
try:
    from ultralytics import YOLO
    YOLO_AVAILABLE = True
except ImportError:
    YOLO_AVAILABLE = False
    print("警告:未安装 ultralytics,请运行: pip install ultralytics")


class CenteredDelegate(QStyledItemDelegate):
    """表格内容居中显示的代理类"""
    
    def initStyleOption(self, option, index):
        super().initStyleOption(option, index)
        option.displayAlignment = Qt.AlignCenter


class TrainingWorker(QThread):
    """训练工作线程,防止界面卡顿"""
    log_signal = pyqtSignal(str)      # 日志输出
    progress_signal = pyqtSignal(int)  # 进度更新
    finished_signal = pyqtSignal(str)  # 训练完成,返回模型路径
    
    def __init__(self, data_yaml, epochs, imgsz, batch, device, name):
        super().__init__()
        self.data_yaml = data_yaml
        self.epochs = epochs
        self.imgsz = imgsz
        self.batch = batch
        self.device = device
        self.name = name
        self.is_cancelled = False
        
    def run(self):
        if not YOLO_AVAILABLE:
            self.log_signal.emit("错误:未安装 ultralytics,无法训练。")
            return
            
        try:
            self.log_signal.emit(f"开始训练,数据集:{self.data_yaml}")
            self.log_signal.emit(f"参数:epochs={self.epochs}, imgsz={self.imgsz}, batch={self.batch}, device={self.device}")
            
            model = YOLO("yolov8n.pt")
            results = model.train(
                data=self.data_yaml,
                epochs=self.epochs,
                imgsz=self.imgsz,
                batch=self.batch,
                device=self.device,
                name=self.name,
                verbose=True
            )
            
            model_path = f"runs/detect/{self.name}/weights/best.pt"
            self.log_signal.emit(f"训练完成!最佳模型保存在:{model_path}")
            self.finished_signal.emit(model_path)
            
        except Exception as e:
            self.log_signal.emit(f"训练失败:{str(e)}")


class ClassroomSystem(QMainWindow):
    """课堂行为检测系统主窗口"""
    
    # 类别名称映射
    CLASS_NAMES = {
        0: '人数', 1: '玩手机', 2: '睡觉',
    }

    def browse_model_file(self):

        file_path, _ = QFileDialog.getOpenFileName(
        self,
        "选择模型文件",
        "",
        "PyTorch模型 (*.pt);;所有文件 (*)"
    )
        if file_path:
        # 将路径中的反斜杠转换为正斜杠(可选,但更规范)
            file_path = file_path.replace('\\', '/')
        # 将路径设置到下拉框的编辑框中
        self.model_combo.setEditText(file_path)
        # 可选:自动加载模型(去掉注释即可自动加载)
        # self.load_model()
    def __init__(self):
        super().__init__()
        self.model = None           # YOLO 模型
        self.cap = None             # 视频捕获
        self.timer = QTimer()       # 定时器
        self.is_detecting = False   # 检测状态
        self.current_result = None  # 当前检测结果
        self.frame_counter = 0      # 帧计数器
        self.train_worker = None    # 训练线程
        
        self.init_ui()
        self.init_signals()
        
    def init_ui(self):
        """初始化界面"""
        self.setWindowTitle("课堂行为检测系统")
        self.setGeometry(100, 100, 1400, 900)
        
        # 主标签页
        self.tab_widget = QTabWidget()
        self.setCentralWidget(self.tab_widget)
        
        # 创建标签页
        self.tab_widget.addTab(self.create_train_tab(), "📊 模型训练")
        self.tab_widget.addTab(self.create_detect_tab(), "🎯 实时检测")
        
        # 状态栏
        self.statusBar = QStatusBar()
        self.setStatusBar(self.statusBar)
        self.statusBar.showMessage("就绪 | 请先安装 ultralytics: pip install ultralytics")
        
    def create_train_tab(self):
        """创建训练标签页"""
        tab = QWidget()
        layout = QHBoxLayout(tab)
        
        # 左侧:参数配置
        left_widget = QWidget()
        left_layout = QVBoxLayout()
        
        # 数据集配置
        data_group = QGroupBox("数据集配置")
        data_form = QFormLayout()
        self.data_yaml_edit = QLineEdit()
        self.data_yaml_edit.setPlaceholderText("例如: datasets/data.yaml")
        btn_browse = QPushButton("浏览...")
        data_layout = QHBoxLayout()
        data_layout.addWidget(self.data_yaml_edit)
        data_layout.addWidget(btn_browse)
        data_form.addRow("数据集文件:", data_layout)
        data_group.setLayout(data_form)
        left_layout.addWidget(data_group)
        
        # 训练参数
        train_group = QGroupBox("训练参数")
        train_form = QFormLayout()
        
        self.epochs_spin = QSpinBox()
        self.epochs_spin.setRange(1, 500)
        self.epochs_spin.setValue(50)
        train_form.addRow("训练轮数:", self.epochs_spin)
        
        self.imgsz_spin = QSpinBox()
        self.imgsz_spin.setRange(320, 1280)
        self.imgsz_spin.setValue(640)
        train_form.addRow("图像尺寸:", self.imgsz_spin)
        
        self.batch_spin = QSpinBox()
        self.batch_spin.setRange(1, 64)
        self.batch_spin.setValue(16)
        train_form.addRow("批次大小:", self.batch_spin)
        
        self.device_edit = QLineEdit("0")
        self.device_edit.setPlaceholderText("cpu 或 0,1")
        train_form.addRow("训练设备:", self.device_edit)
        
        self.name_edit = QLineEdit("classroom_model")
        train_form.addRow("实验名称:", self.name_edit)
        
        train_group.setLayout(train_form)
        left_layout.addWidget(train_group)
        
        # 控制按钮
        control_group = QGroupBox("训练控制")
        control_layout = QVBoxLayout()
        self.train_btn = QPushButton("🚀 开始训练")
        self.cancel_btn = QPushButton("⏹️ 取消训练")
        self.cancel_btn.setEnabled(False)
        btn_layout = QHBoxLayout()
        btn_layout.addWidget(self.train_btn)
        btn_layout.addWidget(self.cancel_btn)
        control_layout.addLayout(btn_layout)
        
        self.train_progress = QProgressBar()
        control_layout.addWidget(self.train_progress)
        control_group.setLayout(control_layout)
        left_layout.addWidget(control_group)
        
        left_widget.setLayout(left_layout)
        
        # 右侧:日志输出
        right_widget = QWidget()
        right_layout = QVBoxLayout()
        
        log_group = QGroupBox("训练日志")
        log_layout = QVBoxLayout()
        self.log_text = QTextEdit()
        self.log_text.setReadOnly(True)
        log_layout.addWidget(self.log_text)
        log_group.setLayout(log_layout)
        right_layout.addWidget(log_group)
        
        right_widget.setLayout(right_layout)
        
        # 添加到主布局
        layout.addWidget(left_widget, stretch=1)
        layout.addWidget(right_widget, stretch=1)
        
        # 绑定事件
        btn_browse.clicked.connect(self.browse_data_yaml)
        self.train_btn.clicked.connect(self.start_training)
        self.cancel_btn.clicked.connect(self.cancel_training)
        
        return tab
    
    def create_detect_tab(self):
        """创建检测标签页"""
        tab = QWidget()
        layout = QHBoxLayout(tab)
        
        # 左侧:图像显示
        left_widget = QWidget()
        left_layout = QVBoxLayout()
        
        # 原始图像
        self.original_group = QGroupBox("原始图像")
        self.original_label = QLabel()
        self.original_label.setAlignment(Qt.AlignCenter)
        self.original_label.setText("等待加载图像/视频...")
        self.original_label.setStyleSheet("background-color: #2B2B2B; border: 1px solid #555; color: white;")
        self.original_label.setMinimumSize(640, 480)
        orig_layout = QVBoxLayout()
        orig_layout.addWidget(self.original_label)
        self.original_group.setLayout(orig_layout)
        left_layout.addWidget(self.original_group)
        
        # 检测结果
        self.result_group = QGroupBox("检测结果")
        self.result_label = QLabel()
        self.result_label.setAlignment(Qt.AlignCenter)
        self.result_label.setText("检测结果将显示在这里")
        self.result_label.setStyleSheet("background-color: #2B2B2B; border: 2px solid #4CAF50; color: white;")
        self.result_label.setMinimumSize(640, 480)
        result_layout = QVBoxLayout()
        result_layout.addWidget(self.result_label)
        self.result_group.setLayout(result_layout)
        left_layout.addWidget(self.result_group)
        
        left_widget.setLayout(left_layout)
        layout.addWidget(left_widget, stretch=3)
        
        # 右侧:控制面板
        right_widget = QWidget()
        right_layout = QVBoxLayout()
        
        # 模型加载
        model_group = QGroupBox("模型设置")
        model_layout = QVBoxLayout()
        h1 = QHBoxLayout()
        h1.addWidget(QLabel("检测模型:"))
        self.model_combo = QComboBox()
        self.model_combo.setEditable(True)
        self.model_combo.addItems(["best.pt", "yolov8n.pt"])
        h1.addWidget(self.model_combo)

        # 添加浏览按钮
        self.browse_model_btn = QPushButton("浏览...")
        self.browse_model_btn.clicked.connect(self.browse_model_file)
        h1.addWidget(self.browse_model_btn)

        self.load_btn = QPushButton("加载模型")
        h1.addWidget(self.load_btn)
        model_layout.addLayout(h1)
        self.model_info = QLabel("未加载模型")
        self.model_info.setWordWrap(True)
        model_layout.addWidget(self.model_info)
        model_group.setLayout(model_layout)
        right_layout.addWidget(model_group)
        
        # 检测参数
        param_group = QGroupBox("检测参数")
        param_layout = QFormLayout()
        
        # 置信度滑块
        conf_layout = QHBoxLayout()
        self.conf_slider = QSlider(Qt.Horizontal)
        self.conf_slider.setRange(1, 99)
        self.conf_slider.setValue(25)
        self.conf_label = QLabel("0.25")
        conf_layout.addWidget(self.conf_slider)
        conf_layout.addWidget(self.conf_label)
        param_layout.addRow("置信度阈值:", conf_layout)
        
        # IoU 滑块
        iou_layout = QHBoxLayout()
        self.iou_slider = QSlider(Qt.Horizontal)
        self.iou_slider.setRange(1, 99)
        self.iou_slider.setValue(45)
        self.iou_label = QLabel("0.45")
        iou_layout.addWidget(self.iou_slider)
        iou_layout.addWidget(self.iou_label)
        param_layout.addRow("IoU 阈值:", iou_layout)
        
        # 检测间隔
        self.detect_interval_spin = QSpinBox()
        self.detect_interval_spin.setRange(1, 10)
        self.detect_interval_spin.setValue(2)
        param_layout.addRow("检测间隔(帧):", self.detect_interval_spin)
        
        param_group.setLayout(param_layout)
        right_layout.addWidget(param_group)
        
        # 功能按钮
        func_group = QGroupBox("检测控制")
        func_layout = QVBoxLayout()
        self.image_btn = QPushButton("📷 图片检测")
        self.video_btn = QPushButton("🎬 视频检测")
        self.camera_btn = QPushButton("📸 摄像头检测")
        self.stop_btn = QPushButton("⏹️ 停止检测")
        self.save_btn = QPushButton("💾 保存结果")
        self.stop_btn.setEnabled(False)
        self.save_btn.setEnabled(False)
        for btn in [self.image_btn, self.video_btn, self.camera_btn, self.stop_btn, self.save_btn]:
            func_layout.addWidget(btn)
        func_group.setLayout(func_layout)
        right_layout.addWidget(func_group)
        
        # 统计面板
        stats_group = QGroupBox("实时统计")
        stats_layout = QVBoxLayout()
        self.stats_label = QLabel("等待检测...")
        self.stats_label.setStyleSheet("background-color: #263238; color: white; padding: 10px; border-radius: 5px;")
        self.stats_label.setWordWrap(True)
        stats_layout.addWidget(self.stats_label)
        stats_group.setLayout(stats_layout)
        right_layout.addWidget(stats_group)
        
        # 结果表格
        table_group = QGroupBox("检测详情")
        table_layout = QVBoxLayout()
        self.result_table = QTableWidget()
        self.result_table.setColumnCount(5)
        self.result_table.setHorizontalHeaderLabels(["类别", "置信度", "位置(X,Y)", "大小(W×H)", "状态"])
        self.result_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        delegate = CenteredDelegate(self.result_table)
        self.result_table.setItemDelegate(delegate)
        table_layout.addWidget(self.result_table)
        table_group.setLayout(table_layout)
        right_layout.addWidget(table_group, stretch=1)
        
        right_widget.setLayout(right_layout)
        layout.addWidget(right_widget, stretch=2)
        
        # 绑定事件
        self.load_btn.clicked.connect(self.load_model)
        self.image_btn.clicked.connect(self.detect_image)
        self.video_btn.clicked.connect(self.detect_video)
        self.camera_btn.clicked.connect(self.detect_camera)
        self.stop_btn.clicked.connect(self.stop_detection)
        self.save_btn.clicked.connect(self.save_result)
        self.conf_slider.valueChanged.connect(lambda v: self.conf_label.setText(f"{v/100:.2f}"))
        self.iou_slider.valueChanged.connect(lambda v: self.iou_label.setText(f"{v/100:.2f}"))
        self.timer.timeout.connect(self.update_frame)
        
        return tab
    
    def init_signals(self):
        """初始化信号连接"""
        pass
    
    def browse_data_yaml(self):
        """浏览数据集配置文件"""
        path, _ = QFileDialog.getOpenFileName(self, "选择 data.yaml", "", "YAML files (*.yaml *.yml)")
        if path:
            self.data_yaml_edit.setText(path)
    
    def start_training(self):
        """开始训练"""
        if not self.data_yaml_edit.text():
            QMessageBox.warning(self, "警告", "请先选择数据集配置文件")
            return
        if not os.path.exists(self.data_yaml_edit.text()):
            QMessageBox.warning(self, "警告", "数据集配置文件不存在")
            return
        
        self.train_btn.setEnabled(False)
        self.cancel_btn.setEnabled(True)
        self.train_progress.setValue(0)
        self.log_text.clear()
        
        self.train_worker = TrainingWorker(
            data_yaml=self.data_yaml_edit.text(),
            epochs=self.epochs_spin.value(),
            imgsz=self.imgsz_spin.value(),
            batch=self.batch_spin.value(),
            device=self.device_edit.text(),
            name=self.name_edit.text()
        )
        self.train_worker.log_signal.connect(self.append_log)
        self.train_worker.finished_signal.connect(self.training_finished)
        self.train_worker.start()
    
    def cancel_training(self):
        """取消训练"""
        if self.train_worker and self.train_worker.isRunning():
            self.train_worker.terminate()
            self.append_log("用户取消了训练")
        self.train_btn.setEnabled(True)
        self.cancel_btn.setEnabled(False)
    
    def append_log(self, text):
        """添加日志"""
        self.log_text.append(text)
        cursor = self.log_text.textCursor()
        cursor.movePosition(cursor.End)
        self.log_text.setTextCursor(cursor)
        QApplication.processEvents()
    
    def training_finished(self, model_path):
        """训练完成"""
        self.train_btn.setEnabled(True)
        self.cancel_btn.setEnabled(False)
        self.train_progress.setValue(100)
        reply = QMessageBox.question(self, "训练完成",
            f"模型已保存至 {model_path}\n是否切换到检测页加载该模型?",
            QMessageBox.Yes | QMessageBox.No)
        if reply == QMessageBox.Yes:
            self.tab_widget.setCurrentIndex(1)
            self.model_combo.setCurrentText(model_path)
            self.load_model()
    
    def load_model(self):
        """加载 YOLO 模型"""
        if not YOLO_AVAILABLE:
            QMessageBox.warning(self, "警告", "请先安装 ultralytics: pip install ultralytics")
            return
            
        model_path = self.model_combo.currentText()
        if not os.path.exists(model_path):
            QMessageBox.warning(self, "警告", f"模型文件 {model_path} 不存在")
            return
        
        try:
            self.model = YOLO(model_path)
            self.model_info.setText(f"✅ 已加载: {model_path}")
            self.statusBar.showMessage(f"模型加载成功", 3000)
            self.image_btn.setEnabled(True)
            self.video_btn.setEnabled(True)
            self.camera_btn.setEnabled(True)
        except Exception as e:
            QMessageBox.critical(self, "错误", f"加载失败: {str(e)}")
    
    def detect_image(self):
        """检测图片"""
        if self.model is None:
            QMessageBox.warning(self, "警告", "请先加载模型")
            return
        
        file_path, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "Images (*.jpg *.png *.bmp)")
        if not file_path:
            return
        
        img = cv2.imread(file_path)
        if img is None:
            QMessageBox.critical(self, "错误", "无法读取图片")
            return
        
        # 显示原图
        self.display_image(cv2.cvtColor(img, cv2.COLOR_BGR2RGB), self.original_label)
        
        # 检测
        results = self.model.predict(img, conf=self.conf_slider.value()/100, iou=self.iou_slider.value()/100, verbose=False)
        if results:
            result_img = results[0].plot()
            self.current_result = result_img
            self.display_image(cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB), self.result_label)
            self.update_result_table(results[0])
            self.update_statistics(results[0])
            self.save_btn.setEnabled(True)
            self.statusBar.showMessage("检测完成", 2000)
    
    def detect_video(self):
        """检测视频"""
        if self.model is None:
            QMessageBox.warning(self, "警告", "请先加载模型")
            return
        
        file_path, _ = QFileDialog.getOpenFileName(self, "选择视频", "", "Video (*.mp4 *.avi *.mov)")
        if not file_path:
            return
        
        self.stop_detection()
        self.cap = cv2.VideoCapture(file_path)
        if not self.cap.isOpened():
            QMessageBox.critical(self, "错误", "无法打开视频")
            return
        
        self.is_detecting = True
        self.stop_btn.setEnabled(True)
        self.image_btn.setEnabled(False)
        self.video_btn.setEnabled(False)
        self.camera_btn.setEnabled(False)
        self.timer.start(30)
        self.frame_counter = 0
        self.statusBar.showMessage("正在处理视频...")
    
    def detect_camera(self):
        """检测摄像头"""
        if self.model is None:
            QMessageBox.warning(self, "警告", "请先加载模型")
            return
        
        idx, ok = QInputDialog.getInt(self, "摄像头", "摄像头ID:", 0, 0, 10)
        if not ok:
            return
        
        self.stop_detection()
        self.cap = cv2.VideoCapture(idx)
        if not self.cap.isOpened():
            QMessageBox.critical(self, "错误", "无法打开摄像头")
            return
        
        self.is_detecting = True
        self.stop_btn.setEnabled(True)
        self.image_btn.setEnabled(False)
        self.video_btn.setEnabled(False)
        self.camera_btn.setEnabled(False)
        self.timer.start(30)
        self.frame_counter = 0
        self.statusBar.showMessage("摄像头已启动")
    
    def update_frame(self):
        """更新视频帧"""
        if not self.is_detecting or self.cap is None:
            return
        
        ret, frame = self.cap.read()
        if not ret:
            self.stop_detection()
            QMessageBox.information(self, "提示", "视频/摄像头结束")
            return
        
        # 显示原始帧
        self.display_image(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB), self.original_label)
        
        # 跳帧检测
        self.frame_counter += 1
        interval = self.detect_interval_spin.value()
        if self.frame_counter % interval == 0:
            results = self.model.predict(frame, conf=self.conf_slider.value()/100, iou=self.iou_slider.value()/100, verbose=False)
            if results:
                result_img = results[0].plot()
                self.current_result = result_img
                self.display_image(cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB), self.result_label)
                self.update_result_table(results[0])
                self.update_statistics(results[0])
    
    def stop_detection(self):
        """停止检测"""
        self.is_detecting = False
        if self.timer.isActive():
            self.timer.stop()
        if self.cap:
            self.cap.release()
            self.cap = None
        self.stop_btn.setEnabled(False)
        self.image_btn.setEnabled(True)
        self.video_btn.setEnabled(True)
        self.camera_btn.setEnabled(True)
        self.statusBar.showMessage("检测已停止")
    
    def save_result(self):
        """保存检测结果"""
        if self.current_result is None:
            QMessageBox.warning(self, "警告", "没有可保存的结果")
            return
        
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        default = f"detection_result_{timestamp}.jpg"
        path, _ = QFileDialog.getSaveFileName(self, "保存图片", default, "JPEG (*.jpg);;PNG (*.png)")
        if path:
            cv2.imwrite(path, self.current_result)
            QMessageBox.information(self, "成功", f"已保存至 {path}")
    
    def update_result_table(self, result):
        """更新结果表格"""
        self.result_table.setRowCount(0)
        boxes = result.boxes
        if boxes is None:
            return
        
        for box in boxes:
            cls_id = int(box.cls[0])
            cls_name = self.CLASS_NAMES.get(cls_id, f"类别{cls_id}")
            conf = float(box.conf[0])
            x1, y1, x2, y2 = map(int, box.xyxy[0].tolist())
            cx, cy = (x1 + x2) // 2, (y1 + y2) // 2
            w, h = x2 - x1, y2 - y1
            
            # 行为判断
            behavior = "正常"
            if cls_id == 1:
                behavior = "⚠️ 使用手机"
            elif cls_id == 2:
                behavior = "😴 瞌睡"

            
            row = self.result_table.rowCount()
            self.result_table.insertRow(row)
            self.result_table.setItem(row, 0, QTableWidgetItem(cls_name))
            self.result_table.setItem(row, 1, QTableWidgetItem(f"{conf:.1%}"))
            self.result_table.setItem(row, 2, QTableWidgetItem(f"({cx},{cy})"))
            self.result_table.setItem(row, 3, QTableWidgetItem(f"{w}×{h}"))
            self.result_table.setItem(row, 4, QTableWidgetItem(behavior))
    
    def update_statistics(self, result):
        """更新统计信息"""
        boxes = result.boxes
        if boxes is None:
            return
        
        stats = {"人数": 0, "玩手机": 0, "睡觉": 0}
        for box in boxes:
            cls_id = int(box.cls[0])
            name = self.CLASS_NAMES.get(cls_id, "")
            if name == "人数":
                stats["人数"] += 1
            elif name == "玩手机":
                stats["玩手机"] += 1
            elif name == "睡觉":
                stats["睡觉"] += 1

        text = f"""
        <b>📊 实时统计</b><br>
        👥 学生人数: {stats['人数']}<br>
        📱 手机使用: <font color="red">{stats['玩手机']}</font><br>
        😴 趴桌睡觉: {stats['睡觉']}<br>
        """
        if stats['玩手机'] > 0:
            text += "<br><font color='red'>⚠️ 警告:检测到手机!</font>"
        self.stats_label.setText(text)
    
    def display_image(self, img, label):
        """显示图片"""
        if img is None:
            return
        h, w, ch = img.shape
        bytes_per_line = ch * w
        qt_img = QImage(img.data, w, h, bytes_per_line, QImage.Format_RGB888)
        pixmap = QPixmap.fromImage(qt_img)
        label_size = label.size()
        if label_size.width() > 0:
            pixmap = pixmap.scaled(label_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
        label.setPixmap(pixmap)


def main():
    app = QApplication(sys.argv)
    window = ClassroomSystem()
    window.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

4、 调试结果

4.1 训练过程监控

训练共进行100个epoch,损失函数和评估指标变化如下:
train/box_loss, train/cls_loss, train/dfl_loss:训练过程中三类损失均稳定下降,表明模型收敛良好,学习过程正常。
val/box_loss, val/cls_loss, val/dfl_loss:验证损失同样呈下降趋势,未出现明显过拟合现象,模型泛化能力较好。
metrics/precision(B), metrics/recall(B):精度和召回率逐步提升,最终达到0.49和0.67左右,模型检测性能稳定。
metrics/mAP50(B), metrics/mAP50-95(B):mAP50最终达到0.565,mAP50-95达到0.333,满足课堂行为检测的基本需求。

在这里插入图片描述

4.2 模型精度评估

训练完成后,在验证集上评估得到以下详细指标:

类别 精度(Precision) 召回率(Recall) mAP50
人数 0.49 0.68 0.57
玩手机 0.43 0.59 0.40
睡觉 0.41 0.58 0.30
所有类别 0.44 0.62 0.42

混淆矩阵(归一化)显示:人类别的识别准确率较高,部分"玩手机"和"睡觉"被误判为"人数",这是由于某些样本中目标较小或姿态与常规差异较大所致,后续可通过增加样本数量优化。

在这里插入图片描述

4.3 检测效果展示

加载训练好的best.pt模型,对课堂图片进行测试,检测结果如下:
人数统计:系统能够准确框出图像中的所有学生,并统计总数,检测置信度普遍在0.9以上。
玩手机检测:当学生出现手持手机的动作时,模型能够以较高置信度(>0.7)标出,并在统计面板中红色警告提示。
睡觉检测:对于趴桌瞌睡的学生,模型同样能够识别并统计,帮助教师及时发现课堂异常状态。

4.4 界面功能验证

系统主界面包含"模型训练"和"实时检测"两个标签页,功能验证如下:
训练页支持选择data.yaml配置文件、设置训练超参数(epochs、batch、imgsz等)、启动训练并实时查看训练日志输出,功能正常运行。

G:\xunlianji\dataset\
├── images/
│   ├── train/       (您的训练图片)
│   └── val/         (至少要有这个空文件夹,或放几张图片)
├── labels/
│   ├── train/       (与 train 图片一一对应的 .txt 文件)
│   └── val/         (与 val 图片对应的 .txt 文件,可以为空)
└── data.yaml

这次的训练模型由于是课程设计需要所以就简单标注了三个数据,train也只有130张照片,val有50张。由于coco没有这三个数据,所以使用T-RexLabel标注数据

data.yaml

path: G:/xunlianji/dataset   
train: images/train
val: images/val
nc: 3
names: ['人数', '玩手机', '睡觉']			##这里需要按照标注数据的顺序写

在这里插入图片描述

检测页支持加载.pt模型文件、选择图片/视频/摄像头三种输入源、调节置信度阈值和IoU阈值,检测结果以表格和统计面板直观展示,所有功能均正常运行,满足设计要求。

1以上是通过课堂行为检测系统检测的大学教室下课的画面

  • 由于设备内存不足这里使用这里先使用训练脚本训练数据,但不代表这个系统的模型训练使用不了,把单次训练图片从16减到4分辨率从640降低到320,训练轮数减到10次还是可以训练并输出模型但是由于次数少的原因置信度不够所以不使用。
    在这里插入图片描述
    在这里插入图片描述

请添加图片描述

5、 附录:文件清单

文件名 描述
1.py 主程序(含训练和检测界面)
data.yaml 数据集配置文件
best.pt 训练好的模型权重
results.csv 训练过程指标记录
results.png 训练曲线图
confusion_matrix_normalized.png 归一化混淆矩阵
BoxPR_curve.png PR 曲线
labels.jpg 标注框位置分布
train_batch0.jpg ~ train_batch2.jpg 训练批次样本

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐