YOLOv8 NMS ONNX 合并
تم الإنشاء في: ١٢ ديسمبر ٢٠٢٤
تم الإنشاء في: ١٢ ديسمبر ٢٠٢٤
我需要把onnx 的 NonMaxSuppression 算子添加到yolov8 onnx模型文件的末尾,合并成yolov8+nms新的模型结构,该模型可以直接输出筛选过后的box conf cls ,并导出合并后的新onnx模型, 导出后的模型用这个代码测试, 请你注意box conf cls 的数据维度, 并且在代码中尽量用slice不要用 split
要将 YOLOv8 的 ONNX 模型末尾添加 NonMaxSuppression(NMS)算子,并导出一个新的包含 NMS 的 ONNX 模型,使其能够直接输出筛选后的边界框(boxes)、置信度(conf)和类别(cls),可以按照以下步骤操作:
NonMaxSuppression 节点确保已安装 onnx 和 onnxruntime 库。如果未安装,可以使用以下命令进行安装:
bashpip install onnx onnxruntime
pythonimport onnx from onnx import helper, TensorProto, numpy_helper # 加载原始 YOLOv8 ONNX模型 model = onnx.load('yolov8.onnx') graph = model.graph
假设 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])
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)
为了让模型输出筛选后的 boxes、conf 和 cls,需要对 nms_output 进行解析。NonMaxSuppression 的输出通常包含以下内容:
selected_indices: 形状为 (max_detections, 3),每一行包含 (batch_index, class_index, box_index)selected_scores: 形状为 (max_detections,)selected_classes: 形状为 (max_detections,)我们需要使用这些索引从原始 boxes 和 scores 中提取相应的信息。
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 有多个输出,具体名称需要通过查看模型来确认。
python# 保存新的模型 onnx.save(model, 'yolov8_with_nms.onnx')
以下是一个测试代码示例,使用 onnxruntime 加载并运行新的模型,确保输出的 boxes、conf 和 cls 的维度正确。代码中尽量使用 Slice 操作而非 Split。
pythonimport 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)
节点名称和输出索引:确保在添加 Slice 和 NonMaxSuppression 节点时,使用正确的输入和输出名称。可以使用 onnx.helper.printable_graph(model.graph) 来查看模型的结构和节点名称。
维度信息:在设置 TensorProto 的维度时,使用 None 表示可变的维度(例如批次大小或检测数量)。根据实际需求,可以具体化这些维度。
使用 Slice 替代 Split:在上述步骤中,使用 Slice 节点来提取特定维度的数据,避免使用 Split 节点,以符合用户的要求。
调试和验证:在修改模型后,建议使用工具如 Netron 可视化模型结构,确保 NonMaxSuppression 节点和后续节点正确连接。
属性调整:NonMaxSuppression 节点的属性(如 iou_threshold、score_threshold 等)可以根据实际需求进行调整。
以下是将上述步骤整合后的完整代码示例:
pythonimport 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():
boxes = helper.make_tensor_value_info('boxes', TensorProto.FLOAT, [1,6,4]) #输入为[]时,代表输入的是0D的张量,也就是标量
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])
selected_indices = helper.make_tensor_value_info('selected_indices', TensorProto.INT64, [2,3])
node_def = helper.make_node(
'NonMaxSuppression', # node name
inputs=['boxes', 'scores', 'max_output_boxes_per_class', 'iou_threshold', 'score_threshold'], # inputs
outputs=['selected_indices'],
center_point_box=0
)
graph_def = helper.make_graph(
[node_def],
'test-model',
[boxes,scores],
[selected_indices],
[max_output_boxes_per_class,iou_threshold,score_threshold]
)
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))
要将您提供的 NonMaxSuppression(NMS)算子集成到 YOLOv8 的 ONNX 模型中,并确保模型在推理时直接输出经过 NMS 筛选后的边界框(boxes)、置信度(conf)和类别(cls),您可以按照以下步骤操作。整个过程包括:
以下是详细的步骤和完整的代码示例:
确保已安装 onnx 和 onnxruntime 库。如果未安装,可以使用以下命令进行安装:
bashpip install onnx onnxruntime
首先,加载您的 YOLOv8 ONNX 模型。假设模型文件名为 yolov8.onnx。
pythonimport onnx from onnx import helper, TensorProto # 加载原始 YOLOv8 ONNX模型 yolov8_model = onnx.load('yolov8.onnx') graph = yolov8_model.graph
假设 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])
接下来,使用您提供的 NMS 代码,在模型中添加 NonMaxSuppression 节点。需要注意的是,ONNX 的 NonMaxSuppression 节点可以有多个输出,包括 selected_indices、selected_scores 和 selected_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)
使用 Gather 和 Slice 节点,从 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。根据具体的模型输出,可能需要调整 Slice 的 starts 和 ends。
将模型的输出修改为 filtered_boxes、filtered_scores 和 filtered_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]) ])
将修改后的模型保存为新的 ONNX 文件,例如 yolov8_with_nms.onnx。
python# 保存新的 ONNX 模型 onnx.save(yolov8_model, 'yolov8_with_nms.onnx') print('新的 ONNX 模型已保存为 yolov8_with_nms.onnx')
使用 onnxruntime 进行推理,确保模型输出正确。以下是一个示例测试代码:
pythonimport 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)
以下是将上述步骤整合后的完整代码示例:
pythonimport 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()
节点名称和连接:
Slice 节点提取 boxes 和 scores,并确保 axes、starts 和 ends 参数设置正确。数据维度:
Slice 和 Gather 节点中,确保轴(axes)、起始位置(starts)和结束位置(ends)与模型的输出维度匹配。[None, ...] 作为输出维度中的动态维度,以适应不同批次大小和检测数量。使用 Slice 替代 Split:
Slice 节点通过指定轴、起始和结束位置来提取张量的一部分,避免了使用 Split 节点。常量节点:
max_output_boxes_per_class、iou_threshold 和 score_threshold 作为常量节点添加到图中,供 NonMaxSuppression 使用。模型检查:
onnx.checker.check_model 确保修改后的模型是有效的。如果模型无效,检查节点连接和维度是否正确。测试模型:
filtered_boxes、filtered_scores 和 filtered_classes 的形状和内容符合预期。调试与验证:
NonMaxSuppression 的参数(如 iou_threshold 和 score_threshold)以优化检测结果。通过上述步骤,您可以成功地将 NonMaxSuppression 算子集成到 YOLOv8 的 ONNX 模型中,生成一个新的包含 NMS 的模型。新的模型将在推理时直接输出经过 NMS 筛选后的边界框、置信度和类别信息。确保在实际应用中根据 YOLOv8 模型的具体输出格式和维度进行必要的调整。