ultralytics.nn.autobackend——autobackend.py子模块代码详读

autobackend模块是 Ultralytics YOLO 项目的核心推理后端,主要功能是自动检测和加载多种格式的模型文件,并提供一个统一的接口进行推理。它支持超过20种不同的模型格式,包括 PyTorch、ONNX、TensorRT、TensorFlow、CoreML 等,使得用户可以用同一个代码接口运行不同格式的模型。

基础依赖导入

python 复制代码
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# 版权声明:使用 AGPL-3.0 许可证的开源项目

from __future__ import annotations
# 启用类型注解的延迟评估,允许在类型注解中引用尚未定义的类

import ast  # 用于解析 Python 表达式字符串
import json  # 用于处理 JSON 格式数据
import platform  # 用于获取平台信息(Linux/Windows/Mac)
import zipfile  # 用于处理 ZIP 压缩文件
from collections import OrderedDict, namedtuple  # OrderedDict 保持插入顺序,namedtuple 创建轻量级对象
from pathlib import Path  # 面向对象的路径操作
from typing import Any  # 类型注解中的任意类型

import cv2  # OpenCV 计算机视觉库
import numpy as np  # 数值计算库
import torch  # PyTorch 深度学习框架
import torch.nn as nn  # PyTorch 神经网络模块
from PIL import Image  # Python 图像处理库

# 从 ultralytics.utils 导入各种工具和常量
from ultralytics.utils import ARM64, IS_JETSON, LINUX, LOGGER, PYTHON_VERSION, ROOT, YAML, is_jetson
# ARM64: 是否是 ARM64 架构
# IS_JETSON: 是否是 NVIDIA Jetson 设备
# LINUX: 是否是 Linux 系统
# LOGGER: 日志记录器
# PYTHON_VERSION: Python 版本信息
# ROOT: 项目根目录路径
# YAML: YAML 文件处理工具
# is_jetson: 检查是否是 Jetson 设备的函数

from ultralytics.utils.checks import check_requirements, check_suffix, check_version, check_yaml, is_rockchip
# check_requirements: 检查依赖包是否安装
# check_suffix: 检查文件后缀
# check_version: 检查版本号
# check_yaml: 检查并加载 YAML 文件
# is_rockchip: 检查是否是 Rockchip 设备

from ultralytics.utils.downloads import attempt_download_asset, is_url
# attempt_download_asset: 尝试下载模型文件
# is_url: 检查字符串是否是 URL

from ultralytics.utils.nms import non_max_suppression
# non_max_suppression: 非极大值抑制算法,用于目标检测后处理

check_class_names函数

python 复制代码
def check_class_names(names: list | dict) -> dict[int, str]:
    """
    检查类别名称并转换为字典格式(如果需要的话)

    参数:
        names (list | dict): 类别名称,可以是列表或字典格式。

    返回:
        (dict): 字典格式的类别名称,整数键对应字符串值。

    抛出:
        KeyError: 如果类别索引对数据集大小无效。
    """
    if isinstance(names, list):  # 如果 names 是列表
        # 使用 enumerate 将列表转换为字典,索引作为键,值作为值
        # 例如:['cat', 'dog'] -> {0: 'cat', 1: 'dog'}
        names = dict(enumerate(names))  # 转换为字典
    
    if isinstance(names, dict):
        # 转换:1) 将字符串键转换为整数(例如 '0' 转换为 0)
        # 2) 将非字符串值转换为字符串(例如 True 转换为 'True')
        names = {int(k): str(v) for k, v in names.items()}
        
        n = len(names)  # 获取类别总数
        # 检查最大键值是否大于等于 n(因为索引从0开始,最大应该是 n-1)
        if max(names.keys()) >= n:
            raise KeyError(
                f"{n}-class dataset requires class indices 0-{n - 1}, but you have invalid class indices "
                f"{min(names.keys())}-{max(names.keys())} defined in your dataset YAML."
            )
        
        # 检查是否是 ImageNet 数据集的类别代码(以 'n0' 开头)
        # 例如:'n01440764' 表示 'tench'(一种鱼)
        if isinstance(names[0], str) and names[0].startswith("n0"):
            # 加载 ImageNet 的人类可读名称映射
            names_map = YAML.load(ROOT / "cfg/datasets/ImageNet.yaml")["map"]
            # 将类别代码替换为人类可读的名称
            names = {k: names_map[v] for k, v in names.items()}
    
    return names  # 返回处理后的类别名称字典

default_class_names函数

python 复制代码
def default_class_names(data: str | Path | None = None) -> dict[int, str]:
    """
    将默认类别名称应用于输入的 YAML 文件,或返回数字类别名称

    参数:
        data (str | Path, optional): 包含类别名称的 YAML 文件路径。

    返回:
        (dict): 映射类别索引到类别名称的字典。
    """
    if data:  # 如果提供了 data 参数
        try:
            # 尝试加载 YAML 文件并获取其中的 names 字段
            return YAML.load(check_yaml(data))["names"]
        except Exception:
            pass  # 如果出错,继续执行下面的默认处理
    
    # 如果 data 为空或加载失败,返回默认的类别名称
    # 创建 0-998 的默认类别名称:class0, class1, ..., class998
    return {i: f"class{i}" for i in range(999)}  # 如果上述操作出错则返回默认值

AutoBackend核心后端推理类

AutoBackend 类是一个高度灵活和强大的模型加载和推理抽象层,其主要特点和功能包括:

  1. 多格式支持
  • 支持超过20种不同的模型格式

  • 自动检测模型类型并根据文件后缀选择对应的后端

  • 统一的接口,无论模型格式如何,调用方式相同

  1. 智能设备管理
  • 自动处理 CPU/GPU 设备选择

  • 根据后端能力自动回退到合适的设备

  • 支持特殊硬件(Jetson、Rockchip、Apple Silicon等)

  1. 性能优化
  • 支持 FP16 半精度推理

  • 层融合优化(Conv2D + BatchNorm)

  • 动态形状支持

  • IO 绑定优化(ONNX Runtime)

  • 异步推理(OpenVINO)

  1. 统一的预处理和后处理
  • 自动处理不同后端的输入输出格式

  • 统一的张量设备管理

  • 自动反归一化处理

  • 类别名称管理

  1. 错误处理和兼容性
  • 详细的错误信息和警告

  • 版本兼容性检查

  • 依赖自动检查

  • 模型下载功能

类层次结构图如下所示:
模块依赖关系图
AutoBackend 初始化流程图
前向传播 (forward) 数据流转全景图
不同后端的输入输出格式差异
python 复制代码
# 可视化对比不同后端的输入输出格式

后端类型      | 输入格式          | 输出格式          | 是否需要转换
------------|-----------------|-----------------|------------
PyTorch     | torch.Tensor    | torch.Tensor    | 否
TorchScript | torch.Tensor    | torch.Tensor    | 否
ONNX        | numpy.ndarray   | numpy.ndarray   | 是 → torch.Tensor
TensorRT    | torch.Tensor    | torch.Tensor    | 否
OpenVINO    | numpy.ndarray   | numpy.ndarray   | 是 → torch.Tensor
CoreML      | PIL.Image/numpy | dict/numpy      | 是 → torch.Tensor
TensorFlow  | numpy.ndarray   | numpy.ndarray   | 是 → torch.Tensor
TFLite      | numpy.ndarray   | numpy.ndarray   | 是 → torch.Tensor
forward 方法的分支逻辑可视化
AutoBackend 类属性分类
方法调用关系图
预热机制工作流程
AutoBackend.init 方法
python 复制代码
class AutoBackend(nn.Module):
    """
    用于运行 Ultralytics YOLO 模型推理的动态后端选择处理器。
    AutoBackend 类旨在为各种推理引擎提供抽象层。它支持多种格式,
    每种格式都有特定的命名约定,如下所示:

        支持的格式和命名约定:
            | 格式                | 文件后缀       |
            | --------------------- | ----------------- |
            | PyTorch               | *.pt              |
            | TorchScript           | *.torchscript     |
            | ONNX Runtime          | *.onnx            |
            | ONNX OpenCV DNN       | *.onnx (dnn=True) |
            | OpenVINO              | *openvino_model/  |
            | CoreML                | *.mlpackage       |
            | TensorRT              | *.engine          |
            | TensorFlow SavedModel | *_saved_model/    |
            | TensorFlow GraphDef   | *.pb              |
            | TensorFlow Lite       | *.tflite          |
            | TensorFlow Edge TPU   | *_edgetpu.tflite  |
            | PaddlePaddle          | *_paddle_model/   |
            | MNN                   | *.mnn             |
            | NCNN                  | *_ncnn_model/     |
            | IMX                   | *_imx_model/      |
            | RKNN                  | *_rknn_model/     |
            | Triton Inference      | triton://model    |
            | ExecuTorch            | *.pte             |
            | Axelera               | *_axelera_model/  |

    属性:
        model (torch.nn.Module): 加载的 YOLO 模型。
        device (torch.device): 模型加载的设备(CPU 或 GPU)。
        task (str): 模型执行的任务类型(detect, segment, classify, pose)。
        names (dict): 模型可以检测的类别名称字典。
        stride (int): 模型步长,YOLO 模型通常为 32。
        fp16 (bool): 模型是否使用半精度(FP16)推理。
        nhwc (bool): 模型是否期望 NHWC 输入格式而不是 NCHW。
        pt (bool): 模型是否是 PyTorch 模型。
        jit (bool): 模型是否是 TorchScript 模型。
        onnx (bool): 模型是否是 ONNX 模型。
        xml (bool): 模型是否是 OpenVINO 模型。
        engine (bool): 模型是否是 TensorRT 引擎。
        coreml (bool): 模型是否是 CoreML 模型。
        saved_model (bool): 模型是否是 TensorFlow SavedModel。
        pb (bool): 模型是否是 TensorFlow GraphDef。
        tflite (bool): 模型是否是 TensorFlow Lite 模型。
        edgetpu (bool): 模型是否是 TensorFlow Edge TPU 模型。
        tfjs (bool): 模型是否是 TensorFlow.js 模型。
        paddle (bool): 模型是否是 PaddlePaddle 模型。
        mnn (bool): 模型是否是 MNN 模型。
        ncnn (bool): 模型是否是 NCNN 模型。
        imx (bool): 模型是否是 IMX 模型。
        rknn (bool): 模型是否是 RKNN 模型。
        triton (bool): 模型是否是 Triton Inference Server 模型。
        pte (bool): 模型是否是 PyTorch ExecuTorch 模型。
        axelera (bool): 模型是否是 Axelera 模型。

    方法:
        forward: 在输入图像上运行推理。
        from_numpy: 将 NumPy 数组转换为模型设备上的张量。
        warmup: 使用虚拟输入预热模型。
        _model_type: 从文件路径确定模型类型。

    示例:
        >>> model = AutoBackend(model="yolo11n.pt", device="cuda")
        >>> results = model(img)
    """

    @torch.no_grad()  # 禁用梯度计算,因为推理阶段不需要梯度
    def __init__(
        self,
        model: str | torch.nn.Module = "yolo11n.pt",  # 模型路径或 PyTorch 模块
        device: torch.device = torch.device("cpu"),  # 运行设备,默认 CPU
        dnn: bool = False,  # 是否使用 OpenCV DNN 进行 ONNX 推理
        data: str | Path | None = None,  # 包含类别名称的 YAML 文件路径
        fp16: bool = False,  # 是否启用半精度推理
        fuse: bool = True,  # 是否融合 Conv2D + BatchNorm 层以优化
        verbose: bool = True,  # 是否启用详细日志
    ):
        """初始化 AutoBackend 进行推理。

        参数:
            model (str | torch.nn.Module): 模型权重文件路径或模块实例。
            device (torch.device): 运行模型的设备。
            dnn (bool): 使用 OpenCV DNN 模块进行 ONNX 推理。
            data (str | Path, optional): 包含类别名称的额外 data.yaml 文件路径。
            fp16 (bool): 启用半精度推理。仅在特定后端支持。
            fuse (bool): 融合 Conv2D + BatchNorm 层以优化。
            verbose (bool): 启用详细日志。
        """
        super().__init__()  # 调用父类 nn.Module 的构造函数
        
        nn_module = isinstance(model, torch.nn.Module)  # 检查 model 是否是 PyTorch 模块
        
        # 调用 _model_type 方法确定模型类型,返回一系列布尔值表示不同格式
        (
            pt,  # PyTorch
            jit,  # TorchScript
            onnx,  # ONNX
            xml,  # OpenVINO
            engine,  # TensorRT
            coreml,  # CoreML
            saved_model,  # TensorFlow SavedModel
            pb,  # TensorFlow GraphDef
            tflite,  # TensorFlow Lite
            edgetpu,  # TensorFlow Edge TPU
            tfjs,  # TensorFlow.js
            paddle,  # PaddlePaddle
            mnn,  # MNN
            ncnn,  # NCNN
            imx,  # IMX
            rknn,  # RKNN
            pte,  # ExecuTorch
            axelera,  # Axelera
            triton,  # Triton
        ) = self._model_type("" if nn_module else model)  # 如果是 PyTorch 模块,传入空字符串
        
        # 设置 fp16 标志:只有特定后端支持半精度
        fp16 &= pt or jit or onnx or xml or engine or nn_module or triton  # FP16
        
        # 设置 nhwc 标志:某些格式使用 BHWC 格式(与 PyTorch 的 BCHW 相反)
        nhwc = coreml or saved_model or pb or tflite or edgetpu or rknn  # BHWC 格式(相对于 torch 的 BCHW)
        
        stride, ch = 32, 3  # 默认步长为 32,默认通道数为 3
        end2end, dynamic = False, False  # 初始化 end2end 和 dynamic 标志
        metadata, task = None, None  # 初始化元数据和任务类型
        
        # 设置设备
        # 检查是否使用 CUDA:设备是 torch.device 类型、CUDA 可用且不是 CPU
        cuda = isinstance(device, torch.device) and torch.cuda.is_available() and device.type != "cpu"  # 使用 CUDA
        
        # 如果不是支持 GPU 数据加载器的格式,则回退到 CPU
        if cuda and not any([nn_module, pt, jit, engine, onnx, paddle]):  # GPU 数据加载器格式
            device = torch.device("cpu")  # 回退到 CPU
            cuda = False  # 更新 CUDA 标志
        
        # 如果不是本地文件,则尝试下载
        w = attempt_download_asset(model) if pt else model  # 权重路径
        
        # PyTorch(内存中或文件)
        if nn_module or pt:  # 如果是 PyTorch 模块或 .pt 文件
            if nn_module:  # 如果是 PyTorch 模块
                pt = True  # 设置 pt 标志为 True
                if fuse:  # 如果需要融合层
                    if IS_JETSON and is_jetson(jetpack=5):  # Jetson Jetpack5 需要在融合前将模型移到设备上
                        model = model.to(device)  # 将模型移到指定设备
                    model = model.fuse(verbose=verbose)  # 融合 Conv2D + BatchNorm 层
                model = model.to(device)  # 将模型移到指定设备
            else:  # 如果是 .pt 文件
                from ultralytics.nn.tasks import load_checkpoint  # 导入加载检查点函数
                
                # 加载模型和检查点
                model, _ = load_checkpoint(model, device=device, fuse=fuse)  # 加载模型,检查点
            
            # 通用 PyTorch 模型处理
            if hasattr(model, "kpt_shape"):  # 检查是否有关键点形状属性(姿态估计任务)
                kpt_shape = model.kpt_shape  # 获取关键点形状(仅姿态估计)
            
            # 获取模型步长:最大步长至少为 32
            stride = max(int(model.stride.max()), 32)  # 模型步长
            
            # 获取类别名称:如果模型有 module 属性(DataParallel/DistributedDataParallel),则使用 model.module.names
            names = model.module.names if hasattr(model, "module") else model.names  # 获取类别名称
            
            # 根据 fp16 标志设置模型精度
            model.half() if fp16 else model.float()  # 如果启用 FP16 则转换为半精度,否则保持单精度
            
            # 获取输入通道数,默认为 3(RGB)
            ch = model.yaml.get("channels", 3)  # 获取通道数,默认为 3
            
            # 冻结所有参数(推理阶段不需要梯度)
            for p in model.parameters():
                p.requires_grad = False  # 禁用梯度计算
            
            self.model = model  # 显式赋值给 self.model,便于后续调用 to(), cpu(), cuda(), half()
            
            # 检查是否是端到端模型(包含 NMS)
            end2end = getattr(model, "end2end", False)
        
        # TorchScript
        elif jit:  # 如果是 TorchScript 格式(*.torchscript)
            import torchvision  # 导入 torchvision,某些 TorchScript 模型需要
            
            LOGGER.info(f"Loading {w} for TorchScript inference...")  # 记录加载信息
            
            extra_files = {"config.txt": ""}  # 模型元数据,额外文件
            
            # 加载 TorchScript 模型
            model = torch.jit.load(w, _extra_files=extra_files, map_location=device)
            
            # 根据 fp16 标志设置模型精度
            model.half() if fp16 else model.float()
            
            # 如果 extra_files 中有配置信息,加载元数据字典
            if extra_files["config.txt"]:  # 加载元数据字典
                # 使用 object_hook 将 JSON 对象转换为字典
                metadata = json.loads(extra_files["config.txt"], object_hook=lambda x: dict(x.items()))
        
        # ONNX OpenCV DNN
        elif dnn:  # 使用 OpenCV DNN 进行 ONNX 推理
            LOGGER.info(f"Loading {w} for ONNX OpenCV DNN inference...")  # 记录加载信息
            
            check_requirements("opencv-python>=4.5.4")  # 检查 OpenCV 依赖
            
            # 使用 OpenCV DNN 读取 ONNX 模型
            net = cv2.dnn.readNetFromONNX(w)
        
        # ONNX Runtime 和 IMX
        elif onnx or imx:  # ONNX Runtime 或 IMX 格式
            LOGGER.info(f"Loading {w} for ONNX Runtime inference...")  # 记录加载信息
            
            # 检查 ONNX 和 ONNX Runtime 依赖
            check_requirements(("onnx", "onnxruntime-gpu" if cuda else "onnxruntime"))
            import onnxruntime  # 导入 ONNX Runtime
            
            # 选择执行提供者:优先顺序为 CUDA > CoreML (mps) > CPU
            available = onnxruntime.get_available_providers()  # 获取可用的执行提供者
            
            if cuda and "CUDAExecutionProvider" in available:  # 如果请求 CUDA 且可用
                # 使用 CUDA 执行提供者,指定设备 ID
                providers = [("CUDAExecutionProvider", {"device_id": device.index}), "CPUExecutionProvider"]
            elif device.type == "mps" and "CoreMLExecutionProvider" in available:  # 如果是 Apple M 系列芯片
                # 使用 CoreML 执行提供者
                providers = ["CoreMLExecutionProvider", "CPUExecutionProvider"]
            else:  # 其他情况使用 CPU
                providers = ["CPUExecutionProvider"]
                if cuda:  # 如果请求了 CUDA 但不可用
                    LOGGER.warning("CUDA requested but CUDAExecutionProvider not available. Using CPU...")
                    device, cuda = torch.device("cpu"), False  # 回退到 CPU
            
            # 记录使用的 ONNX Runtime 版本和执行提供者
            LOGGER.info(
                f"Using ONNX Runtime {onnxruntime.__version__} with {providers[0] if isinstance(providers[0], str) else providers[0][0]}"
            )
            
            if onnx:  # 普通 ONNX 模型
                # 创建推理会话
                session = onnxruntime.InferenceSession(w, providers=providers)
            else:  # IMX 格式
                # 检查 IMX 特定依赖
                check_requirements(("model-compression-toolkit>=2.4.1", "edge-mdt-cl<1.1.0", "onnxruntime-extensions"))
                
                # IMX 格式通常是一个目录,需要找到其中的 .onnx 文件
                w = next(Path(w).glob("*.onnx"))
                LOGGER.info(f"Loading {w} for ONNX IMX inference...")  # 记录加载信息
                
                # 导入 IMX 相关模块
                import mct_quantizers as mctq
                from edgemdt_cl.pytorch.nms import nms_ort  # 注册自定义 NMS 操作
                
                # 获取 ONNX Runtime 会话选项
                session_options = mctq.get_ort_session_options()
                session_options.enable_mem_reuse = False  # 修复来自 onnxruntime 的形状不匹配问题
                
                # 创建推理会话(IMX 只使用 CPU)
                session = onnxruntime.InferenceSession(w, session_options, providers=["CPUExecutionProvider"])
            
            # 获取输出名称
            output_names = [x.name for x in session.get_outputs()]
            
            # 获取模型元数据
            metadata = session.get_modelmeta().custom_metadata_map
            
            # 检查是否是动态模型(输入/输出形状可变)
            dynamic = isinstance(session.get_outputs()[0].shape[0], str)  # 如果第一个维度是字符串,则是动态模型
            
            # 检查输入是否是 FP16 精度
            fp16 = "float16" in session.get_inputs()[0].type
            
            # 为优化推理设置 IO 绑定(仅限 CUDA,CoreML 不支持)
            use_io_binding = not dynamic and cuda
            
            if use_io_binding:  # 如果使用 IO 绑定
                io = session.io_binding()  # 创建 IO 绑定对象
                bindings = []  # 初始化绑定列表
                
                # 为每个输出设置绑定
                for output in session.get_outputs():
                    out_fp16 = "float16" in output.type  # 检查输出是否是 FP16
                    
                    # 创建空的输出张量
                    y_tensor = torch.empty(output.shape, dtype=torch.float16 if out_fp16 else torch.float32).to(device)
                    
                    # 绑定输出
                    io.bind_output(
                        name=output.name,  # 输出名称
                        device_type=device.type,  # 设备类型(cuda/cpu)
                        device_id=device.index if cuda else 0,  # 设备 ID
                        element_type=np.float16 if out_fp16 else np.float32,  # 数据类型
                        shape=tuple(y_tensor.shape),  # 形状
                        buffer_ptr=y_tensor.data_ptr(),  # 数据指针
                    )
                    bindings.append(y_tensor)  # 添加到绑定列表
        
        # OpenVINO
        elif xml:  # OpenVINO 格式(*.xml)
            LOGGER.info(f"Loading {w} for OpenVINO inference...")  # 记录加载信息
            
            check_requirements("openvino>=2024.0.0")  # 检查 OpenVINO 依赖
            import openvino as ov  # 导入 OpenVINO
            
            core = ov.Core()  # 创建 OpenVINO 核心对象
            device_name = "AUTO"  # 默认设备名称为 AUTO(自动选择)
            
            # 处理 Intel 设备特定设置
            if isinstance(device, str) and device.startswith("intel"):
                # 提取设备名称,例如 "intel:gpu" -> "GPU"
                device_name = device.split(":")[1].upper()  # Intel OpenVINO 设备
                device = torch.device("cpu")  # OpenVINO 使用自己的设备管理,PyTorch 设备设为 CPU
                
                # 检查设备是否可用
                if device_name not in core.available_devices:
                    LOGGER.warning(f"OpenVINO device '{device_name}' not available. Using 'AUTO' instead.")
                    device_name = "AUTO"  # 如果不可用,回退到 AUTO
            
            w = Path(w)  # 转换为 Path 对象
            
            # 如果不是 *.xml 文件,则在目录中查找
            if not w.is_file():  # 如果不是文件
                w = next(w.glob("*.xml"))  # 从 *_openvino_model 目录获取 *.xml 文件
            
            # 读取模型(包含权重文件)
            ov_model = core.read_model(model=str(w), weights=w.with_suffix(".bin"))
            
            # 如果模型参数没有布局,设置为 NCHW
            if ov_model.get_parameters()[0].get_layout().empty:
                ov_model.get_parameters()[0].set_layout(ov.Layout("NCHW"))
            
            # 尝试加载元数据
            metadata = w.parent / "metadata.yaml"  # 元数据文件路径
            
            if metadata.exists():  # 如果元数据文件存在
                metadata = YAML.load(metadata)  # 加载 YAML 文件
                batch = metadata["batch"]  # 获取批次大小
                dynamic = metadata.get("args", {}).get("dynamic", dynamic)  # 获取动态标志
            
            # OpenVINO 推理模式:LATENCY(延迟)、THROUGHPUT(吞吐量)、CUMULATIVE_THROUGHPUT(累积吞吐量)
            # 批次大于1且是动态模型时使用 CUMULATIVE_THROUGHPUT,否则使用 LATENCY
            inference_mode = "CUMULATIVE_THROUGHPUT" if batch > 1 and dynamic else "LATENCY"
            
            # 编译模型
            ov_compiled_model = core.compile_model(
                ov_model,
                device_name=device_name,
                config={"PERFORMANCE_HINT": inference_mode},  # 设置性能提示
            )
            
            # 记录使用的设备和模式
            LOGGER.info(
                f"Using OpenVINO {inference_mode} mode for batch={batch} inference on {', '.join(ov_compiled_model.get_property('EXECUTION_DEVICES'))}..."
            )
            
            # 获取输入名称
            input_name = ov_compiled_model.input().get_any_name()
        
        # TensorRT
        elif engine:  # TensorRT 引擎格式(*.engine)
            LOGGER.info(f"Loading {w} for TensorRT inference...")  # 记录加载信息
            
            # Jetson 特定修复
            if IS_JETSON and check_version(PYTHON_VERSION, "<=3.8.10"):
                # 修复错误:对于 JetPack 4 和 Python <= 3.8.10 的 JetPack 5,`np.bool` 是 `bool` 的弃用别名
                check_requirements("numpy==1.23.5")
            
            try:  # 尝试导入 TensorRT
                import tensorrt as trt
            except ImportError:
                if LINUX:  # Linux 系统
                    check_requirements("tensorrt>7.0.0,!=10.1.0")  # 检查 TensorRT 依赖
                import tensorrt as trt
            
            # 检查 TensorRT 版本
            check_version(trt.__version__, ">=7.0.0", hard=True)  # 必须 >=7.0.0
            check_version(trt.__version__, "!=10.1.0", msg="https://github.com/ultralytics/ultralytics/pull/14239")  # 避免 10.1.0 版本
            
            # 如果设备是 CPU,但 TensorRT 需要 GPU
            if device.type == "cpu":
                device = torch.device("cuda:0")  # TensorRT 需要 GPU,切换到第一个 CUDA 设备
            
            # 定义 Binding 命名元组,用于存储绑定信息
            Binding = namedtuple("Binding", ("name", "dtype", "shape", "data", "ptr"))
            
            logger = trt.Logger(trt.Logger.INFO)  # 创建 TensorRT 日志记录器
            
            # 读取 TensorRT 引擎文件
            with open(w, "rb") as f, trt.Runtime(logger) as runtime:
                try:
                    # 读取元数据长度(前4字节)
                    meta_len = int.from_bytes(f.read(4), byteorder="little")  # 读取元数据长度
                    
                    # 读取元数据(JSON格式)
                    metadata = json.loads(f.read(meta_len).decode("utf-8"))  # 读取元数据
                    
                    # 检查是否有 DLA(深度学习加速器)设置
                    dla = metadata.get("dla", None)
                    if dla is not None:
                        runtime.DLA_core = int(dla)  # 设置 DLA 核心
                except UnicodeDecodeError:
                    f.seek(0)  # 引擎文件可能没有嵌入的 Ultralytics 元数据,回到文件开头
                
                # 反序列化 CUDA 引擎
                model = runtime.deserialize_cuda_engine(f.read())  # 读取引擎
            
            # 创建模型执行上下文
            try:
                context = model.create_execution_context()
            except Exception as e:  # 模型为 None 时
                LOGGER.error(f"TensorRT model exported with a different version than {trt.__version__}\n")
                raise e  # 重新抛出异常
            
            bindings = OrderedDict()  # 有序字典存储绑定信息
            output_names = []  # 输出名称列表
            fp16 = False  # 默认 FP16 标志为 False,后续会更新
            dynamic = False  # 动态模型标志
            is_trt10 = not hasattr(model, "num_bindings")  # 检查是否是 TRT10+ API(新版本)
            
            # 遍历所有输入/输出张量
            num = range(model.num_io_tensors) if is_trt10 else range(model.num_bindings)
            
            for i in num:
                # 使用 TRT10+ API 或旧 API 获取张量信息
                if is_trt10:  # TRT10+ 新 API
                    name = model.get_tensor_name(i)  # 获取张量名称
                    dtype = trt.nptype(model.get_tensor_dtype(name))  # 获取数据类型
                    is_input = model.get_tensor_mode(name) == trt.TensorIOMode.INPUT  # 是否是输入
                    shape = tuple(model.get_tensor_shape(name))  # 获取形状
                    profile_shape = tuple(model.get_tensor_profile_shape(name, 0)[2]) if is_input else None  # 配置文件形状(仅输入)
                else:  # 旧 API
                    name = model.get_binding_name(i)  # 获取绑定名称
                    dtype = trt.nptype(model.get_binding_dtype(i))  # 获取数据类型
                    is_input = model.binding_is_input(i)  # 是否是输入
                    shape = tuple(model.get_binding_shape(i))  # 获取形状
                    profile_shape = tuple(model.get_profile_shape(0, i)[1]) if is_input else None  # 配置文件形状(仅输入)
                
                # 处理输入/输出张量
                if is_input:  # 如果是输入张量
                    if -1 in shape:  # 如果形状中有 -1,表示是动态形状
                        dynamic = True  # 设置动态标志
                        if is_trt10:
                            context.set_input_shape(name, profile_shape)  # TRT10+ 设置输入形状
                        else:
                            context.set_binding_shape(i, profile_shape)  # 旧 API 设置绑定形状
                    
                    if dtype == np.float16:  # 如果数据类型是 FP16
                        fp16 = True  # 设置 FP16 标志
                else:  # 如果是输出张量
                    output_names.append(name)  # 添加到输出名称列表
                
                # 获取实际形状(可能是动态设置的)
                shape = tuple(context.get_tensor_shape(name)) if is_trt10 else tuple(context.get_binding_shape(i))
                
                # 创建空的张量
                im = torch.from_numpy(np.empty(shape, dtype=dtype)).to(device)
                
                # 存储绑定信息
                bindings[name] = Binding(name, dtype, shape, im, int(im.data_ptr()))
            
            # 创建绑定地址字典(名称->指针)
            binding_addrs = OrderedDict((n, d.ptr) for n, d in bindings.items())
        
        # CoreML
        elif coreml:  # CoreML 格式(*.mlpackage)
            # 检查依赖:coremltools 和特定版本的 numpy
            check_requirements(
                ["coremltools>=9.0", "numpy>=1.14.5,<=2.3.5"]
            )  # 最新的 numpy 2.4.0rc1 会破坏 coremltools 导出
            
            LOGGER.info(f"Loading {w} for CoreML inference...")  # 记录加载信息
            import coremltools as ct  # 导入 CoreML 工具
            
            model = ct.models.MLModel(w)  # 加载 CoreML 模型
            dynamic = model.get_spec().description.input[0].type.HasField("multiArrayType")  # 检查是否是动态输入
            metadata = dict(model.user_defined_metadata)  # 获取用户定义的元数据
        
        # TensorFlow SavedModel
        elif saved_model:  # TensorFlow SavedModel 格式(目录)
            LOGGER.info(f"Loading {w} for TensorFlow SavedModel inference...")  # 记录加载信息
            import tensorflow as tf  # 导入 TensorFlow
            
            model = tf.saved_model.load(w)  # 加载 SavedModel
            metadata = Path(w) / "metadata.yaml"  # 元数据文件路径
        
        # TensorFlow GraphDef
        elif pb:  # TensorFlow GraphDef 格式(*.pb)
            LOGGER.info(f"Loading {w} for TensorFlow GraphDef inference...")  # 记录加载信息
            import tensorflow as tf  # 导入 TensorFlow
            
            from ultralytics.utils.export.tensorflow import gd_outputs  # 导入 GraphDef 输出处理函数
            
            def wrap_frozen_graph(gd, inputs, outputs):
                """包装冻结的图以进行部署。"""
                # 包装函数以导入 GraphDef
                x = tf.compat.v1.wrap_function(lambda: tf.compat.v1.import_graph_def(gd, name=""), [])  # 包装
                ge = x.graph.as_graph_element  # 获取图元素
                # 修剪图以获取指定的输入和输出
                return x.prune(tf.nest.map_structure(ge, inputs), tf.nest.map_structure(ge, outputs))
            
            gd = tf.Graph().as_graph_def()  # 创建空的 GraphDef
            
            # 读取 GraphDef 文件
            with open(w, "rb") as f:
                gd.ParseFromString(f.read())  # 解析 GraphDef
            
            # 包装冻结的图
            frozen_func = wrap_frozen_graph(gd, inputs="x:0", outputs=gd_outputs(gd))
            
            # 尝试在 SavedModel 旁边查找元数据
            try:
                # 查找与 GraphDef 文件同名的 SavedModel 目录中的元数据文件
                metadata = next(Path(w).resolve().parent.rglob(f"{Path(w).stem}_saved_model*/metadata.yaml"))
            except StopIteration:
                pass  # 如果找不到,保持 metadata 为 None
        
        # TFLite 或 TFLite Edge TPU
        elif tflite or edgetpu:  # TensorFlow Lite 或 Edge TPU 格式
            try:  # 尝试导入 tflite_runtime(适用于嵌入式设备)
                from tflite_runtime.interpreter import Interpreter, load_delegate
            except ImportError:  # 如果失败,使用完整的 TensorFlow
                import tensorflow as tf
                Interpreter, load_delegate = tf.lite.Interpreter, tf.lite.experimental.load_delegate
            
            if edgetpu:  # Edge TPU 格式
                # 处理设备字符串:如果以 "tpu" 开头,去掉前缀
                device = device[3:] if str(device).startswith("tpu") else ":0"
                
                LOGGER.info(f"Loading {w} on device {device[1:]} for TensorFlow Lite Edge TPU inference...")
                
                # 根据操作系统选择 Edge TPU 委托库
                delegate = {"Linux": "libedgetpu.so.1", "Darwin": "libedgetpu.1.dylib", "Windows": "edgetpu.dll"}[
                    platform.system()
                ]
                
                # 创建带 Edge TPU 委托的解释器
                interpreter = Interpreter(
                    model_path=w,
                    experimental_delegates=[load_delegate(delegate, options={"device": device})],
                )
                
                device = "cpu"  # 必须设为 CPU,否则 PyTorch 会尝试使用错误的设备
            else:  # 普通 TFLite
                LOGGER.info(f"Loading {w} for TensorFlow Lite inference...")
                interpreter = Interpreter(model_path=w)  # 加载 TFLite 模型
            
            interpreter.allocate_tensors()  # 分配张量内存
            input_details = interpreter.get_input_details()  # 获取输入详情
            output_details = interpreter.get_output_details()  # 获取输出详情
            
            # 尝试加载元数据
            try:
                # TFLite 模型通常是 ZIP 文件,包含元数据
                with zipfile.ZipFile(w, "r") as zf:
                    name = zf.namelist()[0]  # 获取第一个文件名
                    contents = zf.read(name).decode("utf-8")  # 读取内容
                    
                    if name == "metadata.json":  # Ultralytics 自定义元数据(Python>=3.12)
                        metadata = json.loads(contents)  # 解析 JSON
                    else:
                        metadata = ast.literal_eval(contents)  # 默认的 tflite-support 元数据(Python<=3.11)
            except (zipfile.BadZipFile, SyntaxError, ValueError, json.JSONDecodeError):
                pass  # 如果解析失败,保持 metadata 为 None
        
        # TensorFlow.js
        elif tfjs:  # TensorFlow.js 格式(目前不支持)
            raise NotImplementedError("Ultralytics TF.js inference is not currently supported.")
        
        # PaddlePaddle
        elif paddle:  # PaddlePaddle 格式
            LOGGER.info(f"Loading {w} for PaddlePaddle inference...")  # 记录加载信息
            
            # 根据是否使用 CUDA 选择不同的 PaddlePaddle 版本
            check_requirements(
                "paddlepaddle-gpu>=3.0.0,!=3.3.0"  # GPU 版本,排除 3.3.0(有问题)
                if torch.cuda.is_available()
                else "paddlepaddle==3.0.0"  # ARM64 平台固定 3.0.0 版本
                if ARM64
                else "paddlepaddle>=3.0.0,!=3.3.0"  # CPU 版本,排除 3.3.0
            )
            
            import paddle.inference as pdi  # 导入 PaddlePaddle 推理模块
            
            w = Path(w)  # 转换为 Path 对象
            model_file, params_file = None, None  # 初始化模型文件和参数文件
            
            # 如果是目录,查找其中的文件
            if w.is_dir():
                model_file = next(w.rglob("*.json"), None)  # 查找 JSON 模型文件
                params_file = next(w.rglob("*.pdiparams"), None)  # 查找参数文件
            elif w.suffix == ".pdiparams":  # 如果是参数文件
                model_file = w.with_name("model.json")  # 对应的模型文件
                params_file = w  # 参数文件
            
            # 检查文件是否存在
            if not (model_file and params_file and model_file.is_file() and params_file.is_file()):
                raise FileNotFoundError(f"Paddle model not found in {w}. Both .json and .pdiparams files are required.")
            
            # 创建配置
            config = pdi.Config(str(model_file), str(params_file))
            
            if cuda:  # 如果使用 CUDA
                config.enable_use_gpu(memory_pool_init_size_mb=2048, device_id=0)  # 启用 GPU
            
            predictor = pdi.create_predictor(config)  # 创建预测器
            input_handle = predictor.get_input_handle(predictor.get_input_names()[0])  # 获取输入句柄
            output_names = predictor.get_output_names()  # 获取输出名称
            metadata = w / "metadata.yaml"  # 元数据文件路径
        
        # MNN
        elif mnn:  # MNN 格式(阿里巴巴的移动端推理框架)
            LOGGER.info(f"Loading {w} for MNN inference...")  # 记录加载信息
            
            check_requirements("MNN")  # 检查 MNN 依赖
            import os
            import MNN  # 导入 MNN
            
            # 配置 MNN 运行时:低精度、CPU 后端、线程数设置为 CPU 核心数的一半
            config = {"precision": "low", "backend": "CPU", "numThread": (os.cpu_count() + 1) // 2}
            
            rt = MNN.nn.create_runtime_manager((config,))  # 创建运行时管理器
            net = MNN.nn.load_module_from_file(w, [], [], runtime_manager=rt, rearrange=True)  # 加载模型
            
            # 定义 PyTorch 张量到 MNN 张量的转换函数
            def torch_to_mnn(x):
                return MNN.expr.const(x.data_ptr(), x.shape)
            
            # 从模型信息中获取元数据
            metadata = json.loads(net.get_info()["bizCode"])
        
        # NCNN
        elif ncnn:  # NCNN 格式(腾讯的移动端推理框架)
            LOGGER.info(f"Loading {w} for NCNN inference...")  # 记录加载信息
            
            if ARM64:  # ARM64 架构不支持
                raise NotImplementedError(
                    "NCNN inference is not supported on ARM64"
                )  # https://github.com/Tencent/ncnn/issues/6509
            
            check_requirements("ncnn", cmds="--no-deps")  # 检查 NCNN 依赖
            import ncnn as pyncnn  # 导入 NCNN
            
            net = pyncnn.Net()  # 创建 NCNN 网络
            
            # 设置 Vulkan 计算(GPU 加速)
            if isinstance(cuda, torch.device):
                net.opt.use_vulkan_compute = cuda  # 根据 CUDA 设备设置
            elif isinstance(device, str) and device.startswith("vulkan"):  # 如果是 Vulkan 设备
                net.opt.use_vulkan_compute = True  # 启用 Vulkan
                net.set_vulkan_device(int(device.split(":")[1]))  # 设置 Vulkan 设备 ID
                device = torch.device("cpu")  # PyTorch 设备设为 CPU
            
            w = Path(w)  # 转换为 Path 对象
            
            # 如果不是 *.param 文件,则在目录中查找
            if not w.is_file():  # 如果不是文件
                w = next(w.glob("*.param"))  # 从 *_ncnn_model 目录获取 *.param 文件
            
            net.load_param(str(w))  # 加载网络参数
            net.load_model(str(w.with_suffix(".bin")))  # 加载模型权重(.bin 文件)
            metadata = w.parent / "metadata.yaml"  # 元数据文件路径
        
        # NVIDIA Triton Inference Server
        elif triton:  # Triton 推理服务器
            check_requirements("tritonclient[all]")  # 检查 Triton 客户端依赖
            from ultralytics.utils.triton import TritonRemoteModel  # 导入 Triton 远程模型
            
            model = TritonRemoteModel(w)  # 创建 Triton 远程模型
            metadata = model.metadata  # 获取元数据
        
        # RKNN
        elif rknn:  # Rockchip NPU 格式
            if not is_rockchip():  # 检查是否是 Rockchip 设备
                raise OSError("RKNN inference is only supported on Rockchip devices.")
            
            LOGGER.info(f"Loading {w} for RKNN inference...")  # 记录加载信息
            
            check_requirements("rknn-toolkit-lite2")  # 检查 RKNN 依赖
            from rknnlite.api import RKNNLite  # 导入 RKNN Lite API
            
            w = Path(w)  # 转换为 Path 对象
            
            # 如果不是 *.rknn 文件,则在目录中查找
            if not w.is_file():  # 如果不是文件
                w = next(w.rglob("*.rknn"))  # 从 *_rknn_model 目录获取 *.rknn 文件
            
            rknn_model = RKNNLite()  # 创建 RKNN Lite 实例
            rknn_model.load_rknn(str(w))  # 加载 RKNN 模型
            rknn_model.init_runtime()  # 初始化运行时
            metadata = w.parent / "metadata.yaml"  # 元数据文件路径
        
        # Axelera
        elif axelera:  # Axelera AI 芯片格式
            import os
            
            # 检查 Axelera 运行时环境是否激活
            if not os.environ.get("AXELERA_RUNTIME_DIR"):
                LOGGER.warning(
                    "Axelera runtime environment is not activated."
                    "\nPlease run: source /opt/axelera/sdk/latest/axelera_activate.sh"
                    "\n\nIf this fails, verify driver installation: https://docs.ultralytics.com/integrations/axelera/#axelera-driver-installation"
                )
            
            try:
                from axelera.runtime import op  # 尝试导入 Axelera 运行时
            except ImportError:
                # 如果导入失败,安装依赖
                check_requirements(
                    "axelera_runtime2==0.1.2",
                    cmds="--extra-index-url https://software.axelera.ai/artifactory/axelera-runtime-pypi",
                )
            from axelera.runtime import op  # 导入 Axelera 运行时
            
            w = Path(w)  # 转换为 Path 对象
            
            # 查找 .axm 文件
            if (found := next(w.rglob("*.axm"), None)) is None:
                raise FileNotFoundError(f"No .axm file found in: {w}")
            
            ax_model = op.load(str(found))  # 加载 Axelera 模型
            metadata = found.parent / "metadata.yaml"  # 元数据文件路径
        
        # ExecuTorch
        elif pte:  # PyTorch ExecuTorch 格式(*.pte)
            LOGGER.info(f"Loading {w} for ExecuTorch inference...")  # 记录加载信息
            
            # 兼容性修复
            check_requirements("setuptools<71.0.0")  # Setuptools bug 修复
            check_requirements(("executorch==1.0.1", "flatbuffers"))  # 检查 ExecuTorch 依赖
            from executorch.runtime import Runtime  # 导入 ExecuTorch 运行时
            
            w = Path(w)  # 转换为 Path 对象
            
            # 如果是目录,查找其中的 .pte 文件
            if w.is_dir():
                model_file = next(w.rglob("*.pte"))  # 查找 .pte 文件
                metadata = w / "metadata.yaml"  # 元数据文件路径
            else:  # 如果是单个文件
                model_file = w  # 模型文件
                metadata = w.parent / "metadata.yaml"  # 元数据文件在父目录
            
            program = Runtime.get().load_program(str(model_file))  # 加载程序
            model = program.load_method("forward")  # 加载 forward 方法
        
        # 其他格式(不支持)
        else:
            from ultralytics.engine.exporter import export_formats  # 导入支持的导出格式
            
            raise TypeError(
                f"model='{w}' is not a supported model format. Ultralytics supports: {export_formats()['Format']}\n"
                f"See https://docs.ultralytics.com/modes/predict for help."
            )
        
        # 加载外部元数据 YAML 文件
        if isinstance(metadata, (str, Path)) and Path(metadata).exists():
            metadata = YAML.load(metadata)  # 加载 YAML 文件
        
        # 处理元数据
        if metadata and isinstance(metadata, dict):
            for k, v in metadata.items():
                if k in {"stride", "batch", "channels"}:  # 整数类型字段
                    metadata[k] = int(v)  # 转换为整数
                elif k in {"imgsz", "names", "kpt_shape", "kpt_names", "args"} and isinstance(v, str):
                    metadata[k] = ast.literal_eval(v)  # 使用 ast 安全地评估字符串表达式
            
            # 从元数据中提取关键信息
            stride = metadata["stride"]  # 步长
            task = metadata["task"]  # 任务类型
            batch = metadata["batch"]  # 批次大小
            imgsz = metadata["imgsz"]  # 图像大小
            names = metadata["names"]  # 类别名称
            kpt_shape = metadata.get("kpt_shape")  # 关键点形状(姿态估计)
            kpt_names = metadata.get("kpt_names")  # 关键点名称(姿态估计)
            end2end = metadata.get("end2end", False) or metadata.get("args", {}).get("nms", False)  # 端到端模型标志
            dynamic = metadata.get("args", {}).get("dynamic", dynamic)  # 动态模型标志
            ch = metadata.get("channels", 3)  # 通道数,默认3
        elif not (pt or triton or nn_module):  # 如果不是 PyTorch、Triton 或 nn.Module 格式
            LOGGER.warning(f"Metadata not found for 'model={w}'")  # 警告:找不到元数据
        
        # 检查类别名称
        if "names" not in locals():  # 如果 names 变量不存在
            names = default_class_names(data)  # 使用默认类别名称
        
        names = check_class_names(names)  # 检查并处理类别名称
        
        # 将当前函数的所有局部变量赋值给 self 对象
        # 这样所有加载的模型、配置、标志等都成为对象的属性
        self.__dict__.update(locals())  # 将所有变量分配给 self
AutoBackend.forward 方法
python 复制代码
    def forward(
        self,
        im: torch.Tensor,  # 输入图像张量
        augment: bool = False,  # 是否进行数据增强
        visualize: bool = False,  # 是否可视化输出预测
        embed: list | None = None,  # 要返回的特征向量/嵌入列表
        **kwargs: Any,  # 模型配置的额外关键字参数
    ) -> torch.Tensor | list[torch.Tensor]:
        """
        在 AutoBackend 模型上运行推理。

        参数:
            im (torch.Tensor): 要进行推理的图像张量。
            augment (bool): 是否在推理期间执行数据增强。
            visualize (bool): 是否可视化输出预测。
            embed (list, optional): 要返回的特征向量/嵌入列表。
            **kwargs (Any): 模型配置的额外关键字参数。

        返回:
            (torch.Tensor | list[torch.Tensor]): 来自模型的原始输出张量。
        """
        _b, _ch, h, w = im.shape  # 获取批次大小、通道数、高度、宽度
        
        # 处理 FP16 精度
        if self.fp16 and im.dtype != torch.float16:
            im = im.half()  # 转换为 FP16
        
        # 处理 NHWC 格式(某些后端需要)
        if self.nhwc:
            # 将 PyTorch 的 BCHW 格式转换为 NumPy 的 BHWC 格式
            # shape(1,320,192,3) 表示批次1,高度320,宽度192,通道3
            im = im.permute(0, 2, 3, 1)  # torch BCHW 到 numpy BHWC 形状(1,320,192,3)
        
        # PyTorch 模型推理
        if self.pt or self.nn_module:
            # 直接调用模型的 forward 方法
            y = self.model(im, augment=augment, visualize=visualize, embed=embed, **kwargs)
        
        # TorchScript 模型推理
        elif self.jit:
            # TorchScript 模型只有基本的 forward 方法
            y = self.model(im)
        
        # ONNX OpenCV DNN 推理
        elif self.dnn:
            im = im.cpu().numpy()  # 将张量转换为 NumPy 数组
            self.net.setInput(im)  # 设置输入
            y = self.net.forward()  # 前向传播
        
        # ONNX Runtime 推理
        elif self.onnx or self.imx:
            if self.use_io_binding:  # 如果使用 IO 绑定
                if not self.cuda:  # 如果不是 CUDA
                    im = im.cpu()  # 将张量移到 CPU
                
                # 绑定输入
                self.io.bind_input(
                    name="images",  # 输入名称
                    device_type=im.device.type,  # 设备类型
                    device_id=im.device.index if im.device.type == "cuda" else 0,  # 设备 ID
                    element_type=np.float16 if self.fp16 else np.float32,  # 数据类型
                    shape=tuple(im.shape),  # 形状
                    buffer_ptr=im.data_ptr(),  # 数据指针
                )
                
                self.session.run_with_iobinding(self.io)  # 使用 IO 绑定运行
                y = self.bindings  # 获取输出
            else:  # 不使用 IO 绑定
                im = im.cpu().numpy()  # 将张量转换为 NumPy 数组
                # 运行推理,提供输入字典
                y = self.session.run(self.output_names, {self.session.get_inputs()[0].name: im})
            
            # IMX 特定后处理
            if self.imx:
                if self.task == "detect":  # 检测任务
                    # 合并边界框、置信度和类别:boxes, conf, cls
                    y = np.concatenate([y[0], y[1][:, :, None], y[2][:, :, None]], axis=-1)
                elif self.task == "pose":  # 姿态估计任务
                    # 合并边界框、置信度、关键点:boxes, conf, kpts
                    y = np.concatenate([y[0], y[1][:, :, None], y[2][:, :, None], y[3]], axis=-1, dtype=y[0].dtype)
                elif self.task == "segment":  # 分割任务
                    y = (
                        np.concatenate([y[0], y[1][:, :, None], y[2][:, :, None], y[3]], axis=-1, dtype=y[0].dtype),
                        y[4],
                    )
        
        # OpenVINO 推理
        elif self.xml:
            im = im.cpu().numpy()  # 转换为 NumPy 数组,保持 FP32
            
            # 根据推理模式选择不同的推理策略
            if self.inference_mode in {"THROUGHPUT", "CUMULATIVE_THROUGHPUT"}:  # 针对大批次优化
                n = im.shape[0]  # 批次中的图像数量
                results = [None] * n  # 预分配结果列表,使用 None 填充
                
                def callback(request, userdata):
                    """将结果放入预分配列表中,使用 userdata 索引。"""
                    results[userdata] = request.results  # 存储结果
                
                # 创建异步推理队列
                async_queue = self.ov.AsyncInferQueue(self.ov_compiled_model)
                async_queue.set_callback(callback)  # 设置回调函数
                
                # 为每个输入图像启动异步推理
                for i in range(n):
                    # 启动异步推理,userdata=i 指定结果在列表中的位置
                    async_queue.start_async(inputs={self.input_name: im[i:i+1]}, userdata=i)  # 保持图像为 BCHW
                
                async_queue.wait_all()  # 等待所有推理请求完成
                
                # 处理结果:将每个图像的输出合并
                y = [list(r.values()) for r in results]
                y = [np.concatenate(x) for x in zip(*y)]
            else:  # 推理模式 = "LATENCY",针对批次大小为1的首次结果最快优化
                y = list(self.ov_compiled_model(im).values())
        
        # TensorRT 推理
        elif self.engine:
            # 处理动态形状
            if self.dynamic and im.shape != self.bindings["images"].shape:
                if self.is_trt10:  # TRT10+ API
                    self.context.set_input_shape("images", im.shape)  # 设置输入形状
                    self.bindings["images"] = self.bindings["images"]._replace(shape=im.shape)  # 更新绑定形状
                    
                    # 更新所有输出绑定的形状
                    for name in self.output_names:
                        self.bindings[name].data.resize_(tuple(self.context.get_tensor_shape(name)))
                else:  # 旧 API
                    i = self.model.get_binding_index("images")  # 获取输入绑定索引
                    self.context.set_binding_shape(i, im.shape)  # 设置绑定形状
                    self.bindings["images"] = self.bindings["images"]._replace(shape=im.shape)  # 更新绑定形状
                    
                    # 更新所有输出绑定的形状
                    for name in self.output_names:
                        i = self.model.get_binding_index(name)  # 获取输出绑定索引
                        self.bindings[name].data.resize_(tuple(self.context.get_binding_shape(i)))
            
            # 检查输入形状是否符合要求
            s = self.bindings["images"].shape  # 模型期望的形状
            # 断言:输入形状必须等于(静态)或小于等于(动态)模型最大形状
            assert im.shape == s, f"input size {im.shape} {'>' if self.dynamic else 'not equal to'} max model size {s}"
            
            # 更新输入绑定的数据指针
            self.binding_addrs["images"] = int(im.data_ptr())
            
            # 执行推理
            self.context.execute_v2(list(self.binding_addrs.values()))
            
            # 获取输出数据,按输出名称排序
            y = [self.bindings[x].data for x in sorted(self.output_names)]
        
        # CoreML 推理
        elif self.coreml:
            im = im.cpu().numpy()  # 转换为 NumPy 数组
            
            if self.dynamic:  # 动态模型
                im = im.transpose(0, 3, 1, 2)  # 转换格式
            else:  # 静态模型
                # 将 NumPy 数组转换为 PIL 图像
                im = Image.fromarray((im[0] * 255).astype("uint8"))
            
            # 运行预测
            y = self.model.predict({"image": im})  # 坐标是归一化的 xywh
            
            # 如果输出包含 "confidence",说明 NMS 已包含在模型中
            if "confidence" in y:
                from ultralytics.utils.ops import xywh2xyxy  # 导入坐标转换函数
                
                # 将归一化的 xywh 转换为像素坐标的 xyxy
                box = xywh2xyxy(y["coordinates"] * [[w, h, w, h]])  # xyxy 像素
                
                # 获取最大置信度的类别
                cls = y["confidence"].argmax(1, keepdims=True)
                
                # 合并边界框、置信度和类别
                y = np.concatenate((box, np.take_along_axis(y["confidence"], cls, axis=1), cls), 1)[None]
            else:  # 原始输出
                y = list(y.values())
                
                # 分割模型特殊处理:输出顺序可能相反
                if len(y) == 2 and len(y[1].shape) != 4:  # 分割模型
                    y = list(reversed(y))  # 反转分割模型的输出顺序 (pred, proto)
        
        # PaddlePaddle 推理
        elif self.paddle:
            im = im.cpu().numpy().astype(np.float32)  # 转换为 NumPy 数组,FP32
            self.input_handle.copy_from_cpu(im)  # 复制输入数据
            self.predictor.run()  # 运行推理
            y = [self.predictor.get_output_handle(x).copy_to_cpu() for x in self.output_names]  # 获取输出
        
        # MNN 推理
        elif self.mnn:
            input_var = self.torch_to_mnn(im)  # 转换为 MNN 张量
            output_var = self.net.onForward([input_var])  # 前向传播
            y = [x.read() for x in output_var]  # 读取输出
        
        # NCNN 推理
        elif self.ncnn:
            # 创建 NCNN Mat 对象
            mat_in = self.pyncnn.Mat(im[0].cpu().numpy())
            
            with self.net.create_extractor() as ex:  # 创建提取器
                ex.input(self.net.input_names()[0], mat_in)  # 设置输入
                
                # 警告:'output_names' 排序是为了临时修复 pnnx 问题
                y = [np.array(ex.extract(x)[1])[None] for x in sorted(self.net.output_names())]  # 提取输出
        
        # NVIDIA Triton 推理服务器
        elif self.triton:
            im = im.cpu().numpy()  # 转换为 NumPy 数组
            y = self.model(im)  # 远程推理
        
        # RKNN 推理
        elif self.rknn:
            # 转换为 uint8 并缩放到 0-255(图像通常需要)
            im = (im.cpu().numpy() * 255).astype("uint8")
            im = im if isinstance(im, (list, tuple)) else [im]  # 确保是列表
            y = self.rknn_model.inference(inputs=im)  # 推理
        
        # Axelera 推理
        elif self.axelera:
            y = self.ax_model(im.cpu())  # Axelera 模型需要 CPU 张量
        
        # ExecuTorch 推理
        elif self.pte:
            y = self.model.execute([im])  # 执行推理
        
        # TensorFlow 推理(SavedModel, GraphDef, Lite, Edge TPU)
        else:
            im = im.cpu().numpy()  # 转换为 NumPy 数组
            
            if self.saved_model:  # SavedModel
                y = self.model.serving_default(im)  # 使用 serving_default 签名
                if not isinstance(y, list):  # 如果不是列表
                    y = [y]  # 转换为列表
            
            elif self.pb:  # GraphDef
                y = self.frozen_func(x=self.tf.constant(im))  # 调用冻结的函数
            
            else:  # Lite 或 Edge TPU
                details = self.input_details[0]  # 获取输入详情
                is_int = details["dtype"] in {np.int8, np.int16}  # 检查是否是 TFLite 量化 int8 或 int16 模型
                
                if is_int:  # 如果是量化模型
                    scale, zero_point = details["quantization"]  # 获取量化的缩放因子和零点
                    im = (im / scale + zero_point).astype(details["dtype"])  # 反缩放
                
                self.interpreter.set_tensor(details["index"], im)  # 设置输入张量
                self.interpreter.invoke()  # 调用解释器
                
                y = []  # 输出列表
                for output in self.output_details:  # 遍历所有输出
                    x = self.interpreter.get_tensor(output["index"])  # 获取输出张量
                    
                    if is_int:  # 如果是量化模型
                        scale, zero_point = output["quantization"]  # 获取输出量化参数
                        x = (x.astype(np.float32) - zero_point) * scale  # 重新缩放
                    
                    # 反归一化处理(非分类任务)
                    if x.ndim == 3:  # 如果任务不是分类(不包括掩码,ndim=4)
                        # 通过图像大小反归一化 xywh。参见:https://github.com/ultralytics/ultralytics/pull/1695
                        # 在 TFLite/EdgeTPU 中 xywh 被归一化,以减轻整数模型的量化误差
                        if x.shape[-1] == 6 or self.end2end:  # 端到端模型
                            # 对边界框坐标进行反归一化
                            x[:, :, [0, 2]] *= w  # x 和 width 乘以宽度
                            x[:, :, [1, 3]] *= h  # y 和 height 乘以高度
                            
                            if self.task == "pose":  # 姿态估计任务
                                # 对关键点坐标进行反归一化
                                x[:, :, 6::3] *= w  # 关键点的 x 坐标
                                x[:, :, 7::3] *= h  # 关键点的 y 坐标
                        else:
                            # 标准检测模型
                            x[:, [0, 2]] *= w
                            x[:, [1, 3]] *= h
                            
                            if self.task == "pose":
                                x[:, 5::3] *= w
                                x[:, 6::3] *= h
                    y.append(x)  # 添加到输出列表
            
            # TensorFlow 分割修复:导出顺序与 ONNX 导出相反,且原型被转置
            if len(y) == 2:  # 分割输出,顺序为 (det, proto) 但与 ONNX 相反
                if len(y[1].shape) != 4:  # 如果第二个输出不是4维
                    y = list(reversed(y))  # 应该是 y = (1, 116, 8400), (1, 160, 160, 32)
                
                if y[1].shape[-1] == 6:  # 端到端模型
                    y = [y[1]]  # 只保留一个输出
                else:
                    # 转置原型输出:从 (0, 3, 1, 2) 到 (0, 1, 2, 3)
                    y[1] = np.transpose(y[1], (0, 3, 1, 2))  # 应该是 y = (1, 116, 8400), (1, 32, 160, 160)
            
            # 确保所有输出都是 NumPy 数组
            y = [x if isinstance(x, np.ndarray) else x.numpy() for x in y]
        
        # 后处理:统一输出格式
        if isinstance(y, (list, tuple)):  # 如果输出是列表或元组
            # 特殊处理:如果类别名称未定义且是分割任务或有两个输出
            if len(self.names) == 999 and (self.task == "segment" or len(y) == 2):
                # 计算类别数量:总输出维度 - 掩码维度 - 4(边界框坐标)
                nc = y[0].shape[1] - y[1].shape[1] - 4  # y = (1, 32, 160, 160), (1, 116, 8400)
                self.names = {i: f"class{i}" for i in range(nc)}  # 创建默认类别名称
            
            # 返回单个张量或张量列表
            return self.from_numpy(y[0]) if len(y) == 1 else [self.from_numpy(x) for x in y]
        else:  # 单个输出
            return self.from_numpy(y)
AutoBackend.from_numpy 方法
python 复制代码
def from_numpy(self, x: np.ndarray | torch.Tensor) -> torch.Tensor:
        """
        将 NumPy 数组转换为模型设备上的 torch 张量。

        参数:
            x (np.ndarray | torch.Tensor): 输入数组或张量。

        返回:
            (torch.Tensor): 位于 `self.device` 上的张量。
        """
        # 如果是 NumPy 数组,转换为 torch 张量并移动到模型设备
        # 如果是 torch 张量,直接返回
        return torch.tensor(x).to(self.device) if isinstance(x, np.ndarray) else x
AutoBackend.warmup 方法
python 复制代码
def warmup(self, imgsz: tuple[int, int, int, int] = (1, 3, 640, 640)) -> None:
        """
        通过使用虚拟输入运行一次前向传递来预热模型。

        参数:
            imgsz (tuple[int, int, int, int]): 虚拟输入形状,格式为 (批次, 通道, 高度, 宽度)。
        """
        # 确定哪些模型类型需要预热
        warmup_types = self.pt, self.jit, self.onnx, self.engine, self.saved_model, self.pb, self.triton, self.nn_module
        
        # 如果模型需要预热且设备不是 CPU(或使用 Triton)
        if any(warmup_types) and (self.device.type != "cpu" or self.triton):
            # 创建空的输入张量
            im = torch.empty(*imgsz, dtype=torch.half if self.fp16 else torch.float, device=self.device)  # 输入
            
            # TorchScript 需要两次预热,其他只需要一次
            for _ in range(2 if self.jit else 1):
                self.forward(im)  # 预热模型
                
                # 创建随机的边界框张量用于预热 NMS
                warmup_boxes = torch.rand(1, 84, 16, device=self.device)  # 16个边界框在经验上效果最好
                warmup_boxes[:, :4] *= imgsz[-1]  # 根据图像大小缩放边界框坐标
                
                non_max_suppression(warmup_boxes)  # 预热 NMS
AutoBackend._model_type 静态方法
python 复制代码
staticmethod
    def _model_type(p: str = "path/to/model.pt") -> list[bool]:
        """
        获取模型文件路径并返回模型类型。

        参数:
            p (str): 模型文件路径。

        返回:
            (list[bool]): 表示模型类型的布尔值列表。

        示例:
            >>> types = AutoBackend._model_type("path/to/model.onnx")
            >>> assert types[2]  # onnx
        """
        from ultralytics.engine.exporter import export_formats  # 导入支持的导出格式
        
        sf = export_formats()["Suffix"]  # 导出后缀列表
        
        # 检查路径是否是 URL 或字符串
        if not is_url(p) and not isinstance(p, str):
            check_suffix(p, sf)  # 检查文件后缀
        
        name = Path(p).name  # 获取文件名
        
        # 检查文件名是否包含各种格式的后缀
        types = [s in name for s in sf]  # 创建布尔列表,表示每种格式
        
        types[5] |= name.endswith(".mlmodel")  # 保留对旧版 Apple CoreML *.mlmodel 格式的支持
        
        types[8] &= not types[9]  # tflite 格式且不是 edgetpu 格式
        
        # 如果找到任何支持的格式
        if any(types):
            triton = False  # 不是 Triton
        else:  # 如果没有找到已知格式,检查是否是 Triton URL
            from urllib.parse import urlsplit  # 导入 URL 解析
            
            url = urlsplit(p)  # 解析 URL
            # 检查是否是 Triton 服务器 URL
            triton = bool(url.netloc) and bool(url.path) and url.scheme in {"http", "grpc"}
        
        # 返回所有格式的标志列表,包括 Triton
        return [*types, triton]
内存和计算优化策略如下
错误处理层级

实例化使用流程如下:

python 复制代码
# 可视化使用流程
1. 初始化
   ↓
model = AutoBackend(
    model="yolov8n.pt",  # 可以是任何支持格式
    device="cuda:0",      # 自动选择设备
    fp16=True            # 启用半精度
)

2. 预热(可选)
   ↓
model.warmup(imgsz=(1, 3, 640, 640))

3. 推理
   ↓
results = model(im)  # im: torch.Tensor [B, C, H, W]

4. 后处理(在其他模块)
   ↓
detections = non_max_suppression(results)

AutoBackend 模块是 Ultralytics 的统一推理后端管理器,它的核心设计理念是抽象化统一接口。通过自动检测模型格式并加载对应的推理引擎,为用户提供了完全透明的多格式模型推理能力。模块支持从 PyTorch 到边缘计算芯片的20多种格式,自动处理设备分配、数据预处理和后处理,让开发者能够专注于应用逻辑而不必关心底层实现细节。

相关推荐
ZCXZ12385296a2 小时前
YOLO13-C3k2-ConverseB改进:CCTV监控场景下车辆目标检测详解
人工智能·目标检测·目标跟踪
喵叔哟2 小时前
01-YOLO最新版到底新在哪
yolo
无人装备硬件开发爱好者3 小时前
RV1126B 边缘端 AI 实战:YOLOv8+DNTR 微小目标跟踪监测全栈实现 1
人工智能·yolo·目标跟踪
2501_941322033 小时前
基于YOLOv8的汽车车损检测与评估系统_16种损伤类型识别
yolo·汽车
LASDAaaa12313 小时前
电力巡检实战:基于YOLOv8-SEG-P6的输电线路鸟类检测与识别技术详解
yolo
Piar1231sdafa4 小时前
YOLOv5-AIFI改进_爆炸物检测与识别系统_实现与应用
yolo
Faker66363aaa4 小时前
YOLO11改进蚊虫目标检测模型,AttheHead注意力机制提升检测精度
人工智能·目标检测·计算机视觉
zy_destiny4 小时前
【工业场景】用YOLOv26实现4种输电线隐患检测
人工智能·深度学习·算法·yolo·机器学习·计算机视觉·输电线隐患识别
雍凉明月夜4 小时前
深度学习之目标检测yolo算法Ⅴ-YOLOv8
深度学习·yolo·目标检测