基于YOLOv8的麻将识别检测系统:从数据集制作到UI开发全流程
在传统麻将游戏向数字化转型的过程中,自动识别麻将牌面一直是个技术难点。无论是线上麻将平台的开发,还是专业比赛的自动化记录,都需要准确高效的牌面识别技术。本文将基于YOLOv8目标检测算法,完整实现一套麻将识别检测系统,包含从环境配置、数据集制作到模型训练和UI界面开发的全流程。
这套系统能够识别42种不同类型的麻将牌,包括万子(1-9万)、筒子(1-9筒)、条子(1-9条)以及东南西北风、红中、发财、白板等特殊牌型。系统支持图片检测、视频检测和摄像头实时检测三种模式,并提供了友好的图形界面,方便用户调节检测参数。
1. 项目概述与技术背景
1.1 项目意义与应用场景
麻将作为中国传统文化的重要组成部分,正在经历从实体向数字化的转变。传统的线上麻将游戏大多依赖手动操作,而专业的麻将比赛也需要人工记录牌面情况,效率较低且容易出错。基于深度学习的麻将识别系统可以有效解决这些问题。
主要应用场景包括:
- 线上麻将游戏开发:为虚拟麻将游戏提供自动牌面识别功能
- 专业比赛记录:自动记录比赛过程中的牌面情况,提高公正性和效率
- 麻将教学辅助:实时识别并提示牌面信息,辅助初学者学习
- 智能麻将设备:集成到智能麻将桌等硬件设备中
1.2 YOLOv8技术优势
YOLOv8是Ultralytics公司推出的最新一代目标检测算法,相比前代版本在精度和速度上都有显著提升。其主要优势包括:
- 更高的检测精度:采用新的骨干网络和检测头设计
- 更快的推理速度:优化了网络结构和计算流程
- 更好的多尺度检测:改进了特征金字塔网络
- 更简单的使用方式:提供了友好的Python接口
1.3 系统架构设计
整个系统采用模块化设计,主要包括以下几个核心模块:
麻将识别检测系统 ├── 数据采集与标注模块 ├── 模型训练与优化模块 ├── 推理检测核心模块 ├── 用户界面交互模块 └── 结果输出与保存模块2. 环境配置与依赖安装
2.1 系统环境要求
在开始项目之前,需要确保系统满足以下基本要求:
- 操作系统:Windows 10/11, Ubuntu 18.04+ 或 macOS
- Python版本:3.8-3.10(推荐3.9)
- 内存:至少8GB,推荐16GB以上
- 显卡:支持CUDA的NVIDIA显卡(可选,但推荐)
2.2 创建虚拟环境
为了避免包冲突,建议为项目创建独立的虚拟环境:
# 创建新的conda环境 conda create -n mahjong_yolov8 python=3.9 # 激活环境 conda activate mahjong_yolov8 # 或者使用venv创建虚拟环境 python -m venv mahjong_yolov8 source mahjong_yolov8/bin/activate # Linux/macOS mahjong_yolov8\Scripts\activate # Windows2.3 安装核心依赖
创建requirements.txt文件,包含项目所需的所有依赖:
# requirements.txt ultralytics>=8.0.0 opencv-python>=4.5.0 PyQt5>=5.15.0 numpy>=1.21.0 torch>=1.7.0 torchvision>=0.8.0 Pillow>=8.0.0 matplotlib>=3.3.0 seaborn>=0.11.0 pandas>=1.3.0 scipy>=1.7.0使用pip安装所有依赖:
pip install -r requirements.txt2.4 PyTorch版本选择
根据是否使用GPU,需要安装不同版本的PyTorch:
# 如果使用CUDA显卡(推荐) pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 # 如果只使用CPU pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu2.5 环境验证
安装完成后,可以通过以下代码验证环境配置是否正确:
# environment_test.py import torch import cv2 from PyQt5 import QtWidgets from ultralytics import YOLO import numpy as np print(f"PyTorch版本: {torch.__version__}") print(f"CUDA是否可用: {torch.cuda.is_available()}") print(f"OpenCV版本: {cv2.__version__}") # 测试YOLOv8是否能正常加载 try: model = YOLO('yolov8n.pt') print("YOLOv8环境配置成功!") except Exception as e: print(f"YOLOv8环境配置失败: {e}")3. 数据集准备与制作
3.1 数据集需求分析
麻将识别需要覆盖所有常见的麻将牌型,本项目共包含42个类别:
- 数字牌:1-9万、1-9筒、1-9条
- 风牌:东风、南风、西风、北风
- 箭牌:红中、发财、白板
- 花牌(可选):春、夏、秋、冬等
3.2 数据采集策略
高质量的数据集是模型成功的关键,采集时需要注意:
# data_collection_guidelines.py """ 数据采集最佳实践: 1. 多角度拍摄:正视角、侧视角、倾斜角度 2. 不同光照条件:自然光、室内灯光、强光、弱光 3. 复杂背景:木质桌面、布料背景、手持场景 4. 不同排列方式:单张、多张重叠、牌墙排列 5. 多种麻将牌材质:塑料、竹子、骨头等 """3.3 数据标注工具使用
推荐使用LabelImg进行数据标注:
# 安装LabelImg pip install labelimg # 启动标注工具 labelimg标注时需要遵循以下规范:
- 边界框:紧密贴合牌面边缘
- 类别名称:使用统一的命名规范(如1B代表一万)
- 标注质量:多人交叉验证,确保准确性
3.4 数据集配置文件
创建YOLO格式的数据集配置文件:
# datasets/data.yaml path: /path/to/mahjong_dataset # 数据集根目录 train: images/train # 训练集图片路径 val: images/val # 验证集图片路径 test: images/test # 测试集图片路径 # 类别数量 nc: 42 # 类别名称 names: [ '1B', '1C', '1D', '2B', '2C', '2D', '3B', '3C', '3D', '4B', '4C', '4D', '5B', '5C', '5D', '6B', '6C', '6D', '7B', '7C', '7D', '8B', '8C', '8D', '9B', '9C', '9D', 'EW', 'SW', 'WW', 'NW', 'WD', 'GD', 'RD', '1F', '2F', '3F', '4F', '1S', '2S', '3S', '4S' ]3.5 数据增强策略
为了提高模型泛化能力,需要实施数据增强:
# data_augmentation.py from ultralytics.data.augment import Albumentations import albumentations as A # 定义数据增强管道 augmentation_pipeline = A.Compose([ A.RandomRotate90(p=0.5), A.HorizontalFlip(p=0.5), A.RandomBrightnessContrast(p=0.2), A.GaussianBlur(blur_limit=3, p=0.1), A.RandomGamma(p=0.2), A.CLAHE(p=0.2), A.HueSaturationValue(p=0.3), A.RandomShadow(p=0.1), A.RandomSnow(p=0.1), A.RandomFog(p=0.1) ])4. 模型训练与优化
4.1 YOLOv8模型选择
YOLOv8提供了多种规模的模型,可以根据需求选择:
# model_selection.py from ultralytics import YOLO # 可用模型类型 MODEL_TYPES = { 'nano': 'yolov8n.pt', # 最轻量,适合移动设备 'small': 'yolov8s.pt', # 平衡型,推荐大多数场景 'medium': 'yolov8m.pt', # 中等精度和速度 'large': 'yolov8l.pt', # 高精度,速度较慢 'xlarge': 'yolov8x.pt' # 最高精度,资源消耗大 } def select_model(model_type='small'): """根据需求选择合适的模型""" if model_type not in MODEL_TYPES: raise ValueError(f"不支持的模型类型: {model_type}") model_path = MODEL_TYPES[model_type] return YOLO(model_path)4.2 训练参数配置
合理的训练参数对模型性能至关重要:
# training_config.py from ultralytics import YOLO def setup_training(): model = YOLO('yolov8s.pt') # 使用预训练权重 # 训练参数配置 training_config = { 'data': 'datasets/data.yaml', # 数据集配置 'epochs': 500, # 训练轮数 'batch': 64, # 批次大小 'imgsz': 640, # 输入图像尺寸 'device': '0', # 使用GPU 0 'workers': 8, # 数据加载线程数 'patience': 50, # 早停耐心值 'lr0': 0.01, # 初始学习率 'lrf': 0.01, # 最终学习率 'momentum': 0.937, # 动量 'weight_decay': 0.0005, # 权重衰减 'warmup_epochs': 3.0, # 热身轮数 'warmup_momentum': 0.8, # 热身动量 'box': 7.5, # 框损失权重 'cls': 0.5, # 分类损失权重 'dfl': 1.5, # DFL损失权重 'save_period': -1, # 保存周期 'seed': 42, # 随机种子 'deterministic': True, # 确定性训练 'single_cls': False, # 单类别模式 'rect': False, # 矩形训练 'cos_lr': False, # 余弦学习率调度 'close_mosaic': 10, # 最后N轮关闭马赛克增强 'resume': False, # 恢复训练 'amp': True, # 自动混合精度 'fraction': 1.0, # 数据集比例 'profile': False, # 性能分析 'overlap_mask': True, # 重叠掩码 'mask_ratio': 4, # 掩码下采样比例 'dropout': 0.0, # Dropout率 'val': True, # 验证训练 'save_json': False, # 保存JSON结果 'save_hybrid': False, # 保存混合标签 'conf': 0.001, # 目标置信度阈值 'iou': 0.7, # IoU阈值 'max_det': 300, # 每张图最大检测数 'half': False, # 半精度推理 'dnn': False, # 使用OpenCV DNN 'plots': True, # 保存绘图 'source': None, # 推理源 'show': False, # 显示结果 'save_txt': False, # 保存标签为txt 'save_conf': False, # 保存置信度 'save_crop': False, # 保存裁剪检测 'show_labels': True, # 显示标签 'show_conf': True, # 显示置信度 'vid_stride': 1, # 视频帧步长 'line_width': None, # 线宽 'visualize': False, # 可视化特征 'augment': False, # 增强推理 'agnostic_nms': False, # 类别无关NMS 'classes': None, # 过滤类别 'retina_masks': False, # 高分辨率掩码 'embed': None, # 特征嵌入层 'project': 'runs/detect', # 项目保存路径 'name': 'mahjong_exp', # 实验名称 'exist_ok': False, # 覆盖现有项目 'pretrained': True, # 使用预训练权重 'optimizer': 'auto', # 优化器 'verbose': True, # 详细输出 'evolve': None, # 超参数进化 'cache': False, # 缓存数据集 'image_weights': False, # 图像权重 'multi_scale': False, # 多尺度训练 'overrides': {} # 覆盖参数 } return model, training_config4.3 训练过程监控
实时监控训练过程,及时调整策略:
# training_monitor.py import matplotlib.pyplot as plt from ultralytics.utils import plots class TrainingMonitor: def __init__(self, results_path): self.results_path = results_path def plot_training_curves(self): """绘制训练曲线""" try: results = plots.plot_results( file=self.results_path / 'results.csv', save=False ) plt.tight_layout() plt.savefig(self.results_path / 'training_curves.png', dpi=300) plt.close() except Exception as e: print(f"绘制训练曲线失败: {e}") def analyze_training_results(self): """分析训练结果""" # 读取训练结果 import pandas as pd results_df = pd.read_csv(self.results_path / 'results.csv') # 分析关键指标 final_metrics = { 'map50': results_df['metrics/mAP50(B)'].iloc[-1], 'map50_95': results_df['metrics/mAP50-95(B)'].iloc[-1], 'precision': results_df['metrics/precision(B)'].iloc[-1], 'recall': results_df['metrics/recall(B)'].iloc[-1] } return final_metrics4.4 开始训练
执行训练脚本:
# train.py from ultralytics import YOLO import os def main(): # 加载模型 model = YOLO('yolov8s.pt') # 开始训练 results = model.train( data='datasets/data.yaml', epochs=500, batch=64, imgsz=640, device='0', workers=8, project='runs/detect', name='mahjong_detection', exist_ok=True, patience=50, save=True, val=True, plots=True ) print("训练完成!") # 验证模型性能 metrics = model.val() print(f"验证结果: {metrics}") # 导出模型 model.export(format='onnx') print("模型导出完成!") if __name__ == '__main__': main()5. 图形界面开发
5.1 PyQt5界面设计
使用PyQt5开发用户友好的图形界面:
# main_window.py from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import Qt, QTimer, pyqtSignal from PyQt5.QtGui import QImage, QPixmap, QIcon, QFont from PyQt5.QtWidgets import (QFileDialog, QMessageBox, QTableWidgetItem, QStyledItemDelegate, QHeaderView, QProgressBar) import cv2 import numpy as np from ultralytics import YOLO import os import datetime import sys import json class CenteredDelegate(QStyledItemDelegate): """居中显示表格内容的代理类""" def initStyleOption(self, option, index): super().initStyleOption(option, index) option.displayAlignment = Qt.AlignCenter class DetectionThread(QtCore.QThread): """检测线程,避免界面卡顿""" detection_finished = pyqtSignal(object) progress_updated = pyqtSignal(int) def __init__(self, model, image, conf_threshold, iou_threshold): super().__init__() self.model = model self.image = image self.conf_threshold = conf_threshold self.iou_threshold = iou_threshold self.results = None def run(self): try: # 执行检测 self.results = self.model.predict( self.image, conf=self.conf_threshold, iou=self.iou_threshold, verbose=False ) self.detection_finished.emit(self.results) except Exception as e: print(f"检测失败: {e}") class MahjongDetectionApp(QtWidgets.QMainWindow): """麻将识别检测系统主窗口""" def __init__(self): super().__init__() self.model = None self.current_image = None self.current_result = None self.cap = None self.timer = QTimer() self.is_camera_running = False self.video_writer = None self.output_dir = "output" self.setup_ui() self.setup_connections() # 创建输出目录 if not os.path.exists(self.output_dir): os.makedirs(self.output_dir) def setup_ui(self): """设置用户界面""" self.setWindowTitle("YOLOv8麻将识别检测系统") self.setGeometry(100, 100, 1600, 900) self.setMinimumSize(1200, 700) # 设置窗口图标 self.setWindowIcon(QIcon('icon.ico') if os.path.exists('icon.ico') else self.style().standardIcon(QtWidgets.QStyle.SP_ComputerIcon)) # 创建中央部件 central_widget = QtWidgets.QWidget() self.setCentralWidget(central_widget) # 主布局 main_layout = QtWidgets.QHBoxLayout(central_widget) main_layout.setContentsMargins(10, 10, 10, 10) main_layout.setSpacing(15) # 左侧图像显示区域 left_layout = self.create_left_panel() main_layout.addLayout(left_layout, 3) # 右侧控制面板 right_layout = self.create_right_panel() main_layout.addLayout(right_layout, 1) # 应用样式 self.apply_styles() def create_left_panel(self): """创建左侧图像显示面板""" layout = QtWidgets.QVBoxLayout() layout.setSpacing(15) # 原始图像显示 original_group = QtWidgets.QGroupBox("原始图像") original_group.setMinimumHeight(350) self.original_label = QtWidgets.QLabel() self.original_label.setAlignment(Qt.AlignCenter) self.original_label.setText("请选择图片或开启摄像头") self.original_label.setStyleSheet(""" QLabel { background-color: #f8f9fa; border: 2px dashed #dee2e6; border-radius: 5px; color: #6c757d; font-size: 14px; } """) original_layout = QtWidgets.QVBoxLayout() original_layout.addWidget(self.original_label) original_group.setLayout(original_layout) layout.addWidget(original_group) # 检测结果图像显示 result_group = QtWidgets.QGroupBox("检测结果") result_group.setMinimumHeight(350) self.result_label = QtWidgets.QLabel() self.result_label.setAlignment(Qt.AlignCenter) self.result_label.setText("检测结果将显示在这里") self.result_label.setStyleSheet(""" QLabel { background-color: #f8f9fa; border: 2px dashed #dee2e6; border-radius: 5px; color: #6c757d; font-size: 14px; } """) result_layout = QtWidgets.QVBoxLayout() result_layout.addWidget(self.result_label) result_group.setLayout(result_layout) layout.addWidget(result_group) return layout def create_right_panel(self): """创建右侧控制面板""" layout = QtWidgets.QVBoxLayout() layout.setSpacing(15) # 模型设置组 model_group = self.create_model_group() layout.addWidget(model_group) # 检测参数组 param_group = self.create_parameter_group() layout.addWidget(param_group) # 功能按钮组 func_group = self.create_function_group() layout.addWidget(func_group) # 进度条 self.progress_bar = QProgressBar() self.progress_bar.setVisible(False) layout.addWidget(self.progress_bar) # 检测结果表格 table_group = self.create_result_table() layout.addWidget(table_group, 1) return layout def create_model_group(self): """创建模型设置组""" group = QtWidgets.QGroupBox("模型设置") layout = QtWidgets.QVBoxLayout() # 模型选择 model_layout = QtWidgets.QHBoxLayout() model_layout.addWidget(QtWidgets.QLabel("选择模型:")) self.model_combo = QtWidgets.QComboBox() self.model_combo.addItems(["best.pt", "yolov8s.pt", "yolov8m.pt"]) model_layout.addWidget(self.model_combo) layout.addLayout(model_layout) # 模型加载按钮 self.load_btn = QtWidgets.QPushButton("加载模型") self.load_btn.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_DialogOpenButton)) layout.addWidget(self.load_btn) # 模型状态显示 self.model_status = QtWidgets.QLabel("模型未加载") self.model_status.setStyleSheet("color: #dc3545; font-weight: bold;") layout.addWidget(self.model_status) group.setLayout(layout) return group def create_parameter_group(self): """创建参数设置组""" group = QtWidgets.QGroupBox("检测参数") layout = QtWidgets.QFormLayout() # 置信度阈值 self.conf_slider = QtWidgets.QSlider(Qt.Horizontal) self.conf_slider.setRange(1, 99) self.conf_slider.setValue(25) self.conf_label = QtWidgets.QLabel("0.25") self.conf_label.setStyleSheet("color: #007bff; font-weight: bold;") # IoU阈值 self.iou_slider = QtWidgets.QSlider(Qt.Horizontal) self.iou_slider.setRange(1, 99) self.iou_slider.setValue(45) self.iou_label = QtWidgets.QLabel("0.45") self.iou_label.setStyleSheet("color: #007bff; font-weight: bold;") layout.addRow("置信度阈值:", self.conf_slider) layout.addRow("当前值:", self.conf_label) layout.addRow("IoU阈值:", self.iou_slider) layout.addRow("当前值:", self.iou_label) group.setLayout(layout) return group def create_function_group(self): """创建功能按钮组""" group = QtWidgets.QGroupBox("检测功能") layout = QtWidgets.QVBoxLayout() # 功能按钮 self.image_btn = self.create_button("图片检测", "SP_FileIcon") self.video_btn = self.create_button("视频检测", "SP_MediaPlay") self.camera_btn = self.create_button("摄像头检测", "SP_ComputerIcon") self.stop_btn = self.create_button("停止检测", "SP_MediaStop") self.save_btn = self.create_button("保存结果", "SP_DialogSaveButton") # 初始状态设置 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]: layout.addWidget(btn) group.setLayout(layout) return group def create_button(self, text, icon_name): """创建标准化按钮""" btn = QtWidgets.QPushButton(text) btn.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, icon_name))) btn.setMinimumHeight(40) return btn def create_result_table(self): """创建结果表格""" group = QtWidgets.QGroupBox("检测结果详情") layout = QtWidgets.QVBoxLayout() self.result_table = QtWidgets.QTableWidget() self.result_table.setColumnCount(5) self.result_table.setHorizontalHeaderLabels(["序号", "类别", "置信度", "位置", "尺寸"]) # 设置表格属性 self.result_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.result_table.verticalHeader().setVisible(False) self.result_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) self.result_table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) # 设置居中代理 delegate = CenteredDelegate(self.result_table) self.result_table.setItemDelegate(delegate) layout.addWidget(self.result_table) group.setLayout(layout) return group def apply_styles(self): """应用样式表""" style = """ QMainWindow { background-color: #f5f5f5; } QGroupBox { border: 1px solid #e0e0e0; border-radius: 5px; margin-top: 10px; padding-top: 15px; font-weight: bold; font-size: 12px; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px; } QPushButton { padding: 8px; background-color: #007bff; color: white; border: none; border-radius: 4px; font-size: 12px; } QPushButton:hover { background-color: #0056b3; } QPushButton:disabled { background-color: #6c757d; } QSlider::groove:horizontal { height: 6px; background: #e0e0e0; border-radius: 3px; } QSlider::handle:horizontal { width: 16px; height: 16px; margin: -5px 0; background: #007bff; border-radius: 8px; } QTableWidget { border: 1px solid #e0e0e0; alternate-background-color: #f8f9fa; } QHeaderView::section { background-color: #007bff; color: white; padding: 8px; border: none; } """ self.setStyleSheet(style) def setup_connections(self): """设置信号槽连接""" # 模型加载 self.load_btn.clicked.connect(self.load_model) # 参数变化 self.conf_slider.valueChanged.connect(self.update_conf_label) self.iou_slider.valueChanged.connect(self.update_iou_label) # 功能按钮 self.image_btn.clicked.connect(self.detect_image) self.video_btn.clicked.connect(self.detect_video) self.camera_btn.clicked.connect(self.toggle_camera) self.stop_btn.clicked.connect(self.stop_detection) self.save_btn.clicked.connect(self.save_results) # 定时器 self.timer.timeout.connect(self.update_camera_frame) def load_model(self): """加载YOLOv8模型""" try: model_path = self.model_combo.currentText() if not os.path.exists(model_path): QMessageBox.warning(self, "警告", f"模型文件不存在: {model_path}") return self.model = YOLO(model_path) self.model_status.setText("模型加载成功") self.model_status.setStyleSheet("color: #28a745; font-weight: bold;") # 启用检测功能 self.image_btn.setEnabled(True) self.video_btn.setEnabled(True) self.camera_btn.setEnabled(True) QMessageBox.information(self, "成功", "模型加载成功!") except Exception as e: QMessageBox.critical(self, "错误", f"模型加载失败: {str(e)}") self.model_status.setText("模型加载失败") def update_conf_label(self): """更新置信度阈值显示""" conf = self.conf_slider.value() / 100 self.conf_label.setText(f"{conf:.2f}") def update_iou_label(self): """更新IoU阈值显示""" iou = self.iou_slider.value() / 100 self.iou_label.setText(f"{iou:.2f}") def detect_image(self): """图片检测功能""" if self.model is None: QMessageBox.warning(self, "警告", "请先加载模型") return file_path, _ = QFileDialog.getOpenFileName( self, "选择图片", "", "图片文件 (*.jpg *.jpeg *.png *.bmp *.tiff);;所有文件 (*)" ) if file_path: try: # 读取并显示原始图片 image = cv2.imread(file_path) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) self.display_image(image, self.original_label) self.current_image = image.copy() # 显示进度条 self.progress_bar.setVisible(True) self.progress_bar.setRange(0, 0) # 无限进度条 # 在子线程中执行检测 self.detection_thread = DetectionThread( self.model, image, self.conf_slider.value() / 100, self.iou_slider.value() / 100 ) self.detection_thread.detection_finished.connect(self.on_detection_finished) self.detection_thread.start() except Exception as e: QMessageBox.critical(self, "错误", f"图片加载失败: {str(e)}") def on_detection_finished(self, results): """检测完成回调""" self.progress_bar.setVisible(False) if results and len(results) > 0: # 显示检测结果 result_image = results[0].plot() self.display_image(result_image, self.result_label) self.current_result = result_image.copy() # 更新结果表格 self.update_result_table(results[0]) # 启用保存按钮 self.save_btn.setEnabled(True) # 显示统计信息 detections = len(results[0].boxes) if results[0].boxes else 0 self.statusBar().showMessage(f"检测完成,共检测到 {detections} 个目标", 3000) def detect_video(self): """视频检测功能""" if self.model is None: QMessageBox.warning(self, "警告", "请先加载模型") return file_path, _ = QFileDialog.getOpenFileName( self, "选择视频", "", "视频文件 (*.mp4 *.avi *.mov *.mkv *.wmv);;所有文件 (*)" ) if file_path: self.process_video(file_path) def toggle_camera(self): """切换摄像头状态""" if self.is_camera_running: self.stop_detection() else: self.start_camera() def start_camera(self): """开启摄像头""" try: self.cap = cv2.VideoCapture(0) if not self.cap.isOpened(): QMessageBox.warning(self, "警告", "无法打开摄像头") return self.is_camera_running = True self.timer.start(30) # 30ms更新一帧 self.camera_btn.setText("关闭摄像头") self.stop_btn.setEnabled(True) except Exception as e: QMessageBox.critical(self, "错误", f"摄像头开启失败: {str(e)}") def update_camera_frame(self): """更新摄像头帧""" if self.cap and self.cap.isOpened(): ret, frame = self.cap.read() if ret: # 显示原始帧 rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) self.display_image(rgb_frame, self.original_label) self.current_image = rgb_frame.copy() # 实时检测(可选,避免卡顿) if self.model and hasattr(self, 'last_detection_time'): current_time = datetime.datetime.now().timestamp() if current_time - self.last_detection_time > 1.0: # 每秒检测一次 self.realtime_detection(rgb_frame) self.last_detection_time = current_time def realtime_detection(self, frame): """实时检测""" try: results = self.model.predict( frame, conf=self.conf_slider.value() / 100, iou=self.iou_slider.value() / 100, verbose=False ) if results and len(results) > 0: result_frame = results[0].plot() self.display_image(result_frame, self.result_label) self.update_result_table(results[0