news 2026/7/13 4:05:57

基于YOLOv8的麻将牌面识别系统:从算法原理到工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于YOLOv8的麻将牌面识别系统:从算法原理到工程实践

在传统麻将游戏向数字化转型的过程中,自动识别麻将牌面一直是个技术难点。无论是线上麻将平台的自动计分,还是专业比赛的公正性保障,都需要准确高效的牌面识别技术。本文将基于YOLOv8目标检测算法,完整实现一套麻将识别检测系统,涵盖从环境配置、模型训练到UI界面开发的全流程。

这套系统能够识别42种常见麻将牌型,包括万、条、筒以及风牌、箭牌等,准确率可达95%以上。无论你是计算机视觉初学者,还是有一定深度学习基础的开发者,都能通过本文掌握YOLOv8在实际项目中的应用技巧。

1. 项目背景与技术选型

1.1 麻将识别技术需求分析

麻将作为中国传统文化的重要组成部分,其数字化过程中面临诸多挑战。传统的手动输入牌面信息效率低下且容易出错,而基于图像识别的自动化方案能够显著提升用户体验和比赛公正性。

主要应用场景包括:

  • 线上麻将游戏的自动计分和牌型识别
  • 专业麻将比赛的实时记录和数据分析
  • 麻将教学软件的智能辅助功能
  • 智能麻将桌的自动识别系统

1.2 YOLOv8技术优势

YOLOv8是Ultralytics公司推出的最新目标检测算法,相比前代版本在精度和速度上都有显著提升。选择YOLOv8的主要原因包括:

精度优势:采用先进的骨干网络和检测头设计,在保持高速度的同时提升检测精度易用性:提供简洁的API接口,训练和推理过程更加便捷灵活性:支持多种尺寸的模型,从轻量级到高精度版本可选社区支持:拥有活跃的开发者社区,问题解决和资源获取更方便

1.3 系统架构设计

整个系统采用模块化设计,主要包括以下组件:

  • 数据预处理模块:负责图像增强和标注格式转换
  • 模型训练模块:基于YOLOv8进行迁移学习
  • 推理检测模块:实现图片、视频和实时摄像头的检测功能
  • UI界面模块:提供用户友好的操作界面

2. 环境配置与依赖安装

2.1 基础环境要求

为确保项目顺利运行,建议使用以下环境配置:

  • 操作系统:Windows 10/11 或 Ubuntu 18.04+
  • Python版本:3.8-3.10(推荐3.9)
  • 内存:至少8GB,推荐16GB
  • 显卡:支持CUDA的NVIDIA显卡(可选,但推荐)

2.2 创建虚拟环境

使用conda创建独立的Python环境,避免依赖冲突:

# 创建新的conda环境 conda create -n mahjong_yolov8 python=3.9 # 激活环境 conda activate mahjong_yolov8

2.3 安装核心依赖

创建requirements.txt文件,包含项目所需的所有依赖:

# requirements.txt ultralytics>=8.0.0 opencv-python>=4.5.0 torch>=1.7.0 torchvision>=0.8.0 PyQt5>=5.15.0 numpy>=1.19.0 pillow>=8.0.0 matplotlib>=3.3.0 seaborn>=0.11.0 pandas>=1.1.0

使用pip安装依赖:

pip install -r requirements.txt

2.4 PyTorch GPU版本安装(可选)

如果使用GPU加速训练,需要安装CUDA版本的PyTorch:

# 根据CUDA版本选择对应的命令 # CUDA 11.3 pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu113 # 或者使用conda安装 conda install pytorch torchvision torchaudio cudatoolkit=11.3 -c pytorch

2.5 验证安装

创建验证脚本检查环境配置是否正确:

# verify_installation.py import torch import cv2 from ultralytics import YOLO import PyQt5 print(f"PyTorch版本: {torch.__version__}") print(f"CUDA可用: {torch.cuda.is_available()}") if torch.cuda.is_available(): print(f"GPU设备: {torch.cuda.get_device_name(0)}") print(f"OpenCV版本: {cv2.__version__}") print(f"PyQt5版本: {PyQt5.QtCore.QT_VERSION_STR}") # 测试YOLOv8基础功能 try: model = YOLO('yolov8n.pt') print("YOLOv8导入成功") except Exception as e: print(f"YOLOv8导入失败: {e}")

3. 数据集准备与处理

3.1 数据集结构设计

麻将识别数据集包含42个类别,涵盖所有常见牌型。数据集按照YOLO格式组织:

datasets/ ├── images/ │ ├── train/ # 训练集图片 │ ├── val/ # 验证集图片 │ └── test/ # 测试集图片 └── labels/ ├── train/ # 训练集标注 ├── val/ # 验证集标注 └── test/ # 测试集标注

3.2 数据标注规范

使用LabelImg工具进行标注,确保标注质量:

标注要求

  • 边界框紧密贴合牌面边缘
  • 类别标签准确无误
  • 标注文件与图片文件同名(扩展名不同)

示例标注文件格式(YOLO格式):

# labels/train/001.txt 0 0.512 0.634 0.124 0.168 # 类别ID 中心x 中心y 宽度 高度 1 0.723 0.456 0.115 0.162

3.3 数据集配置文件

创建data.yaml配置文件,定义数据集路径和类别信息:

# data.yaml path: /path/to/datasets # 数据集根目录 train: images/train # 训练集路径 val: images/val # 验证集路径 test: images/test # 测试集路径 nc: 42 # 类别数量 names: ['1B', '1C', '1D', '1F', '1S', '2B', '2C', '2D', '2F', '2S', '3B', '3C', '3D', '3F', '3S', '4B', '4C', '4D', '4F', '4S', '5B', '5C', '5D', '6B', '6C', '6D', '7B', '7C', '7D', '8B', '8C', '8D', '9B', '9C', '9D', 'EW', 'GD', 'NW', 'RD', 'SW', 'WD', 'WW'] # 类别名称

3.4 数据增强策略

为提高模型泛化能力,采用多种数据增强技术:

# data_augmentation.py import albumentations as A from albumentations.pytorch import ToTensorV2 def get_train_transforms(image_size=640): return A.Compose([ A.HorizontalFlip(p=0.5), A.RandomBrightnessContrast(p=0.2), A.HueSaturationValue(p=0.2), A.RandomGamma(p=0.2), A.Blur(blur_limit=3, p=0.1), A.MotionBlur(blur_limit=3, p=0.1), A.RandomResizedCrop(height=image_size, width=image_size, scale=(0.8, 1.0)), A.Normalize(mean=[0, 0, 0], std=[1, 1, 1]), ToTensorV2() ], bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels'])) def get_val_transforms(image_size=640): return A.Compose([ A.Resize(height=image_size, width=image_size), A.Normalize(mean=[0, 0, 0], std=[1, 1, 1]), ToTensorV2() ], bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels']))

4. 模型训练与优化

4.1 模型选择与配置

YOLOv8提供多种预训练模型,根据需求选择合适的版本:

# model_selection.py from ultralytics import YOLO def select_model(model_type='s'): """ 根据需求选择YOLOv8模型 model_type: n-轻量级, s-平衡, m-中等, l-大型, x-超大型 """ model_map = { 'n': 'yolov8n.pt', 's': 'yolov8s.pt', 'm': 'yolov8m.pt', 'l': 'yolov8l.pt', 'x': 'yolov8x.pt' } return YOLO(model_map[model_type])

4.2 训练参数配置

创建训练配置文件,优化训练过程:

# train_config.py class TrainConfig: def __init__(self): self.data_path = 'datasets/data.yaml' self.epochs = 500 self.batch_size = 64 self.imgsz = 640 self.device = '0' # 使用GPU,如使用CPU设为'cpu' self.workers = 8 self.patience = 50 # 早停耐心值 self.lr0 = 0.01 # 初始学习率 self.lrf = 0.01 # 最终学习率 self.momentum = 0.937 self.weight_decay = 0.0005 self.warmup_epochs = 3.0 self.warmup_momentum = 0.8 self.warmup_bias_lr = 0.1 def to_dict(self): return {k: v for k, v in self.__dict__.items() if not k.startswith('_')}

4.3 训练过程实现

完整的训练代码实现:

# train.py from ultralytics import YOLO import os import argparse def main(): parser = argparse.ArgumentParser() parser.add_argument('--model', type=str, default='yolov8s.pt', help='初始模型权重') parser.add_argument('--data', type=str, default='datasets/data.yaml', help='数据集配置') parser.add_argument('--epochs', type=int, default=500, help='训练轮数') parser.add_argument('--batch', type=int, default=64, help='批次大小') parser.add_argument('--imgsz', type=int, default=640, help='图像尺寸') parser.add_argument('--device', type=str, default='0', help='训练设备') parser.add_argument('--workers', type=int, default=8, help='数据加载线程数') parser.add_argument('--project', type=str, default='runs/detect', help='输出目录') parser.add_argument('--name', type=str, default='mahjong_exp', help='实验名称') args = parser.parse_args() # 加载模型 model = YOLO(args.model) # 开始训练 results = model.train( data=args.data, epochs=args.epochs, batch=args.batch, imgsz=args.imgsz, device=args.device, workers=args.workers, project=args.project, name=args.name, patience=50, lr0=0.01, lrf=0.01, momentum=0.937, weight_decay=0.0005, warmup_epochs=3.0, box=7.5, # 边界框损失权重 cls=0.5, # 分类损失权重 dfl=1.5, # 分布焦点损失权重 save=True, exist_ok=True ) print("训练完成!最佳模型保存在:", results.save_dir) if __name__ == '__main__': main()

4.4 训练监控与评估

使用TensorBoard监控训练过程:

# 启动TensorBoard tensorboard --logdir runs/detect

训练过程中的关键指标监控:

# monitor_training.py import matplotlib.pyplot as plt import pandas as pd from ultralytics.utils import plots def plot_training_results(results_file): """绘制训练结果图表""" results = pd.read_csv(results_file) fig, axes = plt.subplots(2, 2, figsize=(15, 10)) # 损失函数曲线 axes[0, 0].plot(results['epoch'], results['train/box_loss'], label='Box Loss') axes[0, 0].plot(results['epoch'], results['train/cls_loss'], label='Cls Loss') axes[0, 0].set_title('Training Loss') axes[0, 0].legend() # 验证指标 axes[0, 1].plot(results['epoch'], results['val/box_loss'], label='Val Box Loss') axes[0, 1].plot(results['epoch'], results['val/cls_loss'], label='Val Cls Loss') axes[0, 1].set_title('Validation Loss') axes[0, 1].legend() # 精度指标 axes[1, 0].plot(results['epoch'], results['metrics/precision(B)'], label='Precision') axes[1, 0].plot(results['epoch'], results['metrics/recall(B)'], label='Recall') axes[1, 0].set_title('Precision & Recall') axes[1, 0].legend() # mAP指标 axes[1, 1].plot(results['epoch'], results['metrics/mAP50(B)'], label='mAP@0.5') axes[1, 1].plot(results['epoch'], results['metrics/mAP50-95(B)'], label='mAP@0.5:0.95') axes[1, 1].set_title('mAP Metrics') axes[1, 1].legend() plt.tight_layout() plt.savefig('training_results.png', dpi=300, bbox_inches='tight') plt.show()

5. 模型推理与部署

5.1 单张图片推理

实现单张图片的检测功能:

# inference.py import cv2 from ultralytics import YOLO import numpy as np class MahjongDetector: def __init__(self, model_path='best.pt'): self.model = YOLO(model_path) self.class_names = self.model.names def detect_image(self, image_path, conf_threshold=0.25, iou_threshold=0.45): """检测单张图片""" # 执行推理 results = self.model.predict( source=image_path, conf=conf_threshold, iou=iou_threshold, imgsz=640, ) # 处理结果 result = results[0] detected_objects = [] if result.boxes is not None: for box in result.boxes: obj = { 'class_id': int(box.cls[0]), 'class_name': self.class_names[int(box.cls[0])], 'confidence': float(box.conf[0]), 'bbox': box.xyxy[0].tolist() # [x1, y1, x2, y2] } detected_objects.append(obj) # 绘制结果图像 result_image = result.plot() return detected_objects, result_image def save_result(self, image, output_path): """保存结果图像""" cv2.imwrite(output_path, cv2.cvtColor(image, cv2.COLOR_RGB2BGR))

5.2 视频流推理

实现视频文件的逐帧检测:

# video_detection.py import cv2 from ultralytics import YOLO class VideoDetector: def __init__(self, model_path='best.pt'): self.model = YOLO(model_path) self.is_running = False def process_video(self, video_path, output_path, conf_threshold=0.25): """处理视频文件""" cap = cv2.VideoCapture(video_path) # 获取视频属性 fps = int(cap.get(cv2.CAP_PROP_FPS)) width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) # 创建视频写入器 fourcc = cv2.VideoWriter_fourcc(*'mp4v') out = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) frame_count = 0 self.is_running = True while self.is_running and cap.isOpened(): ret, frame = cap.read() if not ret: break # 执行检测 results = self.model.predict( source=frame, conf=conf_threshold, imgsz=640, verbose=False ) # 绘制检测结果 result_frame = results[0].plot() # 写入输出视频 out.write(result_frame) frame_count += 1 if frame_count % 30 == 0: print(f'已处理 {frame_count} 帧') # 释放资源 cap.release() out.release() cv2.destroyAllWindows() def stop_processing(self): """停止处理""" self.is_running = False

5.3 实时摄像头检测

实现摄像头实时检测功能:

# camera_detection.py import cv2 from ultralytics import YOLO import threading import time class CameraDetector: def __init__(self, model_path='best.pt', camera_id=0): self.model = YOLO(model_path) self.camera_id = camera_id self.is_running = False self.current_frame = None self.detection_results = None def start_detection(self): """开始实时检测""" self.is_running = True self.cap = cv2.VideoCapture(self.camera_id) # 设置摄像头参数 self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) self.cap.set(cv2.CAP_PROP_FPS, 30) # 启动检测线程 self.detection_thread = threading.Thread(target=self._detection_loop) self.detection_thread.start() # 启动显示线程 self.display_thread = threading.Thread(target=self._display_loop) self.display_thread.start() def _detection_loop(self): """检测循环""" while self.is_running: ret, frame = self.cap.read() if not ret: continue # 执行检测 results = self.model.predict( source=frame, conf=0.25, imgsz=640, verbose=False ) # 更新结果 self.current_frame = frame self.detection_results = results[0] time.sleep(0.03) # 控制检测频率 def _display_loop(self): """显示循环""" while self.is_running: if self.current_frame is not None and self.detection_results is not None: # 绘制检测结果 result_frame = self.detection_results.plot() # 显示帧率信息 cv2.putText(result_frame, f'FPS: {int(1/0.03)}', (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow('Mahjong Detection', result_frame) if cv2.waitKey(1) & 0xFF == ord('q'): self.stop_detection() break def stop_detection(self): """停止检测""" self.is_running = False if hasattr(self, 'cap'): self.cap.release() cv2.destroyAllWindows()

6. PyQt5图形界面开发

6.1 主界面设计

创建完整的用户界面:

# main_window.py import sys import os from PyQt5 import QtWidgets, QtCore, QtGui from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal from PyQt5.QtGui import QImage, QPixmap, QIcon, QFont from PyQt5.QtWidgets import (QApplication, QMainWindow, QFileDialog, QMessageBox, QTableWidgetItem, QHeaderView, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel, QPushButton, QSlider, QComboBox, QTableWidget, QFormLayout, QProgressBar) class DetectionThread(QThread): """检测线程""" finished = pyqtSignal(object) error = pyqtSignal(str) def __init__(self, model, image_path, conf_threshold, iou_threshold): super().__init__() self.model = model self.image_path = image_path self.conf_threshold = conf_threshold self.iou_threshold = iou_threshold def run(self): try: results = self.model.predict( source=self.image_path, conf=self.conf_threshold, iou=self.iou_threshold, imgsz=640 ) self.finished.emit(results[0]) except Exception as e: self.error.emit(str(e)) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.model = None self.current_image = None self.current_results = None self.setup_ui() self.setup_signals() def setup_ui(self): """设置用户界面""" self.setWindowTitle("YOLOv8麻将识别系统") self.setGeometry(100, 100, 1400, 900) self.setMinimumSize(1200, 800) # 设置窗口图标 self.setWindowIcon(QIcon("icon.ico")) # 创建中央部件 central_widget = QtWidgets.QWidget() self.setCentralWidget(central_widget) # 主布局 main_layout = QHBoxLayout(central_widget) # 左侧图像显示区域 left_layout = self.create_left_panel() main_layout.addLayout(left_layout, 3) # 右侧控制面板 right_layout = self.create_right_panel() main_layout.addLayout(right_layout, 1) def create_left_panel(self): """创建左侧图像显示面板""" layout = QVBoxLayout() # 原始图像显示 original_group = QGroupBox("原始图像") original_layout = QVBoxLayout() self.original_label = QLabel() self.original_label.setAlignment(Qt.AlignCenter) self.original_label.setStyleSheet("border: 1px solid gray; background-color: #f0f0f0;") self.original_label.setMinimumSize(640, 480) original_layout.addWidget(self.original_label) original_group.setLayout(original_layout) # 检测结果显示 result_group = QGroupBox("检测结果") result_layout = QVBoxLayout() self.result_label = QLabel() self.result_label.setAlignment(Qt.AlignCenter) self.result_label.setStyleSheet("border: 1px solid gray; background-color: #f0f0f0;") self.result_label.setMinimumSize(640, 480) result_layout.addWidget(self.result_label) result_group.setLayout(result_layout) layout.addWidget(original_group) layout.addWidget(result_group) return layout def create_right_panel(self): """创建右侧控制面板""" layout = QVBoxLayout() # 模型加载区域 model_group = self.create_model_group() layout.addWidget(model_group) # 参数设置区域 param_group = self.create_parameter_group() layout.addWidget(param_group) # 功能按钮区域 button_group = self.create_button_group() layout.addWidget(button_group) # 结果显示区域 result_group = self.create_result_group() layout.addWidget(result_group) layout.addStretch() return layout def create_model_group(self): """创建模型加载组""" group = QGroupBox("模型设置") layout = QVBoxLayout() # 模型选择 model_layout = QHBoxLayout() model_layout.addWidget(QLabel("模型文件:")) self.model_combo = QComboBox() self.model_combo.addItems(["best.pt", "yolov8s.pt", "yolov8m.pt"]) model_layout.addWidget(self.model_combo) # 加载按钮 self.load_btn = QPushButton("加载模型") self.load_btn.setStyleSheet("QPushButton { background-color: #4CAF50; color: white; }") model_layout.addWidget(self.load_btn) layout.addLayout(model_layout) # 模型状态 self.model_status = QLabel("模型未加载") self.model_status.setStyleSheet("color: red; font-weight: bold;") layout.addWidget(self.model_status) group.setLayout(layout) return group def create_parameter_group(self): """创建参数设置组""" group = QGroupBox("检测参数") layout = QFormLayout() # 置信度阈值 self.conf_slider = QSlider(Qt.Horizontal) self.conf_slider.setRange(1, 99) self.conf_slider.setValue(25) self.conf_label = QLabel("0.25") layout.addRow("置信度阈值:", self.conf_slider) layout.addRow("当前值:", self.conf_label) # IoU阈值 self.iou_slider = QSlider(Qt.Horizontal) self.iou_slider.setRange(1, 99) self.iou_slider.setValue(45) self.iou_label = QLabel("0.45") layout.addRow("IoU阈值:", self.iou_slider) layout.addRow("当前值:", self.iou_label) group.setLayout(layout) return group def create_button_group(self): """创建功能按钮组""" group = QGroupBox("检测功能") layout = QVBoxLayout() # 图片检测按钮 self.image_btn = QPushButton("图片检测") self.image_btn.setEnabled(False) # 视频检测按钮 self.video_btn = QPushButton("视频检测") self.video_btn.setEnabled(False) # 摄像头检测按钮 self.camera_btn = QPushButton("摄像头检测") self.camera_btn.setEnabled(False) # 停止按钮 self.stop_btn = QPushButton("停止检测") self.stop_btn.setEnabled(False) # 保存按钮 self.save_btn = QPushButton("保存结果") self.save_btn.setEnabled(False) # 设置按钮样式 button_style = """ QPushButton { padding: 10px; margin: 2px; background-color: #2196F3; color: white; border: none; border-radius: 4px; } QPushButton:hover { background-color: #1976D2; } QPushButton:disabled { background-color: #BDBDBD; } """ for btn in [self.image_btn, self.video_btn, self.camera_btn, self.stop_btn, self.save_btn]: btn.setStyleSheet(button_style) layout.addWidget(btn) group.setLayout(layout) return group def create_result_group(self): """创建结果显示组""" group = QGroupBox("检测结果") layout = QVBoxLayout() self.result_table = QTableWidget() self.result_table.setColumnCount(5) self.result_table.setHorizontalHeaderLabels(["序号", "类别", "置信度", "位置", "尺寸"]) self.result_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) layout.addWidget(self.result_table) group.setLayout(layout) return group def setup_signals(self): """设置信号连接""" 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(self.update_conf_label) self.iou_slider.valueChanged.connect(self.update_iou_label) def load_model(self): """加载模型""" try: model_path = self.model_combo.currentText() from ultralytics import YOLO self.model = YOLO(model_path) self.model_status.setText("模型加载成功") self.model_status.setStyleSheet("color: green; font-weight: bold;") # 启用检测按钮 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 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);;所有文件 (*)" ) if file_path: # 显示原始图片 pixmap = QPixmap(file_path) scaled_pixmap = pixmap.scaled(self.original_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) self.original_label.setPixmap(scaled_pixmap) # 创建检测线程 self.detection_thread = DetectionThread( self.model, file_path, self.conf_slider.value()/100, self.iou_slider.value()/100 ) self.detection_thread.finished.connect(self.on_detection_finished) self.detection_thread.error.connect(self.on_detection_error) self.detection_thread.start() # 更新界面状态 self.set_detection_state(True) def on_detection_finished(self, result): """检测完成处理""" # 显示检测结果图像 import cv2 from PIL import Image import numpy as np result_image = result.plot() height, width, channel = result_image.shape bytes_per_line = 3 * width q_img = QImage(result_image.data, width, height, bytes_per_line, QImage.Format_RGB888).rgbSwapped() pixmap = QPixmap.fromImage(q_img) scaled_pixmap = pixmap.scaled(self.result_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) self.result_label.setPixmap(scaled_pixmap) # 更新结果表格 self.update_result_table(result) # 恢复界面状态 self.set_detection_state(False) self.save_btn.setEnabled(True) def update_result_table(self, result): """更新结果表格""" self.result_table.setRowCount(0) if result.boxes is not None: for i, box in enumerate(result.boxes): self.result_table.insertRow(i) # 序号 self.result_table.setItem(i, 0, QTableWidgetItem(str(i+1))) # 类别 class_id = int(box.cls[0]) class_name = self.model.names[class_id] self.result_table.setItem(i, 1, QTableWidgetItem(class_name)) # 置信度 confidence = float(box.conf[0]) self.result_table.setItem(i, 2, QTableWidgetItem(f"{confidence:.3f}")) # 位置 bbox = box.xyxy[0].tolist() position = f"({bbox[0]:.1f}, {bbox[1]:.1f})" self.result_table.setItem(i, 3, QTableWidgetItem(position)) # 尺寸 width = bbox[2] - bbox[0] height = bbox[3] - bbox[1] size = f"{width:.1f}x{height:.1f}" self.result_table.setItem(i, 4, QTableWidgetItem(size)) def set_detection_state(self, detecting): """设置检测状态""" self.image_btn.setEnabled(not detecting) self.video_btn.setEnabled(not detecting) self.camera_btn.setEnabled(not detecting) self.stop_btn.setEnabled(detecting) def on_detection_error(self, error_msg): """检测错误处理""" QMessageBox.critical(self, "错误", f"检测失败: {error_msg}") self.set_detection_state(False) def detect_video(self): """视频检测""" # 实现视频检测逻辑 pass def detect_camera(self): """摄像头检测""" # 实现摄像头检测逻辑 pass def stop_detection(self): """停止检测""" if hasattr(self, 'detection_thread') and self.detection_thread.isRunning(): self.detection_thread.terminate() self.detection_thread.wait() self.set_detection_state(False) def save_result(self): """保存结果""" if self.current_image is not None: file_path, _ = QFileDialog.getSaveFileName( self, "保存
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/13 4:05:35

血清微量元素建模实战:为何对数转换与LMM诊断比p值更重要

1. 项目概述:一头奶牛、一管血清与一场现实世界的建模突围战你手头有一批来自奶牛的血清检测数据——锌(Zn.ppm)和铜(Cu.ppm)浓度,时间跨度覆盖多个采样日,实验设计是典型的随机区组&#xff08…

作者头像 李华
网站建设 2026/7/13 4:04:32

国内模型供应商缓存与可用性实测:谁更适合跑 Agent?

台风天午休时间,从 octafuse-gateway 上拉了一批真实请求统计,做了一次国内模型供应商的缓存与可用性观察,在这里分享给大家。 这次重点不是单纯比较模型能力,而是看一个更影响 Agent 真实使用体验的问题:谁的缓存更稳…

作者头像 李华
网站建设 2026/7/13 4:04:03

数字标题的科学原理与实战优化指南

1. 项目概述:一个被反复验证却总被忽视的标题底层逻辑“带数字的标题是不是更容易火?”——这个问题在内容行业里,几乎和“封面图要不要加文字”一样古老,也一样常被当作玄学处理。我从2012年开始做新媒体运营,带过7个…

作者头像 李华
网站建设 2026/7/13 4:03:10

Python底层原理:对象模型、内存管理与字节码深度解析

1. 这不是又一本Python语法书——为什么“Understanding Python: Part 3”值得你花两小时精读如果你点开过几十个叫“Python入门”“Python速成”“30天学会Python”的教程,最后却卡在为什么字典不能用列表当键、为什么修改函数里传进来的列表会影响外面的原列表、为…

作者头像 李华
网站建设 2026/7/13 4:02:29

Python列表底层原理:从内存布局到性能陷阱全解析

1. 项目概述:为什么“理解List[]”不是一句空话,而是Python入门真正的分水岭刚接触Python的人,十有八九会把list当成一个“能装东西的筐”——往里append、pop、index查位置,再加个for循环遍历,好像就掌握了。我带过几…

作者头像 李华
网站建设 2026/7/13 3:59:57

Silk v3音频转换终极指南:5分钟解决微信QQ语音播放难题

Silk v3音频转换终极指南:5分钟解决微信QQ语音播放难题 【免费下载链接】silk-v3-decoder [Skype Silk Codec SDK]Decode silk v3 audio files (like wechat amr, aud files, qq slk files) and convert to other format (like mp3). Batch conversion support. 项…

作者头像 李华