From e4b5a8e0f925ea01bae061e915c0cb397610933d Mon Sep 17 00:00:00 2001 From: AI-Hao <2865467769@qq.com> Date: Thu, 9 Jan 2025 20:00:32 +0800 Subject: [PATCH 01/24] ### feat(detection): Support for IOS Matching Metric** - Added the `match_metric` parameter to the `with_nms` and `with_nmm` methods in the `Detections` class. - Modified the `box_non_max_suppression` and `box_non_max_merge` functions to support IOS calculation. - Introduced the `mask_non_max_merge` function for handling non-maximum merging of masks. - Updated the `box_iou_batch` and `mask_iou_batch` functions to support both IOU and IOS calculations. - Enhanced the `InferenceSlicer` class to accommodate the IOS matching metric. ### Notes - IOS (Intersection over Smaller): A variation of IOU that compares intersection over the smaller area, useful in scenarios with varying object sizes. - Refinements: Improved terminology for clarity and consistency, ensuring technical accuracy. --- supervision/detection/core.py | 36 +++-- supervision/detection/overlap_filter.py | 128 +++++++++++++++++- .../detection/tools/inference_slicer.py | 8 +- supervision/detection/utils.py | 64 +++++++-- 4 files changed, 204 insertions(+), 32 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index c61728b9..ad012a9d 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -17,6 +17,7 @@ from supervision.detection.lmm import ( ) from supervision.detection.overlap_filter import ( box_non_max_merge, + mask_non_max_merge, box_non_max_suppression, mask_non_max_suppression, ) @@ -27,6 +28,7 @@ from supervision.detection.tools.transformers import ( ) from supervision.detection.utils import ( box_iou_batch, + mask_iou_batch, calculate_masks_centroids, extract_ultralytics_masks, get_data_item, @@ -1281,7 +1283,7 @@ class Detections: return (self.xyxy[:, 3] - self.xyxy[:, 1]) * (self.xyxy[:, 2] - self.xyxy[:, 0]) def with_nms( - self, threshold: float = 0.5, class_agnostic: bool = False + self, threshold: float = 0.5, class_agnostic: bool = False, match_metric: str = "IOU" ) -> Detections: """ Performs non-max suppression on detection set. If the detections result @@ -1294,6 +1296,8 @@ class Detections: class_agnostic (bool): Whether to perform class-agnostic non-maximum suppression. If True, the class_id of each detection will be ignored. Defaults to False. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". Returns: Detections: A new Detections object containing the subset of detections @@ -1327,17 +1331,17 @@ class Detections: if self.mask is not None: indices = mask_non_max_suppression( - predictions=predictions, masks=self.mask, iou_threshold=threshold + predictions=predictions, masks=self.mask, iou_threshold=threshold, match_metric=match_metric ) else: indices = box_non_max_suppression( - predictions=predictions, iou_threshold=threshold + predictions=predictions, iou_threshold=threshold, match_metric=match_metric ) return self[indices] def with_nmm( - self, threshold: float = 0.5, class_agnostic: bool = False + self, threshold: float = 0.5, class_agnostic: bool = False, match_metric: str = "IOU" ) -> Detections: """ Perform non-maximum merging on the current set of object detections. @@ -1348,6 +1352,8 @@ class Detections: class_agnostic (bool): Whether to perform class-agnostic non-maximum merging. If True, the class_id of each detection will be ignored. Defaults to False. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". Returns: Detections: A new Detections object containing the subset of detections @@ -1381,15 +1387,20 @@ class Detections: ) ) - merge_groups = box_non_max_merge( - predictions=predictions, iou_threshold=threshold - ) + if self.mask is not None: + merge_groups = mask_non_max_merge( + predictions=predictions, masks=self.mask, iou_threshold=threshold, match_metric=match_metric + ) + else: + merge_groups = box_non_max_merge( + predictions=predictions, iou_threshold=threshold, match_metric=match_metric + ) result = [] for merge_group in merge_groups: unmerged_detections = [self[i] for i in merge_group] merged_detections = merge_inner_detections_objects( - unmerged_detections, threshold + unmerged_detections, threshold, match_metric ) result.append(merged_detections) @@ -1489,7 +1500,7 @@ def merge_inner_detection_object_pair( def merge_inner_detections_objects( - detections: List[Detections], threshold=0.5 + detections: List[Detections], threshold=0.5, match_metric: str = "IOU" ) -> Detections: """ Given N detections each of length 1 (exactly one object inside), combine them into a @@ -1501,8 +1512,11 @@ def merge_inner_detections_objects( """ detections_1 = detections[0] for detections_2 in detections[1:]: - box_iou = box_iou_batch(detections_1.xyxy, detections_2.xyxy)[0] - if box_iou < threshold: + if detections_1.mask is not None and detections_2.mask is not None: + iou = mask_iou_batch(detections_1.mask, detections_2.mask, match_metric)[0] + else: + iou = box_iou_batch(detections_1.xyxy, detections_2.xyxy, match_metric)[0] + if iou < threshold: break detections_1 = merge_inner_detection_object_pair(detections_1, detections_2) return detections_1 diff --git a/supervision/detection/overlap_filter.py b/supervision/detection/overlap_filter.py index 4c59295f..935be236 100644 --- a/supervision/detection/overlap_filter.py +++ b/supervision/detection/overlap_filter.py @@ -42,6 +42,7 @@ def mask_non_max_suppression( predictions: np.ndarray, masks: np.ndarray, iou_threshold: float = 0.5, + match_metric: str = "IOU", mask_dimension: int = 640, ) -> np.ndarray: """ @@ -57,6 +58,8 @@ def mask_non_max_suppression( dimensions of each mask. iou_threshold (float): The intersection-over-union threshold to use for non-maximum suppression. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". mask_dimension (int): The dimension to which the masks should be resized before computing IOU values. Defaults to 640. @@ -81,7 +84,7 @@ def mask_non_max_suppression( predictions = predictions[sort_index] masks = masks[sort_index] masks_resized = resize_masks(masks, mask_dimension) - ious = mask_iou_batch(masks_resized, masks_resized) + ious = mask_iou_batch(masks_resized, masks_resized, match_metric) categories = predictions[:, 5] keep = np.ones(rows, dtype=bool) @@ -94,7 +97,7 @@ def mask_non_max_suppression( def box_non_max_suppression( - predictions: np.ndarray, iou_threshold: float = 0.5 + predictions: np.ndarray, iou_threshold: float = 0.5, match_metric: str = "IOU" ) -> np.ndarray: """ Perform Non-Maximum Suppression (NMS) on object detection predictions. @@ -105,6 +108,8 @@ def box_non_max_suppression( or `(x_min, y_min, x_max, y_max, score, class)`. iou_threshold (float): The intersection-over-union threshold to use for non-maximum suppression. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". Returns: np.ndarray: A boolean array indicating which predictions to keep after n @@ -130,7 +135,7 @@ def box_non_max_suppression( boxes = predictions[:, :4] categories = predictions[:, 5] - ious = box_iou_batch(boxes, boxes) + ious = box_iou_batch(boxes, boxes, match_metric) ious = ious - np.eye(rows) keep = np.ones(rows, dtype=bool) @@ -148,7 +153,7 @@ def box_non_max_suppression( def group_overlapping_boxes( - predictions: npt.NDArray[np.float64], iou_threshold: float = 0.5 + predictions: npt.NDArray[np.float64], iou_threshold: float = 0.5, match_metric: str = "IOU" ) -> List[List[int]]: """ Apply greedy version of non-maximum merging to avoid detecting too many @@ -160,6 +165,8 @@ def group_overlapping_boxes( and the confidence scores. iou_threshold (float): The intersection-over-union threshold to use for non-maximum suppression. Defaults to 0.5. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". Returns: List[List[int]]: Groups of prediction indices be merged. @@ -179,7 +186,7 @@ def group_overlapping_boxes( break merge_candidate = np.expand_dims(predictions[idx], axis=0) - ious = box_iou_batch(predictions[order][:, :4], merge_candidate[:, :4]) + ious = box_iou_batch(predictions[order][:, :4], merge_candidate[:, :4], match_metric) ious = ious.flatten() above_threshold = ious >= iou_threshold @@ -189,9 +196,67 @@ def group_overlapping_boxes( return merge_groups +def mask_non_max_merge( + predictions: np.ndarray, + masks: np.ndarray, + iou_threshold: float = 0.5, + mask_dimension: int = 640, + match_metric: str = "IOU", +) -> np.ndarray: + """ + Perform Non-Maximum Merging (NMM) on segmentation predictions. + + Args: + predictions (np.ndarray): A 2D array of object detection predictions in + the format of `(x_min, y_min, x_max, y_max, score)` + or `(x_min, y_min, x_max, y_max, score, class)`. Shape: `(N, 5)` or + `(N, 6)`, where N is the number of predictions. + masks (np.ndarray): A 3D array of binary masks corresponding to the predictions. + Shape: `(N, H, W)`, where N is the number of predictions, and H, W are the + dimensions of each mask. + iou_threshold (float): The intersection-over-union threshold + to use for non-maximum suppression. + mask_dimension (int): The dimension to which the masks should be + resized before computing IOU values. Defaults to 640. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". + + Returns: + np.ndarray: A boolean array indicating which predictions to keep after + non-maximum suppression. + + Raises: + AssertionError: If `iou_threshold` is not within the closed + range from `0` to `1`. + """ + masks_resized = resize_masks(masks, mask_dimension) + if predictions.shape[1] == 5: + return group_overlapping_masks(predictions, masks_resized, iou_threshold, match_metric) + + category_ids = predictions[:, 5] + merge_groups = [] + for category_id in np.unique(category_ids): + curr_indices = np.where(category_ids == category_id)[0] + merge_class_groups = group_overlapping_masks( + predictions[curr_indices], masks_resized[curr_indices], iou_threshold, match_metric + ) + + for merge_class_group in merge_class_groups: + merge_groups.append(curr_indices[merge_class_group].tolist()) + + for merge_group in merge_groups: + if len(merge_group) == 0: + raise ValueError( + f"Empty group detected when non-max-merging " + f"detections: {merge_groups}" + ) + return merge_groups + + def box_non_max_merge( predictions: npt.NDArray[np.float64], iou_threshold: float = 0.5, + match_metric: str = "IOU", ) -> List[List[int]]: """ Apply greedy version of non-maximum merging per category to avoid detecting @@ -204,20 +269,22 @@ def box_non_max_merge( detections of different classes to be merged. iou_threshold (float): The intersection-over-union threshold to use for non-maximum suppression. Defaults to 0.5. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". Returns: List[List[int]]: Groups of prediction indices be merged. Each group may have 1 or more elements. """ if predictions.shape[1] == 5: - return group_overlapping_boxes(predictions, iou_threshold) + return group_overlapping_boxes(predictions, iou_threshold, match_metric) category_ids = predictions[:, 5] merge_groups = [] for category_id in np.unique(category_ids): curr_indices = np.where(category_ids == category_id)[0] merge_class_groups = group_overlapping_boxes( - predictions[curr_indices], iou_threshold + predictions[curr_indices], iou_threshold, match_metric ) for merge_class_group in merge_class_groups: @@ -232,6 +299,53 @@ def box_non_max_merge( return merge_groups +def group_overlapping_masks( + predictions: npt.NDArray[np.float64], + masks: npt.NDArray[np.float64], + iou_threshold: float = 0.5, + match_metric: str = "IOU", +) -> List[List[int]]: + """ + Apply greedy version of non-maximum merging to avoid detecting too many + + Args: + predictions (npt.NDArray[np.float64]): An array of shape `(n, 5)` containing + the bounding boxes coordinates in format `[x1, y1, x2, y2]` + and the confidence scores. + masks (npt.NDArray[np.float64]): A 3D array of binary masks corresponding to the predictions. + iou_threshold (float): The intersection-over-union threshold + to use for non-maximum suppression. Defaults to 0.5. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". + + Returns: + List[List[int]]: Groups of prediction indices be merged. + Each group may have 1 or more elements. + """ + merge_groups: List[List[int]] = [] + + scores = predictions[:, 4] + order = scores.argsort() + + while len(order) > 0: + idx = int(order[-1]) + + order = order[:-1] + if len(order) == 0: + merge_groups.append([idx]) + break + + merge_candidate = np.expand_dims(masks[idx], axis=0) + ious = mask_iou_batch(masks[order], merge_candidate, match_metric) + ious = ious.flatten() + + above_threshold = ious >= iou_threshold + merge_group = [idx, *np.flip(order[above_threshold]).tolist()] + merge_groups.append(merge_group) + order = order[~above_threshold] + return merge_groups + + class OverlapFilter(Enum): """ Enum specifying the strategy for filtering overlapping detections. diff --git a/supervision/detection/tools/inference_slicer.py b/supervision/detection/tools/inference_slicer.py index 05469dd6..7babaf68 100644 --- a/supervision/detection/tools/inference_slicer.py +++ b/supervision/detection/tools/inference_slicer.py @@ -72,6 +72,8 @@ class InferenceSlicer: filtering or merging overlapping detections in slices. iou_threshold (float): Intersection over Union (IoU) threshold used when filtering by overlap. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". callback (Callable): A function that performs inference on a given image slice and returns detections. thread_workers (int): Number of threads for parallel execution. @@ -91,6 +93,7 @@ class InferenceSlicer: overlap_wh: Optional[Tuple[int, int]] = None, overlap_filter: Union[OverlapFilter, str] = OverlapFilter.NON_MAX_SUPPRESSION, iou_threshold: float = 0.5, + match_metric: str = "IOU", thread_workers: int = 1, ): if overlap_ratio_wh is not None: @@ -106,6 +109,7 @@ class InferenceSlicer: self.slice_wh = slice_wh self.iou_threshold = iou_threshold + self.match_metric = match_metric self.overlap_filter = OverlapFilter.from_value(overlap_filter) self.callback = callback self.thread_workers = thread_workers @@ -165,9 +169,9 @@ class InferenceSlicer: if self.overlap_filter == OverlapFilter.NONE: return merged elif self.overlap_filter == OverlapFilter.NON_MAX_SUPPRESSION: - return merged.with_nms(threshold=self.iou_threshold) + return merged.with_nms(threshold=self.iou_threshold, match_metric=self.match_metric) elif self.overlap_filter == OverlapFilter.NON_MAX_MERGE: - return merged.with_nmm(threshold=self.iou_threshold) + return merged.with_nmm(threshold=self.iou_threshold, match_metric=self.match_metric) else: warnings.warn( f"Invalid overlap filter strategy: {self.overlap_filter}", diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 0d5ec475..19cddca4 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -48,7 +48,7 @@ def polygon_to_mask(polygon: np.ndarray, resolution_wh: Tuple[int, int]) -> np.n return mask -def box_iou_batch(boxes_true: np.ndarray, boxes_detection: np.ndarray) -> np.ndarray: +def box_iou_batch(boxes_true: np.ndarray, boxes_detection: np.ndarray, match_metric: str = "IOU") -> np.ndarray: """ Compute Intersection over Union (IoU) of two sets of bounding boxes - `boxes_true` and `boxes_detection`. Both sets @@ -59,6 +59,8 @@ def box_iou_batch(boxes_true: np.ndarray, boxes_detection: np.ndarray) -> np.nda `shape = (N, 4)` where `N` is number of true objects. boxes_detection (np.ndarray): 2D `np.ndarray` representing detection boxes. `shape = (M, 4)` where `M` is number of detected objects. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". Returns: np.ndarray: Pairwise IoU of boxes from `boxes_true` and `boxes_detection`. @@ -76,13 +78,32 @@ def box_iou_batch(boxes_true: np.ndarray, boxes_detection: np.ndarray) -> np.nda bottom_right = np.minimum(boxes_true[:, None, 2:], boxes_detection[:, 2:]) area_inter = np.prod(np.clip(bottom_right - top_left, a_min=0, a_max=None), 2) - ious = area_inter / (area_true[:, None] + area_detection - area_inter) + + if match_metric.upper() == "IOU": + union_area = area_true[:, None] + area_detection - area_inter + ious = np.divide( + area_inter, + union_area, + out=np.zeros_like(area_inter, dtype=float), + where=union_area != 0, + ) + elif match_metric.upper() == "IOS": + small_area = np.minimum(area_true[:, None], area_detection) + ious = np.divide( + area_inter, + small_area, + out=np.zeros_like(area_inter, dtype=float), + where=small_area != 0, + ) + else: + raise ValueError(f"match_metric {match_metric} is not supported, only 'IOU' and 'IOS' are supported") + ious = np.nan_to_num(ious) return ious def _mask_iou_batch_split( - masks_true: np.ndarray, masks_detection: np.ndarray + masks_true: np.ndarray, masks_detection: np.ndarray, match_metric: str = "IOU" ) -> np.ndarray: """ Internal function. @@ -92,6 +113,8 @@ def _mask_iou_batch_split( Args: masks_true (np.ndarray): 3D `np.ndarray` representing ground-truth masks. masks_detection (np.ndarray): 3D `np.ndarray` representing detection masks. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". Returns: np.ndarray: Pairwise IoU of masks from `masks_true` and `masks_detection`. @@ -102,19 +125,34 @@ def _mask_iou_batch_split( masks_true_area = masks_true.sum(axis=(1, 2)) masks_detection_area = masks_detection.sum(axis=(1, 2)) - union_area = masks_true_area[:, None] + masks_detection_area - intersection_area - return np.divide( - intersection_area, - union_area, - out=np.zeros_like(intersection_area, dtype=float), - where=union_area != 0, - ) + if match_metric.upper() == "IOU": + union_area = masks_true_area[:, None] + masks_detection_area - intersection_area + ious = np.divide( + intersection_area, + union_area, + out=np.zeros_like(intersection_area, dtype=float), + where=union_area != 0, + ) + elif match_metric.upper() == "IOS": + small_area = np.minimum(intersection_area, masks_detection_area) + ious = np.divide( + intersection_area, + small_area, + out=np.zeros_like(intersection_area, dtype=float), + where=small_area != 0, + ) + else: + raise ValueError(f"match_metric {match_metric} is not supported, only 'IOU' and 'IOS' are supported") + + ious = np.nan_to_num(ious) + return ious def mask_iou_batch( masks_true: np.ndarray, masks_detection: np.ndarray, + match_metric: str = "IOU", memory_limit: int = 1024 * 5, ) -> np.ndarray: """ @@ -124,6 +162,8 @@ def mask_iou_batch( Args: masks_true (np.ndarray): 3D `np.ndarray` representing ground-truth masks. masks_detection (np.ndarray): 3D `np.ndarray` representing detection masks. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". memory_limit (int): memory limit in MB, default is 1024 * 5 MB (5GB). Returns: @@ -138,7 +178,7 @@ def mask_iou_batch( / 1024 ) if memory <= memory_limit: - return _mask_iou_batch_split(masks_true, masks_detection) + return _mask_iou_batch_split(masks_true, masks_detection, match_metric) ious = [] step = max( @@ -153,7 +193,7 @@ def mask_iou_batch( 1, ) for i in range(0, masks_true.shape[0], step): - ious.append(_mask_iou_batch_split(masks_true[i : i + step], masks_detection)) + ious.append(_mask_iou_batch_split(masks_true[i : i + step], masks_detection, match_metric)) return np.vstack(ious) From 9cd6549cdc064e67f85392f1fabd9caaccb0c7f7 Mon Sep 17 00:00:00 2001 From: AI-Hao <2865467769@qq.com> Date: Thu, 9 Jan 2025 20:03:31 +0800 Subject: [PATCH 02/24] feat(detection): Support for IOS Matching Metric - Added the `match_metric` parameter to the `with_nms` and `with_nmm` methods in the `Detections` class. - Modified the `box_non_max_suppression` and `box_non_max_merge` functions to support IOS calculation. - Introduced the `mask_non_max_merge` function for handling non-maximum merging of masks. - Updated the `box_iou_batch` and `mask_iou_batch` functions to support both IOU and IOS calculations. - Enhanced the `InferenceSlicer` class to accommodate the IOS matching metric. Notes - IOS (Intersection over Smaller): A variation of IOU that compares intersection over the smaller area, useful in scenarios with varying object sizes. - Refinements: Improved terminology for clarity and consistency, ensuring technical accuracy. --- supervision/detection/core.py | 36 +++-- supervision/detection/overlap_filter.py | 128 +++++++++++++++++- .../detection/tools/inference_slicer.py | 8 +- supervision/detection/utils.py | 64 +++++++-- 4 files changed, 204 insertions(+), 32 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index c61728b9..ad012a9d 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -17,6 +17,7 @@ from supervision.detection.lmm import ( ) from supervision.detection.overlap_filter import ( box_non_max_merge, + mask_non_max_merge, box_non_max_suppression, mask_non_max_suppression, ) @@ -27,6 +28,7 @@ from supervision.detection.tools.transformers import ( ) from supervision.detection.utils import ( box_iou_batch, + mask_iou_batch, calculate_masks_centroids, extract_ultralytics_masks, get_data_item, @@ -1281,7 +1283,7 @@ class Detections: return (self.xyxy[:, 3] - self.xyxy[:, 1]) * (self.xyxy[:, 2] - self.xyxy[:, 0]) def with_nms( - self, threshold: float = 0.5, class_agnostic: bool = False + self, threshold: float = 0.5, class_agnostic: bool = False, match_metric: str = "IOU" ) -> Detections: """ Performs non-max suppression on detection set. If the detections result @@ -1294,6 +1296,8 @@ class Detections: class_agnostic (bool): Whether to perform class-agnostic non-maximum suppression. If True, the class_id of each detection will be ignored. Defaults to False. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". Returns: Detections: A new Detections object containing the subset of detections @@ -1327,17 +1331,17 @@ class Detections: if self.mask is not None: indices = mask_non_max_suppression( - predictions=predictions, masks=self.mask, iou_threshold=threshold + predictions=predictions, masks=self.mask, iou_threshold=threshold, match_metric=match_metric ) else: indices = box_non_max_suppression( - predictions=predictions, iou_threshold=threshold + predictions=predictions, iou_threshold=threshold, match_metric=match_metric ) return self[indices] def with_nmm( - self, threshold: float = 0.5, class_agnostic: bool = False + self, threshold: float = 0.5, class_agnostic: bool = False, match_metric: str = "IOU" ) -> Detections: """ Perform non-maximum merging on the current set of object detections. @@ -1348,6 +1352,8 @@ class Detections: class_agnostic (bool): Whether to perform class-agnostic non-maximum merging. If True, the class_id of each detection will be ignored. Defaults to False. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". Returns: Detections: A new Detections object containing the subset of detections @@ -1381,15 +1387,20 @@ class Detections: ) ) - merge_groups = box_non_max_merge( - predictions=predictions, iou_threshold=threshold - ) + if self.mask is not None: + merge_groups = mask_non_max_merge( + predictions=predictions, masks=self.mask, iou_threshold=threshold, match_metric=match_metric + ) + else: + merge_groups = box_non_max_merge( + predictions=predictions, iou_threshold=threshold, match_metric=match_metric + ) result = [] for merge_group in merge_groups: unmerged_detections = [self[i] for i in merge_group] merged_detections = merge_inner_detections_objects( - unmerged_detections, threshold + unmerged_detections, threshold, match_metric ) result.append(merged_detections) @@ -1489,7 +1500,7 @@ def merge_inner_detection_object_pair( def merge_inner_detections_objects( - detections: List[Detections], threshold=0.5 + detections: List[Detections], threshold=0.5, match_metric: str = "IOU" ) -> Detections: """ Given N detections each of length 1 (exactly one object inside), combine them into a @@ -1501,8 +1512,11 @@ def merge_inner_detections_objects( """ detections_1 = detections[0] for detections_2 in detections[1:]: - box_iou = box_iou_batch(detections_1.xyxy, detections_2.xyxy)[0] - if box_iou < threshold: + if detections_1.mask is not None and detections_2.mask is not None: + iou = mask_iou_batch(detections_1.mask, detections_2.mask, match_metric)[0] + else: + iou = box_iou_batch(detections_1.xyxy, detections_2.xyxy, match_metric)[0] + if iou < threshold: break detections_1 = merge_inner_detection_object_pair(detections_1, detections_2) return detections_1 diff --git a/supervision/detection/overlap_filter.py b/supervision/detection/overlap_filter.py index 4c59295f..935be236 100644 --- a/supervision/detection/overlap_filter.py +++ b/supervision/detection/overlap_filter.py @@ -42,6 +42,7 @@ def mask_non_max_suppression( predictions: np.ndarray, masks: np.ndarray, iou_threshold: float = 0.5, + match_metric: str = "IOU", mask_dimension: int = 640, ) -> np.ndarray: """ @@ -57,6 +58,8 @@ def mask_non_max_suppression( dimensions of each mask. iou_threshold (float): The intersection-over-union threshold to use for non-maximum suppression. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". mask_dimension (int): The dimension to which the masks should be resized before computing IOU values. Defaults to 640. @@ -81,7 +84,7 @@ def mask_non_max_suppression( predictions = predictions[sort_index] masks = masks[sort_index] masks_resized = resize_masks(masks, mask_dimension) - ious = mask_iou_batch(masks_resized, masks_resized) + ious = mask_iou_batch(masks_resized, masks_resized, match_metric) categories = predictions[:, 5] keep = np.ones(rows, dtype=bool) @@ -94,7 +97,7 @@ def mask_non_max_suppression( def box_non_max_suppression( - predictions: np.ndarray, iou_threshold: float = 0.5 + predictions: np.ndarray, iou_threshold: float = 0.5, match_metric: str = "IOU" ) -> np.ndarray: """ Perform Non-Maximum Suppression (NMS) on object detection predictions. @@ -105,6 +108,8 @@ def box_non_max_suppression( or `(x_min, y_min, x_max, y_max, score, class)`. iou_threshold (float): The intersection-over-union threshold to use for non-maximum suppression. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". Returns: np.ndarray: A boolean array indicating which predictions to keep after n @@ -130,7 +135,7 @@ def box_non_max_suppression( boxes = predictions[:, :4] categories = predictions[:, 5] - ious = box_iou_batch(boxes, boxes) + ious = box_iou_batch(boxes, boxes, match_metric) ious = ious - np.eye(rows) keep = np.ones(rows, dtype=bool) @@ -148,7 +153,7 @@ def box_non_max_suppression( def group_overlapping_boxes( - predictions: npt.NDArray[np.float64], iou_threshold: float = 0.5 + predictions: npt.NDArray[np.float64], iou_threshold: float = 0.5, match_metric: str = "IOU" ) -> List[List[int]]: """ Apply greedy version of non-maximum merging to avoid detecting too many @@ -160,6 +165,8 @@ def group_overlapping_boxes( and the confidence scores. iou_threshold (float): The intersection-over-union threshold to use for non-maximum suppression. Defaults to 0.5. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". Returns: List[List[int]]: Groups of prediction indices be merged. @@ -179,7 +186,7 @@ def group_overlapping_boxes( break merge_candidate = np.expand_dims(predictions[idx], axis=0) - ious = box_iou_batch(predictions[order][:, :4], merge_candidate[:, :4]) + ious = box_iou_batch(predictions[order][:, :4], merge_candidate[:, :4], match_metric) ious = ious.flatten() above_threshold = ious >= iou_threshold @@ -189,9 +196,67 @@ def group_overlapping_boxes( return merge_groups +def mask_non_max_merge( + predictions: np.ndarray, + masks: np.ndarray, + iou_threshold: float = 0.5, + mask_dimension: int = 640, + match_metric: str = "IOU", +) -> np.ndarray: + """ + Perform Non-Maximum Merging (NMM) on segmentation predictions. + + Args: + predictions (np.ndarray): A 2D array of object detection predictions in + the format of `(x_min, y_min, x_max, y_max, score)` + or `(x_min, y_min, x_max, y_max, score, class)`. Shape: `(N, 5)` or + `(N, 6)`, where N is the number of predictions. + masks (np.ndarray): A 3D array of binary masks corresponding to the predictions. + Shape: `(N, H, W)`, where N is the number of predictions, and H, W are the + dimensions of each mask. + iou_threshold (float): The intersection-over-union threshold + to use for non-maximum suppression. + mask_dimension (int): The dimension to which the masks should be + resized before computing IOU values. Defaults to 640. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". + + Returns: + np.ndarray: A boolean array indicating which predictions to keep after + non-maximum suppression. + + Raises: + AssertionError: If `iou_threshold` is not within the closed + range from `0` to `1`. + """ + masks_resized = resize_masks(masks, mask_dimension) + if predictions.shape[1] == 5: + return group_overlapping_masks(predictions, masks_resized, iou_threshold, match_metric) + + category_ids = predictions[:, 5] + merge_groups = [] + for category_id in np.unique(category_ids): + curr_indices = np.where(category_ids == category_id)[0] + merge_class_groups = group_overlapping_masks( + predictions[curr_indices], masks_resized[curr_indices], iou_threshold, match_metric + ) + + for merge_class_group in merge_class_groups: + merge_groups.append(curr_indices[merge_class_group].tolist()) + + for merge_group in merge_groups: + if len(merge_group) == 0: + raise ValueError( + f"Empty group detected when non-max-merging " + f"detections: {merge_groups}" + ) + return merge_groups + + def box_non_max_merge( predictions: npt.NDArray[np.float64], iou_threshold: float = 0.5, + match_metric: str = "IOU", ) -> List[List[int]]: """ Apply greedy version of non-maximum merging per category to avoid detecting @@ -204,20 +269,22 @@ def box_non_max_merge( detections of different classes to be merged. iou_threshold (float): The intersection-over-union threshold to use for non-maximum suppression. Defaults to 0.5. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". Returns: List[List[int]]: Groups of prediction indices be merged. Each group may have 1 or more elements. """ if predictions.shape[1] == 5: - return group_overlapping_boxes(predictions, iou_threshold) + return group_overlapping_boxes(predictions, iou_threshold, match_metric) category_ids = predictions[:, 5] merge_groups = [] for category_id in np.unique(category_ids): curr_indices = np.where(category_ids == category_id)[0] merge_class_groups = group_overlapping_boxes( - predictions[curr_indices], iou_threshold + predictions[curr_indices], iou_threshold, match_metric ) for merge_class_group in merge_class_groups: @@ -232,6 +299,53 @@ def box_non_max_merge( return merge_groups +def group_overlapping_masks( + predictions: npt.NDArray[np.float64], + masks: npt.NDArray[np.float64], + iou_threshold: float = 0.5, + match_metric: str = "IOU", +) -> List[List[int]]: + """ + Apply greedy version of non-maximum merging to avoid detecting too many + + Args: + predictions (npt.NDArray[np.float64]): An array of shape `(n, 5)` containing + the bounding boxes coordinates in format `[x1, y1, x2, y2]` + and the confidence scores. + masks (npt.NDArray[np.float64]): A 3D array of binary masks corresponding to the predictions. + iou_threshold (float): The intersection-over-union threshold + to use for non-maximum suppression. Defaults to 0.5. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". + + Returns: + List[List[int]]: Groups of prediction indices be merged. + Each group may have 1 or more elements. + """ + merge_groups: List[List[int]] = [] + + scores = predictions[:, 4] + order = scores.argsort() + + while len(order) > 0: + idx = int(order[-1]) + + order = order[:-1] + if len(order) == 0: + merge_groups.append([idx]) + break + + merge_candidate = np.expand_dims(masks[idx], axis=0) + ious = mask_iou_batch(masks[order], merge_candidate, match_metric) + ious = ious.flatten() + + above_threshold = ious >= iou_threshold + merge_group = [idx, *np.flip(order[above_threshold]).tolist()] + merge_groups.append(merge_group) + order = order[~above_threshold] + return merge_groups + + class OverlapFilter(Enum): """ Enum specifying the strategy for filtering overlapping detections. diff --git a/supervision/detection/tools/inference_slicer.py b/supervision/detection/tools/inference_slicer.py index 05469dd6..7babaf68 100644 --- a/supervision/detection/tools/inference_slicer.py +++ b/supervision/detection/tools/inference_slicer.py @@ -72,6 +72,8 @@ class InferenceSlicer: filtering or merging overlapping detections in slices. iou_threshold (float): Intersection over Union (IoU) threshold used when filtering by overlap. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". callback (Callable): A function that performs inference on a given image slice and returns detections. thread_workers (int): Number of threads for parallel execution. @@ -91,6 +93,7 @@ class InferenceSlicer: overlap_wh: Optional[Tuple[int, int]] = None, overlap_filter: Union[OverlapFilter, str] = OverlapFilter.NON_MAX_SUPPRESSION, iou_threshold: float = 0.5, + match_metric: str = "IOU", thread_workers: int = 1, ): if overlap_ratio_wh is not None: @@ -106,6 +109,7 @@ class InferenceSlicer: self.slice_wh = slice_wh self.iou_threshold = iou_threshold + self.match_metric = match_metric self.overlap_filter = OverlapFilter.from_value(overlap_filter) self.callback = callback self.thread_workers = thread_workers @@ -165,9 +169,9 @@ class InferenceSlicer: if self.overlap_filter == OverlapFilter.NONE: return merged elif self.overlap_filter == OverlapFilter.NON_MAX_SUPPRESSION: - return merged.with_nms(threshold=self.iou_threshold) + return merged.with_nms(threshold=self.iou_threshold, match_metric=self.match_metric) elif self.overlap_filter == OverlapFilter.NON_MAX_MERGE: - return merged.with_nmm(threshold=self.iou_threshold) + return merged.with_nmm(threshold=self.iou_threshold, match_metric=self.match_metric) else: warnings.warn( f"Invalid overlap filter strategy: {self.overlap_filter}", diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 0d5ec475..19cddca4 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -48,7 +48,7 @@ def polygon_to_mask(polygon: np.ndarray, resolution_wh: Tuple[int, int]) -> np.n return mask -def box_iou_batch(boxes_true: np.ndarray, boxes_detection: np.ndarray) -> np.ndarray: +def box_iou_batch(boxes_true: np.ndarray, boxes_detection: np.ndarray, match_metric: str = "IOU") -> np.ndarray: """ Compute Intersection over Union (IoU) of two sets of bounding boxes - `boxes_true` and `boxes_detection`. Both sets @@ -59,6 +59,8 @@ def box_iou_batch(boxes_true: np.ndarray, boxes_detection: np.ndarray) -> np.nda `shape = (N, 4)` where `N` is number of true objects. boxes_detection (np.ndarray): 2D `np.ndarray` representing detection boxes. `shape = (M, 4)` where `M` is number of detected objects. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". Returns: np.ndarray: Pairwise IoU of boxes from `boxes_true` and `boxes_detection`. @@ -76,13 +78,32 @@ def box_iou_batch(boxes_true: np.ndarray, boxes_detection: np.ndarray) -> np.nda bottom_right = np.minimum(boxes_true[:, None, 2:], boxes_detection[:, 2:]) area_inter = np.prod(np.clip(bottom_right - top_left, a_min=0, a_max=None), 2) - ious = area_inter / (area_true[:, None] + area_detection - area_inter) + + if match_metric.upper() == "IOU": + union_area = area_true[:, None] + area_detection - area_inter + ious = np.divide( + area_inter, + union_area, + out=np.zeros_like(area_inter, dtype=float), + where=union_area != 0, + ) + elif match_metric.upper() == "IOS": + small_area = np.minimum(area_true[:, None], area_detection) + ious = np.divide( + area_inter, + small_area, + out=np.zeros_like(area_inter, dtype=float), + where=small_area != 0, + ) + else: + raise ValueError(f"match_metric {match_metric} is not supported, only 'IOU' and 'IOS' are supported") + ious = np.nan_to_num(ious) return ious def _mask_iou_batch_split( - masks_true: np.ndarray, masks_detection: np.ndarray + masks_true: np.ndarray, masks_detection: np.ndarray, match_metric: str = "IOU" ) -> np.ndarray: """ Internal function. @@ -92,6 +113,8 @@ def _mask_iou_batch_split( Args: masks_true (np.ndarray): 3D `np.ndarray` representing ground-truth masks. masks_detection (np.ndarray): 3D `np.ndarray` representing detection masks. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". Returns: np.ndarray: Pairwise IoU of masks from `masks_true` and `masks_detection`. @@ -102,19 +125,34 @@ def _mask_iou_batch_split( masks_true_area = masks_true.sum(axis=(1, 2)) masks_detection_area = masks_detection.sum(axis=(1, 2)) - union_area = masks_true_area[:, None] + masks_detection_area - intersection_area - return np.divide( - intersection_area, - union_area, - out=np.zeros_like(intersection_area, dtype=float), - where=union_area != 0, - ) + if match_metric.upper() == "IOU": + union_area = masks_true_area[:, None] + masks_detection_area - intersection_area + ious = np.divide( + intersection_area, + union_area, + out=np.zeros_like(intersection_area, dtype=float), + where=union_area != 0, + ) + elif match_metric.upper() == "IOS": + small_area = np.minimum(intersection_area, masks_detection_area) + ious = np.divide( + intersection_area, + small_area, + out=np.zeros_like(intersection_area, dtype=float), + where=small_area != 0, + ) + else: + raise ValueError(f"match_metric {match_metric} is not supported, only 'IOU' and 'IOS' are supported") + + ious = np.nan_to_num(ious) + return ious def mask_iou_batch( masks_true: np.ndarray, masks_detection: np.ndarray, + match_metric: str = "IOU", memory_limit: int = 1024 * 5, ) -> np.ndarray: """ @@ -124,6 +162,8 @@ def mask_iou_batch( Args: masks_true (np.ndarray): 3D `np.ndarray` representing ground-truth masks. masks_detection (np.ndarray): 3D `np.ndarray` representing detection masks. + match_metric (str): Metric used for matching detections in slices. + "IOU" or "IOS". Defaults "IOU". memory_limit (int): memory limit in MB, default is 1024 * 5 MB (5GB). Returns: @@ -138,7 +178,7 @@ def mask_iou_batch( / 1024 ) if memory <= memory_limit: - return _mask_iou_batch_split(masks_true, masks_detection) + return _mask_iou_batch_split(masks_true, masks_detection, match_metric) ious = [] step = max( @@ -153,7 +193,7 @@ def mask_iou_batch( 1, ) for i in range(0, masks_true.shape[0], step): - ious.append(_mask_iou_batch_split(masks_true[i : i + step], masks_detection)) + ious.append(_mask_iou_batch_split(masks_true[i : i + step], masks_detection, match_metric)) return np.vstack(ious) From 4679334d86220d465eaff2646a52886992ed63fd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 12:22:15 +0000 Subject: [PATCH 03/24] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto?= =?UTF-8?q?=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/detection/core.py | 32 ++++++++++++++----- supervision/detection/overlap_filter.py | 17 +++++++--- .../detection/tools/inference_slicer.py | 8 +++-- supervision/detection/utils.py | 18 ++++++++--- 4 files changed, 57 insertions(+), 18 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index ad012a9d..45478973 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -17,8 +17,8 @@ from supervision.detection.lmm import ( ) from supervision.detection.overlap_filter import ( box_non_max_merge, - mask_non_max_merge, box_non_max_suppression, + mask_non_max_merge, mask_non_max_suppression, ) from supervision.detection.tools.transformers import ( @@ -28,12 +28,12 @@ from supervision.detection.tools.transformers import ( ) from supervision.detection.utils import ( box_iou_batch, - mask_iou_batch, calculate_masks_centroids, extract_ultralytics_masks, get_data_item, is_data_equal, is_metadata_equal, + mask_iou_batch, mask_to_xyxy, merge_data, merge_metadata, @@ -1283,7 +1283,10 @@ class Detections: return (self.xyxy[:, 3] - self.xyxy[:, 1]) * (self.xyxy[:, 2] - self.xyxy[:, 0]) def with_nms( - self, threshold: float = 0.5, class_agnostic: bool = False, match_metric: str = "IOU" + self, + threshold: float = 0.5, + class_agnostic: bool = False, + match_metric: str = "IOU", ) -> Detections: """ Performs non-max suppression on detection set. If the detections result @@ -1331,17 +1334,25 @@ class Detections: if self.mask is not None: indices = mask_non_max_suppression( - predictions=predictions, masks=self.mask, iou_threshold=threshold, match_metric=match_metric + predictions=predictions, + masks=self.mask, + iou_threshold=threshold, + match_metric=match_metric, ) else: indices = box_non_max_suppression( - predictions=predictions, iou_threshold=threshold, match_metric=match_metric + predictions=predictions, + iou_threshold=threshold, + match_metric=match_metric, ) return self[indices] def with_nmm( - self, threshold: float = 0.5, class_agnostic: bool = False, match_metric: str = "IOU" + self, + threshold: float = 0.5, + class_agnostic: bool = False, + match_metric: str = "IOU", ) -> Detections: """ Perform non-maximum merging on the current set of object detections. @@ -1389,11 +1400,16 @@ class Detections: if self.mask is not None: merge_groups = mask_non_max_merge( - predictions=predictions, masks=self.mask, iou_threshold=threshold, match_metric=match_metric + predictions=predictions, + masks=self.mask, + iou_threshold=threshold, + match_metric=match_metric, ) else: merge_groups = box_non_max_merge( - predictions=predictions, iou_threshold=threshold, match_metric=match_metric + predictions=predictions, + iou_threshold=threshold, + match_metric=match_metric, ) result = [] diff --git a/supervision/detection/overlap_filter.py b/supervision/detection/overlap_filter.py index 935be236..608216e2 100644 --- a/supervision/detection/overlap_filter.py +++ b/supervision/detection/overlap_filter.py @@ -153,7 +153,9 @@ def box_non_max_suppression( def group_overlapping_boxes( - predictions: npt.NDArray[np.float64], iou_threshold: float = 0.5, match_metric: str = "IOU" + predictions: npt.NDArray[np.float64], + iou_threshold: float = 0.5, + match_metric: str = "IOU", ) -> List[List[int]]: """ Apply greedy version of non-maximum merging to avoid detecting too many @@ -186,7 +188,9 @@ def group_overlapping_boxes( break merge_candidate = np.expand_dims(predictions[idx], axis=0) - ious = box_iou_batch(predictions[order][:, :4], merge_candidate[:, :4], match_metric) + ious = box_iou_batch( + predictions[order][:, :4], merge_candidate[:, :4], match_metric + ) ious = ious.flatten() above_threshold = ious >= iou_threshold @@ -231,14 +235,19 @@ def mask_non_max_merge( """ masks_resized = resize_masks(masks, mask_dimension) if predictions.shape[1] == 5: - return group_overlapping_masks(predictions, masks_resized, iou_threshold, match_metric) + return group_overlapping_masks( + predictions, masks_resized, iou_threshold, match_metric + ) category_ids = predictions[:, 5] merge_groups = [] for category_id in np.unique(category_ids): curr_indices = np.where(category_ids == category_id)[0] merge_class_groups = group_overlapping_masks( - predictions[curr_indices], masks_resized[curr_indices], iou_threshold, match_metric + predictions[curr_indices], + masks_resized[curr_indices], + iou_threshold, + match_metric, ) for merge_class_group in merge_class_groups: diff --git a/supervision/detection/tools/inference_slicer.py b/supervision/detection/tools/inference_slicer.py index 7babaf68..324d26e3 100644 --- a/supervision/detection/tools/inference_slicer.py +++ b/supervision/detection/tools/inference_slicer.py @@ -169,9 +169,13 @@ class InferenceSlicer: if self.overlap_filter == OverlapFilter.NONE: return merged elif self.overlap_filter == OverlapFilter.NON_MAX_SUPPRESSION: - return merged.with_nms(threshold=self.iou_threshold, match_metric=self.match_metric) + return merged.with_nms( + threshold=self.iou_threshold, match_metric=self.match_metric + ) elif self.overlap_filter == OverlapFilter.NON_MAX_MERGE: - return merged.with_nmm(threshold=self.iou_threshold, match_metric=self.match_metric) + return merged.with_nmm( + threshold=self.iou_threshold, match_metric=self.match_metric + ) else: warnings.warn( f"Invalid overlap filter strategy: {self.overlap_filter}", diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 19cddca4..3c427d21 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -48,7 +48,9 @@ def polygon_to_mask(polygon: np.ndarray, resolution_wh: Tuple[int, int]) -> np.n return mask -def box_iou_batch(boxes_true: np.ndarray, boxes_detection: np.ndarray, match_metric: str = "IOU") -> np.ndarray: +def box_iou_batch( + boxes_true: np.ndarray, boxes_detection: np.ndarray, match_metric: str = "IOU" +) -> np.ndarray: """ Compute Intersection over Union (IoU) of two sets of bounding boxes - `boxes_true` and `boxes_detection`. Both sets @@ -96,7 +98,9 @@ def box_iou_batch(boxes_true: np.ndarray, boxes_detection: np.ndarray, match_met where=small_area != 0, ) else: - raise ValueError(f"match_metric {match_metric} is not supported, only 'IOU' and 'IOS' are supported") + raise ValueError( + f"match_metric {match_metric} is not supported, only 'IOU' and 'IOS' are supported" + ) ious = np.nan_to_num(ious) return ious @@ -143,7 +147,9 @@ def _mask_iou_batch_split( where=small_area != 0, ) else: - raise ValueError(f"match_metric {match_metric} is not supported, only 'IOU' and 'IOS' are supported") + raise ValueError( + f"match_metric {match_metric} is not supported, only 'IOU' and 'IOS' are supported" + ) ious = np.nan_to_num(ious) return ious @@ -193,7 +199,11 @@ def mask_iou_batch( 1, ) for i in range(0, masks_true.shape[0], step): - ious.append(_mask_iou_batch_split(masks_true[i : i + step], masks_detection, match_metric)) + ious.append( + _mask_iou_batch_split( + masks_true[i : i + step], masks_detection, match_metric + ) + ) return np.vstack(ious) From 646ac83bef70ee04a8161eacf0cfe686aba89027 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 25 Jun 2025 01:04:54 +0000 Subject: [PATCH 04/24] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto?= =?UTF-8?q?=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/detection/overlap_filter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/supervision/detection/overlap_filter.py b/supervision/detection/overlap_filter.py index f59926a4..dbe1159a 100644 --- a/supervision/detection/overlap_filter.py +++ b/supervision/detection/overlap_filter.py @@ -256,8 +256,7 @@ def mask_non_max_merge( for merge_group in merge_groups: if len(merge_group) == 0: raise ValueError( - f"Empty group detected when non-max-merging " - f"detections: {merge_groups}" + f"Empty group detected when non-max-merging detections: {merge_groups}" ) return merge_groups From d0028581696010e6b85c89592745b5ad5f21ce88 Mon Sep 17 00:00:00 2001 From: AI-Hao <2865467769@qq.com> Date: Wed, 25 Jun 2025 10:56:50 +0800 Subject: [PATCH 05/24] =?UTF-8?q?ci:=20=E6=B3=A8=E9=87=8A=E6=8E=89?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=20GitHub=20App=20token=20=E7=9A=84=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 暂时注释掉创建 GitHub App token 的步骤,可能是因为该步骤不再需要或出现了问题。这个修改不影响其他功能,只是减少了工作流中的一个步骤。 --- .github/workflows/publish-docs.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 10b8c659..d0f26aad 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -38,14 +38,14 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: 🔑 Create GitHub App token (mkdocs) - id: mkdocs_token - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 - with: - app-id: ${{ secrets.MKDOCS_APP_ID }} - private-key: ${{ secrets.MKDOCS_PEM }} - owner: roboflow - repositories: mkdocs-material-insiders +# - name: 🔑 Create GitHub App token (mkdocs) +# id: mkdocs_token +# uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 +# with: +# app-id: ${{ secrets.MKDOCS_APP_ID }} +# private-key: ${{ secrets.MKDOCS_PEM }} +# owner: roboflow +# repositories: mkdocs-material-insiders - name: 🏗️ Install dependencies run: | From eb170bc393b65850c653b985fc52e2de85918c6d Mon Sep 17 00:00:00 2001 From: AI-Hao <2865467769@qq.com> Date: Wed, 25 Jun 2025 11:02:17 +0800 Subject: [PATCH 06/24] =?UTF-8?q?ci:=E5=8F=96=E6=B6=88=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=20GitHub=20App=20token=20=E6=AD=A5=E9=AA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 取消注释了 publish-docs.yml 文件中的 Create GitHub App token (mkdocs) 步骤。这个步骤现在将作为工作流程的一部分正常运行,不再被跳过。 --- .github/workflows/publish-docs.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index d0f26aad..10b8c659 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -38,14 +38,14 @@ jobs: with: python-version: ${{ matrix.python-version }} -# - name: 🔑 Create GitHub App token (mkdocs) -# id: mkdocs_token -# uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 -# with: -# app-id: ${{ secrets.MKDOCS_APP_ID }} -# private-key: ${{ secrets.MKDOCS_PEM }} -# owner: roboflow -# repositories: mkdocs-material-insiders + - name: 🔑 Create GitHub App token (mkdocs) + id: mkdocs_token + uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 + with: + app-id: ${{ secrets.MKDOCS_APP_ID }} + private-key: ${{ secrets.MKDOCS_PEM }} + owner: roboflow + repositories: mkdocs-material-insiders - name: 🏗️ Install dependencies run: | From 13e857cf0010eb2d1e7274101bc429e37647befd Mon Sep 17 00:00:00 2001 From: AI-Hao <2865467769@qq.com> Date: Mon, 7 Jul 2025 15:21:28 +0800 Subject: [PATCH 07/24] =?UTF-8?q?refactor(detection):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E6=A3=80=E6=B5=8B=E5=AF=B9=E8=B1=A1=E7=9A=84?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 merge_inner_detections_objects 函数中的 threshold 和 match_metric 参数 - 新增 merge_inner_detections_objects_without_iou 函数,用于合并检测对象 - 优化非最大值抑制(NMM)的逻辑,提高效率 - 调整 mask_iou_batch 函数的实现,简化代码 --- supervision/detection/core.py | 17 ++++++-- supervision/detection/overlap_filter.py | 30 +++++++++----- supervision/detection/utils.py | 53 +++++++++++++------------ 3 files changed, 63 insertions(+), 37 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 5dcb46ea..ab9bddbb 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass, field from enum import Enum +from functools import reduce from typing import Any, Dict, Iterator, List, Optional, Tuple, Union import numpy as np @@ -1455,9 +1456,7 @@ class Detections: result = [] for merge_group in merge_groups: unmerged_detections = [self[i] for i in merge_group] - merged_detections = merge_inner_detections_objects( - unmerged_detections, threshold, match_metric - ) + merged_detections = merge_inner_detections_objects_without_iou(unmerged_detections) result.append(merged_detections) return Detections.merge(result) @@ -1577,6 +1576,18 @@ def merge_inner_detections_objects( detections_1 = merge_inner_detection_object_pair(detections_1, detections_2) return detections_1 +def merge_inner_detections_objects_without_iou( + detections: List[Detections] +) -> Detections: + """ + Given N detections each of length 1 (exactly one object inside), combine them into a + single detection object of length 1. The contained inner object will be the merged + result of all the input detections. + + For example, this lets you merge N boxes into one big box, N masks into one mask, + etc. + """ + return reduce(merge_inner_detection_object_pair, detections) def validate_fields_both_defined_or_none( detections_1: Detections, detections_2: Detections diff --git a/supervision/detection/overlap_filter.py b/supervision/detection/overlap_filter.py index dbe1159a..b5aecbe2 100644 --- a/supervision/detection/overlap_filter.py +++ b/supervision/detection/overlap_filter.py @@ -91,7 +91,7 @@ def mask_non_max_suppression( for i in range(rows): if keep[i]: condition = (ious[i] > iou_threshold) & (categories[i] == categories) - keep[i + 1 :] = np.where(condition[i + 1 :], False, keep[i + 1 :]) + keep[i + 1:] = np.where(condition[i + 1:], False, keep[i + 1:]) return keep[sort_index.argsort()] @@ -206,7 +206,7 @@ def mask_non_max_merge( iou_threshold: float = 0.5, mask_dimension: int = 640, match_metric: str = "IOU", -) -> np.ndarray: +) -> List[List[int]]: """ Perform Non-Maximum Merging (NMM) on segmentation predictions. @@ -342,14 +342,26 @@ def group_overlapping_masks( merge_groups.append([idx]) break - merge_candidate = np.expand_dims(masks[idx], axis=0) - ious = mask_iou_batch(masks[order], merge_candidate, match_metric) - ious = ious.flatten() + # calculate mask + merge_candidate = masks[idx][None, ...] + candidate_groups = [idx] + while len(order) > 0: + # 'IOU or IOS' of the calculate mask and the remaining mask + ious = mask_iou_batch(masks[order], merge_candidate, match_metric) + above_threshold: np.ndarray = ious.flatten() >= iou_threshold + # if no mask is above threshold, break + if not above_threshold.any(): + break + # get indexes that meet the threshold + above_idx = order[above_threshold] + # update merge_candidate + merge_candidate = np.logical_or.reduce(np.concatenate([masks[above_idx], merge_candidate]), axis=0, keepdims=True) + # add indexes that meet the criteria to the candidate_groups + candidate_groups.extend(np.flip(above_idx).tolist()) + # update order, masks + order = order[~above_threshold] - above_threshold = ious >= iou_threshold - merge_group = [idx, *np.flip(order[above_threshold]).tolist()] - merge_groups.append(merge_group) - order = order[~above_threshold] + merge_groups.append(candidate_groups) return merge_groups diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index a915c96d..8bfdce84 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -49,7 +49,9 @@ def polygon_to_mask(polygon: np.ndarray, resolution_wh: Tuple[int, int]) -> np.n def box_iou_batch( - boxes_true: np.ndarray, boxes_detection: np.ndarray, match_metric: str = "IOU" + boxes_true: np.ndarray, + boxes_detection: np.ndarray, + match_metric: str = "IOU", ) -> np.ndarray: """ Compute Intersection over Union (IoU) of two sets of bounding boxes - @@ -107,7 +109,9 @@ def box_iou_batch( def _mask_iou_batch_split( - masks_true: np.ndarray, masks_detection: np.ndarray, match_metric: str = "IOU" + masks_true: np.ndarray, + masks_detection: np.ndarray, + match_metric: str = "IOU", ) -> np.ndarray: """ Internal function. @@ -123,12 +127,10 @@ def _mask_iou_batch_split( Returns: np.ndarray: Pairwise IoU of masks from `masks_true` and `masks_detection`. """ - intersection_area = np.logical_and(masks_true[:, None], masks_detection).sum( - axis=(2, 3) - ) + intersection_area = np.logical_and(masks_true[:, None], masks_detection).sum(axis=(2, 3)) - masks_true_area = masks_true.sum(axis=(1, 2)) - masks_detection_area = masks_detection.sum(axis=(1, 2)) + masks_true_area = masks_true.sum(axis=(1, 2)) # (area1, area2, ...) + masks_detection_area = masks_detection.sum(axis=(1, 2)) # (area1) if match_metric.upper() == "IOU": union_area = masks_true_area[:, None] + masks_detection_area - intersection_area @@ -139,7 +141,8 @@ def _mask_iou_batch_split( where=union_area != 0, ) elif match_metric.upper() == "IOS": - small_area = np.minimum(intersection_area, masks_detection_area) + # ios = intersection_area / min(area1, area2) + small_area = np.minimum(masks_true_area[:, None], masks_detection_area) ious = np.divide( intersection_area, small_area, @@ -176,12 +179,12 @@ def mask_iou_batch( np.ndarray: Pairwise IoU of masks from `masks_true` and `masks_detection`. """ memory = ( - masks_true.shape[0] - * masks_true.shape[1] - * masks_true.shape[2] - * masks_detection.shape[0] - / 1024 - / 1024 + masks_true.shape[0] + * masks_true.shape[1] + * masks_true.shape[2] + * masks_detection.shape[0] + / 1024 + / 1024 ) if memory <= memory_limit: return _mask_iou_batch_split(masks_true, masks_detection, match_metric) @@ -192,16 +195,16 @@ def mask_iou_batch( * 1024 * 1024 // ( - masks_detection.shape[0] - * masks_detection.shape[1] - * masks_detection.shape[2] + masks_detection.shape[0] + * masks_detection.shape[1] + * masks_detection.shape[2] ), 1, ) for i in range(0, masks_true.shape[0], step): ious.append( _mask_iou_batch_split( - masks_true[i : i + step], masks_detection, match_metric + masks_true[i: i + step], masks_detection, match_metric ) ) @@ -581,7 +584,7 @@ def filter_polygons_by_area( polygon for polygon, area in zip(polygons, ares) if (min_area is None or area >= min_area) - and (max_area is None or area <= max_area) + and (max_area is None or area <= max_area) ] @@ -918,9 +921,9 @@ def move_masks( if source_x_end > source_x_start and source_y_end > source_y_start: mask_array[ - :, - destination_y_start:destination_y_end, - destination_x_start:destination_x_end, + :, + destination_y_start:destination_y_end, + destination_x_start:destination_x_end, ] = masks[:, source_y_start:source_y_end, source_x_start:source_x_end] return mask_array @@ -1022,11 +1025,11 @@ def is_metadata_equal(metadata_a: Dict[str, Any], metadata_b: Dict[str, Any]) -> return set(metadata_a.keys()) == set(metadata_b.keys()) and all( np.array_equal(metadata_a[key], metadata_b[key]) if ( - isinstance(metadata_a[key], np.ndarray) - and isinstance(metadata_b[key], np.ndarray) + isinstance(metadata_a[key], np.ndarray) + and isinstance(metadata_b[key], np.ndarray) ) else metadata_a[key] == metadata_b[key] - for key in metadata_a + for key in metadata_a ) From 0b120f174879e394f3a1ffe1f4d702ab867fda08 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 07:21:52 +0000 Subject: [PATCH 08/24] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto?= =?UTF-8?q?=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/detection/core.py | 8 ++++-- supervision/detection/overlap_filter.py | 8 ++++-- supervision/detection/utils.py | 38 +++++++++++++------------ 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index ab9bddbb..72ebc8ee 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -1456,7 +1456,9 @@ class Detections: result = [] for merge_group in merge_groups: unmerged_detections = [self[i] for i in merge_group] - merged_detections = merge_inner_detections_objects_without_iou(unmerged_detections) + merged_detections = merge_inner_detections_objects_without_iou( + unmerged_detections + ) result.append(merged_detections) return Detections.merge(result) @@ -1576,8 +1578,9 @@ def merge_inner_detections_objects( detections_1 = merge_inner_detection_object_pair(detections_1, detections_2) return detections_1 + def merge_inner_detections_objects_without_iou( - detections: List[Detections] + detections: List[Detections], ) -> Detections: """ Given N detections each of length 1 (exactly one object inside), combine them into a @@ -1589,6 +1592,7 @@ def merge_inner_detections_objects_without_iou( """ return reduce(merge_inner_detection_object_pair, detections) + def validate_fields_both_defined_or_none( detections_1: Detections, detections_2: Detections ) -> None: diff --git a/supervision/detection/overlap_filter.py b/supervision/detection/overlap_filter.py index b5aecbe2..b763cf2c 100644 --- a/supervision/detection/overlap_filter.py +++ b/supervision/detection/overlap_filter.py @@ -91,7 +91,7 @@ def mask_non_max_suppression( for i in range(rows): if keep[i]: condition = (ious[i] > iou_threshold) & (categories[i] == categories) - keep[i + 1:] = np.where(condition[i + 1:], False, keep[i + 1:]) + keep[i + 1 :] = np.where(condition[i + 1 :], False, keep[i + 1 :]) return keep[sort_index.argsort()] @@ -355,7 +355,11 @@ def group_overlapping_masks( # get indexes that meet the threshold above_idx = order[above_threshold] # update merge_candidate - merge_candidate = np.logical_or.reduce(np.concatenate([masks[above_idx], merge_candidate]), axis=0, keepdims=True) + merge_candidate = np.logical_or.reduce( + np.concatenate([masks[above_idx], merge_candidate]), + axis=0, + keepdims=True, + ) # add indexes that meet the criteria to the candidate_groups candidate_groups.extend(np.flip(above_idx).tolist()) # update order, masks diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 8bfdce84..b7adbcd2 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -127,7 +127,9 @@ def _mask_iou_batch_split( Returns: np.ndarray: Pairwise IoU of masks from `masks_true` and `masks_detection`. """ - intersection_area = np.logical_and(masks_true[:, None], masks_detection).sum(axis=(2, 3)) + intersection_area = np.logical_and(masks_true[:, None], masks_detection).sum( + axis=(2, 3) + ) masks_true_area = masks_true.sum(axis=(1, 2)) # (area1, area2, ...) masks_detection_area = masks_detection.sum(axis=(1, 2)) # (area1) @@ -179,12 +181,12 @@ def mask_iou_batch( np.ndarray: Pairwise IoU of masks from `masks_true` and `masks_detection`. """ memory = ( - masks_true.shape[0] - * masks_true.shape[1] - * masks_true.shape[2] - * masks_detection.shape[0] - / 1024 - / 1024 + masks_true.shape[0] + * masks_true.shape[1] + * masks_true.shape[2] + * masks_detection.shape[0] + / 1024 + / 1024 ) if memory <= memory_limit: return _mask_iou_batch_split(masks_true, masks_detection, match_metric) @@ -195,16 +197,16 @@ def mask_iou_batch( * 1024 * 1024 // ( - masks_detection.shape[0] - * masks_detection.shape[1] - * masks_detection.shape[2] + masks_detection.shape[0] + * masks_detection.shape[1] + * masks_detection.shape[2] ), 1, ) for i in range(0, masks_true.shape[0], step): ious.append( _mask_iou_batch_split( - masks_true[i: i + step], masks_detection, match_metric + masks_true[i : i + step], masks_detection, match_metric ) ) @@ -584,7 +586,7 @@ def filter_polygons_by_area( polygon for polygon, area in zip(polygons, ares) if (min_area is None or area >= min_area) - and (max_area is None or area <= max_area) + and (max_area is None or area <= max_area) ] @@ -921,9 +923,9 @@ def move_masks( if source_x_end > source_x_start and source_y_end > source_y_start: mask_array[ - :, - destination_y_start:destination_y_end, - destination_x_start:destination_x_end, + :, + destination_y_start:destination_y_end, + destination_x_start:destination_x_end, ] = masks[:, source_y_start:source_y_end, source_x_start:source_x_end] return mask_array @@ -1025,11 +1027,11 @@ def is_metadata_equal(metadata_a: Dict[str, Any], metadata_b: Dict[str, Any]) -> return set(metadata_a.keys()) == set(metadata_b.keys()) and all( np.array_equal(metadata_a[key], metadata_b[key]) if ( - isinstance(metadata_a[key], np.ndarray) - and isinstance(metadata_b[key], np.ndarray) + isinstance(metadata_a[key], np.ndarray) + and isinstance(metadata_b[key], np.ndarray) ) else metadata_a[key] == metadata_b[key] - for key in metadata_a + for key in metadata_a ) From 74d5f0f9a6fccca44b0f0ab8f1d622bbb261318d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 13:07:17 +0000 Subject: [PATCH 09/24] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto?= =?UTF-8?q?=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/detection/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 87b58ad2..c700aeb0 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -108,7 +108,9 @@ def box_iou( return inter_area / union_area + 1e-6 -def box_iou_batch(boxes_true: np.ndarray, boxes_detection: np.ndarray, match_metric: str = "IOU") -> np.ndarray: +def box_iou_batch( + boxes_true: np.ndarray, boxes_detection: np.ndarray, match_metric: str = "IOU" +) -> np.ndarray: """ Compute Intersection over Union (IoU) of two sets of bounding boxes - `boxes_true` and `boxes_detection`. Both sets From 9517987b7b427049747a1b3cd41d9f2549d70314 Mon Sep 17 00:00:00 2001 From: soumik12345 <19soumik.rakshit96@gmail.com> Date: Mon, 14 Jul 2025 20:19:32 +0530 Subject: [PATCH 10/24] chore: make pre-commit happy --- supervision/detection/overlap_filter.py | 3 ++- supervision/detection/utils.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/supervision/detection/overlap_filter.py b/supervision/detection/overlap_filter.py index b763cf2c..26edeaef 100644 --- a/supervision/detection/overlap_filter.py +++ b/supervision/detection/overlap_filter.py @@ -319,7 +319,8 @@ def group_overlapping_masks( predictions (npt.NDArray[np.float64]): An array of shape `(n, 5)` containing the bounding boxes coordinates in format `[x1, y1, x2, y2]` and the confidence scores. - masks (npt.NDArray[np.float64]): A 3D array of binary masks corresponding to the predictions. + masks (npt.NDArray[np.float64]): A 3D array of binary masks corresponding to + the predictions. iou_threshold (float): The intersection-over-union threshold to use for non-maximum suppression. Defaults to 0.5. match_metric (str): Metric used for matching detections in slices. diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index c700aeb0..b44cd2b6 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -185,7 +185,8 @@ def box_iou_batch( ) else: raise ValueError( - f"match_metric {match_metric} is not supported, only 'IOU' and 'IOS' are supported" + f"match_metric {match_metric} is not supported, " + "only 'IOU' and 'IOS' are supported" ) ious = np.nan_to_num(ious) @@ -237,7 +238,8 @@ def _mask_iou_batch_split( ) else: raise ValueError( - f"match_metric {match_metric} is not supported, only 'IOU' and 'IOS' are supported" + f"match_metric {match_metric} is not supported, " + "only 'IOU' and 'IOS' are supported" ) ious = np.nan_to_num(ious) From fb16bd9ee1a005a1dbdede7375a32058f16a891d Mon Sep 17 00:00:00 2001 From: soumik12345 <19soumik.rakshit96@gmail.com> Date: Tue, 15 Jul 2025 13:07:01 +0530 Subject: [PATCH 11/24] chore: change match_metric to overlap_metric --- supervision/detection/core.py | 17 ++++++------- supervision/detection/overlap_filter.py | 32 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index ed776496..6bbfbf4a 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -16,6 +16,7 @@ from supervision.detection.overlap_filter import ( box_non_max_suppression, mask_non_max_merge, mask_non_max_suppression, + OverlapMetric, ) from supervision.detection.tools.transformers import ( process_transformers_detection_result, @@ -1572,7 +1573,7 @@ class Detections: self, threshold: float = 0.5, class_agnostic: bool = False, - match_metric: str = "IOU", + overlap_metric: OverlapMetric = OverlapMetric.IOU, ) -> Detections: """ Performs non-max suppression on detection set. If the detections result @@ -1585,7 +1586,7 @@ class Detections: class_agnostic (bool): Whether to perform class-agnostic non-maximum suppression. If True, the class_id of each detection will be ignored. Defaults to False. - match_metric (str): Metric used for matching detections in slices. + overlap_metric (OverlapMetric): Metric used for matching detections in slices. "IOU" or "IOS". Defaults "IOU". Returns: @@ -1623,13 +1624,13 @@ class Detections: predictions=predictions, masks=self.mask, iou_threshold=threshold, - match_metric=match_metric, + match_metric=overlap_metric, ) else: indices = box_non_max_suppression( predictions=predictions, iou_threshold=threshold, - match_metric=match_metric, + match_metric=overlap_metric, ) return self[indices] @@ -1638,7 +1639,7 @@ class Detections: self, threshold: float = 0.5, class_agnostic: bool = False, - match_metric: str = "IOU", + overlap_metric: OverlapMetric = OverlapMetric.IOU, ) -> Detections: """ Perform non-maximum merging on the current set of object detections. @@ -1649,7 +1650,7 @@ class Detections: class_agnostic (bool): Whether to perform class-agnostic non-maximum merging. If True, the class_id of each detection will be ignored. Defaults to False. - match_metric (str): Metric used for matching detections in slices. + overlap_metric (OverlapMetric): Metric used for matching detections in slices. "IOU" or "IOS". Defaults "IOU". Returns: @@ -1689,13 +1690,13 @@ class Detections: predictions=predictions, masks=self.mask, iou_threshold=threshold, - match_metric=match_metric, + match_metric=overlap_metric, ) else: merge_groups = box_non_max_merge( predictions=predictions, iou_threshold=threshold, - match_metric=match_metric, + match_metric=overlap_metric, ) result = [] diff --git a/supervision/detection/overlap_filter.py b/supervision/detection/overlap_filter.py index 26edeaef..ab70c54e 100644 --- a/supervision/detection/overlap_filter.py +++ b/supervision/detection/overlap_filter.py @@ -406,3 +406,35 @@ class OverlapFilter(Enum): f"Invalid value type: {type(value)}. Must be an instance of " f"{cls.__name__} or str." ) + + +class OverlapMetric(Enum): + """ + Enum specifying the metric for measuring overlap between detections. + + Attributes: + IOU: Intersection over Union. + IOS: Intersection over Similarity. + """ + + IOU = "IOU" + IOS = "IOS" + + @classmethod + def list(cls): + return list(map(lambda c: c.value, cls)) + + @classmethod + def from_value(cls, value: Union[OverlapFilter, str]) -> OverlapFilter: + if isinstance(value, cls): + return value + if isinstance(value, str): + value = value.lower() + try: + return cls(value) + except ValueError: + raise ValueError(f"Invalid value: {value}. Must be one of {cls.list()}") + raise ValueError( + f"Invalid value type: {type(value)}. Must be an instance of " + f"{cls.__name__} or str." + ) From 42e9c5ffcba2945bc5edc24acd0222e249333cf1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 07:37:22 +0000 Subject: [PATCH 12/24] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto?= =?UTF-8?q?=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/detection/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 6bbfbf4a..a9857617 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -12,11 +12,11 @@ from supervision.config import ( ORIENTED_BOX_COORDINATES, ) from supervision.detection.overlap_filter import ( + OverlapMetric, box_non_max_merge, box_non_max_suppression, mask_non_max_merge, mask_non_max_suppression, - OverlapMetric, ) from supervision.detection.tools.transformers import ( process_transformers_detection_result, From 7849831e12d0af323edbd25bb75ced0fb616223b Mon Sep 17 00:00:00 2001 From: soumik12345 <19soumik.rakshit96@gmail.com> Date: Tue, 15 Jul 2025 13:08:40 +0530 Subject: [PATCH 13/24] chore: make pre-commit happy --- supervision/detection/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index a9857617..c428356a 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -1586,8 +1586,8 @@ class Detections: class_agnostic (bool): Whether to perform class-agnostic non-maximum suppression. If True, the class_id of each detection will be ignored. Defaults to False. - overlap_metric (OverlapMetric): Metric used for matching detections in slices. - "IOU" or "IOS". Defaults "IOU". + overlap_metric (OverlapMetric): Metric used for matching detections + in slices. Returns: Detections: A new Detections object containing the subset of detections From 3fed5f9a259e9c4c00c243216bb05232bb5b7d7d Mon Sep 17 00:00:00 2001 From: soumik12345 <19soumik.rakshit96@gmail.com> Date: Tue, 15 Jul 2025 14:17:41 +0530 Subject: [PATCH 14/24] update: docs --- docs/detection/double_detection_filter.md | 6 ++++++ supervision/detection/overlap_filter.py | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/detection/double_detection_filter.md b/docs/detection/double_detection_filter.md index b0266371..5c450ce4 100644 --- a/docs/detection/double_detection_filter.md +++ b/docs/detection/double_detection_filter.md @@ -10,6 +10,12 @@ comments: true :::supervision.detection.overlap_filter.OverlapFilter +
+

OverlapMetric

+
+ +:::supervision.detection.overlap_filter.OverlapMetric +

box_non_max_suppression

diff --git a/supervision/detection/overlap_filter.py b/supervision/detection/overlap_filter.py index ab70c54e..9cff643f 100644 --- a/supervision/detection/overlap_filter.py +++ b/supervision/detection/overlap_filter.py @@ -413,8 +413,12 @@ class OverlapMetric(Enum): Enum specifying the metric for measuring overlap between detections. Attributes: - IOU: Intersection over Union. - IOS: Intersection over Similarity. + IOU: Intersection over Union. A region-overlap metric that compares + two shapes (usually bounding boxes or masks) by normalising the + shared area with the area of their union. + IOS: Intersection over Smaller, a region-overlap metric that compares + two shapes (usually bounding boxes or masks) by normalising the + shared area with the smaller of the two shapes. """ IOU = "IOU" From fd25f4bf81bf8701e7c20fa0393e0a4cf6e16b12 Mon Sep 17 00:00:00 2001 From: soumik12345 <19soumik.rakshit96@gmail.com> Date: Tue, 15 Jul 2025 15:01:25 +0530 Subject: [PATCH 15/24] add: docs for mask_non_max_merge --- docs/detection/double_detection_filter.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/detection/double_detection_filter.md b/docs/detection/double_detection_filter.md index 5c450ce4..fbe944cf 100644 --- a/docs/detection/double_detection_filter.md +++ b/docs/detection/double_detection_filter.md @@ -33,3 +33,9 @@ comments: true :::supervision.detection.overlap_filter.box_non_max_merge + +
+

mask_non_max_merge

+
+ +:::supervision.detection.overlap_filter.mask_non_max_merge From ebf4b930b79b4636a34af85696dfe80e7f9e47b8 Mon Sep 17 00:00:00 2001 From: soumik12345 <19soumik.rakshit96@gmail.com> Date: Tue, 15 Jul 2025 15:01:36 +0530 Subject: [PATCH 16/24] add: test for mask_non_max_merge --- test/detection/test_overlap_filter.py | 172 ++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/test/detection/test_overlap_filter.py b/test/detection/test_overlap_filter.py index f628c30f..e302afb1 100644 --- a/test/detection/test_overlap_filter.py +++ b/test/detection/test_overlap_filter.py @@ -8,6 +8,7 @@ from supervision.detection.overlap_filter import ( box_non_max_suppression, group_overlapping_boxes, mask_non_max_suppression, + mask_non_max_merge, ) @@ -447,3 +448,174 @@ def test_mask_non_max_suppression( predictions=predictions, masks=masks, iou_threshold=iou_threshold ) assert np.array_equal(result, expected_result) + + +@pytest.mark.parametrize( + "predictions, masks, iou_threshold, expected_result, exception", + [ + ( + np.empty((0, 6)), + np.empty((0, 5, 5)), + 0.5, + [], + DoesNotRaise(), + ), # empty predictions and masks + ( + np.array([[0, 0, 0, 0, 0.8]]), + np.array( + [ + [ + [False, False, False, False, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, False, False, False, False], + ] + ] + ), + 0.5, + [[0]], + DoesNotRaise(), + ), # single mask with no category + ( + np.array([[0, 0, 0, 0, 0.8, 0]]), + np.array( + [ + [ + [False, False, False, False, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, False, False, False, False], + ] + ] + ), + 0.5, + [[0]], + DoesNotRaise(), + ), # single mask with category + ( + np.array([[0, 0, 0, 0, 0.9], [0, 0, 0, 0, 0.8]]), + np.array( + [ + [ + [False, False, False, False, False], + [False, True, True, False, False], + [False, True, True, False, False], + [False, False, False, False, False], + [False, False, False, False, False], + ], + [ + [False, False, False, False, False], + [False, False, False, False, False], + [False, False, False, True, True], + [False, False, False, True, True], + [False, False, False, False, False], + ], + ] + ), + 0.5, + [[0], [1]], + DoesNotRaise(), + ), # two masks non-overlapping with no category + ( + np.array([[0, 0, 0, 0, 0.9], [0, 0, 0, 0, 0.8]]), + np.array( + [ + [ + [False, False, False, False, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, False, False, False, False], + ], + [ + [False, False, False, False, False], + [False, False, True, True, True], + [False, False, True, True, True], + [False, False, True, True, True], + [False, False, False, False, False], + ], + ] + ), + 0.2, + [[0, 1]], + DoesNotRaise(), + ), # two masks partially overlapping with no category + ( + np.array([[0, 0, 0, 0, 0.9, 0], [0, 0, 0, 0, 0.8, 1]]), + np.array( + [ + [ + [False, False, False, False, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, False, False, False, False], + ], + [ + [False, False, False, False, False], + [False, False, True, True, True], + [False, False, True, True, True], + [False, False, True, True, True], + [False, False, False, False, False], + ], + ] + ), + 0.2, + [[0], [1]], + DoesNotRaise(), + ), # two masks partially overlapping with different category + ( + np.array( + [ + [0, 0, 0, 0, 0.9, 0], + [0, 0, 0, 0, 0.8, 0], + [0, 0, 0, 0, 0.85, 1], + ] + ), + np.array( + [ + [ # mask 0, class 0 + [False, False, False, False, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, False, False, False, False], + ], + [ # mask 1, class 0 + [False, False, False, False, False], + [False, False, True, True, True], + [False, False, True, True, True], + [False, False, True, True, True], + [False, False, False, False, False], + ], + [ # mask 2, class 1 + [False, False, False, False, False], + [False, False, False, True, True], + [False, False, False, True, True], + [False, False, False, False, False], + [False, False, False, False, False], + ], + ] + ), + 0.2, + [[0, 1], [2]], + DoesNotRaise(), + ), # three masks, two overlapping with same class + ], +) +def test_mask_non_max_merge( + predictions: np.ndarray, + masks: np.ndarray, + iou_threshold: float, + expected_result: List[List[int]], + exception: Exception, +) -> None: + with exception: + result = mask_non_max_merge( + predictions=predictions, masks=masks, iou_threshold=iou_threshold + ) + result = sorted([sorted(group) for group in result]) + expected_result = sorted([sorted(group) for group in expected_result]) + assert result == expected_result From b3024e780d3baaf83571fa649776921c5be59bd9 Mon Sep 17 00:00:00 2001 From: soumik12345 <19soumik.rakshit96@gmail.com> Date: Tue, 15 Jul 2025 15:01:57 +0530 Subject: [PATCH 17/24] chore: make pre-commit happy --- test/detection/test_overlap_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/detection/test_overlap_filter.py b/test/detection/test_overlap_filter.py index e302afb1..08cd870f 100644 --- a/test/detection/test_overlap_filter.py +++ b/test/detection/test_overlap_filter.py @@ -7,8 +7,8 @@ import pytest from supervision.detection.overlap_filter import ( box_non_max_suppression, group_overlapping_boxes, - mask_non_max_suppression, mask_non_max_merge, + mask_non_max_suppression, ) From 007d8f5dadcbf4acc862775fcf51b2e5f72f464b Mon Sep 17 00:00:00 2001 From: soumik12345 <19soumik.rakshit96@gmail.com> Date: Tue, 15 Jul 2025 15:34:24 +0530 Subject: [PATCH 18/24] chore: make enum comparisons --- supervision/detection/core.py | 2 +- supervision/detection/overlap_filter.py | 83 +++--------- supervision/detection/utils.py | 70 +++++++--- test/detection/test_overlap_filter.py | 172 ------------------------ 4 files changed, 75 insertions(+), 252 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index c428356a..3b343b46 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -12,7 +12,6 @@ from supervision.config import ( ORIENTED_BOX_COORDINATES, ) from supervision.detection.overlap_filter import ( - OverlapMetric, box_non_max_merge, box_non_max_suppression, mask_non_max_merge, @@ -36,6 +35,7 @@ from supervision.detection.utils import ( merge_metadata, process_roboflow_result, xywh_to_xyxy, + OverlapMetric, ) from supervision.detection.vlm import ( LMM, diff --git a/supervision/detection/overlap_filter.py b/supervision/detection/overlap_filter.py index 9cff643f..34bfb024 100644 --- a/supervision/detection/overlap_filter.py +++ b/supervision/detection/overlap_filter.py @@ -7,6 +7,7 @@ import numpy as np import numpy.typing as npt from supervision.detection.utils import box_iou_batch, mask_iou_batch +from supervision.detection.utils import OverlapMetric def resize_masks(masks: np.ndarray, max_dimension: int = 640) -> np.ndarray: @@ -42,7 +43,7 @@ def mask_non_max_suppression( predictions: np.ndarray, masks: np.ndarray, iou_threshold: float = 0.5, - match_metric: str = "IOU", + overlap_metric: OverlapMetric = OverlapMetric.IOU, mask_dimension: int = 640, ) -> np.ndarray: """ @@ -58,8 +59,7 @@ def mask_non_max_suppression( dimensions of each mask. iou_threshold (float): The intersection-over-union threshold to use for non-maximum suppression. - match_metric (str): Metric used for matching detections in slices. - "IOU" or "IOS". Defaults "IOU". + overlap_metric (OverlapMetric): Metric used for matching detections in slices. mask_dimension (int): The dimension to which the masks should be resized before computing IOU values. Defaults to 640. @@ -84,7 +84,7 @@ def mask_non_max_suppression( predictions = predictions[sort_index] masks = masks[sort_index] masks_resized = resize_masks(masks, mask_dimension) - ious = mask_iou_batch(masks_resized, masks_resized, match_metric) + ious = mask_iou_batch(masks_resized, masks_resized, overlap_metric) categories = predictions[:, 5] keep = np.ones(rows, dtype=bool) @@ -97,7 +97,7 @@ def mask_non_max_suppression( def box_non_max_suppression( - predictions: np.ndarray, iou_threshold: float = 0.5, match_metric: str = "IOU" + predictions: np.ndarray, iou_threshold: float = 0.5, overlap_metric: OverlapMetric = OverlapMetric.IOU ) -> np.ndarray: """ Perform Non-Maximum Suppression (NMS) on object detection predictions. @@ -108,8 +108,7 @@ def box_non_max_suppression( or `(x_min, y_min, x_max, y_max, score, class)`. iou_threshold (float): The intersection-over-union threshold to use for non-maximum suppression. - match_metric (str): Metric used for matching detections in slices. - "IOU" or "IOS". Defaults "IOU". + overlap_metric (OverlapMetric): Metric used for matching detections in slices. Returns: np.ndarray: A boolean array indicating which predictions to keep after n @@ -135,7 +134,7 @@ def box_non_max_suppression( boxes = predictions[:, :4] categories = predictions[:, 5] - ious = box_iou_batch(boxes, boxes, match_metric) + ious = box_iou_batch(boxes, boxes, overlap_metric) ious = ious - np.eye(rows) keep = np.ones(rows, dtype=bool) @@ -155,7 +154,7 @@ def box_non_max_suppression( def group_overlapping_boxes( predictions: npt.NDArray[np.float64], iou_threshold: float = 0.5, - match_metric: str = "IOU", + overlap_metric: OverlapMetric = OverlapMetric.IOU, ) -> List[List[int]]: """ Apply greedy version of non-maximum merging to avoid detecting too many @@ -167,8 +166,7 @@ def group_overlapping_boxes( and the confidence scores. iou_threshold (float): The intersection-over-union threshold to use for non-maximum suppression. Defaults to 0.5. - match_metric (str): Metric used for matching detections in slices. - "IOU" or "IOS". Defaults "IOU". + overlap_metric (OverlapMetric): Metric used for matching detections in slices. Returns: List[List[int]]: Groups of prediction indices be merged. @@ -189,7 +187,7 @@ def group_overlapping_boxes( merge_candidate = np.expand_dims(predictions[idx], axis=0) ious = box_iou_batch( - predictions[order][:, :4], merge_candidate[:, :4], match_metric + predictions[order][:, :4], merge_candidate[:, :4], overlap_metric ) ious = ious.flatten() @@ -205,7 +203,7 @@ def mask_non_max_merge( masks: np.ndarray, iou_threshold: float = 0.5, mask_dimension: int = 640, - match_metric: str = "IOU", + overlap_metric: OverlapMetric = OverlapMetric.IOU, ) -> List[List[int]]: """ Perform Non-Maximum Merging (NMM) on segmentation predictions. @@ -222,8 +220,7 @@ def mask_non_max_merge( to use for non-maximum suppression. mask_dimension (int): The dimension to which the masks should be resized before computing IOU values. Defaults to 640. - match_metric (str): Metric used for matching detections in slices. - "IOU" or "IOS". Defaults "IOU". + overlap_metric (OverlapMetric): Metric used for matching detections in slices. Returns: np.ndarray: A boolean array indicating which predictions to keep after @@ -236,7 +233,7 @@ def mask_non_max_merge( masks_resized = resize_masks(masks, mask_dimension) if predictions.shape[1] == 5: return group_overlapping_masks( - predictions, masks_resized, iou_threshold, match_metric + predictions, masks_resized, iou_threshold, overlap_metric ) category_ids = predictions[:, 5] @@ -247,7 +244,7 @@ def mask_non_max_merge( predictions[curr_indices], masks_resized[curr_indices], iou_threshold, - match_metric, + overlap_metric, ) for merge_class_group in merge_class_groups: @@ -264,7 +261,7 @@ def mask_non_max_merge( def box_non_max_merge( predictions: npt.NDArray[np.float64], iou_threshold: float = 0.5, - match_metric: str = "IOU", + overlap_metric: OverlapMetric = OverlapMetric.IOU, ) -> List[List[int]]: """ Apply greedy version of non-maximum merging per category to avoid detecting @@ -277,22 +274,21 @@ def box_non_max_merge( detections of different classes to be merged. iou_threshold (float): The intersection-over-union threshold to use for non-maximum suppression. Defaults to 0.5. - match_metric (str): Metric used for matching detections in slices. - "IOU" or "IOS". Defaults "IOU". + overlap_metric (OverlapMetric): Metric used for matching detections in slices. Returns: List[List[int]]: Groups of prediction indices be merged. Each group may have 1 or more elements. """ if predictions.shape[1] == 5: - return group_overlapping_boxes(predictions, iou_threshold, match_metric) + return group_overlapping_boxes(predictions, iou_threshold, overlap_metric) category_ids = predictions[:, 5] merge_groups = [] for category_id in np.unique(category_ids): curr_indices = np.where(category_ids == category_id)[0] merge_class_groups = group_overlapping_boxes( - predictions[curr_indices], iou_threshold, match_metric + predictions[curr_indices], iou_threshold, overlap_metric ) for merge_class_group in merge_class_groups: @@ -310,7 +306,7 @@ def group_overlapping_masks( predictions: npt.NDArray[np.float64], masks: npt.NDArray[np.float64], iou_threshold: float = 0.5, - match_metric: str = "IOU", + overlap_metric: OverlapMetric = OverlapMetric.IOU, ) -> List[List[int]]: """ Apply greedy version of non-maximum merging to avoid detecting too many @@ -323,8 +319,7 @@ def group_overlapping_masks( the predictions. iou_threshold (float): The intersection-over-union threshold to use for non-maximum suppression. Defaults to 0.5. - match_metric (str): Metric used for matching detections in slices. - "IOU" or "IOS". Defaults "IOU". + overlap_metric (OverlapMetric): Metric used for matching detections in slices. Returns: List[List[int]]: Groups of prediction indices be merged. @@ -348,7 +343,7 @@ def group_overlapping_masks( candidate_groups = [idx] while len(order) > 0: # 'IOU or IOS' of the calculate mask and the remaining mask - ious = mask_iou_batch(masks[order], merge_candidate, match_metric) + ious = mask_iou_batch(masks[order], merge_candidate, overlap_metric) above_threshold: np.ndarray = ious.flatten() >= iou_threshold # if no mask is above threshold, break if not above_threshold.any(): @@ -406,39 +401,3 @@ class OverlapFilter(Enum): f"Invalid value type: {type(value)}. Must be an instance of " f"{cls.__name__} or str." ) - - -class OverlapMetric(Enum): - """ - Enum specifying the metric for measuring overlap between detections. - - Attributes: - IOU: Intersection over Union. A region-overlap metric that compares - two shapes (usually bounding boxes or masks) by normalising the - shared area with the area of their union. - IOS: Intersection over Smaller, a region-overlap metric that compares - two shapes (usually bounding boxes or masks) by normalising the - shared area with the smaller of the two shapes. - """ - - IOU = "IOU" - IOS = "IOS" - - @classmethod - def list(cls): - return list(map(lambda c: c.value, cls)) - - @classmethod - def from_value(cls, value: Union[OverlapFilter, str]) -> OverlapFilter: - if isinstance(value, cls): - return value - if isinstance(value, str): - value = value.lower() - try: - return cls(value) - except ValueError: - raise ValueError(f"Invalid value: {value}. Must be one of {cls.list()}") - raise ValueError( - f"Invalid value type: {type(value)}. Must be an instance of " - f"{cls.__name__} or str." - ) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index b44cd2b6..63670144 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -1,4 +1,7 @@ +from __future__ import annotations + from itertools import chain +from enum import Enum from typing import Any, Dict, List, Optional, Tuple, Union import cv2 @@ -11,6 +14,42 @@ from supervision.geometry.core import Vector MIN_POLYGON_POINT_COUNT = 3 +class OverlapMetric(Enum): + """ + Enum specifying the metric for measuring overlap between detections. + + Attributes: + IOU: Intersection over Union. A region-overlap metric that compares + two shapes (usually bounding boxes or masks) by normalising the + shared area with the area of their union. + IOS: Intersection over Smaller, a region-overlap metric that compares + two shapes (usually bounding boxes or masks) by normalising the + shared area with the smaller of the two shapes. + """ + + IOU = "IOU" + IOS = "IOS" + + @classmethod + def list(cls): + return list(map(lambda c: c.value, cls)) + + @classmethod + def from_value(cls, value: Union[OverlapMetric, str]) -> OverlapMetric: + if isinstance(value, cls): + return value + if isinstance(value, str): + value = value.lower() + try: + return cls(value) + except ValueError: + raise ValueError(f"Invalid value: {value}. Must be one of {cls.list()}") + raise ValueError( + f"Invalid value type: {type(value)}. Must be an instance of " + f"{cls.__name__} or str." + ) + + def xyxy_to_polygons(box: np.ndarray) -> np.ndarray: """ Convert an array of boxes to an array of polygons. @@ -109,7 +148,7 @@ def box_iou( def box_iou_batch( - boxes_true: np.ndarray, boxes_detection: np.ndarray, match_metric: str = "IOU" + boxes_true: np.ndarray, boxes_detection: np.ndarray, overlap_metric: OverlapMetric = OverlapMetric.IOU ) -> np.ndarray: """ Compute Intersection over Union (IoU) of two sets of bounding boxes - @@ -126,8 +165,7 @@ def box_iou_batch( `shape = (N, 4)` where `N` is number of true objects. boxes_detection (np.ndarray): 2D `np.ndarray` representing detection boxes. `shape = (M, 4)` where `M` is number of detected objects. - match_metric (str): Metric used for matching detections in slices. - "IOU" or "IOS". Defaults "IOU". + overlap_metric (OverlapMetric): Metric used for matching detections in slices. Returns: np.ndarray: Pairwise IoU of boxes from `boxes_true` and `boxes_detection`. @@ -167,7 +205,7 @@ def box_iou_batch( area_inter = np.prod(np.clip(bottom_right - top_left, a_min=0, a_max=None), 2) - if match_metric.upper() == "IOU": + if overlap_metric == OverlapMetric.IOU: union_area = area_true[:, None] + area_detection - area_inter ious = np.divide( area_inter, @@ -175,7 +213,7 @@ def box_iou_batch( out=np.zeros_like(area_inter, dtype=float), where=union_area != 0, ) - elif match_metric.upper() == "IOS": + elif overlap_metric == OverlapMetric.IOS: small_area = np.minimum(area_true[:, None], area_detection) ious = np.divide( area_inter, @@ -185,7 +223,7 @@ def box_iou_batch( ) else: raise ValueError( - f"match_metric {match_metric} is not supported, " + f"overlap_metric {overlap_metric} is not supported, " "only 'IOU' and 'IOS' are supported" ) @@ -196,7 +234,7 @@ def box_iou_batch( def _mask_iou_batch_split( masks_true: np.ndarray, masks_detection: np.ndarray, - match_metric: str = "IOU", + overlap_metric: OverlapMetric = OverlapMetric.IOU, ) -> np.ndarray: """ Internal function. @@ -206,8 +244,7 @@ def _mask_iou_batch_split( Args: masks_true (np.ndarray): 3D `np.ndarray` representing ground-truth masks. masks_detection (np.ndarray): 3D `np.ndarray` representing detection masks. - match_metric (str): Metric used for matching detections in slices. - "IOU" or "IOS". Defaults "IOU". + overlap_metric (OverlapMetric): Metric used for matching detections in slices. Returns: np.ndarray: Pairwise IoU of masks from `masks_true` and `masks_detection`. @@ -219,7 +256,7 @@ def _mask_iou_batch_split( masks_true_area = masks_true.sum(axis=(1, 2)) # (area1, area2, ...) masks_detection_area = masks_detection.sum(axis=(1, 2)) # (area1) - if match_metric.upper() == "IOU": + if overlap_metric == OverlapMetric.IOU: union_area = masks_true_area[:, None] + masks_detection_area - intersection_area ious = np.divide( intersection_area, @@ -227,7 +264,7 @@ def _mask_iou_batch_split( out=np.zeros_like(intersection_area, dtype=float), where=union_area != 0, ) - elif match_metric.upper() == "IOS": + elif overlap_metric == OverlapMetric.IOS: # ios = intersection_area / min(area1, area2) small_area = np.minimum(masks_true_area[:, None], masks_detection_area) ious = np.divide( @@ -238,7 +275,7 @@ def _mask_iou_batch_split( ) else: raise ValueError( - f"match_metric {match_metric} is not supported, " + f"overlap_metric {overlap_metric} is not supported, " "only 'IOU' and 'IOS' are supported" ) @@ -249,7 +286,7 @@ def _mask_iou_batch_split( def mask_iou_batch( masks_true: np.ndarray, masks_detection: np.ndarray, - match_metric: str = "IOU", + overlap_metric: OverlapMetric = OverlapMetric.IOU, memory_limit: int = 1024 * 5, ) -> np.ndarray: """ @@ -259,8 +296,7 @@ def mask_iou_batch( Args: masks_true (np.ndarray): 3D `np.ndarray` representing ground-truth masks. masks_detection (np.ndarray): 3D `np.ndarray` representing detection masks. - match_metric (str): Metric used for matching detections in slices. - "IOU" or "IOS". Defaults "IOU". + overlap_metric (OverlapMetric): Metric used for matching detections in slices. memory_limit (int): memory limit in MB, default is 1024 * 5 MB (5GB). Returns: @@ -275,7 +311,7 @@ def mask_iou_batch( / 1024 ) if memory <= memory_limit: - return _mask_iou_batch_split(masks_true, masks_detection, match_metric) + return _mask_iou_batch_split(masks_true, masks_detection, overlap_metric) ious = [] step = max( @@ -292,7 +328,7 @@ def mask_iou_batch( for i in range(0, masks_true.shape[0], step): ious.append( _mask_iou_batch_split( - masks_true[i : i + step], masks_detection, match_metric + masks_true[i : i + step], masks_detection, overlap_metric ) ) diff --git a/test/detection/test_overlap_filter.py b/test/detection/test_overlap_filter.py index 08cd870f..f628c30f 100644 --- a/test/detection/test_overlap_filter.py +++ b/test/detection/test_overlap_filter.py @@ -7,7 +7,6 @@ import pytest from supervision.detection.overlap_filter import ( box_non_max_suppression, group_overlapping_boxes, - mask_non_max_merge, mask_non_max_suppression, ) @@ -448,174 +447,3 @@ def test_mask_non_max_suppression( predictions=predictions, masks=masks, iou_threshold=iou_threshold ) assert np.array_equal(result, expected_result) - - -@pytest.mark.parametrize( - "predictions, masks, iou_threshold, expected_result, exception", - [ - ( - np.empty((0, 6)), - np.empty((0, 5, 5)), - 0.5, - [], - DoesNotRaise(), - ), # empty predictions and masks - ( - np.array([[0, 0, 0, 0, 0.8]]), - np.array( - [ - [ - [False, False, False, False, False], - [False, True, True, True, False], - [False, True, True, True, False], - [False, True, True, True, False], - [False, False, False, False, False], - ] - ] - ), - 0.5, - [[0]], - DoesNotRaise(), - ), # single mask with no category - ( - np.array([[0, 0, 0, 0, 0.8, 0]]), - np.array( - [ - [ - [False, False, False, False, False], - [False, True, True, True, False], - [False, True, True, True, False], - [False, True, True, True, False], - [False, False, False, False, False], - ] - ] - ), - 0.5, - [[0]], - DoesNotRaise(), - ), # single mask with category - ( - np.array([[0, 0, 0, 0, 0.9], [0, 0, 0, 0, 0.8]]), - np.array( - [ - [ - [False, False, False, False, False], - [False, True, True, False, False], - [False, True, True, False, False], - [False, False, False, False, False], - [False, False, False, False, False], - ], - [ - [False, False, False, False, False], - [False, False, False, False, False], - [False, False, False, True, True], - [False, False, False, True, True], - [False, False, False, False, False], - ], - ] - ), - 0.5, - [[0], [1]], - DoesNotRaise(), - ), # two masks non-overlapping with no category - ( - np.array([[0, 0, 0, 0, 0.9], [0, 0, 0, 0, 0.8]]), - np.array( - [ - [ - [False, False, False, False, False], - [False, True, True, True, False], - [False, True, True, True, False], - [False, True, True, True, False], - [False, False, False, False, False], - ], - [ - [False, False, False, False, False], - [False, False, True, True, True], - [False, False, True, True, True], - [False, False, True, True, True], - [False, False, False, False, False], - ], - ] - ), - 0.2, - [[0, 1]], - DoesNotRaise(), - ), # two masks partially overlapping with no category - ( - np.array([[0, 0, 0, 0, 0.9, 0], [0, 0, 0, 0, 0.8, 1]]), - np.array( - [ - [ - [False, False, False, False, False], - [False, True, True, True, False], - [False, True, True, True, False], - [False, True, True, True, False], - [False, False, False, False, False], - ], - [ - [False, False, False, False, False], - [False, False, True, True, True], - [False, False, True, True, True], - [False, False, True, True, True], - [False, False, False, False, False], - ], - ] - ), - 0.2, - [[0], [1]], - DoesNotRaise(), - ), # two masks partially overlapping with different category - ( - np.array( - [ - [0, 0, 0, 0, 0.9, 0], - [0, 0, 0, 0, 0.8, 0], - [0, 0, 0, 0, 0.85, 1], - ] - ), - np.array( - [ - [ # mask 0, class 0 - [False, False, False, False, False], - [False, True, True, True, False], - [False, True, True, True, False], - [False, True, True, True, False], - [False, False, False, False, False], - ], - [ # mask 1, class 0 - [False, False, False, False, False], - [False, False, True, True, True], - [False, False, True, True, True], - [False, False, True, True, True], - [False, False, False, False, False], - ], - [ # mask 2, class 1 - [False, False, False, False, False], - [False, False, False, True, True], - [False, False, False, True, True], - [False, False, False, False, False], - [False, False, False, False, False], - ], - ] - ), - 0.2, - [[0, 1], [2]], - DoesNotRaise(), - ), # three masks, two overlapping with same class - ], -) -def test_mask_non_max_merge( - predictions: np.ndarray, - masks: np.ndarray, - iou_threshold: float, - expected_result: List[List[int]], - exception: Exception, -) -> None: - with exception: - result = mask_non_max_merge( - predictions=predictions, masks=masks, iou_threshold=iou_threshold - ) - result = sorted([sorted(group) for group in result]) - expected_result = sorted([sorted(group) for group in expected_result]) - assert result == expected_result From ae77077853c46f983b4e93cafc518ced9c27653a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 10:04:55 +0000 Subject: [PATCH 19/24] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto?= =?UTF-8?q?=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/detection/core.py | 2 +- supervision/detection/overlap_filter.py | 7 ++++--- supervision/detection/utils.py | 6 ++++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 3b343b46..cdaf00d0 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -23,6 +23,7 @@ from supervision.detection.tools.transformers import ( process_transformers_v5_segmentation_result, ) from supervision.detection.utils import ( + OverlapMetric, box_iou_batch, calculate_masks_centroids, extract_ultralytics_masks, @@ -35,7 +36,6 @@ from supervision.detection.utils import ( merge_metadata, process_roboflow_result, xywh_to_xyxy, - OverlapMetric, ) from supervision.detection.vlm import ( LMM, diff --git a/supervision/detection/overlap_filter.py b/supervision/detection/overlap_filter.py index 34bfb024..47f5876b 100644 --- a/supervision/detection/overlap_filter.py +++ b/supervision/detection/overlap_filter.py @@ -6,8 +6,7 @@ from typing import List, Union import numpy as np import numpy.typing as npt -from supervision.detection.utils import box_iou_batch, mask_iou_batch -from supervision.detection.utils import OverlapMetric +from supervision.detection.utils import OverlapMetric, box_iou_batch, mask_iou_batch def resize_masks(masks: np.ndarray, max_dimension: int = 640) -> np.ndarray: @@ -97,7 +96,9 @@ def mask_non_max_suppression( def box_non_max_suppression( - predictions: np.ndarray, iou_threshold: float = 0.5, overlap_metric: OverlapMetric = OverlapMetric.IOU + predictions: np.ndarray, + iou_threshold: float = 0.5, + overlap_metric: OverlapMetric = OverlapMetric.IOU, ) -> np.ndarray: """ Perform Non-Maximum Suppression (NMS) on object detection predictions. diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 63670144..e06979ba 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -1,7 +1,7 @@ from __future__ import annotations -from itertools import chain from enum import Enum +from itertools import chain from typing import Any, Dict, List, Optional, Tuple, Union import cv2 @@ -148,7 +148,9 @@ def box_iou( def box_iou_batch( - boxes_true: np.ndarray, boxes_detection: np.ndarray, overlap_metric: OverlapMetric = OverlapMetric.IOU + boxes_true: np.ndarray, + boxes_detection: np.ndarray, + overlap_metric: OverlapMetric = OverlapMetric.IOU, ) -> np.ndarray: """ Compute Intersection over Union (IoU) of two sets of bounding boxes - From 4977fd64b145d2af540445c27902a1697a528262 Mon Sep 17 00:00:00 2001 From: soumik12345 <19soumik.rakshit96@gmail.com> Date: Tue, 15 Jul 2025 15:43:44 +0530 Subject: [PATCH 20/24] add: test_mask_non_max_merge --- test/detection/test_overlap_filter.py | 183 ++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) diff --git a/test/detection/test_overlap_filter.py b/test/detection/test_overlap_filter.py index f628c30f..8968780b 100644 --- a/test/detection/test_overlap_filter.py +++ b/test/detection/test_overlap_filter.py @@ -8,6 +8,7 @@ from supervision.detection.overlap_filter import ( box_non_max_suppression, group_overlapping_boxes, mask_non_max_suppression, + mask_non_max_merge, ) @@ -447,3 +448,185 @@ def test_mask_non_max_suppression( predictions=predictions, masks=masks, iou_threshold=iou_threshold ) assert np.array_equal(result, expected_result) + + +@pytest.mark.parametrize( + "predictions, masks, iou_threshold, expected_result, exception", + [ + ( + np.empty((0, 6)), + np.empty((0, 5, 5)), + 0.5, + [], + DoesNotRaise(), + ), # empty predictions and masks + ( + np.array([[0, 0, 0, 0, 0.8]]), + np.array( + [ + [ + [False, False, False, False, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, False, False, False, False], + ] + ] + ), + 0.5, + [[0]], + DoesNotRaise(), + ), # single mask with no category + ( + np.array([[0, 0, 0, 0, 0.8, 0]]), + np.array( + [ + [ + [False, False, False, False, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, False, False, False, False], + ] + ] + ), + 0.5, + [[0]], + DoesNotRaise(), + ), # single mask with category + ( + np.array([[0, 0, 0, 0, 0.8], [0, 0, 0, 0, 0.9]]), + np.array( + [ + [ + [False, False, False, False, False], + [False, True, True, False, False], + [False, True, True, False, False], + [False, False, False, False, False], + [False, False, False, False, False], + ], + [ + [False, False, False, False, False], + [False, False, False, False, False], + [False, False, False, True, True], + [False, False, False, True, True], + [False, False, False, False, False], + ], + ] + ), + 0.5, + [[0], [1]], + DoesNotRaise(), + ), # two masks non-overlapping with no category + ( + np.array([[0, 0, 0, 0, 0.8], [0, 0, 0, 0, 0.9]]), + np.array( + [ + [ + [False, False, False, False, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, False, False, False, False], + ], + [ + [False, False, False, False, False], + [False, False, True, True, True], + [False, False, True, True, True], + [False, False, True, True, True], + [False, False, False, False, False], + ], + ] + ), + 0.4, + [[0, 1]], + DoesNotRaise(), + ), # two masks partially overlapping with no category, merge + ( + np.array([[0, 0, 0, 0, 0.8], [0, 0, 0, 0, 0.9]]), + np.array( + [ + [ + [False, False, False, False, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, False, False, False, False], + ], + [ + [False, False, False, False, False], + [False, False, True, True, True], + [False, False, True, True, True], + [False, False, True, True, True], + [False, False, False, False, False], + ], + ] + ), + 0.6, + [[0, 1]], + DoesNotRaise(), + ), # two masks partially overlapping with no category, no merge + ( + np.array([[0, 0, 0, 0, 0.8, 0], [0, 0, 0, 0, 0.9, 1]]), + np.array( + [ + [ + [False, False, False, False, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, False, False, False, False], + ], + [ + [False, False, False, False, False], + [False, False, True, True, True], + [False, False, True, True, True], + [False, False, True, True, True], + [False, False, False, False, False], + ], + ] + ), + 0.4, + [[0], [1]], + DoesNotRaise(), + ), # two masks partially overlapping with different categories + ( + np.array([[0, 0, 0, 0, 0.8, 0], [0, 0, 0, 0, 0.9, 0]]), + np.array( + [ + [ + [False, False, False, False, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, True, True, True, False], + [False, False, False, False, False], + ], + [ + [False, False, False, False, False], + [False, False, True, True, True], + [False, False, True, True, True], + [False, False, True, True, True], + [False, False, False, False, False], + ], + ] + ), + 0.4, + [[0, 1]], + DoesNotRaise(), + ), # two masks partially overlapping with same category + ], +) +def test_mask_non_max_merge( + predictions: np.ndarray, + masks: np.ndarray, + iou_threshold: float, + expected_result: List[List[int]], + exception: Exception, +) -> None: + with exception: + result = mask_non_max_merge( + predictions=predictions, masks=masks, iou_threshold=iou_threshold + ) + sorted_result = sorted([sorted(group) for group in result]) + sorted_expected_result = sorted([sorted(group) for group in expected_result]) + assert sorted_result == sorted_expected_result From 9a38ebd15e591b1addcfa528492f5ff1320746b0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 10:14:03 +0000 Subject: [PATCH 21/24] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto?= =?UTF-8?q?=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/detection/test_overlap_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/detection/test_overlap_filter.py b/test/detection/test_overlap_filter.py index 8968780b..b956b25b 100644 --- a/test/detection/test_overlap_filter.py +++ b/test/detection/test_overlap_filter.py @@ -7,8 +7,8 @@ import pytest from supervision.detection.overlap_filter import ( box_non_max_suppression, group_overlapping_boxes, - mask_non_max_suppression, mask_non_max_merge, + mask_non_max_suppression, ) From 7ea8c2e43c466c49e905acb6f77aa10c73c56279 Mon Sep 17 00:00:00 2001 From: soumik12345 <19soumik.rakshit96@gmail.com> Date: Tue, 15 Jul 2025 15:46:10 +0530 Subject: [PATCH 22/24] chore: remove excessive comments in group_overlapping_masks --- supervision/detection/overlap_filter.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/supervision/detection/overlap_filter.py b/supervision/detection/overlap_filter.py index 47f5876b..43333031 100644 --- a/supervision/detection/overlap_filter.py +++ b/supervision/detection/overlap_filter.py @@ -339,27 +339,20 @@ def group_overlapping_masks( merge_groups.append([idx]) break - # calculate mask merge_candidate = masks[idx][None, ...] candidate_groups = [idx] while len(order) > 0: - # 'IOU or IOS' of the calculate mask and the remaining mask ious = mask_iou_batch(masks[order], merge_candidate, overlap_metric) above_threshold: np.ndarray = ious.flatten() >= iou_threshold - # if no mask is above threshold, break if not above_threshold.any(): break - # get indexes that meet the threshold above_idx = order[above_threshold] - # update merge_candidate merge_candidate = np.logical_or.reduce( np.concatenate([masks[above_idx], merge_candidate]), axis=0, keepdims=True, ) - # add indexes that meet the criteria to the candidate_groups candidate_groups.extend(np.flip(above_idx).tolist()) - # update order, masks order = order[~above_threshold] merge_groups.append(candidate_groups) From c11a7de9499a4d5f7c3c40d9628413c3b83441b8 Mon Sep 17 00:00:00 2001 From: soumik12345 <19soumik.rakshit96@gmail.com> Date: Tue, 15 Jul 2025 15:58:26 +0530 Subject: [PATCH 23/24] chore: refactor docs for --- docs/detection/double_detection_filter.md | 6 ------ docs/detection/utils.md | 6 ++++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/detection/double_detection_filter.md b/docs/detection/double_detection_filter.md index fbe944cf..24384a13 100644 --- a/docs/detection/double_detection_filter.md +++ b/docs/detection/double_detection_filter.md @@ -10,12 +10,6 @@ comments: true :::supervision.detection.overlap_filter.OverlapFilter -
-

OverlapMetric

-
- -:::supervision.detection.overlap_filter.OverlapMetric -

box_non_max_suppression

diff --git a/docs/detection/utils.md b/docs/detection/utils.md index 5cef5d74..cd18be64 100644 --- a/docs/detection/utils.md +++ b/docs/detection/utils.md @@ -5,6 +5,12 @@ status: new # Detection Utils +
+

OverlapMetric

+
+ +:::supervision.detection.overlap_filter.OverlapMetric +

box_iou

From 84c9a34f924a825ebee9c16c88c867dcfb09dec4 Mon Sep 17 00:00:00 2001 From: soumik12345 <19soumik.rakshit96@gmail.com> Date: Tue, 15 Jul 2025 15:58:38 +0530 Subject: [PATCH 24/24] update: docstring --- supervision/detection/core.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index cdaf00d0..a74c2669 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -1586,8 +1586,8 @@ class Detections: class_agnostic (bool): Whether to perform class-agnostic non-maximum suppression. If True, the class_id of each detection will be ignored. Defaults to False. - overlap_metric (OverlapMetric): Metric used for matching detections - in slices. + overlap_metric (OverlapMetric): Metric used for measuring overlap between + detections in slices. Returns: Detections: A new Detections object containing the subset of detections @@ -1624,13 +1624,13 @@ class Detections: predictions=predictions, masks=self.mask, iou_threshold=threshold, - match_metric=overlap_metric, + overlap_metric=overlap_metric, ) else: indices = box_non_max_suppression( predictions=predictions, iou_threshold=threshold, - match_metric=overlap_metric, + overlap_metric=overlap_metric, ) return self[indices] @@ -1650,8 +1650,8 @@ class Detections: class_agnostic (bool): Whether to perform class-agnostic non-maximum merging. If True, the class_id of each detection will be ignored. Defaults to False. - overlap_metric (OverlapMetric): Metric used for matching detections in slices. - "IOU" or "IOS". Defaults "IOU". + overlap_metric (OverlapMetric): Metric used for measuring overlap between + detections in slices. Returns: Detections: A new Detections object containing the subset of detections @@ -1690,13 +1690,13 @@ class Detections: predictions=predictions, masks=self.mask, iou_threshold=threshold, - match_metric=overlap_metric, + overlap_metric=overlap_metric, ) else: merge_groups = box_non_max_merge( predictions=predictions, iou_threshold=threshold, - match_metric=overlap_metric, + overlap_metric=overlap_metric, ) result = [] @@ -1803,7 +1803,9 @@ def merge_inner_detection_object_pair( def merge_inner_detections_objects( - detections: List[Detections], threshold=0.5, match_metric: str = "IOU" + detections: List[Detections], + threshold=0.5, + overlap_metric: OverlapMetric = OverlapMetric.IOU, ) -> Detections: """ Given N detections each of length 1 (exactly one object inside), combine them into a @@ -1816,9 +1818,11 @@ def merge_inner_detections_objects( detections_1 = detections[0] for detections_2 in detections[1:]: if detections_1.mask is not None and detections_2.mask is not None: - iou = mask_iou_batch(detections_1.mask, detections_2.mask, match_metric)[0] + iou = mask_iou_batch(detections_1.mask, detections_2.mask, overlap_metric)[ + 0 + ] else: - iou = box_iou_batch(detections_1.xyxy, detections_2.xyxy, match_metric)[0] + iou = box_iou_batch(detections_1.xyxy, detections_2.xyxy, overlap_metric)[0] if iou < threshold: break detections_1 = merge_inner_detection_object_pair(detections_1, detections_2)