488 lines
18 KiB
Python
488 lines
18 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
# pylint: disable=invalid-name
|
|
# ruff: noqa: E741
|
|
"""Common utilities used in Non-maximum suppression operators"""
|
|
|
|
import tvm
|
|
from tvm import te
|
|
from tvm.script.ir_builder import IRBuilder
|
|
from tvm.script.ir_builder import tirx as T
|
|
|
|
|
|
def _get_boundaries(output, box_idx):
|
|
l = tvm.te.min(
|
|
output[box_idx],
|
|
output[box_idx + 2],
|
|
)
|
|
t = tvm.te.min(
|
|
output[box_idx + 1],
|
|
output[box_idx + 3],
|
|
)
|
|
r = tvm.te.max(
|
|
output[box_idx],
|
|
output[box_idx + 2],
|
|
)
|
|
b = tvm.te.max(
|
|
output[box_idx + 1],
|
|
output[box_idx + 3],
|
|
)
|
|
return l, t, r, b
|
|
|
|
|
|
def calculate_overlap(out_tensor, box_a_idx, box_b_idx):
|
|
"""Calculate overlap of two boxes."""
|
|
a_l, a_t, a_r, a_b = _get_boundaries(out_tensor, box_a_idx)
|
|
b_l, b_t, b_r, b_b = _get_boundaries(out_tensor, box_b_idx)
|
|
|
|
# Overlapping width and height
|
|
w = tvm.te.max(0.0, tvm.te.min(a_r, b_r) - tvm.te.max(a_l, b_l))
|
|
h = tvm.te.max(0.0, tvm.te.min(a_b, b_b) - tvm.te.max(a_t, b_t))
|
|
|
|
# Overlapping area
|
|
area = h * w
|
|
|
|
# total area of the figure formed by box a and box b
|
|
# except for overlapping area
|
|
u = (a_r - a_l) * (a_b - a_t) + (b_r - b_l) * (b_b - b_t) - area
|
|
return tvm.tirx.Select(u <= 0.0, 0.0, area / u)
|
|
|
|
|
|
def binary_search(y, num_boxes, scores, score_threshold, out):
|
|
"""Binary search for score_threshold on scores sorted in descending order.
|
|
|
|
Must be called within an IRBuilder context.
|
|
"""
|
|
out = T.buffer_proxy(out)
|
|
lo_buf = T.decl_buffer([1], "int32", scope="local")
|
|
hi_buf = T.decl_buffer([1], "int32", scope="local")
|
|
lo = T.buffer_proxy(lo_buf)
|
|
hi = T.buffer_proxy(hi_buf)
|
|
lo[0] = T.int32(0)
|
|
hi[0] = tvm.tirx.Cast("int32", num_boxes)
|
|
with T.While(lo[0] < hi[0]):
|
|
mid = (hi[0] + lo[0]) >> 1
|
|
with T.If(scores[y, mid] > score_threshold):
|
|
with T.Then():
|
|
lo[0] = mid + 1
|
|
with T.Else():
|
|
hi[0] = mid
|
|
out[y] = lo[0]
|
|
|
|
|
|
def _estimate_max_detections(batch_class, input_image_size=None):
|
|
"""Estimate maximum detections based on input image size and number of classes.
|
|
|
|
This provides a more intelligent default for production environments.
|
|
"""
|
|
if input_image_size is not None:
|
|
# Estimate based on image size: larger images typically have more objects
|
|
if len(input_image_size) >= 2:
|
|
height, width = input_image_size[-2], input_image_size[-1]
|
|
total_pixels = height * width
|
|
|
|
# Base estimation per class based on image size
|
|
if total_pixels < 300000: # Small images (< 300k pixels)
|
|
base_detections_per_class = min(50, max(10, total_pixels // 2000))
|
|
elif total_pixels < 1000000: # Medium images (< 1M pixels)
|
|
base_detections_per_class = min(100, max(25, total_pixels // 3000))
|
|
else: # Large images (>= 1M pixels)
|
|
base_detections_per_class = min(200, max(50, total_pixels // 4000))
|
|
|
|
# Scale down for many classes (more realistic for multi-class scenarios)
|
|
if batch_class > 20:
|
|
# For many classes, reduce per-class detections to avoid explosion
|
|
detections_per_class = min(base_detections_per_class, 50)
|
|
else:
|
|
detections_per_class = base_detections_per_class
|
|
else:
|
|
detections_per_class = 50 # fallback
|
|
else:
|
|
# Fallback to class-based estimation
|
|
if batch_class == 1:
|
|
detections_per_class = 100 # Single class detection
|
|
elif batch_class <= 10:
|
|
detections_per_class = 50 # Small multi-class
|
|
else:
|
|
detections_per_class = 25 # Large multi-class (COCO-like)
|
|
|
|
return batch_class * detections_per_class
|
|
|
|
|
|
def collect_selected_indices(
|
|
num_class,
|
|
selected_indices,
|
|
num_detections,
|
|
row_offsets,
|
|
ir,
|
|
max_output_boxes_per_class=None,
|
|
output_shape=None,
|
|
num_total_detections=None,
|
|
input_image_size=None,
|
|
):
|
|
"""Collect selected indices from the core NMS loop into one linear output.
|
|
|
|
Parameters
|
|
----------
|
|
num_class : int
|
|
selected_indices : tvm.te.Tensor
|
|
2-D tensor with shape (batch_size * num_classes, num_boxes), representing the indices
|
|
of selected boxes by the core NMS loop.
|
|
num_detections : tvm.te.Tensor
|
|
1-D tensor with shape (batch_size * num_classes,), representing
|
|
the number of boxes selected by the core NMS loop, per batch and class.
|
|
row_offsets : tvm.te.Tensor
|
|
1-D tensor with shape (batch_size * num_classes,), this should be the exclusive scan
|
|
of num_detections.
|
|
ir : function
|
|
A function to generate IR for CPU or GPU, see its usage in vision/nms.py and cuda/nms.py.
|
|
|
|
Returns
|
|
-------
|
|
out : tvm.te.Tensor
|
|
The output is indices of size (batch_size * num_class* num_boxes , 3).
|
|
Rows of indices are ordered such that selected boxes from batch 0, class 0 come
|
|
first, in descending of scores, followed by boxes from batch 0, class 1 etc.
|
|
"""
|
|
batch_class, num_boxes = selected_indices.shape
|
|
|
|
if output_shape is not None:
|
|
return te.extern(
|
|
[output_shape],
|
|
[selected_indices, num_detections, row_offsets],
|
|
lambda ins, outs: ir(
|
|
num_class, ins[0], ins[1], ins[2], outs[0], max_output_boxes_per_class
|
|
),
|
|
dtype=["int64"],
|
|
name="collect_indices",
|
|
tag="collect_indices",
|
|
)
|
|
|
|
# TODO: Implement dynamic trimming based on num_total_detections
|
|
if num_total_detections is not None:
|
|
if isinstance(max_output_boxes_per_class, int):
|
|
out_rows = batch_class * max_output_boxes_per_class
|
|
else:
|
|
# Smart fallback based on input image size and typical production scenarios
|
|
out_rows = _estimate_max_detections(batch_class, input_image_size)
|
|
|
|
return te.extern(
|
|
[(out_rows, 3)],
|
|
[selected_indices, num_detections, row_offsets],
|
|
lambda ins, outs: ir(
|
|
num_class, ins[0], ins[1], ins[2], outs[0], max_output_boxes_per_class
|
|
),
|
|
dtype=["int64"],
|
|
name="collect_indices",
|
|
tag="collect_indices",
|
|
)
|
|
|
|
if isinstance(max_output_boxes_per_class, int):
|
|
out_rows = batch_class * max_output_boxes_per_class
|
|
return te.extern(
|
|
[(out_rows, 3)],
|
|
[selected_indices, num_detections, row_offsets],
|
|
lambda ins, outs: ir(
|
|
num_class, ins[0], ins[1], ins[2], outs[0], max_output_boxes_per_class
|
|
),
|
|
dtype=["int64"],
|
|
name="collect_indices",
|
|
tag="collect_indices",
|
|
)
|
|
|
|
if isinstance(max_output_boxes_per_class, te.Tensor):
|
|
try:
|
|
if len(max_output_boxes_per_class.shape) == 0:
|
|
max_boxes_val = int(max_output_boxes_per_class.data.numpy())
|
|
elif (
|
|
len(max_output_boxes_per_class.shape) == 1
|
|
and max_output_boxes_per_class.shape[0] == 1
|
|
):
|
|
max_boxes_val = int(max_output_boxes_per_class.data.numpy()[0])
|
|
else:
|
|
max_boxes_val = num_boxes
|
|
except (ValueError, IndexError, AttributeError):
|
|
max_boxes_val = num_boxes
|
|
|
|
out_rows = batch_class * max_boxes_val
|
|
return te.extern(
|
|
[(out_rows, 3)],
|
|
[selected_indices, num_detections, row_offsets],
|
|
lambda ins, outs: ir(
|
|
num_class, ins[0], ins[1], ins[2], outs[0], max_output_boxes_per_class
|
|
),
|
|
dtype=["int64"],
|
|
name="collect_indices",
|
|
tag="collect_indices",
|
|
)
|
|
|
|
return te.extern(
|
|
[(batch_class * num_boxes, 3)],
|
|
[selected_indices, num_detections, row_offsets],
|
|
lambda ins, outs: ir(
|
|
num_class, ins[0], ins[1], ins[2], outs[0], max_output_boxes_per_class
|
|
),
|
|
dtype=["int64"],
|
|
name="collect_indices",
|
|
tag="collect_indices",
|
|
)
|
|
|
|
|
|
def collect_selected_indices_and_scores(
|
|
selected_indices, selected_scores, num_detections, row_offsets, num_total_detections, ir
|
|
):
|
|
"""Collect selected indices and scores from the core NMS loop into one linear output.
|
|
|
|
Parameters
|
|
----------
|
|
selected_indices : tvm.te.Tensor
|
|
2-D tensor with shape (batch_size * num_classes, num_boxes), representing the indices
|
|
of selected boxes by the core NMS loop.
|
|
selected_scores : tvm.te.Tensor
|
|
2-D tensor with shape (batch_size * num_classes, num_boxes), representing the scores
|
|
of selected boxes by the core NMS loop.
|
|
num_detections : tvm.te.Tensor
|
|
2-D tensor with shape (batch_size, num_classes), representing
|
|
the number of boxes selected by the core NMS loop, per batch and class.
|
|
row_offsets : tvm.te.Tensor
|
|
2-D tensor with shape (batch_size, num_classes), this should be the exclusive scan
|
|
of num_detections along axis 1.
|
|
num_total_detections : tvm.te.Tensor
|
|
Total number of detections.
|
|
ir : function
|
|
A function to generate IR for CPU or GPU, see its usage in vision/nms.py and cuda/nms.py.
|
|
|
|
Returns
|
|
-------
|
|
out : [tvm.te.Tensor, tvm.te.Tensor]
|
|
The output is two tensors. The first is indices of size
|
|
(batch_size, num_class* num_boxes, 2), and the second is scores of size
|
|
(batch_size, num_class* num_boxes).
|
|
"""
|
|
batch_size, num_class = row_offsets.shape
|
|
num_boxes = selected_indices.shape[1]
|
|
return te.extern(
|
|
[(batch_size, num_class * num_boxes, 2), (batch_size, num_class * num_boxes)],
|
|
[selected_indices, selected_scores, num_detections, row_offsets, num_total_detections],
|
|
lambda ins, outs: ir(ins[0], ins[1], ins[2], ins[3], ins[4], outs[0], outs[1]),
|
|
dtype=["int64", "float32"],
|
|
name="collect_indices_and_scores",
|
|
tag="collect_indices_and_scores",
|
|
)
|
|
|
|
|
|
def _all_class_nms_ir(
|
|
boxes,
|
|
sorted_scores,
|
|
sorted_indices,
|
|
valid_count,
|
|
batch_class,
|
|
num_class,
|
|
num_anchors,
|
|
iou_threshold,
|
|
max_output_size_per_class,
|
|
box_indices,
|
|
selected_scores,
|
|
num_valid_boxes,
|
|
nms_loop,
|
|
score_threshold=None,
|
|
):
|
|
with IRBuilder() as ib:
|
|
# Wrap buffers with T.buffer_proxy for flat indexing support
|
|
boxes = T.buffer_proxy(boxes)
|
|
box_indices = T.buffer_proxy(box_indices)
|
|
if selected_scores is not None:
|
|
selected_scores = T.buffer_proxy(selected_scores)
|
|
|
|
if isinstance(iou_threshold, float | int):
|
|
iou_threshold = tvm.tirx.FloatImm("float32", float(iou_threshold))
|
|
elif isinstance(iou_threshold, te.Tensor):
|
|
if len(iou_threshold.shape) == 0:
|
|
iou_threshold = iou_threshold()
|
|
elif len(iou_threshold.shape) == 1 and iou_threshold.shape[0] == 1:
|
|
iou_threshold = iou_threshold[0]
|
|
else:
|
|
iou_threshold = tvm.tirx.FloatImm("float32", 0.5)
|
|
|
|
if isinstance(max_output_size_per_class, int):
|
|
max_output_size_per_class = tvm.tirx.const(max_output_size_per_class)
|
|
elif isinstance(max_output_size_per_class, te.Tensor):
|
|
if len(max_output_size_per_class.shape) == 0:
|
|
max_output_size_per_class = max_output_size_per_class()
|
|
elif (
|
|
len(max_output_size_per_class.shape) == 1
|
|
and max_output_size_per_class.shape[0] == 1
|
|
):
|
|
max_output_size_per_class = max_output_size_per_class[0]
|
|
else:
|
|
max_output_size_per_class = tvm.tirx.const(1000)
|
|
|
|
def calc_overlap(i, j, k):
|
|
offset_j = sorted_indices[i, j] * 4
|
|
offset_k = sorted_indices[i, k] * 4
|
|
batch_id = i // num_class
|
|
base_bbox_idx = batch_id * num_anchors * 4
|
|
return calculate_overlap(
|
|
boxes,
|
|
base_bbox_idx + offset_j,
|
|
base_bbox_idx + offset_k,
|
|
)
|
|
|
|
def on_new_valid_box(tid, num_current_valid_box, i, j):
|
|
with T.If(tid + 0 == 0):
|
|
with T.Then():
|
|
box_indices[i, num_current_valid_box] = sorted_indices[i, j]
|
|
if selected_scores is not None:
|
|
selected_scores[i, num_current_valid_box] = sorted_scores[i, j]
|
|
|
|
def on_new_invalidated_box(*_):
|
|
pass
|
|
|
|
def needs_bbox_check(*_):
|
|
return tvm.tirx.const(True)
|
|
|
|
nms_loop(
|
|
batch_class,
|
|
tvm.tirx.IntImm("int32", -1), # top_k
|
|
iou_threshold,
|
|
max_output_size_per_class,
|
|
valid_count,
|
|
on_new_valid_box,
|
|
on_new_invalidated_box,
|
|
needs_bbox_check,
|
|
calc_overlap,
|
|
sorted_scores,
|
|
num_valid_boxes,
|
|
score_threshold,
|
|
)
|
|
|
|
return ib.get()
|
|
|
|
|
|
def run_all_class_nms(
|
|
boxes,
|
|
sorted_scores,
|
|
sorted_indices,
|
|
valid_count,
|
|
max_output_size_per_class,
|
|
iou_threshold,
|
|
nms_loop,
|
|
return_scores=False,
|
|
score_threshold=None,
|
|
):
|
|
"""The core all class NMS routine.
|
|
|
|
Parameters
|
|
----------
|
|
boxes : tvm.te.Tensor
|
|
3-D tensor with shape (batch_size, num_boxes, 4)
|
|
sorted_scores : tvm.te.Tensor
|
|
2-D tensor with shape (batch_size * num_classes, num_boxes).
|
|
One of the outputs from argsort.
|
|
sorted_indices : tvm.te.Tensor
|
|
2-D tensor with shape (batch_size * num_classes, num_boxes).
|
|
The other output from argsort.
|
|
valid_count : tvm.te.Tensor
|
|
1-D tensor with shape (batch_size * num_classes,), representing
|
|
the number of boxes whose score is above score_threshold, per batch and class.
|
|
max_output_size_per_class : int or tvm.te.Tensor, optional
|
|
The maxinum number of output selected boxes per class.
|
|
iou_threshold : float or tvm.te.Tensor, optional
|
|
IoU test threshold.
|
|
nms_loop : function
|
|
A core NMS loop, see its usage in vision/nms.py and cuda/nms.py.
|
|
return_scores : bool, optional
|
|
Whether or not to return selected scores, needed by the tensorflow output format.
|
|
|
|
Returns
|
|
-------
|
|
out : a list of tvm.te.Tensor
|
|
The output is three tensors, the first and second are indices and scores of size
|
|
(batch_size * num_class, num_boxes), and the third is a tensor
|
|
num_selected_boxes of shape (batch_size * num_class,) representing the total number of
|
|
selected boxes per batch and class. If return_scores is False, the second output is
|
|
None.
|
|
"""
|
|
batch, num_boxes, _ = boxes.shape
|
|
batch_class = sorted_scores.shape[0]
|
|
num_class = batch_class // batch
|
|
|
|
if return_scores is False:
|
|
all_class_num0_buf = tvm.tirx.decl_buffer(
|
|
(batch_class, num_boxes), "int32", "all_class_nms0", data_alignment=8, layout=None
|
|
)
|
|
all_class_num1_buf = tvm.tirx.decl_buffer(
|
|
(batch_class,), "int32", "all_class_nms1", data_alignment=8, layout=None
|
|
)
|
|
extern_inputs = [boxes, sorted_scores, sorted_indices, valid_count]
|
|
if score_threshold is not None:
|
|
extern_inputs.append(score_threshold)
|
|
|
|
selected_indices, num_detections = te.extern(
|
|
[(batch_class, num_boxes), (batch_class,)],
|
|
extern_inputs,
|
|
lambda ins, outs: _all_class_nms_ir(
|
|
ins[0], # boxes
|
|
ins[1], # sorted_scores
|
|
ins[2], # sorted_indices
|
|
ins[3], # valid_count
|
|
batch_class,
|
|
num_class,
|
|
num_boxes,
|
|
iou_threshold,
|
|
max_output_size_per_class,
|
|
outs[0], # box_indices
|
|
None, # scores
|
|
outs[1], # num_selected_boxes
|
|
nms_loop,
|
|
ins[4] if score_threshold is not None else None, # score_threshold
|
|
),
|
|
out_buffers=[all_class_num0_buf, all_class_num1_buf],
|
|
dtype=["int32", "int32"],
|
|
name="all_class_nms",
|
|
tag="all_class_nms",
|
|
)
|
|
return selected_indices, None, num_detections
|
|
|
|
extern_inputs = [boxes, sorted_scores, sorted_indices, valid_count]
|
|
if score_threshold is not None:
|
|
extern_inputs.append(score_threshold)
|
|
|
|
return te.extern(
|
|
[(batch_class, num_boxes), (batch_class, num_boxes), (batch_class,)],
|
|
extern_inputs,
|
|
lambda ins, outs: _all_class_nms_ir(
|
|
ins[0], # boxes
|
|
ins[1], # sorted_scores
|
|
ins[2], # sorted_indices
|
|
ins[3], # valid_count
|
|
batch_class,
|
|
num_class,
|
|
num_boxes,
|
|
iou_threshold,
|
|
max_output_size_per_class,
|
|
outs[0], # box_indices
|
|
outs[1], # selected scores
|
|
outs[2], # num_selected_boxes
|
|
nms_loop,
|
|
ins[4] if score_threshold is not None else None, # score_threshold
|
|
),
|
|
dtype=["int32", "float32", "int32"],
|
|
name="all_class_nms",
|
|
tag="all_class_nms",
|
|
)
|