YOLO11 旋转目标检测 | OBB定向检测 | ONNX模型推理 | 旋转NMS

本文分享YOLO11中,从xxx.pt权重文件转为.onnx文件,然后使用.onnx文件,进行旋转目标检测任务的模型推理

ONNX模型推理,便于算法到开发板或芯片的部署。

本文提供源代码,支持不同尺寸图片输入、支持旋转NMS过滤重复框、支持旋转IOU计算。

备注:本文是使用Python,编写ONNX模型推理代码的

目录

1、导出ONNX模型

2、所需依赖库

3、整体框架思路

4、支持不同尺寸图像输入 函数

5、旋转边界框IoU计算 函数

6、旋转NMS过滤重复框 函数

7、解析ONNX模型输出 函数

8、完整代码


1、导出ONNX模型

首先我们训练好的模型,生成xxx.pt权重文件;

然后用下面代码,导出ONNX模型(简洁版)

from ultralytics import YOLO

# 加载一个模型,路径为 YOLO 模型的 .pt 文件
model = YOLO("runs/obb/train1/weights/best.pt")

# 导出模型,格式为 ONNX
model.export(format="onnx")

运行代码后,会在上面路径中生成best.onnx文件的

  • 比如,填写的路径是:"runs/obb/train3/weights/best.pt"
  • 那么在runs/obb/train3/weights/目录中,会生成与best.pt同名的onnx文件,即best.onnx

上面代码示例是简单版,如果需要更专业设置ONNX,用下面版本的

YOLO11导出ONNX模型(专业版)


from ultralytics import YOLO
 
# 加载一个模型,路径为 YOLO 模型的 .pt 文件
model = YOLO(r"runs/obb/train1/weights/best.pt")
 
# 导出模型,设置多种参数
model.export(
    format="onnx",      # 导出格式为 ONNX
    imgsz=(640, 640),   # 设置输入图像的尺寸
    keras=False,        # 不导出为 Keras 格式
    optimize=False,     # 不进行优化 False, 移动设备优化的参数,用于在导出为TorchScript 格式时进行模型优化
    half=False,         # 不启用 FP16 量化
    int8=False,         # 不启用 INT8 量化
    dynamic=False,      # 不启用动态输入尺寸
    simplify=True,      # 简化 ONNX 模型
    opset=None,         # 使用最新的 opset 版本
    workspace=4.0,      # 为 TensorRT 优化设置最大工作区大小(GiB)
    nms=False,          # 不添加 NMS(非极大值抑制)
    batch=1,            # 指定批处理大小
    device="cpu"        # 指定导出设备为CPU或GPU,对应参数为"cpu" , "0"
)

对于model.export( )函数中,各种参数说明:

  1. format="onnx":指定导出模型的格式为 onnx。
  2. imgsz=(640, 640):输入图像的尺寸设为 640x640。如果需要其他尺寸可以修改这个值。
  3. keras=False:不导出为 Keras 格式的模型。
  4. optimize=False:不应用 TorchScript 移动设备优化。
  5. half=False:不启用 FP16(半精度)量化
  6. int8=False:不启用 INT8 量化
  7. dynamic=False:不启用动态输入尺寸。
  8. simplify=True:简化模型以提升 ONNX 模型的性能。
  9. opset=None:使用默认的 ONNX opset 版本,如果需要可以手动指定。
  10. workspace=4.0:为 TensorRT 优化指定最大工作空间大小为 4 GiB。
  11. nms=False:不为 CoreML 导出添加非极大值抑制(NMS)。
  12. batch=1:设置批处理大小为 1。
  13. device="cpu", 指定导出设备为CPU或GPU,对应参数为"cpu" , "0"

参考官网文档:https://docs.ultralytics.com/modes/export/#arguments

当然了,YOLO11中不仅支持ONNX模型,还支持下面表格中格式

支持的导出格式format参数值生成的模型示例model.export( )函数的参数
PyTorch-yolo11n.pt-
TorchScripttorchscriptyolo11n.torchscriptimgszoptimizebatch
ONNXonnxyolo11n.onnximgszhalfdynamicsimplifyopsetbatch
OpenVINOopenvinoyolo11n_openvino_model/imgszhalfint8batch
TensorRTengineyolo11n.engineimgszhalfdynamicsimplifyworkspaceint8batch
CoreMLcoremlyolo11n.mlpackageimgszhalfint8nmsbatch
TF SavedModelsaved_modelyolo11n_saved_model/imgszkerasint8batch
TF GraphDefpbyolo11n.pbimgszbatch
TF Litetfliteyolo11n.tfliteimgszhalfint8batch
TF Edge TPUedgetpuyolo11n_edgetpu.tfliteimgsz
TF.jstfjsyolo11n_web_model/imgszhalfint8batch
PaddlePaddlepaddleyolo11n_paddle_model/imgszbatch
NCNNncnnyolo11n_ncnn_model/imgszhalfbatch

2、所需依赖库

本文的代码中,主要依赖opencv、onnxruntime、numpy这三个库;

不需要安装torch、ultralytics等库的。

import os

import cv2

import numpy as np

import onnxruntime as ort

import logging

3、整体框架思路

我们编写代码,实现了一个基于 YOLO11 旋转目标检测(OBB)的推理和检测可视化系统。

以下是代码的整体思路分析:

3.1、基本功能与目标

  • YOLO11模型推理:使用ONNX格式的YOLO11模型,对图像中的旋转目标进行检测。
  • 输出解析:解析模型输出,获取检测的旋转边界框坐标、类别和置信度。
  • 旋转边界框处理:支持旋转NMS(非极大值抑制)和 ProbIoU(概率交并比)来处理重复检测框。
  • 可视化检测结果:在图像上绘制旋转边界框,标注检测结果。

3.2、图像预处理 (letterbox 函数)

  • 将输入图像调整为指定的 imgsz 大小(默认为 640x640),保持长宽比并添加填充。
  • 返回图像缩放的 ratio 和填充偏移 dw, dh,以便后续解析输出时恢复原图坐标。

3.3、加载模型 (load_model 函数)

  • 加载 ONNX 格式的 YOLO11 模型,使用 onnxruntime 进行推理。

3.4、旋转边界框的协方差矩阵计算 (_get_covariance_matrix 函数)

  • 基于边界框的宽、高和角度,计算协方差矩阵的元素 a, b, c,这是 ProbIoU 计算的前提。

3.5、旋转边界框的ProbIoU计算 (batch_probiou 函数)

  • 基于两个旋转边界框集合,使用协方差矩阵计算 ProbIoU 值。ProbIoU 计算边界框之间的相似性,考虑了旋转角度的影响。
  • 返回一个矩阵,表示每对边界框的 ProbIoU 值。

3.6、旋转NMS过滤重复框 (rotated_nms_with_probiou 函数)

  • 使用 ProbIoU 执行旋转边界框的 NMS 操作,去除重叠度过高的检测框。
  • 根据置信度分数 scores 降序排列,逐步计算当前检测框与其余框的 ProbIoU,如果 ProbIoU 小于设定的阈值 iou_threshold,则保留当前框。

3.7、推理处理 (run_inference 函数)

  • 从输入字节流中解码图像数据。
  • 将图像经过 letterbox 预处理后转换为模型输入格式。
  • 使用 ONNX 模型进行推理,返回推理结果 result 及缩放 ratio 和填充 dwdh

3.8、解析模型输出 (parse_onnx_output 函数)

  • 提取每个检测框的中心坐标、宽高、类别置信度及旋转角度。
  • 根据设定的 conf_threshold 过滤置信度低的检测框。
  • 多类别模型中,提取所有类别的置信度,并选择置信度最高的类别。
  • 使用 rotated_nms_with_probiou 函数执行旋转NMS去除重复框。
  • 将检测框坐标按比例缩放并去除填充,返回处理后的检测结果,包括位置坐标、类别和置信度。

3.9、旋转边界框四角点计算 (calculate_obb_corners 函数)

  • 根据旋转角度和宽高,计算旋转边界框的四个角点坐标,以便在图像上进行绘制。

3.10、绘制检测结果 (save_detections 函数)

  • 在原始图像上绘制旋转边界框和类别标签,标注置信度。
  • 将绘制完成的图像保存到指定的输出路径。

3.11、批量处理文件夹图像 (process_images_in_folder 函数)

  • 从指定文件夹中逐张读取图像文件,对每张图像执行推理、解析输出、绘制结果,并将绘制后的图像保存到输出文件夹。

3.12、主函数执行参数

  • 定义输入图像文件夹、模型路径、输出文件夹、置信度阈值、IoU阈值及模型输入大小 imgsz 等参数。
  • 执行 process_images_in_folder 对文件夹内的图像批量处理并保存结果。

4、支持不同尺寸图像输入 函数

letterbox 函数的主要任务是将输入图像调整为特定尺寸,使用模型所需的输入大小(如 640x640),同时保持图像的长宽比,并为目标尺寸添加适量的填充

  • 它通过保持原始宽高比来避免图像的变形,通过对称填充使得图像保持中心化
  • 最终得到标准尺寸的图像,适应深度学习模型的输入需求。
  • 参数 autoscale_fillscale_up 提供了不同的选项,以适应不同的应用场景:比如是否需要按指定步幅填充,是否强制拉伸以填满,或者是否允许图像放大。
def letterbox(img, new_shape=(640, 640), color=(0, 0, 0), auto=False, scale_fill=False, scale_up=False, stride=32):
    """
    将图像调整为指定尺寸,同时保持长宽比,添加填充以适应目标输入形状。
    """
    # 获取图像的当前高度和宽度
    shape = img.shape[:2]
    
    # 如果 new_shape 是整数,则将其转换为 (new_shape, new_shape) 的格式
    if isinstance(new_shape, int):
        new_shape = (new_shape, new_shape)

    # 计算缩放比例 r,以便图像适配到 new_shape,保持长宽比
    r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
    
    # 若不允许放大(scale_up=False),限制 r 最大值为 1.0
    if not scale_up:
        r = min(r, 1.0)

    # 保存缩放比例 ratio,计算未填充的新尺寸 new_unpad
    ratio = r, r
    new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
    
    # 计算目标尺寸与缩放后尺寸的宽度、高度差值 dw, dh
    dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]

    # 若 auto=True,则将 dw 和 dh 调整为 stride 的倍数
    if auto:
        dw, dh = np.mod(dw, stride), np.mod(dh, stride)
    # 若 scale_fill=True,则忽略比例,强制缩放到目标尺寸
    elif scale_fill:
        dw, dh = 0.0, 0.0
        new_unpad = (new_shape[1], new_shape[0])
        ratio = new_shape[1] / shape[1], new_shape[0] / shape[0]

    # 将填充值平分到图像四周
    dw /= 2  
    dh /= 2

    # 如果当前图像尺寸与目标尺寸不一致,则将图像缩放到 new_unpad
    if shape[::-1] != new_unpad:
        img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR)

    # 应用上下左右的填充以使图像符合目标尺寸
    top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
    left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
    img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)
    
    # 返回填充后的图像、缩放比例和填充量
    return img, ratio, (dw, dh)

5、旋转边界框IoU计算 函数

这里编写batch_probiou 函数,用于旋转边界框IoU计算,其中使用到ProbIoU方法。

这是一个基于协方差矩阵的方法,用于评估旋转边界框之间的相似性。

def batch_probiou(obb1, obb2, eps=1e-7):
    """
    计算旋转边界框之间的 ProbIoU。
    :param obb1: 第一个旋转边界框集合
    :param obb2: 第二个旋转边界框集合
    :param eps: 防止除零的极小值
    :return: 两个旋转边界框之间的 ProbIoU
    """
    # 提取两个旋转边界框的中心坐标 (x, y)
    x1, y1 = obb1[..., 0], obb1[..., 1]
    x2, y2 = obb2[..., 0], obb2[..., 1]

    # 计算两个旋转边界框的协方差矩阵元素 a, b, c
    a1, b1, c1 = _get_covariance_matrix(obb1)
    a2, b2, c2 = _get_covariance_matrix(obb2)

    # 计算 ProbIoU 的三个部分 t1, t2, t3
    # t1 表示中心点位置差异的贡献
    t1 = ((a1[:, None] + a2) * (y1[:, None] - y2) ** 2 + (b1[:, None] + b2) * (x1[:, None] - x2) ** 2) / (
            (a1[:, None] + a2) * (b1[:, None] + b2) - (c1[:, None] + c2) ** 2 + eps) * 0.25
    # t2 表示旋转角度的耦合贡献
    t2 = ((c1[:, None] + c2) * (x2 - x1[:, None]) * (y1[:, None] - y2)) / (
            (a1[:, None] + a2) * (b1[:, None] + b2) - (c1[:, None] + c2) ** 2 + eps) * 0.5
    # t3 表示面积和形状之间的差异贡献
    t3 = np.log(((a1[:, None] + a2) * (b1[:, None] + b2) - (c1[:, None] + c2) ** 2) /
                (4 * np.sqrt((a1 * b1 - c1 ** 2)[:, None] * (a2 * b2 - c2 ** 2)) + eps) + eps) * 0.5

    # 计算 Bhattacharyya 距离 bd
    bd = np.clip(t1 + t2 + t3, eps, 100.0)  # 将 bd 限制在 [eps, 100.0] 范围内

    # 计算 ProbIoU 值 hd
    hd = np.sqrt(1.0 - np.exp(-bd) + eps)  # 使用 Bhattacharyya 距离计算 hd
    return 1 - hd  # 返回 1 - hd,hd 越小表示相似度越高,1 - hd 即为 ProbIoU

ProbIoU的论文地址:https://arxiv.org/pdf/2106.06072v1.pdf

batch_probiou 函数需要用到协方差矩阵,这里也编写一个函数进行封装

def _get_covariance_matrix(obb):
    """
    计算旋转边界框的协方差矩阵。
    :param obb: 旋转边界框 (Oriented Bounding Box),包含中心坐标、宽、高和旋转角度
    :return: 协方差矩阵的三个元素 a, b, c
    """
    widths = obb[..., 2] / 2  # 获取宽度的一半
    heights = obb[..., 3] / 2  # 获取高度的一半
    angles = obb[..., 4]  # 获取旋转角度

    cos_angle = np.cos(angles)  # 计算旋转角度的余弦值
    sin_angle = np.sin(angles)  # 计算旋转角度的正弦值

    # 计算协方差矩阵的三个元素 a, b, c
    a = (widths * cos_angle) ** 2 + (heights * sin_angle) ** 2
    b = (widths * sin_angle) ** 2 + (heights * cos_angle) ** 2
    c = widths * cos_angle * heights * sin_angle

    return a, b, c

总结:

  • 该代码用于计算两个旋转边界框之间的 ProbIoU,以量化它们的相似程度。
  • 核心思想是通过协方差矩阵来描述每个旋转边界框的形状和旋转角度,结合 Bhattacharyya 距离来评估边界框之间的相似性。
  • ProbIoU 的计算考虑了旋转角度、边界框的中心位置以及大小差异,是比传统 IoU 更加复杂和准确的度量旋转边界框相似性的方法,特别适用于场景中有方向性的对象。

6、旋转NMS过滤重复框 函数

这里编写rotated_nms_with_probiou函数,实现了旋转边界框的非极大值抑制NMS。

  • NMS 的目标是去除冗余的边界框,只保留最有代表性的那个,以减少重叠检测。
  • 在这个实现中,通过 ProbIoU 来计算旋转边界框之间的相似度,用于确定哪些框需要保留。
  • NMS 的核心在于选取当前得分最高的边界框,将它加入保留列表中,
  • 然后与剩余边界框进行相似性计算,通过ProbIoU计算旋转边界框之间的相似度,最终剔除那些相似度高于阈值的边界框。
def rotated_nms_with_probiou(boxes, scores, iou_threshold=0.5):
    """
    使用 ProbIoU 执行旋转边界框的非极大值抑制(NMS)。
    :param boxes: 旋转边界框的集合
    :param scores: 每个边界框的置信度得分
    :param iou_threshold: IoU 阈值,用于确定是否抑制框
    :return: 保留的边界框索引列表
    """
    order = scores.argsort()[::-1]  # 根据置信度得分降序排序
    keep = []  # 用于存储保留的边界框索引

    while len(order) > 0:
        i = order[0]  # 选择当前得分最高的边界框
        keep.append(i)  # 将该边界框的索引加入到保留列表中

        if len(order) == 1:  # 如果只剩下一个边界框,跳出循环
            break

        remaining_boxes = boxes[order[1:]]  # 获取剩余的边界框
        iou_values = batch_probiou(boxes[i:i + 1], remaining_boxes).squeeze(0)  # 计算当前框与剩余框之间的 ProbIoU

        mask = iou_values < iou_threshold  # 找出与当前框 IoU 小于阈值的边界框
        order = order[1:][mask]  # 更新剩余的边界框索引,只保留 IoU 小于阈值的部分

    return keep  # 返回保留的边界框索引列表

7、解析ONNX模型输出 函数

这里编写parse_onnx_output函数,实现了对 ONNX 模型的输出进行解析

  • 提取旋转边界框的坐标、旋转角度、置信度和类别信息,
  • 应用旋转边界框的非极大值抑制(NMS),并计算旋转边界框的四个角点

这里我们需要知道:

ONNX输出格式: x_center, y_center, width, height, class1_confidence, ..., classN_confidence, angle 

def parse_onnx_output(output, ratio, dwdh, conf_threshold=0.5, iou_threshold=0.5):
    """
    解析ONNX模型的输出,提取旋转边界框坐标、置信度和类别信息,并应用旋转NMS。
    :param output: ONNX模型的输出,包含预测的边界框信息
    :param ratio: 缩放比例,用于将坐标还原到原始尺度
    :param dwdh: 填充的宽高,用于调整边界框的中心点坐标
    :param conf_threshold: 置信度阈值,过滤低于该阈值的检测框
    :param iou_threshold: IoU 阈值,用于旋转边界框的非极大值抑制(NMS)
    :return: 符合条件的旋转边界框的检测结果
    """
    boxes, scores, classes, detections = [], [], [], []  # 初始化存储检测结果的列表
    num_detections = output.shape[2]  # 获取检测的边界框数量
    num_classes = output.shape[1] - 6  # 计算类别数量

    # 逐个解析每个检测结果
    for i in range(num_detections):
        detection = output[0, :, i]  # 获取第 i 个检测结果
        x_center, y_center, width, height = detection[0], detection[1], detection[2], detection[3]  # 提取边界框的中心坐标和宽高
        angle = detection[-1]  # 提取旋转角度

        # 处理类别信息和置信度
        if num_classes > 0:
            class_confidences = detection[4:4 + num_classes]  # 获取类别置信度
            if class_confidences.size == 0:
                continue
            class_id = np.argmax(class_confidences)  # 获取置信度最高的类别索引
            confidence = class_confidences[class_id]  # 获取对应的置信度
        else:
            confidence = detection[4]  # 如果没有类别信息,直接使用置信度值
            class_id = 0  # 默认类别为 0

        # 过滤低置信度的检测结果
        if confidence > conf_threshold:
            x_center = (x_center - dwdh[0]) / ratio[0]  # 还原中心点 x 坐标
            y_center = (y_center - dwdh[1]) / ratio[1]  # 还原中心点 y 坐标
            width /= ratio[0]  # 还原宽度
            height /= ratio[1]  # 还原高度

            boxes.append([x_center, y_center, width, height, angle])  # 将边界框信息加入列表
            scores.append(confidence)  # 将置信度加入列表
            classes.append(class_id)  # 将类别加入列表

    if not boxes:  # 如果没有符合条件的边界框,返回空列表
        return []

    # 转换为 NumPy 数组
    boxes = np.array(boxes)
    scores = np.array(scores)
    classes = np.array(classes)

    # 应用旋转 NMS
    keep_indices = rotated_nms_with_probiou(boxes, scores, iou_threshold=iou_threshold)

    # 构建最终检测结果
    for idx in keep_indices:
        x_center, y_center, width, height, angle = boxes[idx]  # 获取保留的边界框信息
        confidence = scores[idx]  # 获取对应的置信度
        class_id = classes[idx]  # 获取类别
        obb_corners = calculate_obb_corners(x_center, y_center, width, height, angle)  # 计算旋转边界框的四个角点

        # 将检测结果添加到列表中
        detections.append({
            "position": obb_corners,  # 旋转边界框的角点坐标
            "confidence": float(confidence),  # 置信度
            "class_id": int(class_id),  # 类别 ID
            "angle": float(angle)  # 旋转角度
        })

    return detections  # 返回检测结果

parse_onnx_output函数需要用到calculate_obb_corners函数,根据旋转角度计算旋转边界框的四个角点

def calculate_obb_corners(x_center, y_center, width, height, angle):
    """
    根据旋转角度计算旋转边界框的四个角点。
    :param x_center: 边界框中心的 x 坐标
    :param y_center: 边界框中心的 y 坐标
    :param width: 边界框的宽度
    :param height: 边界框的高度
    :param angle: 旋转角度
    :return: 旋转边界框的四个角点坐标
    """
    cos_angle = np.cos(angle)  # 计算旋转角度的余弦值
    sin_angle = np.sin(angle)  # 计算旋转角度的正弦值
    dx = width / 2  # 计算宽度的一半
    dy = height / 2  # 计算高度的一半

    # 计算旋转边界框的四个角点坐标
    corners = [
        (int(x_center + cos_angle * dx - sin_angle * dy), int(y_center + sin_angle * dx + cos_angle * dy)),
        (int(x_center - cos_angle * dx - sin_angle * dy), int(y_center - sin_angle * dx + cos_angle * dy)),
        (int(x_center - cos_angle * dx + sin_angle * dy), int(y_center - sin_angle * dx - cos_angle * dy)),
        (int(x_center + cos_angle * dx + sin_angle * dy), int(y_center + sin_angle * dx - cos_angle * dy)),
    ]
    return corners  # 返回角点坐标

8、完整代码

完整代码,如下所示:

import os
import cv2
import numpy as np
import onnxruntime as ort
import logging

"""
YOLO11 旋转目标检测OBB
1、ONNX模型推理、可视化
2、ONNX输出格式: x_center, y_center, width, height, class1_confidence, ..., classN_confidence, angle
3、支持不同尺寸图片输入、支持旋转NMS过滤重复框、支持ProbIoU旋转IOU计算
"""

def letterbox(img, new_shape=(640, 640), color=(0, 0, 0), auto=False, scale_fill=False, scale_up=False, stride=32):
    """
    将图像调整为指定尺寸,同时保持长宽比,添加填充以适应目标输入形状。
    :param img: 输入图像
    :param new_shape: 目标尺寸
    :param color: 填充颜色
    :param auto: 是否自动调整填充为步幅的整数倍
    :param scale_fill: 是否强制缩放以完全填充目标尺寸
    :param scale_up: 是否允许放大图像
    :param stride: 步幅,用于自动调整填充
    :return: 调整后的图像、缩放比例、填充尺寸(dw, dh)
    """
    shape = img.shape[:2]
    if isinstance(new_shape, int):
        new_shape = (new_shape, new_shape)

    r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])  # 计算缩放比例
    if not scale_up:
        r = min(r, 1.0)

    ratio = r, r
    new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
    dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]

    if auto:
        dw, dh = np.mod(dw, stride), np.mod(dh, stride)
    elif scale_fill:
        dw, dh = 0.0, 0.0
        new_unpad = (new_shape[1], new_shape[0])
        ratio = new_shape[1] / shape[1], new_shape[0] / shape[0]

    dw /= 2  # 填充均分
    dh /= 2

    if shape[::-1] != new_unpad:
        img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR)
    top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
    left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
    img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)
    return img, ratio, (dw, dh)

def load_model(weights):
    """
    加载ONNX模型并返回会话对象。
    :param weights: 模型权重文件路径
    :return: ONNX运行会话对象
    """
    session = ort.InferenceSession(weights, providers=['CPUExecutionProvider'])
    logging.info(f"模型加载成功: {weights}")
    return session

def _get_covariance_matrix(obb):
    """
    计算旋转边界框的协方差矩阵。
    :param obb: 旋转边界框 (Oriented Bounding Box),包含中心坐标、宽、高和旋转角度
    :return: 协方差矩阵的三个元素 a, b, c
    """
    widths = obb[..., 2] / 2
    heights = obb[..., 3] / 2
    angles = obb[..., 4]

    cos_angle = np.cos(angles)
    sin_angle = np.sin(angles)

    a = (widths * cos_angle)**2 + (heights * sin_angle)**2
    b = (widths * sin_angle)**2 + (heights * cos_angle)**2
    c = widths * cos_angle * heights * sin_angle

    return a, b, c

def batch_probiou(obb1, obb2, eps=1e-7):
    """
    计算旋转边界框之间的 ProbIoU。
    :param obb1: 第一个旋转边界框集合
    :param obb2: 第二个旋转边界框集合
    :param eps: 防止除零的极小值
    :return: 两个旋转边界框之间的 ProbIoU
    """
    x1, y1 = obb1[..., 0], obb1[..., 1]
    x2, y2 = obb2[..., 0], obb2[..., 1]
    a1, b1, c1 = _get_covariance_matrix(obb1)
    a2, b2, c2 = _get_covariance_matrix(obb2)

    t1 = ((a1[:, None] + a2) * (y1[:, None] - y2)**2 + (b1[:, None] + b2) * (x1[:, None] - x2)**2) / (
            (a1[:, None] + a2) * (b1[:, None] + b2) - (c1[:, None] + c2)**2 + eps) * 0.25
    t2 = ((c1[:, None] + c2) * (x2 - x1[:, None]) * (y1[:, None] - y2)) / (
            (a1[:, None] + a2) * (b1[:, None] + b2) - (c1[:, None] + c2)**2 + eps) * 0.5
    t3 = np.log(((a1[:, None] + a2) * (b1[:, None] + b2) - (c1[:, None] + c2)**2) /
                (4 * np.sqrt((a1 * b1 - c1**2)[:, None] * (a2 * b2 - c2**2)) + eps) + eps) * 0.5

    bd = np.clip(t1 + t2 + t3, eps, 100.0)
    hd = np.sqrt(1.0 - np.exp(-bd) + eps)
    return 1 - hd

def rotated_nms_with_probiou(boxes, scores, iou_threshold=0.5):
    """
    使用 ProbIoU 执行旋转边界框的非极大值抑制(NMS)。
    :param boxes: 旋转边界框的集合
    :param scores: 每个边界框的置信度得分
    :param iou_threshold: IoU 阈值,用于确定是否抑制框
    :return: 保留的边界框索引列表
    """
    order = scores.argsort()[::-1]  # 根据置信度得分降序排序
    keep = []

    while len(order) > 0:
        i = order[0]
        keep.append(i)

        if len(order) == 1:
            break

        remaining_boxes = boxes[order[1:]]
        iou_values = batch_probiou(boxes[i:i+1], remaining_boxes).squeeze(0)

        mask = iou_values < iou_threshold  # 保留 IoU 小于阈值的框
        order = order[1:][mask]

    return keep

def run_inference(session, image_bytes, imgsz=(640, 640)):
    """
    对输入图像进行预处理,然后使用ONNX模型执行推理。
    :param session: ONNX运行会话对象
    :param image_bytes: 输入图像的字节数据
    :param imgsz: 模型输入的尺寸
    :return: 推理结果、缩放比例、填充尺寸
    """
    im0 = cv2.imdecode(np.frombuffer(image_bytes, np.uint8), cv2.IMREAD_COLOR)  # 解码图像字节数据
    if im0 is None:
        raise ValueError("无法从image_bytes解码图像")

    img, ratio, (dw, dh) = letterbox(im0, new_shape=imgsz)  # 调整图像尺寸
    img = img.transpose((2, 0, 1))[::-1]  # 调整通道顺序
    img = np.ascontiguousarray(img)
    img = img[np.newaxis, ...].astype(np.float32) / 255.0  # 归一化处理

    input_name = session.get_inputs()[0].name
    result = session.run(None, {input_name: img})  # 执行模型推理

    return result[0], ratio, (dw, dh)

def parse_onnx_output(output, ratio, dwdh, conf_threshold=0.5, iou_threshold=0.5):
    """
    解析ONNX模型的输出,提取旋转边界框坐标、置信度和类别信息,并应用旋转NMS。
    :param output: ONNX模型的输出,包含预测的边界框信息
    :param ratio: 缩放比例,用于将坐标还原到原始尺度
    :param dwdh: 填充的宽高,用于调整边界框的中心点坐标
    :param conf_threshold: 置信度阈值,过滤低于该阈值的检测框
    :param iou_threshold: IoU 阈值,用于旋转边界框的非极大值抑制(NMS)
    :return: 符合条件的旋转边界框的检测结果
    """
    boxes, scores, classes, detections = [], [], [], []
    num_detections = output.shape[2]  # 获取检测的边界框数量
    num_classes = output.shape[1] - 6  # 计算类别数量

    # 逐个解析每个检测结果
    for i in range(num_detections):
        detection = output[0, :, i]
        x_center, y_center, width, height = detection[0], detection[1], detection[2], detection[3]  # 提取边界框的中心坐标和宽高
        angle = detection[-1]  # 提取旋转角度

        if num_classes > 0:
            class_confidences = detection[4:4 + num_classes]  # 获取类别置信度
            if class_confidences.size == 0:
                continue
            class_id = np.argmax(class_confidences)  # 获取置信度最高的类别索引
            confidence = class_confidences[class_id]  # 获取对应的置信度
        else:
            confidence = detection[4]  # 如果没有类别信息,直接使用置信度值
            class_id = 0  # 默认类别为 0

        if confidence > conf_threshold:  # 过滤掉低置信度的检测结果
            x_center = (x_center - dwdh[0]) / ratio[0]  # 还原中心点 x 坐标
            y_center = (y_center - dwdh[1]) / ratio[1]  # 还原中心点 y 坐标
            width /= ratio[0]  # 还原宽度
            height /= ratio[1]  # 还原高度

            boxes.append([x_center, y_center, width, height, angle])  # 将边界框信息加入列表
            scores.append(confidence)  # 将置信度加入列表
            classes.append(class_id)  # 将类别加入列表

    if not boxes:
        return []

    # 转换为 NumPy 数组
    boxes = np.array(boxes)
    scores = np.array(scores)
    classes = np.array(classes)

    # 应用旋转 NMS
    keep_indices = rotated_nms_with_probiou(boxes, scores, iou_threshold=iou_threshold)

    # 构建最终检测结果
    for idx in keep_indices:
        x_center, y_center, width, height, angle = boxes[idx]  # 获取保留的边界框信息
        confidence = scores[idx]  # 获取对应的置信度
        class_id = classes[idx]  # 获取类别
        obb_corners = calculate_obb_corners(x_center, y_center, width, height, angle)  # 计算旋转边界框的四个角点

        detections.append({
            "position": obb_corners,  # 旋转边界框的角点坐标
            "confidence": float(confidence),  # 置信度
            "class_id": int(class_id),  # 类别 ID
            "angle": float(angle)  # 旋转角度
        })

    return detections

def calculate_obb_corners(x_center, y_center, width, height, angle):
    """
    根据旋转角度计算旋转边界框的四个角点。
    :param x_center: 边界框中心的 x 坐标
    :param y_center: 边界框中心的 y 坐标
    :param width: 边界框的宽度
    :param height: 边界框的高度
    :param angle: 旋转角度
    :return: 旋转边界框的四个角点坐标
    """
    cos_angle = np.cos(angle)  # 计算旋转角度的余弦值
    sin_angle = np.sin(angle)  # 计算旋转角度的正弦值
    dx = width / 2  # 计算宽度的一半
    dy = height / 2  # 计算高度的一半

    # 计算旋转边界框的四个角点坐标
    corners = [
        (int(x_center + cos_angle * dx - sin_angle * dy), int(y_center + sin_angle * dx + cos_angle * dy)),
        (int(x_center - cos_angle * dx - sin_angle * dy), int(y_center - sin_angle * dx + cos_angle * dy)),
        (int(x_center - cos_angle * dx + sin_angle * dy), int(y_center - sin_angle * dx - cos_angle * dy)),
        (int(x_center + cos_angle * dx + sin_angle * dy), int(y_center + sin_angle * dx - cos_angle * dy)),
    ]
    return corners  # 返回角点坐标

def save_detections(image, detections, output_path):
    """
    在图像上绘制旋转边界框检测结果并保存。
    :param image: 原始图像
    :param detections: 检测结果列表
    :param output_path: 保存路径
    """
    for det in detections:
        corners = det['position']  # 获取旋转边界框的四个角点
        confidence = det['confidence']  # 获取置信度
        class_id = det['class_id']  # 获取类别ID

        # 绘制边界框的四条边
        for j in range(4):
            pt1 = corners[j]
            pt2 = corners[(j + 1) % 4]
            cv2.line(image, pt1, pt2, (0, 0, 255), 2)
        
        # 在边界框上方显示类别和置信度
        cv2.putText(image, f'Class: {class_id}, Conf: {confidence:.2f}', 
                    (corners[0][0], corners[0][1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255, 255, 255), 3)

    cv2.imwrite(output_path, image)  # 保存绘制后的图像

def process_images_in_folder(folder_path, model_weights, output_folder, conf_threshold, iou_threshold, imgsz):
    """
    批量处理文件夹中的图像,执行推理、解析和可视化,保存结果。
    :param folder_path: 输入图像文件夹路径
    :param model_weights: ONNX模型权重文件路径
    :param output_folder: 输出结果文件夹路径
    :param conf_threshold: 置信度阈值
    :param iou_threshold: IoU 阈值,用于旋转NMS
    :param imgsz: 模型输入大小
    """
    session = load_model(weights=model_weights)  # 加载ONNX模型
    
    if not os.path.exists(output_folder):
        os.makedirs(output_folder)  # 如果输出文件夹不存在,则创建

    for filename in os.listdir(folder_path):
        if filename.endswith(('.jpg', '.png', '.jpeg')):  # 处理图片文件
            image_path = os.path.join(folder_path, filename)
            with open(image_path, 'rb') as f:
                image_bytes = f.read()
            print("image_path:", image_path)

            raw_output, ratio, dwdh = run_inference(session=session, image_bytes=image_bytes, imgsz=imgsz)  # 执行推理
            detections = parse_onnx_output(raw_output, ratio, dwdh, conf_threshold=conf_threshold, iou_threshold=iou_threshold)  # 解析输出

            im0 = cv2.imdecode(np.frombuffer(image_bytes, np.uint8), cv2.IMREAD_COLOR)  # 解码图像
            output_path = os.path.join(output_folder, filename)
            save_detections(im0, detections, output_path)  # 保存检测结果

# 主函数:加载参数
if __name__ == "__main__":
    folder_path = r"point_offer_20240930_rgb"  # 输入图像文件夹路径
    model_weights = r"YOLO11_obb_exp39_cpu.onnx"  # ONNX模型路径
    output_folder = "results"  # 输出结果文件夹
    conf_threshold = 0.5  # 置信度阈值
    iou_threshold = 0.5  # IoU阈值,用于旋转NMS
    imgsz = (640, 640)  # 模型输入大小

    process_images_in_folder(folder_path, model_weights, output_folder, conf_threshold, iou_threshold, imgsz)  # 执行批量处理

使用这个代码时,需要修改配置参数:

  • folder_path: 输入图像文件夹路径,指向包含待检测图像的目录。

  • model_weights: ONNX 模型文件路径,指向训练好的模型文件。

  • output_folder: 检测结果保存的文件夹路径,输出检测后的图片。

  • conf_threshold: 置信度阈值,用于过滤低置信度的检测框。建议调整以平衡检测精度,默认值为 0.5

  • iou_threshold: IoU 阈值,用于旋转边界框的非极大值抑制(NMS),默认值为 0.5。较低值减少重叠,较高值保留更多边界框。

  • imgsz: 输入图像的尺寸,例如 (640, 640)。应与模型训练时的输入尺寸一致。

 YOLO11相关文章推荐:

一篇文章快速认识YOLO11 | 关键改进点 | 安装使用 | 模型训练和推理-CSDN博客

一篇文章快速认识 YOLO11 | 实例分割 | 模型训练 | 自定义数据集-CSDN博客

一篇文章快速认识YOLO11 | 旋转目标检测 | 原理分析 | 模型训练 | 模型推理_yolov11 obb-CSDN博客

YOLO11模型推理 | 目标检测与跟踪 | 实例分割 | 关键点估计 | OBB旋转目标检测-CSDN博客

YOLO11模型训练 | 目标检测与跟踪 | 实例分割 | 关键点姿态估计-CSDN博客

YOLO11 实例分割 | 自动标注 | 预标注 | 标签格式转换 | 手动校正标签-CSDN博客

YOLO11 实例分割 | 导出ONNX模型 | ONNX模型推理-CSDN博客

YOLO11 目标检测 | 导出ONNX模型 | ONNX模型推理-CSDN博客

YOLO11 目标检测 | 自动标注 | 预标注 | 标签格式转换 | 手动校正标签_yolo11 标注平台-CSDN博客

YOLO11 图像缩放 | 图像填充 | 自适应不同尺寸的图片_yolov11 中图像预处理-CSDN博客

YOLO11 旋转目标检测 | 数据标注 | 自定义数据集 | 模型训练 | 模型推理-CSDN博客

分享完成,欢迎大家多多点赞收藏,谢谢~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/916756.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

oracle查询字段类型长度等字段信息

1.查询oracle数据库的字符集 SELECT * FROM NLS_DATABASE_PARAMETERS WHERE PARAMETER NLS_CHARACTERSET; 2.查询字段长度类型 SELECT * FROM user_tab_columns WHERE table_name user AND COLUMN_NAME SNAME 请确保将user替换为您想要查询的表名。sname为字段名 这里的字…

大模型基础BERT——Transformers的双向编码器表示

大模型基础BERT——Transformers的双向编码器表示 整体概况 BERT&#xff1a;用于语言理解的深度双向Transform的预训练 论文题目&#xff1a;BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding Bidirectional Encoder Representations from…

Ceph层次架构分析

Ceph的层次结构可以从逻辑上自下向上分为以下几个层次&#xff1a; 一、基础存储系统RADOS层 功能&#xff1a;RADOS&#xff08;Reliable Autonomic Distributed Object Store&#xff09;是Ceph的底层存储系统&#xff0c;提供了分布式存储的核心功能。它是一个完整的对象存…

实验6记录网络与故障排除

实验6记录网络与故障排除 实验目的及要求&#xff1a; 通过实验&#xff0c;掌握如何利用文档记录网络设备相关信息并完成网络拓扑结构的绘制。能够使用各种技术和工具来找出连通性问题&#xff0c;使用文档来指导故障排除工作&#xff0c;确定具体的网络问题&#xff0c;实施…

【前端】技术演进发展简史

一、前端 1、概述 1990 年&#xff0c;第一个web浏览器诞生&#xff0c;Tim 以超文本语言 HTML 为基础在 NeXT 电脑上发明了最原始的 Web 浏览器。 1991 年&#xff0c;WWW诞生&#xff0c;这标志着前端技术的开始。 前端&#xff08;Front-end&#xff09;和后端&#xff08;…

【笔记】关于git和GitHub和git bash

如何推送更新的代码到github仓库 如何在此项目已经提交在别的远程仓库的基础上更改远程仓库地址&#xff08;也就是换一个远程仓库提交&#xff09; 如何删除github中的一个文件 第二版 删除github上的一个仓库或者仓库里面的某个文件_github仓库删除一个文件好麻烦-CSDN博客 …

Chromium 中sqlite数据库操作演示c++

本文主要演示sqlite数据库 增删改查创建数据库以及数据库表的基本操作&#xff0c;仅供学习参考。 一、sqlite数据库操作类封装&#xff1a; sql\database.h sql\database.cc // Copyright 2012 The Chromium Authors // Use of this source code is governed by a BSD-sty…

谷歌Gemini发布iOS版App,live语音聊天免费用!

大家好&#xff0c;我是木易&#xff0c;一个持续关注AI领域的互联网技术产品经理&#xff0c;国内Top2本科&#xff0c;美国Top10 CS研究生&#xff0c;MBA。我坚信AI是普通人变强的“外挂”&#xff0c;专注于分享AI全维度知识&#xff0c;包括但不限于AI科普&#xff0c;AI工…

autoDL微调训练qwen2vl大模型

autodl是一家GPU服务厂商&#xff0c;提供专业的GPU租用服务&#xff0c;秒级计费、稳定好用 先去autodl把官方的帮助文档看懂先 AutoDL帮助文档 autodl注册并登陆&#xff0c;充钱&#xff0c;根据自己的情况租用新实例 创建新实例后马上关机&#xff0c;因为有个省钱的办法…

使用大语言模型创建 Graph 数据

Neo4j 是开源的 Graph 数据库&#xff0c;Graph 数据通过三元组进行表示&#xff0c;两个顶点一条边&#xff0c;从语意上可以理解为&#xff1a;主语、谓语和宾语。GraphDB 能够通过图来表达复杂的结构&#xff0c;非常适合存储知识型数据&#xff0c;本文将通过大语言实现图数…

RDIFramework.NET Web敏捷开发框架 V6.1发布(.NET6+、Framework双引擎)

RDIFramwork.NET Web敏捷开发框架V6.1版本发布&#xff0c;本次版本更新得非常多&#xff0c;主要有全面重新设计业务逻辑代码&#xff0c;代码量减少一半以上&#xff0c;开发更加高效。底层引入最易上手的ORM框架SqlSugar&#xff0c;让开发更加便利高效。同时保持与前期版本…

vscode-相关自用插件(倒计时,时间显示,编码对齐,css等编码颜色,简体中文,git提交相关,vue项目)

1.倒计时插件 2.时间显示插件 3.编码对齐格式颜色条 4.css等编码颜色 5.简体中文 6.git提交相关 7.vue项目

推荐一款优秀的Flash幻灯片制作软件:Flash Gallery Factory

iPixSoft Flash Gallery Factory是一款优秀的Flash幻灯片制作软件&#xff0c;可以把图片变换成绚丽多彩的Flash幻灯片和Flash相册&#xff0c;并带有动画模板、过渡效果、装饰及背景音乐等功能&#xff0c;是一款不容错过的软件。 iPixSoft Flash Gallery Factory是一款最佳的…

【Linux】man 手册的使用指南

man 手册的使用指南 man手册中文版上传至资源&#xff08;用心整理&#xff0c;感谢理解&#xff01;&#xff09; man手册官方下载链接&#xff1a;https://mirrors.edge.kernel.org/pub/linux/docs/man-pages/ man 手册页&#xff1a;https://linux.die.net/man/ Linux man…

机器学习-35-提取时间序列信号的特征

文章目录 1 特征提取方法1.1 特征提取过程1.2 两类特征提取方法2 基于数据驱动的方法2.1 领域特定特征提取2.2 基于频率的特征提取2.2.1 模拟信号2.2.2 傅里叶变换2.2.3 抽取最大幅值对应特征2.2.4 抽取峰值幅值对应特征2.3 基于统计的特征提取2.4 基于时间的特征提取3 参考附录…

redis序列化数据查询

可以看到是HashMap&#xff0c;那么是序列化的数据 那么我们来获得反序列化数据 import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.ObjectInputStream; import redis.clients.jedis.Jedis;public class RedisDeserializeDemo {public static…

vue3 中直接使用 JSX ( lang=“tsx“ 的用法)

1. 安装依赖 npm i vitejs/plugin-vue-jsx2. 添加配置 vite.config.ts 中 import vueJsx from vitejs/plugin-vue-jsxplugins 中添加 vueJsx()3. 页面使用 <!-- 注意 lang 的值为 tsx --> <script setup lang"tsx"> const isDark ref(false)// 此处…

uniapp 实现 ble蓝牙同时连接多台蓝牙设备,支持app、苹果(ios)和安卓手机,以及ios连接蓝牙后的一些坑

首先对 uniapp BLE蓝牙API进行封装 这里我封装了一个类&#xff1a;bluetoothService.js 代码&#xff1a; import { throttle } from lodash export default class Bluetooth {constructor() {this.device {};this.connected false;// 使用箭头函数绑定类实例的上下文&am…

波段多空强弱指标案例,源码分享

俗话说&#xff0c;涨有涨势&#xff0c;跌有跌势&#xff0c;最怕涨跌不成型。对于波段来说&#xff0c;不论上涨还是下跌&#xff0c;都是可以进行操作或者回避的。但是波动的走势&#xff0c;往往只有走完才能完全确认。那么能不能量化波段里面涨跌的强弱变化呢&#xff1f;…

第21课-C++[set和map学习和使用]

&#x1f33c;引言 C 标准模板库&#xff08;STL&#xff09;中的 set 和 map 是两种非常实用的关联式容器。它们具备快速查找、有序存储的特点&#xff0c;因而在很多需要高效数据管理的场景中被广泛应用。本文将深入讲解 set 和 map 的用法&#xff0c;并通过实际例子分析如何…