YOLOv8目标检测——详细记录使用ONNX Runtime进行推理部署C++/Python实现

概述

在之前博客中有介绍YOLOv8从环境安装到训练的完整过程,本节主要介绍ONNX Runtime的原理以及使用其进行推理加速,使用Python、C++两种编程语言来实现。
https://blog.csdn.net/MariLN/article/details/143924548?spm=1001.2014.3001.5501

1. ONNX Runtime

ONNX Runtime是一个由微软推出的跨平台机器学习模型加速器,它仅支持 ONNX 模型格式。它适用于桌面、服务器以及移动设备。

  • 多框架支持:支持多种常见的深度学习框架,如 PyTorch、TensorFlow、Keras、scikit-learn 等,使开发者能轻松将不同框架训练的模型移植到 ONNX Runtime 中进行高效推理,促进了模型在不同框架间的共享与流转。
  • 跨平台兼容性:可在 Linux、Windows、macOS 等多种操作系统上运行,还支持在云、边缘、网页和移动等不同环境中部署,能很好地满足各种应用场景的需求。
  • 硬件优化:针对 GPU、CPU 以及各种 AI 加速器(如 Intel MKL、cuDNN、TensorRT 等)进行了优化,能够充分利用硬件资源提升性能。例如,在 GPU 上可实现并行计算,大大加快模型的推理速度。
  • 高效的内存管理:采用零拷贝(Zero-Copy)技术和内存池管理,减少了数据传输的开销,提升了整体运行速度,在处理大规模数据时优势明显。
  • 动态形状支持:允许输入尺寸在运行时发生变化,模型仍能正确处理,增加了模型应用的灵活性,可更好地适应不同的输入数据情况。

2. 模型转换

2.1 .pt与.onnx模型

2.1.1 pt 模型

.pt 模型是 PyTorch 模型的一种常见存储格式。PyTorch 是一个广泛使用的深度学习框架,在训练神经网络模型时,模型的参数(包括权重和偏置等)会被保存下来,这些参数可以以.pt 文件的形式存储在磁盘中。例如,当你使用 PyTorch 训练一个图像分类模型(如 ResNet)后,通过torch.save()函数就可以将训练好的模型保存为.pt 文件。

本质上它是一个二进制文件,它包含了模型的结构定义和参数。模型的结构定义包括网络的层数、每层的类型(如线性层、卷积层、池化层等)、激活函数的类型等信息。参数则是在训练过程中学习到的具体数值,这些数值决定了模型对输入数据的处理方式。

2.1.2 onnx 模型

ONNX(Open Neural Network Exchange)是一种开放的神经网络交换格式,.onnx 文件就是以这种格式存储的模型文件。它的出现是为了解决不同深度学习框架之间模型转换和互用的问题。许多深度学习框架(如 PyTorch、TensorFlow 等)都可以将自己的模型转换为 ONNX 格式。以 PyTorch 为例,通过torch.onnx.export()函数可以将.pt 模型转换为.onnx 模型。

.onnx 文件同样是一种结构化的文件,它以一种中间表示的形式存储了模型的计算图。这个计算图包含了模型中的各种操作(如加法、乘法、卷积等)以及操作之间的连接关系,同时也包含了模型的输入和输出信息。这种中间表示形式使得不同框架训练的模型能够在一个统一的格式下进行转换和推理。

.onnx 模型主要用于模型的跨框架部署和推理。由于它可以被多种推理引擎(如 ONNX Runtime、TensorRT 等)所支持,所以可以将在一个框架下训练好的模型转换为.onnx 格式,然后在其他环境中进行高效的推理。例如,在工业生产环境中,模型可能是在 PyTorch 中训练的,但在实际的产品线上,需要将其部署到一个对性能和效率要求更高的推理引擎上,此时将模型转换为.onnx 文件并使用 ONNX Runtime 等推理引擎进行部署就非常方便。同时,它也方便了不同团队之间的协作,即使不同团队使用不同的深度学习框架,也可以通过.onnx 文件进行模型的共享和集成。

2.2 .pt转换.onnx

将训练好的 YOLOv8 的.pt模型转换为.onnx模型。可以使用ultralytics库来进行转换。

yolo task=detect mode=export model=./runs/detect/train/weights/best.pt format=onnx

from ultralytics import YOLO

# Load a model
model = YOLO('./runs/detect/train/weights/best.pt')  # load a custom trained

# Export the model
success = model.export(format='onnx')

3. 模型推理

3.1 Python实现

3.1.1 环境部署

需要安装onnxruntime、numpy、cv2等库。如果使用 GPU 进行推理,还需安装onnxruntime-gpu。

pip install onnxruntime
pip install onnxruntime-gpu
pip install opencv-python
pip install numpy
pip install gradio

3.1.2 推理步骤

(1)图像预处理
  • 读取图像并将图像的颜色空间从 BGR 格式转换为 RGB 格式。OpenCV 默认使用 BGR 格式,而许多深度学习框架和模型(如 ONNX 模型)则期望输入是 RGB 格式。
  • 调整图像大小,通常将图像 resize 到模型要求的输入尺寸,如 640x640。
  • 对图像进行归一化处理,将像素值归一化到 [0, 1] 区间。
  • 调整图像通道顺序,一般从 HWC(Height, Width, Channel)转换为 CHW 格式,并增加一个批次维度,使其变为 NCHW 格式,N 为批次大小,通常设为 1。
import cv2
import numpy as np

def prepare_input(image, input_width, input_height):
	# 转换为 RGB 格式
	input_img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)# cv2.imread 读取到的图像默认是 BGR 格式的

	# 调整图像尺寸
	input_img = cv2.resize(input_img, (input_width, input_height))# input_width、input_height是模型期望的输入宽度和高度

	# 归一化到 0-1
	input_img = input_img / 255.0

	# 变换通道顺序,并增加 batch 维度,HWC->NCHW
	input_img = input_img.transpose(2, 0, 1)
	input_tensor = input_img[np.newaxis, :, :, :].astype(np.float32)# np.newaxis 用于增加一个新的维度

    return input_tensor

image_path = "test.jpg"
image = cv2.imread(image_path)
input_tensor= prepare_input(image, 640, 640)

(2)模型推理
  • 创建onnxruntime.InferenceSession对象,加载转换后的.onnx模型 。
  • 将预处理后的图像数据作为输入,传递给模型进行推理,并获取输出结果。
def inference(model_path, input_tensor):

    start = time.perf_counter() # 获取一个高精度的时间戳,主要用于代码性能测试和计算时间间隔,精确度通常远高于 time.time()
    
    # 加载 ONNX 模型
    session = onnxruntime.InferenceSession(model_path, providers=onnxruntime.get_available_providers())

	# 获取输入和输出的名字
    input_names = [model_inputs.name for model_inputs in session.get_inputs()]
    output_names = [model_outputs.name for model_outputs in session.get_outputs()]
    
    # 运行模型推理
    outputs = session.run(output_names, {
   input_names[0]: input_tensor})
   
    print(f"Inference time: {
     (time.perf_counter() - start)*1000:.2f} ms")
    return outputs
    
(3)后处理
  • 对模型的输出结果去除批量维度。
  • 获取每个检测框的置信度最高的类别,并根据置信度阈值进行筛选,过滤掉低置信度的目标检测框 。
  • 坐标转换,将预测框还原到原始图像尺寸,并将边界框的表示从中心点坐标 (x_center, y_center) 和宽高 (w, h) 格式转换为左上角和右下角坐标 (x1, y1, x2, y2) 格式。
  • 进行非极大值抑制(NMS),去除重叠度过高的检测框,得到最终的目标检测结果。
def xywh2xyxy(x):
    # 将边界框从 (x_center, y_center, w, h) 格式转换为 (x1, y1, x2, y2)
    y = np.copy(x)

    # 计算左上角坐标 x1 和 y1
    y[..., 0] = x[..., 0] - x[..., 2] / 2  # x1 = x_center - w / 2
    y[..., 1] = x[..., 1] - x[..., 3] / 2  # y1 = y_center - h / 2

    # 计算右下角坐标 x2 和 y2
    y[..., 2] = x[..., 0] + x[..., 2] / 2  # x2 = x_center + w / 2
    y[..., 3] = x[..., 1] + x[..., 3] / 2  # y2 = y_center + h / 2

    return y

def multiclass_nms(boxes, scores, class_ids, iou_threshold):

    # 获取所有唯一的类别索引
    unique_class_ids = np.unique(class_ids)

    keep_boxes = []  # 存储最终保留的边界框索引
    
    for class_id in unique_class_ids:
        # 筛选出属于当前类别的边界框索引
        class_indices = np.where(class_ids == class_id)[0] # np.where返回元组
        
        # 提取属于当前类别的边界框和分数
        class_boxes = boxes[class_indices, :]   # 当前类别的边界框
        class_scores = scores[class_indices]   # 当前类别的分数

        # 执行 NMS 并获取保留下来的索引
        class_keep_boxes = nms(class_boxes, class_scores, iou_threshold)

        # 将保留的索引(对应原始的索引)添加到结果中
        keep_boxes.extend(class_indices[class_keep_boxes])

    return keep_boxes

def nms(boxes, scores, iou_threshold):
    # 根据 scores 对检测框从高到低进行排序,得到排序后的索引
    sorted_indices = np.argsort(scores)[::-1] # [::-1] 反转排序顺序

    keep_boxes = []
    while sorted_indices.size > 0:
        # 保留最高分数的边界框
        box_id = sorted_indices[0]
        keep_boxes.append(box_id)

        # 计算当前最高分数的边界框与剩余边界框的 IoU
        ious = compute_iou(boxes[box_id, :], boxes[sorted_indices[1:], :])

        # 找出 IoU 小于阈值的边界框索引,保留这些框,过滤重叠框
        keep_indices = np.where(ious < iou_threshold)[0]

        # 注意:由于 keep_indices 是相对于 sorted_indices[1:] 的索引,
        # 需要将其整体偏移 +1 来匹配到原始 sorted_indices
        sorted_indices = sorted_indices[keep_indices + 1]

    return keep_boxes

def compute_iou(box, boxes):

    # 计算交集区域的坐标,xmin 和 ymin: 交集左上角的坐标,xmax 和 ymax: 交集右下角的坐标
    xmin = np.maximum(box[0], boxes[:, 0]) 
    ymin = np.maximum(box[1], boxes[:, 1]) 
    xmax = np.minimum(box[2], boxes[:, 2]) 
    ymax = np.minimum(box[3], boxes[:, 3])  

    # 计算交集区域面积,如果两个框没有重叠,交集宽度和高度会为负,使用 np.maximum 保证面积非负
    intersection_area = np.maximum(0, xmax - xmin) * np.maximum(0, ymax - ymin)

    # 计算每个边界框的面积
    box_area = (box[2] - box[0]) * (box[3] - box[1])  
    boxes_area = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1])  

    # 计算并集区域面积
    union_area = box_area + boxes_area - intersection_area

    # 计算 IoU(交并比)
    iou = intersection_area / union_area  # 交集区域面积 / 并集区域面积

    return iou


def process_output(outputs, conf_threshold, iou_threshold, input_width, input_height, img_width, img_height):

    predictions = np.squeeze(outputs[0]).T # 去除数组中形状为1的维度,批量维度,(1, N, M)->(M, N)

    # 获取每个检测框的置信度最高的类别
    scores = np.max(predictions[:, 4:], axis=1) # 在行方向上取最大值
    # 根据置信度阈值过滤掉低置信度的检测框
    predictions = predictions[scores > conf_threshold, :]
    scores = scores[scores > conf_threshold]

    if len(scores) == 0:
        return [], [], []

    # 获取检测框的类别置信度最高的索引
    class_ids = np.argmax(predictions[:, 4:], axis=1) # 返回数组中最大值的索引

    # 提取边界框
    boxes = predictions[:, :4]
    
    # 将边界框坐标从归一化坐标还原到原图尺寸
    input_shape = np.array([input_width, input_height, input_width, input_height])
    boxes = np.divide(boxes, input_shape, dtype=np.float32) # 边界框坐标是相对于输入图像尺寸的,归一化到 [0, 1] 之间
    boxes *= np.array([img_width, img_height, img_width, img_height]) # 将归一化的坐标还原到原图尺寸

    # 转换为 xyxy 格式
    boxes = xywh2xyxy(boxes)

    # 执行非极大值抑制(NMS)
    indices = multiclass_nms(boxes, scores, class_ids, iou_threshold)

    return boxes[indices], scores[indices], class_ids[indices]

3.1.3 代码部署

utils.py

import numpy as np
import cv2

class_names = ['person','head','helmet']

# Create a list of colors for each class where each color is a tuple of 3 integer values
rng = np.random.default_rng(3)
colors = rng.uniform(0, 255, size=(len(class_names), 3))

def nms(boxes, scores, iou_threshold):
    # Sort by score
    sorted_indices = np.argsort(scores)[::-1]

    keep_boxes = []
    while sorted_indices.size > 0:
        # Pick the last box
        box_id = sorted_indices[0]
        keep_boxes.append(box_id)

        # Compute IoU of the picked box with the rest
        ious = compute_iou(boxes[box_id, :], boxes[sorted_indices[1:], :])

        # Remove boxes with IoU over the threshold
        keep_indices = np.where(ious < iou_threshold)[0]

        # print(keep_indices.shape, sorted_indices.shape)
        sorted_indices = sorted_indices[keep_indices + 1]

    return keep_boxes

def multiclass_nms(boxes, scores, class_ids, iou_threshold):

    unique_class_ids = np.unique(class_ids)

    keep_boxes = []
    for class_id in unique_class_ids:
        class_indices = np.where(class_ids == class_id)[0]
        class_boxes = boxes[class_indices,:]
        class_scores = scores[class_indices]

        class_keep_boxes = nms(class_boxes, class_scores, iou_threshold)
        keep_boxes.extend(class_indices[class_keep_boxes])

    return keep_boxes

def compute_iou(box, boxes):
    # Compute xmin, ymin, xmax, ymax for both boxes
    xmin = np.maximum(box[0], boxes[:, 0])
    ymin = np.maximum(box[1], boxes[:, 1])
    xmax = np.minimum(box[2], boxes[:, 2])
    ymax = np.minimum(box[3], boxes[:, 3])

    # Compute intersection area
    intersection_area = np.maximum(0, xmax - xmin) * np.maximum(0, ymax - ymin)

    # Compute union area
    box_area = (box[2] - box[0]) * (box[3] - box[1])
    boxes_area = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1])
    union_area = box_area + boxes_area - intersection_area

    # Compute IoU
    iou = intersection_area / union_area

    return iou

def xywh2xyxy(x):
    # Convert bounding box (x, y, w, h) to bounding box (x1, y1, x2, y2)
    y = np.copy(x)
    y[..., 0] = x[..., 0] - x[..., 2] / 2
    y[..., 1] = x[...
### 使用或集成YOLO到C#中的方法 在C#中使用或集成YOLO(You Only Look Once),可以通过调用Python脚本或使用.NET兼容的库来实现。以下是几种常见的方法和相关技术细节[^1]: #### 1. 调用Python脚本 YOLO通常由Python实现,因此可以使用`Process`类从C#中启动并运行YOLOPython脚本。以下是一个示例代码片段,展示如何通过C#调用Python脚本来执行YOLO推理任务。 ```csharp using System.Diagnostics; public class YoloRunner { public void RunYoloScript(string pythonPath, string scriptPath) { var process = new Process(); process.StartInfo.FileName = pythonPath; // Python解释器路径 process.StartInfo.Arguments = $"\"{scriptPath}\""; // Python脚本路径 process.StartInfo.RedirectStandardOutput = true; process.StartInfo.UseShellExecute = false; process.StartInfo.CreateNoWindow = true; process.Start(); string output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); Console.WriteLine(output); } } ``` 此方法需要确保Python环境已正确配置,并且YOLO模型及相关依赖项已经安装完成[^2]。 #### 2. 使用ONNX格式的YOLO模型 YOLO模型可以导出为ONNX(Open Neural Network Exchange)格式,这是一种与平台无关的模型表示形式。C#可以通过`Microsoft.ML`或`TensorFlowSharp`等库加载ONNX模型进行推理。 以下是一个使用`Microsoft.ML`加载ONNX模型的示例: ```csharp using Microsoft.ML; using Microsoft.ML.Data; public class YoloOnnx { private readonly MLContext _mlContext; public YoloOnnx() { _mlContext = new MLContext(); } public void LoadAndPredict(string onnxModelPath) { ITransformer loadedModel = _mlContext.Model.Load(onnxModelPath, out var modelInputSchema); // 创建数据管道以提供输入数据 var dataView = _mlContext.Data.LoadFromEnumerable(new List<YourInputType>()); // 进行预测 var predictions = loadedModel.Transform(dataView); // 输出结果 var predictionEnumerator = _mlContext.Data.CreateEnumerable<YourOutputType>(predictions, reuseRowObject: false); foreach (var prediction in predictionEnumerator) { Console.WriteLine(prediction); } } } ``` 此方法要求将YOLO模型转换为ONNX格式,并确保输入输出数据结构与模型定义匹配[^3]。 #### 3. 使用第三方库 还有一些专门针对深度学习推理的.NET库支持YOLO模型。例如,`ML.NET`提供了对ONNX模型的支持,而`ImageSharp`可用于图像处理。结合这些库,可以在C#中构建完整的YOLO推理流程。 #### 4. Docker容器化解决方案 如果希望简化环境配置,可以将YOLO模型部署在一个Docker容器中,并通过HTTP API从C#应用程序调用它。这需要使用Flask或其他Web框架创建一个简单的API服务。 --- ### 注意事项 - 确保YOLO模型的版本与使用的工具兼容。 - 如果选择调用Python脚本的方式,需注意跨平台兼容性问题。 - 对于性能敏感的应用场景,建议优先考虑ONNX格式的模型,因为它经过优化,适合部署在多种平台上。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值