From c78ae33e43c95e067e2ae34ff9e7616fe696cac3 Mon Sep 17 00:00:00 2001 From: mario-dg Date: Fri, 13 Oct 2023 18:24:22 +0200 Subject: [PATCH 001/182] =?UTF-8?q?feat:=20=F0=9F=9A=80=20Added=20Non-Maxi?= =?UTF-8?q?mum=20Merging=20to=20Detections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/detection/core.py | 107 ++++++++++ .../detection/tools/inference_slicer.py | 17 +- supervision/detection/utils.py | 190 +++++++++++++++++- 3 files changed, 310 insertions(+), 4 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 77bfca9d..006bc6e7 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -6,7 +6,15 @@ from typing import Any, Iterator, List, Optional, Tuple, Union import numpy as np from supervision.detection.utils import ( + batched_greedy_nmm, + box_iou_batch, extract_ultralytics_masks, + get_merged_bbox, + get_merged_class_id, + get_merged_confidence, + get_merged_mask, + get_merged_tracker_id, + greedy_nmm, non_max_suppression, process_roboflow_result, xywh_to_xyxy, @@ -729,6 +737,105 @@ class Detections: """ return (self.xyxy[:, 3] - self.xyxy[:, 1]) * (self.xyxy[:, 2] - self.xyxy[:, 0]) + def with_nmm( + self, threshold: float = 0.5, class_agnostic: bool = False + ) -> Detections: + """ + Perform non-maximum merging on the current set of object detections. + + Args: + threshold (float, optional): The intersection-over-union threshold + to use for non-maximum merging. Defaults to 0.5. + class_agnostic (bool, optional): Whether to perform class-agnostic + non-maximum merging. If True, the class_id of each detection + will be ignored. Defaults to False. + + Returns: + Detections: A new Detections object containing the subset of detections + after non-maximum merging. + + Raises: + AssertionError: If `confidence` is None and class_agnostic is False. + If `class_id` is None and class_agnostic is False. + """ + if len(self) == 0: + return self + + assert ( + self.confidence is not None + ), "Detections confidence must be given for NMM to be executed." + + if class_agnostic: + predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1))) + keep_to_merge_list = greedy_nmm(predictions, threshold) + else: + predictions = np.hstack( + ( + self.xyxy, + self.confidence.reshape(-1, 1), + self.class_id.reshape(-1, 1), + ) + ) + keep_to_merge_list = batched_greedy_nmm(predictions, threshold) + + result = [] + + for keep_ind, merge_ind_list in keep_to_merge_list.items(): + for merge_ind in merge_ind_list: + if ( + box_iou_batch(self[keep_ind].xyxy, self[merge_ind].xyxy).item() + > threshold + ): + self[keep_ind].xyxy = np.vstack( + ( + self[keep_ind].xyxy, + get_merged_bbox(self.xyxy[keep_ind], self.xyxy[merge_ind]), + ) + ) + self[keep_ind].class_id = np.hstack( + ( + self[keep_ind].class_id, + get_merged_class_id( + self.class_id[keep_ind].item(), + self.class_id[merge_ind].item(), + ), + ) + ) + self[keep_ind].confidence = np.hstack( + ( + self[keep_ind].confidence, + get_merged_confidence( + self.confidence[keep_ind].item(), + self.confidence[merge_ind].item(), + ), + ) + ) + if self.mask is not None: + merged_mask = get_merged_mask( + self.mask[keep_ind], self.mask[merge_ind] + ) + if self[keep_ind].mask is None: + self[keep_ind].mask = np.array([merged_mask]) + else: + self[keep_ind].mask = np.vstack( + (self[keep_ind].mask, merged_mask[np.newaxis]) + ) + if self.tracker_id is not None: + merged_tracker_id = get_merged_tracker_id( + self.tracker_id[keep_ind].item(), + self.tracker_id[merge_ind].item(), + ) + if self[keep_ind].tracker_id is None: + self[keep_ind].tracker_id = np.array( + [merged_tracker_id], dtype=int + ) + else: + self[keep_ind].tracker_id = np.hstack( + (self[keep_ind].tracker_id, merged_tracker_id) + ) + result.append(self[keep_ind]) + return Detections.merge(result) + def with_nms( self, threshold: float = 0.5, class_agnostic: bool = False ) -> Detections: diff --git a/supervision/detection/tools/inference_slicer.py b/supervision/detection/tools/inference_slicer.py index 5f6fb391..2098c79c 100644 --- a/supervision/detection/tools/inference_slicer.py +++ b/supervision/detection/tools/inference_slicer.py @@ -36,6 +36,10 @@ class InferenceSlicer: slices in the format `(width_ratio, height_ratio)`. iou_threshold (Optional[float]): Intersection over Union (IoU) threshold used for non-max suppression. + merge_detections (Optional[bool]): Whether to merge the detection from all + slices or simply concatenate them. If `True`, Non-Maximum Merging (NMM), + otherwise Non-Maximum Suppression (NMS), + is applied to the final detections. callback (Callable): A function that performs inference on a given image slice and returns detections. thread_workers (int): Number of threads for parallel execution. @@ -53,11 +57,13 @@ class InferenceSlicer: slice_wh: Tuple[int, int] = (320, 320), overlap_ratio_wh: Tuple[float, float] = (0.2, 0.2), iou_threshold: Optional[float] = 0.5, + merge_detections: Optional[bool] = False, thread_workers: int = 1, ): self.slice_wh = slice_wh self.overlap_ratio_wh = overlap_ratio_wh self.iou_threshold = iou_threshold + self.merge_detections = merge_detections self.callback = callback self.thread_workers = thread_workers validate_inference_callback(callback=callback) @@ -109,9 +115,14 @@ class InferenceSlicer: for future in as_completed(futures): detections_list.append(future.result()) - return Detections.merge(detections_list=detections_list).with_nms( - threshold=self.iou_threshold - ) + if self.merge_detections: + return Detections.merge(detections_list=detections_list).with_nmm( + threshold=self.iou_threshold + ) + else: + return Detections.merge(detections_list=detections_list).with_nms( + threshold=self.iou_threshold + ) def _run_callback(self, image, offset) -> Detections: """ diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 7a5eb546..b0414eb4 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Tuple +from typing import Dict, List, Optional, Tuple import cv2 import numpy as np @@ -110,6 +110,194 @@ def non_max_suppression( return keep[sort_index.argsort()] +def greedy_nmm(predictions: np.ndarray, threshold: float = 0.5) -> Dict[int, List[int]]: + """ + Apply greedy version of non-maximum merging to avoid detecting too many + overlapping bounding boxes for a given object. + + Args: + predictions (np.ndarray): An array of shape `(n, 5)` containing + the bounding boxes coordinates in format `[x1, y1, x2, y2]` + and the confidence scores. + threshold (float, optional): The intersection-over-union threshold + to use for non-maximum suppression. Defaults to 0.5. + + Returns: + Dict[int, List[int]]: Mapping from prediction indices + to keep to a list of prediction indices to be merged. + """ + keep_to_merge_list = {} + + x1 = predictions[:, 0] + y1 = predictions[:, 1] + x2 = predictions[:, 2] + y2 = predictions[:, 3] + + scores = predictions[:, 4] + + areas = (x2 - x1) * (y2 - y1) + + order = scores.argsort() + + keep = [] + + while len(order) > 0: + idx = order[-1] + + keep.append(idx.tolist()) + + order = order[:-1] + + if len(order) == 0: + keep_to_merge_list[idx.tolist()] = [] + break + + xx1 = np.take(x1, axis=0, indices=order) + xx2 = np.take(x2, axis=0, indices=order) + yy1 = np.take(y1, axis=0, indices=order) + yy2 = np.take(y2, axis=0, indices=order) + + xx1 = np.maximum(xx1, x1[idx]) + yy1 = np.maximum(yy1, y1[idx]) + xx2 = np.minimum(xx2, x2[idx]) + yy2 = np.minimum(yy2, y2[idx]) + + w = np.maximum(0.0, xx2 - xx1) + h = np.maximum(0.0, yy2 - yy1) + + inter = w * h + + rem_areas = np.take(areas, axis=0, indices=order) + + union = (rem_areas - inter) + areas[idx] + match_metric_value = inter / union + + mask = match_metric_value < threshold + mask = mask.astype(np.uint8) + matched_box_indices = np.flip(order[np.where(mask == 0)[0]]) + unmatched_indices = order[np.where(mask == 1)[0]] + + order = unmatched_indices[scores[unmatched_indices].argsort()] + + keep_to_merge_list[idx.tolist()] = [] + + for matched_box_ind in matched_box_indices.tolist(): + keep_to_merge_list[idx.tolist()].append(matched_box_ind) + + return keep_to_merge_list + + +def batched_greedy_nmm( + predictions: np.ndarray, threshold: float = 0.5 +) -> Dict[int, List[int]]: + """ + Apply greedy version of non-maximum merging per category to avoid detecting + too many overlapping bounding boxes for a given object. + + Args: + predictions (np.ndarray): An array of shape `(n, 6)` containing + the bounding boxes coordinates in format `[x1, y1, x2, y2]`, + the confidence scores and class_ids. + threshold (float, optional): The intersection-over-union threshold + to use for non-maximum suppression. Defaults to 0.5. + + Returns: + Dict[int, List[int]]: Mapping from prediction indices + to keep to a list of prediction indices to be merged. + """ + category_ids = predictions[:, 5] + keep_to_merge_list = {} + for category_id in np.unique(category_ids): + curr_indices = np.where(category_ids == category_id)[0] + curr_keep_to_merge_list = greedy_nmm(predictions[curr_indices], threshold) + curr_indices_list = curr_indices.tolist() + for curr_keep, curr_merge_list in curr_keep_to_merge_list.items(): + keep = curr_indices_list[curr_keep] + merge_list = [curr_indices_list[i] for i in curr_merge_list] + keep_to_merge_list[keep] = merge_list + return keep_to_merge_list + + +def get_merged_bbox(bbox1: np.ndarray, bbox2: np.ndarray) -> np.ndarray: + """ + Merges two bounding boxes into one. + + Args: + bbox1 (np.ndarray): A numpy array of shape `(, 4)` where the + row corresponds to a bounding box in + the format `(x_min, y_min, x_max, y_max)`. + bbox2 (np.ndarray): A numpy array of shape `(, 4)` where the + row corresponds to a bounding box in + the format `(x_min, y_min, x_max, y_max)`. + + Returns: + np.ndarray: A numpy array of shape `(, 4)` where the new + bounding box is the merged bounding box of `bbox1` and `bbox2`. + """ + left_top = np.minimum(bbox1[:2], bbox2[:2]) + right_bottom = np.maximum(bbox1[2:], bbox2[2:]) + return np.concatenate([left_top, right_bottom]) + + +def get_merged_class_id(id1: int, id2: int) -> int: + """ + Merges two class ids into one. + + Args: + id1 (int): The first class id. + id2 (int): The second class id. + + Returns: + int: The merged class id. + """ + return max(id1, id2) + + +def get_merged_confidence(confidence1: float, confidence2: float) -> float: + """ + Merges two confidences into one. + + Args: + confidence1 (float): The first confidence. + confidence2 (float): The second confidence. + + Returns: + float: The merged confidence. + """ + return max(confidence1, confidence2) + + +def get_merged_mask(mask1: np.ndarray, mask2: np.ndarray) -> np.ndarray: + """ + Merges two masks into one. + + Args: + mask1 (np.ndarray): A numpy array of shape `(H, W)` where `H` and `W` + are the height and width of the mask, respectively. + mask2 (np.ndarray): A numpy array of shape `(H, W)` where `H` and `W` + are the height and width of the mask, respectively. + + Returns: + np.ndarray: A numpy array of shape `(H, W)` where the new mask is the + merged mask of `mask1` and `mask2`. + """ + return np.logical_or(mask1, mask2) + + +def get_merged_tracker_id(tracker_id1: int, tracker_id2: int) -> int: + """ + Merges two tracker ids into one. + + Args: + tracker_id1 (int): The first tracker id. + tracker_id2 (int): The second tracker id. + + Returns: + int: The merged tracker id. + """ + return max(tracker_id1, tracker_id2) + + def clip_boxes( boxes_xyxy: np.ndarray, frame_resolution_wh: Tuple[int, int] ) -> np.ndarray: From 57b12e6e00069d9064df783eaac40d230c4626bd Mon Sep 17 00:00:00 2001 From: mario-dg Date: Thu, 19 Oct 2023 00:03:36 +0200 Subject: [PATCH 002/182] Added __setitem__ to Detections and refactored the object prediction merging --- supervision/detection/core.py | 104 +++++++++++++++++++--------------- 1 file changed, 58 insertions(+), 46 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 006bc6e7..bd729a96 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -67,6 +67,27 @@ def _validate_tracker_id(tracker_id: Any, n: int) -> None: raise ValueError("tracker_id must be None or 1d np.ndarray with (n,) shape") +def _merge_object_detection_pair(pred1: Detections, pred2: Detections) -> Detections: + merged_bbox = get_merged_bbox(pred1.xyxy, pred2.xyxy) + merged_conf = get_merged_confidence(pred1.confidence, pred2.confidence) + merged_class_id = get_merged_class_id(pred1.class_id, pred2.class_id) + merged_tracker_id = None + merged_mask = None + + if pred1.mask and pred2.mask: + merged_mask = get_merged_mask(pred1.mask, pred2.mask) + if pred1.tracker_id and pred2.tracker_id: + merged_tracker_id = get_merged_tracker_id(pred1.tracker_id, pred2.tracker_id) + + return Detections( + xyxy=merged_bbox, + mask=merged_mask, + confidence=merged_conf, + class_id=merged_class_id, + tracker_id=merged_tracker_id, + ) + + @dataclass class Detections: """ @@ -668,6 +689,38 @@ class Detections: raise ValueError(f"{anchor} is not supported.") + def __setitem__( + self, index: Union[int, slice, List[int], np.ndarray], value: Detections + ) -> None: + """ + Set a subset of the Detections object. + + Args: + index (Union[int, slice, List[int], np.ndarray]): + The index or indices of the subset of the Detections + value (Detections): The new value of the subset of the Detections + + Example: + ```python + >>> import supervision as sv + + >>> detections = sv.Detections(...) + + >>> detections[0] = sv.Detections(...) + ``` + """ + if isinstance(index, int): + index = [index] + self.xyxy[index] = value.xyxy + if self.mask is not None: + self.mask[index] = value.mask + if self.confidence is not None: + self.confidence[index] = value.confidence + if self.class_id is not None: + self.class_id[index] = value.class_id + if self.tracker_id is not None: + self.tracker_id[index] = value.tracker_id + def __getitem__( self, index: Union[int, slice, List[int], np.ndarray] ) -> Detections: @@ -761,6 +814,8 @@ class Detections: if len(self) == 0: return self + assert 0.0 <= threshold <= 1.0, "Threshold must be between 0 and 1." + assert ( self.confidence is not None ), "Detections confidence must be given for NMM to be executed." @@ -786,54 +841,11 @@ class Detections: box_iou_batch(self[keep_ind].xyxy, self[merge_ind].xyxy).item() > threshold ): - self[keep_ind].xyxy = np.vstack( - ( - self[keep_ind].xyxy, - get_merged_bbox(self.xyxy[keep_ind], self.xyxy[merge_ind]), - ) + self[keep_ind] = _merge_object_detection_pair( + self[keep_ind], self[merge_ind] ) - self[keep_ind].class_id = np.hstack( - ( - self[keep_ind].class_id, - get_merged_class_id( - self.class_id[keep_ind].item(), - self.class_id[merge_ind].item(), - ), - ) - ) - self[keep_ind].confidence = np.hstack( - ( - self[keep_ind].confidence, - get_merged_confidence( - self.confidence[keep_ind].item(), - self.confidence[merge_ind].item(), - ), - ) - ) - if self.mask is not None: - merged_mask = get_merged_mask( - self.mask[keep_ind], self.mask[merge_ind] - ) - if self[keep_ind].mask is None: - self[keep_ind].mask = np.array([merged_mask]) - else: - self[keep_ind].mask = np.vstack( - (self[keep_ind].mask, merged_mask[np.newaxis]) - ) - if self.tracker_id is not None: - merged_tracker_id = get_merged_tracker_id( - self.tracker_id[keep_ind].item(), - self.tracker_id[merge_ind].item(), - ) - if self[keep_ind].tracker_id is None: - self[keep_ind].tracker_id = np.array( - [merged_tracker_id], dtype=int - ) - else: - self[keep_ind].tracker_id = np.hstack( - (self[keep_ind].tracker_id, merged_tracker_id) - ) result.append(self[keep_ind]) + return Detections.merge(result) def with_nms( From 9f222736e129df769a9771bda12eb235795e0801 Mon Sep 17 00:00:00 2001 From: mario-dg Date: Thu, 19 Oct 2023 00:05:05 +0200 Subject: [PATCH 003/182] Added standard full image inference after sliced inference to increase large object detection accuracy --- supervision/detection/tools/inference_slicer.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/supervision/detection/tools/inference_slicer.py b/supervision/detection/tools/inference_slicer.py index 2098c79c..c0a30ff6 100644 --- a/supervision/detection/tools/inference_slicer.py +++ b/supervision/detection/tools/inference_slicer.py @@ -38,8 +38,10 @@ class InferenceSlicer: used for non-max suppression. merge_detections (Optional[bool]): Whether to merge the detection from all slices or simply concatenate them. If `True`, Non-Maximum Merging (NMM), - otherwise Non-Maximum Suppression (NMS), - is applied to the final detections. + otherwise Non-Maximum Suppression (NMS), is applied to the detections. + perform_standard_pred (Optional[bool]): Whether to perform inference on the + whole image in addition to the slices to increase the accuracy of + large object detection. callback (Callable): A function that performs inference on a given image slice and returns detections. thread_workers (int): Number of threads for parallel execution. @@ -58,12 +60,14 @@ class InferenceSlicer: overlap_ratio_wh: Tuple[float, float] = (0.2, 0.2), iou_threshold: Optional[float] = 0.5, merge_detections: Optional[bool] = False, + perform_standard_pred: Optional[bool] = False, thread_workers: int = 1, ): self.slice_wh = slice_wh self.overlap_ratio_wh = overlap_ratio_wh self.iou_threshold = iou_threshold self.merge_detections = merge_detections + self.perform_standard_pred = perform_standard_pred self.callback = callback self.thread_workers = thread_workers validate_inference_callback(callback=callback) @@ -115,6 +119,9 @@ class InferenceSlicer: for future in as_completed(futures): detections_list.append(future.result()) + if self.perform_standard_pred: + detections_list.append(self.callback(image)) + if self.merge_detections: return Detections.merge(detections_list=detections_list).with_nmm( threshold=self.iou_threshold From 6f4704625b16ba69068b3a19f6d55bc21c80c434 Mon Sep 17 00:00:00 2001 From: mario-dg Date: Thu, 19 Oct 2023 00:05:42 +0200 Subject: [PATCH 004/182] Refactored merging of Detection attributes to better work with np.ndarrays --- supervision/detection/utils.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index b0414eb4..a79900b4 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -162,8 +162,8 @@ def greedy_nmm(predictions: np.ndarray, threshold: float = 0.5) -> Dict[int, Lis xx2 = np.minimum(xx2, x2[idx]) yy2 = np.minimum(yy2, y2[idx]) - w = np.maximum(0.0, xx2 - xx1) - h = np.maximum(0.0, yy2 - yy1) + w = np.maximum(0, xx2 - xx1) + h = np.maximum(0, yy2 - yy1) inter = w * h @@ -234,37 +234,39 @@ def get_merged_bbox(bbox1: np.ndarray, bbox2: np.ndarray) -> np.ndarray: np.ndarray: A numpy array of shape `(, 4)` where the new bounding box is the merged bounding box of `bbox1` and `bbox2`. """ - left_top = np.minimum(bbox1[:2], bbox2[:2]) - right_bottom = np.maximum(bbox1[2:], bbox2[2:]) - return np.concatenate([left_top, right_bottom]) + left_top = np.minimum(bbox1[0][:2], bbox2[0][:2]) + right_bottom = np.maximum(bbox1[0][2:], bbox2[0][2:]) + return np.array([np.concatenate([left_top, right_bottom])]) -def get_merged_class_id(id1: int, id2: int) -> int: +def get_merged_class_id(id1: np.ndarray, id2: np.ndarray) -> np.ndarray: """ Merges two class ids into one. Args: - id1 (int): The first class id. - id2 (int): The second class id. + id1 (np.ndarray): The first class id. + id2 (np.ndarray): The second class id. Returns: - int: The merged class id. + np.ndarray: The merged class id. """ - return max(id1, id2) + return np.array([max(id1.item(), id2.item())]) -def get_merged_confidence(confidence1: float, confidence2: float) -> float: +def get_merged_confidence( + confidence1: np.ndarray, confidence2: np.ndarray +) -> np.ndarray: """ Merges two confidences into one. Args: - confidence1 (float): The first confidence. - confidence2 (float): The second confidence. + confidence1 (np.ndarray): The first confidence. + confidence2 (np.ndarray): The second confidence. Returns: - float: The merged confidence. + np.ndarray: The merged confidence. """ - return max(confidence1, confidence2) + return np.array([max(confidence1.item(), confidence2.item())]) def get_merged_mask(mask1: np.ndarray, mask2: np.ndarray) -> np.ndarray: From 166a8da9a07b20852c4559624fe029fc87bc8751 Mon Sep 17 00:00:00 2001 From: mario-dg Date: Thu, 11 Apr 2024 12:22:44 +0200 Subject: [PATCH 005/182] Implement Feedback --- supervision/detection/core.py | 182 +++++++++++------- .../detection/tools/inference_slicer.py | 24 +-- supervision/detection/utils.py | 69 +------ 3 files changed, 117 insertions(+), 158 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 66387087..a9a4ee92 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -8,22 +8,17 @@ import numpy as np from supervision.config import CLASS_NAME_DATA_FIELD, ORIENTED_BOX_COORDINATES from supervision.detection.utils import ( - batched_greedy_nmm, + batch_non_max_merge, box_iou_batch, box_non_max_suppression, calculate_masks_centroids, extract_ultralytics_masks, get_data_item, - get_merged_bbox, - get_merged_class_id, - get_merged_confidence, - get_merged_mask, - get_merged_tracker_id, - greedy_nmm, is_data_equal, mask_non_max_suppression, mask_to_xyxy, merge_data, + non_max_merge, process_roboflow_result, validate_detections_fields, xywh_to_xyxy, @@ -32,17 +27,57 @@ from supervision.geometry.core import Position from supervision.utils.internal import deprecated -def _merge_object_detection_pair(pred1: Detections, pred2: Detections) -> Detections: - merged_bbox = get_merged_bbox(pred1.xyxy, pred2.xyxy) - merged_conf = get_merged_confidence(pred1.confidence, pred2.confidence) - merged_class_id = get_merged_class_id(pred1.class_id, pred2.class_id) +def _merge_object_detection_pair(det1: Detections, det2: Detections) -> Detections: + """ + Merges two Detections object into a single Detections object. + + A `winning` detection is determined based on the confidence score of the two + input detections. This winning detection is then used to specify which `class_id`, + `tracker_id`, and `data` to include in the merged Detections object. + The resulting `confidence` of the merged object is calculated by the weighted + contribution of each detection to the merged object. + The bounding boxes and masks of the two input detections are merged into a single + bounding box and mask, respectively. + + Args: + det1 (Detections): + The first Detections object + det2 (Detections): + The second Detections object + + Returns: + Detections: A new Detections object, with merged attributes. + """ + assert ( + len(det1) == len(det2) == 1 + ), "Both Detections should have exactly 1 detected object." + winning_det = det1 if det1.confidence.item() > det2.confidence.item() else det2 + + area_det1 = (det1.xyxy[0][2] - det1.xyxy[0][0]) * ( + det1.xyxy[0][3] - det1.xyxy[0][1] + ) + area_det2 = (det2.xyxy[0][2] - det2.xyxy[0][0]) * ( + det2.xyxy[0][3] - det2.xyxy[0][1] + ) + merged_x1, merged_y1 = np.minimum(det1.xyxy[0][:2], det2.xyxy[0][:2]) + merged_x2, merged_y2 = np.maximum(det1.xyxy[0][2:], det2.xyxy[0][2:]) + merged_area = (merged_x2 - merged_x1) * (merged_y2 - merged_y1) + + merged_conf = ( + area_det1 * det1.confidence.item() + area_det2 * det2.confidence.item() + ) / merged_area + merged_bbox = [np.concatenate([merged_x1, merged_y1, merged_x2, merged_y2])] + merged_class_id = winning_det.class_id.item() merged_tracker_id = None merged_mask = None + merged_data = None - if pred1.mask and pred2.mask: - merged_mask = get_merged_mask(pred1.mask, pred2.mask) - if pred1.tracker_id and pred2.tracker_id: - merged_tracker_id = get_merged_tracker_id(pred1.tracker_id, pred2.tracker_id) + if det1.mask and det2.mask: + merged_mask = np.logical_or(det1.mask, det2.mask) + if det1.tracker_id and det2.tracker_id: + merged_tracker_id = winning_det.tracker_id.item() + if det1.data and det2.data: + merged_data = winning_det.data return Detections( xyxy=merged_bbox, @@ -50,6 +85,7 @@ def _merge_object_detection_pair(pred1: Detections, pred2: Detections) -> Detect confidence=merged_conf, class_id=merged_class_id, tracker_id=merged_tracker_id, + data=merged_data, ) @@ -1091,64 +1127,6 @@ class Detections: """ return (self.xyxy[:, 3] - self.xyxy[:, 1]) * (self.xyxy[:, 2] - self.xyxy[:, 0]) - def with_nmm( - self, threshold: float = 0.5, class_agnostic: bool = False - ) -> Detections: - """ - Perform non-maximum merging on the current set of object detections. - - Args: - threshold (float, optional): The intersection-over-union threshold - to use for non-maximum merging. Defaults to 0.5. - class_agnostic (bool, optional): Whether to perform class-agnostic - non-maximum merging. If True, the class_id of each detection - will be ignored. Defaults to False. - - Returns: - Detections: A new Detections object containing the subset of detections - after non-maximum merging. - - Raises: - AssertionError: If `confidence` is None and class_agnostic is False. - If `class_id` is None and class_agnostic is False. - """ - if len(self) == 0: - return self - - assert 0.0 <= threshold <= 1.0, "Threshold must be between 0 and 1." - - assert ( - self.confidence is not None - ), "Detections confidence must be given for NMM to be executed." - - if class_agnostic: - predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1))) - keep_to_merge_list = greedy_nmm(predictions, threshold) - else: - predictions = np.hstack( - ( - self.xyxy, - self.confidence.reshape(-1, 1), - self.class_id.reshape(-1, 1), - ) - ) - keep_to_merge_list = batched_greedy_nmm(predictions, threshold) - - result = [] - - for keep_ind, merge_ind_list in keep_to_merge_list.items(): - for merge_ind in merge_ind_list: - if ( - box_iou_batch(self[keep_ind].xyxy, self[merge_ind].xyxy).item() - > threshold - ): - self[keep_ind] = _merge_object_detection_pair( - self[keep_ind], self[merge_ind] - ) - result.append(self[keep_ind]) - - return Detections.merge(result) - def with_nms( self, threshold: float = 0.5, class_agnostic: bool = False ) -> Detections: @@ -1204,3 +1182,61 @@ class Detections: ) return self[indices] + + def with_nmm( + self, threshold: float = 0.5, class_agnostic: bool = False + ) -> Detections: + """ + Perform non-maximum merging on the current set of object detections. + + Args: + threshold (float, optional): The intersection-over-union threshold + to use for non-maximum merging. Defaults to 0.5. + class_agnostic (bool, optional): Whether to perform class-agnostic + non-maximum merging. If True, the class_id of each detection + will be ignored. Defaults to False. + + Returns: + Detections: A new Detections object containing the subset of detections + after non-maximum merging. + + Raises: + AssertionError: If `confidence` is None and class_agnostic is False. + If `class_id` is None and class_agnostic is False. + """ + if len(self) == 0: + return self + + assert 0.0 <= threshold <= 1.0, "Threshold must be between 0 and 1." + + assert ( + self.confidence is not None + ), "Detections confidence must be given for NMM to be executed." + + if class_agnostic: + predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1))) + keep_to_merge_list = non_max_merge(predictions, threshold) + else: + predictions = np.hstack( + ( + self.xyxy, + self.confidence.reshape(-1, 1), + self.class_id.reshape(-1, 1), + ) + ) + keep_to_merge_list = batch_non_max_merge(predictions, threshold) + + result = [] + + for keep_ind, merge_ind_list in keep_to_merge_list.items(): + for merge_ind in merge_ind_list: + if ( + box_iou_batch(self[keep_ind].xyxy, self[merge_ind].xyxy).item() + > threshold + ): + self[keep_ind] = _merge_object_detection_pair( + self[keep_ind], self[merge_ind] + ) + result.append(self[keep_ind]) + + return Detections.merge(result) diff --git a/supervision/detection/tools/inference_slicer.py b/supervision/detection/tools/inference_slicer.py index 2aff9f6d..7157723f 100644 --- a/supervision/detection/tools/inference_slicer.py +++ b/supervision/detection/tools/inference_slicer.py @@ -36,12 +36,6 @@ class InferenceSlicer: slices in the format `(width_ratio, height_ratio)`. iou_threshold (Optional[float]): Intersection over Union (IoU) threshold used for non-max suppression. - merge_detections (Optional[bool]): Whether to merge the detection from all - slices or simply concatenate them. If `True`, Non-Maximum Merging (NMM), - otherwise Non-Maximum Suppression (NMS), is applied to the detections. - perform_standard_pred (Optional[bool]): Whether to perform inference on the - whole image in addition to the slices to increase the accuracy of - large object detection. callback (Callable): A function that performs inference on a given image slice and returns detections. thread_workers (int): Number of threads for parallel execution. @@ -59,15 +53,11 @@ class InferenceSlicer: slice_wh: Tuple[int, int] = (320, 320), overlap_ratio_wh: Tuple[float, float] = (0.2, 0.2), iou_threshold: Optional[float] = 0.5, - merge_detections: Optional[bool] = False, - perform_standard_pred: Optional[bool] = False, thread_workers: int = 1, ): self.slice_wh = slice_wh self.overlap_ratio_wh = overlap_ratio_wh self.iou_threshold = iou_threshold - self.merge_detections = merge_detections - self.perform_standard_pred = perform_standard_pred self.callback = callback self.thread_workers = thread_workers @@ -118,17 +108,9 @@ class InferenceSlicer: for future in as_completed(futures): detections_list.append(future.result()) - if self.perform_standard_pred: - detections_list.append(self.callback(image)) - - if self.merge_detections: - return Detections.merge(detections_list=detections_list).with_nmm( - threshold=self.iou_threshold - ) - else: - return Detections.merge(detections_list=detections_list).with_nms( - threshold=self.iou_threshold - ) + return Detections.merge(detections_list=detections_list).with_nms( + threshold=self.iou_threshold + ) def _run_callback(self, image, offset) -> Detections: """ diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index b9edb9d6..9e732aeb 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -274,7 +274,9 @@ def box_non_max_suppression( return keep[sort_index.argsort()] -def greedy_nmm(predictions: np.ndarray, threshold: float = 0.5) -> Dict[int, List[int]]: +def non_max_merge( + predictions: np.ndarray, threshold: float = 0.5 +) -> Dict[int, List[int]]: """ Apply greedy version of non-maximum merging to avoid detecting too many overlapping bounding boxes for a given object. @@ -351,7 +353,7 @@ def greedy_nmm(predictions: np.ndarray, threshold: float = 0.5) -> Dict[int, Lis return keep_to_merge_list -def batched_greedy_nmm( +def batch_non_max_merge( predictions: np.ndarray, threshold: float = 0.5 ) -> Dict[int, List[int]]: """ @@ -373,7 +375,7 @@ def batched_greedy_nmm( keep_to_merge_list = {} for category_id in np.unique(category_ids): curr_indices = np.where(category_ids == category_id)[0] - curr_keep_to_merge_list = greedy_nmm(predictions[curr_indices], threshold) + curr_keep_to_merge_list = non_max_merge(predictions[curr_indices], threshold) curr_indices_list = curr_indices.tolist() for curr_keep, curr_merge_list in curr_keep_to_merge_list.items(): keep = curr_indices_list[curr_keep] @@ -403,67 +405,6 @@ def get_merged_bbox(bbox1: np.ndarray, bbox2: np.ndarray) -> np.ndarray: return np.array([np.concatenate([left_top, right_bottom])]) -def get_merged_class_id(id1: np.ndarray, id2: np.ndarray) -> np.ndarray: - """ - Merges two class ids into one. - - Args: - id1 (np.ndarray): The first class id. - id2 (np.ndarray): The second class id. - - Returns: - np.ndarray: The merged class id. - """ - return np.array([max(id1.item(), id2.item())]) - - -def get_merged_confidence( - confidence1: np.ndarray, confidence2: np.ndarray -) -> np.ndarray: - """ - Merges two confidences into one. - - Args: - confidence1 (np.ndarray): The first confidence. - confidence2 (np.ndarray): The second confidence. - - Returns: - np.ndarray: The merged confidence. - """ - return np.array([max(confidence1.item(), confidence2.item())]) - - -def get_merged_mask(mask1: np.ndarray, mask2: np.ndarray) -> np.ndarray: - """ - Merges two masks into one. - - Args: - mask1 (np.ndarray): A numpy array of shape `(H, W)` where `H` and `W` - are the height and width of the mask, respectively. - mask2 (np.ndarray): A numpy array of shape `(H, W)` where `H` and `W` - are the height and width of the mask, respectively. - - Returns: - np.ndarray: A numpy array of shape `(H, W)` where the new mask is the - merged mask of `mask1` and `mask2`. - """ - return np.logical_or(mask1, mask2) - - -def get_merged_tracker_id(tracker_id1: int, tracker_id2: int) -> int: - """ - Merges two tracker ids into one. - - Args: - tracker_id1 (int): The first tracker id. - tracker_id2 (int): The second tracker id. - - Returns: - int: The merged tracker id. - """ - return max(tracker_id1, tracker_id2) - - def clip_boxes(xyxy: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray: """ Clips bounding boxes coordinates to fit within the frame resolution. From e30d4a9abbd97253312e3f95f9664b7c16e74c16 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Thu, 2 May 2024 12:49:31 +0200 Subject: [PATCH 006/182] add test for generating annotation with mask --- test/dataset/formats/test_coco.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 62d1b75b..40d686db 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -1,5 +1,5 @@ from contextlib import ExitStack as DoesNotRaise -from typing import Dict, List, Tuple +from typing import Dict, List, Tuple, Union import numpy as np import pytest @@ -20,6 +20,8 @@ def mock_cock_coco_annotation( category_id: int = 0, bbox: Tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0), area: float = 0.0, + segmentation: List[list] = None, + iscrowd: bool = False, ) -> dict: return { "id": annotation_id, @@ -27,7 +29,8 @@ def mock_cock_coco_annotation( "category_id": category_id, "bbox": list(bbox), "area": area, - "iscrowd": 0, + "segmentation": segmentation, + "iscrowd": int(iscrowd), } @@ -226,6 +229,21 @@ def test_group_coco_annotations_by_image_id( ), DoesNotRaise(), ), # two image annotations + ( + [ + mock_cock_coco_annotation( + category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, segmentation = [[0,0, 0,9, 9,9, 9,0]], + ) + ], + (20, 20), + True, + Detections( + xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32), + class_id=np.array([0], dtype=int), + mask = np.array([np.fromfunction(lambda i, j:np.bitwise_and(i<10, j<10), (20, 20), dtype=int)]) + ), + DoesNotRaise(), + ), # single image annotations with mask, segmentation mask outlines 10x10 square ], ) def test_coco_annotations_to_detections( From cc5e72dd5b9113abc922ef00f0133a5a6b61f67f Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Thu, 2 May 2024 22:08:08 +0200 Subject: [PATCH 007/182] test for RLE format --- test/dataset/formats/test_coco.py | 33 +++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 40d686db..5055a859 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -229,10 +229,10 @@ def test_group_coco_annotations_by_image_id( ), DoesNotRaise(), ), # two image annotations - ( + ( [ mock_cock_coco_annotation( - category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, segmentation = [[0,0, 0,9, 9,9, 9,0]], + category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, segmentation = [[0,0, 4,0, 4,5, 9,5, 9,9, 0,9]], ) ], (20, 20), @@ -240,10 +240,35 @@ def test_group_coco_annotations_by_image_id( Detections( xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32), class_id=np.array([0], dtype=int), - mask = np.array([np.fromfunction(lambda i, j:np.bitwise_and(i<10, j<10), (20, 20), dtype=int)]) + mask = np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((1,20,20)) ), DoesNotRaise(), - ), # single image annotations with mask, segmentation mask outlines 10x10 square + ), # single image annotations with mask, segmentation mask in L-like shape, like below: + # 1 0 0 0 + # 1 1 0 0 + # 0 0 0 0 + # 0 0 0 0 + ( + [ + mock_cock_coco_annotation( + category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, + segmentation = {'size':[20,20], 'counts':[0, 5, 20, 5, 40, 5, 60, 5, 80, 5, 100, 10, 120, 10, 140, 10, 160, 10, 180, 10]}, iscrowd = True + ) + ], + (20, 20), + True, + Detections( + xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32), + class_id=np.array([0], dtype=int), + mask = np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((1,20,20)) + ), + DoesNotRaise(), + ), # single image annotations with mask, RLE encoded segmentation mask in L-like shape, like below: + # 1 0 0 0 + # 1 1 0 0 + # 0 0 0 0 + # 0 0 0 0 + ], ) def test_coco_annotations_to_detections( From 7f114cba4c13d621ce86177db1fdbbfa2d4575a2 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Fri, 3 May 2024 00:24:46 +0200 Subject: [PATCH 008/182] RLE decoding --- supervision/dataset/formats/coco.py | 33 +++++++++++++++++++++++++---- supervision/detection/utils.py | 10 +++++++++ test/dataset/formats/test_coco.py | 2 +- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 4f8679d5..4105f86e 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -11,7 +11,7 @@ from supervision.dataset.utils import ( map_detections_class_id, ) from supervision.detection.core import Detections -from supervision.detection.utils import polygon_to_mask +from supervision.detection.utils import polygon_to_mask, rle_to_mask from supervision.utils.file import read_json_file, save_json_file @@ -68,6 +68,26 @@ def _polygons_to_masks( dtype=bool, ) +def _rles_to_masks( + rles: List[np.ndarray], resolution_wh: Tuple[int, int] +) -> np.ndarray: + return np.array( + [ + rle_to_mask(rle=rle, resolution_wh=resolution_wh) + for rle in rles + ], + dtype=bool, + ) + +def _concatenate_annotation_masks(mask_polygon, mask_rle): + if mask_polygon.ndim == 3 and mask_rle.ndim == 3: + return np.concatenate((mask_polygon, mask_rle)) + elif mask_polygon.ndim == 3: + return mask_polygon + elif mask_rle.ndim == 3: + return mask_rle + else: + None def coco_annotations_to_detections( image_annotations: List[dict], resolution_wh: Tuple[int, int], with_masks: bool @@ -87,11 +107,16 @@ def coco_annotations_to_detections( np.reshape( np.asarray(image_annotation["segmentation"], dtype=np.int32), (-1, 2) ) - for image_annotation in image_annotations + for image_annotation in image_annotations if not image_annotation["iscrowd"] ] - mask = _polygons_to_masks(polygons=polygons, resolution_wh=resolution_wh) + mask_polygon = _polygons_to_masks(polygons=polygons, resolution_wh=resolution_wh) + + rles = [np.array(image_annotation["segmentation"]["counts"]) + for image_annotation in image_annotations if image_annotation["iscrowd"]] + mask_rle = _rles_to_masks(rles = rles, resolution_wh = resolution_wh) + return Detections( - class_id=np.asarray(class_ids, dtype=int), xyxy=xyxy, mask=mask + class_id=np.asarray(class_ids, dtype=int), xyxy=xyxy, mask=_concatenate_annotation_masks(mask_polygon=mask_polygon, mask_rle=mask_rle) ) return Detections(xyxy=xyxy, class_id=np.asarray(class_ids, dtype=int)) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 3eeba5b4..2b6f7d63 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -766,3 +766,13 @@ def get_data_item( raise TypeError(f"Unsupported data type for key '{key}': {type(value)}") return subset_data + + +def rle_to_mask(rle: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray: + width, height = resolution_wh + + zero_one_values = np.zeros_like(rle) + zero_one_values[1::2]=1 + + decoded_rle = np.repeat(zero_one_values, rle) + return decoded_rle.reshape((height,width), order='F') diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 5055a859..7254c9a9 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -252,7 +252,7 @@ def test_group_coco_annotations_by_image_id( [ mock_cock_coco_annotation( category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, - segmentation = {'size':[20,20], 'counts':[0, 5, 20, 5, 40, 5, 60, 5, 80, 5, 100, 10, 120, 10, 140, 10, 160, 10, 180, 10]}, iscrowd = True + segmentation = {'size':[20,20], 'counts':[0, 10, 10, 10, 10, 10, 10, 10, 10, 10, 15, 5, 15, 5, 15, 5, 15, 5, 15, 5, 210]}, iscrowd = True ) ], (20, 20), From 6913ecf54d59ba270c2efa69586c15e9ac5729fb Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Fri, 3 May 2024 01:19:01 +0200 Subject: [PATCH 009/182] 2 annotations with segmentation, one polygon one RLE --- test/dataset/formats/test_coco.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 7254c9a9..ef627e89 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -263,12 +263,41 @@ def test_group_coco_annotations_by_image_id( mask = np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((1,20,20)) ), DoesNotRaise(), - ), # single image annotations with mask, RLE encoded segmentation mask in L-like shape, like below: + ), # single image annotations with mask, RLE segmentation mask in L-like shape, like below: # 1 0 0 0 # 1 1 0 0 # 0 0 0 0 # 0 0 0 0 + ( + [ + mock_cock_coco_annotation( + category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, segmentation = [[0,0, 4,0, 4,5, 9,5, 9,9, 0,9]] + ), + mock_cock_coco_annotation( + category_id=0, bbox=(5, 0, 5, 5), area=5 * 5, + segmentation = {'size':[20,20], 'counts':[100, 5, 15, 5, 15, 5, 15, 5, 15, 5, 215]}, iscrowd = True + ), + ], + (20, 20), + True, + Detections( + xyxy=np.array( + [[0, 0, 10, 10], [5, 0, 10, 5]], dtype=np.float32 + ), + class_id=np.array([0, 0], dtype=int), + mask = np.array([ + np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((20,20)), + np.array([1 if j>4 and j<10 and i<5 else 0 for i in range(0,20) for j in range(0,20)]).reshape((20,20)) + ]) + ), + DoesNotRaise(), + ), # two image annotations with mask, one mask as polygon in in L-like shape, second as RLE in shape of square, like below (P = polygon, R = RLE): + # P R 0 0 + # P P 0 0 + # 0 0 0 0 + # 0 0 0 0 + ], ) def test_coco_annotations_to_detections( From 4a068ff8d7cf71f1e32e8e168e10f5539643a20d Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Fri, 3 May 2024 09:16:13 +0200 Subject: [PATCH 010/182] binary mask to RL encoding --- supervision/detection/utils.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 2b6f7d63..a915972a 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -1,4 +1,4 @@ -from itertools import chain +from itertools import chain, groupby from typing import Dict, List, Optional, Tuple, Union import cv2 @@ -776,3 +776,12 @@ def rle_to_mask(rle: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray: decoded_rle = np.repeat(zero_one_values, rle) return decoded_rle.reshape((height,width), order='F') + +def mask_to_rle(binary_mask: np.ndarray) -> list: + rle = [] + for _, group in groupby(binary_mask.ravel(order='F')): + rle.append(len(list(group))) + + if binary_mask[0][0] == 1: + rle = [0]+rle + return rle \ No newline at end of file From dc1ce02860e4c4ee7aab3522268391ed7353f652 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Fri, 3 May 2024 23:40:10 +0200 Subject: [PATCH 011/182] move rle encode decode functions to dataset/utils.py --- supervision/dataset/formats/coco.py | 14 +++++++++----- supervision/dataset/utils.py | 20 ++++++++++++++++++++ supervision/detection/utils.py | 21 +-------------------- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 4105f86e..d29f3ade 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -9,9 +9,11 @@ import numpy as np from supervision.dataset.utils import ( approximate_mask_with_polygons, map_detections_class_id, + rle_to_mask, + mask_to_rle, ) from supervision.detection.core import Detections -from supervision.detection.utils import polygon_to_mask, rle_to_mask +from supervision.detection.utils import polygon_to_mask from supervision.utils.file import read_json_file, save_json_file @@ -133,9 +135,9 @@ def detections_to_coco_annotations( coco_annotations = [] for xyxy, mask, _, class_id, _, _ in detections: box_width, box_height = xyxy[2] - xyxy[0], xyxy[3] - xyxy[1] - polygon = [] + segmentation = [] if mask is not None: - polygon = list( + segmentation = list( approximate_mask_with_polygons( mask=mask, min_image_area_percentage=min_image_area_percentage, @@ -143,14 +145,16 @@ def detections_to_coco_annotations( approximation_percentage=approximation_percentage, )[0].flatten() ) + # todo: flag for when to use RLE? + # segmentation = {"counts": mask_to_rle(binary_mask=mask), "size": list(mask.shape[:2])} coco_annotation = { "id": annotation_id, "image_id": image_id, "category_id": int(class_id), "bbox": [xyxy[0], xyxy[1], box_width, box_height], "area": box_width * box_height, - "segmentation": [polygon] if polygon else [], - "iscrowd": 0, + "segmentation": [segmentation] if segmentation else [], + "iscrowd": 0, ## todo: iscrowd depends on flag 1 if RLE 0 if polygon } coco_annotations.append(coco_annotation) annotation_id += 1 diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index 05ee3201..d46aa45b 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -3,6 +3,8 @@ import os import random from pathlib import Path from typing import Dict, List, Optional, Tuple, TypeVar +from itertools import groupby + import cv2 import numpy as np @@ -129,3 +131,21 @@ def train_test_split( split_index = int(len(data) * train_ratio) return data[:split_index], data[split_index:] + +def rle_to_mask(rle: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray: + width, height = resolution_wh + + zero_one_values = np.zeros_like(rle) + zero_one_values[1::2]=1 + + decoded_rle = np.repeat(zero_one_values, rle) + return decoded_rle.reshape((height,width), order='F') + +def mask_to_rle(binary_mask: np.ndarray) -> list: + rle = [] + for _, group in groupby(binary_mask.ravel(order='F')): + rle.append(len(list(group))) + + if binary_mask[0][0] == 1: + rle = [0]+rle + return rle diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index a915972a..3eeba5b4 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -1,4 +1,4 @@ -from itertools import chain, groupby +from itertools import chain from typing import Dict, List, Optional, Tuple, Union import cv2 @@ -766,22 +766,3 @@ def get_data_item( raise TypeError(f"Unsupported data type for key '{key}': {type(value)}") return subset_data - - -def rle_to_mask(rle: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray: - width, height = resolution_wh - - zero_one_values = np.zeros_like(rle) - zero_one_values[1::2]=1 - - decoded_rle = np.repeat(zero_one_values, rle) - return decoded_rle.reshape((height,width), order='F') - -def mask_to_rle(binary_mask: np.ndarray) -> list: - rle = [] - for _, group in groupby(binary_mask.ravel(order='F')): - rle.append(len(list(group))) - - if binary_mask[0][0] == 1: - rle = [0]+rle - return rle \ No newline at end of file From 869204d42debc065c512dd02a5e731307276d2df Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 5 May 2024 11:59:20 +0000 Subject: [PATCH 012/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/formats/coco.py | 33 +++++--- supervision/dataset/utils.py | 15 ++-- test/dataset/formats/test_coco.py | 122 +++++++++++++++++++++------- 3 files changed, 121 insertions(+), 49 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index d29f3ade..9b7e534a 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -10,7 +10,6 @@ from supervision.dataset.utils import ( approximate_mask_with_polygons, map_detections_class_id, rle_to_mask, - mask_to_rle, ) from supervision.detection.core import Detections from supervision.detection.utils import polygon_to_mask @@ -70,17 +69,16 @@ def _polygons_to_masks( dtype=bool, ) + def _rles_to_masks( - rles: List[np.ndarray], resolution_wh: Tuple[int, int] + rles: List[np.ndarray], resolution_wh: Tuple[int, int] ) -> np.ndarray: return np.array( - [ - rle_to_mask(rle=rle, resolution_wh=resolution_wh) - for rle in rles - ], + [rle_to_mask(rle=rle, resolution_wh=resolution_wh) for rle in rles], dtype=bool, ) + def _concatenate_annotation_masks(mask_polygon, mask_rle): if mask_polygon.ndim == 3 and mask_rle.ndim == 3: return np.concatenate((mask_polygon, mask_rle)) @@ -91,6 +89,7 @@ def _concatenate_annotation_masks(mask_polygon, mask_rle): else: None + def coco_annotations_to_detections( image_annotations: List[dict], resolution_wh: Tuple[int, int], with_masks: bool ) -> Detections: @@ -109,16 +108,26 @@ def coco_annotations_to_detections( np.reshape( np.asarray(image_annotation["segmentation"], dtype=np.int32), (-1, 2) ) - for image_annotation in image_annotations if not image_annotation["iscrowd"] + for image_annotation in image_annotations + if not image_annotation["iscrowd"] ] - mask_polygon = _polygons_to_masks(polygons=polygons, resolution_wh=resolution_wh) + mask_polygon = _polygons_to_masks( + polygons=polygons, resolution_wh=resolution_wh + ) - rles = [np.array(image_annotation["segmentation"]["counts"]) - for image_annotation in image_annotations if image_annotation["iscrowd"]] - mask_rle = _rles_to_masks(rles = rles, resolution_wh = resolution_wh) + rles = [ + np.array(image_annotation["segmentation"]["counts"]) + for image_annotation in image_annotations + if image_annotation["iscrowd"] + ] + mask_rle = _rles_to_masks(rles=rles, resolution_wh=resolution_wh) return Detections( - class_id=np.asarray(class_ids, dtype=int), xyxy=xyxy, mask=_concatenate_annotation_masks(mask_polygon=mask_polygon, mask_rle=mask_rle) + class_id=np.asarray(class_ids, dtype=int), + xyxy=xyxy, + mask=_concatenate_annotation_masks( + mask_polygon=mask_polygon, mask_rle=mask_rle + ), ) return Detections(xyxy=xyxy, class_id=np.asarray(class_ids, dtype=int)) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index d46aa45b..ef30aa48 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -1,10 +1,9 @@ import copy import os import random +from itertools import groupby from pathlib import Path from typing import Dict, List, Optional, Tuple, TypeVar -from itertools import groupby - import cv2 import numpy as np @@ -132,20 +131,22 @@ def train_test_split( split_index = int(len(data) * train_ratio) return data[:split_index], data[split_index:] + def rle_to_mask(rle: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray: width, height = resolution_wh - + zero_one_values = np.zeros_like(rle) - zero_one_values[1::2]=1 + zero_one_values[1::2] = 1 decoded_rle = np.repeat(zero_one_values, rle) - return decoded_rle.reshape((height,width), order='F') + return decoded_rle.reshape((height, width), order="F") + def mask_to_rle(binary_mask: np.ndarray) -> list: rle = [] - for _, group in groupby(binary_mask.ravel(order='F')): + for _, group in groupby(binary_mask.ravel(order="F")): rle.append(len(list(group))) if binary_mask[0][0] == 1: - rle = [0]+rle + rle = [0] + rle return rle diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index ef627e89..bfde080d 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -1,5 +1,5 @@ from contextlib import ExitStack as DoesNotRaise -from typing import Dict, List, Tuple, Union +from typing import Dict, List, Tuple import numpy as np import pytest @@ -232,7 +232,10 @@ def test_group_coco_annotations_by_image_id( ( [ mock_cock_coco_annotation( - category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, segmentation = [[0,0, 4,0, 4,5, 9,5, 9,9, 0,9]], + category_id=0, + bbox=(0, 0, 10, 10), + area=10 * 10, + segmentation=[[0, 0, 4, 0, 4, 5, 9, 5, 9, 9, 0, 9]], ) ], (20, 20), @@ -240,19 +243,53 @@ def test_group_coco_annotations_by_image_id( Detections( xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32), class_id=np.array([0], dtype=int), - mask = np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((1,20,20)) + mask=np.array( + [ + 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 + for i in range(0, 20) + for j in range(0, 20) + ] + ).reshape((1, 20, 20)), ), DoesNotRaise(), ), # single image annotations with mask, segmentation mask in L-like shape, like below: - # 1 0 0 0 - # 1 1 0 0 - # 0 0 0 0 - # 0 0 0 0 + # 1 0 0 0 + # 1 1 0 0 + # 0 0 0 0 + # 0 0 0 0 ( [ mock_cock_coco_annotation( - category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, - segmentation = {'size':[20,20], 'counts':[0, 10, 10, 10, 10, 10, 10, 10, 10, 10, 15, 5, 15, 5, 15, 5, 15, 5, 15, 5, 210]}, iscrowd = True + category_id=0, + bbox=(0, 0, 10, 10), + area=10 * 10, + segmentation={ + "size": [20, 20], + "counts": [ + 0, + 10, + 10, + 10, + 10, + 10, + 10, + 10, + 10, + 10, + 15, + 5, + 15, + 5, + 15, + 5, + 15, + 5, + 15, + 5, + 210, + ], + }, + iscrowd=True, ) ], (20, 20), @@ -260,44 +297,69 @@ def test_group_coco_annotations_by_image_id( Detections( xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32), class_id=np.array([0], dtype=int), - mask = np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((1,20,20)) + mask=np.array( + [ + 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 + for i in range(0, 20) + for j in range(0, 20) + ] + ).reshape((1, 20, 20)), ), DoesNotRaise(), ), # single image annotations with mask, RLE segmentation mask in L-like shape, like below: - # 1 0 0 0 - # 1 1 0 0 - # 0 0 0 0 - # 0 0 0 0 - + # 1 0 0 0 + # 1 1 0 0 + # 0 0 0 0 + # 0 0 0 0 ( [ mock_cock_coco_annotation( - category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, segmentation = [[0,0, 4,0, 4,5, 9,5, 9,9, 0,9]] + category_id=0, + bbox=(0, 0, 10, 10), + area=10 * 10, + segmentation=[[0, 0, 4, 0, 4, 5, 9, 5, 9, 9, 0, 9]], ), mock_cock_coco_annotation( - category_id=0, bbox=(5, 0, 5, 5), area=5 * 5, - segmentation = {'size':[20,20], 'counts':[100, 5, 15, 5, 15, 5, 15, 5, 15, 5, 215]}, iscrowd = True + category_id=0, + bbox=(5, 0, 5, 5), + area=5 * 5, + segmentation={ + "size": [20, 20], + "counts": [100, 5, 15, 5, 15, 5, 15, 5, 15, 5, 215], + }, + iscrowd=True, ), ], (20, 20), True, Detections( - xyxy=np.array( - [[0, 0, 10, 10], [5, 0, 10, 5]], dtype=np.float32 - ), + xyxy=np.array([[0, 0, 10, 10], [5, 0, 10, 5]], dtype=np.float32), class_id=np.array([0, 0], dtype=int), - mask = np.array([ - np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((20,20)), - np.array([1 if j>4 and j<10 and i<5 else 0 for i in range(0,20) for j in range(0,20)]).reshape((20,20)) - ]) + mask=np.array( + [ + np.array( + [ + 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 + for i in range(0, 20) + for j in range(0, 20) + ] + ).reshape((20, 20)), + np.array( + [ + 1 if j > 4 and j < 10 and i < 5 else 0 + for i in range(0, 20) + for j in range(0, 20) + ] + ).reshape((20, 20)), + ] + ), ), DoesNotRaise(), ), # two image annotations with mask, one mask as polygon in in L-like shape, second as RLE in shape of square, like below (P = polygon, R = RLE): - # P R 0 0 - # P P 0 0 - # 0 0 0 0 - # 0 0 0 0 - + # P R 0 0 + # P P 0 0 + # 0 0 0 0 + # 0 0 0 0 ], ) def test_coco_annotations_to_detections( From d7e52bee264fb1b3b5c47a3f27b5eb67deae86a6 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Mon, 6 May 2024 17:20:31 +0300 Subject: [PATCH 013/182] NMM: Add None-checks, fix area normalization, style --- supervision/detection/core.py | 179 +++++++++++++++++++++++++--------- 1 file changed, 131 insertions(+), 48 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index b60e3363..3d1c135a 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -30,14 +30,16 @@ from supervision.validators import validate_detections_fields def _merge_object_detection_pair(det1: Detections, det2: Detections) -> Detections: """ Merges two Detections object into a single Detections object. + Assumes each Detections contains exactly one object. A `winning` detection is determined based on the confidence score of the two - input detections. This winning detection is then used to specify which `class_id`, - `tracker_id`, and `data` to include in the merged Detections object. + input detections. This winning detection is then used to specify which + `class_id`, `tracker_id`, and `data` to include in the merged Detections object. + The resulting `confidence` of the merged object is calculated by the weighted contribution of each detection to the merged object. - The bounding boxes and masks of the two input detections are merged into a single - bounding box and mask, respectively. + The bounding boxes and masks of the two input detections are merged into a + single bounding box and mask, respectively. Args: det1 (Detections): @@ -47,11 +49,39 @@ def _merge_object_detection_pair(det1: Detections, det2: Detections) -> Detectio Returns: Detections: A new Detections object, with merged attributes. + + Raises: + ValueError: If the input Detections objects do not have exactly 1 detected + object. + + Example: + ```python + import cv2 + import supervision as sv + from inference import get_model + + image = cv2.imread() + model = get_model(model_id="yolov8s-640") + + result = model.infer(image)[0] + detections = sv.Detections.from_inference(result) + + merged_detections = merge_object_detection_pair( + detections[0], detections[1]) + ``` """ - assert ( - len(det1) == len(det2) == 1 - ), "Both Detections should have exactly 1 detected object." - winning_det = det1 if det1.confidence.item() > det2.confidence.item() else det2 + if len(det1) != 1 or len(det2) != 1: + raise ValueError( + "Both Detections should have exactly 1 detected object.") + + if det2.confidence is None: + winning_det = det1 + elif det1.confidence is None: + winning_det = det2 + elif det1.confidence[0] >= det2.confidence[0]: + winning_det = det1 + else: + winning_det = det2 area_det1 = (det1.xyxy[0][2] - det1.xyxy[0][0]) * ( det1.xyxy[0][3] - det1.xyxy[0][1] @@ -59,33 +89,39 @@ def _merge_object_detection_pair(det1: Detections, det2: Detections) -> Detectio area_det2 = (det2.xyxy[0][2] - det2.xyxy[0][0]) * ( det2.xyxy[0][3] - det2.xyxy[0][1] ) + merged_x1, merged_y1 = np.minimum(det1.xyxy[0][:2], det2.xyxy[0][:2]) merged_x2, merged_y2 = np.maximum(det1.xyxy[0][2:], det2.xyxy[0][2:]) - merged_area = (merged_x2 - merged_x1) * (merged_y2 - merged_y1) - merged_conf = ( - area_det1 * det1.confidence.item() + area_det2 * det2.confidence.item() - ) / merged_area - merged_bbox = [np.concatenate([merged_x1, merged_y1, merged_x2, merged_y2])] - merged_class_id = winning_det.class_id.item() - merged_tracker_id = None + merged_xy = np.array([[merged_x1, merged_y1, merged_x2, merged_y2]]) + + winning_class_id = winning_det.class_id + + if det1.confidence is None or det2.confidence is None: + merged_confidence = None + else: + merged_confidence = ( + area_det1 * det1.confidence[0] + area_det2 * det2.confidence[0] + ) / (area_det1 + area_det2) + merged_confidence = np.array([merged_confidence]) + merged_mask = None - merged_data = None - - if det1.mask and det2.mask: + if det1.mask is not None and det2.mask is not None: merged_mask = np.logical_or(det1.mask, det2.mask) - if det1.tracker_id and det2.tracker_id: - merged_tracker_id = winning_det.tracker_id.item() + + winning_tracker_id = winning_det.tracker_id + + winning_data = None if det1.data and det2.data: - merged_data = winning_det.data + winning_data = winning_det.data return Detections( - xyxy=merged_bbox, + xyxy=merged_xy, mask=merged_mask, - confidence=merged_conf, - class_id=merged_class_id, - tracker_id=merged_tracker_id, - data=merged_data, + confidence=merged_confidence, + class_id=winning_class_id, + tracker_id=winning_tracker_id, + data=winning_data, ) @@ -260,7 +296,8 @@ class Detections: detections = sv.Detections.from_yolov5(result) ``` """ - yolov5_detections_predictions = yolov5_results.pred[0].cpu().cpu().numpy() + yolov5_detections_predictions = yolov5_results.pred[0].cpu( + ).cpu().numpy() return cls( xyxy=yolov5_detections_predictions[:, :4], @@ -307,7 +344,8 @@ class Detections: if "obb" in ultralytics_results and ultralytics_results.obb is not None: class_id = ultralytics_results.obb.cls.cpu().numpy().astype(int) - class_names = np.array([ultralytics_results.names[i] for i in class_id]) + class_names = np.array( + [ultralytics_results.names[i] for i in class_id]) oriented_box_coordinates = ultralytics_results.obb.xyxyxyxy.cpu().numpy() return cls( xyxy=ultralytics_results.obb.xyxy.cpu().numpy(), @@ -323,7 +361,8 @@ class Detections: ) class_id = ultralytics_results.boxes.cls.cpu().numpy().astype(int) - class_names = np.array([ultralytics_results.names[i] for i in class_id]) + class_names = np.array([ultralytics_results.names[i] + for i in class_id]) return cls( xyxy=ultralytics_results.boxes.xyxy.cpu().numpy(), confidence=ultralytics_results.boxes.conf.cpu().numpy(), @@ -411,7 +450,8 @@ class Detections: return cls( xyxy=boxes, confidence=tensorflow_results["detection_scores"][0].numpy(), - class_id=tensorflow_results["detection_classes"][0].numpy().astype(int), + class_id=tensorflow_results["detection_classes"][0].numpy().astype( + int), ) @classmethod @@ -448,7 +488,8 @@ class Detections: return cls( xyxy=np.array(deepsparse_results.boxes[0]), confidence=np.array(deepsparse_results.scores[0]), - class_id=np.array(deepsparse_results.labels[0]).astype(float).astype(int), + class_id=np.array(deepsparse_results.labels[0]).astype( + float).astype(int), ) @classmethod @@ -535,24 +576,29 @@ class Detections: Class names values can be accessed using `detections["class_name"]`. """ # noqa: E501 // docs - class_ids = transformers_results["labels"].cpu().detach().numpy().astype(int) + class_ids = transformers_results["labels"].cpu( + ).detach().numpy().astype(int) data = {} if id2label is not None: - class_names = np.array([id2label[class_id] for class_id in class_ids]) + class_names = np.array([id2label[class_id] + for class_id in class_ids]) data[CLASS_NAME_DATA_FIELD] = class_names if "boxes" in transformers_results: return cls( xyxy=transformers_results["boxes"].cpu().detach().numpy(), - confidence=transformers_results["scores"].cpu().detach().numpy(), + confidence=transformers_results["scores"].cpu( + ).detach().numpy(), class_id=class_ids, data=data, ) elif "masks" in transformers_results: - masks = transformers_results["masks"].cpu().detach().numpy().astype(bool) + masks = transformers_results["masks"].cpu( + ).detach().numpy().astype(bool) return cls( xyxy=mask_to_xyxy(masks), mask=masks, - confidence=transformers_results["scores"].cpu().detach().numpy(), + confidence=transformers_results["scores"].cpu( + ).detach().numpy(), class_id=class_ids, data=data, ) @@ -595,7 +641,8 @@ class Detections: """ return cls( - xyxy=detectron2_results["instances"].pred_boxes.tensor.cpu().numpy(), + xyxy=detectron2_results["instances"].pred_boxes.tensor.cpu( + ).numpy(), confidence=detectron2_results["instances"].scores.cpu().numpy(), class_id=detectron2_results["instances"] .pred_classes.cpu() @@ -638,7 +685,8 @@ class Detections: Class names values can be accessed using `detections["class_name"]`. """ with suppress(AttributeError): - roboflow_result = roboflow_result.dict(exclude_none=True, by_alias=True) + roboflow_result = roboflow_result.dict( + exclude_none=True, by_alias=True) xyxy, confidence, class_id, masks, trackers, data = process_roboflow_result( roboflow_result=roboflow_result ) @@ -730,7 +778,8 @@ class Detections: ) xywh = np.array([mask["bbox"] for mask in sorted_generated_masks]) - mask = np.array([mask["segmentation"] for mask in sorted_generated_masks]) + mask = np.array([mask["segmentation"] + for mask in sorted_generated_masks]) if np.asarray(xywh).shape[0] == 0: return cls.empty() @@ -957,7 +1006,8 @@ class Detections: if all(d.__getattribute__(name) is None for d in detections_list): return None if any(d.__getattribute__(name) is None for d in detections_list): - raise ValueError(f"All or none of the '{name}' fields must be None") + raise ValueError( + f"All or none of the '{name}' fields must be None") return ( np.vstack([d.__getattribute__(name) for d in detections_list]) if name == "mask" @@ -1128,6 +1178,34 @@ class Detections: self.data[key] = value + def _set_at_index(self, index: int, other: Detections): + """ + Set detection values (xyxy, confidence, ...) at a specified index + to those of another Detections object, at index 0. + + Args: + index (int): The index in current detection, where values + will be set. + other (Detections): Detections object with exactly one element + to set the values from. + + Raises: + ValueError: If `other` is not made of exactly one element. + """ + if len(other) != 1: + raise ValueError( + "Detection to set from must have exactly one element.") + + self.xyxy[index] = other.xyxy[0] + if self.mask is not None and other.mask is not None: + self.mask[index] = other.mask[0] + if self.confidence is not None and other.confidence is not None: + self.confidence[index] = other.confidence[0] + if self.class_id is not None and other.class_id is not None: + self.class_id[index] = other.class_id[0] + if self.tracker_id is not None and other.tracker_id is not None: + self.tracker_id[index] = other.tracker_id[0] + @property def area(self) -> np.ndarray: """ @@ -1188,7 +1266,8 @@ class Detections: ), "Detections confidence must be given for NMS to be executed." if class_agnostic: - predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1))) + predictions = np.hstack( + (self.xyxy, self.confidence.reshape(-1, 1))) else: assert self.class_id is not None, ( "Detections class_id must be given for NMS to be executed. If you" @@ -1244,9 +1323,14 @@ class Detections: ), "Detections confidence must be given for NMM to be executed." if class_agnostic: - predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1))) + predictions = np.hstack( + (self.xyxy, self.confidence.reshape(-1, 1))) keep_to_merge_list = non_max_merge(predictions, threshold) else: + assert self.class_id is not None, ( + "Detections class_id must be given for NMS to be executed. If you" + " intended to perform class agnostic NMM set class_agnostic=True." + ) predictions = np.hstack( ( self.xyxy, @@ -1257,16 +1341,15 @@ class Detections: keep_to_merge_list = batch_non_max_merge(predictions, threshold) result = [] - for keep_ind, merge_ind_list in keep_to_merge_list.items(): for merge_ind in merge_ind_list: - if ( - box_iou_batch(self[keep_ind].xyxy, self[merge_ind].xyxy).item() - > threshold - ): - self[keep_ind] = _merge_object_detection_pair( + box_iou = box_iou_batch( + self[keep_ind].xyxy, self[merge_ind].xyxy)[0] + if box_iou > threshold: + merged_detection = _merge_object_detection_pair( self[keep_ind], self[merge_ind] ) + self._set_at_index(keep_ind, merged_detection) result.append(self[keep_ind]) return Detections.merge(result) From bee3252110887fe941028ef696ebe0f36eae3b7e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 14:22:31 +0000 Subject: [PATCH 014/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/detection/core.py | 57 ++++++++++++----------------------- 1 file changed, 19 insertions(+), 38 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 3d1c135a..fa34c158 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -71,8 +71,7 @@ def _merge_object_detection_pair(det1: Detections, det2: Detections) -> Detectio ``` """ if len(det1) != 1 or len(det2) != 1: - raise ValueError( - "Both Detections should have exactly 1 detected object.") + raise ValueError("Both Detections should have exactly 1 detected object.") if det2.confidence is None: winning_det = det1 @@ -296,8 +295,7 @@ class Detections: detections = sv.Detections.from_yolov5(result) ``` """ - yolov5_detections_predictions = yolov5_results.pred[0].cpu( - ).cpu().numpy() + yolov5_detections_predictions = yolov5_results.pred[0].cpu().cpu().numpy() return cls( xyxy=yolov5_detections_predictions[:, :4], @@ -344,8 +342,7 @@ class Detections: if "obb" in ultralytics_results and ultralytics_results.obb is not None: class_id = ultralytics_results.obb.cls.cpu().numpy().astype(int) - class_names = np.array( - [ultralytics_results.names[i] for i in class_id]) + class_names = np.array([ultralytics_results.names[i] for i in class_id]) oriented_box_coordinates = ultralytics_results.obb.xyxyxyxy.cpu().numpy() return cls( xyxy=ultralytics_results.obb.xyxy.cpu().numpy(), @@ -361,8 +358,7 @@ class Detections: ) class_id = ultralytics_results.boxes.cls.cpu().numpy().astype(int) - class_names = np.array([ultralytics_results.names[i] - for i in class_id]) + class_names = np.array([ultralytics_results.names[i] for i in class_id]) return cls( xyxy=ultralytics_results.boxes.xyxy.cpu().numpy(), confidence=ultralytics_results.boxes.conf.cpu().numpy(), @@ -450,8 +446,7 @@ class Detections: return cls( xyxy=boxes, confidence=tensorflow_results["detection_scores"][0].numpy(), - class_id=tensorflow_results["detection_classes"][0].numpy().astype( - int), + class_id=tensorflow_results["detection_classes"][0].numpy().astype(int), ) @classmethod @@ -488,8 +483,7 @@ class Detections: return cls( xyxy=np.array(deepsparse_results.boxes[0]), confidence=np.array(deepsparse_results.scores[0]), - class_id=np.array(deepsparse_results.labels[0]).astype( - float).astype(int), + class_id=np.array(deepsparse_results.labels[0]).astype(float).astype(int), ) @classmethod @@ -576,29 +570,24 @@ class Detections: Class names values can be accessed using `detections["class_name"]`. """ # noqa: E501 // docs - class_ids = transformers_results["labels"].cpu( - ).detach().numpy().astype(int) + class_ids = transformers_results["labels"].cpu().detach().numpy().astype(int) data = {} if id2label is not None: - class_names = np.array([id2label[class_id] - for class_id in class_ids]) + class_names = np.array([id2label[class_id] for class_id in class_ids]) data[CLASS_NAME_DATA_FIELD] = class_names if "boxes" in transformers_results: return cls( xyxy=transformers_results["boxes"].cpu().detach().numpy(), - confidence=transformers_results["scores"].cpu( - ).detach().numpy(), + confidence=transformers_results["scores"].cpu().detach().numpy(), class_id=class_ids, data=data, ) elif "masks" in transformers_results: - masks = transformers_results["masks"].cpu( - ).detach().numpy().astype(bool) + masks = transformers_results["masks"].cpu().detach().numpy().astype(bool) return cls( xyxy=mask_to_xyxy(masks), mask=masks, - confidence=transformers_results["scores"].cpu( - ).detach().numpy(), + confidence=transformers_results["scores"].cpu().detach().numpy(), class_id=class_ids, data=data, ) @@ -641,8 +630,7 @@ class Detections: """ return cls( - xyxy=detectron2_results["instances"].pred_boxes.tensor.cpu( - ).numpy(), + xyxy=detectron2_results["instances"].pred_boxes.tensor.cpu().numpy(), confidence=detectron2_results["instances"].scores.cpu().numpy(), class_id=detectron2_results["instances"] .pred_classes.cpu() @@ -685,8 +673,7 @@ class Detections: Class names values can be accessed using `detections["class_name"]`. """ with suppress(AttributeError): - roboflow_result = roboflow_result.dict( - exclude_none=True, by_alias=True) + roboflow_result = roboflow_result.dict(exclude_none=True, by_alias=True) xyxy, confidence, class_id, masks, trackers, data = process_roboflow_result( roboflow_result=roboflow_result ) @@ -778,8 +765,7 @@ class Detections: ) xywh = np.array([mask["bbox"] for mask in sorted_generated_masks]) - mask = np.array([mask["segmentation"] - for mask in sorted_generated_masks]) + mask = np.array([mask["segmentation"] for mask in sorted_generated_masks]) if np.asarray(xywh).shape[0] == 0: return cls.empty() @@ -1006,8 +992,7 @@ class Detections: if all(d.__getattribute__(name) is None for d in detections_list): return None if any(d.__getattribute__(name) is None for d in detections_list): - raise ValueError( - f"All or none of the '{name}' fields must be None") + raise ValueError(f"All or none of the '{name}' fields must be None") return ( np.vstack([d.__getattribute__(name) for d in detections_list]) if name == "mask" @@ -1193,8 +1178,7 @@ class Detections: ValueError: If `other` is not made of exactly one element. """ if len(other) != 1: - raise ValueError( - "Detection to set from must have exactly one element.") + raise ValueError("Detection to set from must have exactly one element.") self.xyxy[index] = other.xyxy[0] if self.mask is not None and other.mask is not None: @@ -1266,8 +1250,7 @@ class Detections: ), "Detections confidence must be given for NMS to be executed." if class_agnostic: - predictions = np.hstack( - (self.xyxy, self.confidence.reshape(-1, 1))) + predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1))) else: assert self.class_id is not None, ( "Detections class_id must be given for NMS to be executed. If you" @@ -1323,8 +1306,7 @@ class Detections: ), "Detections confidence must be given for NMM to be executed." if class_agnostic: - predictions = np.hstack( - (self.xyxy, self.confidence.reshape(-1, 1))) + predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1))) keep_to_merge_list = non_max_merge(predictions, threshold) else: assert self.class_id is not None, ( @@ -1343,8 +1325,7 @@ class Detections: result = [] for keep_ind, merge_ind_list in keep_to_merge_list.items(): for merge_ind in merge_ind_list: - box_iou = box_iou_batch( - self[keep_ind].xyxy, self[merge_ind].xyxy)[0] + box_iou = box_iou_batch(self[keep_ind].xyxy, self[merge_ind].xyxy)[0] if box_iou > threshold: merged_detection = _merge_object_detection_pair( self[keep_ind], self[merge_ind] From 97c407101a2755db3288613c97cbbcda4e8105c0 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Mon, 6 May 2024 17:24:41 +0300 Subject: [PATCH 015/182] NMM: Move detections merge into Detections class. * No other changes! --- supervision/detection/core.py | 251 ++++++++++++++++++---------------- 1 file changed, 135 insertions(+), 116 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index fa34c158..501a27e9 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -27,103 +27,6 @@ from supervision.utils.internal import deprecated from supervision.validators import validate_detections_fields -def _merge_object_detection_pair(det1: Detections, det2: Detections) -> Detections: - """ - Merges two Detections object into a single Detections object. - Assumes each Detections contains exactly one object. - - A `winning` detection is determined based on the confidence score of the two - input detections. This winning detection is then used to specify which - `class_id`, `tracker_id`, and `data` to include in the merged Detections object. - - The resulting `confidence` of the merged object is calculated by the weighted - contribution of each detection to the merged object. - The bounding boxes and masks of the two input detections are merged into a - single bounding box and mask, respectively. - - Args: - det1 (Detections): - The first Detections object - det2 (Detections): - The second Detections object - - Returns: - Detections: A new Detections object, with merged attributes. - - Raises: - ValueError: If the input Detections objects do not have exactly 1 detected - object. - - Example: - ```python - import cv2 - import supervision as sv - from inference import get_model - - image = cv2.imread() - model = get_model(model_id="yolov8s-640") - - result = model.infer(image)[0] - detections = sv.Detections.from_inference(result) - - merged_detections = merge_object_detection_pair( - detections[0], detections[1]) - ``` - """ - if len(det1) != 1 or len(det2) != 1: - raise ValueError("Both Detections should have exactly 1 detected object.") - - if det2.confidence is None: - winning_det = det1 - elif det1.confidence is None: - winning_det = det2 - elif det1.confidence[0] >= det2.confidence[0]: - winning_det = det1 - else: - winning_det = det2 - - area_det1 = (det1.xyxy[0][2] - det1.xyxy[0][0]) * ( - det1.xyxy[0][3] - det1.xyxy[0][1] - ) - area_det2 = (det2.xyxy[0][2] - det2.xyxy[0][0]) * ( - det2.xyxy[0][3] - det2.xyxy[0][1] - ) - - merged_x1, merged_y1 = np.minimum(det1.xyxy[0][:2], det2.xyxy[0][:2]) - merged_x2, merged_y2 = np.maximum(det1.xyxy[0][2:], det2.xyxy[0][2:]) - - merged_xy = np.array([[merged_x1, merged_y1, merged_x2, merged_y2]]) - - winning_class_id = winning_det.class_id - - if det1.confidence is None or det2.confidence is None: - merged_confidence = None - else: - merged_confidence = ( - area_det1 * det1.confidence[0] + area_det2 * det2.confidence[0] - ) / (area_det1 + area_det2) - merged_confidence = np.array([merged_confidence]) - - merged_mask = None - if det1.mask is not None and det2.mask is not None: - merged_mask = np.logical_or(det1.mask, det2.mask) - - winning_tracker_id = winning_det.tracker_id - - winning_data = None - if det1.data and det2.data: - winning_data = winning_det.data - - return Detections( - xyxy=merged_xy, - mask=merged_mask, - confidence=merged_confidence, - class_id=winning_class_id, - tracker_id=winning_tracker_id, - data=winning_data, - ) - - @dataclass class Detections: """ @@ -295,7 +198,8 @@ class Detections: detections = sv.Detections.from_yolov5(result) ``` """ - yolov5_detections_predictions = yolov5_results.pred[0].cpu().cpu().numpy() + yolov5_detections_predictions = yolov5_results.pred[0].cpu( + ).cpu().numpy() return cls( xyxy=yolov5_detections_predictions[:, :4], @@ -342,7 +246,8 @@ class Detections: if "obb" in ultralytics_results and ultralytics_results.obb is not None: class_id = ultralytics_results.obb.cls.cpu().numpy().astype(int) - class_names = np.array([ultralytics_results.names[i] for i in class_id]) + class_names = np.array( + [ultralytics_results.names[i] for i in class_id]) oriented_box_coordinates = ultralytics_results.obb.xyxyxyxy.cpu().numpy() return cls( xyxy=ultralytics_results.obb.xyxy.cpu().numpy(), @@ -358,7 +263,8 @@ class Detections: ) class_id = ultralytics_results.boxes.cls.cpu().numpy().astype(int) - class_names = np.array([ultralytics_results.names[i] for i in class_id]) + class_names = np.array([ultralytics_results.names[i] + for i in class_id]) return cls( xyxy=ultralytics_results.boxes.xyxy.cpu().numpy(), confidence=ultralytics_results.boxes.conf.cpu().numpy(), @@ -446,7 +352,8 @@ class Detections: return cls( xyxy=boxes, confidence=tensorflow_results["detection_scores"][0].numpy(), - class_id=tensorflow_results["detection_classes"][0].numpy().astype(int), + class_id=tensorflow_results["detection_classes"][0].numpy().astype( + int), ) @classmethod @@ -483,7 +390,8 @@ class Detections: return cls( xyxy=np.array(deepsparse_results.boxes[0]), confidence=np.array(deepsparse_results.scores[0]), - class_id=np.array(deepsparse_results.labels[0]).astype(float).astype(int), + class_id=np.array(deepsparse_results.labels[0]).astype( + float).astype(int), ) @classmethod @@ -570,24 +478,29 @@ class Detections: Class names values can be accessed using `detections["class_name"]`. """ # noqa: E501 // docs - class_ids = transformers_results["labels"].cpu().detach().numpy().astype(int) + class_ids = transformers_results["labels"].cpu( + ).detach().numpy().astype(int) data = {} if id2label is not None: - class_names = np.array([id2label[class_id] for class_id in class_ids]) + class_names = np.array([id2label[class_id] + for class_id in class_ids]) data[CLASS_NAME_DATA_FIELD] = class_names if "boxes" in transformers_results: return cls( xyxy=transformers_results["boxes"].cpu().detach().numpy(), - confidence=transformers_results["scores"].cpu().detach().numpy(), + confidence=transformers_results["scores"].cpu( + ).detach().numpy(), class_id=class_ids, data=data, ) elif "masks" in transformers_results: - masks = transformers_results["masks"].cpu().detach().numpy().astype(bool) + masks = transformers_results["masks"].cpu( + ).detach().numpy().astype(bool) return cls( xyxy=mask_to_xyxy(masks), mask=masks, - confidence=transformers_results["scores"].cpu().detach().numpy(), + confidence=transformers_results["scores"].cpu( + ).detach().numpy(), class_id=class_ids, data=data, ) @@ -630,7 +543,8 @@ class Detections: """ return cls( - xyxy=detectron2_results["instances"].pred_boxes.tensor.cpu().numpy(), + xyxy=detectron2_results["instances"].pred_boxes.tensor.cpu( + ).numpy(), confidence=detectron2_results["instances"].scores.cpu().numpy(), class_id=detectron2_results["instances"] .pred_classes.cpu() @@ -673,7 +587,8 @@ class Detections: Class names values can be accessed using `detections["class_name"]`. """ with suppress(AttributeError): - roboflow_result = roboflow_result.dict(exclude_none=True, by_alias=True) + roboflow_result = roboflow_result.dict( + exclude_none=True, by_alias=True) xyxy, confidence, class_id, masks, trackers, data = process_roboflow_result( roboflow_result=roboflow_result ) @@ -765,7 +680,8 @@ class Detections: ) xywh = np.array([mask["bbox"] for mask in sorted_generated_masks]) - mask = np.array([mask["segmentation"] for mask in sorted_generated_masks]) + mask = np.array([mask["segmentation"] + for mask in sorted_generated_masks]) if np.asarray(xywh).shape[0] == 0: return cls.empty() @@ -992,7 +908,8 @@ class Detections: if all(d.__getattribute__(name) is None for d in detections_list): return None if any(d.__getattribute__(name) is None for d in detections_list): - raise ValueError(f"All or none of the '{name}' fields must be None") + raise ValueError( + f"All or none of the '{name}' fields must be None") return ( np.vstack([d.__getattribute__(name) for d in detections_list]) if name == "mask" @@ -1178,7 +1095,8 @@ class Detections: ValueError: If `other` is not made of exactly one element. """ if len(other) != 1: - raise ValueError("Detection to set from must have exactly one element.") + raise ValueError( + "Detection to set from must have exactly one element.") self.xyxy[index] = other.xyxy[0] if self.mask is not None and other.mask is not None: @@ -1250,7 +1168,8 @@ class Detections: ), "Detections confidence must be given for NMS to be executed." if class_agnostic: - predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1))) + predictions = np.hstack( + (self.xyxy, self.confidence.reshape(-1, 1))) else: assert self.class_id is not None, ( "Detections class_id must be given for NMS to be executed. If you" @@ -1306,7 +1225,8 @@ class Detections: ), "Detections confidence must be given for NMM to be executed." if class_agnostic: - predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1))) + predictions = np.hstack( + (self.xyxy, self.confidence.reshape(-1, 1))) keep_to_merge_list = non_max_merge(predictions, threshold) else: assert self.class_id is not None, ( @@ -1325,12 +1245,111 @@ class Detections: result = [] for keep_ind, merge_ind_list in keep_to_merge_list.items(): for merge_ind in merge_ind_list: - box_iou = box_iou_batch(self[keep_ind].xyxy, self[merge_ind].xyxy)[0] + box_iou = box_iou_batch( + self[keep_ind].xyxy, self[merge_ind].xyxy)[0] if box_iou > threshold: - merged_detection = _merge_object_detection_pair( + merged_detection = self._merge_object_detection_pair( self[keep_ind], self[merge_ind] ) self._set_at_index(keep_ind, merged_detection) result.append(self[keep_ind]) return Detections.merge(result) + + @staticmethod + def _merge_object_detection_pair(det1: Detections, det2: Detections) -> Detections: + """ + Merges two Detections object into a single Detections object. + Assumes each Detections contains exactly one object. + + A `winning` detection is determined based on the confidence score of the two + input detections. This winning detection is then used to specify which + `class_id`, `tracker_id`, and `data` to include in the merged Detections object. + + The resulting `confidence` of the merged object is calculated by the weighted + contribution of each detection to the merged object. + The bounding boxes and masks of the two input detections are merged into a + single bounding box and mask, respectively. + + Args: + det1 (Detections): + The first Detections object + det2 (Detections): + The second Detections object + + Returns: + Detections: A new Detections object, with merged attributes. + + Raises: + ValueError: If the input Detections objects do not have exactly 1 detected + object. + + Example: + ```python + import cv2 + import supervision as sv + from inference import get_model + + image = cv2.imread() + model = get_model(model_id="yolov8s-640") + + result = model.infer(image)[0] + detections = sv.Detections.from_inference(result) + + merged_detections = merge_object_detection_pair( + detections[0], detections[1]) + ``` + """ + if len(det1) != 1 or len(det2) != 1: + raise ValueError( + "Both Detections should have exactly 1 detected object.") + + if det2.confidence is None: + winning_det = det1 + elif det1.confidence is None: + winning_det = det2 + elif det1.confidence[0] >= det2.confidence[0]: + winning_det = det1 + else: + winning_det = det2 + + area_det1 = (det1.xyxy[0][2] - det1.xyxy[0][0]) * ( + det1.xyxy[0][3] - det1.xyxy[0][1] + ) + area_det2 = (det2.xyxy[0][2] - det2.xyxy[0][0]) * ( + det2.xyxy[0][3] - det2.xyxy[0][1] + ) + + merged_x1, merged_y1 = np.minimum(det1.xyxy[0][:2], det2.xyxy[0][:2]) + merged_x2, merged_y2 = np.maximum(det1.xyxy[0][2:], det2.xyxy[0][2:]) + + merged_xy = np.array([[merged_x1, merged_y1, merged_x2, merged_y2]]) + + winning_class_id = winning_det.class_id + + if det1.confidence is None or det2.confidence is None: + merged_confidence = None + else: + merged_confidence = ( + area_det1 * det1.confidence[0] + area_det2 * det2.confidence[0] + ) / (area_det1 + area_det2) + merged_confidence = np.array([merged_confidence]) + + merged_mask = None + if det1.mask is not None and det2.mask is not None: + merged_mask = np.logical_or(det1.mask, det2.mask) + + winning_tracker_id = winning_det.tracker_id + + winning_data = None + if det1.data and det2.data: + winning_data = winning_det.data + + return Detections( + xyxy=merged_xy, + mask=merged_mask, + confidence=merged_confidence, + class_id=winning_class_id, + tracker_id=winning_tracker_id, + data=winning_data, + ) From 204669b08c650378cb03553c55ec417975a4371e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 14:25:13 +0000 Subject: [PATCH 016/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/detection/core.py | 57 ++++++++++++----------------------- 1 file changed, 19 insertions(+), 38 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 501a27e9..beb68923 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -198,8 +198,7 @@ class Detections: detections = sv.Detections.from_yolov5(result) ``` """ - yolov5_detections_predictions = yolov5_results.pred[0].cpu( - ).cpu().numpy() + yolov5_detections_predictions = yolov5_results.pred[0].cpu().cpu().numpy() return cls( xyxy=yolov5_detections_predictions[:, :4], @@ -246,8 +245,7 @@ class Detections: if "obb" in ultralytics_results and ultralytics_results.obb is not None: class_id = ultralytics_results.obb.cls.cpu().numpy().astype(int) - class_names = np.array( - [ultralytics_results.names[i] for i in class_id]) + class_names = np.array([ultralytics_results.names[i] for i in class_id]) oriented_box_coordinates = ultralytics_results.obb.xyxyxyxy.cpu().numpy() return cls( xyxy=ultralytics_results.obb.xyxy.cpu().numpy(), @@ -263,8 +261,7 @@ class Detections: ) class_id = ultralytics_results.boxes.cls.cpu().numpy().astype(int) - class_names = np.array([ultralytics_results.names[i] - for i in class_id]) + class_names = np.array([ultralytics_results.names[i] for i in class_id]) return cls( xyxy=ultralytics_results.boxes.xyxy.cpu().numpy(), confidence=ultralytics_results.boxes.conf.cpu().numpy(), @@ -352,8 +349,7 @@ class Detections: return cls( xyxy=boxes, confidence=tensorflow_results["detection_scores"][0].numpy(), - class_id=tensorflow_results["detection_classes"][0].numpy().astype( - int), + class_id=tensorflow_results["detection_classes"][0].numpy().astype(int), ) @classmethod @@ -390,8 +386,7 @@ class Detections: return cls( xyxy=np.array(deepsparse_results.boxes[0]), confidence=np.array(deepsparse_results.scores[0]), - class_id=np.array(deepsparse_results.labels[0]).astype( - float).astype(int), + class_id=np.array(deepsparse_results.labels[0]).astype(float).astype(int), ) @classmethod @@ -478,29 +473,24 @@ class Detections: Class names values can be accessed using `detections["class_name"]`. """ # noqa: E501 // docs - class_ids = transformers_results["labels"].cpu( - ).detach().numpy().astype(int) + class_ids = transformers_results["labels"].cpu().detach().numpy().astype(int) data = {} if id2label is not None: - class_names = np.array([id2label[class_id] - for class_id in class_ids]) + class_names = np.array([id2label[class_id] for class_id in class_ids]) data[CLASS_NAME_DATA_FIELD] = class_names if "boxes" in transformers_results: return cls( xyxy=transformers_results["boxes"].cpu().detach().numpy(), - confidence=transformers_results["scores"].cpu( - ).detach().numpy(), + confidence=transformers_results["scores"].cpu().detach().numpy(), class_id=class_ids, data=data, ) elif "masks" in transformers_results: - masks = transformers_results["masks"].cpu( - ).detach().numpy().astype(bool) + masks = transformers_results["masks"].cpu().detach().numpy().astype(bool) return cls( xyxy=mask_to_xyxy(masks), mask=masks, - confidence=transformers_results["scores"].cpu( - ).detach().numpy(), + confidence=transformers_results["scores"].cpu().detach().numpy(), class_id=class_ids, data=data, ) @@ -543,8 +533,7 @@ class Detections: """ return cls( - xyxy=detectron2_results["instances"].pred_boxes.tensor.cpu( - ).numpy(), + xyxy=detectron2_results["instances"].pred_boxes.tensor.cpu().numpy(), confidence=detectron2_results["instances"].scores.cpu().numpy(), class_id=detectron2_results["instances"] .pred_classes.cpu() @@ -587,8 +576,7 @@ class Detections: Class names values can be accessed using `detections["class_name"]`. """ with suppress(AttributeError): - roboflow_result = roboflow_result.dict( - exclude_none=True, by_alias=True) + roboflow_result = roboflow_result.dict(exclude_none=True, by_alias=True) xyxy, confidence, class_id, masks, trackers, data = process_roboflow_result( roboflow_result=roboflow_result ) @@ -680,8 +668,7 @@ class Detections: ) xywh = np.array([mask["bbox"] for mask in sorted_generated_masks]) - mask = np.array([mask["segmentation"] - for mask in sorted_generated_masks]) + mask = np.array([mask["segmentation"] for mask in sorted_generated_masks]) if np.asarray(xywh).shape[0] == 0: return cls.empty() @@ -908,8 +895,7 @@ class Detections: if all(d.__getattribute__(name) is None for d in detections_list): return None if any(d.__getattribute__(name) is None for d in detections_list): - raise ValueError( - f"All or none of the '{name}' fields must be None") + raise ValueError(f"All or none of the '{name}' fields must be None") return ( np.vstack([d.__getattribute__(name) for d in detections_list]) if name == "mask" @@ -1095,8 +1081,7 @@ class Detections: ValueError: If `other` is not made of exactly one element. """ if len(other) != 1: - raise ValueError( - "Detection to set from must have exactly one element.") + raise ValueError("Detection to set from must have exactly one element.") self.xyxy[index] = other.xyxy[0] if self.mask is not None and other.mask is not None: @@ -1168,8 +1153,7 @@ class Detections: ), "Detections confidence must be given for NMS to be executed." if class_agnostic: - predictions = np.hstack( - (self.xyxy, self.confidence.reshape(-1, 1))) + predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1))) else: assert self.class_id is not None, ( "Detections class_id must be given for NMS to be executed. If you" @@ -1225,8 +1209,7 @@ class Detections: ), "Detections confidence must be given for NMM to be executed." if class_agnostic: - predictions = np.hstack( - (self.xyxy, self.confidence.reshape(-1, 1))) + predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1))) keep_to_merge_list = non_max_merge(predictions, threshold) else: assert self.class_id is not None, ( @@ -1245,8 +1228,7 @@ class Detections: result = [] for keep_ind, merge_ind_list in keep_to_merge_list.items(): for merge_ind in merge_ind_list: - box_iou = box_iou_batch( - self[keep_ind].xyxy, self[merge_ind].xyxy)[0] + box_iou = box_iou_batch(self[keep_ind].xyxy, self[merge_ind].xyxy)[0] if box_iou > threshold: merged_detection = self._merge_object_detection_pair( self[keep_ind], self[merge_ind] @@ -1301,8 +1283,7 @@ class Detections: ``` """ if len(det1) != 1 or len(det2) != 1: - raise ValueError( - "Both Detections should have exactly 1 detected object.") + raise ValueError("Both Detections should have exactly 1 detected object.") if det2.confidence is None: winning_det = det1 From 18a790e9ea4160d6ab1c2f9a08f62e14ab458784 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Mon, 6 May 2024 20:21:02 +0200 Subject: [PATCH 017/182] typing chanches and doc strings for rle_to_mask and mask_to_rle functions --- supervision/dataset/utils.py | 48 ++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index d46aa45b..1cd038ec 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -8,6 +8,7 @@ from itertools import groupby import cv2 import numpy as np +import numpy.typing as npt from supervision.detection.core import Detections from supervision.detection.utils import ( @@ -132,7 +133,24 @@ def train_test_split( split_index = int(len(data) * train_ratio) return data[:split_index], data[split_index:] -def rle_to_mask(rle: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray: +def rle_to_mask(rle: np.ndarray, resolution_wh: Tuple[int, int]) -> npt.NDArray[np.bool_]: + """ + Converts run-length encoding (RLE) to a binary mask. + + Args: + rle (np.ndarray): The 1D RLE array, the format used in the COCO dataset (column-wise encoding, + values of an array with even indices represent the number of pixels assigned as background, + values of an array with odd indices represent the number of pixels assigned as foreground object). + resolution_wh (Tuple[int, int]): The width (w) and height (h) of the desired binary mask resolution. + + Returns: + npt.NDArray[np.bool_]: The generated 2D Boolean mask of shape (h,w), where the foreground object + is marked with `True`'s and the rest is filled with `False`'s. + + Examples: + rle = [2, 2, 2], resolution_wh = [3, 2] -> mask = [[False, True, False], + [False, True, False]] + """ width, height = resolution_wh zero_one_values = np.zeros_like(rle) @@ -141,11 +159,31 @@ def rle_to_mask(rle: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray: decoded_rle = np.repeat(zero_one_values, rle) return decoded_rle.reshape((height,width), order='F') -def mask_to_rle(binary_mask: np.ndarray) -> list: +def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: + """ + Converts a binary mask into a run-length encoding (RLE). + + Args: + mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground object + and `False` indicates background. + + Returns: + List[int]: the run-length encoded mask. Values of a list with even indices represent the number of pixels assigned as background (`False`), + values of a list with odd indices represent the number of pixels assigned as foreground object (`True`). + + Examples: + mask = [[False, True, True], -> rle = [2, 4] + [False, True, True]] + + mask = [[True, True, True], -> rle = [0, 6] + [True, True, True]] + """ rle = [] - for _, group in groupby(binary_mask.ravel(order='F')): + + if mask[0][0] == 1: + rle = [0] + + for _, group in groupby(mask.ravel(order='F')): rle.append(len(list(group))) - if binary_mask[0][0] == 1: - rle = [0]+rle return rle From 3ade7f060a2db171f89494bc687c6255cca70f87 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Mon, 6 May 2024 22:42:26 +0200 Subject: [PATCH 018/182] fix order caused error with mask generation in coco_annotations_to_detections --- supervision/dataset/formats/coco.py | 48 +++++----------------- test/dataset/formats/test_coco.py | 63 +++++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 45 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index d29f3ade..c8f7cd04 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -59,37 +59,20 @@ def group_coco_annotations_by_image_id( return annotations -def _polygons_to_masks( - polygons: List[np.ndarray], resolution_wh: Tuple[int, int] -) -> np.ndarray: +def _annotations_to_mask(image_annotations: List[dict], resolution_wh: Tuple[int, int]): return np.array( [ - polygon_to_mask(polygon=polygon, resolution_wh=resolution_wh) - for polygon in polygons + rle_to_mask(rle=np.array(image_annotation["segmentation"]["counts"]), + resolution_wh=resolution_wh) + if image_annotation["iscrowd"] + else + polygon_to_mask(polygon= np.reshape(np.asarray(image_annotation["segmentation"], dtype=np.int32), (-1, 2)), + resolution_wh=resolution_wh) + for image_annotation in image_annotations ], dtype=bool, ) -def _rles_to_masks( - rles: List[np.ndarray], resolution_wh: Tuple[int, int] -) -> np.ndarray: - return np.array( - [ - rle_to_mask(rle=rle, resolution_wh=resolution_wh) - for rle in rles - ], - dtype=bool, - ) - -def _concatenate_annotation_masks(mask_polygon, mask_rle): - if mask_polygon.ndim == 3 and mask_rle.ndim == 3: - return np.concatenate((mask_polygon, mask_rle)) - elif mask_polygon.ndim == 3: - return mask_polygon - elif mask_rle.ndim == 3: - return mask_rle - else: - None def coco_annotations_to_detections( image_annotations: List[dict], resolution_wh: Tuple[int, int], with_masks: bool @@ -105,20 +88,9 @@ def coco_annotations_to_detections( xyxy[:, 2:4] += xyxy[:, 0:2] if with_masks: - polygons = [ - np.reshape( - np.asarray(image_annotation["segmentation"], dtype=np.int32), (-1, 2) - ) - for image_annotation in image_annotations if not image_annotation["iscrowd"] - ] - mask_polygon = _polygons_to_masks(polygons=polygons, resolution_wh=resolution_wh) - - rles = [np.array(image_annotation["segmentation"]["counts"]) - for image_annotation in image_annotations if image_annotation["iscrowd"]] - mask_rle = _rles_to_masks(rles = rles, resolution_wh = resolution_wh) - + mask = _annotations_to_mask(image_annotations, resolution_wh) return Detections( - class_id=np.asarray(class_ids, dtype=int), xyxy=xyxy, mask=_concatenate_annotation_masks(mask_polygon=mask_polygon, mask_rle=mask_rle) + class_id=np.asarray(class_ids, dtype=int), xyxy=xyxy, mask=mask ) return Detections(xyxy=xyxy, class_id=np.asarray(class_ids, dtype=int)) diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index ef627e89..610b9350 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -268,31 +268,80 @@ def test_group_coco_annotations_by_image_id( # 1 1 0 0 # 0 0 0 0 # 0 0 0 0 + ( + [ + mock_cock_coco_annotation( + category_id=0, + bbox=(0, 0, 10, 10), + area=10 * 10, + segmentation=[[0, 0, 4, 0, 4, 5, 9, 5, 9, 9, 0, 9]], + ), + mock_cock_coco_annotation( + category_id=0, + bbox=(5, 0, 5, 5), + area=5 * 5, + segmentation={ + "size": [20, 20], + "counts": [100, 5, 15, 5, 15, 5, 15, 5, 15, 5, 215], + }, + iscrowd=True, + ), + ], + (20, 20), + True, + Detections( + xyxy=np.array([[0, 0, 10, 10], [5, 0, 10, 5]], dtype=np.float32), + class_id=np.array([0, 0], dtype=int), + mask=np.array( + [ + np.array( + [ + 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 + for i in range(0, 20) + for j in range(0, 20) + ] + ).reshape((20, 20)), + np.array( + [ + 1 if j > 4 and j < 10 and i < 5 else 0 + for i in range(0, 20) + for j in range(0, 20) + ] + ).reshape((20, 20)), + ] + ), + ), + DoesNotRaise(), + ), # two image annotations with mask, one mask as polygon in in L-like shape, second as RLE in shape of square, like below (P = polygon, R = RLE): + # P R 0 0 + # P P 0 0 + # 0 0 0 0 + # 0 0 0 0 ( [ - mock_cock_coco_annotation( - category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, segmentation = [[0,0, 4,0, 4,5, 9,5, 9,9, 0,9]] - ), mock_cock_coco_annotation( category_id=0, bbox=(5, 0, 5, 5), area=5 * 5, segmentation = {'size':[20,20], 'counts':[100, 5, 15, 5, 15, 5, 15, 5, 15, 5, 215]}, iscrowd = True ), + mock_cock_coco_annotation( + category_id=1, bbox=(0, 0, 10, 10), area=10 * 10, segmentation = [[0,0, 4,0, 4,5, 9,5, 9,9, 0,9]] + ), ], (20, 20), True, Detections( xyxy=np.array( - [[0, 0, 10, 10], [5, 0, 10, 5]], dtype=np.float32 + [[5, 0, 10, 5], [0, 0, 10, 10]], dtype=np.float32 ), - class_id=np.array([0, 0], dtype=int), + class_id=np.array([0, 1], dtype=int), mask = np.array([ + np.array([1 if j>4 and j<10 and i<5 else 0 for i in range(0,20) for j in range(0,20)]).reshape((20,20)), np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((20,20)), - np.array([1 if j>4 and j<10 and i<5 else 0 for i in range(0,20) for j in range(0,20)]).reshape((20,20)) ]) ), DoesNotRaise(), - ), # two image annotations with mask, one mask as polygon in in L-like shape, second as RLE in shape of square, like below (P = polygon, R = RLE): + ), # two image annotations with mask, first mask as RLE in shape of square, second as polygon in in L-like shape, like below (P = polygon, R = RLE): # P R 0 0 # P P 0 0 # 0 0 0 0 From a941583bb0354de5b87ad1167e2b02198da11dd1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 21:07:49 +0000 Subject: [PATCH 019/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/formats/coco.py | 18 ++-- supervision/dataset/utils.py | 20 +++-- test/dataset/formats/test_coco.py | 122 +++++++++++++++++++++------- 3 files changed, 115 insertions(+), 45 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 34cb84ce..febeb8d6 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -61,12 +61,18 @@ def group_coco_annotations_by_image_id( def _annotations_to_mask(image_annotations: List[dict], resolution_wh: Tuple[int, int]): return np.array( [ - rle_to_mask(rle=np.array(image_annotation["segmentation"]["counts"]), - resolution_wh=resolution_wh) - if image_annotation["iscrowd"] - else - polygon_to_mask(polygon= np.reshape(np.asarray(image_annotation["segmentation"], dtype=np.int32), (-1, 2)), - resolution_wh=resolution_wh) + rle_to_mask( + rle=np.array(image_annotation["segmentation"]["counts"]), + resolution_wh=resolution_wh, + ) + if image_annotation["iscrowd"] + else polygon_to_mask( + polygon=np.reshape( + np.asarray(image_annotation["segmentation"], dtype=np.int32), + (-1, 2), + ), + resolution_wh=resolution_wh, + ) for image_annotation in image_annotations ], dtype=bool, diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index d1981bfe..de2511e5 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -133,22 +133,24 @@ def train_test_split( return data[:split_index], data[split_index:] -def rle_to_mask(rle: np.ndarray, resolution_wh: Tuple[int, int]) -> npt.NDArray[np.bool_]: +def rle_to_mask( + rle: np.ndarray, resolution_wh: Tuple[int, int] +) -> npt.NDArray[np.bool_]: """ Converts run-length encoding (RLE) to a binary mask. Args: - rle (np.ndarray): The 1D RLE array, the format used in the COCO dataset (column-wise encoding, + rle (np.ndarray): The 1D RLE array, the format used in the COCO dataset (column-wise encoding, values of an array with even indices represent the number of pixels assigned as background, values of an array with odd indices represent the number of pixels assigned as foreground object). resolution_wh (Tuple[int, int]): The width (w) and height (h) of the desired binary mask resolution. Returns: - npt.NDArray[np.bool_]: The generated 2D Boolean mask of shape (h,w), where the foreground object - is marked with `True`'s and the rest is filled with `False`'s. + npt.NDArray[np.bool_]: The generated 2D Boolean mask of shape (h,w), where the foreground object + is marked with `True`'s and the rest is filled with `False`'s. Examples: - rle = [2, 2, 2], resolution_wh = [3, 2] -> mask = [[False, True, False], + rle = [2, 2, 2], resolution_wh = [3, 2] -> mask = [[False, True, False], [False, True, False]] """ width, height = resolution_wh @@ -157,7 +159,7 @@ def rle_to_mask(rle: np.ndarray, resolution_wh: Tuple[int, int]) -> npt.NDArray[ zero_one_values[1::2] = 1 decoded_rle = np.repeat(zero_one_values, rle) - return decoded_rle.reshape((height,width), order="F") + return decoded_rle.reshape((height, width), order="F") def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: @@ -165,7 +167,7 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: Converts a binary mask into a run-length encoding (RLE). Args: - mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground object + mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground object and `False` indicates background. Returns: @@ -174,10 +176,10 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: Examples: mask = [[False, True, True], -> rle = [2, 4] - [False, True, True]] + [False, True, True]] mask = [[True, True, True], -> rle = [0, 6] - [True, True, True]] + [True, True, True]] """ rle = [] if mask[0][0] == 1: diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 610b9350..bb4a6e64 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -1,5 +1,5 @@ from contextlib import ExitStack as DoesNotRaise -from typing import Dict, List, Tuple, Union +from typing import Dict, List, Tuple import numpy as np import pytest @@ -232,7 +232,10 @@ def test_group_coco_annotations_by_image_id( ( [ mock_cock_coco_annotation( - category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, segmentation = [[0,0, 4,0, 4,5, 9,5, 9,9, 0,9]], + category_id=0, + bbox=(0, 0, 10, 10), + area=10 * 10, + segmentation=[[0, 0, 4, 0, 4, 5, 9, 5, 9, 9, 0, 9]], ) ], (20, 20), @@ -240,19 +243,53 @@ def test_group_coco_annotations_by_image_id( Detections( xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32), class_id=np.array([0], dtype=int), - mask = np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((1,20,20)) + mask=np.array( + [ + 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 + for i in range(0, 20) + for j in range(0, 20) + ] + ).reshape((1, 20, 20)), ), DoesNotRaise(), ), # single image annotations with mask, segmentation mask in L-like shape, like below: - # 1 0 0 0 - # 1 1 0 0 - # 0 0 0 0 - # 0 0 0 0 + # 1 0 0 0 + # 1 1 0 0 + # 0 0 0 0 + # 0 0 0 0 ( [ mock_cock_coco_annotation( - category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, - segmentation = {'size':[20,20], 'counts':[0, 10, 10, 10, 10, 10, 10, 10, 10, 10, 15, 5, 15, 5, 15, 5, 15, 5, 15, 5, 210]}, iscrowd = True + category_id=0, + bbox=(0, 0, 10, 10), + area=10 * 10, + segmentation={ + "size": [20, 20], + "counts": [ + 0, + 10, + 10, + 10, + 10, + 10, + 10, + 10, + 10, + 10, + 15, + 5, + 15, + 5, + 15, + 5, + 15, + 5, + 15, + 5, + 210, + ], + }, + iscrowd=True, ) ], (20, 20), @@ -260,14 +297,20 @@ def test_group_coco_annotations_by_image_id( Detections( xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32), class_id=np.array([0], dtype=int), - mask = np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((1,20,20)) + mask=np.array( + [ + 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 + for i in range(0, 20) + for j in range(0, 20) + ] + ).reshape((1, 20, 20)), ), DoesNotRaise(), ), # single image annotations with mask, RLE segmentation mask in L-like shape, like below: - # 1 0 0 0 - # 1 1 0 0 - # 0 0 0 0 - # 0 0 0 0 + # 1 0 0 0 + # 1 1 0 0 + # 0 0 0 0 + # 0 0 0 0 ( [ mock_cock_coco_annotation( @@ -317,36 +360,55 @@ def test_group_coco_annotations_by_image_id( # P P 0 0 # 0 0 0 0 # 0 0 0 0 - ( [ mock_cock_coco_annotation( - category_id=0, bbox=(5, 0, 5, 5), area=5 * 5, - segmentation = {'size':[20,20], 'counts':[100, 5, 15, 5, 15, 5, 15, 5, 15, 5, 215]}, iscrowd = True + category_id=0, + bbox=(5, 0, 5, 5), + area=5 * 5, + segmentation={ + "size": [20, 20], + "counts": [100, 5, 15, 5, 15, 5, 15, 5, 15, 5, 215], + }, + iscrowd=True, ), mock_cock_coco_annotation( - category_id=1, bbox=(0, 0, 10, 10), area=10 * 10, segmentation = [[0,0, 4,0, 4,5, 9,5, 9,9, 0,9]] + category_id=1, + bbox=(0, 0, 10, 10), + area=10 * 10, + segmentation=[[0, 0, 4, 0, 4, 5, 9, 5, 9, 9, 0, 9]], ), ], (20, 20), True, Detections( - xyxy=np.array( - [[5, 0, 10, 5], [0, 0, 10, 10]], dtype=np.float32 - ), + xyxy=np.array([[5, 0, 10, 5], [0, 0, 10, 10]], dtype=np.float32), class_id=np.array([0, 1], dtype=int), - mask = np.array([ - np.array([1 if j>4 and j<10 and i<5 else 0 for i in range(0,20) for j in range(0,20)]).reshape((20,20)), - np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((20,20)), - ]) + mask=np.array( + [ + np.array( + [ + 1 if j > 4 and j < 10 and i < 5 else 0 + for i in range(0, 20) + for j in range(0, 20) + ] + ).reshape((20, 20)), + np.array( + [ + 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 + for i in range(0, 20) + for j in range(0, 20) + ] + ).reshape((20, 20)), + ] + ), ), DoesNotRaise(), ), # two image annotations with mask, first mask as RLE in shape of square, second as polygon in in L-like shape, like below (P = polygon, R = RLE): - # P R 0 0 - # P P 0 0 - # 0 0 0 0 - # 0 0 0 0 - + # P R 0 0 + # P P 0 0 + # 0 0 0 0 + # 0 0 0 0 ], ) def test_coco_annotations_to_detections( From 88a43cc795c840b57beb0d159b1bcbd9a9bc1259 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Tue, 7 May 2024 00:22:24 +0200 Subject: [PATCH 020/182] unit tests for rle_to_mask and mask_to_rle functions --- supervision/dataset/utils.py | 4 +- test/dataset/test_utils.py | 98 ++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index de2511e5..53593a35 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -134,13 +134,13 @@ def train_test_split( def rle_to_mask( - rle: np.ndarray, resolution_wh: Tuple[int, int] + rle: npt.NDArray[np.int_], resolution_wh: Tuple[int, int] ) -> npt.NDArray[np.bool_]: """ Converts run-length encoding (RLE) to a binary mask. Args: - rle (np.ndarray): The 1D RLE array, the format used in the COCO dataset (column-wise encoding, + rle (npt.NDArray[np.int_]): The 1D RLE array, the format used in the COCO dataset (column-wise encoding, values of an array with even indices represent the number of pixels assigned as background, values of an array with odd indices represent the number of pixels assigned as foreground object). resolution_wh (Tuple[int, int]): The width (w) and height (h) of the desired binary mask resolution. diff --git a/test/dataset/test_utils.py b/test/dataset/test_utils.py index 5ca96ca5..9cefa438 100644 --- a/test/dataset/test_utils.py +++ b/test/dataset/test_utils.py @@ -2,6 +2,8 @@ from contextlib import ExitStack as DoesNotRaise from test.test_utils import mock_detections from typing import Dict, List, Optional, Tuple, TypeVar +import numpy as np +import numpy.typing as npt import pytest from supervision import Detections @@ -10,6 +12,8 @@ from supervision.dataset.utils import ( map_detections_class_id, merge_class_lists, train_test_split, + mask_to_rle, + rle_to_mask, ) T = TypeVar("T") @@ -229,3 +233,97 @@ def test_map_detections_class_id( source_to_target_mapping=source_to_target_mapping, detections=detections ) assert result == expected_result + + +@pytest.mark.parametrize( + "mask, expected_rle, exception", + [ + ( + np.zeros((3,3)).astype(bool), + [9], + DoesNotRaise(), + ), # mask with background only (mask with only False values) + ( + np.ones((3,3)).astype(bool), + [0, 9], + DoesNotRaise(), + ), # mask with foreground only (mask with only True values) + ( + np.array( + [[0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 0, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]] + ).astype(bool), + [6, 3, 2, 1, 1, 1, 2, 3, 6], + DoesNotRaise(), + ), # mask where foreground object has hole + ( + np.array( + [[1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1]] + ).astype(bool), + [0, 5, 5, 5, 5, 5], + DoesNotRaise(), + ), # mask where foreground consists of 3 separate components + ], +) +def test_mask_to_rle_convertion( + mask: npt.NDArray[np.bool_], expected_rle: List[int], exception: Exception +) -> None: + with exception: + result = mask_to_rle(mask=mask) + assert result == expected_rle + + +@pytest.mark.parametrize( + "rle, resolution_wh, expected_mask, exception", + [ + ( + [9], + [3, 3], + np.zeros((3,3)).astype(bool), + DoesNotRaise(), + ), # mask with background only (mask with only False values) + ( + [0, 9], + [3, 3], + np.ones((3,3)).astype(bool), + DoesNotRaise(), + ), # mask with foreground only (mask with only True values) + ( + [6, 3, 2, 1, 1, 1, 2, 3, 6], + [5, 5], + np.array( + [[0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 0, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]] + ).astype(bool), + DoesNotRaise(), + ), # mask where foreground object has hole + ( + [0, 5, 5, 5, 5, 5], + [5, 5], + np.array( + [[1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1]] + ).astype(bool), + DoesNotRaise(), + ), # mask where foreground consists of 3 separate components + ], +) +def test_rle_to_mask_convertion( + rle: npt.NDArray[np.int_], resolution_wh: Tuple[int, int],expected_mask: npt.NDArray[np.bool_], exception: Exception +) -> None: + with exception: + result = rle_to_mask(rle=rle, resolution_wh=resolution_wh) + assert np.all(result == expected_mask) From 6b5dacd8e3d4bfe8742c193d68fe4bbf235a7966 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 22:22:47 +0000 Subject: [PATCH 021/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/dataset/test_utils.py | 65 ++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/test/dataset/test_utils.py b/test/dataset/test_utils.py index 9cefa438..58f2ec22 100644 --- a/test/dataset/test_utils.py +++ b/test/dataset/test_utils.py @@ -10,10 +10,10 @@ from supervision import Detections from supervision.dataset.utils import ( build_class_index_mapping, map_detections_class_id, - merge_class_lists, - train_test_split, mask_to_rle, + merge_class_lists, rle_to_mask, + train_test_split, ) T = TypeVar("T") @@ -239,33 +239,37 @@ def test_map_detections_class_id( "mask, expected_rle, exception", [ ( - np.zeros((3,3)).astype(bool), + np.zeros((3, 3)).astype(bool), [9], DoesNotRaise(), ), # mask with background only (mask with only False values) ( - np.ones((3,3)).astype(bool), + np.ones((3, 3)).astype(bool), [0, 9], DoesNotRaise(), ), # mask with foreground only (mask with only True values) ( np.array( - [[0, 0, 0, 0, 0], - [0, 1, 1, 1, 0], - [0, 1, 0, 1, 0], - [0, 1, 1, 1, 0], - [0, 0, 0, 0, 0]] + [ + [0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 0, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0], + ] ).astype(bool), [6, 3, 2, 1, 1, 1, 2, 3, 6], DoesNotRaise(), ), # mask where foreground object has hole ( np.array( - [[1, 0, 1, 0, 1], - [1, 0, 1, 0, 1], - [1, 0, 1, 0, 1], - [1, 0, 1, 0, 1], - [1, 0, 1, 0, 1]] + [ + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + ] ).astype(bool), [0, 5, 5, 5, 5, 5], DoesNotRaise(), @@ -286,24 +290,26 @@ def test_mask_to_rle_convertion( ( [9], [3, 3], - np.zeros((3,3)).astype(bool), + np.zeros((3, 3)).astype(bool), DoesNotRaise(), ), # mask with background only (mask with only False values) ( [0, 9], [3, 3], - np.ones((3,3)).astype(bool), + np.ones((3, 3)).astype(bool), DoesNotRaise(), ), # mask with foreground only (mask with only True values) ( [6, 3, 2, 1, 1, 1, 2, 3, 6], [5, 5], np.array( - [[0, 0, 0, 0, 0], - [0, 1, 1, 1, 0], - [0, 1, 0, 1, 0], - [0, 1, 1, 1, 0], - [0, 0, 0, 0, 0]] + [ + [0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 0, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0], + ] ).astype(bool), DoesNotRaise(), ), # mask where foreground object has hole @@ -311,18 +317,23 @@ def test_mask_to_rle_convertion( [0, 5, 5, 5, 5, 5], [5, 5], np.array( - [[1, 0, 1, 0, 1], - [1, 0, 1, 0, 1], - [1, 0, 1, 0, 1], - [1, 0, 1, 0, 1], - [1, 0, 1, 0, 1]] + [ + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + ] ).astype(bool), DoesNotRaise(), ), # mask where foreground consists of 3 separate components ], ) def test_rle_to_mask_convertion( - rle: npt.NDArray[np.int_], resolution_wh: Tuple[int, int],expected_mask: npt.NDArray[np.bool_], exception: Exception + rle: npt.NDArray[np.int_], + resolution_wh: Tuple[int, int], + expected_mask: npt.NDArray[np.bool_], + exception: Exception, ) -> None: with exception: result = rle_to_mask(rle=rle, resolution_wh=resolution_wh) From a9f821c28a98430972441fd14bfe80051bdc6631 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Tue, 7 May 2024 17:34:13 +0200 Subject: [PATCH 022/182] Assertion error when number of pixel in RLE does not match the nuber of pixels computed as width*height --- supervision/dataset/utils.py | 6 ++++++ test/dataset/test_utils.py | 10 ++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index 53593a35..96f6a54b 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -149,12 +149,18 @@ def rle_to_mask( npt.NDArray[np.bool_]: The generated 2D Boolean mask of shape (h,w), where the foreground object is marked with `True`'s and the rest is filled with `False`'s. + Raises: + AssertionError: If the sum of pixels encoded in RLE differs from the + number of pixels in the expected mask (computed based on resolution_wh). + Examples: rle = [2, 2, 2], resolution_wh = [3, 2] -> mask = [[False, True, False], [False, True, False]] """ width, height = resolution_wh + assert width*height == np.sum(rle), "the sum of the number of pixels in the RLE must be the same as the number of pixels in the expected mask" + zero_one_values = np.zeros_like(rle) zero_one_values[1::2] = 1 diff --git a/test/dataset/test_utils.py b/test/dataset/test_utils.py index 9cefa438..d88e8349 100644 --- a/test/dataset/test_utils.py +++ b/test/dataset/test_utils.py @@ -272,7 +272,7 @@ def test_map_detections_class_id( ), # mask where foreground consists of 3 separate components ], ) -def test_mask_to_rle_convertion( +def test_mask_to_rle_conversion( mask: npt.NDArray[np.bool_], expected_rle: List[int], exception: Exception ) -> None: with exception: @@ -319,9 +319,15 @@ def test_mask_to_rle_convertion( ).astype(bool), DoesNotRaise(), ), # mask where foreground consists of 3 separate components + ( + [0, 5, 5, 5, 5, 5], + [5, 5], + None, + pytest.raises(AssertionError), + ), # mask where foreground consists of 3 separate components ], ) -def test_rle_to_mask_convertion( +def test_rle_to_mask_conversion( rle: npt.NDArray[np.int_], resolution_wh: Tuple[int, int],expected_mask: npt.NDArray[np.bool_], exception: Exception ) -> None: with exception: From d59ec0f2237616b188e1c30201904713385f9734 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 May 2024 15:36:45 +0000 Subject: [PATCH 023/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index 96f6a54b..0bb432f4 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -150,16 +150,18 @@ def rle_to_mask( is marked with `True`'s and the rest is filled with `False`'s. Raises: - AssertionError: If the sum of pixels encoded in RLE differs from the + AssertionError: If the sum of pixels encoded in RLE differs from the number of pixels in the expected mask (computed based on resolution_wh). - + Examples: rle = [2, 2, 2], resolution_wh = [3, 2] -> mask = [[False, True, False], [False, True, False]] """ width, height = resolution_wh - assert width*height == np.sum(rle), "the sum of the number of pixels in the RLE must be the same as the number of pixels in the expected mask" + assert ( + width * height == np.sum(rle) + ), "the sum of the number of pixels in the RLE must be the same as the number of pixels in the expected mask" zero_one_values = np.zeros_like(rle) zero_one_values[1::2] = 1 From aac1d8862a35c8c1b741b224cf5db8fed64a3cec Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Tue, 7 May 2024 18:01:14 +0200 Subject: [PATCH 024/182] assertion for valid mask in mask_to_rle function --- supervision/dataset/utils.py | 6 ++++++ test/dataset/test_utils.py | 19 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index 0bb432f4..0167dc39 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -182,6 +182,9 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: List[int]: the run-length encoded mask. Values of a list with even indices represent the number of pixels assigned as background (`False`), values of a list with odd indices represent the number of pixels assigned as foreground object (`True`). + Raises: + AssertionError: If imput mask is not 2D or is empty. + Examples: mask = [[False, True, True], -> rle = [2, 4] [False, True, True]] @@ -189,6 +192,9 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: mask = [[True, True, True], -> rle = [0, 6] [True, True, True]] """ + assert mask.ndim == 2, "Input mask must be 2D" + assert mask.size != 0, "Input mask cannot be empty" + rle = [] if mask[0][0] == 1: rle = [0] diff --git a/test/dataset/test_utils.py b/test/dataset/test_utils.py index b810f204..0bf8ecb4 100644 --- a/test/dataset/test_utils.py +++ b/test/dataset/test_utils.py @@ -274,6 +274,20 @@ def test_map_detections_class_id( [0, 5, 5, 5, 5, 5], DoesNotRaise(), ), # mask where foreground consists of 3 separate components + ( + np.array( + [[[]]] + ).astype(bool), + None, + pytest.raises(AssertionError), + ), # raises AssertionError because mask dimentionality is not 2D + ( + np.array( + [[]] + ).astype(bool), + None, + pytest.raises(AssertionError), + ), # raises AssertionError because mask is empty ], ) def test_mask_to_rle_conversion( @@ -329,10 +343,11 @@ def test_mask_to_rle_conversion( ), # mask where foreground consists of 3 separate components ( [0, 5, 5, 5, 5, 5], - [5, 5], + [2, 2], None, pytest.raises(AssertionError), - ), # mask where foreground consists of 3 separate components + ), # raises AssertionError because number of pixels in RLE does not match + # number of pixels in expected mask (width x height). ], ) def test_rle_to_mask_convertion( From 36a301e7fccbb1ffe78aef984046e70f56170279 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 May 2024 16:01:35 +0000 Subject: [PATCH 025/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/dataset/test_utils.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/test/dataset/test_utils.py b/test/dataset/test_utils.py index 0bf8ecb4..e269dde7 100644 --- a/test/dataset/test_utils.py +++ b/test/dataset/test_utils.py @@ -275,19 +275,15 @@ def test_map_detections_class_id( DoesNotRaise(), ), # mask where foreground consists of 3 separate components ( - np.array( - [[[]]] - ).astype(bool), + np.array([[[]]]).astype(bool), None, pytest.raises(AssertionError), - ), # raises AssertionError because mask dimentionality is not 2D + ), # raises AssertionError because mask dimentionality is not 2D ( - np.array( - [[]] - ).astype(bool), + np.array([[]]).astype(bool), None, pytest.raises(AssertionError), - ), # raises AssertionError because mask is empty + ), # raises AssertionError because mask is empty ], ) def test_mask_to_rle_conversion( @@ -346,8 +342,8 @@ def test_mask_to_rle_conversion( [2, 2], None, pytest.raises(AssertionError), - ), # raises AssertionError because number of pixels in RLE does not match - # number of pixels in expected mask (width x height). + ), # raises AssertionError because number of pixels in RLE does not match + # number of pixels in expected mask (width x height). ], ) def test_rle_to_mask_convertion( From 649e27323dbd8c282cc14c245a48b4f27d372357 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Wed, 8 May 2024 12:49:25 +0200 Subject: [PATCH 026/182] fix the line lengths --- supervision/dataset/formats/coco.py | 3 ++- supervision/dataset/utils.py | 29 ++++++++++++++++++----------- test/dataset/formats/test_coco.py | 12 ++++++++---- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index febeb8d6..0489cd08 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -123,7 +123,8 @@ def detections_to_coco_annotations( )[0].flatten() ) # todo: flag for when to use RLE? - # segmentation = {"counts": mask_to_rle(binary_mask=mask), "size": list(mask.shape[:2])} + # segmentation = {"counts": mask_to_rle(binary_mask=mask), + # "size": list(mask.shape[:2])} coco_annotation = { "id": annotation_id, "image_id": image_id, diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index 0167dc39..bd7d7b57 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -140,14 +140,18 @@ def rle_to_mask( Converts run-length encoding (RLE) to a binary mask. Args: - rle (npt.NDArray[np.int_]): The 1D RLE array, the format used in the COCO dataset (column-wise encoding, - values of an array with even indices represent the number of pixels assigned as background, - values of an array with odd indices represent the number of pixels assigned as foreground object). - resolution_wh (Tuple[int, int]): The width (w) and height (h) of the desired binary mask resolution. + rle (npt.NDArray[np.int_]): The 1D RLE array, the format used in the COCO + dataset (column-wise encoding, values of an array with even indices + represent the number of pixels assigned as background, + values of an array with odd indices represent the number of pixels + assigned as foreground object). + resolution_wh (Tuple[int, int]): The width (w) and height (h) + of the desired binary mask resolution. Returns: - npt.NDArray[np.bool_]: The generated 2D Boolean mask of shape (h,w), where the foreground object - is marked with `True`'s and the rest is filled with `False`'s. + npt.NDArray[np.bool_]: The generated 2D Boolean mask of shape (h,w), + where the foreground object is marked with `True`'s and the rest + is filled with `False`'s. Raises: AssertionError: If the sum of pixels encoded in RLE differs from the @@ -161,7 +165,8 @@ def rle_to_mask( assert ( width * height == np.sum(rle) - ), "the sum of the number of pixels in the RLE must be the same as the number of pixels in the expected mask" + ), ("the sum of the number of pixels in the RLE must be the same " + "as the number of pixels in the expected mask") zero_one_values = np.zeros_like(rle) zero_one_values[1::2] = 1 @@ -175,12 +180,14 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: Converts a binary mask into a run-length encoding (RLE). Args: - mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground object - and `False` indicates background. + mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground + object and `False` indicates background. Returns: - List[int]: the run-length encoded mask. Values of a list with even indices represent the number of pixels assigned as background (`False`), - values of a list with odd indices represent the number of pixels assigned as foreground object (`True`). + List[int]: the run-length encoded mask. Values of a list with even indices + represent the number of pixels assigned as background (`False`), values + of a list with odd indices represent the number of pixels assigned + as foreground object (`True`). Raises: AssertionError: If imput mask is not 2D or is empty. diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index bb4a6e64..12ff4f00 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -252,7 +252,8 @@ def test_group_coco_annotations_by_image_id( ).reshape((1, 20, 20)), ), DoesNotRaise(), - ), # single image annotations with mask, segmentation mask in L-like shape, like below: + ), # single image annotations with mask, segmentation mask in L-like shape, + # like below: # 1 0 0 0 # 1 1 0 0 # 0 0 0 0 @@ -306,7 +307,8 @@ def test_group_coco_annotations_by_image_id( ).reshape((1, 20, 20)), ), DoesNotRaise(), - ), # single image annotations with mask, RLE segmentation mask in L-like shape, like below: + ), # single image annotations with mask, RLE segmentation mask in L-like shape, + # like below: # 1 0 0 0 # 1 1 0 0 # 0 0 0 0 @@ -355,7 +357,8 @@ def test_group_coco_annotations_by_image_id( ), ), DoesNotRaise(), - ), # two image annotations with mask, one mask as polygon in in L-like shape, second as RLE in shape of square, like below (P = polygon, R = RLE): + ), # two image annotations with mask, one mask as polygon in in L-like shape, + # second as RLE in shape of square, like below (P = polygon, R = RLE): # P R 0 0 # P P 0 0 # 0 0 0 0 @@ -404,7 +407,8 @@ def test_group_coco_annotations_by_image_id( ), ), DoesNotRaise(), - ), # two image annotations with mask, first mask as RLE in shape of square, second as polygon in in L-like shape, like below (P = polygon, R = RLE): + ), # two image annotations with mask, first mask as RLE in shape of square, + # second as polygon in in L-like shape, like below (P = polygon, R = RLE): # P R 0 0 # P P 0 0 # 0 0 0 0 From 008711aac479453496b0ee9f7655d0b427670bdc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 May 2024 10:50:30 +0000 Subject: [PATCH 027/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/formats/coco.py | 2 +- supervision/dataset/utils.py | 28 ++++++++++++++-------------- test/dataset/formats/test_coco.py | 8 ++++---- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 0489cd08..4beb1dcd 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -123,7 +123,7 @@ def detections_to_coco_annotations( )[0].flatten() ) # todo: flag for when to use RLE? - # segmentation = {"counts": mask_to_rle(binary_mask=mask), + # segmentation = {"counts": mask_to_rle(binary_mask=mask), # "size": list(mask.shape[:2])} coco_annotation = { "id": annotation_id, diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index bd7d7b57..93242e2b 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -140,17 +140,17 @@ def rle_to_mask( Converts run-length encoding (RLE) to a binary mask. Args: - rle (npt.NDArray[np.int_]): The 1D RLE array, the format used in the COCO - dataset (column-wise encoding, values of an array with even indices + rle (npt.NDArray[np.int_]): The 1D RLE array, the format used in the COCO + dataset (column-wise encoding, values of an array with even indices represent the number of pixels assigned as background, - values of an array with odd indices represent the number of pixels + values of an array with odd indices represent the number of pixels assigned as foreground object). - resolution_wh (Tuple[int, int]): The width (w) and height (h) + resolution_wh (Tuple[int, int]): The width (w) and height (h) of the desired binary mask resolution. Returns: - npt.NDArray[np.bool_]: The generated 2D Boolean mask of shape (h,w), - where the foreground object is marked with `True`'s and the rest + npt.NDArray[np.bool_]: The generated 2D Boolean mask of shape (h,w), + where the foreground object is marked with `True`'s and the rest is filled with `False`'s. Raises: @@ -163,10 +163,10 @@ def rle_to_mask( """ width, height = resolution_wh - assert ( - width * height == np.sum(rle) - ), ("the sum of the number of pixels in the RLE must be the same " - "as the number of pixels in the expected mask") + assert width * height == np.sum(rle), ( + "the sum of the number of pixels in the RLE must be the same " + "as the number of pixels in the expected mask" + ) zero_one_values = np.zeros_like(rle) zero_one_values[1::2] = 1 @@ -180,13 +180,13 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: Converts a binary mask into a run-length encoding (RLE). Args: - mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground + mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground object and `False` indicates background. Returns: - List[int]: the run-length encoded mask. Values of a list with even indices - represent the number of pixels assigned as background (`False`), values - of a list with odd indices represent the number of pixels assigned + List[int]: the run-length encoded mask. Values of a list with even indices + represent the number of pixels assigned as background (`False`), values + of a list with odd indices represent the number of pixels assigned as foreground object (`True`). Raises: diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 12ff4f00..f47e796d 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -252,7 +252,7 @@ def test_group_coco_annotations_by_image_id( ).reshape((1, 20, 20)), ), DoesNotRaise(), - ), # single image annotations with mask, segmentation mask in L-like shape, + ), # single image annotations with mask, segmentation mask in L-like shape, # like below: # 1 0 0 0 # 1 1 0 0 @@ -307,7 +307,7 @@ def test_group_coco_annotations_by_image_id( ).reshape((1, 20, 20)), ), DoesNotRaise(), - ), # single image annotations with mask, RLE segmentation mask in L-like shape, + ), # single image annotations with mask, RLE segmentation mask in L-like shape, # like below: # 1 0 0 0 # 1 1 0 0 @@ -357,7 +357,7 @@ def test_group_coco_annotations_by_image_id( ), ), DoesNotRaise(), - ), # two image annotations with mask, one mask as polygon in in L-like shape, + ), # two image annotations with mask, one mask as polygon in in L-like shape, # second as RLE in shape of square, like below (P = polygon, R = RLE): # P R 0 0 # P P 0 0 @@ -407,7 +407,7 @@ def test_group_coco_annotations_by_image_id( ), ), DoesNotRaise(), - ), # two image annotations with mask, first mask as RLE in shape of square, + ), # two image annotations with mask, first mask as RLE in shape of square, # second as polygon in in L-like shape, like below (P = polygon, R = RLE): # P R 0 0 # P P 0 0 From c47b0ce9907fcc776a6a803d97acf9be7bd7770a Mon Sep 17 00:00:00 2001 From: Raif Olson Date: Wed, 8 May 2024 11:07:14 -0400 Subject: [PATCH 028/182] filter out detections with zero area. --- supervision/tracker/byte_tracker/core.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/supervision/tracker/byte_tracker/core.py b/supervision/tracker/byte_tracker/core.py index ce3bbbbf..132bf391 100644 --- a/supervision/tracker/byte_tracker/core.py +++ b/supervision/tracker/byte_tracker/core.py @@ -362,6 +362,13 @@ class ByteTrack: scores = tensors[:, 4] bboxes = tensors[:, :4] + bbox_areas = (bboxes[:, 2] - bboxes[:, 0]) * (bboxes[:, 3] - bboxes[:, 1]) + valid_box_inds = bbox_areas > 0 + + class_ids = class_ids[valid_box_inds] + scores = scores[valid_box_inds] + bboxes = bboxes[valid_box_inds] + remain_inds = scores > self.track_activation_threshold inds_low = scores > 0.1 inds_high = scores < self.track_activation_threshold From e4e66f8a9a9ab132776285cde21107e4c69dba6e Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Fri, 10 May 2024 10:16:34 +0200 Subject: [PATCH 029/182] faster mask to rle --- supervision/dataset/utils.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index bd7d7b57..6789d17a 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -202,11 +202,18 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: assert mask.ndim == 2, "Input mask must be 2D" assert mask.size != 0, "Input mask cannot be empty" - rle = [] - if mask[0][0] == 1: - rle = [0] + on_value_change_indices = np.where(mask.ravel(order='F') != + np.roll(mask.ravel(order='F'),1))[0] + + on_value_change_indices = np.append(on_value_change_indices, mask.size) + # need to add 0 at the beginning when the same value is in the first and + # last element of the flattened mask + if on_value_change_indices[0] != 0: + on_value_change_indices = np.insert(on_value_change_indices, 0, 0) - for _, group in groupby(mask.ravel(order="F")): - rle.append(len(list(group))) + rle = np.diff(on_value_change_indices) - return rle + if mask[0][0]==1: + rle = np.insert(rle, 0, 0) + + return list(rle) From 7bd92b096fbe5a4a6377884ca7c78fd3a9fe556c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 10 May 2024 08:18:35 +0000 Subject: [PATCH 030/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/utils.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index 4fbd1230..9b5ae3cd 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -1,7 +1,6 @@ import copy import os import random -from itertools import groupby from pathlib import Path from typing import Dict, List, Optional, Tuple, TypeVar @@ -202,18 +201,19 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: assert mask.ndim == 2, "Input mask must be 2D" assert mask.size != 0, "Input mask cannot be empty" - on_value_change_indices = np.where(mask.ravel(order='F') != - np.roll(mask.ravel(order='F'),1))[0] - + on_value_change_indices = np.where( + mask.ravel(order="F") != np.roll(mask.ravel(order="F"), 1) + )[0] + on_value_change_indices = np.append(on_value_change_indices, mask.size) - # need to add 0 at the beginning when the same value is in the first and + # need to add 0 at the beginning when the same value is in the first and # last element of the flattened mask - if on_value_change_indices[0] != 0: - on_value_change_indices = np.insert(on_value_change_indices, 0, 0) + if on_value_change_indices[0] != 0: + on_value_change_indices = np.insert(on_value_change_indices, 0, 0) rle = np.diff(on_value_change_indices) - if mask[0][0]==1: - rle = np.insert(rle, 0, 0) + if mask[0][0] == 1: + rle = np.insert(rle, 0, 0) return list(rle) From 8c857dd9a61999b3d7d7c26568a855f617eb6f43 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Sun, 12 May 2024 23:02:22 +0200 Subject: [PATCH 031/182] speed up rle to mask --- supervision/dataset/utils.py | 6 ++++-- test/dataset/test_utils.py | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index 4fbd1230..82491871 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -168,10 +168,12 @@ def rle_to_mask( "as the number of pixels in the expected mask" ) - zero_one_values = np.zeros_like(rle) + zero_one_values = np.zeros(shape = (rle.size,1), dtype=np.uint8) zero_one_values[1::2] = 1 - decoded_rle = np.repeat(zero_one_values, rle) + decoded_rle = np.repeat(zero_one_values, rle, axis=0) + decoded_rle = np.append(decoded_rle, + np.zeros(width * height - len(decoded_rle), dtype=np.uint8)) return decoded_rle.reshape((height, width), order="F") diff --git a/test/dataset/test_utils.py b/test/dataset/test_utils.py index e269dde7..e30b4e5f 100644 --- a/test/dataset/test_utils.py +++ b/test/dataset/test_utils.py @@ -298,19 +298,19 @@ def test_mask_to_rle_conversion( "rle, resolution_wh, expected_mask, exception", [ ( - [9], + np.array([9]), [3, 3], np.zeros((3, 3)).astype(bool), DoesNotRaise(), ), # mask with background only (mask with only False values) ( - [0, 9], + np.array([0, 9]), [3, 3], np.ones((3, 3)).astype(bool), DoesNotRaise(), ), # mask with foreground only (mask with only True values) ( - [6, 3, 2, 1, 1, 1, 2, 3, 6], + np.array([6, 3, 2, 1, 1, 1, 2, 3, 6]), [5, 5], np.array( [ @@ -324,7 +324,7 @@ def test_mask_to_rle_conversion( DoesNotRaise(), ), # mask where foreground object has hole ( - [0, 5, 5, 5, 5, 5], + np.array([0, 5, 5, 5, 5, 5]), [5, 5], np.array( [ @@ -338,7 +338,7 @@ def test_mask_to_rle_conversion( DoesNotRaise(), ), # mask where foreground consists of 3 separate components ( - [0, 5, 5, 5, 5, 5], + np.array([0, 5, 5, 5, 5, 5]), [2, 2], None, pytest.raises(AssertionError), From 14b57345a763a0a4e414a64649882b6054571b78 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 12 May 2024 21:31:16 +0000 Subject: [PATCH 032/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index 61f3b1c8..b0b5577e 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -167,12 +167,13 @@ def rle_to_mask( "as the number of pixels in the expected mask" ) - zero_one_values = np.zeros(shape = (rle.size,1), dtype=np.uint8) + zero_one_values = np.zeros(shape=(rle.size, 1), dtype=np.uint8) zero_one_values[1::2] = 1 decoded_rle = np.repeat(zero_one_values, rle, axis=0) - decoded_rle = np.append(decoded_rle, - np.zeros(width * height - len(decoded_rle), dtype=np.uint8)) + decoded_rle = np.append( + decoded_rle, np.zeros(width * height - len(decoded_rle), dtype=np.uint8) + ) return decoded_rle.reshape((height, width), order="F") From d21c98a4d2fda92300d4931f952fcc0e91669959 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Mon, 13 May 2024 12:06:38 +0200 Subject: [PATCH 033/182] docs updated; `rle_to_mask` and `mask_to_rle` added to `__init__.py` --- docs/{datasets.md => datasets/core.md} | 1 + docs/datasets/utils.md | 18 ++++++++ mkdocs.yml | 4 +- supervision/__init__.py | 4 ++ supervision/dataset/utils.py | 60 ++++++++++++++++++-------- test/dataset/test_utils.py | 12 ++++-- 6 files changed, 76 insertions(+), 23 deletions(-) rename docs/{datasets.md => datasets/core.md} (97%) create mode 100644 docs/datasets/utils.md diff --git a/docs/datasets.md b/docs/datasets/core.md similarity index 97% rename from docs/datasets.md rename to docs/datasets/core.md index 73931515..03d0c196 100644 --- a/docs/datasets.md +++ b/docs/datasets/core.md @@ -1,5 +1,6 @@ --- comments: true +status: new --- # Datasets diff --git a/docs/datasets/utils.md b/docs/datasets/utils.md new file mode 100644 index 00000000..6be56303 --- /dev/null +++ b/docs/datasets/utils.md @@ -0,0 +1,18 @@ +--- +comments: true +status: new +--- + +# Datasets Utils + + + +:::supervision.dataset.utils.rle_to_mask + + + +:::supervision.dataset.utils.mask_to_rle diff --git a/mkdocs.yml b/mkdocs.yml index cf206a82..281c40c9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -61,7 +61,9 @@ nav: - Detection Smoother: detection/tools/smoother.md - Save Detections: detection/tools/save_detections.md - Trackers: trackers.md - - Datasets: datasets.md + - Datasets: + - Core: datasets/core.md + - Utils: datasets/utils.md - Utils: - Video: utils/video.md - Image: utils/image.md diff --git a/supervision/__init__.py b/supervision/__init__.py index bb526514..b7f7a5e8 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -34,6 +34,10 @@ from supervision.dataset.core import ( ClassificationDataset, DetectionDataset, ) +from supervision.dataset.utils import ( + rle_to_mask, + mask_to_rle, +) from supervision.detection.annotate import BoxAnnotator from supervision.detection.core import Detections from supervision.detection.line_zone import LineZone, LineZoneAnnotator diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index b0b5577e..ed7062b9 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -2,7 +2,7 @@ import copy import os import random from pathlib import Path -from typing import Dict, List, Optional, Tuple, TypeVar +from typing import Dict, List, Optional, Tuple, Union, TypeVar import cv2 import numpy as np @@ -133,33 +133,42 @@ def train_test_split( def rle_to_mask( - rle: npt.NDArray[np.int_], resolution_wh: Tuple[int, int] + rle: Union[npt.NDArray[np.int_], List[int]], resolution_wh: Tuple[int, int] ) -> npt.NDArray[np.bool_]: """ Converts run-length encoding (RLE) to a binary mask. Args: - rle (npt.NDArray[np.int_]): The 1D RLE array, the format used in the COCO - dataset (column-wise encoding, values of an array with even indices - represent the number of pixels assigned as background, + rle (Union[npt.NDArray[np.int_], List[int]]): The 1D RLE array, the format + used in the COCO dataset (column-wise encoding, values of an array with + even indices represent the number of pixels assigned as background, values of an array with odd indices represent the number of pixels assigned as foreground object). resolution_wh (Tuple[int, int]): The width (w) and height (h) - of the desired binary mask resolution. + of the desired binary mask. Returns: - npt.NDArray[np.bool_]: The generated 2D Boolean mask of shape (h,w), - where the foreground object is marked with `True`'s and the rest - is filled with `False`'s. + The generated 2D Boolean mask of shape `(h, w)`, where the foreground object is + marked with `True`'s and the rest is filled with `False`'s. Raises: AssertionError: If the sum of pixels encoded in RLE differs from the - number of pixels in the expected mask (computed based on resolution_wh). + number of pixels in the expected mask (computed based on resolution_wh). Examples: - rle = [2, 2, 2], resolution_wh = [3, 2] -> mask = [[False, True, False], - [False, True, False]] + ```python + import supervision as sv + + sv.rle_to_mask([2, 2, 2], (3, 2)) + # array([ + # [False, True, False], + # [False, True, False] + # ]) + ``` """ + if isinstance(rle, list): + rle = np.array(rle, dtype=int) + width, height = resolution_wh assert width * height == np.sum(rle), ( @@ -186,20 +195,33 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: object and `False` indicates background. Returns: - List[int]: the run-length encoded mask. Values of a list with even indices + The run-length encoded mask. Values of a list with even indices represent the number of pixels assigned as background (`False`), values of a list with odd indices represent the number of pixels assigned as foreground object (`True`). Raises: - AssertionError: If imput mask is not 2D or is empty. + AssertionError: If input mask is not 2D or is empty. - Examples: - mask = [[False, True, True], -> rle = [2, 4] - [False, True, True]] + Examples: + ```python + import numpy as np + import supervision as sv - mask = [[True, True, True], -> rle = [0, 6] - [True, True, True]] + mask = np.array([ + [False, True, True], + [False, True, True] + ]) + sv.mask_to_rle(mask) + # [2, 4] + + mask = np.array([ + [True, True, True], + [True, True, True] + ]) + sv.mask_to_rle(mask) + # [0, 6] + ``` """ assert mask.ndim == 2, "Input mask must be 2D" assert mask.size != 0, "Input mask cannot be empty" diff --git a/test/dataset/test_utils.py b/test/dataset/test_utils.py index e30b4e5f..41e1da5b 100644 --- a/test/dataset/test_utils.py +++ b/test/dataset/test_utils.py @@ -286,7 +286,7 @@ def test_map_detections_class_id( ), # raises AssertionError because mask is empty ], ) -def test_mask_to_rle_conversion( +def test_mask_to_rle( mask: npt.NDArray[np.bool_], expected_rle: List[int], exception: Exception ) -> None: with exception: @@ -302,7 +302,13 @@ def test_mask_to_rle_conversion( [3, 3], np.zeros((3, 3)).astype(bool), DoesNotRaise(), - ), # mask with background only (mask with only False values) + ), # mask with background only (mask with only False values); rle as array + ( + [9], + [3, 3], + np.zeros((3, 3)).astype(bool), + DoesNotRaise(), + ), # mask with background only (mask with only False values); rle as list ( np.array([0, 9]), [3, 3], @@ -346,7 +352,7 @@ def test_mask_to_rle_conversion( # number of pixels in expected mask (width x height). ], ) -def test_rle_to_mask_convertion( +def test_rle_to_mask( rle: npt.NDArray[np.int_], resolution_wh: Tuple[int, int], expected_mask: npt.NDArray[np.bool_], From 9a1c11218c8c247a3203ba76c501fe2aac08e1ba Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 10:06:55 +0000 Subject: [PATCH 034/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/__init__.py | 5 +---- supervision/dataset/utils.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/supervision/__init__.py b/supervision/__init__.py index b7f7a5e8..2bc72944 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -34,10 +34,7 @@ from supervision.dataset.core import ( ClassificationDataset, DetectionDataset, ) -from supervision.dataset.utils import ( - rle_to_mask, - mask_to_rle, -) +from supervision.dataset.utils import mask_to_rle, rle_to_mask from supervision.detection.annotate import BoxAnnotator from supervision.detection.core import Detections from supervision.detection.line_zone import LineZone, LineZoneAnnotator diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index ed7062b9..4efc7194 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -2,7 +2,7 @@ import copy import os import random from pathlib import Path -from typing import Dict, List, Optional, Tuple, Union, TypeVar +from typing import Dict, List, Optional, Tuple, TypeVar, Union import cv2 import numpy as np From 59b273ba87e30e406e8036560bd0c3348a86287c Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Mon, 13 May 2024 12:25:25 +0200 Subject: [PATCH 035/182] small refactor --- supervision/dataset/formats/coco.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 4beb1dcd..fc62ac66 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -5,6 +5,7 @@ from typing import Dict, List, Tuple import cv2 import numpy as np +import numpy.typing as npt from supervision.dataset.utils import ( approximate_mask_with_polygons, @@ -58,7 +59,10 @@ def group_coco_annotations_by_image_id( return annotations -def _annotations_to_mask(image_annotations: List[dict], resolution_wh: Tuple[int, int]): +def coco_annotations_to_masks( + image_annotations: List[dict], + resolution_wh: Tuple[int, int] +) -> npt.NDArray[np.bool_]: return np.array( [ rle_to_mask( @@ -93,7 +97,10 @@ def coco_annotations_to_detections( xyxy[:, 2:4] += xyxy[:, 0:2] if with_masks: - mask = _annotations_to_mask(image_annotations, resolution_wh) + mask = coco_annotations_to_masks( + image_annotations=image_annotations, + resolution_wh=resolution_wh + ) return Detections( class_id=np.asarray(class_ids, dtype=int), xyxy=xyxy, mask=mask ) From cccacab62143010e75be4217a9af3c26e4ea3ffc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 10:25:46 +0000 Subject: [PATCH 036/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/formats/coco.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index fc62ac66..b6b2490a 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -60,8 +60,7 @@ def group_coco_annotations_by_image_id( def coco_annotations_to_masks( - image_annotations: List[dict], - resolution_wh: Tuple[int, int] + image_annotations: List[dict], resolution_wh: Tuple[int, int] ) -> npt.NDArray[np.bool_]: return np.array( [ @@ -98,8 +97,7 @@ def coco_annotations_to_detections( if with_masks: mask = coco_annotations_to_masks( - image_annotations=image_annotations, - resolution_wh=resolution_wh + image_annotations=image_annotations, resolution_wh=resolution_wh ) return Detections( class_id=np.asarray(class_ids, dtype=int), xyxy=xyxy, mask=mask From c3b77d05c09f4a0192fb48aa95ab6ef701c557ed Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Tue, 14 May 2024 17:12:19 +0300 Subject: [PATCH 037/182] Rename, remove functions, unit-test & change `merge_object_detection_pair` --- supervision/detection/core.py | 164 ++++++++++++++++----------------- supervision/detection/utils.py | 29 +----- test/detection/test_core.py | 129 +++++++++++++++++++++++++- 3 files changed, 213 insertions(+), 109 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index d56ba516..0777571f 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -8,8 +8,9 @@ import numpy as np from supervision.config import CLASS_NAME_DATA_FIELD, ORIENTED_BOX_COORDINATES from supervision.detection.utils import ( - batch_non_max_merge, + box_batch_non_max_merge, box_iou_batch, + box_non_max_merge, box_non_max_suppression, calculate_masks_centroids, extract_ultralytics_masks, @@ -18,7 +19,6 @@ from supervision.detection.utils import ( mask_non_max_suppression, mask_to_xyxy, merge_data, - non_max_merge, process_roboflow_result, xywh_to_xyxy, ) @@ -1213,7 +1213,7 @@ class Detections: if class_agnostic: predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1))) - keep_to_merge_list = non_max_merge(predictions, threshold) + keep_to_merge_list = box_non_max_merge(predictions, threshold) else: assert self.class_id is not None, ( "Detections class_id must be given for NMS to be executed. If you" @@ -1226,14 +1226,14 @@ class Detections: self.class_id.reshape(-1, 1), ) ) - keep_to_merge_list = batch_non_max_merge(predictions, threshold) + keep_to_merge_list = box_batch_non_max_merge(predictions, threshold) result = [] for keep_ind, merge_ind_list in keep_to_merge_list.items(): for merge_ind in merge_ind_list: box_iou = box_iou_batch(self[keep_ind].xyxy, self[merge_ind].xyxy)[0] if box_iou > threshold: - merged_detection = self._merge_object_detection_pair( + merged_detection = self.merge_object_detection_pair( self[keep_ind], self[merge_ind] ) self._set_at_index(keep_ind, merged_detection) @@ -1241,99 +1241,95 @@ class Detections: return Detections.merge(result) - @staticmethod - def _merge_object_detection_pair(det1: Detections, det2: Detections) -> Detections: - """ - Merges two Detections object into a single Detections object. - Assumes each Detections contains exactly one object. - A `winning` detection is determined based on the confidence score of the two - input detections. This winning detection is then used to specify which - `class_id`, `tracker_id`, and `data` to include in the merged Detections object. +def merge_object_detection_pair(det1: Detections, det2: Detections) -> Detections: + """ + Merges two Detections object into a single Detections object. + Assumes each Detections contains exactly one object. - The resulting `confidence` of the merged object is calculated by the weighted - contribution of each detection to the merged object. - The bounding boxes and masks of the two input detections are merged into a - single bounding box and mask, respectively. + A `winning` detection is determined based on the confidence score of the two + input detections. This winning detection is then used to specify which + `class_id`, `tracker_id`, and `data` to include in the merged Detections object. - Args: - det1 (Detections): - The first Detections object - det2 (Detections): - The second Detections object + The resulting `confidence` of the merged object is calculated by the weighted + contribution of ea detection to the merged object. + The bounding boxes and masks of the two input detections are merged into a + single bounding box and mask, respectively. - Returns: - Detections: A new Detections object, with merged attributes. + Args: + det1 (Detections): + The first Detections object + det2 (Detections): + The second Detections object - Raises: - ValueError: If the input Detections objects do not have exactly 1 detected - object. + Returns: + Detections: A new Detections object, with merged attributes. - Example: - ```python - import cv2 - import supervision as sv - from inference import get_model + Raises: + ValueError: If the input Detections objects do not have exactly 1 detected + object. - image = cv2.imread() - model = get_model(model_id="yolov8s-640") + Example: + ```python + import cv2 + import supervision as sv + from inference import get_model - result = model.infer(image)[0] - detections = sv.Detections.from_inference(result) + image = cv2.imread() + model = get_model(model_id="yolov8s-640") - merged_detections = merge_object_detection_pair( - detections[0], detections[1]) - ``` - """ - if len(det1) != 1 or len(det2) != 1: - raise ValueError("Both Detections should have exactly 1 detected object.") + result = model.infer(image)[0] + detections = sv.Detections.from_inference(result) - if det2.confidence is None: - winning_det = det1 - elif det1.confidence is None: - winning_det = det2 - elif det1.confidence[0] >= det2.confidence[0]: - winning_det = det1 - else: - winning_det = det2 + merged_detections = merge_object_detection_pair( + detections[0], detections[1]) + ``` + """ + if len(det1) != 1 or len(det2) != 1: + raise ValueError("Both Detections should have exactly 1 detected object.") - area_det1 = (det1.xyxy[0][2] - det1.xyxy[0][0]) * ( - det1.xyxy[0][3] - det1.xyxy[0][1] - ) - area_det2 = (det2.xyxy[0][2] - det2.xyxy[0][0]) * ( - det2.xyxy[0][3] - det2.xyxy[0][1] - ) + if det2.confidence is None: + winning_det = det1 + elif det1.confidence is None: + winning_det = det2 + elif det1.confidence[0] >= det2.confidence[0]: + winning_det = det1 + else: + winning_det = det2 - merged_x1, merged_y1 = np.minimum(det1.xyxy[0][:2], det2.xyxy[0][:2]) - merged_x2, merged_y2 = np.maximum(det1.xyxy[0][2:], det2.xyxy[0][2:]) + area_det1 = (det1.xyxy[0][2] - det1.xyxy[0][0]) * ( + det1.xyxy[0][3] - det1.xyxy[0][1] + ) + area_det2 = (det2.xyxy[0][2] - det2.xyxy[0][0]) * ( + det2.xyxy[0][3] - det2.xyxy[0][1] + ) - merged_xy = np.array([[merged_x1, merged_y1, merged_x2, merged_y2]]) + merged_x1, merged_y1 = np.minimum(det1.xyxy[0][:2], det2.xyxy[0][:2]) + merged_x2, merged_y2 = np.maximum(det1.xyxy[0][2:], det2.xyxy[0][2:]) + merged_xy = np.array([[merged_x1, merged_y1, merged_x2, merged_y2]]) - winning_class_id = winning_det.class_id + if det2.mask is None or det1.mask is None: + merged_mask = winning_det.mask + else: + merged_mask = np.logical_or(det1.mask, det2.mask) - if det1.confidence is None or det2.confidence is None: - merged_confidence = None - else: - merged_confidence = ( - area_det1 * det1.confidence[0] + area_det2 * det2.confidence[0] - ) / (area_det1 + area_det2) - merged_confidence = np.array([merged_confidence]) + if det1.confidence is None or det2.confidence is None: + merged_confidence = winning_det.confidence + else: + merged_confidence = ( + area_det1 * det1.confidence[0] + area_det2 * det2.confidence[0] + ) / (area_det1 + area_det2) + merged_confidence = np.array([merged_confidence]) - merged_mask = None - if det1.mask is not None and det2.mask is not None: - merged_mask = np.logical_or(det1.mask, det2.mask) + winning_class_id = winning_det.class_id + winning_tracker_id = winning_det.tracker_id + winning_data = winning_det.data - winning_tracker_id = winning_det.tracker_id - - winning_data = None - if det1.data and det2.data: - winning_data = winning_det.data - - return Detections( - xyxy=merged_xy, - mask=merged_mask, - confidence=merged_confidence, - class_id=winning_class_id, - tracker_id=winning_tracker_id, - data=winning_data, - ) + return Detections( + xyxy=merged_xy, + mask=merged_mask, + confidence=merged_confidence, + class_id=winning_class_id, + tracker_id=winning_tracker_id, + data=winning_data, + ) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index d2e403a4..bd20ab37 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -274,7 +274,7 @@ def box_non_max_suppression( return keep[sort_index.argsort()] -def non_max_merge( +def box_non_max_merge( predictions: np.ndarray, threshold: float = 0.5 ) -> Dict[int, List[int]]: """ @@ -353,7 +353,7 @@ def non_max_merge( return keep_to_merge_list -def batch_non_max_merge( +def box_batch_non_max_merge( predictions: np.ndarray, threshold: float = 0.5 ) -> Dict[int, List[int]]: """ @@ -375,7 +375,9 @@ def batch_non_max_merge( keep_to_merge_list = {} for category_id in np.unique(category_ids): curr_indices = np.where(category_ids == category_id)[0] - curr_keep_to_merge_list = non_max_merge(predictions[curr_indices], threshold) + curr_keep_to_merge_list = box_non_max_merge( + predictions[curr_indices], threshold + ) curr_indices_list = curr_indices.tolist() for curr_keep, curr_merge_list in curr_keep_to_merge_list.items(): keep = curr_indices_list[curr_keep] @@ -384,27 +386,6 @@ def batch_non_max_merge( return keep_to_merge_list -def get_merged_bbox(bbox1: np.ndarray, bbox2: np.ndarray) -> np.ndarray: - """ - Merges two bounding boxes into one. - - Args: - bbox1 (np.ndarray): A numpy array of shape `(, 4)` where the - row corresponds to a bounding box in - the format `(x_min, y_min, x_max, y_max)`. - bbox2 (np.ndarray): A numpy array of shape `(, 4)` where the - row corresponds to a bounding box in - the format `(x_min, y_min, x_max, y_max)`. - - Returns: - np.ndarray: A numpy array of shape `(, 4)` where the new - bounding box is the merged bounding box of `bbox1` and `bbox2`. - """ - left_top = np.minimum(bbox1[0][:2], bbox2[0][:2]) - right_bottom = np.maximum(bbox1[0][2:], bbox2[0][2:]) - return np.array([np.concatenate([left_top, right_bottom])]) - - def clip_boxes(xyxy: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray: """ Clips bounding boxes coordinates to fit within the frame resolution. diff --git a/test/detection/test_core.py b/test/detection/test_core.py index 12f3de28..31e56dec 100644 --- a/test/detection/test_core.py +++ b/test/detection/test_core.py @@ -5,7 +5,7 @@ from typing import List, Optional, Union import numpy as np import pytest -from supervision.detection.core import Detections +from supervision.detection.core import Detections, merge_object_detection_pair from supervision.geometry.core import Position PREDICTIONS = np.array( @@ -421,3 +421,130 @@ def test_equal( detections_a: Detections, detections_b: Detections, expected_result: bool ) -> None: assert (detections_a == detections_b) == expected_result + + +@pytest.mark.parametrize( + "detection_1, detection_2, expected_result, exception", + [ + ( + mock_detections( + xyxy=[[10, 10, 30, 30]], + ), + mock_detections( + xyxy=[[10, 10, 30, 30]], + ), + mock_detections( + xyxy=[[10, 10, 30, 30]], + ), + DoesNotRaise(), + ), # Merge with self + ( + mock_detections( + xyxy=[[10, 10, 30, 30]], + ), + Detections.empty(), + None, + pytest.raises(ValueError), + ), # merge with empty: error + ( + mock_detections( + xyxy=[[10, 10, 30, 30]], + ), + mock_detections( + xyxy=[[10, 10, 30, 30], [40, 40, 60, 60]], + ), + None, + pytest.raises(ValueError), + ), # merge with 2+ objects: error + ( + mock_detections( + xyxy=[[10, 10, 30, 30]], + confidence=[0.1], + class_id=[1], + mask=[np.array([[1, 1, 0], [1, 1, 0], [0, 0, 0]], dtype=bool)], + tracker_id=[1], + data={"key_1": [1]}, + ), + mock_detections( + xyxy=[[20, 20, 40, 40]], + confidence=[0.1], + class_id=[2], + mask=[np.array([[0, 0, 0], [0, 1, 1], [0, 1, 1]], dtype=bool)], + tracker_id=[2], + data={"key_2": [2]}, + ), + mock_detections( + xyxy=[[10, 10, 40, 40]], + confidence=[0.1], + class_id=[1], + mask=[np.array([[1, 1, 0], [1, 1, 1], [0, 1, 1]], dtype=bool)], + tracker_id=[1], + data={"key_1": [1]}, + ), + DoesNotRaise(), + ), # Same confidence - merge box & mask, tiebreak to detection_1 + ( + mock_detections( + xyxy=[[0, 0, 20, 20]], + confidence=[0.1], + class_id=[1], + mask=[np.array([[1, 1, 0], [1, 1, 0], [0, 0, 0]], dtype=bool)], + tracker_id=[1], + data={"key_1": [1]}, + ), + mock_detections( + xyxy=[[10, 10, 50, 50]], + confidence=[0.2], + class_id=[2], + mask=[np.array([[0, 0, 0], [0, 1, 1], [0, 1, 1]], dtype=bool)], + tracker_id=[2], + data={"key_2": [2]}, + ), + mock_detections( + xyxy=[[0, 0, 50, 50]], + confidence=[(1 * 0.1 + 4 * 0.2) / 5], + class_id=[2], + mask=[np.array([[1, 1, 0], [1, 1, 1], [0, 1, 1]], dtype=bool)], + tracker_id=[2], + data={"key_2": [2]}, + ), + DoesNotRaise(), + ), # Different confidence, different area + ( + mock_detections( + xyxy=[[0, 0, 20, 20]], + confidence=None, + class_id=[1], + mask=[np.array([[1, 1, 0], [1, 1, 0], [0, 0, 0]], dtype=bool)], + tracker_id=[1], + data={"key_1": [1]}, + ), + mock_detections( + xyxy=[[10, 10, 30, 30]], + confidence=[0.2], + class_id=[2], + mask=[np.array([[0, 0, 0], [0, 1, 1], [0, 1, 1]], dtype=bool)], + tracker_id=[2], + data={"key_2": [2]}, + ), + mock_detections( + xyxy=[[0, 0, 30, 30]], + confidence=[0.2], + class_id=[2], + mask=[np.array([[1, 1, 0], [1, 1, 1], [0, 1, 1]], dtype=bool)], + tracker_id=[2], + data={"key_2": [2]}, + ), + DoesNotRaise(), + ), # merge with no confidence + ], +) +def test_merge_object_detection_pair( + detection_1: Detections, + detection_2: Detections, + expected_result: Optional[Detections], + exception: Exception, +): + with exception: + result = merge_object_detection_pair(detection_1, detection_2) + assert result == expected_result From 8014e88944b9f1135448761b0c7f0832df7589ae Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Tue, 14 May 2024 17:42:47 +0300 Subject: [PATCH 038/182] Test box_non_max_merge --- supervision/detection/utils.py | 6 +- test/detection/test_utils.py | 126 +++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 3 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index bd20ab37..f177d088 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -275,7 +275,7 @@ def box_non_max_suppression( def box_non_max_merge( - predictions: np.ndarray, threshold: float = 0.5 + predictions: np.ndarray, iou_threshold: float = 0.5 ) -> Dict[int, List[int]]: """ Apply greedy version of non-maximum merging to avoid detecting too many @@ -285,7 +285,7 @@ def box_non_max_merge( predictions (np.ndarray): An array of shape `(n, 5)` containing the bounding boxes coordinates in format `[x1, y1, x2, y2]` and the confidence scores. - threshold (float, optional): The intersection-over-union threshold + iou_threshold (float, optional): The intersection-over-union threshold to use for non-maximum suppression. Defaults to 0.5. Returns: @@ -338,7 +338,7 @@ def box_non_max_merge( union = (rem_areas - inter) + areas[idx] match_metric_value = inter / union - mask = match_metric_value < threshold + mask = match_metric_value < iou_threshold mask = mask.astype(np.uint8) matched_box_indices = np.flip(order[np.where(mask == 0)[0]]) unmatched_indices = order[np.where(mask == 1)[0]] diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index 097c5c6e..e6f33084 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -6,6 +6,7 @@ import pytest from supervision.config import CLASS_NAME_DATA_FIELD from supervision.detection.utils import ( + box_non_max_merge, box_non_max_suppression, calculate_masks_centroids, clip_boxes, @@ -127,6 +128,131 @@ def test_box_non_max_suppression( assert np.array_equal(result, expected_result) +@pytest.mark.parametrize( + "predictions, iou_threshold, expected_result, exception", + [ + ( + np.empty(shape=(0, 5), dtype=float), + 0.5, + {}, + DoesNotRaise(), + ), + ( + np.array([[0, 0, 10, 10, 1.0]]), + 0.5, + {0: []}, + DoesNotRaise(), + ), + ( + np.array([[0, 0, 10, 10, 1.0], [0, 0, 9, 9, 1.0]]), + 0.5, + {1: [0]}, + DoesNotRaise(), + ), # High overlap, tie-break to second det + ( + np.array([[0, 0, 10, 10, 1.0], [0, 0, 9, 9, 0.99]]), + 0.5, + {0: [1]}, + DoesNotRaise(), + ), # High overlap, merge to high confidence + ( + np.array([[0, 0, 10, 10, 0.99], [0, 0, 9, 9, 1.0]]), + 0.5, + {1: [0]}, + DoesNotRaise(), + ), # (test symmetry) High overlap, merge to high confidence + ( + np.array([[0, 0, 10, 10, 0.99], [0, 0, 9, 9, 1.0]]), + 0.5, + {1: [0]}, + DoesNotRaise(), + ), # (test symmetry) High overlap, merge to high confidence + ( + np.array([[0, 0, 10, 10, 1.0], [0, 0, 9, 9, 1.0]]), + 1.0, + {0: [], 1: []}, + DoesNotRaise(), + ), # High IOU required + ( + np.array([[0, 0, 10, 10, 1.0], [0, 0, 9, 9, 1.0]]), + 0.0, + {1: [0]}, + DoesNotRaise(), + ), # No IOU required + ( + np.array([[0, 0, 10, 10, 1.0], [0, 0, 5, 5, 0.9]]), + 0.25, + {0: [1]}, + DoesNotRaise(), + ), # Below IOU requirement + ( + np.array([[0, 0, 10, 10, 1.0], [0, 0, 5, 5, 0.9]]), + 0.26, + {0: [], 1: []}, + DoesNotRaise(), + ), # Above IOU requirement + ( + np.array([[0, 0, 10, 10, 1.0], [0, 0, 9, 9, 1.0], [0, 0, 8, 8, 1.0]]), + 0.5, + {2: [1, 0]}, + DoesNotRaise(), + ), # 3 boxes + ( + np.array( + [ + [0, 0, 10, 10, 1.0], + [0, 0, 9, 9, 1.0], + [5, 5, 10, 10, 1.0], + [6, 6, 10, 10, 1.0], + [9, 9, 10, 10, 1.0], + ] + ), + 0.5, + {1: [0], 3: [2], 4: []}, + DoesNotRaise(), + ), # 5 boxes, 2 merges, 1 separate + ( + np.array( + [ + [0, 0, 2, 1, 1.0], + [1, 0, 3, 1, 1.0], + [2, 0, 4, 1, 1.0], + [3, 0, 5, 1, 1.0], + [4, 0, 6, 1, 1.0], + ] + ), + 0.33, + {0: [], 2: [1], 4: [3]}, + DoesNotRaise(), + ), # sequential merge, half overlap + ( + np.array( + [ + [0, 0, 2, 1, 0.9], + [1, 0, 3, 1, 0.9], + [2, 0, 4, 1, 1.0], + [3, 0, 5, 1, 0.9], + [4, 0, 6, 1, 0.9], + ] + ), + 0.33, + {0: [], 2: [3, 1], 4: []}, + DoesNotRaise(), + ), # confidence + ], +) +def test_box_non_max_merge( + predictions: np.ndarray, + iou_threshold: float, + expected_result: Dict[int, List[int]], + exception: Exception, +) -> None: + with exception: + result = box_non_max_merge(predictions=predictions, iou_threshold=iou_threshold) + + assert result == expected_result + + @pytest.mark.parametrize( "predictions, masks, iou_threshold, expected_result, exception", [ From 26bafec8f732ae921fc44ac068e9ed564a067331 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 15 May 2024 09:23:34 +0300 Subject: [PATCH 039/182] Test box_non_max_merge, rename threshold,to __init__ --- supervision/__init__.py | 4 +++- supervision/detection/core.py | 4 ++-- supervision/detection/utils.py | 8 ++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/supervision/__init__.py b/supervision/__init__.py index 16de484a..3eae2e17 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -35,7 +35,7 @@ from supervision.dataset.core import ( DetectionDataset, ) from supervision.detection.annotate import BoxAnnotator -from supervision.detection.core import Detections +from supervision.detection.core import Detections, merge_object_detection_pair from supervision.detection.line_zone import LineZone, LineZoneAnnotator from supervision.detection.tools.csv_sink import CSVSink from supervision.detection.tools.inference_slicer import InferenceSlicer @@ -43,7 +43,9 @@ from supervision.detection.tools.json_sink import JSONSink from supervision.detection.tools.polygon_zone import PolygonZone, PolygonZoneAnnotator from supervision.detection.tools.smoother import DetectionsSmoother from supervision.detection.utils import ( + batch_box_non_max_merge, box_iou_batch, + box_non_max_merge, box_non_max_suppression, calculate_masks_centroids, clip_boxes, diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 0777571f..1b3a385d 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -8,7 +8,7 @@ import numpy as np from supervision.config import CLASS_NAME_DATA_FIELD, ORIENTED_BOX_COORDINATES from supervision.detection.utils import ( - box_batch_non_max_merge, + batch_box_non_max_merge, box_iou_batch, box_non_max_merge, box_non_max_suppression, @@ -1226,7 +1226,7 @@ class Detections: self.class_id.reshape(-1, 1), ) ) - keep_to_merge_list = box_batch_non_max_merge(predictions, threshold) + keep_to_merge_list = batch_box_non_max_merge(predictions, threshold) result = [] for keep_ind, merge_ind_list in keep_to_merge_list.items(): diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index f177d088..c2f02c1b 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -353,8 +353,8 @@ def box_non_max_merge( return keep_to_merge_list -def box_batch_non_max_merge( - predictions: np.ndarray, threshold: float = 0.5 +def batch_box_non_max_merge( + predictions: np.ndarray, iou_threshold: float = 0.5 ) -> Dict[int, List[int]]: """ Apply greedy version of non-maximum merging per category to avoid detecting @@ -364,7 +364,7 @@ def box_batch_non_max_merge( predictions (np.ndarray): An array of shape `(n, 6)` containing the bounding boxes coordinates in format `[x1, y1, x2, y2]`, the confidence scores and class_ids. - threshold (float, optional): The intersection-over-union threshold + iou_threshold (float, optional): The intersection-over-union threshold to use for non-maximum suppression. Defaults to 0.5. Returns: @@ -376,7 +376,7 @@ def box_batch_non_max_merge( for category_id in np.unique(category_ids): curr_indices = np.where(category_ids == category_id)[0] curr_keep_to_merge_list = box_non_max_merge( - predictions[curr_indices], threshold + predictions[curr_indices], iou_threshold ) curr_indices_list = curr_indices.tolist() for curr_keep, curr_merge_list in curr_keep_to_merge_list.items(): From d2d50fbe467ca3fec33e46619c63ac0548ced50b Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 15 May 2024 09:26:18 +0300 Subject: [PATCH 040/182] renamed bbox -> xyxy --- supervision/detection/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index c2f02c1b..f6308f57 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -458,7 +458,7 @@ def mask_to_xyxy(masks: np.ndarray) -> np.ndarray: `(x_min, y_min, x_max, y_max)` for each mask """ n = masks.shape[0] - bboxes = np.zeros((n, 4), dtype=int) + xyxy = np.zeros((n, 4), dtype=int) for i, mask in enumerate(masks): rows, cols = np.where(mask) @@ -466,9 +466,9 @@ def mask_to_xyxy(masks: np.ndarray) -> np.ndarray: if len(rows) > 0 and len(cols) > 0: x_min, x_max = np.min(cols), np.max(cols) y_min, y_max = np.min(rows), np.max(rows) - bboxes[i, :] = [x_min, y_min, x_max, y_max] + xyxy[i, :] = [x_min, y_min, x_max, y_max] - return bboxes + return xyxy def mask_to_polygons(mask: np.ndarray) -> List[np.ndarray]: From 2d740bdcb6b197f6aefe7436a718191c53884042 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 15 May 2024 09:38:58 +0300 Subject: [PATCH 041/182] fix: merge_object_detection_pair --- 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 1b3a385d..76224bb7 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -1233,7 +1233,7 @@ class Detections: for merge_ind in merge_ind_list: box_iou = box_iou_batch(self[keep_ind].xyxy, self[merge_ind].xyxy)[0] if box_iou > threshold: - merged_detection = self.merge_object_detection_pair( + merged_detection = merge_object_detection_pair( self[keep_ind], self[merge_ind] ) self._set_at_index(keep_ind, merged_detection) From aab861c2db5ae2b641285a225fbeafe6206a6d02 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Wed, 15 May 2024 08:55:47 +0200 Subject: [PATCH 042/182] test_coco_annotations_to_detections result matrices defined in place --- test/dataset/formats/test_coco.py | 216 +++++++++++------------------- 1 file changed, 79 insertions(+), 137 deletions(-) diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index f47e796d..bfde8495 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -233,186 +233,128 @@ def test_group_coco_annotations_by_image_id( [ mock_cock_coco_annotation( category_id=0, - bbox=(0, 0, 10, 10), - area=10 * 10, - segmentation=[[0, 0, 4, 0, 4, 5, 9, 5, 9, 9, 0, 9]], + bbox=(0, 0, 5, 5), + area= 5 * 5, + segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]], ) ], - (20, 20), + (5, 5), True, Detections( - xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32), + xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), class_id=np.array([0], dtype=int), - mask=np.array( - [ - 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 - for i in range(0, 20) - for j in range(0, 20) - ] - ).reshape((1, 20, 20)), + mask=np.array([[[1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1]]]), ), DoesNotRaise(), - ), # single image annotations with mask, segmentation mask in L-like shape, - # like below: - # 1 0 0 0 - # 1 1 0 0 - # 0 0 0 0 - # 0 0 0 0 + ), # single image annotations with mask as polygon ( [ mock_cock_coco_annotation( category_id=0, - bbox=(0, 0, 10, 10), - area=10 * 10, - segmentation={ - "size": [20, 20], - "counts": [ - 0, - 10, - 10, - 10, - 10, - 10, - 10, - 10, - 10, - 10, - 15, - 5, - 15, - 5, - 15, - 5, - 15, - 5, - 15, - 5, - 210, - ], - }, - iscrowd=True, - ) - ], - (20, 20), - True, - Detections( - xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32), - class_id=np.array([0], dtype=int), - mask=np.array( - [ - 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 - for i in range(0, 20) - for j in range(0, 20) - ] - ).reshape((1, 20, 20)), - ), - DoesNotRaise(), - ), # single image annotations with mask, RLE segmentation mask in L-like shape, - # like below: - # 1 0 0 0 - # 1 1 0 0 - # 0 0 0 0 - # 0 0 0 0 - ( - [ - mock_cock_coco_annotation( - category_id=0, - bbox=(0, 0, 10, 10), - area=10 * 10, - segmentation=[[0, 0, 4, 0, 4, 5, 9, 5, 9, 9, 0, 9]], - ), - mock_cock_coco_annotation( - category_id=0, - bbox=(5, 0, 5, 5), + bbox=(0, 0, 5, 5), area=5 * 5, segmentation={ - "size": [20, 20], - "counts": [100, 5, 15, 5, 15, 5, 15, 5, 15, 5, 215], + "size": [5, 5], + "counts": [0, 15, 2, 3, 2, 3], + }, + iscrowd=True, + ) + ], + (5, 5), + True, + Detections( + xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), + class_id=np.array([0], dtype=int), + mask=np.array([[[1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1]]]), + ), + DoesNotRaise(), + ), # single image annotations with mask, RLE segmentation mask + ( + [ + mock_cock_coco_annotation( + category_id=0, + bbox=(0, 0, 5, 5), + area= 5 * 5, + segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]], + ), + mock_cock_coco_annotation( + category_id=0, + bbox=(3, 0, 2, 2), + area=2 * 2, + segmentation={ + "size": [5, 5], + "counts": [15, 2, 3, 2, 3], }, iscrowd=True, ), ], - (20, 20), + (5, 5), True, Detections( - xyxy=np.array([[0, 0, 10, 10], [5, 0, 10, 5]], dtype=np.float32), + xyxy=np.array([[0, 0, 5, 5], [3, 0, 5, 2]], dtype=np.float32), class_id=np.array([0, 0], dtype=int), - mask=np.array( - [ - np.array( - [ - 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 - for i in range(0, 20) - for j in range(0, 20) - ] - ).reshape((20, 20)), - np.array( - [ - 1 if j > 4 and j < 10 and i < 5 else 0 - for i in range(0, 20) - for j in range(0, 20) - ] - ).reshape((20, 20)), - ] - ), + mask=np.array([ [[1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1]], + [[0, 0, 0, 1, 1], + [0, 0, 0, 1, 1], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]]]) ), DoesNotRaise(), - ), # two image annotations with mask, one mask as polygon in in L-like shape, - # second as RLE in shape of square, like below (P = polygon, R = RLE): - # P R 0 0 - # P P 0 0 - # 0 0 0 0 - # 0 0 0 0 + ), # two image annotations with mask, one mask as polygon ans second as RLE ( [ mock_cock_coco_annotation( category_id=0, - bbox=(5, 0, 5, 5), - area=5 * 5, + bbox=(3, 0, 2, 2), + area=2 * 2, segmentation={ - "size": [20, 20], - "counts": [100, 5, 15, 5, 15, 5, 15, 5, 15, 5, 215], + "size": [5, 5], + "counts": [15, 2, 3, 2, 3], }, iscrowd=True, ), mock_cock_coco_annotation( category_id=1, - bbox=(0, 0, 10, 10), - area=10 * 10, - segmentation=[[0, 0, 4, 0, 4, 5, 9, 5, 9, 9, 0, 9]], + bbox=(0, 0, 5, 5), + area= 5 * 5, + segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]], ), ], - (20, 20), + (5, 5), True, Detections( - xyxy=np.array([[5, 0, 10, 5], [0, 0, 10, 10]], dtype=np.float32), + xyxy=np.array([[3, 0, 5, 2], [0, 0, 5, 5]], dtype=np.float32), class_id=np.array([0, 1], dtype=int), mask=np.array( [ - np.array( - [ - 1 if j > 4 and j < 10 and i < 5 else 0 - for i in range(0, 20) - for j in range(0, 20) - ] - ).reshape((20, 20)), - np.array( - [ - 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 - for i in range(0, 20) - for j in range(0, 20) - ] - ).reshape((20, 20)), + [[0, 0, 0, 1, 1], + [0, 0, 0, 1, 1], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]], + [[1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1]] ] ), ), DoesNotRaise(), - ), # two image annotations with mask, first mask as RLE in shape of square, - # second as polygon in in L-like shape, like below (P = polygon, R = RLE): - # P R 0 0 - # P P 0 0 - # 0 0 0 0 - # 0 0 0 0 + ), # two image annotations with mask, first mask as RLE and second as polygon ], ) def test_coco_annotations_to_detections( From f08404eb7d73b001e860c4b56ccc799cdfa6ce8e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 06:56:06 +0000 Subject: [PATCH 043/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/dataset/formats/test_coco.py | 92 +++++++++++++++++++------------ 1 file changed, 58 insertions(+), 34 deletions(-) diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index bfde8495..66e73c0e 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -234,7 +234,7 @@ def test_group_coco_annotations_by_image_id( mock_cock_coco_annotation( category_id=0, bbox=(0, 0, 5, 5), - area= 5 * 5, + area=5 * 5, segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]], ) ], @@ -243,14 +243,20 @@ def test_group_coco_annotations_by_image_id( Detections( xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), class_id=np.array([0], dtype=int), - mask=np.array([[[1, 1, 1, 0, 0], - [1, 1, 1, 0, 0], - [1, 1, 1, 1, 1], - [1, 1, 1, 1, 1], - [1, 1, 1, 1, 1]]]), + mask=np.array( + [ + [ + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + ] + ] + ), ), DoesNotRaise(), - ), # single image annotations with mask as polygon + ), # single image annotations with mask as polygon ( [ mock_cock_coco_annotation( @@ -269,11 +275,17 @@ def test_group_coco_annotations_by_image_id( Detections( xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), class_id=np.array([0], dtype=int), - mask=np.array([[[1, 1, 1, 0, 0], - [1, 1, 1, 0, 0], - [1, 1, 1, 1, 1], - [1, 1, 1, 1, 1], - [1, 1, 1, 1, 1]]]), + mask=np.array( + [ + [ + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + ] + ] + ), ), DoesNotRaise(), ), # single image annotations with mask, RLE segmentation mask @@ -282,7 +294,7 @@ def test_group_coco_annotations_by_image_id( mock_cock_coco_annotation( category_id=0, bbox=(0, 0, 5, 5), - area= 5 * 5, + area=5 * 5, segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]], ), mock_cock_coco_annotation( @@ -301,16 +313,24 @@ def test_group_coco_annotations_by_image_id( Detections( xyxy=np.array([[0, 0, 5, 5], [3, 0, 5, 2]], dtype=np.float32), class_id=np.array([0, 0], dtype=int), - mask=np.array([ [[1, 1, 1, 0, 0], - [1, 1, 1, 0, 0], - [1, 1, 1, 1, 1], - [1, 1, 1, 1, 1], - [1, 1, 1, 1, 1]], - [[0, 0, 0, 1, 1], - [0, 0, 0, 1, 1], - [0, 0, 0, 0, 0], - [0, 0, 0, 0, 0], - [0, 0, 0, 0, 0]]]) + mask=np.array( + [ + [ + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + ], + [ + [0, 0, 0, 1, 1], + [0, 0, 0, 1, 1], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + ] + ), ), DoesNotRaise(), ), # two image annotations with mask, one mask as polygon ans second as RLE @@ -329,7 +349,7 @@ def test_group_coco_annotations_by_image_id( mock_cock_coco_annotation( category_id=1, bbox=(0, 0, 5, 5), - area= 5 * 5, + area=5 * 5, segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]], ), ], @@ -340,16 +360,20 @@ def test_group_coco_annotations_by_image_id( class_id=np.array([0, 1], dtype=int), mask=np.array( [ - [[0, 0, 0, 1, 1], - [0, 0, 0, 1, 1], - [0, 0, 0, 0, 0], - [0, 0, 0, 0, 0], - [0, 0, 0, 0, 0]], - [[1, 1, 1, 0, 0], - [1, 1, 1, 0, 0], - [1, 1, 1, 1, 1], - [1, 1, 1, 1, 1], - [1, 1, 1, 1, 1]] + [ + [0, 0, 0, 1, 1], + [0, 0, 0, 1, 1], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + [ + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + ], ] ), ), From 145b5fe56c1b1daec6e8161fece90a5f23155c76 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 15 May 2024 10:46:56 +0300 Subject: [PATCH 044/182] Rename to batch_box_non_max_merge to box_non_max_merge_batch --- supervision/__init__.py | 2 +- supervision/detection/core.py | 4 ++-- supervision/detection/utils.py | 8 +------- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/supervision/__init__.py b/supervision/__init__.py index 3eae2e17..03f52086 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -43,9 +43,9 @@ from supervision.detection.tools.json_sink import JSONSink from supervision.detection.tools.polygon_zone import PolygonZone, PolygonZoneAnnotator from supervision.detection.tools.smoother import DetectionsSmoother from supervision.detection.utils import ( - batch_box_non_max_merge, box_iou_batch, box_non_max_merge, + box_non_max_merge_batch, box_non_max_suppression, calculate_masks_centroids, clip_boxes, diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 76224bb7..2489ef80 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -8,9 +8,9 @@ import numpy as np from supervision.config import CLASS_NAME_DATA_FIELD, ORIENTED_BOX_COORDINATES from supervision.detection.utils import ( - batch_box_non_max_merge, box_iou_batch, box_non_max_merge, + box_non_max_merge_batch, box_non_max_suppression, calculate_masks_centroids, extract_ultralytics_masks, @@ -1226,7 +1226,7 @@ class Detections: self.class_id.reshape(-1, 1), ) ) - keep_to_merge_list = batch_box_non_max_merge(predictions, threshold) + keep_to_merge_list = box_non_max_merge_batch(predictions, threshold) result = [] for keep_ind, merge_ind_list in keep_to_merge_list.items(): diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index f6308f57..c159de59 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -300,18 +300,12 @@ def box_non_max_merge( y2 = predictions[:, 3] scores = predictions[:, 4] - areas = (x2 - x1) * (y2 - y1) order = scores.argsort() - keep = [] - while len(order) > 0: idx = order[-1] - - keep.append(idx.tolist()) - order = order[:-1] if len(order) == 0: @@ -353,7 +347,7 @@ def box_non_max_merge( return keep_to_merge_list -def batch_box_non_max_merge( +def box_non_max_merge_batch( predictions: np.ndarray, iou_threshold: float = 0.5 ) -> Dict[int, List[int]]: """ From 6c4093526607b4b37db4f2bcb05087ef53db83ad Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 15 May 2024 11:32:30 +0300 Subject: [PATCH 045/182] box_non_max_merge: use our functions to compute iou --- supervision/detection/utils.py | 35 +++++----------------------------- 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index c159de59..cb254552 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -294,14 +294,7 @@ def box_non_max_merge( """ keep_to_merge_list = {} - x1 = predictions[:, 0] - y1 = predictions[:, 1] - x2 = predictions[:, 2] - y2 = predictions[:, 3] - scores = predictions[:, 4] - areas = (x2 - x1) * (y2 - y1) - order = scores.argsort() while len(order) > 0: @@ -312,30 +305,12 @@ def box_non_max_merge( keep_to_merge_list[idx.tolist()] = [] break - xx1 = np.take(x1, axis=0, indices=order) - xx2 = np.take(x2, axis=0, indices=order) - yy1 = np.take(y1, axis=0, indices=order) - yy2 = np.take(y2, axis=0, indices=order) + candidate = np.expand_dims(predictions[idx], axis=0) + ious = box_iou_batch(predictions[order][:, :4], candidate[:, :4]) - xx1 = np.maximum(xx1, x1[idx]) - yy1 = np.maximum(yy1, y1[idx]) - xx2 = np.minimum(xx2, x2[idx]) - yy2 = np.minimum(yy2, y2[idx]) - - w = np.maximum(0, xx2 - xx1) - h = np.maximum(0, yy2 - yy1) - - inter = w * h - - rem_areas = np.take(areas, axis=0, indices=order) - - union = (rem_areas - inter) + areas[idx] - match_metric_value = inter / union - - mask = match_metric_value < iou_threshold - mask = mask.astype(np.uint8) - matched_box_indices = np.flip(order[np.where(mask == 0)[0]]) - unmatched_indices = order[np.where(mask == 1)[0]] + mask = ious < iou_threshold + matched_box_indices = np.flip(order[np.where(mask is False)[0]]) + unmatched_indices = order[np.where(mask is True)[0]] order = unmatched_indices[scores[unmatched_indices].argsort()] From 53f345e91614a72b20a1f19c04d5369fa17a26ed Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 15 May 2024 11:35:59 +0300 Subject: [PATCH 046/182] Minor renaming --- supervision/detection/utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index cb254552..7985c739 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -299,18 +299,18 @@ def box_non_max_merge( while len(order) > 0: idx = order[-1] - order = order[:-1] + merge_candidate = np.expand_dims(predictions[idx], axis=0) + order = order[:-1] if len(order) == 0: keep_to_merge_list[idx.tolist()] = [] break - candidate = np.expand_dims(predictions[idx], axis=0) - ious = box_iou_batch(predictions[order][:, :4], candidate[:, :4]) + ious = box_iou_batch(predictions[order][:, :4], merge_candidate[:, :4]) - mask = ious < iou_threshold - matched_box_indices = np.flip(order[np.where(mask is False)[0]]) - unmatched_indices = order[np.where(mask is True)[0]] + below_threshold = ious < iou_threshold + matched_box_indices = np.flip(order[np.where(below_threshold is False)[0]]) + unmatched_indices = order[np.where(below_threshold is True)[0]] order = unmatched_indices[scores[unmatched_indices].argsort()] From 0e2eec08c8ed9ccc4ae21f63ca8a6f3ae658ca94 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 15 May 2024 11:48:48 +0300 Subject: [PATCH 047/182] Revert np.bool comparisons with `is` * Ruff complains when `== True` is used * Different behaviour with `is True` --- supervision/detection/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 7985c739..56420ed6 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -308,9 +308,9 @@ def box_non_max_merge( ious = box_iou_batch(predictions[order][:, :4], merge_candidate[:, :4]) - below_threshold = ious < iou_threshold - matched_box_indices = np.flip(order[np.where(below_threshold is False)[0]]) - unmatched_indices = order[np.where(below_threshold is True)[0]] + below_threshold = (ious < iou_threshold).astype(np.uint8) + matched_box_indices = np.flip(order[np.where(below_threshold == 0)[0]]) + unmatched_indices = order[np.where(below_threshold == 1)[0]] order = unmatched_indices[scores[unmatched_indices].argsort()] From 559ef90d83507994091cc7d0f76fa79ce9b7a8c1 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 15 May 2024 11:58:15 +0300 Subject: [PATCH 048/182] Simplify box_non_max_merge --- supervision/detection/utils.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 56420ed6..85b741c3 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -292,7 +292,7 @@ def box_non_max_merge( Dict[int, List[int]]: Mapping from prediction indices to keep to a list of prediction indices to be merged. """ - keep_to_merge_list = {} + keep_to_merge_list: Dict[int, List[int]] = {} scores = predictions[:, 4] order = scores.argsort() @@ -307,17 +307,11 @@ def box_non_max_merge( break ious = box_iou_batch(predictions[order][:, :4], merge_candidate[:, :4]) + ious = ious.flatten() - below_threshold = (ious < iou_threshold).astype(np.uint8) - matched_box_indices = np.flip(order[np.where(below_threshold == 0)[0]]) - unmatched_indices = order[np.where(below_threshold == 1)[0]] - - order = unmatched_indices[scores[unmatched_indices].argsort()] - - keep_to_merge_list[idx.tolist()] = [] - - for matched_box_ind in matched_box_indices.tolist(): - keep_to_merge_list[idx.tolist()].append(matched_box_ind) + above_threshold = ious >= iou_threshold + keep_to_merge_list[idx] = np.flip(order[above_threshold]).tolist() + order = order[~above_threshold] return keep_to_merge_list From f8f3647a983529aa2e7f2bff8599d33b2a7ebe83 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 15 May 2024 15:32:26 +0300 Subject: [PATCH 049/182] Removed suprplus NMM code for 20% speedup --- supervision/detection/core.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 2489ef80..2f358c6b 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -8,7 +8,6 @@ import numpy as np from supervision.config import CLASS_NAME_DATA_FIELD, ORIENTED_BOX_COORDINATES from supervision.detection.utils import ( - box_iou_batch, box_non_max_merge, box_non_max_merge_batch, box_non_max_suppression, @@ -1231,12 +1230,10 @@ class Detections: result = [] for keep_ind, merge_ind_list in keep_to_merge_list.items(): for merge_ind in merge_ind_list: - box_iou = box_iou_batch(self[keep_ind].xyxy, self[merge_ind].xyxy)[0] - if box_iou > threshold: - merged_detection = merge_object_detection_pair( - self[keep_ind], self[merge_ind] - ) - self._set_at_index(keep_ind, merged_detection) + merged_detection = merge_object_detection_pair( + self[keep_ind], self[merge_ind] + ) + self._set_at_index(keep_ind, merged_detection) result.append(self[keep_ind]) return Detections.merge(result) From d8679d9c46cbf76fd86b64ea11bf2833a9794781 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Thu, 16 May 2024 08:40:28 +0200 Subject: [PATCH 050/182] automatic RLE for masks with holes or in multiple pieces --- supervision/dataset/formats/coco.py | 47 +++++++---- test/dataset/formats/test_coco.py | 116 +++++++++++++++++++++++++++- 2 files changed, 148 insertions(+), 15 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index b6b2490a..2cc442a9 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -11,6 +11,7 @@ from supervision.dataset.utils import ( approximate_mask_with_polygons, map_detections_class_id, rle_to_mask, + mask_to_rle ) from supervision.detection.core import Detections from supervision.detection.utils import polygon_to_mask @@ -106,6 +107,21 @@ def coco_annotations_to_detections( return Detections(xyxy=xyxy, class_id=np.asarray(class_ids, dtype=int)) +def _mask_has_holes(mask: np.ndarray)-> bool: + _, hierarchy = cv2.findContours(mask.astype(np.uint8), cv2.RETR_CCOMP, + cv2.CHAIN_APPROX_SIMPLE) + parent_countour_index = 3 + for h in hierarchy[0]: + if h[parent_countour_index] != -1: + return True + return False + + +def _mask_has_multiple_segments(mask: np.ndarray)-> bool: + number_of_labels, _ = cv2.connectedComponents(mask.astype(np.uint8), connectivity=4) + return number_of_labels > 2 + + def detections_to_coco_annotations( detections: Detections, image_id: int, @@ -118,26 +134,31 @@ def detections_to_coco_annotations( for xyxy, mask, _, class_id, _, _ in detections: box_width, box_height = xyxy[2] - xyxy[0], xyxy[3] - xyxy[1] segmentation = [] + iscrowd = 0 if mask is not None: - segmentation = list( - approximate_mask_with_polygons( - mask=mask, - min_image_area_percentage=min_image_area_percentage, - max_image_area_percentage=max_image_area_percentage, - approximation_percentage=approximation_percentage, - )[0].flatten() - ) - # todo: flag for when to use RLE? - # segmentation = {"counts": mask_to_rle(binary_mask=mask), - # "size": list(mask.shape[:2])} + iscrowd = _mask_has_holes(mask = mask) or \ + _mask_has_multiple_segments(mask = mask) + + if iscrowd: + segmentation = {"counts": mask_to_rle(mask=mask), + "size": list(mask.shape[:2])} + else: + segmentation = [list( + approximate_mask_with_polygons( + mask=mask, + min_image_area_percentage=min_image_area_percentage, + max_image_area_percentage=max_image_area_percentage, + approximation_percentage=approximation_percentage, + )[0].flatten() + )] # multicomponent masks supported only for rle format coco_annotation = { "id": annotation_id, "image_id": image_id, "category_id": int(class_id), "bbox": [xyxy[0], xyxy[1], box_width, box_height], "area": box_width * box_height, - "segmentation": [segmentation] if segmentation else [], - "iscrowd": 0, ## todo: iscrowd depends on flag 1 if RLE 0 if polygon + "segmentation": segmentation, + "iscrowd": iscrowd, } coco_annotations.append(coco_annotation) annotation_id += 1 diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 66e73c0e..cecb637d 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -1,5 +1,5 @@ from contextlib import ExitStack as DoesNotRaise -from typing import Dict, List, Tuple +from typing import Dict, List, Tuple, Union import numpy as np import pytest @@ -11,6 +11,7 @@ from supervision.dataset.formats.coco import ( coco_annotations_to_detections, coco_categories_to_classes, group_coco_annotations_by_image_id, + detections_to_coco_annotations ) @@ -20,9 +21,11 @@ def mock_cock_coco_annotation( category_id: int = 0, bbox: Tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0), area: float = 0.0, - segmentation: List[list] = None, + segmentation: Union[List[list], Dict] = None, iscrowd: bool = False, ) -> dict: + if not segmentation: + segmentation = [] return { "id": annotation_id, "image_id": image_id, @@ -454,3 +457,112 @@ def test_build_coco_class_index_mapping( coco_categories=coco_categories, target_classes=target_classes ) assert result == expected_result + + +@pytest.mark.parametrize( + "detections, image_id, annotation_id, expected_result, exception", + [ + ( + Detections(xyxy=np.array([[0, 0, 100, 100]], dtype=np.float32), + class_id=np.array([0], dtype=int)), + 0, + 0, + [mock_cock_coco_annotation(category_id=0, bbox=(0, 0, 100, 100), area=100 * 100)], + DoesNotRaise(), + ), # no segmentation mask + # ( + # Detections( + # xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), + # class_id=np.array([0], dtype=int), + # mask=np.array( + # [ + # [ + # [1, 1, 1, 0, 0], + # [1, 1, 1, 0, 0], + # [1, 1, 1, 1, 1], + # [1, 1, 1, 1, 1], + # [1, 1, 1, 1, 1], + # ] + # ] + # ), + # ), + # 0, + # 0, + # [mock_cock_coco_annotation( + # category_id=0, + # bbox=(0, 0, 5, 5), + # area=5 * 5, + # segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]])], + # DoesNotRaise(), + # ), # segmentation mask in single component,no holes in mask, expects polygon mask + ( + Detections( + xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), + class_id=np.array([0], dtype=int), + mask=np.array( + [ + [ + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [0, 0, 0, 1, 1], + [0, 0, 0, 1, 1], + ] + ] + ), + ), + 0, + 0, + [mock_cock_coco_annotation( + category_id=0, + bbox=(0, 0, 5, 5), + area=5 * 5, + segmentation={ + "size": [5, 5], + "counts": [0, 3, 2, 3, 2, 3, 5, 2, 3, 2], + }, + iscrowd=True, )], + DoesNotRaise(), + ), # segmentation mask with 2 components, no holes in mask, expects RLE mask + ( + Detections( + xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), + class_id=np.array([0], dtype=int), + mask=np.array( + [ + [ + [0, 1, 1, 1, 1], + [0, 1, 1, 1, 1], + [1, 1, 0, 0, 1], + [1, 1, 0, 0, 1], + [1, 1, 1, 1, 1], + ] + ] + ), + ), + 0, + 0, + [mock_cock_coco_annotation( + category_id=0, + bbox=(0, 0, 5, 5), + area=5 * 5, + segmentation={ + "size": [5, 5], + "counts": [2, 10, 2, 3, 2, 6], + }, + iscrowd=True, )], + DoesNotRaise(), + ) # segmentation mask in single component, with holes in mask, expects RLE mask + ], +) +def test_detections_to_coco_annotations( + detections: Detections, + image_id: int, + annotation_id: int, + expected_result: List[Dict], + exception: Exception) -> None: + with exception: + result, _ = detections_to_coco_annotations( + detections=detections, image_id=image_id, annotation_id=annotation_id + ) + assert result == expected_result From eefa05cf01c3875f7d21a2b40a71c1fdc4bdc591 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 06:40:55 +0000 Subject: [PATCH 051/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/formats/coco.py | 42 +++++---- test/dataset/formats/test_coco.py | 129 +++++++++++++++------------- 2 files changed, 95 insertions(+), 76 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 2cc442a9..19e7aa3e 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -10,8 +10,8 @@ import numpy.typing as npt from supervision.dataset.utils import ( approximate_mask_with_polygons, map_detections_class_id, + mask_to_rle, rle_to_mask, - mask_to_rle ) from supervision.detection.core import Detections from supervision.detection.utils import polygon_to_mask @@ -107,9 +107,10 @@ def coco_annotations_to_detections( return Detections(xyxy=xyxy, class_id=np.asarray(class_ids, dtype=int)) -def _mask_has_holes(mask: np.ndarray)-> bool: - _, hierarchy = cv2.findContours(mask.astype(np.uint8), cv2.RETR_CCOMP, - cv2.CHAIN_APPROX_SIMPLE) +def _mask_has_holes(mask: np.ndarray) -> bool: + _, hierarchy = cv2.findContours( + mask.astype(np.uint8), cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE + ) parent_countour_index = 3 for h in hierarchy[0]: if h[parent_countour_index] != -1: @@ -117,9 +118,9 @@ def _mask_has_holes(mask: np.ndarray)-> bool: return False -def _mask_has_multiple_segments(mask: np.ndarray)-> bool: +def _mask_has_multiple_segments(mask: np.ndarray) -> bool: number_of_labels, _ = cv2.connectedComponents(mask.astype(np.uint8), connectivity=4) - return number_of_labels > 2 + return number_of_labels > 2 def detections_to_coco_annotations( @@ -136,21 +137,26 @@ def detections_to_coco_annotations( segmentation = [] iscrowd = 0 if mask is not None: - iscrowd = _mask_has_holes(mask = mask) or \ - _mask_has_multiple_segments(mask = mask) + iscrowd = _mask_has_holes(mask=mask) or _mask_has_multiple_segments( + mask=mask + ) if iscrowd: - segmentation = {"counts": mask_to_rle(mask=mask), - "size": list(mask.shape[:2])} + segmentation = { + "counts": mask_to_rle(mask=mask), + "size": list(mask.shape[:2]), + } else: - segmentation = [list( - approximate_mask_with_polygons( - mask=mask, - min_image_area_percentage=min_image_area_percentage, - max_image_area_percentage=max_image_area_percentage, - approximation_percentage=approximation_percentage, - )[0].flatten() - )] # multicomponent masks supported only for rle format + segmentation = [ + list( + approximate_mask_with_polygons( + mask=mask, + min_image_area_percentage=min_image_area_percentage, + max_image_area_percentage=max_image_area_percentage, + approximation_percentage=approximation_percentage, + )[0].flatten() + ) + ] # multicomponent masks supported only for rle format coco_annotation = { "id": annotation_id, "image_id": image_id, diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index cecb637d..139c74f8 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -10,8 +10,8 @@ from supervision.dataset.formats.coco import ( classes_to_coco_categories, coco_annotations_to_detections, coco_categories_to_classes, + detections_to_coco_annotations, group_coco_annotations_by_image_id, - detections_to_coco_annotations ) @@ -463,40 +463,46 @@ def test_build_coco_class_index_mapping( "detections, image_id, annotation_id, expected_result, exception", [ ( - Detections(xyxy=np.array([[0, 0, 100, 100]], dtype=np.float32), - class_id=np.array([0], dtype=int)), - 0, - 0, - [mock_cock_coco_annotation(category_id=0, bbox=(0, 0, 100, 100), area=100 * 100)], - DoesNotRaise(), - ), # no segmentation mask - # ( - # Detections( - # xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), - # class_id=np.array([0], dtype=int), - # mask=np.array( - # [ - # [ - # [1, 1, 1, 0, 0], - # [1, 1, 1, 0, 0], - # [1, 1, 1, 1, 1], - # [1, 1, 1, 1, 1], - # [1, 1, 1, 1, 1], - # ] - # ] - # ), - # ), - # 0, - # 0, - # [mock_cock_coco_annotation( - # category_id=0, - # bbox=(0, 0, 5, 5), - # area=5 * 5, - # segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]])], - # DoesNotRaise(), - # ), # segmentation mask in single component,no holes in mask, expects polygon mask - ( - Detections( + Detections( + xyxy=np.array([[0, 0, 100, 100]], dtype=np.float32), + class_id=np.array([0], dtype=int), + ), + 0, + 0, + [ + mock_cock_coco_annotation( + category_id=0, bbox=(0, 0, 100, 100), area=100 * 100 + ) + ], + DoesNotRaise(), + ), # no segmentation mask + # ( + # Detections( + # xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), + # class_id=np.array([0], dtype=int), + # mask=np.array( + # [ + # [ + # [1, 1, 1, 0, 0], + # [1, 1, 1, 0, 0], + # [1, 1, 1, 1, 1], + # [1, 1, 1, 1, 1], + # [1, 1, 1, 1, 1], + # ] + # ] + # ), + # ), + # 0, + # 0, + # [mock_cock_coco_annotation( + # category_id=0, + # bbox=(0, 0, 5, 5), + # area=5 * 5, + # segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]])], + # DoesNotRaise(), + # ), # segmentation mask in single component,no holes in mask, expects polygon mask + ( + Detections( xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), class_id=np.array([0], dtype=int), mask=np.array( @@ -511,21 +517,24 @@ def test_build_coco_class_index_mapping( ] ), ), - 0, - 0, - [mock_cock_coco_annotation( - category_id=0, - bbox=(0, 0, 5, 5), - area=5 * 5, - segmentation={ + 0, + 0, + [ + mock_cock_coco_annotation( + category_id=0, + bbox=(0, 0, 5, 5), + area=5 * 5, + segmentation={ "size": [5, 5], "counts": [0, 3, 2, 3, 2, 3, 5, 2, 3, 2], }, - iscrowd=True, )], - DoesNotRaise(), - ), # segmentation mask with 2 components, no holes in mask, expects RLE mask - ( - Detections( + iscrowd=True, + ) + ], + DoesNotRaise(), + ), # segmentation mask with 2 components, no holes in mask, expects RLE mask + ( + Detections( xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), class_id=np.array([0], dtype=int), mask=np.array( @@ -540,19 +549,22 @@ def test_build_coco_class_index_mapping( ] ), ), - 0, - 0, - [mock_cock_coco_annotation( - category_id=0, - bbox=(0, 0, 5, 5), - area=5 * 5, - segmentation={ + 0, + 0, + [ + mock_cock_coco_annotation( + category_id=0, + bbox=(0, 0, 5, 5), + area=5 * 5, + segmentation={ "size": [5, 5], "counts": [2, 10, 2, 3, 2, 6], }, - iscrowd=True, )], - DoesNotRaise(), - ) # segmentation mask in single component, with holes in mask, expects RLE mask + iscrowd=True, + ) + ], + DoesNotRaise(), + ), # segmentation mask in single component, with holes in mask, expects RLE mask ], ) def test_detections_to_coco_annotations( @@ -560,7 +572,8 @@ def test_detections_to_coco_annotations( image_id: int, annotation_id: int, expected_result: List[Dict], - exception: Exception) -> None: + exception: Exception, +) -> None: with exception: result, _ = detections_to_coco_annotations( detections=detections, image_id=image_id, annotation_id=annotation_id From 1e8ee47db25b01ed09f413f88624a3f86e8eb55b Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Thu, 16 May 2024 08:56:58 +0200 Subject: [PATCH 052/182] craete copies of mask in _has_holes and _mask_has_multiple_segments --- supervision/dataset/formats/coco.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 19e7aa3e..db65ed57 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -108,9 +108,8 @@ def coco_annotations_to_detections( def _mask_has_holes(mask: np.ndarray) -> bool: - _, hierarchy = cv2.findContours( - mask.astype(np.uint8), cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE - ) + mask_uint8 = mask.astype(np.uint8) + _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) parent_countour_index = 3 for h in hierarchy[0]: if h[parent_countour_index] != -1: @@ -119,7 +118,8 @@ def _mask_has_holes(mask: np.ndarray) -> bool: def _mask_has_multiple_segments(mask: np.ndarray) -> bool: - number_of_labels, _ = cv2.connectedComponents(mask.astype(np.uint8), connectivity=4) + mask_uint8 = mask.astype(np.uint8) + number_of_labels, _ = cv2.connectedComponents(mask_uint8, connectivity=4) return number_of_labels > 2 From ce05a0c5441afb0a604ce9dc30997297952157c0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 06:57:24 +0000 Subject: [PATCH 053/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/formats/coco.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index db65ed57..5a55532c 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -108,7 +108,7 @@ def coco_annotations_to_detections( def _mask_has_holes(mask: np.ndarray) -> bool: - mask_uint8 = mask.astype(np.uint8) + mask_uint8 = mask.astype(np.uint8) _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) parent_countour_index = 3 for h in hierarchy[0]: @@ -118,7 +118,7 @@ def _mask_has_holes(mask: np.ndarray) -> bool: def _mask_has_multiple_segments(mask: np.ndarray) -> bool: - mask_uint8 = mask.astype(np.uint8) + mask_uint8 = mask.astype(np.uint8) number_of_labels, _ = cv2.connectedComponents(mask_uint8, connectivity=4) return number_of_labels > 2 From 3ee72e38c35982983cd3fc971b31499a56d76f58 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Thu, 16 May 2024 09:20:26 +0200 Subject: [PATCH 054/182] check for empty mask --- supervision/dataset/formats/coco.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index db65ed57..91e11dcb 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -109,7 +109,8 @@ def coco_annotations_to_detections( def _mask_has_holes(mask: np.ndarray) -> bool: mask_uint8 = mask.astype(np.uint8) - _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) + _, hierarchy = cv2.findContours( + mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) parent_countour_index = 3 for h in hierarchy[0]: if h[parent_countour_index] != -1: @@ -118,6 +119,8 @@ def _mask_has_holes(mask: np.ndarray) -> bool: def _mask_has_multiple_segments(mask: np.ndarray) -> bool: + if mask.size == 0: + return False mask_uint8 = mask.astype(np.uint8) number_of_labels, _ = cv2.connectedComponents(mask_uint8, connectivity=4) return number_of_labels > 2 From b3b745508bf05895d4061fb8a8018a489e8dde2d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 07:23:53 +0000 Subject: [PATCH 055/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/formats/coco.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 73b5b32a..0e80afb5 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -120,7 +120,7 @@ def _mask_has_holes(mask: np.ndarray) -> bool: def _mask_has_multiple_segments(mask: np.ndarray) -> bool: if mask.size == 0: return False - mask_uint8 = mask.astype(np.uint8) + mask_uint8 = mask.astype(np.uint8) number_of_labels, _ = cv2.connectedComponents(mask_uint8, connectivity=4) return number_of_labels > 2 From 0252cf210c5fd78a0066f0b3c8399eab0df83bab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 May 2024 00:59:54 +0000 Subject: [PATCH 056/182] :arrow_up: Bump notebook from 7.1.3 to 7.2.0 Bumps [notebook](https://github.com/jupyter/notebook) from 7.1.3 to 7.2.0. - [Release notes](https://github.com/jupyter/notebook/releases) - [Changelog](https://github.com/jupyter/notebook/blob/main/CHANGELOG.md) - [Commits](https://github.com/jupyter/notebook/compare/@jupyter-notebook/tree@7.1.3...@jupyter-notebook/tree@7.2.0) --- updated-dependencies: - dependency-name: notebook dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- poetry.lock | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2a689d76..cf0350b1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1566,13 +1566,13 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (> [[package]] name = "jupyterlab" -version = "4.1.2" +version = "4.2.0" description = "JupyterLab computational environment" optional = false python-versions = ">=3.8" files = [ - {file = "jupyterlab-4.1.2-py3-none-any.whl", hash = "sha256:aa88193f03cf4d3555f6712f04d74112b5eb85edd7d222c588c7603a26d33c5b"}, - {file = "jupyterlab-4.1.2.tar.gz", hash = "sha256:5d6348b3ed4085181499f621b7dfb6eb0b1f57f3586857aadfc8e3bf4c4885f9"}, + {file = "jupyterlab-4.2.0-py3-none-any.whl", hash = "sha256:0dfe9278e25a145362289c555d9beb505697d269c10e99909766af7c440ad3cc"}, + {file = "jupyterlab-4.2.0.tar.gz", hash = "sha256:356e9205a6a2ab689c47c8fe4919dba6c076e376d03f26baadc05748c2435dd5"}, ] [package.dependencies] @@ -1580,23 +1580,24 @@ async-lru = ">=1.0.0" httpx = ">=0.25.0" importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} importlib-resources = {version = ">=1.4", markers = "python_version < \"3.9\""} -ipykernel = "*" +ipykernel = ">=6.5.0" jinja2 = ">=3.0.3" jupyter-core = "*" jupyter-lsp = ">=2.0.0" jupyter-server = ">=2.4.0,<3" -jupyterlab-server = ">=2.19.0,<3" +jupyterlab-server = ">=2.27.1,<3" notebook-shim = ">=0.2" packaging = "*" -tomli = {version = "*", markers = "python_version < \"3.11\""} +tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} tornado = ">=6.2.0" traitlets = "*" [package.extras] -dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.2.0)"] +dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.3.5)"] docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<7.3.0)", "sphinx-copybutton"] -docs-screenshots = ["altair (==5.2.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.1)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.0.post6)", "matplotlib (==3.8.2)", "nbconvert (>=7.0.0)", "pandas (==2.2.0)", "scipy (==1.12.0)", "vega-datasets (==0.9.0)"] +docs-screenshots = ["altair (==5.3.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.2)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.1.post2)", "matplotlib (==3.8.3)", "nbconvert (>=7.0.0)", "pandas (==2.2.1)", "scipy (==1.12.0)", "vega-datasets (==0.9.0)"] test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"] +upgrade-extension = ["copier (>=8,<10)", "jinja2-time (<0.3)", "pydantic (<2.0)", "pyyaml-include (<2.0)", "tomli-w (<2.0)"] [[package]] name = "jupyterlab-pygments" @@ -1611,13 +1612,13 @@ files = [ [[package]] name = "jupyterlab-server" -version = "2.25.3" +version = "2.27.1" description = "A set of server components for JupyterLab and JupyterLab like applications." optional = false python-versions = ">=3.8" files = [ - {file = "jupyterlab_server-2.25.3-py3-none-any.whl", hash = "sha256:c48862519fded9b418c71645d85a49b2f0ec50d032ba8316738e9276046088c1"}, - {file = "jupyterlab_server-2.25.3.tar.gz", hash = "sha256:846f125a8a19656611df5b03e5912c8393cea6900859baa64fa515eb64a8dc40"}, + {file = "jupyterlab_server-2.27.1-py3-none-any.whl", hash = "sha256:f5e26156e5258b24d532c84e7c74cc212e203bff93eb856f81c24c16daeecc75"}, + {file = "jupyterlab_server-2.27.1.tar.gz", hash = "sha256:097b5ac709b676c7284ac9c5e373f11930a561f52cd5a86e4fc7e5a9c8a8631d"}, ] [package.dependencies] @@ -1633,7 +1634,7 @@ requests = ">=2.31" [package.extras] docs = ["autodoc-traits", "jinja2 (<3.2.0)", "mistune (<4)", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinxcontrib-openapi (>0.8)"] openapi = ["openapi-core (>=0.18.0,<0.19.0)", "ruamel-yaml"] -test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-validator (>=0.6.0,<0.8.0)", "pytest (>=7.0)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter[server] (>=0.6.2)", "pytest-timeout", "requests-mock", "ruamel-yaml", "sphinxcontrib-spelling", "strict-rfc3339", "werkzeug"] +test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-validator (>=0.6.0,<0.8.0)", "pytest (>=7.0,<8)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter[server] (>=0.6.2)", "pytest-timeout", "requests-mock", "ruamel-yaml", "sphinxcontrib-spelling", "strict-rfc3339", "werkzeug"] [[package]] name = "jupyterlab-widgets" @@ -2484,26 +2485,26 @@ setuptools = "*" [[package]] name = "notebook" -version = "7.1.3" +version = "7.2.0" description = "Jupyter Notebook - A web-based notebook environment for interactive computing" optional = false python-versions = ">=3.8" files = [ - {file = "notebook-7.1.3-py3-none-any.whl", hash = "sha256:919b911e59f41f6e3857ce93c9d93535ba66bb090059712770e5968c07e1004d"}, - {file = "notebook-7.1.3.tar.gz", hash = "sha256:41fcebff44cf7bb9377180808bcbae066629b55d8c7722f1ebbe75ca44f9cfc1"}, + {file = "notebook-7.2.0-py3-none-any.whl", hash = "sha256:b4752d7407d6c8872fc505df0f00d3cae46e8efb033b822adacbaa3f1f3ce8f5"}, + {file = "notebook-7.2.0.tar.gz", hash = "sha256:34a2ba4b08ad5d19ec930db7484fb79746a1784be9e1a5f8218f9af8656a141f"}, ] [package.dependencies] jupyter-server = ">=2.4.0,<3" -jupyterlab = ">=4.1.1,<4.2" -jupyterlab-server = ">=2.22.1,<3" +jupyterlab = ">=4.2.0,<4.3" +jupyterlab-server = ">=2.27.1,<3" notebook-shim = ">=0.2,<0.3" tornado = ">=6.2.0" [package.extras] dev = ["hatch", "pre-commit"] docs = ["myst-parser", "nbsphinx", "pydata-sphinx-theme", "sphinx (>=1.3.6)", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.22.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"] +test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.27.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"] [[package]] name = "notebook-shim" From de5349ad6d02f9a69dfd419284291e23a52b602d Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Fri, 17 May 2024 09:17:38 +0300 Subject: [PATCH 057/182] docs: calculate_optimal_text_scale, not calculate_optimal_font_scale * Latter does not exist --- docs/utils/draw.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/utils/draw.md b/docs/utils/draw.md index 84758e06..f4b86a53 100644 --- a/docs/utils/draw.md +++ b/docs/utils/draw.md @@ -41,7 +41,7 @@ comments: true :::supervision.draw.utils.draw_image :::supervision.draw.utils.calculate_optimal_text_scale From 9024396f6c49f5f5496dac5347859f07721e1f76 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Fri, 17 May 2024 10:58:45 +0300 Subject: [PATCH 058/182] Add npt.NDarray[x] types, remove resolution_wh default val --- supervision/detection/utils.py | 58 +++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 85b741c3..db33ab01 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -3,6 +3,7 @@ from typing import Dict, List, Optional, Tuple, Union import cv2 import numpy as np +import numpy.typing as npt from supervision.config import CLASS_NAME_DATA_FIELD @@ -275,14 +276,14 @@ def box_non_max_suppression( def box_non_max_merge( - predictions: np.ndarray, iou_threshold: float = 0.5 + predictions: npt.NDArray[np.float64], iou_threshold: float = 0.5 ) -> Dict[int, List[int]]: """ Apply greedy version of non-maximum merging to avoid detecting too many overlapping bounding boxes for a given object. Args: - predictions (np.ndarray): An array of shape `(n, 5)` containing + 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. iou_threshold (float, optional): The intersection-over-union threshold @@ -317,14 +318,14 @@ def box_non_max_merge( def box_non_max_merge_batch( - predictions: np.ndarray, iou_threshold: float = 0.5 + predictions: npt.NDArray[np.float64], iou_threshold: float = 0.5 ) -> Dict[int, List[int]]: """ Apply greedy version of non-maximum merging per category to avoid detecting too many overlapping bounding boxes for a given object. Args: - predictions (np.ndarray): An array of shape `(n, 6)` containing + predictions (npt.NDArray[np.float64]): An array of shape `(n, 6)` containing the bounding boxes coordinates in format `[x1, y1, x2, y2]`, the confidence scores and class_ids. iou_threshold (float, optional): The intersection-over-union threshold @@ -667,16 +668,18 @@ def process_roboflow_result( return xyxy, confidence, class_id, masks, tracker_id, data -def move_boxes(xyxy: np.ndarray, offset: np.ndarray) -> np.ndarray: +def move_boxes( + xyxy: npt.NDArray[np.float64], offset: npt.NDArray[np.int32] +) -> npt.NDArray[np.float64]: """ Parameters: - xyxy (np.ndarray): An array of shape `(n, 4)` containing the bounding boxes - coordinates in format `[x1, y1, x2, y2]` + xyxy (npt.NDArray[np.float64]): An array of shape `(n, 4)` containing the + bounding boxes coordinates in format `[x1, y1, x2, y2]` offset (np.array): An array of shape `(2,)` containing offset values in format is `[dx, dy]`. Returns: - np.ndarray: Repositioned bounding boxes. + npt.NDArray[np.float64]: Repositioned bounding boxes. Example: ```python @@ -697,24 +700,25 @@ def move_boxes(xyxy: np.ndarray, offset: np.ndarray) -> np.ndarray: def move_masks( - masks: np.ndarray, - offset: np.ndarray, - resolution_wh: Tuple[int, int] = None, -) -> np.ndarray: + masks: npt.NDArray[np.bool_], + offset: npt.NDArray[np.int32], + resolution_wh: Tuple[int, int], +) -> npt.NDArray[np.bool_]: """ Offset the masks in an array by the specified (x, y) amount. Args: - 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. - offset (np.ndarray): An array of shape `(2,)` containing non-negative int values - `[dx, dy]`. + masks (npt.NDArray[np.bool_]): 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. + offset (npt.NDArray[np.int32]): An array of shape `(2,)` containing non-negative + int values `[dx, dy]`. resolution_wh (Tuple[int, int]): The width and height of the desired mask resolution. Returns: - (np.ndarray) repositioned masks, optionally padded to the specified shape. + (npt.NDArray[np.bool_]) repositioned masks, optionally padded to the specified + shape. """ if offset[0] < 0 or offset[1] < 0: @@ -730,19 +734,21 @@ def move_masks( return mask_array -def scale_boxes(xyxy: np.ndarray, factor: float) -> np.ndarray: +def scale_boxes( + xyxy: npt.NDArray[np.float64], factor: float +) -> npt.NDArray[np.float64]: """ Scale the dimensions of bounding boxes. Parameters: - xyxy (np.ndarray): An array of shape `(n, 4)` containing the bounding boxes - coordinates in format `[x1, y1, x2, y2]` + xyxy (npt.NDArray[np.float64]): An array of shape `(n, 4)` containing the + bounding boxes coordinates in format `[x1, y1, x2, y2]` factor (float): A float value representing the factor by which the box dimensions are scaled. A factor greater than 1 enlarges the boxes, while a factor less than 1 shrinks them. Returns: - np.ndarray: Scaled bounding boxes. + npt.NDArray[np.float64]: Scaled bounding boxes. Example: ```python @@ -810,19 +816,19 @@ def is_data_equal(data_a: Dict[str, np.ndarray], data_b: Dict[str, np.ndarray]) def merge_data( - data_list: List[Dict[str, Union[np.ndarray, List]]], -) -> Dict[str, Union[np.ndarray, List]]: + data_list: List[Dict[str, Union[npt.NDArray[np.generic], List]]], +) -> Dict[str, Union[npt.NDArray[np.generic], List]]: """ Merges the data payloads of a list of Detections instances. Args: data_list: The data payloads of the Detections instances. Each data payload is a dictionary with the same keys, and the values are either lists or - np.ndarray. + npt.NDArray[np.generic]. Returns: A single data payload containing the merged data, preserving the original data - types (list or np.ndarray). + types (list or npt.NDArray[np.generic]). Raises: ValueError: If data values within a single object have different lengths or if From 879ae9416a6598440c39fa8eaaf77f32916e4d51 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Fri, 17 May 2024 10:34:53 +0200 Subject: [PATCH 059/182] test polygon mask when mask in single component and no holes --- supervision/dataset/formats/coco.py | 2 -- test/dataset/formats/test_coco.py | 55 +++++++++++++++-------------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 73b5b32a..723ad463 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -118,8 +118,6 @@ def _mask_has_holes(mask: np.ndarray) -> bool: def _mask_has_multiple_segments(mask: np.ndarray) -> bool: - if mask.size == 0: - return False mask_uint8 = mask.astype(np.uint8) number_of_labels, _ = cv2.connectedComponents(mask_uint8, connectivity=4) return number_of_labels > 2 diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 139c74f8..11085a90 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -476,31 +476,32 @@ def test_build_coco_class_index_mapping( ], DoesNotRaise(), ), # no segmentation mask - # ( - # Detections( - # xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), - # class_id=np.array([0], dtype=int), - # mask=np.array( - # [ - # [ - # [1, 1, 1, 0, 0], - # [1, 1, 1, 0, 0], - # [1, 1, 1, 1, 1], - # [1, 1, 1, 1, 1], - # [1, 1, 1, 1, 1], - # ] - # ] - # ), - # ), - # 0, - # 0, - # [mock_cock_coco_annotation( - # category_id=0, - # bbox=(0, 0, 5, 5), - # area=5 * 5, - # segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]])], - # DoesNotRaise(), - # ), # segmentation mask in single component,no holes in mask, expects polygon mask + ( + Detections( + xyxy=np.array([[0, 0, 4, 5]], dtype=np.float32), + class_id=np.array([0], dtype=int), + mask=np.array( + [ + [ + [1, 1, 1, 1, 0], + [1, 1, 1, 1, 0], + [1, 1, 1, 1, 0], + [1, 1, 1, 1, 0], + [1, 1, 1, 1, 0], + ] + ] + ), + ), + 0, + 0, + [mock_cock_coco_annotation( + category_id=0, + bbox=(0, 0, 4, 5), + area=4 * 5, + segmentation=[[0, 0, 0, 4, 3, 4, 3, 0]])], + DoesNotRaise(), + ), # segmentation mask in single component,no holes in mask, + # expects polygon mask ( Detections( xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), @@ -564,7 +565,7 @@ def test_build_coco_class_index_mapping( ) ], DoesNotRaise(), - ), # segmentation mask in single component, with holes in mask, expects RLE mask + ), # segmentation mask in single component, with holes in mask, expects RLE mask ], ) def test_detections_to_coco_annotations( @@ -576,6 +577,6 @@ def test_detections_to_coco_annotations( ) -> None: with exception: result, _ = detections_to_coco_annotations( - detections=detections, image_id=image_id, annotation_id=annotation_id + detections=detections, image_id=image_id, annotation_id=annotation_id, ) assert result == expected_result From ab775d9f76c7487ffbf74b5dc978479a218b7341 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Fri, 17 May 2024 11:45:00 +0300 Subject: [PATCH 060/182] Attempt to fix SegFault * https://github.com/opencv/opencv-python/issues/706 --- supervision/dataset/formats/coco.py | 3 ++- test/dataset/formats/test_coco.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 0e80afb5..4f6c169e 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -121,7 +121,8 @@ def _mask_has_multiple_segments(mask: np.ndarray) -> bool: if mask.size == 0: return False mask_uint8 = mask.astype(np.uint8) - number_of_labels, _ = cv2.connectedComponents(mask_uint8, connectivity=4) + labels = np.zeros_like(mask_uint8, dtype=np.int32) + number_of_labels, _ = cv2.connectedComponents(mask_uint8, labels, connectivity=4) return number_of_labels > 2 diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 139c74f8..af6b31bc 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -500,7 +500,7 @@ def test_build_coco_class_index_mapping( # area=5 * 5, # segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]])], # DoesNotRaise(), - # ), # segmentation mask in single component,no holes in mask, expects polygon mask + # ), # seg mask in single component,no holes in mask, expects polygon ( Detections( xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), @@ -564,7 +564,7 @@ def test_build_coco_class_index_mapping( ) ], DoesNotRaise(), - ), # segmentation mask in single component, with holes in mask, expects RLE mask + ), # seg mask in single component, with holes in mask, expects RLE mask ], ) def test_detections_to_coco_annotations( From 0cd2c9d1464d2de77db97b7fc5550d05955477eb Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Fri, 17 May 2024 11:16:37 +0200 Subject: [PATCH 061/182] documentation change for as_coco --- supervision/dataset/core.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/supervision/dataset/core.py b/supervision/dataset/core.py index 551e96da..fbbbe6b7 100644 --- a/supervision/dataset/core.py +++ b/supervision/dataset/core.py @@ -430,6 +430,10 @@ class DetectionDataset(BaseDataset): """ Exports the dataset to COCO format. This method saves the images and their corresponding annotations in COCO format. + The format of the mask is determined automatically: + when a mask consists of multiple disconnected elements + or has holes the RLE format is used, + otherwise, the mask is encoded as a polygon. Args: images_directory_path (Optional[str]): The path to the directory From 8d432c78b1cb02d62a8051c972d92454a1cecd54 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 17 May 2024 09:16:57 +0000 Subject: [PATCH 062/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/core.py | 4 ++-- test/dataset/formats/test_coco.py | 37 ++++++++++++++++++------------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/supervision/dataset/core.py b/supervision/dataset/core.py index fbbbe6b7..6ae5844a 100644 --- a/supervision/dataset/core.py +++ b/supervision/dataset/core.py @@ -430,8 +430,8 @@ class DetectionDataset(BaseDataset): """ Exports the dataset to COCO format. This method saves the images and their corresponding annotations in COCO format. - The format of the mask is determined automatically: - when a mask consists of multiple disconnected elements + The format of the mask is determined automatically: + when a mask consists of multiple disconnected elements or has holes the RLE format is used, otherwise, the mask is encoded as a polygon. diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 6be0fa2c..8da8ccc8 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -478,30 +478,33 @@ def test_build_coco_class_index_mapping( ), # no segmentation mask ( Detections( - xyxy=np.array([[0, 0, 4, 5]], dtype=np.float32), - class_id=np.array([0], dtype=int), - mask=np.array( + xyxy=np.array([[0, 0, 4, 5]], dtype=np.float32), + class_id=np.array([0], dtype=int), + mask=np.array( + [ [ - [ - [1, 1, 1, 1, 0], - [1, 1, 1, 1, 0], - [1, 1, 1, 1, 0], - [1, 1, 1, 1, 0], - [1, 1, 1, 1, 0], - ] + [1, 1, 1, 1, 0], + [1, 1, 1, 1, 0], + [1, 1, 1, 1, 0], + [1, 1, 1, 1, 0], + [1, 1, 1, 1, 0], ] - ), + ] ), + ), 0, 0, - [mock_cock_coco_annotation( + [ + mock_cock_coco_annotation( category_id=0, bbox=(0, 0, 4, 5), area=4 * 5, - segmentation=[[0, 0, 0, 4, 3, 4, 3, 0]])], + segmentation=[[0, 0, 0, 4, 3, 4, 3, 0]], + ) + ], DoesNotRaise(), - ), # segmentation mask in single component,no holes in mask, - # expects polygon mask + ), # segmentation mask in single component,no holes in mask, + # expects polygon mask ( Detections( xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), @@ -577,6 +580,8 @@ def test_detections_to_coco_annotations( ) -> None: with exception: result, _ = detections_to_coco_annotations( - detections=detections, image_id=image_id, annotation_id=annotation_id, + detections=detections, + image_id=image_id, + annotation_id=annotation_id, ) assert result == expected_result From da904ad2da5b2fe57bcd759d753e406e7aae86e6 Mon Sep 17 00:00:00 2001 From: tc360950 Date: Fri, 17 May 2024 16:17:31 +0200 Subject: [PATCH 063/182] Add LineZone unit tests --- test/detection/test_line_counter.py | 326 +++++++++++++++++++++++++++- 1 file changed, 323 insertions(+), 3 deletions(-) diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index 73780414..997da352 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -1,10 +1,13 @@ from contextlib import ExitStack as DoesNotRaise -from typing import Optional, Tuple +from itertools import chain, combinations +from test.test_utils import mock_detections +from typing import Optional, Tuple, List +import numpy as np import pytest -from supervision import LineZone -from supervision.geometry.core import Point, Vector +from supervision import Detections, LineZone +from supervision.geometry.core import Point, Position, Vector @pytest.mark.parametrize( @@ -70,3 +73,320 @@ def test_calculate_region_of_interest_limits( with exception: result = LineZone.calculate_region_of_interest_limits(vector=vector) assert result == expected_result + + +@pytest.mark.parametrize( + "vector, bbox_sequence, expected_count_in, expected_count_out", + [ + ( + Vector( + Point(0, 0), + Point(0, 100), + ), + [ + [100, 50, 120, 70], + [-100, 50, -80, 70], + ], + [False, False], + [False, True], + ), + ( + Vector( + Point(0, 0), + Point(0, 100), + ), + [ + [-100, 50, -80, 70], + [100, 50, 120, 70], + ], + [False, True], + [False, False], + ), + ( + Vector( + Point(0, 0), + Point(0, 100), + ), + [ + [-100, 50, -80, 70], + [-10, 50, 20, 70], + [100, 50, 120, 70], + ], + [False, False, True], + [False, False, False], + ), + ( + Vector( + Point(0, 0), + Point(100, 100), + ), + [ + [50, 45, 70, 30], + [40, 50, 50, 40], + [0, 50, 10, 40], + ], + [False, False, False], + [False, False, True], + ), + ( + Vector( + Point(0, 0), + Point(100, 0), + ), + [ + [50, -45, 70, -30], + [40, 50, 50, 40], + ], + [False, False], + [False, True], + ), + ( + Vector( + Point(0, 0), + Point(0, -100), + ), + [ + [100, -50, 120, -70], + [-100, -50, -80, -70], + ], + [False, True], + [False, False], + ), + ( + Vector( + Point(0, 0), + Point(50, 100), + ), + [ + [50, 50, 70, 30], + [40, 50, 50, 40], + [0, 50, 10, 40], + ], + [False, False, False], + [False, False, True], + ), + ( + Vector( + Point(0, 0), + Point(0, 100), + ), + [ + [100, 50, 120, 70], + [-100, 50, -80, 70], + [100, 50, 120, 70], + [-100, 50, -80, 70], + [100, 50, 120, 70], + [-100, 50, -80, 70], + [100, 50, 120, 70], + [-100, 50, -80, 70], + ], + [False, False, True, False, True, False, True, False], + [False, True, False, True, False, True, False, True], + ), + ( + Vector( + Point(0, 0), + Point(-100, 0), + ), + [ + [-50, 70, -40, 50], + [-50, -70, -40, -50], + [-50, 70, -40, 50], + [-50, -70, -40, -50], + [-50, 70, -40, 50], + [-50, -70, -40, -50], + [-50, 70, -40, 50], + [-50, -70, -40, -50], + ], + [False, False, True, False, True, False, True, False], + [False, True, False, True, False, True, False, True], + ), + ], +) +def test_line_zone_single_detection( + vector, bbox_sequence, expected_count_in: List[bool], expected_count_out: List[bool] +) -> None: + line_zone = LineZone(start=vector.start, end=vector.end) + for i, bbox in enumerate(bbox_sequence): + detections = mock_detections( + xyxy=[bbox], + tracker_id=[i for i in range(0, 1)], + ) + count_in, count_out = line_zone.trigger(detections) + assert count_in[0] == expected_count_in[i] + assert count_out[0] == expected_count_out[i] + assert line_zone.in_count == sum(expected_count_in[: (i + 1)]) + assert line_zone.out_count == sum(expected_count_out[: (i + 1)]) + + +@pytest.mark.parametrize( + "vector, bbox_sequence, expected_count_in, expected_count_out, crossing_anchors", + [ + ( + Vector( + Point(0, 0), + Point(100, 100), + ), + [ + [50, 30, 60, 20], + [20, 50, 40, 30], + ], + [False, False], + [False, True], + [Position.TOP_LEFT, Position.TOP_RIGHT, Position.BOTTOM_LEFT], + ), + ( + Vector( + Point(0, 0), + Point(0, 100), + ), + [ + [-100, 50, -80, 70], + [-100, 50, 120, 70], + ], + [False, True], + [False, False], + [Position.TOP_RIGHT, Position.BOTTOM_RIGHT], + ), + ], +) +def test_line_zone_single_detection_on_subset_of_anchors( + vector, + bbox_sequence, + expected_count_in: List[bool], + expected_count_out: List[bool], + crossing_anchors, +) -> None: + def powerset(s): + return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1)) + + for anchors in powerset( + [ + Position.TOP_LEFT, + Position.TOP_RIGHT, + Position.BOTTOM_LEFT, + Position.BOTTOM_RIGHT, + ] + ): + if not anchors: + continue + line_zone = LineZone( + start=vector.start, end=vector.end, triggering_anchors=anchors + ) + for i, bbox in enumerate(bbox_sequence): + detections = mock_detections( + xyxy=[bbox], + tracker_id=[i for i in range(0, 1)], + ) + count_in, count_out = line_zone.trigger(detections) + if all(anchor in crossing_anchors for anchor in anchors): + assert count_in == expected_count_in[i] + assert count_out == expected_count_out[i] + else: + assert np.all(not count_in) + assert np.all(not count_out) + + +@pytest.mark.parametrize( + "vector, bbox_sequence, expected_count_in, expected_count_out", + [ + ( + Vector( + Point(0, 0), + Point(0, 100), + ), + [ + [[100, 50, 120, 70], [100, 50, 120, 70]], + [[-100, 50, -80, 70], [100, 50, 120, 70]], + [[100, 50, 120, 70], [100, 50, 120, 70]], + ], + [[False, False], [False, False], [True, False]], + [[False, False], [True, False], [False, False]], + ), + ( + Vector( + Point(0, 0), + Point(-100, 0), + ), + [ + [[-50, 70, -40, 50], [-80, -50, -70, -40]], + [[-50, -70, -40, -50], [-80, 50, -70, 40]], + [[-50, 70, -40, 50], [-80, 50, -70, 40]], + [[-50, -70, -40, -50], [-80, 50, -70, 40]], + [[-50, 70, -40, 50], [-80, 50, -70, 40]], + [[-50, -70, -40, -50], [-80, 50, -70, 40]], + [[-50, 70, -40, 50], [-80, 50, -70, 40]], + [[-50, -70, -40, -50], [-80, -50, -70, -40]], + ], + [ + (False, False), + (False, True), + (True, False), + (False, False), + (True, False), + (False, False), + (True, False), + (False, False), + ], + [ + (False, False), + (True, False), + (False, False), + (True, False), + (False, False), + (True, False), + (False, False), + (True, True), + ], + ), + ], +) +def test_line_zone_multiple_detections( + vector, bbox_sequence, expected_count_in: List[bool], expected_count_out: List[bool] +) -> None: + line_zone = LineZone(start=vector.start, end=vector.end) + for i, bboxes in enumerate(bbox_sequence): + detections = mock_detections( + xyxy=bboxes, + tracker_id=[i for i in range(0, len(bboxes))], + ) + count_in, count_out = line_zone.trigger(detections) + assert np.all(count_in == expected_count_in[i]) + assert np.all(count_out == expected_count_out[i]) + + +@pytest.mark.parametrize( + "vector, bbox_sequence", + [ + ( + Vector( + Point(0, 0), + Point(0, 100), + ), + [ + [100, 50, 120, 70], + [-100, 50, -80, 70], + ], + ), + ( + Vector( + Point(0, 0), + Point(0, 100), + ), + [ + [-100, 50, -80, 70], + [100, 50, 120, 70], + ], + ), + ], +) +def test_line_zone_does_not_count_detections_without_tracker_id(vector, bbox_sequence): + line_zone = LineZone(start=vector.start, end=vector.end) + for bbox in bbox_sequence: + detections = Detections( + xyxy=np.array([bbox]).reshape((-1, 4)), + tracker_id=np.array([None for _ in range(0, 1)]), + ) + count_in, count_out = line_zone.trigger(detections) + assert np.all(not count_in) + assert np.all(not count_out) From 6c9f8302a53758c85e4763f89ebe0911a81379b8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 17 May 2024 14:19:50 +0000 Subject: [PATCH 064/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/detection/test_line_counter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index 997da352..875d8c0c 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -1,7 +1,7 @@ from contextlib import ExitStack as DoesNotRaise from itertools import chain, combinations from test.test_utils import mock_detections -from typing import Optional, Tuple, List +from typing import List, Optional, Tuple import numpy as np import pytest From 12a455eafa2f14aa82a2aa45a7836ce48ef2fe31 Mon Sep 17 00:00:00 2001 From: tc360950 Date: Sat, 18 May 2024 17:05:09 +0200 Subject: [PATCH 065/182] Replace unnecessary generator expressions with explicit lists --- test/detection/test_line_counter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index 875d8c0c..c8a2b037 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -210,7 +210,7 @@ def test_line_zone_single_detection( for i, bbox in enumerate(bbox_sequence): detections = mock_detections( xyxy=[bbox], - tracker_id=[i for i in range(0, 1)], + tracker_id=[0], ) count_in, count_out = line_zone.trigger(detections) assert count_in[0] == expected_count_in[i] @@ -276,7 +276,7 @@ def test_line_zone_single_detection_on_subset_of_anchors( for i, bbox in enumerate(bbox_sequence): detections = mock_detections( xyxy=[bbox], - tracker_id=[i for i in range(0, 1)], + tracker_id=[0], ) count_in, count_out = line_zone.trigger(detections) if all(anchor in crossing_anchors for anchor in anchors): @@ -385,7 +385,7 @@ def test_line_zone_does_not_count_detections_without_tracker_id(vector, bbox_seq for bbox in bbox_sequence: detections = Detections( xyxy=np.array([bbox]).reshape((-1, 4)), - tracker_id=np.array([None for _ in range(0, 1)]), + tracker_id=np.array([None]), ) count_in, count_out = line_zone.trigger(detections) assert np.all(not count_in) From b761a5c32b6c04ec078c2b87bc2c1daecd343cbd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 00:53:40 +0000 Subject: [PATCH 066/182] :arrow_up: Bump pytest from 8.2.0 to 8.2.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.2.0 to 8.2.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.2.0...8.2.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index db11d029..9ed05cfb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3039,13 +3039,13 @@ tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [[package]] name = "pytest" -version = "8.2.0" +version = "8.2.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, - {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, + {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, + {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, ] [package.dependencies] From 41ca70aab6792845e0555fb3f124b881dd1083b7 Mon Sep 17 00:00:00 2001 From: tc360950 Date: Mon, 20 May 2024 17:01:46 +0200 Subject: [PATCH 067/182] Add missing type hints and consistent variable naming --- test/detection/test_line_counter.py | 66 +++++++++++++++++------------ 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index c8a2b037..76f87c2d 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -76,7 +76,7 @@ def test_calculate_region_of_interest_limits( @pytest.mark.parametrize( - "vector, bbox_sequence, expected_count_in, expected_count_out", + "vector, bbox_sequence, expected_crossed_in, expected_crossed_out", [ ( Vector( @@ -204,7 +204,10 @@ def test_calculate_region_of_interest_limits( ], ) def test_line_zone_single_detection( - vector, bbox_sequence, expected_count_in: List[bool], expected_count_out: List[bool] + vector: Vector, + bbox_sequence: List[List[int]], + expected_crossed_in: List[bool], + expected_crossed_out: List[bool], ) -> None: line_zone = LineZone(start=vector.start, end=vector.end) for i, bbox in enumerate(bbox_sequence): @@ -212,15 +215,19 @@ def test_line_zone_single_detection( xyxy=[bbox], tracker_id=[0], ) - count_in, count_out = line_zone.trigger(detections) - assert count_in[0] == expected_count_in[i] - assert count_out[0] == expected_count_out[i] - assert line_zone.in_count == sum(expected_count_in[: (i + 1)]) - assert line_zone.out_count == sum(expected_count_out[: (i + 1)]) + crossed_in, crossed_out = line_zone.trigger(detections) + assert crossed_in[0] == expected_crossed_in[i] + assert crossed_out[0] == expected_crossed_out[i] + assert line_zone.in_count == sum(expected_crossed_in[: (i + 1)]) + assert line_zone.out_count == sum(expected_crossed_out[: (i + 1)]) @pytest.mark.parametrize( - "vector, bbox_sequence, expected_count_in, expected_count_out, crossing_anchors", + "vector," + "bbox_sequence," + "expected_crossed_in," + "expected_crossed_out," + "crossing_anchors", [ ( Vector( @@ -251,11 +258,11 @@ def test_line_zone_single_detection( ], ) def test_line_zone_single_detection_on_subset_of_anchors( - vector, - bbox_sequence, - expected_count_in: List[bool], - expected_count_out: List[bool], - crossing_anchors, + vector: Vector, + bbox_sequence: List[List[int]], + expected_crossed_in: List[bool], + expected_crossed_out: List[bool], + crossing_anchors: List[Position], ) -> None: def powerset(s): return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1)) @@ -278,17 +285,17 @@ def test_line_zone_single_detection_on_subset_of_anchors( xyxy=[bbox], tracker_id=[0], ) - count_in, count_out = line_zone.trigger(detections) + crossed_in, crossed_out = line_zone.trigger(detections) if all(anchor in crossing_anchors for anchor in anchors): - assert count_in == expected_count_in[i] - assert count_out == expected_count_out[i] + assert crossed_in == expected_crossed_in[i] + assert crossed_out == expected_crossed_out[i] else: - assert np.all(not count_in) - assert np.all(not count_out) + assert np.all(not crossed_in) + assert np.all(not crossed_out) @pytest.mark.parametrize( - "vector, bbox_sequence, expected_count_in, expected_count_out", + "vector, bbox_sequence, expected_crossed_in, expected_crossed_out", [ ( Vector( @@ -342,7 +349,10 @@ def test_line_zone_single_detection_on_subset_of_anchors( ], ) def test_line_zone_multiple_detections( - vector, bbox_sequence, expected_count_in: List[bool], expected_count_out: List[bool] + vector: Vector, + bbox_sequence: List[List[List[int]]], + expected_crossed_in: List[bool], + expected_crossed_out: List[bool], ) -> None: line_zone = LineZone(start=vector.start, end=vector.end) for i, bboxes in enumerate(bbox_sequence): @@ -350,9 +360,9 @@ def test_line_zone_multiple_detections( xyxy=bboxes, tracker_id=[i for i in range(0, len(bboxes))], ) - count_in, count_out = line_zone.trigger(detections) - assert np.all(count_in == expected_count_in[i]) - assert np.all(count_out == expected_count_out[i]) + crossed_in, crossed_out = line_zone.trigger(detections) + assert np.all(crossed_in == expected_crossed_in[i]) + assert np.all(crossed_out == expected_crossed_out[i]) @pytest.mark.parametrize( @@ -380,13 +390,15 @@ def test_line_zone_multiple_detections( ), ], ) -def test_line_zone_does_not_count_detections_without_tracker_id(vector, bbox_sequence): +def test_line_zone_does_not_count_detections_without_tracker_id( + vector: Vector, bbox_sequence: List[List[int]] +): line_zone = LineZone(start=vector.start, end=vector.end) for bbox in bbox_sequence: detections = Detections( xyxy=np.array([bbox]).reshape((-1, 4)), tracker_id=np.array([None]), ) - count_in, count_out = line_zone.trigger(detections) - assert np.all(not count_in) - assert np.all(not count_out) + crossed_in, crossed_out = line_zone.trigger(detections) + assert np.all(not crossed_in) + assert np.all(not crossed_out) From a40bf78bb20dcca49ad19c75b808a51d913da9f4 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Mon, 20 May 2024 18:17:03 +0200 Subject: [PATCH 068/182] coco mock method refactoring --- supervision/dataset/formats/coco.py | 2 +- test/dataset/formats/test_coco.py | 68 ++++++++++++++--------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index c8b73758..c07c2b52 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -157,7 +157,7 @@ def detections_to_coco_annotations( approximation_percentage=approximation_percentage, )[0].flatten() ) - ] # multicomponent masks supported only for rle format + ] coco_annotation = { "id": annotation_id, "image_id": image_id, diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 6be0fa2c..926a56e8 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -15,7 +15,7 @@ from supervision.dataset.formats.coco import ( ) -def mock_cock_coco_annotation( +def mock_coco_annotation( annotation_id: int = 0, image_id: int = 0, category_id: int = 0, @@ -107,10 +107,10 @@ def test_classes_to_coco_categories_and_back_to_classes( [ ([], {}, DoesNotRaise()), # empty coco annotations ( - [mock_cock_coco_annotation(annotation_id=0, image_id=0, category_id=0)], + [mock_coco_annotation(annotation_id=0, image_id=0, category_id=0)], { 0: [ - mock_cock_coco_annotation( + mock_coco_annotation( annotation_id=0, image_id=0, category_id=0 ) ] @@ -119,17 +119,17 @@ def test_classes_to_coco_categories_and_back_to_classes( ), # single coco annotation ( [ - mock_cock_coco_annotation(annotation_id=0, image_id=0, category_id=0), - mock_cock_coco_annotation(annotation_id=1, image_id=1, category_id=0), + mock_coco_annotation(annotation_id=0, image_id=0, category_id=0), + mock_coco_annotation(annotation_id=1, image_id=1, category_id=0), ], { 0: [ - mock_cock_coco_annotation( + mock_coco_annotation( annotation_id=0, image_id=0, category_id=0 ) ], 1: [ - mock_cock_coco_annotation( + mock_coco_annotation( annotation_id=1, image_id=1, category_id=0 ) ], @@ -138,41 +138,41 @@ def test_classes_to_coco_categories_and_back_to_classes( ), # two coco annotations ( [ - mock_cock_coco_annotation(annotation_id=0, image_id=0, category_id=0), - mock_cock_coco_annotation(annotation_id=1, image_id=1, category_id=1), - mock_cock_coco_annotation(annotation_id=2, image_id=1, category_id=2), - mock_cock_coco_annotation(annotation_id=3, image_id=2, category_id=3), - mock_cock_coco_annotation(annotation_id=4, image_id=3, category_id=1), - mock_cock_coco_annotation(annotation_id=5, image_id=3, category_id=2), - mock_cock_coco_annotation(annotation_id=5, image_id=3, category_id=3), + mock_coco_annotation(annotation_id=0, image_id=0, category_id=0), + mock_coco_annotation(annotation_id=1, image_id=1, category_id=1), + mock_coco_annotation(annotation_id=2, image_id=1, category_id=2), + mock_coco_annotation(annotation_id=3, image_id=2, category_id=3), + mock_coco_annotation(annotation_id=4, image_id=3, category_id=1), + mock_coco_annotation(annotation_id=5, image_id=3, category_id=2), + mock_coco_annotation(annotation_id=5, image_id=3, category_id=3), ], { 0: [ - mock_cock_coco_annotation( + mock_coco_annotation( annotation_id=0, image_id=0, category_id=0 ), ], 1: [ - mock_cock_coco_annotation( + mock_coco_annotation( annotation_id=1, image_id=1, category_id=1 ), - mock_cock_coco_annotation( + mock_coco_annotation( annotation_id=2, image_id=1, category_id=2 ), ], 2: [ - mock_cock_coco_annotation( + mock_coco_annotation( annotation_id=3, image_id=2, category_id=3 ), ], 3: [ - mock_cock_coco_annotation( + mock_coco_annotation( annotation_id=4, image_id=3, category_id=1 ), - mock_cock_coco_annotation( + mock_coco_annotation( annotation_id=5, image_id=3, category_id=2 ), - mock_cock_coco_annotation( + mock_coco_annotation( annotation_id=5, image_id=3, category_id=3 ), ], @@ -201,7 +201,7 @@ def test_group_coco_annotations_by_image_id( ), # empty image annotations ( [ - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(0, 0, 100, 100), area=100 * 100 ) ], @@ -215,10 +215,10 @@ def test_group_coco_annotations_by_image_id( ), # single image annotations ( [ - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(0, 0, 100, 100), area=100 * 100 ), - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(100, 100, 100, 100), area=100 * 100 ), ], @@ -234,7 +234,7 @@ def test_group_coco_annotations_by_image_id( ), # two image annotations ( [ - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(0, 0, 5, 5), area=5 * 5, @@ -262,7 +262,7 @@ def test_group_coco_annotations_by_image_id( ), # single image annotations with mask as polygon ( [ - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(0, 0, 5, 5), area=5 * 5, @@ -294,13 +294,13 @@ def test_group_coco_annotations_by_image_id( ), # single image annotations with mask, RLE segmentation mask ( [ - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(0, 0, 5, 5), area=5 * 5, segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]], ), - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(3, 0, 2, 2), area=2 * 2, @@ -339,7 +339,7 @@ def test_group_coco_annotations_by_image_id( ), # two image annotations with mask, one mask as polygon ans second as RLE ( [ - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(3, 0, 2, 2), area=2 * 2, @@ -349,7 +349,7 @@ def test_group_coco_annotations_by_image_id( }, iscrowd=True, ), - mock_cock_coco_annotation( + mock_coco_annotation( category_id=1, bbox=(0, 0, 5, 5), area=5 * 5, @@ -470,7 +470,7 @@ def test_build_coco_class_index_mapping( 0, 0, [ - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(0, 0, 100, 100), area=100 * 100 ) ], @@ -494,7 +494,7 @@ def test_build_coco_class_index_mapping( ), 0, 0, - [mock_cock_coco_annotation( + [mock_coco_annotation( category_id=0, bbox=(0, 0, 4, 5), area=4 * 5, @@ -521,7 +521,7 @@ def test_build_coco_class_index_mapping( 0, 0, [ - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(0, 0, 5, 5), area=5 * 5, @@ -553,7 +553,7 @@ def test_build_coco_class_index_mapping( 0, 0, [ - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(0, 0, 5, 5), area=5 * 5, From 2626263ff3f552634b301501b4824df160f2ee47 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Mon, 20 May 2024 18:54:10 +0200 Subject: [PATCH 069/182] move has_holes and mask_has_multiple_segments to detections utils --- supervision/dataset/formats/coco.py | 25 ++++----------- supervision/detection/utils.py | 47 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index c07c2b52..b1d67fff 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -14,7 +14,11 @@ from supervision.dataset.utils import ( rle_to_mask, ) from supervision.detection.core import Detections -from supervision.detection.utils import polygon_to_mask +from supervision.detection.utils import ( + polygon_to_mask, + mask_has_multiple_segments, + mask_has_holes +) from supervision.utils.file import read_json_file, save_json_file @@ -107,23 +111,6 @@ def coco_annotations_to_detections( return Detections(xyxy=xyxy, class_id=np.asarray(class_ids, dtype=int)) -def _mask_has_holes(mask: np.ndarray) -> bool: - mask_uint8 = mask.astype(np.uint8) - _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) - parent_countour_index = 3 - for h in hierarchy[0]: - if h[parent_countour_index] != -1: - return True - return False - - -def _mask_has_multiple_segments(mask: np.ndarray) -> bool: - mask_uint8 = mask.astype(np.uint8) - labels = np.zeros_like(mask_uint8, dtype=np.int32) - number_of_labels, _ = cv2.connectedComponents(mask_uint8, labels, connectivity=4) - return number_of_labels > 2 - - def detections_to_coco_annotations( detections: Detections, image_id: int, @@ -138,7 +125,7 @@ def detections_to_coco_annotations( segmentation = [] iscrowd = 0 if mask is not None: - iscrowd = _mask_has_holes(mask=mask) or _mask_has_multiple_segments( + iscrowd = mask_has_holes(mask=mask) or mask_has_multiple_segments( mask=mask ) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 3eeba5b4..1dd208bb 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -3,6 +3,7 @@ from typing import Dict, List, Optional, Tuple, Union import cv2 import numpy as np +import numpy.typing as npt from supervision.config import CLASS_NAME_DATA_FIELD @@ -766,3 +767,49 @@ def get_data_item( raise TypeError(f"Unsupported data type for key '{key}': {type(value)}") return subset_data + + +def mask_has_holes(mask: npt.NDArray[np.bool_]) -> bool: + """ + Checks if target objects in binary mask contain holes + (A hole is when background pixels are fully enclosed by foreground pixels) + + Args: + mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground + object and `False` indicates background. + Returns: + True when holes are detected, False otherwise. + """ + mask_uint8 = mask.astype(np.uint8) + _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) + parent_countour_index = 3 + for h in hierarchy[0]: + if h[parent_countour_index] != -1: + return True + return False + + +def mask_has_multiple_segments(mask: npt.NDArray[np.bool_], + connectivity:int = 4) -> bool: + """ + Checks if the binary mask consists of multiple not connected elements representing + the foreground objects. + Args: + mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground + object and `False` indicates background. + connectivity (int) : Default: 4 is 4-way connectivity, which means that + foreground pixels are the part of the same segment/component + if their edges touch. + Alternatively: 8 for 8-way connectivity, when foreground pixels are + connected by their edges or corners touch. + Returns: + True when the mask contains multiple not connected components, False otherwise. + """ + if connectivity!=4 and connectivity!=8: + raise ValueError('''Incorrect connectivity value,''' + ''' possible connectivity values: 4 or 8''') + mask_uint8 = mask.astype(np.uint8) + labels = np.zeros_like(mask_uint8, dtype=np.int32) + number_of_labels, _ = cv2.connectedComponents(mask_uint8, labels, + connectivity=connectivity) + return number_of_labels > 2 From 7ee84c48e02c22c6e51bb42f71372f5851cca65c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 16:56:46 +0000 Subject: [PATCH 070/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/formats/coco.py | 8 ++--- supervision/detection/utils.py | 30 +++++++++-------- test/dataset/formats/test_coco.py | 51 +++++++---------------------- 3 files changed, 32 insertions(+), 57 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index b1d67fff..3c684871 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -15,9 +15,9 @@ from supervision.dataset.utils import ( ) from supervision.detection.core import Detections from supervision.detection.utils import ( - polygon_to_mask, + mask_has_holes, mask_has_multiple_segments, - mask_has_holes + polygon_to_mask, ) from supervision.utils.file import read_json_file, save_json_file @@ -125,9 +125,7 @@ def detections_to_coco_annotations( segmentation = [] iscrowd = 0 if mask is not None: - iscrowd = mask_has_holes(mask=mask) or mask_has_multiple_segments( - mask=mask - ) + iscrowd = mask_has_holes(mask=mask) or mask_has_multiple_segments(mask=mask) if iscrowd: segmentation = { diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 1dd208bb..f00641fb 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -771,7 +771,7 @@ def get_data_item( def mask_has_holes(mask: npt.NDArray[np.bool_]) -> bool: """ - Checks if target objects in binary mask contain holes + Checks if target objects in binary mask contain holes (A hole is when background pixels are fully enclosed by foreground pixels) Args: @@ -789,27 +789,31 @@ def mask_has_holes(mask: npt.NDArray[np.bool_]) -> bool: return False -def mask_has_multiple_segments(mask: npt.NDArray[np.bool_], - connectivity:int = 4) -> bool: +def mask_has_multiple_segments( + mask: npt.NDArray[np.bool_], connectivity: int = 4 +) -> bool: """ - Checks if the binary mask consists of multiple not connected elements representing + Checks if the binary mask consists of multiple not connected elements representing the foreground objects. Args: mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground object and `False` indicates background. - connectivity (int) : Default: 4 is 4-way connectivity, which means that - foreground pixels are the part of the same segment/component - if their edges touch. - Alternatively: 8 for 8-way connectivity, when foreground pixels are + connectivity (int) : Default: 4 is 4-way connectivity, which means that + foreground pixels are the part of the same segment/component + if their edges touch. + Alternatively: 8 for 8-way connectivity, when foreground pixels are connected by their edges or corners touch. Returns: True when the mask contains multiple not connected components, False otherwise. """ - if connectivity!=4 and connectivity!=8: - raise ValueError('''Incorrect connectivity value,''' - ''' possible connectivity values: 4 or 8''') + if connectivity != 4 and connectivity != 8: + raise ValueError( + """Incorrect connectivity value,""" + """ possible connectivity values: 4 or 8""" + ) mask_uint8 = mask.astype(np.uint8) labels = np.zeros_like(mask_uint8, dtype=np.int32) - number_of_labels, _ = cv2.connectedComponents(mask_uint8, labels, - connectivity=connectivity) + number_of_labels, _ = cv2.connectedComponents( + mask_uint8, labels, connectivity=connectivity + ) return number_of_labels > 2 diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 9c89b964..7e269dae 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -108,13 +108,7 @@ def test_classes_to_coco_categories_and_back_to_classes( ([], {}, DoesNotRaise()), # empty coco annotations ( [mock_coco_annotation(annotation_id=0, image_id=0, category_id=0)], - { - 0: [ - mock_coco_annotation( - annotation_id=0, image_id=0, category_id=0 - ) - ] - }, + {0: [mock_coco_annotation(annotation_id=0, image_id=0, category_id=0)]}, DoesNotRaise(), ), # single coco annotation ( @@ -123,16 +117,8 @@ def test_classes_to_coco_categories_and_back_to_classes( mock_coco_annotation(annotation_id=1, image_id=1, category_id=0), ], { - 0: [ - mock_coco_annotation( - annotation_id=0, image_id=0, category_id=0 - ) - ], - 1: [ - mock_coco_annotation( - annotation_id=1, image_id=1, category_id=0 - ) - ], + 0: [mock_coco_annotation(annotation_id=0, image_id=0, category_id=0)], + 1: [mock_coco_annotation(annotation_id=1, image_id=1, category_id=0)], }, DoesNotRaise(), ), # two coco annotations @@ -148,33 +134,19 @@ def test_classes_to_coco_categories_and_back_to_classes( ], { 0: [ - mock_coco_annotation( - annotation_id=0, image_id=0, category_id=0 - ), + mock_coco_annotation(annotation_id=0, image_id=0, category_id=0), ], 1: [ - mock_coco_annotation( - annotation_id=1, image_id=1, category_id=1 - ), - mock_coco_annotation( - annotation_id=2, image_id=1, category_id=2 - ), + mock_coco_annotation(annotation_id=1, image_id=1, category_id=1), + mock_coco_annotation(annotation_id=2, image_id=1, category_id=2), ], 2: [ - mock_coco_annotation( - annotation_id=3, image_id=2, category_id=3 - ), + mock_coco_annotation(annotation_id=3, image_id=2, category_id=3), ], 3: [ - mock_coco_annotation( - annotation_id=4, image_id=3, category_id=1 - ), - mock_coco_annotation( - annotation_id=5, image_id=3, category_id=2 - ), - mock_coco_annotation( - annotation_id=5, image_id=3, category_id=3 - ), + mock_coco_annotation(annotation_id=4, image_id=3, category_id=1), + mock_coco_annotation(annotation_id=5, image_id=3, category_id=2), + mock_coco_annotation(annotation_id=5, image_id=3, category_id=3), ], }, DoesNotRaise(), @@ -494,7 +466,8 @@ def test_build_coco_class_index_mapping( ), 0, 0, - [mock_coco_annotation( + [ + mock_coco_annotation( category_id=0, bbox=(0, 0, 4, 5), area=4 * 5, From 68f07e18f38c208b5e9177b4a957d511a2eaf247 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Mon, 20 May 2024 19:23:22 +0200 Subject: [PATCH 071/182] tests for mask_has_holes --- supervision/detection/utils.py | 15 ++++++--- test/detection/test_utils.py | 59 ++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 1dd208bb..b39b7b38 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -782,10 +782,12 @@ def mask_has_holes(mask: npt.NDArray[np.bool_]) -> bool: """ mask_uint8 = mask.astype(np.uint8) _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) - parent_countour_index = 3 - for h in hierarchy[0]: - if h[parent_countour_index] != -1: - return True + + if hierarchy: # at least one contour was found + parent_countour_index = 3 + for h in hierarchy[0]: + if h[parent_countour_index] != -1: + return True return False @@ -794,6 +796,7 @@ def mask_has_multiple_segments(mask: npt.NDArray[np.bool_], """ Checks if the binary mask consists of multiple not connected elements representing the foreground objects. + Args: mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground object and `False` indicates background. @@ -802,8 +805,12 @@ def mask_has_multiple_segments(mask: npt.NDArray[np.bool_], if their edges touch. Alternatively: 8 for 8-way connectivity, when foreground pixels are connected by their edges or corners touch. + Returns: True when the mask contains multiple not connected components, False otherwise. + + Raises: + ValueError: If connectivity(int) parameter value is not 4 or 8. """ if connectivity!=4 and connectivity!=8: raise ValueError('''Incorrect connectivity value,''' diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index 1c4a1d34..1214cfed 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -2,6 +2,7 @@ from contextlib import ExitStack as DoesNotRaise from typing import Any, Dict, List, Optional, Tuple import numpy as np +import numpy.typing as npt import pytest from supervision.config import CLASS_NAME_DATA_FIELD @@ -16,6 +17,8 @@ from supervision.detection.utils import ( move_boxes, process_roboflow_result, scale_boxes, + mask_has_holes, + mask_has_multiple_segments, ) TEST_MASK = np.zeros((1, 1000, 1000), dtype=bool) @@ -1203,3 +1206,59 @@ def test_get_data_item( assert ( result[key] == expected_result[key] ), f"Mismatch in non-array data for key {key}" + + +@pytest.mark.parametrize( + "mask, expected_result, exception", + [ + (np.array([[0, 0, 0, 0], + [0, 1, 1, 0], + [0, 1, 0, 0], + [0, 1, 1, 0]]).astype(bool), + False, + DoesNotRaise(), + ), # foreground object in one continuous piece + (np.array([[1, 0, 0, 0], + [1, 0, 0, 0], + [0, 0, 0, 0], + [0, 1, 1, 0]]).astype(bool), + False, + DoesNotRaise(), + ), # foreground object in 2 seperate elements + (np.array([[0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]]).astype(bool), + False, + DoesNotRaise(), + ), # no foreground pixels in mask + (np.array([[1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1]]).astype(bool), + False, + DoesNotRaise(), + ), # only foreground pixels in mask + (np.array([[1, 1, 1, 0], + [1, 0, 1, 0], + [1, 1, 1, 0], + [0, 0, 0, 0]]).astype(bool), + True, + DoesNotRaise(), + ), # foreground object has 1 hole + (np.array([[1, 1, 1, 0], + [1, 0, 1, 1], + [1, 1, 0, 1], + [0, 1, 1, 1]]).astype(bool), + True, + DoesNotRaise(), + ), # foreground object has 2 holes + ], +) +def test_mask_has_holes( + mask: npt.NDArray[np.bool_], expected_result: bool, exception: Exception +) -> None: + with exception: + result = mask_has_holes(mask) + assert result == expected_result + \ No newline at end of file From c26eb1a70f4271930ca0f17ee3f13aa70892b0be Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Mon, 20 May 2024 19:52:48 +0200 Subject: [PATCH 072/182] unit tests for test_mask_has_multiple_segments --- test/detection/test_utils.py | 81 +++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index 1214cfed..a9472f85 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -1261,4 +1261,83 @@ def test_mask_has_holes( with exception: result = mask_has_holes(mask) assert result == expected_result - \ No newline at end of file + + +@pytest.mark.parametrize( + "mask, connectivity, expected_result, exception", + [ + (np.array([[0, 0, 0, 0], + [0, 1, 1, 0], + [0, 1, 0, 0], + [0, 1, 1, 0]]).astype(bool), + 4, + False, + DoesNotRaise(), + ), # foreground object in one continuous piece + (np.array([[1, 0, 0, 0], + [1, 0, 0, 0], + [0, 0, 0, 0], + [0, 1, 1, 0]]).astype(bool), + 4, + True, + DoesNotRaise(), + ), # foreground object in 2 seperate elements + (np.array([[0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]]).astype(bool), + 4, + False, + DoesNotRaise(), + ), # no foreground pixels in mask + (np.array([[1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1]]).astype(bool), + 4, + False, + DoesNotRaise(), + ), # only foreground pixels in mask + (np.array([[1, 1, 1, 0], + [1, 0, 1, 1], + [1, 1, 0, 1], + [0, 1, 1, 1]]).astype(bool), + 4, + False, + DoesNotRaise(), + ), # foreground object has 2 holes, but is in single piece + (np.array([[1, 1, 0, 0], + [1, 1, 0, 1], + [1, 0, 1, 1], + [0, 0, 1, 1]]).astype(bool), + 4, + True, + DoesNotRaise(), + ), # foreground object in 2 elements with respect to 4-way connectivity + (np.array([[1, 1, 0, 0], + [1, 1, 0, 1], + [1, 0, 1, 1], + [0, 0, 1, 1]]).astype(bool), + 8, + False, + DoesNotRaise(), + ), # foreground object in single piece with respect to 8-way connectivity + (np.array([[1, 1, 0, 0], + [1, 1, 0, 1], + [1, 0, 1, 1], + [0, 0, 1, 1]]).astype(bool), + 5, + None, + pytest.raises(ValueError), + ), # Incorrect connectivity parameter value, raises ValueError + ], +) +def test_mask_has_multiple_segments( + mask: npt.NDArray[np.bool_], + connectivity: int, + expected_result: bool, + exception: Exception +) -> None: + with exception: + result = mask_has_multiple_segments(mask = mask, connectivity=connectivity) + assert result == expected_result From dfac30cba7eb626236d0c60bf01091e42db82949 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Mon, 20 May 2024 20:45:17 +0200 Subject: [PATCH 073/182] change docu for mask_has_multiple_segments and mask_has_holes and add to global __init__.py --- docs/detection/utils.md | 12 ++++++++++++ supervision/__init__.py | 2 ++ 2 files changed, 14 insertions(+) diff --git a/docs/detection/utils.md b/docs/detection/utils.md index abacdc21..9271c23d 100644 --- a/docs/detection/utils.md +++ b/docs/detection/utils.md @@ -70,3 +70,15 @@ status: new :::supervision.detection.utils.scale_boxes + + + +:::supervision.detection.utils.mask_has_holes + + + +:::supervision.detection.utils.mask_has_multiple_segments diff --git a/supervision/__init__.py b/supervision/__init__.py index 2bc72944..caa0e5ae 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -56,6 +56,8 @@ from supervision.detection.utils import ( polygon_to_mask, polygon_to_xyxy, scale_boxes, + mask_has_holes, + mask_has_multiple_segments, ) from supervision.draw.color import Color, ColorPalette from supervision.draw.utils import ( From ab55e810876749e4bc62397c9aa39f1e71d4812c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 19:41:32 +0000 Subject: [PATCH 074/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/detection/utils.md | 2 +- supervision/__init__.py | 4 +- supervision/detection/utils.py | 2 +- test/detection/test_utils.py | 228 ++++++++++++++++----------------- 4 files changed, 118 insertions(+), 118 deletions(-) diff --git a/docs/detection/utils.md b/docs/detection/utils.md index 8b9f91f7..0c42a69e 100644 --- a/docs/detection/utils.md +++ b/docs/detection/utils.md @@ -99,4 +99,4 @@ status: new

mask_has_multiple_segments

-:::supervision.detection.utils.mask_has_multiple_segments \ No newline at end of file +:::supervision.detection.utils.mask_has_multiple_segments diff --git a/supervision/__init__.py b/supervision/__init__.py index 7f50a7ad..51b43ef0 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -50,6 +50,8 @@ from supervision.detection.utils import ( calculate_masks_centroids, clip_boxes, filter_polygons_by_area, + mask_has_holes, + mask_has_multiple_segments, mask_iou_batch, mask_non_max_suppression, mask_to_polygons, @@ -60,8 +62,6 @@ from supervision.detection.utils import ( polygon_to_mask, polygon_to_xyxy, scale_boxes, - mask_has_holes, - mask_has_multiple_segments, ) from supervision.draw.color import Color, ColorPalette from supervision.draw.utils import ( diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 9dd3d734..35510060 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -857,7 +857,7 @@ def mask_has_holes(mask: npt.NDArray[np.bool_]) -> bool: mask_uint8 = mask.astype(np.uint8) _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) - if hierarchy: # at least one contour was found + if hierarchy: # at least one contour was found parent_countour_index = 3 for h in hierarchy[0]: if h[parent_countour_index] != -1: diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index b72ca4bf..2821aed2 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -12,13 +12,13 @@ from supervision.detection.utils import ( clip_boxes, filter_polygons_by_area, get_data_item, + mask_has_holes, + mask_has_multiple_segments, mask_non_max_suppression, merge_data, move_boxes, process_roboflow_result, scale_boxes, - mask_has_holes, - mask_has_multiple_segments, ) TEST_MASK = np.zeros((1, 1000, 1000), dtype=bool) @@ -1273,48 +1273,48 @@ def test_get_data_item( @pytest.mark.parametrize( "mask, expected_result, exception", [ - (np.array([[0, 0, 0, 0], - [0, 1, 1, 0], - [0, 1, 0, 0], - [0, 1, 1, 0]]).astype(bool), - False, - DoesNotRaise(), - ), # foreground object in one continuous piece - (np.array([[1, 0, 0, 0], - [1, 0, 0, 0], - [0, 0, 0, 0], - [0, 1, 1, 0]]).astype(bool), - False, - DoesNotRaise(), - ), # foreground object in 2 seperate elements - (np.array([[0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0]]).astype(bool), - False, - DoesNotRaise(), - ), # no foreground pixels in mask - (np.array([[1, 1, 1, 1], - [1, 1, 1, 1], - [1, 1, 1, 1], - [1, 1, 1, 1]]).astype(bool), - False, - DoesNotRaise(), - ), # only foreground pixels in mask - (np.array([[1, 1, 1, 0], - [1, 0, 1, 0], - [1, 1, 1, 0], - [0, 0, 0, 0]]).astype(bool), - True, - DoesNotRaise(), - ), # foreground object has 1 hole - (np.array([[1, 1, 1, 0], - [1, 0, 1, 1], - [1, 1, 0, 1], - [0, 1, 1, 1]]).astype(bool), - True, - DoesNotRaise(), - ), # foreground object has 2 holes + ( + np.array([[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 0, 0], [0, 1, 1, 0]]).astype( + bool + ), + False, + DoesNotRaise(), + ), # foreground object in one continuous piece + ( + np.array([[1, 0, 0, 0], [1, 0, 0, 0], [0, 0, 0, 0], [0, 1, 1, 0]]).astype( + bool + ), + False, + DoesNotRaise(), + ), # foreground object in 2 seperate elements + ( + np.array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]).astype( + bool + ), + False, + DoesNotRaise(), + ), # no foreground pixels in mask + ( + np.array([[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]).astype( + bool + ), + False, + DoesNotRaise(), + ), # only foreground pixels in mask + ( + np.array([[1, 1, 1, 0], [1, 0, 1, 0], [1, 1, 1, 0], [0, 0, 0, 0]]).astype( + bool + ), + True, + DoesNotRaise(), + ), # foreground object has 1 hole + ( + np.array([[1, 1, 1, 0], [1, 0, 1, 1], [1, 1, 0, 1], [0, 1, 1, 1]]).astype( + bool + ), + True, + DoesNotRaise(), + ), # foreground object has 2 holes ], ) def test_mask_has_holes( @@ -1322,84 +1322,84 @@ def test_mask_has_holes( ) -> None: with exception: result = mask_has_holes(mask) - assert result == expected_result + assert result == expected_result @pytest.mark.parametrize( "mask, connectivity, expected_result, exception", [ - (np.array([[0, 0, 0, 0], - [0, 1, 1, 0], - [0, 1, 0, 0], - [0, 1, 1, 0]]).astype(bool), - 4, - False, - DoesNotRaise(), - ), # foreground object in one continuous piece - (np.array([[1, 0, 0, 0], - [1, 0, 0, 0], - [0, 0, 0, 0], - [0, 1, 1, 0]]).astype(bool), - 4, - True, - DoesNotRaise(), - ), # foreground object in 2 seperate elements - (np.array([[0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0]]).astype(bool), - 4, - False, - DoesNotRaise(), - ), # no foreground pixels in mask - (np.array([[1, 1, 1, 1], - [1, 1, 1, 1], - [1, 1, 1, 1], - [1, 1, 1, 1]]).astype(bool), - 4, - False, - DoesNotRaise(), - ), # only foreground pixels in mask - (np.array([[1, 1, 1, 0], - [1, 0, 1, 1], - [1, 1, 0, 1], - [0, 1, 1, 1]]).astype(bool), - 4, - False, - DoesNotRaise(), - ), # foreground object has 2 holes, but is in single piece - (np.array([[1, 1, 0, 0], - [1, 1, 0, 1], - [1, 0, 1, 1], - [0, 0, 1, 1]]).astype(bool), - 4, - True, - DoesNotRaise(), - ), # foreground object in 2 elements with respect to 4-way connectivity - (np.array([[1, 1, 0, 0], - [1, 1, 0, 1], - [1, 0, 1, 1], - [0, 0, 1, 1]]).astype(bool), - 8, - False, - DoesNotRaise(), - ), # foreground object in single piece with respect to 8-way connectivity - (np.array([[1, 1, 0, 0], - [1, 1, 0, 1], - [1, 0, 1, 1], - [0, 0, 1, 1]]).astype(bool), - 5, - None, - pytest.raises(ValueError), - ), # Incorrect connectivity parameter value, raises ValueError + ( + np.array([[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 0, 0], [0, 1, 1, 0]]).astype( + bool + ), + 4, + False, + DoesNotRaise(), + ), # foreground object in one continuous piece + ( + np.array([[1, 0, 0, 0], [1, 0, 0, 0], [0, 0, 0, 0], [0, 1, 1, 0]]).astype( + bool + ), + 4, + True, + DoesNotRaise(), + ), # foreground object in 2 seperate elements + ( + np.array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]).astype( + bool + ), + 4, + False, + DoesNotRaise(), + ), # no foreground pixels in mask + ( + np.array([[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]).astype( + bool + ), + 4, + False, + DoesNotRaise(), + ), # only foreground pixels in mask + ( + np.array([[1, 1, 1, 0], [1, 0, 1, 1], [1, 1, 0, 1], [0, 1, 1, 1]]).astype( + bool + ), + 4, + False, + DoesNotRaise(), + ), # foreground object has 2 holes, but is in single piece + ( + np.array([[1, 1, 0, 0], [1, 1, 0, 1], [1, 0, 1, 1], [0, 0, 1, 1]]).astype( + bool + ), + 4, + True, + DoesNotRaise(), + ), # foreground object in 2 elements with respect to 4-way connectivity + ( + np.array([[1, 1, 0, 0], [1, 1, 0, 1], [1, 0, 1, 1], [0, 0, 1, 1]]).astype( + bool + ), + 8, + False, + DoesNotRaise(), + ), # foreground object in single piece with respect to 8-way connectivity + ( + np.array([[1, 1, 0, 0], [1, 1, 0, 1], [1, 0, 1, 1], [0, 0, 1, 1]]).astype( + bool + ), + 5, + None, + pytest.raises(ValueError), + ), # Incorrect connectivity parameter value, raises ValueError ], ) def test_mask_has_multiple_segments( mask: npt.NDArray[np.bool_], - connectivity: int, - expected_result: bool, - exception: Exception + connectivity: int, + expected_result: bool, + exception: Exception, ) -> None: with exception: - result = mask_has_multiple_segments(mask = mask, connectivity=connectivity) - assert result == expected_result + result = mask_has_multiple_segments(mask=mask, connectivity=connectivity) + assert result == expected_result From 6dbf3f61e3e764c56c6152f18088f7624354d1dd Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Mon, 20 May 2024 21:47:04 +0200 Subject: [PATCH 075/182] fix for unit tests for test_mask_has_holes --- supervision/detection/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 35510060..89728c7a 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -857,7 +857,7 @@ def mask_has_holes(mask: npt.NDArray[np.bool_]) -> bool: mask_uint8 = mask.astype(np.uint8) _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) - if hierarchy: # at least one contour was found + if hierarchy is not None: # at least one contour was found parent_countour_index = 3 for h in hierarchy[0]: if h[parent_countour_index] != -1: From 414d3e9afbbd9326bb91f3e2eb58a426bce4e8c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 01:01:12 +0000 Subject: [PATCH 076/182] --- updated-dependencies: - dependency-name: requests dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9ed05cfb..38eae842 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3461,13 +3461,13 @@ files = [ [[package]] name = "requests" -version = "2.31.0" +version = "2.32.1" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, + {file = "requests-2.32.1-py3-none-any.whl", hash = "sha256:21ac9465cdf8c1650fe1ecde8a71669a93d4e6f147550483a2967d08396a56a5"}, + {file = "requests-2.32.1.tar.gz", hash = "sha256:eb97e87e64c79e64e5b8ac75cee9dd1f97f49e289b083ee6be96268930725685"}, ] [package.dependencies] @@ -4258,4 +4258,4 @@ desktop = ["opencv-python"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "29af5aa06f97e77a2dba94c5a6d77d7d1903448724df07416026a378d3c6a64d" +content-hash = "7ecea27cde915f67ee71e0ed55cabd890c6473e5f222434efd0b1f4392713312" diff --git a/pyproject.toml b/pyproject.toml index 8ceacbff..252bb49d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ pyyaml = ">=5.3" defusedxml = "^0.7.1" opencv-python = { version = ">=4.5.5.64", optional = true } opencv-python-headless = ">=4.5.5.64" -requests = { version = ">=2.26.0,<=2.31.0", optional = true } +requests = { version = ">=2.26.0,<=2.32.1", optional = true } tqdm = { version = ">=4.62.3,<=4.66.4", optional = true } pillow = ">=9.4" From 32f773548e6ce233c8fd0ae6afd1327861940e15 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 01:02:35 +0000 Subject: [PATCH 077/182] --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9ed05cfb..c9807aef 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2199,13 +2199,13 @@ pygments = ">2.12.0" [[package]] name = "mkdocs-material" -version = "9.5.23" +version = "9.5.24" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.23-py3-none-any.whl", hash = "sha256:ffd08a5beaef3cd135aceb58ded8b98bbbbf2b70e5b656f6a14a63c917d9b001"}, - {file = "mkdocs_material-9.5.23.tar.gz", hash = "sha256:4627fc3f15de2cba2bde9debc2fd59b9888ef494beabfe67eb352e23d14bf288"}, + {file = "mkdocs_material-9.5.24-py3-none-any.whl", hash = "sha256:e12cd75954c535b61e716f359cf2a5056bf4514889d17161fdebd5df4b0153c6"}, + {file = "mkdocs_material-9.5.24.tar.gz", hash = "sha256:02d5aaba0ee755e707c3ef6e748f9acb7b3011187c0ea766db31af8905078a34"}, ] [package.dependencies] From f2716252d59ac60da041f1e3bab1b1744726d79a Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Tue, 21 May 2024 11:43:09 +0200 Subject: [PATCH 078/182] small refactor + docs improvements --- docs/detection/utils.md | 10 +-- supervision/__init__.py | 4 +- supervision/dataset/formats/coco.py | 6 +- supervision/detection/utils.py | 106 ++++++++++++++++++++++------ test/detection/test_utils.py | 12 ++-- 5 files changed, 100 insertions(+), 38 deletions(-) diff --git a/docs/detection/utils.md b/docs/detection/utils.md index 0c42a69e..f9c9473b 100644 --- a/docs/detection/utils.md +++ b/docs/detection/utils.md @@ -78,7 +78,7 @@ status: new :::supervision.detection.utils.scale_boxes :::supervision.detection.utils.clip_boxes @@ -90,13 +90,13 @@ status: new :::supervision.detection.utils.pad_boxes -:::supervision.detection.utils.mask_has_holes +:::supervision.detection.utils.contains_holes -:::supervision.detection.utils.mask_has_multiple_segments +:::supervision.detection.utils.contains_multiple_segments diff --git a/supervision/__init__.py b/supervision/__init__.py index 51b43ef0..af5f8dcc 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -50,8 +50,8 @@ from supervision.detection.utils import ( calculate_masks_centroids, clip_boxes, filter_polygons_by_area, - mask_has_holes, - mask_has_multiple_segments, + contains_holes, + contains_multiple_segments, mask_iou_batch, mask_non_max_suppression, mask_to_polygons, diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 3c684871..353e33f5 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -15,8 +15,8 @@ from supervision.dataset.utils import ( ) from supervision.detection.core import Detections from supervision.detection.utils import ( - mask_has_holes, - mask_has_multiple_segments, + contains_holes, + contains_multiple_segments, polygon_to_mask, ) from supervision.utils.file import read_json_file, save_json_file @@ -125,7 +125,7 @@ def detections_to_coco_annotations( segmentation = [] iscrowd = 0 if mask is not None: - iscrowd = mask_has_holes(mask=mask) or mask_has_multiple_segments(mask=mask) + iscrowd = contains_holes(mask=mask) or contains_multiple_segments(mask=mask) if iscrowd: segmentation = { diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 89728c7a..5a8e7ba2 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -609,13 +609,16 @@ def move_boxes(xyxy: np.ndarray, offset: np.ndarray) -> np.ndarray: import numpy as np import supervision as sv - boxes = np.array([[10, 10, 20, 20], [30, 30, 40, 40]]) + xyxy = np.array([ + [10, 10, 20, 20], + [30, 30, 40, 40] + ]) offset = np.array([5, 5]) - moved_box = sv.move_boxes(boxes, offset) - print(moved_box) - # np.array([ + + sv.move_boxes(xyxy=xyxy, offset=offset) + # array([ # [15, 15, 25, 25], - # [35, 35, 45, 45] + # [35, 35, 45, 45] # ]) ``` """ @@ -675,11 +678,13 @@ def scale_boxes(xyxy: np.ndarray, factor: float) -> np.ndarray: import numpy as np import supervision as sv - boxes = np.array([[10, 10, 20, 20], [30, 30, 40, 40]]) - factor = 1.5 - scaled_bb = sv.scale_boxes(boxes, factor) - print(scaled_bb) - # np.array([ + xyxy = np.array([ + [10, 10, 20, 20], + [30, 30, 40, 40] + ]) + + scaled_bb = sv.scale_boxes(xyxy=xyxy, factor=1.5) + # array([ # [ 7.5, 7.5, 22.5, 22.5], # [27.5, 27.5, 42.5, 42.5] # ]) @@ -843,34 +848,62 @@ def get_data_item( return subset_data -def mask_has_holes(mask: npt.NDArray[np.bool_]) -> bool: +def contains_holes(mask: npt.NDArray[np.bool_]) -> bool: """ - Checks if target objects in binary mask contain holes - (A hole is when background pixels are fully enclosed by foreground pixels) + Checks if the binary mask contains holes (background pixels fully enclosed by + foreground pixels). Args: mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground object and `False` indicates background. + Returns: - True when holes are detected, False otherwise. + True if holes are detected, False otherwise. + + Examples: + ```python + import numpy as np + import supervision as sv + + mask = np.array([ + [0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 0, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0] + ]).astype(bool) + + sv.contains_holes(mask=mask) + # True + + mask = np.array([ + [0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0] + ]).astype(bool) + + sv.contains_holes(mask=mask) + # False + ``` """ mask_uint8 = mask.astype(np.uint8) _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) - if hierarchy is not None: # at least one contour was found - parent_countour_index = 3 + if hierarchy is not None: + parent_contour_index = 3 for h in hierarchy[0]: - if h[parent_countour_index] != -1: + if h[parent_contour_index] != -1: return True return False -def mask_has_multiple_segments( +def contains_multiple_segments( mask: npt.NDArray[np.bool_], connectivity: int = 4 ) -> bool: """ - Checks if the binary mask consists of multiple not connected elements representing - the foreground objects. + Checks if the binary mask contains multiple unconnected foreground segments. Args: mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground @@ -886,11 +919,40 @@ def mask_has_multiple_segments( Raises: ValueError: If connectivity(int) parameter value is not 4 or 8. + + Examples: + ```python + import numpy as np + import supervision as sv + + mask = np.array([ + [0, 0, 0, 0, 0, 0], + [0, 1, 1, 0, 1, 1], + [0, 1, 1, 0, 1, 1], + [0, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 0, 0] + ]).astype(bool) + + sv.contains_multiple_segments(mask=mask, connectivity=4) + # True + + mask = np.array([ + [0, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0] + ]).astype(bool) + + sv.contains_multiple_segments(mask=mask, connectivity=4) + # False + ``` """ if connectivity != 4 and connectivity != 8: raise ValueError( - """Incorrect connectivity value,""" - """ possible connectivity values: 4 or 8""" + "Incorrect connectivity value. Possible connectivity values: 4 or 8." ) mask_uint8 = mask.astype(np.uint8) labels = np.zeros_like(mask_uint8, dtype=np.int32) diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index 2821aed2..c7ed674a 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -12,8 +12,8 @@ from supervision.detection.utils import ( clip_boxes, filter_polygons_by_area, get_data_item, - mask_has_holes, - mask_has_multiple_segments, + contains_holes, + contains_multiple_segments, mask_non_max_suppression, merge_data, move_boxes, @@ -1317,11 +1317,11 @@ def test_get_data_item( ), # foreground object has 2 holes ], ) -def test_mask_has_holes( +def test_contains_holes( mask: npt.NDArray[np.bool_], expected_result: bool, exception: Exception ) -> None: with exception: - result = mask_has_holes(mask) + result = contains_holes(mask) assert result == expected_result @@ -1394,12 +1394,12 @@ def test_mask_has_holes( ), # Incorrect connectivity parameter value, raises ValueError ], ) -def test_mask_has_multiple_segments( +def test_contains_multiple_segments( mask: npt.NDArray[np.bool_], connectivity: int, expected_result: bool, exception: Exception, ) -> None: with exception: - result = mask_has_multiple_segments(mask=mask, connectivity=connectivity) + result = contains_multiple_segments(mask=mask, connectivity=connectivity) assert result == expected_result From 996b0a3af346f15fb0e692b6d58bcbf98a2de3ba Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 09:43:28 +0000 Subject: [PATCH 079/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/__init__.py | 2 +- test/detection/test_utils.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/supervision/__init__.py b/supervision/__init__.py index af5f8dcc..abe63390 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -49,9 +49,9 @@ from supervision.detection.utils import ( box_non_max_suppression, calculate_masks_centroids, clip_boxes, - filter_polygons_by_area, contains_holes, contains_multiple_segments, + filter_polygons_by_area, mask_iou_batch, mask_non_max_suppression, mask_to_polygons, diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index c7ed674a..6a2070da 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -10,10 +10,10 @@ from supervision.detection.utils import ( box_non_max_suppression, calculate_masks_centroids, clip_boxes, - filter_polygons_by_area, - get_data_item, contains_holes, contains_multiple_segments, + filter_polygons_by_area, + get_data_item, mask_non_max_suppression, merge_data, move_boxes, From 1d43042c55418ecba8fcfbfe7973e65e15147f03 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Tue, 21 May 2024 12:01:05 +0200 Subject: [PATCH 080/182] small refactor + docs improvements --- supervision/dataset/core.py | 38 ++++++++++++++++++++-------------- supervision/detection/utils.py | 4 ++-- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/supervision/dataset/core.py b/supervision/dataset/core.py index 6ae5844a..c8863df3 100644 --- a/supervision/dataset/core.py +++ b/supervision/dataset/core.py @@ -116,13 +116,12 @@ class DetectionDataset(BaseDataset): Tuple[DetectionDataset, DetectionDataset]: A tuple containing the training and testing datasets. - Example: + Examples: ```python import supervision as sv ds = sv.DetectionDataset(...) - train_ds, test_ds = ds.split(split_ratio=0.7, - random_state=42, shuffle=True) + train_ds, test_ds = ds.split(split_ratio=0.7, random_state=42, shuffle=True) len(train_ds), len(test_ds) # (700, 300) ``` @@ -229,7 +228,7 @@ class DetectionDataset(BaseDataset): DetectionDataset: A DetectionDataset instance containing the loaded images and annotations. - Example: + Examples: ```python import roboflow from roboflow import Roboflow @@ -286,7 +285,7 @@ class DetectionDataset(BaseDataset): DetectionDataset: A DetectionDataset instance containing the loaded images and annotations. - Example: + Examples: ```python import roboflow from roboflow import Roboflow @@ -391,7 +390,7 @@ class DetectionDataset(BaseDataset): DetectionDataset: A DetectionDataset instance containing the loaded images and annotations. - Example: + Examples: ```python import roboflow from roboflow import Roboflow @@ -430,10 +429,20 @@ class DetectionDataset(BaseDataset): """ Exports the dataset to COCO format. This method saves the images and their corresponding annotations in COCO format. - The format of the mask is determined automatically: - when a mask consists of multiple disconnected elements - or has holes the RLE format is used, - otherwise, the mask is encoded as a polygon. + + !!! tip + + The format of the mask is determined automatically based on its structure: + + - If a mask contains multiple disconnected components or holes, it will be + saved using the Run-Length Encoding (RLE) format for efficient storage and + processing. + - If a mask consists of a single, contiguous region without any holes, it + will be encoded as a polygon, preserving the outline of the object. + + This automatic selection ensures that the masks are stored in the most + appropriate and space-efficient format, complying with COCO dataset + standards. Args: images_directory_path (Optional[str]): The path to the directory @@ -486,7 +495,7 @@ class DetectionDataset(BaseDataset): (DetectionDataset): A single `DetectionDataset` object containing the merged data from the input list. - Example: + Examples: ```python import supervision as sv @@ -571,13 +580,12 @@ class ClassificationDataset(BaseDataset): Tuple[ClassificationDataset, ClassificationDataset]: A tuple containing the training and testing datasets. - Example: + Examples: ```python import supervision as sv cd = sv.ClassificationDataset(...) - train_cd,test_cd = cd.split(split_ratio=0.7, - random_state=42,shuffle=True) + train_cd,test_cd = cd.split(split_ratio=0.7, random_state=42,shuffle=True) len(train_cd), len(test_cd) # (700, 300) ``` @@ -639,7 +647,7 @@ class ClassificationDataset(BaseDataset): Returns: ClassificationDataset: The dataset. - Example: + Examples: ```python import roboflow from roboflow import Roboflow diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 5a8e7ba2..f1e77883 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -604,7 +604,7 @@ def move_boxes(xyxy: np.ndarray, offset: np.ndarray) -> np.ndarray: Returns: np.ndarray: Repositioned bounding boxes. - Example: + Examples: ```python import numpy as np import supervision as sv @@ -673,7 +673,7 @@ def scale_boxes(xyxy: np.ndarray, factor: float) -> np.ndarray: Returns: np.ndarray: Scaled bounding boxes. - Example: + Examples: ```python import numpy as np import supervision as sv From 40b537c0bebe535e93ed7b7eef719c3a2a2e57e0 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Tue, 21 May 2024 13:58:20 +0300 Subject: [PATCH 081/182] tracker_id contents are never None --- supervision/detection/line_zone.py | 10 +++++++--- supervision/detection/tools/smoother.py | 2 -- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/supervision/detection/line_zone.py b/supervision/detection/line_zone.py index 53d762a0..fe850894 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -140,6 +140,13 @@ class LineZone: if len(detections) == 0: return crossed_in, crossed_out + if detections.tracker_id is None: + print( + "Line zone conting skipped. LineZone requires tracker_id. Refer to " + "https://supervision.roboflow.com/latest/trackers for more information." + ) + return crossed_in, crossed_out + all_anchors = np.array( [ detections.get_anchors_coordinates(anchor) @@ -148,9 +155,6 @@ class LineZone: ) for i, tracker_id in enumerate(detections.tracker_id): - if tracker_id is None: - continue - box_anchors = [Point(x=x, y=y) for x, y in all_anchors[:, i, :]] in_limits = all( diff --git a/supervision/detection/tools/smoother.py b/supervision/detection/tools/smoother.py index f58f3299..6b20bdd1 100644 --- a/supervision/detection/tools/smoother.py +++ b/supervision/detection/tools/smoother.py @@ -78,8 +78,6 @@ class DetectionsSmoother: for detection_idx in range(len(detections)): tracker_id = detections.tracker_id[detection_idx] - if tracker_id is None: - continue self.tracks[tracker_id].append(detections[detection_idx]) From c3855c982847f6a74d69da3062ee76ed98690997 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Tue, 21 May 2024 13:08:02 +0200 Subject: [PATCH 082/182] small refactor + docs improvements --- supervision/dataset/utils.py | 28 ++++++++++++++++++---------- supervision/detection/utils.py | 8 ++++++-- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index 4efc7194..a43fbe5a 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -159,10 +159,12 @@ def rle_to_mask( ```python import supervision as sv - sv.rle_to_mask([2, 2, 2], (3, 2)) + sv.rle_to_mask([5, 2, 2, 2, 5], (4, 4)) # array([ - # [False, True, False], - # [False, True, False] + # [False, False, False, False], + # [False, True, True, False], + # [False, True, True, False], + # [False, False, False, False], # ]) ``` """ @@ -209,20 +211,26 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: import supervision as sv mask = np.array([ - [False, True, True], - [False, True, True] + [True, True, True, True], + [True, True, True, True], + [True, True, True, True], + [True, True, True, True], ]) sv.mask_to_rle(mask) - # [2, 4] + # [0, 16] mask = np.array([ - [True, True, True], - [True, True, True] + [False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False], ]) sv.mask_to_rle(mask) - # [0, 6] + # [5, 2, 2, 2, 5] ``` - """ + + ![mask_to_rle](https://media.roboflow.com/supervision-docs/mask-to-rle.png){ align=center width="800" } + """ # noqa E501 // docs assert mask.ndim == 2, "Input mask must be 2D" assert mask.size != 0, "Input mask cannot be empty" diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index f1e77883..f9f71f98 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -887,7 +887,9 @@ def contains_holes(mask: npt.NDArray[np.bool_]) -> bool: sv.contains_holes(mask=mask) # False ``` - """ + + ![contains_holes](https://media.roboflow.com/supervision-docs/contains-holes.png){ align=center width="800" } + """ # noqa E501 // docs mask_uint8 = mask.astype(np.uint8) _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) @@ -949,7 +951,9 @@ def contains_multiple_segments( sv.contains_multiple_segments(mask=mask, connectivity=4) # False ``` - """ + + ![contains_multiple_segments](https://media.roboflow.com/supervision-docs/contains-multiple-segments.png){ align=center width="800" } + """ # noqa E501 // docs if connectivity != 4 and connectivity != 8: raise ValueError( "Incorrect connectivity value. Possible connectivity values: 4 or 8." From 58408898180f9dc5370c5a17e043aa31db44c7a5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 11:08:19 +0000 Subject: [PATCH 083/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/utils.py | 2 +- supervision/detection/utils.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index a43fbe5a..32ece6bf 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -228,7 +228,7 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: sv.mask_to_rle(mask) # [5, 2, 2, 2, 5] ``` - + ![mask_to_rle](https://media.roboflow.com/supervision-docs/mask-to-rle.png){ align=center width="800" } """ # noqa E501 // docs assert mask.ndim == 2, "Input mask must be 2D" diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index f9f71f98..742f94f6 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -887,7 +887,7 @@ def contains_holes(mask: npt.NDArray[np.bool_]) -> bool: sv.contains_holes(mask=mask) # False ``` - + ![contains_holes](https://media.roboflow.com/supervision-docs/contains-holes.png){ align=center width="800" } """ # noqa E501 // docs mask_uint8 = mask.astype(np.uint8) @@ -951,7 +951,7 @@ def contains_multiple_segments( sv.contains_multiple_segments(mask=mask, connectivity=4) # False ``` - + ![contains_multiple_segments](https://media.roboflow.com/supervision-docs/contains-multiple-segments.png){ align=center width="800" } """ # noqa E501 // docs if connectivity != 4 and connectivity != 8: From 089916e26f14842056c0ad2b94480bba343f4393 Mon Sep 17 00:00:00 2001 From: tc360950 Date: Tue, 21 May 2024 21:08:18 +0200 Subject: [PATCH 084/182] Improve variable naming in LineZone unit tests, remove redundant test for empty tracker_id --- test/detection/test_line_counter.py | 57 +++++------------------------ 1 file changed, 9 insertions(+), 48 deletions(-) diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index 76f87c2d..77784c40 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -76,7 +76,7 @@ def test_calculate_region_of_interest_limits( @pytest.mark.parametrize( - "vector, bbox_sequence, expected_crossed_in, expected_crossed_out", + "vector, xyxy_sequence, expected_crossed_in, expected_crossed_out", [ ( Vector( @@ -205,12 +205,12 @@ def test_calculate_region_of_interest_limits( ) def test_line_zone_single_detection( vector: Vector, - bbox_sequence: List[List[int]], + xyxy_sequence: List[List[int]], expected_crossed_in: List[bool], expected_crossed_out: List[bool], ) -> None: line_zone = LineZone(start=vector.start, end=vector.end) - for i, bbox in enumerate(bbox_sequence): + for i, bbox in enumerate(xyxy_sequence): detections = mock_detections( xyxy=[bbox], tracker_id=[0], @@ -224,7 +224,7 @@ def test_line_zone_single_detection( @pytest.mark.parametrize( "vector," - "bbox_sequence," + "xyxy_sequence," "expected_crossed_in," "expected_crossed_out," "crossing_anchors", @@ -259,7 +259,7 @@ def test_line_zone_single_detection( ) def test_line_zone_single_detection_on_subset_of_anchors( vector: Vector, - bbox_sequence: List[List[int]], + xyxy_sequence: List[List[int]], expected_crossed_in: List[bool], expected_crossed_out: List[bool], crossing_anchors: List[Position], @@ -280,7 +280,7 @@ def test_line_zone_single_detection_on_subset_of_anchors( line_zone = LineZone( start=vector.start, end=vector.end, triggering_anchors=anchors ) - for i, bbox in enumerate(bbox_sequence): + for i, bbox in enumerate(xyxy_sequence): detections = mock_detections( xyxy=[bbox], tracker_id=[0], @@ -295,7 +295,7 @@ def test_line_zone_single_detection_on_subset_of_anchors( @pytest.mark.parametrize( - "vector, bbox_sequence, expected_crossed_in, expected_crossed_out", + "vector, xyxy_sequence, expected_crossed_in, expected_crossed_out", [ ( Vector( @@ -350,12 +350,12 @@ def test_line_zone_single_detection_on_subset_of_anchors( ) def test_line_zone_multiple_detections( vector: Vector, - bbox_sequence: List[List[List[int]]], + xyxy_sequence: List[List[List[int]]], expected_crossed_in: List[bool], expected_crossed_out: List[bool], ) -> None: line_zone = LineZone(start=vector.start, end=vector.end) - for i, bboxes in enumerate(bbox_sequence): + for i, bboxes in enumerate(xyxy_sequence): detections = mock_detections( xyxy=bboxes, tracker_id=[i for i in range(0, len(bboxes))], @@ -363,42 +363,3 @@ def test_line_zone_multiple_detections( crossed_in, crossed_out = line_zone.trigger(detections) assert np.all(crossed_in == expected_crossed_in[i]) assert np.all(crossed_out == expected_crossed_out[i]) - - -@pytest.mark.parametrize( - "vector, bbox_sequence", - [ - ( - Vector( - Point(0, 0), - Point(0, 100), - ), - [ - [100, 50, 120, 70], - [-100, 50, -80, 70], - ], - ), - ( - Vector( - Point(0, 0), - Point(0, 100), - ), - [ - [-100, 50, -80, 70], - [100, 50, 120, 70], - ], - ), - ], -) -def test_line_zone_does_not_count_detections_without_tracker_id( - vector: Vector, bbox_sequence: List[List[int]] -): - line_zone = LineZone(start=vector.start, end=vector.end) - for bbox in bbox_sequence: - detections = Detections( - xyxy=np.array([bbox]).reshape((-1, 4)), - tracker_id=np.array([None]), - ) - crossed_in, crossed_out = line_zone.trigger(detections) - assert np.all(not crossed_in) - assert np.all(not crossed_out) From 54a0422b5841245920b4258ecb828fb6a141befc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 19:08:33 +0000 Subject: [PATCH 085/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/detection/test_line_counter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index 77784c40..803eed9a 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -6,7 +6,7 @@ from typing import List, Optional, Tuple import numpy as np import pytest -from supervision import Detections, LineZone +from supervision import LineZone from supervision.geometry.core import Point, Position, Vector From dcfa916c7507f3f93505b05da13d753575a5f670 Mon Sep 17 00:00:00 2001 From: tc360950 Date: Tue, 21 May 2024 21:17:58 +0200 Subject: [PATCH 086/182] Add docstrings with test description for LineZone tests --- test/detection/test_line_counter.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index 803eed9a..9c530cca 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -209,6 +209,12 @@ def test_line_zone_single_detection( expected_crossed_in: List[bool], expected_crossed_out: List[bool], ) -> None: + """ + Test LineZone with single detection which crosses the line. + The detection is represented by a sequence of xyxy bboxes which represent + subsequent positions of the detected object. If a line is crossed (in either + direction) it is crossed by all anchors simultaneously. + """ line_zone = LineZone(start=vector.start, end=vector.end) for i, bbox in enumerate(xyxy_sequence): detections = mock_detections( @@ -264,6 +270,13 @@ def test_line_zone_single_detection_on_subset_of_anchors( expected_crossed_out: List[bool], crossing_anchors: List[Position], ) -> None: + """ + Test LineZone with single detection which crosses the line with only a subset of + anchors. + The detection is represented by a sequence of xyxy bboxes which represent + subsequent positions of the detected object. The line is crossed by only a subset + of anchors - this subset is given by @crossing_anchors. + """ def powerset(s): return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1)) @@ -354,6 +367,12 @@ def test_line_zone_multiple_detections( expected_crossed_in: List[bool], expected_crossed_out: List[bool], ) -> None: + """ + Test LineZone with multiple detections. + A detection is represented by a sequence of xyxy bboxes which represent + subsequent positions of the detected object. If a line is crossed (in either + direction) by a detection it is crossed by all its anchors simultaneously. + """ line_zone = LineZone(start=vector.start, end=vector.end) for i, bboxes in enumerate(xyxy_sequence): detections = mock_detections( From 8a2f5d4726f33a5cd10521b7a6d22a546b7a13d6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 19:19:14 +0000 Subject: [PATCH 087/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/detection/test_line_counter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index 9c530cca..9083a2df 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -277,6 +277,7 @@ def test_line_zone_single_detection_on_subset_of_anchors( subsequent positions of the detected object. The line is crossed by only a subset of anchors - this subset is given by @crossing_anchors. """ + def powerset(s): return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1)) From 6b7861d4d1d17abcf09c2d3df51ec5c643e71067 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 00:29:25 +0000 Subject: [PATCH 088/182] --- updated-dependencies: - dependency-name: requests dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index e672448a..85615d1c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3461,13 +3461,13 @@ files = [ [[package]] name = "requests" -version = "2.32.1" +version = "2.32.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" files = [ - {file = "requests-2.32.1-py3-none-any.whl", hash = "sha256:21ac9465cdf8c1650fe1ecde8a71669a93d4e6f147550483a2967d08396a56a5"}, - {file = "requests-2.32.1.tar.gz", hash = "sha256:eb97e87e64c79e64e5b8ac75cee9dd1f97f49e289b083ee6be96268930725685"}, + {file = "requests-2.32.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"}, + {file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"}, ] [package.dependencies] @@ -4258,4 +4258,4 @@ desktop = ["opencv-python"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "7ecea27cde915f67ee71e0ed55cabd890c6473e5f222434efd0b1f4392713312" +content-hash = "ad8402ec1767f9427ab38bad7dab54b302a30f9e08b6489fad224c8481745b37" diff --git a/pyproject.toml b/pyproject.toml index 252bb49d..ff83f5fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ pyyaml = ">=5.3" defusedxml = "^0.7.1" opencv-python = { version = ">=4.5.5.64", optional = true } opencv-python-headless = ">=4.5.5.64" -requests = { version = ">=2.26.0,<=2.32.1", optional = true } +requests = { version = ">=2.26.0,<=2.32.2", optional = true } tqdm = { version = ">=4.62.3,<=4.66.4", optional = true } pillow = ">=9.4" From a2195aa3789403e24ca44b941e0f14c3b49655ce Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Wed, 22 May 2024 14:24:52 +0200 Subject: [PATCH 089/182] initial commit adding support for `from_lmm` and specifically for PaliGemma --- supervision/detection/core.py | 47 ++++++++++++++ supervision/detection/lmm.py | 62 +++++++++++++++++++ test/detection/test_lmm.py | 113 ++++++++++++++++++++++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 supervision/detection/lmm.py create mode 100644 test/detection/test_lmm.py diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 0ba9e4f4..d6a04efb 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -7,6 +7,7 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple, Union import numpy as np from supervision.config import CLASS_NAME_DATA_FIELD, ORIENTED_BOX_COORDINATES +from supervision.detection.lmm import LMM, validate_lmm_and_kwargs, from_paligemma from supervision.detection.utils import ( box_non_max_suppression, calculate_masks_centroids, @@ -805,6 +806,52 @@ class Detections: class_id=paddledet_result["bbox"][:, 0].astype(int), ) + @classmethod + def from_lmm(cls, lmm: Union[LMM, str], result: str, **kwargs) -> Detections: + """ + Creates a Detections object from the given result string based on the specified + Large Multimodal Model (LMM). + + Args: + lmm (Union[LMM, str]): The type of LMM (Large Multimodal Model) to use. + result (str): The result string containing the detection data. + **kwargs: Additional keyword arguments required by the specified LMM. + + Returns: + Detections: A new Detections object. + + Raises: + ValueError: If the LMM is invalid, required arguments are missing, or + disallowed arguments are provided. + ValueError: If the specified LMM is not supported. + + Examples: + ```python + import supervision as sv + + paligemma_result = " cat" + detections = sv.Detections.from_lmm( + sv.LMM.PALIGEMMA, + paligemma_result, + resolution_wh=(1000, 1000), + classes=['cat', 'dog'] + ) + detections.xyxy + # array([[250., 250., 750., 750.]]) + + detections.class_id + # array([0]) + ``` + """ + lmm = validate_lmm_and_kwargs(lmm, kwargs) + + if lmm == LMM.PALIGEMMA: + xyxy, class_id, class_name = from_paligemma(result, **kwargs) + data = {CLASS_NAME_DATA_FIELD: class_name} + return cls(xyxy=xyxy, class_id=class_id, data=data) + + raise ValueError(f"Unsupported LMM: {lmm}") + @classmethod def empty(cls) -> Detections: """ diff --git a/supervision/detection/lmm.py b/supervision/detection/lmm.py new file mode 100644 index 00000000..1c4b90dc --- /dev/null +++ b/supervision/detection/lmm.py @@ -0,0 +1,62 @@ +import re +import numpy as np +from enum import Enum +from typing import Dict, List, Tuple, Optional, Union, Any + + +class LMM(Enum): + PALIGEMMA = 'paligemma' + + +REQUIRED_ARGUMENTS: Dict[LMM, List[str]] = { + LMM.PALIGEMMA: ['resolution_wh'] +} + +ALLOWED_ARGUMENTS: Dict[LMM, List[str]] = { + LMM.PALIGEMMA: ['resolution_wh', 'classes'] +} + + +def validate_lmm_and_kwargs(lmm: Union[LMM, str], kwargs: Dict[str, Any]) -> LMM: + if isinstance(lmm, str): + try: + lmm = LMM(lmm.lower()) + except ValueError: + raise ValueError( + f"Invalid lmm value: {lmm}. Must be one of {[e.value for e in LMM]}" + ) + + required_args = REQUIRED_ARGUMENTS.get(lmm, []) + for arg in required_args: + if arg not in kwargs: + raise ValueError(f"Missing required argument: {arg}") + + allowed_args = ALLOWED_ARGUMENTS.get(lmm, []) + for arg in kwargs: + if arg not in allowed_args: + raise ValueError(f"Argument {arg} is not allowed for {lmm.name}") + + return lmm + + +def from_paligemma( + result: str, + resolution_wh: Tuple[int, int], + classes: Optional[List[str]] = None +) -> Tuple[np.ndarray, Optional[np.ndarray], np.ndarray]: + w, h = resolution_wh + pattern = re.compile( + r'(?) (\w+)') + matches = pattern.findall(result) + matches = np.array(matches) if matches else np.empty((0, 5)) + + xyxy, class_name = matches[:, [1, 0, 3, 2]], matches[:, 4] + xyxy = xyxy.astype(int) / 1024 * np.array([w, h, w, h]) + class_id = None + + if classes is not None: + mask = np.array([name in classes for name in class_name]) + xyxy, class_name = xyxy[mask], class_name[mask] + class_id = np.array([classes.index(name) for name in class_name]) + + return xyxy, class_id, class_name.astype(np.dtype('U')) diff --git a/test/detection/test_lmm.py b/test/detection/test_lmm.py new file mode 100644 index 00000000..b7f7c5b4 --- /dev/null +++ b/test/detection/test_lmm.py @@ -0,0 +1,113 @@ +import numpy as np +from typing import Tuple, Optional, List + +import pytest + +from supervision.detection.lmm import from_paligemma + + +@pytest.mark.parametrize( + "result, resolution_wh, classes, expected_results", + [ + ( + "", + (1000, 1000), + None, + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + ), # empty response + ( + "\n", + (1000, 1000), + None, + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + ), # new line response + ( + "the quick brown fox jumps over the lazy dog.", + (1000, 1000), + None, + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + ), # response with no location + ( + " cat", + (1000, 1000), + None, + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + ), # response with missing location + ( + " cat", + (1000, 1000), + None, + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + ), # response with extra location + ( + "", + (1000, 1000), + None, + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + ), # response with no class + ( + " catt", + (1000, 1000), + ['cat', 'dog'], + (np.empty((0, 4)), np.empty(0), np.empty(0).astype(np.dtype('U'))) + ), # response with invalid class + ( + " cat", + (1000, 1000), + None, + ( + np.array([[250., 250., 750., 750.]]), + None, + np.array(['cat']).astype(np.dtype('U')) + ) + ), # correct response; no classes + ( + " cat ;", + (1000, 1000), + ['cat', 'dog'], + ( + np.array([[250., 250., 750., 750.]]), + np.array([0]), + np.array(['cat']).astype(np.dtype('U')) + ) + ), # correct response; with classes + ( + " cat ; cat", + (1000, 1000), + ['cat', 'dog'], + ( + np.array([[250., 250., 750., 750.]]), + np.array([0]), + np.array(['cat']).astype(np.dtype('U')) + ) + ), # partially correct response; with classes + ( + " cat ; cat", + (1000, 1000), + ['cat', 'dog'], + ( + np.array([[250., 250., 750., 750.]]), + np.array([0]), + np.array(['cat']).astype(np.dtype('U')) + ) + ), # partially correct response; with classes + ] +) +def test_from_paligemma( + result: str, + resolution_wh: Tuple[int, int], + classes: Optional[List[str]], + expected_results: Tuple[np.ndarray, Optional[np.ndarray], np.ndarray] +) -> None: + result = from_paligemma(result=result, resolution_wh=resolution_wh, classes=classes) + + print(result[0].dtype) + print(expected_results[0].dtype) + # print(result[1]) + # print(expected_results[1]) + print(result[2].dtype) + print(expected_results[2].dtype) + + np.testing.assert_array_equal(result[0], expected_results[0]) + np.testing.assert_array_equal(result[1], expected_results[1]) + np.testing.assert_array_equal(result[2], expected_results[2]) From 36a73f7d4912a56ab289d38d22b17b1538bd39ed Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Wed, 22 May 2024 14:26:13 +0200 Subject: [PATCH 090/182] clean up --- test/detection/test_lmm.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/detection/test_lmm.py b/test/detection/test_lmm.py index b7f7c5b4..f8ea91ef 100644 --- a/test/detection/test_lmm.py +++ b/test/detection/test_lmm.py @@ -100,14 +100,6 @@ def test_from_paligemma( expected_results: Tuple[np.ndarray, Optional[np.ndarray], np.ndarray] ) -> None: result = from_paligemma(result=result, resolution_wh=resolution_wh, classes=classes) - - print(result[0].dtype) - print(expected_results[0].dtype) - # print(result[1]) - # print(expected_results[1]) - print(result[2].dtype) - print(expected_results[2].dtype) - np.testing.assert_array_equal(result[0], expected_results[0]) np.testing.assert_array_equal(result[1], expected_results[1]) np.testing.assert_array_equal(result[2], expected_results[2]) From 7eb918282cf81c0c30e80ddad279265fe35528a9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 12:27:53 +0000 Subject: [PATCH 091/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=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/lmm.py | 24 +++++++--------- test/detection/test_lmm.py | 54 +++++++++++++++++------------------ 3 files changed, 38 insertions(+), 42 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index d6a04efb..e8599817 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -7,7 +7,7 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple, Union import numpy as np from supervision.config import CLASS_NAME_DATA_FIELD, ORIENTED_BOX_COORDINATES -from supervision.detection.lmm import LMM, validate_lmm_and_kwargs, from_paligemma +from supervision.detection.lmm import LMM, from_paligemma, validate_lmm_and_kwargs from supervision.detection.utils import ( box_non_max_suppression, calculate_masks_centroids, diff --git a/supervision/detection/lmm.py b/supervision/detection/lmm.py index 1c4b90dc..67921328 100644 --- a/supervision/detection/lmm.py +++ b/supervision/detection/lmm.py @@ -1,20 +1,17 @@ import re -import numpy as np from enum import Enum -from typing import Dict, List, Tuple, Optional, Union, Any +from typing import Any, Dict, List, Optional, Tuple, Union + +import numpy as np class LMM(Enum): - PALIGEMMA = 'paligemma' + PALIGEMMA = "paligemma" -REQUIRED_ARGUMENTS: Dict[LMM, List[str]] = { - LMM.PALIGEMMA: ['resolution_wh'] -} +REQUIRED_ARGUMENTS: Dict[LMM, List[str]] = {LMM.PALIGEMMA: ["resolution_wh"]} -ALLOWED_ARGUMENTS: Dict[LMM, List[str]] = { - LMM.PALIGEMMA: ['resolution_wh', 'classes'] -} +ALLOWED_ARGUMENTS: Dict[LMM, List[str]] = {LMM.PALIGEMMA: ["resolution_wh", "classes"]} def validate_lmm_and_kwargs(lmm: Union[LMM, str], kwargs: Dict[str, Any]) -> LMM: @@ -40,13 +37,12 @@ def validate_lmm_and_kwargs(lmm: Union[LMM, str], kwargs: Dict[str, Any]) -> LMM def from_paligemma( - result: str, - resolution_wh: Tuple[int, int], - classes: Optional[List[str]] = None + result: str, resolution_wh: Tuple[int, int], classes: Optional[List[str]] = None ) -> Tuple[np.ndarray, Optional[np.ndarray], np.ndarray]: w, h = resolution_wh pattern = re.compile( - r'(?) (\w+)') + r"(?) (\w+)" + ) matches = pattern.findall(result) matches = np.array(matches) if matches else np.empty((0, 5)) @@ -59,4 +55,4 @@ def from_paligemma( xyxy, class_name = xyxy[mask], class_name[mask] class_id = np.array([classes.index(name) for name in class_name]) - return xyxy, class_id, class_name.astype(np.dtype('U')) + return xyxy, class_id, class_name.astype(np.dtype("U")) diff --git a/test/detection/test_lmm.py b/test/detection/test_lmm.py index f8ea91ef..5066a7a3 100644 --- a/test/detection/test_lmm.py +++ b/test/detection/test_lmm.py @@ -1,6 +1,6 @@ -import numpy as np -from typing import Tuple, Optional, List +from typing import List, Optional, Tuple +import numpy as np import pytest from supervision.detection.lmm import from_paligemma @@ -13,91 +13,91 @@ from supervision.detection.lmm import from_paligemma "", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype("U"))), ), # empty response ( "\n", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype("U"))), ), # new line response ( "the quick brown fox jumps over the lazy dog.", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype("U"))), ), # response with no location ( " cat", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype("U"))), ), # response with missing location ( " cat", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype("U"))), ), # response with extra location ( "", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype("U"))), ), # response with no class ( " catt", (1000, 1000), - ['cat', 'dog'], - (np.empty((0, 4)), np.empty(0), np.empty(0).astype(np.dtype('U'))) + ["cat", "dog"], + (np.empty((0, 4)), np.empty(0), np.empty(0).astype(np.dtype("U"))), ), # response with invalid class ( " cat", (1000, 1000), None, ( - np.array([[250., 250., 750., 750.]]), + np.array([[250.0, 250.0, 750.0, 750.0]]), None, - np.array(['cat']).astype(np.dtype('U')) - ) + np.array(["cat"]).astype(np.dtype("U")), + ), ), # correct response; no classes ( " cat ;", (1000, 1000), - ['cat', 'dog'], + ["cat", "dog"], ( - np.array([[250., 250., 750., 750.]]), + np.array([[250.0, 250.0, 750.0, 750.0]]), np.array([0]), - np.array(['cat']).astype(np.dtype('U')) - ) + np.array(["cat"]).astype(np.dtype("U")), + ), ), # correct response; with classes ( " cat ; cat", (1000, 1000), - ['cat', 'dog'], + ["cat", "dog"], ( - np.array([[250., 250., 750., 750.]]), + np.array([[250.0, 250.0, 750.0, 750.0]]), np.array([0]), - np.array(['cat']).astype(np.dtype('U')) - ) + np.array(["cat"]).astype(np.dtype("U")), + ), ), # partially correct response; with classes ( " cat ; cat", (1000, 1000), - ['cat', 'dog'], + ["cat", "dog"], ( - np.array([[250., 250., 750., 750.]]), + np.array([[250.0, 250.0, 750.0, 750.0]]), np.array([0]), - np.array(['cat']).astype(np.dtype('U')) - ) + np.array(["cat"]).astype(np.dtype("U")), + ), ), # partially correct response; with classes - ] + ], ) def test_from_paligemma( result: str, resolution_wh: Tuple[int, int], classes: Optional[List[str]], - expected_results: Tuple[np.ndarray, Optional[np.ndarray], np.ndarray] + expected_results: Tuple[np.ndarray, Optional[np.ndarray], np.ndarray], ) -> None: result = from_paligemma(result=result, resolution_wh=resolution_wh, classes=classes) np.testing.assert_array_equal(result[0], expected_results[0]) From b81c5e84758487494f90ad67805f9ddba4564ebe Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Wed, 22 May 2024 16:28:24 +0200 Subject: [PATCH 092/182] update to allow multi-word class names --- supervision/detection/lmm.py | 5 ++-- test/detection/test_lmm.py | 45 +++++++++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/supervision/detection/lmm.py b/supervision/detection/lmm.py index 1c4b90dc..9cd7434e 100644 --- a/supervision/detection/lmm.py +++ b/supervision/detection/lmm.py @@ -46,12 +46,13 @@ def from_paligemma( ) -> Tuple[np.ndarray, Optional[np.ndarray], np.ndarray]: w, h = resolution_wh pattern = re.compile( - r'(?) (\w+)') + r'(?) ([\w\s]+)') matches = pattern.findall(result) matches = np.array(matches) if matches else np.empty((0, 5)) xyxy, class_name = matches[:, [1, 0, 3, 2]], matches[:, 4] xyxy = xyxy.astype(int) / 1024 * np.array([w, h, w, h]) + class_name = np.char.strip(class_name.astype(str)) class_id = None if classes is not None: @@ -59,4 +60,4 @@ def from_paligemma( xyxy, class_name = xyxy[mask], class_name[mask] class_id = np.array([classes.index(name) for name in class_name]) - return xyxy, class_id, class_name.astype(np.dtype('U')) + return xyxy, class_id, class_name diff --git a/test/detection/test_lmm.py b/test/detection/test_lmm.py index f8ea91ef..91840d88 100644 --- a/test/detection/test_lmm.py +++ b/test/detection/test_lmm.py @@ -13,43 +13,43 @@ from supervision.detection.lmm import from_paligemma "", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(str)) ), # empty response ( "\n", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(str)) ), # new line response ( "the quick brown fox jumps over the lazy dog.", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(str)) ), # response with no location ( " cat", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(str)) ), # response with missing location ( " cat", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(str)) ), # response with extra location ( "", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(str)) ), # response with no class ( " catt", (1000, 1000), ['cat', 'dog'], - (np.empty((0, 4)), np.empty(0), np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), np.empty(0), np.empty(0).astype(str)) ), # response with invalid class ( " cat", @@ -58,7 +58,17 @@ from supervision.detection.lmm import from_paligemma ( np.array([[250., 250., 750., 750.]]), None, - np.array(['cat']).astype(np.dtype('U')) + np.array(['cat']).astype(str) + ) + ), # correct response; no classes + ( + " black cat", + (1000, 1000), + None, + ( + np.array([[250., 250., 750., 750.]]), + None, + np.array(['black cat']).astype(np.dtype('U')) ) ), # correct response; no classes ( @@ -68,7 +78,20 @@ from supervision.detection.lmm import from_paligemma ( np.array([[250., 250., 750., 750.]]), np.array([0]), - np.array(['cat']).astype(np.dtype('U')) + np.array(['cat']).astype(str) + ) + ), # correct response; with classes + ( + " cat ; dog", + (1000, 1000), + ['cat', 'dog'], + ( + np.array([ + [250., 250., 750., 750.], + [250., 250., 750., 750.] + ]), + np.array([0, 1]), + np.array(['cat', 'dog']).astype(np.dtype('U')) ) ), # correct response; with classes ( @@ -78,7 +101,7 @@ from supervision.detection.lmm import from_paligemma ( np.array([[250., 250., 750., 750.]]), np.array([0]), - np.array(['cat']).astype(np.dtype('U')) + np.array(['cat']).astype(str) ) ), # partially correct response; with classes ( @@ -88,7 +111,7 @@ from supervision.detection.lmm import from_paligemma ( np.array([[250., 250., 750., 750.]]), np.array([0]), - np.array(['cat']).astype(np.dtype('U')) + np.array(['cat']).astype(str) ) ), # partially correct response; with classes ] From bf43d6566b8bfa9ac6c317bceb312c414268a60b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 14:29:20 +0000 Subject: [PATCH 093/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/detection/lmm.py | 22 +++++------ test/detection/test_lmm.py | 71 +++++++++++++++++------------------- 2 files changed, 43 insertions(+), 50 deletions(-) diff --git a/supervision/detection/lmm.py b/supervision/detection/lmm.py index 9cd7434e..3660fb68 100644 --- a/supervision/detection/lmm.py +++ b/supervision/detection/lmm.py @@ -1,20 +1,17 @@ import re -import numpy as np from enum import Enum -from typing import Dict, List, Tuple, Optional, Union, Any +from typing import Any, Dict, List, Optional, Tuple, Union + +import numpy as np class LMM(Enum): - PALIGEMMA = 'paligemma' + PALIGEMMA = "paligemma" -REQUIRED_ARGUMENTS: Dict[LMM, List[str]] = { - LMM.PALIGEMMA: ['resolution_wh'] -} +REQUIRED_ARGUMENTS: Dict[LMM, List[str]] = {LMM.PALIGEMMA: ["resolution_wh"]} -ALLOWED_ARGUMENTS: Dict[LMM, List[str]] = { - LMM.PALIGEMMA: ['resolution_wh', 'classes'] -} +ALLOWED_ARGUMENTS: Dict[LMM, List[str]] = {LMM.PALIGEMMA: ["resolution_wh", "classes"]} def validate_lmm_and_kwargs(lmm: Union[LMM, str], kwargs: Dict[str, Any]) -> LMM: @@ -40,13 +37,12 @@ def validate_lmm_and_kwargs(lmm: Union[LMM, str], kwargs: Dict[str, Any]) -> LMM def from_paligemma( - result: str, - resolution_wh: Tuple[int, int], - classes: Optional[List[str]] = None + result: str, resolution_wh: Tuple[int, int], classes: Optional[List[str]] = None ) -> Tuple[np.ndarray, Optional[np.ndarray], np.ndarray]: w, h = resolution_wh pattern = re.compile( - r'(?) ([\w\s]+)') + r"(?) ([\w\s]+)" + ) matches = pattern.findall(result) matches = np.array(matches) if matches else np.empty((0, 5)) diff --git a/test/detection/test_lmm.py b/test/detection/test_lmm.py index 91840d88..5b4f31ba 100644 --- a/test/detection/test_lmm.py +++ b/test/detection/test_lmm.py @@ -1,6 +1,6 @@ -import numpy as np -from typing import Tuple, Optional, List +from typing import List, Optional, Tuple +import numpy as np import pytest from supervision.detection.lmm import from_paligemma @@ -13,114 +13,111 @@ from supervision.detection.lmm import from_paligemma "", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(str)) + (np.empty((0, 4)), None, np.empty(0).astype(str)), ), # empty response ( "\n", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(str)) + (np.empty((0, 4)), None, np.empty(0).astype(str)), ), # new line response ( "the quick brown fox jumps over the lazy dog.", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(str)) + (np.empty((0, 4)), None, np.empty(0).astype(str)), ), # response with no location ( " cat", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(str)) + (np.empty((0, 4)), None, np.empty(0).astype(str)), ), # response with missing location ( " cat", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(str)) + (np.empty((0, 4)), None, np.empty(0).astype(str)), ), # response with extra location ( "", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(str)) + (np.empty((0, 4)), None, np.empty(0).astype(str)), ), # response with no class ( " catt", (1000, 1000), - ['cat', 'dog'], - (np.empty((0, 4)), np.empty(0), np.empty(0).astype(str)) + ["cat", "dog"], + (np.empty((0, 4)), np.empty(0), np.empty(0).astype(str)), ), # response with invalid class ( " cat", (1000, 1000), None, ( - np.array([[250., 250., 750., 750.]]), + np.array([[250.0, 250.0, 750.0, 750.0]]), None, - np.array(['cat']).astype(str) - ) + np.array(["cat"]).astype(str), + ), ), # correct response; no classes ( " black cat", (1000, 1000), None, ( - np.array([[250., 250., 750., 750.]]), + np.array([[250.0, 250.0, 750.0, 750.0]]), None, - np.array(['black cat']).astype(np.dtype('U')) - ) + np.array(["black cat"]).astype(np.dtype("U")), + ), ), # correct response; no classes ( " cat ;", (1000, 1000), - ['cat', 'dog'], + ["cat", "dog"], ( - np.array([[250., 250., 750., 750.]]), + np.array([[250.0, 250.0, 750.0, 750.0]]), np.array([0]), - np.array(['cat']).astype(str) - ) + np.array(["cat"]).astype(str), + ), ), # correct response; with classes ( " cat ; dog", (1000, 1000), - ['cat', 'dog'], + ["cat", "dog"], ( - np.array([ - [250., 250., 750., 750.], - [250., 250., 750., 750.] - ]), + np.array([[250.0, 250.0, 750.0, 750.0], [250.0, 250.0, 750.0, 750.0]]), np.array([0, 1]), - np.array(['cat', 'dog']).astype(np.dtype('U')) - ) + np.array(["cat", "dog"]).astype(np.dtype("U")), + ), ), # correct response; with classes ( " cat ; cat", (1000, 1000), - ['cat', 'dog'], + ["cat", "dog"], ( - np.array([[250., 250., 750., 750.]]), + np.array([[250.0, 250.0, 750.0, 750.0]]), np.array([0]), - np.array(['cat']).astype(str) - ) + np.array(["cat"]).astype(str), + ), ), # partially correct response; with classes ( " cat ; cat", (1000, 1000), - ['cat', 'dog'], + ["cat", "dog"], ( - np.array([[250., 250., 750., 750.]]), + np.array([[250.0, 250.0, 750.0, 750.0]]), np.array([0]), - np.array(['cat']).astype(str) - ) + np.array(["cat"]).astype(str), + ), ), # partially correct response; with classes - ] + ], ) def test_from_paligemma( result: str, resolution_wh: Tuple[int, int], classes: Optional[List[str]], - expected_results: Tuple[np.ndarray, Optional[np.ndarray], np.ndarray] + expected_results: Tuple[np.ndarray, Optional[np.ndarray], np.ndarray], ) -> None: result = from_paligemma(result=result, resolution_wh=resolution_wh, classes=classes) np.testing.assert_array_equal(result[0], expected_results[0]) From 07c36c6223a49bbc496503ec3db34f2b0ac775f8 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Wed, 22 May 2024 17:47:52 +0200 Subject: [PATCH 094/182] make linter happy --- test/detection/test_lmm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/detection/test_lmm.py b/test/detection/test_lmm.py index 5b4f31ba..b6232bd8 100644 --- a/test/detection/test_lmm.py +++ b/test/detection/test_lmm.py @@ -82,7 +82,7 @@ from supervision.detection.lmm import from_paligemma ), ), # correct response; with classes ( - " cat ; dog", + " cat ; dog", # noqa: E501 (1000, 1000), ["cat", "dog"], ( @@ -92,7 +92,7 @@ from supervision.detection.lmm import from_paligemma ), ), # correct response; with classes ( - " cat ; cat", + " cat ; cat", # noqa: E501 (1000, 1000), ["cat", "dog"], ( @@ -102,7 +102,7 @@ from supervision.detection.lmm import from_paligemma ), ), # partially correct response; with classes ( - " cat ; cat", + " cat ; cat", # noqa: E501 (1000, 1000), ["cat", "dog"], ( From 8b4884ec9ed72c7c9d3203a0ca2a3ef6125fd624 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 22 May 2024 21:33:34 +0300 Subject: [PATCH 095/182] tracker, smoother: SupervisionWarnings --- supervision/detection/line_zone.py | 10 +++++++--- supervision/detection/tools/smoother.py | 8 ++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/supervision/detection/line_zone.py b/supervision/detection/line_zone.py index fe850894..cd4400e5 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -1,3 +1,4 @@ +import warnings from typing import Dict, Iterable, Optional, Tuple import cv2 @@ -7,6 +8,7 @@ from supervision.detection.core import Detections from supervision.draw.color import Color from supervision.draw.utils import draw_text from supervision.geometry.core import Point, Position, Vector +from supervision.utils.internal import SupervisionWarnings class LineZone: @@ -141,9 +143,11 @@ class LineZone: return crossed_in, crossed_out if detections.tracker_id is None: - print( - "Line zone conting skipped. LineZone requires tracker_id. Refer to " - "https://supervision.roboflow.com/latest/trackers for more information." + warnings.warn( + "Line zone counting skipped. LineZone requires tracker_id. Refer to " + "https://supervision.roboflow.com/latest/trackers for more " + "information.", + category=SupervisionWarnings, ) return crossed_in, crossed_out diff --git a/supervision/detection/tools/smoother.py b/supervision/detection/tools/smoother.py index 6b20bdd1..5768c3e8 100644 --- a/supervision/detection/tools/smoother.py +++ b/supervision/detection/tools/smoother.py @@ -1,3 +1,4 @@ +import warnings from collections import defaultdict, deque from copy import deepcopy from typing import Optional @@ -5,6 +6,7 @@ from typing import Optional import numpy as np from supervision.detection.core import Detections +from supervision.utils.internal import SupervisionWarnings class DetectionsSmoother: @@ -70,9 +72,11 @@ class DetectionsSmoother: """ if detections.tracker_id is None: - print( + warnings.warn( "Smoothing skipped. DetectionsSmoother requires tracker_id. Refer to " - "https://supervision.roboflow.com/latest/trackers for more information." + "https://supervision.roboflow.com/latest/trackers for more " + "information.", + category=SupervisionWarnings, ) return detections From 251185ba677d152fdbaaabe9d3d67bbd9425dbf1 Mon Sep 17 00:00:00 2001 From: Onuralp SEZER Date: Wed, 22 May 2024 19:48:39 +0300 Subject: [PATCH 096/182] =?UTF-8?q?fix:=20=F0=9F=90=9E=20yolo=20obb=20hass?= =?UTF-8?q?tr=20added=20into=20condition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onuralp SEZER --- 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 0ba9e4f4..6563de7d 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -240,7 +240,7 @@ class Detections: Class names values can be accessed using `detections["class_name"]`. """ # noqa: E501 // docs - if "obb" in ultralytics_results and ultralytics_results.obb is not None: + if hasattr(ultralytics_results, "obb") and ultralytics_results.obb is not None: class_id = ultralytics_results.obb.cls.cpu().numpy().astype(int) class_names = np.array([ultralytics_results.names[i] for i in class_id]) oriented_box_coordinates = ultralytics_results.obb.xyxyxyxy.cpu().numpy() From 99854a5b6693e7c70dd338df05839e9ff466c213 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 22 May 2024 23:14:17 +0300 Subject: [PATCH 097/182] minor docs change: rename How To - Track Objects --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 281c40c9..54f1eda2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -41,7 +41,7 @@ nav: - Save Detections: how_to/save_detections.md - Filter Detections: how_to/filter_detections.md - Detect Small Objects: how_to/detect_small_objects.md - - Track Objects: how_to/track_objects.md + - Detect and Track Objects on Video: how_to/track_objects.md - API: - Detection and Segmentation: From c9c2dad316a24f445e89357017113dc0ae708ebb Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 22 May 2024 23:47:45 +0300 Subject: [PATCH 098/182] Docs: shorter title for object tracking how-to --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 54f1eda2..f257238d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -41,7 +41,7 @@ nav: - Save Detections: how_to/save_detections.md - Filter Detections: how_to/filter_detections.md - Detect Small Objects: how_to/detect_small_objects.md - - Detect and Track Objects on Video: how_to/track_objects.md + - Track Objects on Video: how_to/track_objects.md - API: - Detection and Segmentation: From 0115ef8b7d40b9a242ed3799820b897b43b5da7e Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Thu, 23 May 2024 09:11:47 +0200 Subject: [PATCH 099/182] small fix when `mask` is empty --- supervision/detection/lmm.py | 2 +- test/detection/test_lmm.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/supervision/detection/lmm.py b/supervision/detection/lmm.py index 3660fb68..0278fc00 100644 --- a/supervision/detection/lmm.py +++ b/supervision/detection/lmm.py @@ -52,7 +52,7 @@ def from_paligemma( class_id = None if classes is not None: - mask = np.array([name in classes for name in class_name]) + mask = np.array([name in classes for name in class_name]).astype(bool) xyxy, class_name = xyxy[mask], class_name[mask] class_id = np.array([classes.index(name) for name in class_name]) diff --git a/test/detection/test_lmm.py b/test/detection/test_lmm.py index b6232bd8..e20b947d 100644 --- a/test/detection/test_lmm.py +++ b/test/detection/test_lmm.py @@ -15,6 +15,12 @@ from supervision.detection.lmm import from_paligemma None, (np.empty((0, 4)), None, np.empty(0).astype(str)), ), # empty response + ( + "", + (1000, 1000), + ['cat', 'dog'], + (np.empty((0, 4)), None, np.empty(0).astype(str)), + ), # empty response with classes ( "\n", (1000, 1000), From ad2220bc1da2e018d1ce08685359eb02ab3c5bd4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 23 May 2024 07:12:17 +0000 Subject: [PATCH 100/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/detection/test_lmm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/detection/test_lmm.py b/test/detection/test_lmm.py index e20b947d..129aa44b 100644 --- a/test/detection/test_lmm.py +++ b/test/detection/test_lmm.py @@ -18,7 +18,7 @@ from supervision.detection.lmm import from_paligemma ( "", (1000, 1000), - ['cat', 'dog'], + ["cat", "dog"], (np.empty((0, 4)), None, np.empty(0).astype(str)), ), # empty response with classes ( From 6fbca8333e373d06312e823e03ef8899208f1a7a Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Thu, 23 May 2024 16:01:34 +0300 Subject: [PATCH 101/182] Address review comments, simplify merge * Reintroduced iou check before response - necessary for algorithm --- supervision/__init__.py | 3 +- supervision/detection/core.py | 145 ++++++++++++++++++++++----------- supervision/detection/utils.py | 118 ++++++++++++++++----------- test/detection/test_core.py | 77 +++++++++++++---- test/detection/test_utils.py | 56 +++++++------ 5 files changed, 265 insertions(+), 134 deletions(-) diff --git a/supervision/__init__.py b/supervision/__init__.py index 03f52086..816142b9 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -35,7 +35,7 @@ from supervision.dataset.core import ( DetectionDataset, ) from supervision.detection.annotate import BoxAnnotator -from supervision.detection.core import Detections, merge_object_detection_pair +from supervision.detection.core import Detections from supervision.detection.line_zone import LineZone, LineZoneAnnotator from supervision.detection.tools.csv_sink import CSVSink from supervision.detection.tools.inference_slicer import InferenceSlicer @@ -45,7 +45,6 @@ from supervision.detection.tools.smoother import DetectionsSmoother from supervision.detection.utils import ( box_iou_batch, box_non_max_merge, - box_non_max_merge_batch, box_non_max_suppression, calculate_masks_centroids, clip_boxes, diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 2f358c6b..6abc8dad 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -8,8 +8,8 @@ import numpy as np from supervision.config import CLASS_NAME_DATA_FIELD, ORIENTED_BOX_COORDINATES from supervision.detection.utils import ( + box_iou_batch, box_non_max_merge, - box_non_max_merge_batch, box_non_max_suppression, calculate_masks_centroids, extract_ultralytics_masks, @@ -1198,24 +1198,21 @@ class Detections: after non-maximum merging. Raises: - AssertionError: If `confidence` is None and class_agnostic is False. - If `class_id` is None and class_agnostic is False. + AssertionError: If `confidence` is None or `class_id` is None and + class_agnostic is False. """ if len(self) == 0: return self - assert 0.0 <= threshold <= 1.0, "Threshold must be between 0 and 1." - assert ( self.confidence is not None ), "Detections confidence must be given for NMM to be executed." if class_agnostic: predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1))) - keep_to_merge_list = box_non_max_merge(predictions, threshold) else: assert self.class_id is not None, ( - "Detections class_id must be given for NMS to be executed. If you" + "Detections class_id must be given for NMM to be executed. If you" " intended to perform class agnostic NMM set class_agnostic=True." ) predictions = np.hstack( @@ -1225,21 +1222,25 @@ class Detections: self.class_id.reshape(-1, 1), ) ) - keep_to_merge_list = box_non_max_merge_batch(predictions, threshold) + + merge_groups = box_non_max_merge( + predictions=predictions, iou_threshold=threshold + ) result = [] - for keep_ind, merge_ind_list in keep_to_merge_list.items(): - for merge_ind in merge_ind_list: - merged_detection = merge_object_detection_pair( - self[keep_ind], self[merge_ind] - ) - self._set_at_index(keep_ind, merged_detection) - result.append(self[keep_ind]) + for merge_group in merge_groups: + unmerged_detections = [self[i] for i in merge_group] + merged_detections = _merge_inner_detections_objects( + unmerged_detections, threshold + ) + result.append(merged_detections) return Detections.merge(result) -def merge_object_detection_pair(det1: Detections, det2: Detections) -> Detections: +def _merge_inner_detection_object_pair( + detections_1: Detections, detections_2: Detections +) -> Detections: """ Merges two Detections object into a single Detections object. Assumes each Detections contains exactly one object. @@ -1254,9 +1255,9 @@ def merge_object_detection_pair(det1: Detections, det2: Detections) -> Detection single bounding box and mask, respectively. Args: - det1 (Detections): + detections_1 (Detections): The first Detections object - det2 (Detections): + detections_2 (Detections): The second Detections object Returns: @@ -1282,51 +1283,99 @@ def merge_object_detection_pair(det1: Detections, det2: Detections) -> Detection detections[0], detections[1]) ``` """ - if len(det1) != 1 or len(det2) != 1: + if len(detections_1) != 1 or len(detections_2) != 1: raise ValueError("Both Detections should have exactly 1 detected object.") - if det2.confidence is None: - winning_det = det1 - elif det1.confidence is None: - winning_det = det2 - elif det1.confidence[0] >= det2.confidence[0]: - winning_det = det1 - else: - winning_det = det2 - - area_det1 = (det1.xyxy[0][2] - det1.xyxy[0][0]) * ( - det1.xyxy[0][3] - det1.xyxy[0][1] - ) - area_det2 = (det2.xyxy[0][2] - det2.xyxy[0][0]) * ( - det2.xyxy[0][3] - det2.xyxy[0][1] - ) - - merged_x1, merged_y1 = np.minimum(det1.xyxy[0][:2], det2.xyxy[0][:2]) - merged_x2, merged_y2 = np.maximum(det1.xyxy[0][2:], det2.xyxy[0][2:]) - merged_xy = np.array([[merged_x1, merged_y1, merged_x2, merged_y2]]) - - if det2.mask is None or det1.mask is None: - merged_mask = winning_det.mask - else: - merged_mask = np.logical_or(det1.mask, det2.mask) - - if det1.confidence is None or det2.confidence is None: - merged_confidence = winning_det.confidence + _verify_fields_both_defined_or_none(detections_1, detections_2) + + if detections_1.confidence is None and detections_2.confidence is None: + merged_confidence = None else: + area_det1 = (detections_1.xyxy[0][2] - detections_1.xyxy[0][0]) * ( + detections_1.xyxy[0][3] - detections_1.xyxy[0][1] + ) + area_det2 = (detections_2.xyxy[0][2] - detections_2.xyxy[0][0]) * ( + detections_2.xyxy[0][3] - detections_2.xyxy[0][1] + ) merged_confidence = ( - area_det1 * det1.confidence[0] + area_det2 * det2.confidence[0] + area_det1 * detections_1.confidence[0] + + area_det2 * detections_2.confidence[0] ) / (area_det1 + area_det2) merged_confidence = np.array([merged_confidence]) + merged_x1, merged_y1 = np.minimum( + detections_1.xyxy[0][:2], detections_2.xyxy[0][:2] + ) + merged_x2, merged_y2 = np.maximum( + detections_1.xyxy[0][2:], detections_2.xyxy[0][2:] + ) + merged_xyxy = np.array([[merged_x1, merged_y1, merged_x2, merged_y2]]) + + if detections_1.mask is None and detections_2.mask is None: + merged_mask = None + else: + merged_mask = np.logical_or(detections_1.mask, detections_2.mask) + + if detections_1.confidence is None and detections_2.confidence is None: + winning_det = detections_1 + elif detections_1.confidence[0] >= detections_2.confidence[0]: + winning_det = detections_1 + else: + winning_det = detections_2 + winning_class_id = winning_det.class_id winning_tracker_id = winning_det.tracker_id winning_data = winning_det.data return Detections( - xyxy=merged_xy, + xyxy=merged_xyxy, mask=merged_mask, confidence=merged_confidence, class_id=winning_class_id, tracker_id=winning_tracker_id, data=winning_data, ) + + +def _merge_inner_detections_objects( + detections: List[Detections], threshold=0.5 +) -> 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. + """ + 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: + break + detections_1 = _merge_inner_detection_object_pair(detections_1, detections_2) + return detections_1 + + +def _verify_fields_both_defined_or_none( + detections_1: Detections, detections_2: Detections +) -> None: + """ + Verify that for each optional field in the Detections, both instances either have + the field set to None or both have it set to non-None values. + + `data` field is ignored. + + Raises: + ValueError: If one field is None and the other is not, for any of the fields. + """ + attributes = ["mask", "confidence", "class_id", "tracker_id"] + for attribute in attributes: + value_1 = getattr(detections_1, attribute) + value_2 = getattr(detections_2, attribute) + + if (value_1 is None) != (value_2 is None): + raise ValueError( + f"Field '{attribute}' should be consistently None or not None in both " + "Detections." + ) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index db33ab01..b8b8f7c1 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -56,7 +56,8 @@ def box_iou_batch(boxes_true: np.ndarray, boxes_detection: np.ndarray) -> np.nda top_left = np.maximum(boxes_true[:, None, :2], boxes_detection[:, :2]) 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) + area_inter = np.prod( + np.clip(bottom_right - top_left, a_min=0, a_max=None), 2) return area_inter / (area_true[:, None] + area_detection - area_inter) @@ -81,7 +82,8 @@ 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 + union_area = masks_true_area[:, None] + \ + masks_detection_area - intersection_area return np.divide( intersection_area, @@ -132,7 +134,8 @@ 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)) return np.vstack(ious) @@ -162,7 +165,8 @@ def resize_masks(masks: np.ndarray, max_dimension: int = 640) -> np.ndarray: resized_masks = masks[:, yv, xv] - resized_masks = resized_masks.reshape(masks.shape[0], new_height, new_width) + resized_masks = resized_masks.reshape( + masks.shape[0], new_height, new_width) return resized_masks @@ -215,8 +219,9 @@ def mask_non_max_suppression( keep = np.ones(rows, dtype=bool) 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 :]) + condition = (ious[i] > iou_threshold) & ( + categories[i] == categories) + keep[i + 1:] = np.where(condition[i + 1:], False, keep[i + 1:]) return keep[sort_index.argsort()] @@ -275,9 +280,9 @@ def box_non_max_suppression( return keep[sort_index.argsort()] -def box_non_max_merge( +def _box_non_max_merge_all( predictions: npt.NDArray[np.float64], iou_threshold: float = 0.5 -) -> Dict[int, List[int]]: +) -> List[List[int]]: """ Apply greedy version of non-maximum merging to avoid detecting too many overlapping bounding boxes for a given object. @@ -290,64 +295,74 @@ def box_non_max_merge( to use for non-maximum suppression. Defaults to 0.5. Returns: - Dict[int, List[int]]: Mapping from prediction indices - to keep to a list of prediction indices to be merged. + List[List[int]]: Groups of prediction indices be merged. + Each group may have 1 or more elements. """ - keep_to_merge_list: Dict[int, List[int]] = {} + merge_groups: List[List[int]] = [] scores = predictions[:, 4] order = scores.argsort() while len(order) > 0: - idx = order[-1] - merge_candidate = np.expand_dims(predictions[idx], axis=0) + idx = int(order[-1]) order = order[:-1] if len(order) == 0: - keep_to_merge_list[idx.tolist()] = [] + merge_groups.append([idx]) break + merge_candidate = np.expand_dims(predictions[idx], axis=0) ious = box_iou_batch(predictions[order][:, :4], merge_candidate[:, :4]) ious = ious.flatten() above_threshold = ious >= iou_threshold - keep_to_merge_list[idx] = np.flip(order[above_threshold]).tolist() + merge_group = [idx] + np.flip(order[above_threshold]).tolist() + merge_groups.append(merge_group) order = order[~above_threshold] - - return keep_to_merge_list + return merge_groups -def box_non_max_merge_batch( - predictions: npt.NDArray[np.float64], iou_threshold: float = 0.5 -) -> Dict[int, List[int]]: +def box_non_max_merge( + predictions: npt.NDArray[np.float64], + iou_threshold: float = 0.5, +) -> List[List[int]]: """ Apply greedy version of non-maximum merging per category to avoid detecting too many overlapping bounding boxes for a given object. Args: - predictions (npt.NDArray[np.float64]): An array of shape `(n, 6)` containing - the bounding boxes coordinates in format `[x1, y1, x2, y2]`, - the confidence scores and class_ids. + predictions (npt.NDArray[np.float64]): An array of shape `(n, 5)` or `(n, 6)` + containing the bounding boxes coordinates in format `[x1, y1, x2, y2]`, + the confidence scores and class_ids. Omit class_id column to allow + detections of different classes to be merged. iou_threshold (float, optional): The intersection-over-union threshold to use for non-maximum suppression. Defaults to 0.5. Returns: - Dict[int, List[int]]: Mapping from prediction indices - to keep to a list of prediction indices to be merged. + List[List[int]]: Groups of prediction indices be merged. + Each group may have 1 or more elements. """ + if predictions.shape[1] == 5: + return _box_non_max_merge_all(predictions, iou_threshold) + category_ids = predictions[:, 5] - keep_to_merge_list = {} + merge_groups = [] for category_id in np.unique(category_ids): curr_indices = np.where(category_ids == category_id)[0] - curr_keep_to_merge_list = box_non_max_merge( + merge_class_groups = _box_non_max_merge_all( predictions[curr_indices], iou_threshold ) - curr_indices_list = curr_indices.tolist() - for curr_keep, curr_merge_list in curr_keep_to_merge_list.items(): - keep = curr_indices_list[curr_keep] - merge_list = [curr_indices_list[i] for i in curr_merge_list] - keep_to_merge_list[keep] = merge_list - return keep_to_merge_list + + 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 clip_boxes(xyxy: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray: @@ -552,7 +567,8 @@ def approximate_polygon( approximated_points = polygon while True: epsilon += epsilon_step - new_approximated_points = cv2.approxPolyDP(polygon, epsilon, closed=True) + new_approximated_points = cv2.approxPolyDP( + polygon, epsilon, closed=True) if len(new_approximated_points) > target_points: approximated_points = new_approximated_points else: @@ -581,7 +597,8 @@ def extract_ultralytics_masks(yolov8_results) -> Optional[np.ndarray]: ) top, left = int(pad[1]), int(pad[0]) - bottom, right = int(inference_shape[0] - pad[1]), int(inference_shape[1] - pad[0]) + bottom, right = int( + inference_shape[0] - pad[1]), int(inference_shape[1] - pad[0]) mask_maps = [] masks = yolov8_results.masks.data.cpu().numpy() @@ -648,7 +665,8 @@ def process_roboflow_result( polygon = np.array( [[point["x"], point["y"]] for point in prediction["points"]], dtype=int ) - mask = polygon_to_mask(polygon, resolution_wh=(image_width, image_height)) + mask = polygon_to_mask( + polygon, resolution_wh=(image_width, image_height)) xyxy.append([x_min, y_min, x_max, y_max]) class_id.append(prediction["class_id"]) class_name.append(prediction["class"]) @@ -659,10 +677,12 @@ def process_roboflow_result( xyxy = np.array(xyxy) if len(xyxy) > 0 else np.empty((0, 4)) confidence = np.array(confidence) if len(confidence) > 0 else np.empty(0) - class_id = np.array(class_id).astype(int) if len(class_id) > 0 else np.empty(0) + class_id = np.array(class_id).astype( + int) if len(class_id) > 0 else np.empty(0) class_name = np.array(class_name) if len(class_name) > 0 else np.empty(0) masks = np.array(masks, dtype=bool) if len(masks) > 0 else None - tracker_id = np.array(tracker_ids).astype(int) if len(tracker_ids) > 0 else None + tracker_id = np.array(tracker_ids).astype( + int) if len(tracker_ids) > 0 else None data = {CLASS_NAME_DATA_FIELD: class_name} return xyxy, confidence, class_id, masks, tracker_id, data @@ -722,13 +742,15 @@ def move_masks( """ if offset[0] < 0 or offset[1] < 0: - raise ValueError(f"Offset values must be non-negative integers. Got: {offset}") + raise ValueError( + f"Offset values must be non-negative integers. Got: {offset}") - mask_array = np.full((masks.shape[0], resolution_wh[1], resolution_wh[0]), False) + mask_array = np.full( + (masks.shape[0], resolution_wh[1], resolution_wh[0]), False) mask_array[ :, - offset[1] : masks.shape[1] + offset[1], - offset[0] : masks.shape[2] + offset[0], + offset[1]: masks.shape[1] + offset[1], + offset[0]: masks.shape[2] + offset[0], ] = masks return mask_array @@ -794,8 +816,10 @@ def calculate_masks_centroids(masks: np.ndarray) -> np.ndarray: return np.tensordot(masks, indices, axes=axis) aggregation_axis = ([1, 2], [0, 1]) - centroid_x = sum_over_mask(horizontal_indices, aggregation_axis) / total_pixels - centroid_y = sum_over_mask(vertical_indices, aggregation_axis) / total_pixels + centroid_x = sum_over_mask( + horizontal_indices, aggregation_axis) / total_pixels + centroid_y = sum_over_mask( + vertical_indices, aggregation_axis) / total_pixels return np.column_stack((centroid_x, centroid_y)).astype(int) @@ -873,7 +897,8 @@ def merge_data( elif ndim > 1: merged_data[key] = np.vstack(merged_data[key]) else: - raise ValueError(f"Unexpected array dimension for key '{key}'.") + raise ValueError( + f"Unexpected array dimension for key '{key}'.") else: raise ValueError( f"Inconsistent data types for key '{key}'. Only np.ndarray and list " @@ -918,6 +943,7 @@ def get_data_item( else: raise TypeError(f"Unsupported index type: {type(index)}") else: - raise TypeError(f"Unsupported data type for key '{key}': {type(value)}") + raise TypeError( + f"Unsupported data type for key '{key}': {type(value)}") return subset_data diff --git a/test/detection/test_core.py b/test/detection/test_core.py index 31e56dec..bef511e5 100644 --- a/test/detection/test_core.py +++ b/test/detection/test_core.py @@ -5,7 +5,7 @@ from typing import List, Optional, Union import numpy as np import pytest -from supervision.detection.core import Detections, merge_object_detection_pair +from supervision.detection.core import Detections, _merge_inner_detection_object_pair from supervision.geometry.core import Position PREDICTIONS = np.array( @@ -193,7 +193,8 @@ TEST_DET_DIFFERENT_DATA = Detections( DoesNotRaise(), ), # take only first detection by index slice (1, 3) (DETECTIONS, 10, None, pytest.raises(IndexError)), # index out of range - (DETECTIONS, [0, 2, 10], None, pytest.raises(IndexError)), # index out of range + (DETECTIONS, [0, 2, 10], None, pytest.raises( + IndexError)), # index out of range (DETECTIONS, np.array([0, 2, 10]), None, pytest.raises(IndexError)), ( DETECTIONS, @@ -482,7 +483,7 @@ def test_equal( data={"key_1": [1]}, ), DoesNotRaise(), - ), # Same confidence - merge box & mask, tiebreak to detection_1 + ), # Same confidence - merge box & mask, tie-break to detection_1 ( mock_detections( xyxy=[[0, 0, 20, 20]], @@ -512,7 +513,7 @@ def test_equal( ), # Different confidence, different area ( mock_detections( - xyxy=[[0, 0, 20, 20]], + xyxy=[[10, 10, 30, 30]], confidence=None, class_id=[1], mask=[np.array([[1, 1, 0], [1, 1, 0], [0, 0, 0]], dtype=bool)], @@ -520,31 +521,79 @@ def test_equal( data={"key_1": [1]}, ), mock_detections( - xyxy=[[10, 10, 30, 30]], - confidence=[0.2], + xyxy=[[20, 20, 40, 40]], + confidence=None, class_id=[2], mask=[np.array([[0, 0, 0], [0, 1, 1], [0, 1, 1]], dtype=bool)], tracker_id=[2], data={"key_2": [2]}, ), mock_detections( - xyxy=[[0, 0, 30, 30]], - confidence=[0.2], - class_id=[2], + xyxy=[[10, 10, 40, 40]], + confidence=None, + class_id=[1], mask=[np.array([[1, 1, 0], [1, 1, 1], [0, 1, 1]], dtype=bool)], - tracker_id=[2], - data={"key_2": [2]}, + tracker_id=[1], + data={"key_1": [1]}, ), DoesNotRaise(), - ), # merge with no confidence + ), # No confidence at all + ( + mock_detections( + xyxy=[[0, 0, 20, 20]], + confidence=None, + ), + mock_detections( + xyxy=[[10, 10, 30, 30]], + confidence=[0.2], + ), + None, + pytest.raises(ValueError), + ), # confidence: None + [x] + ( + mock_detections( + xyxy=[[0, 0, 20, 20]], + mask=[np.array([[1, 1, 0], [1, 1, 0], [0, 0, 0]], dtype=bool)], + ), + mock_detections( + xyxy=[[10, 10, 30, 30]], + mask=None, + ), + None, + pytest.raises(ValueError), + ), # mask: None + [x] + ( + mock_detections( + xyxy=[[0, 0, 20, 20]], + tracker_id=[1] + ), + mock_detections( + xyxy=[[10, 10, 30, 30]], + tracker_id=None, + ), + None, + pytest.raises(ValueError), + ), # tracker_id: None + [] + ( + mock_detections( + xyxy=[[0, 0, 20, 20]], + class_id=[1] + ), + mock_detections( + xyxy=[[10, 10, 30, 30]], + class_id=None, + ), + None, + pytest.raises(ValueError), + ) # class_id: None + [] ], ) -def test_merge_object_detection_pair( +def test_merge_inner_detection_object_pair( detection_1: Detections, detection_2: Detections, expected_result: Optional[Detections], exception: Exception, ): with exception: - result = merge_object_detection_pair(detection_1, detection_2) + result = _merge_inner_detection_object_pair(detection_1, detection_2) assert result == expected_result diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index e6f33084..cb7537e1 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -6,7 +6,7 @@ import pytest from supervision.config import CLASS_NAME_DATA_FIELD from supervision.detection.utils import ( - box_non_max_merge, + _box_non_max_merge_all, box_non_max_suppression, calculate_masks_centroids, clip_boxes, @@ -134,67 +134,67 @@ def test_box_non_max_suppression( ( np.empty(shape=(0, 5), dtype=float), 0.5, - {}, + [], DoesNotRaise(), ), ( np.array([[0, 0, 10, 10, 1.0]]), 0.5, - {0: []}, + [[0]], DoesNotRaise(), ), ( np.array([[0, 0, 10, 10, 1.0], [0, 0, 9, 9, 1.0]]), 0.5, - {1: [0]}, + [[1, 0]], DoesNotRaise(), ), # High overlap, tie-break to second det ( np.array([[0, 0, 10, 10, 1.0], [0, 0, 9, 9, 0.99]]), 0.5, - {0: [1]}, + [[0, 1]], DoesNotRaise(), ), # High overlap, merge to high confidence ( np.array([[0, 0, 10, 10, 0.99], [0, 0, 9, 9, 1.0]]), 0.5, - {1: [0]}, + [[1, 0]], DoesNotRaise(), ), # (test symmetry) High overlap, merge to high confidence ( - np.array([[0, 0, 10, 10, 0.99], [0, 0, 9, 9, 1.0]]), + np.array([[0, 0, 10, 10, 0.90], [0, 0, 9, 9, 1.0]]), 0.5, - {1: [0]}, + [[1, 0]], DoesNotRaise(), ), # (test symmetry) High overlap, merge to high confidence ( np.array([[0, 0, 10, 10, 1.0], [0, 0, 9, 9, 1.0]]), 1.0, - {0: [], 1: []}, + [[1], [0]], DoesNotRaise(), ), # High IOU required ( np.array([[0, 0, 10, 10, 1.0], [0, 0, 9, 9, 1.0]]), 0.0, - {1: [0]}, + [[1, 0]], DoesNotRaise(), ), # No IOU required ( np.array([[0, 0, 10, 10, 1.0], [0, 0, 5, 5, 0.9]]), 0.25, - {0: [1]}, + [[0, 1]], DoesNotRaise(), ), # Below IOU requirement ( np.array([[0, 0, 10, 10, 1.0], [0, 0, 5, 5, 0.9]]), 0.26, - {0: [], 1: []}, + [[0], [1]], DoesNotRaise(), ), # Above IOU requirement ( np.array([[0, 0, 10, 10, 1.0], [0, 0, 9, 9, 1.0], [0, 0, 8, 8, 1.0]]), 0.5, - {2: [1, 0]}, + [[2, 1, 0]], DoesNotRaise(), ), # 3 boxes ( @@ -208,7 +208,7 @@ def test_box_non_max_suppression( ] ), 0.5, - {1: [0], 3: [2], 4: []}, + [[4], [3, 2], [1, 0]], DoesNotRaise(), ), # 5 boxes, 2 merges, 1 separate ( @@ -222,7 +222,7 @@ def test_box_non_max_suppression( ] ), 0.33, - {0: [], 2: [1], 4: [3]}, + [[4, 3], [2, 1], [0]], DoesNotRaise(), ), # sequential merge, half overlap ( @@ -236,7 +236,7 @@ def test_box_non_max_suppression( ] ), 0.33, - {0: [], 2: [3, 1], 4: []}, + [[2, 3, 1], [4], [0]], DoesNotRaise(), ), # confidence ], @@ -244,11 +244,13 @@ def test_box_non_max_suppression( def test_box_non_max_merge( predictions: np.ndarray, iou_threshold: float, - expected_result: Dict[int, List[int]], + expected_result: List[List[int]], exception: Exception, ) -> None: with exception: - result = box_non_max_merge(predictions=predictions, iou_threshold=iou_threshold) + result = _box_non_max_merge_all( + predictions=predictions, iou_threshold=iou_threshold + ) assert result == expected_result @@ -664,7 +666,8 @@ def test_filter_polygons_by_area( "image": {"width": 1000, "height": 1000}, }, ( - np.array([[175.0, 275.0, 225.0, 325.0], [450.0, 450.0, 550.0, 550.0]]), + np.array([[175.0, 275.0, 225.0, 325.0], + [450.0, 450.0, 550.0, 550.0]]), np.array([0.9, 0.8]), np.array([0, 7]), None, @@ -1118,8 +1121,10 @@ def test_calculate_masks_centroids( ), # two data dicts with the same field name and np.array values as 2D arrays ( [ - {"test_1": np.array([1, 2, 3]), "test_2": np.array(["a", "b", "c"])}, - {"test_1": np.array([3, 2, 1]), "test_2": np.array(["c", "b", "a"])}, + {"test_1": np.array([1, 2, 3]), + "test_2": np.array(["a", "b", "c"])}, + {"test_1": np.array([3, 2, 1]), + "test_2": np.array(["c", "b", "a"])}, ], { "test_1": np.array([1, 2, 3, 3, 2, 1]), @@ -1148,8 +1153,10 @@ def test_calculate_masks_centroids( ), # two data dicts with the same field name and 1D and 2D arrays values ( [ - {"test_1": np.array([1, 2, 3]), "test_2": np.array(["a", "b"])}, - {"test_1": np.array([3, 2, 1]), "test_2": np.array(["c", "b", "a"])}, + {"test_1": np.array([1, 2, 3]), + "test_2": np.array(["a", "b"])}, + {"test_1": np.array([3, 2, 1]), + "test_2": np.array(["c", "b", "a"])}, ], None, pytest.raises(ValueError), @@ -1160,7 +1167,8 @@ def test_calculate_masks_centroids( DoesNotRaise(), ), # two data dicts; one empty and one non-empty dict ( - [{"test_1": [], "test_2": []}, {"test_1": [1, 2, 3], "test_2": [1, 2, 3]}], + [{"test_1": [], "test_2": []}, { + "test_1": [1, 2, 3], "test_2": [1, 2, 3]}], {"test_1": [1, 2, 3], "test_2": [1, 2, 3]}, DoesNotRaise(), ), # two data dicts; one empty and one non-empty dict; same keys From db1b4737fec31de88de5c0f946faf95a4ca88372 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 23 May 2024 13:04:09 +0000 Subject: [PATCH 102/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/detection/utils.py | 54 ++++++++++++---------------------- test/detection/test_core.py | 17 ++++------- test/detection/test_utils.py | 18 ++++-------- 3 files changed, 30 insertions(+), 59 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index b8b8f7c1..4beea2ed 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -56,8 +56,7 @@ def box_iou_batch(boxes_true: np.ndarray, boxes_detection: np.ndarray) -> np.nda top_left = np.maximum(boxes_true[:, None, :2], boxes_detection[:, :2]) 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) + area_inter = np.prod(np.clip(bottom_right - top_left, a_min=0, a_max=None), 2) return area_inter / (area_true[:, None] + area_detection - area_inter) @@ -82,8 +81,7 @@ 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 + union_area = masks_true_area[:, None] + masks_detection_area - intersection_area return np.divide( intersection_area, @@ -134,8 +132,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)) return np.vstack(ious) @@ -165,8 +162,7 @@ def resize_masks(masks: np.ndarray, max_dimension: int = 640) -> np.ndarray: resized_masks = masks[:, yv, xv] - resized_masks = resized_masks.reshape( - masks.shape[0], new_height, new_width) + resized_masks = resized_masks.reshape(masks.shape[0], new_height, new_width) return resized_masks @@ -219,9 +215,8 @@ def mask_non_max_suppression( keep = np.ones(rows, dtype=bool) 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:]) + condition = (ious[i] > iou_threshold) & (categories[i] == categories) + keep[i + 1 :] = np.where(condition[i + 1 :], False, keep[i + 1 :]) return keep[sort_index.argsort()] @@ -567,8 +562,7 @@ def approximate_polygon( approximated_points = polygon while True: epsilon += epsilon_step - new_approximated_points = cv2.approxPolyDP( - polygon, epsilon, closed=True) + new_approximated_points = cv2.approxPolyDP(polygon, epsilon, closed=True) if len(new_approximated_points) > target_points: approximated_points = new_approximated_points else: @@ -597,8 +591,7 @@ def extract_ultralytics_masks(yolov8_results) -> Optional[np.ndarray]: ) top, left = int(pad[1]), int(pad[0]) - bottom, right = int( - inference_shape[0] - pad[1]), int(inference_shape[1] - pad[0]) + bottom, right = int(inference_shape[0] - pad[1]), int(inference_shape[1] - pad[0]) mask_maps = [] masks = yolov8_results.masks.data.cpu().numpy() @@ -665,8 +658,7 @@ def process_roboflow_result( polygon = np.array( [[point["x"], point["y"]] for point in prediction["points"]], dtype=int ) - mask = polygon_to_mask( - polygon, resolution_wh=(image_width, image_height)) + mask = polygon_to_mask(polygon, resolution_wh=(image_width, image_height)) xyxy.append([x_min, y_min, x_max, y_max]) class_id.append(prediction["class_id"]) class_name.append(prediction["class"]) @@ -677,12 +669,10 @@ def process_roboflow_result( xyxy = np.array(xyxy) if len(xyxy) > 0 else np.empty((0, 4)) confidence = np.array(confidence) if len(confidence) > 0 else np.empty(0) - class_id = np.array(class_id).astype( - int) if len(class_id) > 0 else np.empty(0) + class_id = np.array(class_id).astype(int) if len(class_id) > 0 else np.empty(0) class_name = np.array(class_name) if len(class_name) > 0 else np.empty(0) masks = np.array(masks, dtype=bool) if len(masks) > 0 else None - tracker_id = np.array(tracker_ids).astype( - int) if len(tracker_ids) > 0 else None + tracker_id = np.array(tracker_ids).astype(int) if len(tracker_ids) > 0 else None data = {CLASS_NAME_DATA_FIELD: class_name} return xyxy, confidence, class_id, masks, tracker_id, data @@ -742,15 +732,13 @@ def move_masks( """ if offset[0] < 0 or offset[1] < 0: - raise ValueError( - f"Offset values must be non-negative integers. Got: {offset}") + raise ValueError(f"Offset values must be non-negative integers. Got: {offset}") - mask_array = np.full( - (masks.shape[0], resolution_wh[1], resolution_wh[0]), False) + mask_array = np.full((masks.shape[0], resolution_wh[1], resolution_wh[0]), False) mask_array[ :, - offset[1]: masks.shape[1] + offset[1], - offset[0]: masks.shape[2] + offset[0], + offset[1] : masks.shape[1] + offset[1], + offset[0] : masks.shape[2] + offset[0], ] = masks return mask_array @@ -816,10 +804,8 @@ def calculate_masks_centroids(masks: np.ndarray) -> np.ndarray: return np.tensordot(masks, indices, axes=axis) aggregation_axis = ([1, 2], [0, 1]) - centroid_x = sum_over_mask( - horizontal_indices, aggregation_axis) / total_pixels - centroid_y = sum_over_mask( - vertical_indices, aggregation_axis) / total_pixels + centroid_x = sum_over_mask(horizontal_indices, aggregation_axis) / total_pixels + centroid_y = sum_over_mask(vertical_indices, aggregation_axis) / total_pixels return np.column_stack((centroid_x, centroid_y)).astype(int) @@ -897,8 +883,7 @@ def merge_data( elif ndim > 1: merged_data[key] = np.vstack(merged_data[key]) else: - raise ValueError( - f"Unexpected array dimension for key '{key}'.") + raise ValueError(f"Unexpected array dimension for key '{key}'.") else: raise ValueError( f"Inconsistent data types for key '{key}'. Only np.ndarray and list " @@ -943,7 +928,6 @@ def get_data_item( else: raise TypeError(f"Unsupported index type: {type(index)}") else: - raise TypeError( - f"Unsupported data type for key '{key}': {type(value)}") + raise TypeError(f"Unsupported data type for key '{key}': {type(value)}") return subset_data diff --git a/test/detection/test_core.py b/test/detection/test_core.py index bef511e5..dc58c9e8 100644 --- a/test/detection/test_core.py +++ b/test/detection/test_core.py @@ -193,8 +193,7 @@ TEST_DET_DIFFERENT_DATA = Detections( DoesNotRaise(), ), # take only first detection by index slice (1, 3) (DETECTIONS, 10, None, pytest.raises(IndexError)), # index out of range - (DETECTIONS, [0, 2, 10], None, pytest.raises( - IndexError)), # index out of range + (DETECTIONS, [0, 2, 10], None, pytest.raises(IndexError)), # index out of range (DETECTIONS, np.array([0, 2, 10]), None, pytest.raises(IndexError)), ( DETECTIONS, @@ -550,7 +549,7 @@ def test_equal( None, pytest.raises(ValueError), ), # confidence: None + [x] - ( + ( mock_detections( xyxy=[[0, 0, 20, 20]], mask=[np.array([[1, 1, 0], [1, 1, 0], [0, 0, 0]], dtype=bool)], @@ -563,10 +562,7 @@ def test_equal( pytest.raises(ValueError), ), # mask: None + [x] ( - mock_detections( - xyxy=[[0, 0, 20, 20]], - tracker_id=[1] - ), + mock_detections(xyxy=[[0, 0, 20, 20]], tracker_id=[1]), mock_detections( xyxy=[[10, 10, 30, 30]], tracker_id=None, @@ -575,17 +571,14 @@ def test_equal( pytest.raises(ValueError), ), # tracker_id: None + [] ( - mock_detections( - xyxy=[[0, 0, 20, 20]], - class_id=[1] - ), + mock_detections(xyxy=[[0, 0, 20, 20]], class_id=[1]), mock_detections( xyxy=[[10, 10, 30, 30]], class_id=None, ), None, pytest.raises(ValueError), - ) # class_id: None + [] + ), # class_id: None + [] ], ) def test_merge_inner_detection_object_pair( diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index cb7537e1..9a1fa8c9 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -666,8 +666,7 @@ def test_filter_polygons_by_area( "image": {"width": 1000, "height": 1000}, }, ( - np.array([[175.0, 275.0, 225.0, 325.0], - [450.0, 450.0, 550.0, 550.0]]), + np.array([[175.0, 275.0, 225.0, 325.0], [450.0, 450.0, 550.0, 550.0]]), np.array([0.9, 0.8]), np.array([0, 7]), None, @@ -1121,10 +1120,8 @@ def test_calculate_masks_centroids( ), # two data dicts with the same field name and np.array values as 2D arrays ( [ - {"test_1": np.array([1, 2, 3]), - "test_2": np.array(["a", "b", "c"])}, - {"test_1": np.array([3, 2, 1]), - "test_2": np.array(["c", "b", "a"])}, + {"test_1": np.array([1, 2, 3]), "test_2": np.array(["a", "b", "c"])}, + {"test_1": np.array([3, 2, 1]), "test_2": np.array(["c", "b", "a"])}, ], { "test_1": np.array([1, 2, 3, 3, 2, 1]), @@ -1153,10 +1150,8 @@ def test_calculate_masks_centroids( ), # two data dicts with the same field name and 1D and 2D arrays values ( [ - {"test_1": np.array([1, 2, 3]), - "test_2": np.array(["a", "b"])}, - {"test_1": np.array([3, 2, 1]), - "test_2": np.array(["c", "b", "a"])}, + {"test_1": np.array([1, 2, 3]), "test_2": np.array(["a", "b"])}, + {"test_1": np.array([3, 2, 1]), "test_2": np.array(["c", "b", "a"])}, ], None, pytest.raises(ValueError), @@ -1167,8 +1162,7 @@ def test_calculate_masks_centroids( DoesNotRaise(), ), # two data dicts; one empty and one non-empty dict ( - [{"test_1": [], "test_2": []}, { - "test_1": [1, 2, 3], "test_2": [1, 2, 3]}], + [{"test_1": [], "test_2": []}, {"test_1": [1, 2, 3], "test_2": [1, 2, 3]}], {"test_1": [1, 2, 3], "test_2": [1, 2, 3]}, DoesNotRaise(), ), # two data dicts; one empty and one non-empty dict; same keys From 0721bc289b8f9cea901ac3e9004e2b305f618c9b Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Thu, 23 May 2024 16:21:54 +0300 Subject: [PATCH 103/182] Remove _set_at_index --- supervision/detection/core.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 6abc8dad..069eaf09 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -1068,33 +1068,6 @@ class Detections: self.data[key] = value - def _set_at_index(self, index: int, other: Detections): - """ - Set detection values (xyxy, confidence, ...) at a specified index - to those of another Detections object, at index 0. - - Args: - index (int): The index in current detection, where values - will be set. - other (Detections): Detections object with exactly one element - to set the values from. - - Raises: - ValueError: If `other` is not made of exactly one element. - """ - if len(other) != 1: - raise ValueError("Detection to set from must have exactly one element.") - - self.xyxy[index] = other.xyxy[0] - if self.mask is not None and other.mask is not None: - self.mask[index] = other.mask[0] - if self.confidence is not None and other.confidence is not None: - self.confidence[index] = other.confidence[0] - if self.class_id is not None and other.class_id is not None: - self.class_id[index] = other.class_id[0] - if self.tracker_id is not None and other.tracker_id is not None: - self.tracker_id[index] = other.tracker_id[0] - @property def area(self) -> np.ndarray: """ From 19296a73f612536e8b1b9f2f82ee9dee707dd225 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Thu, 23 May 2024 18:44:23 +0300 Subject: [PATCH 104/182] ious: Replace 0-area check with nan conversion --- supervision/detection/utils.py | 4 +++- supervision/tracker/byte_tracker/core.py | 7 ------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 3eeba5b4..80742fd3 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -56,7 +56,9 @@ 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) - return area_inter / (area_true[:, None] + area_detection - area_inter) + ious = area_inter / (area_true[:, None] + area_detection - area_inter) + ious = np.nan_to_num(ious) + return ious def _mask_iou_batch_split( diff --git a/supervision/tracker/byte_tracker/core.py b/supervision/tracker/byte_tracker/core.py index 132bf391..ce3bbbbf 100644 --- a/supervision/tracker/byte_tracker/core.py +++ b/supervision/tracker/byte_tracker/core.py @@ -362,13 +362,6 @@ class ByteTrack: scores = tensors[:, 4] bboxes = tensors[:, :4] - bbox_areas = (bboxes[:, 2] - bboxes[:, 0]) * (bboxes[:, 3] - bboxes[:, 1]) - valid_box_inds = bbox_areas > 0 - - class_ids = class_ids[valid_box_inds] - scores = scores[valid_box_inds] - bboxes = bboxes[valid_box_inds] - remain_inds = scores > self.track_activation_threshold inds_low = scores > 0.1 inds_high = scores < self.track_activation_threshold From 9afca17854236faf5c92296bfb95f796d614578c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 May 2024 00:13:26 +0000 Subject: [PATCH 105/182] :arrow_up: Bump ruff from 0.4.4 to 0.4.5 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.4 to 0.4.5. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.4.4...v0.4.5) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/poetry.lock b/poetry.lock index 85615d1c..0a9276f8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3662,28 +3662,28 @@ files = [ [[package]] name = "ruff" -version = "0.4.4" +version = "0.4.5" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29d44ef5bb6a08e235c8249294fa8d431adc1426bfda99ed493119e6f9ea1bf6"}, - {file = "ruff-0.4.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c4efe62b5bbb24178c950732ddd40712b878a9b96b1d02b0ff0b08a090cbd891"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c8e2f1e8fc12d07ab521a9005d68a969e167b589cbcaee354cb61e9d9de9c15"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60ed88b636a463214905c002fa3eaab19795679ed55529f91e488db3fe8976ab"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b90fc5e170fc71c712cc4d9ab0e24ea505c6a9e4ebf346787a67e691dfb72e85"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8e7e6ebc10ef16dcdc77fd5557ee60647512b400e4a60bdc4849468f076f6eef"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9ddb2c494fb79fc208cd15ffe08f32b7682519e067413dbaf5f4b01a6087bcd"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c51c928a14f9f0a871082603e25a1588059b7e08a920f2f9fa7157b5bf08cfe9"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5eb0a4bfd6400b7d07c09a7725e1a98c3b838be557fee229ac0f84d9aa49c36"}, - {file = "ruff-0.4.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b1867ee9bf3acc21778dcb293db504692eda5f7a11a6e6cc40890182a9f9e595"}, - {file = "ruff-0.4.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1aecced1269481ef2894cc495647392a34b0bf3e28ff53ed95a385b13aa45768"}, - {file = "ruff-0.4.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da73eb616b3241a307b837f32756dc20a0b07e2bcb694fec73699c93d04a69e"}, - {file = "ruff-0.4.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:958b4ea5589706a81065e2a776237de2ecc3e763342e5cc8e02a4a4d8a5e6f95"}, - {file = "ruff-0.4.4-py3-none-win32.whl", hash = "sha256:cb53473849f011bca6e754f2cdf47cafc9c4f4ff4570003a0dad0b9b6890e876"}, - {file = "ruff-0.4.4-py3-none-win_amd64.whl", hash = "sha256:424e5b72597482543b684c11def82669cc6b395aa8cc69acc1858b5ef3e5daae"}, - {file = "ruff-0.4.4-py3-none-win_arm64.whl", hash = "sha256:39df0537b47d3b597293edbb95baf54ff5b49589eb7ff41926d8243caa995ea6"}, - {file = "ruff-0.4.4.tar.gz", hash = "sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af"}, + {file = "ruff-0.4.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8f58e615dec58b1a6b291769b559e12fdffb53cc4187160a2fc83250eaf54e96"}, + {file = "ruff-0.4.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:84dd157474e16e3a82745d2afa1016c17d27cb5d52b12e3d45d418bcc6d49264"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25f483ad9d50b00e7fd577f6d0305aa18494c6af139bce7319c68a17180087f4"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63fde3bf6f3ad4e990357af1d30e8ba2730860a954ea9282c95fc0846f5f64af"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e3ba4620dee27f76bbcad97067766026c918ba0f2d035c2fc25cbdd04d9c97"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:441dab55c568e38d02bbda68a926a3d0b54f5510095c9de7f95e47a39e0168aa"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1169e47e9c4136c997f08f9857ae889d614c5035d87d38fda9b44b4338909cdf"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:755ac9ac2598a941512fc36a9070a13c88d72ff874a9781493eb237ab02d75df"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4b02a65985be2b34b170025a8b92449088ce61e33e69956ce4d316c0fe7cce0"}, + {file = "ruff-0.4.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:75a426506a183d9201e7e5664de3f6b414ad3850d7625764106f7b6d0486f0a1"}, + {file = "ruff-0.4.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6e1b139b45e2911419044237d90b60e472f57285950e1492c757dfc88259bb06"}, + {file = "ruff-0.4.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a6f29a8221d2e3d85ff0c7b4371c0e37b39c87732c969b4d90f3dad2e721c5b1"}, + {file = "ruff-0.4.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d6ef817124d72b54cc923f3444828ba24fa45c3164bc9e8f1813db2f3d3a8a11"}, + {file = "ruff-0.4.5-py3-none-win32.whl", hash = "sha256:aed8166c18b1a169a5d3ec28a49b43340949e400665555b51ee06f22813ef062"}, + {file = "ruff-0.4.5-py3-none-win_amd64.whl", hash = "sha256:b0b03c619d2b4350b4a27e34fd2ac64d0dabe1afbf43de57d0f9d8a05ecffa45"}, + {file = "ruff-0.4.5-py3-none-win_arm64.whl", hash = "sha256:9d15de3425f53161b3f5a5658d4522e4eee5ea002bf2ac7aa380743dd9ad5fba"}, + {file = "ruff-0.4.5.tar.gz", hash = "sha256:286eabd47e7d4d521d199cab84deca135557e6d1e0f0d01c29e757c3cb151b54"}, ] [[package]] From 7d0488efc06b972db2d4b48fabe7069ebf7227df Mon Sep 17 00:00:00 2001 From: tc360950 Date: Fri, 24 May 2024 16:39:30 +0200 Subject: [PATCH 106/182] Add unit tests for negative coordinates and empty anchors --- supervision/detection/line_zone.py | 4 +- test/detection/test_line_counter.py | 101 ++++++++++++++++++++++++---- 2 files changed, 91 insertions(+), 14 deletions(-) diff --git a/supervision/detection/line_zone.py b/supervision/detection/line_zone.py index 53d762a0..dc1751f4 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -80,7 +80,9 @@ class LineZone: self.tracker_state: Dict[str, bool] = {} self.in_count: int = 0 self.out_count: int = 0 - self.triggering_anchors = triggering_anchors + self.triggering_anchors = list(triggering_anchors) + if not self.triggering_anchors: + raise ValueError("Triggering anchors cannot be empty.") @staticmethod def calculate_region_of_interest_limits(vector: Vector) -> Tuple[Vector, Vector]: diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index 9c530cca..931f5f52 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -201,7 +201,48 @@ def test_calculate_region_of_interest_limits( [False, False, True, False, True, False, True, False], [False, True, False, True, False, True, False, True], ), - ], + ( + Vector( + Point(0, 100), + Point(0, 200), + ), + [ + [-100, 150, -80, 170], + [-100, 50, -80, 70], + [-10, 50, 20, 70], + [100, 50, 120, 70], + ], # detection goes "around" line start and hence never crosses it + [False, False, False, False], + [False, False, False, False], + ), + ( + Vector( + Point(0, 100), + Point(0, 200), + ), + [ + [-100, 150, -80, 170], + [-100, 250, -80, 270], + [-10, 250, 20, 270], + [100, 250, 120, 270], + ], # detection goes "around" line end and hence never crosses it + [False, False, False, False], + [False, False, False, False], + ), + ( + Vector( + Point(-50, -50), + Point(-100, -150), + ), + [ + [-30, -80, -20, -100], + [-150, -60, -110, -70], + [-10, -100, 20, -130], + ], + [False, True, False], + [False, False, True], + ) + ], ) def test_line_zone_single_detection( vector: Vector, @@ -210,7 +251,7 @@ def test_line_zone_single_detection( expected_crossed_out: List[bool], ) -> None: """ - Test LineZone with single detection which crosses the line. + Test LineZone with single detection. The detection is represented by a sequence of xyxy bboxes which represent subsequent positions of the detected object. If a line is crossed (in either direction) it is crossed by all anchors simultaneously. @@ -308,7 +349,7 @@ def test_line_zone_single_detection_on_subset_of_anchors( @pytest.mark.parametrize( - "vector, xyxy_sequence, expected_crossed_in, expected_crossed_out", + "vector, xyxy_sequence, expected_crossed_in, expected_crossed_out, anchors, exception", [ ( Vector( @@ -322,6 +363,8 @@ def test_line_zone_single_detection_on_subset_of_anchors( ], [[False, False], [False, False], [True, False]], [[False, False], [True, False], [False, False]], + [Position.TOP_LEFT, Position.TOP_RIGHT, Position.BOTTOM_LEFT, Position.BOTTOM_RIGHT], + DoesNotRaise(), ), ( Vector( @@ -358,7 +401,34 @@ def test_line_zone_single_detection_on_subset_of_anchors( (False, False), (True, True), ], + [Position.TOP_LEFT, Position.TOP_RIGHT, Position.BOTTOM_LEFT, Position.BOTTOM_RIGHT], + DoesNotRaise(), ), + ( + Vector( + Point(-50, -50), + Point(-100, -150), + ), + [ + [[-30, -80, -20, -100], [100, 50, 120, 70]], + [[-100, -80, -20, -100], [100, 50, 120, 70]], + ], + [[False, False], [True, False]], + [[False, False], [False, False]], + [Position.TOP_LEFT], + DoesNotRaise(), + ), + ( + Vector( + Point(0, 0), + Point(-100, 0), + ), + [[[-50, 70, -40, 50], [-80, -50, -70, -40]]], + [(False, False)], + [(False, False)], + [], # raise because of empty anchors + pytest.raises(ValueError), + ) ], ) def test_line_zone_multiple_detections( @@ -366,19 +436,24 @@ def test_line_zone_multiple_detections( xyxy_sequence: List[List[List[int]]], expected_crossed_in: List[bool], expected_crossed_out: List[bool], + anchors: list[Position], + exception: Exception + ) -> None: """ Test LineZone with multiple detections. A detection is represented by a sequence of xyxy bboxes which represent subsequent positions of the detected object. If a line is crossed (in either - direction) by a detection it is crossed by all its anchors simultaneously. + direction) by a detection it is crossed by exactly all anchors from @anchors. """ - line_zone = LineZone(start=vector.start, end=vector.end) - for i, bboxes in enumerate(xyxy_sequence): - detections = mock_detections( - xyxy=bboxes, - tracker_id=[i for i in range(0, len(bboxes))], - ) - crossed_in, crossed_out = line_zone.trigger(detections) - assert np.all(crossed_in == expected_crossed_in[i]) - assert np.all(crossed_out == expected_crossed_out[i]) + with exception: + line_zone = LineZone(start=vector.start, end=vector.end, triggering_anchors=anchors) + for i, bboxes in enumerate(xyxy_sequence): + detections = mock_detections( + xyxy=bboxes, + tracker_id=[i for i in range(0, len(bboxes))], + ) + crossed_in, crossed_out = line_zone.trigger(detections) + assert np.all(crossed_in == expected_crossed_in[i]) + assert np.all(crossed_out == expected_crossed_out[i]) + From 1a021d57922cb31bb5c987383cb6fabbd0b1f98a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 24 May 2024 14:40:29 +0000 Subject: [PATCH 107/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/detection/test_line_counter.py | 36 ++++++++++++++++++----------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index 0b7a7fd8..454afb5b 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -211,7 +211,7 @@ def test_calculate_region_of_interest_limits( [-100, 50, -80, 70], [-10, 50, 20, 70], [100, 50, 120, 70], - ], # detection goes "around" line start and hence never crosses it + ], # detection goes "around" line start and hence never crosses it [False, False, False, False], [False, False, False, False], ), @@ -225,7 +225,7 @@ def test_calculate_region_of_interest_limits( [-100, 250, -80, 270], [-10, 250, 20, 270], [100, 250, 120, 270], - ], # detection goes "around" line end and hence never crosses it + ], # detection goes "around" line end and hence never crosses it [False, False, False, False], [False, False, False, False], ), @@ -241,8 +241,8 @@ def test_calculate_region_of_interest_limits( ], [False, True, False], [False, False, True], - ) - ], + ), + ], ) def test_line_zone_single_detection( vector: Vector, @@ -364,7 +364,12 @@ def test_line_zone_single_detection_on_subset_of_anchors( ], [[False, False], [False, False], [True, False]], [[False, False], [True, False], [False, False]], - [Position.TOP_LEFT, Position.TOP_RIGHT, Position.BOTTOM_LEFT, Position.BOTTOM_RIGHT], + [ + Position.TOP_LEFT, + Position.TOP_RIGHT, + Position.BOTTOM_LEFT, + Position.BOTTOM_RIGHT, + ], DoesNotRaise(), ), ( @@ -402,7 +407,12 @@ def test_line_zone_single_detection_on_subset_of_anchors( (False, False), (True, True), ], - [Position.TOP_LEFT, Position.TOP_RIGHT, Position.BOTTOM_LEFT, Position.BOTTOM_RIGHT], + [ + Position.TOP_LEFT, + Position.TOP_RIGHT, + Position.BOTTOM_LEFT, + Position.BOTTOM_RIGHT, + ], DoesNotRaise(), ), ( @@ -427,9 +437,9 @@ def test_line_zone_single_detection_on_subset_of_anchors( [[[-50, 70, -40, 50], [-80, -50, -70, -40]]], [(False, False)], [(False, False)], - [], # raise because of empty anchors + [], # raise because of empty anchors pytest.raises(ValueError), - ) + ), ], ) def test_line_zone_multiple_detections( @@ -437,9 +447,8 @@ def test_line_zone_multiple_detections( xyxy_sequence: List[List[List[int]]], expected_crossed_in: List[bool], expected_crossed_out: List[bool], - anchors: list[Position], - exception: Exception - + anchors: list[Position], + exception: Exception, ) -> None: """ Test LineZone with multiple detections. @@ -448,7 +457,9 @@ def test_line_zone_multiple_detections( direction) by a detection it is crossed by exactly all anchors from @anchors. """ with exception: - line_zone = LineZone(start=vector.start, end=vector.end, triggering_anchors=anchors) + line_zone = LineZone( + start=vector.start, end=vector.end, triggering_anchors=anchors + ) for i, bboxes in enumerate(xyxy_sequence): detections = mock_detections( xyxy=bboxes, @@ -457,4 +468,3 @@ def test_line_zone_multiple_detections( crossed_in, crossed_out = line_zone.trigger(detections) assert np.all(crossed_in == expected_crossed_in[i]) assert np.all(crossed_out == expected_crossed_out[i]) - From 4e2eb0afc24d97692c12fd44b82f273c4ef62131 Mon Sep 17 00:00:00 2001 From: tc360950 Date: Fri, 24 May 2024 16:42:40 +0200 Subject: [PATCH 108/182] Code reformatting --- test/detection/test_line_counter.py | 36 ++++++++++++++++++----------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index 0b7a7fd8..7e141277 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -211,7 +211,7 @@ def test_calculate_region_of_interest_limits( [-100, 50, -80, 70], [-10, 50, 20, 70], [100, 50, 120, 70], - ], # detection goes "around" line start and hence never crosses it + ], # detection goes "around" line start and hence never crosses it [False, False, False, False], [False, False, False, False], ), @@ -225,7 +225,7 @@ def test_calculate_region_of_interest_limits( [-100, 250, -80, 270], [-10, 250, 20, 270], [100, 250, 120, 270], - ], # detection goes "around" line end and hence never crosses it + ], # detection goes "around" line end and hence never crosses it [False, False, False, False], [False, False, False, False], ), @@ -241,8 +241,8 @@ def test_calculate_region_of_interest_limits( ], [False, True, False], [False, False, True], - ) - ], + ), + ], ) def test_line_zone_single_detection( vector: Vector, @@ -364,7 +364,12 @@ def test_line_zone_single_detection_on_subset_of_anchors( ], [[False, False], [False, False], [True, False]], [[False, False], [True, False], [False, False]], - [Position.TOP_LEFT, Position.TOP_RIGHT, Position.BOTTOM_LEFT, Position.BOTTOM_RIGHT], + [ + Position.TOP_LEFT, + Position.TOP_RIGHT, + Position.BOTTOM_LEFT, + Position.BOTTOM_RIGHT, + ], DoesNotRaise(), ), ( @@ -402,7 +407,12 @@ def test_line_zone_single_detection_on_subset_of_anchors( (False, False), (True, True), ], - [Position.TOP_LEFT, Position.TOP_RIGHT, Position.BOTTOM_LEFT, Position.BOTTOM_RIGHT], + [ + Position.TOP_LEFT, + Position.TOP_RIGHT, + Position.BOTTOM_LEFT, + Position.BOTTOM_RIGHT, + ], DoesNotRaise(), ), ( @@ -427,9 +437,9 @@ def test_line_zone_single_detection_on_subset_of_anchors( [[[-50, 70, -40, 50], [-80, -50, -70, -40]]], [(False, False)], [(False, False)], - [], # raise because of empty anchors + [], # raise because of empty anchors pytest.raises(ValueError), - ) + ), ], ) def test_line_zone_multiple_detections( @@ -437,9 +447,8 @@ def test_line_zone_multiple_detections( xyxy_sequence: List[List[List[int]]], expected_crossed_in: List[bool], expected_crossed_out: List[bool], - anchors: list[Position], - exception: Exception - + anchors: List[Position], + exception: Exception, ) -> None: """ Test LineZone with multiple detections. @@ -448,7 +457,9 @@ def test_line_zone_multiple_detections( direction) by a detection it is crossed by exactly all anchors from @anchors. """ with exception: - line_zone = LineZone(start=vector.start, end=vector.end, triggering_anchors=anchors) + line_zone = LineZone( + start=vector.start, end=vector.end, triggering_anchors=anchors + ) for i, bboxes in enumerate(xyxy_sequence): detections = mock_detections( xyxy=bboxes, @@ -457,4 +468,3 @@ def test_line_zone_multiple_detections( crossed_in, crossed_out = line_zone.trigger(detections) assert np.all(crossed_in == expected_crossed_in[i]) assert np.all(crossed_out == expected_crossed_out[i]) - From fb30bab0fd0d79849742f1d288d35eb6066e9f11 Mon Sep 17 00:00:00 2001 From: tc360950 Date: Fri, 24 May 2024 16:50:42 +0200 Subject: [PATCH 109/182] Vectorize line zone --- supervision/detection/line_zone.py | 98 +++++++++++++++++------------- 1 file changed, 55 insertions(+), 43 deletions(-) diff --git a/supervision/detection/line_zone.py b/supervision/detection/line_zone.py index 53d762a0..7df6dd2a 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -55,15 +55,15 @@ class LineZone: """ # noqa: E501 // docs def __init__( - self, - start: Point, - end: Point, - triggering_anchors: Iterable[Position] = ( - Position.TOP_LEFT, - Position.TOP_RIGHT, - Position.BOTTOM_LEFT, - Position.BOTTOM_RIGHT, - ), + self, + start: Point, + end: Point, + triggering_anchors: Iterable[Position] = ( + Position.TOP_LEFT, + Position.TOP_RIGHT, + Position.BOTTOM_LEFT, + Position.BOTTOM_RIGHT, + ), ): """ Args: @@ -147,31 +147,28 @@ class LineZone: ] ) + cross_products_1 = self._cross_product(all_anchors, self.limits[0]) + cross_products_2 = self._cross_product(all_anchors, self.limits[1]) + # anchor is in limits if it's on the same side of both limit vectors + in_limits = ~ np.logical_xor(cross_products_1 > 0, cross_products_2 > 0) + # Reduce array to find out if all anchors for a detection are within limits + in_limits = np.min(in_limits, axis=0) + + triggers = self._cross_product(all_anchors, self.vector) < 0 + max_triggers = np.max(triggers, axis=0) + min_triggers = np.min(triggers, axis=0) for i, tracker_id in enumerate(detections.tracker_id): if tracker_id is None: continue - box_anchors = [Point(x=x, y=y) for x, y in all_anchors[:, i, :]] - - in_limits = all( - [ - self.is_point_in_limits(point=anchor, limits=self.limits) - for anchor in box_anchors - ] - ) - - if not in_limits: + if not in_limits[i]: continue - triggers = [ - self.vector.cross_product(point=anchor) < 0 for anchor in box_anchors - ] - - if len(set(triggers)) == 2: + if min_triggers[i] != max_triggers[i]: + # One anchor lies to the left of the line whilst another lies to the right continue - tracker_state = triggers[0] - + tracker_state = max_triggers[i] if tracker_id not in self.tracker_state: self.tracker_state[tracker_id] = tracker_state continue @@ -189,21 +186,36 @@ class LineZone: return crossed_in, crossed_out + @staticmethod + def _cross_product(anchors: np.ndarray, vector: Vector) -> np.ndarray: + """ + Get array of cross products of each anchor with a vector. + Args: + anchors: Array of anchors of shape (number of anchors, detections, 2) + vector: Vector to calculate cross product with + + Returns: + Array of cross products of shape (number of anchors, detections) + """ + vector_at_zero = np.array([vector.end.x - vector.start.x, vector.end.y - vector.start.y]) + vector_start = np.array([vector.start.x, vector.start.y]) + return np.cross(vector_at_zero, anchors - vector_start) + class LineZoneAnnotator: def __init__( - self, - thickness: float = 2, - color: Color = Color.WHITE, - text_thickness: float = 2, - text_color: Color = Color.BLACK, - text_scale: float = 0.5, - text_offset: float = 1.5, - text_padding: int = 10, - custom_in_text: Optional[str] = None, - custom_out_text: Optional[str] = None, - display_in_count: bool = True, - display_out_count: bool = True, + self, + thickness: float = 2, + color: Color = Color.WHITE, + text_thickness: float = 2, + text_color: Color = Color.BLACK, + text_scale: float = 0.5, + text_offset: float = 1.5, + text_padding: int = 10, + custom_in_text: Optional[str] = None, + custom_out_text: Optional[str] = None, + display_in_count: bool = True, + display_out_count: bool = True, ): """ Initialize the LineCounterAnnotator object with default values. @@ -233,11 +245,11 @@ class LineZoneAnnotator: self.display_out_count: bool = display_out_count def _annotate_count( - self, - frame: np.ndarray, - center_text_anchor: Point, - text: str, - is_in_count: bool, + self, + frame: np.ndarray, + center_text_anchor: Point, + text: str, + is_in_count: bool, ) -> None: """This method is drawing the text on the frame. From 80feb2b12ff997a345f0a2c415b5a0debe67f120 Mon Sep 17 00:00:00 2001 From: tc360950 Date: Fri, 24 May 2024 17:43:02 +0200 Subject: [PATCH 110/182] Add comments --- supervision/detection/line_zone.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/supervision/detection/line_zone.py b/supervision/detection/line_zone.py index 7df6dd2a..4d6db6ef 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -154,7 +154,10 @@ class LineZone: # Reduce array to find out if all anchors for a detection are within limits in_limits = np.min(in_limits, axis=0) + # Calculate which anchors lie to the left of the line triggers = self._cross_product(all_anchors, self.vector) < 0 + # Reduce to find out if all anchors for a + # detection lie to the left (or right) of the line max_triggers = np.max(triggers, axis=0) min_triggers = np.min(triggers, axis=0) for i, tracker_id in enumerate(detections.tracker_id): From d23cc008f86d5a46aa9b39da7d8c29db56945e6f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 24 May 2024 16:02:29 +0000 Subject: [PATCH 111/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/detection/line_zone.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/supervision/detection/line_zone.py b/supervision/detection/line_zone.py index f85604c4..e01001c0 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -161,7 +161,7 @@ class LineZone: cross_products_1 = self._cross_product(all_anchors, self.limits[0]) cross_products_2 = self._cross_product(all_anchors, self.limits[1]) # anchor is in limits if it's on the same side of both limit vectors - in_limits = ~ np.logical_xor(cross_products_1 > 0, cross_products_2 > 0) + in_limits = ~np.logical_xor(cross_products_1 > 0, cross_products_2 > 0) # Reduce array to find out if all anchors for a detection are within limits in_limits = np.min(in_limits, axis=0) @@ -208,7 +208,9 @@ class LineZone: Returns: Array of cross products of shape (number of anchors, detections) """ - vector_at_zero = np.array([vector.end.x - vector.start.x, vector.end.y - vector.start.y]) + vector_at_zero = np.array( + [vector.end.x - vector.start.x, vector.end.y - vector.start.y] + ) vector_start = np.array([vector.start.x, vector.start.y]) return np.cross(vector_at_zero, anchors - vector_start) From ed5c26527a77f7b71072e0f34f86ec291bc91718 Mon Sep 17 00:00:00 2001 From: tc360950 Date: Fri, 24 May 2024 18:04:43 +0200 Subject: [PATCH 112/182] Code reformatting --- supervision/detection/line_zone.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/supervision/detection/line_zone.py b/supervision/detection/line_zone.py index f85604c4..e31cc6cd 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -176,7 +176,8 @@ class LineZone: continue if min_triggers[i] != max_triggers[i]: - # One anchor lies to the left of the line whilst another lies to the right + # One anchor lies to the left of the line + # whilst another lies to the right continue tracker_state = max_triggers[i] @@ -208,7 +209,9 @@ class LineZone: Returns: Array of cross products of shape (number of anchors, detections) """ - vector_at_zero = np.array([vector.end.x - vector.start.x, vector.end.y - vector.start.y]) + vector_at_zero = np.array( + [vector.end.x - vector.start.x, vector.end.y - vector.start.y] + ) vector_start = np.array([vector.start.x, vector.start.y]) return np.cross(vector_at_zero, anchors - vector_start) From 404be1612690a9e5645351c54e110ed64a9cabea Mon Sep 17 00:00:00 2001 From: tc360950 Date: Fri, 24 May 2024 18:14:18 +0200 Subject: [PATCH 113/182] Code reformatting --- test/detection/test_line_counter.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index 7e141277..cd184b73 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -350,7 +350,11 @@ def test_line_zone_single_detection_on_subset_of_anchors( @pytest.mark.parametrize( - "vector, xyxy_sequence, expected_crossed_in, expected_crossed_out, anchors, exception", + "vector," + "xyxy_sequence," + "expected_crossed_in," + "expected_crossed_out," + "anchors, exception", [ ( Vector( From 2c3fa279293b6b9f3f2c5cf74a644059495ea5c0 Mon Sep 17 00:00:00 2001 From: David Redo Date: Sun, 26 May 2024 20:14:09 +0200 Subject: [PATCH 114/182] add GHUM Skeleton enum --- supervision/keypoint/skeletons.py | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/supervision/keypoint/skeletons.py b/supervision/keypoint/skeletons.py index 6c110854..7822b309 100644 --- a/supervision/keypoint/skeletons.py +++ b/supervision/keypoint/skeletons.py @@ -25,6 +25,44 @@ class Skeleton(Enum): (17, 15), ] + GHUM = [ + (1, 2), + (1, 5), + (2, 3), + (3, 4), + (4, 8), + (5, 6), + (6, 7), + (7, 9), + (10, 11), + (12, 13), + (12, 14), + (12, 24), + (13, 15), + (13, 25), + (14, 16), + (15, 17), + (16, 18), + (15, 19), + (16, 22), + (17, 19), + (17, 21), + (17, 23), + (18, 20), + (19, 21), + (24, 25), + (24, 26), + (25, 27), + (26, 28), + (27, 29), + (28, 30), + (28, 32), + (29, 31), + (29, 33), + (30, 32), + (31, 33), + ] + SKELETONS_BY_EDGE_COUNT: Dict[int, Edges] = {} SKELETONS_BY_VERTEX_COUNT: Dict[int, Edges] = {} From 490a341d5dd97fed78df55fea671558f9bb0beb8 Mon Sep 17 00:00:00 2001 From: David Redo Date: Sun, 26 May 2024 20:14:57 +0200 Subject: [PATCH 115/182] add method in KeyPoints class --- supervision/keypoint/core.py | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index 87ad9579..06f7e88b 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -1,5 +1,6 @@ from __future__ import annotations +import math from contextlib import suppress from dataclasses import dataclass, field from typing import Any, Dict, Iterator, List, Optional, Tuple, Union @@ -185,6 +186,55 @@ class KeyPoints: data=data, ) + @classmethod + def from_mediapipe( + cls, mediapipe_results, resolution: Tuple[int, int] + ) -> KeyPoints: + """ + Creates a KeyPoints instance from a + [Pose landmark detection](https://ai.google.dev/edge/mediapipe/) inference result. + + Args: + mediapipe_results (mediapipe.python.solution_base.SolutionOutputs): + The output Results from Mediapipe + resolution (Tuple[int, int]): image resolution (w, h) since mediapipe only provides + normalized coordinates + + Returns: + KeyPoints: A new KeyPoints object. + + Example: + ```python + import cv2 + import mediapipe as mp + import supervision as sv + + image = cv2.imread() + pose = mp.solutions.pose.Pose(static_image_mode=True, min_detection_confidence=0.5) + results = pose.process(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) + keypoints = sv.KeyPoints.from_mediapipe(results) + ``` + """ + if mediapipe_results.pose_landmarks is None: + return cls.empty() + + prediction_xy = [] + prediction_confidence = [] + + for landmark in mediapipe_results.pose_landmarks.landmark: + prediction_xy.append( + [ + min(math.floor(landmark.x * resolution[0]), resolution[0] - 1), + min(math.floor(landmark.y * resolution[1]), resolution[1] - 1), + ] + ) + prediction_confidence.append(landmark.visibility) + + return cls( + xy=np.array([prediction_xy], dtype=np.float32), + confidence=np.array([prediction_confidence], dtype=np.float32), + ) + @classmethod def from_ultralytics(cls, ultralytics_results) -> KeyPoints: """ From 4fea0031ba36814ac3d43cf9161080d9430625d8 Mon Sep 17 00:00:00 2001 From: David Redo Date: Sun, 26 May 2024 21:45:59 +0200 Subject: [PATCH 116/182] Updated docstring comment due to line too large --- supervision/keypoint/core.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index 06f7e88b..a6122953 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -192,13 +192,14 @@ class KeyPoints: ) -> KeyPoints: """ Creates a KeyPoints instance from a - [Pose landmark detection](https://ai.google.dev/edge/mediapipe/) inference result. + [Pose landmark detection](https://ai.google.dev/edge/mediapipe/) + inference result. Args: mediapipe_results (mediapipe.python.solution_base.SolutionOutputs): The output Results from Mediapipe - resolution (Tuple[int, int]): image resolution (w, h) since mediapipe only provides - normalized coordinates + resolution (Tuple[int, int]): image resolution (w, h) since mediapipe + only provides normalized coordinates Returns: KeyPoints: A new KeyPoints object. @@ -209,8 +210,9 @@ class KeyPoints: import mediapipe as mp import supervision as sv + mp_pose = mp.solutions.pose image = cv2.imread() - pose = mp.solutions.pose.Pose(static_image_mode=True, min_detection_confidence=0.5) + pose = mp_pose.Pose(static_image_mode=True, min_detection_confidence=0.5) results = pose.process(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) keypoints = sv.KeyPoints.from_mediapipe(results) ``` From 530e1d01e152e45bd9f5bb37553f8bacbc6aeb75 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Mon, 27 May 2024 16:17:27 +0300 Subject: [PATCH 117/182] Address comments --- supervision/detection/core.py | 52 ++++++++++++++--------------------- test/detection/test_core.py | 4 +-- 2 files changed, 23 insertions(+), 33 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 069eaf09..f85d403d 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -1203,7 +1203,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( + merged_detections = merge_inner_detections_objects( unmerged_detections, threshold ) result.append(merged_detections) @@ -1211,7 +1211,7 @@ class Detections: return Detections.merge(result) -def _merge_inner_detection_object_pair( +def merge_inner_detection_object_pair( detections_1: Detections, detections_2: Detections ) -> Detections: """ @@ -1259,29 +1259,23 @@ def _merge_inner_detection_object_pair( if len(detections_1) != 1 or len(detections_2) != 1: raise ValueError("Both Detections should have exactly 1 detected object.") - _verify_fields_both_defined_or_none(detections_1, detections_2) + validate_fields_both_defined_or_none(detections_1, detections_2) + xyxy_1 = detections_1.xyxy[0] + xyxy_2 = detections_2.xyxy[0] if detections_1.confidence is None and detections_2.confidence is None: merged_confidence = None else: - area_det1 = (detections_1.xyxy[0][2] - detections_1.xyxy[0][0]) * ( - detections_1.xyxy[0][3] - detections_1.xyxy[0][1] - ) - area_det2 = (detections_2.xyxy[0][2] - detections_2.xyxy[0][0]) * ( - detections_2.xyxy[0][3] - detections_2.xyxy[0][1] - ) + detection_1_area = (xyxy_1[2] - xyxy_1[0]) * (xyxy_1[3] - xyxy_1[1]) + detections_2_area = (xyxy_2[2] - xyxy_2[0]) * (xyxy_2[3] - xyxy_2[1]) merged_confidence = ( - area_det1 * detections_1.confidence[0] - + area_det2 * detections_2.confidence[0] - ) / (area_det1 + area_det2) + detection_1_area * detections_1.confidence[0] + + detections_2_area * detections_2.confidence[0] + ) / (detection_1_area + detections_2_area) merged_confidence = np.array([merged_confidence]) - merged_x1, merged_y1 = np.minimum( - detections_1.xyxy[0][:2], detections_2.xyxy[0][:2] - ) - merged_x2, merged_y2 = np.maximum( - detections_1.xyxy[0][2:], detections_2.xyxy[0][2:] - ) + merged_x1, merged_y1 = np.minimum(xyxy_1[:2], xyxy_2[:2]) + merged_x2, merged_y2 = np.maximum(xyxy_1[2:], xyxy_2[2:]) merged_xyxy = np.array([[merged_x1, merged_y1, merged_x2, merged_y2]]) if detections_1.mask is None and detections_2.mask is None: @@ -1290,27 +1284,23 @@ def _merge_inner_detection_object_pair( merged_mask = np.logical_or(detections_1.mask, detections_2.mask) if detections_1.confidence is None and detections_2.confidence is None: - winning_det = detections_1 + winning_detection = detections_1 elif detections_1.confidence[0] >= detections_2.confidence[0]: - winning_det = detections_1 + winning_detection = detections_1 else: - winning_det = detections_2 - - winning_class_id = winning_det.class_id - winning_tracker_id = winning_det.tracker_id - winning_data = winning_det.data + winning_detection = detections_2 return Detections( xyxy=merged_xyxy, mask=merged_mask, confidence=merged_confidence, - class_id=winning_class_id, - tracker_id=winning_tracker_id, - data=winning_data, + class_id=winning_detection.class_id, + tracker_id=winning_detection.tracker_id, + data=winning_detection.data, ) -def _merge_inner_detections_objects( +def merge_inner_detections_objects( detections: List[Detections], threshold=0.5 ) -> Detections: """ @@ -1326,11 +1316,11 @@ def _merge_inner_detections_objects( box_iou = box_iou_batch(detections_1.xyxy, detections_2.xyxy)[0] if box_iou < threshold: break - detections_1 = _merge_inner_detection_object_pair(detections_1, detections_2) + detections_1 = merge_inner_detection_object_pair(detections_1, detections_2) return detections_1 -def _verify_fields_both_defined_or_none( +def validate_fields_both_defined_or_none( detections_1: Detections, detections_2: Detections ) -> None: """ diff --git a/test/detection/test_core.py b/test/detection/test_core.py index dc58c9e8..af1d5876 100644 --- a/test/detection/test_core.py +++ b/test/detection/test_core.py @@ -5,7 +5,7 @@ from typing import List, Optional, Union import numpy as np import pytest -from supervision.detection.core import Detections, _merge_inner_detection_object_pair +from supervision.detection.core import Detections, merge_inner_detection_object_pair from supervision.geometry.core import Position PREDICTIONS = np.array( @@ -588,5 +588,5 @@ def test_merge_inner_detection_object_pair( exception: Exception, ): with exception: - result = _merge_inner_detection_object_pair(detection_1, detection_2) + result = merge_inner_detection_object_pair(detection_1, detection_2) assert result == expected_result From 2ee9e08446a071c50ff8acf000f80fdc0bb6c0a9 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Mon, 27 May 2024 16:21:40 +0300 Subject: [PATCH 118/182] Renamed to group_overlapping_boxes --- supervision/detection/utils.py | 6 +++--- test/detection/test_utils.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 4beea2ed..74726995 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -275,7 +275,7 @@ def box_non_max_suppression( return keep[sort_index.argsort()] -def _box_non_max_merge_all( +def group_overlapping_boxes( predictions: npt.NDArray[np.float64], iou_threshold: float = 0.5 ) -> List[List[int]]: """ @@ -338,13 +338,13 @@ def box_non_max_merge( Each group may have 1 or more elements. """ if predictions.shape[1] == 5: - return _box_non_max_merge_all(predictions, iou_threshold) + return group_overlapping_boxes(predictions, iou_threshold) 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 = _box_non_max_merge_all( + merge_class_groups = group_overlapping_boxes( predictions[curr_indices], iou_threshold ) diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index 9a1fa8c9..b62faa61 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -6,12 +6,12 @@ import pytest from supervision.config import CLASS_NAME_DATA_FIELD from supervision.detection.utils import ( - _box_non_max_merge_all, box_non_max_suppression, calculate_masks_centroids, clip_boxes, filter_polygons_by_area, get_data_item, + group_overlapping_boxes, mask_non_max_suppression, merge_data, move_boxes, @@ -241,14 +241,14 @@ def test_box_non_max_suppression( ), # confidence ], ) -def test_box_non_max_merge( +def test_group_overlapping_boxes( predictions: np.ndarray, iou_threshold: float, expected_result: List[List[int]], exception: Exception, ) -> None: with exception: - result = _box_non_max_merge_all( + result = group_overlapping_boxes( predictions=predictions, iou_threshold=iou_threshold ) From c1ebb81d475d180d0c6d6dcbdbb33f000bdcaa27 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 17:42:51 +0000 Subject: [PATCH 119/182] =?UTF-8?q?chore(pre=5Fcommit):=20=E2=AC=86=20pre?= =?UTF-8?q?=5Fcommit=20autoupdate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.4.4 → v0.4.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.4...v0.4.5) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9465c2af..0c47f2b6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.4 + rev: v0.4.5 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 4a365fb907ac4f5efa8623664e4c05273751f372 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Mon, 27 May 2024 21:31:25 +0200 Subject: [PATCH 120/182] add `LMM` to `__init__.py` --- supervision/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/supervision/__init__.py b/supervision/__init__.py index abe63390..715b9085 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -39,6 +39,7 @@ from supervision.dataset.utils import mask_to_rle, rle_to_mask from supervision.detection.annotate import BoxAnnotator from supervision.detection.core import Detections from supervision.detection.line_zone import LineZone, LineZoneAnnotator +from supervision.detection.lmm import LMM from supervision.detection.tools.csv_sink import CSVSink from supervision.detection.tools.inference_slicer import InferenceSlicer from supervision.detection.tools.json_sink import JSONSink From e7dfc04fd363e54595df13769910749865b18c5c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 May 2024 00:31:55 +0000 Subject: [PATCH 121/182] :arrow_up: Bump mkdocs-material from 9.5.24 to 9.5.25 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.5.24 to 9.5.25. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.24...9.5.25) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0a9276f8..e5dd7c74 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2199,13 +2199,13 @@ pygments = ">2.12.0" [[package]] name = "mkdocs-material" -version = "9.5.24" +version = "9.5.25" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.24-py3-none-any.whl", hash = "sha256:e12cd75954c535b61e716f359cf2a5056bf4514889d17161fdebd5df4b0153c6"}, - {file = "mkdocs_material-9.5.24.tar.gz", hash = "sha256:02d5aaba0ee755e707c3ef6e748f9acb7b3011187c0ea766db31af8905078a34"}, + {file = "mkdocs_material-9.5.25-py3-none-any.whl", hash = "sha256:68fdab047a0b9bfbefe79ce267e8a7daaf5128bcf7867065fcd201ee335fece1"}, + {file = "mkdocs_material-9.5.25.tar.gz", hash = "sha256:d0662561efb725b712207e0ee01f035ca15633f29a64628e24f01ec99d7078f4"}, ] [package.dependencies] From f0e88b1982f8a562cf480bdc5e9263aacda8bc88 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Tue, 28 May 2024 09:52:46 +0300 Subject: [PATCH 122/182] Select overlap filtering strategy --- supervision/__init__.py | 1 + .../detection/tools/inference_slicer.py | 26 ++++++++++++++----- supervision/detection/utils.py | 17 ++++++++++++ 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/supervision/__init__.py b/supervision/__init__.py index 1a46226f..6cd5f9ff 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -46,6 +46,7 @@ from supervision.detection.tools.json_sink import JSONSink from supervision.detection.tools.polygon_zone import PolygonZone, PolygonZoneAnnotator from supervision.detection.tools.smoother import DetectionsSmoother from supervision.detection.utils import ( + OverlapFilter, box_iou_batch, box_non_max_merge, box_non_max_suppression, diff --git a/supervision/detection/tools/inference_slicer.py b/supervision/detection/tools/inference_slicer.py index 82551434..3302b139 100644 --- a/supervision/detection/tools/inference_slicer.py +++ b/supervision/detection/tools/inference_slicer.py @@ -1,10 +1,11 @@ +import warnings from concurrent.futures import ThreadPoolExecutor, as_completed from typing import Callable, Optional, Tuple import numpy as np from supervision.detection.core import Detections -from supervision.detection.utils import move_boxes, move_masks +from supervision.detection.utils import OverlapFilter, move_boxes, move_masks from supervision.utils.image import crop_image @@ -50,8 +51,10 @@ class InferenceSlicer: `(width, height)`. overlap_ratio_wh (Tuple[float, float]): Overlap ratio between consecutive slices in the format `(width_ratio, height_ratio)`. - iou_threshold (Optional[float]): Intersection over Union (IoU) threshold - used for non-max suppression. + overlap_filter (OverlapFilter): Strategy for + filtering or merging overlapping detections in slices. + iou_threshold (float): Intersection over Union (IoU) threshold + used when filtering by overlap. callback (Callable): A function that performs inference on a given image slice and returns detections. thread_workers (int): Number of threads for parallel execution. @@ -68,12 +71,14 @@ class InferenceSlicer: callback: Callable[[np.ndarray], Detections], slice_wh: Tuple[int, int] = (320, 320), overlap_ratio_wh: Tuple[float, float] = (0.2, 0.2), - iou_threshold: Optional[float] = 0.5, + overlap_filter: OverlapFilter = OverlapFilter.NON_MAX_SUPPRESSION, + iou_threshold: float = 0.5, thread_workers: int = 1, ): self.slice_wh = slice_wh self.overlap_ratio_wh = overlap_ratio_wh self.iou_threshold = iou_threshold + self.overlap_filter = overlap_filter self.callback = callback self.thread_workers = thread_workers @@ -124,9 +129,16 @@ class InferenceSlicer: for future in as_completed(futures): detections_list.append(future.result()) - return Detections.merge(detections_list=detections_list).with_nms( - threshold=self.iou_threshold - ) + merged = Detections.merge(detections_list=detections_list) + if self.overlap_filter == OverlapFilter.NONE: + return merged + elif self.overlap_filter == OverlapFilter.NON_MAX_SUPPRESSION: + return merged.with_nms(threshold=self.iou_threshold) + elif self.overlap_filter == OverlapFilter.NON_MAX_MERGE: + return merged.with_nmm(threshold=self.iou_threshold) + else: + warnings.warn(f"Invalid overlap filter strategy: {self.overlap_filter}") + return merged def _run_callback(self, image, offset) -> Detections: """ diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 1ca48791..86b730be 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -1,3 +1,4 @@ +from enum import Enum from itertools import chain from typing import Dict, List, Optional, Tuple, Union @@ -1056,3 +1057,19 @@ def contains_multiple_segments( mask_uint8, labels, connectivity=connectivity ) return number_of_labels > 2 + + +class OverlapFilter(Enum): + """ + Enum specifying the strategy for filtering overlapping detections. + + Attributes: + NONE: Do not filter detections based on overlap. + NON_MAX_SUPPRESSION: Filter detections using non-max suppression. + NON_MAX_MERGE: Merge detections with non-max-merging instead of + discarding them. + """ + + NONE = "none" + NON_MAX_SUPPRESSION = "non_max_suppression" + NON_MAX_MERGE = "non_max_merge" From 1d133975038c5d059f01a797f558216645f5c20d Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Tue, 28 May 2024 10:38:54 +0300 Subject: [PATCH 123/182] Dynamically select Detections fields, not hardcoded --- supervision/detection/core.py | 4 ++-- supervision/utils/internal.py | 42 ++++++++++++++++++++++++++++++++++- test/utils/test_internal.py | 38 +++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 test/utils/test_internal.py diff --git a/supervision/detection/core.py b/supervision/detection/core.py index be610482..f93aed1c 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -23,7 +23,7 @@ from supervision.detection.utils import ( xywh_to_xyxy, ) from supervision.geometry.core import Position -from supervision.utils.internal import deprecated +from supervision.utils.internal import deprecated, get_instance_variables from supervision.validators import validate_detections_fields @@ -1379,7 +1379,7 @@ def validate_fields_both_defined_or_none( Raises: ValueError: If one field is None and the other is not, for any of the fields. """ - attributes = ["mask", "confidence", "class_id", "tracker_id"] + attributes = get_instance_variables(detections_1) for attribute in attributes: value_1 = getattr(detections_1, attribute) value_2 = getattr(detections_2, attribute) diff --git a/supervision/utils/internal.py b/supervision/utils/internal.py index 978a1448..1e84da61 100644 --- a/supervision/utils/internal.py +++ b/supervision/utils/internal.py @@ -1,7 +1,8 @@ import functools +import inspect import os import warnings -from typing import Callable +from typing import Any, Callable, Set class SupervisionWarnings(Warning): @@ -141,3 +142,42 @@ class classproperty(property): The result of calling the function stored in 'fget' with 'owner_cls'. """ return self.fget(owner_cls) + + +def get_instance_variables(cls: Any, include_properties=False) -> Set[str]: + """ + Get the non-private variables of a class or instance. + Some variables are only during initialization, so passing an instance + is more reliable. + + Args: + cls (Any): The class or instance + include_properties (bool): Whether to include properties in the result + + Usage: + ```python + detections = Detections(xyxy=np.array([1,2,3,4])) + variables = get_class_variables(detections) + # Returns ["xyxy", "mask", "confidence", ..., "data"] + ``` + """ + fields = set( + ( + name + for name, val in inspect.getmembers(cls) + if not name.startswith("__") and not callable(val) + ) + ) + + if not include_properties: + class_type = cls if isinstance(cls, type) else type(cls) + properties = set( + ( + name + for name, val in inspect.getmembers(class_type) + if isinstance(val, property) + ) + ) + fields -= properties + + return fields diff --git a/test/utils/test_internal.py b/test/utils/test_internal.py new file mode 100644 index 00000000..d268a837 --- /dev/null +++ b/test/utils/test_internal.py @@ -0,0 +1,38 @@ +import pytest +from contextlib import ExitStack as DoesNotRaise +from supervision.detection.core import Detections +from supervision.utils.internal import get_instance_variables + + +@pytest.mark.parametrize( + "input_obj, include_properties, expected, exception", + [ + ( + Detections, + False, + {"class_id", "confidence", "mask", "tracker_id"}, + DoesNotRaise() + ), + ( + Detections.empty(), + False, + {"xyxy", "class_id", "confidence", "mask", "tracker_id", "data"}, + DoesNotRaise() + ), + ( + Detections, + True, + {"class_id", "confidence", "mask", "tracker_id", "area", "box_area"}, + DoesNotRaise() + ), + ( + Detections.empty(), + True, + {"xyxy", "class_id", "confidence", "mask", "tracker_id", "data", "area", "box_area"}, + DoesNotRaise() + ), + ], +) +def test_get_instance_variables(input_obj, include_properties, expected, exception) -> None: + result = get_instance_variables(input_obj, include_properties=include_properties) + assert result == expected From 3c3a0792f3c97e431f361b78d858e696f81b46e4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 28 May 2024 07:42:31 +0000 Subject: [PATCH 124/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/utils/test_internal.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/test/utils/test_internal.py b/test/utils/test_internal.py index d268a837..ecb1ff29 100644 --- a/test/utils/test_internal.py +++ b/test/utils/test_internal.py @@ -1,5 +1,7 @@ -import pytest from contextlib import ExitStack as DoesNotRaise + +import pytest + from supervision.detection.core import Detections from supervision.utils.internal import get_instance_variables @@ -11,28 +13,39 @@ from supervision.utils.internal import get_instance_variables Detections, False, {"class_id", "confidence", "mask", "tracker_id"}, - DoesNotRaise() + DoesNotRaise(), ), ( Detections.empty(), False, {"xyxy", "class_id", "confidence", "mask", "tracker_id", "data"}, - DoesNotRaise() + DoesNotRaise(), ), ( Detections, True, {"class_id", "confidence", "mask", "tracker_id", "area", "box_area"}, - DoesNotRaise() + DoesNotRaise(), ), ( Detections.empty(), True, - {"xyxy", "class_id", "confidence", "mask", "tracker_id", "data", "area", "box_area"}, - DoesNotRaise() + { + "xyxy", + "class_id", + "confidence", + "mask", + "tracker_id", + "data", + "area", + "box_area", + }, + DoesNotRaise(), ), ], ) -def test_get_instance_variables(input_obj, include_properties, expected, exception) -> None: +def test_get_instance_variables( + input_obj, include_properties, expected, exception +) -> None: result = get_instance_variables(input_obj, include_properties=include_properties) assert result == expected From a6c995c9a97113b031951037ab8a49c76a411afa Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Tue, 28 May 2024 11:14:29 +0300 Subject: [PATCH 125/182] More tests for `get_instance_variables` --- test/utils/test_internal.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/utils/test_internal.py b/test/utils/test_internal.py index ecb1ff29..f5dfe429 100644 --- a/test/utils/test_internal.py +++ b/test/utils/test_internal.py @@ -1,5 +1,6 @@ from contextlib import ExitStack as DoesNotRaise +import numpy as np import pytest from supervision.detection.core import Detections @@ -42,6 +43,39 @@ from supervision.utils.internal import get_instance_variables }, DoesNotRaise(), ), + ( + Detections(xyxy=np.array([[1, 2, 3, 4]])), + False, + { + "xyxy", + "class_id", + "confidence", + "mask", + "tracker_id", + "data", + }, + DoesNotRaise(), + ), + ( + Detections( + xyxy=np.array([[1, 2, 3, 4], [5, 6, 7, 8]]), + class_id=np.array([1, 2]), + confidence=np.array([0.1, 0.2]), + mask=np.array([[[1]], [[2]]]), + tracker_id=np.array([1, 2]), + data={"key_1": [1, 2], "key_2": [3, 4]}, + ), + False, + { + "xyxy", + "class_id", + "confidence", + "mask", + "tracker_id", + "data", + }, + DoesNotRaise(), + ), ], ) def test_get_instance_variables( From f24c6f38a059d22586bf56927742b8b78b934de0 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Tue, 28 May 2024 14:09:38 +0300 Subject: [PATCH 126/182] get_instance_variables only accepts instance, more tests --- supervision/utils/internal.py | 18 ++--- test/utils/test_internal.py | 119 ++++++++++++++++++++++++++++++---- 2 files changed, 117 insertions(+), 20 deletions(-) diff --git a/supervision/utils/internal.py b/supervision/utils/internal.py index 1e84da61..b773e6e6 100644 --- a/supervision/utils/internal.py +++ b/supervision/utils/internal.py @@ -144,14 +144,12 @@ class classproperty(property): return self.fget(owner_cls) -def get_instance_variables(cls: Any, include_properties=False) -> Set[str]: +def get_instance_variables(instance: Any, include_properties=False) -> Set[str]: """ - Get the non-private variables of a class or instance. - Some variables are only during initialization, so passing an instance - is more reliable. + Get the public variables of a class instance. Args: - cls (Any): The class or instance + instance (Any): The class or instance include_properties (bool): Whether to include properties in the result Usage: @@ -161,20 +159,22 @@ def get_instance_variables(cls: Any, include_properties=False) -> Set[str]: # Returns ["xyxy", "mask", "confidence", ..., "data"] ``` """ + if isinstance(instance, type): + raise ValueError("Only class instances are supported, not classes.") + fields = set( ( name - for name, val in inspect.getmembers(cls) - if not name.startswith("__") and not callable(val) + for name, val in inspect.getmembers(instance) + if not callable(val) and not name.startswith("_") ) ) if not include_properties: - class_type = cls if isinstance(cls, type) else type(cls) properties = set( ( name - for name, val in inspect.getmembers(class_type) + for name, val in inspect.getmembers(instance.__class__) if isinstance(val, property) ) ) diff --git a/test/utils/test_internal.py b/test/utils/test_internal.py index f5dfe429..b3aedad7 100644 --- a/test/utils/test_internal.py +++ b/test/utils/test_internal.py @@ -1,4 +1,6 @@ from contextlib import ExitStack as DoesNotRaise +from dataclasses import dataclass +from typing import Any, Set import numpy as np import pytest @@ -7,14 +9,106 @@ from supervision.detection.core import Detections from supervision.utils.internal import get_instance_variables +class MockClass: + def __init__(self): + self.public = 0 + self._protected = 1 + self.__private = 2 + + def public_method(self): + pass + + def _protected_method(self): + pass + + def __private_method(self): + pass + + @property + def public_property(self): + return 0 + + @property + def _protected_property(self): + return 1 + + @property + def __private_property(self): + return 2 + + +@dataclass +class MockDataclass: + public: int = 0 + _protected: int = 1 + __private: int = 2 + + def public_method(self): + pass + + def _protected_method(self): + pass + + def __private_method(self): + pass + + @property + def public_property(self): + return 0 + + @property + def _protected_property(self): + return 1 + + @property + def __private_property(self): + return 2 + + @pytest.mark.parametrize( "input_obj, include_properties, expected, exception", [ + ( + MockClass, + False, + None, + pytest.raises(ValueError), + ), + ( + MockClass(), + False, + {"public"}, + DoesNotRaise(), + ), + ( + MockClass(), + True, + {"public", "public_property"}, + DoesNotRaise(), + ), + ( + MockDataclass(), + False, + {"public"}, + DoesNotRaise(), + ), + ( + MockDataclass(), + True, + {"public", "public_property"}, + DoesNotRaise(), + ), ( Detections, False, - {"class_id", "confidence", "mask", "tracker_id"}, - DoesNotRaise(), + None, + pytest.raises(ValueError), + ), + ( + Detections, + True, + None, + pytest.raises(ValueError), ), ( Detections.empty(), @@ -22,12 +116,6 @@ from supervision.utils.internal import get_instance_variables {"xyxy", "class_id", "confidence", "mask", "tracker_id", "data"}, DoesNotRaise(), ), - ( - Detections, - True, - {"class_id", "confidence", "mask", "tracker_id", "area", "box_area"}, - DoesNotRaise(), - ), ( Detections.empty(), True, @@ -76,10 +164,19 @@ from supervision.utils.internal import get_instance_variables }, DoesNotRaise(), ), + ( + Detections.empty(), + False, + {"xyxy", "class_id", "confidence", "mask", "tracker_id", "data"}, + DoesNotRaise(), + ), ], ) def test_get_instance_variables( - input_obj, include_properties, expected, exception + input_obj: Any, include_properties: bool, expected: Set[str], exception: Exception ) -> None: - result = get_instance_variables(input_obj, include_properties=include_properties) - assert result == expected + with exception: + result = get_instance_variables( + input_obj, include_properties=include_properties + ) + assert result == expected From c0486b7f947457e08d068440b78b54decc6666c9 Mon Sep 17 00:00:00 2001 From: LinasKo Date: Tue, 28 May 2024 14:46:55 +0300 Subject: [PATCH 127/182] Remove docstrings The class names were descriptive enough --- test/detection/test_line_counter.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index cd184b73..d9c21f15 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -250,12 +250,6 @@ def test_line_zone_single_detection( expected_crossed_in: List[bool], expected_crossed_out: List[bool], ) -> None: - """ - Test LineZone with single detection. - The detection is represented by a sequence of xyxy bboxes which represent - subsequent positions of the detected object. If a line is crossed (in either - direction) it is crossed by all anchors simultaneously. - """ line_zone = LineZone(start=vector.start, end=vector.end) for i, bbox in enumerate(xyxy_sequence): detections = mock_detections( @@ -311,14 +305,6 @@ def test_line_zone_single_detection_on_subset_of_anchors( expected_crossed_out: List[bool], crossing_anchors: List[Position], ) -> None: - """ - Test LineZone with single detection which crosses the line with only a subset of - anchors. - The detection is represented by a sequence of xyxy bboxes which represent - subsequent positions of the detected object. The line is crossed by only a subset - of anchors - this subset is given by @crossing_anchors. - """ - def powerset(s): return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1)) @@ -454,12 +440,6 @@ def test_line_zone_multiple_detections( anchors: List[Position], exception: Exception, ) -> None: - """ - Test LineZone with multiple detections. - A detection is represented by a sequence of xyxy bboxes which represent - subsequent positions of the detected object. If a line is crossed (in either - direction) by a detection it is crossed by exactly all anchors from @anchors. - """ with exception: line_zone = LineZone( start=vector.start, end=vector.end, triggering_anchors=anchors From d59467b4f42b56efe19c36a80c5ef9b5cae3b9dd Mon Sep 17 00:00:00 2001 From: LinasKo Date: Tue, 28 May 2024 15:12:48 +0300 Subject: [PATCH 128/182] Add anchor check and tests to polygon zone --- supervision/detection/line_zone.py | 4 ++-- supervision/detection/tools/polygon_zone.py | 2 ++ test/detection/test_polygonzone.py | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/supervision/detection/line_zone.py b/supervision/detection/line_zone.py index 761d27c0..45d4c1ed 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -82,8 +82,8 @@ class LineZone: self.tracker_state: Dict[str, bool] = {} self.in_count: int = 0 self.out_count: int = 0 - self.triggering_anchors = list(triggering_anchors) - if not self.triggering_anchors: + self.triggering_anchors = triggering_anchors + if not list(self.triggering_anchors): raise ValueError("Triggering anchors cannot be empty.") @staticmethod diff --git a/supervision/detection/tools/polygon_zone.py b/supervision/detection/tools/polygon_zone.py index a1997212..f1c48f94 100644 --- a/supervision/detection/tools/polygon_zone.py +++ b/supervision/detection/tools/polygon_zone.py @@ -54,6 +54,8 @@ class PolygonZone: self.polygon = polygon.astype(int) self.triggering_anchors = triggering_anchors + if not list(self.triggering_anchors): + raise ValueError("Triggering anchors cannot be empty.") self.current_count = 0 diff --git a/test/detection/test_polygonzone.py b/test/detection/test_polygonzone.py index 1a86a45b..ed899615 100644 --- a/test/detection/test_polygonzone.py +++ b/test/detection/test_polygonzone.py @@ -92,3 +92,19 @@ def test_polygon_zone_trigger( with exception: in_zone = polygon_zone.trigger(detections) assert np.all(in_zone == expected_results) + + +@pytest.mark.parametrize( + "polygon, triggering_anchors, exception", + [ + (POLYGON, [sv.Position.CENTER], DoesNotRaise()), + ( + POLYGON, + [], + pytest.raises(ValueError), + ), + ], +) +def test_polygon_zone_initialization(polygon, triggering_anchors, exception): + with exception: + sv.PolygonZone(polygon, FRAME_RESOLUTION, triggering_anchors=triggering_anchors) From ba2b60ffb4a4d8df40f63ab29139978e50db1ca5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 May 2024 01:04:41 +0000 Subject: [PATCH 129/182] :arrow_up: Bump ipywidgets from 8.1.2 to 8.1.3 Bumps [ipywidgets](https://github.com/jupyter-widgets/ipywidgets) from 8.1.2 to 8.1.3. - [Release notes](https://github.com/jupyter-widgets/ipywidgets/releases) - [Commits](https://github.com/jupyter-widgets/ipywidgets/compare/8.1.2...8.1.3) --- updated-dependencies: - dependency-name: ipywidgets dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/poetry.lock b/poetry.lock index e5dd7c74..3187955d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1253,21 +1253,21 @@ test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pa [[package]] name = "ipywidgets" -version = "8.1.2" +version = "8.1.3" description = "Jupyter interactive widgets" optional = false python-versions = ">=3.7" files = [ - {file = "ipywidgets-8.1.2-py3-none-any.whl", hash = "sha256:bbe43850d79fb5e906b14801d6c01402857996864d1e5b6fa62dd2ee35559f60"}, - {file = "ipywidgets-8.1.2.tar.gz", hash = "sha256:d0b9b41e49bae926a866e613a39b0f0097745d2b9f1f3dd406641b4a57ec42c9"}, + {file = "ipywidgets-8.1.3-py3-none-any.whl", hash = "sha256:efafd18f7a142248f7cb0ba890a68b96abd4d6e88ddbda483c9130d12667eaf2"}, + {file = "ipywidgets-8.1.3.tar.gz", hash = "sha256:f5f9eeaae082b1823ce9eac2575272952f40d748893972956dc09700a6392d9c"}, ] [package.dependencies] comm = ">=0.1.3" ipython = ">=6.1.0" -jupyterlab-widgets = ">=3.0.10,<3.1.0" +jupyterlab-widgets = ">=3.0.11,<3.1.0" traitlets = ">=4.3.1" -widgetsnbextension = ">=4.0.10,<4.1.0" +widgetsnbextension = ">=4.0.11,<4.1.0" [package.extras] test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"] @@ -1638,13 +1638,13 @@ test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-v [[package]] name = "jupyterlab-widgets" -version = "3.0.10" +version = "3.0.11" description = "Jupyter interactive widgets for JupyterLab" optional = false python-versions = ">=3.7" files = [ - {file = "jupyterlab_widgets-3.0.10-py3-none-any.whl", hash = "sha256:dd61f3ae7a5a7f80299e14585ce6cf3d6925a96c9103c978eda293197730cb64"}, - {file = "jupyterlab_widgets-3.0.10.tar.gz", hash = "sha256:04f2ac04976727e4f9d0fa91cdc2f1ab860f965e504c29dbd6a65c882c9d04c0"}, + {file = "jupyterlab_widgets-3.0.11-py3-none-any.whl", hash = "sha256:78287fd86d20744ace330a61625024cf5521e1c012a352ddc0a3cdc2348becd0"}, + {file = "jupyterlab_widgets-3.0.11.tar.gz", hash = "sha256:dd5ac679593c969af29c9bed054c24f26842baa51352114736756bc035deee27"}, ] [[package]] @@ -4227,13 +4227,13 @@ test = ["pytest (>=6.0.0)", "setuptools (>=65)"] [[package]] name = "widgetsnbextension" -version = "4.0.10" +version = "4.0.11" description = "Jupyter interactive widgets for Jupyter Notebook" optional = false python-versions = ">=3.7" files = [ - {file = "widgetsnbextension-4.0.10-py3-none-any.whl", hash = "sha256:d37c3724ec32d8c48400a435ecfa7d3e259995201fbefa37163124a9fcb393cc"}, - {file = "widgetsnbextension-4.0.10.tar.gz", hash = "sha256:64196c5ff3b9a9183a8e699a4227fb0b7002f252c814098e66c4d1cd0644688f"}, + {file = "widgetsnbextension-4.0.11-py3-none-any.whl", hash = "sha256:55d4d6949d100e0d08b94948a42efc3ed6dfdc0e9468b2c4b128c9a2ce3a7a36"}, + {file = "widgetsnbextension-4.0.11.tar.gz", hash = "sha256:8b22a8f1910bfd188e596fe7fc05dcbd87e810c8a4ba010bdb3da86637398474"}, ] [[package]] From 8fb843ec8efb64efc26343aa61d228ee63219e09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 May 2024 01:10:56 +0000 Subject: [PATCH 130/182] :arrow_up: Bump ruff from 0.4.5 to 0.4.6 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.5 to 0.4.6. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.4.5...v0.4.6) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/poetry.lock b/poetry.lock index e5dd7c74..e5c1a29a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3662,28 +3662,28 @@ files = [ [[package]] name = "ruff" -version = "0.4.5" +version = "0.4.6" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8f58e615dec58b1a6b291769b559e12fdffb53cc4187160a2fc83250eaf54e96"}, - {file = "ruff-0.4.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:84dd157474e16e3a82745d2afa1016c17d27cb5d52b12e3d45d418bcc6d49264"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25f483ad9d50b00e7fd577f6d0305aa18494c6af139bce7319c68a17180087f4"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63fde3bf6f3ad4e990357af1d30e8ba2730860a954ea9282c95fc0846f5f64af"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e3ba4620dee27f76bbcad97067766026c918ba0f2d035c2fc25cbdd04d9c97"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:441dab55c568e38d02bbda68a926a3d0b54f5510095c9de7f95e47a39e0168aa"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1169e47e9c4136c997f08f9857ae889d614c5035d87d38fda9b44b4338909cdf"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:755ac9ac2598a941512fc36a9070a13c88d72ff874a9781493eb237ab02d75df"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4b02a65985be2b34b170025a8b92449088ce61e33e69956ce4d316c0fe7cce0"}, - {file = "ruff-0.4.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:75a426506a183d9201e7e5664de3f6b414ad3850d7625764106f7b6d0486f0a1"}, - {file = "ruff-0.4.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6e1b139b45e2911419044237d90b60e472f57285950e1492c757dfc88259bb06"}, - {file = "ruff-0.4.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a6f29a8221d2e3d85ff0c7b4371c0e37b39c87732c969b4d90f3dad2e721c5b1"}, - {file = "ruff-0.4.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d6ef817124d72b54cc923f3444828ba24fa45c3164bc9e8f1813db2f3d3a8a11"}, - {file = "ruff-0.4.5-py3-none-win32.whl", hash = "sha256:aed8166c18b1a169a5d3ec28a49b43340949e400665555b51ee06f22813ef062"}, - {file = "ruff-0.4.5-py3-none-win_amd64.whl", hash = "sha256:b0b03c619d2b4350b4a27e34fd2ac64d0dabe1afbf43de57d0f9d8a05ecffa45"}, - {file = "ruff-0.4.5-py3-none-win_arm64.whl", hash = "sha256:9d15de3425f53161b3f5a5658d4522e4eee5ea002bf2ac7aa380743dd9ad5fba"}, - {file = "ruff-0.4.5.tar.gz", hash = "sha256:286eabd47e7d4d521d199cab84deca135557e6d1e0f0d01c29e757c3cb151b54"}, + {file = "ruff-0.4.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ef995583a038cd4a7edf1422c9e19118e2511b8ba0b015861b4abd26ec5367c5"}, + {file = "ruff-0.4.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:602ebd7ad909eab6e7da65d3c091547781bb06f5f826974a53dbe563d357e53c"}, + {file = "ruff-0.4.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f9ced5cbb7510fd7525448eeb204e0a22cabb6e99a3cb160272262817d49786"}, + {file = "ruff-0.4.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04a80acfc862e0e1630c8b738e70dcca03f350bad9e106968a8108379e12b31f"}, + {file = "ruff-0.4.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be47700ecb004dfa3fd4dcdddf7322d4e632de3c06cd05329d69c45c0280e618"}, + {file = "ruff-0.4.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1ff930d6e05f444090a0139e4e13e1e2e1f02bd51bb4547734823c760c621e79"}, + {file = "ruff-0.4.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f13410aabd3b5776f9c5699f42b37a3a348d65498c4310589bc6e5c548dc8a2f"}, + {file = "ruff-0.4.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0cf5cc02d3ae52dfb0c8a946eb7a1d6ffe4d91846ffc8ce388baa8f627e3bd50"}, + {file = "ruff-0.4.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea3424793c29906407e3cf417f28fc33f689dacbbadfb52b7e9a809dd535dcef"}, + {file = "ruff-0.4.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1fa8561489fadf483ffbb091ea94b9c39a00ed63efacd426aae2f197a45e67fc"}, + {file = "ruff-0.4.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4d5b914818d8047270308fe3e85d9d7f4a31ec86c6475c9f418fbd1624d198e0"}, + {file = "ruff-0.4.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4f02284335c766678778475e7698b7ab83abaf2f9ff0554a07b6f28df3b5c259"}, + {file = "ruff-0.4.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3a6a0a4f4b5f54fff7c860010ab3dd81425445e37d35701a965c0248819dde7a"}, + {file = "ruff-0.4.6-py3-none-win32.whl", hash = "sha256:9018bf59b3aa8ad4fba2b1dc0299a6e4e60a4c3bc62bbeaea222679865453062"}, + {file = "ruff-0.4.6-py3-none-win_amd64.whl", hash = "sha256:a769ae07ac74ff1a019d6bd529426427c3e30d75bdf1e08bb3d46ac8f417326a"}, + {file = "ruff-0.4.6-py3-none-win_arm64.whl", hash = "sha256:735a16407a1a8f58e4c5b913ad6102722e80b562dd17acb88887685ff6f20cf6"}, + {file = "ruff-0.4.6.tar.gz", hash = "sha256:a797a87da50603f71e6d0765282098245aca6e3b94b7c17473115167d8dfb0b7"}, ] [[package]] From c98698d44242d5dfe08f622be7da3cee09d42674 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 29 May 2024 11:08:17 +0300 Subject: [PATCH 131/182] get_instance_variables: test dataclass fields --- supervision/utils/internal.py | 4 ++-- test/utils/test_internal.py | 23 +++++++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/supervision/utils/internal.py b/supervision/utils/internal.py index b773e6e6..072c03b7 100644 --- a/supervision/utils/internal.py +++ b/supervision/utils/internal.py @@ -149,14 +149,14 @@ def get_instance_variables(instance: Any, include_properties=False) -> Set[str]: Get the public variables of a class instance. Args: - instance (Any): The class or instance + instance (Any): The instance of a class include_properties (bool): Whether to include properties in the result Usage: ```python detections = Detections(xyxy=np.array([1,2,3,4])) variables = get_class_variables(detections) - # Returns ["xyxy", "mask", "confidence", ..., "data"] + # ["xyxy", "mask", "confidence", ..., "data"] ``` """ if isinstance(instance, type): diff --git a/test/utils/test_internal.py b/test/utils/test_internal.py index b3aedad7..eee614e6 100644 --- a/test/utils/test_internal.py +++ b/test/utils/test_internal.py @@ -1,5 +1,5 @@ from contextlib import ExitStack as DoesNotRaise -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any, Set import numpy as np @@ -43,6 +43,14 @@ class MockDataclass: _protected: int = 1 __private: int = 2 + public_field: int = field(default=0) + _protected_field: int = field(default=1) + __private_field: int = field(default=2) + + public_field_with_factory: dict = field(default_factory=dict) + _protected_field_with_factory: dict = field(default_factory=dict) + __private_field_with_factory: dict = field(default_factory=dict) + def public_method(self): pass @@ -66,7 +74,7 @@ class MockDataclass: @pytest.mark.parametrize( - "input_obj, include_properties, expected, exception", + "input_instance, include_properties, expected, exception", [ ( MockClass, @@ -89,13 +97,13 @@ class MockDataclass: ( MockDataclass(), False, - {"public"}, + {"public", "public_field", "public_field_with_factory"}, DoesNotRaise(), ), ( MockDataclass(), True, - {"public", "public_property"}, + {"public", "public_field", "public_field_with_factory", "public_property"}, DoesNotRaise(), ), ( @@ -173,10 +181,13 @@ class MockDataclass: ], ) def test_get_instance_variables( - input_obj: Any, include_properties: bool, expected: Set[str], exception: Exception + input_instance: Any, + include_properties: bool, + expected: Set[str], + exception: Exception, ) -> None: with exception: result = get_instance_variables( - input_obj, include_properties=include_properties + input_instance, include_properties=include_properties ) assert result == expected From 4dc001d9bf17e058fa7981deca1aad3a16c2d046 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 29 May 2024 15:53:04 +0300 Subject: [PATCH 132/182] Renamed overlap strategy, added into InferenceSlicer docs page --- docs/detection/tools/inference_slicer.md | 4 +++ supervision/__init__.py | 2 +- .../detection/tools/inference_slicer.py | 36 ++++++++++++++----- supervision/detection/utils.py | 25 ++++++++++--- 4 files changed, 53 insertions(+), 14 deletions(-) diff --git a/docs/detection/tools/inference_slicer.md b/docs/detection/tools/inference_slicer.md index 5d5d08bc..3a00b879 100644 --- a/docs/detection/tools/inference_slicer.md +++ b/docs/detection/tools/inference_slicer.md @@ -5,3 +5,7 @@ comments: true # InferenceSlicer :::supervision.detection.tools.inference_slicer.InferenceSlicer + +# Overlap Handling Strategy + +:::supervision.detection.utils.OverlapHandlingStrategy diff --git a/supervision/__init__.py b/supervision/__init__.py index 6cd5f9ff..084af390 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -46,7 +46,7 @@ from supervision.detection.tools.json_sink import JSONSink from supervision.detection.tools.polygon_zone import PolygonZone, PolygonZoneAnnotator from supervision.detection.tools.smoother import DetectionsSmoother from supervision.detection.utils import ( - OverlapFilter, + OverlapHandlingStrategy, box_iou_batch, box_non_max_merge, box_non_max_suppression, diff --git a/supervision/detection/tools/inference_slicer.py b/supervision/detection/tools/inference_slicer.py index 3302b139..89369271 100644 --- a/supervision/detection/tools/inference_slicer.py +++ b/supervision/detection/tools/inference_slicer.py @@ -1,12 +1,18 @@ import warnings from concurrent.futures import ThreadPoolExecutor, as_completed -from typing import Callable, Optional, Tuple +from typing import Callable, Optional, Tuple, Union import numpy as np from supervision.detection.core import Detections -from supervision.detection.utils import OverlapFilter, move_boxes, move_masks +from supervision.detection.utils import ( + OverlapHandlingStrategy, + move_boxes, + move_masks, + validate_overlapping_handling_strategy, +) from supervision.utils.image import crop_image +from supervision.utils.internal import SupervisionWarnings def move_detections( @@ -51,7 +57,7 @@ class InferenceSlicer: `(width, height)`. overlap_ratio_wh (Tuple[float, float]): Overlap ratio between consecutive slices in the format `(width_ratio, height_ratio)`. - overlap_filter (OverlapFilter): Strategy for + overlap_handling_strategy (Union[OverlapHandlingStrategy, str]): Strategy for filtering or merging overlapping detections in slices. iou_threshold (float): Intersection over Union (IoU) threshold used when filtering by overlap. @@ -71,14 +77,20 @@ class InferenceSlicer: callback: Callable[[np.ndarray], Detections], slice_wh: Tuple[int, int] = (320, 320), overlap_ratio_wh: Tuple[float, float] = (0.2, 0.2), - overlap_filter: OverlapFilter = OverlapFilter.NON_MAX_SUPPRESSION, + overlap_handling_strategy: Union[ + OverlapHandlingStrategy, str + ] = OverlapHandlingStrategy.NON_MAX_SUPPRESSION, iou_threshold: float = 0.5, thread_workers: int = 1, ): + overlap_handling_strategy = validate_overlapping_handling_strategy( + overlap_handling_strategy + ) + self.slice_wh = slice_wh self.overlap_ratio_wh = overlap_ratio_wh self.iou_threshold = iou_threshold - self.overlap_filter = overlap_filter + self.overlap_handling_strategy = overlap_handling_strategy self.callback = callback self.thread_workers = thread_workers @@ -130,14 +142,20 @@ class InferenceSlicer: detections_list.append(future.result()) merged = Detections.merge(detections_list=detections_list) - if self.overlap_filter == OverlapFilter.NONE: + if self.overlap_handling_strategy == OverlapHandlingStrategy.NONE: return merged - elif self.overlap_filter == OverlapFilter.NON_MAX_SUPPRESSION: + elif ( + self.overlap_handling_strategy + == OverlapHandlingStrategy.NON_MAX_SUPPRESSION + ): return merged.with_nms(threshold=self.iou_threshold) - elif self.overlap_filter == OverlapFilter.NON_MAX_MERGE: + elif self.overlap_handling_strategy == OverlapHandlingStrategy.NON_MAX_MERGE: return merged.with_nmm(threshold=self.iou_threshold) else: - warnings.warn(f"Invalid overlap filter strategy: {self.overlap_filter}") + warnings.warn( + f"Invalid overlap filter strategy: {self.overlap_handling_strategy}", + category=SupervisionWarnings, + ) return merged def _run_callback(self, image, offset) -> Detections: diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 86b730be..71ca4786 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -1059,17 +1059,34 @@ def contains_multiple_segments( return number_of_labels > 2 -class OverlapFilter(Enum): +class OverlapHandlingStrategy(Enum): """ Enum specifying the strategy for filtering overlapping detections. Attributes: NONE: Do not filter detections based on overlap. - NON_MAX_SUPPRESSION: Filter detections using non-max suppression. - NON_MAX_MERGE: Merge detections with non-max-merging instead of - discarding them. + NON_MAX_SUPPRESSION: Filter detections using non-max suppression. This means, + detections that overlap by more than a set threshold will be discarded, + except for the one with the highest confidence. + NON_MAX_MERGE: Merge detections with non-max-merging. This means, + detections that overlap by more than a set threshold will be merged + into a single detection. """ NONE = "none" NON_MAX_SUPPRESSION = "non_max_suppression" NON_MAX_MERGE = "non_max_merge" + + +def validate_overlapping_handling_strategy( + strategy: Union[OverlapHandlingStrategy, str], +) -> OverlapHandlingStrategy: + if isinstance(strategy, str): + try: + strategy = OverlapHandlingStrategy(strategy.lower()) + except ValueError: + raise ValueError( + f"Invalid strategy value: {strategy}. Must be one of " + f"{[e.value for e in OverlapHandlingStrategy]}" + ) + return strategy From 315c4295ffb43c02c26003261eb0695cc8245b6b Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 29 May 2024 16:17:20 +0300 Subject: [PATCH 133/182] Move NMS and NMM related methods from utils into overlap_handling.py --- supervision/__init__.py | 10 +- supervision/detection/core.py | 8 +- supervision/detection/overlap_handling.py | 263 ++++++++++ .../detection/tools/inference_slicer.py | 5 +- supervision/detection/utils.py | 257 ---------- test/detection/test_overlap_handling.py | 449 ++++++++++++++++++ test/detection/test_utils.py | 441 ----------------- 7 files changed, 725 insertions(+), 708 deletions(-) create mode 100644 supervision/detection/overlap_handling.py create mode 100644 test/detection/test_overlap_handling.py diff --git a/supervision/__init__.py b/supervision/__init__.py index 084af390..83e67795 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -40,23 +40,25 @@ from supervision.detection.annotate import BoxAnnotator from supervision.detection.core import Detections from supervision.detection.line_zone import LineZone, LineZoneAnnotator from supervision.detection.lmm import LMM +from supervision.detection.overlap_handling import ( + OverlapHandlingStrategy, + box_non_max_merge, + box_non_max_suppression, + mask_non_max_suppression, +) from supervision.detection.tools.csv_sink import CSVSink from supervision.detection.tools.inference_slicer import InferenceSlicer from supervision.detection.tools.json_sink import JSONSink from supervision.detection.tools.polygon_zone import PolygonZone, PolygonZoneAnnotator from supervision.detection.tools.smoother import DetectionsSmoother from supervision.detection.utils import ( - OverlapHandlingStrategy, box_iou_batch, - box_non_max_merge, - box_non_max_suppression, calculate_masks_centroids, clip_boxes, contains_holes, contains_multiple_segments, filter_polygons_by_area, mask_iou_batch, - mask_non_max_suppression, mask_to_polygons, mask_to_xyxy, move_boxes, diff --git a/supervision/detection/core.py b/supervision/detection/core.py index be610482..482e1109 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -8,15 +8,17 @@ import numpy as np from supervision.config import CLASS_NAME_DATA_FIELD, ORIENTED_BOX_COORDINATES from supervision.detection.lmm import LMM, from_paligemma, validate_lmm_and_kwargs -from supervision.detection.utils import ( - box_iou_batch, +from supervision.detection.overlap_handling import ( box_non_max_merge, box_non_max_suppression, + mask_non_max_suppression, +) +from supervision.detection.utils import ( + box_iou_batch, calculate_masks_centroids, extract_ultralytics_masks, get_data_item, is_data_equal, - mask_non_max_suppression, mask_to_xyxy, merge_data, process_roboflow_result, diff --git a/supervision/detection/overlap_handling.py b/supervision/detection/overlap_handling.py new file mode 100644 index 00000000..7fa21b7e --- /dev/null +++ b/supervision/detection/overlap_handling.py @@ -0,0 +1,263 @@ +from enum import Enum +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 + + +def resize_masks(masks: np.ndarray, max_dimension: int = 640) -> np.ndarray: + """ + Resize all masks in the array to have a maximum dimension of max_dimension, + maintaining aspect ratio. + + Args: + masks (np.ndarray): 3D array of binary masks with shape (N, H, W). + max_dimension (int): The maximum dimension for the resized masks. + + Returns: + np.ndarray: Array of resized masks. + """ + max_height = np.max(masks.shape[1]) + max_width = np.max(masks.shape[2]) + scale = min(max_dimension / max_height, max_dimension / max_width) + + new_height = int(scale * max_height) + new_width = int(scale * max_width) + + x = np.linspace(0, max_width - 1, new_width).astype(int) + y = np.linspace(0, max_height - 1, new_height).astype(int) + xv, yv = np.meshgrid(x, y) + + resized_masks = masks[:, yv, xv] + + resized_masks = resized_masks.reshape(masks.shape[0], new_height, new_width) + return resized_masks + + +def mask_non_max_suppression( + predictions: np.ndarray, + masks: np.ndarray, + iou_threshold: float = 0.5, + mask_dimension: int = 640, +) -> np.ndarray: + """ + Perform Non-Maximum Suppression (NMS) 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, optional): The intersection-over-union threshold + to use for non-maximum suppression. + mask_dimension (int, optional): The dimension to which the masks should be + resized before computing IOU values. Defaults to 640. + + 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`. + """ + assert 0 <= iou_threshold <= 1, ( + "Value of `iou_threshold` must be in the closed range from 0 to 1, " + f"{iou_threshold} given." + ) + rows, columns = predictions.shape + + if columns == 5: + predictions = np.c_[predictions, np.zeros(rows)] + + sort_index = predictions[:, 4].argsort()[::-1] + predictions = predictions[sort_index] + masks = masks[sort_index] + masks_resized = resize_masks(masks, mask_dimension) + ious = mask_iou_batch(masks_resized, masks_resized) + categories = predictions[:, 5] + + keep = np.ones(rows, dtype=bool) + 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 :]) + + return keep[sort_index.argsort()] + + +def box_non_max_suppression( + predictions: np.ndarray, iou_threshold: float = 0.5 +) -> np.ndarray: + """ + Perform Non-Maximum Suppression (NMS) on object detection predictions. + + Args: + predictions (np.ndarray): An 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)`. + iou_threshold (float, optional): The intersection-over-union threshold + to use for non-maximum suppression. + + Returns: + np.ndarray: A boolean array indicating which predictions to keep after n + on-maximum suppression. + + Raises: + AssertionError: If `iou_threshold` is not within the + closed range from `0` to `1`. + """ + assert 0 <= iou_threshold <= 1, ( + "Value of `iou_threshold` must be in the closed range from 0 to 1, " + f"{iou_threshold} given." + ) + rows, columns = predictions.shape + + # add column #5 - category filled with zeros for agnostic nms + if columns == 5: + predictions = np.c_[predictions, np.zeros(rows)] + + # sort predictions column #4 - score + sort_index = np.flip(predictions[:, 4].argsort()) + predictions = predictions[sort_index] + + boxes = predictions[:, :4] + categories = predictions[:, 5] + ious = box_iou_batch(boxes, boxes) + ious = ious - np.eye(rows) + + keep = np.ones(rows, dtype=bool) + + for index, (iou, category) in enumerate(zip(ious, categories)): + if not keep[index]: + continue + + # drop detections with iou > iou_threshold and + # same category as current detections + condition = (iou > iou_threshold) & (categories == category) + keep = keep & ~condition + + return keep[sort_index.argsort()] + + +def group_overlapping_boxes( + predictions: npt.NDArray[np.float64], iou_threshold: float = 0.5 +) -> List[List[int]]: + """ + Apply greedy version of non-maximum merging to avoid detecting too many + overlapping bounding boxes for a given object. + + 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. + iou_threshold (float, optional): The intersection-over-union threshold + to use for non-maximum suppression. Defaults to 0.5. + + 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(predictions[idx], axis=0) + ious = box_iou_batch(predictions[order][:, :4], merge_candidate[:, :4]) + 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 + + +def box_non_max_merge( + predictions: npt.NDArray[np.float64], + iou_threshold: float = 0.5, +) -> List[List[int]]: + """ + Apply greedy version of non-maximum merging per category to avoid detecting + too many overlapping bounding boxes for a given object. + + Args: + predictions (npt.NDArray[np.float64]): An array of shape `(n, 5)` or `(n, 6)` + containing the bounding boxes coordinates in format `[x1, y1, x2, y2]`, + the confidence scores and class_ids. Omit class_id column to allow + detections of different classes to be merged. + iou_threshold (float, optional): The intersection-over-union threshold + to use for non-maximum suppression. Defaults to 0.5. + + 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) + + 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 + ) + + 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 + + +class OverlapHandlingStrategy(Enum): + """ + Enum specifying the strategy for filtering overlapping detections. + + Attributes: + NONE: Do not filter detections based on overlap. + NON_MAX_SUPPRESSION: Filter detections using non-max suppression. This means, + detections that overlap by more than a set threshold will be discarded, + except for the one with the highest confidence. + NON_MAX_MERGE: Merge detections with non-max-merging. This means, + detections that overlap by more than a set threshold will be merged + into a single detection. + """ + + NONE = "none" + NON_MAX_SUPPRESSION = "non_max_suppression" + NON_MAX_MERGE = "non_max_merge" + + +def validate_overlapping_handling_strategy( + strategy: Union[OverlapHandlingStrategy, str], +) -> OverlapHandlingStrategy: + if isinstance(strategy, str): + try: + strategy = OverlapHandlingStrategy(strategy.lower()) + except ValueError: + raise ValueError( + f"Invalid strategy value: {strategy}. Must be one of " + f"{[e.value for e in OverlapHandlingStrategy]}" + ) + return strategy diff --git a/supervision/detection/tools/inference_slicer.py b/supervision/detection/tools/inference_slicer.py index 89369271..372352e6 100644 --- a/supervision/detection/tools/inference_slicer.py +++ b/supervision/detection/tools/inference_slicer.py @@ -5,12 +5,11 @@ from typing import Callable, Optional, Tuple, Union import numpy as np from supervision.detection.core import Detections -from supervision.detection.utils import ( +from supervision.detection.overlap_handling import ( OverlapHandlingStrategy, - move_boxes, - move_masks, validate_overlapping_handling_strategy, ) +from supervision.detection.utils import move_boxes, move_masks from supervision.utils.image import crop_image from supervision.utils.internal import SupervisionWarnings diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 71ca4786..b36b6853 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -1,4 +1,3 @@ -from enum import Enum from itertools import chain from typing import Dict, List, Optional, Tuple, Union @@ -140,229 +139,6 @@ def mask_iou_batch( return np.vstack(ious) -def resize_masks(masks: np.ndarray, max_dimension: int = 640) -> np.ndarray: - """ - Resize all masks in the array to have a maximum dimension of max_dimension, - maintaining aspect ratio. - - Args: - masks (np.ndarray): 3D array of binary masks with shape (N, H, W). - max_dimension (int): The maximum dimension for the resized masks. - - Returns: - np.ndarray: Array of resized masks. - """ - max_height = np.max(masks.shape[1]) - max_width = np.max(masks.shape[2]) - scale = min(max_dimension / max_height, max_dimension / max_width) - - new_height = int(scale * max_height) - new_width = int(scale * max_width) - - x = np.linspace(0, max_width - 1, new_width).astype(int) - y = np.linspace(0, max_height - 1, new_height).astype(int) - xv, yv = np.meshgrid(x, y) - - resized_masks = masks[:, yv, xv] - - resized_masks = resized_masks.reshape(masks.shape[0], new_height, new_width) - return resized_masks - - -def mask_non_max_suppression( - predictions: np.ndarray, - masks: np.ndarray, - iou_threshold: float = 0.5, - mask_dimension: int = 640, -) -> np.ndarray: - """ - Perform Non-Maximum Suppression (NMS) 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, optional): The intersection-over-union threshold - to use for non-maximum suppression. - mask_dimension (int, optional): The dimension to which the masks should be - resized before computing IOU values. Defaults to 640. - - 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`. - """ - assert 0 <= iou_threshold <= 1, ( - "Value of `iou_threshold` must be in the closed range from 0 to 1, " - f"{iou_threshold} given." - ) - rows, columns = predictions.shape - - if columns == 5: - predictions = np.c_[predictions, np.zeros(rows)] - - sort_index = predictions[:, 4].argsort()[::-1] - predictions = predictions[sort_index] - masks = masks[sort_index] - masks_resized = resize_masks(masks, mask_dimension) - ious = mask_iou_batch(masks_resized, masks_resized) - categories = predictions[:, 5] - - keep = np.ones(rows, dtype=bool) - 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 :]) - - return keep[sort_index.argsort()] - - -def box_non_max_suppression( - predictions: np.ndarray, iou_threshold: float = 0.5 -) -> np.ndarray: - """ - Perform Non-Maximum Suppression (NMS) on object detection predictions. - - Args: - predictions (np.ndarray): An 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)`. - iou_threshold (float, optional): The intersection-over-union threshold - to use for non-maximum suppression. - - Returns: - np.ndarray: A boolean array indicating which predictions to keep after n - on-maximum suppression. - - Raises: - AssertionError: If `iou_threshold` is not within the - closed range from `0` to `1`. - """ - assert 0 <= iou_threshold <= 1, ( - "Value of `iou_threshold` must be in the closed range from 0 to 1, " - f"{iou_threshold} given." - ) - rows, columns = predictions.shape - - # add column #5 - category filled with zeros for agnostic nms - if columns == 5: - predictions = np.c_[predictions, np.zeros(rows)] - - # sort predictions column #4 - score - sort_index = np.flip(predictions[:, 4].argsort()) - predictions = predictions[sort_index] - - boxes = predictions[:, :4] - categories = predictions[:, 5] - ious = box_iou_batch(boxes, boxes) - ious = ious - np.eye(rows) - - keep = np.ones(rows, dtype=bool) - - for index, (iou, category) in enumerate(zip(ious, categories)): - if not keep[index]: - continue - - # drop detections with iou > iou_threshold and - # same category as current detections - condition = (iou > iou_threshold) & (categories == category) - keep = keep & ~condition - - return keep[sort_index.argsort()] - - -def group_overlapping_boxes( - predictions: npt.NDArray[np.float64], iou_threshold: float = 0.5 -) -> List[List[int]]: - """ - Apply greedy version of non-maximum merging to avoid detecting too many - overlapping bounding boxes for a given object. - - 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. - iou_threshold (float, optional): The intersection-over-union threshold - to use for non-maximum suppression. Defaults to 0.5. - - 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(predictions[idx], axis=0) - ious = box_iou_batch(predictions[order][:, :4], merge_candidate[:, :4]) - 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 - - -def box_non_max_merge( - predictions: npt.NDArray[np.float64], - iou_threshold: float = 0.5, -) -> List[List[int]]: - """ - Apply greedy version of non-maximum merging per category to avoid detecting - too many overlapping bounding boxes for a given object. - - Args: - predictions (npt.NDArray[np.float64]): An array of shape `(n, 5)` or `(n, 6)` - containing the bounding boxes coordinates in format `[x1, y1, x2, y2]`, - the confidence scores and class_ids. Omit class_id column to allow - detections of different classes to be merged. - iou_threshold (float, optional): The intersection-over-union threshold - to use for non-maximum suppression. Defaults to 0.5. - - 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) - - 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 - ) - - 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 clip_boxes(xyxy: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray: """ Clips bounding boxes coordinates to fit within the frame resolution. @@ -1057,36 +833,3 @@ def contains_multiple_segments( mask_uint8, labels, connectivity=connectivity ) return number_of_labels > 2 - - -class OverlapHandlingStrategy(Enum): - """ - Enum specifying the strategy for filtering overlapping detections. - - Attributes: - NONE: Do not filter detections based on overlap. - NON_MAX_SUPPRESSION: Filter detections using non-max suppression. This means, - detections that overlap by more than a set threshold will be discarded, - except for the one with the highest confidence. - NON_MAX_MERGE: Merge detections with non-max-merging. This means, - detections that overlap by more than a set threshold will be merged - into a single detection. - """ - - NONE = "none" - NON_MAX_SUPPRESSION = "non_max_suppression" - NON_MAX_MERGE = "non_max_merge" - - -def validate_overlapping_handling_strategy( - strategy: Union[OverlapHandlingStrategy, str], -) -> OverlapHandlingStrategy: - if isinstance(strategy, str): - try: - strategy = OverlapHandlingStrategy(strategy.lower()) - except ValueError: - raise ValueError( - f"Invalid strategy value: {strategy}. Must be one of " - f"{[e.value for e in OverlapHandlingStrategy]}" - ) - return strategy diff --git a/test/detection/test_overlap_handling.py b/test/detection/test_overlap_handling.py new file mode 100644 index 00000000..0186a23e --- /dev/null +++ b/test/detection/test_overlap_handling.py @@ -0,0 +1,449 @@ +from contextlib import ExitStack as DoesNotRaise +from typing import List, Optional + +import numpy as np +import pytest + +from supervision.detection.overlap_handling import ( + box_non_max_suppression, + group_overlapping_boxes, + mask_non_max_suppression, +) + + +@pytest.mark.parametrize( + "predictions, iou_threshold, expected_result, exception", + [ + ( + np.empty(shape=(0, 5), dtype=float), + 0.5, + [], + DoesNotRaise(), + ), + ( + np.array([[0, 0, 10, 10, 1.0]]), + 0.5, + [[0]], + DoesNotRaise(), + ), + ( + np.array([[0, 0, 10, 10, 1.0], [0, 0, 9, 9, 1.0]]), + 0.5, + [[1, 0]], + DoesNotRaise(), + ), # High overlap, tie-break to second det + ( + np.array([[0, 0, 10, 10, 1.0], [0, 0, 9, 9, 0.99]]), + 0.5, + [[0, 1]], + DoesNotRaise(), + ), # High overlap, merge to high confidence + ( + np.array([[0, 0, 10, 10, 0.99], [0, 0, 9, 9, 1.0]]), + 0.5, + [[1, 0]], + DoesNotRaise(), + ), # (test symmetry) High overlap, merge to high confidence + ( + np.array([[0, 0, 10, 10, 0.90], [0, 0, 9, 9, 1.0]]), + 0.5, + [[1, 0]], + DoesNotRaise(), + ), # (test symmetry) High overlap, merge to high confidence + ( + np.array([[0, 0, 10, 10, 1.0], [0, 0, 9, 9, 1.0]]), + 1.0, + [[1], [0]], + DoesNotRaise(), + ), # High IOU required + ( + np.array([[0, 0, 10, 10, 1.0], [0, 0, 9, 9, 1.0]]), + 0.0, + [[1, 0]], + DoesNotRaise(), + ), # No IOU required + ( + np.array([[0, 0, 10, 10, 1.0], [0, 0, 5, 5, 0.9]]), + 0.25, + [[0, 1]], + DoesNotRaise(), + ), # Below IOU requirement + ( + np.array([[0, 0, 10, 10, 1.0], [0, 0, 5, 5, 0.9]]), + 0.26, + [[0], [1]], + DoesNotRaise(), + ), # Above IOU requirement + ( + np.array([[0, 0, 10, 10, 1.0], [0, 0, 9, 9, 1.0], [0, 0, 8, 8, 1.0]]), + 0.5, + [[2, 1, 0]], + DoesNotRaise(), + ), # 3 boxes + ( + np.array( + [ + [0, 0, 10, 10, 1.0], + [0, 0, 9, 9, 1.0], + [5, 5, 10, 10, 1.0], + [6, 6, 10, 10, 1.0], + [9, 9, 10, 10, 1.0], + ] + ), + 0.5, + [[4], [3, 2], [1, 0]], + DoesNotRaise(), + ), # 5 boxes, 2 merges, 1 separate + ( + np.array( + [ + [0, 0, 2, 1, 1.0], + [1, 0, 3, 1, 1.0], + [2, 0, 4, 1, 1.0], + [3, 0, 5, 1, 1.0], + [4, 0, 6, 1, 1.0], + ] + ), + 0.33, + [[4, 3], [2, 1], [0]], + DoesNotRaise(), + ), # sequential merge, half overlap + ( + np.array( + [ + [0, 0, 2, 1, 0.9], + [1, 0, 3, 1, 0.9], + [2, 0, 4, 1, 1.0], + [3, 0, 5, 1, 0.9], + [4, 0, 6, 1, 0.9], + ] + ), + 0.33, + [[2, 3, 1], [4], [0]], + DoesNotRaise(), + ), # confidence + ], +) +def test_group_overlapping_boxes( + predictions: np.ndarray, + iou_threshold: float, + expected_result: List[List[int]], + exception: Exception, +) -> None: + with exception: + result = group_overlapping_boxes( + predictions=predictions, iou_threshold=iou_threshold + ) + + assert result == expected_result + + +@pytest.mark.parametrize( + "predictions, iou_threshold, expected_result, exception", + [ + ( + np.empty(shape=(0, 5)), + 0.5, + np.array([]), + DoesNotRaise(), + ), # single box with no category + ( + np.array([[10.0, 10.0, 40.0, 40.0, 0.8]]), + 0.5, + np.array([True]), + DoesNotRaise(), + ), # single box with no category + ( + np.array([[10.0, 10.0, 40.0, 40.0, 0.8, 0]]), + 0.5, + np.array([True]), + DoesNotRaise(), + ), # single box with category + ( + np.array( + [ + [10.0, 10.0, 40.0, 40.0, 0.8], + [15.0, 15.0, 40.0, 40.0, 0.9], + ] + ), + 0.5, + np.array([False, True]), + DoesNotRaise(), + ), # two boxes with no category + ( + np.array( + [ + [10.0, 10.0, 40.0, 40.0, 0.8, 0], + [15.0, 15.0, 40.0, 40.0, 0.9, 1], + ] + ), + 0.5, + np.array([True, True]), + DoesNotRaise(), + ), # two boxes with different category + ( + np.array( + [ + [10.0, 10.0, 40.0, 40.0, 0.8, 0], + [15.0, 15.0, 40.0, 40.0, 0.9, 0], + ] + ), + 0.5, + np.array([False, True]), + DoesNotRaise(), + ), # two boxes with same category + ( + np.array( + [ + [0.0, 0.0, 30.0, 40.0, 0.8], + [5.0, 5.0, 35.0, 45.0, 0.9], + [10.0, 10.0, 40.0, 50.0, 0.85], + ] + ), + 0.5, + np.array([False, True, False]), + DoesNotRaise(), + ), # three boxes with no category + ( + np.array( + [ + [0.0, 0.0, 30.0, 40.0, 0.8, 0], + [5.0, 5.0, 35.0, 45.0, 0.9, 1], + [10.0, 10.0, 40.0, 50.0, 0.85, 2], + ] + ), + 0.5, + np.array([True, True, True]), + DoesNotRaise(), + ), # three boxes with same category + ( + np.array( + [ + [0.0, 0.0, 30.0, 40.0, 0.8, 0], + [5.0, 5.0, 35.0, 45.0, 0.9, 0], + [10.0, 10.0, 40.0, 50.0, 0.85, 1], + ] + ), + 0.5, + np.array([False, True, True]), + DoesNotRaise(), + ), # three boxes with different category + ], +) +def test_box_non_max_suppression( + predictions: np.ndarray, + iou_threshold: float, + expected_result: Optional[np.ndarray], + exception: Exception, +) -> None: + with exception: + result = box_non_max_suppression( + predictions=predictions, 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, + np.array([]), + 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, + np.array([True]), + 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, + np.array([True]), + 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, + np.array([True, True]), + 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, + np.array([False, True]), + DoesNotRaise(), + ), # two masks partially overlapping with no category + ( + 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.5, + np.array([True, True]), + DoesNotRaise(), + ), # two masks partially overlapping with different category + ( + np.array( + [ + [0, 0, 0, 0, 0.8], + [0, 0, 0, 0, 0.85], + [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, 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, True, True], + [False, False, False, True, True], + [False, False, False, False, False], + [False, False, False, False, False], + ], + ] + ), + 0.5, + np.array([False, True, True]), + DoesNotRaise(), + ), # three masks with no category + ( + np.array( + [ + [0, 0, 0, 0, 0.8, 0], + [0, 0, 0, 0, 0.85, 1], + [0, 0, 0, 0, 0.9, 2], + ] + ), + 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, True, True, False, False], + [False, True, True, False, False], + [False, True, True, 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, False, False], + [False, False, False, False, False], + ], + ] + ), + 0.5, + np.array([True, True, True]), + DoesNotRaise(), + ), # three masks with different category + ], +) +def test_mask_non_max_suppression( + predictions: np.ndarray, + masks: np.ndarray, + iou_threshold: float, + expected_result: Optional[np.ndarray], + exception: Exception, +) -> None: + with exception: + result = mask_non_max_suppression( + predictions=predictions, masks=masks, iou_threshold=iou_threshold + ) + assert np.array_equal(result, expected_result) diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index 837b3b84..f0f0a6b1 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -7,15 +7,12 @@ import pytest from supervision.config import CLASS_NAME_DATA_FIELD from supervision.detection.utils import ( - box_non_max_suppression, calculate_masks_centroids, clip_boxes, contains_holes, contains_multiple_segments, filter_polygons_by_area, get_data_item, - group_overlapping_boxes, - mask_non_max_suppression, merge_data, move_boxes, process_roboflow_result, @@ -26,444 +23,6 @@ TEST_MASK = np.zeros((1, 1000, 1000), dtype=bool) TEST_MASK[:, 300:351, 200:251] = True -@pytest.mark.parametrize( - "predictions, iou_threshold, expected_result, exception", - [ - ( - np.empty(shape=(0, 5)), - 0.5, - np.array([]), - DoesNotRaise(), - ), # single box with no category - ( - np.array([[10.0, 10.0, 40.0, 40.0, 0.8]]), - 0.5, - np.array([True]), - DoesNotRaise(), - ), # single box with no category - ( - np.array([[10.0, 10.0, 40.0, 40.0, 0.8, 0]]), - 0.5, - np.array([True]), - DoesNotRaise(), - ), # single box with category - ( - np.array( - [ - [10.0, 10.0, 40.0, 40.0, 0.8], - [15.0, 15.0, 40.0, 40.0, 0.9], - ] - ), - 0.5, - np.array([False, True]), - DoesNotRaise(), - ), # two boxes with no category - ( - np.array( - [ - [10.0, 10.0, 40.0, 40.0, 0.8, 0], - [15.0, 15.0, 40.0, 40.0, 0.9, 1], - ] - ), - 0.5, - np.array([True, True]), - DoesNotRaise(), - ), # two boxes with different category - ( - np.array( - [ - [10.0, 10.0, 40.0, 40.0, 0.8, 0], - [15.0, 15.0, 40.0, 40.0, 0.9, 0], - ] - ), - 0.5, - np.array([False, True]), - DoesNotRaise(), - ), # two boxes with same category - ( - np.array( - [ - [0.0, 0.0, 30.0, 40.0, 0.8], - [5.0, 5.0, 35.0, 45.0, 0.9], - [10.0, 10.0, 40.0, 50.0, 0.85], - ] - ), - 0.5, - np.array([False, True, False]), - DoesNotRaise(), - ), # three boxes with no category - ( - np.array( - [ - [0.0, 0.0, 30.0, 40.0, 0.8, 0], - [5.0, 5.0, 35.0, 45.0, 0.9, 1], - [10.0, 10.0, 40.0, 50.0, 0.85, 2], - ] - ), - 0.5, - np.array([True, True, True]), - DoesNotRaise(), - ), # three boxes with same category - ( - np.array( - [ - [0.0, 0.0, 30.0, 40.0, 0.8, 0], - [5.0, 5.0, 35.0, 45.0, 0.9, 0], - [10.0, 10.0, 40.0, 50.0, 0.85, 1], - ] - ), - 0.5, - np.array([False, True, True]), - DoesNotRaise(), - ), # three boxes with different category - ], -) -def test_box_non_max_suppression( - predictions: np.ndarray, - iou_threshold: float, - expected_result: Optional[np.ndarray], - exception: Exception, -) -> None: - with exception: - result = box_non_max_suppression( - predictions=predictions, iou_threshold=iou_threshold - ) - assert np.array_equal(result, expected_result) - - -@pytest.mark.parametrize( - "predictions, iou_threshold, expected_result, exception", - [ - ( - np.empty(shape=(0, 5), dtype=float), - 0.5, - [], - DoesNotRaise(), - ), - ( - np.array([[0, 0, 10, 10, 1.0]]), - 0.5, - [[0]], - DoesNotRaise(), - ), - ( - np.array([[0, 0, 10, 10, 1.0], [0, 0, 9, 9, 1.0]]), - 0.5, - [[1, 0]], - DoesNotRaise(), - ), # High overlap, tie-break to second det - ( - np.array([[0, 0, 10, 10, 1.0], [0, 0, 9, 9, 0.99]]), - 0.5, - [[0, 1]], - DoesNotRaise(), - ), # High overlap, merge to high confidence - ( - np.array([[0, 0, 10, 10, 0.99], [0, 0, 9, 9, 1.0]]), - 0.5, - [[1, 0]], - DoesNotRaise(), - ), # (test symmetry) High overlap, merge to high confidence - ( - np.array([[0, 0, 10, 10, 0.90], [0, 0, 9, 9, 1.0]]), - 0.5, - [[1, 0]], - DoesNotRaise(), - ), # (test symmetry) High overlap, merge to high confidence - ( - np.array([[0, 0, 10, 10, 1.0], [0, 0, 9, 9, 1.0]]), - 1.0, - [[1], [0]], - DoesNotRaise(), - ), # High IOU required - ( - np.array([[0, 0, 10, 10, 1.0], [0, 0, 9, 9, 1.0]]), - 0.0, - [[1, 0]], - DoesNotRaise(), - ), # No IOU required - ( - np.array([[0, 0, 10, 10, 1.0], [0, 0, 5, 5, 0.9]]), - 0.25, - [[0, 1]], - DoesNotRaise(), - ), # Below IOU requirement - ( - np.array([[0, 0, 10, 10, 1.0], [0, 0, 5, 5, 0.9]]), - 0.26, - [[0], [1]], - DoesNotRaise(), - ), # Above IOU requirement - ( - np.array([[0, 0, 10, 10, 1.0], [0, 0, 9, 9, 1.0], [0, 0, 8, 8, 1.0]]), - 0.5, - [[2, 1, 0]], - DoesNotRaise(), - ), # 3 boxes - ( - np.array( - [ - [0, 0, 10, 10, 1.0], - [0, 0, 9, 9, 1.0], - [5, 5, 10, 10, 1.0], - [6, 6, 10, 10, 1.0], - [9, 9, 10, 10, 1.0], - ] - ), - 0.5, - [[4], [3, 2], [1, 0]], - DoesNotRaise(), - ), # 5 boxes, 2 merges, 1 separate - ( - np.array( - [ - [0, 0, 2, 1, 1.0], - [1, 0, 3, 1, 1.0], - [2, 0, 4, 1, 1.0], - [3, 0, 5, 1, 1.0], - [4, 0, 6, 1, 1.0], - ] - ), - 0.33, - [[4, 3], [2, 1], [0]], - DoesNotRaise(), - ), # sequential merge, half overlap - ( - np.array( - [ - [0, 0, 2, 1, 0.9], - [1, 0, 3, 1, 0.9], - [2, 0, 4, 1, 1.0], - [3, 0, 5, 1, 0.9], - [4, 0, 6, 1, 0.9], - ] - ), - 0.33, - [[2, 3, 1], [4], [0]], - DoesNotRaise(), - ), # confidence - ], -) -def test_group_overlapping_boxes( - predictions: np.ndarray, - iou_threshold: float, - expected_result: List[List[int]], - exception: Exception, -) -> None: - with exception: - result = group_overlapping_boxes( - predictions=predictions, iou_threshold=iou_threshold - ) - - assert result == expected_result - - -@pytest.mark.parametrize( - "predictions, masks, iou_threshold, expected_result, exception", - [ - ( - np.empty((0, 6)), - np.empty((0, 5, 5)), - 0.5, - np.array([]), - 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, - np.array([True]), - 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, - np.array([True]), - 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, - np.array([True, True]), - 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, - np.array([False, True]), - DoesNotRaise(), - ), # two masks partially overlapping with no category - ( - 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.5, - np.array([True, True]), - DoesNotRaise(), - ), # two masks partially overlapping with different category - ( - np.array( - [ - [0, 0, 0, 0, 0.8], - [0, 0, 0, 0, 0.85], - [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, 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, True, True], - [False, False, False, True, True], - [False, False, False, False, False], - [False, False, False, False, False], - ], - ] - ), - 0.5, - np.array([False, True, True]), - DoesNotRaise(), - ), # three masks with no category - ( - np.array( - [ - [0, 0, 0, 0, 0.8, 0], - [0, 0, 0, 0, 0.85, 1], - [0, 0, 0, 0, 0.9, 2], - ] - ), - 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, True, True, False, False], - [False, True, True, False, False], - [False, True, True, 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, False, False], - [False, False, False, False, False], - ], - ] - ), - 0.5, - np.array([True, True, True]), - DoesNotRaise(), - ), # three masks with different category - ], -) -def test_mask_non_max_suppression( - predictions: np.ndarray, - masks: np.ndarray, - iou_threshold: float, - expected_result: Optional[np.ndarray], - exception: Exception, -) -> None: - with exception: - result = mask_non_max_suppression( - predictions=predictions, masks=masks, iou_threshold=iou_threshold - ) - assert np.array_equal(result, expected_result) - - @pytest.mark.parametrize( "xyxy, resolution_wh, expected_result", [ From 6ad32406afcc06daca058701cae5d63f80b8bf70 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 29 May 2024 16:31:29 +0300 Subject: [PATCH 134/182] fix: change doc path for overlap_handling, add box_non_max_merge --- docs/detection/tools/inference_slicer.md | 2 +- docs/detection/utils.md | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/detection/tools/inference_slicer.md b/docs/detection/tools/inference_slicer.md index 3a00b879..51301e86 100644 --- a/docs/detection/tools/inference_slicer.md +++ b/docs/detection/tools/inference_slicer.md @@ -8,4 +8,4 @@ comments: true # Overlap Handling Strategy -:::supervision.detection.utils.OverlapHandlingStrategy +:::supervision.detection.overlap_handling.OverlapHandlingStrategy diff --git a/docs/detection/utils.md b/docs/detection/utils.md index f9c9473b..dd14a23e 100644 --- a/docs/detection/utils.md +++ b/docs/detection/utils.md @@ -18,16 +18,22 @@ status: new :::supervision.detection.utils.mask_iou_batch -:::supervision.detection.utils.box_non_max_suppression +:::supervision.detection.overlap_handling.box_non_max_suppression -:::supervision.detection.utils.mask_non_max_suppression +:::supervision.detection.overlap_handling.mask_non_max_suppression + + + +:::supervision.detection.overlap_handling.box_non_max_merge

polygon_to_mask

From 34353aac543163e51b933843744b8409125f178f Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 29 May 2024 18:39:41 +0300 Subject: [PATCH 135/182] Add overlap handling example image :) --- supervision/detection/overlap_handling.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/supervision/detection/overlap_handling.py b/supervision/detection/overlap_handling.py index 7fa21b7e..f9acc4a6 100644 --- a/supervision/detection/overlap_handling.py +++ b/supervision/detection/overlap_handling.py @@ -242,6 +242,8 @@ class OverlapHandlingStrategy(Enum): NON_MAX_MERGE: Merge detections with non-max-merging. This means, detections that overlap by more than a set threshold will be merged into a single detection. + + ![overlap-handling-strategies-example](https://media.roboflow.com/supervision-docs/overlap-handling-strategies-example.png) """ NONE = "none" From dc1249969f9e28424f02723122ddf28b8bee5bf0 Mon Sep 17 00:00:00 2001 From: tc360950 Date: Wed, 29 May 2024 18:34:35 +0200 Subject: [PATCH 136/182] Improve variable naming --- supervision/detection/line_zone.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/supervision/detection/line_zone.py b/supervision/detection/line_zone.py index 45bc9644..4198135d 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -167,20 +167,19 @@ class LineZone: # Calculate which anchors lie to the left of the line triggers = self._cross_product(all_anchors, self.vector) < 0 - # Reduce to find out if all anchors for a - # detection lie to the left (or right) of the line - max_triggers = np.max(triggers, axis=0) - min_triggers = np.min(triggers, axis=0) + has_any_left_trigger = np.any(triggers, axis=0) + has_any_right_trigger = np.any(~triggers, axis=0) + is_uniformly_triggered = ~(has_any_left_trigger & has_any_right_trigger) for i, tracker_id in enumerate(detections.tracker_id): if not in_limits[i]: continue - if min_triggers[i] != max_triggers[i]: + if not is_uniformly_triggered[i]: # One anchor lies to the left of the line # whilst another lies to the right continue - tracker_state = max_triggers[i] + tracker_state = has_any_left_trigger[i] if tracker_id not in self.tracker_state: self.tracker_state[tracker_id] = tracker_state continue From bdc10f83fe429474d74d5bd5469afdc3d6d5522d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 May 2024 01:05:20 +0000 Subject: [PATCH 137/182] :arrow_up: Bump requests from 2.32.2 to 2.32.3 Bumps [requests](https://github.com/psf/requests) from 2.32.2 to 2.32.3. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.32.2...v2.32.3) --- updated-dependencies: - dependency-name: requests dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 061b284b..319db4a3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3461,13 +3461,13 @@ files = [ [[package]] name = "requests" -version = "2.32.2" +version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" files = [ - {file = "requests-2.32.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"}, - {file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -4258,4 +4258,4 @@ desktop = ["opencv-python"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "ad8402ec1767f9427ab38bad7dab54b302a30f9e08b6489fad224c8481745b37" +content-hash = "e3d79f6c93041323b04c7b45e93bb3c4198b21889044004af8a0485a6145a207" diff --git a/pyproject.toml b/pyproject.toml index ff83f5fa..640d462e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ pyyaml = ">=5.3" defusedxml = "^0.7.1" opencv-python = { version = ">=4.5.5.64", optional = true } opencv-python-headless = ">=4.5.5.64" -requests = { version = ">=2.26.0,<=2.32.2", optional = true } +requests = { version = ">=2.26.0,<=2.32.3", optional = true } tqdm = { version = ">=4.62.3,<=4.66.4", optional = true } pillow = ">=9.4" From f7bd7011fedd9f2ec802997f20b6d061e595d710 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Thu, 30 May 2024 13:45:12 +0300 Subject: [PATCH 138/182] OverlapFilter: Add and validate docs, rename --- docs/detection/double_detection_filter.md | 30 ++++++++++++++++ docs/detection/tools/inference_slicer.md | 4 --- docs/detection/utils.md | 18 ---------- mkdocs.yml | 1 + supervision/__init__.py | 4 +-- supervision/detection/core.py | 2 +- ...{overlap_handling.py => overlap_filter.py} | 16 ++++----- .../detection/tools/inference_slicer.py | 35 ++++++++----------- ...lap_handling.py => test_overlap_filter.py} | 2 +- 9 files changed, 57 insertions(+), 55 deletions(-) create mode 100644 docs/detection/double_detection_filter.md rename supervision/detection/{overlap_handling.py => overlap_filter.py} (94%) rename test/detection/{test_overlap_handling.py => test_overlap_filter.py} (99%) diff --git a/docs/detection/double_detection_filter.md b/docs/detection/double_detection_filter.md new file mode 100644 index 00000000..1631852f --- /dev/null +++ b/docs/detection/double_detection_filter.md @@ -0,0 +1,30 @@ +--- +comments: true +status: new +--- + +# Double Detection Filter + + + +:::supervision.detection.overlap_filter.OverlapFilter + + + +:::supervision.detection.overlap_filter.box_non_max_suppression + + + +:::supervision.detection.overlap_filter.mask_non_max_suppression + + + +:::supervision.detection.overlap_filter.box_non_max_merge diff --git a/docs/detection/tools/inference_slicer.md b/docs/detection/tools/inference_slicer.md index 51301e86..5d5d08bc 100644 --- a/docs/detection/tools/inference_slicer.md +++ b/docs/detection/tools/inference_slicer.md @@ -5,7 +5,3 @@ comments: true # InferenceSlicer :::supervision.detection.tools.inference_slicer.InferenceSlicer - -# Overlap Handling Strategy - -:::supervision.detection.overlap_handling.OverlapHandlingStrategy diff --git a/docs/detection/utils.md b/docs/detection/utils.md index dd14a23e..369746a3 100644 --- a/docs/detection/utils.md +++ b/docs/detection/utils.md @@ -17,24 +17,6 @@ status: new :::supervision.detection.utils.mask_iou_batch - - -:::supervision.detection.overlap_handling.box_non_max_suppression - - - -:::supervision.detection.overlap_handling.mask_non_max_suppression - - - -:::supervision.detection.overlap_handling.box_non_max_merge - diff --git a/mkdocs.yml b/mkdocs.yml index f257238d..19d6a4fd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -48,6 +48,7 @@ nav: - Core: detection/core.md - Annotators: detection/annotators.md - Metrics: detection/metrics.md + - Double Detection Filter: detection/double_detection_filter.md - Utils: detection/utils.md - Keypoint Detection: - Core: keypoint/core.md diff --git a/supervision/__init__.py b/supervision/__init__.py index 83e67795..4f28d49f 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -40,8 +40,8 @@ from supervision.detection.annotate import BoxAnnotator from supervision.detection.core import Detections from supervision.detection.line_zone import LineZone, LineZoneAnnotator from supervision.detection.lmm import LMM -from supervision.detection.overlap_handling import ( - OverlapHandlingStrategy, +from supervision.detection.overlap_filter import ( + OverlapFilter, box_non_max_merge, box_non_max_suppression, mask_non_max_suppression, diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 482e1109..c47bc819 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -8,7 +8,7 @@ import numpy as np from supervision.config import CLASS_NAME_DATA_FIELD, ORIENTED_BOX_COORDINATES from supervision.detection.lmm import LMM, from_paligemma, validate_lmm_and_kwargs -from supervision.detection.overlap_handling import ( +from supervision.detection.overlap_filter import ( box_non_max_merge, box_non_max_suppression, mask_non_max_suppression, diff --git a/supervision/detection/overlap_handling.py b/supervision/detection/overlap_filter.py similarity index 94% rename from supervision/detection/overlap_handling.py rename to supervision/detection/overlap_filter.py index f9acc4a6..ab4408d1 100644 --- a/supervision/detection/overlap_handling.py +++ b/supervision/detection/overlap_filter.py @@ -230,7 +230,7 @@ def box_non_max_merge( return merge_groups -class OverlapHandlingStrategy(Enum): +class OverlapFilter(Enum): """ Enum specifying the strategy for filtering overlapping detections. @@ -239,11 +239,9 @@ class OverlapHandlingStrategy(Enum): NON_MAX_SUPPRESSION: Filter detections using non-max suppression. This means, detections that overlap by more than a set threshold will be discarded, except for the one with the highest confidence. - NON_MAX_MERGE: Merge detections with non-max-merging. This means, + NON_MAX_MERGE: Merge detections with non-max merging. This means, detections that overlap by more than a set threshold will be merged into a single detection. - - ![overlap-handling-strategies-example](https://media.roboflow.com/supervision-docs/overlap-handling-strategies-example.png) """ NONE = "none" @@ -251,15 +249,15 @@ class OverlapHandlingStrategy(Enum): NON_MAX_MERGE = "non_max_merge" -def validate_overlapping_handling_strategy( - strategy: Union[OverlapHandlingStrategy, str], -) -> OverlapHandlingStrategy: +def validate_overlap_filter( + strategy: Union[OverlapFilter, str], +) -> OverlapFilter: if isinstance(strategy, str): try: - strategy = OverlapHandlingStrategy(strategy.lower()) + strategy = OverlapFilter(strategy.lower()) except ValueError: raise ValueError( f"Invalid strategy value: {strategy}. Must be one of " - f"{[e.value for e in OverlapHandlingStrategy]}" + f"{[e.value for e in OverlapFilter]}" ) return strategy diff --git a/supervision/detection/tools/inference_slicer.py b/supervision/detection/tools/inference_slicer.py index 372352e6..134361bd 100644 --- a/supervision/detection/tools/inference_slicer.py +++ b/supervision/detection/tools/inference_slicer.py @@ -5,10 +5,7 @@ from typing import Callable, Optional, Tuple, Union import numpy as np from supervision.detection.core import Detections -from supervision.detection.overlap_handling import ( - OverlapHandlingStrategy, - validate_overlapping_handling_strategy, -) +from supervision.detection.overlap_filter import OverlapFilter, validate_overlap_filter from supervision.detection.utils import move_boxes, move_masks from supervision.utils.image import crop_image from supervision.utils.internal import SupervisionWarnings @@ -56,7 +53,7 @@ class InferenceSlicer: `(width, height)`. overlap_ratio_wh (Tuple[float, float]): Overlap ratio between consecutive slices in the format `(width_ratio, height_ratio)`. - overlap_handling_strategy (Union[OverlapHandlingStrategy, str]): Strategy for + overlap_filter_strategy (Union[OverlapFilter, str]): Strategy for filtering or merging overlapping detections in slices. iou_threshold (float): Intersection over Union (IoU) threshold used when filtering by overlap. @@ -76,20 +73,18 @@ class InferenceSlicer: callback: Callable[[np.ndarray], Detections], slice_wh: Tuple[int, int] = (320, 320), overlap_ratio_wh: Tuple[float, float] = (0.2, 0.2), - overlap_handling_strategy: Union[ - OverlapHandlingStrategy, str - ] = OverlapHandlingStrategy.NON_MAX_SUPPRESSION, + overlap_filter_strategy: Union[ + OverlapFilter, str + ] = OverlapFilter.NON_MAX_SUPPRESSION, iou_threshold: float = 0.5, thread_workers: int = 1, ): - overlap_handling_strategy = validate_overlapping_handling_strategy( - overlap_handling_strategy - ) + overlap_filter_strategy = validate_overlap_filter(overlap_filter_strategy) self.slice_wh = slice_wh self.overlap_ratio_wh = overlap_ratio_wh self.iou_threshold = iou_threshold - self.overlap_handling_strategy = overlap_handling_strategy + self.overlap_filter_strategy = overlap_filter_strategy self.callback = callback self.thread_workers = thread_workers @@ -120,7 +115,10 @@ class InferenceSlicer: result = model(image_slice)[0] return sv.Detections.from_ultralytics(result) - slicer = sv.InferenceSlicer(callback = callback) + slicer = sv.InferenceSlicer( + callback=callback, + overlap_filter_strategy=sv.OverlapFilter.NON_MAX_SUPPRESSION, + ) detections = slicer(image) ``` @@ -141,18 +139,15 @@ class InferenceSlicer: detections_list.append(future.result()) merged = Detections.merge(detections_list=detections_list) - if self.overlap_handling_strategy == OverlapHandlingStrategy.NONE: + if self.overlap_filter_strategy == OverlapFilter.NONE: return merged - elif ( - self.overlap_handling_strategy - == OverlapHandlingStrategy.NON_MAX_SUPPRESSION - ): + elif self.overlap_filter_strategy == OverlapFilter.NON_MAX_SUPPRESSION: return merged.with_nms(threshold=self.iou_threshold) - elif self.overlap_handling_strategy == OverlapHandlingStrategy.NON_MAX_MERGE: + elif self.overlap_filter_strategy == OverlapFilter.NON_MAX_MERGE: return merged.with_nmm(threshold=self.iou_threshold) else: warnings.warn( - f"Invalid overlap filter strategy: {self.overlap_handling_strategy}", + f"Invalid overlap filter strategy: {self.overlap_filter_strategy}", category=SupervisionWarnings, ) return merged diff --git a/test/detection/test_overlap_handling.py b/test/detection/test_overlap_filter.py similarity index 99% rename from test/detection/test_overlap_handling.py rename to test/detection/test_overlap_filter.py index 0186a23e..f628c30f 100644 --- a/test/detection/test_overlap_handling.py +++ b/test/detection/test_overlap_filter.py @@ -4,7 +4,7 @@ from typing import List, Optional import numpy as np import pytest -from supervision.detection.overlap_handling import ( +from supervision.detection.overlap_filter import ( box_non_max_suppression, group_overlapping_boxes, mask_non_max_suppression, From 33e10ff722c4708b0ed39f35a841a2e5ffc6729d Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Fri, 31 May 2024 20:13:13 +0300 Subject: [PATCH 139/182] Add unit tests covering most single-detection cases * Add Colab for visuals: https://colab.research.google.com/drive/179sq8joJ-7JSYMqYNBQlMIPYEClq1PDi?usp=sharing * Need to visualise other cases - not sure if all are necessary --- test/detection/test_line_counter.py | 501 ++++++++++++++++------------ 1 file changed, 279 insertions(+), 222 deletions(-) diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index d9c21f15..7642d965 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -1,5 +1,4 @@ from contextlib import ExitStack as DoesNotRaise -from itertools import chain, combinations from test.test_utils import mock_detections from typing import List, Optional, Tuple @@ -78,279 +77,331 @@ def test_calculate_region_of_interest_limits( @pytest.mark.parametrize( "vector, xyxy_sequence, expected_crossed_in, expected_crossed_out", [ - ( - Vector( - Point(0, 0), - Point(0, 100), - ), + ( # Vertical line, simple crossing + Vector(Point(0, 0), Point(0, 10)), [ - [100, 50, 120, 70], - [-100, 50, -80, 70], + [4, 4, 6, 6], + [4 - 10, 4, 6 - 10, 6], + [4, 4, 6, 6], + [4 - 10, 4, 6 - 10, 6], ], - [False, False], - [False, True], + [False, False, True, False], + [False, True, False, True], ), - ( - Vector( - Point(0, 0), - Point(0, 100), - ), + ( # Vertical line reversed, simple crossing + Vector(Point(0, 10), Point(0, 0)), [ - [-100, 50, -80, 70], - [100, 50, 120, 70], + [4, 4, 6, 6], + [4 - 10, 4, 6 - 10, 6], + [4, 4, 6, 6], + [4 - 10, 4, 6 - 10, 6], ], - [False, True], - [False, False], + [False, True, False, True], + [False, False, True, False], ), - ( - Vector( - Point(0, 0), - Point(0, 100), - ), + ( # Horizontal line, simple crossing + Vector(Point(0, 0), Point(10, 0)), [ - [-100, 50, -80, 70], - [-10, 50, 20, 70], - [100, 50, 120, 70], + [4, 4, 6, 6], + [4, 4 - 10, 6, 6 - 10], + [4, 4, 6, 6], + [4, 4 - 10, 6, 6 - 10], ], - [False, False, True], - [False, False, False], + [False, True, False, True], + [False, False, True, False], ), - ( - Vector( - Point(0, 0), - Point(100, 100), - ), + ( # Horizontal line reversed, simple crossing + Vector(Point(10, 0), Point(0, 0)), [ - [50, 45, 70, 30], - [40, 50, 50, 40], - [0, 50, 10, 40], + [4, 4, 6, 6], + [4, 4 - 10, 6, 6 - 10], + [4, 4, 6, 6], + [4, 4 - 10, 6, 6 - 10], ], - [False, False, False], - [False, False, True], + [False, False, True, False], + [False, True, False, True], ), - ( - Vector( - Point(0, 0), - Point(100, 0), - ), + ( # Diagonal line, simple crossing + Vector(Point(5, 0), Point(0, 5)), [ - [50, -45, 70, -30], - [40, 50, 50, 40], + [0, 0, 2, 2], + [0 + 10, 0 + 10, 2 + 10, 2 + 10], + [0, 0, 2, 2], + [0 + 10, 0 + 10, 2 + 10, 2 + 10], ], - [False, False], - [False, True], + [False, True, False, True], + [False, False, True, False], ), - ( - Vector( - Point(0, 0), - Point(0, -100), - ), + ( # Crossing beside - right side + Vector(Point(0, 0), Point(10, 0)), [ - [100, -50, 120, -70], - [-100, -50, -80, -70], + [20, 4, 24, 6], + [20, 4 - 10, 24, 6 - 10], + [20, 4, 24, 6], + [20, 4 - 10, 24, 6 - 10], ], - [False, True], - [False, False], - ), - ( - Vector( - Point(0, 0), - Point(50, 100), - ), - [ - [50, 50, 70, 30], - [40, 50, 50, 40], - [0, 50, 10, 40], - ], - [False, False, False], - [False, False, True], - ), - ( - Vector( - Point(0, 0), - Point(0, 100), - ), - [ - [100, 50, 120, 70], - [-100, 50, -80, 70], - [100, 50, 120, 70], - [-100, 50, -80, 70], - [100, 50, 120, 70], - [-100, 50, -80, 70], - [100, 50, 120, 70], - [-100, 50, -80, 70], - ], - [False, False, True, False, True, False, True, False], - [False, True, False, True, False, True, False, True], - ), - ( - Vector( - Point(0, 0), - Point(-100, 0), - ), - [ - [-50, 70, -40, 50], - [-50, -70, -40, -50], - [-50, 70, -40, 50], - [-50, -70, -40, -50], - [-50, 70, -40, 50], - [-50, -70, -40, -50], - [-50, 70, -40, 50], - [-50, -70, -40, -50], - ], - [False, False, True, False, True, False, True, False], - [False, True, False, True, False, True, False, True], - ), - ( - Vector( - Point(0, 100), - Point(0, 200), - ), - [ - [-100, 150, -80, 170], - [-100, 50, -80, 70], - [-10, 50, 20, 70], - [100, 50, 120, 70], - ], # detection goes "around" line start and hence never crosses it [False, False, False, False], [False, False, False, False], ), - ( - Vector( - Point(0, 100), - Point(0, 200), - ), + ( # Crossing beside - left side + Vector(Point(0, 0), Point(10, 0)), [ - [-100, 150, -80, 170], - [-100, 250, -80, 270], - [-10, 250, 20, 270], - [100, 250, 120, 270], - ], # detection goes "around" line end and hence never crosses it - [False, False, False, False], - [False, False, False, False], - ), - ( - Vector( - Point(-50, -50), - Point(-100, -150), - ), - [ - [-30, -80, -20, -100], - [-150, -60, -110, -70], - [-10, -100, 20, -130], + [-20, 4, -24, 6], + [-20, 4 - 10, -24, 6 - 10], + [-20, 4, -24, 6], + [-20, 4 - 10, -24, 6 - 10], ], - [False, True, False], - [False, False, True], + [False, False, False, False], + [False, False, False, False], ), + ( # Move above + Vector(Point(0, 0), Point(10, 0)), + [ + [-4, 4, -2, 6], + [-4 + 20, 4, -2 + 20, 6], + [-4, 4, -2, 6], + [-4 + 20, 4, -2 + 20, 6], + ], + [False, False, False, False], + [False, False, False, False], + ), + ( # Move below + Vector(Point(0, 0), Point(10, 0)), + [ + [-4, -6, -2, -4], + [-4 + 20, -6, -2 + 20, -4], + [-4, -6, -2, -4], + [-4 + 20, -6, -2 + 20, -4], + ], + [False, False, False, False], + [False, False, False, False], + ), + ( # Move into line partway + Vector(Point(0, 0), Point(10, 0)), + [ + [4, 4, 6, 6], + [4 + 5, 4, 6 + 5, 6], + [4, 4, 6, 6], + [4 + 5, 4, 6 + 5, 6], + ], + [False, False, False, False], + [False, False, False, False], + ), + # ( # Diagonal crossing of a straight line - does not work. + # Vector(Point(0, 0), Point(10, 0)), + # [ + # [-4, 4, -2, 8], + # [-4 + 16, -4, -2 + 16, -6], + # [-4, 4, -2, 8], + # [-4 + 16, -4, -2 + 16, -6] + # ], + # [False, False, True, False], + # [False, True, False, True], + # ), + # ( # V-shaped crossing - does not work. + # Vector(Point(0, 0), Point(10, 0)), + # [ + # [-3, 6, -1, 8], + # [4, -6, 6, -4], + # [11, 6, 13, 8] + # ], + # [False, False, True], + # [False, True, False] + # ), ], ) -def test_line_zone_single_detection( +def test_line_zone_one_detection_default_anchors( vector: Vector, xyxy_sequence: List[List[int]], expected_crossed_in: List[bool], expected_crossed_out: List[bool], ) -> None: line_zone = LineZone(start=vector.start, end=vector.end) + + crossed_in_list = [] + crossed_out_list = [] for i, bbox in enumerate(xyxy_sequence): detections = mock_detections( xyxy=[bbox], tracker_id=[0], ) crossed_in, crossed_out = line_zone.trigger(detections) - assert crossed_in[0] == expected_crossed_in[i] - assert crossed_out[0] == expected_crossed_out[i] - assert line_zone.in_count == sum(expected_crossed_in[: (i + 1)]) - assert line_zone.out_count == sum(expected_crossed_out[: (i + 1)]) + crossed_in_list.append(crossed_in[0]) + crossed_out_list.append(crossed_out[0]) + + assert ( + crossed_in_list == expected_crossed_in + ), f"expected {expected_crossed_in}, got {crossed_in_list}" + assert ( + crossed_out_list == expected_crossed_out + ), f"expected {expected_crossed_out}, got {crossed_out_list}" @pytest.mark.parametrize( - "vector," - "xyxy_sequence," - "expected_crossed_in," - "expected_crossed_out," - "crossing_anchors", + "vector, xyxy_sequence, triggering_anchors, expected_crossed_in, " + "expected_crossed_out", [ - ( - Vector( - Point(0, 0), - Point(100, 100), - ), + ( # Scrape line, left side, corner anchors + Vector(Point(0, 0), Point(10, 0)), [ - [50, 30, 60, 20], - [20, 50, 40, 30], + [-2, 4, 2, 6], + [-2, 4 - 10, 2, 6 - 10], + [-2, 4, 2, 6], + [-2, 4 - 10, 2, 6 - 10], ], - [False, False], - [False, True], - [Position.TOP_LEFT, Position.TOP_RIGHT, Position.BOTTOM_LEFT], + [ + Position.TOP_LEFT, + Position.BOTTOM_LEFT, + Position.TOP_RIGHT, + Position.BOTTOM_RIGHT, + ], + [False, False, False, False], + [False, False, False, False], ), - ( - Vector( - Point(0, 0), - Point(0, 100), - ), + ( # Scrape line, left side, right anchors + Vector(Point(0, 0), Point(10, 0)), [ - [-100, 50, -80, 70], - [-100, 50, 120, 70], + [-2, 4, 2, 6], + [-2, 4 - 10, 2, 6 - 10], + [-2, 4, 2, 6], + [-2, 4 - 10, 2, 6 - 10], ], - [False, True], - [False, False], [Position.TOP_RIGHT, Position.BOTTOM_RIGHT], + [False, True, False, True], + [False, False, True, False], + ), + ( # Scrape line, left side, center anchor (along line point) + Vector(Point(0, 0), Point(10, 0)), + [ + [-2, 4, 2, 6], + [-2, 4 - 10, 2, 6 - 10], + [-2, 4, 2, 6], + [-2, 4 - 10, 2, 6 - 10], + ], + [Position.CENTER], + [False, True, False, True], + [False, False, True, False], + ), + ( # Scrape line, right side, corner anchors + Vector(Point(0, 0), Point(10, 0)), + [ + [8, 4, 12, 6], + [8, 4 - 10, 12, 6 - 10], + [8, 4, 12, 6], + [8, 4 - 10, 12, 6 - 10], + ], + [ + Position.TOP_LEFT, + Position.BOTTOM_LEFT, + Position.TOP_RIGHT, + Position.BOTTOM_RIGHT, + ], + [False, False, False, False], + [False, False, False, False], + ), + ( # Scrape line, right side, left anchors + Vector(Point(0, 0), Point(10, 0)), + [ + [8, 4, 12, 6], + [8, 4 - 10, 12, 6 - 10], + [8, 4, 12, 6], + [8, 4 - 10, 12, 6 - 10], + ], + [Position.TOP_LEFT, Position.BOTTOM_LEFT], + [False, True, False, True], + [False, False, True, False], + ), + ( # Scrape line, right side, center anchor (along line point) + Vector(Point(0, 0), Point(10, 0)), + [ + [8, 4, 12, 6], + [8, 4 - 10, 12, 6 - 10], + [8, 4, 12, 6], + [8, 4 - 10, 12, 6 - 10], + ], + [Position.CENTER], + [False, True, False, True], + [False, False, True, False], + ), + ( # Simple crossing, one anchor + Vector(Point(0, 0), Point(10, 0)), + [ + [4, 4, 6, 6], + [4, 4 - 10, 6, 6 - 10], + [4, 4, 6, 6], + [4, 4 - 10, 6, 6 - 10], + ], + [Position.CENTER], + [False, True, False, True], + [False, False, True, False], + ), + ( # Simple crossing, all box anchors + Vector(Point(0, 0), Point(10, 0)), + [ + [4, 4, 6, 6], + [4, 4 - 10, 6, 6 - 10], + [4, 4, 6, 6], + [4, 4 - 10, 6, 6 - 10], + ], + [ + Position.CENTER, + Position.CENTER_LEFT, + Position.CENTER_RIGHT, + Position.TOP_CENTER, + Position.TOP_LEFT, + Position.TOP_RIGHT, + Position.BOTTOM_LEFT, + Position.BOTTOM_CENTER, + Position.BOTTOM_RIGHT, + ], + [False, True, False, True], + [False, False, True, False], ), ], ) -def test_line_zone_single_detection_on_subset_of_anchors( +def test_line_zone_one_detection( vector: Vector, xyxy_sequence: List[List[int]], + triggering_anchors: List[Position], expected_crossed_in: List[bool], expected_crossed_out: List[bool], - crossing_anchors: List[Position], ) -> None: - def powerset(s): - return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1)) + line_zone = LineZone( + start=vector.start, end=vector.end, triggering_anchors=triggering_anchors + ) - for anchors in powerset( - [ - Position.TOP_LEFT, - Position.TOP_RIGHT, - Position.BOTTOM_LEFT, - Position.BOTTOM_RIGHT, - ] - ): - if not anchors: - continue - line_zone = LineZone( - start=vector.start, end=vector.end, triggering_anchors=anchors + crossed_in_list = [] + crossed_out_list = [] + for i, bbox in enumerate(xyxy_sequence): + detections = mock_detections( + xyxy=[bbox], + tracker_id=[0], ) - for i, bbox in enumerate(xyxy_sequence): - detections = mock_detections( - xyxy=[bbox], - tracker_id=[0], - ) - crossed_in, crossed_out = line_zone.trigger(detections) - if all(anchor in crossing_anchors for anchor in anchors): - assert crossed_in == expected_crossed_in[i] - assert crossed_out == expected_crossed_out[i] - else: - assert np.all(not crossed_in) - assert np.all(not crossed_out) + crossed_in, crossed_out = line_zone.trigger(detections) + crossed_in_list.append(crossed_in[0]) + crossed_out_list.append(crossed_out[0]) + + assert ( + crossed_in_list == expected_crossed_in + ), f"expected {expected_crossed_in}, got {crossed_in_list}" + assert ( + crossed_out_list == expected_crossed_out + ), f"expected {expected_crossed_out}, got {crossed_out_list}" @pytest.mark.parametrize( - "vector," - "xyxy_sequence," - "expected_crossed_in," - "expected_crossed_out," + "vector, xyxy_sequence, expected_crossed_in, expected_crossed_out, " "anchors, exception", [ ( Vector( Point(0, 0), - Point(0, 100), + Point(0, 10), ), [ - [[100, 50, 120, 70], [100, 50, 120, 70]], - [[-100, 50, -80, 70], [100, 50, 120, 70]], - [[100, 50, 120, 70], [100, 50, 120, 70]], + [[10, 5, 12, 7], [10, 5, 12, 7]], + [[-10, 5, -8, 7], [10, 5, 12, 7]], + [[10, 5, 12, 7], [10, 5, 12, 7]], ], [[False, False], [False, False], [True, False]], [[False, False], [True, False], [False, False]], @@ -365,17 +416,17 @@ def test_line_zone_single_detection_on_subset_of_anchors( ( Vector( Point(0, 0), - Point(-100, 0), + Point(-10, 0), ), [ - [[-50, 70, -40, 50], [-80, -50, -70, -40]], - [[-50, -70, -40, -50], [-80, 50, -70, 40]], - [[-50, 70, -40, 50], [-80, 50, -70, 40]], - [[-50, -70, -40, -50], [-80, 50, -70, 40]], - [[-50, 70, -40, 50], [-80, 50, -70, 40]], - [[-50, -70, -40, -50], [-80, 50, -70, 40]], - [[-50, 70, -40, 50], [-80, 50, -70, 40]], - [[-50, -70, -40, -50], [-80, -50, -70, -40]], + [[-5, 7, -4, 5], [-8, -5, -7, -4]], + [[-5, -7, -4, -5], [-8, 5, -7, 4]], + [[-5, 7, -4, 5], [-8, 5, -7, 4]], + [[-5, -7, -4, -5], [-8, 5, -7, 4]], + [[-5, 7, -4, 5], [-8, 5, -7, 4]], + [[-5, -7, -4, -5], [-8, 5, -7, 4]], + [[-5, 7, -4, 5], [-8, 5, -7, 4]], + [[-5, -7, -4, -5], [-8, -5, -7, -4]], ], [ (False, False), @@ -407,12 +458,12 @@ def test_line_zone_single_detection_on_subset_of_anchors( ), ( Vector( - Point(-50, -50), - Point(-100, -150), + Point(-5, -5), + Point(-10, -15), ), [ - [[-30, -80, -20, -100], [100, 50, 120, 70]], - [[-100, -80, -20, -100], [100, 50, 120, 70]], + [[-3, -8, -2, -10], [10, 5, 12, 7]], + [[-10, -8, -2, -10], [10, 5, 12, 7]], ], [[False, False], [True, False]], [[False, False], [False, False]], @@ -422,9 +473,9 @@ def test_line_zone_single_detection_on_subset_of_anchors( ( Vector( Point(0, 0), - Point(-100, 0), + Point(-10, 0), ), - [[[-50, 70, -40, 50], [-80, -50, -70, -40]]], + [[[-5, 7, -4, 5], [-8, -5, -7, -4]]], [(False, False)], [(False, False)], [], # raise because of empty anchors @@ -440,6 +491,12 @@ def test_line_zone_multiple_detections( anchors: List[Position], exception: Exception, ) -> None: + """ + Test LineZone with multiple detections. + A detection is represented by a sequence of xyxy bboxes which represent + subsequent positions of the detected object. If a line is crossed (in either + direction) by a detection it is crossed by exactly all anchors from @anchors. + """ with exception: line_zone = LineZone( start=vector.start, end=vector.end, triggering_anchors=anchors From d2f169bcfa043028dfec9db90ba44417008fc3a0 Mon Sep 17 00:00:00 2001 From: LinasKo Date: Sat, 1 Jun 2024 15:59:46 +0300 Subject: [PATCH 140/182] Line zone tests - replace multi detection tests --- test/detection/test_line_counter.py | 128 ++++++++-------------------- 1 file changed, 37 insertions(+), 91 deletions(-) diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index 7642d965..6804b20e 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -2,7 +2,6 @@ from contextlib import ExitStack as DoesNotRaise from test.test_utils import mock_detections from typing import List, Optional, Tuple -import numpy as np import pytest from supervision import LineZone @@ -212,7 +211,7 @@ def test_calculate_region_of_interest_limits( ) def test_line_zone_one_detection_default_anchors( vector: Vector, - xyxy_sequence: List[List[int]], + xyxy_sequence: List[List[float]], expected_crossed_in: List[bool], expected_crossed_out: List[bool], ) -> None: @@ -361,7 +360,7 @@ def test_line_zone_one_detection_default_anchors( ) def test_line_zone_one_detection( vector: Vector, - xyxy_sequence: List[List[int]], + xyxy_sequence: List[List[float]], triggering_anchors: List[Position], expected_crossed_in: List[bool], expected_crossed_out: List[bool], @@ -390,63 +389,16 @@ def test_line_zone_one_detection( @pytest.mark.parametrize( - "vector, xyxy_sequence, expected_crossed_in, expected_crossed_out, " - "anchors, exception", + "vector, xyxy_sequence, anchors, expected_crossed_in, " + "expected_crossed_out, exception", [ - ( - Vector( - Point(0, 0), - Point(0, 10), - ), + ( # One stays, one crosses + Vector(Point(0, 0), Point(10, 0)), [ - [[10, 5, 12, 7], [10, 5, 12, 7]], - [[-10, 5, -8, 7], [10, 5, 12, 7]], - [[10, 5, 12, 7], [10, 5, 12, 7]], - ], - [[False, False], [False, False], [True, False]], - [[False, False], [True, False], [False, False]], - [ - Position.TOP_LEFT, - Position.TOP_RIGHT, - Position.BOTTOM_LEFT, - Position.BOTTOM_RIGHT, - ], - DoesNotRaise(), - ), - ( - Vector( - Point(0, 0), - Point(-10, 0), - ), - [ - [[-5, 7, -4, 5], [-8, -5, -7, -4]], - [[-5, -7, -4, -5], [-8, 5, -7, 4]], - [[-5, 7, -4, 5], [-8, 5, -7, 4]], - [[-5, -7, -4, -5], [-8, 5, -7, 4]], - [[-5, 7, -4, 5], [-8, 5, -7, 4]], - [[-5, -7, -4, -5], [-8, 5, -7, 4]], - [[-5, 7, -4, 5], [-8, 5, -7, 4]], - [[-5, -7, -4, -5], [-8, -5, -7, -4]], - ], - [ - (False, False), - (False, True), - (True, False), - (False, False), - (True, False), - (False, False), - (True, False), - (False, False), - ], - [ - (False, False), - (True, False), - (False, False), - (True, False), - (False, False), - (True, False), - (False, False), - (True, True), + [[4, 4, 6, 6], [4, 4, 6, 6]], + [[4, 4, 6, 6], [4, 4 - 10, 6, 6 - 10]], + [[4, 4, 6, 6], [4, 4, 6, 6]], + [[4, 4, 6, 6], [4, 4 - 10, 6, 6 - 10]], ], [ Position.TOP_LEFT, @@ -454,58 +406,52 @@ def test_line_zone_one_detection( Position.BOTTOM_LEFT, Position.BOTTOM_RIGHT, ], + [[False, False], [False, True], [False, False], [False, True]], + [[False, False], [False, False], [False, True], [False, False]], DoesNotRaise(), ), - ( - Vector( - Point(-5, -5), - Point(-10, -15), - ), + ( # Both cross at the same time + Vector(Point(0, 0), Point(10, 0)), [ - [[-3, -8, -2, -10], [10, 5, 12, 7]], - [[-10, -8, -2, -10], [10, 5, 12, 7]], + [[4, 4, 6, 6], [4, 4, 6, 6]], + [[4, 4 - 10, 6, 6 - 10], [4, 4 - 10, 6, 6 - 10]], + [[4, 4, 6, 6], [4, 4, 6, 6]], + [[4, 4 - 10, 6, 6 - 10], [4, 4 - 10, 6, 6 - 10]], ], - [[False, False], [True, False]], - [[False, False], [False, False]], - [Position.TOP_LEFT], + [ + Position.TOP_LEFT, + Position.TOP_RIGHT, + Position.BOTTOM_LEFT, + Position.BOTTOM_RIGHT, + ], + [[False, False], [True, True], [False, False], [True, True]], + [[False, False], [False, False], [True, True], [False, False]], DoesNotRaise(), ), - ( - Vector( - Point(0, 0), - Point(-10, 0), - ), - [[[-5, 7, -4, 5], [-8, -5, -7, -4]]], - [(False, False)], - [(False, False)], - [], # raise because of empty anchors - pytest.raises(ValueError), - ), ], ) def test_line_zone_multiple_detections( vector: Vector, - xyxy_sequence: List[List[List[int]]], - expected_crossed_in: List[bool], - expected_crossed_out: List[bool], + xyxy_sequence: List[List[List[float]]], anchors: List[Position], + expected_crossed_in: List[List[bool]], + expected_crossed_out: List[List[bool]], exception: Exception, ) -> None: - """ - Test LineZone with multiple detections. - A detection is represented by a sequence of xyxy bboxes which represent - subsequent positions of the detected object. If a line is crossed (in either - direction) by a detection it is crossed by exactly all anchors from @anchors. - """ with exception: line_zone = LineZone( start=vector.start, end=vector.end, triggering_anchors=anchors ) - for i, bboxes in enumerate(xyxy_sequence): + crossed_in_list = [] + crossed_out_list = [] + for bboxes in xyxy_sequence: detections = mock_detections( xyxy=bboxes, tracker_id=[i for i in range(0, len(bboxes))], ) crossed_in, crossed_out = line_zone.trigger(detections) - assert np.all(crossed_in == expected_crossed_in[i]) - assert np.all(crossed_out == expected_crossed_out[i]) + crossed_in_list.append(list(crossed_in)) + crossed_out_list.append(list(crossed_out)) + + assert crossed_in_list == expected_crossed_in + assert crossed_out_list == expected_crossed_out From b6ff9242fc2e094393bd8369c27edff43b1c7b16 Mon Sep 17 00:00:00 2001 From: LinasKo Date: Sat, 1 Jun 2024 16:19:09 +0300 Subject: [PATCH 141/182] LineZone tests: add extreme coordinate test --- test/detection/test_line_counter.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index 6804b20e..84977130 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -142,6 +142,17 @@ def test_calculate_region_of_interest_limits( [False, False, False, False], [False, False, False, False], ), + ( # Horizontal line, simple crossing, far away + Vector(Point(0, 0), Point(10, 0)), + [ + [4, 1e32, 6, 1e32 + 2], + [4, -1e32, 6, -1e32 + 2], + [4, 1e32, 6, 1e32 + 2], + [4, -1e32, 6, -1e32 + 2], + ], + [False, True, False, True], + [False, False, True, False], + ), ( # Crossing beside - left side Vector(Point(0, 0), Point(10, 0)), [ From 2b5d4303824275cfcc8ae85015234c012d81893f Mon Sep 17 00:00:00 2001 From: LinasKo Date: Sat, 1 Jun 2024 15:44:20 +0200 Subject: [PATCH 142/182] LineZone - does not work with movement to or from zones outside limits --- test/detection/test_line_counter.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index 84977130..3984be94 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -218,6 +218,24 @@ def test_calculate_region_of_interest_limits( # [False, False, True], # [False, True, False] # ), + # ( # Diagonal movement, from within limits to outside - does not work + # Vector(Point(0, 0), Point(10, 0)), + # [ + # [4, 1, 6, 3], + # [11, 1-20, 13, 3-20] + # ], + # [False, False], + # [False, True] + # ), + # ( # Diagonal movement, from within outside limits to inside - does not work + # Vector(Point(0, 0), Point(10, 0)), + # [ + # [11, 21, 13, 23], + # [4, -3, 6, -1], + # ], + # [False, False], + # [False, True] + # ) ], ) def test_line_zone_one_detection_default_anchors( From daf13442f35c0e3895c8500b526c262b9417879d Mon Sep 17 00:00:00 2001 From: David Redo Date: Sun, 2 Jun 2024 17:34:36 +0200 Subject: [PATCH 143/182] Updated from_mediapipe method to support two results types --- supervision/keypoint/core.py | 96 +++++++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 23 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index a6122953..b5f3b96e 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -188,18 +188,23 @@ class KeyPoints: @classmethod def from_mediapipe( - cls, mediapipe_results, resolution: Tuple[int, int] + cls, mediapipe_results, resolution_wh: Tuple[int, int] ) -> KeyPoints: """ Creates a KeyPoints instance from a - [Pose landmark detection](https://ai.google.dev/edge/mediapipe/) - inference result. + [Pose landmark detection](https://ai.google.dev/edge/mediapipe/solutions/vision/pose_landmarker/python) + inference result. + Args: - mediapipe_results (mediapipe.python.solution_base.SolutionOutputs): - The output Results from Mediapipe - resolution (Tuple[int, int]): image resolution (w, h) since mediapipe - only provides normalized coordinates + mediapipe_results + (Union[mediapipe.tasks.python.vision.pose_landmarker.PoseLandmarkerResult, + mediapipe.python.solution_base.SolutionOutputs]): + The output results from Mediapipe. It supports both: the inference result from + mp.tasks.vision.pose_landmaker.PoseLandmarker and the legacy one from + mp.solutions.pose.Pose. + resolution_wh (Tuple[int, int]): A tuple of the form `(width, height)` + representing the resolution of the frame. Returns: KeyPoints: A new KeyPoints object. @@ -210,31 +215,76 @@ class KeyPoints: import mediapipe as mp import supervision as sv - mp_pose = mp.solutions.pose image = cv2.imread() - pose = mp_pose.Pose(static_image_mode=True, min_detection_confidence=0.5) + image_height, image_width, _ = image.shape + mp_image = mp.Image( + image_format=mp.ImageFormat.SRGB, + data=cv2.cvtColor(image, cv2.COLOR_BGR2RGB), + ) + + options = mp.tasks.vision.PoseLandmarkerOptions( + base_options=mp.tasks.BaseOptions( + model_asset_path="pose_landmarker_heavy.task" + ), + running_mode=mp.tasks.vision.RunningMode.IMAGE, + num_poses=2, + ) + + with mp.tasks.vision.PoseLandmarker.create_from_options(options) as landmarker: + pose_landmarker_result = landmarker.detect(mp_image) + + keypoints = sv.KeyPoints.from_mediapipe( + pose_landmarker_result, [image_width, image_height] + ) + ``` + + ```python + import cv2 + import mediapipe as mp + import supervision as sv + + image = cv2.imread() + image_height, image_width, _ = image.shape + pose = mp.solutions.pose.Pose( + static_image_mode=True, + min_detection_confidence=0.5, + ) results = pose.process(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) - keypoints = sv.KeyPoints.from_mediapipe(results) + keypoints = sv.KeyPoints.from_mediapipe( + pose_landmarker_result, [image_width, image_height] + ) ``` """ - if mediapipe_results.pose_landmarks is None: + results = mediapipe_results.pose_landmarks + + if not isinstance(mediapipe_results.pose_landmarks, list): + if not mediapipe_results.pose_landmarks is None: + results = [ + [landmark for landmark in mediapipe_results.pose_landmarks.landmark] + ] + else: + results = [] + + if len(results) == 0: return cls.empty() - prediction_xy = [] - prediction_confidence = [] + xy = [] + confidence = [] + for pose in results: + prediction_xy = [] + prediction_confidence = [] + for landmark in pose: + prediction_xy.append( + [landmark.x * resolution_wh[0], landmark.y * resolution_wh[1]] + ) + prediction_confidence.append(landmark.visibility) - for landmark in mediapipe_results.pose_landmarks.landmark: - prediction_xy.append( - [ - min(math.floor(landmark.x * resolution[0]), resolution[0] - 1), - min(math.floor(landmark.y * resolution[1]), resolution[1] - 1), - ] - ) - prediction_confidence.append(landmark.visibility) + xy.append(prediction_xy) + confidence.append(prediction_confidence) return cls( - xy=np.array([prediction_xy], dtype=np.float32), - confidence=np.array([prediction_confidence], dtype=np.float32), + xy=np.array(xy, dtype=np.float32), + confidence=np.array(confidence, dtype=np.float32), ) @classmethod From 26d70219bf04ecf5b2f43edf519a49b1e0d84d42 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 2 Jun 2024 15:34:51 +0000 Subject: [PATCH 144/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/keypoint/core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index b5f3b96e..2d45f4d7 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -1,6 +1,5 @@ from __future__ import annotations -import math from contextlib import suppress from dataclasses import dataclass, field from typing import Any, Dict, Iterator, List, Optional, Tuple, Union @@ -258,7 +257,7 @@ class KeyPoints: results = mediapipe_results.pose_landmarks if not isinstance(mediapipe_results.pose_landmarks, list): - if not mediapipe_results.pose_landmarks is None: + if mediapipe_results.pose_landmarks is not None: results = [ [landmark for landmark in mediapipe_results.pose_landmarks.landmark] ] From 612ac6145a5ae85c74ecbff726446ce50a052952 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 01:05:47 +0000 Subject: [PATCH 145/182] :arrow_up: Bump mkdocs-git-revision-date-localized-plugin Bumps [mkdocs-git-revision-date-localized-plugin](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin) from 1.2.5 to 1.2.6. - [Release notes](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/releases) - [Commits](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/compare/v1.2.5...v1.2.6) --- updated-dependencies: - dependency-name: mkdocs-git-revision-date-localized-plugin dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 319db4a3..e78589a6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2164,13 +2164,13 @@ requests = "*" [[package]] name = "mkdocs-git-revision-date-localized-plugin" -version = "1.2.5" +version = "1.2.6" description = "Mkdocs plugin that enables displaying the localized date of the last git modification of a markdown file." optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_git_revision_date_localized_plugin-1.2.5-py3-none-any.whl", hash = "sha256:d796a18b07cfcdb154c133e3ec099d2bb5f38389e4fd54d3eb516a8a736815b8"}, - {file = "mkdocs_git_revision_date_localized_plugin-1.2.5.tar.gz", hash = "sha256:0c439816d9d0dba48e027d9d074b2b9f1d7cd179f74ba46b51e4da7bb3dc4b9b"}, + {file = "mkdocs_git_revision_date_localized_plugin-1.2.6-py3-none-any.whl", hash = "sha256:f015cb0f3894a39b33447b18e270ae391c4e25275cac5a626e80b243784e2692"}, + {file = "mkdocs_git_revision_date_localized_plugin-1.2.6.tar.gz", hash = "sha256:e432942ce4ee8aa9b9f4493e993dee9d2cc08b3ea2b40a3d6b03ca0f2a4bcaa2"}, ] [package.dependencies] From 8f198e80cde962cb9b018c93ea0b1a1f4cdb7ee3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 01:14:18 +0000 Subject: [PATCH 146/182] :arrow_up: Bump ruff from 0.4.6 to 0.4.7 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.6 to 0.4.7. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.4.6...v0.4.7) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/poetry.lock b/poetry.lock index 319db4a3..7e7fbac4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3662,28 +3662,28 @@ files = [ [[package]] name = "ruff" -version = "0.4.6" +version = "0.4.7" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ef995583a038cd4a7edf1422c9e19118e2511b8ba0b015861b4abd26ec5367c5"}, - {file = "ruff-0.4.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:602ebd7ad909eab6e7da65d3c091547781bb06f5f826974a53dbe563d357e53c"}, - {file = "ruff-0.4.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f9ced5cbb7510fd7525448eeb204e0a22cabb6e99a3cb160272262817d49786"}, - {file = "ruff-0.4.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04a80acfc862e0e1630c8b738e70dcca03f350bad9e106968a8108379e12b31f"}, - {file = "ruff-0.4.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be47700ecb004dfa3fd4dcdddf7322d4e632de3c06cd05329d69c45c0280e618"}, - {file = "ruff-0.4.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1ff930d6e05f444090a0139e4e13e1e2e1f02bd51bb4547734823c760c621e79"}, - {file = "ruff-0.4.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f13410aabd3b5776f9c5699f42b37a3a348d65498c4310589bc6e5c548dc8a2f"}, - {file = "ruff-0.4.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0cf5cc02d3ae52dfb0c8a946eb7a1d6ffe4d91846ffc8ce388baa8f627e3bd50"}, - {file = "ruff-0.4.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea3424793c29906407e3cf417f28fc33f689dacbbadfb52b7e9a809dd535dcef"}, - {file = "ruff-0.4.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1fa8561489fadf483ffbb091ea94b9c39a00ed63efacd426aae2f197a45e67fc"}, - {file = "ruff-0.4.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4d5b914818d8047270308fe3e85d9d7f4a31ec86c6475c9f418fbd1624d198e0"}, - {file = "ruff-0.4.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4f02284335c766678778475e7698b7ab83abaf2f9ff0554a07b6f28df3b5c259"}, - {file = "ruff-0.4.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3a6a0a4f4b5f54fff7c860010ab3dd81425445e37d35701a965c0248819dde7a"}, - {file = "ruff-0.4.6-py3-none-win32.whl", hash = "sha256:9018bf59b3aa8ad4fba2b1dc0299a6e4e60a4c3bc62bbeaea222679865453062"}, - {file = "ruff-0.4.6-py3-none-win_amd64.whl", hash = "sha256:a769ae07ac74ff1a019d6bd529426427c3e30d75bdf1e08bb3d46ac8f417326a"}, - {file = "ruff-0.4.6-py3-none-win_arm64.whl", hash = "sha256:735a16407a1a8f58e4c5b913ad6102722e80b562dd17acb88887685ff6f20cf6"}, - {file = "ruff-0.4.6.tar.gz", hash = "sha256:a797a87da50603f71e6d0765282098245aca6e3b94b7c17473115167d8dfb0b7"}, + {file = "ruff-0.4.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e089371c67892a73b6bb1525608e89a2aca1b77b5440acf7a71dda5dac958f9e"}, + {file = "ruff-0.4.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:10f973d521d910e5f9c72ab27e409e839089f955be8a4c8826601a6323a89753"}, + {file = "ruff-0.4.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59c3d110970001dfa494bcd95478e62286c751126dfb15c3c46e7915fc49694f"}, + {file = "ruff-0.4.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa9773c6c00f4958f73b317bc0fd125295110c3776089f6ef318f4b775f0abe4"}, + {file = "ruff-0.4.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07fc80bbb61e42b3b23b10fda6a2a0f5a067f810180a3760c5ef1b456c21b9db"}, + {file = "ruff-0.4.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fa4dafe3fe66d90e2e2b63fa1591dd6e3f090ca2128daa0be33db894e6c18648"}, + {file = "ruff-0.4.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7c0083febdec17571455903b184a10026603a1de078428ba155e7ce9358c5f6"}, + {file = "ruff-0.4.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad1b20e66a44057c326168437d680a2166c177c939346b19c0d6b08a62a37589"}, + {file = "ruff-0.4.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbf5d818553add7511c38b05532d94a407f499d1a76ebb0cad0374e32bc67202"}, + {file = "ruff-0.4.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:50e9651578b629baec3d1513b2534de0ac7ed7753e1382272b8d609997e27e83"}, + {file = "ruff-0.4.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8874a9df7766cb956b218a0a239e0a5d23d9e843e4da1e113ae1d27ee420877a"}, + {file = "ruff-0.4.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b9de9a6e49f7d529decd09381c0860c3f82fa0b0ea00ea78409b785d2308a567"}, + {file = "ruff-0.4.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:13a1768b0691619822ae6d446132dbdfd568b700ecd3652b20d4e8bc1e498f78"}, + {file = "ruff-0.4.7-py3-none-win32.whl", hash = "sha256:769e5a51df61e07e887b81e6f039e7ed3573316ab7dd9f635c5afaa310e4030e"}, + {file = "ruff-0.4.7-py3-none-win_amd64.whl", hash = "sha256:9e3ab684ad403a9ed1226894c32c3ab9c2e0718440f6f50c7c5829932bc9e054"}, + {file = "ruff-0.4.7-py3-none-win_arm64.whl", hash = "sha256:10f2204b9a613988e3484194c2c9e96a22079206b22b787605c255f130db5ed7"}, + {file = "ruff-0.4.7.tar.gz", hash = "sha256:2331d2b051dc77a289a653fcc6a42cce357087c5975738157cd966590b18b5e1"}, ] [[package]] From a5d4612402462677423784c354e0046ac34d4ac0 Mon Sep 17 00:00:00 2001 From: LinasKo Date: Mon, 3 Jun 2024 10:43:31 +0200 Subject: [PATCH 147/182] Revert Detections.merge to 0.20.0, but filter out empty detections --- supervision/detection/core.py | 19 +++++++++++++------ supervision/detection/utils.py | 20 +++++--------------- test/detection/test_core.py | 26 ++++++++++++++++++++++---- test/detection/test_utils.py | 4 ++-- 4 files changed, 42 insertions(+), 27 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index a1239d96..7699ade3 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -928,6 +928,12 @@ class Detections: array([0.1, 0.2, 0.3]) ``` """ + detections_list = [ + detections + for detections in detections_list + if detections != Detections.empty() + ] + if len(detections_list) == 0: return Detections.empty() @@ -946,12 +952,13 @@ class Detections: def stack_or_none(name: str): if all(d.__getattribute__(name) is None for d in detections_list): return None - stack_list = [ - d.__getattribute__(name) - for d in detections_list - if d.__getattribute__(name) is not None - ] - return np.vstack(stack_list) if name == "mask" else np.hstack(stack_list) + if any(d.__getattribute__(name) is None for d in detections_list): + raise ValueError(f"All or none of the '{name}' fields must be None") + return ( + np.vstack([d.__getattribute__(name) for d in detections_list]) + if name == "mask" + else np.hstack([d.__getattribute__(name) for d in detections_list]) + ) mask = stack_or_none("mask") confidence = stack_or_none("confidence") diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index b36b6853..d712bc65 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -631,6 +631,10 @@ def merge_data( if not data_list: return {} + all_keys_sets = [set(data.keys()) for data in data_list] + if not all(keys_set == all_keys_sets[0] for keys_set in all_keys_sets): + raise ValueError("All data dictionaries must have the same keys to merge.") + for data in data_list: lengths = [len(value) for value in data.values()] if len(set(lengths)) > 1: @@ -638,21 +642,7 @@ def merge_data( "All data values within a single object must have equal length." ) - keys_by_data = [set(data.keys()) for data in data_list] - keys_by_data = [keys for keys in keys_by_data if len(keys) > 0] - if not keys_by_data: - return {} - - common_keys = set.intersection(*keys_by_data) - all_keys = set.union(*keys_by_data) - if common_keys != all_keys: - raise ValueError( - f"All sv.Detections.data dictionaries must have the same keys. Common " - f"keys: {common_keys}, but some dictionaries have additional keys: " - f"{all_keys.difference(common_keys)}." - ) - - merged_data = {key: [] for key in all_keys} + merged_data = {key: [] for key in all_keys_sets[0]} for data in data_list: for key in data: merged_data[key].append(data[key]) diff --git a/test/detection/test_core.py b/test/detection/test_core.py index af1d5876..237e5e08 100644 --- a/test/detection/test_core.py +++ b/test/detection/test_core.py @@ -245,7 +245,6 @@ def test_getitem( TEST_DET_1_2, DoesNotRaise(), ), # Fields with same keys - # Fields and empty ( [TEST_DET_1, Detections.empty()], TEST_DET_1, @@ -264,9 +263,9 @@ def test_getitem( TEST_DET_1, TEST_DET_NONE, ], - TEST_DET_1, - DoesNotRaise(), - ), # Single detection and None fields (+ missing Dict keys) + None, + pytest.raises(ValueError), + ), # Empty detection, but not Detections.empty() # Errors: Non-zero-length differently defined keys & data ( [TEST_DET_1, TEST_DET_DIFFERENT_FIELDS], @@ -278,6 +277,22 @@ def test_getitem( None, pytest.raises(ValueError), ), # Non-empty detections with different data keys + ( + [ + mock_detections( + xyxy=[[10, 10, 20, 20]], + class_id=[1], + mask=[np.zeros((4, 4), dtype=bool)], + ), + Detections.empty(), + ], + mock_detections( + xyxy=[[10, 10, 20, 20]], + class_id=[1], + mask=[np.zeros((4, 4), dtype=bool)], + ), + DoesNotRaise(), + ), # Segmentation + Empty ], ) def test_merge( @@ -285,6 +300,9 @@ def test_merge( expected_result: Optional[Detections], exception: Exception, ) -> None: + print(len(detections_list)) + for det in detections_list: + print(det) with exception: result = Detections.merge(detections_list=detections_list) assert result == expected_result diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index f0f0a6b1..20c818e6 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -720,8 +720,8 @@ def test_calculate_masks_centroids( ), # two data dicts with the same field name and different length arrays values ( [{}, {"test_1": [1, 2, 3]}], - {"test_1": [1, 2, 3]}, - DoesNotRaise(), + None, + pytest.raises(ValueError), ), # two data dicts; one empty and one non-empty dict ( [{"test_1": [], "test_2": []}, {"test_1": [1, 2, 3], "test_2": [1, 2, 3]}], From 5bfd51fdec38fa163666ea8f538f44db71abcf2c Mon Sep 17 00:00:00 2001 From: LinasKo Date: Mon, 3 Jun 2024 14:01:03 +0200 Subject: [PATCH 148/182] is_empty check that includes data * We need to decide if this or from_inference data appended to empty Detections is correct --- supervision/detection/core.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 7699ade3..0abece68 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -929,9 +929,7 @@ class Detections: ``` """ detections_list = [ - detections - for detections in detections_list - if detections != Detections.empty() + detections for detections in detections_list if not is_empty(detections) ] if len(detections_list) == 0: @@ -1398,3 +1396,9 @@ def validate_fields_both_defined_or_none( f"Field '{attribute}' should be consistently None or not None in both " "Detections." ) + + +def is_empty(detections: Detections) -> bool: + empty_detections = Detections.empty() + empty_detections.data = detections.data + return detections == empty_detections From 56eb8e169819d6f7ed770be725b6f92cf00c728b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 17:47:16 +0000 Subject: [PATCH 149/182] =?UTF-8?q?chore(pre=5Fcommit):=20=E2=AC=86=20pre?= =?UTF-8?q?=5Fcommit=20autoupdate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.4.5 → v0.4.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.5...v0.4.7) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0c47f2b6..2acdf342 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.5 + rev: v0.4.7 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 38d442c176e02a6da104f7f5c48044ed8e09a0ef Mon Sep 17 00:00:00 2001 From: tc360950 Date: Tue, 4 Jun 2024 21:06:51 +0200 Subject: [PATCH 150/182] PR fixes --- supervision/detection/line_zone.py | 32 ++++++------------------------ supervision/detection/utils.py | 18 +++++++++++++++++ 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/supervision/detection/line_zone.py b/supervision/detection/line_zone.py index 4198135d..a1642aee 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -5,6 +5,7 @@ import cv2 import numpy as np from supervision.detection.core import Detections +from supervision.detection.utils import cross_product from supervision.draw.color import Color from supervision.draw.utils import draw_text from supervision.geometry.core import Point, Position, Vector @@ -158,15 +159,13 @@ class LineZone: ] ) - cross_products_1 = self._cross_product(all_anchors, self.limits[0]) - cross_products_2 = self._cross_product(all_anchors, self.limits[1]) + cross_products_1 = cross_product(all_anchors, self.limits[0]) + cross_products_2 = cross_product(all_anchors, self.limits[1]) # anchor is in limits if it's on the same side of both limit vectors - in_limits = ~np.logical_xor(cross_products_1 > 0, cross_products_2 > 0) - # Reduce array to find out if all anchors for a detection are within limits - in_limits = np.min(in_limits, axis=0) + in_limits = (cross_products_1 > 0) == (cross_products_2 > 0) + in_limits = np.all(in_limits, axis=0) - # Calculate which anchors lie to the left of the line - triggers = self._cross_product(all_anchors, self.vector) < 0 + triggers = cross_product(all_anchors, self.vector) < 0 has_any_left_trigger = np.any(triggers, axis=0) has_any_right_trigger = np.any(~triggers, axis=0) is_uniformly_triggered = ~(has_any_left_trigger & has_any_right_trigger) @@ -175,8 +174,6 @@ class LineZone: continue if not is_uniformly_triggered[i]: - # One anchor lies to the left of the line - # whilst another lies to the right continue tracker_state = has_any_left_trigger[i] @@ -197,23 +194,6 @@ class LineZone: return crossed_in, crossed_out - @staticmethod - def _cross_product(anchors: np.ndarray, vector: Vector) -> np.ndarray: - """ - Get array of cross products of each anchor with a vector. - Args: - anchors: Array of anchors of shape (number of anchors, detections, 2) - vector: Vector to calculate cross product with - - Returns: - Array of cross products of shape (number of anchors, detections) - """ - vector_at_zero = np.array( - [vector.end.x - vector.start.x, vector.end.y - vector.start.y] - ) - vector_start = np.array([vector.start.x, vector.start.y]) - return np.cross(vector_at_zero, anchors - vector_start) - class LineZoneAnnotator: def __init__( diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index aac0d627..671ac8ac 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -6,6 +6,7 @@ import numpy as np import numpy.typing as npt from supervision.config import CLASS_NAME_DATA_FIELD +from supervision.geometry.core import Vector MIN_POLYGON_POINT_COUNT = 3 @@ -966,3 +967,20 @@ def contains_multiple_segments( mask_uint8, labels, connectivity=connectivity ) return number_of_labels > 2 + + +def cross_product(anchors: np.ndarray, vector: Vector) -> np.ndarray: + """ + Get array of cross products of each anchor with a vector. + Args: + anchors: Array of anchors of shape (number of anchors, detections, 2) + vector: Vector to calculate cross product with + + Returns: + Array of cross products of shape (number of anchors, detections) + """ + vector_at_zero = np.array( + [vector.end.x - vector.start.x, vector.end.y - vector.start.y] + ) + vector_start = np.array([vector.start.x, vector.start.y]) + return np.cross(vector_at_zero, anchors - vector_start) \ No newline at end of file From 8c92072ecd39b7c6191e26cb579418d2c23ec31a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 19:07:57 +0000 Subject: [PATCH 151/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/detection/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 671ac8ac..5c9447fb 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -983,4 +983,4 @@ def cross_product(anchors: np.ndarray, vector: Vector) -> np.ndarray: [vector.end.x - vector.start.x, vector.end.y - vector.start.y] ) vector_start = np.array([vector.start.x, vector.start.y]) - return np.cross(vector_at_zero, anchors - vector_start) \ No newline at end of file + return np.cross(vector_at_zero, anchors - vector_start) From 24b1db9e2573fead97efca708c83c4d5ebe1ede7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Jun 2024 00:43:32 +0000 Subject: [PATCH 152/182] :arrow_up: Bump pytest from 8.2.1 to 8.2.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.2.1 to 8.2.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.2.1...8.2.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 4bdbe42b..4d1b52ae 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3039,13 +3039,13 @@ tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [[package]] name = "pytest" -version = "8.2.1" +version = "8.2.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, - {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, + {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, + {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, ] [package.dependencies] From c27a28235efb0f97a20001e0dfb89bf0a39aabf9 Mon Sep 17 00:00:00 2001 From: LinasKo Date: Wed, 5 Jun 2024 10:24:25 +0200 Subject: [PATCH 153/182] LineZone: Remove comment --- supervision/detection/line_zone.py | 1 - 1 file changed, 1 deletion(-) diff --git a/supervision/detection/line_zone.py b/supervision/detection/line_zone.py index a1642aee..facb6aff 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -161,7 +161,6 @@ class LineZone: cross_products_1 = cross_product(all_anchors, self.limits[0]) cross_products_2 = cross_product(all_anchors, self.limits[1]) - # anchor is in limits if it's on the same side of both limit vectors in_limits = (cross_products_1 > 0) == (cross_products_2 > 0) in_limits = np.all(in_limits, axis=0) From 57fa637eea13885d81afd1260389dfd92d3f138e Mon Sep 17 00:00:00 2001 From: LinasKo Date: Wed, 5 Jun 2024 12:18:57 +0200 Subject: [PATCH 154/182] Move is_empty into Detections --- supervision/detection/core.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 0abece68..100d21d0 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -878,6 +878,14 @@ class Detections: class_id=np.array([], dtype=int), ) + def is_empty(self) -> bool: + """ + Returns `True` if the `Detections` object is considered empty. + """ + empty_detections = Detections.empty() + empty_detections.data = self.data + return self == empty_detections + @classmethod def merge(cls, detections_list: List[Detections]) -> Detections: """ @@ -929,7 +937,7 @@ class Detections: ``` """ detections_list = [ - detections for detections in detections_list if not is_empty(detections) + detections for detections in detections_list if not detections.is_empty() ] if len(detections_list) == 0: @@ -1396,9 +1404,3 @@ def validate_fields_both_defined_or_none( f"Field '{attribute}' should be consistently None or not None in both " "Detections." ) - - -def is_empty(detections: Detections) -> bool: - empty_detections = Detections.empty() - empty_detections.data = detections.data - return detections == empty_detections From ca482967c46aade16f32be0a1653e358c1a6bf1a Mon Sep 17 00:00:00 2001 From: LinasKo Date: Wed, 5 Jun 2024 12:28:09 +0200 Subject: [PATCH 155/182] Add note that merge ignores empty detections --- supervision/detection/core.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 100d21d0..7b2e8ba9 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -898,6 +898,10 @@ class Detections: For example, if merging Detections with 3 and 4 detected objects, this method will return a Detections with 7 objects (7 entries in `xyxy`, `mask`, etc). + !!! Note + + When merging, empty `Detections` objects are ignored. + Args: detections_list (List[Detections]): A list of Detections objects to merge. From c28c93bd4fa8b96d5f64b7d5cfd1cb530541533d Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Wed, 5 Jun 2024 15:40:28 +0200 Subject: [PATCH 156/182] non-max-merging visualization + `from_lmm` updates --- supervision/detection/core.py | 4 +++- supervision/detection/lmm.py | 2 +- test/detection/test_lmm.py | 22 +++++++++++++++++++++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index a1239d96..9fbb357d 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -1222,7 +1222,9 @@ class Detections: Raises: AssertionError: If `confidence` is None or `class_id` is None and class_agnostic is False. - """ + + ![non-max-merging](https://media.roboflow.com/supervision-docs/non-max-merging.png){ align=center width="800" } + """ # noqa: E501 // docs if len(self) == 0: return self diff --git a/supervision/detection/lmm.py b/supervision/detection/lmm.py index 0278fc00..5f61db0a 100644 --- a/supervision/detection/lmm.py +++ b/supervision/detection/lmm.py @@ -41,7 +41,7 @@ def from_paligemma( ) -> Tuple[np.ndarray, Optional[np.ndarray], np.ndarray]: w, h = resolution_wh pattern = re.compile( - r"(?) ([\w\s]+)" + r"(?) ([\w\s\-]+)" ) matches = pattern.findall(result) matches = np.array(matches) if matches else np.empty((0, 5)) diff --git a/test/detection/test_lmm.py b/test/detection/test_lmm.py index 129aa44b..4448d8db 100644 --- a/test/detection/test_lmm.py +++ b/test/detection/test_lmm.py @@ -76,7 +76,27 @@ from supervision.detection.lmm import from_paligemma None, np.array(["black cat"]).astype(np.dtype("U")), ), - ), # correct response; no classes + ), # correct response; class name with space; no classes + ( + " black-cat", + (1000, 1000), + None, + ( + np.array([[250.0, 250.0, 750.0, 750.0]]), + None, + np.array(["black-cat"]).astype(np.dtype("U")), + ), + ), # correct response; class name with hyphen; no classes + ( + " black_cat", + (1000, 1000), + None, + ( + np.array([[250.0, 250.0, 750.0, 750.0]]), + None, + np.array(["black_cat"]).astype(np.dtype("U")), + ), + ), # correct response; class name with underscore; no classes ( " cat ;", (1000, 1000), From ceecb8bbd7666f9a6e882b8332d610805c42ffe6 Mon Sep 17 00:00:00 2001 From: LinasKo Date: Wed, 5 Jun 2024 15:42:00 +0200 Subject: [PATCH 157/182] Cleanup: Remove print statements from tests --- test/detection/test_core.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/detection/test_core.py b/test/detection/test_core.py index 237e5e08..300d6dfe 100644 --- a/test/detection/test_core.py +++ b/test/detection/test_core.py @@ -300,9 +300,6 @@ def test_merge( expected_result: Optional[Detections], exception: Exception, ) -> None: - print(len(detections_list)) - for det in detections_list: - print(det) with exception: result = Detections.merge(detections_list=detections_list) assert result == expected_result From a77a2ba8aa8ed6b05f587e38d6b481b80a31b785 Mon Sep 17 00:00:00 2001 From: LinasKo Date: Wed, 5 Jun 2024 16:27:53 +0200 Subject: [PATCH 158/182] LineZone: uncomment unit tests, set expected results --- test/detection/test_line_counter.py | 71 +++++++++++++---------------- 1 file changed, 32 insertions(+), 39 deletions(-) diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index 3984be94..66118e97 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -197,45 +197,38 @@ def test_calculate_region_of_interest_limits( [False, False, False, False], [False, False, False, False], ), - # ( # Diagonal crossing of a straight line - does not work. - # Vector(Point(0, 0), Point(10, 0)), - # [ - # [-4, 4, -2, 8], - # [-4 + 16, -4, -2 + 16, -6], - # [-4, 4, -2, 8], - # [-4 + 16, -4, -2 + 16, -6] - # ], - # [False, False, True, False], - # [False, True, False, True], - # ), - # ( # V-shaped crossing - does not work. - # Vector(Point(0, 0), Point(10, 0)), - # [ - # [-3, 6, -1, 8], - # [4, -6, 6, -4], - # [11, 6, 13, 8] - # ], - # [False, False, True], - # [False, True, False] - # ), - # ( # Diagonal movement, from within limits to outside - does not work - # Vector(Point(0, 0), Point(10, 0)), - # [ - # [4, 1, 6, 3], - # [11, 1-20, 13, 3-20] - # ], - # [False, False], - # [False, True] - # ), - # ( # Diagonal movement, from within outside limits to inside - does not work - # Vector(Point(0, 0), Point(10, 0)), - # [ - # [11, 21, 13, 23], - # [4, -3, 6, -1], - # ], - # [False, False], - # [False, True] - # ) + ( # V-shaped crossing from outside limits - not supported. + Vector(Point(0, 0), Point(10, 0)), + [[-3, 6, -1, 8], [4, -6, 6, -4], [11, 6, 13, 8]], + [False, False, False], + [False, False, False], + ), + ( # Diagonal movement, from within limits to outside - not supported + Vector(Point(0, 0), Point(10, 0)), + [[4, 1, 6, 3], [11, 1 - 20, 13, 3 - 20]], + [False, False], + [False, False], + ), + ( # Diagonal movement, from outside limits to within - not supported + Vector(Point(0, 0), Point(10, 0)), + [ + [11, 21, 13, 23], + [4, -3, 6, -1], + ], + [False, False], + [False, False], + ), + ( # Diagonal crossing, from outside to outside limits - not supported. + Vector(Point(0, 0), Point(10, 0)), + [ + [-4, 4, -2, 8], + [-4 + 16, -4, -2 + 16, -6], + [-4, 4, -2, 8], + [-4 + 16, -4, -2 + 16, -6], + ], + [False, False, False, False], + [False, False, False, False], + ), ], ) def test_line_zone_one_detection_default_anchors( From 3cee266f7111266978117521590626a03cbb26eb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 5 Jun 2024 14:33:06 +0000 Subject: [PATCH 159/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=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 9fbb357d..53d576fb 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -1222,7 +1222,7 @@ class Detections: Raises: AssertionError: If `confidence` is None or `class_id` is None and class_agnostic is False. - + ![non-max-merging](https://media.roboflow.com/supervision-docs/non-max-merging.png){ align=center width="800" } """ # noqa: E501 // docs if len(self) == 0: From b476acc1f3ac4451fca265277a8967c7c4ef1266 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Wed, 5 Jun 2024 16:52:18 +0200 Subject: [PATCH 160/182] bump version from `0.21.0rc5` to `0.21.0` --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 640d462e..59d4176d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "supervision" -version = "0.21.0rc5" +version = "0.21.0" description = "A set of easy-to-use utils that will come in handy in any Computer Vision project" authors = ["Piotr Skalski "] maintainers = ["Piotr Skalski "] From 9a42372ff649605e9dd3ea8291bf164cf9b1a1ba Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Wed, 5 Jun 2024 17:37:01 +0200 Subject: [PATCH 161/182] changelog updated --- docs/changelog.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 47d160aa..024edd1a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,83 @@ +### 0.21.0 Jun 5, 2024 + +- Added [#500](https://github.com/roboflow/supervision/pull/500): [`sv.Detections.with_nmm`](https://supervision.roboflow.com/develop/detection/core/#supervision.detection.core.Detections.with_nmm) to perform non-maximum merging on the current set of object detections. + +- Added [#1221](https://github.com/roboflow/supervision/pull/1221): [`sv.Detections.from_lmm`](https://supervision.roboflow.com/develop/detection/core/#supervision.detection.core.Detections.from_lmm) allowing to parse Large Multimodal Model (LMM) text result into [`sv.Detections`](https://supervision.roboflow.com/develop/detection/core/) object. For now `from_lmm` supports only [PaliGemma](https://colab.research.google.com/github/roboflow-ai/notebooks/blob/main/notebooks/how-to-finetune-paligemma-on-detection-dataset.ipynb) result parsing. + +```python +import supervision as sv + +paligemma_result = " cat" +detections = sv.Detections.from_lmm( + sv.LMM.PALIGEMMA, + paligemma_result, + resolution_wh=(1000, 1000), + classes=['cat', 'dog'] +) +detections.xyxy +# array([[250., 250., 750., 750.]]) + +detections.class_id +# array([0]) +``` + +- Added [#1236](https://github.com/roboflow/supervision/pull/1236): [`sv.VertexLabelAnnotator`](https://supervision.roboflow.com/develop/keypoint/annotators/#supervision.keypoint.annotators.EdgeAnnotator.annotate) allowing to annotate every vertex of a keypoint skeleton with custom text and color. + +```python +import supervision as sv + +image = ... +key_points = sv.KeyPoints(...) + +edge_annotator = sv.EdgeAnnotator( + color=sv.Color.GREEN, + thickness=5 +) +annotated_frame = edge_annotator.annotate( + scene=image.copy(), + key_points=key_points +) +``` + +- Added [#1147](https://github.com/roboflow/supervision/pull/1147): [`sv.KeyPoints.from_inference`](https://supervision.roboflow.com/develop/keypoint/core/#supervision.keypoint.core.KeyPoints.from_inference) allowing to create [`sv.KeyPoints`](https://supervision.roboflow.com/develop/keypoint/core/#supervision.keypoint.core.KeyPoints) from [Inference](https://github.com/roboflow/inference) result. + +- Added [#1138](https://github.com/roboflow/supervision/pull/1138): [`sv.KeyPoints.from_yolo_nas`](https://supervision.roboflow.com/develop/keypoint/core/#supervision.keypoint.core.KeyPoints.from_yolo_nas) allowing to create [`sv.KeyPoints`](https://supervision.roboflow.com/develop/keypoint/core/#supervision.keypoint.core.KeyPoints) from [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) result. + +- Added [#1163](https://github.com/roboflow/supervision/pull/1163): [`sv.mask_to_rle`](https://supervision.roboflow.com/develop/datasets/utils/#supervision.dataset.utils.rle_to_mask) and [`sv.rle_to_mask`](https://supervision.roboflow.com/develop/datasets/utils/#supervision.dataset.utils.rle_to_mask) allowing for easy conversion between mask and rle formats. + +- Changed [#1236](https://github.com/roboflow/supervision/pull/1236): [`sv.InferenceSlicer`](https://supervision.roboflow.com/develop/detection/tools/inference_slicer/) allowing to select overlap filtering strategy (`NONE`, `NON_MAX_SUPPRESSION` and `NON_MAX_MERGE`). + +- Changed [#1178](https://github.com/roboflow/supervision/pull/1178): [`sv.InferenceSlicer`](https://supervision.roboflow.com/develop/detection/tools/inference_slicer/) adding instance segmentation model support. + +```python +import cv2 +import numpy as np +import supervision as sv +from inference import get_model + +model = get_model(model_id="yolov8x-seg-640") +image = cv2.imread() + +def callback(image_slice: np.ndarray) -> sv.Detections: + results = model.infer(image_slice)[0] + return sv.Detections.from_inference(results) + +slicer = sv.InferenceSlicer(callback = callback) +detections = slicer(image) + +mask_annotator = sv.MaskAnnotator() +label_annotator = sv.LabelAnnotator() + +annotated_image = mask_annotator.annotate( + scene=image, detections=detections) +annotated_image = label_annotator.annotate( + scene=annotated_image, detections=detections) +``` + +- Changed [#1228](https://github.com/roboflow/supervision/pull/1228): [`sv.LineZone`](https://supervision.roboflow.com/develop/detection/tools/line_zone/) making it 10-20 times faster, depending on the use case. + +- Changed [#1163](https://github.com/roboflow/supervision/pull/1163): [`sv.DetectionDataset.from_coco`](https://supervision.roboflow.com/develop/datasets/core/#supervision.dataset.core.DetectionDataset.from_coco) and [`sv.DetectionDataset.as_coco`](https://supervision.roboflow.com/develop/datasets/core/#supervision.dataset.core.DetectionDataset.as_coco) adding support for run-length encoding (RLE) mask format. + ### 0.20.0 April 24, 2024 - Added [#1128](https://github.com/roboflow/supervision/pull/1128): [`sv.KeyPoints`](/0.20.0/keypoint/core/#supervision.keypoint.core.KeyPoints) to provide initial support for pose estimation and broader keypoint detection models. From a3f299ceb04155c7385ed0772092d07a2cdb1f4f Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Wed, 5 Jun 2024 17:40:39 +0200 Subject: [PATCH 162/182] update docs headers --- mkdocs.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 19d6a4fd..96728971 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -35,15 +35,15 @@ extra_css: nav: - - Home: index.md - - How to: + - Supervision: index.md + - Learn: - Detect and Annotate: how_to/detect_and_annotate.md - Save Detections: how_to/save_detections.md - Filter Detections: how_to/filter_detections.md - Detect Small Objects: how_to/detect_small_objects.md - Track Objects on Video: how_to/track_objects.md - - API: + - Reference - Code API: - Detection and Segmentation: - Core: detection/core.md - Annotators: detection/annotators.md @@ -79,7 +79,7 @@ nav: - Contributing: contributing.md - Code of Conduct: code_of_conduct.md - License: license.md - - Changelog: + - Release Notes: - Changelog: changelog.md - Deprecated: deprecated.md From 13b46dda67f646b334c3a66535d620ef5e6dc64e Mon Sep 17 00:00:00 2001 From: LinasKo Date: Wed, 5 Jun 2024 17:44:25 +0200 Subject: [PATCH 163/182] Update "New" tags in docs, fix typo in one heading --- docs/cookbooks.md | 1 - docs/detection/annotators.md | 1 - docs/detection/tools/line_zone.md | 1 + docs/detection/tools/save_detections.md | 1 - docs/detection/utils.md | 1 - docs/how_to/detect_and_annotate.md | 1 - docs/how_to/save_detections.md | 1 - docs/trackers.md | 1 - docs/utils/image.md | 3 +-- docs/utils/iterables.md | 1 - 10 files changed, 2 insertions(+), 10 deletions(-) diff --git a/docs/cookbooks.md b/docs/cookbooks.md index dd963edb..6f87958f 100644 --- a/docs/cookbooks.md +++ b/docs/cookbooks.md @@ -1,7 +1,6 @@ --- template: cookbooks.html comments: true -status: new hide: - navigation - toc diff --git a/docs/detection/annotators.md b/docs/detection/annotators.md index 958f2a74..4d912cae 100644 --- a/docs/detection/annotators.md +++ b/docs/detection/annotators.md @@ -1,6 +1,5 @@ --- comments: true -status: new --- # Annotators diff --git a/docs/detection/tools/line_zone.md b/docs/detection/tools/line_zone.md index 8d13822c..22e9c0a8 100644 --- a/docs/detection/tools/line_zone.md +++ b/docs/detection/tools/line_zone.md @@ -1,5 +1,6 @@ --- comments: true +status: new ---
diff --git a/docs/detection/tools/save_detections.md b/docs/detection/tools/save_detections.md index a82ce5df..a24cee57 100644 --- a/docs/detection/tools/save_detections.md +++ b/docs/detection/tools/save_detections.md @@ -1,6 +1,5 @@ --- comments: true -status: new --- # Save Detections diff --git a/docs/detection/utils.md b/docs/detection/utils.md index 369746a3..ea98c868 100644 --- a/docs/detection/utils.md +++ b/docs/detection/utils.md @@ -1,6 +1,5 @@ --- comments: true -status: new --- # Detection Utils diff --git a/docs/how_to/detect_and_annotate.md b/docs/how_to/detect_and_annotate.md index a9a4405e..52e3174b 100644 --- a/docs/how_to/detect_and_annotate.md +++ b/docs/how_to/detect_and_annotate.md @@ -1,6 +1,5 @@ --- comments: true -status: new --- # Detect and Annotate diff --git a/docs/how_to/save_detections.md b/docs/how_to/save_detections.md index 94de6c61..05d5faad 100644 --- a/docs/how_to/save_detections.md +++ b/docs/how_to/save_detections.md @@ -1,6 +1,5 @@ --- comments: true -status: new --- # Save Detections diff --git a/docs/trackers.md b/docs/trackers.md index 47f70061..cb44441f 100644 --- a/docs/trackers.md +++ b/docs/trackers.md @@ -1,6 +1,5 @@ --- comments: true -status: new --- # ByteTrack diff --git a/docs/utils/image.md b/docs/utils/image.md index 8f170d35..8e39136a 100644 --- a/docs/utils/image.md +++ b/docs/utils/image.md @@ -1,6 +1,5 @@ --- comments: true -status: new --- # Image Utils @@ -12,7 +11,7 @@ status: new :::supervision.utils.image.crop_image :::supervision.utils.image.scale_image diff --git a/docs/utils/iterables.md b/docs/utils/iterables.md index b65cd954..5ae92dc9 100644 --- a/docs/utils/iterables.md +++ b/docs/utils/iterables.md @@ -1,6 +1,5 @@ --- comments: true -status: new --- # Iterables Utils From 2d5de66a9baaca93a13c858a624b2fcbe3462f8c Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Wed, 5 Jun 2024 17:53:38 +0200 Subject: [PATCH 164/182] code snippets updates --- supervision/detection/utils.py | 38 +++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index f39a8e03..38a1bba8 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -155,6 +155,25 @@ def clip_boxes(xyxy: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray: np.ndarray: A numpy array of shape `(N, 4)` where each row corresponds to a bounding box with coordinates clipped to fit within the frame resolution. + + Examples: + ```python + import numpy as np + import supervision as sv + + xyxy = np.array([ + [10, 20, 300, 200], + [15, 25, 350, 450], + [-10, -20, 30, 40] + ]) + + sv.clip_boxes(xyxy=xyxy, resolution_wh=(320, 240)) + # array([ + # [ 10, 20, 300, 200], + # [ 15, 25, 320, 240], + # [ 0, 0, 30, 40] + # ]) + ``` """ result = np.copy(xyxy) width, height = resolution_wh @@ -181,6 +200,23 @@ def pad_boxes(xyxy: np.ndarray, px: int, py: Optional[int] = None) -> np.ndarray np.ndarray: A numpy array of shape `(N, 4)` where each row corresponds to a bounding box with coordinates padded according to the provided padding values. + + Examples: + ```python + import numpy as np + import supervision as sv + + xyxy = np.array([ + [10, 20, 30, 40], + [15, 25, 35, 45] + ]) + + sv.pad_boxes(xyxy=xyxy, px=5, py=10) + # array([ + # [ 5, 10, 35, 50], + # [10, 15, 40, 55] + # ]) + ``` """ if py is None: py = px @@ -553,7 +589,7 @@ def scale_boxes( [30, 30, 40, 40] ]) - scaled_bb = sv.scale_boxes(xyxy=xyxy, factor=1.5) + sv.scale_boxes(xyxy=xyxy, factor=1.5) # array([ # [ 7.5, 7.5, 22.5, 22.5], # [27.5, 27.5, 42.5, 42.5] From 0200fd307bef332cd406abc99ad6fa86de3f75f8 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Wed, 5 Jun 2024 18:00:54 +0200 Subject: [PATCH 165/182] mark `InferenceSlicer` with new status --- docs/detection/tools/inference_slicer.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/detection/tools/inference_slicer.md b/docs/detection/tools/inference_slicer.md index 5d5d08bc..7a5d3e57 100644 --- a/docs/detection/tools/inference_slicer.md +++ b/docs/detection/tools/inference_slicer.md @@ -1,5 +1,6 @@ --- comments: true +status: new --- # InferenceSlicer From 560760993aaa49fa2b438b9c8bbab0ed1a787d42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Jun 2024 01:31:57 +0000 Subject: [PATCH 166/182] :arrow_up: Bump tox from 4.15.0 to 4.15.1 Bumps [tox](https://github.com/tox-dev/tox) from 4.15.0 to 4.15.1. - [Release notes](https://github.com/tox-dev/tox/releases) - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/tox/compare/4.15.0...4.15.1) --- updated-dependencies: - dependency-name: tox dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 4d1b52ae..b68ced36 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3948,13 +3948,13 @@ files = [ [[package]] name = "tox" -version = "4.15.0" +version = "4.15.1" description = "tox is a generic virtualenv management and test command line tool" optional = false python-versions = ">=3.8" files = [ - {file = "tox-4.15.0-py3-none-any.whl", hash = "sha256:300055f335d855b2ab1b12c5802de7f62a36d4fd53f30bd2835f6a201dda46ea"}, - {file = "tox-4.15.0.tar.gz", hash = "sha256:7a0beeef166fbe566f54f795b4906c31b428eddafc0102ac00d20998dd1933f6"}, + {file = "tox-4.15.1-py3-none-any.whl", hash = "sha256:f00a5dc4222b358e69694e47e3da0227ac41253509bca9f45aa8f012053e8d9d"}, + {file = "tox-4.15.1.tar.gz", hash = "sha256:53a092527d65e873e39213ebd4bd027a64623320b6b0326136384213f95b7076"}, ] [package.dependencies] From 0739e24b2552d4aba1d2a7a88ea143c8f6046049 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Jun 2024 01:33:50 +0000 Subject: [PATCH 167/182] :arrow_up: Bump ruff from 0.4.7 to 0.4.8 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.7 to 0.4.8. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.4.7...v0.4.8) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/poetry.lock b/poetry.lock index 4d1b52ae..cdb69a75 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3662,28 +3662,28 @@ files = [ [[package]] name = "ruff" -version = "0.4.7" +version = "0.4.8" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e089371c67892a73b6bb1525608e89a2aca1b77b5440acf7a71dda5dac958f9e"}, - {file = "ruff-0.4.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:10f973d521d910e5f9c72ab27e409e839089f955be8a4c8826601a6323a89753"}, - {file = "ruff-0.4.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59c3d110970001dfa494bcd95478e62286c751126dfb15c3c46e7915fc49694f"}, - {file = "ruff-0.4.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa9773c6c00f4958f73b317bc0fd125295110c3776089f6ef318f4b775f0abe4"}, - {file = "ruff-0.4.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07fc80bbb61e42b3b23b10fda6a2a0f5a067f810180a3760c5ef1b456c21b9db"}, - {file = "ruff-0.4.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fa4dafe3fe66d90e2e2b63fa1591dd6e3f090ca2128daa0be33db894e6c18648"}, - {file = "ruff-0.4.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7c0083febdec17571455903b184a10026603a1de078428ba155e7ce9358c5f6"}, - {file = "ruff-0.4.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad1b20e66a44057c326168437d680a2166c177c939346b19c0d6b08a62a37589"}, - {file = "ruff-0.4.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbf5d818553add7511c38b05532d94a407f499d1a76ebb0cad0374e32bc67202"}, - {file = "ruff-0.4.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:50e9651578b629baec3d1513b2534de0ac7ed7753e1382272b8d609997e27e83"}, - {file = "ruff-0.4.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8874a9df7766cb956b218a0a239e0a5d23d9e843e4da1e113ae1d27ee420877a"}, - {file = "ruff-0.4.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b9de9a6e49f7d529decd09381c0860c3f82fa0b0ea00ea78409b785d2308a567"}, - {file = "ruff-0.4.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:13a1768b0691619822ae6d446132dbdfd568b700ecd3652b20d4e8bc1e498f78"}, - {file = "ruff-0.4.7-py3-none-win32.whl", hash = "sha256:769e5a51df61e07e887b81e6f039e7ed3573316ab7dd9f635c5afaa310e4030e"}, - {file = "ruff-0.4.7-py3-none-win_amd64.whl", hash = "sha256:9e3ab684ad403a9ed1226894c32c3ab9c2e0718440f6f50c7c5829932bc9e054"}, - {file = "ruff-0.4.7-py3-none-win_arm64.whl", hash = "sha256:10f2204b9a613988e3484194c2c9e96a22079206b22b787605c255f130db5ed7"}, - {file = "ruff-0.4.7.tar.gz", hash = "sha256:2331d2b051dc77a289a653fcc6a42cce357087c5975738157cd966590b18b5e1"}, + {file = "ruff-0.4.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7663a6d78f6adb0eab270fa9cf1ff2d28618ca3a652b60f2a234d92b9ec89066"}, + {file = "ruff-0.4.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eeceb78da8afb6de0ddada93112869852d04f1cd0f6b80fe464fd4e35c330913"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aad360893e92486662ef3be0a339c5ca3c1b109e0134fcd37d534d4be9fb8de3"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:284c2e3f3396fb05f5f803c9fffb53ebbe09a3ebe7dda2929ed8d73ded736deb"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7354f921e3fbe04d2a62d46707e569f9315e1a613307f7311a935743c51a764"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:72584676164e15a68a15778fd1b17c28a519e7a0622161eb2debdcdabdc71883"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9678d5c9b43315f323af2233a04d747409d1e3aa6789620083a82d1066a35199"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704977a658131651a22b5ebeb28b717ef42ac6ee3b11e91dc87b633b5d83142b"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05f8d6f0c3cce5026cecd83b7a143dcad503045857bc49662f736437380ad45"}, + {file = "ruff-0.4.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ea874950daca5697309d976c9afba830d3bf0ed66887481d6bca1673fc5b66a"}, + {file = "ruff-0.4.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fc95aac2943ddf360376be9aa3107c8cf9640083940a8c5bd824be692d2216dc"}, + {file = "ruff-0.4.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:384154a1c3f4bf537bac69f33720957ee49ac8d484bfc91720cc94172026ceed"}, + {file = "ruff-0.4.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e9d5ce97cacc99878aa0d084c626a15cd21e6b3d53fd6f9112b7fc485918e1fa"}, + {file = "ruff-0.4.8-py3-none-win32.whl", hash = "sha256:6d795d7639212c2dfd01991259460101c22aabf420d9b943f153ab9d9706e6a9"}, + {file = "ruff-0.4.8-py3-none-win_amd64.whl", hash = "sha256:e14a3a095d07560a9d6769a72f781d73259655919d9b396c650fc98a8157555d"}, + {file = "ruff-0.4.8-py3-none-win_arm64.whl", hash = "sha256:14019a06dbe29b608f6b7cbcec300e3170a8d86efaddb7b23405cb7f7dcaf780"}, + {file = "ruff-0.4.8.tar.gz", hash = "sha256:16d717b1d57b2e2fd68bd0bf80fb43931b79d05a7131aa477d66fc40fbd86268"}, ] [[package]] From bcc1bc1ff2915f33b16580969f0478292fdbe2ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Jun 2024 23:39:07 +0000 Subject: [PATCH 168/182] :arrow_up: Bump tornado from 6.4 to 6.4.1 Bumps [tornado](https://github.com/tornadoweb/tornado) from 6.4 to 6.4.1. - [Changelog](https://github.com/tornadoweb/tornado/blob/master/docs/releases.rst) - [Commits](https://github.com/tornadoweb/tornado/compare/v6.4.0...v6.4.1) --- updated-dependencies: - dependency-name: tornado dependency-type: indirect ... Signed-off-by: dependabot[bot] --- poetry.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/poetry.lock b/poetry.lock index aa6da1e6..3fd66d46 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3928,22 +3928,22 @@ files = [ [[package]] name = "tornado" -version = "6.4" +version = "6.4.1" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." optional = false -python-versions = ">= 3.8" +python-versions = ">=3.8" files = [ - {file = "tornado-6.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0"}, - {file = "tornado-6.4-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:27787de946a9cffd63ce5814c33f734c627a87072ec7eed71f7fc4417bb16263"}, - {file = "tornado-6.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7894c581ecdcf91666a0912f18ce5e757213999e183ebfc2c3fdbf4d5bd764e"}, - {file = "tornado-6.4-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e43bc2e5370a6a8e413e1e1cd0c91bedc5bd62a74a532371042a18ef19e10579"}, - {file = "tornado-6.4-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0251554cdd50b4b44362f73ad5ba7126fc5b2c2895cc62b14a1c2d7ea32f212"}, - {file = "tornado-6.4-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fd03192e287fbd0899dd8f81c6fb9cbbc69194d2074b38f384cb6fa72b80e9c2"}, - {file = "tornado-6.4-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:88b84956273fbd73420e6d4b8d5ccbe913c65d31351b4c004ae362eba06e1f78"}, - {file = "tornado-6.4-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:71ddfc23a0e03ef2df1c1397d859868d158c8276a0603b96cf86892bff58149f"}, - {file = "tornado-6.4-cp38-abi3-win32.whl", hash = "sha256:6f8a6c77900f5ae93d8b4ae1196472d0ccc2775cc1dfdc9e7727889145c45052"}, - {file = "tornado-6.4-cp38-abi3-win_amd64.whl", hash = "sha256:10aeaa8006333433da48dec9fe417877f8bcc21f48dda8d661ae79da357b2a63"}, - {file = "tornado-6.4.tar.gz", hash = "sha256:72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee"}, + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, + {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, + {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, + {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, ] [[package]] From 99957716042637b9c3e35d908f3275b33b60b483 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Jun 2024 01:07:22 +0000 Subject: [PATCH 169/182] :arrow_up: Bump mkdocs-material from 9.5.25 to 9.5.26 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.5.25 to 9.5.26. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.25...9.5.26) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index aa6da1e6..9a1efca7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2199,13 +2199,13 @@ pygments = ">2.12.0" [[package]] name = "mkdocs-material" -version = "9.5.25" +version = "9.5.26" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.25-py3-none-any.whl", hash = "sha256:68fdab047a0b9bfbefe79ce267e8a7daaf5128bcf7867065fcd201ee335fece1"}, - {file = "mkdocs_material-9.5.25.tar.gz", hash = "sha256:d0662561efb725b712207e0ee01f035ca15633f29a64628e24f01ec99d7078f4"}, + {file = "mkdocs_material-9.5.26-py3-none-any.whl", hash = "sha256:5d01fb0aa1c7946a1e3ae8689aa2b11a030621ecb54894e35aabb74c21016312"}, + {file = "mkdocs_material-9.5.26.tar.gz", hash = "sha256:56aeb91d94cffa43b6296fa4fbf0eb7c840136e563eecfd12c2d9e92e50ba326"}, ] [package.dependencies] From 165c77d830e818e9bc5cd2f4966e099f83217064 Mon Sep 17 00:00:00 2001 From: mqasim41 Date: Fri, 7 Jun 2024 18:03:12 +0500 Subject: [PATCH 170/182] Fix Grammatical Mistake In CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0003f1aa..d32ab0cf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,7 +64,7 @@ To run the pre-commit tool, follow these steps: 3. Run the command `pre-commit run --all-files`. This will execute the pre-commit hooks configured for this project against the modified files. If any issues are found, the pre-commit tool will provide feedback on how to resolve them. Make the necessary changes and re-run the pre-commit command until all issues are resolved. -4. You can also install pre-commit as a git hook by execute `pre-commit install`. Every time you made `git commit` pre-commit run automatically for you. +4. You can also install pre-commit as a git hook by executing `pre-commit install`. Every time you do a `git commit` pre-commit run automatically for you. ### Docstrings From 6f5b477ac0c1af44134623d06ed21c4fd5c9dfb7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 01:16:42 +0000 Subject: [PATCH 171/182] :arrow_up: Bump notebook from 7.2.0 to 7.2.1 Bumps [notebook](https://github.com/jupyter/notebook) from 7.2.0 to 7.2.1. - [Release notes](https://github.com/jupyter/notebook/releases) - [Changelog](https://github.com/jupyter/notebook/blob/@jupyter-notebook/tree@7.2.1/CHANGELOG.md) - [Commits](https://github.com/jupyter/notebook/compare/@jupyter-notebook/tree@7.2.0...@jupyter-notebook/tree@7.2.1) --- updated-dependencies: - dependency-name: notebook dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index ef7bde4e..ecad075d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2485,13 +2485,13 @@ setuptools = "*" [[package]] name = "notebook" -version = "7.2.0" +version = "7.2.1" description = "Jupyter Notebook - A web-based notebook environment for interactive computing" optional = false python-versions = ">=3.8" files = [ - {file = "notebook-7.2.0-py3-none-any.whl", hash = "sha256:b4752d7407d6c8872fc505df0f00d3cae46e8efb033b822adacbaa3f1f3ce8f5"}, - {file = "notebook-7.2.0.tar.gz", hash = "sha256:34a2ba4b08ad5d19ec930db7484fb79746a1784be9e1a5f8218f9af8656a141f"}, + {file = "notebook-7.2.1-py3-none-any.whl", hash = "sha256:f45489a3995746f2195a137e0773e2130960b51c9ac3ce257dbc2705aab3a6ca"}, + {file = "notebook-7.2.1.tar.gz", hash = "sha256:4287b6da59740b32173d01d641f763d292f49c30e7a51b89c46ba8473126341e"}, ] [package.dependencies] From 68c2504d61a6546fb4be7ccb02a35ddee6eb3636 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 17:50:26 +0000 Subject: [PATCH 172/182] =?UTF-8?q?chore(pre=5Fcommit):=20=E2=AC=86=20pre?= =?UTF-8?q?=5Fcommit=20autoupdate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.4.7 → v0.4.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.7...v0.4.8) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2acdf342..fb85a294 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.7 + rev: v0.4.8 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From fd1a07cb6b094b1f206c04706a17180114c40988 Mon Sep 17 00:00:00 2001 From: LinasKo Date: Tue, 11 Jun 2024 13:14:38 +0300 Subject: [PATCH 173/182] Adjust long lines --- supervision/keypoint/core.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index 2d45f4d7..ac889bc5 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -199,9 +199,9 @@ class KeyPoints: mediapipe_results (Union[mediapipe.tasks.python.vision.pose_landmarker.PoseLandmarkerResult, mediapipe.python.solution_base.SolutionOutputs]): - The output results from Mediapipe. It supports both: the inference result from - mp.tasks.vision.pose_landmaker.PoseLandmarker and the legacy one from - mp.solutions.pose.Pose. + The output results from Mediapipe. It supports both: the inference + result from mp.tasks.vision.pose_landmaker.PoseLandmarker and the legacy + one from mp.solutions.pose.Pose. resolution_wh (Tuple[int, int]): A tuple of the form `(width, height)` representing the resolution of the frame. @@ -229,7 +229,8 @@ class KeyPoints: num_poses=2, ) - with mp.tasks.vision.PoseLandmarker.create_from_options(options) as landmarker: + PoseLandmarker = mp.tasks.vision.PoseLandmarker + with PoseLandmarker.create_from_options(options) as landmarker: pose_landmarker_result = landmarker.detect(mp_image) keypoints = sv.KeyPoints.from_mediapipe( From a4bdfc2062e84df6ce83965f6358a1b726c563ca Mon Sep 17 00:00:00 2001 From: LinasKo Date: Tue, 11 Jun 2024 13:51:23 +0300 Subject: [PATCH 174/182] minor tidy-up for mediapipe --- supervision/keypoint/core.py | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index ac889bc5..374bb2a1 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -216,11 +216,12 @@ class KeyPoints: image = cv2.imread() image_height, image_width, _ = image.shape - mp_image = mp.Image( + mediapipe_image = mp.Image( image_format=mp.ImageFormat.SRGB, data=cv2.cvtColor(image, cv2.COLOR_BGR2RGB), ) + # Download model task from: https://ai.google.dev/edge/mediapipe/solutions/vision/pose_landmarker/index#models options = mp.tasks.vision.PoseLandmarkerOptions( base_options=mp.tasks.BaseOptions( model_asset_path="pose_landmarker_heavy.task" @@ -231,27 +232,10 @@ class KeyPoints: PoseLandmarker = mp.tasks.vision.PoseLandmarker with PoseLandmarker.create_from_options(options) as landmarker: - pose_landmarker_result = landmarker.detect(mp_image) + pose_landmarker_result = landmarker.detect(mediapipe_image) keypoints = sv.KeyPoints.from_mediapipe( - pose_landmarker_result, [image_width, image_height] - ) - ``` - - ```python - import cv2 - import mediapipe as mp - import supervision as sv - - image = cv2.imread() - image_height, image_width, _ = image.shape - pose = mp.solutions.pose.Pose( - static_image_mode=True, - min_detection_confidence=0.5, - ) - results = pose.process(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) - keypoints = sv.KeyPoints.from_mediapipe( - pose_landmarker_result, [image_width, image_height] + pose_landmarker_result, (image_width, image_height) ) ``` """ From 93b21a8d2df7177a7313065929bb9229fb7c5551 Mon Sep 17 00:00:00 2001 From: LinasKo Date: Tue, 11 Jun 2024 14:02:52 +0300 Subject: [PATCH 175/182] minor improvement to code clarity --- supervision/keypoint/core.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index 374bb2a1..b4243c18 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -240,14 +240,13 @@ class KeyPoints: ``` """ results = mediapipe_results.pose_landmarks - if not isinstance(mediapipe_results.pose_landmarks, list): - if mediapipe_results.pose_landmarks is not None: + if mediapipe_results.pose_landmarks is None: + results = [] + else: results = [ [landmark for landmark in mediapipe_results.pose_landmarks.landmark] ] - else: - results = [] if len(results) == 0: return cls.empty() @@ -258,9 +257,11 @@ class KeyPoints: prediction_xy = [] prediction_confidence = [] for landmark in pose: - prediction_xy.append( - [landmark.x * resolution_wh[0], landmark.y * resolution_wh[1]] - ) + keypoint_xy = [ + landmark.x * resolution_wh[0], + landmark.y * resolution_wh[1], + ] + prediction_xy.append(keypoint_xy) prediction_confidence.append(landmark.visibility) xy.append(prediction_xy) From 3725beb2dbe6c98db7cb41ecdbd806f259658c74 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Thu, 13 Jun 2024 13:50:27 -0400 Subject: [PATCH 176/182] docs improvements --- supervision/keypoint/core.py | 189 +++++++++++++++++++++++------------ 1 file changed, 123 insertions(+), 66 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index b4243c18..79008683 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -18,12 +18,13 @@ class KeyPoints: The `sv.KeyPoints` class in the Supervision library standardizes results from various keypoint detection and pose estimation models into a consistent format. This class simplifies data manipulation and filtering, providing a uniform API for - integration with Supervision annotators. + integration with Supervision [keypoints annotators](/keypoint/annotators). === "Ultralytics" Use [`sv.KeyPoints.from_ultralytics`](/keypoint/core/#supervision.keypoint.core.KeyPoints.from_ultralytics) - method, which accepts model results. + method, which accepts [YOLOv8](https://github.com/ultralytics/ultralytics) + pose result. ```python import cv2 @@ -32,9 +33,59 @@ class KeyPoints: image = cv2.imread() model = YOLO('yolov8s-pose.pt') + result = model(image)[0] key_points = sv.KeyPoints.from_ultralytics(result) ``` + + === "Inference" + + Use [`sv.KeyPoints.from_inference`](/keypoint/core/#supervision.keypoint.core.KeyPoints.from_inference) + method, which accepts [Inference](https://inference.roboflow.com/) pose result. + + ```python + import cv2 + import supervision as sv + from inference import get_model + + image = cv2.imread() + model = get_model(model_id=, api_key=) + + result = model.infer(image)[0] + key_points = sv.KeyPoints.from_inference(result) + ``` + + === "MediaPipe" + + Use [`sv.KeyPoints.from_mediapipe`](/keypoint/core/#supervision.keypoint.core.KeyPoints.from_mediapipe) + method, which accepts [MediaPipe](https://github.com/google-ai-edge/mediapipe) + pose result. + + ```python + import cv2 + import mediapipe as mp + import supervision as sv + + image = cv2.imread() + image_height, image_width, _ = image.shape + mediapipe_image = mp.Image( + image_format=mp.ImageFormat.SRGB, + data=cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) + + options = mp.tasks.vision.PoseLandmarkerOptions( + base_options=mp.tasks.BaseOptions( + model_asset_path="pose_landmarker_heavy.task" + ), + running_mode=mp.tasks.vision.RunningMode.IMAGE, + num_poses=2) + + PoseLandmarker = mp.tasks.vision.PoseLandmarker + with PoseLandmarker.create_from_options(options) as landmarker: + pose_landmarker_result = landmarker.detect(mediapipe_image) + + key_points = sv.KeyPoints.from_mediapipe( + pose_landmarker_result, (image_width, image_height)) + ``` Attributes: xy (np.ndarray): An array of shape `(n, 2)` containing @@ -63,7 +114,7 @@ class KeyPoints: def __len__(self) -> int: """ - Returns the number of keypoints in the keypoints object. + Returns the number of keypoints in the `sv.KeyPoints` object. """ return len(self.xy) @@ -106,18 +157,17 @@ class KeyPoints: """ Create a `sv.KeyPoints` object from the [Roboflow](https://roboflow.com/) API inference result or the [Inference](https://inference.roboflow.com/) - package results. When a keypoint detection model is used, this method - extracts the keypoint coordinates, class IDs, confidences, and class names. + package results. Args: inference_result (dict, any): The result from the Roboflow API or Inference package containing predictions with keypoints. Returns: - (KeyPoints): A KeyPoints object containing the keypoint coordinates, - class IDs, and confidences of each keypoint. + A `sv.KeyPoints` object containing the keypoint coordinates, class IDs, + and class names, and confidences of each keypoint. - Example: + Examples: ```python import cv2 import supervision as sv @@ -151,8 +201,6 @@ class KeyPoints: "You can retrieve it like so: inference_result = model.infer(image)[0]" ) - # Unpack the result if received from inference.get_model, - # rather than inference_sdk.InferenceHTTPClient with suppress(AttributeError): inference_result = inference_result.dict(exclude_none=True, by_alias=True) @@ -190,25 +238,26 @@ class KeyPoints: cls, mediapipe_results, resolution_wh: Tuple[int, int] ) -> KeyPoints: """ - Creates a KeyPoints instance from a - [Pose landmark detection](https://ai.google.dev/edge/mediapipe/solutions/vision/pose_landmarker/python) - inference result. - + Creates a `sv.KeyPoints` instance from a + [MediaPipe](https://github.com/google-ai-edge/mediapipe) + pose landmark detection inference result. Args: - mediapipe_results - (Union[mediapipe.tasks.python.vision.pose_landmarker.PoseLandmarkerResult, - mediapipe.python.solution_base.SolutionOutputs]): + mediapipe_results (Union[PoseLandmarkerResult, SolutionOutputs]): The output results from Mediapipe. It supports both: the inference - result from mp.tasks.vision.pose_landmaker.PoseLandmarker and the legacy - one from mp.solutions.pose.Pose. + result `PoseLandmarker` and the legacy one from `Pose`. resolution_wh (Tuple[int, int]): A tuple of the form `(width, height)` representing the resolution of the frame. Returns: - KeyPoints: A new KeyPoints object. + A `sv.KeyPoints` object containing the keypoint coordinates and + confidences of each keypoint. + + !!! tip + Before you start, download model bundles from the + [MediaPipe website](https://ai.google.dev/edge/mediapipe/solutions/vision/pose_landmarker/index#models). - Example: + Examples: ```python import cv2 import mediapipe as mp @@ -218,27 +267,23 @@ class KeyPoints: image_height, image_width, _ = image.shape mediapipe_image = mp.Image( image_format=mp.ImageFormat.SRGB, - data=cv2.cvtColor(image, cv2.COLOR_BGR2RGB), - ) + data=cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) - # Download model task from: https://ai.google.dev/edge/mediapipe/solutions/vision/pose_landmarker/index#models options = mp.tasks.vision.PoseLandmarkerOptions( base_options=mp.tasks.BaseOptions( model_asset_path="pose_landmarker_heavy.task" ), running_mode=mp.tasks.vision.RunningMode.IMAGE, - num_poses=2, - ) + num_poses=2) PoseLandmarker = mp.tasks.vision.PoseLandmarker with PoseLandmarker.create_from_options(options) as landmarker: pose_landmarker_result = landmarker.detect(mediapipe_image) - keypoints = sv.KeyPoints.from_mediapipe( - pose_landmarker_result, (image_width, image_height) - ) + key_points = sv.KeyPoints.from_mediapipe( + pose_landmarker_result, (image_width, image_height)) ``` - """ + """ # noqa: E501 // docs results = mediapipe_results.pose_landmarks if not isinstance(mediapipe_results.pose_landmarks, list): if mediapipe_results.pose_landmarks is None: @@ -275,17 +320,18 @@ class KeyPoints: @classmethod def from_ultralytics(cls, ultralytics_results) -> KeyPoints: """ - Creates a KeyPoints instance from a - [YOLOv8](https://github.com/ultralytics/ultralytics) inference result. + Creates a `sv.KeyPoints` instance from a + [YOLOv8](https://github.com/ultralytics/ultralytics) pose inference result. Args: ultralytics_results (ultralytics.engine.results.Keypoints): The output Results instance from YOLOv8 Returns: - KeyPoints: A new KeyPoints object. + A `sv.KeyPoints` object containing the keypoint coordinates, class IDs, + and class names, and confidences of each keypoint. - Example: + Examples: ```python import cv2 import supervision as sv @@ -293,8 +339,9 @@ class KeyPoints: image = cv2.imread() model = YOLO('yolov8s-pose.pt') + result = model(image)[0] - keypoints = sv.KeyPoints.from_ultralytics(result) + key_points = sv.KeyPoints.from_ultralytics(result) ``` """ if ultralytics_results.keypoints.xy.numel() == 0: @@ -311,16 +358,18 @@ class KeyPoints: @classmethod def from_yolo_nas(cls, yolo_nas_results) -> KeyPoints: """ - Create a KeyPoints instance from a YOLO NAS results. + Create a `sv.KeyPoints` instance from a [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS-POSE.md) + pose inference results. Args: - yolo_nas_results (ImagePoseEstimationPrediction): - The output object from YOLO NAS. + yolo_nas_results (ImagePoseEstimationPrediction): The output object from + YOLO NAS. Returns: - KeyPoints: A new KeyPoints object. + A `sv.KeyPoints` object containing the keypoint coordinates, class IDs, + and class names, and confidences of each keypoint. - Example: + Examples: ```python import cv2 import torch @@ -330,13 +379,13 @@ class KeyPoints: image = cv2.imread() device = "cuda" if torch.cuda.is_available() else "cpu" - yolo_nas = super_gradients.training.models.get( + model = super_gradients.training.models.get( "yolo_nas_pose_s", pretrained_weights="coco_pose").to(device) - results = yolo_nas.predict(image, conf=0.1) - keypoints = sv.KeyPoints.from_yolo_nas(results) + results = model.predict(image, conf=0.1) + key_points = sv.KeyPoints.from_yolo_nas(results) ``` - """ + """ # noqa: E501 // docs if len(yolo_nas_results.prediction.poses) == 0: return cls.empty() @@ -369,34 +418,41 @@ class KeyPoints: self, index: Union[int, slice, List[int], np.ndarray, str] ) -> Union[KeyPoints, List, np.ndarray, None]: """ - Get a subset of the KeyPoints object or access an item from its data field. + Get a subset of the `sv.KeyPoints` object or access an item from its data field. When provided with an integer, slice, list of integers, or a numpy array, this - method returns a new KeyPoints object that represents a subset of the original - keypoints. When provided with a string, it accesses the corresponding item in - the data dictionary. + method returns a new `sv.KeyPoints` object that represents a subset of the + original `sv.KeyPoints`. When provided with a string, it accesses the + corresponding item in the data dictionary. Args: index (Union[int, slice, List[int], np.ndarray, str]): The index, indices, - or key to access a subset of the KeyPoints or an item from the data. + or key to access a subset of the `sv.KeyPoints` or an item from the + data. Returns: - Union[KeyPoints, Any]: A subset of the KeyPoints object or an item from - the data field. + A subset of the `sv.KeyPoints` object or an item from the data field. - Example: + Examples: ```python import supervision as sv - keypoints = sv.KeyPoints() + key_points = sv.KeyPoints() - first_detection = keypoints[0] - first_10_keypoints = keypoints[0:10] - some_keypoints = keypoints[[0, 2, 4]] - class_0_keypoints = keypoints[keypoints.class_id == 0] - high_confidence_keypoints = keypoints[keypoints.confidence > 0.5] + # access the first keypoint using an integer index + key_points[0] - feature_vector = keypoints['feature_vector'] + # access the first 10 keypoints using index slice + key_points[0:10] + + # access selected keypoints using a list of indices + key_points[[0, 2, 4]] + + # access keypoints with selected class_id + key_points[key_points.class_id == 0] + + # access keypoints with confidence greater than 0.5 + key_points[key_points.confidence > 0.5] ``` """ if isinstance(index, str): @@ -412,13 +468,13 @@ class KeyPoints: def __setitem__(self, key: str, value: Union[np.ndarray, List]): """ - Set a value in the data dictionary of the KeyPoints object. + Set a value in the data dictionary of the `sv.KeyPoints` object. Args: key (str): The key in the data dictionary to set. value (Union[np.ndarray, List]): The value to set for the key. - Example: + Examples: ```python import cv2 import supervision as sv @@ -430,7 +486,7 @@ class KeyPoints: result = model(image)[0] keypoints = sv.KeyPoints.from_ultralytics(result) - keypoints['names'] = [ + keypoints['class_name'] = [ model.model.names[class_id] for class_id in keypoints.class_id @@ -451,12 +507,13 @@ class KeyPoints: Create an empty Keypoints object with no keypoints. Returns: - (KeyPoints): An empty Keypoints object. + An empty `sv.KeyPoints` object. - Example: + Examples: ```python - from supervision import Keypoints - empty_keypoints = Keypoints.empty() + import supervision as sv + + key_points = sv.KeyPoints.empty() ``` """ return cls(xy=np.empty((0, 0, 2), dtype=np.float32)) From 07cb0d8ccfdf2511e5839c2f46e111d6093eb362 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:50:43 +0000 Subject: [PATCH 177/182] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/keypoint/core.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index 79008683..16e28845 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -23,7 +23,7 @@ class KeyPoints: === "Ultralytics" Use [`sv.KeyPoints.from_ultralytics`](/keypoint/core/#supervision.keypoint.core.KeyPoints.from_ultralytics) - method, which accepts [YOLOv8](https://github.com/ultralytics/ultralytics) + method, which accepts [YOLOv8](https://github.com/ultralytics/ultralytics) pose result. ```python @@ -33,16 +33,16 @@ class KeyPoints: image = cv2.imread() model = YOLO('yolov8s-pose.pt') - + result = model(image)[0] key_points = sv.KeyPoints.from_ultralytics(result) ``` - + === "Inference" - + Use [`sv.KeyPoints.from_inference`](/keypoint/core/#supervision.keypoint.core.KeyPoints.from_inference) method, which accepts [Inference](https://inference.roboflow.com/) pose result. - + ```python import cv2 import supervision as sv @@ -54,13 +54,13 @@ class KeyPoints: result = model.infer(image)[0] key_points = sv.KeyPoints.from_inference(result) ``` - + === "MediaPipe" - + Use [`sv.KeyPoints.from_mediapipe`](/keypoint/core/#supervision.keypoint.core.KeyPoints.from_mediapipe) - method, which accepts [MediaPipe](https://github.com/google-ai-edge/mediapipe) + method, which accepts [MediaPipe](https://github.com/google-ai-edge/mediapipe) pose result. - + ```python import cv2 import mediapipe as mp @@ -238,23 +238,23 @@ class KeyPoints: cls, mediapipe_results, resolution_wh: Tuple[int, int] ) -> KeyPoints: """ - Creates a `sv.KeyPoints` instance from a + Creates a `sv.KeyPoints` instance from a [MediaPipe](https://github.com/google-ai-edge/mediapipe) pose landmark detection inference result. Args: - mediapipe_results (Union[PoseLandmarkerResult, SolutionOutputs]): + mediapipe_results (Union[PoseLandmarkerResult, SolutionOutputs]): The output results from Mediapipe. It supports both: the inference result `PoseLandmarker` and the legacy one from `Pose`. resolution_wh (Tuple[int, int]): A tuple of the form `(width, height)` representing the resolution of the frame. Returns: - A `sv.KeyPoints` object containing the keypoint coordinates and + A `sv.KeyPoints` object containing the keypoint coordinates and confidences of each keypoint. - + !!! tip - Before you start, download model bundles from the + Before you start, download model bundles from the [MediaPipe website](https://ai.google.dev/edge/mediapipe/solutions/vision/pose_landmarker/index#models). Examples: @@ -358,7 +358,7 @@ class KeyPoints: @classmethod def from_yolo_nas(cls, yolo_nas_results) -> KeyPoints: """ - Create a `sv.KeyPoints` instance from a [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS-POSE.md) + Create a `sv.KeyPoints` instance from a [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS-POSE.md) pose inference results. Args: From 4eaa897f2cde1fbcf3a646766a3b3ed6131d84c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 00:44:28 +0000 Subject: [PATCH 178/182] :arrow_up: Bump mkdocs-material from 9.5.26 to 9.5.27 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.5.26 to 9.5.27. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.26...9.5.27) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index ecad075d..9a653be9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2199,13 +2199,13 @@ pygments = ">2.12.0" [[package]] name = "mkdocs-material" -version = "9.5.26" +version = "9.5.27" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.26-py3-none-any.whl", hash = "sha256:5d01fb0aa1c7946a1e3ae8689aa2b11a030621ecb54894e35aabb74c21016312"}, - {file = "mkdocs_material-9.5.26.tar.gz", hash = "sha256:56aeb91d94cffa43b6296fa4fbf0eb7c840136e563eecfd12c2d9e92e50ba326"}, + {file = "mkdocs_material-9.5.27-py3-none-any.whl", hash = "sha256:af8cc263fafa98bb79e9e15a8c966204abf15164987569bd1175fd66a7705182"}, + {file = "mkdocs_material-9.5.27.tar.gz", hash = "sha256:a7d4a35f6d4a62b0c43a0cfe7e987da0980c13587b5bc3c26e690ad494427ec0"}, ] [package.dependencies] From a43970fb6538886745a9aa87043d1460710d3f7c Mon Sep 17 00:00:00 2001 From: LinasKo Date: Mon, 17 Jun 2024 16:46:47 +0300 Subject: [PATCH 179/182] Pin numpy to <2.0.0 --- poetry.lock | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index ecad075d..e1894540 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4258,4 +4258,4 @@ desktop = ["opencv-python"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "e3d79f6c93041323b04c7b45e93bb3c4198b21889044004af8a0485a6145a207" +content-hash = "19649609326e741c97bf1b32b28dc5dee72e18d9ce2a53c554980c81e0fe49a9" diff --git a/pyproject.toml b/pyproject.toml index 59d4176d..d366c5b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ classifiers=[ [tool.poetry.dependencies] python = "^3.8" -numpy = ">=1.21.2" +numpy = ">=1.21.2,<2.0.0" scipy = [ { version = "1.10.0", python = "<3.9" }, { version = "^1.10.0", python = ">=3.9" } From b9e98a5e200b6d2e12e990c169756e16e4735d11 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 15:05:59 +0000 Subject: [PATCH 180/182] :arrow_up: Bump ruff from 0.4.8 to 0.4.9 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.8 to 0.4.9. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.4.8...v0.4.9) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/poetry.lock b/poetry.lock index e1894540..db52ec17 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3662,28 +3662,28 @@ files = [ [[package]] name = "ruff" -version = "0.4.8" +version = "0.4.9" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7663a6d78f6adb0eab270fa9cf1ff2d28618ca3a652b60f2a234d92b9ec89066"}, - {file = "ruff-0.4.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eeceb78da8afb6de0ddada93112869852d04f1cd0f6b80fe464fd4e35c330913"}, - {file = "ruff-0.4.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aad360893e92486662ef3be0a339c5ca3c1b109e0134fcd37d534d4be9fb8de3"}, - {file = "ruff-0.4.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:284c2e3f3396fb05f5f803c9fffb53ebbe09a3ebe7dda2929ed8d73ded736deb"}, - {file = "ruff-0.4.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7354f921e3fbe04d2a62d46707e569f9315e1a613307f7311a935743c51a764"}, - {file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:72584676164e15a68a15778fd1b17c28a519e7a0622161eb2debdcdabdc71883"}, - {file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9678d5c9b43315f323af2233a04d747409d1e3aa6789620083a82d1066a35199"}, - {file = "ruff-0.4.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704977a658131651a22b5ebeb28b717ef42ac6ee3b11e91dc87b633b5d83142b"}, - {file = "ruff-0.4.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05f8d6f0c3cce5026cecd83b7a143dcad503045857bc49662f736437380ad45"}, - {file = "ruff-0.4.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ea874950daca5697309d976c9afba830d3bf0ed66887481d6bca1673fc5b66a"}, - {file = "ruff-0.4.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fc95aac2943ddf360376be9aa3107c8cf9640083940a8c5bd824be692d2216dc"}, - {file = "ruff-0.4.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:384154a1c3f4bf537bac69f33720957ee49ac8d484bfc91720cc94172026ceed"}, - {file = "ruff-0.4.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e9d5ce97cacc99878aa0d084c626a15cd21e6b3d53fd6f9112b7fc485918e1fa"}, - {file = "ruff-0.4.8-py3-none-win32.whl", hash = "sha256:6d795d7639212c2dfd01991259460101c22aabf420d9b943f153ab9d9706e6a9"}, - {file = "ruff-0.4.8-py3-none-win_amd64.whl", hash = "sha256:e14a3a095d07560a9d6769a72f781d73259655919d9b396c650fc98a8157555d"}, - {file = "ruff-0.4.8-py3-none-win_arm64.whl", hash = "sha256:14019a06dbe29b608f6b7cbcec300e3170a8d86efaddb7b23405cb7f7dcaf780"}, - {file = "ruff-0.4.8.tar.gz", hash = "sha256:16d717b1d57b2e2fd68bd0bf80fb43931b79d05a7131aa477d66fc40fbd86268"}, + {file = "ruff-0.4.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b262ed08d036ebe162123170b35703aaf9daffecb698cd367a8d585157732991"}, + {file = "ruff-0.4.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:98ec2775fd2d856dc405635e5ee4ff177920f2141b8e2d9eb5bd6efd50e80317"}, + {file = "ruff-0.4.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4555056049d46d8a381f746680db1c46e67ac3b00d714606304077682832998e"}, + {file = "ruff-0.4.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e91175fbe48f8a2174c9aad70438fe9cb0a5732c4159b2a10a3565fea2d94cde"}, + {file = "ruff-0.4.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e8e7b95673f22e0efd3571fb5b0cf71a5eaaa3cc8a776584f3b2cc878e46bff"}, + {file = "ruff-0.4.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2d45ddc6d82e1190ea737341326ecbc9a61447ba331b0a8962869fcada758505"}, + {file = "ruff-0.4.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78de3fdb95c4af084087628132336772b1c5044f6e710739d440fc0bccf4d321"}, + {file = "ruff-0.4.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:06b60f91bfa5514bb689b500a25ba48e897d18fea14dce14b48a0c40d1635893"}, + {file = "ruff-0.4.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88bffe9c6a454bf8529f9ab9091c99490578a593cc9f9822b7fc065ee0712a06"}, + {file = "ruff-0.4.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:673bddb893f21ab47a8334c8e0ea7fd6598ecc8e698da75bcd12a7b9d0a3206e"}, + {file = "ruff-0.4.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8c1aff58c31948cc66d0b22951aa19edb5af0a3af40c936340cd32a8b1ab7438"}, + {file = "ruff-0.4.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:784d3ec9bd6493c3b720a0b76f741e6c2d7d44f6b2be87f5eef1ae8cc1d54c84"}, + {file = "ruff-0.4.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:732dd550bfa5d85af8c3c6cbc47ba5b67c6aed8a89e2f011b908fc88f87649db"}, + {file = "ruff-0.4.9-py3-none-win32.whl", hash = "sha256:8064590fd1a50dcf4909c268b0e7c2498253273309ad3d97e4a752bb9df4f521"}, + {file = "ruff-0.4.9-py3-none-win_amd64.whl", hash = "sha256:e0a22c4157e53d006530c902107c7f550b9233e9706313ab57b892d7197d8e52"}, + {file = "ruff-0.4.9-py3-none-win_arm64.whl", hash = "sha256:5d5460f789ccf4efd43f265a58538a2c24dbce15dbf560676e430375f20a8198"}, + {file = "ruff-0.4.9.tar.gz", hash = "sha256:f1cb0828ac9533ba0135d148d214e284711ede33640465e706772645483427e3"}, ] [[package]] From e5fb43aea6cdacebb55904da502d823ecdc577a9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 17:46:44 +0000 Subject: [PATCH 181/182] =?UTF-8?q?chore(pre=5Fcommit):=20=E2=AC=86=20pre?= =?UTF-8?q?=5Fcommit=20autoupdate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/bandit: 1.7.8 → 1.7.9](https://github.com/PyCQA/bandit/compare/1.7.8...1.7.9) - [github.com/astral-sh/ruff-pre-commit: v0.4.8 → v0.4.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.8...v0.4.9) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fb85a294..1b12937d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,7 +28,7 @@ repos: - repo: https://github.com/PyCQA/bandit - rev: '1.7.8' + rev: '1.7.9' hooks: - id: bandit args: ["-c", "pyproject.toml"] @@ -45,7 +45,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.8 + rev: v0.4.9 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From eceebb33e3f2aac26e2cbe849a894621e3b4ed79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 23:02:23 +0000 Subject: [PATCH 182/182] :arrow_up: Bump urllib3 from 2.2.1 to 2.2.2 Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.2.1 to 2.2.2. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.2.1...2.2.2) --- updated-dependencies: - dependency-name: urllib3 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index db52ec17..47915d98 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4068,13 +4068,13 @@ dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake [[package]] name = "urllib3" -version = "2.2.1" +version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras]