基于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 或 macOSPython版本3.8-3.10推荐3.9内存至少8GB推荐16GB以上显卡支持CUDA的NVIDIA显卡可选但推荐2.2 创建虚拟环境为了避免包冲突建议为项目创建独立的虚拟环境# 创建新的conda环境 conda create -n mahjong_yolov8 python3.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 ultralytics8.0.0 opencv-python4.5.0 PyQt55.15.0 numpy1.21.0 torch1.7.0 torchvision0.8.0 Pillow8.0.0 matplotlib3.3.0 seaborn0.11.0 pandas1.3.0 scipy1.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(fPyTorch版本: {torch.__version__}) print(fCUDA是否可用: {torch.cuda.is_available()}) print(fOpenCV版本: {cv2.__version__}) # 测试YOLOv8是否能正常加载 try: model YOLO(yolov8n.pt) print(YOLOv8环境配置成功!) except Exception as e: print(fYOLOv8环境配置失败: {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(p0.5), A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(p0.2), A.GaussianBlur(blur_limit3, p0.1), A.RandomGamma(p0.2), A.CLAHE(p0.2), A.HueSaturationValue(p0.3), A.RandomShadow(p0.1), A.RandomSnow(p0.1), A.RandomFog(p0.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_typesmall): 根据需求选择合适的模型 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( fileself.results_path / results.csv, saveFalse ) plt.tight_layout() plt.savefig(self.results_path / training_curves.png, dpi300) 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( datadatasets/data.yaml, epochs500, batch64, imgsz640, device0, workers8, projectruns/detect, namemahjong_detection, exist_okTrue, patience50, saveTrue, valTrue, plotsTrue ) print(训练完成!) # 验证模型性能 metrics model.val() print(f验证结果: {metrics}) # 导出模型 model.export(formatonnx) 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, confself.conf_threshold, iouself.iou_threshold, verboseFalse ) 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, confself.conf_slider.value() / 100, iouself.iou_slider.value() / 100, verboseFalse ) 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