Files
onnx--onnx/onnx/reference/ops/op_non_max_suppression.py
wehub-resource-sync 5cbd3f29e3
Fuzz / Run fuzz harnesses (${{ github.event_name == 'schedule' && 'nightly' || 'smoke' }}) (push) Has been cancelled
Create Releases / call-mac (push) Has been cancelled
Create Releases / call-linux (push) Has been cancelled
Create Releases / call-sdist (push) Has been cancelled
Create Releases / call-win (push) Has been cancelled
Create Releases / call-pyodide (push) Has been cancelled
Windows_No_Exception_CI / build (x64, 3.10) (push) Has been cancelled
Check URLs / build (push) Has been cancelled
Create Releases / Attest CI build artifacts (push) Has been cancelled
Create Releases / Check for Publish release build to pypi (push) Has been cancelled
Create Releases / Check for Publish preview build to test.pypi-weekly (push) Has been cancelled
Create Releases / Publish preview build to test.pypi-weekly (push) Has been cancelled
Create Releases / Check for Publish release build to test.pypi (rc-candidates) (push) Has been cancelled
Create Releases / Publish release build to test.pypi (push) Has been cancelled
Create Releases / Check for Publish preview build to pypi-weekly (push) Has been cancelled
Create Releases / Publish preview build to pypi-weekly (push) Has been cancelled
Create Releases / Publish release build to pypi (push) Has been cancelled
Create Releases / test source distribution (push) Has been cancelled
clang-tidy / clang-tidy (push) Has been cancelled
Lint / Validate SBOM (push) Has been cancelled
Lint / Enforce style (push) Has been cancelled
CI / Test windows-2022, 3.14, External, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test windows-latest, 3.10, Internal, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test windows-latest, 3.14, Internal, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test windows-latest, 3.14t, Internal, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test ubuntu-24.04, 3.14, Internal, debug=1, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test ubuntu-24.04, 3.14, External, debug=0, unity_build=1, onnx_ml=1, autogen=1 (push) Has been cancelled
CI / Test ubuntu-24.04, 3.14, External, debug=0, unity_build=0, onnx_ml=0, autogen=0 (push) Has been cancelled
CI / Test macos-latest, 3.10, Internal, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test macos-latest, 3.14, Internal, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test macos-latest, 3.14t, Internal, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test ubuntu-24.04, 3.14, External, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test ubuntu-24.04, 3.10, Internal, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test ubuntu-24.04, 3.14, Internal, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test ubuntu-24.04, 3.14t, Internal, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
Pixi CI / Install and lint (ubuntu-24.04-arm) (push) Has been cancelled
Pixi CI / Install and lint (windows-2022) (push) Has been cancelled
Pixi CI / Xcode generator build (push) Has been cancelled
Pixi CI / Install and test (macos-latest, default) (push) Has been cancelled
Pixi CI / Install and test (ubuntu-24.04-arm, default) (push) Has been cancelled
Pixi CI / Install and test (ubuntu-latest, default) (push) Has been cancelled
Pixi CI / Install and test (windows-2022, default) (push) Has been cancelled
Pixi CI / Install and test (macos-latest, oldies) (push) Has been cancelled
Pixi CI / Install and test (ubuntu-24.04-arm, oldies) (push) Has been cancelled
Pixi CI / Install and test (ubuntu-latest, oldies) (push) Has been cancelled
Pixi CI / Install and test (windows-2022, oldies) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (cpp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled
Generate and publish ONNX docs / build (push) Has been cancelled
Generate and publish ONNX docs / deploy (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:41:19 +08:00

272 lines
9.1 KiB
Python

# Copyright (c) ONNX Project Contributors
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import dataclasses
import numpy as np
from onnx.reference.op_run import OpRun
@dataclasses.dataclass
class PrepareContext:
boxes_data_: np.ndarray | None = None
boxes_size_: int = 0
scores_data_: np.ndarray | None = None
scores_size_: int = 0
max_output_boxes_per_class_: np.ndarray | None = None
score_threshold_: np.ndarray | None = None
iou_threshold_: np.ndarray | None = None
num_batches_: int = 0
num_classes_: int = 0
num_boxes_: int = 0
class SelectedIndex:
__slots__ = ("batch_index_", "box_index_", "class_index_")
def __init__(
self, batch_index: int = 0, class_index: int = 0, box_index: int = 0
) -> None:
self.batch_index_ = batch_index
self.class_index_ = class_index
self.box_index_ = box_index
def max_min(lhs: float, rhs: float) -> tuple[float, float]:
if lhs >= rhs:
return rhs, lhs
return lhs, rhs
def suppress_by_iou( # noqa: PLR0911
boxes_data: np.ndarray,
box_index1: int,
box_index2: int,
center_point_box: int,
iou_threshold: float,
) -> bool:
box1 = boxes_data[box_index1]
box2 = boxes_data[box_index2]
# center_point_box_ only support 0 or 1
if center_point_box == 0:
# boxes data format [y1, x1, y2, x2]
x1_min, x1_max = max_min(box1[1], box1[3])
x2_min, x2_max = max_min(box2[1], box2[3])
intersection_x_min = max(x1_min, x2_min)
intersection_x_max = min(x1_max, x2_max)
if intersection_x_max <= intersection_x_min:
return False
y1_min, y1_max = max_min(box1[0], box1[2])
y2_min, y2_max = max_min(box2[0], box2[2])
intersection_y_min = max(y1_min, y2_min)
intersection_y_max = min(y1_max, y2_max)
if intersection_y_max <= intersection_y_min:
return False
else:
# 1 == center_point_box_ => boxes data format [x_center, y_center, width, height]
box1_width_half = box1[2] / 2
box1_height_half = box1[3] / 2
box2_width_half = box2[2] / 2
box2_height_half = box2[3] / 2
x1_min = box1[0] - box1_width_half
x1_max = box1[0] + box1_width_half
x2_min = box2[0] - box2_width_half
x2_max = box2[0] + box2_width_half
intersection_x_min = max(x1_min, x2_min)
intersection_x_max = min(x1_max, x2_max)
if intersection_x_max <= intersection_x_min:
return False
y1_min = box1[1] - box1_height_half
y1_max = box1[1] + box1_height_half
y2_min = box2[1] - box2_height_half
y2_max = box2[1] + box2_height_half
intersection_y_min = max(y1_min, y2_min)
intersection_y_max = min(y1_max, y2_max)
if intersection_y_max <= intersection_y_min:
return False
intersection_area = (intersection_x_max - intersection_x_min) * (
intersection_y_max - intersection_y_min
)
if intersection_area <= 0:
return False
area1 = (x1_max - x1_min) * (y1_max - y1_min)
area2 = (x2_max - x2_min) * (y2_max - y2_min)
union_area = area1 + area2 - intersection_area
if area1 <= 0 or area2 <= 0 or union_area <= 0:
return False
intersection_over_union = intersection_area / union_area
return intersection_over_union > iou_threshold
class BoxInfo:
def __init__(self, score: float = 0, idx: int = -1):
self.score_ = score
self.idx_ = idx
def __lt__(self, rhs) -> bool:
return self.score_ < rhs.score_ or (
self.score_ == rhs.score_ and self.idx_ > rhs.idx_
)
def __repr__(self) -> str:
return f"BoxInfo({self.score_}, {self.idx_})"
class NonMaxSuppression(OpRun):
def get_thresholds_from_inputs(
self,
pc: PrepareContext,
max_output_boxes_per_class: int,
iou_threshold: float,
score_threshold: float,
) -> tuple[int, float, float]:
if pc.max_output_boxes_per_class_ is not None:
max_output_boxes_per_class = max(pc.max_output_boxes_per_class_[0], 0)
if pc.iou_threshold_ is not None:
iou_threshold = pc.iou_threshold_[0]
if pc.score_threshold_ is not None:
score_threshold = pc.score_threshold_[0]
return max_output_boxes_per_class, iou_threshold, score_threshold
def prepare_compute(
self,
pc: PrepareContext,
boxes_tensor: np.ndarray, # float
scores_tensor: np.ndarray, # float
max_output_boxes_per_class_tensor: np.ndarray, # int
iou_threshold_tensor: np.ndarray, # float
score_threshold_tensor: np.ndarray, # float
):
pc.boxes_data_ = boxes_tensor
pc.scores_data_ = scores_tensor
if max_output_boxes_per_class_tensor.size != 0:
pc.max_output_boxes_per_class_ = max_output_boxes_per_class_tensor
if iou_threshold_tensor.size != 0:
pc.iou_threshold_ = iou_threshold_tensor
if score_threshold_tensor is not None and score_threshold_tensor.size != 0:
pc.score_threshold_ = score_threshold_tensor
pc.boxes_size_ = boxes_tensor.size
pc.scores_size_ = scores_tensor.size
boxes_dims = boxes_tensor.shape
scores_dims = scores_tensor.shape
pc.num_batches_ = boxes_dims[0]
pc.num_classes_ = scores_dims[1]
pc.num_boxes_ = boxes_dims[1]
def _run(
self,
boxes,
scores,
max_output_boxes_per_class=None,
iou_threshold=None,
score_threshold=None,
center_point_box=None,
):
pc = PrepareContext()
self.prepare_compute(
pc,
boxes,
scores,
max_output_boxes_per_class,
iou_threshold,
score_threshold,
)
(
max_output_boxes_per_class,
iou_threshold,
score_threshold,
) = self.get_thresholds_from_inputs(pc, 0, 0, 0)
if max_output_boxes_per_class.size == 0:
return (np.empty((0,), dtype=np.int64),)
boxes_data = pc.boxes_data_
scores_data = pc.scores_data_
selected_indices = []
# std::vector<BoxInfo> selected_boxes_inside_class;
# selected_boxes_inside_class.reserve(
# std::min<size_t>(static_cast<size_t>(max_output_boxes_per_class), pc.num_boxes_));
for batch_index in range(pc.num_batches_):
for class_index in range(pc.num_classes_):
box_score_offset = (batch_index, class_index)
batch_boxes = boxes_data[batch_index]
# std::vector<BoxInfo> candidate_boxes;
# candidate_boxes.reserve(pc.num_boxes_);
# Filter by score_threshold_
candidate_boxes = []
class_scores = scores_data[box_score_offset]
if pc.score_threshold_ is not None:
for box_index in range(pc.num_boxes_):
if class_scores[box_index] > score_threshold:
candidate_boxes.append( # noqa: PERF401
BoxInfo(class_scores[box_index], box_index)
)
else:
for box_index in range(pc.num_boxes_):
candidate_boxes.append( # noqa: PERF401
BoxInfo(class_scores[box_index], box_index)
)
sorted_boxes = sorted(candidate_boxes)
selected_boxes_inside_class = []
# Get the next box with top score, filter by iou_threshold.
while (
len(sorted_boxes) > 0
and len(selected_boxes_inside_class) < max_output_boxes_per_class
):
next_top_score = sorted_boxes[-1]
selected = True
# Check with existing selected boxes for this class,
# suppress if exceed the IOU (Intersection Over Union) threshold.
for selected_index in selected_boxes_inside_class:
if suppress_by_iou(
batch_boxes,
next_top_score.idx_,
selected_index.idx_,
center_point_box,
iou_threshold,
):
selected = False
break
if selected:
selected_boxes_inside_class.append(next_top_score)
selected_indices.append(
SelectedIndex(batch_index, class_index, next_top_score.idx_)
)
sorted_boxes.pop()
result = np.empty((len(selected_indices), 3), dtype=np.int64)
for i in range(result.shape[0]):
result[i, 0] = selected_indices[i].batch_index_
result[i, 1] = selected_indices[i].class_index_
result[i, 2] = selected_indices[i].box_index_
return (result,)