Non-Maximum Merging for Oriented BBox

Hi,
I just read this amazing blog on a technique called NMM (Non Maximum Merging). I want to know how can I apply this for oriented bounding boxes. If anyone can help me it would be amazing.

Hi,

Thanks a lot for the kind words. Regarding non-max merging (NMM) for oriented bounding boxes (OBB), supervision currently doesn’t support such an operation. As far as I know, there is no established convention for performing this operation, but I spent an hour drafting a rough solution.

Here is the code defining the input OBBs that we would like to merge.

import numpy as np
import supervision as sv


xyxyxyxy = np.array([
    [
        [200, 200],
        [300, 210],
        [280, 320],
        [180, 310]
    ],
    [
        [260, 240],
        [360, 250],
        [340, 360],
        [240, 350]
    ],
    [
        [220, 300],
        [320, 310],
        [310, 400],
        [210, 390]
    ]
], dtype=int)

image = np.zeros((600, 600, 3), dtype=np.uint8)

def xyxyxyxy_to_xyxy(obbs):
    x_min = obbs[:, :, 0].min(axis=1)
    x_max = obbs[:, :, 0].max(axis=1)
    y_min = obbs[:, :, 1].min(axis=1)
    y_max = obbs[:, :, 1].max(axis=1)
    return np.stack([x_min, y_min, x_max, y_max], axis=1)

detections = sv.Detections(
    xyxy=xyxyxyxy_to_xyxy(xyxyxyxy), 
    data={'xyxyxyxy': xyxyxyxy}
)

  • We rely on shapely to treat each oriented bounding box (OBB) as a Polygon and then unify them with unary_union.
  • If those polygons overlap, Shapely merges them into one or more larger shapes.
  • Using minimum_rotated_rectangle on each merged shape gives the smallest oriented box that covers it.
  • Extracting the rectangle’s corners provides the final merged OBBs.
from shapely.geometry import Polygon
from shapely.ops import unary_union


def merge_xyxyxyxy(xyxyxyxy):
    # Convert each OBB's 4 corners into a Shapely polygon
    polygons = [Polygon(box) for box in xyxyxyxy]

    # Merge all polygons into one geometry (could be a single Polygon or MultiPolygon)
    merged = unary_union(polygons)

    # If it's a single Polygon, wrap it in a list; if MultiPolygon, gather individual polygons
    if merged.geom_type == 'Polygon':
        polygons_merged = [merged]
    else:
        polygons_merged = list(merged.geoms)

    # For each merged region (Polygon), compute its minimum-area bounding rectangle
    results = []
    for poly in polygons_merged:
        # Shapely's minimum_rotated_rectangle returns a Polygon
        # with 5 points, the last one repeating the first
        mabr = poly.minimum_rotated_rectangle
        coords = list(mabr.exterior.coords)[:-1]  # drop repeated last corner

        # Append the 4 corners as (4, 2) into the results
        results.append(coords)

    # Convert to an (M, 4, 2) NumPy array (M can be 1 or more if there are disjoint merges)
    return np.array(results, dtype=np.float32)

xyxyxyxy2 = merge_xyxyxyxy(xyxyxyxy)

detections2 = sv.Detections(
    xyxy=xyxyxyxy_to_xyxy(xyxyxyxy2), 
    data={'xyxyxyxy': xyxyxyxy2}
)