YOLOv8 NMS ONNX 合并

Создано: 12 декабря 2024 г.

Отвечено с использованием O1 от Chat01

Вопрос

我需要把onnx 的 NonMaxSuppression 算子添加到yolov8 onnx模型文件的末尾,合并成yolov8+nms新的模型结构,该模型可以直接输出筛选过后的box conf cls ,并导出合并后的新onnx模型, 导出后的模型用这个代码测试, 请你注意box conf cls 的数据维度, 并且在代码中尽量用slice不要用 split

Мысль

画图模型

接收数据

思考,持续 几秒

Ответ

要将 YOLOv8 的 ONNX 模型末尾添加 NonMaxSuppression(NMS)算子,并导出一个新的包含 NMS 的 ONNX 模型,使其能够直接输出筛选后的边界框(boxes)、置信度(conf)和类别(cls),可以按照以下步骤操作:

步骤概述

  1. 加载原始 YOLOv8 ONNX 模型
  2. 解析模型的输出,提取边界框、置信度和类别
  3. 添加 NonMaxSuppression 节点
  4. 调整模型输出,确保输出为筛选后的 boxes、conf 和 cls
  5. 保存新的 ONNX 模型
  6. 编写测试代码验证新模型

详细步骤

1. 安装必要的库

确保已安装 onnxonnxruntime 库。如果未安装,可以使用以下命令进行安装:

bash
pip install onnx onnxruntime

2. 加载原始 YOLOv8 ONNX 模型

python
import onnx from onnx import helper, TensorProto, numpy_helper # 加载原始 YOLOv8 ONNX模型 model = onnx.load('yolov8.onnx') graph = model.graph

3. 解析模型输出

假设 YOLOv8 模型的输出包含边界框、置信度和类别信息。通常,YOLO 模型的输出维度为 (batch_size, num_predictions, 85),其中 85 = 4 (bbox) + 1 (objectness) + 80 (classes)(以 COCO 数据集为例)。

为了确保兼容性,请根据实际的 YOLOv8 输出调整维度。

python
# 假设模型的输出名为 'output' output = graph.output[0].name # 创建常量节点,用于分割输出 # 使用 Slice 操作代替 Split # 假设输出的最后一个维度为 85 # boxes: [batch, num_predictions, 4] boxes = helper.make_tensor_value_info('boxes', TensorProto.FLOAT, ['batch', 'num_predictions', 4]) # scores: [batch, num_predictions, num_classes] scores = helper.make_tensor_value_info('scores', TensorProto.FLOAT, ['batch', 'num_predictions', 80]) # 添加 Slice 节点提取 boxes和scores # 提取 boxes slice_boxes = helper.make_node( 'Slice', inputs=[output], outputs=['boxes'], axes=[-1], starts=[0], ends=[4] ) # 提取 scores (objectness * class scores) slice_scores = helper.make_node( 'Slice', inputs=[output], outputs=['scores'], axes=[-1], starts=[5], ends=[85] ) # 将 Slice 节点添加到图中 graph.node.extend([slice_boxes, slice_scores])

4. 添加 NonMaxSuppression 节点

python
# 定义 NonMaxSuppression 的输入和输出 nms_input_boxes = 'boxes' nms_input_scores = 'scores' nms_output = 'nms_output' # 添加 NonMaxSuppression 节点 nms_node = helper.make_node( 'NonMaxSuppression', inputs=[nms_input_boxes, nms_input_scores], outputs=[nms_output], max_output_boxes_per_class=100, iou_threshold=0.5, score_threshold=0.5, center_point_box=0 ) graph.node.append(nms_node)

5. 调整模型输出

为了让模型输出筛选后的 boxes、conf 和 cls,需要对 nms_output 进行解析。NonMaxSuppression 的输出通常包含以下内容:

  1. selected_indices: 形状为 (max_detections, 3),每一行包含 (batch_index, class_index, box_index)
  2. selected_scores: 形状为 (max_detections,)
  3. selected_classes: 形状为 (max_detections,)

我们需要使用这些索引从原始 boxesscores 中提取相应的信息。

python
# 添加 Gather 节点提取筛选后的 boxes gather_boxes = helper.make_node( 'Gather', inputs=['boxes', 'nms_output_1'], # 'nms_output_1' 是 selected_indices 的第二列(box_index) outputs=['filtered_boxes'], axis=1 ) # 添加 Gather 节点提取筛选后的 scores gather_scores = helper.make_node( 'Gather', inputs=['scores', 'nms_output_1'], # 'nms_output_1' 是 selected_scores outputs=['filtered_scores'], axis=1 ) # 添加 Identity 节点将 selected_classes 作为输出 filtered_classes = helper.make_node( 'Identity', inputs=['nms_output_2'], # 'nms_output_2' 是 selected_classes outputs=['filtered_classes'] ) graph.node.extend([gather_boxes, gather_scores, filtered_classes]) # 设置新的输出 graph.output.clear() graph.output.extend([ helper.make_tensor_value_info('filtered_boxes', TensorProto.FLOAT, [None, 4]), helper.make_tensor_value_info('filtered_scores', TensorProto.FLOAT, [None]), helper.make_tensor_value_info('filtered_classes', TensorProto.INT64, [None]) ])

注意:上述代码中的 'nms_output_1''nms_output_2' 需要根据实际 NonMaxSuppression 输出的顺序进行调整。通常,NonMaxSuppression 有多个输出,具体名称需要通过查看模型来确认。

6. 保存新的 ONNX 模型

python
# 保存新的模型 onnx.save(model, 'yolov8_with_nms.onnx')

7. 测试新的 ONNX 模型

以下是一个测试代码示例,使用 onnxruntime 加载并运行新的模型,确保输出的 boxes、conf 和 cls 的维度正确。代码中尽量使用 Slice 操作而非 Split

python
import onnxruntime as ort import numpy as np # 加载新的模型 session = ort.InferenceSession('yolov8_with_nms.onnx') # 准备输入数据 # 假设输入名为 'input',需要根据实际模型调整 input_name = session.get_inputs()[0].name input_shape = session.get_inputs()[0].shape # 生成一个随机输入,实际使用时替换为真实数据 input_data = np.random.randn(*input_shape).astype(np.float32) # 运行推理 outputs = session.run(None, {input_name: input_data}) # 获取筛选后的 boxes, scores, classes filtered_boxes, filtered_scores, filtered_classes = outputs # 打印输出信息 print("Filtered Boxes:", filtered_boxes.shape) print(filtered_boxes) print("Filtered Scores:", filtered_scores.shape) print(filtered_scores) print("Filtered Classes:", filtered_classes.shape) print(filtered_classes)

注意事项

  1. 节点名称和输出索引:确保在添加 SliceNonMaxSuppression 节点时,使用正确的输入和输出名称。可以使用 onnx.helper.printable_graph(model.graph) 来查看模型的结构和节点名称。

  2. 维度信息:在设置 TensorProto 的维度时,使用 None 表示可变的维度(例如批次大小或检测数量)。根据实际需求,可以具体化这些维度。

  3. 使用 Slice 替代 Split:在上述步骤中,使用 Slice 节点来提取特定维度的数据,避免使用 Split 节点,以符合用户的要求。

  4. 调试和验证:在修改模型后,建议使用工具如 Netron 可视化模型结构,确保 NonMaxSuppression 节点和后续节点正确连接。

  5. 属性调整NonMaxSuppression 节点的属性(如 iou_thresholdscore_threshold 等)可以根据实际需求进行调整。

完整示例代码

以下是将上述步骤整合后的完整代码示例:

python
import onnx from onnx import helper, TensorProto import onnxruntime as ort import numpy as np # 加载原始 YOLOv8 ONNX模型 model = onnx.load('yolov8.onnx') graph = model.graph # 获取模型的输出名称 original_output = graph.output[0].name # 创建 Slice 节点提取 boxes slice_boxes = helper.make_node( 'Slice', inputs=[original_output], outputs=['boxes'], axes=[-1], starts=[0], ends=[4] ) # 创建 Slice 节点提取 scores slice_scores = helper.make_node( 'Slice', inputs=[original_output], outputs=['scores'], axes=[-1], starts=[5], ends=[85] ) # 添加 Slice 节点到图中 graph.node.extend([slice_boxes, slice_scores]) # 添加 NonMaxSuppression 节点 nms_node = helper.make_node( 'NonMaxSuppression', inputs=['boxes', 'scores'], outputs=['selected_indices', 'selected_scores', 'selected_classes'], max_output_boxes_per_class=100, iou_threshold=0.5, score_threshold=0.5, center_point_box=0 ) graph.node.append(nms_node) # 使用 Gather 和 Identity 节点提取最终输出 # 假设 'selected_indices' 的第三列是 box_index # 需要添加节点来提取这些索引并获取对应的 boxes 和 scores # 创建 Constant 节点用于获取 box_index constant_box_index = helper.make_node( 'Constant', inputs=[], outputs=['box_index_offset'], value=helper.make_tensor('box_index_offset', TensorProto.INT64, [1], [2]) # 假设 box_index 在第三列 ) # 添加 Constant 节点 graph.node.append(constant_box_index) # 使用 Slice 提取 box_index slice_box_index = helper.make_node( 'Slice', inputs=['selected_indices'], outputs=['box_indices'], axes=[1], starts=[2], ends=[3] ) graph.node.append(slice_box_index) # Gather boxes using box_indices gather_boxes = helper.make_node( 'Gather', inputs=['boxes', 'box_indices'], outputs=['filtered_boxes'], axis=1 ) # Gather scores using box_indices gather_scores = helper.make_node( 'Gather', inputs=['scores', 'box_indices'], outputs=['filtered_scores'], axis=1 ) # Identity node for classes identity_classes = helper.make_node( 'Identity', inputs=['selected_classes'], outputs=['filtered_classes'] ) graph.node.extend([gather_boxes, gather_scores, identity_classes]) # 设置新的输出 graph.output.clear() graph.output.extend([ helper.make_tensor_value_info('filtered_boxes', TensorProto.FLOAT, [None, 4]), helper.make_tensor_value_info('filtered_scores', TensorProto.FLOAT, [None]), helper.make_tensor_value_info('filtered_classes', TensorProto.INT64, [None]) ]) # 保存新的模型 onnx.save(model, 'yolov8_with_nms.onnx') # 测试新的模型 session = ort.InferenceSession('yolov8_with_nms.onnx') # 获取输入名称 input_name = session.get_inputs()[0].name input_shape = session.get_inputs()[0].shape # 生成一个随机输入,实际使用时替换为真实数据 input_data = np.random.randn(*input_shape).astype(np.float32) # 运行推理 outputs = session.run(None, {input_name: input_data}) # 获取筛选后的 boxes, scores, classes filtered_boxes, filtered_scores, filtered_classes = outputs # 打印输出信息 print("Filtered Boxes:", filtered_boxes.shape) print(filtered_boxes) print("Filtered Scores:", filtered_scores.shape) print(filtered_scores) print("Filtered Classes:", filtered_classes.shape) print(filtered_classes)

结论

通过上述步骤,您可以成功地将 NonMaxSuppression 算子添加到 YOLOv8 的 ONNX 模型末尾,生成一个新的包含 NMS 的模型,并确保输出为筛选后的边界框、置信度和类别信息。测试代码将帮助您验证模型的正确性,并确保数据维度符合预期。

Вопрос

这是我生成nms 的代码, 请你把我的这个代码和yolov8的推理代码结合到一起,让yolov8 的onnx模型增加一个nms, 并且要返回nms得到的index 所对应的box cls conf。 请你注意数据的维度和格式是否正确, 请你尽量用slice 不要用split def gen_onnx_model():

The protobuf definition can be found here:

https://github.com/onnx/onnx/blob/master/onnx/onnx.proto

Create one input (ValueInfoProto)

boxes = helper.make_tensor_value_info('boxes', TensorProto.FLOAT, [1,6,4]) #输入为[]时,代表输入的是0D的张量,也就是标量

Create one output (ValueInfoProto)

scores = helper.make_tensor_value_info('scores', TensorProto.FLOAT, [1,1,6])
max_output_boxes_per_class = helper.make_tensor('max_output_boxes_per_class', TensorProto.INT64, [1], [2])
iou_threshold = helper.make_tensor('iou_threshold', TensorProto.FLOAT, [1], [0.7])
score_threshold = helper.make_tensor('score_threshold', TensorProto.FLOAT, [1], [0.0])

Create one output (ValueInfoProto)

selected_indices = helper.make_tensor_value_info('selected_indices', TensorProto.INT64, [2,3])

Create a node (NodeProto) - This is based on Pad-11

node_def = helper.make_node(
'NonMaxSuppression', # node name
inputs=['boxes', 'scores', 'max_output_boxes_per_class', 'iou_threshold', 'score_threshold'], # inputs

inputs=['boxes', 'scores'], # inputs

outputs=['selected_indices'],

outputs

center_point_box=0
)

Create the graph (GraphProto)

graph_def = helper.make_graph(
[node_def],
'test-model',
[boxes,scores],
[selected_indices],
[max_output_boxes_per_class,iou_threshold,score_threshold]
)

Create the model (ModelProto)

model_def = onnx.helper.make_model(graph_def, producer_name='zyx')
model_def.opset_import[0].version = 11 #指定版本,我们的版本是11版本
try:
onnx.checker.check_model(model_def)
except onnx.checker.ValidationError as e:
print("model is invalid: %s" % (e))
else:
onnx.save(model_def, "nms_11_new.onnx")
print('The model is:\n{}'.format(model_def))

Мысль

整合生成

我正在将生成的NMS节点代码合并到YOLOv8的ONNX模型中,确保数据维度和格式正确,使用Slice操作代替Split。

加载并整合

加载YOLOv8的ONNX模型,添加NMS操作并使用Slice解析,可能需要修改图以连接NMS节点并输出结果,确保维度正确处理。

更新模型结构

我正在提取YOLOv8输出的张量中的框和分数,添加NMS节点并使用新节点调整模型输出,保存更新后的模型。

整合代码

将用户提供的NMS代码与YOLOv8代码结合,确保输出形状正确。

调整输出格式

确保NMS节点定义包括selected_scores和selected_classes,以匹配ONNX的多输出需求。

更新节点定义
我正在调整NMS节点定义,确保包括selected_scores和selected_classes,以支持ONNX的多输出需求。

集成与优化

虽然用户指的是如何调整模型图,而非运行时推理代码,但我将结合步骤,确保使用Slice操作,数据维度正确,模型输出NMS过滤的框、类和分数。

思考,持续 7 秒

Ответ

要将您提供的 NonMaxSuppression(NMS)算子集成到 YOLOv8 的 ONNX 模型中,并确保模型在推理时直接输出经过 NMS 筛选后的边界框(boxes)、置信度(conf)和类别(cls),您可以按照以下步骤操作。整个过程包括:

  1. 加载原始 YOLOv8 ONNX 模型
  2. 解析 YOLOv8 模型的输出,提取 boxes 和 scores
  3. 添加 NMS 节点
  4. 提取 NMS 输出的 boxes、scores 和 classes
  5. 调整模型输出
  6. 保存新的 ONNX 模型
  7. 编写测试代码验证新模型

以下是详细的步骤和完整的代码示例:

1. 安装必要的库

确保已安装 onnxonnxruntime 库。如果未安装,可以使用以下命令进行安装:

bash
pip install onnx onnxruntime

2. 加载原始 YOLOv8 ONNX 模型

首先,加载您的 YOLOv8 ONNX 模型。假设模型文件名为 yolov8.onnx

python
import onnx from onnx import helper, TensorProto # 加载原始 YOLOv8 ONNX模型 yolov8_model = onnx.load('yolov8.onnx') graph = yolov8_model.graph

3. 解析 YOLOv8 模型的输出

假设 YOLOv8 模型的输出为一个张量,包含了边界框、置信度和类别信息。通常,这个输出的形状为 (batch_size, num_predictions, 85),其中 85 = 4(bbox)+ 1(objectness)+ 80(classes)。根据实际情况,调整以下代码中的维度信息。

python
# 获取原始输出的名称 original_output = graph.output[0].name # 假设输出的最后一个维度为85 # 使用 Slice 节点提取 boxes 和 scores # boxes: [batch, num_predictions, 4] # scores: [batch, num_predictions, num_classes] # 添加 Slice 节点提取 boxes slice_boxes = helper.make_node( 'Slice', inputs=[original_output], outputs=['boxes'], axes=[-1], starts=[0], ends=[4] ) # 添加 Slice 节点提取 scores(objectness * class scores) slice_scores = helper.make_node( 'Slice', inputs=[original_output], outputs=['scores'], axes=[-1], starts=[5], # 从第6个元素开始(0索引) ends=[85] # 到第85个元素结束 ) # 将 Slice 节点添加到图中 graph.node.extend([slice_boxes, slice_scores])

4. 添加 NMS 节点

接下来,使用您提供的 NMS 代码,在模型中添加 NonMaxSuppression 节点。需要注意的是,ONNX 的 NonMaxSuppression 节点可以有多个输出,包括 selected_indicesselected_scoresselected_classes。我们需要确保这些输出能够被后续节点正确解析。

python
# 创建常量节点用于 NMS 的参数 max_output_boxes_per_class = helper.make_tensor('max_output_boxes_per_class', TensorProto.INT64, [1], [100]) iou_threshold = helper.make_tensor('iou_threshold', TensorProto.FLOAT, [1], [0.5]) score_threshold = helper.make_tensor('score_threshold', TensorProto.FLOAT, [1], [0.5]) # 添加常量节点到图中 graph.initializer.extend([max_output_boxes_per_class, iou_threshold, score_threshold]) # 定义 NonMaxSuppression 的输入和输出 nms_inputs = ['boxes', 'scores', 'max_output_boxes_per_class', 'iou_threshold', 'score_threshold'] nms_outputs = ['selected_indices', 'selected_scores', 'selected_classes'] # 创建 NonMaxSuppression 节点 nms_node = helper.make_node( 'NonMaxSuppression', inputs=nms_inputs, outputs=nms_outputs, center_point_box=0 ) # 将 NMS 节点添加到图中 graph.node.append(nms_node)

5. 提取 NMS 输出的 Boxes、Scores 和 Classes

使用 GatherSlice 节点,从 selected_indices 中提取相应的边界框、置信度和类别。这里我们尽量使用 Slice 而非 Split 来操作张量。

python
# 提取 selected_indices 中的 box_index(假设 box_index 在第三列) # selected_indices 的形状为 [max_detections, 3],每行包含 [batch_index, class_index, box_index] # 使用 Slice 提取 box_index slice_box_index = helper.make_node( 'Slice', inputs=['selected_indices'], outputs=['box_indices'], axes=[1], starts=[2], ends=[3] ) # 将 Slice 节点添加到图中 graph.node.append(slice_box_index) # 使用 Gather 节点根据 box_indices 提取对应的 boxes gather_boxes = helper.make_node( 'Gather', inputs=['boxes', 'box_indices'], outputs=['filtered_boxes'], axis=1 ) # 使用 Gather 节点根据 box_indices 提取对应的 scores gather_scores = helper.make_node( 'Gather', inputs=['scores', 'box_indices'], outputs=['filtered_scores'], axis=1 ) # 使用 Gather 节点根据 selected_indices 提取对应的 classes # classes 位于 selected_indices 的第二列 slice_class_index = helper.make_node( 'Slice', inputs=['selected_indices'], outputs=['class_indices'], axes=[1], starts=[1], ends=[2] ) gather_classes = helper.make_node( 'Gather', inputs=['scores', 'box_indices'], outputs=['filtered_classes'], axis=2 ) # 将 Gather 节点添加到图中 graph.node.extend([gather_boxes, gather_scores, slice_class_index, gather_classes])

注意:上述代码假设 selected_indices 的第三列为 box_index,第二列为 class_index。根据具体的模型输出,可能需要调整 Slicestartsends

6. 调整模型输出

将模型的输出修改为 filtered_boxesfiltered_scoresfiltered_classes

python
# 清除原有的输出 graph.output.clear() # 设置新的输出 graph.output.extend([ helper.make_tensor_value_info('filtered_boxes', TensorProto.FLOAT, [None, 4]), helper.make_tensor_value_info('filtered_scores', TensorProto.FLOAT, [None]), helper.make_tensor_value_info('filtered_classes', TensorProto.INT64, [None]) ])

7. 保存新的 ONNX 模型

将修改后的模型保存为新的 ONNX 文件,例如 yolov8_with_nms.onnx

python
# 保存新的 ONNX 模型 onnx.save(yolov8_model, 'yolov8_with_nms.onnx') print('新的 ONNX 模型已保存为 yolov8_with_nms.onnx')

8. 测试新的 ONNX 模型

使用 onnxruntime 进行推理,确保模型输出正确。以下是一个示例测试代码:

python
import onnxruntime as ort import numpy as np # 加载新的 ONNX 模型 session = ort.InferenceSession('yolov8_with_nms.onnx') # 获取输入名称 input_name = session.get_inputs()[0].name input_shape = session.get_inputs()[0].shape # 生成一个随机输入,实际使用时替换为真实数据 # 例如,YOLOv8 通常需要 [batch_size, channels, height, width] # 这里以 [1, 3, 640, 640] 为例,根据实际模型调整 input_data = np.random.randn(*input_shape).astype(np.float32) # 运行推理 outputs = session.run(None, {input_name: input_data}) # 获取筛选后的 boxes, scores, classes filtered_boxes, filtered_scores, filtered_classes = outputs # 打印输出信息 print("Filtered Boxes:", filtered_boxes.shape) print(filtered_boxes) print("Filtered Scores:", filtered_scores.shape) print(filtered_scores) print("Filtered Classes:", filtered_classes.shape) print(filtered_classes)

完整示例代码

以下是将上述步骤整合后的完整代码示例:

python
import onnx from onnx import helper, TensorProto import onnxruntime as ort import numpy as np def integrate_nms_into_yolov8(): # 加载原始 YOLOv8 ONNX模型 yolov8_model = onnx.load('yolov8.onnx') graph = yolov8_model.graph # 获取原始输出的名称 original_output = graph.output[0].name # 使用 Slice 节点提取 boxes slice_boxes = helper.make_node( 'Slice', inputs=[original_output], outputs=['boxes'], axes=[-1], starts=[0], ends=[4] ) # 使用 Slice 节点提取 scores(objectness * class scores) slice_scores = helper.make_node( 'Slice', inputs=[original_output], outputs=['scores'], axes=[-1], starts=[5], ends=[85] ) # 将 Slice 节点添加到图中 graph.node.extend([slice_boxes, slice_scores]) # 创建常量节点用于 NMS 的参数 max_output_boxes_per_class = helper.make_tensor('max_output_boxes_per_class', TensorProto.INT64, [1], [100]) iou_threshold = helper.make_tensor('iou_threshold', TensorProto.FLOAT, [1], [0.5]) score_threshold = helper.make_tensor('score_threshold', TensorProto.FLOAT, [1], [0.5]) # 添加常量节点到图中 graph.initializer.extend([max_output_boxes_per_class, iou_threshold, score_threshold]) # 定义 NonMaxSuppression 的输入和输出 nms_inputs = ['boxes', 'scores', 'max_output_boxes_per_class', 'iou_threshold', 'score_threshold'] nms_outputs = ['selected_indices', 'selected_scores', 'selected_classes'] # 创建 NonMaxSuppression 节点 nms_node = helper.make_node( 'NonMaxSuppression', inputs=nms_inputs, outputs=nms_outputs, center_point_box=0 ) # 将 NMS 节点添加到图中 graph.node.append(nms_node) # 使用 Slice 提取 box_index slice_box_index = helper.make_node( 'Slice', inputs=['selected_indices'], outputs=['box_indices'], axes=[1], starts=[2], ends=[3] ) # 将 Slice 节点添加到图中 graph.node.append(slice_box_index) # 使用 Gather 节点根据 box_indices 提取对应的 boxes gather_boxes = helper.make_node( 'Gather', inputs=['boxes', 'box_indices'], outputs=['filtered_boxes'], axis=1 ) # 使用 Gather 节点根据 box_indices 提取对应的 scores gather_scores = helper.make_node( 'Gather', inputs=['scores', 'box_indices'], outputs=['filtered_scores'], axis=1 ) # 使用 Slice 提取 class_index slice_class_index = helper.make_node( 'Slice', inputs=['selected_indices'], outputs=['class_indices'], axes=[1], starts=[1], ends=[2] ) # 使用 Gather 节点根据 class_indices 提取对应的 classes # 由于 classes 通常是独立于 scores 的,需要根据实际模型调整 # 这里假设 classes 信息在 selected_indices 的第二列 # 因此,直接使用 class_indices 作为 classes 输出 gather_classes = helper.make_node( 'Identity', inputs=['class_indices'], outputs=['filtered_classes'] ) # 将 Gather 节点添加到图中 graph.node.extend([gather_boxes, gather_scores, slice_class_index, gather_classes]) # 清除原有的输出 graph.output.clear() # 设置新的输出 graph.output.extend([ helper.make_tensor_value_info('filtered_boxes', TensorProto.FLOAT, [None, 4]), helper.make_tensor_value_info('filtered_scores', TensorProto.FLOAT, [None]), helper.make_tensor_value_info('filtered_classes', TensorProto.INT64, [None]) ]) # 检查模型的有效性 try: onnx.checker.check_model(yolov8_model) except onnx.checker.ValidationError as e: print("模型无效: %s" % e) else: # 保存新的模型 onnx.save(yolov8_model, 'yolov8_with_nms.onnx') print('新的 ONNX 模型已保存为 yolov8_with_nms.onnx') def test_new_model(): # 加载新的 ONNX 模型 session = ort.InferenceSession('yolov8_with_nms.onnx') # 获取输入名称 input_name = session.get_inputs()[0].name input_shape = session.get_inputs()[0].shape # 生成一个随机输入,实际使用时替换为真实数据 # 例如,YOLOv8 通常需要 [batch_size, channels, height, width] # 这里以 [1, 3, 640, 640] 为例,根据实际模型调整 input_data = np.random.randn(*input_shape).astype(np.float32) # 运行推理 outputs = session.run(None, {input_name: input_data}) # 获取筛选后的 boxes, scores, classes filtered_boxes, filtered_scores, filtered_classes = outputs # 打印输出信息 print("筛选后的 Boxes:", filtered_boxes.shape) print(filtered_boxes) print("筛选后的 Scores:", filtered_scores.shape) print(filtered_scores) print("筛选后的 Classes:", filtered_classes.shape) print(filtered_classes) if __name__ == "__main__": integrate_nms_into_yolov8() test_new_model()

解释与注意事项

  1. 节点名称和连接

    • 确保节点名称唯一且正确连接。ONNX 节点之间的连接通过输入和输出名称进行。
    • 使用 Slice 节点提取 boxesscores,并确保 axesstartsends 参数设置正确。
  2. 数据维度

    • SliceGather 节点中,确保轴(axes)、起始位置(starts)和结束位置(ends)与模型的输出维度匹配。
    • 使用 [None, ...] 作为输出维度中的动态维度,以适应不同批次大小和检测数量。
  3. 使用 Slice 替代 Split

    • Slice 节点通过指定轴、起始和结束位置来提取张量的一部分,避免了使用 Split 节点。
  4. 常量节点

    • max_output_boxes_per_classiou_thresholdscore_threshold 作为常量节点添加到图中,供 NonMaxSuppression 使用。
  5. 模型检查

    • 使用 onnx.checker.check_model 确保修改后的模型是有效的。如果模型无效,检查节点连接和维度是否正确。
  6. 测试模型

    • 使用随机数据进行测试,实际应用中应使用真实的输入数据。
    • 确保输出的 filtered_boxesfiltered_scoresfiltered_classes 的形状和内容符合预期。
  7. 调试与验证

    • 使用工具如 Netron 可视化模型结构,检查节点连接是否正确。
    • 根据需要调整 NonMaxSuppression 的参数(如 iou_thresholdscore_threshold)以优化检测结果。

结论

通过上述步骤,您可以成功地将 NonMaxSuppression 算子集成到 YOLOv8 的 ONNX 模型中,生成一个新的包含 NMS 的模型。新的模型将在推理时直接输出经过 NMS 筛选后的边界框、置信度和类别信息。确保在实际应用中根据 YOLOv8 模型的具体输出格式和维度进行必要的调整。

Поделиться этим Q&A