From c78ae33e43c95e067e2ae34ff9e7616fe696cac3 Mon Sep 17 00:00:00 2001 From: mario-dg Date: Fri, 13 Oct 2023 18:24:22 +0200 Subject: [PATCH 001/274] =?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 77bfca9da..006bc6e7e 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -6,7 +6,15 @@ 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 @@ def box_area(self) -> np.ndarray: """ 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 5f6fb391d..2098c79c8 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 @@ def __init__( 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 @@ def __call__(self, image: np.ndarray) -> Detections: 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 7a5eb5469..b0414eb44 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/274] 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 006bc6e7e..bd729a964 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 @@ def get_anchor_coordinates(self, anchor: Position) -> np.ndarray: 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 @@ def with_nmm( 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 @@ def with_nmm( 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/274] 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 2098c79c8..c0a30ff66 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 @@ def __init__( 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 @@ def __call__(self, image: np.ndarray) -> Detections: 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/274] 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 b0414eb44..a79900b4b 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 823dc56057a6e0ae0887f4ce58eaab04ba9ee30a Mon Sep 17 00:00:00 2001 From: Seongjun Choi Date: Tue, 23 Jan 2024 15:14:56 +0900 Subject: [PATCH 005/274] Modify: from_mmdetection add mask. See #703 --- supervision/detection/core.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 39e129e11..9a284775e 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -330,6 +330,9 @@ def from_mmdetection(cls, mmdet_results) -> Detections: xyxy=mmdet_results.pred_instances.bboxes.cpu().numpy(), confidence=mmdet_results.pred_instances.scores.cpu().numpy(), class_id=mmdet_results.pred_instances.labels.cpu().numpy().astype(int), + mask=mmdet_results.pred_instances.masks.cpu().numpy() + if 'masks' in mmdet_results.pred_instances + else None, ) @classmethod From bddb72fb28c211509fa5784c122a567f7cc5fd7b Mon Sep 17 00:00:00 2001 From: Seongjun Choi Date: Tue, 23 Jan 2024 15:14:56 +0900 Subject: [PATCH 006/274] Modify: from_mmdetection add mask. See #703 From 27b61ab4a32d7e2195dcdd9401bef6778bbeac8b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 23:31:30 +0000 Subject: [PATCH 007/274] =?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 9a284775e..6b2d01f61 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -331,7 +331,7 @@ def from_mmdetection(cls, mmdet_results) -> Detections: confidence=mmdet_results.pred_instances.scores.cpu().numpy(), class_id=mmdet_results.pred_instances.labels.cpu().numpy().astype(int), mask=mmdet_results.pred_instances.masks.cpu().numpy() - if 'masks' in mmdet_results.pred_instances + if "masks" in mmdet_results.pred_instances else None, ) From ad0e6c3b51498beb25046b74704c86ac87128503 Mon Sep 17 00:00:00 2001 From: Raif Olson Date: Fri, 29 Mar 2024 16:59:29 -0400 Subject: [PATCH 008/274] Add minimum consecutive frames to ByteTrack --- supervision/tracker/byte_tracker/core.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/supervision/tracker/byte_tracker/core.py b/supervision/tracker/byte_tracker/core.py index c77878cf9..6475bf508 100644 --- a/supervision/tracker/byte_tracker/core.py +++ b/supervision/tracker/byte_tracker/core.py @@ -13,7 +13,7 @@ class STrack(BaseTrack): shared_kalman = KalmanFilter() - def __init__(self, tlwh, score, class_ids): + def __init__(self, tlwh, score, class_ids, minimum_consecutive_frames): # wait activate self._tlwh = np.asarray(tlwh, dtype=np.float32) self.kalman_filter = None @@ -24,6 +24,8 @@ def __init__(self, tlwh, score, class_ids): self.class_ids = class_ids self.tracklet_len = 0 + self.minimum_consecutive_frames = minimum_consecutive_frames + def predict(self): mean_state = self.mean.copy() if self.state != TrackState.Tracked: @@ -60,7 +62,7 @@ def activate(self, kalman_filter, frame_id): self.tracklet_len = 0 self.state = TrackState.Tracked - if frame_id == 1: + if frame_id == 1 and self.minimum_consecutive_frames == 1: self.is_activated = True self.frame_id = frame_id self.start_frame = frame_id @@ -71,7 +73,7 @@ def re_activate(self, new_track, frame_id, new_id=False): ) self.tracklet_len = 0 self.state = TrackState.Tracked - self.is_activated = True + self.frame_id = frame_id if new_id: self.track_id = self.next_id() @@ -93,7 +95,8 @@ def update(self, new_track, frame_id): self.mean, self.covariance, self.tlwh_to_xyah(new_tlwh) ) self.state = TrackState.Tracked - self.is_activated = True + if self.tracklet_len >= self.minimum_consecutive_frames: + self.is_activated = True self.score = new_track.score @@ -186,6 +189,10 @@ class ByteTrack: Increasing minimum_matching_threshold improves accuracy but risks fragmentation. Decreasing it improves completeness but risks false positives and drift. frame_rate (int, optional): The frame rate of the video. + minimum_consecutive_frames (int, optional): Number of consecutive frames that an object must + be tracked before it is considered a 'valid' track. + Increasing minimum_consecutive_frames prevents the creation of accidental tracks from + false detection or double detection, but risks missing shorter tracks. """ # noqa: E501 // docs @deprecated_parameter( @@ -218,6 +225,7 @@ def __init__( lost_track_buffer: int = 30, minimum_matching_threshold: float = 0.8, frame_rate: int = 30, + minimum_consecutive_frames: int = 1, ): self.track_activation_threshold = track_activation_threshold self.minimum_matching_threshold = minimum_matching_threshold @@ -225,6 +233,7 @@ def __init__( self.frame_id = 0 self.det_thresh = self.track_activation_threshold + 0.1 self.max_time_lost = int(frame_rate / 30.0 * lost_track_buffer) + self.minimum_consecutive_frames = minimum_consecutive_frames self.kalman_filter = KalmanFilter() self.tracked_tracks: List[STrack] = [] @@ -290,6 +299,7 @@ def callback(frame: np.ndarray, index: int) -> np.ndarray: return detections[detections.tracker_id != -1] else: + detections = Detections.empty() detections.tracker_id = np.array([], dtype=int) return detections @@ -345,7 +355,7 @@ def update_with_tensors(self, tensors: np.ndarray) -> List[STrack]: if len(dets) > 0: """Detections""" detections = [ - STrack(STrack.tlbr_to_tlwh(tlbr), s, c) + STrack(STrack.tlbr_to_tlwh(tlbr), s, c, self.minimum_consecutive_frames) for (tlbr, s, c) in zip(dets, scores_keep, class_ids_keep) ] else: @@ -387,7 +397,7 @@ def update_with_tensors(self, tensors: np.ndarray) -> List[STrack]: if len(dets_second) > 0: """Detections""" detections_second = [ - STrack(STrack.tlbr_to_tlwh(tlbr), s, c) + STrack(STrack.tlbr_to_tlwh(tlbr), s, c, self.minimum_consecutive_frames) for (tlbr, s, c) in zip(dets_second, scores_second, class_ids_second) ] else: From 1a7309e7c9f7d0d9c699e1c29c7adb77fa3c5b7e Mon Sep 17 00:00:00 2001 From: Jeslin P James Date: Tue, 9 Apr 2024 16:51:41 +0530 Subject: [PATCH 009/274] RichLabelAnnotator class added --- supervision/annotators/core.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index 9f6cdb367..d6ae599ea 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -3,6 +3,7 @@ import cv2 import numpy as np +from PIL import ImageFont from supervision.annotators.base import BaseAnnotator, ImageType from supervision.annotators.utils import ColorLookup, Trace, resolve_color @@ -1147,6 +1148,27 @@ def draw_rounded_rectangle( ) return scene +class RichLabelAnnotator: + + def __init__( + self, + color: Union[Color, ColorPalette] = ColorPalette.DEFAULT, + text_color: Color = Color.WHITE, + font_path: str = "/content/Arial Unicode Font.ttf", + font_size: int = 14, + text_padding: int = 10, + text_position: Position = Position.TOP_LEFT, + color_lookup: ColorLookup = ColorLookup.CLASS, + border_radius: int = 0, + ): + self.color = color + self.text_color = text_color + self.font = ImageFont.truetype(font_path, font_size) + self.text_padding = text_padding + self.text_anchor = text_position + self.color_lookup = color_lookup + self.border_radius = border_radius + class BlurAnnotator(BaseAnnotator): """ From 2417b36006c48583a48883ba22d495f77b09273b Mon Sep 17 00:00:00 2001 From: Jeslin P James Date: Tue, 9 Apr 2024 20:39:53 +0530 Subject: [PATCH 010/274] resolve_text_background_xyxy() function added to RichLabelAnnotator --- supervision/annotators/core.py | 53 ++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index d6ae599ea..a86d9f9e1 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -1168,6 +1168,59 @@ def __init__( self.text_anchor = text_position self.color_lookup = color_lookup self.border_radius = border_radius + + @staticmethod + def resolve_text_background_xyxy( + center_coordinates: Tuple[int, int], + text_wh: Tuple[int, int], + position: Position, + ) -> Tuple[int, int, int, int]: + center_x, center_y = center_coordinates + text_w, text_h = text_wh + + if position == Position.TOP_LEFT: + return center_x, center_y - text_h, center_x + text_w, center_y + elif position == Position.TOP_RIGHT: + return center_x - text_w, center_y - text_h, center_x, center_y + elif position == Position.TOP_CENTER: + return ( + center_x - text_w // 2, + center_y - text_h, + center_x + text_w // 2, + center_y, + ) + elif position == Position.CENTER or position == Position.CENTER_OF_MASS: + return ( + center_x - text_w // 2, + center_y - text_h // 2, + center_x + text_w // 2, + center_y + text_h // 2, + ) + elif position == Position.BOTTOM_LEFT: + return center_x, center_y, center_x + text_w, center_y + text_h + elif position == Position.BOTTOM_RIGHT: + return center_x - text_w, center_y, center_x, center_y + text_h + elif position == Position.BOTTOM_CENTER: + return ( + center_x - text_w // 2, + center_y, + center_x + text_w // 2, + center_y + text_h, + ) + elif position == Position.CENTER_LEFT: + return ( + center_x - text_w, + center_y - text_h // 2, + center_x, + center_y + text_h // 2, + ) + elif position == Position.CENTER_RIGHT: + return ( + center_x, + center_y - text_h // 2, + center_x + text_w, + center_y + text_h // 2, + ) class BlurAnnotator(BaseAnnotator): From 9e906c78f5f1566e62c79a940c9d16e0c7cd6b3e Mon Sep 17 00:00:00 2001 From: Jeslin P James Date: Tue, 9 Apr 2024 22:14:36 +0530 Subject: [PATCH 011/274] annotate function added in RichLabelAnnotator --- supervision/annotators/core.py | 72 +++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index a86d9f9e1..f69421ccf 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -3,7 +3,7 @@ import cv2 import numpy as np -from PIL import ImageFont +from PIL import ImageFont, ImageDraw from supervision.annotators.base import BaseAnnotator, ImageType from supervision.annotators.utils import ColorLookup, Trace, resolve_color @@ -1221,6 +1221,76 @@ def resolve_text_background_xyxy( center_x + text_w, center_y + text_h // 2, ) + + + def annotate( + self, + scene: ImageType, + detections: Detections, + labels: List[str] = None, + custom_color_lookup: Optional[np.ndarray] = None, + ) -> ImageType: + draw = ImageDraw.Draw(scene) + anchors_coordinates = detections.get_anchors_coordinates( + anchor=self.text_anchor + ).astype(int) + if labels is not None and len(labels) != len(detections): + raise ValueError( + f"The number of labels provided ({len(labels)}) does not match the " + f"number of detections ({len(detections)}). Each detection should have " + f"a corresponding label. This discrepancy can occur if the labels and " + f"detections are not aligned or if an incorrect number of labels has " + f"been provided. Please ensure that the labels array has the same " + f"length as the Detections object." + ) + for detection_idx, center_coordinates in enumerate(anchors_coordinates): + color = resolve_color( + color=self.color, + detections=detections, + detection_idx=detection_idx, + color_lookup=( + self.color_lookup + if custom_color_lookup is None + else custom_color_lookup + ), + ) + if labels is not None: + text = labels[detection_idx] + elif detections[CLASS_NAME_DATA_FIELD] is not None: + text = detections[CLASS_NAME_DATA_FIELD][detection_idx] + elif detections.class_id is not None: + text = str(detections.class_id[detection_idx]) + else: + text = str(detection_idx) + + left, top, right, bottom = draw.textbbox((0, 0), text, font=self.font) + text_width = right - left + text_height = bottom - top + text_w_padded = text_width + 2 * self.text_padding + text_h_padded = text_height + 2 * self.text_padding + text_background_xyxy = self.resolve_text_background_xyxy( + center_coordinates=tuple(center_coordinates), + text_wh=(text_w_padded, text_h_padded), + position=self.text_anchor, + ) + + text_x = text_background_xyxy[0] + self.text_padding - left + text_y = text_background_xyxy[1] + self.text_padding - top + + draw.rounded_rectangle( + text_background_xyxy, + radius=self.border_radius, + fill=color.as_rgb(), + outline=None, + ) + draw.text( + xy=(text_x, text_y), + text=text, + font=self.font, + fill=self.text_color.as_rgb(), + ) + + return scene class BlurAnnotator(BaseAnnotator): From eaff97f9027fc9b1b67c92156d29d36407b80128 Mon Sep 17 00:00:00 2001 From: Raif Olson Date: Wed, 10 Apr 2024 17:33:16 -0400 Subject: [PATCH 012/274] add external and internal track_ids to keep valid track ids sequential --- supervision/tracker/byte_tracker/core.py | 46 +++++++++++++++--------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/supervision/tracker/byte_tracker/core.py b/supervision/tracker/byte_tracker/core.py index 6475bf508..bf82044a6 100644 --- a/supervision/tracker/byte_tracker/core.py +++ b/supervision/tracker/byte_tracker/core.py @@ -12,6 +12,7 @@ class STrack(BaseTrack): shared_kalman = KalmanFilter() + _external_count = 0 def __init__(self, tlwh, score, class_ids, minimum_consecutive_frames): # wait activate @@ -24,6 +25,8 @@ def __init__(self, tlwh, score, class_ids, minimum_consecutive_frames): self.class_ids = class_ids self.tracklet_len = 0 + self.external_track_id = -1 + self.minimum_consecutive_frames = minimum_consecutive_frames def predict(self): @@ -55,7 +58,7 @@ def multi_predict(stracks): def activate(self, kalman_filter, frame_id): """Start a new tracklet""" self.kalman_filter = kalman_filter - self.track_id = self.next_id() + self.internal_track_id = self.next_id() self.mean, self.covariance = self.kalman_filter.initiate( self.tlwh_to_xyah(self._tlwh) ) @@ -76,7 +79,7 @@ def re_activate(self, new_track, frame_id, new_id=False): self.frame_id = frame_id if new_id: - self.track_id = self.next_id() + self.internal_track_id = self.next_id() self.score = new_track.score def update(self, new_track, frame_id): @@ -95,8 +98,9 @@ def update(self, new_track, frame_id): self.mean, self.covariance, self.tlwh_to_xyah(new_tlwh) ) self.state = TrackState.Tracked - if self.tracklet_len >= self.minimum_consecutive_frames: + if self.tracklet_len == self.minimum_consecutive_frames: self.is_activated = True + self.external_track_id = self.next_external_id() self.score = new_track.score @@ -133,6 +137,15 @@ def tlwh_to_xyah(tlwh): def to_xyah(self): return self.tlwh_to_xyah(self.tlwh) + + @staticmethod + def next_external_id(): + STrack._external_count += 1 + return STrack._external_count + + @staticmethod + def reset_external_counter(): + STrack._external_count = 0 @staticmethod def tlbr_to_tlwh(tlbr): @@ -147,7 +160,7 @@ def tlwh_to_tlbr(tlwh): return ret def __repr__(self): - return "OT_{}_({}-{})".format(self.track_id, self.start_frame, self.end_frame) + return "OT_{}_({}-{})".format(self.internal_track_id, self.start_frame, self.end_frame) def detections2boxes(detections: Detections) -> np.ndarray: @@ -294,7 +307,7 @@ def callback(frame: np.ndarray, index: int) -> np.ndarray: matches, _, _ = matching.linear_assignment(iou_costs, 0.5) detections.tracker_id = np.full(len(detections), -1, dtype=int) for i_detection, i_track in matches: - detections.tracker_id[i_detection] = int(tracks[i_track].track_id) + detections.tracker_id[i_detection] = int(tracks[i_track].external_track_id) return detections[detections.tracker_id != -1] @@ -318,6 +331,7 @@ def reset(self): self.lost_tracks: List[STrack] = [] self.removed_tracks: List[STrack] = [] BaseTrack.reset_counter() + STrack.reset_external_counter() def update_with_tensors(self, tensors: np.ndarray) -> List[STrack]: """ @@ -478,22 +492,22 @@ def joint_tracks( ) -> List[STrack]: """ Joins two lists of tracks, ensuring that the resulting list does not - contain tracks with duplicate track_id values. + contain tracks with duplicate internal_track_id values. Parameters: - track_list_a: First list of tracks (with track_id attribute). - track_list_b: Second list of tracks (with track_id attribute). + track_list_a: First list of tracks (with internal_track_id attribute). + track_list_b: Second list of tracks (with internal_track_id attribute). Returns: Combined list of tracks from track_list_a and track_list_b - without duplicate track_id values. + without duplicate internal_track_id values. """ seen_track_ids = set() result = [] for track in track_list_a + track_list_b: - if track.track_id not in seen_track_ids: - seen_track_ids.add(track.track_id) + if track.internal_track_id not in seen_track_ids: + seen_track_ids.add(track.internal_track_id) result.append(track) return result @@ -502,17 +516,17 @@ def joint_tracks( def sub_tracks(track_list_a: List, track_list_b: List) -> List[int]: """ Returns a list of tracks from track_list_a after removing any tracks - that share the same track_id with tracks in track_list_b. + that share the same internal_track_id with tracks in track_list_b. Parameters: - track_list_a: List of tracks (with track_id attribute). - track_list_b: List of tracks (with track_id attribute) to + track_list_a: List of tracks (with internal_track_id attribute). + track_list_b: List of tracks (with internal_track_id attribute) to be subtracted from track_list_a. Returns: List of remaining tracks from track_list_a after subtraction. """ - tracks = {track.track_id: track for track in track_list_a} - track_ids_b = {track.track_id for track in track_list_b} + tracks = {track.internal_track_id: track for track in track_list_a} + track_ids_b = {track.internal_track_id for track in track_list_b} for track_id in track_ids_b: tracks.pop(track_id, None) From a2ab113f214d92e785bc5ddba5d0f64e669fe39f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Apr 2024 21:37:41 +0000 Subject: [PATCH 013/274] =?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/tracker/byte_tracker/core.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/supervision/tracker/byte_tracker/core.py b/supervision/tracker/byte_tracker/core.py index bf82044a6..8e8104f4c 100644 --- a/supervision/tracker/byte_tracker/core.py +++ b/supervision/tracker/byte_tracker/core.py @@ -137,7 +137,7 @@ def tlwh_to_xyah(tlwh): def to_xyah(self): return self.tlwh_to_xyah(self.tlwh) - + @staticmethod def next_external_id(): STrack._external_count += 1 @@ -160,7 +160,9 @@ def tlwh_to_tlbr(tlwh): return ret def __repr__(self): - return "OT_{}_({}-{})".format(self.internal_track_id, self.start_frame, self.end_frame) + return "OT_{}_({}-{})".format( + self.internal_track_id, self.start_frame, self.end_frame + ) def detections2boxes(detections: Detections) -> np.ndarray: @@ -307,7 +309,9 @@ def callback(frame: np.ndarray, index: int) -> np.ndarray: matches, _, _ = matching.linear_assignment(iou_costs, 0.5) detections.tracker_id = np.full(len(detections), -1, dtype=int) for i_detection, i_track in matches: - detections.tracker_id[i_detection] = int(tracks[i_track].external_track_id) + detections.tracker_id[i_detection] = int( + tracks[i_track].external_track_id + ) return detections[detections.tracker_id != -1] From 2c9a1f69c5ad1f460c2c5a413d802b2cef967d64 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Apr 2024 21:54:23 +0000 Subject: [PATCH 014/274] =?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/tracker/byte_tracker/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supervision/tracker/byte_tracker/core.py b/supervision/tracker/byte_tracker/core.py index 8e8104f4c..1fa988649 100644 --- a/supervision/tracker/byte_tracker/core.py +++ b/supervision/tracker/byte_tracker/core.py @@ -142,7 +142,7 @@ def to_xyah(self): def next_external_id(): STrack._external_count += 1 return STrack._external_count - + @staticmethod def reset_external_counter(): STrack._external_count = 0 From 166a8da9a07b20852c4559624fe029fc87bc8751 Mon Sep 17 00:00:00 2001 From: mario-dg Date: Thu, 11 Apr 2024 12:22:44 +0200 Subject: [PATCH 015/274] Implement Feedback --- supervision/detection/core.py | 154 +++++++++++------- .../detection/tools/inference_slicer.py | 24 +-- supervision/detection/utils.py | 69 +------- 3 files changed, 103 insertions(+), 144 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 66387087c..a9a4ee92d 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -8,22 +8,17 @@ 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.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,22 +1127,24 @@ def box_area(self) -> np.ndarray: """ return (self.xyxy[:, 3] - self.xyxy[:, 1]) * (self.xyxy[:, 2] - self.xyxy[:, 0]) - def with_nmm( + def with_nms( self, threshold: float = 0.5, class_agnostic: bool = False ) -> Detections: """ - Perform non-maximum merging on the current set of object detections. + Performs non-max suppression on detection set. If the detections result + from a segmentation model, the IoU mask is applied. Otherwise, box IoU is used. Args: threshold (float, optional): The intersection-over-union threshold - to use for non-maximum merging. Defaults to 0.5. + to use for non-maximum suppression. I'm the lower the value the more + restrictive the NMS becomes. Defaults to 0.5. class_agnostic (bool, optional): Whether to perform class-agnostic - non-maximum merging. If True, the class_id of each detection + non-maximum suppression. 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. + after non-maximum suppression. Raises: AssertionError: If `confidence` is None and class_agnostic is False. @@ -1115,16 +1153,17 @@ def with_nmm( 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." + ), "Detections confidence must be given for NMS 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: + 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 NMS set class_agnostic=True." + ) predictions = np.hstack( ( self.xyxy, @@ -1132,41 +1171,34 @@ def with_nmm( 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]) + if self.mask is not None: + indices = mask_non_max_suppression( + predictions=predictions, masks=self.mask, iou_threshold=threshold + ) + else: + indices = box_non_max_suppression( + predictions=predictions, iou_threshold=threshold + ) - return Detections.merge(result) + return self[indices] - def with_nms( + def with_nmm( self, threshold: float = 0.5, class_agnostic: bool = False ) -> Detections: """ - Performs non-max suppression on detection set. If the detections result - from a segmentation model, the IoU mask is applied. Otherwise, box IoU is used. + 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 suppression. I'm the lower the value the more - restrictive the NMS becomes. Defaults to 0.5. + to use for non-maximum merging. Defaults to 0.5. class_agnostic (bool, optional): Whether to perform class-agnostic - non-maximum suppression. If True, the class_id of each detection + 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 suppression. + after non-maximum merging. Raises: AssertionError: If `confidence` is None and class_agnostic is False. @@ -1175,17 +1207,16 @@ def with_nms( 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 NMS to be executed." + ), "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: - 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 NMS set class_agnostic=True." - ) predictions = np.hstack( ( self.xyxy, @@ -1193,14 +1224,19 @@ def with_nms( self.class_id.reshape(-1, 1), ) ) + keep_to_merge_list = batch_non_max_merge(predictions, threshold) - if self.mask is not None: - indices = mask_non_max_suppression( - predictions=predictions, masks=self.mask, iou_threshold=threshold - ) - else: - indices = box_non_max_suppression( - predictions=predictions, iou_threshold=threshold - ) + result = [] - return self[indices] + 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 2aff9f6de..7157723f9 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 @@ def __init__( 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 @@ def callback(image_slice: np.ndarray) -> sv.Detections: 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 b9edb9d63..9e732aeb4 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 6166d924588b8ca3fe6cf821f02a098a5fc34f67 Mon Sep 17 00:00:00 2001 From: Raif Olson Date: Thu, 11 Apr 2024 08:21:26 -0400 Subject: [PATCH 016/274] fix reassigning track_id after a track is re-activated. --- supervision/tracker/byte_tracker/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supervision/tracker/byte_tracker/core.py b/supervision/tracker/byte_tracker/core.py index 1fa988649..f35e62df5 100644 --- a/supervision/tracker/byte_tracker/core.py +++ b/supervision/tracker/byte_tracker/core.py @@ -98,7 +98,7 @@ def update(self, new_track, frame_id): self.mean, self.covariance, self.tlwh_to_xyah(new_tlwh) ) self.state = TrackState.Tracked - if self.tracklet_len == self.minimum_consecutive_frames: + if self.tracklet_len == self.minimum_consecutive_frames and not self.is_activated: self.is_activated = True self.external_track_id = self.next_external_id() From 755f292ed1ef87f593cf2f057b9a95e7e84a2b59 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 11 Apr 2024 12:21:44 +0000 Subject: [PATCH 017/274] =?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/tracker/byte_tracker/core.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/supervision/tracker/byte_tracker/core.py b/supervision/tracker/byte_tracker/core.py index f35e62df5..10b7f0fe3 100644 --- a/supervision/tracker/byte_tracker/core.py +++ b/supervision/tracker/byte_tracker/core.py @@ -98,7 +98,10 @@ def update(self, new_track, frame_id): self.mean, self.covariance, self.tlwh_to_xyah(new_tlwh) ) self.state = TrackState.Tracked - if self.tracklet_len == self.minimum_consecutive_frames and not self.is_activated: + if ( + self.tracklet_len == self.minimum_consecutive_frames + and not self.is_activated + ): self.is_activated = True self.external_track_id = self.next_external_id() From 7924a13dc59dd26f7cd2cc9e83c098fd4f453da5 Mon Sep 17 00:00:00 2001 From: Raif Olson Date: Thu, 11 Apr 2024 08:28:08 -0400 Subject: [PATCH 018/274] only assign external track_id once --- supervision/tracker/byte_tracker/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supervision/tracker/byte_tracker/core.py b/supervision/tracker/byte_tracker/core.py index 10b7f0fe3..6c320073f 100644 --- a/supervision/tracker/byte_tracker/core.py +++ b/supervision/tracker/byte_tracker/core.py @@ -100,7 +100,7 @@ def update(self, new_track, frame_id): self.state = TrackState.Tracked if ( self.tracklet_len == self.minimum_consecutive_frames - and not self.is_activated + and self.external_track_id == -1 ): self.is_activated = True self.external_track_id = self.next_external_id() From b1d5d4faf07f28d3f99fcb36dc2e65eb65167776 Mon Sep 17 00:00:00 2001 From: Raif Olson Date: Thu, 11 Apr 2024 09:21:32 -0400 Subject: [PATCH 019/274] fix to exactly match existing release version when minimum_consecutive_frames = 1 --- supervision/tracker/byte_tracker/core.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/supervision/tracker/byte_tracker/core.py b/supervision/tracker/byte_tracker/core.py index 6c320073f..d7dc2461b 100644 --- a/supervision/tracker/byte_tracker/core.py +++ b/supervision/tracker/byte_tracker/core.py @@ -65,8 +65,12 @@ def activate(self, kalman_filter, frame_id): self.tracklet_len = 0 self.state = TrackState.Tracked - if frame_id == 1 and self.minimum_consecutive_frames == 1: + if frame_id == 1: self.is_activated = True + + if self.minimum_consecutive_frames == 1: + self.external_track_id = self.next_external_id() + self.frame_id = frame_id self.start_frame = frame_id From bcb31f880958af32e2b7aa788f1afe37389458bf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 11 Apr 2024 13:21:48 +0000 Subject: [PATCH 020/274] =?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/tracker/byte_tracker/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supervision/tracker/byte_tracker/core.py b/supervision/tracker/byte_tracker/core.py index d7dc2461b..410c1050c 100644 --- a/supervision/tracker/byte_tracker/core.py +++ b/supervision/tracker/byte_tracker/core.py @@ -70,7 +70,7 @@ def activate(self, kalman_filter, frame_id): if self.minimum_consecutive_frames == 1: self.external_track_id = self.next_external_id() - + self.frame_id = frame_id self.start_frame = frame_id From f98ded49a226557bc474cbc46253689023f9664b Mon Sep 17 00:00:00 2001 From: Raif Olson Date: Thu, 11 Apr 2024 09:27:24 -0400 Subject: [PATCH 021/274] fix --- supervision/tracker/byte_tracker/core.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/supervision/tracker/byte_tracker/core.py b/supervision/tracker/byte_tracker/core.py index d7dc2461b..8b072fa23 100644 --- a/supervision/tracker/byte_tracker/core.py +++ b/supervision/tracker/byte_tracker/core.py @@ -102,12 +102,10 @@ def update(self, new_track, frame_id): self.mean, self.covariance, self.tlwh_to_xyah(new_tlwh) ) self.state = TrackState.Tracked - if ( - self.tracklet_len == self.minimum_consecutive_frames - and self.external_track_id == -1 - ): + if (self.tracklet_len == self.minimum_consecutive_frames): self.is_activated = True - self.external_track_id = self.next_external_id() + if (self.external_track_id == -1): + self.external_track_id = self.next_external_id() self.score = new_track.score From 361e444f53f2281e2b0297f5b267f66136292d5f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 11 Apr 2024 13:27:40 +0000 Subject: [PATCH 022/274] =?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/tracker/byte_tracker/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/supervision/tracker/byte_tracker/core.py b/supervision/tracker/byte_tracker/core.py index cfaae1305..55db6293d 100644 --- a/supervision/tracker/byte_tracker/core.py +++ b/supervision/tracker/byte_tracker/core.py @@ -102,9 +102,9 @@ def update(self, new_track, frame_id): self.mean, self.covariance, self.tlwh_to_xyah(new_tlwh) ) self.state = TrackState.Tracked - if (self.tracklet_len == self.minimum_consecutive_frames): + if self.tracklet_len == self.minimum_consecutive_frames: self.is_activated = True - if (self.external_track_id == -1): + if self.external_track_id == -1: self.external_track_id = self.next_external_id() self.score = new_track.score From 26c2a7ef9cb17332795501de2f2f867bccd8c8e8 Mon Sep 17 00:00:00 2001 From: Jeslin P James Date: Sat, 13 Apr 2024 10:18:30 +0530 Subject: [PATCH 023/274] updated examples --- examples/count_people_in_zone/inference_example.py | 2 +- examples/count_people_in_zone/ultralytics_example.py | 2 +- examples/speed_estimation/inference_example.py | 4 +--- examples/speed_estimation/ultralytics_example.py | 4 +--- examples/speed_estimation/yolo_nas_example.py | 4 +--- 5 files changed, 5 insertions(+), 11 deletions(-) diff --git a/examples/count_people_in_zone/inference_example.py b/examples/count_people_in_zone/inference_example.py index 6fd0f3408..163f1771a 100644 --- a/examples/count_people_in_zone/inference_example.py +++ b/examples/count_people_in_zone/inference_example.py @@ -46,7 +46,7 @@ def initiate_annotators( box_annotators = [] for index, polygon in enumerate(polygons): - zone = sv.PolygonZone(polygon=polygon, frame_resolution_wh=resolution_wh) + zone = sv.PolygonZone(polygon=polygon) zone_annotator = sv.PolygonZoneAnnotator( zone=zone, color=COLORS.by_idx(index), diff --git a/examples/count_people_in_zone/ultralytics_example.py b/examples/count_people_in_zone/ultralytics_example.py index cfd37bcc7..861db951e 100644 --- a/examples/count_people_in_zone/ultralytics_example.py +++ b/examples/count_people_in_zone/ultralytics_example.py @@ -44,7 +44,7 @@ def initiate_annotators( box_annotators = [] for index, polygon in enumerate(polygons): - zone = sv.PolygonZone(polygon=polygon, frame_resolution_wh=resolution_wh) + zone = sv.PolygonZone(polygon=polygon) zone_annotator = sv.PolygonZoneAnnotator( zone=zone, color=COLORS.by_idx(index), diff --git a/examples/speed_estimation/inference_example.py b/examples/speed_estimation/inference_example.py index b0ff84dd4..a435261c7 100644 --- a/examples/speed_estimation/inference_example.py +++ b/examples/speed_estimation/inference_example.py @@ -116,9 +116,7 @@ def parse_arguments() -> argparse.Namespace: frame_generator = sv.get_video_frames_generator(source_path=args.source_video_path) - polygon_zone = sv.PolygonZone( - polygon=SOURCE, frame_resolution_wh=video_info.resolution_wh - ) + polygon_zone = sv.PolygonZone(polygon=SOURCE) view_transformer = ViewTransformer(source=SOURCE, target=TARGET) coordinates = defaultdict(lambda: deque(maxlen=video_info.fps)) diff --git a/examples/speed_estimation/ultralytics_example.py b/examples/speed_estimation/ultralytics_example.py index 4b0436a00..3218d2688 100644 --- a/examples/speed_estimation/ultralytics_example.py +++ b/examples/speed_estimation/ultralytics_example.py @@ -94,9 +94,7 @@ def parse_arguments() -> argparse.Namespace: frame_generator = sv.get_video_frames_generator(source_path=args.source_video_path) - polygon_zone = sv.PolygonZone( - polygon=SOURCE, frame_resolution_wh=video_info.resolution_wh - ) + polygon_zone = sv.PolygonZone(polygon=SOURCE) view_transformer = ViewTransformer(source=SOURCE, target=TARGET) coordinates = defaultdict(lambda: deque(maxlen=video_info.fps)) diff --git a/examples/speed_estimation/yolo_nas_example.py b/examples/speed_estimation/yolo_nas_example.py index 77f1fd0f7..187981668 100644 --- a/examples/speed_estimation/yolo_nas_example.py +++ b/examples/speed_estimation/yolo_nas_example.py @@ -95,9 +95,7 @@ def parse_arguments() -> argparse.Namespace: frame_generator = sv.get_video_frames_generator(source_path=args.source_video_path) - polygon_zone = sv.PolygonZone( - polygon=SOURCE, frame_resolution_wh=video_info.resolution_wh - ) + polygon_zone = sv.PolygonZone(polygon=SOURCE) view_transformer = ViewTransformer(source=SOURCE, target=TARGET) coordinates = defaultdict(lambda: deque(maxlen=video_info.fps)) From 15b1473c2f71c6ff9e4c318efe95b73c168b3dc0 Mon Sep 17 00:00:00 2001 From: Jeslin P James Date: Sat, 13 Apr 2024 10:28:11 +0530 Subject: [PATCH 024/274] Updated examples to remove frame_resolution_wh from PolygonZone --- examples/time_in_zone/inference_file_example.py | 4 ---- examples/time_in_zone/inference_naive_stream_example.py | 4 ---- examples/time_in_zone/inference_stream_example.py | 2 -- examples/time_in_zone/ultralytics_file_example.py | 4 ---- examples/time_in_zone/ultralytics_naive_stream_example.py | 4 ---- examples/time_in_zone/ultralytics_stream_example.py | 2 -- examples/traffic_analysis/inference_example.py | 6 ++---- examples/traffic_analysis/ultralytics_example.py | 6 ++---- 8 files changed, 4 insertions(+), 28 deletions(-) diff --git a/examples/time_in_zone/inference_file_example.py b/examples/time_in_zone/inference_file_example.py index 5feb1d836..f49554644 100644 --- a/examples/time_in_zone/inference_file_example.py +++ b/examples/time_in_zone/inference_file_example.py @@ -29,14 +29,10 @@ def main( video_info = sv.VideoInfo.from_video_path(video_path=source_video_path) frames_generator = sv.get_video_frames_generator(source_video_path) - frame = next(frames_generator) - resolution_wh = frame.shape[1], frame.shape[0] - polygons = load_zones_config(file_path=zone_configuration_path) zones = [ sv.PolygonZone( polygon=polygon, - frame_resolution_wh=resolution_wh, triggering_anchors=(sv.Position.CENTER,), ) for polygon in polygons diff --git a/examples/time_in_zone/inference_naive_stream_example.py b/examples/time_in_zone/inference_naive_stream_example.py index dd2d68a5d..21880269f 100644 --- a/examples/time_in_zone/inference_naive_stream_example.py +++ b/examples/time_in_zone/inference_naive_stream_example.py @@ -29,14 +29,10 @@ def main( frames_generator = get_stream_frames_generator(rtsp_url=rtsp_url) fps_monitor = sv.FPSMonitor() - frame = next(frames_generator) - resolution_wh = frame.shape[1], frame.shape[0] - polygons = load_zones_config(file_path=zone_configuration_path) zones = [ sv.PolygonZone( polygon=polygon, - frame_resolution_wh=resolution_wh, triggering_anchors=(sv.Position.CENTER,), ) for polygon in polygons diff --git a/examples/time_in_zone/inference_stream_example.py b/examples/time_in_zone/inference_stream_example.py index e1fae57f9..604aade42 100644 --- a/examples/time_in_zone/inference_stream_example.py +++ b/examples/time_in_zone/inference_stream_example.py @@ -28,11 +28,9 @@ def __init__(self, zone_configuration_path: str, classes: List[int]): def on_prediction(self, result: dict, frame: VideoFrame) -> None: if self.zones is None: - resolution_wh = frame.image.shape[1], frame.image.shape[0] self.zones = [ sv.PolygonZone( polygon=polygon, - frame_resolution_wh=resolution_wh, triggering_anchors=(sv.Position.CENTER,), ) for polygon in self.polygons diff --git a/examples/time_in_zone/ultralytics_file_example.py b/examples/time_in_zone/ultralytics_file_example.py index fe8ce58df..b470b7a31 100644 --- a/examples/time_in_zone/ultralytics_file_example.py +++ b/examples/time_in_zone/ultralytics_file_example.py @@ -30,14 +30,10 @@ def main( video_info = sv.VideoInfo.from_video_path(video_path=source_video_path) frames_generator = sv.get_video_frames_generator(source_video_path) - frame = next(frames_generator) - resolution_wh = frame.shape[1], frame.shape[0] - polygons = load_zones_config(file_path=zone_configuration_path) zones = [ sv.PolygonZone( polygon=polygon, - frame_resolution_wh=resolution_wh, triggering_anchors=(sv.Position.CENTER,), ) for polygon in polygons diff --git a/examples/time_in_zone/ultralytics_naive_stream_example.py b/examples/time_in_zone/ultralytics_naive_stream_example.py index 1cc82b446..d69221436 100644 --- a/examples/time_in_zone/ultralytics_naive_stream_example.py +++ b/examples/time_in_zone/ultralytics_naive_stream_example.py @@ -30,14 +30,10 @@ def main( frames_generator = get_stream_frames_generator(rtsp_url=rtsp_url) fps_monitor = sv.FPSMonitor() - frame = next(frames_generator) - resolution_wh = frame.shape[1], frame.shape[0] - polygons = load_zones_config(file_path=zone_configuration_path) zones = [ sv.PolygonZone( polygon=polygon, - frame_resolution_wh=resolution_wh, triggering_anchors=(sv.Position.CENTER,), ) for polygon in polygons diff --git a/examples/time_in_zone/ultralytics_stream_example.py b/examples/time_in_zone/ultralytics_stream_example.py index 25dc874f8..6262d5947 100644 --- a/examples/time_in_zone/ultralytics_stream_example.py +++ b/examples/time_in_zone/ultralytics_stream_example.py @@ -29,11 +29,9 @@ def __init__(self, zone_configuration_path: str, classes: List[int]): def on_prediction(self, detections: sv.Detections, frame: VideoFrame) -> None: if self.zones is None: - resolution_wh = frame.image.shape[1], frame.image.shape[0] self.zones = [ sv.PolygonZone( polygon=polygon, - frame_resolution_wh=resolution_wh, triggering_anchors=(sv.Position.CENTER,), ) for polygon in self.polygons diff --git a/examples/traffic_analysis/inference_example.py b/examples/traffic_analysis/inference_example.py index 7da5f37cb..76096be2a 100644 --- a/examples/traffic_analysis/inference_example.py +++ b/examples/traffic_analysis/inference_example.py @@ -60,13 +60,11 @@ def update( def initiate_polygon_zones( polygons: List[np.ndarray], - frame_resolution_wh: Tuple[int, int], triggering_anchors: Iterable[sv.Position] = [sv.Position.CENTER], ) -> List[sv.PolygonZone]: return [ sv.PolygonZone( polygon=polygon, - frame_resolution_wh=frame_resolution_wh, triggering_anchors=triggering_anchors, ) for polygon in polygons @@ -93,10 +91,10 @@ def __init__( self.video_info = sv.VideoInfo.from_video_path(source_video_path) self.zones_in = initiate_polygon_zones( - ZONE_IN_POLYGONS, self.video_info.resolution_wh, [sv.Position.CENTER] + ZONE_IN_POLYGONS, [sv.Position.CENTER] ) self.zones_out = initiate_polygon_zones( - ZONE_OUT_POLYGONS, self.video_info.resolution_wh, [sv.Position.CENTER] + ZONE_OUT_POLYGONS, [sv.Position.CENTER] ) self.bounding_box_annotator = sv.BoundingBoxAnnotator(color=COLORS) diff --git a/examples/traffic_analysis/ultralytics_example.py b/examples/traffic_analysis/ultralytics_example.py index 78189d29c..0872100e5 100644 --- a/examples/traffic_analysis/ultralytics_example.py +++ b/examples/traffic_analysis/ultralytics_example.py @@ -58,13 +58,11 @@ def update( def initiate_polygon_zones( polygons: List[np.ndarray], - frame_resolution_wh: Tuple[int, int], triggering_anchors: Iterable[sv.Position] = [sv.Position.CENTER], ) -> List[sv.PolygonZone]: return [ sv.PolygonZone( polygon=polygon, - frame_resolution_wh=frame_resolution_wh, triggering_anchors=triggering_anchors, ) for polygon in polygons @@ -90,10 +88,10 @@ def __init__( self.video_info = sv.VideoInfo.from_video_path(source_video_path) self.zones_in = initiate_polygon_zones( - ZONE_IN_POLYGONS, self.video_info.resolution_wh, [sv.Position.CENTER] + ZONE_IN_POLYGONS, [sv.Position.CENTER] ) self.zones_out = initiate_polygon_zones( - ZONE_OUT_POLYGONS, self.video_info.resolution_wh, [sv.Position.CENTER] + ZONE_OUT_POLYGONS, [sv.Position.CENTER] ) self.bounding_box_annotator = sv.BoundingBoxAnnotator(color=COLORS) From d531c59f85333fcdcb969741261c2e5b705f0fe3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 13 Apr 2024 05:39:02 +0000 Subject: [PATCH 025/274] =?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 --- examples/traffic_analysis/inference_example.py | 10 +++------- examples/traffic_analysis/ultralytics_example.py | 10 +++------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/examples/traffic_analysis/inference_example.py b/examples/traffic_analysis/inference_example.py index 76096be2a..3cf750add 100644 --- a/examples/traffic_analysis/inference_example.py +++ b/examples/traffic_analysis/inference_example.py @@ -1,6 +1,6 @@ import argparse import os -from typing import Dict, Iterable, List, Set, Tuple +from typing import Dict, Iterable, List, Set import cv2 import numpy as np @@ -90,12 +90,8 @@ def __init__( self.tracker = sv.ByteTrack() self.video_info = sv.VideoInfo.from_video_path(source_video_path) - self.zones_in = initiate_polygon_zones( - ZONE_IN_POLYGONS, [sv.Position.CENTER] - ) - self.zones_out = initiate_polygon_zones( - ZONE_OUT_POLYGONS, [sv.Position.CENTER] - ) + self.zones_in = initiate_polygon_zones(ZONE_IN_POLYGONS, [sv.Position.CENTER]) + self.zones_out = initiate_polygon_zones(ZONE_OUT_POLYGONS, [sv.Position.CENTER]) self.bounding_box_annotator = sv.BoundingBoxAnnotator(color=COLORS) self.label_annotator = sv.LabelAnnotator( diff --git a/examples/traffic_analysis/ultralytics_example.py b/examples/traffic_analysis/ultralytics_example.py index 0872100e5..16ba6d8cf 100644 --- a/examples/traffic_analysis/ultralytics_example.py +++ b/examples/traffic_analysis/ultralytics_example.py @@ -1,5 +1,5 @@ import argparse -from typing import Dict, Iterable, List, Set, Tuple +from typing import Dict, Iterable, List, Set import cv2 import numpy as np @@ -87,12 +87,8 @@ def __init__( self.tracker = sv.ByteTrack() self.video_info = sv.VideoInfo.from_video_path(source_video_path) - self.zones_in = initiate_polygon_zones( - ZONE_IN_POLYGONS, [sv.Position.CENTER] - ) - self.zones_out = initiate_polygon_zones( - ZONE_OUT_POLYGONS, [sv.Position.CENTER] - ) + self.zones_in = initiate_polygon_zones(ZONE_IN_POLYGONS, [sv.Position.CENTER]) + self.zones_out = initiate_polygon_zones(ZONE_OUT_POLYGONS, [sv.Position.CENTER]) self.bounding_box_annotator = sv.BoundingBoxAnnotator(color=COLORS) self.label_annotator = sv.LabelAnnotator( From 8329d155e4dc62f5da2a4b7b3df1936694765a91 Mon Sep 17 00:00:00 2001 From: Jeslin P James Date: Sat, 13 Apr 2024 17:27:27 +0530 Subject: [PATCH 026/274] Use default font when font_path isnt specified --- supervision/annotators/core.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index dfdf99905..a10335a73 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -3,7 +3,7 @@ import cv2 import numpy as np -from PIL import ImageFont, ImageDraw +from PIL import ImageFont, ImageDraw, Image from supervision.annotators.base import BaseAnnotator, ImageType from supervision.annotators.utils import ColorLookup, Trace, resolve_color @@ -1154,7 +1154,7 @@ def __init__( self, color: Union[Color, ColorPalette] = ColorPalette.DEFAULT, text_color: Color = Color.WHITE, - font_path: str = "/content/Arial Unicode Font.ttf", + font_path: str = None, font_size: int = 14, text_padding: int = 10, text_position: Position = Position.TOP_LEFT, @@ -1163,12 +1163,19 @@ def __init__( ): self.color = color self.text_color = text_color - self.font = ImageFont.truetype(font_path, font_size) self.text_padding = text_padding self.text_anchor = text_position self.color_lookup = color_lookup self.border_radius = border_radius - + if font_path is not None: + try: + self.font = ImageFont.truetype(font_path, font_size) + except OSError: + print(f"Font path '{font_path}' not found. Using a system font.") + self.font = ImageFont.load_default(size=font_size) + else: + self.font = ImageFont.load_default(size=font_size) + @staticmethod def resolve_text_background_xyxy( center_coordinates: Tuple[int, int], @@ -1221,8 +1228,7 @@ def resolve_text_background_xyxy( center_x + text_w, center_y + text_h // 2, ) - - + def annotate( self, scene: ImageType, @@ -1230,6 +1236,8 @@ def annotate( labels: List[str] = None, custom_color_lookup: Optional[np.ndarray] = None, ) -> ImageType: + if isinstance(scene, np.ndarray): + scene = Image.fromarray(cv2.cvtColor(scene, cv2.COLOR_BGR2RGB)) draw = ImageDraw.Draw(scene) anchors_coordinates = detections.get_anchors_coordinates( anchor=self.text_anchor @@ -1292,7 +1300,6 @@ def annotate( return scene - class BlurAnnotator(BaseAnnotator): """ A class for blurring regions in an image using provided detections. From 8255bcb1cab76923300dfff429dfa300754db97e Mon Sep 17 00:00:00 2001 From: Jeslin P James Date: Sat, 13 Apr 2024 17:43:52 +0530 Subject: [PATCH 027/274] Moved resolve_text_background_xyxy() function into utils.py --- supervision/annotators/core.py | 112 +------------------------------- supervision/annotators/utils.py | 54 ++++++++++++++- 2 files changed, 56 insertions(+), 110 deletions(-) diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index a10335a73..61eb9fd3f 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -6,7 +6,7 @@ from PIL import ImageFont, ImageDraw, Image from supervision.annotators.base import BaseAnnotator, ImageType -from supervision.annotators.utils import ColorLookup, Trace, resolve_color +from supervision.annotators.utils import ColorLookup, Trace, resolve_color, resolve_text_background_xyxy from supervision.config import CLASS_NAME_DATA_FIELD, ORIENTED_BOX_COORDINATES from supervision.detection.core import Detections from supervision.detection.utils import clip_boxes, mask_to_polygons @@ -937,59 +937,6 @@ def __init__( self.text_anchor: Position = text_position self.color_lookup: ColorLookup = color_lookup - @staticmethod - def resolve_text_background_xyxy( - center_coordinates: Tuple[int, int], - text_wh: Tuple[int, int], - position: Position, - ) -> Tuple[int, int, int, int]: - center_x, center_y = center_coordinates - text_w, text_h = text_wh - - if position == Position.TOP_LEFT: - return center_x, center_y - text_h, center_x + text_w, center_y - elif position == Position.TOP_RIGHT: - return center_x - text_w, center_y - text_h, center_x, center_y - elif position == Position.TOP_CENTER: - return ( - center_x - text_w // 2, - center_y - text_h, - center_x + text_w // 2, - center_y, - ) - elif position == Position.CENTER or position == Position.CENTER_OF_MASS: - return ( - center_x - text_w // 2, - center_y - text_h // 2, - center_x + text_w // 2, - center_y + text_h // 2, - ) - elif position == Position.BOTTOM_LEFT: - return center_x, center_y, center_x + text_w, center_y + text_h - elif position == Position.BOTTOM_RIGHT: - return center_x - text_w, center_y, center_x, center_y + text_h - elif position == Position.BOTTOM_CENTER: - return ( - center_x - text_w // 2, - center_y, - center_x + text_w // 2, - center_y + text_h, - ) - elif position == Position.CENTER_LEFT: - return ( - center_x - text_w, - center_y - text_h // 2, - center_x, - center_y + text_h // 2, - ) - elif position == Position.CENTER_RIGHT: - return ( - center_x, - center_y - text_h // 2, - center_x + text_w, - center_y + text_h // 2, - ) - @convert_for_annotation_method def annotate( self, @@ -1079,7 +1026,7 @@ def annotate( )[0] text_w_padded = text_w + 2 * self.text_padding text_h_padded = text_h + 2 * self.text_padding - text_background_xyxy = self.resolve_text_background_xyxy( + text_background_xyxy = resolve_text_background_xyxy( center_coordinates=tuple(center_coordinates), text_wh=(text_w_padded, text_h_padded), position=self.text_anchor, @@ -1176,59 +1123,6 @@ def __init__( else: self.font = ImageFont.load_default(size=font_size) - @staticmethod - def resolve_text_background_xyxy( - center_coordinates: Tuple[int, int], - text_wh: Tuple[int, int], - position: Position, - ) -> Tuple[int, int, int, int]: - center_x, center_y = center_coordinates - text_w, text_h = text_wh - - if position == Position.TOP_LEFT: - return center_x, center_y - text_h, center_x + text_w, center_y - elif position == Position.TOP_RIGHT: - return center_x - text_w, center_y - text_h, center_x, center_y - elif position == Position.TOP_CENTER: - return ( - center_x - text_w // 2, - center_y - text_h, - center_x + text_w // 2, - center_y, - ) - elif position == Position.CENTER or position == Position.CENTER_OF_MASS: - return ( - center_x - text_w // 2, - center_y - text_h // 2, - center_x + text_w // 2, - center_y + text_h // 2, - ) - elif position == Position.BOTTOM_LEFT: - return center_x, center_y, center_x + text_w, center_y + text_h - elif position == Position.BOTTOM_RIGHT: - return center_x - text_w, center_y, center_x, center_y + text_h - elif position == Position.BOTTOM_CENTER: - return ( - center_x - text_w // 2, - center_y, - center_x + text_w // 2, - center_y + text_h, - ) - elif position == Position.CENTER_LEFT: - return ( - center_x - text_w, - center_y - text_h // 2, - center_x, - center_y + text_h // 2, - ) - elif position == Position.CENTER_RIGHT: - return ( - center_x, - center_y - text_h // 2, - center_x + text_w, - center_y + text_h // 2, - ) - def annotate( self, scene: ImageType, @@ -1276,7 +1170,7 @@ def annotate( text_height = bottom - top text_w_padded = text_width + 2 * self.text_padding text_h_padded = text_height + 2 * self.text_padding - text_background_xyxy = self.resolve_text_background_xyxy( + text_background_xyxy = resolve_text_background_xyxy( center_coordinates=tuple(center_coordinates), text_wh=(text_w_padded, text_h_padded), position=self.text_anchor, diff --git a/supervision/annotators/utils.py b/supervision/annotators/utils.py index e206c8cbb..bdc17975c 100644 --- a/supervision/annotators/utils.py +++ b/supervision/annotators/utils.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Optional, Union +from typing import Optional, Union, Tuple import numpy as np @@ -62,6 +62,58 @@ def resolve_color_idx( ) return detections.tracker_id[detection_idx] +def resolve_text_background_xyxy( + center_coordinates: Tuple[int, int], + text_wh: Tuple[int, int], + position: Position, +) -> Tuple[int, int, int, int]: + center_x, center_y = center_coordinates + text_w, text_h = text_wh + + if position == Position.TOP_LEFT: + return center_x, center_y - text_h, center_x + text_w, center_y + elif position == Position.TOP_RIGHT: + return center_x - text_w, center_y - text_h, center_x, center_y + elif position == Position.TOP_CENTER: + return ( + center_x - text_w // 2, + center_y - text_h, + center_x + text_w // 2, + center_y, + ) + elif position == Position.CENTER or position == Position.CENTER_OF_MASS: + return ( + center_x - text_w // 2, + center_y - text_h // 2, + center_x + text_w // 2, + center_y + text_h // 2, + ) + elif position == Position.BOTTOM_LEFT: + return center_x, center_y, center_x + text_w, center_y + text_h + elif position == Position.BOTTOM_RIGHT: + return center_x - text_w, center_y, center_x, center_y + text_h + elif position == Position.BOTTOM_CENTER: + return ( + center_x - text_w // 2, + center_y, + center_x + text_w // 2, + center_y + text_h, + ) + elif position == Position.CENTER_LEFT: + return ( + center_x - text_w, + center_y - text_h // 2, + center_x, + center_y + text_h // 2, + ) + elif position == Position.CENTER_RIGHT: + return ( + center_x, + center_y - text_h // 2, + center_x + text_w, + center_y + text_h // 2, + ) + def get_color_by_index(color: Union[Color, ColorPalette], idx: int) -> Color: if isinstance(color, ColorPalette): From 8bb8ce03167a68f0cda1a9089026306719a2d0aa Mon Sep 17 00:00:00 2001 From: Jeslin P James Date: Sat, 13 Apr 2024 18:03:08 +0530 Subject: [PATCH 028/274] changed error message and formatted code --- supervision/annotators/core.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index 61eb9fd3f..baf045740 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -6,7 +6,12 @@ from PIL import ImageFont, ImageDraw, Image from supervision.annotators.base import BaseAnnotator, ImageType -from supervision.annotators.utils import ColorLookup, Trace, resolve_color, resolve_text_background_xyxy +from supervision.annotators.utils import ( + ColorLookup, + Trace, + resolve_color, + resolve_text_background_xyxy, +) from supervision.config import CLASS_NAME_DATA_FIELD, ORIENTED_BOX_COORDINATES from supervision.detection.core import Detections from supervision.detection.utils import clip_boxes, mask_to_polygons @@ -1004,9 +1009,11 @@ def annotate( color=self.color, detections=detections, detection_idx=detection_idx, - color_lookup=self.color_lookup - if custom_color_lookup is None - else custom_color_lookup, + color_lookup=( + self.color_lookup + if custom_color_lookup is None + else custom_color_lookup + ), ) if labels is not None: @@ -1095,6 +1102,7 @@ def draw_rounded_rectangle( ) return scene + class RichLabelAnnotator: def __init__( @@ -1102,7 +1110,7 @@ def __init__( color: Union[Color, ColorPalette] = ColorPalette.DEFAULT, text_color: Color = Color.WHITE, font_path: str = None, - font_size: int = 14, + font_size: int = 10, text_padding: int = 10, text_position: Position = Position.TOP_LEFT, color_lookup: ColorLookup = ColorLookup.CLASS, @@ -1118,7 +1126,7 @@ def __init__( try: self.font = ImageFont.truetype(font_path, font_size) except OSError: - print(f"Font path '{font_path}' not found. Using a system font.") + print(f"Font path '{font_path}' not found. Using PIL's default font.") self.font = ImageFont.load_default(size=font_size) else: self.font = ImageFont.load_default(size=font_size) From cb220479ebaf63464f758550b58e7eb321ed0f87 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 13 Apr 2024 13:16:13 +0000 Subject: [PATCH 029/274] =?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/annotators/core.py | 4 ++-- supervision/annotators/utils.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index baf045740..4a8d0b668 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -3,7 +3,7 @@ import cv2 import numpy as np -from PIL import ImageFont, ImageDraw, Image +from PIL import Image, ImageDraw, ImageFont from supervision.annotators.base import BaseAnnotator, ImageType from supervision.annotators.utils import ( @@ -1104,7 +1104,6 @@ def draw_rounded_rectangle( class RichLabelAnnotator: - def __init__( self, color: Union[Color, ColorPalette] = ColorPalette.DEFAULT, @@ -1202,6 +1201,7 @@ def annotate( return scene + class BlurAnnotator(BaseAnnotator): """ A class for blurring regions in an image using provided detections. diff --git a/supervision/annotators/utils.py b/supervision/annotators/utils.py index bdc17975c..f0de9fa51 100644 --- a/supervision/annotators/utils.py +++ b/supervision/annotators/utils.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Optional, Union, Tuple +from typing import Optional, Tuple, Union import numpy as np @@ -62,6 +62,7 @@ def resolve_color_idx( ) return detections.tracker_id[detection_idx] + def resolve_text_background_xyxy( center_coordinates: Tuple[int, int], text_wh: Tuple[int, int], From df4a6f857cc138e5e9b680f223be0ef6527a40d6 Mon Sep 17 00:00:00 2001 From: Jeslin P James Date: Sat, 13 Apr 2024 18:58:18 +0530 Subject: [PATCH 030/274] added docs for RichLabelAnnotator --- supervision/annotators/core.py | 60 ++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index 4a8d0b668..f2b552cd6 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -1104,6 +1104,11 @@ def draw_rounded_rectangle( class RichLabelAnnotator: + """ + A class for annotating labels on an image using provided detections, + with support for Unicode characters by using a custom font. + """ + def __init__( self, color: Union[Color, ColorPalette] = ColorPalette.DEFAULT, @@ -1115,6 +1120,22 @@ def __init__( color_lookup: ColorLookup = ColorLookup.CLASS, border_radius: int = 0, ): + """ + Args: + color (Union[Color, ColorPalette]): The color or color palette to use for + annotating the text background. + text_color (Color): The color to use for the text. + font_path (str): Path to the font file (e.g., ".ttf" or ".otf") to use for rendering text. + If `None`, the default PIL font will be used. + font_size (int): Font size for the text. + text_padding (int): Padding around the text within its background box. + text_position (Position): Position of the text relative to the detection. + Possible values are defined in the `Position` enum. + color_lookup (ColorLookup): Strategy for mapping colors to annotations. + Options are `INDEX`, `CLASS`, `TRACK`. + border_radius (int): The radius to apply round edges. If the selected + value is higher than the lower dimension, width or height, is clipped. + """ self.color = color self.text_color = text_color self.text_padding = text_padding @@ -1137,6 +1158,45 @@ def annotate( labels: List[str] = None, custom_color_lookup: Optional[np.ndarray] = None, ) -> ImageType: + """ + Annotates the given scene with labels based on the provided + detections, with support for Unicode characters. + + Args: + scene (ImageType): The image where labels will be drawn. + `ImageType` is a flexible type, accepting either `numpy.ndarray` + or `PIL.Image.Image`. + detections (Detections): Object detections to annotate. + labels (List[str]): Optional. Custom labels for each detection. + custom_color_lookup (Optional[np.ndarray]): Custom color lookup array. + Allows to override the default color mapping strategy. + + Returns: + The annotated image, matching the type of `scene` (`numpy.ndarray` + or `PIL.Image.Image`) + + Example: + ```python + import supervision as sv + + image = ... + detections = sv.Detections(...) + + labels = [ + f"{class_name} {confidence:.2f}" + for class_name, confidence + in zip(detections['class_name'], detections.confidence) + ] + + label_annotator = sv.RichLabelAnnotator(font_path="path/to/font.ttf") + annotated_frame = label_annotator.annotate( + scene=image.copy(), + detections=detections, + labels=labels + ) + ``` + + """ if isinstance(scene, np.ndarray): scene = Image.fromarray(cv2.cvtColor(scene, cv2.COLOR_BGR2RGB)) draw = ImageDraw.Draw(scene) From a45341ce63d98d982ce7e975ba6df94a5468cdb0 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Wed, 24 Apr 2024 22:41:59 +0200 Subject: [PATCH 031/274] final cleanup; setting `supervision>=0.20.0` in `requirements.txt` and refactoring stream processing scripts --- examples/count_people_in_zone/requirements.txt | 2 +- examples/speed_estimation/requirements.txt | 2 +- .../time_in_zone/inference_stream_example.py | 17 +++++++---------- examples/time_in_zone/requirements.txt | 2 +- .../time_in_zone/ultralytics_stream_example.py | 17 +++++++---------- examples/traffic_analysis/requirements.txt | 2 +- 6 files changed, 18 insertions(+), 24 deletions(-) diff --git a/examples/count_people_in_zone/requirements.txt b/examples/count_people_in_zone/requirements.txt index d9e272646..114447fc1 100644 --- a/examples/count_people_in_zone/requirements.txt +++ b/examples/count_people_in_zone/requirements.txt @@ -1,5 +1,5 @@ gdown inference -supervision==0.19.0 +supervision>=0.20.0 tqdm ultralytics diff --git a/examples/speed_estimation/requirements.txt b/examples/speed_estimation/requirements.txt index 36de970db..af2e1c13c 100644 --- a/examples/speed_estimation/requirements.txt +++ b/examples/speed_estimation/requirements.txt @@ -1,4 +1,4 @@ -supervision==0.19.0 +supervision>=0.20.0 tqdm==4.66.1 requests ultralytics==8.0.237 diff --git a/examples/time_in_zone/inference_stream_example.py b/examples/time_in_zone/inference_stream_example.py index 604aade42..0dfdf660b 100644 --- a/examples/time_in_zone/inference_stream_example.py +++ b/examples/time_in_zone/inference_stream_example.py @@ -24,18 +24,15 @@ def __init__(self, zone_configuration_path: str, classes: List[int]): self.fps_monitor = sv.FPSMonitor() self.polygons = load_zones_config(file_path=zone_configuration_path) self.timers = [ClockBasedTimer() for _ in self.polygons] - self.zones = None + self.zones = [ + sv.PolygonZone( + polygon=polygon, + triggering_anchors=(sv.Position.CENTER,), + ) + for polygon in self.polygons + ] def on_prediction(self, result: dict, frame: VideoFrame) -> None: - if self.zones is None: - self.zones = [ - sv.PolygonZone( - polygon=polygon, - triggering_anchors=(sv.Position.CENTER,), - ) - for polygon in self.polygons - ] - self.fps_monitor.tick() fps = self.fps_monitor.fps diff --git a/examples/time_in_zone/requirements.txt b/examples/time_in_zone/requirements.txt index fa17b9864..752b4a8f5 100644 --- a/examples/time_in_zone/requirements.txt +++ b/examples/time_in_zone/requirements.txt @@ -1,5 +1,5 @@ opencv-python -supervision +supervision>=0.20.0 ultralytics inference pytube diff --git a/examples/time_in_zone/ultralytics_stream_example.py b/examples/time_in_zone/ultralytics_stream_example.py index 6262d5947..8b8ecd607 100644 --- a/examples/time_in_zone/ultralytics_stream_example.py +++ b/examples/time_in_zone/ultralytics_stream_example.py @@ -25,18 +25,15 @@ def __init__(self, zone_configuration_path: str, classes: List[int]): self.fps_monitor = sv.FPSMonitor() self.polygons = load_zones_config(file_path=zone_configuration_path) self.timers = [ClockBasedTimer() for _ in self.polygons] - self.zones = None + self.zones = [ + sv.PolygonZone( + polygon=polygon, + triggering_anchors=(sv.Position.CENTER,), + ) + for polygon in self.polygons + ] def on_prediction(self, detections: sv.Detections, frame: VideoFrame) -> None: - if self.zones is None: - self.zones = [ - sv.PolygonZone( - polygon=polygon, - triggering_anchors=(sv.Position.CENTER,), - ) - for polygon in self.polygons - ] - self.fps_monitor.tick() fps = self.fps_monitor.fps diff --git a/examples/traffic_analysis/requirements.txt b/examples/traffic_analysis/requirements.txt index 6e72dd55e..114447fc1 100644 --- a/examples/traffic_analysis/requirements.txt +++ b/examples/traffic_analysis/requirements.txt @@ -1,5 +1,5 @@ gdown inference -supervision>=0.19.0 +supervision>=0.20.0 tqdm ultralytics From e253f86e8f2a68fbed76d273092d6529972076e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 00:46:24 +0000 Subject: [PATCH 032/274] :arrow_up: Bump mypy from 1.9.0 to 1.10.0 Bumps [mypy](https://github.com/python/mypy) from 1.9.0 to 1.10.0. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/1.9.0...v1.10.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- poetry.lock | 56 ++++++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/poetry.lock b/poetry.lock index 95cbef017..e279ed9a5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2277,38 +2277,38 @@ files = [ [[package]] name = "mypy" -version = "1.9.0" +version = "1.10.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, - {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, - {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, - {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, - {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, - {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, - {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, - {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, - {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, - {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, - {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, - {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, - {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, - {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, - {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, - {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, - {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, - {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, - {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, + {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, + {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, + {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, + {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, + {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, + {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, + {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, + {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, + {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, + {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, + {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, + {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, + {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, + {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, + {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, + {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, + {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, ] [package.dependencies] From cab06f2c2b424e547b7c4e80f142b0ba61aeb70d Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Thu, 25 Apr 2024 12:29:08 +0300 Subject: [PATCH 033/274] Implementation of from_yolo_nas for keypoints * Missing batch image support --- supervision/keypoint/core.py | 55 ++++++++++++++++++++++++++++--- supervision/keypoint/skeletons.py | 24 ++++++++++++++ 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index 8a97b51c3..587d459c8 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -50,7 +50,8 @@ class simplifies data manipulation and filtering, providing a uniform API for xy: npt.NDArray[np.float32] class_id: Optional[npt.NDArray[np.int_]] = None confidence: Optional[npt.NDArray[np.float32]] = None - data: Dict[str, Union[npt.NDArray[Any], List]] = field(default_factory=dict) + data: Dict[str, Union[npt.NDArray[Any], List] + ] = field(default_factory=dict) def __post_init__(self): validate_keypoints_fields( @@ -103,7 +104,7 @@ def __eq__(self, other: KeyPoints) -> bool: @classmethod def from_ultralytics(cls, ultralytics_results) -> KeyPoints: """ - Creates a Keypoints instance from a + Creates a KeyPoints instance from a [YOLOv8](https://github.com/ultralytics/ultralytics) inference result. Args: @@ -111,7 +112,7 @@ def from_ultralytics(cls, ultralytics_results) -> KeyPoints: The output Results instance from YOLOv8 Returns: - KeyPoints: A new Keypoints object. + KeyPoints: A new KeyPoints object. Example: ```python @@ -130,12 +131,56 @@ def from_ultralytics(cls, ultralytics_results) -> KeyPoints: xy = ultralytics_results.keypoints.xy.cpu().numpy() 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]) confidence = ultralytics_results.keypoints.conf.cpu().numpy() data = {CLASS_NAME_DATA_FIELD: class_names} return cls(xy, class_id, confidence, data) + @classmethod + def from_yolo_nas(cls, yolo_nas_results) -> KeyPoints: + """ + Create a KeyPoints instance from a YOLO NAS results. + + Args: + yolo_nas_results (ImagePoseEstimationPrediction): The output object from YOLO NAS. + + Returns: + KeyPoints: A new KeyPoints object. + + Example: + ```python + import cv2 + import supervision as sv + import super_gradients + + image = cv2.imread() + + yolo_nas = super_gradients.training.models.get( + "yolo_nas_pose_s", pretrained_weights="coco_pose").to("cuda") + + results = yolo_nas.predict(image, conf=0.1) + keypoints = sv.KeyPoints.from_yolo_nas(results) + ``` + """ + if len(yolo_nas_results.prediction.poses) == 0: + return cls.empty() + + # TODO: multi-image input + + xy = yolo_nas_results.prediction.poses[:, :, :2] + confidence = yolo_nas_results.prediction.poses[:, :, 2] + class_id = [0] * len(xy) + data = {CLASS_NAME_DATA_FIELD: [f"person" for _ in xy]} + + return cls( + xy=xy, + confidence=confidence, + class_id=np.array(class_id), + data=data, + ) + def __getitem__( self, index: Union[int, slice, List[int], np.ndarray, str] ) -> Union["KeyPoints", List, np.ndarray, None]: @@ -216,7 +261,7 @@ def __setitem__(self, key: str, value: Union[np.ndarray, List]): self.data[key] = value - @classmethod + @ classmethod def empty(cls) -> KeyPoints: """ Create an empty Keypoints object with no keypoints. diff --git a/supervision/keypoint/skeletons.py b/supervision/keypoint/skeletons.py index 6c1108541..01f768b19 100644 --- a/supervision/keypoint/skeletons.py +++ b/supervision/keypoint/skeletons.py @@ -25,6 +25,30 @@ class Skeleton(Enum): (17, 15), ] + # Hardcoding here, but it also comes within model results + # Note: the classes in the model are 0-indexed, while the skeleton is 1-indexed + YOLO_NAS = [ + (0, 1), + (0, 2), + (1, 2), + (1, 3), + (2, 4), + (3, 5), + (4, 6), + (5, 6), + (5, 7), + (5, 11), + (6, 8), + (6, 12), + (7, 9), + (8, 10), + (11, 12), + (11, 13), + (12, 14), + (13, 15), + (14, 16) + ] + SKELETONS_BY_EDGE_COUNT: Dict[int, Edges] = {} SKELETONS_BY_VERTEX_COUNT: Dict[int, Edges] = {} From 18b3c17ed423649ff933826f72ed68c44b15b89a Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Thu, 25 Apr 2024 12:42:33 +0300 Subject: [PATCH 034/274] Styling --- supervision/keypoint/core.py | 13 ++++++------- supervision/keypoint/skeletons.py | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index 587d459c8..2f2e9aa40 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -50,8 +50,7 @@ class simplifies data manipulation and filtering, providing a uniform API for xy: npt.NDArray[np.float32] class_id: Optional[npt.NDArray[np.int_]] = None confidence: Optional[npt.NDArray[np.float32]] = None - data: Dict[str, Union[npt.NDArray[Any], List] - ] = field(default_factory=dict) + data: Dict[str, Union[npt.NDArray[Any], List]] = field(default_factory=dict) def __post_init__(self): validate_keypoints_fields( @@ -131,8 +130,7 @@ def from_ultralytics(cls, ultralytics_results) -> KeyPoints: xy = ultralytics_results.keypoints.xy.cpu().numpy() 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]) confidence = ultralytics_results.keypoints.conf.cpu().numpy() data = {CLASS_NAME_DATA_FIELD: class_names} @@ -144,7 +142,8 @@ def from_yolo_nas(cls, yolo_nas_results) -> KeyPoints: Create a KeyPoints instance from a YOLO NAS 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. @@ -172,7 +171,7 @@ def from_yolo_nas(cls, yolo_nas_results) -> KeyPoints: xy = yolo_nas_results.prediction.poses[:, :, :2] confidence = yolo_nas_results.prediction.poses[:, :, 2] class_id = [0] * len(xy) - data = {CLASS_NAME_DATA_FIELD: [f"person" for _ in xy]} + data = {CLASS_NAME_DATA_FIELD: ["person" for _ in xy]} return cls( xy=xy, @@ -261,7 +260,7 @@ def __setitem__(self, key: str, value: Union[np.ndarray, List]): self.data[key] = value - @ classmethod + @classmethod def empty(cls) -> KeyPoints: """ Create an empty Keypoints object with no keypoints. diff --git a/supervision/keypoint/skeletons.py b/supervision/keypoint/skeletons.py index 01f768b19..beb112810 100644 --- a/supervision/keypoint/skeletons.py +++ b/supervision/keypoint/skeletons.py @@ -46,7 +46,7 @@ class Skeleton(Enum): (11, 13), (12, 14), (13, 15), - (14, 16) + (14, 16), ] From c8c2ff5c454f36d5caf4823e9c680aa1bc8c8ca5 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Thu, 25 Apr 2024 12:53:09 +0300 Subject: [PATCH 035/274] Minor styling --- supervision/keypoint/core.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index 2f2e9aa40..968ee3d45 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -166,8 +166,6 @@ def from_yolo_nas(cls, yolo_nas_results) -> KeyPoints: if len(yolo_nas_results.prediction.poses) == 0: return cls.empty() - # TODO: multi-image input - xy = yolo_nas_results.prediction.poses[:, :, :2] confidence = yolo_nas_results.prediction.poses[:, :, 2] class_id = [0] * len(xy) From 399342513ab321c8b800aa41b566643ec5bd6b6d Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Thu, 25 Apr 2024 15:00:01 +0300 Subject: [PATCH 036/274] Keypoints: rm NAS skeleton, improve docs examples --- supervision/keypoint/core.py | 12 ++++++++---- supervision/keypoint/skeletons.py | 24 ------------------------ 2 files changed, 8 insertions(+), 28 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index 968ee3d45..cc02f84bd 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -50,7 +50,8 @@ class simplifies data manipulation and filtering, providing a uniform API for xy: npt.NDArray[np.float32] class_id: Optional[npt.NDArray[np.int_]] = None confidence: Optional[npt.NDArray[np.float32]] = None - data: Dict[str, Union[npt.NDArray[Any], List]] = field(default_factory=dict) + data: Dict[str, Union[npt.NDArray[Any], List] + ] = field(default_factory=dict) def __post_init__(self): validate_keypoints_fields( @@ -130,7 +131,8 @@ def from_ultralytics(cls, ultralytics_results) -> KeyPoints: xy = ultralytics_results.keypoints.xy.cpu().numpy() 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]) confidence = ultralytics_results.keypoints.conf.cpu().numpy() data = {CLASS_NAME_DATA_FIELD: class_names} @@ -151,13 +153,15 @@ def from_yolo_nas(cls, yolo_nas_results) -> KeyPoints: Example: ```python import cv2 + import torch import supervision as sv import super_gradients image = cv2.imread() + device = "cuda" if torch.cuda.is_available() else "cpu" yolo_nas = super_gradients.training.models.get( - "yolo_nas_pose_s", pretrained_weights="coco_pose").to("cuda") + "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) @@ -180,7 +184,7 @@ def from_yolo_nas(cls, yolo_nas_results) -> KeyPoints: def __getitem__( self, index: Union[int, slice, List[int], np.ndarray, str] - ) -> Union["KeyPoints", List, np.ndarray, None]: + ) -> Union[KeyPoints, List, np.ndarray, None]: """ Get a subset of the KeyPoints object or access an item from its data field. diff --git a/supervision/keypoint/skeletons.py b/supervision/keypoint/skeletons.py index beb112810..6c1108541 100644 --- a/supervision/keypoint/skeletons.py +++ b/supervision/keypoint/skeletons.py @@ -25,30 +25,6 @@ class Skeleton(Enum): (17, 15), ] - # Hardcoding here, but it also comes within model results - # Note: the classes in the model are 0-indexed, while the skeleton is 1-indexed - YOLO_NAS = [ - (0, 1), - (0, 2), - (1, 2), - (1, 3), - (2, 4), - (3, 5), - (4, 6), - (5, 6), - (5, 7), - (5, 11), - (6, 8), - (6, 12), - (7, 9), - (8, 10), - (11, 12), - (11, 13), - (12, 14), - (13, 15), - (14, 16), - ] - SKELETONS_BY_EDGE_COUNT: Dict[int, Edges] = {} SKELETONS_BY_VERTEX_COUNT: Dict[int, Edges] = {} From 4bd593fe3c0376bd57aa974be3e8da919268f33c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 12:02:20 +0000 Subject: [PATCH 037/274] =?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 | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index cc02f84bd..4790acd7b 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -50,8 +50,7 @@ class simplifies data manipulation and filtering, providing a uniform API for xy: npt.NDArray[np.float32] class_id: Optional[npt.NDArray[np.int_]] = None confidence: Optional[npt.NDArray[np.float32]] = None - data: Dict[str, Union[npt.NDArray[Any], List] - ] = field(default_factory=dict) + data: Dict[str, Union[npt.NDArray[Any], List]] = field(default_factory=dict) def __post_init__(self): validate_keypoints_fields( @@ -131,8 +130,7 @@ def from_ultralytics(cls, ultralytics_results) -> KeyPoints: xy = ultralytics_results.keypoints.xy.cpu().numpy() 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]) confidence = ultralytics_results.keypoints.conf.cpu().numpy() data = {CLASS_NAME_DATA_FIELD: class_names} From 059583e2ba1d8cab60308a5801ce8a32ea8b5a47 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Apr 2024 00:31:25 +0000 Subject: [PATCH 038/274] :arrow_up: Bump mkdocs-material from 9.5.18 to 9.5.19 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.5.18 to 9.5.19. - [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.18...9.5.19) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/poetry.lock b/poetry.lock index e279ed9a5..f94112097 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2083,34 +2083,34 @@ files = [ [[package]] name = "mkdocs" -version = "1.5.3" +version = "1.6.0" description = "Project documentation with Markdown." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"}, - {file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"}, + {file = "mkdocs-1.6.0-py3-none-any.whl", hash = "sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7"}, + {file = "mkdocs-1.6.0.tar.gz", hash = "sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512"}, ] [package.dependencies] click = ">=7.0" colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} ghp-import = ">=1.0" -importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} jinja2 = ">=2.11.1" -markdown = ">=3.2.1" +markdown = ">=3.3.6" markupsafe = ">=2.0.1" mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" packaging = ">=20.5" pathspec = ">=0.11.1" -platformdirs = ">=2.2.0" pyyaml = ">=5.1" pyyaml-env-tag = ">=0.1" watchdog = ">=2.0" [package.extras] i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pathspec (==0.11.1)", "platformdirs (==2.2.0)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] [[package]] name = "mkdocs-autorefs" @@ -2127,6 +2127,23 @@ files = [ Markdown = ">=3.3" mkdocs = ">=1.1" +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + [[package]] name = "mkdocs-git-committers-plugin-2" version = "2.3.0" @@ -2180,13 +2197,13 @@ pygments = ">2.12.0" [[package]] name = "mkdocs-material" -version = "9.5.18" +version = "9.5.19" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.18-py3-none-any.whl", hash = "sha256:1e0e27fc9fe239f9064318acf548771a4629d5fd5dfd45444fd80a953fe21eb4"}, - {file = "mkdocs_material-9.5.18.tar.gz", hash = "sha256:a43f470947053fa2405c33995f282d24992c752a50114f23f30da9d8d0c57e62"}, + {file = "mkdocs_material-9.5.19-py3-none-any.whl", hash = "sha256:ea96e150b6c95f5e4ffe47d78bb712c7bacdd91d2a0bec47f46b6fa0705a86ec"}, + {file = "mkdocs_material-9.5.19.tar.gz", hash = "sha256:7473e06e17e23af608a30ef583fdde8f36389dd3ef56b1d503eed54c89c9618c"}, ] [package.dependencies] @@ -2195,7 +2212,7 @@ cairosvg = {version = ">=2.6,<3.0", optional = true, markers = "extra == \"imagi colorama = ">=0.4,<1.0" jinja2 = ">=3.0,<4.0" markdown = ">=3.2,<4.0" -mkdocs = ">=1.5.3,<1.6.0" +mkdocs = ">=1.6,<2.0" mkdocs-material-extensions = ">=1.3,<2.0" paginate = ">=0.5,<1.0" pillow = {version = ">=10.2,<11.0", optional = true, markers = "extra == \"imaging\""} From 8436f52f8a1d02f7ff7c0251ee2d1b3962684981 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Apr 2024 00:33:10 +0000 Subject: [PATCH 039/274] :arrow_up: Bump ruff from 0.4.1 to 0.4.2 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.1 to 0.4.2. - [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.1...v0.4.2) --- 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 e279ed9a5..ebe49f100 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3643,28 +3643,28 @@ files = [ [[package]] name = "ruff" -version = "0.4.1" +version = "0.4.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2d9ef6231e3fbdc0b8c72404a1a0c46fd0dcea84efca83beb4681c318ea6a953"}, - {file = "ruff-0.4.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9485f54a7189e6f7433e0058cf8581bee45c31a25cd69009d2a040d1bd4bfaef"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2921ac03ce1383e360e8a95442ffb0d757a6a7ddd9a5be68561a671e0e5807e"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eec8d185fe193ad053eda3a6be23069e0c8ba8c5d20bc5ace6e3b9e37d246d3f"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa27d9d72a94574d250f42b7640b3bd2edc4c58ac8ac2778a8c82374bb27984"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f1ee41580bff1a651339eb3337c20c12f4037f6110a36ae4a2d864c52e5ef954"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0926cefb57fc5fced629603fbd1a23d458b25418681d96823992ba975f050c2b"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c6e37f2e3cd74496a74af9a4fa67b547ab3ca137688c484749189bf3a686ceb"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd703a5975ac1998c2cc5e9494e13b28f31e66c616b0a76e206de2562e0843c"}, - {file = "ruff-0.4.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b92f03b4aa9fa23e1799b40f15f8b95cdc418782a567d6c43def65e1bbb7f1cf"}, - {file = "ruff-0.4.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1c859f294f8633889e7d77de228b203eb0e9a03071b72b5989d89a0cf98ee262"}, - {file = "ruff-0.4.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b34510141e393519a47f2d7b8216fec747ea1f2c81e85f076e9f2910588d4b64"}, - {file = "ruff-0.4.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6e68d248ed688b9d69fd4d18737edcbb79c98b251bba5a2b031ce2470224bdf9"}, - {file = "ruff-0.4.1-py3-none-win32.whl", hash = "sha256:b90506f3d6d1f41f43f9b7b5ff845aeefabed6d2494307bc7b178360a8805252"}, - {file = "ruff-0.4.1-py3-none-win_amd64.whl", hash = "sha256:c7d391e5936af5c9e252743d767c564670dc3889aff460d35c518ee76e4b26d7"}, - {file = "ruff-0.4.1-py3-none-win_arm64.whl", hash = "sha256:a1eaf03d87e6a7cd5e661d36d8c6e874693cb9bc3049d110bc9a97b350680c43"}, - {file = "ruff-0.4.1.tar.gz", hash = "sha256:d592116cdbb65f8b1b7e2a2b48297eb865f6bdc20641879aa9d7b9c11d86db79"}, + {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d14dc8953f8af7e003a485ef560bbefa5f8cc1ad994eebb5b12136049bbccc5"}, + {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:24016ed18db3dc9786af103ff49c03bdf408ea253f3cb9e3638f39ac9cf2d483"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2e06459042ac841ed510196c350ba35a9b24a643e23db60d79b2db92af0c2b"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3afabaf7ba8e9c485a14ad8f4122feff6b2b93cc53cd4dad2fd24ae35112d5c5"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:799eb468ea6bc54b95527143a4ceaf970d5aa3613050c6cff54c85fda3fde480"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ec4ba9436a51527fb6931a8839af4c36a5481f8c19e8f5e42c2f7ad3a49f5069"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a2243f8f434e487c2a010c7252150b1fdf019035130f41b77626f5655c9ca22"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8772130a063f3eebdf7095da00c0b9898bd1774c43b336272c3e98667d4fb8fa"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab165ef5d72392b4ebb85a8b0fbd321f69832a632e07a74794c0e598e7a8376"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f32cadf44c2020e75e0c56c3408ed1d32c024766bd41aedef92aa3ca28eef68"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:22e306bf15e09af45ca812bc42fa59b628646fa7c26072555f278994890bc7ac"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82986bb77ad83a1719c90b9528a9dd663c9206f7c0ab69282af8223566a0c34e"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:652e4ba553e421a6dc2a6d4868bc3b3881311702633eb3672f9f244ded8908cd"}, + {file = "ruff-0.4.2-py3-none-win32.whl", hash = "sha256:7891ee376770ac094da3ad40c116258a381b86c7352552788377c6eb16d784fe"}, + {file = "ruff-0.4.2-py3-none-win_amd64.whl", hash = "sha256:5ec481661fb2fd88a5d6cf1f83403d388ec90f9daaa36e40e2c003de66751798"}, + {file = "ruff-0.4.2-py3-none-win_arm64.whl", hash = "sha256:cbd1e87c71bca14792948c4ccb51ee61c3296e164019d2d484f3eaa2d360dfaf"}, + {file = "ruff-0.4.2.tar.gz", hash = "sha256:33bcc160aee2520664bc0859cfeaebc84bb7323becff3f303b8f1f2d81cb4edc"}, ] [[package]] From 25bdb560eba3081386028bfcfd32a15d604ec24b Mon Sep 17 00:00:00 2001 From: LinasKo Date: Fri, 26 Apr 2024 17:53:19 +0300 Subject: [PATCH 040/274] from_inference, support direct inference results --- supervision/keypoint/core.py | 48 ++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index 7019aa958..b37049295 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -1,5 +1,6 @@ from __future__ import annotations +from contextlib import suppress from dataclasses import dataclass, field from typing import Any, Dict, Iterator, List, Optional, Tuple, Union @@ -101,7 +102,44 @@ def __eq__(self, other: KeyPoints) -> bool: ) @classmethod - def from_inference(cls, inference_result: dict) -> KeyPoints: + def from_inference(cls, inference_result: Union[dict, Any]) -> 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. + + 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. + + 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] + keypoints = sv.KeyPoints.from_inference(result) + ``` + + """ + if isinstance(inference_result, list): + raise ValueError("from_inference() operates on a single result at a time." + "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) + if not inference_result.get("predictions"): return cls.empty() @@ -120,7 +158,13 @@ def from_inference(cls, inference_result: dict) -> KeyPoints: confidence.append(prediction_confidence) class_id.append(prediction["class_id"]) - class_names.append(prediction["class"]) + + if "class" in prediction: + class_names.append(prediction["class"]) + elif "class_name" in prediction: + class_names.append(prediction["class_name"]) + else: + raise KeyError("Neither 'class' nor 'class_name' found in prediction.") data = {CLASS_NAME_DATA_FIELD: np.array(class_names)} From 619da9575a7d56c5ff13a16e75f956a8f4d6ae32 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Fri, 26 Apr 2024 17:55:17 +0300 Subject: [PATCH 041/274] pre-commit tidy-up --- 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 b37049295..e206b320c 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -129,12 +129,14 @@ class IDs, and confidences of each keypoint. result = model.infer(image)[0] keypoints = sv.KeyPoints.from_inference(result) ``` - + """ if isinstance(inference_result, list): - raise ValueError("from_inference() operates on a single result at a time." - "You can retrieve it like so: inference_result = model.infer(image)[0]") - + raise ValueError( + "from_inference() operates on a single result at a time." + "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): From ba2d30d110b5e8a74281397feb91b75e851bfb17 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Fri, 26 Apr 2024 17:36:50 +0200 Subject: [PATCH 042/274] `calculate_dynamic_line_thickness` -> `calculate_optimal_line_thickness` `calculate_dynamic_text_scale` -> `calculate_optimal_text_scale` --- examples/count_people_in_zone/inference_example.py | 4 ++-- examples/count_people_in_zone/ultralytics_example.py | 4 ++-- examples/speed_estimation/inference_example.py | 4 ++-- examples/speed_estimation/ultralytics_example.py | 4 ++-- examples/speed_estimation/yolo_nas_example.py | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/count_people_in_zone/inference_example.py b/examples/count_people_in_zone/inference_example.py index 163f1771a..c50868064 100644 --- a/examples/count_people_in_zone/inference_example.py +++ b/examples/count_people_in_zone/inference_example.py @@ -38,8 +38,8 @@ def initiate_annotators( ) -> Tuple[ List[sv.PolygonZone], List[sv.PolygonZoneAnnotator], List[sv.BoundingBoxAnnotator] ]: - line_thickness = sv.calculate_dynamic_line_thickness(resolution_wh=resolution_wh) - text_scale = sv.calculate_dynamic_text_scale(resolution_wh=resolution_wh) + line_thickness = sv.calculate_optimal_line_thickness(resolution_wh=resolution_wh) + text_scale = sv.calculate_optimal_text_scale(resolution_wh=resolution_wh) zones = [] zone_annotators = [] diff --git a/examples/count_people_in_zone/ultralytics_example.py b/examples/count_people_in_zone/ultralytics_example.py index 861db951e..c17e4d7b7 100644 --- a/examples/count_people_in_zone/ultralytics_example.py +++ b/examples/count_people_in_zone/ultralytics_example.py @@ -36,8 +36,8 @@ def initiate_annotators( ) -> Tuple[ List[sv.PolygonZone], List[sv.PolygonZoneAnnotator], List[sv.BoundingBoxAnnotator] ]: - line_thickness = sv.calculate_dynamic_line_thickness(resolution_wh=resolution_wh) - text_scale = sv.calculate_dynamic_text_scale(resolution_wh=resolution_wh) + line_thickness = sv.calculate_optimal_line_thickness(resolution_wh=resolution_wh) + text_scale = sv.calculate_optimal_text_scale(resolution_wh=resolution_wh) zones = [] zone_annotators = [] diff --git a/examples/speed_estimation/inference_example.py b/examples/speed_estimation/inference_example.py index a435261c7..e715d88e8 100644 --- a/examples/speed_estimation/inference_example.py +++ b/examples/speed_estimation/inference_example.py @@ -98,10 +98,10 @@ def parse_arguments() -> argparse.Namespace: frame_rate=video_info.fps, track_thresh=args.confidence_threshold ) - thickness = sv.calculate_dynamic_line_thickness( + thickness = sv.calculate_optimal_line_thickness( resolution_wh=video_info.resolution_wh ) - text_scale = sv.calculate_dynamic_text_scale(resolution_wh=video_info.resolution_wh) + text_scale = sv.calculate_optimal_text_scale(resolution_wh=video_info.resolution_wh) bounding_box_annotator = sv.BoundingBoxAnnotator(thickness=thickness) label_annotator = sv.LabelAnnotator( text_scale=text_scale, diff --git a/examples/speed_estimation/ultralytics_example.py b/examples/speed_estimation/ultralytics_example.py index 3218d2688..4440d4d9b 100644 --- a/examples/speed_estimation/ultralytics_example.py +++ b/examples/speed_estimation/ultralytics_example.py @@ -76,10 +76,10 @@ def parse_arguments() -> argparse.Namespace: frame_rate=video_info.fps, track_thresh=args.confidence_threshold ) - thickness = sv.calculate_dynamic_line_thickness( + thickness = sv.calculate_optimal_line_thickness( resolution_wh=video_info.resolution_wh ) - text_scale = sv.calculate_dynamic_text_scale(resolution_wh=video_info.resolution_wh) + text_scale = sv.calculate_optimal_text_scale(resolution_wh=video_info.resolution_wh) bounding_box_annotator = sv.BoundingBoxAnnotator(thickness=thickness) label_annotator = sv.LabelAnnotator( text_scale=text_scale, diff --git a/examples/speed_estimation/yolo_nas_example.py b/examples/speed_estimation/yolo_nas_example.py index 187981668..4bb1d9602 100644 --- a/examples/speed_estimation/yolo_nas_example.py +++ b/examples/speed_estimation/yolo_nas_example.py @@ -77,10 +77,10 @@ def parse_arguments() -> argparse.Namespace: frame_rate=video_info.fps, track_thresh=args.confidence_threshold ) - thickness = sv.calculate_dynamic_line_thickness( + thickness = sv.calculate_optimal_line_thickness( resolution_wh=video_info.resolution_wh ) - text_scale = sv.calculate_dynamic_text_scale(resolution_wh=video_info.resolution_wh) + text_scale = sv.calculate_optimal_text_scale(resolution_wh=video_info.resolution_wh) bounding_box_annotator = sv.BoundingBoxAnnotator(thickness=thickness) label_annotator = sv.LabelAnnotator( text_scale=text_scale, From a5326706119ddf9b29017fcc2d372b39cbceadb4 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Fri, 26 Apr 2024 17:55:34 +0200 Subject: [PATCH 043/274] bump version from 0.20.0 to 0.21.0rc1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index de0df7485..5960768a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "supervision" -version = "0.20.0" +version = "0.21.0rc1" 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 c5a2f87bb91c8c27d27b50ba6ba84681ab20f432 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 01:20:56 +0000 Subject: [PATCH 044/274] :arrow_up: Bump pytest from 8.1.1 to 8.2.0 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.1.1 to 8.2.0. - [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.1.1...8.2.0) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- poetry.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/poetry.lock b/poetry.lock index bc59c72e3..bc65995e4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2832,13 +2832,13 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest- [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] @@ -3037,13 +3037,13 @@ tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [[package]] name = "pytest" -version = "8.1.1" +version = "8.2.0" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, - {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, + {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, + {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, ] [package.dependencies] @@ -3051,11 +3051,11 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.4,<2.0" +pluggy = ">=1.5,<2.0" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "python-dateutil" From 31844c165bc43397a9f6253e13bb01994f537ed6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 01:24:07 +0000 Subject: [PATCH 045/274] :arrow_up: Bump mkdocstrings from 0.24.3 to 0.25.0 Bumps [mkdocstrings](https://github.com/mkdocstrings/mkdocstrings) from 0.24.3 to 0.25.0. - [Release notes](https://github.com/mkdocstrings/mkdocstrings/releases) - [Changelog](https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md) - [Commits](https://github.com/mkdocstrings/mkdocstrings/compare/0.24.3...0.25.0) --- updated-dependencies: - dependency-name: mkdocstrings dependency-type: direct:development update-type: version-update:semver-minor ... 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 bc59c72e3..303c91955 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2239,13 +2239,13 @@ files = [ [[package]] name = "mkdocstrings" -version = "0.24.3" +version = "0.25.0" description = "Automatic documentation from sources, for MkDocs." optional = false python-versions = ">=3.8" files = [ - {file = "mkdocstrings-0.24.3-py3-none-any.whl", hash = "sha256:5c9cf2a32958cd161d5428699b79c8b0988856b0d4a8c5baf8395fc1bf4087c3"}, - {file = "mkdocstrings-0.24.3.tar.gz", hash = "sha256:f327b234eb8d2551a306735436e157d0a22d45f79963c60a8b585d5f7a94c1d2"}, + {file = "mkdocstrings-0.25.0-py3-none-any.whl", hash = "sha256:df1b63f26675fcde8c1b77e7ea996cd2f93220b148e06455428f676f5dc838f1"}, + {file = "mkdocstrings-0.25.0.tar.gz", hash = "sha256:066986b3fb5b9ef2d37c4417255a808f7e63b40ff8f67f6cab8054d903fbc91d"}, ] [package.dependencies] @@ -4267,4 +4267,4 @@ desktop = ["opencv-python"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "56ddae6824a9f28c9954badd4c642c57f687b099c6169f97fa0372e294500c17" +content-hash = "a2019dd5d779d7fe9ae684d9b133bceb519c87123fd9917b42a86984bc2a7d3b" diff --git a/pyproject.toml b/pyproject.toml index 5960768a9..f8de1a9ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,7 @@ nbconvert = "^7.14.2" [tool.poetry.group.docs.dependencies] mkdocs-material = {extras = ["imaging"], version = "^9.5.5"} -mkdocstrings = {extras = ["python"], version = ">=0.20,<0.25"} +mkdocstrings = {extras = ["python"], version = ">=0.20,<0.26"} mike = "^2.0.0" # For Documentation Development use Python 3.10 or above # Use Latest mkdocs-jupyter min 0.24.6 for Jupyter Notebook Theme support From df0b2df2f68ffa72c4a58b069bd6dfdf09034ce6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 01:25:37 +0000 Subject: [PATCH 046/274] :arrow_up: Bump tox from 4.14.2 to 4.15.0 Bumps [tox](https://github.com/tox-dev/tox) from 4.14.2 to 4.15.0. - [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.14.2...4.15.0) --- updated-dependencies: - dependency-name: tox dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index bc59c72e3..d8ed39d2d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3957,13 +3957,13 @@ files = [ [[package]] name = "tox" -version = "4.14.2" +version = "4.15.0" description = "tox is a generic virtualenv management and test command line tool" optional = false python-versions = ">=3.8" files = [ - {file = "tox-4.14.2-py3-none-any.whl", hash = "sha256:2900c4eb7b716af4a928a7fdc2ed248ad6575294ed7cfae2ea41203937422847"}, - {file = "tox-4.14.2.tar.gz", hash = "sha256:0defb44f6dafd911b61788325741cc6b2e12ea71f987ac025ad4d649f1f1a104"}, + {file = "tox-4.15.0-py3-none-any.whl", hash = "sha256:300055f335d855b2ab1b12c5802de7f62a36d4fd53f30bd2835f6a201dda46ea"}, + {file = "tox-4.15.0.tar.gz", hash = "sha256:7a0beeef166fbe566f54f795b4906c31b428eddafc0102ac00d20998dd1933f6"}, ] [package.dependencies] From d2d69f986eedfa3bb1d064ec2659dc283ea51f9d Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Mon, 29 Apr 2024 09:46:12 +0300 Subject: [PATCH 047/274] from_infernece: Example suggests users find their own model * Example incorrectly suggested yolov8s-640 * Replaced with "" until we have a COCO human pose model on prod. I don't think "horse-pose/3" is helpful - the use case is uncommon and the training quality is poor. --- supervision/keypoint/core.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index e206b320c..d1021b946 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -51,7 +51,8 @@ class simplifies data manipulation and filtering, providing a uniform API for xy: npt.NDArray[np.float32] class_id: Optional[npt.NDArray[np.int_]] = None confidence: Optional[npt.NDArray[np.float32]] = None - data: Dict[str, Union[npt.NDArray[Any], List]] = field(default_factory=dict) + data: Dict[str, Union[npt.NDArray[Any], List] + ] = field(default_factory=dict) def __post_init__(self): validate_keypoints_fields( @@ -124,7 +125,7 @@ class IDs, and confidences of each keypoint. from inference import get_model image = cv2.imread() - model = get_model(model_id="yolov8s-640") + model = get_model(model_id="") result = model.infer(image)[0] keypoints = sv.KeyPoints.from_inference(result) @@ -140,7 +141,8 @@ class IDs, and confidences of each keypoint. # 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) + inference_result = inference_result.dict( + exclude_none=True, by_alias=True) if not inference_result.get("predictions"): return cls.empty() @@ -166,7 +168,8 @@ class IDs, and confidences of each keypoint. elif "class_name" in prediction: class_names.append(prediction["class_name"]) else: - raise KeyError("Neither 'class' nor 'class_name' found in prediction.") + raise KeyError( + "Neither 'class' nor 'class_name' found in prediction.") data = {CLASS_NAME_DATA_FIELD: np.array(class_names)} @@ -207,7 +210,8 @@ def from_ultralytics(cls, ultralytics_results) -> KeyPoints: xy = ultralytics_results.keypoints.xy.cpu().numpy() 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]) confidence = ultralytics_results.keypoints.conf.cpu().numpy() data = {CLASS_NAME_DATA_FIELD: class_names} From 0dd8d7dca843f7e93336469c4157b3de79b65d58 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 06:53:33 +0000 Subject: [PATCH 048/274] =?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 | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index d1021b946..09c307f59 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -51,8 +51,7 @@ class simplifies data manipulation and filtering, providing a uniform API for xy: npt.NDArray[np.float32] class_id: Optional[npt.NDArray[np.int_]] = None confidence: Optional[npt.NDArray[np.float32]] = None - data: Dict[str, Union[npt.NDArray[Any], List] - ] = field(default_factory=dict) + data: Dict[str, Union[npt.NDArray[Any], List]] = field(default_factory=dict) def __post_init__(self): validate_keypoints_fields( @@ -141,8 +140,7 @@ class IDs, and confidences of each keypoint. # 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) + inference_result = inference_result.dict(exclude_none=True, by_alias=True) if not inference_result.get("predictions"): return cls.empty() @@ -168,8 +166,7 @@ class IDs, and confidences of each keypoint. elif "class_name" in prediction: class_names.append(prediction["class_name"]) else: - raise KeyError( - "Neither 'class' nor 'class_name' found in prediction.") + raise KeyError("Neither 'class' nor 'class_name' found in prediction.") data = {CLASS_NAME_DATA_FIELD: np.array(class_names)} @@ -210,8 +207,7 @@ def from_ultralytics(cls, ultralytics_results) -> KeyPoints: xy = ultralytics_results.keypoints.xy.cpu().numpy() 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]) confidence = ultralytics_results.keypoints.conf.cpu().numpy() data = {CLASS_NAME_DATA_FIELD: class_names} From 7331577e6905b87506a7a8f761af528e9faec82e Mon Sep 17 00:00:00 2001 From: LinasKo Date: Mon, 29 Apr 2024 13:37:37 +0300 Subject: [PATCH 049/274] keypoints, from_inference: removed class_name option --- supervision/keypoint/core.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index 09c307f59..8456eb6f5 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -160,13 +160,7 @@ class IDs, and confidences of each keypoint. confidence.append(prediction_confidence) class_id.append(prediction["class_id"]) - - if "class" in prediction: - class_names.append(prediction["class"]) - elif "class_name" in prediction: - class_names.append(prediction["class_name"]) - else: - raise KeyError("Neither 'class' nor 'class_name' found in prediction.") + class_names.append(prediction["class"]) data = {CLASS_NAME_DATA_FIELD: np.array(class_names)} From 67160c96b04278bcd43ceba272553e3292717528 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:52:51 +0000 Subject: [PATCH 050/274] =?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.1 → v0.4.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.1...v0.4.2) --- .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 ef3620b13..c0903f3af 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.1 + rev: v0.4.2 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From a70588284a1f37989bde166987e498f15982bcff Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Mon, 29 Apr 2024 20:36:05 +0200 Subject: [PATCH 051/274] add Roboflow API example --- supervision/keypoint/core.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index 8456eb6f5..bbabca661 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -124,12 +124,26 @@ class IDs, and confidences of each keypoint. from inference import get_model image = cv2.imread() - model = get_model(model_id="") + model = get_model(model_id=) result = model.infer(image)[0] - keypoints = sv.KeyPoints.from_inference(result) + key_points = sv.KeyPoints.from_inference(result) ``` + ```python + import cv2 + import supervision as sv + from inference_sdk import InferenceHTTPClient + + image = cv2.imread() + client = InferenceHTTPClient( + api_url="https://detect.roboflow.com", + api_key= + ) + + result = client.infer(image, model_id=) + key_points = sv.KeyPoints.from_inference(result) + ``` """ if isinstance(inference_result, list): raise ValueError( From 66f6a86bbfb0ef7da6fd9a6ab7f1ff474691b350 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Mon, 29 Apr 2024 20:37:56 +0200 Subject: [PATCH 052/274] add Roboflow API KEY to example --- supervision/keypoint/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index bbabca661..45d2d9b43 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -124,7 +124,7 @@ class IDs, and confidences of each keypoint. from inference import get_model image = cv2.imread() - model = get_model(model_id=) + model = get_model(model_id=, api_key=) result = model.infer(image)[0] key_points = sv.KeyPoints.from_inference(result) From d4b1375db7bbb9716921e2076138593b901257db Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Mon, 29 Apr 2024 21:46:07 +0200 Subject: [PATCH 053/274] pin inference version --- examples/speed_estimation/requirements.txt | 2 +- examples/time_in_zone/requirements.txt | 2 +- examples/traffic_analysis/requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/speed_estimation/requirements.txt b/examples/speed_estimation/requirements.txt index af2e1c13c..b3aa24560 100644 --- a/examples/speed_estimation/requirements.txt +++ b/examples/speed_estimation/requirements.txt @@ -3,4 +3,4 @@ tqdm==4.66.1 requests ultralytics==8.0.237 super-gradients==3.5.0 -inference==0.9.8 +inference<=0.9.21 diff --git a/examples/time_in_zone/requirements.txt b/examples/time_in_zone/requirements.txt index 752b4a8f5..df94e4bda 100644 --- a/examples/time_in_zone/requirements.txt +++ b/examples/time_in_zone/requirements.txt @@ -1,5 +1,5 @@ opencv-python supervision>=0.20.0 ultralytics -inference +inference<=0.9.21 pytube diff --git a/examples/traffic_analysis/requirements.txt b/examples/traffic_analysis/requirements.txt index 114447fc1..536ac6533 100644 --- a/examples/traffic_analysis/requirements.txt +++ b/examples/traffic_analysis/requirements.txt @@ -1,5 +1,5 @@ gdown -inference +inference<=0.9.21 supervision>=0.20.0 tqdm ultralytics From 4732b6f1d0142192a9faea635670e64696cae004 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Mon, 29 Apr 2024 22:58:13 +0200 Subject: [PATCH 054/274] bump version from 0.21.0rc1 to 0.21.0rc2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f8de1a9ea..426ffb7ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "supervision" -version = "0.21.0rc1" +version = "0.21.0rc2" 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 71919f1d6fcbbd9a5debb4f102ae15e5118e63c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 00:33:04 +0000 Subject: [PATCH 055/274] :arrow_up: Bump nbconvert from 7.16.3 to 7.16.4 Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 7.16.3 to 7.16.4. - [Release notes](https://github.com/jupyter/nbconvert/releases) - [Changelog](https://github.com/jupyter/nbconvert/blob/main/CHANGELOG.md) - [Commits](https://github.com/jupyter/nbconvert/compare/v7.16.3...v7.16.4) --- updated-dependencies: - dependency-name: nbconvert dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 754f37f23..be9666ff8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2374,13 +2374,13 @@ test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>= [[package]] name = "nbconvert" -version = "7.16.3" +version = "7.16.4" description = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)." optional = false python-versions = ">=3.8" files = [ - {file = "nbconvert-7.16.3-py3-none-any.whl", hash = "sha256:ddeff14beeeedf3dd0bc506623e41e4507e551736de59df69a91f86700292b3b"}, - {file = "nbconvert-7.16.3.tar.gz", hash = "sha256:a6733b78ce3d47c3f85e504998495b07e6ea9cf9bf6ec1c98dda63ec6ad19142"}, + {file = "nbconvert-7.16.4-py3-none-any.whl", hash = "sha256:05873c620fe520b6322bf8a5ad562692343fe3452abda5765c7a34b7d1aa3eb3"}, + {file = "nbconvert-7.16.4.tar.gz", hash = "sha256:86ca91ba266b0a448dc96fa6c5b9d98affabde2867b363258703536807f9f7f4"}, ] [package.dependencies] @@ -2402,9 +2402,9 @@ tinycss2 = "*" traitlets = ">=5.1" [package.extras] -all = ["nbconvert[docs,qtpdf,serve,test,webpdf]"] +all = ["flaky", "ipykernel", "ipython", "ipywidgets (>=7.5)", "myst-parser", "nbsphinx (>=0.2.12)", "playwright", "pydata-sphinx-theme", "pyqtwebengine (>=5.15)", "pytest (>=7)", "sphinx (==5.0.2)", "sphinxcontrib-spelling", "tornado (>=6.1)"] docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"] -qtpdf = ["nbconvert[qtpng]"] +qtpdf = ["pyqtwebengine (>=5.15)"] qtpng = ["pyqtwebengine (>=5.15)"] serve = ["tornado (>=6.1)"] test = ["flaky", "ipykernel", "ipywidgets (>=7.5)", "pytest (>=7)"] From 71fd3178ccdcd38e095b638f69d4b0e8d9174c85 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 00:35:39 +0000 Subject: [PATCH 056/274] :arrow_up: Bump mkdocs-material from 9.5.19 to 9.5.20 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.5.19 to 9.5.20. - [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.19...9.5.20) --- 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 754f37f23..367af17b9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2197,13 +2197,13 @@ pygments = ">2.12.0" [[package]] name = "mkdocs-material" -version = "9.5.19" +version = "9.5.20" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.19-py3-none-any.whl", hash = "sha256:ea96e150b6c95f5e4ffe47d78bb712c7bacdd91d2a0bec47f46b6fa0705a86ec"}, - {file = "mkdocs_material-9.5.19.tar.gz", hash = "sha256:7473e06e17e23af608a30ef583fdde8f36389dd3ef56b1d503eed54c89c9618c"}, + {file = "mkdocs_material-9.5.20-py3-none-any.whl", hash = "sha256:ad0094a7597bcb5d0cc3e8e543a10927c2581f7f647b9bb4861600f583180f9b"}, + {file = "mkdocs_material-9.5.20.tar.gz", hash = "sha256:986eef0250d22f70fb06ce0f4eac64cc92bd797a589ec3892ce31fad976fe3da"}, ] [package.dependencies] From 7fd5d80a3bd7b104f87e614c5b28f689de26d82e Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Tue, 30 Apr 2024 12:30:44 +0200 Subject: [PATCH 057/274] bump version from 0.21.0rc2 to 0.21.0rc3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 426ffb7ca..1ea80ebf4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "supervision" -version = "0.21.0rc2" +version = "0.21.0rc3" 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 ffda94b1e526aa1fa047df38db1336b700ee40fd Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Tue, 30 Apr 2024 16:06:02 +0200 Subject: [PATCH 058/274] pin inference version to 0.9.17 --- examples/count_people_in_zone/requirements.txt | 2 +- examples/speed_estimation/requirements.txt | 2 +- examples/time_in_zone/requirements.txt | 2 +- examples/tracking/requirements.txt | 2 +- examples/traffic_analysis/requirements.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/count_people_in_zone/requirements.txt b/examples/count_people_in_zone/requirements.txt index 114447fc1..1cd58655d 100644 --- a/examples/count_people_in_zone/requirements.txt +++ b/examples/count_people_in_zone/requirements.txt @@ -1,5 +1,5 @@ gdown -inference +inference==0.9.17 supervision>=0.20.0 tqdm ultralytics diff --git a/examples/speed_estimation/requirements.txt b/examples/speed_estimation/requirements.txt index b3aa24560..aa51418f4 100644 --- a/examples/speed_estimation/requirements.txt +++ b/examples/speed_estimation/requirements.txt @@ -3,4 +3,4 @@ tqdm==4.66.1 requests ultralytics==8.0.237 super-gradients==3.5.0 -inference<=0.9.21 +inference==0.9.17 diff --git a/examples/time_in_zone/requirements.txt b/examples/time_in_zone/requirements.txt index df94e4bda..6154c3ebf 100644 --- a/examples/time_in_zone/requirements.txt +++ b/examples/time_in_zone/requirements.txt @@ -1,5 +1,5 @@ opencv-python supervision>=0.20.0 ultralytics -inference<=0.9.21 +inference==0.9.17 pytube diff --git a/examples/tracking/requirements.txt b/examples/tracking/requirements.txt index 8d5a9233a..a45d9291b 100644 --- a/examples/tracking/requirements.txt +++ b/examples/tracking/requirements.txt @@ -1,4 +1,4 @@ -inference +inference==0.9.17 supervision==0.19.0 tqdm ultralytics diff --git a/examples/traffic_analysis/requirements.txt b/examples/traffic_analysis/requirements.txt index 536ac6533..1cd58655d 100644 --- a/examples/traffic_analysis/requirements.txt +++ b/examples/traffic_analysis/requirements.txt @@ -1,5 +1,5 @@ gdown -inference<=0.9.21 +inference==0.9.17 supervision>=0.20.0 tqdm ultralytics From 33e69b2da323e134e81b5a03eec908a584fd5f19 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 00:35:55 +0000 Subject: [PATCH 059/274] :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.4 to 1.2.5. - [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.4...v1.2.5) --- 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 a980eb6a9..89c2d4cd0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2162,13 +2162,13 @@ requests = "*" [[package]] name = "mkdocs-git-revision-date-localized-plugin" -version = "1.2.4" +version = "1.2.5" 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.4.tar.gz", hash = "sha256:08fd0c6f33c8da9e00daf40f7865943113b3879a1c621b2bbf0fa794ffe997d3"}, - {file = "mkdocs_git_revision_date_localized_plugin-1.2.4-py3-none-any.whl", hash = "sha256:1f94eb510862ef94e982a2910404fa17a1657ecf29f45a07b0f438c00767fc85"}, + {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"}, ] [package.dependencies] From ba30244adda2bea4f39acd03d1ac6fc583839ff6 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 1 May 2024 15:20:50 +0300 Subject: [PATCH 060/274] Remove hardcoding of class_names, class_id. * Still hardcoded ID to -1 and name to "" if not provided - this lets us stick to (n,) shape. * Found no way to add class_names to pose tracker, so used the response of YOLO NAS detection instead, to check what the response looks like. --- supervision/keypoint/core.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index 4790acd7b..10dfb7a0f 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -50,7 +50,8 @@ class simplifies data manipulation and filtering, providing a uniform API for xy: npt.NDArray[np.float32] class_id: Optional[npt.NDArray[np.int_]] = None confidence: Optional[npt.NDArray[np.float32]] = None - data: Dict[str, Union[npt.NDArray[Any], List]] = field(default_factory=dict) + data: Dict[str, Union[npt.NDArray[Any], List] + ] = field(default_factory=dict) def __post_init__(self): validate_keypoints_fields( @@ -130,7 +131,8 @@ def from_ultralytics(cls, ultralytics_results) -> KeyPoints: xy = ultralytics_results.keypoints.xy.cpu().numpy() 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]) confidence = ultralytics_results.keypoints.conf.cpu().numpy() data = {CLASS_NAME_DATA_FIELD: class_names} @@ -170,13 +172,27 @@ def from_yolo_nas(cls, yolo_nas_results) -> KeyPoints: xy = yolo_nas_results.prediction.poses[:, :, :2] confidence = yolo_nas_results.prediction.poses[:, :, 2] - class_id = [0] * len(xy) - data = {CLASS_NAME_DATA_FIELD: ["person" for _ in xy]} + + # yolo_nas_results treats params differently. + # prediction.labels may not exist, whereas class_names might be None + if hasattr(yolo_nas_results.prediction, "labels"): + class_id = yolo_nas_results.prediction.labels # np.array[int] + else: + class_id = np.array([-1] * len(xy)) + + class_names = [""] * len(class_id) + if yolo_nas_results.class_names: + for i, c_id in enumerate(class_id): + if c_id != -1: + name = yolo_nas_results.class_names[c_id] # tuple[str] + class_names[i] = name + + data = {CLASS_NAME_DATA_FIELD: class_names} return cls( xy=xy, confidence=confidence, - class_id=np.array(class_id), + class_id=class_id, data=data, ) From aab3ed2ba9fecc0cb5ed13cf5f72aed817c2e540 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 12:23:51 +0000 Subject: [PATCH 061/274] =?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 | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index 10dfb7a0f..89a560564 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -50,8 +50,7 @@ class simplifies data manipulation and filtering, providing a uniform API for xy: npt.NDArray[np.float32] class_id: Optional[npt.NDArray[np.int_]] = None confidence: Optional[npt.NDArray[np.float32]] = None - data: Dict[str, Union[npt.NDArray[Any], List] - ] = field(default_factory=dict) + data: Dict[str, Union[npt.NDArray[Any], List]] = field(default_factory=dict) def __post_init__(self): validate_keypoints_fields( @@ -131,8 +130,7 @@ def from_ultralytics(cls, ultralytics_results) -> KeyPoints: xy = ultralytics_results.keypoints.xy.cpu().numpy() 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]) confidence = ultralytics_results.keypoints.conf.cpu().numpy() data = {CLASS_NAME_DATA_FIELD: class_names} From e30d4a9abbd97253312e3f95f9664b7c16e74c16 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Thu, 2 May 2024 12:49:31 +0200 Subject: [PATCH 062/274] 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 62d1b75bd..40d686db8 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 079008cd176fd0ab207c8905b83ea0e753e09f7b Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Thu, 2 May 2024 16:30:01 +0300 Subject: [PATCH 063/274] from_yolo_nas - None class_id & names if not found --- supervision/keypoint/core.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index 89a560564..d056819e3 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -176,16 +176,15 @@ def from_yolo_nas(cls, yolo_nas_results) -> KeyPoints: if hasattr(yolo_nas_results.prediction, "labels"): class_id = yolo_nas_results.prediction.labels # np.array[int] else: - class_id = np.array([-1] * len(xy)) - - class_names = [""] * len(class_id) - if yolo_nas_results.class_names: - for i, c_id in enumerate(class_id): - if c_id != -1: - name = yolo_nas_results.class_names[c_id] # tuple[str] - class_names[i] = name - - data = {CLASS_NAME_DATA_FIELD: class_names} + class_id = None + + data = {} + if class_id is not None and yolo_nas_results.class_names is not None: + class_names = [] + for c_id in class_id: + name = yolo_nas_results.class_names[c_id] # tuple[str] + class_names.append(name) + data[CLASS_NAME_DATA_FIELD] = class_names return cls( xy=xy, From 347b75935b114da82431b0a9f9ec973eb5e4d1e4 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Thu, 2 May 2024 16:31:48 +0300 Subject: [PATCH 064/274] Ruff --- supervision/keypoint/core.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index d056819e3..ae08e38a0 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -50,7 +50,8 @@ class simplifies data manipulation and filtering, providing a uniform API for xy: npt.NDArray[np.float32] class_id: Optional[npt.NDArray[np.int_]] = None confidence: Optional[npt.NDArray[np.float32]] = None - data: Dict[str, Union[npt.NDArray[Any], List]] = field(default_factory=dict) + data: Dict[str, Union[npt.NDArray[Any], List] + ] = field(default_factory=dict) def __post_init__(self): validate_keypoints_fields( @@ -130,7 +131,8 @@ def from_ultralytics(cls, ultralytics_results) -> KeyPoints: xy = ultralytics_results.keypoints.xy.cpu().numpy() 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]) confidence = ultralytics_results.keypoints.conf.cpu().numpy() data = {CLASS_NAME_DATA_FIELD: class_names} From 7e79fa3d07466f90e928d12644d84f31b051eb69 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 May 2024 13:34:11 +0000 Subject: [PATCH 065/274] =?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 | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index ae08e38a0..d056819e3 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -50,8 +50,7 @@ class simplifies data manipulation and filtering, providing a uniform API for xy: npt.NDArray[np.float32] class_id: Optional[npt.NDArray[np.int_]] = None confidence: Optional[npt.NDArray[np.float32]] = None - data: Dict[str, Union[npt.NDArray[Any], List] - ] = field(default_factory=dict) + data: Dict[str, Union[npt.NDArray[Any], List]] = field(default_factory=dict) def __post_init__(self): validate_keypoints_fields( @@ -131,8 +130,7 @@ def from_ultralytics(cls, ultralytics_results) -> KeyPoints: xy = ultralytics_results.keypoints.xy.cpu().numpy() 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]) confidence = ultralytics_results.keypoints.conf.cpu().numpy() data = {CLASS_NAME_DATA_FIELD: class_names} From cc5e72dd5b9113abc922ef00f0133a5a6b61f67f Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Thu, 2 May 2024 22:08:08 +0200 Subject: [PATCH 066/274] 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 40d686db8..5055a859b 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -229,10 +229,30 @@ 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, 4,0, 4,5, 9,5, 9,9, 0,9]], + ) + ], + (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, 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, 0,9, 9,9, 9,0]], + 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), @@ -240,10 +260,15 @@ 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, 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 067/274] 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 4f8679d52..4105f86e7 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -11,7 +11,7 @@ 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 3eeba5b44..2b6f7d636 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 5055a859b..7254c9a93 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 068/274] 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 7254c9a93..ef627e89e 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 7d5a0bce45aeeb5a54d6e906e99509a33cd779ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 May 2024 00:33:20 +0000 Subject: [PATCH 069/274] :arrow_up: Bump tqdm from 4.66.2 to 4.66.4 Bumps [tqdm](https://github.com/tqdm/tqdm) from 4.66.2 to 4.66.4. - [Release notes](https://github.com/tqdm/tqdm/releases) - [Commits](https://github.com/tqdm/tqdm/compare/v4.66.2...v4.66.4) --- updated-dependencies: - dependency-name: tqdm 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 89c2d4cd0..b895934ca 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3984,13 +3984,13 @@ testing = ["build[virtualenv] (>=1.0.3)", "covdefaults (>=2.3)", "detect-test-po [[package]] name = "tqdm" -version = "4.66.2" +version = "4.66.4" description = "Fast, Extensible Progress Meter" optional = true python-versions = ">=3.7" files = [ - {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"}, - {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"}, + {file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"}, + {file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"}, ] [package.dependencies] @@ -4267,4 +4267,4 @@ desktop = ["opencv-python"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "a2019dd5d779d7fe9ae684d9b133bceb519c87123fd9917b42a86984bc2a7d3b" +content-hash = "29af5aa06f97e77a2dba94c5a6d77d7d1903448724df07416026a378d3c6a64d" diff --git a/pyproject.toml b/pyproject.toml index 1ea80ebf4..509c05b92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ 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 } -tqdm = { version = ">=4.62.3,<=4.66.2", optional = true } +tqdm = { version = ">=4.62.3,<=4.66.4", optional = true } pillow = ">=9.4" [tool.poetry.extras] From 29b0c514cf1be01b4c7954b10cad7c0ea1de8204 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 May 2024 00:36:24 +0000 Subject: [PATCH 070/274] :arrow_up: Bump mike from 2.0.0 to 2.1.0 Bumps [mike](https://github.com/jimporter/mike) from 2.0.0 to 2.1.0. - [Release notes](https://github.com/jimporter/mike/releases) - [Changelog](https://github.com/jimporter/mike/blob/master/CHANGES.md) - [Commits](https://github.com/jimporter/mike/compare/v2.0.0...v2.1.0) --- updated-dependencies: - dependency-name: mike dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 89c2d4cd0..76605a889 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2048,13 +2048,13 @@ files = [ [[package]] name = "mike" -version = "2.0.0" +version = "2.1.0" description = "Manage multiple versions of your MkDocs-powered documentation" optional = false python-versions = "*" files = [ - {file = "mike-2.0.0-py3-none-any.whl", hash = "sha256:87f496a65900f93ba92d72940242b65c86f3f2f82871bc60ebdcffc91fad1d9e"}, - {file = "mike-2.0.0.tar.gz", hash = "sha256:566f1cab1a58cc50b106fb79ea2f1f56e7bfc8b25a051e95e6eaee9fba0922de"}, + {file = "mike-2.1.0-py3-none-any.whl", hash = "sha256:b3885f9b9e31fc4b0d61de473750d38ac170a6b291585076effb51a806245608"}, + {file = "mike-2.1.0.tar.gz", hash = "sha256:f0b8e51cbfae1273d648ffb602a4ab3061e57972ca1cd6836df1c51c01a36eb5"}, ] [package.dependencies] From 4a068ff8d7cf71f1e32e8e168e10f5539643a20d Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Fri, 3 May 2024 09:16:13 +0200 Subject: [PATCH 071/274] 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 2b6f7d636..a915972a9 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 f72d06b05e7283946f2600be237d1cf020d13c01 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 May 2024 21:51:32 +0000 Subject: [PATCH 072/274] Bump tqdm from 4.66.1 to 4.66.3 in /examples/speed_estimation Bumps [tqdm](https://github.com/tqdm/tqdm) from 4.66.1 to 4.66.3. - [Release notes](https://github.com/tqdm/tqdm/releases) - [Commits](https://github.com/tqdm/tqdm/compare/v4.66.1...v4.66.3) --- updated-dependencies: - dependency-name: tqdm dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- examples/speed_estimation/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/speed_estimation/requirements.txt b/examples/speed_estimation/requirements.txt index aa51418f4..343a687d6 100644 --- a/examples/speed_estimation/requirements.txt +++ b/examples/speed_estimation/requirements.txt @@ -1,5 +1,5 @@ supervision>=0.20.0 -tqdm==4.66.1 +tqdm==4.66.3 requests ultralytics==8.0.237 super-gradients==3.5.0 From dc1ce02860e4c4ee7aab3522268391ed7353f652 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Fri, 3 May 2024 23:40:10 +0200 Subject: [PATCH 073/274] 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 4105f86e7..d29f3adeb 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -9,9 +9,11 @@ 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 05ee32013..d46aa45b9 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -3,6 +3,8 @@ 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 a915972a9..3eeba5b44 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 074/274] =?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 d29f3adeb..9b7e534a4 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -10,7 +10,6 @@ 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 d46aa45b9..ef30aa481 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 ef627e89e..bfde080d3 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 d37a5f90a494d338f04e33c59e9bba3c735628e3 Mon Sep 17 00:00:00 2001 From: tc360950 Date: Sun, 5 May 2024 17:16:05 +0200 Subject: [PATCH 075/274] Remove old tracklets from ByteTrack.removed_tracks collection --- supervision/tracker/byte_tracker/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supervision/tracker/byte_tracker/core.py b/supervision/tracker/byte_tracker/core.py index 55db6293d..ce3bbbbff 100644 --- a/supervision/tracker/byte_tracker/core.py +++ b/supervision/tracker/byte_tracker/core.py @@ -487,7 +487,7 @@ def update_with_tensors(self, tensors: np.ndarray) -> List[STrack]: self.lost_tracks = sub_tracks(self.lost_tracks, self.tracked_tracks) self.lost_tracks.extend(lost_stracks) self.lost_tracks = sub_tracks(self.lost_tracks, self.removed_tracks) - self.removed_tracks.extend(removed_stracks) + self.removed_tracks = removed_stracks self.tracked_tracks, self.lost_tracks = remove_duplicate_tracks( self.tracked_tracks, self.lost_tracks ) From 92d56eda150941d69bfe71a48b21e4edd024ad5c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 01:06:46 +0000 Subject: [PATCH 076/274] :arrow_up: Bump mike from 2.1.0 to 2.1.1 Bumps [mike](https://github.com/jimporter/mike) from 2.1.0 to 2.1.1. - [Release notes](https://github.com/jimporter/mike/releases) - [Changelog](https://github.com/jimporter/mike/blob/master/CHANGES.md) - [Commits](https://github.com/jimporter/mike/compare/v2.1.0...v2.1.1) --- updated-dependencies: - dependency-name: mike dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2497d8025..e4ec3872d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2048,13 +2048,13 @@ files = [ [[package]] name = "mike" -version = "2.1.0" +version = "2.1.1" description = "Manage multiple versions of your MkDocs-powered documentation" optional = false python-versions = "*" files = [ - {file = "mike-2.1.0-py3-none-any.whl", hash = "sha256:b3885f9b9e31fc4b0d61de473750d38ac170a6b291585076effb51a806245608"}, - {file = "mike-2.1.0.tar.gz", hash = "sha256:f0b8e51cbfae1273d648ffb602a4ab3061e57972ca1cd6836df1c51c01a36eb5"}, + {file = "mike-2.1.1-py3-none-any.whl", hash = "sha256:0b1d01a397a423284593eeb1b5f3194e37169488f929b860c9bfe95c0d5efb79"}, + {file = "mike-2.1.1.tar.gz", hash = "sha256:f39ed39f3737da83ad0adc33e9f885092ed27f8c9e7ff0523add0480352a2c22"}, ] [package.dependencies] @@ -2064,6 +2064,7 @@ jinja2 = ">=2.7" mkdocs = ">=1.0" pyparsing = ">=3.0" pyyaml = ">=5.1" +pyyaml-env-tag = "*" verspec = "*" [package.extras] From f7d21acd3a40bfaab5729ba107915248edfdcbd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 01:09:12 +0000 Subject: [PATCH 077/274] :arrow_up: Bump mkdocs-material from 9.5.20 to 9.5.21 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.5.20 to 9.5.21. - [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.20...9.5.21) --- 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 2497d8025..4608f00f0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2197,13 +2197,13 @@ pygments = ">2.12.0" [[package]] name = "mkdocs-material" -version = "9.5.20" +version = "9.5.21" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.20-py3-none-any.whl", hash = "sha256:ad0094a7597bcb5d0cc3e8e543a10927c2581f7f647b9bb4861600f583180f9b"}, - {file = "mkdocs_material-9.5.20.tar.gz", hash = "sha256:986eef0250d22f70fb06ce0f4eac64cc92bd797a589ec3892ce31fad976fe3da"}, + {file = "mkdocs_material-9.5.21-py3-none-any.whl", hash = "sha256:210e1f179682cd4be17d5c641b2f4559574b9dea2f589c3f0e7c17c5bd1959bc"}, + {file = "mkdocs_material-9.5.21.tar.gz", hash = "sha256:049f82770f40559d3c2aa2259c562ea7257dbb4aaa9624323b5ef27b2d95a450"}, ] [package.dependencies] From ff52352b006ca0ffcee80792289aeb165ece9017 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 01:12:45 +0000 Subject: [PATCH 078/274] :arrow_up: Bump jupytext from 1.16.1 to 1.16.2 Bumps [jupytext](https://github.com/mwouts/jupytext) from 1.16.1 to 1.16.2. - [Release notes](https://github.com/mwouts/jupytext/releases) - [Changelog](https://github.com/mwouts/jupytext/blob/main/CHANGELOG.md) - [Commits](https://github.com/mwouts/jupytext/compare/v1.16.1...v1.16.2) --- updated-dependencies: - dependency-name: jupytext dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2497d8025..87dc07817 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1648,13 +1648,13 @@ files = [ [[package]] name = "jupytext" -version = "1.16.1" +version = "1.16.2" description = "Jupyter notebooks as Markdown documents, Julia, Python or R scripts" optional = false python-versions = ">=3.8" files = [ - {file = "jupytext-1.16.1-py3-none-any.whl", hash = "sha256:796ec4f68ada663569e5d38d4ef03738a01284bfe21c943c485bc36433898bd0"}, - {file = "jupytext-1.16.1.tar.gz", hash = "sha256:68c7b68685e870e80e60fda8286fbd6269e9c74dc1df4316df6fe46eabc94c99"}, + {file = "jupytext-1.16.2-py3-none-any.whl", hash = "sha256:197a43fef31dca612b68b311e01b8abd54441c7e637810b16b6cb8f2ab66065e"}, + {file = "jupytext-1.16.2.tar.gz", hash = "sha256:8627dd9becbbebd79cc4a4ed4727d89d78e606b4b464eab72357b3b029023a14"}, ] [package.dependencies] @@ -1663,16 +1663,16 @@ mdit-py-plugins = "*" nbformat = "*" packaging = "*" pyyaml = "*" -toml = "*" +tomli = {version = "*", markers = "python_version < \"3.11\""} [package.extras] -dev = ["jupytext[test-cov,test-external]"] +dev = ["autopep8", "black", "flake8", "gitpython", "ipykernel", "isort", "jupyter-fs (<0.4.0)", "jupyter-server (!=2.11)", "nbconvert", "pre-commit", "pytest", "pytest-cov (>=2.6.1)", "pytest-randomly", "pytest-xdist", "sphinx-gallery (<0.8)"] docs = ["myst-parser", "sphinx", "sphinx-copybutton", "sphinx-rtd-theme"] test = ["pytest", "pytest-randomly", "pytest-xdist"] -test-cov = ["jupytext[test-integration]", "pytest-cov (>=2.6.1)"] -test-external = ["autopep8", "black", "flake8", "gitpython", "isort", "jupyter-fs (<0.4.0)", "jupytext[test-integration]", "pre-commit", "sphinx-gallery (<0.8)"] -test-functional = ["jupytext[test]"] -test-integration = ["ipykernel", "jupyter-server (!=2.11)", "jupytext[test-functional]", "nbconvert"] +test-cov = ["ipykernel", "jupyter-server (!=2.11)", "nbconvert", "pytest", "pytest-cov (>=2.6.1)", "pytest-randomly", "pytest-xdist"] +test-external = ["autopep8", "black", "flake8", "gitpython", "ipykernel", "isort", "jupyter-fs (<0.4.0)", "jupyter-server (!=2.11)", "nbconvert", "pre-commit", "pytest", "pytest-randomly", "pytest-xdist", "sphinx-gallery (<0.8)"] +test-functional = ["pytest", "pytest-randomly", "pytest-xdist"] +test-integration = ["ipykernel", "jupyter-server (!=2.11)", "nbconvert", "pytest", "pytest-randomly", "pytest-xdist"] test-ui = ["calysto-bash"] [[package]] @@ -3913,17 +3913,6 @@ webencodings = ">=0.4" doc = ["sphinx", "sphinx_rtd_theme"] test = ["flake8", "isort", "pytest"] -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] - [[package]] name = "tomli" version = "2.0.1" From aebbd01830b807def90a6a1d36636ccaa2ba94df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 01:15:08 +0000 Subject: [PATCH 079/274] :arrow_up: Bump mkdocstrings from 0.25.0 to 0.25.1 Bumps [mkdocstrings](https://github.com/mkdocstrings/mkdocstrings) from 0.25.0 to 0.25.1. - [Release notes](https://github.com/mkdocstrings/mkdocstrings/releases) - [Changelog](https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md) - [Commits](https://github.com/mkdocstrings/mkdocstrings/compare/0.25.0...0.25.1) --- updated-dependencies: - dependency-name: mkdocstrings 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 2497d8025..f209f5949 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2239,13 +2239,13 @@ files = [ [[package]] name = "mkdocstrings" -version = "0.25.0" +version = "0.25.1" description = "Automatic documentation from sources, for MkDocs." optional = false python-versions = ">=3.8" files = [ - {file = "mkdocstrings-0.25.0-py3-none-any.whl", hash = "sha256:df1b63f26675fcde8c1b77e7ea996cd2f93220b148e06455428f676f5dc838f1"}, - {file = "mkdocstrings-0.25.0.tar.gz", hash = "sha256:066986b3fb5b9ef2d37c4417255a808f7e63b40ff8f67f6cab8054d903fbc91d"}, + {file = "mkdocstrings-0.25.1-py3-none-any.whl", hash = "sha256:da01fcc2670ad61888e8fe5b60afe9fee5781017d67431996832d63e887c2e51"}, + {file = "mkdocstrings-0.25.1.tar.gz", hash = "sha256:c3a2515f31577f311a9ee58d089e4c51fc6046dbd9e9b4c3de4c3194667fe9bf"}, ] [package.dependencies] From edd944fff887bfee404470a8efb408a84d10dfbc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 01:18:43 +0000 Subject: [PATCH 080/274] :arrow_up: Bump ruff from 0.4.2 to 0.4.3 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.2 to 0.4.3. - [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.2...v0.4.3) --- 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 2497d8025..d01eeae7d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3660,28 +3660,28 @@ files = [ [[package]] name = "ruff" -version = "0.4.2" +version = "0.4.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d14dc8953f8af7e003a485ef560bbefa5f8cc1ad994eebb5b12136049bbccc5"}, - {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:24016ed18db3dc9786af103ff49c03bdf408ea253f3cb9e3638f39ac9cf2d483"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2e06459042ac841ed510196c350ba35a9b24a643e23db60d79b2db92af0c2b"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3afabaf7ba8e9c485a14ad8f4122feff6b2b93cc53cd4dad2fd24ae35112d5c5"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:799eb468ea6bc54b95527143a4ceaf970d5aa3613050c6cff54c85fda3fde480"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ec4ba9436a51527fb6931a8839af4c36a5481f8c19e8f5e42c2f7ad3a49f5069"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a2243f8f434e487c2a010c7252150b1fdf019035130f41b77626f5655c9ca22"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8772130a063f3eebdf7095da00c0b9898bd1774c43b336272c3e98667d4fb8fa"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab165ef5d72392b4ebb85a8b0fbd321f69832a632e07a74794c0e598e7a8376"}, - {file = "ruff-0.4.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f32cadf44c2020e75e0c56c3408ed1d32c024766bd41aedef92aa3ca28eef68"}, - {file = "ruff-0.4.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:22e306bf15e09af45ca812bc42fa59b628646fa7c26072555f278994890bc7ac"}, - {file = "ruff-0.4.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82986bb77ad83a1719c90b9528a9dd663c9206f7c0ab69282af8223566a0c34e"}, - {file = "ruff-0.4.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:652e4ba553e421a6dc2a6d4868bc3b3881311702633eb3672f9f244ded8908cd"}, - {file = "ruff-0.4.2-py3-none-win32.whl", hash = "sha256:7891ee376770ac094da3ad40c116258a381b86c7352552788377c6eb16d784fe"}, - {file = "ruff-0.4.2-py3-none-win_amd64.whl", hash = "sha256:5ec481661fb2fd88a5d6cf1f83403d388ec90f9daaa36e40e2c003de66751798"}, - {file = "ruff-0.4.2-py3-none-win_arm64.whl", hash = "sha256:cbd1e87c71bca14792948c4ccb51ee61c3296e164019d2d484f3eaa2d360dfaf"}, - {file = "ruff-0.4.2.tar.gz", hash = "sha256:33bcc160aee2520664bc0859cfeaebc84bb7323becff3f303b8f1f2d81cb4edc"}, + {file = "ruff-0.4.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b70800c290f14ae6fcbb41bbe201cf62dfca024d124a1f373e76371a007454ce"}, + {file = "ruff-0.4.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08a0d6a22918ab2552ace96adeaca308833873a4d7d1d587bb1d37bae8728eb3"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba1f14df3c758dd7de5b55fbae7e1c8af238597961e5fb628f3de446c3c40c5"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:819fb06d535cc76dfddbfe8d3068ff602ddeb40e3eacbc90e0d1272bb8d97113"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bfc9e955e6dc6359eb6f82ea150c4f4e82b660e5b58d9a20a0e42ec3bb6342b"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:510a67d232d2ebe983fddea324dbf9d69b71c4d2dfeb8a862f4a127536dd4cfb"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9ff11cd9a092ee7680a56d21f302bdda14327772cd870d806610a3503d001f"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29efff25bf9ee685c2c8390563a5b5c006a3fee5230d28ea39f4f75f9d0b6f2f"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b00e0bcccf0fc8d7186ed21e311dffd19761cb632241a6e4fe4477cc80ef6e"}, + {file = "ruff-0.4.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262f5635e2c74d80b7507fbc2fac28fe0d4fef26373bbc62039526f7722bca1b"}, + {file = "ruff-0.4.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7363691198719c26459e08cc17c6a3dac6f592e9ea3d2fa772f4e561b5fe82a3"}, + {file = "ruff-0.4.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eeb039f8428fcb6725bb63cbae92ad67b0559e68b5d80f840f11914afd8ddf7f"}, + {file = "ruff-0.4.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:927b11c1e4d0727ce1a729eace61cee88a334623ec424c0b1c8fe3e5f9d3c865"}, + {file = "ruff-0.4.3-py3-none-win32.whl", hash = "sha256:25cacda2155778beb0d064e0ec5a3944dcca9c12715f7c4634fd9d93ac33fd30"}, + {file = "ruff-0.4.3-py3-none-win_amd64.whl", hash = "sha256:7a1c3a450bc6539ef00da6c819fb1b76b6b065dec585f91456e7c0d6a0bbc725"}, + {file = "ruff-0.4.3-py3-none-win_arm64.whl", hash = "sha256:71ca5f8ccf1121b95a59649482470c5601c60a416bf189d553955b0338e34614"}, + {file = "ruff-0.4.3.tar.gz", hash = "sha256:ff0a3ef2e3c4b6d133fbedcf9586abfbe38d076041f2dc18ffb2c7e0485d5a07"}, ] [[package]] From aaf5df2f6aa41edc89e8988ba7dfab86e339b55b Mon Sep 17 00:00:00 2001 From: Manzar Iqbal Malik Date: Mon, 6 May 2024 12:15:38 +0100 Subject: [PATCH 081/274] Update README.md Aligned draw zones file name for both examples, fixed file name for ultralytics example --- examples/time_in_zone/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/time_in_zone/README.md b/examples/time_in_zone/README.md index 985879992..05b2e15f7 100644 --- a/examples/time_in_zone/README.md +++ b/examples/time_in_zone/README.md @@ -103,7 +103,7 @@ python scripts/draw_zones.py \ ```bash python scripts/draw_zones.py \ --source_path "data/traffic/video.mp4" \ ---zone_configuration_path "data/traffic/custom_config.json" +--zone_configuration_path "data/traffic/config.json" ``` https://github.com/roboflow/supervision/assets/26109316/9d514c9e-2a61-418b-ae49-6ac1ad6ae5ac @@ -192,7 +192,7 @@ Script to run object detection on a video file using the Ultralytics YOLOv8 mode - `--iou_threshold`: IOU threshold for non-max suppression. Default is `0.7`. ```bash -python inference_file_example.py \ +python ultralytics_file_example.py \ --zone_configuration_path "data/checkout/config.json" \ --source_video_path "data/checkout/video.mp4" \ --weights "yolov8x.pt" \ @@ -203,7 +203,7 @@ python inference_file_example.py \ ``` ```bash -python inference_file_example.py \ +python ultralytics_file_example.py \ --zone_configuration_path "data/traffic/config.json" \ --source_video_path "data/traffic/video.mp4" \ --weights "yolov8x.pt" \ @@ -226,7 +226,7 @@ Script to run object detection on a video stream using the Ultralytics YOLOv8 mo - `--iou_threshold`: IOU threshold for non-max suppression. Default is `0.7`. ```bash -python inference_file_example.py \ +python ultralytics_stream_example.py \ --zone_configuration_path "data/checkout/config.json" \ --rtsp_url "rtsp://localhost:8554/live0.stream" \ --weights "yolov8x.pt" \ @@ -237,7 +237,7 @@ python inference_file_example.py \ ``` ```bash -python inference_file_example.py \ +python ultralytics_stream_example.py \ --zone_configuration_path "data/traffic/config.json" \ --rtsp_url "rtsp://localhost:8554/live0.stream" \ --weights "yolov8x.pt" \ From 5d381207432f03f00dd704c4bb2c71e2237966ce Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Mon, 6 May 2024 15:39:59 +0200 Subject: [PATCH 082/274] added `from_xyxy` and `as_xyxy_int_tuple` methods to `Rect`. added `draw_rounded_rectangle` function. --- supervision/draw/utils.py | 52 ++++++++++++++++++++++++++++++++++++ supervision/geometry/core.py | 13 +++++++++ 2 files changed, 65 insertions(+) diff --git a/supervision/draw/utils.py b/supervision/draw/utils.py index 638e6b750..6783ae252 100644 --- a/supervision/draw/utils.py +++ b/supervision/draw/utils.py @@ -81,6 +81,58 @@ def draw_filled_rectangle(scene: np.ndarray, rect: Rect, color: Color) -> np.nda return scene +def draw_rounded_rectangle( + scene: np.ndarray, + rect: Rect, + color: Color, + border_radius: int, +) -> np.ndarray: + """ + Draws a rounded rectangle on an image. + + Parameters: + scene (np.ndarray): The image on which the rounded rectangle will be drawn. + rect (Rect): The rectangle to be drawn. + color (Color): The color of the rounded rectangle. + border_radius (int): The radius of the corner rounding. + + Returns: + np.ndarray: The image with the rounded rectangle drawn on it. + """ + x1, y1, x2, y2 = rect.as_xyxy_int_tuple() + width, height = x2 - x1, y2 - y1 + border_radius = min(border_radius, min(width, height) // 2) + + rectangle_coordinates = [ + ((x1 + border_radius, y1), (x2 - border_radius, y2)), + ((x1, y1 + border_radius), (x2, y2 - border_radius)), + ] + circle_centers = [ + (x1 + border_radius, y1 + border_radius), + (x2 - border_radius, y1 + border_radius), + (x1 + border_radius, y2 - border_radius), + (x2 - border_radius, y2 - border_radius), + ] + + for coordinates in rectangle_coordinates: + cv2.rectangle( + img=scene, + pt1=coordinates[0], + pt2=coordinates[1], + color=color.as_bgr(), + thickness=-1, + ) + for center in circle_centers: + cv2.circle( + img=scene, + center=center, + radius=border_radius, + color=color.as_bgr(), + thickness=-1, + ) + return scene + + def draw_polygon( scene: np.ndarray, polygon: np.ndarray, color: Color, thickness: int = 2 ) -> np.ndarray: diff --git a/supervision/geometry/core.py b/supervision/geometry/core.py index 39d42c60d..810568002 100644 --- a/supervision/geometry/core.py +++ b/supervision/geometry/core.py @@ -98,6 +98,11 @@ class Rect: width: float height: float + @classmethod + def from_xyxy(cls, xyxy: Tuple[float, float, float, float]) -> Rect: + x1, y1, x2, y2 = xyxy + return cls(x=x1, y=y1, width=x2 - x1, height=y2 - y1) + @property def top_left(self) -> Point: return Point(x=self.x, y=self.y) @@ -113,3 +118,11 @@ def pad(self, padding) -> Rect: width=self.width + 2 * padding, height=self.height + 2 * padding, ) + + def as_xyxy_int_tuple(self) -> Tuple[int, int, int, int]: + return ( + int(self.x), + int(self.y), + int(self.x + self.width), + int(self.y + self.height) + ) \ No newline at end of file From 5c539c3bda76632d5dd39b833b0ccb99a358f110 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 13:49:22 +0000 Subject: [PATCH 083/274] =?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/geometry/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/supervision/geometry/core.py b/supervision/geometry/core.py index 810568002..a884a9daa 100644 --- a/supervision/geometry/core.py +++ b/supervision/geometry/core.py @@ -124,5 +124,5 @@ def as_xyxy_int_tuple(self) -> Tuple[int, int, int, int]: int(self.x), int(self.y), int(self.x + self.width), - int(self.y + self.height) - ) \ No newline at end of file + int(self.y + self.height), + ) From d7e52bee264fb1b3b5c47a3f27b5eb67deae86a6 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Mon, 6 May 2024 17:20:31 +0300 Subject: [PATCH 084/274] NMM: Add None-checks, fix area normalization, style --- supervision/detection/core.py | 181 +++++++++++++++++++++++++--------- 1 file changed, 132 insertions(+), 49 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index b60e33632..3d1c135a3 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -30,14 +30,16 @@ 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_mask = None - merged_data = None - if det1.mask and det2.mask: + 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) - 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 @@ def from_yolov5(cls, yolov5_results) -> 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 @@ def from_ultralytics(cls, ultralytics_results) -> 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 @@ def from_ultralytics(cls, ultralytics_results) -> 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 @@ def from_tensorflow( 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 @@ def from_deepsparse(cls, deepsparse_results) -> 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 names. If provided, the resulting Detections object will contain 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 IDs, and confidences of the predictions. """ 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 @@ def from_inference(cls, roboflow_result: Union[dict, Any]) -> 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 @@ def from_sam(cls, sam_result: List[dict]) -> 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 @@ def stack_or_none(name: str): 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 @@ def __setitem__(self, key: str, value: Union[np.ndarray, List]): 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 @@ def with_nms( ), "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 @@ def with_nmm( ), "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 @@ def with_nmm( 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 085/274] =?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 3d1c135a3..fa34c158d 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 @@ def from_yolov5(cls, yolov5_results) -> 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 @@ def from_ultralytics(cls, ultralytics_results) -> 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 @@ def from_ultralytics(cls, ultralytics_results) -> 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 @@ def from_tensorflow( 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 @@ def from_deepsparse(cls, deepsparse_results) -> 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 names. If provided, the resulting Detections object will contain 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 IDs, and confidences of the predictions. """ 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 @@ def from_inference(cls, roboflow_result: Union[dict, Any]) -> 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 @@ def from_sam(cls, sam_result: List[dict]) -> 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 @@ def stack_or_none(name: str): 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 @@ def _set_at_index(self, index: int, other: 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 @@ def with_nms( ), "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 @@ def with_nmm( ), "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 @@ def with_nmm( 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 086/274] 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 fa34c158d..501a27e9d 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -27,103 +27,6 @@ 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 @@ def from_yolov5(cls, yolov5_results) -> 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 @@ def from_ultralytics(cls, ultralytics_results) -> 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 @@ def from_ultralytics(cls, ultralytics_results) -> 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 @@ def from_tensorflow( 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 @@ def from_deepsparse(cls, deepsparse_results) -> 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 names. If provided, the resulting Detections object will contain 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 IDs, and confidences of the predictions. """ 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 @@ def from_inference(cls, roboflow_result: Union[dict, Any]) -> 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 @@ def from_sam(cls, sam_result: List[dict]) -> 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 @@ def stack_or_none(name: str): 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 @@ def _set_at_index(self, index: int, other: 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 @@ def with_nms( ), "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 @@ def with_nmm( ), "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 @@ def with_nmm( 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 087/274] =?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 501a27e9d..beb68923d 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -198,8 +198,7 @@ def from_yolov5(cls, yolov5_results) -> 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 @@ def from_ultralytics(cls, ultralytics_results) -> 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 @@ def from_ultralytics(cls, ultralytics_results) -> 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 @@ def from_tensorflow( 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 @@ def from_deepsparse(cls, deepsparse_results) -> 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 names. If provided, the resulting Detections object will contain 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 IDs, and confidences of the predictions. """ 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 @@ def from_inference(cls, roboflow_result: Union[dict, Any]) -> 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 @@ def from_sam(cls, sam_result: List[dict]) -> 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 @@ def stack_or_none(name: str): 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 @@ def _set_at_index(self, index: int, other: 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 @@ def with_nms( ), "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 @@ def with_nmm( ), "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 @@ def with_nmm( 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 @@ 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 From 95c243c058062370d63df001bbd30127873a27a6 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Mon, 6 May 2024 17:40:33 +0200 Subject: [PATCH 088/274] better `draw_rounded_rectangle` implementation --- supervision/draw/utils.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/supervision/draw/utils.py b/supervision/draw/utils.py index 6783ae252..1980cbe12 100644 --- a/supervision/draw/utils.py +++ b/supervision/draw/utils.py @@ -103,26 +103,22 @@ def draw_rounded_rectangle( width, height = x2 - x1, y2 - y1 border_radius = min(border_radius, min(width, height) // 2) - rectangle_coordinates = [ - ((x1 + border_radius, y1), (x2 - border_radius, y2)), - ((x1, y1 + border_radius), (x2, y2 - border_radius)), - ] - circle_centers = [ - (x1 + border_radius, y1 + border_radius), - (x2 - border_radius, y1 + border_radius), - (x1 + border_radius, y2 - border_radius), - (x2 - border_radius, y2 - border_radius), + corners = [ + (x1 + border_radius, y1), + (x2 - border_radius, y1), + (x2, y1 + border_radius), + (x2, y2 - border_radius), + (x2 - border_radius, y2), + (x1 + border_radius, y2), + (x1, y2 - border_radius), + (x1, y1 + border_radius), ] - for coordinates in rectangle_coordinates: - cv2.rectangle( - img=scene, - pt1=coordinates[0], - pt2=coordinates[1], - color=color.as_bgr(), - thickness=-1, - ) - for center in circle_centers: + pts = np.array(corners, np.int32) + pts = pts.reshape((-1, 1, 2)) + cv2.fillPoly(scene, [pts], color.as_bgr()) + + for center in corners: cv2.circle( img=scene, center=center, @@ -130,6 +126,7 @@ def draw_rounded_rectangle( color=color.as_bgr(), thickness=-1, ) + return scene From ec6864ff04d1b49a1afc8168ea2dd3469e0f61ab Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Mon, 6 May 2024 18:00:13 +0200 Subject: [PATCH 089/274] roll back new `draw_rounded_rectangle` implementation --- supervision/draw/utils.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/supervision/draw/utils.py b/supervision/draw/utils.py index 1980cbe12..6783ae252 100644 --- a/supervision/draw/utils.py +++ b/supervision/draw/utils.py @@ -103,22 +103,26 @@ def draw_rounded_rectangle( width, height = x2 - x1, y2 - y1 border_radius = min(border_radius, min(width, height) // 2) - corners = [ - (x1 + border_radius, y1), - (x2 - border_radius, y1), - (x2, y1 + border_radius), - (x2, y2 - border_radius), - (x2 - border_radius, y2), - (x1 + border_radius, y2), - (x1, y2 - border_radius), - (x1, y1 + border_radius), + rectangle_coordinates = [ + ((x1 + border_radius, y1), (x2 - border_radius, y2)), + ((x1, y1 + border_radius), (x2, y2 - border_radius)), + ] + circle_centers = [ + (x1 + border_radius, y1 + border_radius), + (x2 - border_radius, y1 + border_radius), + (x1 + border_radius, y2 - border_radius), + (x2 - border_radius, y2 - border_radius), ] - pts = np.array(corners, np.int32) - pts = pts.reshape((-1, 1, 2)) - cv2.fillPoly(scene, [pts], color.as_bgr()) - - for center in corners: + for coordinates in rectangle_coordinates: + cv2.rectangle( + img=scene, + pt1=coordinates[0], + pt2=coordinates[1], + color=color.as_bgr(), + thickness=-1, + ) + for center in circle_centers: cv2.circle( img=scene, center=center, @@ -126,7 +130,6 @@ def draw_rounded_rectangle( color=color.as_bgr(), thickness=-1, ) - return scene From ab0882dd55a29be6b3ff23b3b96545b8cfab2721 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Mon, 6 May 2024 18:15:05 +0200 Subject: [PATCH 090/274] initial version of `VertexLabelAnnotator` --- docs/detection/utils.md | 12 +++ supervision/__init__.py | 8 +- supervision/detection/utils.py | 29 +++++++ supervision/keypoint/annotators.py | 123 +++++++++++++++++++++++++++-- 4 files changed, 163 insertions(+), 9 deletions(-) diff --git a/docs/detection/utils.md b/docs/detection/utils.md index abacdc210..76116fc7c 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.clip_boxes + + + +:::supervision.detection.utils.pad_boxes diff --git a/supervision/__init__.py b/supervision/__init__.py index bb5265141..43760686f 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -55,6 +55,8 @@ polygon_to_mask, polygon_to_xyxy, scale_boxes, + clip_boxes, + pad_boxes ) from supervision.draw.color import Color, ColorPalette from supervision.draw.utils import ( @@ -69,7 +71,11 @@ ) from supervision.geometry.core import Point, Position, Rect from supervision.geometry.utils import get_polygon_center -from supervision.keypoint.annotators import EdgeAnnotator, VertexAnnotator +from supervision.keypoint.annotators import ( + EdgeAnnotator, + VertexAnnotator, + VertexLabelAnnotator +) from supervision.keypoint.core import KeyPoints from supervision.metrics.detection import ConfusionMatrix, MeanAveragePrecision from supervision.tracker.byte_tracker.core import ByteTrack diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 3eeba5b44..2b088cce1 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -297,6 +297,35 @@ def clip_boxes(xyxy: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray: return result +def pad_boxes(xyxy: np.ndarray, px: int, py: Optional[int] = None) -> np.ndarray: + """ + Pads bounding boxes coordinates with a constant padding. + + Args: + xyxy (np.ndarray): A numpy array of shape `(N, 4)` where each + row corresponds to a bounding box in the format + `(x_min, y_min, x_max, y_max)`. + px (int): The padding value to be added to both the left and right sides of + each bounding box. + py (Optional[int]): The padding value to be added to both the top and bottom + sides of each bounding box. If not provided, `px` will be used for both + dimensions. + + Returns: + 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. + """ + if py is None: + py = px + + result = xyxy.copy() + result[:, [0, 1]] -= [px, py] + result[:, [2, 3]] += [px, py] + + return result + + def xywh_to_xyxy(boxes_xywh: np.ndarray) -> np.ndarray: xyxy = boxes_xywh.copy() xyxy[:, 2] = boxes_xywh[:, 0] + boxes_xywh[:, 2] diff --git a/supervision/keypoint/annotators.py b/supervision/keypoint/annotators.py index 4b43765c2..01059ac66 100644 --- a/supervision/keypoint/annotators.py +++ b/supervision/keypoint/annotators.py @@ -1,12 +1,14 @@ from abc import ABC, abstractmethod from logging import warn -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union import cv2 import numpy as np +from supervision import Rect, pad_boxes from supervision.annotators.base import ImageType from supervision.draw.color import Color +from supervision.draw.utils import draw_rounded_rectangle from supervision.keypoint.core import KeyPoints from supervision.keypoint.skeletons import SKELETONS_BY_VERTEX_COUNT from supervision.utils.conversion import convert_for_annotation_method @@ -26,9 +28,9 @@ class VertexAnnotator(BaseKeyPointAnnotator): """ def __init__( - self, - color: Color = Color.ROBOFLOW, - radius: int = 4, + self, + color: Color = Color.ROBOFLOW, + radius: int = 4, ) -> None: """ Args: @@ -96,10 +98,10 @@ class EdgeAnnotator(BaseKeyPointAnnotator): """ def __init__( - self, - color: Color = Color.ROBOFLOW, - thickness: int = 2, - edges: Optional[List[Tuple[int, int]]] = None, + self, + color: Color = Color.ROBOFLOW, + thickness: int = 2, + edges: Optional[List[Tuple[int, int]]] = None, ) -> None: """ Args: @@ -175,3 +177,108 @@ def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType: ) return scene + + +class VertexLabelAnnotator: + """ + A class for annotating vertex labels on an image using provided detections. + """ + + def __init__( + self, + color: Union[Color, List[Color]] = Color.ROBOFLOW, + text_color: Color = Color.WHITE, + text_scale: float = 0.5, + text_thickness: int = 1, + text_padding: int = 10, + border_radius: int = 0, + ): + self.border_radius: int = border_radius + self.color: Union[Color, List[Color]] = color + self.text_color: Color = text_color + self.text_scale: float = text_scale + self.text_thickness: int = text_thickness + self.text_padding: int = text_padding + + @staticmethod + def get_text_bounding_box( + text: str, + font: int, + text_scale: float, + text_thickness: int, + center_coordinates: Tuple[int, int] + ) -> Tuple[int, int, int, int]: + text_w, text_h = cv2.getTextSize( + text=text, + fontFace=font, + fontScale=text_scale, + thickness=text_thickness, + )[0] + center_x, center_y = center_coordinates + return ( + center_x - text_w // 2, + center_y - text_h // 2, + center_x + text_w // 2, + center_y + text_h // 2, + ) + + def annotate( + self, + scene: ImageType, + key_points: KeyPoints, + labels: List[str] = None + ) -> ImageType: + font = cv2.FONT_HERSHEY_SIMPLEX + + N, K, _ = key_points.xy.shape + + if N == 0: + return scene + + anchors = key_points.xy.reshape(K * N, 2).astype(int) + colors = np.array(self.color * N) if isinstance(self.color, list) else np.array( + [self.color] * K * N) + labels = np.array(labels * N) + + mask = np.all(anchors != 0, axis=1) + + if np.all(mask == False): + return scene + + anchors = anchors[mask] + colors = colors[mask] + labels = labels[mask] + + xyxy = np.array([ + self.get_text_bounding_box( + text=label, + font=font, + text_scale=self.text_scale, + text_thickness=self.text_thickness, + center_coordinates=tuple(anchor) + ) + for anchor, label + in zip(anchors, labels) + ]) + + xyxy_padded = pad_boxes(xyxy=xyxy, px=self.text_padding) + + for text, color, box, box_padded in zip(labels, colors, xyxy, xyxy_padded): + draw_rounded_rectangle( + scene=scene, + rect=Rect.from_xyxy(box_padded), + color=color, + border_radius=self.border_radius, + ) + cv2.putText( + img=scene, + text=text, + org=(box[0], box[1] + self.text_padding), + fontFace=font, + fontScale=self.text_scale, + color=self.text_color.as_rgb(), + thickness=self.text_thickness, + lineType=cv2.LINE_AA, + ) + + return scene From 7823e0de89326a6fff4ca9a5d3f2404b51f89d77 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 16:16:37 +0000 Subject: [PATCH 091/274] =?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 | 6 +-- supervision/keypoint/annotators.py | 73 +++++++++++++++--------------- 2 files changed, 40 insertions(+), 39 deletions(-) diff --git a/supervision/__init__.py b/supervision/__init__.py index 43760686f..fc79a81fb 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -46,17 +46,17 @@ box_iou_batch, box_non_max_suppression, calculate_masks_centroids, + clip_boxes, filter_polygons_by_area, mask_iou_batch, mask_non_max_suppression, mask_to_polygons, mask_to_xyxy, move_boxes, + pad_boxes, polygon_to_mask, polygon_to_xyxy, scale_boxes, - clip_boxes, - pad_boxes ) from supervision.draw.color import Color, ColorPalette from supervision.draw.utils import ( @@ -74,7 +74,7 @@ from supervision.keypoint.annotators import ( EdgeAnnotator, VertexAnnotator, - VertexLabelAnnotator + VertexLabelAnnotator, ) from supervision.keypoint.core import KeyPoints from supervision.metrics.detection import ConfusionMatrix, MeanAveragePrecision diff --git a/supervision/keypoint/annotators.py b/supervision/keypoint/annotators.py index 01059ac66..56eafdf91 100644 --- a/supervision/keypoint/annotators.py +++ b/supervision/keypoint/annotators.py @@ -28,9 +28,9 @@ class VertexAnnotator(BaseKeyPointAnnotator): """ def __init__( - self, - color: Color = Color.ROBOFLOW, - radius: int = 4, + self, + color: Color = Color.ROBOFLOW, + radius: int = 4, ) -> None: """ Args: @@ -98,10 +98,10 @@ class EdgeAnnotator(BaseKeyPointAnnotator): """ def __init__( - self, - color: Color = Color.ROBOFLOW, - thickness: int = 2, - edges: Optional[List[Tuple[int, int]]] = None, + self, + color: Color = Color.ROBOFLOW, + thickness: int = 2, + edges: Optional[List[Tuple[int, int]]] = None, ) -> None: """ Args: @@ -185,13 +185,13 @@ class VertexLabelAnnotator: """ def __init__( - self, - color: Union[Color, List[Color]] = Color.ROBOFLOW, - text_color: Color = Color.WHITE, - text_scale: float = 0.5, - text_thickness: int = 1, - text_padding: int = 10, - border_radius: int = 0, + self, + color: Union[Color, List[Color]] = Color.ROBOFLOW, + text_color: Color = Color.WHITE, + text_scale: float = 0.5, + text_thickness: int = 1, + text_padding: int = 10, + border_radius: int = 0, ): self.border_radius: int = border_radius self.color: Union[Color, List[Color]] = color @@ -202,11 +202,11 @@ def __init__( @staticmethod def get_text_bounding_box( - text: str, - font: int, - text_scale: float, - text_thickness: int, - center_coordinates: Tuple[int, int] + text: str, + font: int, + text_scale: float, + text_thickness: int, + center_coordinates: Tuple[int, int], ) -> Tuple[int, int, int, int]: text_w, text_h = cv2.getTextSize( text=text, @@ -223,10 +223,7 @@ def get_text_bounding_box( ) def annotate( - self, - scene: ImageType, - key_points: KeyPoints, - labels: List[str] = None + self, scene: ImageType, key_points: KeyPoints, labels: List[str] = None ) -> ImageType: font = cv2.FONT_HERSHEY_SIMPLEX @@ -236,8 +233,11 @@ def annotate( return scene anchors = key_points.xy.reshape(K * N, 2).astype(int) - colors = np.array(self.color * N) if isinstance(self.color, list) else np.array( - [self.color] * K * N) + colors = ( + np.array(self.color * N) + if isinstance(self.color, list) + else np.array([self.color] * K * N) + ) labels = np.array(labels * N) mask = np.all(anchors != 0, axis=1) @@ -249,17 +249,18 @@ def annotate( colors = colors[mask] labels = labels[mask] - xyxy = np.array([ - self.get_text_bounding_box( - text=label, - font=font, - text_scale=self.text_scale, - text_thickness=self.text_thickness, - center_coordinates=tuple(anchor) - ) - for anchor, label - in zip(anchors, labels) - ]) + xyxy = np.array( + [ + self.get_text_bounding_box( + text=label, + font=font, + text_scale=self.text_scale, + text_thickness=self.text_thickness, + center_coordinates=tuple(anchor), + ) + for anchor, label in zip(anchors, labels) + ] + ) xyxy_padded = pad_boxes(xyxy=xyxy, px=self.text_padding) From 478fbd5751480cc0fb7a48ce32995c8c27a427e4 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 17:48:31 +0000 Subject: [PATCH 092/274] =?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.2 → v0.4.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.2...v0.4.3) --- .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 c0903f3af..b0f628973 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.2 + rev: v0.4.3 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 18a790e9ea4160d6ab1c2f9a08f62e14ab458784 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Mon, 6 May 2024 20:21:02 +0200 Subject: [PATCH 093/274] 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 d46aa45b9..1cd038ecd 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -8,6 +8,7 @@ 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 094/274] fix order caused error with mask generation in coco_annotations_to_detections --- supervision/dataset/formats/coco.py | 48 +++++------------------ test/dataset/formats/test_coco.py | 61 ++++++++++++++++++++++++++--- 2 files changed, 65 insertions(+), 44 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index d29f3adeb..c8f7cd049 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 ef627e89e..610b93501 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]] + 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=(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 4d2543745904c4bd1851fd77cc1aab9313123652 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 20:43:41 +0000 Subject: [PATCH 095/274] :arrow_up: Bump jinja2 from 3.1.3 to 3.1.4 Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.3 to 3.1.4. - [Release notes](https://github.com/pallets/jinja/releases) - [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/jinja/compare/3.1.3...3.1.4) --- updated-dependencies: - dependency-name: jinja2 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 178fda0d9..0eb454a80 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1340,13 +1340,13 @@ trio = ["async_generator", "trio"] [[package]] name = "jinja2" -version = "3.1.3" +version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, - {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] [package.dependencies] 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 096/274] =?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 34cb84ce5..febeb8d62 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 d1981bfee..de2511e58 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 610b93501..bb4a6e646 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 097/274] 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 de2511e58..53593a35b 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 5ca96ca59..9cefa4383 100644 --- a/test/dataset/test_utils.py +++ b/test/dataset/test_utils.py @@ -2,6 +2,8 @@ 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 @@ 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 098/274] =?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 9cefa4383..58f2ec229 100644 --- a/test/dataset/test_utils.py +++ b/test/dataset/test_utils.py @@ -10,10 +10,10 @@ 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 d7b6af84e6ed892ac27c535c623c83bae77cf9d7 Mon Sep 17 00:00:00 2001 From: Piotr Skalski Date: Tue, 7 May 2024 12:25:41 +0200 Subject: [PATCH 099/274] Update README.md fix errors in example commands --- examples/time_in_zone/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/time_in_zone/README.md b/examples/time_in_zone/README.md index 05b2e15f7..0a366a945 100644 --- a/examples/time_in_zone/README.md +++ b/examples/time_in_zone/README.md @@ -157,7 +157,7 @@ Script to run object detection on a video stream using the Roboflow Inference mo - `--iou_threshold`: IOU threshold for non-max suppression. Default is `0.7`. ```bash -python inference_file_example.py \ +python inference_stream_example.py \ --zone_configuration_path "data/checkout/config.json" \ --rtsp_url "rtsp://localhost:8554/live0.stream" \ --model_id "yolov8x-640" \ @@ -167,7 +167,7 @@ python inference_file_example.py \ ``` ```bash -python inference_file_example.py \ +python inference_stream_example.py \ --zone_configuration_path "data/traffic/config.json" \ --rtsp_url "rtsp://localhost:8554/live0.stream" \ --model_id "yolov8x-640" \ From fb49be8197136d0c9ab6d7bd62c07405098849ee Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Tue, 7 May 2024 16:06:58 +0300 Subject: [PATCH 100/274] Tests for the new merge function --- test/detection/test_core.py | 163 ++++++++++++++++++++++++++++-------- test/test_utils.py | 18 ++-- 2 files changed, 139 insertions(+), 42 deletions(-) diff --git a/test/detection/test_core.py b/test/detection/test_core.py index f3b739e88..8912f4a65 100644 --- a/test/detection/test_core.py +++ b/test/detection/test_core.py @@ -30,7 +30,81 @@ ) -@pytest.mark.parametrize( +# Merge test +TEST_MASK = np.zeros((1000, 1000), dtype=bool) +TEST_MASK[300:351, 200:251] = True +TEST_DET_1 = mock_detections( + xyxy=[[10, 10, 20, 20], [30, 30, 40, 40], [50, 50, 60, 60]], + mask=[TEST_MASK, TEST_MASK, TEST_MASK], + confidence=[0.1, 0.2, 0.3], + class_id=[1, 2, 3], + tracker_id=[1, 2, 3], + data={ + "some_key": [1, 2, 3], + "other_key": [["1", "2"], ["3", "4"], ["5", "6"]], + } +) +TEST_DET_2 = mock_detections( + xyxy=[[70, 70, 80, 80], [90, 90, 100, 100]], + mask=[TEST_MASK, TEST_MASK], + confidence=[0.4, 0.5], + class_id=[4, 5], + tracker_id=[4, 5], + data={ + "some_key": [4, 5], + "other_key": [["7", "8"], ["9", "10"]], + } +) +TEST_DET_1_2 = mock_detections( + xyxy=[[10, 10, 20, 20], [30, 30, 40, 40], [ + 50, 50, 60, 60], [70, 70, 80, 80], [90, 90, 100, 100]], + mask=[TEST_MASK, TEST_MASK, TEST_MASK, TEST_MASK, TEST_MASK], + confidence=[0.1, 0.2, 0.3, 0.4, 0.5], + class_id=[1, 2, 3, 4, 5], + tracker_id=[1, 2, 3, 4, 5], + data={ + "some_key": [1, 2, 3, 4, 5], + "other_key": [["1", "2"], ["3", "4"], ["5", "6"], ["7", "8"], ["9", "10"]], + } +) +TEST_DET_ZERO_LENGTH = mock_detections( + xyxy=np.empty((0, 4), dtype=np.float32), + mask=np.empty((0, *TEST_MASK.shape), dtype=bool), + confidence=[], + class_id=[], + tracker_id=[], + data={ + "some_key": [], + "other_key": [], + } +) +TEST_DET_NONE = mock_detections( + xyxy=np.empty((0, 4), dtype=np.float32), +) +TEST_DET_DIFFERENT_FIELDS = mock_detections( + xyxy=[[88, 88, 99, 99]], + mask=[np.logical_not(TEST_MASK)], + confidence=None, + class_id=None, + tracker_id=[9], + data={ + "some_key": [9], + "other_key": [["11", "12"]] + } +) +TEST_DET_DIFFERENT_DATA = mock_detections( + xyxy=[[88, 88, 99, 99]], + mask=[np.logical_not(TEST_MASK)], + confidence=[0.9], + class_id=[9], + tracker_id=[9], + data={ + "never_seen_key": [9], + } +) + + +@ pytest.mark.parametrize( "detections, index, expected_result, exception", [ ( @@ -115,7 +189,8 @@ 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, @@ -138,63 +213,79 @@ def test_getitem( assert result == expected_result -@pytest.mark.parametrize( +@ pytest.mark.parametrize( "detections_list, expected_result, exception", [ + # Nothing ([], Detections.empty(), DoesNotRaise()), # empty detections list + + # Single ( [Detections.empty()], Detections.empty(), DoesNotRaise(), ), # single empty detections ( - [mock_detections(xyxy=[[10, 10, 20, 20]])], - mock_detections(xyxy=[[10, 10, 20, 20]]), + [TEST_DET_1], + TEST_DET_1, DoesNotRaise(), - ), # single detection with xyxy field + ), # single detection with fields ( - [ - mock_detections(xyxy=[[10, 10, 20, 20]]), - mock_detections(xyxy=np.empty((0, 4), dtype=np.float32)), - ], - mock_detections(xyxy=[[10, 10, 20, 20]]), + [TEST_DET_NONE], + TEST_DET_NONE, DoesNotRaise(), - ), # single detection with xyxy field + empty detection + ), # Single weakly-defined detection + + # Similar ( - [ - mock_detections(xyxy=[[10, 10, 20, 20]]), - mock_detections(xyxy=[[20, 20, 30, 30]]), - ], - mock_detections(xyxy=[[10, 10, 20, 20], [20, 20, 30, 30]]), + [Detections.empty(), Detections.empty()], + Detections.empty(), DoesNotRaise(), - ), # two detections with xyxy field + ), # Two empty + ( + [TEST_DET_1, TEST_DET_2], + TEST_DET_1_2, + DoesNotRaise(), + ), # Fields with same keys + + # Fields and empty ( [ - mock_detections(xyxy=[[10, 10, 20, 20]], class_id=[0]), - mock_detections(xyxy=[[20, 20, 30, 30]]), + TEST_DET_1, + Detections.empty() ], - mock_detections(xyxy=[[10, 10, 20, 20], [20, 20, 30, 30]]), - pytest.raises(ValueError), - ), # detection with xyxy, class_id fields + detection with xyxy field + TEST_DET_1, + DoesNotRaise(), + ), # single detection with fields ( [ - mock_detections(xyxy=[[10, 10, 20, 20]], class_id=[0]), - mock_detections(xyxy=[[20, 20, 30, 30]], class_id=[1]), + TEST_DET_1, + TEST_DET_ZERO_LENGTH, ], - mock_detections(xyxy=[[10, 10, 20, 20], [20, 20, 30, 30]], class_id=[0, 1]), + TEST_DET_1, DoesNotRaise(), - ), # two detections with xyxy, class_id fields + ), # Single detection and empty-array fields ( [ - mock_detections(xyxy=[[10, 10, 20, 20]], data={"test": [1]}), - mock_detections(xyxy=[[20, 20, 30, 30]], data={"test": [2]}), + TEST_DET_1, + TEST_DET_NONE, ], - mock_detections( - xyxy=[[10, 10, 20, 20], [20, 20, 30, 30]], data={"test": [1, 2]} - ), + TEST_DET_1, DoesNotRaise(), - ), # two detections with xyxy, data fields - ], + ), # Single detection and None fields (+ missing Dict keys) + + # Errors: Non-zero-length differently defined keys & data + ( + [TEST_DET_1, TEST_DET_DIFFERENT_FIELDS], + None, + pytest.raises(ValueError) + ), # Non-empty detections with different fields + ( + [TEST_DET_1, TEST_DET_DIFFERENT_DATA], + None, + pytest.raises(ValueError), + ), # Non-empty detections with different data keys + ] ) def test_merge( detections_list: List[Detections], @@ -206,7 +297,7 @@ def test_merge( assert result == expected_result -@pytest.mark.parametrize( +@ pytest.mark.parametrize( "detections, anchor, expected_result, exception", [ ( @@ -288,7 +379,7 @@ def test_get_anchor_coordinates( assert np.array_equal(result, expected_result) -@pytest.mark.parametrize( +@ pytest.mark.parametrize( "detections_a, detections_b, expected_result", [ ( diff --git a/test/test_utils.py b/test/test_utils.py index b676cb549..37be31d38 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -21,11 +21,14 @@ def convert_data(data: Dict[str, List[Any]]): xyxy=np.array(xyxy, dtype=np.float32), mask=(mask if mask is None else np.array(mask, dtype=bool)), confidence=( - confidence if confidence is None else np.array(confidence, dtype=np.float32) + confidence if confidence is None else np.array( + confidence, dtype=np.float32) ), - class_id=(class_id if class_id is None else np.array(class_id, dtype=int)), + class_id=(class_id if class_id is None else np.array( + class_id, dtype=int)), tracker_id=( - tracker_id if tracker_id is None else np.array(tracker_id, dtype=int) + tracker_id if tracker_id is None else np.array( + tracker_id, dtype=int) ), data=convert_data(data) if data else {}, ) @@ -43,12 +46,15 @@ def convert_data(data: Dict[str, List[Any]]): return KeyPoints( xy=np.array(xy, dtype=np.float32), confidence=( - confidence if confidence is None else np.array(confidence, dtype=np.float32) + confidence if confidence is None else np.array( + confidence, dtype=np.float32) ), - class_id=(class_id if class_id is None else np.array(class_id, dtype=int)), + class_id=(class_id if class_id is None else np.array( + class_id, dtype=int)), data=convert_data(data) if data else {}, ) def assert_almost_equal(actual, expected, tolerance=1e-5): - assert abs(actual - expected) < tolerance, f"Expected {expected}, but got {actual}." + assert abs( + actual - expected) < tolerance, f"Expected {expected}, but got {actual}." From 7bb94ce966465bbcb9815acc8a0b1c8a2100e0a9 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Tue, 7 May 2024 17:30:12 +0300 Subject: [PATCH 101/274] Detections.merge merges None and [] * Detections.merge is much friendlier now. If there's a None or an empty array, it will merge it happily rather than complaining that everything needs to be either None or []. * Data merge follows suit. --- supervision/detection/core.py | 20 +++++++------- supervision/detection/utils.py | 50 ++++++++++++++++++++++++++-------- test/detection/test_core.py | 48 ++++++++++++++------------------ test/detection/test_utils.py | 23 ++++++++++++++++ test/test_utils.py | 18 ++++-------- 5 files changed, 99 insertions(+), 60 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 1900954de..e9baef7a2 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -831,9 +831,10 @@ def merge(cls, detections_list: List[Detections]) -> Detections: This method takes a list of Detections objects and combines their respective fields (`xyxy`, `mask`, `confidence`, `class_id`, and `tracker_id`) - into a single Detections object. If all elements in a field are not - `None`, the corresponding field will be stacked. - Otherwise, the field will be set to `None`. + into a single Detections object. + + 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). Args: detections_list (List[Detections]): A list of Detections objects to merge. @@ -891,13 +892,12 @@ def merge(cls, detections_list: List[Detections]) -> Detections: def stack_or_none(name: str): 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") - 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]) - ) + 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) mask = stack_or_none("mask") confidence = stack_or_none("confidence") diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 3eeba5b44..8fac9f906 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -1,5 +1,5 @@ from itertools import chain -from typing import Dict, List, Optional, Tuple, Union +from typing import Dict, List, Optional, Set, Tuple, Union import cv2 import numpy as np @@ -691,23 +691,51 @@ 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: + lengths_set = [len(value) for value in data.values()] + if len(set(lengths_set)) > 1: raise ValueError( "All data values within a single object must have equal length." ) - merged_data = {key: [] for key in all_keys_sets[0]} - + all_keys: Set[str] = set() for data in data_list: - for key in merged_data: - merged_data[key].append(data[key]) + all_keys.update(data.keys()) + + # Naively merging entries and then validating length comes with a problem: + # N values may come from data[0]["key_1"] and N values from data[1]["key_2"]. + # These should not be joined together. + # Here, as soon as we find data of len > 0, we lock the key set and raise + # a ValueError if later we find a value of len > 0 with an unknown key. + key_set = None + merged_data = {key: [] for key in all_keys} + for data in data_list: + data_key_set = set() + for key in data: + if len(data[key]) > 0: + if key_set is None: + data_key_set.add(key) + elif key not in key_set: + raise ValueError(f"Unknown key '{key}' found in data payload.") + merged_data[key].append(data[key]) + + if key_set is None and data_key_set: + key_set = data_key_set + + merged_data = {key: val for key, val in merged_data.items() if len(val) > 0} + + sum_lengths = {} # Validation. More useful than set for error message + for key, value in merged_data.items(): + sum_length = sum(len(item) for item in value) + sum_lengths[key] = sum_length + lengths_set = set(sum_lengths.values()) + if len(lengths_set) > 1: + raise ValueError( + f"All data fields should have the same lengths after merge." + f"Resulting lengths: {sum_lengths}" + ) + key_set = set() for key in merged_data: if all(isinstance(item, list) for item in merged_data[key]): merged_data[key] = list(chain.from_iterable(merged_data[key])) diff --git a/test/detection/test_core.py b/test/detection/test_core.py index 8912f4a65..8f156238e 100644 --- a/test/detection/test_core.py +++ b/test/detection/test_core.py @@ -42,7 +42,7 @@ data={ "some_key": [1, 2, 3], "other_key": [["1", "2"], ["3", "4"], ["5", "6"]], - } + }, ) TEST_DET_2 = mock_detections( xyxy=[[70, 70, 80, 80], [90, 90, 100, 100]], @@ -53,11 +53,16 @@ data={ "some_key": [4, 5], "other_key": [["7", "8"], ["9", "10"]], - } + }, ) TEST_DET_1_2 = mock_detections( - xyxy=[[10, 10, 20, 20], [30, 30, 40, 40], [ - 50, 50, 60, 60], [70, 70, 80, 80], [90, 90, 100, 100]], + xyxy=[ + [10, 10, 20, 20], + [30, 30, 40, 40], + [50, 50, 60, 60], + [70, 70, 80, 80], + [90, 90, 100, 100], + ], mask=[TEST_MASK, TEST_MASK, TEST_MASK, TEST_MASK, TEST_MASK], confidence=[0.1, 0.2, 0.3, 0.4, 0.5], class_id=[1, 2, 3, 4, 5], @@ -65,7 +70,7 @@ data={ "some_key": [1, 2, 3, 4, 5], "other_key": [["1", "2"], ["3", "4"], ["5", "6"], ["7", "8"], ["9", "10"]], - } + }, ) TEST_DET_ZERO_LENGTH = mock_detections( xyxy=np.empty((0, 4), dtype=np.float32), @@ -76,7 +81,7 @@ data={ "some_key": [], "other_key": [], - } + }, ) TEST_DET_NONE = mock_detections( xyxy=np.empty((0, 4), dtype=np.float32), @@ -87,10 +92,7 @@ confidence=None, class_id=None, tracker_id=[9], - data={ - "some_key": [9], - "other_key": [["11", "12"]] - } + data={"some_key": [9], "other_key": [["11", "12"]]}, ) TEST_DET_DIFFERENT_DATA = mock_detections( xyxy=[[88, 88, 99, 99]], @@ -100,11 +102,11 @@ tracker_id=[9], data={ "never_seen_key": [9], - } + }, ) -@ pytest.mark.parametrize( +@pytest.mark.parametrize( "detections, index, expected_result, exception", [ ( @@ -189,8 +191,7 @@ 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, @@ -213,12 +214,11 @@ def test_getitem( assert result == expected_result -@ pytest.mark.parametrize( +@pytest.mark.parametrize( "detections_list, expected_result, exception", [ # Nothing ([], Detections.empty(), DoesNotRaise()), # empty detections list - # Single ( [Detections.empty()], @@ -235,7 +235,6 @@ def test_getitem( TEST_DET_NONE, DoesNotRaise(), ), # Single weakly-defined detection - # Similar ( [Detections.empty(), Detections.empty()], @@ -247,13 +246,9 @@ def test_getitem( TEST_DET_1_2, DoesNotRaise(), ), # Fields with same keys - # Fields and empty ( - [ - TEST_DET_1, - Detections.empty() - ], + [TEST_DET_1, Detections.empty()], TEST_DET_1, DoesNotRaise(), ), # single detection with fields @@ -273,19 +268,18 @@ def test_getitem( TEST_DET_1, DoesNotRaise(), ), # Single detection and None fields (+ missing Dict keys) - # Errors: Non-zero-length differently defined keys & data ( [TEST_DET_1, TEST_DET_DIFFERENT_FIELDS], None, - pytest.raises(ValueError) + pytest.raises(ValueError), ), # Non-empty detections with different fields ( [TEST_DET_1, TEST_DET_DIFFERENT_DATA], None, pytest.raises(ValueError), ), # Non-empty detections with different data keys - ] + ], ) def test_merge( detections_list: List[Detections], @@ -297,7 +291,7 @@ def test_merge( assert result == expected_result -@ pytest.mark.parametrize( +@pytest.mark.parametrize( "detections, anchor, expected_result, exception", [ ( @@ -379,7 +373,7 @@ def test_get_anchor_coordinates( assert np.array_equal(result, expected_result) -@ pytest.mark.parametrize( +@pytest.mark.parametrize( "detections_a, detections_b, expected_result", [ ( diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index 1c4a1d349..0a48be36e 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -1012,6 +1012,29 @@ def test_calculate_masks_centroids( None, pytest.raises(ValueError), ), # two data dicts with the same field name and different length arrays values + ( + [{}, {"test_1": [1, 2, 3]}], + {"test_1": [1, 2, 3]}, + DoesNotRaise(), + ), # No keys in one dict + ( + [{"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(), + ), # Empty values dicts + ( + [{"test_1": []}, {"test_1": [1, 2, 3], "test_2": [1, 2, 3]}], + {"test_1": [1, 2, 3], "test_2": [1, 2, 3]}, + DoesNotRaise(), + ), # Mix of missing key and empty values + ( + [ + {"test_1": [1, 2, 3]}, + {"test_1": [1, 2, 3], "test_2": [1, 2, 3]}, + ], + None, + pytest.raises(ValueError), + ), # some keys missing in one dict ], ) def test_merge_data( diff --git a/test/test_utils.py b/test/test_utils.py index 37be31d38..b676cb549 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -21,14 +21,11 @@ def convert_data(data: Dict[str, List[Any]]): xyxy=np.array(xyxy, dtype=np.float32), mask=(mask if mask is None else np.array(mask, dtype=bool)), confidence=( - confidence if confidence is None else np.array( - confidence, dtype=np.float32) + confidence if confidence is None else np.array(confidence, dtype=np.float32) ), - class_id=(class_id if class_id is None else np.array( - class_id, dtype=int)), + class_id=(class_id if class_id is None else np.array(class_id, dtype=int)), tracker_id=( - tracker_id if tracker_id is None else np.array( - tracker_id, dtype=int) + tracker_id if tracker_id is None else np.array(tracker_id, dtype=int) ), data=convert_data(data) if data else {}, ) @@ -46,15 +43,12 @@ def convert_data(data: Dict[str, List[Any]]): return KeyPoints( xy=np.array(xy, dtype=np.float32), confidence=( - confidence if confidence is None else np.array( - confidence, dtype=np.float32) + confidence if confidence is None else np.array(confidence, dtype=np.float32) ), - class_id=(class_id if class_id is None else np.array( - class_id, dtype=int)), + class_id=(class_id if class_id is None else np.array(class_id, dtype=int)), data=convert_data(data) if data else {}, ) def assert_almost_equal(actual, expected, tolerance=1e-5): - assert abs( - actual - expected) < tolerance, f"Expected {expected}, but got {actual}." + assert abs(actual - expected) < tolerance, f"Expected {expected}, but got {actual}." From a9f821c28a98430972441fd14bfe80051bdc6631 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Tue, 7 May 2024 17:34:13 +0200 Subject: [PATCH 102/274] 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 53593a35b..96f6a54b2 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 9cefa4383..d88e8349e 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 103/274] =?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 96f6a54b2..0bb432f48 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 104/274] 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 0bb432f48..0167dc395 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 b810f204f..0bf8ecb42 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 105/274] =?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 0bf8ecb42..e269dde75 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 106/274] 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 febeb8d62..0489cd086 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 0167dc395..bd7d7b57c 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 bb4a6e646..12ff4f00b 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 107/274] =?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 0489cd086..4beb1dcd6 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 bd7d7b57c..93242e2b8 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 12ff4f00b..f47e796d2 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 45563dfddff9b590398fbb5555179b0c1d2d5a42 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 8 May 2024 14:07:07 +0300 Subject: [PATCH 108/274] InferenceSlicer: Now works with segmentation --- .../detection/tools/inference_slicer.py | 19 +++++++-- supervision/detection/utils.py | 40 +++++++++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/supervision/detection/tools/inference_slicer.py b/supervision/detection/tools/inference_slicer.py index 7157723f9..99a2bb2dd 100644 --- a/supervision/detection/tools/inference_slicer.py +++ b/supervision/detection/tools/inference_slicer.py @@ -4,20 +4,29 @@ import numpy as np from supervision.detection.core import Detections -from supervision.detection.utils import move_boxes +from supervision.detection.utils import move_boxes, move_masks from supervision.utils.image import crop_image -def move_detections(detections: Detections, offset: np.array) -> Detections: +def move_detections( + detections: Detections, offset: np.ndarray, image_shape: np.ndarray +) -> Detections: """ Args: detections (sv.Detections): Detections object to be moved. - offset (np.array): An array of shape `(2,)` containing offset values in format + offset (np.ndarray): An array of shape `(2,)` containing offset values in format is `[dx, dy]`. + image_size (np.ndarray): An array of shape `(2,)` or `(3,)`, size of the image + in format is `[width, height]`. Returns: (sv.Detections) repositioned Detections object. """ detections.xyxy = move_boxes(xyxy=detections.xyxy, offset=offset) + if detections.mask is not None: + shape_xy = image_shape[:2][::-1] + detections.mask = move_masks( + masks=detections.mask, offset=offset, desired_shape=shape_xy + ) return detections @@ -126,7 +135,9 @@ def _run_callback(self, image, offset) -> Detections: """ image_slice = crop_image(image=image, xyxy=offset) detections = self.callback(image_slice) - detections = move_detections(detections=detections, offset=offset[:2]) + detections = move_detections( + detections=detections, offset=offset[:2], image_shape=image.shape + ) return detections diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 8fac9f906..c3b99b49f 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -592,6 +592,46 @@ def move_boxes(xyxy: np.ndarray, offset: np.ndarray) -> np.ndarray: return xyxy + np.hstack([offset, offset]) +def move_masks( + masks: np.ndarray, offset: np.ndarray, desired_shape: Optional[np.ndarray] = None +) -> np.ndarray: + """ + Offset the masks in an array by the specified (x, y) amount. + + Note the axis orders: + + - `masks`: array of shape `(n, y, x)` + - `offset`: array of ints: `(x, y)` + - `desired_shape`: array of ints `(x, y)` + + Args: + masks (np.ndarray): array of bools + offset (np.ndarray): An array of shape `(2,)` containing non-negative int values + `[dx, dy]`. + desired_shape (Tuple[int, int], optional): Final shape of the mask in the format + `(width, height)`. If provided, the masks will be padded to match this + shape. Note the axis order (x,y)! + + Returns: + (np.ndarray) repositioned masks, optionally padded to the specified shape. + """ + if offset[0] < 0 or offset[1] < 0: + raise ValueError(f"Offset values must be non-negative integers. Got: {offset}") + + size_x, size_y = masks.shape[1:] + offset[::-1] + if desired_shape is not None: + size_x, size_y = desired_shape + + mask_arr = np.full((masks.shape[0], size_y, size_x), False) + mask_arr[ + :, + offset[1] : masks.shape[1] + offset[1], + offset[0] : masks.shape[2] + offset[0], + ] = masks + + return mask_arr + + def scale_boxes(xyxy: np.ndarray, factor: float) -> np.ndarray: """ Scale the dimensions of bounding boxes. From c47b0ce9907fcc776a6a803d97acf9be7bd7770a Mon Sep 17 00:00:00 2001 From: Raif Olson Date: Wed, 8 May 2024 11:07:14 -0400 Subject: [PATCH 109/274] 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 ce3bbbbff..132bf391f 100644 --- a/supervision/tracker/byte_tracker/core.py +++ b/supervision/tracker/byte_tracker/core.py @@ -362,6 +362,13 @@ def update_with_tensors(self, tensors: np.ndarray) -> List[STrack]: 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 1ebbe3a8d104dfc20cd52a5270cd983581861e74 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Thu, 9 May 2024 09:17:37 +0300 Subject: [PATCH 110/274] Removed comments, deindented, removed unused var --- supervision/detection/utils.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 8fac9f906..2f180405b 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -702,29 +702,26 @@ def merge_data( for data in data_list: all_keys.update(data.keys()) - # Naively merging entries and then validating length comes with a problem: - # N values may come from data[0]["key_1"] and N values from data[1]["key_2"]. - # These should not be joined together. - # Here, as soon as we find data of len > 0, we lock the key set and raise - # a ValueError if later we find a value of len > 0 with an unknown key. key_set = None merged_data = {key: [] for key in all_keys} for data in data_list: data_key_set = set() for key in data: - if len(data[key]) > 0: - if key_set is None: - data_key_set.add(key) - elif key not in key_set: - raise ValueError(f"Unknown key '{key}' found in data payload.") - merged_data[key].append(data[key]) + if len(data[key]) == 0: + continue + + if key_set is None: + data_key_set.add(key) + elif key not in key_set: + raise ValueError(f"Unknown key '{key}' found in data payload.") + merged_data[key].append(data[key]) if key_set is None and data_key_set: key_set = data_key_set merged_data = {key: val for key, val in merged_data.items() if len(val) > 0} - sum_lengths = {} # Validation. More useful than set for error message + sum_lengths = {} for key, value in merged_data.items(): sum_length = sum(len(item) for item in value) sum_lengths[key] = sum_length @@ -735,7 +732,6 @@ def merge_data( f"Resulting lengths: {sum_lengths}" ) - key_set = set() for key in merged_data: if all(isinstance(item, list) for item in merged_data[key]): merged_data[key] = list(chain.from_iterable(merged_data[key])) From 8c58ebe30db597045b235b640cda80bb8ef16168 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Thu, 9 May 2024 17:06:51 +0300 Subject: [PATCH 111/274] Roll back flex-merge on data, merge when key missing --- supervision/detection/utils.py | 58 ++++++++++++++++------------------ test/detection/test_utils.py | 27 ++++++++++++---- 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 2f180405b..254a01e8d 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -1,5 +1,5 @@ from itertools import chain -from typing import Dict, List, Optional, Set, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union import cv2 import numpy as np @@ -692,48 +692,46 @@ def merge_data( return {} for data in data_list: - lengths_set = [len(value) for value in data.values()] - if len(set(lengths_set)) > 1: + lengths = [len(value) for value in data.values()] + if len(set(lengths)) > 1: raise ValueError( "All data values within a single object must have equal length." ) - all_keys: Set[str] = set() - for data in data_list: - all_keys.update(data.keys()) + data_keys = [set(data.keys()) for data in data_list] + data_keys = [key_set for key_set in data_keys if len(key_set) > 0] + if not data_keys: + return {} + + common_keys = set.intersection(*data_keys) + all_keys = set.union(*data_keys) + if common_keys != all_keys: + raise ValueError( + f"All data dictionaries must have the same keys to merge. Found {data_keys}" + ) + + data_types = {} + for key in all_keys: + for data in data_list: + if key not in data: + continue + data_types[key] = type(data[key]) + break - key_set = None merged_data = {key: [] for key in all_keys} for data in data_list: - data_key_set = set() for key in data: if len(data[key]) == 0: continue - - if key_set is None: - data_key_set.add(key) - elif key not in key_set: - raise ValueError(f"Unknown key '{key}' found in data payload.") merged_data[key].append(data[key]) - if key_set is None and data_key_set: - key_set = data_key_set - - merged_data = {key: val for key, val in merged_data.items() if len(val) > 0} - - sum_lengths = {} - for key, value in merged_data.items(): - sum_length = sum(len(item) for item in value) - sum_lengths[key] = sum_length - lengths_set = set(sum_lengths.values()) - if len(lengths_set) > 1: - raise ValueError( - f"All data fields should have the same lengths after merge." - f"Resulting lengths: {sum_lengths}" - ) - for key in merged_data: - if all(isinstance(item, list) for item in merged_data[key]): + if len(merged_data[key]) == 0: + if data_types[key] == np.ndarray: + merged_data[key] = np.array(merged_data[key]) + else: + merged_data[key] = list(merged_data[key]) + elif all(isinstance(item, list) for item in merged_data[key]): merged_data[key] = list(chain.from_iterable(merged_data[key])) elif all(isinstance(item, np.ndarray) for item in merged_data[key]): ndim = merged_data[key][0].ndim diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index 0a48be36e..22d0a4307 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -1016,17 +1016,29 @@ def test_calculate_masks_centroids( [{}, {"test_1": [1, 2, 3]}], {"test_1": [1, 2, 3]}, DoesNotRaise(), - ), # No keys in one dict + ), # Empty, no keys ( [{"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(), - ), # Empty values dicts + ), # Empty, same keys ( - [{"test_1": []}, {"test_1": [1, 2, 3], "test_2": [1, 2, 3]}], - {"test_1": [1, 2, 3], "test_2": [1, 2, 3]}, - DoesNotRaise(), - ), # Mix of missing key and empty values + [{"test_1": []}, {"test_1": [1, 2, 3], "test_2": [4, 5, 6]}], + None, + pytest.raises(ValueError), + ), # Empty, missing key + ( + [ + { + "test_1": [1, 2, 3], + "test_2": [4, 5, 6], + "test_3": [7, 8, 9], + }, + {"test_1": [1, 2, 3], "test_2": [4, 5, 6]}, + ], + None, + pytest.raises(ValueError), + ), # Empty, too many keys ( [ {"test_1": [1, 2, 3]}, @@ -1044,6 +1056,9 @@ def test_merge_data( ): with exception: result = merge_data(data_list=data_list) + if expected_result is None: + assert False, f"Expected an error, but got result {result}" + for key in result: if isinstance(result[key], np.ndarray): assert np.array_equal( From b6a55694f2bc0fa52293ae2882e94471f1447c20 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Thu, 9 May 2024 17:12:53 +0300 Subject: [PATCH 112/274] Move type resolution logic to loop where it's used --- supervision/detection/utils.py | 66 +++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 254a01e8d..28bde1b6c 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -55,7 +55,8 @@ def box_area(box): 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) @@ -80,7 +81,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, @@ -131,7 +133,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) @@ -161,7 +164,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 @@ -214,8 +218,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()] @@ -447,7 +452,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: @@ -476,7 +482,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() @@ -543,7 +550,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"]) @@ -554,10 +562,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 @@ -650,8 +660,10 @@ def sum_over_mask(indices: np.ndarray, axis: tuple) -> 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) @@ -710,14 +722,6 @@ def merge_data( f"All data dictionaries must have the same keys to merge. Found {data_keys}" ) - data_types = {} - for key in all_keys: - for data in data_list: - if key not in data: - continue - data_types[key] = type(data[key]) - break - merged_data = {key: [] for key in all_keys} for data in data_list: for key in data: @@ -727,10 +731,20 @@ def merge_data( for key in merged_data: if len(merged_data[key]) == 0: - if data_types[key] == np.ndarray: + for data in data_list: + if key not in data: + continue + data_type = type(data[key]) + break + if data_type == np.ndarray: merged_data[key] = np.array(merged_data[key]) - else: + elif data_type == list: merged_data[key] = list(merged_data[key]) + else: + raise ValueError( + f"Inconsistent data types for key '{key}'. Only np.ndarray and list " + f"types are allowed." + ) elif all(isinstance(item, list) for item in merged_data[key]): merged_data[key] = list(chain.from_iterable(merged_data[key])) elif all(isinstance(item, np.ndarray) for item in merged_data[key]): @@ -740,7 +754,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 " @@ -785,6 +800,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 From 4cd1fbcef9ef71b5924a44d416cbad08d4786b81 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 9 May 2024 14:15:29 +0000 Subject: [PATCH 113/274] =?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 | 44 ++++++++++++---------------------- 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 28bde1b6c..805f6bfbf 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -55,8 +55,7 @@ def box_area(box): 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,8 +80,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, @@ -133,8 +131,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) @@ -164,8 +161,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 @@ -218,9 +214,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()] @@ -452,8 +447,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: @@ -482,8 +476,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() @@ -550,8 +543,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"]) @@ -562,12 +554,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 @@ -660,10 +650,8 @@ def sum_over_mask(indices: np.ndarray, axis: tuple) -> 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) @@ -754,8 +742,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 " @@ -800,7 +787,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 From 01f7eb5be61b52c590fbc03da5e8641a2f44b313 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Thu, 9 May 2024 18:19:51 +0300 Subject: [PATCH 114/274] Retain type info by not excluding empty detections --- supervision/detection/utils.py | 19 +------- test/detection/test_core.py | 80 +++++++++++++++++----------------- 2 files changed, 42 insertions(+), 57 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 805f6bfbf..512489ca5 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -713,27 +713,10 @@ def merge_data( merged_data = {key: [] for key in all_keys} for data in data_list: for key in data: - if len(data[key]) == 0: - continue merged_data[key].append(data[key]) for key in merged_data: - if len(merged_data[key]) == 0: - for data in data_list: - if key not in data: - continue - data_type = type(data[key]) - break - if data_type == np.ndarray: - merged_data[key] = np.array(merged_data[key]) - elif data_type == list: - merged_data[key] = list(merged_data[key]) - else: - raise ValueError( - f"Inconsistent data types for key '{key}'. Only np.ndarray and list " - f"types are allowed." - ) - elif all(isinstance(item, list) for item in merged_data[key]): + if all(isinstance(item, list) for item in merged_data[key]): merged_data[key] = list(chain.from_iterable(merged_data[key])) elif all(isinstance(item, np.ndarray) for item in merged_data[key]): ndim = merged_data[key][0].ndim diff --git a/test/detection/test_core.py b/test/detection/test_core.py index 8f156238e..4dd6e467d 100644 --- a/test/detection/test_core.py +++ b/test/detection/test_core.py @@ -33,73 +33,75 @@ # Merge test TEST_MASK = np.zeros((1000, 1000), dtype=bool) TEST_MASK[300:351, 200:251] = True -TEST_DET_1 = mock_detections( - xyxy=[[10, 10, 20, 20], [30, 30, 40, 40], [50, 50, 60, 60]], - mask=[TEST_MASK, TEST_MASK, TEST_MASK], - confidence=[0.1, 0.2, 0.3], - class_id=[1, 2, 3], - tracker_id=[1, 2, 3], +TEST_DET_1 = Detections( + xyxy=np.array([[10, 10, 20, 20], [30, 30, 40, 40], [50, 50, 60, 60]]), + mask=np.array([TEST_MASK, TEST_MASK, TEST_MASK]), + confidence=np.array([0.1, 0.2, 0.3]), + class_id=np.array([1, 2, 3]), + tracker_id=np.array([1, 2, 3]), data={ "some_key": [1, 2, 3], "other_key": [["1", "2"], ["3", "4"], ["5", "6"]], }, ) -TEST_DET_2 = mock_detections( - xyxy=[[70, 70, 80, 80], [90, 90, 100, 100]], - mask=[TEST_MASK, TEST_MASK], - confidence=[0.4, 0.5], - class_id=[4, 5], - tracker_id=[4, 5], +TEST_DET_2 = Detections( + xyxy=np.array([[70, 70, 80, 80], [90, 90, 100, 100]]), + mask=np.array([TEST_MASK, TEST_MASK]), + confidence=np.array([0.4, 0.5]), + class_id=np.array([4, 5]), + tracker_id=np.array([4, 5]), data={ "some_key": [4, 5], "other_key": [["7", "8"], ["9", "10"]], }, ) -TEST_DET_1_2 = mock_detections( - xyxy=[ - [10, 10, 20, 20], - [30, 30, 40, 40], - [50, 50, 60, 60], - [70, 70, 80, 80], - [90, 90, 100, 100], - ], - mask=[TEST_MASK, TEST_MASK, TEST_MASK, TEST_MASK, TEST_MASK], - confidence=[0.1, 0.2, 0.3, 0.4, 0.5], - class_id=[1, 2, 3, 4, 5], - tracker_id=[1, 2, 3, 4, 5], +TEST_DET_1_2 = Detections( + xyxy=np.array( + [ + [10, 10, 20, 20], + [30, 30, 40, 40], + [50, 50, 60, 60], + [70, 70, 80, 80], + [90, 90, 100, 100], + ] + ), + mask=np.array([TEST_MASK, TEST_MASK, TEST_MASK, TEST_MASK, TEST_MASK]), + confidence=np.array([0.1, 0.2, 0.3, 0.4, 0.5]), + class_id=np.array([1, 2, 3, 4, 5]), + tracker_id=np.array([1, 2, 3, 4, 5]), data={ "some_key": [1, 2, 3, 4, 5], "other_key": [["1", "2"], ["3", "4"], ["5", "6"], ["7", "8"], ["9", "10"]], }, ) -TEST_DET_ZERO_LENGTH = mock_detections( +TEST_DET_ZERO_LENGTH = Detections( xyxy=np.empty((0, 4), dtype=np.float32), mask=np.empty((0, *TEST_MASK.shape), dtype=bool), - confidence=[], - class_id=[], - tracker_id=[], + confidence=np.empty((0,)), + class_id=np.empty((0,)), + tracker_id=np.empty((0,)), data={ "some_key": [], "other_key": [], }, ) -TEST_DET_NONE = mock_detections( +TEST_DET_NONE = Detections( xyxy=np.empty((0, 4), dtype=np.float32), ) -TEST_DET_DIFFERENT_FIELDS = mock_detections( - xyxy=[[88, 88, 99, 99]], - mask=[np.logical_not(TEST_MASK)], +TEST_DET_DIFFERENT_FIELDS = Detections( + xyxy=np.array([[88, 88, 99, 99]]), + mask=np.array([np.logical_not(TEST_MASK)]), confidence=None, class_id=None, - tracker_id=[9], + tracker_id=np.array([9]), data={"some_key": [9], "other_key": [["11", "12"]]}, ) -TEST_DET_DIFFERENT_DATA = mock_detections( - xyxy=[[88, 88, 99, 99]], - mask=[np.logical_not(TEST_MASK)], - confidence=[0.9], - class_id=[9], - tracker_id=[9], +TEST_DET_DIFFERENT_DATA = Detections( + xyxy=np.array([[88, 88, 99, 99]]), + mask=np.array([np.logical_not(TEST_MASK)]), + confidence=np.array([0.9]), + class_id=np.array([9]), + tracker_id=np.array([9]), data={ "never_seen_key": [9], }, From 3b61ee674ea94ee1b7ae5db3dcb11f6fbbe3349d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 May 2024 00:28:34 +0000 Subject: [PATCH 115/274] :arrow_up: Bump ruff from 0.4.3 to 0.4.4 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.3 to 0.4.4. - [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.3...v0.4.4) --- 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 0eb454a80..5dc8c24c2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3661,28 +3661,28 @@ files = [ [[package]] name = "ruff" -version = "0.4.3" +version = "0.4.4" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b70800c290f14ae6fcbb41bbe201cf62dfca024d124a1f373e76371a007454ce"}, - {file = "ruff-0.4.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08a0d6a22918ab2552ace96adeaca308833873a4d7d1d587bb1d37bae8728eb3"}, - {file = "ruff-0.4.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba1f14df3c758dd7de5b55fbae7e1c8af238597961e5fb628f3de446c3c40c5"}, - {file = "ruff-0.4.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:819fb06d535cc76dfddbfe8d3068ff602ddeb40e3eacbc90e0d1272bb8d97113"}, - {file = "ruff-0.4.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bfc9e955e6dc6359eb6f82ea150c4f4e82b660e5b58d9a20a0e42ec3bb6342b"}, - {file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:510a67d232d2ebe983fddea324dbf9d69b71c4d2dfeb8a862f4a127536dd4cfb"}, - {file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9ff11cd9a092ee7680a56d21f302bdda14327772cd870d806610a3503d001f"}, - {file = "ruff-0.4.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29efff25bf9ee685c2c8390563a5b5c006a3fee5230d28ea39f4f75f9d0b6f2f"}, - {file = "ruff-0.4.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b00e0bcccf0fc8d7186ed21e311dffd19761cb632241a6e4fe4477cc80ef6e"}, - {file = "ruff-0.4.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262f5635e2c74d80b7507fbc2fac28fe0d4fef26373bbc62039526f7722bca1b"}, - {file = "ruff-0.4.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7363691198719c26459e08cc17c6a3dac6f592e9ea3d2fa772f4e561b5fe82a3"}, - {file = "ruff-0.4.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eeb039f8428fcb6725bb63cbae92ad67b0559e68b5d80f840f11914afd8ddf7f"}, - {file = "ruff-0.4.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:927b11c1e4d0727ce1a729eace61cee88a334623ec424c0b1c8fe3e5f9d3c865"}, - {file = "ruff-0.4.3-py3-none-win32.whl", hash = "sha256:25cacda2155778beb0d064e0ec5a3944dcca9c12715f7c4634fd9d93ac33fd30"}, - {file = "ruff-0.4.3-py3-none-win_amd64.whl", hash = "sha256:7a1c3a450bc6539ef00da6c819fb1b76b6b065dec585f91456e7c0d6a0bbc725"}, - {file = "ruff-0.4.3-py3-none-win_arm64.whl", hash = "sha256:71ca5f8ccf1121b95a59649482470c5601c60a416bf189d553955b0338e34614"}, - {file = "ruff-0.4.3.tar.gz", hash = "sha256:ff0a3ef2e3c4b6d133fbedcf9586abfbe38d076041f2dc18ffb2c7e0485d5a07"}, + {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"}, ] [[package]] From e4e66f8a9a9ab132776285cde21107e4c69dba6e Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Fri, 10 May 2024 10:16:34 +0200 Subject: [PATCH 116/274] 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 bd7d7b57c..6789d17a3 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 117/274] =?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 4fbd12300..9b5ae3cda 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 2364abf481bfea634d3066d7c31f561170097e00 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Fri, 10 May 2024 15:08:18 +0200 Subject: [PATCH 118/274] small error message update + few more test cases for `merge_data` --- supervision/detection/utils.py | 14 +++++---- test/detection/test_utils.py | 54 ++++++++++++++++++++++++++++------ 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 512489ca5..c2a8e6ddc 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -698,16 +698,18 @@ def merge_data( "All data values within a single object must have equal length." ) - data_keys = [set(data.keys()) for data in data_list] - data_keys = [key_set for key_set in data_keys if len(key_set) > 0] - if not data_keys: + 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(*data_keys) - all_keys = set.union(*data_keys) + common_keys = set.intersection(*keys_by_data) + all_keys = set.union(*keys_by_data) if common_keys != all_keys: raise ValueError( - f"All data dictionaries must have the same keys to merge. Found {data_keys}" + 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} diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index 22d0a4307..0fb72a285 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -911,6 +911,14 @@ def test_calculate_masks_centroids( {"test_1": []}, DoesNotRaise(), ), # single data dict with a single field name and empty list values + ( + [ + {"test_1": []}, + {"test_1": []}, + ], + {"test_1": []}, + DoesNotRaise(), + ), # two data dicts with the same field name and empty list values ( [ {"test_1": np.array([])}, @@ -918,6 +926,14 @@ def test_calculate_masks_centroids( {"test_1": np.array([])}, DoesNotRaise(), ), # single data dict with a single field name and empty np.array values + ( + [ + {"test_1": np.array([])}, + {"test_1": np.array([])}, + ], + {"test_1": np.array([])}, + DoesNotRaise(), + ), # two data dicts with the same field name and empty np.array values ( [ {"test_1": [1, 2, 3]}, @@ -932,7 +948,7 @@ def test_calculate_masks_centroids( ], {"test_1": [3, 2, 1]}, DoesNotRaise(), - ), # two data dicts with the same field name and empty and list values + ), # two data dicts with the same field name; one of with empty list as value ( [ {"test_1": [1, 2, 3]}, @@ -1013,20 +1029,29 @@ def test_calculate_masks_centroids( pytest.raises(ValueError), ), # two data dicts with the same field name and different length arrays values ( - [{}, {"test_1": [1, 2, 3]}], + [ + {}, + {"test_1": [1, 2, 3]} + ], {"test_1": [1, 2, 3]}, DoesNotRaise(), - ), # Empty, no keys + ), # 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(), - ), # Empty, same keys + ), # two data dicts; one empty and one non-empty dict; same keys ( - [{"test_1": []}, {"test_1": [1, 2, 3], "test_2": [4, 5, 6]}], + [ + {"test_1": []}, + {"test_1": [1, 2, 3], "test_2": [4, 5, 6]} + ], None, pytest.raises(ValueError), - ), # Empty, missing key + ), # two data dicts; one empty and one non-empty dict; different keys ( [ { @@ -1034,11 +1059,14 @@ def test_calculate_masks_centroids( "test_2": [4, 5, 6], "test_3": [7, 8, 9], }, - {"test_1": [1, 2, 3], "test_2": [4, 5, 6]}, + { + "test_1": [1, 2, 3], + "test_2": [4, 5, 6] + }, ], None, pytest.raises(ValueError), - ), # Empty, too many keys + ), # two data dicts; one with three keys, one with two keys ( [ {"test_1": [1, 2, 3]}, @@ -1047,6 +1075,14 @@ def test_calculate_masks_centroids( None, pytest.raises(ValueError), ), # some keys missing in one dict + ( + [ + {"test_1": [1, 2, 3], "test_2": ['a', 'b']}, + {"test_1": [4, 5], "test_2": ['c', 'd', 'e']}, + ], + None, + pytest.raises(ValueError), + ), # different value lengths for the same key ], ) def test_merge_data( From 3d0c3d91822507d034c2a6c00286589b35b78004 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 13:08:32 +0000 Subject: [PATCH 119/274] =?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_utils.py | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index 0fb72a285..097c5c6e5 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -1029,26 +1029,17 @@ def test_calculate_masks_centroids( pytest.raises(ValueError), ), # two data dicts with the same field name and different length arrays values ( - [ - {}, - {"test_1": [1, 2, 3]} - ], + [{}, {"test_1": [1, 2, 3]}], {"test_1": [1, 2, 3]}, 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 ( - [ - {"test_1": []}, - {"test_1": [1, 2, 3], "test_2": [4, 5, 6]} - ], + [{"test_1": []}, {"test_1": [1, 2, 3], "test_2": [4, 5, 6]}], None, pytest.raises(ValueError), ), # two data dicts; one empty and one non-empty dict; different keys @@ -1059,10 +1050,7 @@ def test_calculate_masks_centroids( "test_2": [4, 5, 6], "test_3": [7, 8, 9], }, - { - "test_1": [1, 2, 3], - "test_2": [4, 5, 6] - }, + {"test_1": [1, 2, 3], "test_2": [4, 5, 6]}, ], None, pytest.raises(ValueError), @@ -1077,8 +1065,8 @@ def test_calculate_masks_centroids( ), # some keys missing in one dict ( [ - {"test_1": [1, 2, 3], "test_2": ['a', 'b']}, - {"test_1": [4, 5], "test_2": ['c', 'd', 'e']}, + {"test_1": [1, 2, 3], "test_2": ["a", "b"]}, + {"test_1": [4, 5], "test_2": ["c", "d", "e"]}, ], None, pytest.raises(ValueError), From a8c44cfaff9780d045316c2695e6565678052423 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Fri, 10 May 2024 15:36:44 +0200 Subject: [PATCH 120/274] ready for merge --- supervision/detection/utils.py | 4 +++- test/detection/test_core.py | 13 +++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index c2a8e6ddc..6b3780422 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -678,7 +678,9 @@ def merge_data( Merges the data payloads of a list of Detections instances. Args: - data_list: The data payloads of the instances. + 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. Returns: A single data payload containing the merged data, preserving the original data diff --git a/test/detection/test_core.py b/test/detection/test_core.py index 4dd6e467d..12f3de281 100644 --- a/test/detection/test_core.py +++ b/test/detection/test_core.py @@ -219,14 +219,17 @@ def test_getitem( @pytest.mark.parametrize( "detections_list, expected_result, exception", [ - # Nothing ([], Detections.empty(), DoesNotRaise()), # empty detections list - # Single ( [Detections.empty()], Detections.empty(), DoesNotRaise(), ), # single empty detections + ( + [Detections.empty(), Detections.empty()], + Detections.empty(), + DoesNotRaise(), + ), # two empty detections ( [TEST_DET_1], TEST_DET_1, @@ -237,12 +240,6 @@ def test_getitem( TEST_DET_NONE, DoesNotRaise(), ), # Single weakly-defined detection - # Similar - ( - [Detections.empty(), Detections.empty()], - Detections.empty(), - DoesNotRaise(), - ), # Two empty ( [TEST_DET_1, TEST_DET_2], TEST_DET_1_2, From 17c8e41fb3cdb3b3a18ff275c1332526cf43afc4 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Fri, 10 May 2024 15:50:03 +0200 Subject: [PATCH 121/274] bump version from `0.21.0rc3` to `0.21.0rc4` --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 509c05b92..a98e91f37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "supervision" -version = "0.21.0rc3" +version = "0.21.0rc4" 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 6c6171b5d56d6cdf6e2010562823492229e1b1a3 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Fri, 10 May 2024 18:25:02 +0300 Subject: [PATCH 122/274] Simpify passing of shape --- supervision/detection/tools/inference_slicer.py | 5 ++--- supervision/detection/utils.py | 10 +++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/supervision/detection/tools/inference_slicer.py b/supervision/detection/tools/inference_slicer.py index 99a2bb2dd..bd4a24254 100644 --- a/supervision/detection/tools/inference_slicer.py +++ b/supervision/detection/tools/inference_slicer.py @@ -17,15 +17,14 @@ def move_detections( offset (np.ndarray): An array of shape `(2,)` containing offset values in format is `[dx, dy]`. image_size (np.ndarray): An array of shape `(2,)` or `(3,)`, size of the image - in format is `[width, height]`. + is in format `[width, height]`. Returns: (sv.Detections) repositioned Detections object. """ detections.xyxy = move_boxes(xyxy=detections.xyxy, offset=offset) if detections.mask is not None: - shape_xy = image_shape[:2][::-1] detections.mask = move_masks( - masks=detections.mask, offset=offset, desired_shape=shape_xy + masks=detections.mask, offset=offset, desired_shape=image_shape ) return detections diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 05f29d270..d5b908b01 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -602,15 +602,15 @@ def move_masks( - `masks`: array of shape `(n, y, x)` - `offset`: array of ints: `(x, y)` - - `desired_shape`: array of ints `(x, y)` + - `desired_shape`: array of ints, shaped `(y, x, ...)` Args: masks (np.ndarray): array of bools offset (np.ndarray): An array of shape `(2,)` containing non-negative int values `[dx, dy]`. - desired_shape (Tuple[int, int], optional): Final shape of the mask in the format - `(width, height)`. If provided, the masks will be padded to match this - shape. Note the axis order (x,y)! + desired_shape (np.ndarray, optional): Final shape of the mask in the format + `(height, width, ...)`. If provided, the masks will be padded to match this + shape. Note the axis order (y,x)! Returns: (np.ndarray) repositioned masks, optionally padded to the specified shape. @@ -620,7 +620,7 @@ def move_masks( size_x, size_y = masks.shape[1:] + offset[::-1] if desired_shape is not None: - size_x, size_y = desired_shape + size_y, size_x = desired_shape[:2] mask_arr = np.full((masks.shape[0], size_y, size_x), False) mask_arr[ From 528c96058771eccbe43838f87fef1bfb56dd7069 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Sat, 11 May 2024 13:38:23 +0300 Subject: [PATCH 123/274] Minor docs update: missing return in inference slicer callback --- docs/how_to/detect_small_objects.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/how_to/detect_small_objects.md b/docs/how_to/detect_small_objects.md index e2d023284..b0447a64f 100644 --- a/docs/how_to/detect_small_objects.md +++ b/docs/how_to/detect_small_objects.md @@ -6,7 +6,7 @@ status: new # Detect Small Objects This guide shows how to detect small objects -with the [Inference](https://github.com/roboflow/inference), +with the [Inference](https://github.com/roboflow/inference), [Ultralytics](https://github.com/ultralytics/ultralytics) or [Transformers](https://github.com/huggingface/transformers) packages using [`InferenceSlicer`](/latest/detection/tools/inference_slicer/#supervision.detection.tools.inference_slicer.InferenceSlicer). @@ -175,7 +175,7 @@ objects within each, and aggregating the results. def callback(image_slice: np.ndarray) -> sv.Detections: results = model.infer(image_slice)[0] - detections = sv.Detections.from_inference(results) + return sv.Detections.from_inference(results) slicer = sv.InferenceSlicer(callback = callback) detections = slicer(image) From 8c857dd9a61999b3d7d7c26568a855f617eb6f43 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Sun, 12 May 2024 23:02:22 +0200 Subject: [PATCH 124/274] 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 4fbd12300..82491871b 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 e269dde75..e30b4e5f7 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 125/274] =?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 61f3b1c8f..b0b5577e6 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 3c8995758d38c77b8cee47851bee06fb15cae317 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 01:21:27 +0000 Subject: [PATCH 126/274] :arrow_up: Bump mkdocs-material from 9.5.21 to 9.5.22 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.5.21 to 9.5.22. - [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.21...9.5.22) --- 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 5dc8c24c2..71b6addb4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2198,13 +2198,13 @@ pygments = ">2.12.0" [[package]] name = "mkdocs-material" -version = "9.5.21" +version = "9.5.22" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.21-py3-none-any.whl", hash = "sha256:210e1f179682cd4be17d5c641b2f4559574b9dea2f589c3f0e7c17c5bd1959bc"}, - {file = "mkdocs_material-9.5.21.tar.gz", hash = "sha256:049f82770f40559d3c2aa2259c562ea7257dbb4aaa9624323b5ef27b2d95a450"}, + {file = "mkdocs_material-9.5.22-py3-none-any.whl", hash = "sha256:8c7a377d323567934e6cd46915e64dc209efceaec0dec1cf2202184f5649862c"}, + {file = "mkdocs_material-9.5.22.tar.gz", hash = "sha256:22a853a456ae8c581c4628159574d6fc7c71b2c7569dc9c3a82cc70432219599"}, ] [package.dependencies] From 816aecb51bda00d6560f05573788d945e2abfdba Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Mon, 13 May 2024 11:35:23 +0300 Subject: [PATCH 127/274] PR comments: tuples, docs, optional --- docs/detection/utils.md | 6 ++++++ supervision/__init__.py | 1 + .../detection/tools/inference_slicer.py | 12 +++++++---- supervision/detection/utils.py | 21 +++++++++++-------- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/docs/detection/utils.md b/docs/detection/utils.md index abacdc210..02fb813ea 100644 --- a/docs/detection/utils.md +++ b/docs/detection/utils.md @@ -65,6 +65,12 @@ status: new :::supervision.detection.utils.move_boxes + + +:::supervision.detection.utils.move_masks + diff --git a/supervision/__init__.py b/supervision/__init__.py index bb5265141..71fba5fe4 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -52,6 +52,7 @@ mask_to_polygons, mask_to_xyxy, move_boxes, + move_masks, polygon_to_mask, polygon_to_xyxy, scale_boxes, diff --git a/supervision/detection/tools/inference_slicer.py b/supervision/detection/tools/inference_slicer.py index bd4a24254..a951664c3 100644 --- a/supervision/detection/tools/inference_slicer.py +++ b/supervision/detection/tools/inference_slicer.py @@ -1,5 +1,5 @@ from concurrent.futures import ThreadPoolExecutor, as_completed -from typing import Callable, Optional, Tuple +from typing import Callable, Optional, Tuple, Union import numpy as np @@ -9,15 +9,19 @@ def move_detections( - detections: Detections, offset: np.ndarray, image_shape: np.ndarray + detections: Detections, + offset: np.ndarray, + image_shape: Optional[Union[Tuple[int, int, int], Tuple[int, int]]] = None, ) -> Detections: """ Args: detections (sv.Detections): Detections object to be moved. offset (np.ndarray): An array of shape `(2,)` containing offset values in format is `[dx, dy]`. - image_size (np.ndarray): An array of shape `(2,)` or `(3,)`, size of the image - is in format `[width, height]`. + image_size (Tuple, optional): A tuple of image shape. Can be `(2,)` or `(3,)`. + Important when moving for segmentation detections, as it defines mask array + size. + Returns: (sv.Detections) repositioned Detections object. """ diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index d5b908b01..d20cadea4 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -593,7 +593,9 @@ def move_boxes(xyxy: np.ndarray, offset: np.ndarray) -> np.ndarray: def move_masks( - masks: np.ndarray, offset: np.ndarray, desired_shape: Optional[np.ndarray] = None + masks: np.ndarray, + offset: np.ndarray, + desired_shape: Optional[Union[Tuple[int, int, int], Tuple[int, int]]] = None, ) -> np.ndarray: """ Offset the masks in an array by the specified (x, y) amount. @@ -602,34 +604,35 @@ def move_masks( - `masks`: array of shape `(n, y, x)` - `offset`: array of ints: `(x, y)` - - `desired_shape`: array of ints, shaped `(y, x, ...)` + - `desired_shape`: tuple of ints, shaped `(y, x)` or `(y, x, ...)` Args: masks (np.ndarray): array of bools offset (np.ndarray): An array of shape `(2,)` containing non-negative int values `[dx, dy]`. - desired_shape (np.ndarray, optional): Final shape of the mask in the format - `(height, width, ...)`. If provided, the masks will be padded to match this - shape. Note the axis order (y,x)! + desired_shape (Tuple, optional): Final shape of the mask in the format + `(height, width)`, `(height, width, ...)`. The masks will be padded to match + the first 2 shape dimensions. Note the axis order (y,x)! Returns: (np.ndarray) repositioned masks, optionally padded to the specified shape. """ + if offset[0] < 0 or offset[1] < 0: raise ValueError(f"Offset values must be non-negative integers. Got: {offset}") - size_x, size_y = masks.shape[1:] + offset[::-1] + size_y, size_x = masks.shape[1:] + offset[::-1] if desired_shape is not None: size_y, size_x = desired_shape[:2] - mask_arr = np.full((masks.shape[0], size_y, size_x), False) - mask_arr[ + mask_array = np.full((masks.shape[0], size_y, size_x), False) + mask_array[ :, offset[1] : masks.shape[1] + offset[1], offset[0] : masks.shape[2] + offset[0], ] = masks - return mask_arr + return mask_array def scale_boxes(xyxy: np.ndarray, factor: float) -> np.ndarray: From d21c98a4d2fda92300d4931f952fcc0e91669959 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Mon, 13 May 2024 12:06:38 +0200 Subject: [PATCH 128/274] 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 | 62 +++++++++++++++++--------- test/dataset/test_utils.py | 12 +++-- 6 files changed, 77 insertions(+), 24 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 739315150..03d0c1966 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 000000000..6be56303f --- /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 cf206a82e..281c40c97 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 bb5265141..b7f7a5e86 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -34,6 +34,10 @@ 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 b0b5577e6..ed7062b93 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -2,7 +2,7 @@ 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. - - Examples: - mask = [[False, True, True], -> rle = [2, 4] - [False, True, True]] + AssertionError: If input mask is not 2D or is empty. - mask = [[True, True, True], -> rle = [0, 6] - [True, True, True]] + Examples: + ```python + import numpy as np + import supervision as sv + + 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 e30b4e5f7..41e1da5bc 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 129/274] =?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 b7f7a5e86..2bc729442 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -34,10 +34,7 @@ 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 ed7062b93..4efc71947 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -2,7 +2,7 @@ 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 130/274] 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 4beb1dcd6..fc62ac660 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -5,6 +5,7 @@ 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 131/274] =?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 fc62ac660..b6b2490ac 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 82ab6c57cb8cb2f863e27c551e31e310e18fdb38 Mon Sep 17 00:00:00 2001 From: LinasKo Date: Mon, 13 May 2024 14:14:42 +0300 Subject: [PATCH 132/274] infrenceSlicer: resolution_wh --- .../detection/tools/inference_slicer.py | 19 ++++++++++++------- supervision/detection/utils.py | 19 ++++--------------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/supervision/detection/tools/inference_slicer.py b/supervision/detection/tools/inference_slicer.py index a951664c3..82551434e 100644 --- a/supervision/detection/tools/inference_slicer.py +++ b/supervision/detection/tools/inference_slicer.py @@ -1,5 +1,5 @@ from concurrent.futures import ThreadPoolExecutor, as_completed -from typing import Callable, Optional, Tuple, Union +from typing import Callable, Optional, Tuple import numpy as np @@ -11,24 +11,28 @@ def move_detections( detections: Detections, offset: np.ndarray, - image_shape: Optional[Union[Tuple[int, int, int], Tuple[int, int]]] = None, + resolution_wh: Optional[Tuple[int, int]] = None, ) -> Detections: """ Args: detections (sv.Detections): Detections object to be moved. offset (np.ndarray): An array of shape `(2,)` containing offset values in format is `[dx, dy]`. - image_size (Tuple, optional): A tuple of image shape. Can be `(2,)` or `(3,)`. - Important when moving for segmentation detections, as it defines mask array - size. + resolution_wh (Tuple[int, int]): The width and height of the desired mask + resolution. Required for segmentation detections. Returns: (sv.Detections) repositioned Detections object. """ detections.xyxy = move_boxes(xyxy=detections.xyxy, offset=offset) if detections.mask is not None: + if resolution_wh is None: + raise ValueError( + "Resolution width and height are required for moving segmentation " + "detections. This should be the same as (width, height) of image shape." + ) detections.mask = move_masks( - masks=detections.mask, offset=offset, desired_shape=image_shape + masks=detections.mask, offset=offset, resolution_wh=resolution_wh ) return detections @@ -138,8 +142,9 @@ def _run_callback(self, image, offset) -> Detections: """ image_slice = crop_image(image=image, xyxy=offset) detections = self.callback(image_slice) + resolution_wh = (image.shape[1], image.shape[0]) detections = move_detections( - detections=detections, offset=offset[:2], image_shape=image.shape + detections=detections, offset=offset[:2], resolution_wh=resolution_wh ) return detections diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index d20cadea4..49b91795f 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -595,24 +595,17 @@ def move_boxes(xyxy: np.ndarray, offset: np.ndarray) -> np.ndarray: def move_masks( masks: np.ndarray, offset: np.ndarray, - desired_shape: Optional[Union[Tuple[int, int, int], Tuple[int, int]]] = None, + resolution_wh: Tuple[int, int] = None, ) -> np.ndarray: """ Offset the masks in an array by the specified (x, y) amount. - Note the axis orders: - - - `masks`: array of shape `(n, y, x)` - - `offset`: array of ints: `(x, y)` - - `desired_shape`: tuple of ints, shaped `(y, x)` or `(y, x, ...)` - Args: masks (np.ndarray): array of bools offset (np.ndarray): An array of shape `(2,)` containing non-negative int values `[dx, dy]`. - desired_shape (Tuple, optional): Final shape of the mask in the format - `(height, width)`, `(height, width, ...)`. The masks will be padded to match - the first 2 shape dimensions. Note the axis order (y,x)! + 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. @@ -621,11 +614,7 @@ def move_masks( if offset[0] < 0 or offset[1] < 0: raise ValueError(f"Offset values must be non-negative integers. Got: {offset}") - size_y, size_x = masks.shape[1:] + offset[::-1] - if desired_shape is not None: - size_y, size_x = desired_shape[:2] - - mask_array = np.full((masks.shape[0], size_y, size_x), False) + mask_array = np.full((masks.shape[0], resolution_wh[1], resolution_wh[0]), False) mask_array[ :, offset[1] : masks.shape[1] + offset[1], From f56d7a173365c0688172f6ecc2363a1f661f624a Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Mon, 13 May 2024 14:01:07 +0200 Subject: [PATCH 133/274] changes after code review --- supervision/keypoint/annotators.py | 155 ++++++++++++++++++++++------- 1 file changed, 117 insertions(+), 38 deletions(-) diff --git a/supervision/keypoint/annotators.py b/supervision/keypoint/annotators.py index 56eafdf91..348a42fb7 100644 --- a/supervision/keypoint/annotators.py +++ b/supervision/keypoint/annotators.py @@ -48,8 +48,8 @@ def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType: points. It draws circles at each key point location. Args: - scene (ImageType): The image where bounding boxes will be drawn. `ImageType` - is a flexible type, accepting either `numpy.ndarray` or + scene (ImageType): The image where skeleton vertices will be drawn. + `ImageType` is a flexible type, accepting either `numpy.ndarray` or `PIL.Image.Image`. key_points (KeyPoints): A collection of key points where each key point consists of x and y coordinates. @@ -121,7 +121,7 @@ def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType: edges. Args: - scene (ImageType): The image where bounding boxes will be drawn. `ImageType` + scene (ImageType): The image where skeleton edges will be drawn. `ImageType` is a flexible type, accepting either `numpy.ndarray` or `PIL.Image.Image`. key_points (KeyPoints): A collection of key points where each key point @@ -181,7 +181,8 @@ def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType: class VertexLabelAnnotator: """ - A class for annotating vertex labels on an image using provided detections. + A class that draws labels of skeleton vertices on images. It uses specified key + points to determine the locations where the vertices should be drawn. """ def __init__( @@ -193,6 +194,18 @@ def __init__( text_padding: int = 10, border_radius: int = 0, ): + """ + Args: + color (Union[Color, List[Color]], optional): The color to use for each + keypoint label. If a list is provided, the colors will be used in order + for each keypoint. + text_color (Color, optional): The color to use for the labels. + text_scale (float, optional): The scale of the text. + text_thickness (int, optional): The thickness of the text. + text_padding (int, optional): The padding around the text. + border_radius (int, optional): The radius of the rounded corners of the + boxes. Set to a high value to produce circles. + """ self.border_radius: int = border_radius self.color: Union[Color, List[Color]] = color self.text_color: Color = text_color @@ -200,51 +213,62 @@ def __init__( self.text_thickness: int = text_thickness self.text_padding: int = text_padding - @staticmethod - def get_text_bounding_box( - text: str, - font: int, - text_scale: float, - text_thickness: int, - center_coordinates: Tuple[int, int], - ) -> Tuple[int, int, int, int]: - text_w, text_h = cv2.getTextSize( - text=text, - fontFace=font, - fontScale=text_scale, - thickness=text_thickness, - )[0] - center_x, center_y = center_coordinates - return ( - center_x - text_w // 2, - center_y - text_h // 2, - center_x + text_w // 2, - center_y + text_h // 2, - ) - def annotate( self, scene: ImageType, key_points: KeyPoints, labels: List[str] = None ) -> ImageType: - font = cv2.FONT_HERSHEY_SIMPLEX + """ + A class that draws labels of skeleton vertices on images. It uses specified key + points to determine the locations where the vertices should be drawn. - N, K, _ = key_points.xy.shape + Args: + scene (ImageType): The image where vertex labels will be drawn. `ImageType` + is a flexible type, accepting either `numpy.ndarray` or + `PIL.Image.Image`. + key_points (KeyPoints): A collection of key points where each key point + consists of x and y coordinates. + labels (List[str], optional): A list of labels to be displayed on the + annotated image. If not provided, keypoint indices will be used. - if N == 0: - return scene + Returns: + The annotated image, matching the type of `scene` (`numpy.ndarray` + or `PIL.Image.Image`) - anchors = key_points.xy.reshape(K * N, 2).astype(int) - colors = ( - np.array(self.color * N) - if isinstance(self.color, list) - else np.array([self.color] * K * N) - ) - labels = np.array(labels * N) + Example: + ```python + import supervision as sv + + image = ... + key_points = sv.KeyPoints(...) + vertex_label_annotator = sv.VertexLabelAnnotator() + annotated_frame = vertex_label_annotator.annotate( + scene=image.copy(), + key_points=key_points + ) + ``` + """ + font = cv2.FONT_HERSHEY_SIMPLEX + + skeletons_count, points_count, _ = key_points.xy.shape + if skeletons_count == 0: + return scene + + anchors = key_points.xy.reshape(points_count * skeletons_count, 2).astype(int) mask = np.all(anchors != 0, axis=1) - if np.all(mask == False): + if not np.any(mask): return scene + colors = self.preprocess_and_validate_colors( + colors=self.color, + points_count=points_count, + skeletons_count=skeletons_count) + + labels = self.preprocess_and_validate_labels( + labels=labels, + points_count=points_count, + skeletons_count=skeletons_count) + anchors = anchors[mask] colors = colors[mask] labels = labels[mask] @@ -283,3 +307,58 @@ def annotate( ) return scene + + @staticmethod + def get_text_bounding_box( + text: str, + font: int, + text_scale: float, + text_thickness: int, + center_coordinates: Tuple[int, int], + ) -> Tuple[int, int, int, int]: + text_w, text_h = cv2.getTextSize( + text=text, + fontFace=font, + fontScale=text_scale, + thickness=text_thickness, + )[0] + center_x, center_y = center_coordinates + return ( + center_x - text_w // 2, + center_y - text_h // 2, + center_x + text_w // 2, + center_y + text_h // 2, + ) + + @staticmethod + def preprocess_and_validate_labels( + labels: Optional[List[str]], + points_count: int, + skeletons_count: int + ) -> np.array: + if labels and len(labels) != points_count: + raise ValueError( + f"Number of labels ({len(labels)}) must match number of key points " + f"({points_count})." + ) + if labels is None: + labels = [str(i) for i in range(points_count)] + + return np.array(labels * skeletons_count) + + @staticmethod + def preprocess_and_validate_colors( + colors: Optional[Union[Color, List[Color]]], + points_count: int, + skeletons_count: int + ) -> np.array: + if isinstance(colors, list) and len(colors) != points_count: + raise ValueError( + f"Number of colors ({len(colors)}) must match number of key points " + f"({points_count})." + ) + return ( + np.array(colors * skeletons_count) + if isinstance(colors, list) + else np.array([colors] * points_count * skeletons_count) + ) From 1ebccebf4c194af8679924101b41ee9cacf45eb6 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 12:01:23 +0000 Subject: [PATCH 134/274] =?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/annotators.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/supervision/keypoint/annotators.py b/supervision/keypoint/annotators.py index 348a42fb7..888e7e2a9 100644 --- a/supervision/keypoint/annotators.py +++ b/supervision/keypoint/annotators.py @@ -262,12 +262,12 @@ def annotate( colors = self.preprocess_and_validate_colors( colors=self.color, points_count=points_count, - skeletons_count=skeletons_count) + skeletons_count=skeletons_count, + ) labels = self.preprocess_and_validate_labels( - labels=labels, - points_count=points_count, - skeletons_count=skeletons_count) + labels=labels, points_count=points_count, skeletons_count=skeletons_count + ) anchors = anchors[mask] colors = colors[mask] @@ -332,9 +332,7 @@ def get_text_bounding_box( @staticmethod def preprocess_and_validate_labels( - labels: Optional[List[str]], - points_count: int, - skeletons_count: int + labels: Optional[List[str]], points_count: int, skeletons_count: int ) -> np.array: if labels and len(labels) != points_count: raise ValueError( @@ -350,7 +348,7 @@ def preprocess_and_validate_labels( def preprocess_and_validate_colors( colors: Optional[Union[Color, List[Color]]], points_count: int, - skeletons_count: int + skeletons_count: int, ) -> np.array: if isinstance(colors, list) and len(colors) != points_count: raise ValueError( From 051fe5178bb75f205087e13e5571cdbbc7ae3280 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Mon, 13 May 2024 14:30:55 +0200 Subject: [PATCH 135/274] VertexLabelAnnotator plugged into docs --- docs/keypoint/annotators.md | 41 ++++++++++++++++++++++++++++-- supervision/keypoint/annotators.py | 19 +++++++++++--- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/docs/keypoint/annotators.md b/docs/keypoint/annotators.md index b5f998bc0..e7adeb098 100644 --- a/docs/keypoint/annotators.md +++ b/docs/keypoint/annotators.md @@ -13,7 +13,10 @@ status: new image = ... key_points = sv.KeyPoints(...) - vertex_annotator = sv.VertexAnnotator(color=sv.Color.GREEN, radius=10) + vertex_annotator = sv.VertexAnnotator( + color=sv.Color.GREEN, + radius=10 + ) annotated_frame = vertex_annotator.annotate( scene=image.copy(), key_points=key_points @@ -34,7 +37,10 @@ status: new image = ... key_points = sv.KeyPoints(...) - edge_annotator = sv.EdgeAnnotator(color=sv.Color.GREEN, thickness=5) + edge_annotator = sv.EdgeAnnotator( + color=sv.Color.GREEN, + thickness=5 + ) annotated_frame = edge_annotator.annotate( scene=image.copy(), key_points=key_points @@ -47,6 +53,31 @@ status: new +=== "VertexLabelAnnotator" + + ```python + import supervision as sv + + image = ... + key_points = sv.KeyPoints(...) + + vertex_label_annotator = sv.VertexLabelAnnotator( + color=sv.Color.GREEN, + text_color=sv.Color.BLACK, + border_radius=5 + ) + annotated_frame = vertex_label_annotator.annotate( + scene=image.copy(), + key_points=key_points + ) + ``` + +
+ + ![vertex-label-annotator-example](https://media.roboflow.com/supervision-annotator-examples/vertex-label-annotator-example.png){ align=center width="800" } + +
+ @@ -58,3 +89,9 @@ status: new :::supervision.keypoint.annotators.EdgeAnnotator + + + +:::supervision.keypoint.annotators.VertexLabelAnnotator diff --git a/supervision/keypoint/annotators.py b/supervision/keypoint/annotators.py index 348a42fb7..e87872737 100644 --- a/supervision/keypoint/annotators.py +++ b/supervision/keypoint/annotators.py @@ -65,7 +65,10 @@ def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType: image = ... key_points = sv.KeyPoints(...) - vertex_annotator = sv.VertexAnnotator(color=sv.Color.GREEN, radius=10) + vertex_annotator = sv.VertexAnnotator( + color=sv.Color.GREEN, + radius=10 + ) annotated_frame = vertex_annotator.annotate( scene=image.copy(), key_points=key_points @@ -139,7 +142,10 @@ def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType: image = ... key_points = sv.KeyPoints(...) - edge_annotator = sv.EdgeAnnotator(color=sv.Color.GREEN, thickness=5) + edge_annotator = sv.EdgeAnnotator( + color=sv.Color.GREEN, + thickness=5 + ) annotated_frame = edge_annotator.annotate( scene=image.copy(), key_points=key_points @@ -240,12 +246,19 @@ def annotate( image = ... key_points = sv.KeyPoints(...) - vertex_label_annotator = sv.VertexLabelAnnotator() + vertex_label_annotator = sv.VertexLabelAnnotator( + color=sv.Color.GREEN, + text_color=sv.Color.BLACK, + border_radius=5 + ) annotated_frame = vertex_label_annotator.annotate( scene=image.copy(), key_points=key_points ) ``` + + ![vertex-label-annotator-example](https://media.roboflow.com/ + supervision-annotator-examples/vertex-label-annotator-example.png) """ font = cv2.FONT_HERSHEY_SIMPLEX From e13c7dc22c66554d15bcdd9934b9a37b1847377c 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 12:31:34 +0000 Subject: [PATCH 136/274] =?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/keypoint/annotators.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/keypoint/annotators.md b/docs/keypoint/annotators.md index e7adeb098..30a970ecd 100644 --- a/docs/keypoint/annotators.md +++ b/docs/keypoint/annotators.md @@ -14,7 +14,7 @@ status: new key_points = sv.KeyPoints(...) vertex_annotator = sv.VertexAnnotator( - color=sv.Color.GREEN, + color=sv.Color.GREEN, radius=10 ) annotated_frame = vertex_annotator.annotate( @@ -38,7 +38,7 @@ status: new key_points = sv.KeyPoints(...) edge_annotator = sv.EdgeAnnotator( - color=sv.Color.GREEN, + color=sv.Color.GREEN, thickness=5 ) annotated_frame = edge_annotator.annotate( From 4c36b39f94c749203165c445ad9f6f4de0d3a4dc Mon Sep 17 00:00:00 2001 From: LinasKo Date: Mon, 13 May 2024 15:41:27 +0300 Subject: [PATCH 137/274] Untested: Small object segmentationation --- docs/how_to/detect_small_objects.md | 108 ++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/docs/how_to/detect_small_objects.md b/docs/how_to/detect_small_objects.md index e2d023284..42105480c 100644 --- a/docs/how_to/detect_small_objects.md +++ b/docs/how_to/detect_small_objects.md @@ -264,3 +264,111 @@ objects within each, and aggregating the results. ``` ![detection-with-inference-slicer](https://media.roboflow.com/supervision_detect_small_objects_example_3.png) + + +## Small Object Segmentation + +[`InferenceSlicer`](/latest/detection/tools/inference_slicer/#supervision.detection.tools.inference_slicer.InferenceSlicer) can perform segmentation tasks too. + +=== "Inference" + + ```{ .py hl_lines="6 16 19" } + 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] + detections = 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) + ``` + +=== "Ultralytics" + + ```{ .py hl_lines="6 16 19" } + import cv2 + import numpy as np + import supervision as sv + from ultralytics import YOLO + + model = YOLO("yolov8x-seg.pt") + image = cv2.imread() + + def callback(image_slice: np.ndarray) -> sv.Detections: + result = model(image_slice)[0] + return sv.Detections.from_ultralytics(result) + + 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) + ``` + +=== "Transformers" + + ```{ .py hl_lines="8-9 23 30 39" } + import cv2 + import torch + import numpy as np + import supervision as sv + from PIL import Image + from transformers import DetrImageProcessor, DetrForObjectDetection + + processor = DetrImageProcessor.from_pretrained("facebook/detr-resnet-50-panoptic") + model = DetrForObjectDetection.from_pretrained("facebook/detr-resnet-50-panoptic") + + image = cv2.imread() + + def callback(image_slice: np.ndarray) -> sv.Detections: + image_slice = cv2.cvtColor(image_slice, cv2.COLOR_BGR2RGB) + image_slice = Image.fromarray(image_slice) + inputs = processor(images=image_slice, return_tensors="pt") + + with torch.no_grad(): + outputs = model(**inputs) + + width, height = image.size + target_size = torch.tensor([[height, width]]) + results = processor.post_process_segmentation( + outputs=outputs, target_sizes=target_size)[0] + return sv.Detections.from_transformers(results) + + slicer = sv.InferenceSlicer(callback = callback) + detections = slicer(image) + + mask_annotator = sv.MaskAnnotator() + label_annotator = sv.LabelAnnotator() + + labels = [ + model.config.id2label[class_id] + for class_id + in detections.class_id + ] + + annotated_image = mask_annotator.annotate( + scene=image, detections=detections) + annotated_image = label_annotator.annotate( + scene=annotated_image, detections=detections, labels=labels) + ``` + +![detection-with-inference-slicer](https://media.roboflow.com/supervision-docs/inference-slicer-segmentation-example.png) From 45e5f49c5aaab506d9054ad62c2f0255d1b24809 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Mon, 13 May 2024 16:20:05 +0300 Subject: [PATCH 138/274] Infrence slicer docs: fix callbacks in transfomers --- docs/how_to/detect_small_objects.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/how_to/detect_small_objects.md b/docs/how_to/detect_small_objects.md index 42105480c..e4da7dde6 100644 --- a/docs/how_to/detect_small_objects.md +++ b/docs/how_to/detect_small_objects.md @@ -6,7 +6,7 @@ status: new # Detect Small Objects This guide shows how to detect small objects -with the [Inference](https://github.com/roboflow/inference), +with the [Inference](https://github.com/roboflow/inference), [Ultralytics](https://github.com/ultralytics/ultralytics) or [Transformers](https://github.com/huggingface/transformers) packages using [`InferenceSlicer`](/latest/detection/tools/inference_slicer/#supervision.detection.tools.inference_slicer.InferenceSlicer). @@ -68,10 +68,10 @@ size relative to the image resolution. import torch import supervision as sv from PIL import Image - from transformers import DetrImageProcessor, DetrForObjectDetection + from transformers import DetrImageProcessor, DetrForSegmentation processor = DetrImageProcessor.from_pretrained("facebook/detr-resnet-50") - model = DetrForObjectDetection.from_pretrained("facebook/detr-resnet-50") + model = DetrForSegmentation.from_pretrained("facebook/detr-resnet-50") image = Image.open() inputs = processor(images=image, return_tensors="pt") @@ -79,8 +79,8 @@ size relative to the image resolution. with torch.no_grad(): outputs = model(**inputs) - width, height = image.size - target_size = torch.tensor([[height, width]]) + width, height = image_slice.size + target_size = torch.tensor([[width, height]]) results = processor.post_process_object_detection( outputs=outputs, target_sizes=target_size)[0] detections = sv.Detections.from_transformers(results) @@ -239,8 +239,8 @@ objects within each, and aggregating the results. with torch.no_grad(): outputs = model(**inputs) - width, height = image.size - target_size = torch.tensor([[height, width]]) + width, height = image_slice.size + target_size = torch.tensor([[width, height]]) results = processor.post_process_object_detection( outputs=outputs, target_sizes=target_size)[0] return sv.Detections.from_transformers(results) @@ -265,7 +265,6 @@ objects within each, and aggregating the results. ![detection-with-inference-slicer](https://media.roboflow.com/supervision_detect_small_objects_example_3.png) - ## Small Object Segmentation [`InferenceSlicer`](/latest/detection/tools/inference_slicer/#supervision.detection.tools.inference_slicer.InferenceSlicer) can perform segmentation tasks too. @@ -326,7 +325,7 @@ objects within each, and aggregating the results. === "Transformers" - ```{ .py hl_lines="8-9 23 30 39" } + ```{ .py hl_lines="6 8-9 23 30 39" } import cv2 import torch import numpy as np @@ -347,8 +346,8 @@ objects within each, and aggregating the results. with torch.no_grad(): outputs = model(**inputs) - width, height = image.size - target_size = torch.tensor([[height, width]]) + width, height = image_slice.size + target_size = torch.tensor([[width, height]]) results = processor.post_process_segmentation( outputs=outputs, target_sizes=target_size)[0] return sv.Detections.from_transformers(results) From 452e1f1ea46301688cd32de314821690e4d596d6 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Mon, 13 May 2024 15:21:25 +0200 Subject: [PATCH 139/274] more `VertexLabelAnnotator` docs --- supervision/keypoint/annotators.py | 43 ++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/supervision/keypoint/annotators.py b/supervision/keypoint/annotators.py index b5723aa4a..e6ff1fcfd 100644 --- a/supervision/keypoint/annotators.py +++ b/supervision/keypoint/annotators.py @@ -259,6 +259,49 @@ def annotate( ![vertex-label-annotator-example](https://media.roboflow.com/ supervision-annotator-examples/vertex-label-annotator-example.png) + + !!! tip + + `VertexLabelAnnotator` allows to customize the color of each keypoint label + values. + + Example: + ```python + import supervision as sv + + image = ... + key_points = sv.KeyPoints(...) + + LABELS = [ + "nose", "left eye", "right eye", "left ear", + "right ear", "left shoulder", "right shoulder", "left elbow", + "right elbow", "left wrist", "right wrist", "left hip", + "right hip", "left knee", "right knee", "left ankle", + "right ankle" + ] + + COLORS = [ + "#FF6347", "#FF6347", "#FF6347", "#FF6347", + "#FF6347", "#FF1493", "#00FF00", "#FF1493", + "#00FF00", "#FF1493", "#00FF00", "#FFD700", + "#00BFFF", "#FFD700", "#00BFFF", "#FFD700", + "#00BFFF" + ] + COLORS = [sv.Color.from_hex(color_hex=c) for c in COLORS] + + vertex_label_annotator = sv.VertexLabelAnnotator( + color=COLORS, + text_color=sv.Color.BLACK, + border_radius=5 + ) + annotated_frame = vertex_label_annotator.annotate( + scene=image.copy(), + key_points=key_points, + labels=labels + ) + ``` + ![vertex-label-annotator-custom-example](https://media.roboflow.com/ + supervision-annotator-examples/vertex-label-annotator-custom-example.png) """ font = cv2.FONT_HERSHEY_SIMPLEX From 93398108ee27e5a648f9a0d2657e09a3511ccf15 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Mon, 13 May 2024 16:26:35 +0300 Subject: [PATCH 140/274] Slier docs: remove transfomer example - has issues. --- docs/how_to/detect_small_objects.md | 47 ----------------------------- 1 file changed, 47 deletions(-) diff --git a/docs/how_to/detect_small_objects.md b/docs/how_to/detect_small_objects.md index e4da7dde6..683b6616c 100644 --- a/docs/how_to/detect_small_objects.md +++ b/docs/how_to/detect_small_objects.md @@ -323,51 +323,4 @@ objects within each, and aggregating the results. scene=annotated_image, detections=detections) ``` -=== "Transformers" - - ```{ .py hl_lines="6 8-9 23 30 39" } - import cv2 - import torch - import numpy as np - import supervision as sv - from PIL import Image - from transformers import DetrImageProcessor, DetrForObjectDetection - - processor = DetrImageProcessor.from_pretrained("facebook/detr-resnet-50-panoptic") - model = DetrForObjectDetection.from_pretrained("facebook/detr-resnet-50-panoptic") - - image = cv2.imread() - - def callback(image_slice: np.ndarray) -> sv.Detections: - image_slice = cv2.cvtColor(image_slice, cv2.COLOR_BGR2RGB) - image_slice = Image.fromarray(image_slice) - inputs = processor(images=image_slice, return_tensors="pt") - - with torch.no_grad(): - outputs = model(**inputs) - - width, height = image_slice.size - target_size = torch.tensor([[width, height]]) - results = processor.post_process_segmentation( - outputs=outputs, target_sizes=target_size)[0] - return sv.Detections.from_transformers(results) - - slicer = sv.InferenceSlicer(callback = callback) - detections = slicer(image) - - mask_annotator = sv.MaskAnnotator() - label_annotator = sv.LabelAnnotator() - - labels = [ - model.config.id2label[class_id] - for class_id - in detections.class_id - ] - - annotated_image = mask_annotator.annotate( - scene=image, detections=detections) - annotated_image = label_annotator.annotate( - scene=annotated_image, detections=detections, labels=labels) - ``` - ![detection-with-inference-slicer](https://media.roboflow.com/supervision-docs/inference-slicer-segmentation-example.png) From 85e4c0ec6576b68b0642ba13f79bdd0482c1e6fb Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Mon, 13 May 2024 16:51:59 +0300 Subject: [PATCH 141/274] Slicer: highlight & mask docstring --- docs/how_to/detect_small_objects.md | 4 +- supervision/detection/utils.py | 58 +++++++++++++++++++---------- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/docs/how_to/detect_small_objects.md b/docs/how_to/detect_small_objects.md index 683b6616c..84affd961 100644 --- a/docs/how_to/detect_small_objects.md +++ b/docs/how_to/detect_small_objects.md @@ -271,7 +271,7 @@ objects within each, and aggregating the results. === "Inference" - ```{ .py hl_lines="6 16 19" } + ```{ .py hl_lines="6 16 19-20" } import cv2 import numpy as np import supervision as sv @@ -298,7 +298,7 @@ objects within each, and aggregating the results. === "Ultralytics" - ```{ .py hl_lines="6 16 19" } + ```{ .py hl_lines="6 16 19-20" } import cv2 import numpy as np import supervision as sv diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 49b91795f..a6ba041a5 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -55,7 +55,8 @@ def box_area(box): 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) @@ -80,7 +81,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, @@ -131,7 +133,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) @@ -161,7 +164,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 @@ -214,8 +218,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()] @@ -447,7 +452,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: @@ -476,7 +482,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() @@ -543,7 +550,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"]) @@ -554,10 +562,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 @@ -601,7 +611,9 @@ def move_masks( Offset the masks in an array by the specified (x, y) amount. Args: - masks (np.ndarray): array of bools + 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]`. resolution_wh (Tuple[int, int]): The width and height of the desired mask @@ -612,13 +624,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 @@ -682,8 +696,10 @@ def sum_over_mask(indices: np.ndarray, axis: tuple) -> 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) @@ -761,7 +777,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 " @@ -806,6 +823,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 From 8901192b9285488d37a09945180262b90bf46066 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Mon, 13 May 2024 16:53:06 +0300 Subject: [PATCH 142/274] ruff --- supervision/detection/utils.py | 54 ++++++++++++---------------------- 1 file changed, 19 insertions(+), 35 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 6f338ffcf..b5af2964d 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -55,8 +55,7 @@ def box_area(box): 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,8 +80,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, @@ -133,8 +131,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) @@ -164,8 +161,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 @@ -218,9 +214,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()] @@ -481,8 +476,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: @@ -511,8 +505,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() @@ -579,8 +572,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"]) @@ -591,12 +583,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 @@ -653,15 +643,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 @@ -725,10 +713,8 @@ def sum_over_mask(indices: np.ndarray, axis: tuple) -> 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) @@ -806,8 +792,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 " @@ -852,7 +837,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 From 19396771f17357679658b40014b78d9ae119c566 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Mon, 13 May 2024 15:58:22 +0200 Subject: [PATCH 143/274] bump package version from `0.21.0rc4` to `0.21.0rc5` --- pyproject.toml | 2 +- supervision/detection/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a98e91f37..8ceacbff0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "supervision" -version = "0.21.0rc4" +version = "0.21.0rc5" description = "A set of easy-to-use utils that will come in handy in any Computer Vision project" authors = ["Piotr Skalski "] maintainers = ["Piotr Skalski "] diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index b5af2964d..9232089e5 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -529,7 +529,7 @@ def process_roboflow_result( np.ndarray, Optional[np.ndarray], Optional[np.ndarray], - Dict[str, List[np.ndarray]], + Dict[str, Union[List[np.ndarray], np.ndarray]], ]: if not roboflow_result["predictions"]: return ( From 2bcd81e5fd0b5daf854815e5e9f7466c0b8e6947 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 17:41:06 +0000 Subject: [PATCH 144/274] =?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.3 → v0.4.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.3...v0.4.4) --- .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 b0f628973..9465c2af8 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.3 + rev: v0.4.4 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From c3b77d05c09f4a0192fb48aa95ab6ef701c557ed Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Tue, 14 May 2024 17:12:19 +0300 Subject: [PATCH 145/274] Rename, remove functions, unit-test & change `merge_object_detection_pair` --- supervision/detection/core.py | 176 ++++++++++++++++----------------- supervision/detection/utils.py | 29 +----- test/detection/test_core.py | 129 +++++++++++++++++++++++- 3 files changed, 219 insertions(+), 115 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index d56ba5160..0777571fc 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -8,8 +8,9 @@ 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 @@ mask_non_max_suppression, mask_to_xyxy, merge_data, - non_max_merge, process_roboflow_result, xywh_to_xyxy, ) @@ -1213,7 +1213,7 @@ def with_nmm( 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 @@ def with_nmm( 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 @@ def with_nmm( 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) +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. - 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 + 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. - 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] - ) + 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. - 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:]) + Args: + det1 (Detections): + The first Detections object + det2 (Detections): + The second Detections object - merged_xy = np.array([[merged_x1, merged_y1, merged_x2, merged_y2]]) + Returns: + Detections: A new Detections object, with merged attributes. - winning_class_id = winning_det.class_id + Raises: + ValueError: If the input Detections objects do not have exactly 1 detected + object. - 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]) + Example: + ```python + import cv2 + import supervision as sv + from inference import get_model - merged_mask = None - if det1.mask is not None and det2.mask is not None: - merged_mask = np.logical_or(det1.mask, det2.mask) + image = cv2.imread() + model = get_model(model_id="yolov8s-640") - winning_tracker_id = winning_det.tracker_id + result = model.infer(image)[0] + detections = sv.Detections.from_inference(result) - winning_data = None - if det1.data and det2.data: - winning_data = winning_det.data + 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] + ) - return Detections( - xyxy=merged_xy, - mask=merged_mask, - confidence=merged_confidence, - class_id=winning_class_id, - tracker_id=winning_tracker_id, - data=winning_data, - ) + 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 + else: + merged_confidence = ( + area_det1 * det1.confidence[0] + area_det2 * det2.confidence[0] + ) / (area_det1 + area_det2) + merged_confidence = np.array([merged_confidence]) + + winning_class_id = winning_det.class_id + winning_tracker_id = winning_det.tracker_id + 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, + ) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index d2e403a49..bd20ab37d 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 12f3de281..31e56decd 100644 --- a/test/detection/test_core.py +++ b/test/detection/test_core.py @@ -5,7 +5,7 @@ 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 146/274] 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 bd20ab37d..f177d0886 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 097c5c6e5..e6f330841 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -6,6 +6,7 @@ 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 147/274] 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 16de484a3..3eae2e178 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -35,7 +35,7 @@ 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.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 0777571fc..1b3a385de 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -8,7 +8,7 @@ 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 @@ def with_nmm( 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 f177d0886..c2f02c1b9 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 148/274] 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 c2f02c1b9..f6308f57a 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 149/274] 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 1b3a385de..76224bb72 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -1233,7 +1233,7 @@ def with_nmm( 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 5ef934e78099d29f507c29cfd9521d37db1d9886 Mon Sep 17 00:00:00 2001 From: Christoforos Aristeidou Date: Wed, 15 May 2024 09:51:55 +0300 Subject: [PATCH 150/274] fixed sentence spacing --- supervision/annotators/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/supervision/annotators/utils.py b/supervision/annotators/utils.py index e206c8cbb..23e42752c 100644 --- a/supervision/annotators/utils.py +++ b/supervision/annotators/utils.py @@ -34,14 +34,14 @@ def resolve_color_idx( ) -> int: if detection_idx >= len(detections): raise ValueError( - f"Detection index {detection_idx}" + f"Detection index {detection_idx} " f"is out of bounds for detections of length {len(detections)}" ) if isinstance(color_lookup, np.ndarray): if len(color_lookup) != len(detections): raise ValueError( - f"Length of color lookup {len(color_lookup)}" + f"Length of color lookup {len(color_lookup)} " f"does not match length of detections {len(detections)}" ) return color_lookup[detection_idx] @@ -50,14 +50,14 @@ def resolve_color_idx( elif color_lookup == ColorLookup.CLASS: if detections.class_id is None: raise ValueError( - "Could not resolve color by class because" + "Could not resolve color by class because " "Detections do not have class_id" ) return detections.class_id[detection_idx] elif color_lookup == ColorLookup.TRACK: if detections.tracker_id is None: raise ValueError( - "Could not resolve color by track because" + "Could not resolve color by track because " "Detections do not have tracker_id" ) return detections.tracker_id[detection_idx] From aab861c2db5ae2b641285a225fbeafe6206a6d02 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Wed, 15 May 2024 08:55:47 +0200 Subject: [PATCH 151/274] test_coco_annotations_to_detections result matrices defined in place --- test/dataset/formats/test_coco.py | 184 ++++++++++-------------------- 1 file changed, 63 insertions(+), 121 deletions(-) diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index f47e796d2..bfde8495f 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, + bbox=(0, 0, 5, 5), + area=5 * 5, 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, - ], + "size": [5, 5], + "counts": [0, 15, 2, 3, 2, 3], }, iscrowd=True, ) ], - (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, 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 + ), # single image annotations with mask, RLE segmentation mask ( [ 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]], ), 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, ), ], - (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 152/274] =?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 bfde8495f..66e73c0e3 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 153/274] 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 3eae2e178..03f52086f 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -43,9 +43,9 @@ 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 76224bb72..2489ef801 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -8,9 +8,9 @@ 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 @@ def with_nmm( 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 f6308f57a..c159de596 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 154/274] 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 c159de596..cb2545522 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) - - 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 + candidate = np.expand_dims(predictions[idx], axis=0) + ious = box_iou_batch(predictions[order][:, :4], candidate[:, :4]) - 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 155/274] 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 cb2545522..7985c7391 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 156/274] 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 7985c7391..56420ed6e 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 157/274] 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 56420ed6e..85b741c35 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 158/274] 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 2489ef801..2f358c6b7 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -8,7 +8,6 @@ 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 @@ def with_nmm( 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 24d1840ea1c8b6dab615b6d8c1b4913b131ddff8 Mon Sep 17 00:00:00 2001 From: Onuralp SEZER Date: Wed, 15 May 2024 18:35:36 +0300 Subject: [PATCH 159/274] =?UTF-8?q?docs:=20=F0=9F=93=9D=20RichLabelAnnotat?= =?UTF-8?q?or=20documentation=20added=20into=20docs=20and=20minor=20var=20?= =?UTF-8?q?fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onuralp SEZER --- docs/annotators.md | 37 ++++++++++++++++++++++++++++++++++ supervision/__init__.py | 1 + supervision/annotators/core.py | 4 ++-- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/docs/annotators.md b/docs/annotators.md index e1f4b115f..958f2a748 100644 --- a/docs/annotators.md +++ b/docs/annotators.md @@ -285,6 +285,37 @@ status: new +=== "RichLabel" + + ```python + import supervision as sv + + image = ... + detections = sv.Detections(...) + + labels = [ + f"{class_name} {confidence:.2f}" + for class_name, confidence + in zip(detections['class_name'], detections.confidence) + ] + + rich_label_annotator = sv.RichLabelAnnotator( + font_path=".../font.ttf", + text_position=sv.Position.CENTER + ) + annotated_frame = label_annotator.annotate( + scene=image.copy(), + detections=detections, + labels=labels + ) + ``` + +
+ + ![label-annotator-example](https://media.roboflow.com/supervision-annotator-examples/label-annotator-example-purple.png){ align=center width="800" } + +
+ === "Crop" ```python @@ -492,6 +523,12 @@ status: new :::supervision.annotators.core.LabelAnnotator + + +:::supervision.annotators.core.RichLabelAnnotator + diff --git a/supervision/__init__.py b/supervision/__init__.py index f8e7a8324..d2b502b9d 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -18,6 +18,7 @@ HaloAnnotator, HeatMapAnnotator, LabelAnnotator, + RichLabelAnnotator, MaskAnnotator, OrientedBoxAnnotator, PercentageBarAnnotator, diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index f2b552cd6..75373e333 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -1177,7 +1177,7 @@ def annotate( Example: ```python - import supervision as sv + import supervision as sv image = ... detections = sv.Detections(...) @@ -1188,7 +1188,7 @@ def annotate( in zip(detections['class_name'], detections.confidence) ] - label_annotator = sv.RichLabelAnnotator(font_path="path/to/font.ttf") + rich_label_annotator = sv.RichLabelAnnotator(font_path="path/to/font.ttf") annotated_frame = label_annotator.annotate( scene=image.copy(), detections=detections, From d983177c3a3af42d88facff59a755f924c8933d9 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 15:54:51 +0000 Subject: [PATCH 160/274] =?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 +- supervision/annotators/core.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/supervision/__init__.py b/supervision/__init__.py index da7549281..646800125 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -18,12 +18,12 @@ HaloAnnotator, HeatMapAnnotator, LabelAnnotator, - RichLabelAnnotator, MaskAnnotator, OrientedBoxAnnotator, PercentageBarAnnotator, PixelateAnnotator, PolygonAnnotator, + RichLabelAnnotator, RoundBoxAnnotator, TraceAnnotator, TriangleAnnotator, diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index 75373e333..1c0ad8af1 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -1125,8 +1125,8 @@ def __init__( color (Union[Color, ColorPalette]): The color or color palette to use for annotating the text background. text_color (Color): The color to use for the text. - font_path (str): Path to the font file (e.g., ".ttf" or ".otf") to use for rendering text. - If `None`, the default PIL font will be used. + font_path (str): Path to the font file (e.g., ".ttf" or ".otf") to use for + rendering text. If `None`, the default PIL font will be used. font_size (int): Font size for the text. text_padding (int): Padding around the text within its background box. text_position (Position): Position of the text relative to the detection. From a615dfccb619c347f704d4e98682ddf01681ffb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 00:44:29 +0000 Subject: [PATCH 161/274] :arrow_up: Bump mkdocs-material from 9.5.22 to 9.5.23 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.5.22 to 9.5.23. - [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.22...9.5.23) --- 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 71b6addb4..2a689d76e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2198,13 +2198,13 @@ pygments = ">2.12.0" [[package]] name = "mkdocs-material" -version = "9.5.22" +version = "9.5.23" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.22-py3-none-any.whl", hash = "sha256:8c7a377d323567934e6cd46915e64dc209efceaec0dec1cf2202184f5649862c"}, - {file = "mkdocs_material-9.5.22.tar.gz", hash = "sha256:22a853a456ae8c581c4628159574d6fc7c71b2c7569dc9c3a82cc70432219599"}, + {file = "mkdocs_material-9.5.23-py3-none-any.whl", hash = "sha256:ffd08a5beaef3cd135aceb58ded8b98bbbbf2b70e5b656f6a14a63c917d9b001"}, + {file = "mkdocs_material-9.5.23.tar.gz", hash = "sha256:4627fc3f15de2cba2bde9debc2fd59b9888ef494beabfe67eb352e23d14bf288"}, ] [package.dependencies] From d8679d9c46cbf76fd86b64ea11bf2833a9794781 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Thu, 16 May 2024 08:40:28 +0200 Subject: [PATCH 162/274] 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 b6b2490ac..2cc442a9f 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -11,6 +11,7 @@ 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 66e73c0e3..cecb637d2 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 @@ 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 163/274] =?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 2cc442a9f..19e7aa3e6 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -10,8 +10,8 @@ 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 cecb637d2..139c74f80 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -10,8 +10,8 @@ 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 164/274] 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 19e7aa3e6..db65ed57f 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 165/274] =?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 db65ed57f..5a55532c8 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 2c769e2f720e198a6d784c9b977dd8ec7c1b17ae Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Thu, 16 May 2024 10:11:03 +0300 Subject: [PATCH 166/274] Missing return in another slicer callback --- docs/how_to/detect_small_objects.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how_to/detect_small_objects.md b/docs/how_to/detect_small_objects.md index 638016821..175b4f36b 100644 --- a/docs/how_to/detect_small_objects.md +++ b/docs/how_to/detect_small_objects.md @@ -282,7 +282,7 @@ objects within each, and aggregating the results. def callback(image_slice: np.ndarray) -> sv.Detections: results = model.infer(image_slice)[0] - detections = sv.Detections.from_inference(results) + return sv.Detections.from_inference(results) slicer = sv.InferenceSlicer(callback = callback) detections = slicer(image) From 3ee72e38c35982983cd3fc971b31499a56d76f58 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Thu, 16 May 2024 09:20:26 +0200 Subject: [PATCH 167/274] 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 db65ed57f..91e11dcb6 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 168/274] =?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 73b5b32a3..0e80afb5e 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 169/274] :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 2a689d76e..cf0350b12 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 eb84d41c617710851aebf2f5e578bde1f7624511 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 May 2024 01:00:50 +0000 Subject: [PATCH 170/274] :arrow_up: Bump twine from 5.0.0 to 5.1.0 Bumps [twine](https://github.com/pypa/twine) from 5.0.0 to 5.1.0. - [Release notes](https://github.com/pypa/twine/releases) - [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst) - [Commits](https://github.com/pypa/twine/compare/5.0.0...5.1.0) --- updated-dependencies: - dependency-name: twine dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2a689d76e..e72cb0b65 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4009,13 +4009,13 @@ test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0, [[package]] name = "twine" -version = "5.0.0" +version = "5.1.0" description = "Collection of utilities for publishing packages on PyPI" optional = false python-versions = ">=3.8" files = [ - {file = "twine-5.0.0-py3-none-any.whl", hash = "sha256:a262933de0b484c53408f9edae2e7821c1c45a3314ff2df9bdd343aa7ab8edc0"}, - {file = "twine-5.0.0.tar.gz", hash = "sha256:89b0cc7d370a4b66421cc6102f269aa910fe0f1861c124f573cf2ddedbc10cf4"}, + {file = "twine-5.1.0-py3-none-any.whl", hash = "sha256:fe1d814395bfe50cfbe27783cb74efe93abeac3f66deaeb6c8390e4e92bacb43"}, + {file = "twine-5.1.0.tar.gz", hash = "sha256:4d74770c88c4fcaf8134d2a6a9d863e40f08255ff7d8e2acb3cbbd57d25f6e9d"}, ] [package.dependencies] From de5349ad6d02f9a69dfd419284291e23a52b602d Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Fri, 17 May 2024 09:17:38 +0300 Subject: [PATCH 171/274] 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 84758e06f..f4b86a53f 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 172/274] 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 85b741c35..db33ab01d 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -3,6 +3,7 @@ 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 173/274] 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 73b5b32a3..723ad4637 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 139c74f80..11085a90c 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 174/274] 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 0e80afb5e..4f6c169e6 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 139c74f80..af6b31bc6 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 175/274] 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 551e96da0..fbbbe6b73 100644 --- a/supervision/dataset/core.py +++ b/supervision/dataset/core.py @@ -430,6 +430,10 @@ def as_coco( """ 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 176/274] =?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 fbbbe6b73..6ae5844a5 100644 --- a/supervision/dataset/core.py +++ b/supervision/dataset/core.py @@ -430,8 +430,8 @@ def as_coco( """ 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 6be0fa2ca..8da8ccc8f 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 177/274] 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 73780414c..997da3528 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 178/274] =?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 997da3528..875d8c0c9 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 179/274] 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 875d8c0c9..c8a2b0378 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 powerset(s): 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 180/274] :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 db11d029e..9ed05cfb0 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 181/274] 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 c8a2b0378..76f87c2dc 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 powerset(s): 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 powerset(s): ], ) 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 182/274] 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 c8b73758f..c07c2b52f 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 6be0fa2ca..926a56e84 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -15,7 +15,7 @@ ) -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 183/274] 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 c07c2b52f..b1d67fffa 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -14,7 +14,11 @@ 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 3eeba5b44..1dd208bb7 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -3,6 +3,7 @@ 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 184/274] =?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 b1d67fffa..3c6848718 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -15,9 +15,9 @@ ) 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 1dd208bb7..f00641fbe 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 9c89b964c..7e269dae4 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 185/274] 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 1dd208bb7..b39b7b384 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 1c4a1d349..1214cfed9 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -2,6 +2,7 @@ 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 @@ 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 186/274] 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 1214cfed9..a9472f85d 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 187/274] 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 abacdc210..9271c23d3 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 2bc729442..caa0e5ae3 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -56,6 +56,8 @@ 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 188/274] =?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 8b9f91f76..0c42a69e5 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 7f50a7ad0..51b43ef0d 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -50,6 +50,8 @@ 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 @@ 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 9dd3d734b..355100605 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 b72ca4bf6..2821aed22 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -12,13 +12,13 @@ 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 189/274] 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 355100605..89728c7a5 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 190/274] --- 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 9ed05cfb0..38eae8421 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 8ceacbff0..252bb49d4 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 191/274] --- 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 9ed05cfb0..c9807aef6 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 192/274] 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 0c42a69e5..f9c9473bc 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 51b43ef0d..af5f8dcc4 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -50,8 +50,8 @@ 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 3c6848718..353e33f5e 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -15,8 +15,8 @@ ) 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 89728c7a5..5a8e7ba28 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 2821aed22..c7ed674ae 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -12,8 +12,8 @@ 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 193/274] =?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 af5f8dcc4..abe633908 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -49,9 +49,9 @@ 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 c7ed674ae..6a2070dae 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -10,10 +10,10 @@ 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 194/274] 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 6ae5844a5..c8863df35 100644 --- a/supervision/dataset/core.py +++ b/supervision/dataset/core.py @@ -116,13 +116,12 @@ def split( 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 @@ def from_pascal_voc( DetectionDataset: A DetectionDataset instance containing the loaded images and annotations. - Example: + Examples: ```python import roboflow from roboflow import Roboflow @@ -286,7 +285,7 @@ def from_yolo( DetectionDataset: A DetectionDataset instance containing the loaded images and annotations. - Example: + Examples: ```python import roboflow from roboflow import Roboflow @@ -391,7 +390,7 @@ def from_coco( DetectionDataset: A DetectionDataset instance containing the loaded images and annotations. - Example: + Examples: ```python import roboflow from roboflow import Roboflow @@ -430,10 +429,20 @@ def as_coco( """ 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 @@ def merge(cls, dataset_list: List[DetectionDataset]) -> DetectionDataset: (DetectionDataset): A single `DetectionDataset` object containing the merged data from the input list. - Example: + Examples: ```python import supervision as sv @@ -571,13 +580,12 @@ def split( 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 @@ def from_folder_structure(cls, root_directory_path: str) -> ClassificationDatase 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 5a8e7ba28..f1e778831 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 195/274] 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 53d762a0f..fe850894a 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -140,6 +140,13 @@ def trigger(self, detections: Detections) -> Tuple[np.ndarray, np.ndarray]: 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 @@ def trigger(self, detections: Detections) -> Tuple[np.ndarray, np.ndarray]: ) 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 f58f32998..6b20bdd1a 100644 --- a/supervision/detection/tools/smoother.py +++ b/supervision/detection/tools/smoother.py @@ -78,8 +78,6 @@ def update_with_detections(self, detections: Detections) -> Detections: 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 196/274] 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 4efc71947..a43fbe5a3 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 f1e778831..f9f71f98d 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 197/274] =?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 a43fbe5a3..32ece6bf1 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 f9f71f98d..742f94f6a 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 198/274] 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 76f87c2dc..77784c407 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 powerset(s): 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 powerset(s): @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 powerset(s): ) 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 199/274] =?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 77784c407..803eed9ac 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -6,7 +6,7 @@ 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 200/274] 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 803eed9ac..9c530cca3 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 201/274] =?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 9c530cca3..9083a2df9 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 202/274] --- 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 e672448a9..85615d1c9 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 252bb49d4..ff83f5fae 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 203/274] 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 0ba9e4f42..d6a04efb6 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -7,6 +7,7 @@ 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 @@ def from_paddledet(cls, paddledet_result) -> 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 000000000..1c4b90dc1 --- /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 000000000..b7f7c5b4c --- /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 204/274] 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 b7f7c5b4c..f8ea91ef9 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 205/274] =?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 d6a04efb6..e85998173 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -7,7 +7,7 @@ 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 1c4b90dc1..679213288 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 f8ea91ef9..5066a7a3e 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 @@ "", (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 206/274] 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 1c4b90dc1..9cd7434ec 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 f8ea91ef9..91840d887 100644 --- a/test/detection/test_lmm.py +++ b/test/detection/test_lmm.py @@ -13,43 +13,43 @@ "", (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 @@ ( 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 @@ ( 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 @@ ( 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 @@ ( 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 207/274] =?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 9cd7434ec..3660fb68e 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 91840d887..5b4f31ba2 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 @@ "", (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 208/274] 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 5b4f31ba2..b6232bd85 100644 --- a/test/detection/test_lmm.py +++ b/test/detection/test_lmm.py @@ -82,7 +82,7 @@ ), ), # correct response; with classes ( - " cat ; dog", + " cat ; dog", # noqa: E501 (1000, 1000), ["cat", "dog"], ( @@ -92,7 +92,7 @@ ), ), # correct response; with classes ( - " cat ; cat", + " cat ; cat", # noqa: E501 (1000, 1000), ["cat", "dog"], ( @@ -102,7 +102,7 @@ ), ), # 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 209/274] 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 fe850894a..cd4400e53 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.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 @@ def trigger(self, detections: Detections) -> Tuple[np.ndarray, np.ndarray]: 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 6b20bdd1a..5768c3e8d 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 @@ import numpy as np from supervision.detection.core import Detections +from supervision.utils.internal import SupervisionWarnings class DetectionsSmoother: @@ -70,9 +72,11 @@ def update_with_detections(self, detections: Detections) -> Detections: """ 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 210/274] =?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 0ba9e4f42..6563de7d3 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -240,7 +240,7 @@ def from_ultralytics(cls, ultralytics_results) -> 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 211/274] 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 281c40c97..54f1eda22 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 212/274] 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 54f1eda22..f257238df 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 213/274] 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 3660fb68e..0278fc004 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 b6232bd85..e20b947d3 100644 --- a/test/detection/test_lmm.py +++ b/test/detection/test_lmm.py @@ -15,6 +15,12 @@ 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 214/274] =?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 e20b947d3..129aa44b4 100644 --- a/test/detection/test_lmm.py +++ b/test/detection/test_lmm.py @@ -18,7 +18,7 @@ ( "", (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 215/274] Address review comments, simplify merge * Reintroduced iou check before response - necessary for algorithm --- supervision/__init__.py | 3 +- supervision/detection/core.py | 137 ++++++++++++++++++++++----------- supervision/detection/utils.py | 118 +++++++++++++++++----------- test/detection/test_core.py | 77 ++++++++++++++---- test/detection/test_utils.py | 56 ++++++++------ 5 files changed, 261 insertions(+), 130 deletions(-) diff --git a/supervision/__init__.py b/supervision/__init__.py index 03f52086f..816142b90 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -35,7 +35,7 @@ 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.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 2f358c6b7..6abc8dadd 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -8,8 +8,8 @@ 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 @@ def with_nmm( 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 @@ def with_nmm( 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 + _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: - winning_det = det2 + 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 * detections_1.confidence[0] + + area_det2 * detections_2.confidence[0] + ) / (area_det1 + area_det2) + merged_confidence = np.array([merged_confidence]) - area_det1 = (det1.xyxy[0][2] - det1.xyxy[0][0]) * ( - det1.xyxy[0][3] - det1.xyxy[0][1] + merged_x1, merged_y1 = np.minimum( + detections_1.xyxy[0][:2], detections_2.xyxy[0][:2] ) - area_det2 = (det2.xyxy[0][2] - det2.xyxy[0][0]) * ( - det2.xyxy[0][3] - det2.xyxy[0][1] + 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]]) - 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 + if detections_1.mask is None and detections_2.mask is None: + merged_mask = None else: - merged_mask = np.logical_or(det1.mask, det2.mask) + merged_mask = np.logical_or(detections_1.mask, detections_2.mask) - if det1.confidence is None or det2.confidence is None: - merged_confidence = winning_det.confidence + 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: - merged_confidence = ( - area_det1 * det1.confidence[0] + area_det2 * det2.confidence[0] - ) / (area_det1 + area_det2) - merged_confidence = np.array([merged_confidence]) + 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 db33ab01d..b8b8f7c19 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -56,7 +56,8 @@ def box_area(box): 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 sum_over_mask(indices: np.ndarray, axis: tuple) -> 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 31e56decd..bef511e53 100644 --- a/test/detection/test_core.py +++ b/test/detection/test_core.py @@ -5,7 +5,7 @@ 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 @@ 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 e6f330841..cb7537e19 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -6,7 +6,7 @@ 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 216/274] =?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 b8b8f7c19..4beea2ed5 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -56,8 +56,7 @@ def box_area(box): 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 sum_over_mask(indices: np.ndarray, axis: tuple) -> 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 bef511e53..dc58c9e8c 100644 --- a/test/detection/test_core.py +++ b/test/detection/test_core.py @@ -193,8 +193,7 @@ 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 cb7537e19..9a1fa8c93 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 217/274] 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 6abc8dadd..069eaf09c 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -1068,33 +1068,6 @@ def __setitem__(self, key: str, value: Union[np.ndarray, List]): 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 218/274] 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 3eeba5b44..80742fd3a 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -56,7 +56,9 @@ def box_area(box): 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 132bf391f..ce3bbbbff 100644 --- a/supervision/tracker/byte_tracker/core.py +++ b/supervision/tracker/byte_tracker/core.py @@ -362,13 +362,6 @@ def update_with_tensors(self, tensors: np.ndarray) -> List[STrack]: 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 219/274] :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 85615d1c9..0a9276f82 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 220/274] 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 53d762a0f..dc1751f4d 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -80,7 +80,9 @@ def __init__( 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 9c530cca3..931f5f52a 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 powerset(s): @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 powerset(s): ], [[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 powerset(s): (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 221/274] =?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 0b7a7fd86..454afb5bb 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 powerset(s): ], [[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 powerset(s): (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 powerset(s): [[[-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 222/274] 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 0b7a7fd86..7e1412772 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 powerset(s): ], [[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 powerset(s): (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 powerset(s): [[[-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 223/274] 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 53d762a0f..7df6dd2a4 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 @@ def trigger(self, detections: Detections) -> Tuple[np.ndarray, np.ndarray]: ] ) + 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 @@ def trigger(self, detections: Detections) -> Tuple[np.ndarray, np.ndarray]: 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 @@ def __init__( 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 224/274] 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 7df6dd2a4..4d6db6efe 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -154,7 +154,10 @@ def trigger(self, detections: Detections) -> Tuple[np.ndarray, np.ndarray]: # 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 225/274] =?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 f85604c47..e01001c07 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -161,7 +161,7 @@ def trigger(self, detections: Detections) -> Tuple[np.ndarray, np.ndarray]: 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 @@ def _cross_product(anchors: np.ndarray, vector: Vector) -> np.ndarray: 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 226/274] 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 f85604c47..e31cc6cd1 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -176,7 +176,8 @@ def trigger(self, detections: Detections) -> Tuple[np.ndarray, np.ndarray]: 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 @@ def _cross_product(anchors: np.ndarray, vector: Vector) -> np.ndarray: 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 227/274] 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 7e1412772..cd184b73f 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -350,7 +350,11 @@ def powerset(s): @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 530e1d01e152e45bd9f5bb37553f8bacbc6aeb75 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Mon, 27 May 2024 16:17:27 +0300 Subject: [PATCH 228/274] 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 069eaf09c..f85d403d7 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -1203,7 +1203,7 @@ def with_nmm( 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 @@ def with_nmm( 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 dc58c9e8c..af1d58762 100644 --- a/test/detection/test_core.py +++ b/test/detection/test_core.py @@ -5,7 +5,7 @@ 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 229/274] 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 4beea2ed5..74726995e 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 9a1fa8c93..b62faa619 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -6,12 +6,12 @@ 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 230/274] =?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 9465c2af8..0c47f2b68 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 231/274] 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 abe633908..715b9085c 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -39,6 +39,7 @@ 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 232/274] :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 0a9276f82..e5dd7c74d 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 233/274] 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 1a46226fd..6cd5f9ff8 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -46,6 +46,7 @@ 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 82551434e..3302b1390 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 @@ def __init__( 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 @@ def callback(image_slice: np.ndarray) -> sv.Detections: 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 1ca487916..86b730be6 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 234/274] 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 be6104820..f93aed1c4 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -23,7 +23,7 @@ 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 978a14485..1e84da612 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 @@ def __get__(self, owner_self: object, owner_cls: type) -> object: 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 000000000..d268a837f --- /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 235/274] =?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 d268a837f..ecb1ff290 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 @@ 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 236/274] 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 ecb1ff290..f5dfe4297 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 @@ }, 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 237/274] get_instance_variables only accepts instance, more tests --- supervision/utils/internal.py | 18 +++--- test/utils/test_internal.py | 113 +++++++++++++++++++++++++++++++--- 2 files changed, 114 insertions(+), 17 deletions(-) diff --git a/supervision/utils/internal.py b/supervision/utils/internal.py index 1e84da612..b773e6e60 100644 --- a/supervision/utils/internal.py +++ b/supervision/utils/internal.py @@ -144,14 +144,12 @@ def __get__(self, owner_self: object, owner_cls: type) -> object: 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 f5dfe4297..b3aedad77 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,25 +9,111 @@ 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", [ ( - Detections, + MockClass, False, - {"class_id", "confidence", "mask", "tracker_id"}, + None, + pytest.raises(ValueError), + ), + ( + MockClass(), + False, + {"public"}, DoesNotRaise(), ), ( - Detections.empty(), + MockClass(), + True, + {"public", "public_property"}, + DoesNotRaise(), + ), + ( + MockDataclass(), False, - {"xyxy", "class_id", "confidence", "mask", "tracker_id", "data"}, + {"public"}, DoesNotRaise(), ), + ( + MockDataclass(), + True, + {"public", "public_property"}, + DoesNotRaise(), + ), + ( + Detections, + False, + None, + pytest.raises(ValueError), + ), ( Detections, True, - {"class_id", "confidence", "mask", "tracker_id", "area", "box_area"}, + None, + pytest.raises(ValueError), + ), + ( + Detections.empty(), + False, + {"xyxy", "class_id", "confidence", "mask", "tracker_id", "data"}, DoesNotRaise(), ), ( @@ -76,10 +164,19 @@ }, 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 238/274] 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 cd184b73f..d9c21f15d 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 239/274] 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 761d27c0d..45d4c1ed3 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -82,8 +82,8 @@ def __init__( 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 a19972127..f1c48f942 100644 --- a/supervision/detection/tools/polygon_zone.py +++ b/supervision/detection/tools/polygon_zone.py @@ -54,6 +54,8 @@ def __init__( 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 1a86a45b4..ed899615d 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 240/274] :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 e5dd7c74d..3187955d6 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 241/274] :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 e5dd7c74d..e5c1a29a1 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 242/274] 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 b773e6e60..072c03b79 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 b3aedad77..eee614e6c 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 @@ def __private_property(self): @pytest.mark.parametrize( - "input_obj, include_properties, expected, exception", + "input_instance, include_properties, expected, exception", [ ( MockClass, @@ -89,13 +97,13 @@ def __private_property(self): ( 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 @@ def __private_property(self): ], ) 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 243/274] 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 5d5d08bc5..3a00b879d 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 6cd5f9ff8..084af390b 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -46,7 +46,7 @@ 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 3302b1390..893692712 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 @@ def __init__( 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 @@ def callback(image_slice: np.ndarray) -> sv.Detections: 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 86b730be6..71ca47865 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 244/274] 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 084af390b..83e677952 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -40,23 +40,25 @@ 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 be6104820..482e11093 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -8,15 +8,17 @@ 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 000000000..7fa21b7e0 --- /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 893692712..372352e6c 100644 --- a/supervision/detection/tools/inference_slicer.py +++ b/supervision/detection/tools/inference_slicer.py @@ -5,12 +5,11 @@ 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 71ca47865..b36b6853f 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 000000000..0186a23e2 --- /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 837b3b840..f0f0a6b13 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -7,15 +7,12 @@ 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[:, 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 245/274] 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 3a00b879d..51301e86f 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 f9c9473bc..dd14a23e2 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 246/274] 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 7fa21b7e0..f9acc4a6c 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 247/274] 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 45bc9644d..4198135de 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -167,20 +167,19 @@ def trigger(self, detections: Detections) -> Tuple[np.ndarray, np.ndarray]: # 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 248/274] :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 061b284b6..319db4a38 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 ff83f5fae..640d462e0 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 249/274] 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 000000000..1631852f4 --- /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 51301e86f..5d5d08bc5 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 dd14a23e2..369746a3e 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 f257238df..19d6a4fd4 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 83e677952..4f28d49f4 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -40,8 +40,8 @@ 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 482e11093..c47bc8197 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -8,7 +8,7 @@ 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 f9acc4a6c..ab4408d18 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 372352e6c..134361bd3 100644 --- a/supervision/detection/tools/inference_slicer.py +++ b/supervision/detection/tools/inference_slicer.py @@ -5,10 +5,7 @@ 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 @@ def __init__( 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 @@ def callback(image_slice: np.ndarray) -> sv.Detections: 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 @@ def callback(image_slice: np.ndarray) -> sv.Detections: 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 0186a23e2..f628c30f9 100644 --- a/test/detection/test_overlap_handling.py +++ b/test/detection/test_overlap_filter.py @@ -4,7 +4,7 @@ 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 250/274] 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 | 481 ++++++++++++++++------------ 1 file changed, 269 insertions(+), 212 deletions(-) diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index d9c21f15d..7642d965f 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], + [False, False, False, False], + [False, False, False, False], ), - ( - Vector( - Point(0, 0), - Point(0, 100), - ), + ( # Crossing beside - left side + Vector(Point(0, 0), Point(10, 0)), [ - [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], + [-20, 4, -24, 6], + [-20, 4 - 10, -24, 6 - 10], + [-20, 4, -24, 6], + [-20, 4 - 10, -24, 6 - 10], ], - [False, False, True, False, True, False, True, False], - [False, True, False, True, False, True, False, True], + [False, False, False, False], + [False, False, False, False], ), - ( - Vector( - Point(0, 0), - Point(-100, 0), - ), + ( # Move above + Vector(Point(0, 0), Point(10, 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], + [-4, 4, -2, 6], + [-4 + 20, 4, -2 + 20, 6], + [-4, 4, -2, 6], + [-4 + 20, 4, -2 + 20, 6], ], - [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), - ), + ( # Move below + 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 + [-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], ), - ( - Vector( - Point(-50, -50), - Point(-100, -150), - ), + ( # Move into line partway + Vector(Point(0, 0), Point(10, 0)), [ - [-30, -80, -20, -100], - [-150, -60, -110, -70], - [-10, -100, 20, -130], + [4, 4, 6, 6], + [4 + 5, 4, 6 + 5, 6], + [4, 4, 6, 6], + [4 + 5, 4, 6 + 5, 6], ], - [False, True, False], - [False, False, True], + [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 powerset(s): ( 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 powerset(s): ), ( 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 powerset(s): ( 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 251/274] Line zone tests - replace multi detection tests --- test/detection/test_line_counter.py | 116 ++++++++-------------------- 1 file changed, 31 insertions(+), 85 deletions(-) diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index 7642d965f..6804b20e6 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -2,7 +2,6 @@ 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,34 @@ 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]], + [[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]], ], - [[False, False], [False, False], [True, False]], - [[False, False], [True, False], [False, False]], [ Position.TOP_LEFT, Position.TOP_RIGHT, 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(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), - ], + ( # Both cross at the same time + Vector(Point(0, 0), Point(10, 0)), [ - (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 - 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]], ], [ Position.TOP_LEFT, @@ -454,58 +424,34 @@ def test_line_zone_one_detection( 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(-5, -5), - Point(-10, -15), - ), - [ - [[-3, -8, -2, -10], [10, 5, 12, 7]], - [[-10, -8, -2, -10], [10, 5, 12, 7]], - ], - [[False, False], [True, False]], - [[False, False], [False, False]], - [Position.TOP_LEFT], - 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 252/274] 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 6804b20e6..84977130c 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 253/274] 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 84977130c..3984be948 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 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 254/274] :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 319db4a38..e78589a68 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 255/274] :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 319db4a38..7e7fbac4d 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 256/274] 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 a1239d969..7699ade3f 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -928,6 +928,12 @@ def merge(cls, detections_list: List[Detections]) -> 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 @@ def merge(cls, detections_list: List[Detections]) -> 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 b36b6853f..d712bc658 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 af1d58762..237e5e085 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 f0f0a6b13..20c818e61 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 257/274] 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 7699ade3f..0abece68c 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -929,9 +929,7 @@ def merge(cls, detections_list: List[Detections]) -> 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 258/274] =?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 0c47f2b68..2acdf342d 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 259/274] 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 4198135de..a1642aeee 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -5,6 +5,7 @@ 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 @@ def trigger(self, detections: Detections) -> Tuple[np.ndarray, np.ndarray]: ] ) - 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 @@ def trigger(self, detections: Detections) -> Tuple[np.ndarray, np.ndarray]: 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 @@ def trigger(self, detections: Detections) -> Tuple[np.ndarray, np.ndarray]: 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 aac0d627c..671ac8ac1 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -6,6 +6,7 @@ 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 260/274] =?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 671ac8ac1..5c9447fb3 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 261/274] :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 4bdbe42b5..4d1b52aed 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 262/274] 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 a1642aeee..facb6aff0 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -161,7 +161,6 @@ def trigger(self, detections: Detections) -> Tuple[np.ndarray, np.ndarray]: 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 263/274] 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 0abece68c..100d21d00 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -878,6 +878,14 @@ def empty(cls) -> 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 @@ def merge(cls, detections_list: List[Detections]) -> 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 264/274] 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 100d21d00..7b2e8ba98 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -898,6 +898,10 @@ def merge(cls, detections_list: List[Detections]) -> 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 265/274] 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 a1239d969..9fbb357d4 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -1222,7 +1222,9 @@ def with_nmm( 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 0278fc004..5f61db0a5 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 129aa44b4..4448d8db3 100644 --- a/test/detection/test_lmm.py +++ b/test/detection/test_lmm.py @@ -76,7 +76,27 @@ 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 266/274] 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 237e5e085..300d6dfe3 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 267/274] 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 3984be948..66118e971 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 268/274] =?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 9fbb357d4..53d576fb3 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -1222,7 +1222,7 @@ def with_nmm( 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 269/274] 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 640d462e0..59d4176d5 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 270/274] changelog updated --- docs/changelog.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 47d160aa6..024edd1a8 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 271/274] update docs headers --- mkdocs.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 19d6a4fd4..96728971d 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 272/274] 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 dd963edb0..6f87958f8 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 958f2a748..4d912cae3 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 8d13822cf..22e9c0a88 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 a82ce5dfb..a24cee578 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 369746a3e..ea98c8683 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 a9a4405e2..52e3174b4 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 94de6c618..05d5faada 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 47f700619..cb44441f1 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 8f170d353..8e39136a8 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 b65cd954b..5ae92dc98 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 273/274] 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 f39a8e030..38a1bba8b 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 274/274] 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 5d5d08bc5..7a5d3e573 100644 --- a/docs/detection/tools/inference_slicer.md +++ b/docs/detection/tools/inference_slicer.md @@ -1,5 +1,6 @@ --- comments: true +status: new --- # InferenceSlicer