YOLO26 实测记录:从模型下载、预测验证到 ONNX Runtime 推理部署

1. 前言

本文记录一次 YOLO26 模型的完整测试流程,主要包括环境验证、官方模型下载、测试模型预测、自定义数据集训练、模型导出为 ONNX、.pt.onnx 推理结果对比,以及使用 ONNX Runtime 手动验证导出的 ONNX 模型。

这篇文章的目标不是系统分析 YOLO26 的网络结构,而是从工程实践角度梳理一套可复现的测试流程。整个流程可以概括为:

模型环境验证 → 官方模型预测 → 自定义数据训练 → 模型导出 ONNX → PT 与 ONNX 结果对比 → ONNX Runtime 独立推理验证。

这样做的意义在于,不能只看模型能训练、能导出,还需要确认导出的模型在不同推理后端下结果一致。尤其在后续进入 RKNN、TensorRT、OpenVINO 或其他端侧部署前,ONNX Runtime 是一个非常重要的中间验证环节。

2. YOLO26 简要说明

YOLO26 是 Ultralytics YOLO 系列中的新一代目标检测模型,仍然延续了 Ultralytics 统一接口的使用方式,可以通过 YOLO() 完成模型加载、训练、验证、预测和导出。Ultralytics 官方文档说明,YOLO26 面向部署效率进行了优化,包括原生端到端 NMS-free 推理、移除 DFL 回归结构、轻量化检测头以及更适合导出的融合模型结构。

从工程使用角度看,YOLO26 的一个明显变化是导出的 ONNX 输出形式与传统 YOLO 模型不同。传统 YOLO 模型常见输出可能是类似 [1, 84, 8400] 的原始检测头结果,需要额外进行 decode、置信度筛选和 NMS。而本文测试的 YOLO26 ONNX 模型输出为:

复制代码
[1, 300, 6]

这个输出更接近最终检测结果格式,可以理解为最多输出 300 个检测目标,每个目标包含 6 个参数。

用数学形式表示,模型输出为:

其中:

复制代码
1   表示 batch size,即一次输入 1 张图片;
300 表示最多输出 300 个检测结果;
6   表示每个检测结果包含 6 个参数。

每个检测结果的 6 个参数可以表示为:

含义如下:

复制代码
x1       检测框左上角 x 坐标;
y1       检测框左上角 y 坐标;
x2       检测框右下角 x 坐标;
y2       检测框右下角 y 坐标;
score    检测置信度;
class_id 类别编号。

因此,对于该模型而言,传统 YOLO 中最复杂的 decode 和 NMS 后处理已经大幅简化。但这并不意味着完全不需要预处理和后处理。输入端仍然需要完成 resize、letterbox、颜色通道转换、归一化和维度转换;输出端仍然需要完成置信度过滤、类别映射、坐标还原和结果绘制。

3. 测试环境

本文测试环境如下:

复制代码
操作系统:Windows
Python:3.8.19
Ultralytics:8.4.60
PyTorch:2.1.0+cu118
ONNX:1.14.1
ONNX Runtime:1.18.1
GPU:NVIDIA GeForce RTX 3060 Ti 8GB
CUDA:可用

训练日志中可以看到 GPU 被正常识别:

复制代码
Ultralytics 8.4.60  Python-3.8.19 torch-2.1.0+cu118 CUDA:0 (NVIDIA GeForce RTX 3060 Ti, 8192MiB)

ONNX Runtime 独立推理时,本文使用的是 CPU 后端:

复制代码
Using ONNX Runtime 1.18.1 with CPUExecutionProvider

这里需要说明一点:训练时使用 GPU,ONNX Runtime 验证时使用 CPU,是为了验证模型在通用 ONNX 推理后端上的可用性。

4. YOLO26 下载与版本验证

Ultralytics 官方文档说明,可以直接使用 YOLO("yolo26n.pt") 加载官方模型,如果本地没有对应权重文件,程序会自动尝试下载。YOLO26 也支持通过 Ultralytics 的统一接口导出为 ONNX 等格式。

在实际测试中,建议先做两件事。

第一,确认当前导入的 ultralytics 包来自哪里。因为如果项目目录下本身存在一个 ultralytics 源码文件夹,那么 Python 会优先导入当前项目路径下的源码,而不是虚拟环境 site-packages 中安装的版本。

第二,确认 Ultralytics 版本。本文使用版本为:

复制代码
Ultralytics 8.4.60

这一步很重要,因为 YOLO26 属于较新的模型,较旧版本的 Ultralytics 可能无法正常识别模型结构或导出格式。

验证代码:

python 复制代码
import ultralytics
from ultralytics import YOLO

print("ultralytics 导入路径:")
print(ultralytics.__file__)

try:
    print("ultralytics 版本:", ultralytics.__version__)
except Exception as e:
    print("无法读取版本:", e)

model = YOLO("yolo26n.pt")
# print(model)

测试结果:

ultralytics 导入路径:

D:\Pycharm_project\Yolo26\ultralytics\init.py

ultralytics 版本: 8.4.60

5. 官方模型预测测试

在完成环境验证后,首先使用官方 yolo26n.pt 模型对测试图片进行预测。测试图片来自 Ultralytics 项目中的 assets 目录。

python 复制代码
from ultralytics import YOLO
import os

def main():
    model_path = "yolo26n.pt"
    image_path = r"D:\Pycharm_project\Yolo26\ultralytics\assets"

    model = YOLO(model_path)

    results = model.predict(
        source=image_path,
        imgsz=640,
        conf=0.25,
        device="cpu",
        save=True,
        show=False
    )

    # 打印本次结果保存目录
    if len(results) > 0:
        print("\n预测结果保存目录:")
        print(results[0].save_dir)

    # 按图片分别打印检测结果
    for i, result in enumerate(results):
        image_name = os.path.basename(result.path)

        print("\n" + "=" * 80)
        print(f"第 {i + 1} 张图片:{image_name}")
        print(f"原始图片路径:{result.path}")
        print("=" * 80)

        if result.boxes is None or len(result.boxes) == 0:
            print("未检测到目标")
            continue

        for j, box in enumerate(result.boxes):
            cls_id = int(box.cls[0])
            conf = float(box.conf[0])
            x1, y1, x2, y2 = box.xyxy[0].tolist()

            print(
                f"目标 {j + 1}: "
                f"类别={result.names[cls_id]}, "
                f"置信度={conf:.3f}, "
                f"框=[{x1:.1f}, {y1:.1f}, {x2:.1f}, {y2:.1f}]"
            )

    print("\n预测完成。")
    # input("按回车键退出...")


if __name__ == "__main__":
    main()

测试结果:

预测结果保存目录:

D:\Pycharm_project\Yolo26\runs\detect\predict

================================================================================

第 1 张图片:bus.jpg

原始图片路径:D:\Pycharm_project\Yolo26\ultralytics\assets\bus.jpg

================================================================================

目标 1: 类别=bus, 置信度=0.924, 框=0.0, 229.4, 806.1, 757.1

目标 2: 类别=person, 置信度=0.913, 框=222.5, 404.9, 345.4, 861.6

目标 3: 类别=person, 置信度=0.905, 框=47.7, 399.3, 239.3, 903.0

目标 4: 类别=person, 置信度=0.870, 框=670.7, 391.5, 809.8, 879.0

目标 5: 类别=person, 置信度=0.535, 框=0.1, 556.7, 60.0, 870.1

================================================================================

第 2 张图片:zidane.jpg

原始图片路径:D:\Pycharm_project\Yolo26\ultralytics\assets\zidane.jpg

================================================================================

目标 1: 类别=person, 置信度=0.915, 框=744.6, 40.5, 1150.3, 711.8

目标 2: 类别=person, 置信度=0.910, 框=120.4, 201.7, 981.5, 713.7

目标 3: 类别=tie, 置信度=0.527, 框=433.2, 430.7, 525.6, 714.9

预测完成。

6. 自定义数据集训练

完成官方模型测试后,使用自定义 AVS 数据集训练 YOLO26。数据集配置文件为:

复制代码
D:\Pycharm_project\Yolo26\AVS_dataset\data.yaml

data.yaml:

python 复制代码
path:  D:\Pycharm_project\Yolo26\AVS_dataset    
train: D:\Pycharm_project\Yolo26\AVS_dataset\images\train 
val:  D:\Pycharm_project\Yolo26\AVS_dataset\images\val    
test:    # test images (optional)

nc: 2
# Classes
names:
  0: AVS
  1: handle

该数据集包含两个类别:

复制代码
AVS
handle

训练时使用官方 yolo26n.pt 作为预训练权重。训练参数如下:

复制代码
epochs=200
imgsz=640
batch=16
device=0
project=D:\Pycharm_project\Yolo26\runs
name=AVS_Yolo26

训练启动后,日志中出现:

复制代码
Overriding model.yaml nc=80 with nc=2

这说明原始 COCO 80 类模型的检测头被替换为当前数据集的 2 类检测头。

7. 训练结果

训练完成后,模型自动验证 best.pt,最终验证日志如下:

复制代码
Validating D:\Pycharm_project\Yolo26\runs\AVS_Yolo26\weights\best.pt...
YOLO26n summary (fused): 122 layers, 2,375,226 parameters, 0 gradients

Class     Images  Instances      Box(P          R      mAP50  mAP50-95)
all        135        135      0.987      0.993       0.99      0.916
AVS         67         67      0.977          1      0.995      0.885
handle      68         68      0.998      0.985      0.985      0.946

Speed: 0.4ms preprocess, 2.4ms inference, 0.0ms loss, 0.2ms postprocess per image

整体结果如下:

复制代码
Precision = 0.987
Recall = 0.993
mAP50 = 0.990
mAP50-95 = 0.916

Precision 表示预测出来的目标中有多少是真实目标;Recall 表示真实目标中有多少被模型检测出来;mAP 则用于综合衡量检测精度。

mAP 可以简化表示为:

其中:

复制代码
N    表示类别数量;
AP_i 表示第 i 个类别的 Average Precision。

从结果看,两个类别的检测效果都比较好。其中 AVS 类别的召回率达到 1.000,handle 类别的 mAP50-95 达到 0.946。

8. 导出 ONNX 模型

训练完成后,将 .pt 模型导出为 ONNX。Ultralytics 官方 Export 文档说明,YOLO26 支持通过 Python API 或 CLI 导出为 ONNX 格式,并且还支持 TensorRT、CoreML、OpenVINO、TFLite 等多种部署格式。

本文首先对官方 yolo26n.pt 进行了 ONNX 导出测试。导出日志如下:

复制代码
ONNX: starting export with onnx 1.14.1 opset 17...
ONNX: slimming with onnxslim 0.1.94...
ONNX: export success  1.9s, saved as 'D:\Pycharm_project\Yolo26\yolo26n.onnx' (9.5 MB)

这里有几个关键信息。

第一,ONNX opset 使用的是 17。

第二,onnxslim 已经正常工作:

复制代码
ONNX: slimming with onnxslim 0.1.94...

之前没有安装 onnxslim 时,导出也可以成功,但会出现简化失败提示。安装后重新导出,ONNX 模型结构更干净。

为了确认 ONNX 导出是否正确,使用同一张测试图片分别调用 .pt.onnx 模型进行预测。

测试代码:

python 复制代码
from ultralytics import YOLO
import os

def print_results(model_name, results):
    print("\n" + "=" * 80)
    print(model_name)
    print("=" * 80)

    for result in results:
        image_name = os.path.basename(result.path)
        print(f"\n图片:{image_name}")

        if result.boxes is None or len(result.boxes) == 0:
            print("未检测到目标")
            continue

        for i, box in enumerate(result.boxes):
            cls_id = int(box.cls[0])
            conf = float(box.conf[0])
            x1, y1, x2, y2 = box.xyxy[0].tolist()

            print(
                f"目标 {i + 1}: "
                f"类别={result.names[cls_id]}, "
                f"置信度={conf:.3f}, "
                f"框=[{x1:.1f}, {y1:.1f}, {x2:.1f}, {y2:.1f}]"
            )


def main():
    image_path = r"D:\Pycharm_project\Yolo26\ultralytics\assets\bus.jpg"

    pt_model = YOLO(r"D:\Pycharm_project\Yolo26\yolo26n.pt")
    onnx_model = YOLO(r"D:\Pycharm_project\Yolo26\yolo26n.onnx")

    pt_results = pt_model.predict(
        source=image_path,
        imgsz=640,
        conf=0.25,
        device="cpu",
        save=True,
        project=r"D:\Pycharm_project\Yolo26\runs\compare",
        name="pt",
        exist_ok=True
    )

    onnx_results = onnx_model.predict(
        source=image_path,
        imgsz=640,
        conf=0.25,
        device="cpu",
        save=True,
        project=r"D:\Pycharm_project\Yolo26\runs\compare",
        name="onnx",
        exist_ok=True
    )

    print_results("PyTorch .pt 结果", pt_results)
    print_results("ONNX 结果", onnx_results)

    print("\n.pt 保存目录:", pt_results[0].save_dir)
    print(".onnx 保存目录:", onnx_results[0].save_dir)


if __name__ == "__main__":
    main()

测试图片为:

复制代码
D:\Pycharm_project\Yolo26\ultralytics\assets\bus.jpg

.pt 模型推理结果如下:

复制代码
目标 1: 类别=bus,    置信度=0.924, 框=[0.0, 229.4, 806.1, 757.1]
目标 2: 类别=person, 置信度=0.913, 框=[222.5, 404.9, 345.4, 861.6]
目标 3: 类别=person, 置信度=0.905, 框=[47.7, 399.3, 239.3, 903.0]
目标 4: 类别=person, 置信度=0.870, 框=[670.7, 391.5, 809.8, 879.0]
目标 5: 类别=person, 置信度=0.535, 框=[0.1, 556.7, 60.0, 870.1]

.onnx 模型推理结果如下:

复制代码
目标 1: 类别=bus,    置信度=0.925, 框=[5.6, 228.9, 807.2, 749.2]
目标 2: 类别=person, 置信度=0.915, 框=[46.6, 398.7, 237.0, 901.7]
目标 3: 类别=person, 置信度=0.899, 框=[223.1, 405.3, 345.2, 863.1]
目标 4: 类别=person, 置信度=0.860, 框=[668.6, 391.2, 810.0, 879.7]
目标 5: 类别=person, 置信度=0.525, 框=[0.0, 553.8, 64.8, 874.0]

从结果看,二者检测到的目标类别和数量一致,均为:

复制代码
4 persons, 1 bus

置信度非常接近。

可以认为.pt 与 .onnx 推理结果一致性较好。

9. 使用 ONNX Runtime 独立推理验证

完成 .pt.onnx 对比后,进一步使用 ONNX Runtime 独立推理,不再依赖 Ultralytics 的 model.predict()

测试代码:

python 复制代码
import cv2
import numpy as np
import onnxruntime as ort
import os
import time


# COCO 80 类
# COCO_NAMES = [
#     "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck",
#     "boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench",
#     "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra",
#     "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee",
#     "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove",
#     "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup",
#     "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange",
#     "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch",
#     "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse",
#     "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink",
#     "refrigerator", "book", "clock", "vase", "scissors", "teddy bear",
#     "hair drier", "toothbrush"
# ]

COCO_NAMES = [
     "AVS", "handle"
]


def letterbox(image, new_shape=(640, 640), color=(114, 114, 114)):
    """
    等比例缩放图片,并填充到指定大小。
    """
    h, w = image.shape[:2]
    new_h, new_w = new_shape

    r = min(new_w / w, new_h / h)

    resized_w = int(round(w * r))
    resized_h = int(round(h * r))

    dw = new_w - resized_w
    dh = new_h - resized_h

    dw /= 2
    dh /= 2

    resized = cv2.resize(
        image,
        (resized_w, resized_h),
        interpolation=cv2.INTER_LINEAR
    )

    top = int(round(dh - 0.1))
    bottom = int(round(dh + 0.1))
    left = int(round(dw - 0.1))
    right = int(round(dw + 0.1))

    padded = cv2.copyMakeBorder(
        resized,
        top,
        bottom,
        left,
        right,
        cv2.BORDER_CONSTANT,
        value=color
    )

    return padded, r, (left, top)


def preprocess(image_path, input_size=(640, 640)):
    """
    预处理:
    1. 读取图片
    2. letterbox 到 640x640
    3. BGR 转 RGB
    4. 归一化到 0~1
    5. HWC 转 CHW
    6. 增加 batch 维度
    """
    image = cv2.imread(image_path)

    if image is None:
        raise FileNotFoundError(f"无法读取图片: {image_path}")

    original_image = image.copy()

    img, ratio, pad = letterbox(
        image,
        new_shape=input_size
    )

    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    img = img.astype(np.float32) / 255.0
    img = np.transpose(img, (2, 0, 1))
    img = np.expand_dims(img, axis=0)
    img = np.ascontiguousarray(img)

    return img, original_image, ratio, pad


def scale_boxes(box, ratio, pad, original_shape):
    """
    将 640x640 输入尺度下的检测框还原到原图尺度。
    box: [x1, y1, x2, y2]
    """
    left, top = pad

    x1, y1, x2, y2 = box

    x1 = (x1 - left) / ratio
    y1 = (y1 - top) / ratio
    x2 = (x2 - left) / ratio
    y2 = (y2 - top) / ratio

    h, w = original_shape[:2]

    x1 = max(0, min(w - 1, x1))
    y1 = max(0, min(h - 1, y1))
    x2 = max(0, min(w - 1, x2))
    y2 = max(0, min(h - 1, y2))

    return [x1, y1, x2, y2]


def create_onnx_session(onnx_path):
    """
    创建 ONNX Runtime Session。
    注意:这个过程只做一次,不要每张图片都创建。
    """
    t0 = time.perf_counter()

    session = ort.InferenceSession(
        onnx_path,
        providers=["CPUExecutionProvider"]
    )

    t1 = time.perf_counter()

    input_name = session.get_inputs()[0].name
    output_name = session.get_outputs()[0].name

    print("ONNX 模型加载完成")
    print("输入节点:", input_name)
    print("输出节点:", output_name)
    print(f"Session 创建耗时: {(t1 - t0) * 1000:.2f} ms")

    return session, input_name, output_name


def predict_one_image(
    session,
    input_name,
    output_name,
    image_path,
    save_path,
    conf_thres=0.25
):
    """
    使用 ONNX Runtime 预测单张图片。
    """

    t_total_start = time.perf_counter()

    # 1. 预处理
    t_pre_start = time.perf_counter()

    input_tensor, original_image, ratio, pad = preprocess(
        image_path,
        input_size=(640, 640)
    )

    t_pre_end = time.perf_counter()

    # 2. ONNX 推理
    t_infer_start = time.perf_counter()

    outputs = session.run(
        [output_name],
        {input_name: input_tensor}
    )

    t_infer_end = time.perf_counter()

    pred = outputs[0]

    # pred shape: [1, 300, 6]
    pred = pred[0]

    # 3. 后处理 + 画框
    t_post_start = time.perf_counter()

    result_count = 0

    for det in pred:
        x1, y1, x2, y2, score, cls_id = det

        if score < conf_thres:
            continue

        cls_id = int(cls_id)

        if cls_id < 0 or cls_id >= len(COCO_NAMES):
            class_name = f"class_{cls_id}"
        else:
            class_name = COCO_NAMES[cls_id]

        x1, y1, x2, y2 = scale_boxes(
            [x1, y1, x2, y2],
            ratio,
            pad,
            original_image.shape
        )

        x1, y1, x2, y2 = map(int, [x1, y1, x2, y2])

        result_count += 1

        print(
            f"目标 {result_count}: "
            f"类别={class_name}, "
            f"置信度={score:.3f}, "
            f"框=[{x1}, {y1}, {x2}, {y2}]"
        )

        cv2.rectangle(
            original_image,
            (x1, y1),
            (x2, y2),
            (0, 255, 0),
            2
        )

        label = f"{class_name} {score:.2f}"

        cv2.putText(
            original_image,
            label,
            (x1, max(0, y1 - 5)),
            cv2.FONT_HERSHEY_SIMPLEX,
            0.6,
            (0, 255, 0),
            2
        )

    if result_count == 0:
        print("未检测到目标")

    t_post_end = time.perf_counter()

    # 4. 保存图片
    t_save_start = time.perf_counter()

    cv2.imwrite(save_path, original_image)

    t_save_end = time.perf_counter()

    t_total_end = time.perf_counter()

    print("预测结果已保存到:", save_path)

    print("\n耗时统计:")
    print(f"预处理耗时:        {(t_pre_end - t_pre_start) * 1000:.2f} ms")
    print(f"ONNX 推理耗时:     {(t_infer_end - t_infer_start) * 1000:.2f} ms")
    print(f"后处理+画框耗时:   {(t_post_end - t_post_start) * 1000:.2f} ms")
    print(f"保存图片耗时:      {(t_save_end - t_save_start) * 1000:.2f} ms")
    print(f"单张图片总耗时:    {(t_total_end - t_total_start) * 1000:.2f} ms")

    return {
        "preprocess_ms": (t_pre_end - t_pre_start) * 1000,
        "inference_ms": (t_infer_end - t_infer_start) * 1000,
        "postprocess_ms": (t_post_end - t_post_start) * 1000,
        "save_ms": (t_save_end - t_save_start) * 1000,
        "total_ms": (t_total_end - t_total_start) * 1000,
        "result_count": result_count
    }


def predict_image_folder(
    session,
    input_name,
    output_name,
    image_dir,
    save_dir,
    conf_thres=0.25
):
    """
    批量预测文件夹中的图片。
    """
    os.makedirs(save_dir, exist_ok=True)

    image_exts = (".jpg", ".jpeg", ".png", ".bmp")

    image_files = [
        f for f in os.listdir(image_dir)
        if f.lower().endswith(image_exts)
    ]

    if len(image_files) == 0:
        print("文件夹中没有找到图片:", image_dir)
        return

    total_pre = 0.0
    total_infer = 0.0
    total_post = 0.0
    total_save = 0.0
    total_time = 0.0

    for idx, file_name in enumerate(image_files):
        image_path = os.path.join(image_dir, file_name)
        save_path = os.path.join(save_dir, file_name)

        print("\n" + "=" * 80)
        print(f"正在预测第 {idx + 1}/{len(image_files)} 张图片: {file_name}")
        print("=" * 80)

        time_info = predict_one_image(
            session=session,
            input_name=input_name,
            output_name=output_name,
            image_path=image_path,
            save_path=save_path,
            conf_thres=conf_thres
        )

        total_pre += time_info["preprocess_ms"]
        total_infer += time_info["inference_ms"]
        total_post += time_info["postprocess_ms"]
        total_save += time_info["save_ms"]
        total_time += time_info["total_ms"]

    n = len(image_files)

    print("\n" + "=" * 80)
    print("批量预测完成")
    print("=" * 80)
    print(f"图片数量: {n}")
    print(f"平均预处理耗时:        {total_pre / n:.2f} ms")
    print(f"平均 ONNX 推理耗时:     {total_infer / n:.2f} ms")
    print(f"平均后处理+画框耗时:   {total_post / n:.2f} ms")
    print(f"平均保存图片耗时:      {total_save / n:.2f} ms")
    print(f"平均单张总耗时:        {total_time / n:.2f} ms")
    print(f"平均 FPS,不含保存:     {1000.0 / ((total_pre + total_infer + total_post) / n):.2f}")
    print(f"平均 FPS,包含保存:     {1000.0 / (total_time / n):.2f}")
    print("结果保存目录:", save_dir)


if __name__ == "__main__":
    onnx_path = r"D:\Pycharm_project\Yolo26\runs\AVS_Yolo26\weights\best.onnx"

    # 单张图片预测
    image_path = r"D:\Pycharm_project\Yolo26\AVS_dataset\images\train\1751872431.png"
    save_path = r"D:\Pycharm_project\Yolo26\onnxruntime_result.jpg"

    # 文件夹批量预测
    image_dir = r"D:\Pycharm_project\Yolo26\ultralytics\assets"
    save_dir = r"D:\Pycharm_project\Yolo26\onnxruntime_results"

    session, input_name, output_name = create_onnx_session(onnx_path)

    # ========== 模式 1:预测单张图片 ==========
    predict_one_image(
        session=session,
        input_name=input_name,
        output_name=output_name,
        image_path=image_path,
        save_path=save_path,
        conf_thres=0.25
    )

    # ========== 模式 2:预测整个文件夹 ==========
    # 如果你要批量预测,取消下面这段注释即可

    # predict_image_folder(
    #     session=session,
    #     input_name=input_name,
    #     output_name=output_name,
    #     image_dir=image_dir,
    #     save_dir=save_dir,
    #     conf_thres=0.25
    # )

测试结果:

ONNX 模型加载完成

输入节点: images

输出节点: output0

Session 创建耗时: 95.79 ms

目标 1: 类别=AVS, 置信度=0.971, 框=682, 328, 770, 434

预测结果已保存到: D:\Pycharm_project\Yolo26\onnxruntime_result.jpg

耗时统计:

预处理耗时: 25.13 ms

ONNX 推理耗时: 21.38 ms

后处理+画框耗时: 1.45 ms

保存图片耗时: 16.05 ms

单张图片总耗时: 64.02 ms

ONNX Runtime 推理流程为:

复制代码
1. 创建 ONNX Runtime Session;
2. 获取输入节点名称 images;
3. 获取输出节点名称 output0;
4. 对原图进行 letterbox 预处理;
5. 构造 [1, 3, 640, 640] 输入张量;
6. 执行 session.run;
7. 得到 [1, 300, 6] 输出;
8. 按 [x1, y1, x2, y2, score, class_id] 解析;
9. 过滤低置信度框;
10. 坐标还原到原图;
11. 绘制检测框并保存图片。

10. 关于预处理与后处理

YOLO26 ONNX 输出为 [1, 300, 6],这容易让人误以为"不需要预处理和后处理"。实际上这个理解不准确。

预处理仍然必须做,因为模型输入不是原始图片,而是固定格式的张量:

复制代码
float32[1, 3, 640, 640]

也就是说,原始图片必须被处理成:

复制代码
NCHW 格式;
RGB 通道顺序;
float32 数据类型;
像素值归一化到 0~1;
尺寸为 640×640。

后处理虽然已经不需要传统 YOLO 的复杂 decode 和 NMS,但仍然需要完成:

复制代码
1. 置信度过滤;
2. 类别编号解析;
3. 坐标从 letterbox 输入尺度还原到原图尺度;
4. 绘制检测框;
5. 保存结果图片。

11. 总结

本文完成了 YOLO26 从模型测试到 ONNX Runtime 推理验证的完整流程。

整体流程如下:

复制代码
1. 验证 Ultralytics 环境;
2. 加载 yolo26n.pt 官方模型;
3. 使用官方模型完成预测测试;
4. 使用 AVS 自定义数据集训练 YOLO26;
5. 得到 best.pt 并完成验证;
6. 将模型导出为 ONNX;
7. 使用 Ultralytics 对比 .pt 与 .onnx 推理结果;
8. 使用 ONNX Runtime 独立完成推理;
9. 确认 [1, 300, 6] 输出格式;
10. 为后续 RKNN 或其他端侧部署建立验证基准。

对于工程部署来说,最重要的不是"模型能否导出",而是"导出后的模型能否在目标推理后端上得到一致结果"。因此,.pt → .onnx → ONNX Runtime 这一套验证流程非常有必要。只有在 ONNX Runtime 结果正确后,再进入 RKNN、TensorRT 或其他端侧部署,问题定位才会更清晰。

后续我将会继续更新计算机视觉的相关内容。

相关推荐
大奎帝国1 小时前
Segearth-R2-02
人工智能·机器学习·计算机视觉
大奎帝国3 小时前
Segearth-R2-03
深度学习·机器学习·计算机视觉
YOLO数据集集合3 小时前
无人机航拍RGBT双模态行人检测数据集 | 可见光红外对齐 低空小目标检测 多模态计算机视觉基准数据
人工智能·深度学习·目标检测·计算机视觉·无人机
动物园猫3 小时前
用于实验室智能识别的目标检测数据集分享(适用于YOLO系列深度学习分类检测任务)
深度学习·yolo·目标检测
jay神3 小时前
基于 YOLOv8 + CRNN 的车牌识别系统
深度学习·yolo·目标检测·计算机视觉·车牌识别
装不满的克莱因瓶3 小时前
掌握基于YOLO v5实现车牌目标检测任务的完整流程——从数据到部署的工业级实践
人工智能·python·深度学习·yolo·目标检测·计算机视觉·目标跟踪
大奎帝国4 小时前
Segearth-r2-05
计算机视觉·cv
奔袭的算法工程师4 小时前
论文解读--Sparse4D v3: Advancing End-to-End 3D Detection and Tracking
人工智能·目标检测·计算机视觉·自动驾驶·信号处理
stsdddd4 小时前
YOLO系列目标检测数据集大全【第二十八期】
yolo·目标检测·目标跟踪