YOLOv8麻将识别检测系统从数据集构建到完整部署实战麻将作为中国传统棋牌文化的代表在智能棋牌设备、在线教学、直播解说等场景中有着广泛的应用需求。然而麻将牌面识别一直是个技术难点——不同牌面之间纹理相似度高、拍摄角度多变、光照条件复杂传统基于规则或模板匹配的方法往往难以适应真实环境的变化。今天要介绍的基于YOLOv8的麻将识别检测系统在验证集上实现了92.6%的mAP50精度单张推理速度仅需5.4毫秒真正做到了高精度与实时性的平衡。本文将完整分享从数据集构建、模型训练到界面开发的全流程实战经验无论你是想了解YOLOv8在实际项目中的应用还是需要解决类似的细粒度目标检测问题都能从中获得实用价值。1. 项目核心价值与技术难点分析1.1 为什么麻将识别值得关注麻将识别看似是个细分领域实则包含了目标检测中的多个典型挑战细粒度分类、小目标检测、相似物体区分、复杂背景干扰等。成功解决麻将识别问题意味着掌握了处理类似复杂场景的关键技术能力。在实际应用中这套系统可以用于智能棋牌记录系统自动记录对局过程生成棋谱分析AI辅助教学平台实时识别牌面提供策略建议直播互动场景自动识别主播手牌增强观众体验棋牌游戏开发为AR/VR麻将游戏提供技术支撑1.2 技术难点与解决方案麻将识别的主要技术难点包括牌面相似度高数字牌中的6条与8条、3筒与5筒在视觉特征上极为相似传统方法容易误判。YOLOv8通过深度特征学习和多尺度检测能够捕捉细微的纹理差异。光照条件多变实战环境中光线角度、强度变化大会影响颜色和纹理的呈现。系统通过数据增强和模型鲁棒性设计应对这一挑战。拍摄角度多样牌面可能以各种角度出现在画面中增加了识别难度。YOLOv8的旋转不变性特征提取能力在此发挥关键作用。实时性要求对局过程中需要快速响应不能有明显延迟。YOLOv8的轻量化设计和优化推理速度确保了实时检测能力。2. YOLOv8核心原理与改进优势2.1 YOLOv8架构概述YOLOv8是Ultralytics公司推出的最新一代目标检测模型在之前版本的基础上进行了多项重要改进骨干网络优化采用更高效的CSPDarknet53结构在保持特征提取能力的同时减少计算量。特征金字塔增强改进的PAN-FPN结构更好地融合多尺度特征提升小目标检测效果。损失函数创新使用Task Aligned Assigner进行正负样本分配提高训练效率。标签分配策略引入Distribution Focal Loss改善类别不平衡问题。2.2 相对于前代模型的优势与YOLOv5相比YOLOv8在以下方面有明显提升精度更高在相同数据集上mAP通常有2-4个百分点的提升速度更快优化了模型结构和推理流程推理速度提升约15%训练更稳定改进的损失函数和训练策略使收敛更加平稳部署更友好支持ONNX、TensorRT等多种格式便于跨平台部署3. 环境配置与依赖安装3.1 硬件要求与推荐配置最低配置GPUNVIDIA GTX 1060 6GB或同等性能内存8GB RAM存储20GB可用空间推荐配置GPUNVIDIA RTX 3080 Ti或更高内存16GB RAM或更多存储SSD硬盘50GB可用空间3.2 Python环境配置首先创建并激活conda环境# 创建Python3.9环境 conda create -n yolov8-mahjong python3.9 conda activate yolov8-mahjong # 安装PyTorch根据CUDA版本选择 # CUDA 11.3 pip install torch1.12.1cu113 torchvision0.13.1cu113 torchaudio0.12.1 --extra-index-url https://download.pytorch.org/whl/cu113 # 或者CPU版本 pip install torch1.12.1cpu torchvision0.13.1cpu torchaudio0.12.1 --extra-index-url https://download.pytorch.org/whl/cpu3.3 项目依赖安装创建requirements.txt文件ultralytics8.0.0 opencv-python4.7.0.72 numpy1.24.3 pillow9.5.0 pyqt55.15.9 scipy1.10.1 matplotlib3.7.1 seaborn0.12.2 pandas2.0.2安装依赖pip install -r requirements.txt3.4 验证安装创建验证脚本check_env.pyimport torch import cv2 import numpy as np from PIL import Image print(fPyTorch版本: {torch.__version__}) print(fCUDA可用: {torch.cuda.is_available()}) print(fCUDA版本: {torch.version.cuda}) print(fOpenCV版本: {cv2.__version__}) if torch.cuda.is_available(): print(fGPU设备: {torch.cuda.get_device_name(0)}) print(fGPU内存: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f} GB) # 测试基本功能 img np.random.randint(0, 255, (640, 640, 3), dtypenp.uint8) print(环境测试通过!)4. 数据集构建与标注规范4.1 数据集结构设计麻将识别数据集包含42个类别涵盖所有常见牌型# 类别定义 classes [ 1B, 2B, 3B, 4B, 5B, 6B, 7B, 8B, 9B, # 筒子 1C, 2C, 3C, 4C, 5C, 6C, 7C, 8C, 9C, # 条子 1D, 2D, 3D, 4D, 5D, 6D, 7D, 8D, 9D, # 万子 EW, SW, WW, NW, # 风牌东南西北 GD, RD, WD, # 箭牌中发白 1F, 2F, 3F, 4F, 5F, 6F, 7F, 8F, 9F # 花牌可选 ]4.2 数据采集要点高质量的数据集是模型成功的基础采集时需要注意多样性保证不同光照条件自然光、灯光、混合光多种拍摄角度俯拍、斜拍、平拍各种背景环境桌面、手部、复杂背景不同摆放状态整齐排列、散乱放置、部分遮挡数据量规划训练集5565张图像验证集684张图像测试集482张图像每个类别至少100-150张图像4.3 标注工具与规范推荐使用LabelImg进行标注# 安装LabelImg pip install labelimg # 启动标注工具 labelimg标注规范文件labelmap.txt1B 2B 3B ...所有42个类别YOLO格式标注示例1B.txt0 0.512 0.634 0.124 0.156其中类别ID、中心点x、中心点y、宽度、高度均归一化到0-14.4 数据增强策略创建数据增强配置文件data_augmentation.pyimport albumentations as A from albumentations.pytorch import ToTensorV2 def get_train_transforms(image_size640): return A.Compose([ A.RandomResizedCrop(image_size, image_size, scale(0.8, 1.0)), A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(p0.2), A.HueSaturationValue(p0.2), A.GaussianBlur(blur_limit3, p0.1), A.RandomGamma(p0.2), A.Normalize(mean[0, 0, 0], std[1, 1, 1]), ToTensorV2(), ]) def get_val_transforms(image_size640): return A.Compose([ A.Resize(image_size, image_size), A.Normalize(mean[0, 0, 0], std[1, 1, 1]), ToTensorV2(), ])5. 模型训练与调优实战5.1 数据集配置文件创建dataset.yaml配置文件# dataset.yaml path: /path/to/mahjong_dataset train: images/train val: images/val test: images/test nc: 42 # 类别数量 names: [1B, 2B, 3B, 4B, 5B, 6B, 7B, 8B, 9B, 1C, 2C, 3C, 4C, 5C, 6C, 7C, 8C, 9C, 1D, 2D, 3D, 4D, 5D, 6D, 7D, 8D, 9D, EW, SW, WW, NW, GD, RD, WD, 1F, 2F, 3F, 4F, 5F, 6F, 7F, 8F, 9F]5.2 训练脚本实现创建训练脚本train.pyfrom ultralytics import YOLO import torch import os def train_mahjong_detector(): # 检查GPU可用性 device cuda if torch.cuda.is_available() else cpu print(f使用设备: {device}) # 加载预训练模型 model YOLO(yolov8n.pt) # 可根据需求选择yolov8s.pt、yolov8m.pt等 # 训练参数配置 results model.train( datadataset.yaml, epochs100, imgsz640, batch16, patience10, devicedevice, workers4, optimizerAdamW, lr00.001, weight_decay0.0005, saveTrue, save_period10, pretrainedTrue, verboseTrue ) return results if __name__ __main__: train_mahjong_detector()5.3 高级训练技巧学习率调度# 自定义学习率调度 def custom_lr_scheduler(optimizer, epoch): if epoch 10: lr 0.001 elif epoch 50: lr 0.0001 else: lr 0.00001 for param_group in optimizer.param_groups: param_group[lr] lr早停策略class EarlyStopping: def __init__(self, patience10, min_delta0.001): self.patience patience self.min_delta min_delta self.best_loss float(inf) self.counter 0 def __call__(self, val_loss): if val_loss self.best_loss - self.min_delta: self.best_loss val_loss self.counter 0 else: self.counter 1 if self.counter self.patience: return True return False5.4 模型评估与分析训练完成后进行模型评估from ultralytics import YOLO import matplotlib.pyplot as plt def evaluate_model(): # 加载最佳模型 model YOLO(runs/detect/train/weights/best.pt) # 在验证集上评估 metrics model.val( datadataset.yaml, splitval, imgsz640, conf0.25, iou0.45 ) # 输出评估结果 print(fmAP50: {metrics.box.map50:.3f}) print(fmAP50-95: {metrics.box.map:.3f}) print(fPrecision: {metrics.box.precision:.3f}) print(fRecall: {metrics.box.recall:.3f}) # 绘制混淆矩阵 model.confusion_matrix() return metrics if __name__ __main__: evaluate_model()6. PyQt5界面开发详解6.1 主界面架构设计创建主界面类MainWindow.pyimport sys import os from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QSlider, QCheckBox, QGroupBox, QListWidget, QTabWidget, QStatusBar, QFileDialog, QMessageBox, QProgressBar) from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QDateTime from PyQt5.QtGui import QPixmap, QImage, QFont import cv2 import numpy as np from ultralytics import YOLO class DetectionThread(QThread): 检测线程类 frame_processed pyqtSignal(np.ndarray, list) progress_updated pyqtSignal(int) finished_signal pyqtSignal() def __init__(self, model_path, source_type, source_pathNone): super().__init__() self.model_path model_path self.source_type source_type # image, video, camera self.source_path source_path self.conf_threshold 0.5 self.iou_threshold 0.45 self.selected_classes [] self.is_running True def run(self): 线程运行主函数 try: # 加载模型 self.model YOLO(self.model_path) if self.source_type image: self.process_image() elif self.source_type video: self.process_video() elif self.source_type camera: self.process_camera() except Exception as e: print(f检测线程错误: {e}) finally: self.finished_signal.emit() def process_image(self): 处理单张图片 image cv2.imread(self.source_path) results self.model.predict( sourceimage, confself.conf_threshold, iouself.iou_threshold, classesself.selected_classes if self.selected_classes else None ) # 绘制检测结果 annotated_image results[0].plot() detections [] for box in results[0].boxes: class_id int(box.cls) confidence float(box.conf) bbox box.xyxy[0].tolist() detections.append({ class_id: class_id, confidence: confidence, bbox: bbox }) self.frame_processed.emit(annotated_image, detections) def process_video(self): 处理视频文件 cap cv2.VideoCapture(self.source_path) total_frames int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) frame_count 0 while self.is_running and cap.isOpened(): ret, frame cap.read() if not ret: break # 处理每一帧 results self.model.predict( sourceframe, confself.conf_threshold, iouself.iou_threshold, classesself.selected_classes if self.selected_classes else None ) annotated_frame results[0].plot() detections [] for box in results[0].boxes: class_id int(box.cls) confidence float(box.conf) bbox box.xyxy[0].tolist() detections.append({ class_id: class_id, confidence: confidence, bbox: bbox }) self.frame_processed.emit(annotated_frame, detections) frame_count 1 progress int((frame_count / total_frames) * 100) self.progress_updated.emit(progress) cap.release() def process_camera(self): 处理摄像头实时流 cap cv2.VideoCapture(0) # 默认摄像头 while self.is_running and cap.isOpened(): ret, frame cap.read() if not ret: break results self.model.predict( sourceframe, confself.conf_threshold, iouself.iou_threshold, classesself.selected_classes if self.selected_classes else None ) annotated_frame results[0].plot() detections [] for box in results[0].boxes: class_id int(box.cls) confidence float(box.conf) bbox box.xyxy[0].tolist() detections.append({ class_id: class_id, confidence: confidence, bbox: bbox }) self.frame_processed.emit(annotated_frame, detections) cap.release() def stop(self): 停止检测 self.is_running False class MainWindow(QMainWindow): 主窗口类 def __init__(self): super().__init__() self.model None self.detection_thread None self.init_ui() self.setup_connections() def init_ui(self): 初始化界面 self.setWindowTitle(YOLOv8麻将识别检测系统) self.setGeometry(100, 100, 1400, 900) # 中心部件 central_widget QWidget() self.setCentralWidget(central_widget) # 主布局 main_layout QHBoxLayout() central_widget.setLayout(main_layout) # 左侧控制面板 left_panel self.create_left_panel() main_layout.addWidget(left_panel, 1) # 中间显示区域 center_panel self.create_center_panel() main_layout.addWidget(center_panel, 3) # 右侧信息面板 right_panel self.create_right_panel() main_layout.addWidget(right_panel, 1) # 状态栏 self.status_bar QStatusBar() self.setStatusBar(self.status_bar) self.status_bar.showMessage(系统就绪) def create_left_panel(self): 创建左侧控制面板 panel QWidget() layout QVBoxLayout() # 模型加载区域 model_group QGroupBox(模型配置) model_layout QVBoxLayout() self.model_path_label QLabel(未加载模型) self.load_model_btn QPushButton(加载模型) model_layout.addWidget(self.model_path_label) model_layout.addWidget(self.load_model_btn) model_group.setLayout(model_layout) # 检测参数区域 param_group QGroupBox(检测参数) param_layout QVBoxLayout() # 置信度阈值 conf_layout QHBoxLayout() conf_layout.addWidget(QLabel(置信度:)) self.conf_slider QSlider(Qt.Horizontal) self.conf_slider.setRange(0, 100) self.conf_slider.setValue(50) self.conf_label QLabel(0.50) conf_layout.addWidget(self.conf_slider) conf_layout.addWidget(self.conf_label) # IoU阈值 iou_layout QHBoxLayout() iou_layout.addWidget(QLabel(IoU阈值:)) self.iou_slider QSlider(Qt.Horizontal) self.iou_slider.setRange(0, 100) 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.addLayout(conf_layout) param_layout.addLayout(iou_layout) param_group.setLayout(param_layout) # 类别选择区域 class_group QGroupBox(检测类别) class_layout QVBoxLayout() self.class_list QListWidget() # 添加麻将牌类别 classes [1筒, 2筒, 3筒, 4筒, 5筒, 6筒, 7筒, 8筒, 9筒, 1条, 2条, 3条, 4条, 5条, 6条, 7条, 8条, 9条, 1万, 2万, 3万, 4万, 5万, 6万, 7万, 8万, 9万, 东, 南, 西, 北, 中, 发, 白] for cls in classes: self.class_list.addItem(cls) self.class_list.setSelectionMode(QListWidget.MultiSelection) class_layout.addWidget(self.class_list) param_group.setLayout(class_layout) layout.addWidget(model_group) layout.addWidget(param_group) layout.addStretch() panel.setLayout(layout) return panel def create_center_panel(self): 创建中间显示区域 panel QWidget() layout QVBoxLayout() # 图像显示标签 self.image_label QLabel() self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setStyleSheet(border: 1px solid gray;) self.image_label.setText(请选择检测源) self.image_label.setMinimumSize(640, 480) # 工具栏 toolbar_layout QHBoxLayout() self.image_btn QPushButton(图片检测) self.video_btn QPushButton(视频检测) self.camera_btn QPushButton(摄像头检测) self.stop_btn QPushButton(停止检测) self.save_btn QPushButton(保存结果) toolbar_layout.addWidget(self.image_btn) toolbar_layout.addWidget(self.video_btn) toolbar_layout.addWidget(self.camera_btn) toolbar_layout.addWidget(self.stop_btn) toolbar_layout.addWidget(self.save_btn) toolbar_layout.addStretch() # 进度条 self.progress_bar QProgressBar() self.progress_bar.setVisible(False) layout.addWidget(self.image_label) layout.addLayout(toolbar_layout) layout.addWidget(self.progress_bar) panel.setLayout(layout) return panel def create_right_panel(self): 创建右侧信息面板 panel QWidget() layout QVBoxLayout() # 检测结果区域 result_group QGroupBox(检测结果) result_layout QVBoxLayout() self.result_list QListWidget() self.fps_label QLabel(FPS: --) self.object_count_label QLabel(检测目标: 0) result_layout.addWidget(self.fps_label) result_layout.addWidget(self.object_count_label) result_layout.addWidget(self.result_list) result_group.setLayout(result_layout) # 日志区域 log_group QGroupBox(系统日志) log_layout QVBoxLayout() self.log_text QListWidget() log_layout.addWidget(self.log_text) result_group.setLayout(log_layout) layout.addWidget(result_group) layout.addWidget(log_group) panel.setLayout(layout) return panel def setup_connections(self): 设置信号槽连接 self.load_model_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(self.update_conf_threshold) self.iou_slider.valueChanged.connect(self.update_iou_threshold) def load_model(self): 加载模型 try: model_path, _ QFileDialog.getOpenFileName( self, 选择模型文件, , PyTorch Model (*.pt) ) if model_path: self.model YOLO(model_path) self.model_path_label.setText(os.path.basename(model_path)) self.add_log(模型加载成功) except Exception as e: QMessageBox.critical(self, 错误, f模型加载失败: {str(e)}) def detect_image(self): 图片检测 if not self.model: QMessageBox.warning(self, 警告, 请先加载模型) return image_path, _ QFileDialog.getOpenFileName( self, 选择图片, , Image Files (*.jpg *.jpeg *.png *.bmp) ) if image_path: self.detection_thread DetectionThread( self.model.ckpt_path, image, image_path ) self.setup_detection_thread() self.detection_thread.start() def setup_detection_thread(self): 设置检测线程信号连接 if self.detection_thread: self.detection_thread.frame_processed.connect(self.update_display) self.detection_thread.progress_updated.connect(self.update_progress) self.detection_thread.finished_signal.connect(self.detection_finished) def update_display(self, image, detections): 更新显示 # 转换图像格式 image_rgb cv2.cvtColor(image, cv2.COLOR_BGR2RGB) h, w, ch image_rgb.shape bytes_per_line ch * w qt_image QImage(image_rgb.data, w, h, bytes_per_line, QImage.Format_RGB888) # 缩放显示 pixmap QPixmap.fromImage(qt_image) scaled_pixmap pixmap.scaled( self.image_label.width(), self.image_label.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation ) self.image_label.setPixmap(scaled_pixmap) # 更新检测结果列表 self.update_detection_list(detections) def update_detection_list(self, detections): 更新检测结果列表 self.result_list.clear() for detection in detections: class_name self.get_class_name(detection[class_id]) confidence detection[confidence] item_text f{class_name}: {confidence:.3f} self.result_list.addItem(item_text) self.object_count_label.setText(f检测目标: {len(detections)}) def get_class_name(self, class_id): 根据类别ID获取类别名称 class_names [1筒, 2筒, 3筒, 4筒, 5筒, 6筒, 7筒, 8筒, 9筒, 1条, 2条, 3条, 4条, 5条, 6条, 7条, 8条, 9条, 1万, 2万, 3万, 4万, 5万, 6万, 7万, 8万, 9万, 东, 南, 西, 北, 中, 发, 白] return class_names[class_id] if class_id len(class_names) else f类别{class_id} def add_log(self, message): 添加日志 timestamp QDateTime.currentDateTime().toString(hh:mm:ss) self.log_text.addItem(f[{timestamp}] {message}) self.log_text.scrollToBottom() def update_conf_threshold(self, value): 更新置信度阈值 conf value / 100.0 self.conf_label.setText(f{conf:.2f}) if self.detection_thread: self.detection_thread.conf_threshold conf def update_iou_threshold(self, value): 更新IoU阈值 iou value / 100.0 self.iou_label.setText(f{iou:.2f}) if self.detection_thread: self.detection_thread.iou_threshold iou def stop_detection(self): 停止检测 if self.detection_thread and self.detection_thread.isRunning(): self.detection_thread.stop() self.detection_thread.wait() self.add_log(检测已停止) def detection_finished(self): 检测完成 self.progress_bar.setVisible(False) self.add_log(检测完成) def update_progress(self, value): 更新进度条 self.progress_bar.setValue(value) self.progress_bar.setVisible(True) def main(): app QApplication(sys.argv) window MainWindow() window.show() sys.exit(app.exec_()) if __name__ __main__: main()6.2 用户管理模块创建用户管理模块user_manager.pyimport json import hashlib import os from datetime import datetime class UserManager: def __init__(self, user_fileusers.json): self.user_file user_file self.users self.load_users() def load_users(self): 加载用户数据 if os.path.exists(self.user_file): with open(self.user_file, r, encodingutf-8) as f: return json.load(f) return {} def save_users(self): 保存用户数据 with open(self.user_file, w, encodingutf-8) as f: json.dump(self.users, f, ensure_asciiFalse, indent2) def hash_password(self, password): 密码加密 return hashlib.sha256(password.encode()).hexdigest() def register(self, username, password, email): 用户注册 if username in self.users: return False, 用户名已存在 if len(username) 3: return False, 用户名至少3个字符 if len(password) 6: return False, 密码至少6个字符 self.users[username] { password: self.hash_password(password), email: email, register_time: datetime.now().isoformat(), last_login: None } self.save_users() return True, 注册成功 def login(self, username, password): 用户登录 if username not in self.users: return False, 用户不存在 if self.users[username][password] ! self.hash_password(password): return False, 密码错误 self.users[username][last_login] datetime.now().isoformat() self.save_users() return True, 登录成功 def validate_email(self, email): 邮箱格式验证 import re pattern r^[a-zA-Z0-9._%-][a-zA-Z0-9.-]\.[a-zA-Z]{2,}$ return re.match(pattern, email) is not None7. 系统集成与部署实战7.1 项目结构规划完整的项目结构如下yolov8_mahjong_detector/ ├── data/ # 数据目录 │ ├── images/ # 图像数据 │ │ ├── train/ # 训练集 │ │ ├── val/ # 验证集 │ │ └── test/ # 测试集 │ └── labels/ # 标注文件 ├── models/ # 模型文件 │ ├── best.pt # 最佳模型权重 │ └── last.pt # 最后模型权重 ├── src/ # 源代码 │ ├── main.py # 主程序入口 │ ├── ui/ # 界面模块 │ │ ├── main_window.py # 主窗口 │ │ └── login_dialog.py # 登录对话框 │ ├── detection/ # 检测模块 │ │ ├── detector.py # 检测器类 │ │ └── utils.py # 工具函数 │ ├── data/ # 数据处理 │ │ ├── dataset.py # 数据集类 │ │ └── augmentations.py # 数据增强 │ └── utils/ # 工具模块 │ ├── config.py # 配置管理 │ └── logger.py # 日志管理 ├── configs/ # 配置文件 │ ├── dataset.yaml # 数据集配置 │ └── train_config.yaml # 训练配置 ├── requirements.txt # 依赖列表 ├── train.py # 训练脚本 └── README.md # 项目说明7.2 配置文件管理创建统一的配置管理系统config_manager.pyimport yaml import os from typing import Dict, Any class ConfigManager: def __init__(self, config_pathconfigs/): self.config_path config_path self.configs {} def load_config(self, config_name: str) - Dict[str, Any]: 加载配置文件 config_file os.path.join(self.config_path, f{config_name}.yaml) if not os.path.exists(config_file): raise FileNotFoundError(f配置文件不存在: {config_file}) with open(config_file, r, encodingutf-8) as f: config yaml.safe_load(f) self.configs[config_name] config return config def get_config(self, config_name: str, key: str None) - Any: 获取
YOLOv8麻将识别检测系统:从数据集构建到完整部署实战
发布时间:2026/7/13 2:48:40
YOLOv8麻将识别检测系统从数据集构建到完整部署实战麻将作为中国传统棋牌文化的代表在智能棋牌设备、在线教学、直播解说等场景中有着广泛的应用需求。然而麻将牌面识别一直是个技术难点——不同牌面之间纹理相似度高、拍摄角度多变、光照条件复杂传统基于规则或模板匹配的方法往往难以适应真实环境的变化。今天要介绍的基于YOLOv8的麻将识别检测系统在验证集上实现了92.6%的mAP50精度单张推理速度仅需5.4毫秒真正做到了高精度与实时性的平衡。本文将完整分享从数据集构建、模型训练到界面开发的全流程实战经验无论你是想了解YOLOv8在实际项目中的应用还是需要解决类似的细粒度目标检测问题都能从中获得实用价值。1. 项目核心价值与技术难点分析1.1 为什么麻将识别值得关注麻将识别看似是个细分领域实则包含了目标检测中的多个典型挑战细粒度分类、小目标检测、相似物体区分、复杂背景干扰等。成功解决麻将识别问题意味着掌握了处理类似复杂场景的关键技术能力。在实际应用中这套系统可以用于智能棋牌记录系统自动记录对局过程生成棋谱分析AI辅助教学平台实时识别牌面提供策略建议直播互动场景自动识别主播手牌增强观众体验棋牌游戏开发为AR/VR麻将游戏提供技术支撑1.2 技术难点与解决方案麻将识别的主要技术难点包括牌面相似度高数字牌中的6条与8条、3筒与5筒在视觉特征上极为相似传统方法容易误判。YOLOv8通过深度特征学习和多尺度检测能够捕捉细微的纹理差异。光照条件多变实战环境中光线角度、强度变化大会影响颜色和纹理的呈现。系统通过数据增强和模型鲁棒性设计应对这一挑战。拍摄角度多样牌面可能以各种角度出现在画面中增加了识别难度。YOLOv8的旋转不变性特征提取能力在此发挥关键作用。实时性要求对局过程中需要快速响应不能有明显延迟。YOLOv8的轻量化设计和优化推理速度确保了实时检测能力。2. YOLOv8核心原理与改进优势2.1 YOLOv8架构概述YOLOv8是Ultralytics公司推出的最新一代目标检测模型在之前版本的基础上进行了多项重要改进骨干网络优化采用更高效的CSPDarknet53结构在保持特征提取能力的同时减少计算量。特征金字塔增强改进的PAN-FPN结构更好地融合多尺度特征提升小目标检测效果。损失函数创新使用Task Aligned Assigner进行正负样本分配提高训练效率。标签分配策略引入Distribution Focal Loss改善类别不平衡问题。2.2 相对于前代模型的优势与YOLOv5相比YOLOv8在以下方面有明显提升精度更高在相同数据集上mAP通常有2-4个百分点的提升速度更快优化了模型结构和推理流程推理速度提升约15%训练更稳定改进的损失函数和训练策略使收敛更加平稳部署更友好支持ONNX、TensorRT等多种格式便于跨平台部署3. 环境配置与依赖安装3.1 硬件要求与推荐配置最低配置GPUNVIDIA GTX 1060 6GB或同等性能内存8GB RAM存储20GB可用空间推荐配置GPUNVIDIA RTX 3080 Ti或更高内存16GB RAM或更多存储SSD硬盘50GB可用空间3.2 Python环境配置首先创建并激活conda环境# 创建Python3.9环境 conda create -n yolov8-mahjong python3.9 conda activate yolov8-mahjong # 安装PyTorch根据CUDA版本选择 # CUDA 11.3 pip install torch1.12.1cu113 torchvision0.13.1cu113 torchaudio0.12.1 --extra-index-url https://download.pytorch.org/whl/cu113 # 或者CPU版本 pip install torch1.12.1cpu torchvision0.13.1cpu torchaudio0.12.1 --extra-index-url https://download.pytorch.org/whl/cpu3.3 项目依赖安装创建requirements.txt文件ultralytics8.0.0 opencv-python4.7.0.72 numpy1.24.3 pillow9.5.0 pyqt55.15.9 scipy1.10.1 matplotlib3.7.1 seaborn0.12.2 pandas2.0.2安装依赖pip install -r requirements.txt3.4 验证安装创建验证脚本check_env.pyimport torch import cv2 import numpy as np from PIL import Image print(fPyTorch版本: {torch.__version__}) print(fCUDA可用: {torch.cuda.is_available()}) print(fCUDA版本: {torch.version.cuda}) print(fOpenCV版本: {cv2.__version__}) if torch.cuda.is_available(): print(fGPU设备: {torch.cuda.get_device_name(0)}) print(fGPU内存: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f} GB) # 测试基本功能 img np.random.randint(0, 255, (640, 640, 3), dtypenp.uint8) print(环境测试通过!)4. 数据集构建与标注规范4.1 数据集结构设计麻将识别数据集包含42个类别涵盖所有常见牌型# 类别定义 classes [ 1B, 2B, 3B, 4B, 5B, 6B, 7B, 8B, 9B, # 筒子 1C, 2C, 3C, 4C, 5C, 6C, 7C, 8C, 9C, # 条子 1D, 2D, 3D, 4D, 5D, 6D, 7D, 8D, 9D, # 万子 EW, SW, WW, NW, # 风牌东南西北 GD, RD, WD, # 箭牌中发白 1F, 2F, 3F, 4F, 5F, 6F, 7F, 8F, 9F # 花牌可选 ]4.2 数据采集要点高质量的数据集是模型成功的基础采集时需要注意多样性保证不同光照条件自然光、灯光、混合光多种拍摄角度俯拍、斜拍、平拍各种背景环境桌面、手部、复杂背景不同摆放状态整齐排列、散乱放置、部分遮挡数据量规划训练集5565张图像验证集684张图像测试集482张图像每个类别至少100-150张图像4.3 标注工具与规范推荐使用LabelImg进行标注# 安装LabelImg pip install labelimg # 启动标注工具 labelimg标注规范文件labelmap.txt1B 2B 3B ...所有42个类别YOLO格式标注示例1B.txt0 0.512 0.634 0.124 0.156其中类别ID、中心点x、中心点y、宽度、高度均归一化到0-14.4 数据增强策略创建数据增强配置文件data_augmentation.pyimport albumentations as A from albumentations.pytorch import ToTensorV2 def get_train_transforms(image_size640): return A.Compose([ A.RandomResizedCrop(image_size, image_size, scale(0.8, 1.0)), A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(p0.2), A.HueSaturationValue(p0.2), A.GaussianBlur(blur_limit3, p0.1), A.RandomGamma(p0.2), A.Normalize(mean[0, 0, 0], std[1, 1, 1]), ToTensorV2(), ]) def get_val_transforms(image_size640): return A.Compose([ A.Resize(image_size, image_size), A.Normalize(mean[0, 0, 0], std[1, 1, 1]), ToTensorV2(), ])5. 模型训练与调优实战5.1 数据集配置文件创建dataset.yaml配置文件# dataset.yaml path: /path/to/mahjong_dataset train: images/train val: images/val test: images/test nc: 42 # 类别数量 names: [1B, 2B, 3B, 4B, 5B, 6B, 7B, 8B, 9B, 1C, 2C, 3C, 4C, 5C, 6C, 7C, 8C, 9C, 1D, 2D, 3D, 4D, 5D, 6D, 7D, 8D, 9D, EW, SW, WW, NW, GD, RD, WD, 1F, 2F, 3F, 4F, 5F, 6F, 7F, 8F, 9F]5.2 训练脚本实现创建训练脚本train.pyfrom ultralytics import YOLO import torch import os def train_mahjong_detector(): # 检查GPU可用性 device cuda if torch.cuda.is_available() else cpu print(f使用设备: {device}) # 加载预训练模型 model YOLO(yolov8n.pt) # 可根据需求选择yolov8s.pt、yolov8m.pt等 # 训练参数配置 results model.train( datadataset.yaml, epochs100, imgsz640, batch16, patience10, devicedevice, workers4, optimizerAdamW, lr00.001, weight_decay0.0005, saveTrue, save_period10, pretrainedTrue, verboseTrue ) return results if __name__ __main__: train_mahjong_detector()5.3 高级训练技巧学习率调度# 自定义学习率调度 def custom_lr_scheduler(optimizer, epoch): if epoch 10: lr 0.001 elif epoch 50: lr 0.0001 else: lr 0.00001 for param_group in optimizer.param_groups: param_group[lr] lr早停策略class EarlyStopping: def __init__(self, patience10, min_delta0.001): self.patience patience self.min_delta min_delta self.best_loss float(inf) self.counter 0 def __call__(self, val_loss): if val_loss self.best_loss - self.min_delta: self.best_loss val_loss self.counter 0 else: self.counter 1 if self.counter self.patience: return True return False5.4 模型评估与分析训练完成后进行模型评估from ultralytics import YOLO import matplotlib.pyplot as plt def evaluate_model(): # 加载最佳模型 model YOLO(runs/detect/train/weights/best.pt) # 在验证集上评估 metrics model.val( datadataset.yaml, splitval, imgsz640, conf0.25, iou0.45 ) # 输出评估结果 print(fmAP50: {metrics.box.map50:.3f}) print(fmAP50-95: {metrics.box.map:.3f}) print(fPrecision: {metrics.box.precision:.3f}) print(fRecall: {metrics.box.recall:.3f}) # 绘制混淆矩阵 model.confusion_matrix() return metrics if __name__ __main__: evaluate_model()6. PyQt5界面开发详解6.1 主界面架构设计创建主界面类MainWindow.pyimport sys import os from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QSlider, QCheckBox, QGroupBox, QListWidget, QTabWidget, QStatusBar, QFileDialog, QMessageBox, QProgressBar) from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QDateTime from PyQt5.QtGui import QPixmap, QImage, QFont import cv2 import numpy as np from ultralytics import YOLO class DetectionThread(QThread): 检测线程类 frame_processed pyqtSignal(np.ndarray, list) progress_updated pyqtSignal(int) finished_signal pyqtSignal() def __init__(self, model_path, source_type, source_pathNone): super().__init__() self.model_path model_path self.source_type source_type # image, video, camera self.source_path source_path self.conf_threshold 0.5 self.iou_threshold 0.45 self.selected_classes [] self.is_running True def run(self): 线程运行主函数 try: # 加载模型 self.model YOLO(self.model_path) if self.source_type image: self.process_image() elif self.source_type video: self.process_video() elif self.source_type camera: self.process_camera() except Exception as e: print(f检测线程错误: {e}) finally: self.finished_signal.emit() def process_image(self): 处理单张图片 image cv2.imread(self.source_path) results self.model.predict( sourceimage, confself.conf_threshold, iouself.iou_threshold, classesself.selected_classes if self.selected_classes else None ) # 绘制检测结果 annotated_image results[0].plot() detections [] for box in results[0].boxes: class_id int(box.cls) confidence float(box.conf) bbox box.xyxy[0].tolist() detections.append({ class_id: class_id, confidence: confidence, bbox: bbox }) self.frame_processed.emit(annotated_image, detections) def process_video(self): 处理视频文件 cap cv2.VideoCapture(self.source_path) total_frames int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) frame_count 0 while self.is_running and cap.isOpened(): ret, frame cap.read() if not ret: break # 处理每一帧 results self.model.predict( sourceframe, confself.conf_threshold, iouself.iou_threshold, classesself.selected_classes if self.selected_classes else None ) annotated_frame results[0].plot() detections [] for box in results[0].boxes: class_id int(box.cls) confidence float(box.conf) bbox box.xyxy[0].tolist() detections.append({ class_id: class_id, confidence: confidence, bbox: bbox }) self.frame_processed.emit(annotated_frame, detections) frame_count 1 progress int((frame_count / total_frames) * 100) self.progress_updated.emit(progress) cap.release() def process_camera(self): 处理摄像头实时流 cap cv2.VideoCapture(0) # 默认摄像头 while self.is_running and cap.isOpened(): ret, frame cap.read() if not ret: break results self.model.predict( sourceframe, confself.conf_threshold, iouself.iou_threshold, classesself.selected_classes if self.selected_classes else None ) annotated_frame results[0].plot() detections [] for box in results[0].boxes: class_id int(box.cls) confidence float(box.conf) bbox box.xyxy[0].tolist() detections.append({ class_id: class_id, confidence: confidence, bbox: bbox }) self.frame_processed.emit(annotated_frame, detections) cap.release() def stop(self): 停止检测 self.is_running False class MainWindow(QMainWindow): 主窗口类 def __init__(self): super().__init__() self.model None self.detection_thread None self.init_ui() self.setup_connections() def init_ui(self): 初始化界面 self.setWindowTitle(YOLOv8麻将识别检测系统) self.setGeometry(100, 100, 1400, 900) # 中心部件 central_widget QWidget() self.setCentralWidget(central_widget) # 主布局 main_layout QHBoxLayout() central_widget.setLayout(main_layout) # 左侧控制面板 left_panel self.create_left_panel() main_layout.addWidget(left_panel, 1) # 中间显示区域 center_panel self.create_center_panel() main_layout.addWidget(center_panel, 3) # 右侧信息面板 right_panel self.create_right_panel() main_layout.addWidget(right_panel, 1) # 状态栏 self.status_bar QStatusBar() self.setStatusBar(self.status_bar) self.status_bar.showMessage(系统就绪) def create_left_panel(self): 创建左侧控制面板 panel QWidget() layout QVBoxLayout() # 模型加载区域 model_group QGroupBox(模型配置) model_layout QVBoxLayout() self.model_path_label QLabel(未加载模型) self.load_model_btn QPushButton(加载模型) model_layout.addWidget(self.model_path_label) model_layout.addWidget(self.load_model_btn) model_group.setLayout(model_layout) # 检测参数区域 param_group QGroupBox(检测参数) param_layout QVBoxLayout() # 置信度阈值 conf_layout QHBoxLayout() conf_layout.addWidget(QLabel(置信度:)) self.conf_slider QSlider(Qt.Horizontal) self.conf_slider.setRange(0, 100) self.conf_slider.setValue(50) self.conf_label QLabel(0.50) conf_layout.addWidget(self.conf_slider) conf_layout.addWidget(self.conf_label) # IoU阈值 iou_layout QHBoxLayout() iou_layout.addWidget(QLabel(IoU阈值:)) self.iou_slider QSlider(Qt.Horizontal) self.iou_slider.setRange(0, 100) 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.addLayout(conf_layout) param_layout.addLayout(iou_layout) param_group.setLayout(param_layout) # 类别选择区域 class_group QGroupBox(检测类别) class_layout QVBoxLayout() self.class_list QListWidget() # 添加麻将牌类别 classes [1筒, 2筒, 3筒, 4筒, 5筒, 6筒, 7筒, 8筒, 9筒, 1条, 2条, 3条, 4条, 5条, 6条, 7条, 8条, 9条, 1万, 2万, 3万, 4万, 5万, 6万, 7万, 8万, 9万, 东, 南, 西, 北, 中, 发, 白] for cls in classes: self.class_list.addItem(cls) self.class_list.setSelectionMode(QListWidget.MultiSelection) class_layout.addWidget(self.class_list) param_group.setLayout(class_layout) layout.addWidget(model_group) layout.addWidget(param_group) layout.addStretch() panel.setLayout(layout) return panel def create_center_panel(self): 创建中间显示区域 panel QWidget() layout QVBoxLayout() # 图像显示标签 self.image_label QLabel() self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setStyleSheet(border: 1px solid gray;) self.image_label.setText(请选择检测源) self.image_label.setMinimumSize(640, 480) # 工具栏 toolbar_layout QHBoxLayout() self.image_btn QPushButton(图片检测) self.video_btn QPushButton(视频检测) self.camera_btn QPushButton(摄像头检测) self.stop_btn QPushButton(停止检测) self.save_btn QPushButton(保存结果) toolbar_layout.addWidget(self.image_btn) toolbar_layout.addWidget(self.video_btn) toolbar_layout.addWidget(self.camera_btn) toolbar_layout.addWidget(self.stop_btn) toolbar_layout.addWidget(self.save_btn) toolbar_layout.addStretch() # 进度条 self.progress_bar QProgressBar() self.progress_bar.setVisible(False) layout.addWidget(self.image_label) layout.addLayout(toolbar_layout) layout.addWidget(self.progress_bar) panel.setLayout(layout) return panel def create_right_panel(self): 创建右侧信息面板 panel QWidget() layout QVBoxLayout() # 检测结果区域 result_group QGroupBox(检测结果) result_layout QVBoxLayout() self.result_list QListWidget() self.fps_label QLabel(FPS: --) self.object_count_label QLabel(检测目标: 0) result_layout.addWidget(self.fps_label) result_layout.addWidget(self.object_count_label) result_layout.addWidget(self.result_list) result_group.setLayout(result_layout) # 日志区域 log_group QGroupBox(系统日志) log_layout QVBoxLayout() self.log_text QListWidget() log_layout.addWidget(self.log_text) result_group.setLayout(log_layout) layout.addWidget(result_group) layout.addWidget(log_group) panel.setLayout(layout) return panel def setup_connections(self): 设置信号槽连接 self.load_model_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(self.update_conf_threshold) self.iou_slider.valueChanged.connect(self.update_iou_threshold) def load_model(self): 加载模型 try: model_path, _ QFileDialog.getOpenFileName( self, 选择模型文件, , PyTorch Model (*.pt) ) if model_path: self.model YOLO(model_path) self.model_path_label.setText(os.path.basename(model_path)) self.add_log(模型加载成功) except Exception as e: QMessageBox.critical(self, 错误, f模型加载失败: {str(e)}) def detect_image(self): 图片检测 if not self.model: QMessageBox.warning(self, 警告, 请先加载模型) return image_path, _ QFileDialog.getOpenFileName( self, 选择图片, , Image Files (*.jpg *.jpeg *.png *.bmp) ) if image_path: self.detection_thread DetectionThread( self.model.ckpt_path, image, image_path ) self.setup_detection_thread() self.detection_thread.start() def setup_detection_thread(self): 设置检测线程信号连接 if self.detection_thread: self.detection_thread.frame_processed.connect(self.update_display) self.detection_thread.progress_updated.connect(self.update_progress) self.detection_thread.finished_signal.connect(self.detection_finished) def update_display(self, image, detections): 更新显示 # 转换图像格式 image_rgb cv2.cvtColor(image, cv2.COLOR_BGR2RGB) h, w, ch image_rgb.shape bytes_per_line ch * w qt_image QImage(image_rgb.data, w, h, bytes_per_line, QImage.Format_RGB888) # 缩放显示 pixmap QPixmap.fromImage(qt_image) scaled_pixmap pixmap.scaled( self.image_label.width(), self.image_label.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation ) self.image_label.setPixmap(scaled_pixmap) # 更新检测结果列表 self.update_detection_list(detections) def update_detection_list(self, detections): 更新检测结果列表 self.result_list.clear() for detection in detections: class_name self.get_class_name(detection[class_id]) confidence detection[confidence] item_text f{class_name}: {confidence:.3f} self.result_list.addItem(item_text) self.object_count_label.setText(f检测目标: {len(detections)}) def get_class_name(self, class_id): 根据类别ID获取类别名称 class_names [1筒, 2筒, 3筒, 4筒, 5筒, 6筒, 7筒, 8筒, 9筒, 1条, 2条, 3条, 4条, 5条, 6条, 7条, 8条, 9条, 1万, 2万, 3万, 4万, 5万, 6万, 7万, 8万, 9万, 东, 南, 西, 北, 中, 发, 白] return class_names[class_id] if class_id len(class_names) else f类别{class_id} def add_log(self, message): 添加日志 timestamp QDateTime.currentDateTime().toString(hh:mm:ss) self.log_text.addItem(f[{timestamp}] {message}) self.log_text.scrollToBottom() def update_conf_threshold(self, value): 更新置信度阈值 conf value / 100.0 self.conf_label.setText(f{conf:.2f}) if self.detection_thread: self.detection_thread.conf_threshold conf def update_iou_threshold(self, value): 更新IoU阈值 iou value / 100.0 self.iou_label.setText(f{iou:.2f}) if self.detection_thread: self.detection_thread.iou_threshold iou def stop_detection(self): 停止检测 if self.detection_thread and self.detection_thread.isRunning(): self.detection_thread.stop() self.detection_thread.wait() self.add_log(检测已停止) def detection_finished(self): 检测完成 self.progress_bar.setVisible(False) self.add_log(检测完成) def update_progress(self, value): 更新进度条 self.progress_bar.setValue(value) self.progress_bar.setVisible(True) def main(): app QApplication(sys.argv) window MainWindow() window.show() sys.exit(app.exec_()) if __name__ __main__: main()6.2 用户管理模块创建用户管理模块user_manager.pyimport json import hashlib import os from datetime import datetime class UserManager: def __init__(self, user_fileusers.json): self.user_file user_file self.users self.load_users() def load_users(self): 加载用户数据 if os.path.exists(self.user_file): with open(self.user_file, r, encodingutf-8) as f: return json.load(f) return {} def save_users(self): 保存用户数据 with open(self.user_file, w, encodingutf-8) as f: json.dump(self.users, f, ensure_asciiFalse, indent2) def hash_password(self, password): 密码加密 return hashlib.sha256(password.encode()).hexdigest() def register(self, username, password, email): 用户注册 if username in self.users: return False, 用户名已存在 if len(username) 3: return False, 用户名至少3个字符 if len(password) 6: return False, 密码至少6个字符 self.users[username] { password: self.hash_password(password), email: email, register_time: datetime.now().isoformat(), last_login: None } self.save_users() return True, 注册成功 def login(self, username, password): 用户登录 if username not in self.users: return False, 用户不存在 if self.users[username][password] ! self.hash_password(password): return False, 密码错误 self.users[username][last_login] datetime.now().isoformat() self.save_users() return True, 登录成功 def validate_email(self, email): 邮箱格式验证 import re pattern r^[a-zA-Z0-9._%-][a-zA-Z0-9.-]\.[a-zA-Z]{2,}$ return re.match(pattern, email) is not None7. 系统集成与部署实战7.1 项目结构规划完整的项目结构如下yolov8_mahjong_detector/ ├── data/ # 数据目录 │ ├── images/ # 图像数据 │ │ ├── train/ # 训练集 │ │ ├── val/ # 验证集 │ │ └── test/ # 测试集 │ └── labels/ # 标注文件 ├── models/ # 模型文件 │ ├── best.pt # 最佳模型权重 │ └── last.pt # 最后模型权重 ├── src/ # 源代码 │ ├── main.py # 主程序入口 │ ├── ui/ # 界面模块 │ │ ├── main_window.py # 主窗口 │ │ └── login_dialog.py # 登录对话框 │ ├── detection/ # 检测模块 │ │ ├── detector.py # 检测器类 │ │ └── utils.py # 工具函数 │ ├── data/ # 数据处理 │ │ ├── dataset.py # 数据集类 │ │ └── augmentations.py # 数据增强 │ └── utils/ # 工具模块 │ ├── config.py # 配置管理 │ └── logger.py # 日志管理 ├── configs/ # 配置文件 │ ├── dataset.yaml # 数据集配置 │ └── train_config.yaml # 训练配置 ├── requirements.txt # 依赖列表 ├── train.py # 训练脚本 └── README.md # 项目说明7.2 配置文件管理创建统一的配置管理系统config_manager.pyimport yaml import os from typing import Dict, Any class ConfigManager: def __init__(self, config_pathconfigs/): self.config_path config_path self.configs {} def load_config(self, config_name: str) - Dict[str, Any]: 加载配置文件 config_file os.path.join(self.config_path, f{config_name}.yaml) if not os.path.exists(config_file): raise FileNotFoundError(f配置文件不存在: {config_file}) with open(config_file, r, encodingutf-8) as f: config yaml.safe_load(f) self.configs[config_name] config return config def get_config(self, config_name: str, key: str None) - Any: 获取