# 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", )