From 823dc56057a6e0ae0887f4ce58eaab04ba9ee30a Mon Sep 17 00:00:00 2001 From: Seongjun Choi Date: Tue, 23 Jan 2024 15:14:56 +0900 Subject: [PATCH 01/44] 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 39e129e1..9a284775 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -330,6 +330,9 @@ class 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 02/44] 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 03/44] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto?= =?UTF-8?q?=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/detection/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 9a284775..6b2d01f6 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -331,7 +331,7 @@ class 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 d37a5f90a494d338f04e33c59e9bba3c735628e3 Mon Sep 17 00:00:00 2001 From: tc360950 Date: Sun, 5 May 2024 17:16:05 +0200 Subject: [PATCH 04/44] 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 55db6293..ce3bbbbf 100644 --- a/supervision/tracker/byte_tracker/core.py +++ b/supervision/tracker/byte_tracker/core.py @@ -487,7 +487,7 @@ class ByteTrack: 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 aaf5df2f6aa41edc89e8988ba7dfab86e339b55b Mon Sep 17 00:00:00 2001 From: Manzar Iqbal Malik Date: Mon, 6 May 2024 12:15:38 +0100 Subject: [PATCH 05/44] 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 98587999..05b2e15f 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 06/44] 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 638e6b75..6783ae25 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 39d42c60..81056800 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 @@ class 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 07/44] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto?= =?UTF-8?q?=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/geometry/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/supervision/geometry/core.py b/supervision/geometry/core.py index 81056800..a884a9da 100644 --- a/supervision/geometry/core.py +++ b/supervision/geometry/core.py @@ -124,5 +124,5 @@ class Rect: 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 95c243c058062370d63df001bbd30127873a27a6 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Mon, 6 May 2024 17:40:33 +0200 Subject: [PATCH 08/44] 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 6783ae25..1980cbe1 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 09/44] 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 1980cbe1..6783ae25 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 10/44] 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 abacdc21..76116fc7 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 bb526514..43760686 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -55,6 +55,8 @@ from supervision.detection.utils import ( 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.draw.utils import ( ) 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 3eeba5b4..2b088cce 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 4b43765c..01059ac6 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 @@ class EdgeAnnotator(BaseKeyPointAnnotator): ) 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 11/44] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto?= =?UTF-8?q?=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/__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 43760686..fc79a81f 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -46,17 +46,17 @@ from supervision.detection.utils import ( 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.geometry.utils import get_polygon_center 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 01059ac6..56eafdf9 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 @@ class VertexLabelAnnotator: @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 @@ class VertexLabelAnnotator: ) 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 @@ class VertexLabelAnnotator: 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 @@ class VertexLabelAnnotator: 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 12/44] =?UTF-8?q?chore(pre=5Fcommit):=20=E2=AC=86=20pre=5F?= =?UTF-8?q?commit=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 c0903f3a..b0f62897 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 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 13/44] :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 178fda0d..0eb454a8 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 d7b6af84e6ed892ac27c535c623c83bae77cf9d7 Mon Sep 17 00:00:00 2001 From: Piotr Skalski Date: Tue, 7 May 2024 12:25:41 +0200 Subject: [PATCH 14/44] 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 05b2e15f..0a366a94 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 15/44] Tests for the new merge function --- test/detection/test_core.py | 169 +++++++++++++++++++++++++++--------- test/test_utils.py | 18 ++-- 2 files changed, 142 insertions(+), 45 deletions(-) diff --git a/test/detection/test_core.py b/test/detection/test_core.py index f3b739e8..8912f4a6 100644 --- a/test/detection/test_core.py +++ b/test/detection/test_core.py @@ -30,7 +30,81 @@ DETECTIONS = Detections( ) -@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 @@ DETECTIONS = Detections( DoesNotRaise(), ), # take only first detection by index slice (1, 3) (DETECTIONS, 10, None, pytest.raises(IndexError)), # index out of range - (DETECTIONS, [0, 2, 10], None, pytest.raises(IndexError)), # index out of range + (DETECTIONS, [0, 2, 10], None, pytest.raises( + IndexError)), # index out of range (DETECTIONS, np.array([0, 2, 10]), None, pytest.raises(IndexError)), ( DETECTIONS, @@ -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 + ( + [TEST_DET_NONE], + 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, + DoesNotRaise(), + ), # Fields with same keys + + # Fields and empty ( [ - mock_detections(xyxy=[[10, 10, 20, 20]]), - mock_detections(xyxy=np.empty((0, 4), dtype=np.float32)), + TEST_DET_1, + Detections.empty() ], - mock_detections(xyxy=[[10, 10, 20, 20]]), + TEST_DET_1, DoesNotRaise(), - ), # single detection with xyxy field + empty detection + ), # single detection with fields ( [ - mock_detections(xyxy=[[10, 10, 20, 20]]), - mock_detections(xyxy=[[20, 20, 30, 30]]), + TEST_DET_1, + TEST_DET_ZERO_LENGTH, ], - mock_detections(xyxy=[[10, 10, 20, 20], [20, 20, 30, 30]]), + TEST_DET_1, DoesNotRaise(), - ), # two detections with xyxy field + ), # Single detection and empty-array fields ( [ - mock_detections(xyxy=[[10, 10, 20, 20]], class_id=[0]), - mock_detections(xyxy=[[20, 20, 30, 30]]), + TEST_DET_1, + TEST_DET_NONE, ], - mock_detections(xyxy=[[10, 10, 20, 20], [20, 20, 30, 30]]), + 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) + ), # Non-empty detections with different fields + ( + [TEST_DET_1, TEST_DET_DIFFERENT_DATA], + None, pytest.raises(ValueError), - ), # detection with xyxy, class_id fields + detection with xyxy field - ( - [ - mock_detections(xyxy=[[10, 10, 20, 20]], class_id=[0]), - mock_detections(xyxy=[[20, 20, 30, 30]], class_id=[1]), - ], - mock_detections(xyxy=[[10, 10, 20, 20], [20, 20, 30, 30]], class_id=[0, 1]), - DoesNotRaise(), - ), # two detections with xyxy, class_id fields - ( - [ - mock_detections(xyxy=[[10, 10, 20, 20]], data={"test": [1]}), - mock_detections(xyxy=[[20, 20, 30, 30]], data={"test": [2]}), - ], - mock_detections( - xyxy=[[10, 10, 20, 20], [20, 20, 30, 30]], data={"test": [1, 2]} - ), - DoesNotRaise(), - ), # two detections with xyxy, data fields - ], + ), # 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 b676cb54..37be31d3 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -21,11 +21,14 @@ def mock_detections( 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 mock_keypoints( 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 16/44] 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 1900954d..e9baef7a 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -831,9 +831,10 @@ class 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 @@ class 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 3eeba5b4..8fac9f90 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 8912f4a6..8f156238 100644 --- a/test/detection/test_core.py +++ b/test/detection/test_core.py @@ -42,7 +42,7 @@ TEST_DET_1 = mock_detections( 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 @@ TEST_DET_2 = mock_detections( 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 @@ TEST_DET_1_2 = mock_detections( 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 @@ TEST_DET_ZERO_LENGTH = mock_detections( data={ "some_key": [], "other_key": [], - } + }, ) TEST_DET_NONE = mock_detections( xyxy=np.empty((0, 4), dtype=np.float32), @@ -87,10 +92,7 @@ TEST_DET_DIFFERENT_FIELDS = mock_detections( 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 @@ TEST_DET_DIFFERENT_DATA = mock_detections( tracker_id=[9], data={ "never_seen_key": [9], - } + }, ) -@ pytest.mark.parametrize( +@pytest.mark.parametrize( "detections, index, expected_result, exception", [ ( @@ -189,8 +191,7 @@ TEST_DET_DIFFERENT_DATA = mock_detections( DoesNotRaise(), ), # take only first detection by index slice (1, 3) (DETECTIONS, 10, None, pytest.raises(IndexError)), # index out of range - (DETECTIONS, [0, 2, 10], None, pytest.raises( - IndexError)), # index out of range + (DETECTIONS, [0, 2, 10], None, pytest.raises(IndexError)), # index out of range (DETECTIONS, np.array([0, 2, 10]), None, pytest.raises(IndexError)), ( DETECTIONS, @@ -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 1c4a1d34..0a48be36 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 37be31d3..b676cb54 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -21,14 +21,11 @@ def mock_detections( 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 mock_keypoints( 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 45563dfddff9b590398fbb5555179b0c1d2d5a42 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 8 May 2024 14:07:07 +0300 Subject: [PATCH 17/44] 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 7157723f..99a2bb2d 100644 --- a/supervision/detection/tools/inference_slicer.py +++ b/supervision/detection/tools/inference_slicer.py @@ -4,20 +4,29 @@ from typing import Callable, Optional, Tuple 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 @@ class InferenceSlicer: """ 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 8fac9f90..c3b99b49 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 1ebbe3a8d104dfc20cd52a5270cd983581861e74 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Thu, 9 May 2024 09:17:37 +0300 Subject: [PATCH 18/44] 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 8fac9f90..2f180405 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 19/44] 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 2f180405..254a01e8 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 0a48be36..22d0a430 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 20/44] 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 254a01e8..28bde1b6 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -55,7 +55,8 @@ def box_iou_batch(boxes_true: np.ndarray, boxes_detection: np.ndarray) -> np.nda top_left = np.maximum(boxes_true[:, None, :2], boxes_detection[:, :2]) bottom_right = np.minimum(boxes_true[:, None, 2:], boxes_detection[:, 2:]) - area_inter = np.prod(np.clip(bottom_right - top_left, a_min=0, a_max=None), 2) + area_inter = np.prod( + np.clip(bottom_right - top_left, a_min=0, a_max=None), 2) return area_inter / (area_true[:, None] + area_detection - area_inter) @@ -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 calculate_masks_centroids(masks: np.ndarray) -> np.ndarray: return np.tensordot(masks, indices, axes=axis) aggregation_axis = ([1, 2], [0, 1]) - centroid_x = sum_over_mask(horizontal_indices, aggregation_axis) / total_pixels - centroid_y = sum_over_mask(vertical_indices, aggregation_axis) / total_pixels + centroid_x = sum_over_mask( + horizontal_indices, aggregation_axis) / total_pixels + centroid_y = sum_over_mask( + vertical_indices, aggregation_axis) / total_pixels return np.column_stack((centroid_x, centroid_y)).astype(int) @@ -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 21/44] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto?= =?UTF-8?q?=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/detection/utils.py | 44 ++++++++++++---------------------- 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 28bde1b6..805f6bfb 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -55,8 +55,7 @@ def box_iou_batch(boxes_true: np.ndarray, boxes_detection: np.ndarray) -> np.nda top_left = np.maximum(boxes_true[:, None, :2], boxes_detection[:, :2]) bottom_right = np.minimum(boxes_true[:, None, 2:], boxes_detection[:, 2:]) - area_inter = np.prod( - np.clip(bottom_right - top_left, a_min=0, a_max=None), 2) + area_inter = np.prod(np.clip(bottom_right - top_left, a_min=0, a_max=None), 2) return area_inter / (area_true[:, None] + area_detection - area_inter) @@ -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 calculate_masks_centroids(masks: np.ndarray) -> np.ndarray: return np.tensordot(masks, indices, axes=axis) aggregation_axis = ([1, 2], [0, 1]) - centroid_x = sum_over_mask( - horizontal_indices, aggregation_axis) / total_pixels - centroid_y = sum_over_mask( - vertical_indices, aggregation_axis) / total_pixels + centroid_x = sum_over_mask(horizontal_indices, aggregation_axis) / total_pixels + centroid_y = sum_over_mask(vertical_indices, aggregation_axis) / total_pixels return np.column_stack((centroid_x, centroid_y)).astype(int) @@ -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 22/44] 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 805f6bfb..512489ca 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 8f156238..4dd6e467 100644 --- a/test/detection/test_core.py +++ b/test/detection/test_core.py @@ -33,73 +33,75 @@ DETECTIONS = Detections( # 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 23/44] :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 0eb454a8..5dc8c24c 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 2364abf481bfea634d3066d7c31f561170097e00 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Fri, 10 May 2024 15:08:18 +0200 Subject: [PATCH 24/44] 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 512489ca..c2a8e6dd 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 22d0a430..0fb72a28 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 25/44] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto?= =?UTF-8?q?=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/detection/test_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 0fb72a28..097c5c6e 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 26/44] 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 c2a8e6dd..6b378042 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 4dd6e467..12f3de28 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 27/44] 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 509c05b9..a98e91f3 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 28/44] 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 99a2bb2d..bd4a2425 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 05f29d27..d5b908b0 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 29/44] 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 e2d02328..b0447a64 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 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 30/44] :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 5dc8c24c..71b6addb 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 31/44] 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 abacdc21..02fb813e 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 bb526514..71fba5fe 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -52,6 +52,7 @@ from supervision.detection.utils import ( 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 bd4a2425..a951664c 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 @@ from supervision.utils.image import crop_image 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 d5b908b0..d20cadea 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 82ab6c57cb8cb2f863e27c551e31e310e18fdb38 Mon Sep 17 00:00:00 2001 From: LinasKo Date: Mon, 13 May 2024 14:14:42 +0300 Subject: [PATCH 32/44] 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 a951664c..82551434 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 @@ from supervision.utils.image import crop_image 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 @@ class InferenceSlicer: """ 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 d20cadea..49b91795 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 33/44] 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 56eafdf9..348a42fb 100644 --- a/supervision/keypoint/annotators.py +++ b/supervision/keypoint/annotators.py @@ -48,8 +48,8 @@ class VertexAnnotator(BaseKeyPointAnnotator): 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 @@ class EdgeAnnotator(BaseKeyPointAnnotator): 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 @@ class EdgeAnnotator(BaseKeyPointAnnotator): 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 @@ class VertexLabelAnnotator: 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 @@ class VertexLabelAnnotator: 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: + """ + 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. + + 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. + + Returns: + The annotated image, matching the type of `scene` (`numpy.ndarray` + or `PIL.Image.Image`) + + 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 - N, K, _ = key_points.xy.shape - - if N == 0: + skeletons_count, points_count, _ = key_points.xy.shape + if skeletons_count == 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) - + 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 @@ class VertexLabelAnnotator: ) 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 34/44] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto?= =?UTF-8?q?=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/keypoint/annotators.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/supervision/keypoint/annotators.py b/supervision/keypoint/annotators.py index 348a42fb..888e7e2a 100644 --- a/supervision/keypoint/annotators.py +++ b/supervision/keypoint/annotators.py @@ -262,12 +262,12 @@ class VertexLabelAnnotator: 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 @@ class VertexLabelAnnotator: @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 @@ class VertexLabelAnnotator: 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 35/44] 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 b5f998bc..e7adeb09 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 348a42fb..e8787273 100644 --- a/supervision/keypoint/annotators.py +++ b/supervision/keypoint/annotators.py @@ -65,7 +65,10 @@ class VertexAnnotator(BaseKeyPointAnnotator): 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 @@ class EdgeAnnotator(BaseKeyPointAnnotator): 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 @@ class VertexLabelAnnotator: 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 36/44] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto?= =?UTF-8?q?=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 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 e7adeb09..30a970ec 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 37/44] 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 e2d02328..42105480 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 38/44] 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 42105480..e4da7dde 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 39/44] 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 b5723aa4..e6ff1fcf 100644 --- a/supervision/keypoint/annotators.py +++ b/supervision/keypoint/annotators.py @@ -259,6 +259,49 @@ class VertexLabelAnnotator: ![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 40/44] 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 e4da7dde..683b6616 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 41/44] 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 683b6616..84affd96 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 49b91795..a6ba041a 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -55,7 +55,8 @@ def box_iou_batch(boxes_true: np.ndarray, boxes_detection: np.ndarray) -> np.nda top_left = np.maximum(boxes_true[:, None, :2], boxes_detection[:, :2]) bottom_right = np.minimum(boxes_true[:, None, 2:], boxes_detection[:, 2:]) - area_inter = np.prod(np.clip(bottom_right - top_left, a_min=0, a_max=None), 2) + area_inter = np.prod( + np.clip(bottom_right - top_left, a_min=0, a_max=None), 2) return area_inter / (area_true[:, None] + area_detection - area_inter) @@ -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 calculate_masks_centroids(masks: np.ndarray) -> np.ndarray: return np.tensordot(masks, indices, axes=axis) aggregation_axis = ([1, 2], [0, 1]) - centroid_x = sum_over_mask(horizontal_indices, aggregation_axis) / total_pixels - centroid_y = sum_over_mask(vertical_indices, aggregation_axis) / total_pixels + centroid_x = sum_over_mask( + horizontal_indices, aggregation_axis) / total_pixels + centroid_y = sum_over_mask( + vertical_indices, aggregation_axis) / total_pixels return np.column_stack((centroid_x, centroid_y)).astype(int) @@ -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 42/44] 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 6f338ffc..b5af2964 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -55,8 +55,7 @@ def box_iou_batch(boxes_true: np.ndarray, boxes_detection: np.ndarray) -> np.nda top_left = np.maximum(boxes_true[:, None, :2], boxes_detection[:, :2]) bottom_right = np.minimum(boxes_true[:, None, 2:], boxes_detection[:, 2:]) - area_inter = np.prod( - np.clip(bottom_right - top_left, a_min=0, a_max=None), 2) + area_inter = np.prod(np.clip(bottom_right - top_left, a_min=0, a_max=None), 2) return area_inter / (area_true[:, None] + area_detection - area_inter) @@ -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 calculate_masks_centroids(masks: np.ndarray) -> np.ndarray: return np.tensordot(masks, indices, axes=axis) aggregation_axis = ([1, 2], [0, 1]) - centroid_x = sum_over_mask( - horizontal_indices, aggregation_axis) / total_pixels - centroid_y = sum_over_mask( - vertical_indices, aggregation_axis) / total_pixels + centroid_x = sum_over_mask(horizontal_indices, aggregation_axis) / total_pixels + centroid_y = sum_over_mask(vertical_indices, aggregation_axis) / total_pixels return np.column_stack((centroid_x, centroid_y)).astype(int) @@ -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 43/44] 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 a98e91f3..8ceacbff 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 b5af2964..9232089e 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 44/44] =?UTF-8?q?chore(pre=5Fcommit):=20=E2=AC=86=20pre=5F?= =?UTF-8?q?commit=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 b0f62897..9465c2af 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]