#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
PyCV基础类 - 图像处理功能的基础类
"""

import re
import cv2
import numpy as np
from typing import Dict, Any, Optional
import time

import sys
import os
from pathlib import Path
from datetime import datetime
import shutil
import threading
import queue
import torch
import torchvision.transforms as transforms
from PIL import Image

import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
import torch
import time

try:
    from ultralytics import YOLO
    ULTRALYTICS_AVAILABLE = True
except ImportError:
    ULTRALYTICS_AVAILABLE = False


# deleted_list元素信息
# {
#     "类型编号": 0,                    # int - 类别ID (0, 1, 2, 3, 4, 5)
#     "类型字符": "damianNG",           # str - 类别名称 (damianNG, quekou1, posun, quekou3, sekuai, yaying)
#     "AOI位置": {                     # dict - 边界框位置信息
#         "x1": 100,                   # int - 左上角X坐标
#         "y1": 200,                   # int - 左上角Y坐标  
#         "x2": 300,                   # int - 右下角X坐标
#         "y2": 400,                   # int - 右下角Y坐标
#         "center_x": 200,             # int - 中心点X坐标
#         "center_y": 300,             # int - 中心点Y坐标
#         "width": 200,                # int - 框宽度 (x2 - x1)
#         "height": 200,               # int - 框高度 (y2 - y1)
#     },
#     "检测置信度": 0.8542,            # float - 原始检测置信度 (保留4位小数)
#     "分类置信度": 0.8542,            # float - 分类置信度 (通常与检测置信度相同)
#     "综合置信度": 0.8542,            # float - 综合置信度分数 (保留4位小数)
#     "面积": 40000,                   # int - 检测框面积 (width * height)
#     "索引": 0                        # int - 检测结果在批次中的索引 (从0开始)
# }


class yolo_model_loder:
    """YOLO模型加载器类，使用纯ONNX Runtime进行推理"""
    
    def __init__(self, device: str = None, verbose: bool = True):
        """
        初始化模型加载器
        Args:
            device: 设备选择，可选 'cpu', 'cuda', 'auto'。默认为自动选择
            verbose: 是否显示详细日志信息，默认为True
        """
        self.session = None  # ONNX推理会话
        self.model = None    # PyTorch模型对象
        self.model_path = None
        self.device = self._select_device(device)
        self.is_model_loaded = False
        self.model_load_error = None
        self.model_type = 'detect'  # 固定为检测类型
        self.model_format = 'onnx'  # 固定为ONNX格式
        self.input_size = 640       # 默认输入尺寸
        self.class_names = {0: "Class0"}  # 默认类别名
        self.verbose = False
        self.usr_class_name = None
        
        # ONNX会话相关属性
        self.input_name = None      # 输入节点名称
        self.output_names = []      # 输出节点名称列表
        self.input_shape = None     # 输入形状
        self.is_warmed_up = False   # 是否已预热
        
        # 分类独立置信度相关属性
        self.use_independent_conf = False  # 是否启用分类独立置信度
        self.global_conf = 0.25           # 全局置信度阈值
        self.class_conf_thresholds = {}   # 各分类的独立置信度阈值 {class_id: conf_value}

    def _log(self, message: str):
        """
        内部日志输出方法
        Args:
            message: 日志消息
        """
        if self.verbose:
            print(message)
        
    def _select_device(self, device: str = None) -> str:
        """
        自动选择设备
        Args:
            device: 指定设备，可选 'cpu', 'cuda', 'auto' 或具体的GPU编号如 '0', '1'
        Returns:
            选择的设备字符串
        """
        if device == 'cpu':
            return 'cpu'
        elif device and device.startswith('cuda'):
            return device
        elif device and device.isdigit():
            return f'cuda:{device}'
        else:
            # 自动选择设备
            if torch.cuda.is_available():
                return 'cuda:0'
            else:
                return 'cpu'
    
    def _validate_model_path(self, model_path: str) -> tuple[bool, str]:
        """
        验证模型路径和格式
        Args:
            model_path: 模型文件路径
        Returns:
            (是否有效, 错误信息)
        """
        if not model_path:
            return False, "模型路径不能为空"
            
        model_path = Path(model_path)
        if not model_path.exists():
            return False, f"模型文件不存在: {model_path}"
            
        # 检查支持的格式
        supported_formats = {'.pt', '.onnx', '.engine', '.torchscript'}
        if model_path.suffix.lower() not in supported_formats:
            return False, f"不支持的模型格式: {model_path.suffix}，支持的格式: {supported_formats}"
            
        return True, ""
    
    def load_from_path(self, model_path: str, **kwargs) -> bool:
        """
        从路径加载YOLO模型，根据文件后缀选择加载方式
        - .onnx 文件使用 onnxruntime 直接加载
        - .pt 文件使用 ultralytics YOLO 加载
        Args:
            model_path: 模型文件路径
            **kwargs: 其他参数，如 imgsz, conf, iou 等
        Returns:
            是否加载成功
        """
        try:
            # 验证模型路径
            is_valid, error_msg = self._validate_model_path(model_path)
            if not is_valid:
                self.model_load_error = error_msg
                return False
            
            # 检查是否已经加载了相同路径的模型
            if (self.is_model_loaded and 
                self.model_path == str(model_path)):
                self._log(f"模型已加载，路径相同: {model_path}")
                return True
            
            # 卸载已有模型
            self.unload_model()
            
            file_ext = Path(model_path).suffix.lower()
            self._log(f"正在加载模型: {model_path}")
            self._log(f"检测到文件格式: {file_ext}")
            
            # 根据文件后缀选择加载方式
            if file_ext == '.onnx':
                return self._load_onnx_model(model_path, **kwargs)
            elif file_ext == '.pt':
                return self._load_pytorch_model(model_path, **kwargs)
            else:
                self.model_load_error = f"不支持的模型格式: {file_ext}，仅支持 .onnx 和 .pt"
                self._log(self.model_load_error)
                return False
            
        except Exception as e:
            self.model_load_error = f"模型加载失败: {str(e)}"
            self._log(self.model_load_error)
            self.session = None
            self.is_model_loaded = False
            return False

    def _load_onnx_model(self, model_path: str, **kwargs) -> bool:
        """使用ONNX Runtime直接加载ONNX模型"""
        try:
            import onnxruntime as ort
            
            self._log("🔄 ONNX模型：使用ONNX Runtime直接加载...")
            
            # 配置执行提供者
            providers = self._get_onnx_providers(self.device)
            
            # 创建推理会话
            start_time = time.time()
            self.session = ort.InferenceSession(model_path, providers=providers)
            load_time = time.time() - start_time
            
            # 获取模型信息
            self._extract_onnx_model_info()
            
            # 设置模型路径和格式
            self.model_path = str(model_path)
            self.model_format = 'onnx'
            self.is_model_loaded = True
            self.model_load_error = None
            
            self._log(f"✅ ONNX模型加载完成，耗时: {load_time:.2f}秒")
            self._log(f"  输入名称: {self.input_name}")
            self._log(f"  输入形状: {self.input_shape}")
            self._log(f"  输出数量: {len(self.output_names)}")
            
            # 预热模型
            self._warmup_onnx_model()
            
            return True
            
        except Exception as e:
            self.model_load_error = f"ONNX模型加载失败: {str(e)}"
            self._log(self.model_load_error)
            # 确保失败时清理状态
            self.session = None
            self.is_model_loaded = False
            self.model_path = None
            return False
    
    def _load_pytorch_model(self, model_path: str, **kwargs) -> bool:
        """使用ultralytics YOLO加载PyTorch模型"""
        try:
            # 检查ultralytics是否可用
            if not ULTRALYTICS_AVAILABLE:
                self.model_load_error = "ultralytics库未安装，请安装: pip install ultralytics"
                return False
            
            self._log("🔄 PyTorch模型：使用ultralytics YOLO加载...")
            
            # 加载模型
            from ultralytics import YOLO
            start_time = time.time()
            self.model = YOLO(model_path, task='detect')
            load_time = time.time() - start_time
            
            # 对于PyTorch模型进行设备操作
            if hasattr(self.model, 'to'):
                self.model.to(self.device)
            
            # 设置模型信息
            self.model_path = str(model_path)
            self.model_format = 'pt'
            
            # 提取模型信息
            try:
                self._extract_pytorch_model_info()
            except Exception as e:
                self._log(f"⚠️ 提取PyTorch模型信息失败，使用默认值: {e}")
                self.model_type = 'detect'
                self.input_size = 640
                self.class_names = {0: "Class0"}
            
            self.is_model_loaded = True
            self.model_load_error = None
            
            self._log(f"✅ PyTorch模型加载完成，耗时: {load_time:.2f}秒")
            self._log(f"  类型: {self.model_type}")
            self._log(f"  输入尺寸: {self.input_size}")
            self._log(f"  类别数: {len(self.class_names) if self.class_names else 'Unknown'}")
            
            return True
            
        except Exception as e:
            self.model_load_error = f"PyTorch模型加载失败: {str(e)}"
            self._log(self.model_load_error)
            return False
    
    def _get_onnx_providers(self, target_device: str) -> list:
        """获取ONNX Runtime执行提供者配置"""
        try:
            if target_device == 'cpu':
                providers = [('CPUExecutionProvider', {
                    'intra_op_num_threads': 4,
                    'inter_op_num_threads': 2,
                })]
                self._log("🔧 配置ONNX CPU执行提供者")
                
            else:
                # GPU配置
                if not torch.cuda.is_available():
                    self._log("⚠️ CUDA不可用，回退到CPU")
                    return self._get_onnx_providers('cpu')
                
                # 解析GPU ID
                gpu_id = 0
                if ':' in target_device:
                    try:
                        gpu_id = int(target_device.split(':')[1])
                    except:
                        gpu_id = 0
                
                # 检查GPU是否存在
                if gpu_id >= torch.cuda.device_count():
                    self._log(f"⚠️ GPU {gpu_id} 不存在，使用GPU 0")
                    gpu_id = 0
                
                # CUDA提供者配置
                cuda_provider_options = {
                    'device_id': gpu_id,
                    'arena_extend_strategy': 'kSameAsRequested',  # 保守配置
                    'cudnn_conv_algo_search': 'HEURISTIC',       # 保守配置
                    'do_copy_in_default_stream': True,
                }
                
                providers = [
                    ('CUDAExecutionProvider', cuda_provider_options),
                    'CPUExecutionProvider'
                ]
                
                self._log(f"🔧 配置ONNX CUDA执行提供者: GPU={gpu_id}")
                
                # 显示GPU信息
                if torch.cuda.is_available():
                    gpu_name = torch.cuda.get_device_name(gpu_id)
                    gpu_memory = torch.cuda.get_device_properties(gpu_id).total_memory / (1024**3)
                    self._log(f"   GPU设备: {gpu_name} ({gpu_memory:.1f}GB)")
            
            return providers
            
        except Exception as e:
            self._log(f"⚠️ 配置ONNX提供者失败，使用CPU: {e}")
            return [('CPUExecutionProvider', {})]
    
    def _extract_onnx_model_info(self):
        """提取ONNX模型信息"""
        if not self.session:
            return
        
        # 获取输入信息
        input_info = self.session.get_inputs()[0]
        self.input_name = input_info.name
        self.input_shape = input_info.shape
        
        # 推测输入尺寸
        if self.input_shape and len(self.input_shape) >= 4:
            self.input_size = self.input_shape[-1]  # 通常是 [batch, channels, height, width]
        
        # 获取输出信息
        self.output_names = [output.name for output in self.session.get_outputs()]
        
        # 设置默认类别名（ONNX模型通常不包含类别名信息）
        self.class_names = {0: "Class0"}
        self.model_type = 'detect'
    
    def _extract_pytorch_model_info(self):
        """提取PyTorch模型信息"""
        if not self.model:
            return
        
        # 获取模型信息
        if hasattr(self.model, 'model'):
            if hasattr(self.model.model, 'args'):
                self.input_size = getattr(self.model.model.args, 'imgsz', 640)
            elif hasattr(self.model.model, 'yaml'):
                yaml_dict = self.model.model.yaml
                if isinstance(yaml_dict, dict) and 'imgsz' in yaml_dict:
                    self.input_size = yaml_dict['imgsz']
                else:
                    self.input_size = 640
        
        # 获取类别名
        if hasattr(self.model, 'names'):
            self.class_names = self.model.names
        elif hasattr(self.model, 'model') and hasattr(self.model.model, 'names'):
            self.class_names = self.model.model.names
        
        self.model_type = 'detect'
    
    def _warmup_onnx_model(self):
        """预热ONNX模型"""
        if not self.session or self.is_warmed_up:
            return
        
        try:
            # 创建虚拟输入
            if self.input_shape:
                # 处理动态形状
                shape = []
                for dim in self.input_shape:
                    if isinstance(dim, str) or dim == -1:
                        shape.append(1)  # 对于动态维度使用1
                    else:
                        shape.append(dim)
                
                dummy_input = np.random.rand(*shape).astype(np.float32)
                
                # 执行一次推理进行预热
                self.session.run(None, {self.input_name: dummy_input})
                self.is_warmed_up = True
                self._log("🔥 ONNX模型预热完成")
        except Exception as e:
            self._log(f"⚠️ ONNX模型预热失败: {e}")


    def _optimize_onnx_providers_for_device(self, target_device: str) -> list:
        """为指定设备优化ONNX Runtime执行提供者配置 - 按测试程序的可靠模式"""
        try:
            import onnxruntime as ort
            
            if target_device == 'cpu':
                # CPU配置
                cpu_provider_options = {
                    'intra_op_num_threads': 4,
                    'inter_op_num_threads': 2,
                }
                
                providers = [('CPUExecutionProvider', cpu_provider_options)]
                
                if self.verbose:
                    print(f"🚀 配置ONNX CPU执行提供者")
                
            else:
                # GPU配置 - 按照测试程序的成功模式
                if not torch.cuda.is_available():
                    if self.verbose:
                        print("⚠️ CUDA不可用，回退到CPU")
                    return self._optimize_onnx_providers_for_device('cpu')
                
                # 解析GPU ID
                gpu_id = 0
                if ':' in target_device:
                    try:
                        gpu_id = int(target_device.split(':')[1])
                    except:
                        gpu_id = 0
                
                # 检查GPU是否存在
                if gpu_id >= torch.cuda.device_count():
                    if self.verbose:
                        print(f"⚠️ GPU {gpu_id} 不存在，使用GPU 0")
                    gpu_id = 0
                
                # CUDA提供者配置 - 关键：device_id设置为实际GPU编号
                cuda_provider_options = {
                    'device_id': gpu_id,  # 直接使用实际GPU ID
                    'arena_extend_strategy': 'kNextPowerOfTwo',
                    'cudnn_conv_algo_search': 'EXHAUSTIVE',
                    'do_copy_in_default_stream': True,
                }
                
                providers = [
                    ('CUDAExecutionProvider', cuda_provider_options),
                    'CPUExecutionProvider'  # 备选
                ]
                
                if self.verbose:
                    print(f"🚀 配置ONNX CUDA执行提供者: GPU={gpu_id}")
                    print(f"   CUDA提供者device_id: {gpu_id}")
                    
                    # 显示GPU信息
                    if torch.cuda.is_available():
                        gpu_name = torch.cuda.get_device_name(gpu_id)
                        gpu_memory = torch.cuda.get_device_properties(gpu_id).total_memory / (1024**3)
                        print(f"   GPU信息: {gpu_name} ({gpu_memory:.1f}GB)")
            
            return providers
            
        except ImportError:
            if self.verbose:
                print("⚠️ 无法导入onnxruntime，使用默认配置")
            return None
        except Exception as e:
            if self.verbose:
                print(f"⚠️ 配置ONNX执行提供者失败: {str(e)}")
            return None

    def _warmup_onnx_model_on_device(self, target_device: str, warmup_runs: int = 2):
        """在指定设备上预热ONNX模型"""
        try:
            if self.model and hasattr(self.model, 'predict'):
                if self.verbose:
                    print(f"🔥 在设备 {target_device} 上预热ONNX模型...")
                
                # 创建假输入数据进行预热
                dummy_input = np.random.randint(0, 255, (512, 512, 3), dtype=np.uint8)
                
                warmup_times = []
                for i in range(warmup_runs):
                    start_time = time.perf_counter()
                    
                    # 执行预热推理 - 关键：指定device参数
                    _ = self.model.predict(
                        dummy_input,
                        verbose=False,
                        save=False,
                        conf=0.5,
                        device=target_device  # 重要：明确指定设备
                    )
                    
                    warmup_time = time.perf_counter() - start_time
                    warmup_times.append(warmup_time)
                    
                    if i == 0:  # 第一次通常最慢
                        if self.verbose:
                            print(f"   首次预热: {warmup_time:.3f}秒")
                
                if len(warmup_times) > 1:
                    avg_warmup_time = sum(warmup_times[1:]) / len(warmup_times[1:])
                    if self.verbose:
                        print(f"✅ ONNX预热完成，平均时间: {avg_warmup_time:.3f}秒")
                
                self.is_warmed_up = True
                
        except Exception as e:
            if self.verbose:
                print(f"⚠️ ONNX模型预热失败: {e}")

    def _verify_onnx_device_usage(self, expected_device: str) -> bool:
        """验证ONNX模型是否在期望设备上运行"""
        try:
            # 对于ONNX模型，检查session而不是model
            if self.model_format == 'onnx' and not self.session:
                return False
            elif self.model_format != 'onnx' and not hasattr(self, 'model'):
                return False
            
            # 创建测试输入
            test_input = np.random.randint(0, 255, (512, 512, 3), dtype=np.uint8)
            
            start_time = time.perf_counter()
            
            # 根据模型类型执行推理
            if self.model_format == 'onnx':
                # ONNX模型使用predictEx
                result = self.predictEx(test_input, device=expected_device, verbose=False)
            else:
                # PyTorch模型使用predict
                result = self.model.predict(
                    test_input,
                    device=expected_device,
                    verbose=False,
                    save=False
                )
            
            inference_time = time.perf_counter() - start_time
            
            # 验证是否有结果
            success = result is not None
            
            if self.verbose:
                device_type = "CPU" if expected_device == 'cpu' else f"GPU ({expected_device})"
                status = "✅ 运行正常" if success else "❌ 运行异常"
                print(f"🔍 ONNX设备验证: {device_type} - {status}, 耗时: {inference_time:.3f}秒")
            
            return success
            
        except Exception as e:
            if self.verbose:
                print(f"❌ ONNX设备验证失败: {str(e)}")
            return False

    def load_from_path_with_device(self, model_path: str, force_device: str = None, **kwargs) -> bool:
        """
        从路径加载YOLO模型，支持强制指定设备
        Args:
            model_path: 模型文件路径
            force_device: 强制指定设备，可选值：
                - 'cpu': 强制使用CPU
                - 'cuda': 使用第一个GPU
                - 'cuda:0', 'cuda:1': 指定具体GPU
                - 'auto': 自动选择（默认行为）
                - None: 使用初始化时的设备设置
            **kwargs: 其他参数，如 imgsz, conf, iou 等
        Returns:
            是否加载成功
        """
        try:
            # 检查ultralytics是否可用
            if not ULTRALYTICS_AVAILABLE:
                self.model_load_error = "ultralytics库未安装，请安装: pip install ultralytics"
                return False
            
            # 验证模型路径
            is_valid, error_msg = self._validate_model_path(model_path)
            if not is_valid:
                self.model_load_error = error_msg
                return False
            
            # 处理设备参数
            original_device = self.device  # 保存原设备设置
            target_device = self.device    # 默认使用原设备
            
            if force_device is not None:
                if force_device.lower() == 'cpu':
                    target_device = 'cpu'
                    self._log(f"🔧 强制使用CPU设备")
                elif force_device.lower() == 'cuda':
                    if torch.cuda.is_available():
                        target_device = 'cuda:0'
                        self._log(f"🔧 强制使用GPU: cuda:0")
                    else:
                        self._log(f"⚠️ CUDA不可用，回退到CPU")
                        target_device = 'cpu'
                elif force_device.lower().startswith('cuda:'):
                    gpu_id = force_device.split(':')[1]
                    if torch.cuda.is_available() and int(gpu_id) < torch.cuda.device_count():
                        target_device = force_device.lower()
                        self._log(f"🔧 强制使用GPU: {target_device}")
                        print(f"🔧 强制使用GPU: {target_device}")
                    else:
                        self._log(f"⚠️ GPU {force_device} 不可用，回退到CPU")
                        target_device = 'cpu'
                elif force_device.lower() == 'auto':
                    target_device = self._select_device('auto')
                    self._log(f"🔧 自动选择设备: {target_device}")
                else:
                    self._log(f"⚠️ 无效的设备参数 '{force_device}'，使用默认设备")
            
            # 更新设备设置
            self.device = target_device
            
            # 检查是否已经加载了相同路径和设备的模型
            if (self.is_model_loaded and 
                self.model_path == str(model_path) and 
                self.device == target_device):
                # 检查模型对象是否存在（PyTorch用model，ONNX用session）
                model_exists = (hasattr(self, 'model') and self.model is not None) or \
                              (hasattr(self, 'session') and self.session is not None)
                if model_exists:
                    self._log(f"模型已加载，路径和设备相同: {model_path} -> {target_device}")
                    return True
            
            # 卸载已有模型
            self.unload_model()
            
            # 加载模型
            self._log(f"正在加载模型: {model_path}")
            self._log(f"目标设备: {target_device}")
            
            # 根据文件后缀选择加载方式
            file_ext = Path(model_path).suffix.lower()
            self._log(f"检测到文件格式: {file_ext}")
            
            # 根据文件后缀选择加载方式
            if file_ext == '.onnx':
                success = self._load_onnx_model(model_path, **kwargs)
            elif file_ext == '.pt':
                success = self._load_pytorch_model(model_path, **kwargs)
            else:
                self.model_load_error = f"不支持的模型格式: {file_ext}，仅支持 .onnx 和 .pt"
                self._log(self.model_load_error)
                return False
            
            if not success:
                # 恢复原设备设置
                self.device = original_device
                return False
            
            self._log(f"✅ 模型加载成功:")
            self._log(f"   路径: {self.model_path}")
            self._log(f"   设备: {self.device}")
            self._log(f"   类型: {self.model_type}")
            self._log(f"   格式: {self.model_format}")
            self._log(f"   输入尺寸: {self.input_size}")
            self._log(f"   类别数: {len(self.class_names) if self.class_names else 'Unknown'}")
            
            return True
            
        except Exception as e:
            # 恢复原设备设置
            self.device = original_device
            self.model_load_error = f"模型加载失败: {str(e)}"
            self._log(f"❌ {self.model_load_error}")
            return False

    def _verify_device_placement(self, expected_device: str):
        """
        验证模型是否正确放置在指定设备上
        Args:
            expected_device: 期望的设备
        """
        try:
            if self.model and hasattr(self.model, 'model'):
                # 检查模型参数的设备
                model_device = next(self.model.model.parameters()).device
                
                if expected_device == 'cpu':
                    if model_device.type == 'cpu':
                        self._log(f"✅ 模型已正确放置在CPU")
                    else:
                        self._log(f"⚠️ 预期CPU但模型在 {model_device}")
                else:
                    expected_cuda_id = int(expected_device.split(':')[1]) if ':' in expected_device else 0
                    if model_device.type == 'cuda' and model_device.index == expected_cuda_id:
                        self._log(f"✅ 模型已正确放置在 {expected_device}")
                    else:
                        self._log(f"⚠️ 预期 {expected_device} 但模型在 {model_device}")
                        
        except Exception as e:
            self._log(f"⚠️ 无法验证设备放置: {e}")
    
    def _set_onnx_optimization_options(self):
        """设置ONNX Runtime优化选项"""
        try:
            import os
            
            # 设置ONNX Runtime优化环境变量
            os.environ['ORT_DISABLE_ALL_OPTIMIZATION'] = '0'
            os.environ['ORT_ENABLE_BASIC_OPTIMIZATION'] = '1'
            os.environ['ORT_ENABLE_EXTENDED_OPTIMIZATION'] = '1'
            
            # 减少内存拷贝
            os.environ['CUDA_LAUNCH_BLOCKING'] = '0'
            os.environ['CUDA_CACHE_DISABLE'] = '0'
            
            # 优化CUDA图执行
            os.environ['ORT_CUDA_GRAPH_CAPTURE'] = '1'
            
            # 设置日志级别，减少Memcpy警告
            os.environ['ORT_LOG_SEVERITY_LEVEL'] = '3'  # 只显示ERROR级别日志
            
            self._log("✅ ONNX Runtime优化选项已设置")
            self.onnx_optimized = True
            
        except Exception as e:
            self._log(f"⚠️ 设置ONNX优化选项失败: {e}")
    
    def _optimize_onnx_providers(self, target_device: str) -> list:
        """优化ONNX Runtime执行提供者配置"""
        try:
            import onnxruntime as ort
            
            if target_device != 'cpu' and torch.cuda.is_available():
                # GPU优化配置
                cuda_provider_options = {
                    'device_id': int(target_device.split(':')[1]) if ':' in target_device else 0,
                    'arena_extend_strategy': 'kNextPowerOfTwo',
                    'gpu_mem_limit': 2 * 1024 * 1024 * 1024,  # 2GB限制
                    'cudnn_conv_algo_search': 'EXHAUSTIVE',
                    'do_copy_in_default_stream': True,
                }
                
                providers = [
                    ('CUDAExecutionProvider', cuda_provider_options),
                    'CPUExecutionProvider'
                ]
                
                self._log(f"🚀 配置CUDA执行提供者: GPU={cuda_provider_options['device_id']}")
            else:
                # CPU优化配置
                cpu_provider_options = {
                    'intra_op_num_threads': 4,
                    'inter_op_num_threads': 2,
                }
                
                providers = [
                    ('CPUExecutionProvider', cpu_provider_options)
                ]
                
                self._log(f"🚀 配置CPU执行提供者")
            
            return providers
            
        except ImportError:
            self._log("⚠️ 无法导入onnxruntime，使用默认配置")
            return None
    
    def _warmup_onnx_model_legacy(self, warmup_runs: int = 3):
        """预热ONNX模型以提高后续推理速度 - 保留用于PyTorch模型的预热"""
        try:
            if hasattr(self, 'model') and self.model and hasattr(self.model, 'predict'):
                self._log(f"🔥 开始预热PyTorch模型 ({warmup_runs}次)")
                
                # 创建假输入数据进行预热 - 使用测试程序的512尺寸
                dummy_input = np.random.randint(0, 255, (512, 512, 3), dtype=np.uint8)
                
                warmup_times = []
                for i in range(warmup_runs):
                    start_time = time.perf_counter()
                    
                    # 执行预热推理
                    _ = self.model.predict(
                        dummy_input,
                        verbose=False,
                        save=False,
                        conf=0.5,
                        device=self.device
                    )
                    
                    warmup_time = time.perf_counter() - start_time
                    warmup_times.append(warmup_time)
                    
                    if i == 0:  # 第一次通常最慢
                        self._log(f"   首次预热: {warmup_time:.3f}秒")
                
                avg_warmup_time = sum(warmup_times[1:]) / max(1, len(warmup_times) - 1)  # 排除第一次
                self._log(f"✅ PyTorch预热完成，优化后平均时间: {avg_warmup_time:.3f}秒")
                self.is_warmed_up = True
            elif self.session:
                # 对于ONNX模型，调用专用的预热方法
                self._warmup_onnx_session()
                
        except Exception as e:
            self._log(f"⚠️ 模型预热失败: {e}")
    
    def _warmup_onnx_session(self):
        """专门为ONNX会话进行预热"""
        if not self.session or self.is_warmed_up:
            return
        
        try:
            # 创建虚拟输入
            if self.input_shape:
                # 处理动态形状
                shape = []
                for dim in self.input_shape:
                    if isinstance(dim, str) or dim == -1:
                        shape.append(1)  # 对于动态维度使用1
                    else:
                        shape.append(dim)
                
                dummy_input = np.random.rand(*shape).astype(np.float32)
                
                # 执行一次推理进行预热
                self.session.run(None, {self.input_name: dummy_input})
                self.is_warmed_up = True
                self._log("🔥 ONNX会话预热完成")
        except Exception as e:
            self._log(f"⚠️ ONNX会话预热失败: {e}")

    def load_cpu(self, model_path: str, **kwargs) -> bool:
        """
        强制加载模型到CPU的便捷方法
        Args:
            model_path: 模型文件路径
            **kwargs: 其他参数
        Returns:
            是否加载成功
        """
        return self.load_from_path_with_device(model_path, force_device='cpu', **kwargs)

    def load_gpu(self, model_path: str, gpu_id: int = 0, **kwargs) -> bool:
        """
        强制加载模型到指定GPU的便捷方法
        Args:
            model_path: 模型文件路径
            gpu_id: GPU编号，默认为0
            **kwargs: 其他参数
        Returns:
            是否加载成功
        """
        return self.load_from_path_with_device(model_path, force_device=f'cuda:{gpu_id}', **kwargs)

    def reload_to_device(self, target_device: str) -> bool:
        """
        将已加载的模型重新加载到指定设备
        Args:
            target_device: 目标设备 ('cpu', 'cuda:0', 'cuda:1', 等)
        Returns:
            是否重新加载成功
        """
        if not self.is_model_loaded or not self.model_path:
            self.model_load_error = "没有已加载的模型可以重新加载"
            return False
        
        self._log(f"🔄 重新加载模型到设备: {target_device}")
        return self.load_from_path_with_device(self.model_path, force_device=target_device)

    def _extract_model_info(self):
        """提取模型信息，安全版本，兼容ONNX和PyTorch模型"""
        try:
            if self.model_format == 'onnx':
                # ONNX模型的信息已在_extract_onnx_model_info中设置
                # 这里只需要确认信息完整性
                if not hasattr(self, 'model_type') or not self.model_type:
                    self.model_type = 'detect'
                if not hasattr(self, 'input_size') or not self.input_size:
                    self.input_size = 640
                if not hasattr(self, 'class_names') or not self.class_names:
                    self.class_names = {0: "Class0"}
                    
                self._log(f"ONNX模型信息确认: 类型={self.model_type}, 尺寸={self.input_size}")
                
            elif hasattr(self, 'model') and self.model:
                # PyTorch模型信息提取
                # 获取模型类型（安全方式）
                if hasattr(self.model, 'task'):
                    self.model_type = self.model.task
                else:
                    self.model_type = 'detect'  # 默认为检测
                
                # 获取输入尺寸（安全方式）
                if hasattr(self.model, 'imgsz'):
                    self.input_size = self.model.imgsz
                else:
                    self.input_size = 640  # 默认值
                
                # 获取类别名称（安全方式）
                if hasattr(self.model, 'names'):
                    self.class_names = self.model.names
                else:
                    self.class_names = None
                    
                self._log(f"PyTorch模型信息提取成功: 类型={self.model_type}, 尺寸={self.input_size}")
            else:
                # 默认信息
                self.model_type = 'detect'
                self.input_size = 640
                self.class_names = {0: "Class0"}
                self._log(f"使用默认模型信息: 类型={self.model_type}, 尺寸={self.input_size}")
                    
        except Exception as e:
            self._log(f"提取模型信息时出错: {e}")
            # 设置默认值
            self.model_type = 'detect'
            self.input_size = 640
            self.class_names = {0: "Class0"}
    
    def unload_model(self):
        """卸载当前模型"""
        try:
            # 卸载ONNX模型
            if hasattr(self, 'session') and self.session is not None:
                del self.session
                self.session = None
                self._log("ONNX会话已卸载")
            
            # 卸载PyTorch模型
            if hasattr(self, 'model') and self.model is not None:
                # 清理GPU内存
                if hasattr(self.model, 'cpu'):
                    self.model.cpu()
                del self.model
                self.model = None
                self._log("PyTorch模型已卸载")
            elif not hasattr(self, 'model'):
                # 确保model属性存在
                self.model = None
            
            # 清理GPU缓存
            if torch.cuda.is_available():
                torch.cuda.empty_cache()
            
            # 重置状态
            self.is_model_loaded = False
            self.model_path = None
            self.is_warmed_up = False
            
            self._log("模型卸载完成")
            
        except Exception as e:
            self._log(f"卸载模型时出错: {e}")
        finally:
            self.session = None
            if hasattr(self, 'model'):
                self.model = None
            self.is_model_loaded = False
            self.model_path = None
            self.model_type = None
            self.model_format = None
            self.input_size = None
            self.class_names = None
    
    def get_model_info(self) -> Dict[str, Any]:
        """
        获取模型信息
        Returns:
            包含模型信息的字典
        """
        return {
            'is_loaded': self.is_model_loaded,
            'model_path': self.model_path,
            'model_type': self.model_type,
            'model_format': self.model_format,
            'input_size': self.input_size,
            'class_names': self.class_names,
            'device': self.device,
            'error': self.model_load_error
        }
    
    def preprocess_image(self, image: np.ndarray, target_size: int = None) -> np.ndarray:
        """
        图像预处理
        Args:
            image: 输入图像 (BGR格式)
            target_size: 目标尺寸，如果为None则使用模型默认尺寸
        Returns:
            预处理后的图像
        """
        if not self.is_model_loaded:
            raise RuntimeError("模型未加载")
        
        if target_size is None:
            target_size = self.input_size if self.input_size else 640
            
        # 转换为RGB
        if len(image.shape) == 3:
            image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        else:
            image_rgb = image
            
        # 调整尺寸保持宽高比
        h, w = image_rgb.shape[:2]
        scale = min(target_size / h, target_size / w)
        new_h, new_w = int(h * scale), int(w * scale)
        
        # 缩放图像
        resized = cv2.resize(image_rgb, (new_w, new_h))
        
        # 填充到目标尺寸
        padded = np.full((target_size, target_size, 3), 114, dtype=np.uint8)
        y_offset = (target_size - new_h) // 2
        x_offset = (target_size - new_w) // 2
        padded[y_offset:y_offset+new_h, x_offset:x_offset+new_w] = resized
        
        return padded
    
    def predict(self, image: np.ndarray, **kwargs) -> Any:
        """
        优化的预测方法，特别针对ONNX模型，每次推理都显示模型信息
        Args:
            image: 输入图像 (BGR格式)
            **kwargs: 预测参数，如 conf, iou, save 等
        Returns:
            预测结果
        """
        if not self.is_model_loaded:
            raise RuntimeError("模型未加载")
        
        # 获取模型实际运行的设备
        actual_device = self._get_actual_model_device()
        device_display = actual_device.upper() if actual_device != 'cpu' else 'CPU'
        model_type_display = self.model_format.upper() if self.model_format else 'UNKNOWN'
        print(f"🚀 推理信息: {model_type_display}模型 | 设备: {device_display}")
            
        try:
            start_time = time.perf_counter()
            
            # ONNX模型特殊优化 - 按照测试程序的可靠推理模式
            if self.model_format == 'onnx':
                # 获取实际GPU ID
                gpu_id = int(actual_device.split(':')[1]) if ':' in actual_device else 0
                
                # 优化ONNX推理参数
                onnx_kwargs = kwargs.copy()
                
                # 强制指定设备（关键：直接指定GPU ID）
                onnx_kwargs['device'] = f'cuda:{gpu_id}' if actual_device != 'cpu' else 'cpu'
                
                # 强制使用批处理大小1（ONNX通常对单张图片更优化）
                onnx_kwargs['batch'] = False
                
                # 禁用不必要的后处理
                onnx_kwargs['save'] = False
                onnx_kwargs['save_txt'] = False
                onnx_kwargs['save_crop'] = False
                
                # 设置合适的输入尺寸（避免动态尺寸变化）
                if 'imgsz' not in onnx_kwargs:
                    onnx_kwargs['imgsz'] = 512  # 使用测试程序的512尺寸
                
                # 禁用ultralytics的详细输出
                onnx_kwargs['verbose'] = False
                
                results = self.model.predict(image, **onnx_kwargs)
            else:
                # PyTorch模型使用原始参数
                if 'device' not in kwargs:
                    kwargs['device'] = actual_device
                results = self.model(image, **kwargs)
            
            inference_time = time.perf_counter() - start_time
            print(f"⚡ 推理耗时: {inference_time:.4f}秒")
            
            return results
            
        except Exception as e:
            error_msg = f"预测失败 ({model_type_display}/{device_display}): {str(e)}"
            print(f"❌ {error_msg}")
            raise RuntimeError(error_msg)
    
    def predictEx(self, image: np.ndarray, **kwargs) -> Any:
        """
        增强预测方法，自动处理独立置信度设置，移除外部conf参数依赖
        
        Args:
            image: 输入图像 (BGR格式)
            **kwargs: 预测参数，如 iou, imgsz, device, save, verbose 等
                     注意：conf参数会被自动处理，外部传入的conf参数会被忽略
            
        Returns:
            预测结果
        """
        if not self.is_model_loaded:
            raise RuntimeError("模型未加载")
        
        # 🔧 关键修改：优先使用kwargs中的device参数，而不是自动检测的设备
        target_device = kwargs.get('device', self._get_actual_model_device())
        device_display = target_device.upper() if target_device != 'cpu' else 'CPU'
        model_type_display = self.model_format.upper() if self.model_format else 'UNKNOWN'
        
        try:
            start_time = time.perf_counter()
            
            # 准备预测参数，移除外部传入的conf参数
            predict_kwargs = kwargs.copy()
            if 'conf' in predict_kwargs:
                del predict_kwargs['conf']  # 删除外部传入的conf参数
                if self.verbose:
                    self._log("已移除外部conf参数，使用内部置信度管理")
            
            # 🔧 关键：确保设备参数正确传递
            predict_kwargs['device'] = target_device
            predict_kwargs['verbose'] = False
            
            # 根据独立置信度设置决定处理方式
            if self.use_independent_conf and self.class_conf_thresholds:
                print(f"🚀 推理信息: {model_type_display}模型 | 设备: {device_display} | 独立置信度模式")
                
                if self.verbose:
                    self._log("独立置信度设置:")
                    self._log(f"  全局置信度: {self.global_conf}")
                    for class_key, conf_val in self.class_conf_thresholds.items():
                        self._log(f"  {class_key}: {conf_val}")
                
                # 使用最低置信度进行初步预测，然后过滤
                min_conf = min(self.global_conf, min(self.class_conf_thresholds.values()) if self.class_conf_thresholds else self.global_conf)
                predict_kwargs['conf'] = min_conf
                
                if self.verbose:
                    self._log(f"使用最低置信度进行预测: {min_conf}")
                
                # 执行预测
                results = self._execute_prediction(image, predict_kwargs, model_type_display)
                
                # 应用独立置信度过滤
                if results:
                    results = self._apply_independent_confidence_filtering(results)
                    
            else:
                # 标准模式：使用全局置信度
                print(f"🚀 推理信息: {model_type_display}模型 | 设备: {device_display} | 全局置信度模式")
                
                predict_kwargs['conf'] = self.global_conf
                
                if self.verbose:
                    self._log(f"使用全局置信度: {self.global_conf}")
                
                # 执行预测
                results = self._execute_prediction(image, predict_kwargs, model_type_display)
            
            inference_time = time.perf_counter() - start_time
            print(f"⚡ 推理耗时: {inference_time:.4f}秒")
            
            return results
            
        except Exception as e:
            error_msg = f"predictEx失败 ({model_type_display}/{device_display}): {str(e)}"
            print(f"❌ {error_msg}")
            raise RuntimeError(error_msg)
    
    def _execute_prediction(self, image: np.ndarray, predict_kwargs: dict, model_type_display: str) -> Any:
        """
        执行实际的预测操作，根据模型格式选择推理方式
        
        Args:
            image: 输入图像
            predict_kwargs: 预测参数字典
            model_type_display: 模型类型显示名称
            
        Returns:
            预测结果
        """
        try:
            if self.model_format == 'onnx':
                # 使用纯ONNX Runtime推理
                return self._onnx_predict(image, predict_kwargs)
            else:
                # PyTorch模型使用ultralytics推理
                return self._pytorch_predict(image, predict_kwargs)
            
        except Exception as e:
            raise RuntimeError(f"执行预测失败: {str(e)}")
    
    def _onnx_predict(self, image: np.ndarray, predict_kwargs: dict):
        """使用纯ONNX Runtime进行推理"""
        # 详细的状态检查和诊断
        if not self.session:
            error_details = []
            error_details.append(f"ONNX会话未初始化")
            error_details.append(f"模型加载状态: {self.is_model_loaded}")
            error_details.append(f"模型路径: {self.model_path}")
            error_details.append(f"模型格式: {self.model_format}")
            error_details.append(f"加载错误: {self.model_load_error}")
            
            full_error = "ONNX推理失败 - " + " | ".join(error_details)
            self._log(f"❌ {full_error}")
            raise RuntimeError(full_error)
        
        if not self.input_name:
            raise RuntimeError("ONNX输入名称未设置")
        
        # 预处理图像 - 使用模型的实际输入尺寸
        target_size = self.input_size if self.input_size else predict_kwargs.get('imgsz', 640)
        if self.verbose:
            print(f"🔧 ONNX预处理: 目标尺寸={target_size}")
        
        processed_image = self._preprocess_for_onnx(image, target_size)
        
        # 执行ONNX推理
        start_time = time.time()
        outputs = self.session.run(None, {self.input_name: processed_image})
        inference_time = time.time() - start_time
        
        # 后处理为与ultralytics兼容的格式
        results = self._postprocess_onnx_outputs(outputs, image.shape, predict_kwargs)
        
        if self.verbose:
            print(f"🔥 ONNX纯推理耗时: {inference_time:.4f}秒")
        
        return results
    
    def _pytorch_predict(self, image: np.ndarray, predict_kwargs: dict):
        """使用ultralytics进行PyTorch推理"""
        if not hasattr(self, 'model') or self.model is None:
            raise RuntimeError("PyTorch模型未加载")
        
        return self.model.predict(image, **predict_kwargs)
    
    def _preprocess_for_onnx(self, image: np.ndarray, target_size: int = 640) -> np.ndarray:
        """为ONNX推理预处理图像"""
        # 转换为RGB
        if len(image.shape) == 3 and image.shape[2] == 3:
            image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        else:
            image_rgb = image
        
        # 调整尺寸保持宽高比
        h, w = image_rgb.shape[:2]
        scale = min(target_size / h, target_size / w)
        new_h, new_w = int(h * scale), int(w * scale)
        
        # 缩放图像
        resized = cv2.resize(image_rgb, (new_w, new_h))
        
        # 填充到目标尺寸
        padded = np.full((target_size, target_size, 3), 114, dtype=np.uint8)
        y_offset = (target_size - new_h) // 2
        x_offset = (target_size - new_w) // 2
        padded[y_offset:y_offset+new_h, x_offset:x_offset+new_w] = resized
        
        # 归一化并转换为CHW格式
        normalized = padded.astype(np.float32) / 255.0
        transposed = normalized.transpose(2, 0, 1)  # HWC -> CHW
        batched = np.expand_dims(transposed, 0)  # Add batch dimension
        
        return batched
    
    def _postprocess_onnx_outputs(self, outputs, original_shape, predict_kwargs):
        """将ONNX输出后处理为与ultralytics兼容的格式"""
        try:
            # 尝试使用ultralytics的后处理
            return self._postprocess_with_ultralytics(outputs, original_shape, predict_kwargs)
        except Exception as e:
            if self.verbose:
                print(f"⚠️ ultralytics后处理失败，使用简化后处理: {e}")
            # 回退到简化后处理
            return self._postprocess_simple(outputs, original_shape, predict_kwargs)
    
    def _postprocess_with_ultralytics(self, outputs, original_shape, predict_kwargs):
        """使用ultralytics的后处理方法"""
        try:
            # 尝试不同的导入方法
            import torch
            
            # 方法1：尝试从ultralytics.utils.ops导入
            try:
                from ultralytics.utils.ops import non_max_suppression, scale_boxes
                from ultralytics.engine.results import Results
            except ImportError:
                # 方法2：尝试从ultralytics.yolo.utils导入（旧版本）
                try:
                    from ultralytics.yolo.utils import non_max_suppression, scale_coords as scale_boxes
                    from ultralytics.yolo.engine.results import Results
                except ImportError:
                    # 方法3：尝试从其他位置导入
                    from ultralytics.utils import non_max_suppression, scale_boxes
                    from ultralytics.models.yolo.detect.predict import Results
            
            # 转换输出为torch tensor
            predictions = torch.from_numpy(outputs[0])  # [batch, num_detections, 85]
            
            # 应用NMS
            conf_threshold = predict_kwargs.get('conf', 0.25)
            iou_threshold = predict_kwargs.get('iou', 0.45)
            
            detections = non_max_suppression(
                predictions, 
                conf_thres=conf_threshold,
                iou_thres=iou_threshold,
                max_det=300
            )
            
            # 创建Results对象
            results = []
            for i, det in enumerate(detections):
                if len(det):
                    # 缩放坐标到原始图像尺寸
                    target_size = predict_kwargs.get('imgsz', 640)
                    det[:, :4] = scale_boxes((target_size, target_size), det[:, :4], original_shape[:2])
                
                # 创建Results对象
                result = Results(
                    orig_img=np.zeros(original_shape, dtype=np.uint8),  # 占位符
                    path=None,
                    names=self.class_names,
                    boxes=det.cpu().numpy() if len(det) else None
                )
                results.append(result)
            
            return results
            
        except Exception as e:
            raise ImportError(f"ultralytics后处理失败: {e}")
    
    def _postprocess_simple(self, outputs, original_shape, predict_kwargs):
        """简化的后处理方法，不依赖ultralytics的具体实现"""
        try:
            import torch
            
            # 获取原始输出并检查形状
            raw_predictions = outputs[0]  # 第一个输出
            
            if self.verbose:
                print(f"🔍 ONNX输出调试:")
                print(f"   输出数量: {len(outputs)}")
                print(f"   第一个输出形状: {raw_predictions.shape}")
                print(f"   第一个输出数据类型: {raw_predictions.dtype}")
            
            # 转换为torch tensor方便处理
            predictions = torch.from_numpy(raw_predictions)
            
            # 根据实际输出形状调整处理逻辑
            if len(predictions.shape) == 2:
                # 可能是 [num_detections, 85] 格式
                predictions = predictions.unsqueeze(0)  # 添加batch维度变为 [1, num_detections, 85]
            elif len(predictions.shape) == 3:
                # 标准的 [batch, num_detections, 85] 格式
                pass
            else:
                # 其他形状，尝试reshape
                if self.verbose:
                    print(f"⚠️ 非标准输出形状: {predictions.shape}，尝试自适应处理")
                # 如果总元素数能被85整除，尝试reshape
                total_elements = predictions.numel()
                if total_elements % 85 == 0:
                    num_detections = total_elements // 85
                    predictions = predictions.reshape(1, num_detections, 85)
                else:
                    raise ValueError(f"无法处理的输出形状: {predictions.shape}")
            
            # 简单的置信度过滤
            conf_threshold = predict_kwargs.get('conf', 0.25)
            
            if self.verbose:
                print(f"   处理后形状: {predictions.shape}")
                print(f"   置信度阈值: {conf_threshold}")
            
            results = []
            for batch_idx in range(predictions.shape[0]):
                batch_pred = predictions[batch_idx]  # [num_detections, 85]
                
                # 根据实际输出形状调整处理逻辑
                if batch_pred.shape[1] == 5376:
                    # 这是一种特殊的YOLO输出格式 (1, 10, 5376)
                    # 需要reshape到标准格式
                    if self.verbose:
                        print(f"   检测到特殊YOLO输出格式: {batch_pred.shape}")
                    
                    # 尝试将 (10, 5376) reshape 到更标准的格式
                    # 可能是 (num_anchors * grid_size^2, num_classes + 5)
                    # 5376 可能等于 3个anchor * 某个grid size * grid size
                    
                    # 创建一个简单的结果，因为这种格式需要特殊的后处理
                    class SimpleResult:
                        def __init__(self, raw_data):
                            self.raw_outputs = raw_data
                            self.detections = np.array([])  # 暂时返回空检测
                            self.boxes = None
                            
                        def __str__(self):
                            return f"ONNX Raw Result: shape={self.raw_outputs.shape}"
                    
                    results.append(SimpleResult(batch_pred.numpy()))
                    continue
                
                elif batch_pred.shape[1] < 85:
                    # 如果列数不够，创建空结果
                    class SimpleResult:
                        def __init__(self):
                            self.detections = np.array([])
                            self.boxes = None
                    results.append(SimpleResult())
                    continue
                
                # 标准YOLO格式处理 [num_detections, 85]
                # 提取坐标和置信度
                boxes = batch_pred[:, :4]  # x1, y1, x2, y2
                confidences = batch_pred[:, 4]  # 目标置信度
                class_probs = batch_pred[:, 5:]  # 类别概率
                
                # 计算最终置信度和类别
                if class_probs.shape[1] > 0:
                    class_confs, class_ids = torch.max(class_probs, dim=1)
                    final_confs = confidences * class_confs
                else:
                    # 如果没有类别概率，只使用目标置信度
                    final_confs = confidences
                    class_ids = torch.zeros(confidences.shape[0], dtype=torch.long)
                
                # 置信度过滤
                keep_mask = final_confs > conf_threshold
                
                if keep_mask.sum() > 0:
                    filtered_boxes = boxes[keep_mask]
                    filtered_confs = final_confs[keep_mask]
                    filtered_classes = class_ids[keep_mask]
                    
                    # 简单的IoU过滤（可选）
                    keep_indices = self._simple_nms(filtered_boxes, filtered_confs, 
                                                  iou_threshold=predict_kwargs.get('iou', 0.45))
                    
                    if len(keep_indices) > 0:
                        final_boxes = filtered_boxes[keep_indices]
                        final_confs = filtered_confs[keep_indices]
                        final_classes = filtered_classes[keep_indices]
                        
                        # 组合结果 [x1, y1, x2, y2, conf, class]
                        detection_results = torch.cat([
                            final_boxes,
                            final_confs.unsqueeze(1),
                            final_classes.unsqueeze(1).float()
                        ], dim=1)
                        
                        # 缩放到原始图像尺寸
                        target_size = predict_kwargs.get('imgsz', 640)
                        scale_x = original_shape[1] / target_size
                        scale_y = original_shape[0] / target_size
                        
                        detection_results[:, [0, 2]] *= scale_x  # x坐标
                        detection_results[:, [1, 3]] *= scale_y  # y坐标
                        
                        # 创建简化的结果对象
                        class SimpleResult:
                            def __init__(self, detections):
                                self.detections = detections.cpu().numpy()
                                self.boxes = self.detections if len(self.detections) > 0 else None
                        
                        results.append(SimpleResult(detection_results))
                    else:
                        # 无检测结果
                        class SimpleResult:
                            def __init__(self):
                                self.detections = np.array([])
                                self.boxes = None
                        results.append(SimpleResult())
                else:
                    # 无检测结果
                    class SimpleResult:
                        def __init__(self):
                            self.detections = np.array([])
                            self.boxes = None
                    results.append(SimpleResult())
            
            return results
            
        except Exception as e:
            if self.verbose:
                print(f"⚠️ 简化后处理也失败，返回原始输出: {e}")
            
            # 最后的回退方案
            class SimpleResult:
                def __init__(self, raw_outputs):
                    self.raw_outputs = raw_outputs
                    self.boxes = None
            
            return [SimpleResult(outputs)]
    
    def _simple_nms(self, boxes, scores, iou_threshold=0.45):
        """简单的NMS实现"""
        try:
            import torch
            
            if len(boxes) == 0:
                return []
            
            # 按置信度排序
            _, order = scores.sort(descending=True)
            
            keep = []
            while len(order) > 0:
                i = order[0]
                keep.append(i)
                
                if len(order) == 1:
                    break
                
                # 计算IoU
                ious = self._calculate_iou(boxes[i:i+1], boxes[order[1:]])
                
                # 保留IoU小于阈值的框
                keep_mask = ious < iou_threshold
                order = order[1:][keep_mask]
            
            return keep
            
        except Exception:
            # NMS失败时返回置信度最高的几个
            _, indices = torch.topk(scores, min(len(scores), 100))
            return indices.tolist()
    
    def _calculate_iou(self, box1, boxes2):
        """计算IoU"""
        try:
            import torch
            
            # box1: [1, 4], boxes2: [N, 4]
            # 格式: [x1, y1, x2, y2]
            
            x1 = torch.max(box1[:, 0:1], boxes2[:, 0:1].t())
            y1 = torch.max(box1[:, 1:2], boxes2[:, 1:2].t())
            x2 = torch.min(box1[:, 2:3], boxes2[:, 2:3].t())
            y2 = torch.min(box1[:, 3:4], boxes2[:, 3:4].t())
            
            intersection = torch.clamp(x2 - x1, min=0) * torch.clamp(y2 - y1, min=0)
            
            area1 = (box1[:, 2] - box1[:, 0]) * (box1[:, 3] - box1[:, 1])
            area2 = (boxes2[:, 2] - boxes2[:, 0]) * (boxes2[:, 3] - boxes2[:, 1])
            
            union = area1[:, None] + area2[None, :] - intersection
            
            iou = intersection / torch.clamp(union, min=1e-6)
            
            return iou.flatten()
            
        except Exception:
            # 如果IoU计算失败，返回全0（表示无重叠）
            import torch
            return torch.zeros(len(boxes2))
    
    def _apply_independent_confidence_filtering(self, results):
        """
        应用独立置信度过滤
        
        Args:
            results: 原始预测结果
            
        Returns:
            过滤后的预测结果
        """
        try:
            if not results or len(results) == 0:
                return results
            
            filtered_results = []
            
            # 输出调试信息（仅在verbose模式下）
            if self.verbose:
                print(f"       🔍 开始独立置信度过滤...")
                print(f"       📋 当前独立置信度设置: {dict(self.class_conf_thresholds)}")
                print(f"       📋 全局置信度: {self.global_conf}")
            
            # 获取模型类别信息
            model_names = {}
            
            # 兼容ONNX和PyTorch模型的类别名称获取
            if self.model_format == 'onnx':
                # 对于ONNX模型，使用self.class_names
                if self.class_names:
                    model_names = self.class_names
                    if self.verbose:
                        print(f"       🏷️ ONNX模型类别映射: {model_names}")
            elif hasattr(self, 'model') and self.model and hasattr(self.model, 'names') and self.model.names:
                # 对于PyTorch模型，使用self.model.names
                model_names = self.model.names
                if self.verbose:
                    print(f"       🏷️ PyTorch模型类别映射: {model_names}")
            
            if not model_names and self.verbose:
                print(f"       ⚠️ 无可用的模型类别名称信息")
            
            for result in results:
                if hasattr(result, 'boxes') and result.boxes is not None:
                    boxes = result.boxes
                    
                    if len(boxes) == 0:
                        filtered_results.append(result)
                        continue
                    
                    # 获取检测框信息
                    confidences = boxes.conf.cpu().numpy()
                    class_ids = boxes.cls.cpu().numpy()
                    
                    # 创建保留掩码
                    keep_mask = []
                    
                    for i in range(len(boxes)):
                        class_id = int(class_ids[i])
                        confidence = float(confidences[i])
                        
                        # 获取该类别的置信度阈值
                        class_threshold = self.global_conf  # 默认使用全局置信度
                        threshold_source = "全局"
                        match_key = None
                        
                        # 1. 首先检查类别ID是否直接在设置中
                        if class_id in self.class_conf_thresholds:
                            class_threshold = self.class_conf_thresholds[class_id]
                            threshold_source = f"独立ID({class_id})"
                            match_key = class_id
                        
                        # 2. 检查模型的类别名是否在设置中
                        elif class_id in model_names:
                            model_class_name = model_names[class_id]
                            if model_class_name in self.class_conf_thresholds:
                                class_threshold = self.class_conf_thresholds[model_class_name]
                                threshold_source = f"独立名({model_class_name})"
                                match_key = model_class_name
                        
                        # 3. 尝试所有可能的类别名称匹配
                        if match_key is None:
                            # 遍历所有设置的类别名，看是否有匹配的
                            for set_class_name in self.class_conf_thresholds.keys():
                                if isinstance(set_class_name, str):
                                    # 检查是否为该类别ID的任何可能名称
                                    possible_matches = [
                                        f"class{class_id}",
                                        f"Class{class_id}", 
                                        str(class_id),
                                        set_class_name.lower(),
                                        set_class_name.upper()
                                    ]
                                    
                                    # 如果有模型类别名，也加入比较
                                    if class_id in model_names:
                                        model_name = model_names[class_id]
                                        possible_matches.extend([
                                            model_name,
                                            model_name.lower(), 
                                            model_name.upper()
                                        ])
                                    
                                    if set_class_name in possible_matches or any(set_class_name == match for match in possible_matches):
                                        class_threshold = self.class_conf_thresholds[set_class_name]
                                        threshold_source = f"独立匹配({set_class_name})"
                                        match_key = set_class_name
                                        break
                        
                        # 判断是否保留
                        keep = confidence >= class_threshold
                        keep_mask.append(keep)
                        
                        # 输出详细的过滤信息（仅在verbose模式下）
                        if self.verbose:
                            model_name_info = f"({model_names.get(class_id, '未知')})" if class_id in model_names else ""
                            print(f"         🎯 类别ID={class_id}{model_name_info}, 置信度={confidence:.3f}, "
                                  f"{threshold_source}阈值={class_threshold:.3f}, "
                                  f"匹配键={match_key}, {'✅保留' if keep else '❌过滤'}")
                    
                    # 应用过滤掩码
                    if any(keep_mask):
                        # 使用torch的布尔索引进行过滤
                        import torch
                        keep_indices = torch.tensor(keep_mask, dtype=torch.bool)
                        
                        # 创建新的结果对象
                        import copy
                        filtered_result = copy.copy(result)
                        
                        # 直接修改原有的boxes对象，只修改data张量
                        filtered_result.boxes = copy.deepcopy(result.boxes)
                        
                        # 只过滤主要的data张量，其他属性会自动计算
                        filtered_result.boxes.data = result.boxes.data[keep_indices]
                        
                        filtered_results.append(filtered_result)
                        
                        original_count = len(keep_mask)
                        filtered_count = sum(keep_mask)
                        if self.verbose:
                            print(f"         📊 过滤结果: {original_count} -> {filtered_count} 个检测框")
                    else:
                        # 所有检测框都被过滤掉，返回空结果
                        import copy
                        import torch
                        filtered_result = copy.copy(result)
                        
                        # 创建空的boxes对象，保持原有结构但清空数据
                        filtered_result.boxes = copy.deepcopy(result.boxes)
                        
                        # 只清空主要的data张量
                        empty_tensor_2d = torch.empty((0, result.boxes.data.shape[1] if len(result.boxes.data.shape) > 1 else 6))
                        filtered_result.boxes.data = empty_tensor_2d.to(result.boxes.data.device)
                        
                        filtered_results.append(filtered_result)
                        
                        if self.verbose:
                            print(f"         ❌ 所有检测框都被独立置信度过滤掉")
                else:
                    # 没有检测框的结果直接保留
                    filtered_results.append(result)
            
            if self.verbose:
                print(f"       ✅ 独立置信度过滤完成")
            return filtered_results
            
        except Exception as e:
            print(f"       ❌ 独立置信度过滤失败: {e}")
            if self.verbose:
                import traceback
                print(f"       📍 错误详情: {traceback.format_exc()}")
            # 过滤失败时返回原始结果
            return results
    
    def predict_batch(self, images: list, **kwargs) -> list:
        """
        批量预测
        Args:
            images: 图像列表
            **kwargs: 预测参数
        Returns:
            预测结果列表
        """
        if not self.is_model_loaded:
            raise RuntimeError("模型未加载")
            
        try:
            results = []
            for i, image in enumerate(images):
                if self.verbose:
                    self._log(f"处理图像 {i+1}/{len(images)}")
                result = self.predict(image, **kwargs)
                results.append(result)
            return results
        except Exception as e:
            error_msg = f"批量预测失败: {str(e)}"
            if self.verbose:
                self._log(error_msg)
            raise RuntimeError(error_msg)
    
    def benchmark_model(self, test_image: np.ndarray, runs: int = 10) -> float:
        """
        对比模型性能
        Args:
            test_image: 测试图像
            runs: 测试轮数
        Returns:
            平均推理时间
        """
        if not self.is_model_loaded:
            print("❌ 模型未加载，无法进行性能测试")
            return 0.0
        
        # 获取模型实际运行的设备
        actual_device = self._get_actual_model_device()
        device_display = actual_device.upper() if actual_device != 'cpu' else 'CPU'
        model_type_display = self.model_format.upper() if self.model_format else 'UNKNOWN'
        
        print(f"📊 开始性能测试: {model_type_display}模型 | 设备: {device_display} | 测试轮数: {runs}")
        
        times = []
        for i in range(runs):
            start_time = time.perf_counter()
            
            try:
                if self.model_format == 'onnx':
                    results = self.model.predict(test_image, conf=0.4, iou=0.45, verbose=False, save=False)
                else:
                    results = self.model(test_image, conf=0.4, iou=0.45, verbose=False)
            except Exception as e:
                print(f"⚠️ 测试轮次 {i+1} 失败: {e}")
                continue
            
            inference_time = time.perf_counter() - start_time
            times.append(inference_time)
            
            if i == 0:  # 第一次通常最慢
                print(f"   首次推理: {inference_time:.4f}秒")
        
        if not times:
            print("❌ 所有测试轮次都失败了")
            return 0.0
        
        avg_time = sum(times) / len(times)
        min_time = min(times)
        max_time = max(times)
        
        print(f"📈 性能测试结果 ({model_type_display}/{device_display}):")
        print(f"   平均时间: {avg_time:.4f}秒")
        print(f"   最快时间: {min_time:.4f}秒")
        print(f"   最慢时间: {max_time:.4f}秒")
        
        return avg_time
    
    def set_independent_conf_enabled(self, enabled: bool) -> bool:
        """
        开启或关闭分类独立置信度功能
        
        Args:
            enabled: True启用分类独立置信度, False使用全局置信度
            
        Returns:
            bool: 设置是否成功
        """
        try:
            self.use_independent_conf = enabled
            status = "启用" if enabled else "禁用"
            if self.verbose:
                self._log(f"分类独立置信度功能已{status}")
            return True
        except Exception as e:
            if self.verbose:
                self._log(f"设置分类独立置信度功能失败: {e}")
            return False
    
    def set_global_confidence(self, conf_threshold: float) -> bool:
        """
        设置全局置信度阈值
        
        Args:
            conf_threshold: 置信度阈值 (0.0 - 1.0)
            
        Returns:
            bool: 设置是否成功
        """
        try:
            if not 0.0 <= conf_threshold <= 1.0:
                if self.verbose:
                    self._log(f"置信度阈值必须在0.0-1.0之间，当前值: {conf_threshold}")
                return False
                
            self.global_conf = conf_threshold
            if self.verbose:
                self._log(f"全局置信度阈值已设置为: {conf_threshold}")
            return True
        except Exception as e:
            if self.verbose:
                self._log(f"设置全局置信度失败: {e}")
            return False
    
    def set_class_confidence(self, class_identifier, conf_threshold: float) -> bool:
        """
        设置单个分类的置信度阈值（支持类别ID或类型名）
        
        Args:
            class_identifier: 类别标识符，可以是int(类别ID)或str(类型名)
            conf_threshold: 置信度阈值 (0.0-1.0)
            
        Returns:
            bool: 设置是否成功
        """
        try:
            # 验证置信度阈值范围
            if not (0.0 <= conf_threshold <= 1.0):
                if self.verbose:
                    self._log(f"置信度阈值必须在0.0-1.0范围内: {conf_threshold}")
                return False
            
            # 处理类别标识符
            if isinstance(class_identifier, str):
                # 如果是字符串，直接使用类型名作为键
                class_key = class_identifier
                display_name = class_identifier
            elif isinstance(class_identifier, int):
                # 如果是整数，使用类别ID作为键
                if class_identifier < 0:
                    if self.verbose:
                        self._log(f"类别ID必须是非负整数，当前值: {class_identifier}")
                    return False
                class_key = class_identifier
                display_name = self.get_class_name(class_identifier)
            else:
                if self.verbose:
                    self._log(f"不支持的类别标识符类型: {type(class_identifier)}")
                return False
            
            # 设置独立置信度
            self.class_conf_thresholds[class_key] = conf_threshold
            
            if self.verbose:
                self._log(f"类别 {display_name} 置信度阈值已设置为: {conf_threshold}")
            
            return True
            
        except Exception as e:
            if self.verbose:
                self._log(f"设置分类置信度失败: {str(e)}")
            return False

    def get_independent_confidence_status(self) -> dict:
        """
        获取独立置信度设置状态
        
        Returns:
            dict: 独立置信度状态信息
        """
        status = {
            "独立置信度启用": self.use_independent_conf,
            "全局置信度": self.global_conf,
            "类别置信度设置数量": len(self.class_conf_thresholds),
            "类别置信度详情": dict(self.class_conf_thresholds)
        }
        
        if self.class_conf_thresholds:
            min_conf = min(self.class_conf_thresholds.values())
            max_conf = max(self.class_conf_thresholds.values())
            status["类别置信度范围"] = f"{min_conf:.3f} - {max_conf:.3f}"
            status["推荐预测置信度"] = min(self.global_conf, min_conf)
        else:
            status["类别置信度范围"] = "未设置"
            status["推荐预测置信度"] = self.global_conf
        
        return status

    def reset_confidence_settings(self):
        """
        重置所有置信度设置为默认值
        """
        self.use_independent_conf = False
        self.global_conf = 0.25
        self.class_conf_thresholds = {}
        
        if self.verbose:
            self._log("置信度设置已重置为默认值")

    def _get_actual_model_device(self) -> str:
        """
        获取模型实际运行的设备
        
        Returns:
            str: 实际设备名称，如 'cpu', 'cuda:0', 'cuda:1' 等
        """
        try:
            if not self.is_model_loaded:
                return 'unknown'
            
            # 方法1: 对于ONNX模型，检查会话的执行提供者
            if self.model_format == 'onnx' and self.session:
                try:
                    providers = self.session.get_providers()
                    if 'CUDAExecutionProvider' in providers:
                        # 尝试从环境变量获取GPU ID
                        cuda_visible = os.environ.get('CUDA_VISIBLE_DEVICES', None)
                        if cuda_visible is not None and cuda_visible != '':
                            gpu_ids = [int(x) for x in cuda_visible.split(',') if x.strip().isdigit()]
                            if gpu_ids:
                                return f'cuda:{gpu_ids[0]}'
                        
                        # 从设备配置中获取GPU ID
                        if hasattr(self, 'device') and 'cuda:' in self.device:
                            return self.device
                        
                        return 'cuda:0'  # 默认GPU
                    else:
                        return 'cpu'
                except Exception as e:
                    if self.verbose:
                        self._log(f"⚠️ 检查ONNX会话提供者失败: {e}")
            
            # 方法2: 对于PyTorch模型，检查模型参数的设备
            elif self.model_format in ['pt', 'pth', 'torchscript'] and hasattr(self, 'model'):
                try:
                    # 检查模型的第一个参数所在设备
                    if hasattr(self.model, 'model') and hasattr(self.model.model, 'parameters'):
                        first_param = next(self.model.model.parameters(), None)
                        if first_param is not None:
                            param_device = first_param.device
                            if param_device.type == 'cuda':
                                return f'cuda:{param_device.index}' if param_device.index is not None else 'cuda:0'
                            else:
                                return param_device.type
                    
                    # 备选方法：检查模型的device属性
                    if hasattr(self.model, 'device'):
                        model_device = str(self.model.device)
                        return model_device
                    
                    # 再备选：检查模型的model属性的device
                    if hasattr(self.model, 'model') and hasattr(self.model.model, 'device'):
                        return str(self.model.model.device)
                        
                except Exception as e:
                    if self.verbose:
                        self._log(f"⚠️ 检查PyTorch模型设备失败: {e}")
            
            # 方法3: 回退到自设定的设备
            if self.verbose:
                self._log("⚠️ 无法获取模型实际设备，使用设置的设备")
            return self.device if self.device else 'cpu'
            
        except Exception as e:
            if self.verbose:
                self._log(f"⚠️ 获取模型实际设备时出错: {e}")
            return self.device if self.device else 'cpu'

    def set_device(self, device: str) -> bool:
        """
        设置计算设备
        Args:
            device: 设备名称
        Returns:
            是否设置成功
        """
        try:
            new_device = self._select_device(device)
            if self.is_model_loaded and hasattr(self.model, 'to'):
                self.model.to(new_device)
            self.device = new_device
            if self.verbose:
                self._log(f"设备已切换到: {self.device}")
            return True
        except Exception as e:
            if self.verbose:
                self._log(f"设备切换失败: {e}")
            return False

    def set_class_names(self, class_names) -> str:
        self.usr_class_name = class_names

    def get_class_name(self, class_id: int) -> str:
        """
        根据类别ID获取类别名称（可以根据实际模型自定义）
        
        Args:
            class_id: 类别ID
            
        Returns:
            str: 类别名称
        """   
        # 优先使用用户自定义的类别名称
        if self.usr_class_name is not None:
            # 尝试直接用整数键查找
            if class_id in self.usr_class_name:
                return self.usr_class_name[class_id]
            # 尝试用字符串键查找 (如 'class0', 'class1' 等)
            class_key = f'class{class_id}'
            if class_key in self.usr_class_name:
                return self.usr_class_name[class_key]
        
        # 其次使用模型默认的类别名称
        if hasattr(self, 'class_names') and self.class_names is not None:
            return self.class_names.get(class_id, f"Class{class_id}")
        
        # 最后返回默认格式
        return f"Class{class_id}"
    
    def extract_detection_list(self, model_results) -> list:
        """
        从模型推理结果中提取检测框信息为结构化列表
        自动识别YOLO格式和ONNX格式的输出
        
        Args:
            model_results: 模型推理结果 (支持YOLO Results对象或ONNX原始输出)
            
        Returns:
            list: 检测框信息列表，每个元素包含：
                {
                    "类型编号": int,           # 类别ID
                    "类型字符": str,           # 类别名称
                    "AOI位置": {              # 边界框坐标
                        "x1": int,
                        "y1": int, 
                        "x2": int,
                        "y2": int,
                        "center_x": int,
                        "center_y": int,
                        "width": int,
                        "height": int
                    },
                    "检测置信度": float,       # 原始检测置信度
                    "分类置信度": float,       # 分类置信度（如果可用）
                    "综合置信度": float,       # 综合置信度分数
                    "面积": int,              # 检测框面积
                    "索引": int               # 检测结果索引
                }
        """
        detection_list = []
        
        try:
            if not model_results or len(model_results) == 0:
                if self.verbose:
                    self._log("⚠️ 无检测结果可提取")
                return detection_list
            
            result = model_results[0]
            
            # 🔍 自动识别输出格式类型
            is_yolo_format = self._detect_result_format(result)
            
            if is_yolo_format:
                # ✅ YOLO格式处理 (ultralytics Results对象)
                return self._extract_from_yolo_format(result)
            else:
                # 🔧 ONNX格式处理 (原始输出或简化对象)
                return self._extract_from_onnx_format(result)
            
        except Exception as e:
            if self.verbose:
                self._log(f"❌ 提取检测结果失败: {str(e)}")
            return detection_list
    
    def _detect_result_format(self, result) -> bool:
        """检测结果格式类型"""
        try:
            # 检查是否为标准YOLO Results对象
            if hasattr(result, 'boxes') and hasattr(result, 'names'):
                return True
            
            # 检查是否为简化Results对象但有标准boxes属性
            if hasattr(result, 'boxes') and result.boxes is not None:
                if hasattr(result.boxes, 'xyxy') and hasattr(result.boxes, 'conf'):
                    return True
            
            # 其他情况视为ONNX格式
            return False
            
        except Exception:
            return False
    
    def _extract_from_yolo_format(self, result) -> list:
        """从YOLO格式结果中提取检测信息"""
        detection_list = []
        
        try:
            # 检查是否有检测框
            if result.boxes is None or len(result.boxes) == 0:
                if self.verbose:
                    self._log("⚠️ 未检测到任何目标")
                return detection_list
            
            # 获取检测结果
            boxes = result.boxes.xyxy.cpu().numpy()  # [x1, y1, x2, y2]
            confidences = result.boxes.conf.cpu().numpy()  # 置信度
            class_ids = result.boxes.cls.cpu().numpy()  # 类别ID
            
            if self.verbose:
                self._log(f"📋 开始提取 {len(boxes)} 个YOLO检测结果")
            
            # 提取每个检测框的信息
            for i in range(len(boxes)):
                x1, y1, x2, y2 = boxes[i].astype(int)
                confidence = float(confidences[i])
                class_id = int(class_ids[i])
                
                # 计算中心点和尺寸
                center_x = int((x1 + x2) / 2)
                center_y = int((y1 + y2) / 2)
                width = int(x2 - x1)
                height = int(y2 - y1)
                area = width * height
                
                # 获取类别名称
                class_name = self.get_class_name(class_id)
                
                # 计算不同类型的置信度
                detection_confidence = confidence  # 检测置信度
                classification_confidence = confidence  # 分类置信度（YOLO中通常相同）
                combined_confidence = confidence  # 综合置信度
                
                # 如果有额外的置信度信息，可以在这里处理
                if hasattr(result.boxes, 'cls_conf'):
                    try:
                        classification_confidence = float(result.boxes.cls_conf[i].cpu().numpy())
                        combined_confidence = (detection_confidence + classification_confidence) / 2
                    except:
                        pass  # 如果获取失败，使用默认值
                
                # 构建检测信息字典
                detection_info = {
                    "类型编号": class_id,
                    "类型字符": class_name,
                    "AOI位置": {
                        "x1": int(x1),
                        "y1": int(y1),
                        "x2": int(x2),
                        "y2": int(y2),
                        "center_x": center_x,
                        "center_y": center_y,
                        "width": width,
                        "height": height
                    },
                    "检测置信度": round(detection_confidence, 4),
                    "分类置信度": round(classification_confidence, 4),
                    "综合置信度": round(combined_confidence, 4),
                    "面积": area,
                    "索引": i
                }
                
                detection_list.append(detection_info)
                
                if self.verbose:
                    self._log(f"  - 检测{i}: {class_name}(ID:{class_id}), 置信度:{confidence:.3f}, "
                          f"位置:[{x1},{y1},{x2},{y2}], 面积:{area}")
            
            if self.verbose:
                self._log(f"✅ 成功提取 {len(detection_list)} 个YOLO检测结果")
            return detection_list
            
        except Exception as e:
            if self.verbose:
                self._log(f"❌ YOLO格式提取失败: {str(e)}")
            return detection_list
    
    def _extract_from_onnx_format(self, result) -> list:
        """从ONNX格式结果中提取检测信息"""
        detection_list = []
        
        try:
            # 处理ONNX原始输出或简化结果对象
            detections_data = None
            
            # 🔧 优先检查raw_outputs，但仅在ONNX模式下使用特殊处理
            if hasattr(result, 'raw_outputs') and result.raw_outputs is not None:
                # 检查是否为ONNX模式（通过检查是否有session属性）
                is_onnx_mode = hasattr(self, 'session') and self.session is not None
                
                if is_onnx_mode:
                    # ONNX模式：处理特殊格式 (如10,5376)
                    raw_outputs = result.raw_outputs
                    if self.verbose:
                        self._log(f"🔧 ONNX模式检测到特殊输出格式: {raw_outputs.shape}")
                    
                    # 处理特殊ONNX格式：前4行是坐标，后N行是类别置信度
                    if len(raw_outputs.shape) == 2 and raw_outputs.shape[0] >= 5:
                        return self._process_special_onnx_format(raw_outputs)
                    else:
                        if self.verbose:
                            self._log("⚠️ 未识别的ONNX原始输出格式")
                        return detection_list
                else:
                    # 非ONNX模式：跳过raw_outputs特殊处理
                    if self.verbose:
                        self._log("🔄 非ONNX模式，跳过raw_outputs特殊处理")
                    
            if hasattr(result, 'detections') and result.detections is not None:
                # 简化结果对象，有处理后的detections
                if len(result.detections) == 0:
                    if self.verbose:
                        self._log("⚠️ ONNX结果无检测数据")
                    return detection_list
                
                detections_data = result.detections
                
            else:
                if self.verbose:
                    self._log("⚠️ 未识别的ONNX结果格式")
                return detection_list
            
            # 处理标准检测数据 [N, 6] -> [x1, y1, x2, y2, conf, class]
            if self.verbose:
                self._log(f"📋 开始提取 {len(detections_data)} 个ONNX检测结果")
            
            for i, detection in enumerate(detections_data):
                x1, y1, x2, y2 = detection[:4].astype(int)
                confidence = float(detection[4])
                class_id = int(detection[5])
                
                # 计算中心点和尺寸
                center_x = int((x1 + x2) / 2)
                center_y = int((y1 + y2) / 2)
                width = int(x2 - x1)
                height = int(y2 - y1)
                area = width * height
                
                # 获取类别名称
                class_name = self.get_class_name(class_id)
                
                # 构建检测信息字典
                detection_info = {
                    "类型编号": class_id,
                    "类型字符": class_name,
                    "AOI位置": {
                        "x1": int(x1),
                        "y1": int(y1),
                        "x2": int(x2),
                        "y2": int(y2),
                        "center_x": center_x,
                        "center_y": center_y,
                        "width": width,
                        "height": height
                    },
                    "检测置信度": round(confidence, 4),
                    "分类置信度": round(confidence, 4),
                    "综合置信度": round(confidence, 4),
                    "面积": area,
                    "索引": i
                }
                
                detection_list.append(detection_info)
                
                if self.verbose:
                    self._log(f"  - 检测{i}: {class_name}(ID:{class_id}), 置信度:{confidence:.3f}, "
                          f"位置:[{x1},{y1},{x2},{y2}], 面积:{area}")
            
            if self.verbose:
                self._log(f"✅ 成功提取 {len(detection_list)} 个ONNX检测结果")
            return detection_list
            
        except Exception as e:
            if self.verbose:
                self._log(f"❌ ONNX格式提取失败: {str(e)}")
            return detection_list
    
    def _process_special_onnx_format(self, raw_outputs) -> list:
        """
        处理特殊的ONNX输出格式 (动态类别数量)
        前4行是边界框坐标 (x, y, w, h)
        后N行是类别置信度 (N个类别)
        """
        detection_list = []
        
        try:
            if self.verbose:
                self._log(f"🔧 处理特殊ONNX格式: {raw_outputs.shape}")
            
            # 动态计算类别数量
            total_rows = raw_outputs.shape[0]
            num_classes = total_rows - 4  # 总行数减去4行坐标
            
            if num_classes <= 0:
                if self.verbose:
                    self._log(f"❌ 无效的ONNX输出格式，总行数: {total_rows}, 类别数: {num_classes}")
                return detection_list
            
            if self.verbose:
                self._log(f"🔍 检测到 {num_classes} 个类别，总行数: {total_rows}")
            
            # 解析格式：前4行坐标，后N行类别
            x_coords = raw_outputs[0]  # x坐标
            y_coords = raw_outputs[1]  # y坐标 
            w_coords = raw_outputs[2]  # 宽度
            h_coords = raw_outputs[3]  # 高度
            
            # N个类别的置信度 (行4到4+N)
            class_confidences = raw_outputs[4:4+num_classes]  # shape: (N, 5376)
            
            # 🚀 关键优化：向量化计算所有检测点的最大类别和置信度，替代5376次循环
            max_class_ids = np.argmax(class_confidences, axis=0)  # shape: (5376,)
            max_confidences = np.max(class_confidences, axis=0)   # shape: (5376,)
            
            # 🚀 关键优化：预计算所有置信度阈值，避免重复查找
            if self.use_independent_conf and self.class_conf_thresholds:
                # 为每个类别ID预计算阈值
                class_thresholds = np.full(num_classes, self.global_conf)  # 默认使用全局置信度
                
                for class_id in range(num_classes):
                    class_name = self.get_class_name(class_id)
                    # 快速查找该类别的置信度阈值
                    threshold = self.global_conf
                    if class_id in self.class_conf_thresholds:
                        threshold = self.class_conf_thresholds[class_id]
                    elif class_name in self.class_conf_thresholds:
                        threshold = self.class_conf_thresholds[class_name]
                    elif f"class{class_id}" in self.class_conf_thresholds:
                        threshold = self.class_conf_thresholds[f"class{class_id}"]
                    
                    class_thresholds[class_id] = threshold
                
                # 🚀 向量化置信度过滤：直接索引获取对应阈值
                required_thresholds = class_thresholds[max_class_ids]  # shape: (5376,)
                valid_mask = max_confidences >= required_thresholds   # shape: (5376,)
            else:
                # 使用全局置信度的向量化过滤
                valid_mask = max_confidences >= self.global_conf
            
            # 🚀 向量化坐标过滤和批量处理
            valid_indices = np.where(valid_mask)[0]
            
            if len(valid_indices) == 0:
                if self.verbose:
                    self._log("⚠️ 没有检测结果满足置信度要求")
                return detection_list
            
            # 批量获取有效检测的坐标和置信度
            valid_x = x_coords[valid_indices]
            valid_y = y_coords[valid_indices] 
            valid_w = w_coords[valid_indices]
            valid_h = h_coords[valid_indices]
            valid_class_ids = max_class_ids[valid_indices]
            valid_confidences = max_confidences[valid_indices]
            
            # 向量化过滤无效尺寸
            size_mask = (valid_w > 0) & (valid_h > 0)
            valid_x = valid_x[size_mask]
            valid_y = valid_y[size_mask]
            valid_w = valid_w[size_mask]
            valid_h = valid_h[size_mask]
            valid_class_ids = valid_class_ids[size_mask]
            valid_confidences = valid_confidences[size_mask]
            
            # 向量化边界框计算
            x1_array = (valid_x - valid_w / 2).astype(int)
            y1_array = (valid_y - valid_h / 2).astype(int)
            x2_array = (valid_x + valid_w / 2).astype(int)
            y2_array = (valid_y + valid_h / 2).astype(int)
            
            # 过滤掉无效边界框
            bbox_mask = (x1_array < x2_array) & (y1_array < y2_array)
            final_count = np.sum(bbox_mask)
            
            if final_count == 0:
                if self.verbose:
                    self._log("⚠️ 没有有效的检测框")
                return detection_list
            
            # 应用最终过滤掩码
            x1_array = x1_array[bbox_mask]
            y1_array = y1_array[bbox_mask] 
            x2_array = x2_array[bbox_mask]
            y2_array = y2_array[bbox_mask]
            valid_x = valid_x[bbox_mask]
            valid_y = valid_y[bbox_mask]
            valid_w = valid_w[bbox_mask]
            valid_h = valid_h[bbox_mask]
            valid_class_ids = valid_class_ids[bbox_mask]
            valid_confidences = valid_confidences[bbox_mask]
            
            # 🚀 批量构建检测结果，替代原来的5376次循环
            for i in range(len(valid_x)):
                class_id = int(valid_class_ids[i])
                confidence = float(valid_confidences[i])
                x1, y1, x2, y2 = x1_array[i], y1_array[i], x2_array[i], y2_array[i]
                center_x, center_y = int(valid_x[i]), int(valid_y[i])
                width, height = int(valid_w[i]), int(valid_h[i])
                
                class_name = self.get_class_name(class_id)
                area = width * height
                
                detection_info = {
                    "类型编号": class_id,
                    "类型字符": class_name,
                    "AOI位置": {
                        "x1": x1,
                        "y1": y1,
                        "x2": x2,
                        "y2": y2,
                        "center_x": center_x,
                        "center_y": center_y,
                        "width": width,
                        "height": height
                    },
                    "检测置信度": round(confidence, 4),
                    "分类置信度": round(confidence, 4),
                    "综合置信度": round(confidence, 4),
                    "面积": area,
                    "索引": i
                }
                
                detection_list.append(detection_info)
            
            if self.verbose:
                self._log(f"✅ 向量化优化提取 {len(detection_list)} 个检测结果 (从{len(valid_indices)}个候选中筛选)")
            
            return detection_list
            
        except Exception as e:
            if self.verbose:
                self._log(f"❌ 特殊ONNX格式处理失败: {str(e)}")
            return detection_list
    
    def get_detection_summary(self, detection_list: list) -> dict:
        """
        获取检测结果的统计摘要
        
        Args:
            detection_list: 检测结果列表
            
        Returns:
            dict: 统计摘要信息
        """
        if not detection_list:
            return {
                "总数": 0,
                "类别统计": {},
                "置信度统计": {},
                "面积统计": {}
            }
        
        # 统计各类别数量
        class_count = {}
        confidences = []
        areas = []
        
        for detection in detection_list:
            class_name = detection["类型字符"]
            class_count[class_name] = class_count.get(class_name, 0) + 1
            confidences.append(detection["检测置信度"])
            areas.append(detection["面积"])
        
        import numpy as np
        
        summary = {
            "总数": len(detection_list),
            "类别统计": class_count,
            "置信度统计": {
                "最小值": round(min(confidences), 4),
                "最大值": round(max(confidences), 4),
                "平均值": round(np.mean(confidences), 4),
                "中位数": round(np.median(confidences), 4)
            },
            "面积统计": {
                "最小面积": min(areas),
                "最大面积": max(areas), 
                "平均面积": round(np.mean(areas), 2),
                "总面积": sum(areas)
            }
        }
        
        return summary
    
    def merge_same_class_boxes(self, detection_list: list, overlap_threshold: float = 0.5) -> list:
        """
        合并同类别的重叠检测框
        
        Args:
            detection_list: 检测结果列表
            overlap_threshold: 重叠度阈值 (0.0-1.0)，高于此阈值的同类框将被合并
            
        Returns:
            list: 合并后的检测结果列表
        """
        try:
            if not detection_list or len(detection_list) <= 1:
                return detection_list
            
            if self.verbose:
                self._log(f"🔄 开始合并同类框，重叠阈值: {overlap_threshold:.2f}")
                self._log(f"   原始检测框数量: {len(detection_list)}")
            
            # 按类别分组
            class_groups = {}
            for detection in detection_list:
                class_id = detection["类型编号"]
                if class_id not in class_groups:
                    class_groups[class_id] = []
                class_groups[class_id].append(detection)
            
            merged_list = []
            total_merged = 0
            
            # 对每个类别单独处理
            for class_id, detections in class_groups.items():
                class_name = detections[0]["类型字符"] if detections else f"Class{class_id}"
                if self.verbose:
                    self._log(f"   处理类别 {class_name} (ID:{class_id})，共 {len(detections)} 个框")
                
                if len(detections) == 1:
                    # 只有一个框，直接添加
                    merged_list.extend(detections)
                    continue
                
                # 合并该类别的重叠框
                merged_class_boxes = self._merge_boxes_in_class(detections, overlap_threshold)
                merged_count = len(detections) - len(merged_class_boxes)
                total_merged += merged_count
                
                if self.verbose:
                    self._log(f"     合并了 {merged_count} 个框，剩余 {len(merged_class_boxes)} 个")
                merged_list.extend(merged_class_boxes)
            
            # 重新分配索引
            for i, detection in enumerate(merged_list):
                detection["索引"] = i
            
            if self.verbose:
                self._log(f"✅ 合并完成:")
                self._log(f"   原始框数: {len(detection_list)}")
                self._log(f"   合并后框数: {len(merged_list)}")
                self._log(f"   总共合并: {total_merged} 个框")
            
            return merged_list
            
        except Exception as e:
            if self.verbose:
                self._log(f"❌ 合并同类框失败: {str(e)}")
            return detection_list

    def _merge_boxes_in_class(self, detections: list, overlap_threshold: float) -> list:
        """
        合并同一类别中的重叠框
        
        Args:
            detections: 同一类别的检测结果列表
            overlap_threshold: 重叠度阈值
            
        Returns:
            list: 合并后的检测结果列表
        """
        if len(detections) <= 1:
            return detections
        
        merged_boxes = []
        used_indices = set()
        
        for i, detection_i in enumerate(detections):
            if i in used_indices:
                continue
            
            # 找到与当前框重叠的所有框
            overlapping_boxes = [detection_i]
            overlapping_indices = {i}
            
            for j, detection_j in enumerate(detections):
                if i == j or j in used_indices:
                    continue
                
                # 计算重叠度
                overlap_ratio = self._calculate_overlap_ratio(detection_i, detection_j)
                
                if overlap_ratio > overlap_threshold:
                    overlapping_boxes.append(detection_j)
                    overlapping_indices.add(j)
            
            # 合并重叠的框
            if len(overlapping_boxes) > 1:
                merged_box = self._merge_multiple_boxes(overlapping_boxes)
                merged_boxes.append(merged_box)
                used_indices.update(overlapping_indices)
                
                if self.verbose:
                    self._log(f"       合并 {len(overlapping_boxes)} 个重叠框")
            else:
                # 没有重叠的框，直接添加
                merged_boxes.append(detection_i)
                used_indices.add(i)
        
        return merged_boxes

    def _calculate_overlap_ratio(self, detection1: dict, detection2: dict) -> float:
        """
        计算两个检测框的重叠度 (重叠面积 / 较小框面积)
        
        Args:
            detection1: 第一个检测框
            detection2: 第二个检测框
            
        Returns:
            float: 重叠度比例 (0.0-1.0)
        """
        try:
            aoi1 = detection1["AOI位置"]
            aoi2 = detection2["AOI位置"]
            
            x1_1, y1_1, x2_1, y2_1 = aoi1["x1"], aoi1["y1"], aoi1["x2"], aoi1["y2"]
            x1_2, y1_2, x2_2, y2_2 = aoi2["x1"], aoi2["y1"], aoi2["x2"], aoi2["y2"]
            
            # 计算重叠区域
            overlap_x1 = max(x1_1, x1_2)
            overlap_y1 = max(y1_1, y1_2)
            overlap_x2 = min(x2_1, x2_2)
            overlap_y2 = min(y2_1, y2_2)
            
            # 检查是否有重叠
            if overlap_x1 >= overlap_x2 or overlap_y1 >= overlap_y2:
                return 0.0
            
            # 计算重叠面积
            overlap_area = (overlap_x2 - overlap_x1) * (overlap_y2 - overlap_y1)
            
            # 计算两个框的面积
            area1 = detection1["面积"]
            area2 = detection2["面积"]
            
            # 重叠度 = 重叠面积 / 较小框的面积
            smaller_area = min(area1, area2)
            overlap_ratio = overlap_area / smaller_area if smaller_area > 0 else 0.0
            
            return overlap_ratio
            
        except Exception as e:
            if self.verbose:
                self._log(f"⚠️ 计算重叠度失败: {str(e)}")
            return 0.0

    def _merge_multiple_boxes(self, boxes: list) -> dict:
        """
        合并多个检测框为一个
        
        Args:
            boxes: 要合并的检测框列表
            
        Returns:
            dict: 合并后的检测框
        """
        if len(boxes) == 1:
            return boxes[0]
        
        # 找到置信度最高的框作为基准
        best_box = max(boxes, key=lambda x: x["检测置信度"])
        
        # 计算最小外接矩形
        x1_min = min(box["AOI位置"]["x1"] for box in boxes)
        y1_min = min(box["AOI位置"]["y1"] for box in boxes)
        x2_max = max(box["AOI位置"]["x2"] for box in boxes)
        y2_max = max(box["AOI位置"]["y2"] for box in boxes)
        
        # 计算新的中心点和尺寸
        center_x = int((x1_min + x2_max) / 2)
        center_y = int((y1_min + y2_max) / 2)
        width = int(x2_max - x1_min)
        height = int(y2_max - y1_min)
        area = width * height
        
        # 计算平均置信度（可选，这里使用最高置信度）
        confidences = [box["检测置信度"] for box in boxes]
        max_confidence = max(confidences)
        
        # 构建合并后的检测框
        merged_box = {
            "类型编号": best_box["类型编号"],
            "类型字符": best_box["类型字符"],
            "AOI位置": {
                "x1": int(x1_min),
                "y1": int(y1_min),
                "x2": int(x2_max),
                "y2": int(y2_max),
                "center_x": center_x,
                "center_y": center_y,
                "width": width,
                "height": height
            },
            "检测置信度": round(max_confidence, 4),  # 使用最高置信度
            "分类置信度": round(max_confidence, 4),
            "综合置信度": round(max_confidence, 4),
            "面积": area,
            "索引": best_box["索引"]  # 临时使用，后续会重新分配
        }
        
        if self.verbose:
            self._log(f"         合并框: 置信度{max_confidence:.3f}, 尺寸{width}x{height}, 面积{area}")
        
        return merged_box

    def merge_overlapping_detections_inplace(self, detection_list: list, overlap_threshold: float = 0.5) -> tuple[bool, str]:
        """
        就地合并重叠的同类检测框（直接修改原列表）
        
        Args:
            detection_list: 检测结果列表（会被直接修改）
            overlap_threshold: 重叠度阈值
            
        Returns:
            tuple[bool, str]: (是否成功, 操作信息)
        """
        try:
            original_count = len(detection_list)
            
            if original_count <= 1:
                return True, f"检测框数量 <= 1，无需合并"
            
            # 执行合并
            merged_list = self.merge_same_class_boxes(detection_list, overlap_threshold)
            
            # 清空原列表并添加合并后的结果
            detection_list.clear()
            detection_list.extend(merged_list)
            
            merged_count = original_count - len(detection_list)
            
            result_msg = f"合并完成: 原始{original_count}个框 → 合并后{len(detection_list)}个框 (合并了{merged_count}个)"
            
            return True, result_msg
            
        except Exception as e:
            error_msg = f"合并检测框失败: {str(e)}"
            if self.verbose:
                self._log(f"❌ {error_msg}")
            return False, error_msg
    
    def draw_detections_from_list(self, image: np.ndarray, deleted_list: list) -> np.ndarray:
        """
        根据检测结果列表在图像上标注检测框
        
        Args:
            image: 输入图像 (numpy.ndarray)
            deleted_list: 检测结果列表，每个元素包含完整的检测信息
            
        Returns:
            np.ndarray: 标注后的图像
        """
        try:
            if deleted_list is None or len(deleted_list) == 0:
                if self.verbose:
                    self._log("⚠️ 检测列表为空，无需标注")
                return image
            
            annotated_image = image.copy()
            
            if self.verbose:
                self._log(f"🎯 开始标注 {len(deleted_list)} 个检测结果")
            
            # 定义不同类别的颜色 (BGR格式)
            class_colors = {
                0: (0, 255, 0),     # damianNG - 绿色
                1: (255, 0, 0),     # quekou1 - 蓝色
                2: (0, 0, 255),     # posun - 红色
                3: (255, 255, 0),   # quekou3 - 青色
                4: (255, 0, 255),   # sekuai - 洋红色
                5: (0, 255, 255),   # yaying - 黄色
            }
            
            # 默认颜色（当类别ID不在预定义范围内时）
            default_colors = [
                (128, 0, 128),   # 紫色
                (255, 165, 0),   # 橙色
                (0, 128, 255),   # 浅蓝色
                (128, 255, 0),   # 浅绿色
            ]
            
            # 标注每个检测结果
            for detection in deleted_list:
                try:
                    if '不显示' in detection:
                        continue
                    # 获取检测信息
                    class_id = detection["类型编号"]
                    class_name = detection["类型字符"]
                    aoi_position = detection["AOI位置"]
                    confidence = detection["检测置信度"]
                    area = detection["面积"]
                    index = detection["索引"]
                    
                    # 获取边界框坐标
                    x1 = aoi_position["x1"]
                    y1 = aoi_position["y1"]
                    x2 = aoi_position["x2"]
                    y2 = aoi_position["y2"]
                    
                    # 选择颜色
                    if class_id in class_colors:
                        color = class_colors[class_id]
                    else:
                        color = default_colors[class_id % len(default_colors)]
                    
                    # 绘制检测框
                    cv2.rectangle(annotated_image, (x1, y1), (x2, y2), color, 2)
                    
                    # 准备标签文本
                    label_parts = [
                        f"{class_name} :{confidence:.3f} ",
                        #f"confidence:{confidence:.3f}",
                        #f"area:{area}"
                    ]
                    
                    # 绘制多行标签
                    font = cv2.FONT_HERSHEY_SIMPLEX
                    font_scale = 0.5
                    thickness = 1
                    line_height = 20
                    
                    # 计算标签背景尺寸
                    max_text_width = 0
                    text_heights = []
                    
                    for label_part in label_parts:
                        (text_width, text_height), baseline = cv2.getTextSize(
                            label_part, font, font_scale, thickness
                        )
                        max_text_width = max(max_text_width, text_width)
                        text_heights.append(text_height + baseline)
                    
                    total_height = len(label_parts) * line_height + 5
                    
                    # 计算标签位置（避免超出图像边界）
                    label_x = x1
                    label_y = y1 - total_height
                    
                    # 如果标签会超出图像顶部，放到框下面
                    if label_y < 0:
                        label_y = y2 + line_height
                    
                    # 如果标签会超出图像右边，向左移动
                    if label_x + max_text_width > annotated_image.shape[1]:
                        label_x = annotated_image.shape[1] - max_text_width - 10
                    
                    # 绘制标签背景
                    # cv2.rectangle(annotated_image,
                    #             (label_x - 5, label_y - 5),
                    #             (label_x + max_text_width + 5, label_y + total_height),
                    #             color, -1)  # 填充背景
                    
                    # 添加半透明效果
                    overlay = annotated_image.copy()
                    cv2.rectangle(overlay,
                                (label_x - 5, label_y - 5),
                                (label_x + max_text_width + 5, label_y + total_height),
                                color, -1)
                    cv2.addWeighted(overlay, 0.2, annotated_image, 0.8, 0, annotated_image)
                    
                    # 绘制多行文本
                    for i, label_part in enumerate(label_parts):
                        text_y = label_y + (i + 1) * line_height
                        cv2.putText(annotated_image, label_part,
                                (label_x, text_y),
                                font, font_scale,
                                (255, 255, 255), thickness)  # 白色文字
                    
                    # 绘制中心点
                    center_x = aoi_position["center_x"]
                    center_y = aoi_position["center_y"]
                    #cv2.circle(annotated_image, (center_x, center_y), 3, color, -1)
                    
                    # 绘制检测索引（在框的右上角）
                    # index_text = f"#{index}"
                    # (idx_width, idx_height), _ = cv2.getTextSize(
                    #     index_text, font, 0.4, 1
                    # )
                    #cv2.rectangle(annotated_image,
                                #(x2 - idx_width - 5, y1),
                                #(x2, y1 + idx_height + 5),
                                #(0, 0, 0), -1)  # 黑色背景
                    #cv2.putText(annotated_image, index_text,
                            #(x2 - idx_width - 2, y1 + idx_height + 2),
                            #font, 0.4, (255, 255, 255), 1)
                    
                    if self.verbose:
                        self._log(f"  ✅ 标注完成 - 索引:{index}, 类别:{class_name}, "
                            f"置信度:{confidence:.3f}, 坐标:[{x1},{y1},{x2},{y2}]")
                    
                except KeyError as ke:
                    if self.verbose:
                        self._log(f"  ❌ 检测数据格式错误，缺少字段: {ke}")
                    continue
                except Exception as de:
                    if self.verbose:
                        self._log(f"  ❌ 标注单个检测结果失败: {str(de)}")
                    continue
            
            # 在图像顶部添加统计信息
            #self.draw_detection_summary(annotated_image, deleted_list)
            
            if self.verbose:
                self._log(f"✅ 所有检测结果标注完成")
            return annotated_image
            
        except Exception as e:
            if self.verbose:
                self._log(f"❌ 标注检测结果失败: {str(e)}")
            return image

    def draw_detection_summary(self, image: np.ndarray, deleted_list: list):
        """
        在图像顶部绘制检测统计摘要
        
        Args:
            image: 要绘制的图像
            deleted_list: 检测结果列表
        """
        try:
            if not deleted_list:
                return
            
            # 获取统计信息
            summary = self.get_detection_summary(deleted_list)
            
            # 准备统计文本
            total_count = summary["总数"]
            class_stats = summary["类别统计"]
            avg_confidence = summary["置信度统计"]["平均值"]
            
            # 构建统计文本
            summary_text = f"total_count : {total_count}"
            class_text = " | ".join([f"{cls}:{count}" for cls, count in class_stats.items()])
            confidence_text = f"avg_confidence: {avg_confidence:.3f}"
            
            # 绘制统计信息背景
            font = cv2.FONT_HERSHEY_SIMPLEX
            font_scale = 0.6
            thickness = 2
            
            # 计算文本尺寸
            texts = [summary_text, class_text, confidence_text]
            max_width = 0
            total_height = 0
            
            for text in texts:
                (text_width, text_height), baseline = cv2.getTextSize(
                    text, font, font_scale, thickness
                )
                max_width = max(max_width, text_width)
                total_height += text_height + baseline + 5
            
            # 绘制背景
            cv2.rectangle(image, (10, 10), (max_width + 20, total_height + 20),
                        (0, 0, 0), -1)  # 黑色背景
            cv2.rectangle(image, (10, 10), (max_width + 20, total_height + 20),
                        (255, 255, 255), 2)  # 白色边框
            
            # 绘制文本
            y_offset = 30
            for text in texts:
                cv2.putText(image, text, (15, y_offset),
                        font, font_scale, (255, 255, 255), thickness)
                y_offset += 25
            
        except Exception as e:
            if self.verbose:
                self._log(f"⚠️ 绘制统计摘要失败: {str(e)}")

    def extract_single_deleted_contours(self, deleted_item: dict, scale_masks: bool = False) -> bool:
        """
        从单个deleted字典中提取轮廓信息，直接使用其内部的sam_result
        
        Args:
            deleted_item: 单个检测结果字典，包含sam_result字段
            scale_masks: bool, 是否将掩码数组也缩放到原始图像尺寸
                - True: 缩放掩码到原始尺寸（消耗更多内存，但坐标一致）
                - False: 保持掩码为推理尺寸，只缩放坐标点（默认，效率更高）
            
        Returns:
            bool: 是否成功提取轮廓
            
        该方法会在deleted_item中添加以下字段：
            "轮廓列表": [轮廓信息列表]
            "轮廓总数": int
            "主要轮廓索引": int  
            "轮廓处理状态": str
            "原始完整掩码": np.ndarray
            "掩码形状": tuple
        """
        try:
            if not isinstance(deleted_item, dict):
                if self.verbose:
                    self._log("❌ 输入不是字典格式")
                return False
            
            # 获取基本信息
            class_name = deleted_item.get("类型字符", "Unknown")
            aoi_info = deleted_item.get("AOI位置", {})
            sam_result = deleted_item.get("sam_result", None)
            
            # 计算缩放信息
            scale_info = self._calculate_scale_info(deleted_item, sam_result)
            
            if self.verbose:
                self._log(f"🔍 提取单个检测项轮廓: {class_name}")
                if scale_info:
                    self._log(f"  📐 缩放信息: scale_x={scale_info['scale_x']:.3f}, scale_y={scale_info['scale_y']:.3f}")
                    self._log(f"     推理尺寸: {scale_info['inference_size']}, 原始尺寸: {scale_info['original_size']}")
                self._log(f"  🎯 掩码缩放模式: {'启用' if scale_masks else '禁用'}")
            
            # 初始化轮廓相关字段
            deleted_item["轮廓列表"] = []
            deleted_item["轮廓总数"] = 0
            deleted_item["主要轮廓索引"] = -1
            deleted_item["原始完整掩码"] = None
            deleted_item["掩码形状"] = (0, 0)
            
            # 检查sam_result
            if not sam_result:
                deleted_item["轮廓处理状态"] = "no_sam_result"
                if self.verbose:
                    self._log("  ❌ 无sam_result数据")
                return False
            
            if not sam_result.get("success", False):
                deleted_item["轮廓处理状态"] = "sam_failed"
                if self.verbose:
                    self._log(f"  ❌ SAM处理失败: {sam_result.get('error', '未知错误')}")
                return False
            
            mask_count = sam_result.get("mask_count", 0)
            if mask_count == 0:
                deleted_item["轮廓处理状态"] = "no_masks"
                if self.verbose:
                    self._log("  ❌ SAM结果中无掩码")
                return False
            
            if self.verbose:
                self._log(f"  📊 处理 {mask_count} 个SAM掩码")
            
            # 处理所有掩码并提取轮廓
            all_contours_info = []
            
            for mask_idx, mask_info in enumerate(sam_result["masks"]):
                try:
                    mask_array = mask_info["mask_array"]
                    
                    if self.verbose:
                        self._log(f"    处理掩码 {mask_idx}: 面积={mask_info.get('mask_area', 'N/A')}, 形状={mask_array.shape}")
                    
                    # 确保掩码是2D数组
                    if len(mask_array.shape) > 2:
                        # 如果是3D数组，取第一个通道
                        if mask_array.shape[0] == 1:
                            mask_array = mask_array[0]
                        else:
                            mask_array = mask_array[0] if mask_array.shape[2] == 1 else mask_array[:,:,0]
                    
                    # 确保掩码是二值化的
                    if mask_array.dtype in [np.float32, np.float64]:
                        binary_mask = (mask_array > 0.5).astype(np.uint8) * 255
                    else:
                        binary_mask = (mask_array > 0).astype(np.uint8) * 255
                    
                    # 检查掩码是否有效
                    if np.sum(binary_mask) == 0:
                        if self.verbose:
                            self._log(f"      ⚠️ 掩码 {mask_idx} 为空掩码，跳过")
                        continue
                    
                    # 从单个掩码提取轮廓
                    contours_info = self._extract_contours_from_mask(binary_mask, aoi_info, scale_info, scale_masks, deleted_item.get('image'))
                    
                    if contours_info and contours_info.get("contour_list"):
                        # 为每个轮廓添加来源掩码信息
                        for contour in contours_info["contour_list"]:
                            contour["来源掩码索引"] = mask_info.get("mask_index", mask_idx)
                            contour["来源掩码面积"] = mask_info.get("mask_area", 0)
                            contour["是否来自最大掩码"] = mask_info.get("is_largest", False)
                        
                        all_contours_info.extend(contours_info["contour_list"])
                        
                        if self.verbose:
                            self._log(f"      ✅ 提取到 {len(contours_info['contour_list'])} 个轮廓")
                    else:
                        if self.verbose:
                            self._log(f"      ⚠️ 掩码 {mask_idx} 未提取到轮廓 (掩码非零像素数: {np.sum(binary_mask > 0)})")
                            
                except Exception as mask_e:
                    if self.verbose:
                        self._log(f"      ❌ 处理掩码 {mask_idx} 时出错: {str(mask_e)}")
                        import traceback
                        self._log(f"        详细错误: {traceback.format_exc()}")
                    continue
            
            # 处理提取结果
            if all_contours_info:
                # 按轮廓面积排序
                all_contours_info.sort(key=lambda x: x["轮廓面积"], reverse=True)
                
                # 重新编号轮廓索引
                for idx, contour in enumerate(all_contours_info):
                    contour["轮廓索引"] = idx
                    contour["是否主要轮廓"] = (idx == 0)  # 面积最大的为主要轮廓
                
                # 更新deleted_item
                deleted_item["轮廓列表"] = all_contours_info
                deleted_item["轮廓总数"] = len(all_contours_info)
                deleted_item["主要轮廓索引"] = 0
                deleted_item["轮廓处理状态"] = "success"
                
                # 设置原始完整掩码（使用最大掩码）
                largest_mask_index = sam_result.get("largest_mask_index", 0)
                if largest_mask_index < len(sam_result["masks"]):
                    deleted_item["原始完整掩码"] = sam_result["masks"][largest_mask_index]["mask_array"]
                    deleted_item["掩码形状"] = deleted_item["原始完整掩码"].shape
                
                if self.verbose:
                    self._log(f"  ✅ 成功提取 {len(all_contours_info)} 个轮廓")
                    self._log(f"     主要轮廓面积: {all_contours_info[0]['轮廓面积']}")
                
                return True
            else:
                # 有掩码但未提取到轮廓
                deleted_item["轮廓处理状态"] = "no_contours_found"
                if self.verbose:
                    self._log(f"  ❌ 从 {mask_count} 个掩码中未提取到任何轮廓")
                return False
                
        except Exception as e:
            if self.verbose:
                self._log(f"❌ 提取单个deleted轮廓失败: {str(e)}")
            
            # 设置错误状态
            if isinstance(deleted_item, dict):
                deleted_item["轮廓列表"] = []
                deleted_item["轮廓总数"] = 0
                deleted_item["主要轮廓索引"] = -1
                deleted_item["轮廓处理状态"] = "error"
                deleted_item["原始完整掩码"] = None
                deleted_item["掩码形状"] = (0, 0)
            
            return False


    def _calculate_scale_info(self, deleted_item: dict, sam_result: dict) -> dict:
        """
        计算从SAM推理尺寸到原始图像尺寸的缩放信息（支持动态推理尺寸）
        
        Args:
            deleted_item: 检测结果字典，包含原始图像信息
            sam_result: SAM推理结果，包含推理参数和实际推理尺寸
            
        Returns:
            dict: 缩放信息字典，如果无法计算则返回None
        """
        try:
            # 获取原始图像尺寸
            original_image = deleted_item.get("image")
            if original_image is None:
                if self.verbose:
                    self._log("    ❌ 无法获取原始图像信息")
                return None
            
            # 从图像获取原始尺寸
            if hasattr(original_image, 'shape'):
                original_height, original_width = original_image.shape[:2]
            else:
                if self.verbose:
                    self._log("    ❌ 原始图像格式不支持")
                return None
            
            # 获取SAM推理尺寸
            inference_size = None
            inference_height, inference_width = None, None
            
            # 方法1：从sam_result中获取实际推理尺寸（优先，支持动态推理）
            if sam_result:
                # 获取实际掩码尺寸
                actual_size = sam_result.get("inference_actual_size")
                if actual_size:
                    inference_height, inference_width = actual_size
                    inference_size = (inference_height, inference_width)
                    
                    if self.verbose:
                        self._log(f"    📐 从sam_result获取实际推理尺寸: {inference_size}")
                
                # 备选：从sam_result中获取原始尺寸信息（用于验证）
                if not inference_size:
                    original_size_from_sam = sam_result.get("original_size")
                    if original_size_from_sam:
                        if self.verbose:
                            self._log(f"    📐 sam_result中的原始尺寸: {original_size_from_sam}")
            
            # 方法2：从第一个掩码获取推理尺寸（兼容性）
            if not inference_size and sam_result and "masks" in sam_result and sam_result["masks"]:
                first_mask = sam_result["masks"][0]
                mask_array = first_mask.get("mask_array")
                if mask_array is not None and hasattr(mask_array, 'shape'):
                    if len(mask_array.shape) >= 2:
                        inference_height, inference_width = mask_array.shape[:2]
                        inference_size = (inference_height, inference_width)
                        
                        if self.verbose:
                            self._log(f"    📐 从掩码数组获取推理尺寸: {inference_size}")
            
            # 方法3：使用默认推理尺寸（最后的备选）
            if not inference_size:
                # 使用通用的正方形推理尺寸
                inference_size = (512, 512)
                inference_height, inference_width = inference_size
                
                if self.verbose:
                    self._log(f"    ⚠️ 无法获取实际推理尺寸，使用默认: {inference_size}")
            
            # 计算缩放比例 - 注意坐标对应关系
            # X轴对应宽度(width)，Y轴对应高度(height)
            scale_x = original_width / inference_width
            scale_y = original_height / inference_height
            
            scale_info = {
                "scale_x": scale_x,
                "scale_y": scale_y,
                "inference_size": inference_size,
                "original_size": (original_height, original_width),
                "inference_width": inference_width,
                "inference_height": inference_height,
                "original_width": original_width,
                "original_height": original_height,
                # 新增：动态推理相关信息
                "is_dynamic_inference": True,
                "inference_method": "dynamic" if sam_result and sam_result.get("inference_actual_size") else "fallback"
            }
            
            if self.verbose:
                self._log(f"    ✅ 计算缩放信息成功 (动态推理):")
                self._log(f"       原始尺寸: {original_width}x{original_height}")
                self._log(f"       推理尺寸: {inference_width}x{inference_height}")
                self._log(f"       缩放比例: X={scale_x:.3f}, Y={scale_y:.3f}")
                self._log(f"       推理方法: {scale_info['inference_method']}")
            
            return scale_info
            
        except Exception as e:
            if self.verbose:
                self._log(f"    ❌ 计算缩放信息失败: {str(e)}")
            return None


    def _extract_contours_from_mask(self, seg_mask: np.ndarray, aoi_info: dict, scale_info: dict = None, scale_masks: bool = False, original_image: np.ndarray = None) -> dict:
        """
        从分割掩码中提取轮廓信息
        
        Args:
            seg_mask: 分割掩码数组
            aoi_info: AOI位置信息
            scale_info: 缩放信息字典，包含从推理尺寸到原始图像的缩放参数
                {
                    "scale_x": float,           # x方向缩放因子
                    "scale_y": float,           # y方向缩放因子
                    "inference_size": tuple,    # SAM推理尺寸
                    "original_size": tuple      # 原始图像尺寸
                }
            scale_masks: bool, 是否将掩码数组也缩放到原始图像尺寸
                - True: 缩放掩码到原始尺寸（消耗更多内存，但坐标一致）
                - False: 保持掩码为推理尺寸，只缩放坐标点（默认，效率更高）
            original_image: 原始图像数据，用于计算轮廓内像素的RGB统计信息
            
        Returns:
            dict: 轮廓信息字典，包含轮廓列表和统计信息
        """
        import cv2
        import numpy as np
        
        try:
            # 确保掩码是numpy数组且是二值化的uint8格式
            if not isinstance(seg_mask, np.ndarray):
                seg_mask = np.array(seg_mask)
            
            # 如果输入已经是处理过的二值掩码，直接使用
            if seg_mask.dtype == np.uint8 and np.all((seg_mask == 0) | (seg_mask == 255)):
                binary_mask = seg_mask
            else:
                # 处理其他格式的掩码
                if seg_mask.dtype in [np.float32, np.float64]:
                    if np.max(seg_mask) <= 1.0:
                        binary_mask = (seg_mask > 0.5).astype(np.uint8) * 255
                    else:
                        binary_mask = (seg_mask > 127).astype(np.uint8) * 255
                elif seg_mask.dtype == np.uint8:
                    binary_mask = (seg_mask > 127).astype(np.uint8) * 255
                else:
                    binary_mask = seg_mask.astype(np.uint8)
                    binary_mask = (binary_mask > 127) * 255
            
            if self.verbose:
                mask_pixels = np.sum(binary_mask > 0)
                self._log(f"        掩码处理: 形状={binary_mask.shape}, 非零像素={mask_pixels}")
            
            # 查找轮廓
            contours, _ = cv2.findContours(binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
            
            if not contours:
                if self.verbose:
                    self._log(f"        未找到任何轮廓")
                return None
            
            if self.verbose:
                self._log(f"        找到 {len(contours)} 个轮廓")
            
            # 获取AOI位置
            aoi_x1 = aoi_info.get("x1", 0)
            aoi_y1 = aoi_info.get("y1", 0)
            
            contour_list = []
            contour_areas = []
            
            for idx, contour in enumerate(contours):
                try:
                    # 计算轮廓基本属性
                    area = cv2.contourArea(contour)
                    perimeter = cv2.arcLength(contour, True)
                    
                    # 跳过过小的轮廓
                    if area < 5:  # 面积小于5像素的轮廓忽略
                        if self.verbose:
                            self._log(f"          跳过过小轮廓 {idx}: 面积={area}")
                        continue
                    
                    contour_areas.append(area)
                    
                    # 计算边界框
                    x, y, w, h = cv2.boundingRect(contour)
                    
                    # 计算轮廓中心（质心）
                    M = cv2.moments(contour)
                    if M["m00"] != 0:
                        cx = int(M["m10"] / M["m00"])
                        cy = int(M["m01"] / M["m00"])
                    else:
                        cx, cy = x + w//2, y + h//2
                    
                    # 计算轮廓特征
                    # 凸包
                    hull = cv2.convexHull(contour)
                    hull_area = cv2.contourArea(hull)
                    convexity = area / hull_area if hull_area > 0 else 0
                    
                    # 圆形度
                    circularity = (4 * np.pi * area) / (perimeter * perimeter) if perimeter > 0 else 0
                    
                    # 矩形度
                    rect_area = w * h
                    rectangularity = area / rect_area if rect_area > 0 else 0
                    
                    # 椭圆拟合（需要至少5个点）
                    eccentricity = 0
                    if len(contour) >= 5:
                        try:
                            ellipse = cv2.fitEllipse(contour)
                            axes = ellipse[1]  # (长轴, 短轴)
                            if axes[0] > 0 and axes[1] > 0:
                                # 计算离心率
                                a = max(axes) / 2  # 半长轴
                                b = min(axes) / 2  # 半短轴
                                eccentricity = np.sqrt(1 - (b*b)/(a*a)) if a > 0 else 0
                        except:
                            eccentricity = 0
                    
                    # 应用坐标缩放（如果提供了缩放信息）
                    if scale_info:
                        scale_x = scale_info.get("scale_x", 1.0)
                        scale_y = scale_info.get("scale_y", 1.0)
                        
                        # 缩放轮廓坐标
                        x = int(x * scale_x)
                        y = int(y * scale_y)
                        w = int(w * scale_x)
                        h = int(h * scale_y)
                        cx = int(cx * scale_x)
                        cy = int(cy * scale_y)
                        
                        # 缩放面积和周长
                        area = int(area * scale_x * scale_y)
                        perimeter = perimeter * max(scale_x, scale_y)  # 使用较大的缩放因子近似
                        
                        if self.verbose:
                            self._log(f"          应用坐标缩放: scale_x={scale_x:.3f}, scale_y={scale_y:.3f}")
                    
                    # 紧密度
                    compactness = area / hull_area if hull_area > 0 else 0
                    
                    # 计算轮廓内像素的RGB统计信息
                    rgb_stats = {}
                    if original_image is not None and len(original_image.shape) == 3:
                        try:
                            # 创建单独的轮廓掩码（推理尺寸）
                            single_contour_mask = np.zeros(binary_mask.shape, dtype=np.uint8)
                            cv2.fillPoly(single_contour_mask, [contour], 255)
                            
                            # original_image就是AOI区域的图像，直接将轮廓掩码缩放到这个尺寸
                            roi_height, roi_width = original_image.shape[:2]
                            roi_mask = cv2.resize(single_contour_mask, (roi_width, roi_height), interpolation=cv2.INTER_NEAREST)
                            
                            # 获取掩码区域内的像素
                            mask_pixels = roi_mask > 0
                            if np.any(mask_pixels):
                                # 提取掩码区域内的RGB像素值
                                masked_pixels = original_image[mask_pixels]  # shape: (n_pixels, 3)
                                
                                if len(masked_pixels) > 0:
                                    # 计算RGB各通道的均值和中值
                                    rgb_mean = np.mean(masked_pixels, axis=0)
                                    rgb_median = np.median(masked_pixels, axis=0)
                                    
                                    rgb_stats = {
                                        "RGB均值": {
                                            "R": round(float(rgb_mean[2]), 2),  # OpenCV是BGR格式
                                            "G": round(float(rgb_mean[1]), 2),
                                            "B": round(float(rgb_mean[0]), 2)
                                        },
                                        "RGB中值": {
                                            "R": round(float(rgb_median[2]), 2),
                                            "G": round(float(rgb_median[1]), 2),
                                            "B": round(float(rgb_median[0]), 2)
                                        },
                                        "像素数量": int(len(masked_pixels))
                                    }
                                    
                                    if self.verbose:
                                        self._log(f"          轮廓 {idx} RGB统计: 像素数={len(masked_pixels)}, RGB均值=({rgb_mean[2]:.1f},{rgb_mean[1]:.1f},{rgb_mean[0]:.1f})")
                                else:
                                    rgb_stats = {"RGB均值": {"R": 0, "G": 0, "B": 0}, "RGB中值": {"R": 0, "G": 0, "B": 0}, "像素数量": 0}
                                    if self.verbose:
                                        self._log(f"          轮廓 {idx} RGB统计: 掩码区域无有效像素")
                            else:
                                rgb_stats = {"RGB均值": {"R": 0, "G": 0, "B": 0}, "RGB中值": {"R": 0, "G": 0, "B": 0}, "像素数量": 0}
                                if self.verbose:
                                    self._log(f"          轮廓 {idx} RGB统计: 掩码为空")
                                
                        except Exception as rgb_e:
                            if self.verbose:
                                self._log(f"          ⚠️ 计算轮廓 {idx} RGB统计时出错: {str(rgb_e)}")
                            rgb_stats = {"RGB均值": {"R": 0, "G": 0, "B": 0}, "RGB中值": {"R": 0, "G": 0, "B": 0}, "像素数量": 0}
                    else:
                        rgb_stats = {"RGB均值": {"R": 0, "G": 0, "B": 0}, "RGB中值": {"R": 0, "G": 0, "B": 0}, "像素数量": 0}
                        if self.verbose and original_image is not None:
                            self._log(f"          轮廓 {idx} RGB统计: 原始图像不是3通道彩色图像")
                    
                    # 构建轮廓信息
                    contour_info = {
                        "轮廓索引": len(contour_list),
                        "轮廓面积": int(area),
                        "轮廓长度": round(perimeter, 2),
                        "边界框": {
                            "x": x, "y": y,
                            "width": w, "height": h
                        },
                        "绝对边界框": {
                            "x1": aoi_x1 + x,
                            "y1": aoi_y1 + y,
                            "x2": aoi_x1 + x + w,
                            "y2": aoi_y1 + y + h
                        },
                        "轮廓中心": {
                            "x": cx, "y": cy
                        },
                        "绝对中心": {
                            "x": aoi_x1 + cx,
                            "y": aoi_y1 + cy
                        },
                        "轮廓特征": {
                            "凸性": round(convexity, 4),
                            "圆形度": round(circularity, 4),
                            "矩形度": round(rectangularity, 4),
                            "离心率": round(eccentricity, 4),
                            "紧密度": round(compactness, 4),
                            "RGB均值": rgb_stats.get("RGB均值", {"R": 0, "G": 0, "B": 0}),
                            "RGB中值": rgb_stats.get("RGB中值", {"R": 0, "G": 0, "B": 0}),
                            "像素数量": rgb_stats.get("像素数量", 0),
                        },
                        "轮廓点数": len(contour),
                        "是否主要轮廓": False,  # 稍后设置
                        "filter": False,  # 新增：绘制过滤标志，默认为false，只有为true时才绘制
                        "轮廓掩码数据": {
                            "单独轮廓掩码": None,     # 只包含该轮廓的掩码
                            "轮廓边界掩码": None,     # 轮廓边界线掩码
                            "轮廓点坐标": None,       # 将在下面设置缩放后的坐标
                            "轮廓凸包点": None,      # 凸包点坐标
                            "掩码形状": binary_mask.shape,  # 掩码整体形状
                            "原始轮廓点": contour.reshape(-1, 2).tolist(),  # 原始推理尺寸的点坐标
                        }
                    }
                    
                    contour_list.append(contour_info)
                    
                    if self.verbose:
                        self._log(f"          轮廓 {idx}: 面积={int(area)}, 周长={perimeter:.2f}, 点数={len(contour)}")
                    
                except Exception as contour_e:
                    if self.verbose:
                        self._log(f"    ⚠️ 处理轮廓 {idx} 时出错: {str(contour_e)}")
                    continue
            
            if not contour_list:
                return None
            
            # 为每个轮廓生成单独的掩码数据
            for i, contour_info in enumerate(contour_list):
                try:
                    # 获取对应的轮廓
                    contour = contours[i]
                    
                    # 根据scale_masks参数决定掩码处理方式
                    if scale_info and scale_masks:
                        # 缩放掩码模式：将掩码也缩放到原始图像尺寸
                        scale_x = scale_info.get("scale_x", 1.0)
                        scale_y = scale_info.get("scale_y", 1.0)
                        original_size = scale_info.get("original_size", binary_mask.shape[:2])
                        
                        # 计算目标尺寸
                        target_height, target_width = original_size
                        
                        # 重新计算当前轮廓的凸包
                        current_hull = cv2.convexHull(contour)
                        
                        # 缩放轮廓坐标到原始尺寸
                        # 注意：轮廓点的格式是 [[x, y], [x, y], ...]，所以缩放顺序是 [scale_x, scale_y]
                        scaled_contour = (contour * np.array([scale_x, scale_y])).astype(np.int32)
                        scaled_hull = (current_hull * np.array([scale_x, scale_y])).astype(np.int32)
                        
                        # 创建原始尺寸的掩码
                        single_contour_mask = np.zeros((target_height, target_width), dtype=np.uint8)
                        cv2.fillPoly(single_contour_mask, [scaled_contour], 255)
                        
                        contour_boundary_mask = np.zeros((target_height, target_width), dtype=np.uint8)
                        cv2.drawContours(contour_boundary_mask, [scaled_contour], -1, 255, thickness=int(max(scale_x, scale_y)))
                        
                        # 坐标点也使用缩放后的
                        contour_info["轮廓掩码数据"]["轮廓点坐标"] = scaled_contour.reshape(-1, 2).tolist()
                        contour_info["轮廓掩码数据"]["轮廓凸包点"] = scaled_hull.reshape(-1, 2).tolist()
                        contour_info["轮廓掩码数据"]["掩码坐标系"] = "original_size"
                        
                        if self.verbose:
                            self._log(f"    轮廓 {i}: 掩码已缩放到原始尺寸 {target_width}x{target_height}")
                            self._log(f"         缩放因子: X={scale_x:.3f}, Y={scale_y:.3f}")
                        
                    else:
                        # 标准模式：掩码保持推理尺寸，只缩放坐标点
                        # 创建单独轮廓的掩码（推理尺寸）
                        single_contour_mask = np.zeros_like(binary_mask, dtype=np.uint8)
                        cv2.fillPoly(single_contour_mask, [contour], 255)
                        
                        # 创建轮廓边界线掩码（推理尺寸）
                        contour_boundary_mask = np.zeros_like(binary_mask, dtype=np.uint8)
                        cv2.drawContours(contour_boundary_mask, [contour], -1, 255, thickness=1)
                        
                        # 重新计算当前轮廓的凸包
                        current_hull = cv2.convexHull(contour)
                        
                        # 处理坐标点
                        contour_points = contour.reshape(-1, 2).tolist()
                        hull_points = current_hull.reshape(-1, 2).tolist()
                        
                        if scale_info:
                            scale_x = scale_info.get("scale_x", 1.0)
                            scale_y = scale_info.get("scale_y", 1.0)
                            original_size = scale_info.get("original_size", binary_mask.shape)
                            
                            # 只缩放坐标点，掩码保持推理尺寸
                            # 注意坐标点的顺序：[x, y] -> [x*scale_x, y*scale_y]
                            scaled_contour_points = [[int(pt[0] * scale_x), int(pt[1] * scale_y)] for pt in contour_points]
                            scaled_hull_points = [[int(pt[0] * scale_x), int(pt[1] * scale_y)] for pt in hull_points]
                            
                            contour_info["轮廓掩码数据"]["轮廓点坐标"] = scaled_contour_points
                            contour_info["轮廓掩码数据"]["轮廓凸包点"] = scaled_hull_points
                            contour_info["轮廓掩码数据"]["掩码坐标系"] = "original_size"  # 修正：坐标已缩放到原始尺寸
                            
                            if self.verbose:
                                self._log(f"    轮廓 {i}: 坐标已缩放，掩码保持推理尺寸")
                                self._log(f"         缩放因子: X={scale_x:.3f}, Y={scale_y:.3f}")
                        else:
                            # 无缩放信息
                            contour_info["轮廓掩码数据"]["轮廓点坐标"] = contour_points
                            contour_info["轮廓掩码数据"]["轮廓凸包点"] = hull_points
                            contour_info["轮廓掩码数据"]["掩码坐标系"] = "original_size"
                    
                    # 通用的缩放信息保存
                    if scale_info:
                        contour_info["轮廓掩码数据"]["缩放信息"] = {
                            "scale_x": scale_info.get("scale_x", 1.0),
                            "scale_y": scale_info.get("scale_y", 1.0),
                            "inference_size": scale_info.get("inference_size", binary_mask.shape),
                            "original_size": scale_info.get("original_size", binary_mask.shape),
                            "masks_scaled": scale_masks  # 标记掩码是否被缩放
                        }
                    else:
                        contour_info["轮廓掩码数据"]["缩放信息"] = None
                    
                    # 更新轮廓掩码数据
                    contour_info["轮廓掩码数据"]["单独轮廓掩码"] = single_contour_mask
                    contour_info["轮廓掩码数据"]["轮廓边界掩码"] = contour_boundary_mask
                    
                    if self.verbose:
                        mask_pixels = np.sum(single_contour_mask > 0)
                        boundary_pixels = np.sum(contour_boundary_mask > 0)
                        self._log(f"    轮廓 {i}: 掩码像素={mask_pixels}, 边界像素={boundary_pixels}")
                    
                except Exception as mask_e:
                    if self.verbose:
                        self._log(f"    ⚠️ 生成轮廓 {i} 掩码数据时出错: {str(mask_e)}")
                    # 设置为空掩码
                    contour_info["轮廓掩码数据"]["单独轮廓掩码"] = np.zeros_like(binary_mask, dtype=np.uint8)
                    contour_info["轮廓掩码数据"]["轮廓边界掩码"] = np.zeros_like(binary_mask, dtype=np.uint8)
                    contour_info["轮廓掩码数据"]["轮廓凸包点"] = []
            
            # 确定主要轮廓（面积最大的轮廓）
            if contour_areas:
                main_contour_idx = np.argmax(contour_areas)
                if main_contour_idx < len(contour_list):
                    contour_list[main_contour_idx]["是否主要轮廓"] = True
            else:
                main_contour_idx = -1
            
            return {
                "contour_list": contour_list,
                "total_count": len(contour_list),
                "main_contour_index": main_contour_idx,
                "原始掩码": binary_mask,  # 添加原始完整掩码
                "掩码形状": binary_mask.shape
            }
            
        except Exception as e:
            if self.verbose:
                self._log(f"⚠️ 从掩码提取轮廓失败: {str(e)}")
            return None
    

    def draw_deleted_mask(self, image: np.ndarray, deleted_item: dict, draw_type: str = "contour", mask_alpha: float = 0.3) -> None:
        """
        在图像上绘制检测项的轮廓掩码 (直接修改输入图像)
        
        Args:
            image: 输入图像 (BGR格式) - 将被直接修改
            deleted_item: 检测结果字典，包含轮廓信息
            draw_type: 绘制类型，可选值：
                - "contour": 绘制轮廓线条（默认）
                - "mask": 绘制半透明填充掩码
                - "hull": 绘制凸包
                - "boundary": 绘制边界线
                - "points": 绘制轮廓点
                - "combined": 组合绘制（轮廓+凸包+中心点）
                - "all": 绘制所有类型
            mask_alpha: 掩码透明度，范围0-1（仅在draw_type为"mask"时使用）
        
        Returns:
            None: 直接修改输入图像，无返回值
        """
        try:
            if not isinstance(deleted_item, dict):
                if self.verbose:
                    self._log("❌ deleted_item不是字典格式")
                return
            
            # 检查是否有轮廓数据
            if deleted_item.get("轮廓处理状态") != "success":
                if self.verbose:
                    self._log(f"⚠️ 轮廓处理状态异常: {deleted_item.get('轮廓处理状态', 'unknown')}")
                return
            
            contour_list = deleted_item.get("轮廓列表", [])
            if not contour_list:
                if self.verbose:
                    self._log("⚠️ 轮廓列表为空")
                return
            
            # 直接在原图像上绘制，提高效率
            # 获取基本信息
            class_name = deleted_item.get("类型字符", "Unknown")
            class_id = deleted_item.get("类型编号", 0)
            
            if self.verbose:
                self._log(f"🎨 绘制轮廓: {class_name}, 类型: {draw_type}, 轮廓数: {len(contour_list)}")
            
            # 定义颜色方案
            colors = self._get_draw_colors(class_id)
            
            # 根据绘制类型选择绘制方法，直接修改原图像
            if draw_type == "contour":
                self._draw_contour_lines(image, contour_list, colors)
            elif draw_type == "mask":
                self._draw_filled_masks(image, contour_list, colors, mask_alpha)
            elif draw_type == "hull":
                self._draw_convex_hulls(image, contour_list, colors)
            elif draw_type == "boundary":
                self._draw_boundaries(image, contour_list, colors)
            elif draw_type == "points":
                self._draw_contour_points(image, contour_list, colors)
            elif draw_type == "combined":
                self._draw_combined(image, contour_list, colors, mask_alpha * 0.7)  # 组合模式使用较低透明度
            elif draw_type == "all":
                self._draw_all_types(image, contour_list, colors, mask_alpha * 0.7)  # 全部模式使用较低透明度
            else:
                if self.verbose:
                    self._log(f"⚠️ 未知的绘制类型: {draw_type}，使用默认轮廓线条")
                self._draw_contour_lines(image, contour_list, colors)
            
            # 添加标签信息
            self._add_contour_labels(image, deleted_item, colors)
            
            if self.verbose:
                self._log(f"✅ 轮廓绘制完成: {class_name}")
            
        except Exception as e:
            if self.verbose:
                self._log(f"❌ 绘制轮廓失败: {str(e)}")
            return
    
    def _get_draw_colors(self, class_id: int) -> dict:
        """获取绘制颜色方案"""
        # 预定义类别颜色 (BGR格式)
        class_colors = {
            0: (0, 255, 0),     # damianNG - 绿色
            1: (255, 0, 0),     # quekou1 - 蓝色
            2: (0, 0, 255),     # posun - 红色
            3: (255, 255, 0),   # quekou3 - 青色
            4: (255, 0, 255),   # sekuai - 洋红色
            5: (0, 255, 255),   # yaying - 黄色
        }
        
        base_color = class_colors.get(class_id, (128, 128, 128))  # 默认灰色
        
        return {
            'contour': base_color,                          # 轮廓线颜色
            'mask': (*base_color, 100),                     # 填充掩码颜色（带透明度）
            'hull': tuple(min(255, c + 50) for c in base_color),  # 凸包颜色（更亮）
            'boundary': tuple(max(0, c - 50) for c in base_color), # 边界颜色（更暗）
            'points': (255, 255, 255),                      # 点颜色（白色）
            'center': (0, 0, 255),                          # 中心点颜色（红色）
            'text': (255, 255, 255),                        # 文字颜色（白色）
            'background': (0, 0, 0)                         # 背景颜色（黑色）
        }
    
    def _get_contour_color(self, base_color: tuple, contour_index: int, total_contours: int) -> tuple:
        """
        为单个轮廓生成色移颜色
        
        Args:
            base_color: 基础颜色 (B, G, R)
            contour_index: 轮廓索引
            total_contours: 总轮廓数
            
        Returns:
            tuple: 色移后的颜色 (B, G, R)
        """
        if total_contours <= 1:
            return base_color
            
        # 计算色移参数
        shift_ratio = contour_index / max(1, total_contours - 1)  # 0到1之间
        
        # 使用HSV色彩空间进行色移
        import cv2
        import numpy as np
        
        # 将BGR转换为HSV
        bgr_array = np.uint8([[base_color]])
        hsv_array = cv2.cvtColor(bgr_array, cv2.COLOR_BGR2HSV)
        h, s, v = hsv_array[0, 0]
        
        # 色相偏移：每个轮廓在色轮上分散
        hue_shift = int(shift_ratio * 60)  # 最大偏移60度
        new_h = (h + hue_shift) % 180
        
        # 饱和度和亮度微调，增加区分度（减小色移范围，避免越界）
        sat_shift = int(shift_ratio * 30 - 15)  # -15到+15，减小范围
        val_shift = int(shift_ratio * 30 - 15)  # -15到+15，减小范围
        
        # 确保饱和度和亮度在有效范围内
        new_s = max(30, min(255, int(s) + sat_shift))  # 保持最低饱和度30
        new_v = max(80, min(255, int(v) + val_shift))  # 保持最低亮度80
        
        # 转换回BGR
        new_hsv = np.uint8([[[new_h, new_s, new_v]]])
        new_bgr = cv2.cvtColor(new_hsv, cv2.COLOR_HSV2BGR)
        
        # 确保所有颜色值都在0-255范围内
        result_color = tuple(max(0, min(255, int(x))) for x in new_bgr[0, 0])
        
        return result_color
    
    def _get_aoi_offset(self, contour_info: dict) -> dict:
        """
        获取AOI偏移信息
        
        Args:
            contour_info: 轮廓信息字典
            
        Returns:
            dict: 包含x_offset, y_offset的偏移信息
        """
        # 从轮廓信息中获取绝对边界框信息
        abs_bbox = contour_info.get("绝对边界框", {})
        if abs_bbox:
            # 如果有绝对边界框，使用其x1, y1作为偏移
            return {
                'x_offset': abs_bbox.get("x1", 0),
                'y_offset': abs_bbox.get("y1", 0)
            }
        else:
            # 如果没有绝对边界框，返回0偏移
            return {'x_offset': 0, 'y_offset': 0}

    def _draw_contour_lines(self, image: np.ndarray, contour_list: list, colors: dict):
        """绘制轮廓线条，直接修改输入图像，每个轮廓使用不同的色移颜色"""
        base_color = colors['contour']
        total_contours = len(contour_list)
        
        for contour_index, contour_info in enumerate(contour_list):
            # 检查过滤标志，只绘制filter为true的轮廓
            if not contour_info.get("filter", False):
                if self.verbose:
                    self._log(f"      轮廓 {contour_index}: 跳过绘制 (filter=false)")
                continue
                
            contour_data = contour_info.get("轮廓掩码数据", {})
            contour_points = contour_data.get("轮廓点坐标", [])
            
            if contour_points and len(contour_points) > 0:
                try:
                    # 转换为numpy数组
                    points = np.array(contour_points, dtype=np.int32)
                    
                    # 注意：如果轮廓坐标已经是绝对坐标（经过缩放），则不需要再加AOI偏移
                    # 检查轮廓掩码数据中的坐标系标记
                    coord_system = contour_data.get("掩码坐标系", "original_size")
                    
                    if coord_system == "inference_size":
                        # 如果是推理尺寸的坐标，需要加上AOI偏移
                        aoi_info = self._get_aoi_offset(contour_info)
                        points[:, 0] += aoi_info['x_offset']
                        points[:, 1] += aoi_info['y_offset']
                    
                    # 确保点的格式正确
                    if len(points.shape) == 2 and points.shape[1] == 2:
                        # 重塑为轮廓格式
                        contour = points.reshape((-1, 1, 2))
                        
                        # 为每个轮廓生成色移颜色
                        contour_color = self._get_contour_color(base_color, contour_index, total_contours)
                        
                        # 额外的颜色值安全检查
                        safe_color = tuple(max(0, min(255, int(c))) for c in contour_color)
                        
                        # 绘制轮廓
                        is_main = contour_info.get("是否主要轮廓", False)
                        thickness = 3 if is_main else 2
                        cv2.drawContours(image, [contour], -1, safe_color, thickness)
                        
                        if self.verbose:
                            self._log(f"      绘制轮廓线条: {len(points)} 个点, 颜色: {safe_color}, 厚度: {thickness}")
                    
                except Exception as e:
                    if self.verbose:
                        self._log(f"      ⚠️ 绘制轮廓线条失败: {str(e)}")
                    continue
    
    def _draw_filled_masks(self, image: np.ndarray, contour_list: list, colors: dict, alpha: float = 0.3):
        """绘制半透明填充掩码，直接修改输入图像，每个轮廓使用不同的色移颜色"""
        if not contour_list:
            return
            
        base_color = colors['mask'][:3] if len(colors['mask']) > 3 else colors['contour']
        total_contours = len(contour_list)
        
        if self.verbose:
            self._log(f"      开始绘制 {total_contours} 个填充掩码，透明度: {alpha}")
        
        # 为每个轮廓单独进行透明混合，避免重复叠加
        for contour_index, contour_info in enumerate(contour_list):
            # 检查过滤标志，只绘制filter为true的轮廓
            if not contour_info.get("filter", False):
                if self.verbose:
                    self._log(f"        轮廓 {contour_index}: 跳过绘制 (filter=false)")
                continue
                
            contour_data = contour_info.get("轮廓掩码数据", {})
            
            # 为每个轮廓生成色移颜色
            contour_color = self._get_contour_color(base_color, contour_index, total_contours)
            safe_color = tuple(max(0, min(255, int(c))) for c in contour_color)
            
            # 优先使用轮廓点坐标
            contour_points = contour_data.get("轮廓点坐标", [])
            
            if contour_points and len(contour_points) > 0:
                try:
                    points = np.array(contour_points, dtype=np.int32)
                    
                    # 检查坐标系并调整
                    coord_system = contour_data.get("掩码坐标系", "original_size")
                    if coord_system == "inference_size":
                        aoi_info = self._get_aoi_offset(contour_info)
                        points[:, 0] += aoi_info['x_offset']
                        points[:, 1] += aoi_info['y_offset']
                    
                    # 创建单独的掩码图像，只包含当前轮廓
                    mask = np.zeros(image.shape[:2], dtype=np.uint8)
                    contour = points.reshape((-1, 1, 2))
                    cv2.fillPoly(mask, [contour], 255)
                    
                    # 只在掩码区域应用颜色和透明度
                    mask_bool = mask > 0
                    if np.any(mask_bool):
                        # 直接在原图像上混合
                        for c in range(3):
                            image[mask_bool, c] = (
                                image[mask_bool, c] * (1 - alpha) + 
                                safe_color[c] * alpha
                            ).astype(np.uint8)
                    
                    if self.verbose:
                        pixels_drawn = np.sum(mask_bool)
                        self._log(f"        轮廓 {contour_index}: 绘制 {pixels_drawn} 个像素，颜色 {safe_color}")
                        
                except Exception as e:
                    if self.verbose:
                        self._log(f"        ⚠️ 绘制轮廓 {contour_index} 失败: {str(e)}")
                    continue
            
            # 备选方案：使用单独轮廓掩码
            else:
                single_mask = contour_data.get("单独轮廓掩码")
                if single_mask is not None:
                    try:
                        # 检查掩码的坐标系，以便正确处理位置
                        coord_system = contour_data.get("掩码坐标系", "original_size")
                        scale_info = contour_data.get("缩放信息")
                        
                        if coord_system == "inference_size" and scale_info:
                            # 掩码是推理尺寸，需要缩放到原始尺寸
                            scale_x = scale_info.get("scale_x", 1.0)
                            scale_y = scale_info.get("scale_y", 1.0)
                            
                            # 缩放掩码到原始尺寸
                            original_h = int(single_mask.shape[0] * scale_y)
                            original_w = int(single_mask.shape[1] * scale_x)
                            scaled_mask = cv2.resize(single_mask, (original_w, original_h))
                            
                            # 获取AOI偏移（这个应该已经是原始尺寸的）
                            aoi_info = self._get_aoi_offset(contour_info)
                            x_offset, y_offset = aoi_info['x_offset'], aoi_info['y_offset']
                            
                            # 计算在原始图像上的位置
                            mask_h, mask_w = scaled_mask.shape
                            y1, y2 = y_offset, y_offset + mask_h
                            x1, x2 = x_offset, x_offset + mask_w
                            
                            # 确保在图像边界内
                            y1, y2 = max(0, y1), min(image.shape[0], y2)
                            x1, x2 = max(0, x1), min(image.shape[1], x2)
                            
                            if y2 > y1 and x2 > x1:
                                # 调整掩码大小以匹配实际绘制区域
                                target_h, target_w = y2 - y1, x2 - x1
                                if target_h != mask_h or target_w != mask_w:
                                    final_mask = cv2.resize(scaled_mask, (target_w, target_h))
                                else:
                                    final_mask = scaled_mask
                                    
                                mask_bool = final_mask > 0
                                if np.any(mask_bool):
                                    # 直接在原图像上混合
                                    region = image[y1:y2, x1:x2]
                                    for c in range(3):
                                        region[mask_bool, c] = (
                                            region[mask_bool, c] * (1 - alpha) + 
                                            safe_color[c] * alpha
                                        ).astype(np.uint8)
                                
                                if self.verbose:
                                    pixels_drawn = np.sum(mask_bool)
                                    self._log(f"        轮廓 {contour_index}: 绘制 {pixels_drawn} 个像素 (缩放掩码)，颜色 {safe_color}")
                        else:
                            # 掩码已经是原始尺寸，直接使用
                            aoi_info = self._get_aoi_offset(contour_info)
                            x_offset, y_offset = aoi_info['x_offset'], aoi_info['y_offset']
                            
                            mask_h, mask_w = single_mask.shape
                            y1, y2 = y_offset, y_offset + mask_h
                            x1, x2 = x_offset, x_offset + mask_w
                            
                            # 确保在图像边界内
                            y1, y2 = max(0, y1), min(image.shape[0], y2)
                            x1, x2 = max(0, x1), min(image.shape[1], x2)
                            
                            if y2 > y1 and x2 > x1:
                                target_h, target_w = y2 - y1, x2 - x1
                                if target_h != mask_h or target_w != mask_w:
                                    resized_mask = cv2.resize(single_mask, (target_w, target_h))
                                else:
                                    resized_mask = single_mask
                                
                                mask_bool = resized_mask > 0
                                if np.any(mask_bool):
                                    # 直接在原图像上混合
                                    region = image[y1:y2, x1:x2]
                                    for c in range(3):
                                        region[mask_bool, c] = (
                                            region[mask_bool, c] * (1 - alpha) + 
                                            safe_color[c] * alpha
                                        ).astype(np.uint8)
                                
                                if self.verbose:
                                    pixels_drawn = np.sum(mask_bool)
                                    self._log(f"        轮廓 {contour_index}: 绘制 {pixels_drawn} 个像素 (原始掩码)，颜色 {safe_color}")
                                
                    except Exception as e:
                        if self.verbose:
                            self._log(f"        ⚠️ 绘制轮廓掩码 {contour_index} 失败: {str(e)}")
                        continue
        
        if self.verbose:
            self._log(f"      ✅ 完成所有填充掩码绘制")
    
    def _draw_convex_hulls(self, image: np.ndarray, contour_list: list, colors: dict):
        """绘制凸包，直接修改输入图像，每个轮廓使用不同的色移颜色"""
        base_color = colors['hull']
        total_contours = len(contour_list)
        
        for contour_index, contour_info in enumerate(contour_list):
            # 检查过滤标志，只绘制filter为true的轮廓
            if not contour_info.get("filter", False):
                continue
                
            hull_data = contour_info.get("轮廓掩码数据", {})
            hull_points = hull_data.get("轮廓凸包点", [])
            
            if hull_points:
                # 转换为numpy数组并调整坐标
                points = np.array(hull_points, dtype=np.int32)
                aoi_info = self._get_aoi_offset(contour_info)
                points[:, 0] += aoi_info['x_offset']
                points[:, 1] += aoi_info['y_offset']
                
                # 为每个轮廓生成色移颜色
                contour_color = self._get_contour_color(base_color, contour_index, total_contours)
                
                # 颜色值安全检查
                safe_color = tuple(max(0, min(255, int(c))) for c in contour_color)
                
                # 绘制凸包
                is_main = contour_info.get("是否主要轮廓", False)
                thickness = 2 if is_main else 1
                cv2.drawContours(image, [points], -1, safe_color, thickness)
    
    def _draw_boundaries(self, image: np.ndarray, contour_list: list, colors: dict):
        """绘制边界线，直接修改输入图像，每个轮廓使用不同的色移颜色"""
        base_color = colors['boundary']
        total_contours = len(contour_list)
        
        for contour_index, contour_info in enumerate(contour_list):
            # 检查过滤标志，只绘制filter为true的轮廓
            if not contour_info.get("filter", False):
                continue
                
            mask_data = contour_info.get("轮廓掩码数据", {})
            boundary_mask = mask_data.get("轮廓边界掩码")
            
            if boundary_mask is not None:
                # 获取AOI偏移
                aoi_info = self._get_aoi_offset(contour_info)
                x_offset, y_offset = aoi_info['x_offset'], aoi_info['y_offset']
                
                # 在正确位置绘制边界
                mask_h, mask_w = boundary_mask.shape
                y1, y2 = y_offset, y_offset + mask_h
                x1, x2 = x_offset, x_offset + mask_w
                
                # 确保不超出图像边界
                y1, y2 = max(0, y1), min(image.shape[0], y2)
                x1, x2 = max(0, x1), min(image.shape[1], x2)
                
                if y2 > y1 and x2 > x1:
                    # 调整掩码大小
                    target_h, target_w = y2 - y1, x2 - x1
                    if target_h != mask_h or target_w != mask_w:
                        resized_mask = cv2.resize(boundary_mask, (target_w, target_h))
                    else:
                        resized_mask = boundary_mask
                    
                    # 为每个轮廓生成色移颜色
                    contour_color = self._get_contour_color(base_color, contour_index, total_contours)
                    
                    # 颜色值安全检查
                    safe_color = tuple(max(0, min(255, int(c))) for c in contour_color)
                    
                    # 应用边界颜色
                    image[y1:y2, x1:x2][resized_mask > 0] = safe_color
    
    def _draw_contour_points(self, image: np.ndarray, contour_list: list, colors: dict):
        """绘制轮廓点，直接修改输入图像，每个轮廓使用不同的色移颜色"""
        base_color = colors['points']
        total_contours = len(contour_list)
        
        for contour_index, contour_info in enumerate(contour_list):
            # 检查过滤标志，只绘制filter为true的轮廓
            if not contour_info.get("filter", False):
                continue
                
            contour_data = contour_info.get("轮廓掩码数据", {})
            contour_points = contour_data.get("轮廓点坐标", [])
            
            if contour_points:
                aoi_info = self._get_aoi_offset(contour_info)
                
                # 为每个轮廓生成色移颜色
                contour_color = self._get_contour_color(base_color, contour_index, total_contours)
                
                # 颜色值安全检查
                safe_color = tuple(max(0, min(255, int(c))) for c in contour_color)
                
                # 绘制轮廓点（只绘制部分点以避免过于密集）
                step = max(1, len(contour_points) // 50)  # 最多绘制50个点
                for i in range(0, len(contour_points), step):
                    point = contour_points[i]
                    x = point[0] + aoi_info['x_offset']
                    y = point[1] + aoi_info['y_offset']
                    cv2.circle(image, (x, y), 2, safe_color, -1)
    
    def _draw_combined(self, image: np.ndarray, contour_list: list, colors: dict, alpha: float = 0.2):
        """组合绘制（轮廓+凸包+中心点），直接修改输入图像"""
        # 先绘制填充掩码（半透明）
        self._draw_filled_masks(image, contour_list, colors, alpha)
        
        # 绘制轮廓线
        self._draw_contour_lines(image, contour_list, colors)
        
        # 绘制凸包（虚线效果）
        for contour_info in contour_list:
            # 检查过滤标志
            if not contour_info.get("filter", False):
                continue
                
            hull_data = contour_info.get("轮廓掩码数据", {})
            hull_points = hull_data.get("轮廓凸包点", [])
            
            if hull_points and len(hull_points) > 2:
                points = np.array(hull_points, dtype=np.int32)
                aoi_info = self._get_aoi_offset(contour_info)
                points[:, 0] += aoi_info['x_offset']
                points[:, 1] += aoi_info['y_offset']
                
                # 绘制虚线凸包
                for i in range(len(points)):
                    start_point = tuple(points[i])
                    end_point = tuple(points[(i + 1) % len(points)])
                    self._draw_dashed_line(image, start_point, end_point, colors['hull'], 1)
        
        # 绘制中心点
        for contour_info in contour_list:
            # 检查过滤标志
            if not contour_info.get("filter", False):
                continue
                
            center = contour_info.get("绝对中心", {})
            if center:
                cx, cy = center.get("x", 0), center.get("y", 0)
                is_main = contour_info.get("是否主要轮廓", False)
                radius = 5 if is_main else 3
                cv2.circle(image, (cx, cy), radius, colors['center'], -1)
                cv2.circle(image, (cx, cy), radius + 1, colors['text'], 1)
    
    def _draw_all_types(self, image: np.ndarray, contour_list: list, colors: dict, alpha: float = 0.2):
        """绘制所有类型，直接修改输入图像"""
        # 按层次绘制
        self._draw_filled_masks(image, contour_list, colors, alpha)
        self._draw_boundaries(image, contour_list, colors)
        self._draw_convex_hulls(image, contour_list, colors)
        self._draw_contour_lines(image, contour_list, colors)
        self._draw_contour_points(image, contour_list, colors)
        
        # 绘制中心点和标签
        for contour_info in contour_list:
            # 检查过滤标志
            if not contour_info.get("filter", False):
                continue
                
            center = contour_info.get("绝对中心", {})
            if center:
                cx, cy = center.get("x", 0), center.get("y", 0)
                cv2.circle(image, (cx, cy), 4, colors['center'], -1)
                cv2.circle(image, (cx, cy), 5, colors['text'], 1)
    
    def _get_aoi_offset(self, contour_info: dict) -> dict:
        """获取AOI偏移量"""
        # 从轮廓的绝对边界框计算偏移
        abs_bbox = contour_info.get("绝对边界框", {})
        rel_bbox = contour_info.get("边界框", {})
        
        if abs_bbox and rel_bbox:
            x_offset = abs_bbox.get("x1", 0) - rel_bbox.get("x", 0)
            y_offset = abs_bbox.get("y1", 0) - rel_bbox.get("y", 0)
        else:
            x_offset, y_offset = 0, 0
        
        return {'x_offset': x_offset, 'y_offset': y_offset}
    
    def _draw_dashed_line(self, image: np.ndarray, pt1: tuple, pt2: tuple, color: tuple, thickness: int):
        """绘制虚线"""
        import math
        
        dist = math.sqrt((pt2[0] - pt1[0])**2 + (pt2[1] - pt1[1])**2)
        dash_length = 5
        gap_length = 3
        
        if dist < dash_length:
            cv2.line(image, pt1, pt2, color, thickness)
            return
        
        num_dashes = int(dist // (dash_length + gap_length))
        
        for i in range(num_dashes):
            start_ratio = i * (dash_length + gap_length) / dist
            end_ratio = (i * (dash_length + gap_length) + dash_length) / dist
            
            start_x = int(pt1[0] + (pt2[0] - pt1[0]) * start_ratio)
            start_y = int(pt1[1] + (pt2[1] - pt1[1]) * start_ratio)
            end_x = int(pt1[0] + (pt2[0] - pt1[0]) * end_ratio)
            end_y = int(pt1[1] + (pt2[1] - pt1[1]) * end_ratio)
            
            cv2.line(image, (start_x, start_y), (end_x, end_y), color, thickness)
    
    def _add_contour_labels(self, image: np.ndarray, deleted_item: dict, colors: dict):
        """添加轮廓标签信息，直接修改输入图像"""
        try:
            class_name = deleted_item.get("类型字符", "Unknown")
            contour_count = deleted_item.get("轮廓总数", 0)
            
            # 找到主要轮廓的位置作为标签位置
            main_idx = deleted_item.get("主要轮廓索引", -1)
            if main_idx >= 0:
                contour_list = deleted_item.get("轮廓列表", [])
                if main_idx < len(contour_list):
                    main_contour = contour_list[main_idx]
                    center = main_contour.get("绝对中心", {})
                    
                    if center:
                        cx, cy = center.get("x", 0), center.get("y", 0)
                        
                        # 准备标签文本 - 使用英文
                        labels = [
                            f"{class_name}",
                            f"Contours: {contour_count}",
                            f"Area: {main_contour.get('轮廓面积', 0)}"
                        ]
                        
                        # 绘制标签背景和文本
                        self._draw_multi_line_text(image, labels, (cx + 10, cy - 30), colors)
            
        except Exception as e:
            if self.verbose:
                self._log(f"⚠️ 添加标签失败: {str(e)}")
            return image
    
    def _draw_multi_line_text(self, image: np.ndarray, texts: list, position: tuple, colors: dict):
        """绘制多行文本"""
        font = cv2.FONT_HERSHEY_SIMPLEX
        font_scale = 0.5
        thickness = 1
        line_height = 20
        
        x, y = position
        
        # 计算文本区域尺寸
        max_width = 0
        for text in texts:
            (text_width, text_height), _ = cv2.getTextSize(text, font, font_scale, thickness)
            max_width = max(max_width, text_width)
        
        total_height = len(texts) * line_height + 10
        
        # 绘制背景
        cv2.rectangle(image, (x - 5, y - 5), (x + max_width + 5, y + total_height), 
                     colors['background'], -1)
        cv2.rectangle(image, (x - 5, y - 5), (x + max_width + 5, y + total_height), 
                     colors['text'], 1)
        
        # 绘制文本
        for i, text in enumerate(texts):
            text_y = y + (i + 1) * line_height
            cv2.putText(image, text, (x, text_y), font, font_scale, colors['text'], thickness)

    def calculate_aoi_relationships(self, main_aoi: dict, sub_aoi: dict, tolerance: int = 5) -> dict:
        """
        计算两个AOI框的各类数学关系
        
        Args:
            main_aoi: 主框 {'x': int, 'y': int, 'width': int, 'height': int}
            sub_aoi: 副框 {'x': int, 'y': int, 'width': int, 'height': int}
            tolerance: 对齐判断的容差（像素）
            
        Returns:
            dict: 包含各类数学关系的字典
        """
        try:
            if not all(key in main_aoi for key in ['x', 'y', 'width', 'height']) or \
            not all(key in sub_aoi for key in ['x', 'y', 'width', 'height']):
                return {"错误": "AOI数据格式不完整"}
            
            # 计算基础信息
            main_area = main_aoi['width'] * main_aoi['height']
            sub_area = sub_aoi['width'] * sub_aoi['height']
            main_center = [main_aoi['x'] + main_aoi['width'] // 2, main_aoi['y'] + main_aoi['height'] // 2]
            sub_center = [sub_aoi['x'] + sub_aoi['width'] // 2, sub_aoi['y'] + sub_aoi['height'] // 2]
            main_aspect_ratio = main_aoi['width'] / main_aoi['height'] if main_aoi['height'] > 0 else float('inf')
            sub_aspect_ratio = sub_aoi['width'] / sub_aoi['height'] if sub_aoi['height'] > 0 else float('inf')
            
            # 计算边界坐标
            main_x1, main_y1 = main_aoi['x'], main_aoi['y']
            main_x2, main_y2 = main_x1 + main_aoi['width'], main_y1 + main_aoi['height']
            sub_x1, sub_y1 = sub_aoi['x'], sub_aoi['y']
            sub_x2, sub_y2 = sub_x1 + sub_aoi['width'], sub_y1 + sub_aoi['height']
            
            # 计算位置关系
            center_dx = sub_center[0] - main_center[0]
            center_dy = sub_center[1] - main_center[1]
            center_distance = np.sqrt(center_dx**2 + center_dy**2)
            
            # 计算连线角度（0-360度）
            import math
            angle = math.degrees(math.atan2(center_dy, center_dx))
            if angle < 0:
                angle += 360
            
            # 判断相对位置
            if abs(center_dx) < tolerance and abs(center_dy) < tolerance:
                relative_position = "居中"
            elif abs(center_dx) > abs(center_dy):
                relative_position = "右侧" if center_dx > 0 else "左侧"
            else:
                relative_position = "下方" if center_dy > 0 else "上方"
            
            # 计算最近边距离
            dx = max(0, max(main_x1 - sub_x2, sub_x1 - main_x2))
            dy = max(0, max(main_y1 - sub_y2, sub_y1 - main_y2))
            nearest_distance = math.sqrt(dx**2 + dy**2)
            
            # 计算最远边距离
            far_dx = max(abs(main_x1 - sub_x2), abs(main_x2 - sub_x1))
            far_dy = max(abs(main_y1 - sub_y2), abs(main_y2 - sub_y1))
            farthest_distance = math.sqrt(far_dx**2 + far_dy**2)
            
            # 计算重叠关系
            overlap_x1 = max(main_x1, sub_x1)
            overlap_y1 = max(main_y1, sub_y1)
            overlap_x2 = min(main_x2, sub_x2)
            overlap_y2 = min(main_y2, sub_y2)
            
            is_overlapping = overlap_x1 < overlap_x2 and overlap_y1 < overlap_y2
            overlap_area = max(0, overlap_x2 - overlap_x1) * max(0, overlap_y2 - overlap_y1) if is_overlapping else 0
            
            # 判断重叠类型
            if overlap_area == 0:
                if nearest_distance == 0:
                    overlap_type = "相切"
                else:
                    overlap_type = "分离"
            else:
                if overlap_area == main_area and overlap_area == sub_area:
                    overlap_type = "完全重合"
                elif overlap_area == sub_area:
                    overlap_type = "主框包含副框"
                elif overlap_area == main_area:
                    overlap_type = "副框包含主框"
                else:
                    overlap_type = "部分重叠"
            
            # 计算并集面积和交并比
            union_area = main_area + sub_area - overlap_area
            iou = overlap_area / union_area if union_area > 0 else 0
            
            # 计算尺寸比较
            area_ratio = main_area / sub_area if sub_area > 0 else float('inf')
            width_ratio = main_aoi['width'] / sub_aoi['width'] if sub_aoi['width'] > 0 else float('inf')
            height_ratio = main_aoi['height'] / sub_aoi['height'] if sub_aoi['height'] > 0 else float('inf')
            aspect_ratio_diff = abs(main_aspect_ratio - sub_aspect_ratio)
            
            # 判断尺寸关系
            if abs(area_ratio - 1.0) < 0.1:
                size_relationship = "尺寸相近"
            elif area_ratio > 1.0:
                size_relationship = "主框更大"
            else:
                size_relationship = "副框更大"
            
            # 计算对齐关系
            left_aligned = abs(main_x1 - sub_x1) <= tolerance
            right_aligned = abs(main_x2 - sub_x2) <= tolerance
            top_aligned = abs(main_y1 - sub_y1) <= tolerance
            bottom_aligned = abs(main_y2 - sub_y2) <= tolerance
            h_center_aligned = abs(main_center[0] - sub_center[0]) <= tolerance
            v_center_aligned = abs(main_center[1] - sub_center[1]) <= tolerance
            
            # 新增：计算具体的边界距离
            left_distance = sub_x1 - main_x1      # 副框左边相对主框左边的距离（正值表示副框在右侧）
            right_distance = sub_x2 - main_x2     # 副框右边相对主框右边的距离（正值表示副框更右）
            top_distance = sub_y1 - main_y1       # 副框上边相对主框上边的距离（正值表示副框在下方）
            bottom_distance = sub_y2 - main_y2    # 副框下边相对主框下边的距离（正值表示副框更下）
            
            # 计算边界关系
            min_bounding_x1 = min(main_x1, sub_x1)
            min_bounding_y1 = min(main_y1, sub_y1)
            min_bounding_x2 = max(main_x2, sub_x2)
            min_bounding_y2 = max(main_y2, sub_y2)
            min_bounding_rect = {
                'x': min_bounding_x1,
                'y': min_bounding_y1,
                'width': min_bounding_x2 - min_bounding_x1,
                'height': min_bounding_y2 - min_bounding_y1
            }
            
            # 计算间隙
            h_gap = max(main_x1 - sub_x2, sub_x1 - main_x2) if not is_overlapping else min(overlap_x1 - max(main_x1, sub_x1), min(main_x2, sub_x2) - overlap_x2)
            v_gap = max(main_y1 - sub_y2, sub_y1 - main_y2) if not is_overlapping else min(overlap_y1 - max(main_y1, sub_y1), min(main_y2, sub_y2) - overlap_y2)
            
            # 判断是否相邻、同行、同列
            is_adjacent = nearest_distance <= tolerance
            same_row = not (main_y2 <= sub_y1 or sub_y2 <= main_y1)  # Y坐标范围有重叠
            same_column = not (main_x2 <= sub_x1 or sub_x2 <= main_x1)  # X坐标范围有重叠
            
            # 计算几何特征
            # 象限关系（以主框中心为原点）
            if center_dx > tolerance and center_dy < -tolerance:
                quadrant = "第一象限"
            elif center_dx < -tolerance and center_dy < -tolerance:
                quadrant = "第二象限"
            elif center_dx < -tolerance and center_dy > tolerance:
                quadrant = "第三象限"
            elif center_dx > tolerance and center_dy > tolerance:
                quadrant = "第四象限"
            elif abs(center_dx) <= tolerance and abs(center_dy) <= tolerance:
                quadrant = "原点"
            elif abs(center_dx) <= tolerance:
                quadrant = "Y轴"
            else:
                quadrant = "X轴"
            
            # 包含关系
            if overlap_type == "主框包含副框":
                containment = "主框包含副框"
            elif overlap_type == "副框包含主框":
                containment = "副框包含主框"
            else:
                containment = "无包含"
            
            # 相对大小
            if area_ratio > 1.1:
                relative_size = "主框大于副框"
            elif area_ratio < 0.9:
                relative_size = "副框大于主框"
            else:
                relative_size = "大小相等"
            
            # 形状相似度（基于长宽比）
            if main_aspect_ratio == float('inf') or sub_aspect_ratio == float('inf'):
                shape_similarity = 0.0
            else:
                ratio_diff = abs(main_aspect_ratio - sub_aspect_ratio)
                max_ratio = max(main_aspect_ratio, sub_aspect_ratio)
                shape_similarity = 1.0 - min(1.0, ratio_diff / max_ratio)
            
            # 构建返回字典
            result = {
                "基础信息": {
                    "主框": {
                        "x": main_aoi['x'],
                        "y": main_aoi['y'],
                        "width": main_aoi['width'],
                        "height": main_aoi['height'],
                        "面积": main_area,
                        "中心点": main_center,
                        "长宽比": round(main_aspect_ratio, 3)
                    },
                    "副框": {
                        "x": sub_aoi['x'],
                        "y": sub_aoi['y'],
                        "width": sub_aoi['width'],
                        "height": sub_aoi['height'],
                        "面积": sub_area,
                        "中心点": sub_center,
                        "长宽比": round(sub_aspect_ratio, 3)
                    }
                },
                
                "位置关系": {
                    "相对位置": relative_position,
                    "中心距离": round(center_distance, 2),
                    "水平距离": center_dx,
                    "垂直距离": center_dy,
                    "最近边距离": round(nearest_distance, 2),
                    "最远边距离": round(farthest_distance, 2),
                    "连线角度": round(angle, 1)
                },
                
                "重叠关系": {
                    "是否重叠": is_overlapping,
                    "重叠类型": overlap_type,
                    "重叠面积": overlap_area,
                    "重叠占主框比例": round(overlap_area / main_area if main_area > 0 else 0, 3),
                    "重叠占副框比例": round(overlap_area / sub_area if sub_area > 0 else 0, 3),
                    "并集面积": union_area,
                    "交并比": round(iou, 3)
                },
                
                "尺寸比较": {
                    "面积比例": round(area_ratio, 3),
                    "宽度比例": round(width_ratio, 3),
                    "高度比例": round(height_ratio, 3),
                    "长宽比差异": round(aspect_ratio_diff, 3),
                    "尺寸关系": size_relationship
                },
                
                "对齐关系": {
                    "左边缘对齐": left_aligned,
                    "右边缘对齐": right_aligned,
                    "顶边缘对齐": top_aligned,
                    "底边缘对齐": bottom_aligned,
                    "水平中心线对齐": h_center_aligned,
                    "垂直中心线对齐": v_center_aligned,
                    "对齐容差": tolerance,
                    # 新增：具体的边界距离
                    "左边距离": left_distance,      # 副框左边相对主框左边的距离
                    "右边距离": right_distance,     # 副框右边相对主框右边的距离
                    "上边距离": top_distance,       # 副框上边相对主框上边的距离
                    "下边距离": bottom_distance,    # 副框下边相对主框下边的距离
                },
                
                "边界关系": {
                    "最小外接矩形": min_bounding_rect,
                    "最小水平间隙": h_gap,
                    "最小垂直间隙": v_gap,
                    "是否相邻": is_adjacent,
                    "是否同行": same_row,
                    "是否同列": same_column
                },
                
                "几何特征": {
                    "象限关系": quadrant,
                    "包含关系": containment,
                    "相对大小": relative_size,
                    "形状相似度": round(shape_similarity, 3)
                }
            }
            
            if self.verbose:
                self._log(f"🔢 计算AOI关系完成:")
                self._log(f"   主框: ({main_aoi['x']},{main_aoi['y']}) {main_aoi['width']}x{main_aoi['height']}")
                self._log(f"   副框: ({sub_aoi['x']},{sub_aoi['y']}) {sub_aoi['width']}x{sub_aoi['height']}")
                self._log(f"   关系: {relative_position}, 距离: {center_distance:.1f}, 重叠: {overlap_type}")
                self._log(f"   边界距离: 左={left_distance}, 右={right_distance}, 上={top_distance}, 下={bottom_distance}")
            
            return result
            
        except Exception as e:
            if self.verbose:
                self._log(f"❌ 计算AOI关系出错: {str(e)}")
            return {"错误": f"计算出错: {str(e)}"}

def mulity_sam_run_optimized(sam_model_list: list, deleted_list: list, batch_size: int = None, seg_params: dict = None) -> bool:
    """
    多模型FastSAM分割推理（优化版）
    
    使用ThreadPoolExecutor进行高效的并行处理，减少线程管理和同步开销。
    
    主要优化：
    1. 使用ThreadPoolExecutor替代手动线程管理
    2. 预分配任务，避免队列竞争  
    3. 减少不必要的内存拷贝
    4. 简化日志输出
    
    Args:
        sam_model_list: FastSAM模型实例列表 [yolo_model_loder, ...]
        deleted_list: 检测项列表，每个检测项必须包含'image'字段
        batch_size: 保留参数（兼容性），实际不使用
        seg_params: 分割参数字典
    
    Returns:
        bool: 是否所有任务都成功完成
    """
    
    if not sam_model_list or not deleted_list:
        return not deleted_list  # 空列表返回True，空模型返回False
    
    # 默认参数
    if seg_params is None:
        seg_params = {}
    
    seg_confidence_threshold = seg_params.get("置信度阈值", 0.3)
    seg_iou_threshold = seg_params.get("IOU阈值", 0.8)
    seg_imgsz = seg_params.get("输入尺寸", 512)
    seg_retina_masks = seg_params.get("高精掩码", False)
    verbose = seg_params.get("verbose", False)
    
    # 验证模型
    available_models = [m for m in sam_model_list 
                       if isinstance(m, yolo_model_loder) and m.is_model_loaded]
    
    if not available_models:
        print("❌ 没有可用的已加载模型")
        return False
    
    # 筛选有效任务
    valid_tasks = [(i, item) for i, item in enumerate(deleted_list) 
                   if isinstance(item, dict) and 'image' in item]
    
    if not valid_tasks:
        print("❌ 没有有效的任务")
        return False
    
    total_tasks = len(valid_tasks)
    num_workers = len(available_models)
    
    if verbose:
        print(f"🚀 开始FastSAM并行推理 (优化版)")
        print(f"   可用模型数量: {num_workers}, 任务数: {total_tasks}")
    
    # 定义处理单个任务的函数
    def process_single_task(args):
        """处理单个分割任务"""
        task_id, detect_item, model, worker_id = args
        
        task_start_time = time.perf_counter()
        
        try:
            # FastSAM推理
            inference_start = time.perf_counter()

            # 计算动态推理尺寸
            seg_imgsz_cur = min(max(detect_item['image'].shape[:2]), seg_imgsz)
            
            # 获取原始图像尺寸用于后续缩放计算
            original_height, original_width = detect_item['image'].shape[:2]
            
            seg_results = model.model(
                detect_item['image'],
                device=model.device,
                retina_masks=seg_retina_masks,
                imgsz=seg_imgsz_cur,
                conf=seg_confidence_threshold,
                iou=seg_iou_threshold,
                max_det=10,
                verbose=False
            )
            
            inference_time = time.perf_counter() - inference_start
            
            # 处理分割结果
            success = False
            mask_area = 0
            mask_count = 0
            
            if seg_results and seg_results[0].masks is not None:
                masks = seg_results[0].masks.data
                mask_count = len(masks)
                
                if mask_count > 0:
                    # 保存所有SAM结果到sam_result字段
                    sam_result = {
                        "mask_count": mask_count,
                        "masks": [],
                        "inference_time": round(inference_time, 4),
                        "processing_worker": worker_id,
                        "success": True,
                        # 新增：保存动态推理尺寸信息
                        "inference_size": seg_imgsz_cur,
                        "original_size": (original_height, original_width),
                        "inference_actual_size": None  # 将在下面设置实际掩码尺寸
                    }
                    
                    # 计算所有掩码面积
                    mask_areas = torch.sum(masks.view(mask_count, -1), dim=1)
                    total_mask_area = 0
                    
                    # 保存每个掩码的信息
                    for idx in range(mask_count):
                        mask_data = masks[idx].cpu().numpy()
                        mask_area_single = mask_areas[idx].item()
                        total_mask_area += mask_area_single
                        
                        # 从第一个掩码获取实际推理尺寸
                        if idx == 0:
                            sam_result["inference_actual_size"] = mask_data.shape[:2]  # (height, width)
                        
                        mask_info = {
                            "mask_index": idx,
                            "mask_array": mask_data,
                            "mask_area": int(mask_area_single),
                            "mask_shape": mask_data.shape,
                            "mask_dtype": str(mask_data.dtype),
                            "is_largest": False  # 稍后设置
                        }
                        sam_result["masks"].append(mask_info)
                    
                    # 标记面积最大的掩码
                    if mask_count > 0:
                        largest_idx = torch.argmax(mask_areas).item()
                        sam_result["masks"][largest_idx]["is_largest"] = True
                        sam_result["largest_mask_index"] = largest_idx
                        sam_result["largest_mask_area"] = int(mask_areas[largest_idx].item())
                    
                    # 计算统计信息
                    sam_result["total_mask_area"] = int(total_mask_area)
                    sam_result["average_mask_area"] = round(total_mask_area / mask_count, 2)
                    sam_result["mask_areas"] = [int(area.item()) for area in mask_areas]
                    
                    # 将sam_result保存到检测项中
                    detect_item['sam_result'] = sam_result
                    
                    # 为了向后兼容，仍保留原有字段（使用最大掩码）
                    largest_area = mask_areas[largest_idx].item()
                    detect_item['分割掩码'] = masks[largest_idx].cpu().numpy()
                    detect_item['掩码面积'] = int(largest_area)
                    detect_item['有精细分割'] = True
                    detect_item['分割推理时间'] = round(inference_time, 4)
                    detect_item['分割掩码数量'] = mask_count
                    detect_item['处理worker'] = worker_id
                    
                    # 计算覆盖率
                    if 'AOI位置' in detect_item:
                        box_area = detect_item['AOI位置'].get('width', 1) * detect_item['AOI位置'].get('height', 1)
                        detect_item['掩码覆盖率'] = round(largest_area / max(box_area, 1), 4)
                        # 也添加到sam_result中
                        sam_result["largest_mask_coverage"] = detect_item['掩码覆盖率']
                    
                    success = True
                    mask_area = int(largest_area)
                else:
                    # 找到掩码但都无效
                    detect_item['sam_result'] = {
                        "mask_count": 0,
                        "masks": [],
                        "inference_time": round(inference_time, 4),
                        "processing_worker": worker_id,
                        "success": False,
                        "error": "找到掩码但都无效",
                        # 添加推理尺寸信息（即使失败也保存）
                        "inference_size": seg_imgsz_cur,
                        "original_size": (original_height, original_width),
                        "inference_actual_size": None
                    }
            else:
                # 没有找到掩码
                detect_item['sam_result'] = {
                    "mask_count": 0,
                    "masks": [],
                    "inference_time": round(inference_time, 4),
                    "processing_worker": worker_id,
                    "success": False,
                    "error": "未检测到分割掩码",
                    # 添加推理尺寸信息（即使失败也保存）
                    "inference_size": seg_imgsz_cur,
                    "original_size": (original_height, original_width),
                    "inference_actual_size": None
                }
            
            if not success:
                # 未找到有效掩码
                detect_item.update({
                    '分割掩码': None,
                    '掩码面积': 0,
                    '有精细分割': False,
                    '分割推理时间': round(inference_time, 4),
                    '分割掩码数量': mask_count,
                    '处理worker': worker_id
                })
                
        except Exception as e:
            # 异常处理
            detect_item['sam_result'] = {
                "mask_count": 0,
                "masks": [],
                "inference_time": 0,
                "processing_worker": worker_id,
                "success": False,
                "error": str(e),
                # 添加推理尺寸信息（即使异常也尽量保存）
                "inference_size": seg_imgsz_cur if 'seg_imgsz_cur' in locals() else None,
                "original_size": (original_height, original_width) if 'original_height' in locals() and 'original_width' in locals() else None,
                "inference_actual_size": None
            }
            
            detect_item.update({
                '分割掩码': None,
                '掩码面积': 0,
                '有精细分割': False,
                '分割错误': str(e),
                '处理worker': worker_id
            })
            inference_time = 0
            mask_area = 0
            success = False
        
        task_total_time = time.perf_counter() - task_start_time
        
        return {
            'success': success,
            'task_id': task_id,
            'worker_id': worker_id,
            'mask_area': mask_area,
            'inference_time': inference_time,
            'total_task_time': task_total_time
        }
    
    # 使用ThreadPoolExecutor并行处理
    from concurrent.futures import ThreadPoolExecutor, as_completed
    
    start_time = time.perf_counter()
    
    # 预分配任务到模型（轮询分配）
    task_args = []
    for i, (task_id, detect_item) in enumerate(valid_tasks):
        model_idx = i % num_workers
        model = available_models[model_idx]
        task_args.append((task_id, detect_item, model, model_idx))
    
    # 并行执行
    results = []
    with ThreadPoolExecutor(max_workers=num_workers, thread_name_prefix="FastSAM-Worker") as executor:
        future_to_task = {executor.submit(process_single_task, args): args for args in task_args}
        
        for future in as_completed(future_to_task):
            try:
                result = future.result()
                results.append(result)
                
                # 简化日志输出
                if verbose and len(results) % max(1, total_tasks // 10) == 0:  # 只输出10%进度
                    progress = len(results) / total_tasks * 100
                    print(f"📊 进度: {progress:.0f}% ({len(results)}/{total_tasks})")
                    
            except Exception as e:
                task_args_for_future = future_to_task[future]
                task_id, _, _, worker_id = task_args_for_future
                
                results.append({
                    'success': False,
                    'task_id': task_id,
                    'worker_id': worker_id,
                    'mask_area': 0,
                    'inference_time': 0,
                    'total_task_time': 0
                })
                
                if verbose:
                    print(f"❌ Worker-{worker_id} 处理任务 {task_id} 失败: {str(e)}")
    
    # 统计结果
    end_time = time.perf_counter()
    total_time = end_time - start_time
    
    success_count = sum(1 for r in results if r['success'])
    failed_count = len(results) - success_count
    total_inference_time = sum(r['inference_time'] for r in results)
    
    if verbose:
        print(f"\n📊 FastSAM推理完成 (优化版):")
        print(f"   总任务: {total_tasks}, 成功: {success_count}, 失败: {failed_count}")
        print(f"   总耗时: {total_time:.3f}秒")
        
        if len(results) > 0:
            avg_inference_time = total_inference_time / len(results)
            theoretical_speedup = total_inference_time / total_time if total_time > 0 else 0
            throughput = len(results) / total_time if total_time > 0 else 0
            
            print(f"   平均推理时间: {avg_inference_time:.3f}秒")
            print(f"   理论加速比: {theoretical_speedup:.2f}x")
            print(f"   实际吞吐量: {throughput:.2f} 任务/秒")
    
    return failed_count == 0


def mulity_detect_run_optimized(detect_model_list: list, src_image, batch_size: int = None, detect_params: dict = None):
    """
    多模型YOLO检测推理（优化版）- 负载均衡模式
    
    从多个检测模型中挑选一个不忙的模型来处理单个图像
    
    Args:
        detect_model_list: YOLO检测模型实例列表 [yolo_model_loder, ...]（已初始化完成）
        src_image: 输入图像 (np.ndarray)
        batch_size: 保留参数（兼容性），实际不使用
        detect_params: 检测参数字典
    
    Returns:
        model_results: 检测结果，如果所有模型都忙则返回None
    """
    import time
    
    if not detect_model_list or src_image is None:
        return None
    
    # 默认参数
    if detect_params is None:
        detect_params = {}
    
    model_iou_threshold = detect_params.get("IOU阈值", 0.45)
    imgsz = detect_params.get("输入尺寸", 640)
    verbose = detect_params.get("verbose", False)
    max_wait_time = detect_params.get("最大等待时间", 5.0)  # 最大等待时间（秒）
    
    # 查找可用的模型（不忙的模型）
    available_model = None
    selected_model_idx = -1
    
    # 第一轮：查找完全空闲的模型
    for i, model in enumerate(detect_model_list):
        if model is None:
            continue
            
        # 检查模型是否忙碌（如果支持状态管理）
        if hasattr(model, 'is_model_busy'):
            if not model.is_model_busy():
                available_model = model
                selected_model_idx = i
                if verbose:
                    print(f"🎯 选择空闲模型 {i}")
                break
        else:
            # 如果模型不支持状态管理，直接使用第一个可用模型
            available_model = model
            selected_model_idx = i
            if verbose:
                print(f"🎯 选择模型 {i} (无状态管理)")
            break
    
    # 第二轮：如果没有完全空闲的模型，选择负载最轻的模型
    if available_model is None:
        min_load = float('inf')
        for i, model in enumerate(detect_model_list):
            if model is None:
                continue
                
            try:
                if hasattr(model, 'get_status'):
                    status = model.get_status()
                    current_load = status.get('task_duration', 0)  # 当前任务持续时间
                    if current_load < min_load:
                        min_load = current_load
                        available_model = model
                        selected_model_idx = i
                else:
                    # 如果没有状态信息，随机选择一个
                    available_model = model
                    selected_model_idx = i
                    break
            except:
                continue
        
        if available_model and verbose:
            print(f"🎯 选择负载最轻模型 {selected_model_idx} (负载: {min_load:.3f}s)")
    
    # 第三轮：如果仍然没有可用模型，等待或者强制使用第一个模型
    if available_model is None:
        if verbose:
            print("⚠️ 所有模型都忙，等待或使用第一个可用模型...")
        
        # 等待第一个非None模型变为空闲
        for model in detect_model_list:
            if model is not None:
                available_model = model
                selected_model_idx = detect_model_list.index(model)
                
                # 如果支持等待，尝试等待模型空闲
                if hasattr(model, 'wait_until_idle'):
                    try:
                        model.wait_until_idle(timeout=max_wait_time)
                        if verbose:
                            print(f"✅ 模型 {selected_model_idx} 已变为空闲")
                    except:
                        if verbose:
                            print(f"⚠️ 等待模型 {selected_model_idx} 空闲超时，强制使用")
                break
    
    # 如果仍然没有可用模型，返回None
    if available_model is None:
        if verbose:
            print("❌ 没有可用的检测模型")
        return None
    
    try:
        # 使用选中的模型进行检测推理
        if verbose:
            print(f"🚀 使用模型 {selected_model_idx} 进行检测推理")
        
        model_start = time.perf_counter()
        
        # 直接调用模型的predictEx方法，参考delected_fanzhe中的调用方式
        model_results = available_model.predictEx(
            src_image,                                          # 输入图像
            device=available_model.device,                      # 设备
            iou=model_iou_threshold,                            # IOU阈值
            imgsz=imgsz,                                        # 输入尺寸
            verbose=False                                       # 是否输出详细信息
        )
        
        model_end = time.perf_counter()
        inference_time = model_end - model_start
        
        if verbose:
            print(f"⚡ 模型 {selected_model_idx} 推理完成，耗时: {inference_time:.4f}秒")
        
        return model_results
        
    except Exception as e:
        if verbose:
            print(f"❌ 模型 {selected_model_idx} 推理失败: {str(e)}")
        return None
    return model_results_list

# {
#     "基础信息": {
#         "主框": {"x": int, "y": int, "width": int, "height": int, "面积": int, "中心点": [x, y], "长宽比": float},
#         "副框": {"x": int, "y": int, "width": int, "height": int, "面积": int, "中心点": [x, y], "长宽比": float}
#     },
    
#     "位置关系": {
#         "相对位置": str,  # "左上", "右上", "左下", "右下", "居中", "左侧", "右侧", "上方", "下方"
#         "中心距离": float,
#         "水平距离": int,  # 正值表示副框在右侧，负值表示在左侧
#         "垂直距离": int,  # 正值表示副框在下方，负值表示在上方
#         "最近边距离": float,
#         "最远边距离": float,
#         "连线角度": float  # 0-360度，从主框中心到副框中心
#     },
    
#     "重叠关系": {
#         "是否重叠": bool,
#         "重叠类型": str,  # "分离", "相切", "部分重叠", "主框包含副框", "副框包含主框", "完全重合"
#         "重叠面积": int,
#         "重叠占主框比例": float,  # 0-1
#         "重叠占副框比例": float,  # 0-1
#         "并集面积": int,
#         "交并比": float  # IoU值
#     },
    
#     "尺寸比较": {
#         "面积比例": float,  # 主框面积/副框面积
#         "宽度比例": float,  # 主框宽度/副框宽度
#         "高度比例": float,  # 主框高度/副框高度
#         "长宽比差异": float,  # abs(主框长宽比 - 副框长宽比)
#         "尺寸关系": str  # "主框更大", "副框更大", "尺寸相近"
#     },
    
#     "对齐关系": {
#         "左边缘对齐": bool,
#         "右边缘对齐": bool,
#         "顶边缘对齐": bool,
#         "底边缘对齐": bool,
#         "水平中心线对齐": bool,
#         "垂直中心线对齐": bool,
#         "对齐容差": int  # 用于判断"对齐"的像素容差
#     },
    
#     "边界关系": {
#         "最小外接矩形": {"x": int, "y": int, "width": int, "height": int},
#         "最小水平间隙": int,  # 可能为负值（重叠时）
#         "最小垂直间隙": int,  # 可能为负值（重叠时）
#         "是否相邻": bool,     # 间隙小于等于指定阈值
#         "是否同行": bool,     # Y坐标范围有重叠
#         "是否同列": bool      # X坐标范围有重叠
#     },
    
#     "几何特征": {
#         "象限关系": str,  # "第一象限", "第二象限", "第三象限", "第四象限", "X轴", "Y轴", "原点"
#         "包含关系": str,  # "无包含", "主框包含副框", "副框包含主框"
#         "相对大小": str,  # "主框大于副框", "副框大于主框", "大小相等"
#         "形状相似度": float  # 基于长宽比计算的相似度 0-1
#     }