From 823dc56057a6e0ae0887f4ce58eaab04ba9ee30a Mon Sep 17 00:00:00 2001 From: Seongjun Choi Date: Tue, 23 Jan 2024 15:14:56 +0900 Subject: [PATCH 001/136] 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 002/136] 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 003/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/detection/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 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 1a7309e7c9f7d0d9c699e1c29c7adb77fa3c5b7e Mon Sep 17 00:00:00 2001 From: Jeslin P James Date: Tue, 9 Apr 2024 16:51:41 +0530 Subject: [PATCH 004/136] RichLabelAnnotator class added --- supervision/annotators/core.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index 9f6cdb36..d6ae599e 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -3,6 +3,7 @@ from typing import List, Optional, Tuple, Union import cv2 import numpy as np +from PIL import ImageFont from supervision.annotators.base import BaseAnnotator, ImageType from supervision.annotators.utils import ColorLookup, Trace, resolve_color @@ -1147,6 +1148,27 @@ class LabelAnnotator: ) return scene +class RichLabelAnnotator: + + def __init__( + self, + color: Union[Color, ColorPalette] = ColorPalette.DEFAULT, + text_color: Color = Color.WHITE, + font_path: str = "/content/Arial Unicode Font.ttf", + font_size: int = 14, + text_padding: int = 10, + text_position: Position = Position.TOP_LEFT, + color_lookup: ColorLookup = ColorLookup.CLASS, + border_radius: int = 0, + ): + self.color = color + self.text_color = text_color + self.font = ImageFont.truetype(font_path, font_size) + self.text_padding = text_padding + self.text_anchor = text_position + self.color_lookup = color_lookup + self.border_radius = border_radius + class BlurAnnotator(BaseAnnotator): """ From 2417b36006c48583a48883ba22d495f77b09273b Mon Sep 17 00:00:00 2001 From: Jeslin P James Date: Tue, 9 Apr 2024 20:39:53 +0530 Subject: [PATCH 005/136] resolve_text_background_xyxy() function added to RichLabelAnnotator --- supervision/annotators/core.py | 53 ++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index d6ae599e..a86d9f9e 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -1168,6 +1168,59 @@ class RichLabelAnnotator: self.text_anchor = text_position self.color_lookup = color_lookup self.border_radius = border_radius + + @staticmethod + def resolve_text_background_xyxy( + center_coordinates: Tuple[int, int], + text_wh: Tuple[int, int], + position: Position, + ) -> Tuple[int, int, int, int]: + center_x, center_y = center_coordinates + text_w, text_h = text_wh + + if position == Position.TOP_LEFT: + return center_x, center_y - text_h, center_x + text_w, center_y + elif position == Position.TOP_RIGHT: + return center_x - text_w, center_y - text_h, center_x, center_y + elif position == Position.TOP_CENTER: + return ( + center_x - text_w // 2, + center_y - text_h, + center_x + text_w // 2, + center_y, + ) + elif position == Position.CENTER or position == Position.CENTER_OF_MASS: + return ( + center_x - text_w // 2, + center_y - text_h // 2, + center_x + text_w // 2, + center_y + text_h // 2, + ) + elif position == Position.BOTTOM_LEFT: + return center_x, center_y, center_x + text_w, center_y + text_h + elif position == Position.BOTTOM_RIGHT: + return center_x - text_w, center_y, center_x, center_y + text_h + elif position == Position.BOTTOM_CENTER: + return ( + center_x - text_w // 2, + center_y, + center_x + text_w // 2, + center_y + text_h, + ) + elif position == Position.CENTER_LEFT: + return ( + center_x - text_w, + center_y - text_h // 2, + center_x, + center_y + text_h // 2, + ) + elif position == Position.CENTER_RIGHT: + return ( + center_x, + center_y - text_h // 2, + center_x + text_w, + center_y + text_h // 2, + ) class BlurAnnotator(BaseAnnotator): From 9e906c78f5f1566e62c79a940c9d16e0c7cd6b3e Mon Sep 17 00:00:00 2001 From: Jeslin P James Date: Tue, 9 Apr 2024 22:14:36 +0530 Subject: [PATCH 006/136] annotate function added in RichLabelAnnotator --- supervision/annotators/core.py | 72 +++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index a86d9f9e..f69421cc 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -3,7 +3,7 @@ from typing import List, Optional, Tuple, Union import cv2 import numpy as np -from PIL import ImageFont +from PIL import ImageFont, ImageDraw from supervision.annotators.base import BaseAnnotator, ImageType from supervision.annotators.utils import ColorLookup, Trace, resolve_color @@ -1221,6 +1221,76 @@ class RichLabelAnnotator: center_x + text_w, center_y + text_h // 2, ) + + + def annotate( + self, + scene: ImageType, + detections: Detections, + labels: List[str] = None, + custom_color_lookup: Optional[np.ndarray] = None, + ) -> ImageType: + draw = ImageDraw.Draw(scene) + anchors_coordinates = detections.get_anchors_coordinates( + anchor=self.text_anchor + ).astype(int) + if labels is not None and len(labels) != len(detections): + raise ValueError( + f"The number of labels provided ({len(labels)}) does not match the " + f"number of detections ({len(detections)}). Each detection should have " + f"a corresponding label. This discrepancy can occur if the labels and " + f"detections are not aligned or if an incorrect number of labels has " + f"been provided. Please ensure that the labels array has the same " + f"length as the Detections object." + ) + for detection_idx, center_coordinates in enumerate(anchors_coordinates): + color = resolve_color( + color=self.color, + detections=detections, + detection_idx=detection_idx, + color_lookup=( + self.color_lookup + if custom_color_lookup is None + else custom_color_lookup + ), + ) + if labels is not None: + text = labels[detection_idx] + elif detections[CLASS_NAME_DATA_FIELD] is not None: + text = detections[CLASS_NAME_DATA_FIELD][detection_idx] + elif detections.class_id is not None: + text = str(detections.class_id[detection_idx]) + else: + text = str(detection_idx) + + left, top, right, bottom = draw.textbbox((0, 0), text, font=self.font) + text_width = right - left + text_height = bottom - top + text_w_padded = text_width + 2 * self.text_padding + text_h_padded = text_height + 2 * self.text_padding + text_background_xyxy = self.resolve_text_background_xyxy( + center_coordinates=tuple(center_coordinates), + text_wh=(text_w_padded, text_h_padded), + position=self.text_anchor, + ) + + text_x = text_background_xyxy[0] + self.text_padding - left + text_y = text_background_xyxy[1] + self.text_padding - top + + draw.rounded_rectangle( + text_background_xyxy, + radius=self.border_radius, + fill=color.as_rgb(), + outline=None, + ) + draw.text( + xy=(text_x, text_y), + text=text, + font=self.font, + fill=self.text_color.as_rgb(), + ) + + return scene class BlurAnnotator(BaseAnnotator): From 8329d155e4dc62f5da2a4b7b3df1936694765a91 Mon Sep 17 00:00:00 2001 From: Jeslin P James Date: Sat, 13 Apr 2024 17:27:27 +0530 Subject: [PATCH 007/136] Use default font when font_path isnt specified --- supervision/annotators/core.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index dfdf9990..a10335a7 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -3,7 +3,7 @@ from typing import List, Optional, Tuple, Union import cv2 import numpy as np -from PIL import ImageFont, ImageDraw +from PIL import ImageFont, ImageDraw, Image from supervision.annotators.base import BaseAnnotator, ImageType from supervision.annotators.utils import ColorLookup, Trace, resolve_color @@ -1154,7 +1154,7 @@ class RichLabelAnnotator: self, color: Union[Color, ColorPalette] = ColorPalette.DEFAULT, text_color: Color = Color.WHITE, - font_path: str = "/content/Arial Unicode Font.ttf", + font_path: str = None, font_size: int = 14, text_padding: int = 10, text_position: Position = Position.TOP_LEFT, @@ -1163,12 +1163,19 @@ class RichLabelAnnotator: ): self.color = color self.text_color = text_color - self.font = ImageFont.truetype(font_path, font_size) self.text_padding = text_padding self.text_anchor = text_position self.color_lookup = color_lookup self.border_radius = border_radius - + if font_path is not None: + try: + self.font = ImageFont.truetype(font_path, font_size) + except OSError: + print(f"Font path '{font_path}' not found. Using a system font.") + self.font = ImageFont.load_default(size=font_size) + else: + self.font = ImageFont.load_default(size=font_size) + @staticmethod def resolve_text_background_xyxy( center_coordinates: Tuple[int, int], @@ -1221,8 +1228,7 @@ class RichLabelAnnotator: center_x + text_w, center_y + text_h // 2, ) - - + def annotate( self, scene: ImageType, @@ -1230,6 +1236,8 @@ class RichLabelAnnotator: labels: List[str] = None, custom_color_lookup: Optional[np.ndarray] = None, ) -> ImageType: + if isinstance(scene, np.ndarray): + scene = Image.fromarray(cv2.cvtColor(scene, cv2.COLOR_BGR2RGB)) draw = ImageDraw.Draw(scene) anchors_coordinates = detections.get_anchors_coordinates( anchor=self.text_anchor @@ -1292,7 +1300,6 @@ class RichLabelAnnotator: return scene - class BlurAnnotator(BaseAnnotator): """ A class for blurring regions in an image using provided detections. From 8255bcb1cab76923300dfff429dfa300754db97e Mon Sep 17 00:00:00 2001 From: Jeslin P James Date: Sat, 13 Apr 2024 17:43:52 +0530 Subject: [PATCH 008/136] Moved resolve_text_background_xyxy() function into utils.py --- supervision/annotators/core.py | 112 +------------------------------- supervision/annotators/utils.py | 54 ++++++++++++++- 2 files changed, 56 insertions(+), 110 deletions(-) diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index a10335a7..61eb9fd3 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -6,7 +6,7 @@ import numpy as np from PIL import ImageFont, ImageDraw, Image from supervision.annotators.base import BaseAnnotator, ImageType -from supervision.annotators.utils import ColorLookup, Trace, resolve_color +from supervision.annotators.utils import ColorLookup, Trace, resolve_color, resolve_text_background_xyxy from supervision.config import CLASS_NAME_DATA_FIELD, ORIENTED_BOX_COORDINATES from supervision.detection.core import Detections from supervision.detection.utils import clip_boxes, mask_to_polygons @@ -937,59 +937,6 @@ class LabelAnnotator: self.text_anchor: Position = text_position self.color_lookup: ColorLookup = color_lookup - @staticmethod - def resolve_text_background_xyxy( - center_coordinates: Tuple[int, int], - text_wh: Tuple[int, int], - position: Position, - ) -> Tuple[int, int, int, int]: - center_x, center_y = center_coordinates - text_w, text_h = text_wh - - if position == Position.TOP_LEFT: - return center_x, center_y - text_h, center_x + text_w, center_y - elif position == Position.TOP_RIGHT: - return center_x - text_w, center_y - text_h, center_x, center_y - elif position == Position.TOP_CENTER: - return ( - center_x - text_w // 2, - center_y - text_h, - center_x + text_w // 2, - center_y, - ) - elif position == Position.CENTER or position == Position.CENTER_OF_MASS: - return ( - center_x - text_w // 2, - center_y - text_h // 2, - center_x + text_w // 2, - center_y + text_h // 2, - ) - elif position == Position.BOTTOM_LEFT: - return center_x, center_y, center_x + text_w, center_y + text_h - elif position == Position.BOTTOM_RIGHT: - return center_x - text_w, center_y, center_x, center_y + text_h - elif position == Position.BOTTOM_CENTER: - return ( - center_x - text_w // 2, - center_y, - center_x + text_w // 2, - center_y + text_h, - ) - elif position == Position.CENTER_LEFT: - return ( - center_x - text_w, - center_y - text_h // 2, - center_x, - center_y + text_h // 2, - ) - elif position == Position.CENTER_RIGHT: - return ( - center_x, - center_y - text_h // 2, - center_x + text_w, - center_y + text_h // 2, - ) - @convert_for_annotation_method def annotate( self, @@ -1079,7 +1026,7 @@ class LabelAnnotator: )[0] text_w_padded = text_w + 2 * self.text_padding text_h_padded = text_h + 2 * self.text_padding - text_background_xyxy = self.resolve_text_background_xyxy( + text_background_xyxy = resolve_text_background_xyxy( center_coordinates=tuple(center_coordinates), text_wh=(text_w_padded, text_h_padded), position=self.text_anchor, @@ -1176,59 +1123,6 @@ class RichLabelAnnotator: else: self.font = ImageFont.load_default(size=font_size) - @staticmethod - def resolve_text_background_xyxy( - center_coordinates: Tuple[int, int], - text_wh: Tuple[int, int], - position: Position, - ) -> Tuple[int, int, int, int]: - center_x, center_y = center_coordinates - text_w, text_h = text_wh - - if position == Position.TOP_LEFT: - return center_x, center_y - text_h, center_x + text_w, center_y - elif position == Position.TOP_RIGHT: - return center_x - text_w, center_y - text_h, center_x, center_y - elif position == Position.TOP_CENTER: - return ( - center_x - text_w // 2, - center_y - text_h, - center_x + text_w // 2, - center_y, - ) - elif position == Position.CENTER or position == Position.CENTER_OF_MASS: - return ( - center_x - text_w // 2, - center_y - text_h // 2, - center_x + text_w // 2, - center_y + text_h // 2, - ) - elif position == Position.BOTTOM_LEFT: - return center_x, center_y, center_x + text_w, center_y + text_h - elif position == Position.BOTTOM_RIGHT: - return center_x - text_w, center_y, center_x, center_y + text_h - elif position == Position.BOTTOM_CENTER: - return ( - center_x - text_w // 2, - center_y, - center_x + text_w // 2, - center_y + text_h, - ) - elif position == Position.CENTER_LEFT: - return ( - center_x - text_w, - center_y - text_h // 2, - center_x, - center_y + text_h // 2, - ) - elif position == Position.CENTER_RIGHT: - return ( - center_x, - center_y - text_h // 2, - center_x + text_w, - center_y + text_h // 2, - ) - def annotate( self, scene: ImageType, @@ -1276,7 +1170,7 @@ class RichLabelAnnotator: text_height = bottom - top text_w_padded = text_width + 2 * self.text_padding text_h_padded = text_height + 2 * self.text_padding - text_background_xyxy = self.resolve_text_background_xyxy( + text_background_xyxy = resolve_text_background_xyxy( center_coordinates=tuple(center_coordinates), text_wh=(text_w_padded, text_h_padded), position=self.text_anchor, diff --git a/supervision/annotators/utils.py b/supervision/annotators/utils.py index e206c8cb..bdc17975 100644 --- a/supervision/annotators/utils.py +++ b/supervision/annotators/utils.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Optional, Union +from typing import Optional, Union, Tuple import numpy as np @@ -62,6 +62,58 @@ def resolve_color_idx( ) return detections.tracker_id[detection_idx] +def resolve_text_background_xyxy( + center_coordinates: Tuple[int, int], + text_wh: Tuple[int, int], + position: Position, +) -> Tuple[int, int, int, int]: + center_x, center_y = center_coordinates + text_w, text_h = text_wh + + if position == Position.TOP_LEFT: + return center_x, center_y - text_h, center_x + text_w, center_y + elif position == Position.TOP_RIGHT: + return center_x - text_w, center_y - text_h, center_x, center_y + elif position == Position.TOP_CENTER: + return ( + center_x - text_w // 2, + center_y - text_h, + center_x + text_w // 2, + center_y, + ) + elif position == Position.CENTER or position == Position.CENTER_OF_MASS: + return ( + center_x - text_w // 2, + center_y - text_h // 2, + center_x + text_w // 2, + center_y + text_h // 2, + ) + elif position == Position.BOTTOM_LEFT: + return center_x, center_y, center_x + text_w, center_y + text_h + elif position == Position.BOTTOM_RIGHT: + return center_x - text_w, center_y, center_x, center_y + text_h + elif position == Position.BOTTOM_CENTER: + return ( + center_x - text_w // 2, + center_y, + center_x + text_w // 2, + center_y + text_h, + ) + elif position == Position.CENTER_LEFT: + return ( + center_x - text_w, + center_y - text_h // 2, + center_x, + center_y + text_h // 2, + ) + elif position == Position.CENTER_RIGHT: + return ( + center_x, + center_y - text_h // 2, + center_x + text_w, + center_y + text_h // 2, + ) + def get_color_by_index(color: Union[Color, ColorPalette], idx: int) -> Color: if isinstance(color, ColorPalette): From 8bb8ce03167a68f0cda1a9089026306719a2d0aa Mon Sep 17 00:00:00 2001 From: Jeslin P James Date: Sat, 13 Apr 2024 18:03:08 +0530 Subject: [PATCH 009/136] changed error message and formatted code --- supervision/annotators/core.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index 61eb9fd3..baf04574 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -6,7 +6,12 @@ import numpy as np from PIL import ImageFont, ImageDraw, Image from supervision.annotators.base import BaseAnnotator, ImageType -from supervision.annotators.utils import ColorLookup, Trace, resolve_color, resolve_text_background_xyxy +from supervision.annotators.utils import ( + ColorLookup, + Trace, + resolve_color, + resolve_text_background_xyxy, +) from supervision.config import CLASS_NAME_DATA_FIELD, ORIENTED_BOX_COORDINATES from supervision.detection.core import Detections from supervision.detection.utils import clip_boxes, mask_to_polygons @@ -1004,9 +1009,11 @@ class LabelAnnotator: color=self.color, detections=detections, detection_idx=detection_idx, - color_lookup=self.color_lookup - if custom_color_lookup is None - else custom_color_lookup, + color_lookup=( + self.color_lookup + if custom_color_lookup is None + else custom_color_lookup + ), ) if labels is not None: @@ -1095,6 +1102,7 @@ class LabelAnnotator: ) return scene + class RichLabelAnnotator: def __init__( @@ -1102,7 +1110,7 @@ class RichLabelAnnotator: color: Union[Color, ColorPalette] = ColorPalette.DEFAULT, text_color: Color = Color.WHITE, font_path: str = None, - font_size: int = 14, + font_size: int = 10, text_padding: int = 10, text_position: Position = Position.TOP_LEFT, color_lookup: ColorLookup = ColorLookup.CLASS, @@ -1118,7 +1126,7 @@ class RichLabelAnnotator: try: self.font = ImageFont.truetype(font_path, font_size) except OSError: - print(f"Font path '{font_path}' not found. Using a system font.") + print(f"Font path '{font_path}' not found. Using PIL's default font.") self.font = ImageFont.load_default(size=font_size) else: self.font = ImageFont.load_default(size=font_size) From cb220479ebaf63464f758550b58e7eb321ed0f87 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 13 Apr 2024 13:16:13 +0000 Subject: [PATCH 010/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/annotators/core.py | 4 ++-- supervision/annotators/utils.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index baf04574..4a8d0b66 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -3,7 +3,7 @@ from typing import List, Optional, Tuple, Union import cv2 import numpy as np -from PIL import ImageFont, ImageDraw, Image +from PIL import Image, ImageDraw, ImageFont from supervision.annotators.base import BaseAnnotator, ImageType from supervision.annotators.utils import ( @@ -1104,7 +1104,6 @@ class LabelAnnotator: class RichLabelAnnotator: - def __init__( self, color: Union[Color, ColorPalette] = ColorPalette.DEFAULT, @@ -1202,6 +1201,7 @@ class RichLabelAnnotator: return scene + class BlurAnnotator(BaseAnnotator): """ A class for blurring regions in an image using provided detections. diff --git a/supervision/annotators/utils.py b/supervision/annotators/utils.py index bdc17975..f0de9fa5 100644 --- a/supervision/annotators/utils.py +++ b/supervision/annotators/utils.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Optional, Union, Tuple +from typing import Optional, Tuple, Union import numpy as np @@ -62,6 +62,7 @@ def resolve_color_idx( ) return detections.tracker_id[detection_idx] + def resolve_text_background_xyxy( center_coordinates: Tuple[int, int], text_wh: Tuple[int, int], From df4a6f857cc138e5e9b680f223be0ef6527a40d6 Mon Sep 17 00:00:00 2001 From: Jeslin P James Date: Sat, 13 Apr 2024 18:58:18 +0530 Subject: [PATCH 011/136] added docs for RichLabelAnnotator --- supervision/annotators/core.py | 60 ++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index 4a8d0b66..f2b552cd 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -1104,6 +1104,11 @@ class LabelAnnotator: class RichLabelAnnotator: + """ + A class for annotating labels on an image using provided detections, + with support for Unicode characters by using a custom font. + """ + def __init__( self, color: Union[Color, ColorPalette] = ColorPalette.DEFAULT, @@ -1115,6 +1120,22 @@ class RichLabelAnnotator: color_lookup: ColorLookup = ColorLookup.CLASS, border_radius: int = 0, ): + """ + Args: + color (Union[Color, ColorPalette]): The color or color palette to use for + annotating the text background. + text_color (Color): The color to use for the text. + font_path (str): Path to the font file (e.g., ".ttf" or ".otf") to use for rendering text. + If `None`, the default PIL font will be used. + font_size (int): Font size for the text. + text_padding (int): Padding around the text within its background box. + text_position (Position): Position of the text relative to the detection. + Possible values are defined in the `Position` enum. + color_lookup (ColorLookup): Strategy for mapping colors to annotations. + Options are `INDEX`, `CLASS`, `TRACK`. + border_radius (int): The radius to apply round edges. If the selected + value is higher than the lower dimension, width or height, is clipped. + """ self.color = color self.text_color = text_color self.text_padding = text_padding @@ -1137,6 +1158,45 @@ class RichLabelAnnotator: labels: List[str] = None, custom_color_lookup: Optional[np.ndarray] = None, ) -> ImageType: + """ + Annotates the given scene with labels based on the provided + detections, with support for Unicode characters. + + Args: + scene (ImageType): The image where labels will be drawn. + `ImageType` is a flexible type, accepting either `numpy.ndarray` + or `PIL.Image.Image`. + detections (Detections): Object detections to annotate. + labels (List[str]): Optional. Custom labels for each detection. + custom_color_lookup (Optional[np.ndarray]): Custom color lookup array. + Allows to override the default color mapping strategy. + + Returns: + The annotated image, matching the type of `scene` (`numpy.ndarray` + or `PIL.Image.Image`) + + Example: + ```python + import supervision as sv + + image = ... + detections = sv.Detections(...) + + labels = [ + f"{class_name} {confidence:.2f}" + for class_name, confidence + in zip(detections['class_name'], detections.confidence) + ] + + label_annotator = sv.RichLabelAnnotator(font_path="path/to/font.ttf") + annotated_frame = label_annotator.annotate( + scene=image.copy(), + detections=detections, + labels=labels + ) + ``` + + """ if isinstance(scene, np.ndarray): scene = Image.fromarray(cv2.cvtColor(scene, cv2.COLOR_BGR2RGB)) draw = ImageDraw.Draw(scene) From e30d4a9abbd97253312e3f95f9664b7c16e74c16 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Thu, 2 May 2024 12:49:31 +0200 Subject: [PATCH 012/136] add test for generating annotation with mask --- test/dataset/formats/test_coco.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 62d1b75b..40d686db 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -1,5 +1,5 @@ from contextlib import ExitStack as DoesNotRaise -from typing import Dict, List, Tuple +from typing import Dict, List, Tuple, Union import numpy as np import pytest @@ -20,6 +20,8 @@ def mock_cock_coco_annotation( category_id: int = 0, bbox: Tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0), area: float = 0.0, + segmentation: List[list] = None, + iscrowd: bool = False, ) -> dict: return { "id": annotation_id, @@ -27,7 +29,8 @@ def mock_cock_coco_annotation( "category_id": category_id, "bbox": list(bbox), "area": area, - "iscrowd": 0, + "segmentation": segmentation, + "iscrowd": int(iscrowd), } @@ -226,6 +229,21 @@ def test_group_coco_annotations_by_image_id( ), DoesNotRaise(), ), # two image annotations + ( + [ + mock_cock_coco_annotation( + category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, segmentation = [[0,0, 0,9, 9,9, 9,0]], + ) + ], + (20, 20), + True, + Detections( + xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32), + class_id=np.array([0], dtype=int), + mask = np.array([np.fromfunction(lambda i, j:np.bitwise_and(i<10, j<10), (20, 20), dtype=int)]) + ), + DoesNotRaise(), + ), # single image annotations with mask, segmentation mask outlines 10x10 square ], ) def test_coco_annotations_to_detections( From cc5e72dd5b9113abc922ef00f0133a5a6b61f67f Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Thu, 2 May 2024 22:08:08 +0200 Subject: [PATCH 013/136] test for RLE format --- test/dataset/formats/test_coco.py | 33 +++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 40d686db..5055a859 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -229,10 +229,10 @@ def test_group_coco_annotations_by_image_id( ), DoesNotRaise(), ), # two image annotations - ( + ( [ mock_cock_coco_annotation( - category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, segmentation = [[0,0, 0,9, 9,9, 9,0]], + category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, segmentation = [[0,0, 4,0, 4,5, 9,5, 9,9, 0,9]], ) ], (20, 20), @@ -240,10 +240,35 @@ def test_group_coco_annotations_by_image_id( Detections( xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32), class_id=np.array([0], dtype=int), - mask = np.array([np.fromfunction(lambda i, j:np.bitwise_and(i<10, j<10), (20, 20), dtype=int)]) + mask = np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((1,20,20)) ), DoesNotRaise(), - ), # single image annotations with mask, segmentation mask outlines 10x10 square + ), # single image annotations with mask, segmentation mask in L-like shape, like below: + # 1 0 0 0 + # 1 1 0 0 + # 0 0 0 0 + # 0 0 0 0 + ( + [ + mock_cock_coco_annotation( + category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, + segmentation = {'size':[20,20], 'counts':[0, 5, 20, 5, 40, 5, 60, 5, 80, 5, 100, 10, 120, 10, 140, 10, 160, 10, 180, 10]}, iscrowd = True + ) + ], + (20, 20), + True, + Detections( + xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32), + class_id=np.array([0], dtype=int), + mask = np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((1,20,20)) + ), + DoesNotRaise(), + ), # single image annotations with mask, RLE encoded segmentation mask in L-like shape, like below: + # 1 0 0 0 + # 1 1 0 0 + # 0 0 0 0 + # 0 0 0 0 + ], ) def test_coco_annotations_to_detections( From 7f114cba4c13d621ce86177db1fdbbfa2d4575a2 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Fri, 3 May 2024 00:24:46 +0200 Subject: [PATCH 014/136] RLE decoding --- supervision/dataset/formats/coco.py | 33 +++++++++++++++++++++++++---- supervision/detection/utils.py | 10 +++++++++ test/dataset/formats/test_coco.py | 2 +- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 4f8679d5..4105f86e 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -11,7 +11,7 @@ from supervision.dataset.utils import ( map_detections_class_id, ) from supervision.detection.core import Detections -from supervision.detection.utils import polygon_to_mask +from supervision.detection.utils import polygon_to_mask, rle_to_mask from supervision.utils.file import read_json_file, save_json_file @@ -68,6 +68,26 @@ def _polygons_to_masks( dtype=bool, ) +def _rles_to_masks( + rles: List[np.ndarray], resolution_wh: Tuple[int, int] +) -> np.ndarray: + return np.array( + [ + rle_to_mask(rle=rle, resolution_wh=resolution_wh) + for rle in rles + ], + dtype=bool, + ) + +def _concatenate_annotation_masks(mask_polygon, mask_rle): + if mask_polygon.ndim == 3 and mask_rle.ndim == 3: + return np.concatenate((mask_polygon, mask_rle)) + elif mask_polygon.ndim == 3: + return mask_polygon + elif mask_rle.ndim == 3: + return mask_rle + else: + None def coco_annotations_to_detections( image_annotations: List[dict], resolution_wh: Tuple[int, int], with_masks: bool @@ -87,11 +107,16 @@ def coco_annotations_to_detections( np.reshape( np.asarray(image_annotation["segmentation"], dtype=np.int32), (-1, 2) ) - for image_annotation in image_annotations + for image_annotation in image_annotations if not image_annotation["iscrowd"] ] - mask = _polygons_to_masks(polygons=polygons, resolution_wh=resolution_wh) + mask_polygon = _polygons_to_masks(polygons=polygons, resolution_wh=resolution_wh) + + rles = [np.array(image_annotation["segmentation"]["counts"]) + for image_annotation in image_annotations if image_annotation["iscrowd"]] + mask_rle = _rles_to_masks(rles = rles, resolution_wh = resolution_wh) + return Detections( - class_id=np.asarray(class_ids, dtype=int), xyxy=xyxy, mask=mask + class_id=np.asarray(class_ids, dtype=int), xyxy=xyxy, mask=_concatenate_annotation_masks(mask_polygon=mask_polygon, mask_rle=mask_rle) ) return Detections(xyxy=xyxy, class_id=np.asarray(class_ids, dtype=int)) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 3eeba5b4..2b6f7d63 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -766,3 +766,13 @@ def get_data_item( raise TypeError(f"Unsupported data type for key '{key}': {type(value)}") return subset_data + + +def rle_to_mask(rle: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray: + width, height = resolution_wh + + zero_one_values = np.zeros_like(rle) + zero_one_values[1::2]=1 + + decoded_rle = np.repeat(zero_one_values, rle) + return decoded_rle.reshape((height,width), order='F') diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 5055a859..7254c9a9 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -252,7 +252,7 @@ def test_group_coco_annotations_by_image_id( [ mock_cock_coco_annotation( category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, - segmentation = {'size':[20,20], 'counts':[0, 5, 20, 5, 40, 5, 60, 5, 80, 5, 100, 10, 120, 10, 140, 10, 160, 10, 180, 10]}, iscrowd = True + segmentation = {'size':[20,20], 'counts':[0, 10, 10, 10, 10, 10, 10, 10, 10, 10, 15, 5, 15, 5, 15, 5, 15, 5, 15, 5, 210]}, iscrowd = True ) ], (20, 20), From 6913ecf54d59ba270c2efa69586c15e9ac5729fb Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Fri, 3 May 2024 01:19:01 +0200 Subject: [PATCH 015/136] 2 annotations with segmentation, one polygon one RLE --- test/dataset/formats/test_coco.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 7254c9a9..ef627e89 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -263,12 +263,41 @@ def test_group_coco_annotations_by_image_id( mask = np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((1,20,20)) ), DoesNotRaise(), - ), # single image annotations with mask, RLE encoded segmentation mask in L-like shape, like below: + ), # single image annotations with mask, RLE segmentation mask in L-like shape, like below: # 1 0 0 0 # 1 1 0 0 # 0 0 0 0 # 0 0 0 0 + ( + [ + mock_cock_coco_annotation( + category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, segmentation = [[0,0, 4,0, 4,5, 9,5, 9,9, 0,9]] + ), + mock_cock_coco_annotation( + category_id=0, bbox=(5, 0, 5, 5), area=5 * 5, + segmentation = {'size':[20,20], 'counts':[100, 5, 15, 5, 15, 5, 15, 5, 15, 5, 215]}, iscrowd = True + ), + ], + (20, 20), + True, + Detections( + xyxy=np.array( + [[0, 0, 10, 10], [5, 0, 10, 5]], dtype=np.float32 + ), + class_id=np.array([0, 0], dtype=int), + mask = np.array([ + np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((20,20)), + np.array([1 if j>4 and j<10 and i<5 else 0 for i in range(0,20) for j in range(0,20)]).reshape((20,20)) + ]) + ), + DoesNotRaise(), + ), # two image annotations with mask, one mask as polygon in in L-like shape, second as RLE in shape of square, like below (P = polygon, R = RLE): + # P R 0 0 + # P P 0 0 + # 0 0 0 0 + # 0 0 0 0 + ], ) def test_coco_annotations_to_detections( From 4a068ff8d7cf71f1e32e8e168e10f5539643a20d Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Fri, 3 May 2024 09:16:13 +0200 Subject: [PATCH 016/136] binary mask to RL encoding --- supervision/detection/utils.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 2b6f7d63..a915972a 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -1,4 +1,4 @@ -from itertools import chain +from itertools import chain, groupby from typing import Dict, List, Optional, Tuple, Union import cv2 @@ -776,3 +776,12 @@ def rle_to_mask(rle: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray: decoded_rle = np.repeat(zero_one_values, rle) return decoded_rle.reshape((height,width), order='F') + +def mask_to_rle(binary_mask: np.ndarray) -> list: + rle = [] + for _, group in groupby(binary_mask.ravel(order='F')): + rle.append(len(list(group))) + + if binary_mask[0][0] == 1: + rle = [0]+rle + return rle \ No newline at end of file From dc1ce02860e4c4ee7aab3522268391ed7353f652 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Fri, 3 May 2024 23:40:10 +0200 Subject: [PATCH 017/136] move rle encode decode functions to dataset/utils.py --- supervision/dataset/formats/coco.py | 14 +++++++++----- supervision/dataset/utils.py | 20 ++++++++++++++++++++ supervision/detection/utils.py | 21 +-------------------- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 4105f86e..d29f3ade 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -9,9 +9,11 @@ import numpy as np from supervision.dataset.utils import ( approximate_mask_with_polygons, map_detections_class_id, + rle_to_mask, + mask_to_rle, ) from supervision.detection.core import Detections -from supervision.detection.utils import polygon_to_mask, rle_to_mask +from supervision.detection.utils import polygon_to_mask from supervision.utils.file import read_json_file, save_json_file @@ -133,9 +135,9 @@ def detections_to_coco_annotations( coco_annotations = [] for xyxy, mask, _, class_id, _, _ in detections: box_width, box_height = xyxy[2] - xyxy[0], xyxy[3] - xyxy[1] - polygon = [] + segmentation = [] if mask is not None: - polygon = list( + segmentation = list( approximate_mask_with_polygons( mask=mask, min_image_area_percentage=min_image_area_percentage, @@ -143,14 +145,16 @@ def detections_to_coco_annotations( approximation_percentage=approximation_percentage, )[0].flatten() ) + # todo: flag for when to use RLE? + # segmentation = {"counts": mask_to_rle(binary_mask=mask), "size": list(mask.shape[:2])} coco_annotation = { "id": annotation_id, "image_id": image_id, "category_id": int(class_id), "bbox": [xyxy[0], xyxy[1], box_width, box_height], "area": box_width * box_height, - "segmentation": [polygon] if polygon else [], - "iscrowd": 0, + "segmentation": [segmentation] if segmentation else [], + "iscrowd": 0, ## todo: iscrowd depends on flag 1 if RLE 0 if polygon } coco_annotations.append(coco_annotation) annotation_id += 1 diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index 05ee3201..d46aa45b 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -3,6 +3,8 @@ import os import random from pathlib import Path from typing import Dict, List, Optional, Tuple, TypeVar +from itertools import groupby + import cv2 import numpy as np @@ -129,3 +131,21 @@ def train_test_split( split_index = int(len(data) * train_ratio) return data[:split_index], data[split_index:] + +def rle_to_mask(rle: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray: + width, height = resolution_wh + + zero_one_values = np.zeros_like(rle) + zero_one_values[1::2]=1 + + decoded_rle = np.repeat(zero_one_values, rle) + return decoded_rle.reshape((height,width), order='F') + +def mask_to_rle(binary_mask: np.ndarray) -> list: + rle = [] + for _, group in groupby(binary_mask.ravel(order='F')): + rle.append(len(list(group))) + + if binary_mask[0][0] == 1: + rle = [0]+rle + return rle diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index a915972a..3eeba5b4 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -1,4 +1,4 @@ -from itertools import chain, groupby +from itertools import chain from typing import Dict, List, Optional, Tuple, Union import cv2 @@ -766,22 +766,3 @@ def get_data_item( raise TypeError(f"Unsupported data type for key '{key}': {type(value)}") return subset_data - - -def rle_to_mask(rle: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray: - width, height = resolution_wh - - zero_one_values = np.zeros_like(rle) - zero_one_values[1::2]=1 - - decoded_rle = np.repeat(zero_one_values, rle) - return decoded_rle.reshape((height,width), order='F') - -def mask_to_rle(binary_mask: np.ndarray) -> list: - rle = [] - for _, group in groupby(binary_mask.ravel(order='F')): - rle.append(len(list(group))) - - if binary_mask[0][0] == 1: - rle = [0]+rle - return rle \ No newline at end of file From 869204d42debc065c512dd02a5e731307276d2df Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 5 May 2024 11:59:20 +0000 Subject: [PATCH 018/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/formats/coco.py | 33 +++++--- supervision/dataset/utils.py | 15 ++-- test/dataset/formats/test_coco.py | 122 +++++++++++++++++++++------- 3 files changed, 121 insertions(+), 49 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index d29f3ade..9b7e534a 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -10,7 +10,6 @@ from supervision.dataset.utils import ( approximate_mask_with_polygons, map_detections_class_id, rle_to_mask, - mask_to_rle, ) from supervision.detection.core import Detections from supervision.detection.utils import polygon_to_mask @@ -70,17 +69,16 @@ def _polygons_to_masks( dtype=bool, ) + def _rles_to_masks( - rles: List[np.ndarray], resolution_wh: Tuple[int, int] + rles: List[np.ndarray], resolution_wh: Tuple[int, int] ) -> np.ndarray: return np.array( - [ - rle_to_mask(rle=rle, resolution_wh=resolution_wh) - for rle in rles - ], + [rle_to_mask(rle=rle, resolution_wh=resolution_wh) for rle in rles], dtype=bool, ) + def _concatenate_annotation_masks(mask_polygon, mask_rle): if mask_polygon.ndim == 3 and mask_rle.ndim == 3: return np.concatenate((mask_polygon, mask_rle)) @@ -91,6 +89,7 @@ def _concatenate_annotation_masks(mask_polygon, mask_rle): else: None + def coco_annotations_to_detections( image_annotations: List[dict], resolution_wh: Tuple[int, int], with_masks: bool ) -> Detections: @@ -109,16 +108,26 @@ def coco_annotations_to_detections( np.reshape( np.asarray(image_annotation["segmentation"], dtype=np.int32), (-1, 2) ) - for image_annotation in image_annotations if not image_annotation["iscrowd"] + for image_annotation in image_annotations + if not image_annotation["iscrowd"] ] - mask_polygon = _polygons_to_masks(polygons=polygons, resolution_wh=resolution_wh) + mask_polygon = _polygons_to_masks( + polygons=polygons, resolution_wh=resolution_wh + ) - rles = [np.array(image_annotation["segmentation"]["counts"]) - for image_annotation in image_annotations if image_annotation["iscrowd"]] - mask_rle = _rles_to_masks(rles = rles, resolution_wh = resolution_wh) + rles = [ + np.array(image_annotation["segmentation"]["counts"]) + for image_annotation in image_annotations + if image_annotation["iscrowd"] + ] + mask_rle = _rles_to_masks(rles=rles, resolution_wh=resolution_wh) return Detections( - class_id=np.asarray(class_ids, dtype=int), xyxy=xyxy, mask=_concatenate_annotation_masks(mask_polygon=mask_polygon, mask_rle=mask_rle) + class_id=np.asarray(class_ids, dtype=int), + xyxy=xyxy, + mask=_concatenate_annotation_masks( + mask_polygon=mask_polygon, mask_rle=mask_rle + ), ) return Detections(xyxy=xyxy, class_id=np.asarray(class_ids, dtype=int)) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index d46aa45b..ef30aa48 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -1,10 +1,9 @@ import copy import os import random +from itertools import groupby from pathlib import Path from typing import Dict, List, Optional, Tuple, TypeVar -from itertools import groupby - import cv2 import numpy as np @@ -132,20 +131,22 @@ def train_test_split( split_index = int(len(data) * train_ratio) return data[:split_index], data[split_index:] + def rle_to_mask(rle: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray: width, height = resolution_wh - + zero_one_values = np.zeros_like(rle) - zero_one_values[1::2]=1 + zero_one_values[1::2] = 1 decoded_rle = np.repeat(zero_one_values, rle) - return decoded_rle.reshape((height,width), order='F') + return decoded_rle.reshape((height, width), order="F") + def mask_to_rle(binary_mask: np.ndarray) -> list: rle = [] - for _, group in groupby(binary_mask.ravel(order='F')): + for _, group in groupby(binary_mask.ravel(order="F")): rle.append(len(list(group))) if binary_mask[0][0] == 1: - rle = [0]+rle + rle = [0] + rle return rle diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index ef627e89..bfde080d 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -1,5 +1,5 @@ from contextlib import ExitStack as DoesNotRaise -from typing import Dict, List, Tuple, Union +from typing import Dict, List, Tuple import numpy as np import pytest @@ -232,7 +232,10 @@ def test_group_coco_annotations_by_image_id( ( [ mock_cock_coco_annotation( - category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, segmentation = [[0,0, 4,0, 4,5, 9,5, 9,9, 0,9]], + category_id=0, + bbox=(0, 0, 10, 10), + area=10 * 10, + segmentation=[[0, 0, 4, 0, 4, 5, 9, 5, 9, 9, 0, 9]], ) ], (20, 20), @@ -240,19 +243,53 @@ def test_group_coco_annotations_by_image_id( Detections( xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32), class_id=np.array([0], dtype=int), - mask = np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((1,20,20)) + mask=np.array( + [ + 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 + for i in range(0, 20) + for j in range(0, 20) + ] + ).reshape((1, 20, 20)), ), DoesNotRaise(), ), # single image annotations with mask, segmentation mask in L-like shape, like below: - # 1 0 0 0 - # 1 1 0 0 - # 0 0 0 0 - # 0 0 0 0 + # 1 0 0 0 + # 1 1 0 0 + # 0 0 0 0 + # 0 0 0 0 ( [ mock_cock_coco_annotation( - category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, - segmentation = {'size':[20,20], 'counts':[0, 10, 10, 10, 10, 10, 10, 10, 10, 10, 15, 5, 15, 5, 15, 5, 15, 5, 15, 5, 210]}, iscrowd = True + category_id=0, + bbox=(0, 0, 10, 10), + area=10 * 10, + segmentation={ + "size": [20, 20], + "counts": [ + 0, + 10, + 10, + 10, + 10, + 10, + 10, + 10, + 10, + 10, + 15, + 5, + 15, + 5, + 15, + 5, + 15, + 5, + 15, + 5, + 210, + ], + }, + iscrowd=True, ) ], (20, 20), @@ -260,44 +297,69 @@ def test_group_coco_annotations_by_image_id( Detections( xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32), class_id=np.array([0], dtype=int), - mask = np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((1,20,20)) + mask=np.array( + [ + 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 + for i in range(0, 20) + for j in range(0, 20) + ] + ).reshape((1, 20, 20)), ), DoesNotRaise(), ), # single image annotations with mask, RLE segmentation mask in L-like shape, like below: - # 1 0 0 0 - # 1 1 0 0 - # 0 0 0 0 - # 0 0 0 0 - + # 1 0 0 0 + # 1 1 0 0 + # 0 0 0 0 + # 0 0 0 0 ( [ mock_cock_coco_annotation( - category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, segmentation = [[0,0, 4,0, 4,5, 9,5, 9,9, 0,9]] + category_id=0, + bbox=(0, 0, 10, 10), + area=10 * 10, + segmentation=[[0, 0, 4, 0, 4, 5, 9, 5, 9, 9, 0, 9]], ), mock_cock_coco_annotation( - category_id=0, bbox=(5, 0, 5, 5), area=5 * 5, - segmentation = {'size':[20,20], 'counts':[100, 5, 15, 5, 15, 5, 15, 5, 15, 5, 215]}, iscrowd = True + category_id=0, + bbox=(5, 0, 5, 5), + area=5 * 5, + segmentation={ + "size": [20, 20], + "counts": [100, 5, 15, 5, 15, 5, 15, 5, 15, 5, 215], + }, + iscrowd=True, ), ], (20, 20), True, Detections( - xyxy=np.array( - [[0, 0, 10, 10], [5, 0, 10, 5]], dtype=np.float32 - ), + xyxy=np.array([[0, 0, 10, 10], [5, 0, 10, 5]], dtype=np.float32), class_id=np.array([0, 0], dtype=int), - mask = np.array([ - np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((20,20)), - np.array([1 if j>4 and j<10 and i<5 else 0 for i in range(0,20) for j in range(0,20)]).reshape((20,20)) - ]) + mask=np.array( + [ + np.array( + [ + 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 + for i in range(0, 20) + for j in range(0, 20) + ] + ).reshape((20, 20)), + np.array( + [ + 1 if j > 4 and j < 10 and i < 5 else 0 + for i in range(0, 20) + for j in range(0, 20) + ] + ).reshape((20, 20)), + ] + ), ), DoesNotRaise(), ), # two image annotations with mask, one mask as polygon in in L-like shape, second as RLE in shape of square, like below (P = polygon, R = RLE): - # P R 0 0 - # P P 0 0 - # 0 0 0 0 - # 0 0 0 0 - + # P R 0 0 + # P P 0 0 + # 0 0 0 0 + # 0 0 0 0 ], ) def test_coco_annotations_to_detections( From d37a5f90a494d338f04e33c59e9bba3c735628e3 Mon Sep 17 00:00:00 2001 From: tc360950 Date: Sun, 5 May 2024 17:16:05 +0200 Subject: [PATCH 019/136] 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 92d56eda150941d69bfe71a48b21e4edd024ad5c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 01:06:46 +0000 Subject: [PATCH 020/136] :arrow_up: Bump mike from 2.1.0 to 2.1.1 Bumps [mike](https://github.com/jimporter/mike) from 2.1.0 to 2.1.1. - [Release notes](https://github.com/jimporter/mike/releases) - [Changelog](https://github.com/jimporter/mike/blob/master/CHANGES.md) - [Commits](https://github.com/jimporter/mike/compare/v2.1.0...v2.1.1) --- updated-dependencies: - dependency-name: mike dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2497d802..e4ec3872 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2048,13 +2048,13 @@ files = [ [[package]] name = "mike" -version = "2.1.0" +version = "2.1.1" description = "Manage multiple versions of your MkDocs-powered documentation" optional = false python-versions = "*" files = [ - {file = "mike-2.1.0-py3-none-any.whl", hash = "sha256:b3885f9b9e31fc4b0d61de473750d38ac170a6b291585076effb51a806245608"}, - {file = "mike-2.1.0.tar.gz", hash = "sha256:f0b8e51cbfae1273d648ffb602a4ab3061e57972ca1cd6836df1c51c01a36eb5"}, + {file = "mike-2.1.1-py3-none-any.whl", hash = "sha256:0b1d01a397a423284593eeb1b5f3194e37169488f929b860c9bfe95c0d5efb79"}, + {file = "mike-2.1.1.tar.gz", hash = "sha256:f39ed39f3737da83ad0adc33e9f885092ed27f8c9e7ff0523add0480352a2c22"}, ] [package.dependencies] @@ -2064,6 +2064,7 @@ jinja2 = ">=2.7" mkdocs = ">=1.0" pyparsing = ">=3.0" pyyaml = ">=5.1" +pyyaml-env-tag = "*" verspec = "*" [package.extras] From f7d21acd3a40bfaab5729ba107915248edfdcbd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 01:09:12 +0000 Subject: [PATCH 021/136] :arrow_up: Bump mkdocs-material from 9.5.20 to 9.5.21 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.5.20 to 9.5.21. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.20...9.5.21) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2497d802..4608f00f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2197,13 +2197,13 @@ pygments = ">2.12.0" [[package]] name = "mkdocs-material" -version = "9.5.20" +version = "9.5.21" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.20-py3-none-any.whl", hash = "sha256:ad0094a7597bcb5d0cc3e8e543a10927c2581f7f647b9bb4861600f583180f9b"}, - {file = "mkdocs_material-9.5.20.tar.gz", hash = "sha256:986eef0250d22f70fb06ce0f4eac64cc92bd797a589ec3892ce31fad976fe3da"}, + {file = "mkdocs_material-9.5.21-py3-none-any.whl", hash = "sha256:210e1f179682cd4be17d5c641b2f4559574b9dea2f589c3f0e7c17c5bd1959bc"}, + {file = "mkdocs_material-9.5.21.tar.gz", hash = "sha256:049f82770f40559d3c2aa2259c562ea7257dbb4aaa9624323b5ef27b2d95a450"}, ] [package.dependencies] From ff52352b006ca0ffcee80792289aeb165ece9017 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 01:12:45 +0000 Subject: [PATCH 022/136] :arrow_up: Bump jupytext from 1.16.1 to 1.16.2 Bumps [jupytext](https://github.com/mwouts/jupytext) from 1.16.1 to 1.16.2. - [Release notes](https://github.com/mwouts/jupytext/releases) - [Changelog](https://github.com/mwouts/jupytext/blob/main/CHANGELOG.md) - [Commits](https://github.com/mwouts/jupytext/compare/v1.16.1...v1.16.2) --- updated-dependencies: - dependency-name: jupytext dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2497d802..87dc0781 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1648,13 +1648,13 @@ files = [ [[package]] name = "jupytext" -version = "1.16.1" +version = "1.16.2" description = "Jupyter notebooks as Markdown documents, Julia, Python or R scripts" optional = false python-versions = ">=3.8" files = [ - {file = "jupytext-1.16.1-py3-none-any.whl", hash = "sha256:796ec4f68ada663569e5d38d4ef03738a01284bfe21c943c485bc36433898bd0"}, - {file = "jupytext-1.16.1.tar.gz", hash = "sha256:68c7b68685e870e80e60fda8286fbd6269e9c74dc1df4316df6fe46eabc94c99"}, + {file = "jupytext-1.16.2-py3-none-any.whl", hash = "sha256:197a43fef31dca612b68b311e01b8abd54441c7e637810b16b6cb8f2ab66065e"}, + {file = "jupytext-1.16.2.tar.gz", hash = "sha256:8627dd9becbbebd79cc4a4ed4727d89d78e606b4b464eab72357b3b029023a14"}, ] [package.dependencies] @@ -1663,16 +1663,16 @@ mdit-py-plugins = "*" nbformat = "*" packaging = "*" pyyaml = "*" -toml = "*" +tomli = {version = "*", markers = "python_version < \"3.11\""} [package.extras] -dev = ["jupytext[test-cov,test-external]"] +dev = ["autopep8", "black", "flake8", "gitpython", "ipykernel", "isort", "jupyter-fs (<0.4.0)", "jupyter-server (!=2.11)", "nbconvert", "pre-commit", "pytest", "pytest-cov (>=2.6.1)", "pytest-randomly", "pytest-xdist", "sphinx-gallery (<0.8)"] docs = ["myst-parser", "sphinx", "sphinx-copybutton", "sphinx-rtd-theme"] test = ["pytest", "pytest-randomly", "pytest-xdist"] -test-cov = ["jupytext[test-integration]", "pytest-cov (>=2.6.1)"] -test-external = ["autopep8", "black", "flake8", "gitpython", "isort", "jupyter-fs (<0.4.0)", "jupytext[test-integration]", "pre-commit", "sphinx-gallery (<0.8)"] -test-functional = ["jupytext[test]"] -test-integration = ["ipykernel", "jupyter-server (!=2.11)", "jupytext[test-functional]", "nbconvert"] +test-cov = ["ipykernel", "jupyter-server (!=2.11)", "nbconvert", "pytest", "pytest-cov (>=2.6.1)", "pytest-randomly", "pytest-xdist"] +test-external = ["autopep8", "black", "flake8", "gitpython", "ipykernel", "isort", "jupyter-fs (<0.4.0)", "jupyter-server (!=2.11)", "nbconvert", "pre-commit", "pytest", "pytest-randomly", "pytest-xdist", "sphinx-gallery (<0.8)"] +test-functional = ["pytest", "pytest-randomly", "pytest-xdist"] +test-integration = ["ipykernel", "jupyter-server (!=2.11)", "nbconvert", "pytest", "pytest-randomly", "pytest-xdist"] test-ui = ["calysto-bash"] [[package]] @@ -3913,17 +3913,6 @@ webencodings = ">=0.4" doc = ["sphinx", "sphinx_rtd_theme"] test = ["flake8", "isort", "pytest"] -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] - [[package]] name = "tomli" version = "2.0.1" From aebbd01830b807def90a6a1d36636ccaa2ba94df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 01:15:08 +0000 Subject: [PATCH 023/136] :arrow_up: Bump mkdocstrings from 0.25.0 to 0.25.1 Bumps [mkdocstrings](https://github.com/mkdocstrings/mkdocstrings) from 0.25.0 to 0.25.1. - [Release notes](https://github.com/mkdocstrings/mkdocstrings/releases) - [Changelog](https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md) - [Commits](https://github.com/mkdocstrings/mkdocstrings/compare/0.25.0...0.25.1) --- updated-dependencies: - dependency-name: mkdocstrings dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2497d802..f209f594 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2239,13 +2239,13 @@ files = [ [[package]] name = "mkdocstrings" -version = "0.25.0" +version = "0.25.1" description = "Automatic documentation from sources, for MkDocs." optional = false python-versions = ">=3.8" files = [ - {file = "mkdocstrings-0.25.0-py3-none-any.whl", hash = "sha256:df1b63f26675fcde8c1b77e7ea996cd2f93220b148e06455428f676f5dc838f1"}, - {file = "mkdocstrings-0.25.0.tar.gz", hash = "sha256:066986b3fb5b9ef2d37c4417255a808f7e63b40ff8f67f6cab8054d903fbc91d"}, + {file = "mkdocstrings-0.25.1-py3-none-any.whl", hash = "sha256:da01fcc2670ad61888e8fe5b60afe9fee5781017d67431996832d63e887c2e51"}, + {file = "mkdocstrings-0.25.1.tar.gz", hash = "sha256:c3a2515f31577f311a9ee58d089e4c51fc6046dbd9e9b4c3de4c3194667fe9bf"}, ] [package.dependencies] From edd944fff887bfee404470a8efb408a84d10dfbc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 01:18:43 +0000 Subject: [PATCH 024/136] :arrow_up: Bump ruff from 0.4.2 to 0.4.3 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.2 to 0.4.3. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.4.2...v0.4.3) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2497d802..d01eeae7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3660,28 +3660,28 @@ files = [ [[package]] name = "ruff" -version = "0.4.2" +version = "0.4.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d14dc8953f8af7e003a485ef560bbefa5f8cc1ad994eebb5b12136049bbccc5"}, - {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:24016ed18db3dc9786af103ff49c03bdf408ea253f3cb9e3638f39ac9cf2d483"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2e06459042ac841ed510196c350ba35a9b24a643e23db60d79b2db92af0c2b"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3afabaf7ba8e9c485a14ad8f4122feff6b2b93cc53cd4dad2fd24ae35112d5c5"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:799eb468ea6bc54b95527143a4ceaf970d5aa3613050c6cff54c85fda3fde480"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ec4ba9436a51527fb6931a8839af4c36a5481f8c19e8f5e42c2f7ad3a49f5069"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a2243f8f434e487c2a010c7252150b1fdf019035130f41b77626f5655c9ca22"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8772130a063f3eebdf7095da00c0b9898bd1774c43b336272c3e98667d4fb8fa"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab165ef5d72392b4ebb85a8b0fbd321f69832a632e07a74794c0e598e7a8376"}, - {file = "ruff-0.4.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f32cadf44c2020e75e0c56c3408ed1d32c024766bd41aedef92aa3ca28eef68"}, - {file = "ruff-0.4.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:22e306bf15e09af45ca812bc42fa59b628646fa7c26072555f278994890bc7ac"}, - {file = "ruff-0.4.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82986bb77ad83a1719c90b9528a9dd663c9206f7c0ab69282af8223566a0c34e"}, - {file = "ruff-0.4.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:652e4ba553e421a6dc2a6d4868bc3b3881311702633eb3672f9f244ded8908cd"}, - {file = "ruff-0.4.2-py3-none-win32.whl", hash = "sha256:7891ee376770ac094da3ad40c116258a381b86c7352552788377c6eb16d784fe"}, - {file = "ruff-0.4.2-py3-none-win_amd64.whl", hash = "sha256:5ec481661fb2fd88a5d6cf1f83403d388ec90f9daaa36e40e2c003de66751798"}, - {file = "ruff-0.4.2-py3-none-win_arm64.whl", hash = "sha256:cbd1e87c71bca14792948c4ccb51ee61c3296e164019d2d484f3eaa2d360dfaf"}, - {file = "ruff-0.4.2.tar.gz", hash = "sha256:33bcc160aee2520664bc0859cfeaebc84bb7323becff3f303b8f1f2d81cb4edc"}, + {file = "ruff-0.4.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b70800c290f14ae6fcbb41bbe201cf62dfca024d124a1f373e76371a007454ce"}, + {file = "ruff-0.4.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08a0d6a22918ab2552ace96adeaca308833873a4d7d1d587bb1d37bae8728eb3"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba1f14df3c758dd7de5b55fbae7e1c8af238597961e5fb628f3de446c3c40c5"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:819fb06d535cc76dfddbfe8d3068ff602ddeb40e3eacbc90e0d1272bb8d97113"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bfc9e955e6dc6359eb6f82ea150c4f4e82b660e5b58d9a20a0e42ec3bb6342b"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:510a67d232d2ebe983fddea324dbf9d69b71c4d2dfeb8a862f4a127536dd4cfb"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9ff11cd9a092ee7680a56d21f302bdda14327772cd870d806610a3503d001f"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29efff25bf9ee685c2c8390563a5b5c006a3fee5230d28ea39f4f75f9d0b6f2f"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b00e0bcccf0fc8d7186ed21e311dffd19761cb632241a6e4fe4477cc80ef6e"}, + {file = "ruff-0.4.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262f5635e2c74d80b7507fbc2fac28fe0d4fef26373bbc62039526f7722bca1b"}, + {file = "ruff-0.4.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7363691198719c26459e08cc17c6a3dac6f592e9ea3d2fa772f4e561b5fe82a3"}, + {file = "ruff-0.4.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eeb039f8428fcb6725bb63cbae92ad67b0559e68b5d80f840f11914afd8ddf7f"}, + {file = "ruff-0.4.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:927b11c1e4d0727ce1a729eace61cee88a334623ec424c0b1c8fe3e5f9d3c865"}, + {file = "ruff-0.4.3-py3-none-win32.whl", hash = "sha256:25cacda2155778beb0d064e0ec5a3944dcca9c12715f7c4634fd9d93ac33fd30"}, + {file = "ruff-0.4.3-py3-none-win_amd64.whl", hash = "sha256:7a1c3a450bc6539ef00da6c819fb1b76b6b065dec585f91456e7c0d6a0bbc725"}, + {file = "ruff-0.4.3-py3-none-win_arm64.whl", hash = "sha256:71ca5f8ccf1121b95a59649482470c5601c60a416bf189d553955b0338e34614"}, + {file = "ruff-0.4.3.tar.gz", hash = "sha256:ff0a3ef2e3c4b6d133fbedcf9586abfbe38d076041f2dc18ffb2c7e0485d5a07"}, ] [[package]] From aaf5df2f6aa41edc89e8988ba7dfab86e339b55b Mon Sep 17 00:00:00 2001 From: Manzar Iqbal Malik Date: Mon, 6 May 2024 12:15:38 +0100 Subject: [PATCH 025/136] 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 026/136] 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 027/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/geometry/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/supervision/geometry/core.py b/supervision/geometry/core.py index 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 028/136] 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 029/136] 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 030/136] 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 031/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/__init__.py | 6 +-- supervision/keypoint/annotators.py | 73 +++++++++++++++--------------- 2 files changed, 40 insertions(+), 39 deletions(-) diff --git a/supervision/__init__.py b/supervision/__init__.py index 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 032/136] =?UTF-8?q?chore(pre=5Fcommit):=20=E2=AC=86=20pre?= =?UTF-8?q?=5Fcommit=20autoupdate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.4.2 → v0.4.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.2...v0.4.3) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 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 18a790e9ea4160d6ab1c2f9a08f62e14ab458784 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Mon, 6 May 2024 20:21:02 +0200 Subject: [PATCH 033/136] typing chanches and doc strings for rle_to_mask and mask_to_rle functions --- supervision/dataset/utils.py | 48 ++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index d46aa45b..1cd038ec 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -8,6 +8,7 @@ from itertools import groupby import cv2 import numpy as np +import numpy.typing as npt from supervision.detection.core import Detections from supervision.detection.utils import ( @@ -132,7 +133,24 @@ def train_test_split( split_index = int(len(data) * train_ratio) return data[:split_index], data[split_index:] -def rle_to_mask(rle: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray: +def rle_to_mask(rle: np.ndarray, resolution_wh: Tuple[int, int]) -> npt.NDArray[np.bool_]: + """ + Converts run-length encoding (RLE) to a binary mask. + + Args: + rle (np.ndarray): The 1D RLE array, the format used in the COCO dataset (column-wise encoding, + values of an array with even indices represent the number of pixels assigned as background, + values of an array with odd indices represent the number of pixels assigned as foreground object). + resolution_wh (Tuple[int, int]): The width (w) and height (h) of the desired binary mask resolution. + + Returns: + npt.NDArray[np.bool_]: The generated 2D Boolean mask of shape (h,w), where the foreground object + is marked with `True`'s and the rest is filled with `False`'s. + + Examples: + rle = [2, 2, 2], resolution_wh = [3, 2] -> mask = [[False, True, False], + [False, True, False]] + """ width, height = resolution_wh zero_one_values = np.zeros_like(rle) @@ -141,11 +159,31 @@ def rle_to_mask(rle: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray: decoded_rle = np.repeat(zero_one_values, rle) return decoded_rle.reshape((height,width), order='F') -def mask_to_rle(binary_mask: np.ndarray) -> list: +def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: + """ + Converts a binary mask into a run-length encoding (RLE). + + Args: + mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground object + and `False` indicates background. + + Returns: + List[int]: the run-length encoded mask. Values of a list with even indices represent the number of pixels assigned as background (`False`), + values of a list with odd indices represent the number of pixels assigned as foreground object (`True`). + + Examples: + mask = [[False, True, True], -> rle = [2, 4] + [False, True, True]] + + mask = [[True, True, True], -> rle = [0, 6] + [True, True, True]] + """ rle = [] - for _, group in groupby(binary_mask.ravel(order='F')): + + if mask[0][0] == 1: + rle = [0] + + for _, group in groupby(mask.ravel(order='F')): rle.append(len(list(group))) - if binary_mask[0][0] == 1: - rle = [0]+rle return rle From 3ade7f060a2db171f89494bc687c6255cca70f87 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Mon, 6 May 2024 22:42:26 +0200 Subject: [PATCH 034/136] fix order caused error with mask generation in coco_annotations_to_detections --- supervision/dataset/formats/coco.py | 48 +++++----------------- test/dataset/formats/test_coco.py | 63 +++++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 45 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index d29f3ade..c8f7cd04 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -59,37 +59,20 @@ def group_coco_annotations_by_image_id( return annotations -def _polygons_to_masks( - polygons: List[np.ndarray], resolution_wh: Tuple[int, int] -) -> np.ndarray: +def _annotations_to_mask(image_annotations: List[dict], resolution_wh: Tuple[int, int]): return np.array( [ - polygon_to_mask(polygon=polygon, resolution_wh=resolution_wh) - for polygon in polygons + rle_to_mask(rle=np.array(image_annotation["segmentation"]["counts"]), + resolution_wh=resolution_wh) + if image_annotation["iscrowd"] + else + polygon_to_mask(polygon= np.reshape(np.asarray(image_annotation["segmentation"], dtype=np.int32), (-1, 2)), + resolution_wh=resolution_wh) + for image_annotation in image_annotations ], dtype=bool, ) -def _rles_to_masks( - rles: List[np.ndarray], resolution_wh: Tuple[int, int] -) -> np.ndarray: - return np.array( - [ - rle_to_mask(rle=rle, resolution_wh=resolution_wh) - for rle in rles - ], - dtype=bool, - ) - -def _concatenate_annotation_masks(mask_polygon, mask_rle): - if mask_polygon.ndim == 3 and mask_rle.ndim == 3: - return np.concatenate((mask_polygon, mask_rle)) - elif mask_polygon.ndim == 3: - return mask_polygon - elif mask_rle.ndim == 3: - return mask_rle - else: - None def coco_annotations_to_detections( image_annotations: List[dict], resolution_wh: Tuple[int, int], with_masks: bool @@ -105,20 +88,9 @@ def coco_annotations_to_detections( xyxy[:, 2:4] += xyxy[:, 0:2] if with_masks: - polygons = [ - np.reshape( - np.asarray(image_annotation["segmentation"], dtype=np.int32), (-1, 2) - ) - for image_annotation in image_annotations if not image_annotation["iscrowd"] - ] - mask_polygon = _polygons_to_masks(polygons=polygons, resolution_wh=resolution_wh) - - rles = [np.array(image_annotation["segmentation"]["counts"]) - for image_annotation in image_annotations if image_annotation["iscrowd"]] - mask_rle = _rles_to_masks(rles = rles, resolution_wh = resolution_wh) - + mask = _annotations_to_mask(image_annotations, resolution_wh) return Detections( - class_id=np.asarray(class_ids, dtype=int), xyxy=xyxy, mask=_concatenate_annotation_masks(mask_polygon=mask_polygon, mask_rle=mask_rle) + class_id=np.asarray(class_ids, dtype=int), xyxy=xyxy, mask=mask ) return Detections(xyxy=xyxy, class_id=np.asarray(class_ids, dtype=int)) diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index ef627e89..610b9350 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -268,31 +268,80 @@ def test_group_coco_annotations_by_image_id( # 1 1 0 0 # 0 0 0 0 # 0 0 0 0 + ( + [ + mock_cock_coco_annotation( + category_id=0, + bbox=(0, 0, 10, 10), + area=10 * 10, + segmentation=[[0, 0, 4, 0, 4, 5, 9, 5, 9, 9, 0, 9]], + ), + mock_cock_coco_annotation( + category_id=0, + bbox=(5, 0, 5, 5), + area=5 * 5, + segmentation={ + "size": [20, 20], + "counts": [100, 5, 15, 5, 15, 5, 15, 5, 15, 5, 215], + }, + iscrowd=True, + ), + ], + (20, 20), + True, + Detections( + xyxy=np.array([[0, 0, 10, 10], [5, 0, 10, 5]], dtype=np.float32), + class_id=np.array([0, 0], dtype=int), + mask=np.array( + [ + np.array( + [ + 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 + for i in range(0, 20) + for j in range(0, 20) + ] + ).reshape((20, 20)), + np.array( + [ + 1 if j > 4 and j < 10 and i < 5 else 0 + for i in range(0, 20) + for j in range(0, 20) + ] + ).reshape((20, 20)), + ] + ), + ), + DoesNotRaise(), + ), # two image annotations with mask, one mask as polygon in in L-like shape, second as RLE in shape of square, like below (P = polygon, R = RLE): + # P R 0 0 + # P P 0 0 + # 0 0 0 0 + # 0 0 0 0 ( [ - mock_cock_coco_annotation( - category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, segmentation = [[0,0, 4,0, 4,5, 9,5, 9,9, 0,9]] - ), mock_cock_coco_annotation( category_id=0, bbox=(5, 0, 5, 5), area=5 * 5, segmentation = {'size':[20,20], 'counts':[100, 5, 15, 5, 15, 5, 15, 5, 15, 5, 215]}, iscrowd = True ), + mock_cock_coco_annotation( + category_id=1, bbox=(0, 0, 10, 10), area=10 * 10, segmentation = [[0,0, 4,0, 4,5, 9,5, 9,9, 0,9]] + ), ], (20, 20), True, Detections( xyxy=np.array( - [[0, 0, 10, 10], [5, 0, 10, 5]], dtype=np.float32 + [[5, 0, 10, 5], [0, 0, 10, 10]], dtype=np.float32 ), - class_id=np.array([0, 0], dtype=int), + class_id=np.array([0, 1], dtype=int), mask = np.array([ + np.array([1 if j>4 and j<10 and i<5 else 0 for i in range(0,20) for j in range(0,20)]).reshape((20,20)), np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((20,20)), - np.array([1 if j>4 and j<10 and i<5 else 0 for i in range(0,20) for j in range(0,20)]).reshape((20,20)) ]) ), DoesNotRaise(), - ), # two image annotations with mask, one mask as polygon in in L-like shape, second as RLE in shape of square, like below (P = polygon, R = RLE): + ), # two image annotations with mask, first mask as RLE in shape of square, second as polygon in in L-like shape, like below (P = polygon, R = RLE): # P R 0 0 # P P 0 0 # 0 0 0 0 From 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 035/136] :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 a941583bb0354de5b87ad1167e2b02198da11dd1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 21:07:49 +0000 Subject: [PATCH 036/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/formats/coco.py | 18 ++-- supervision/dataset/utils.py | 20 +++-- test/dataset/formats/test_coco.py | 122 +++++++++++++++++++++------- 3 files changed, 115 insertions(+), 45 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 34cb84ce..febeb8d6 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -61,12 +61,18 @@ def group_coco_annotations_by_image_id( def _annotations_to_mask(image_annotations: List[dict], resolution_wh: Tuple[int, int]): return np.array( [ - rle_to_mask(rle=np.array(image_annotation["segmentation"]["counts"]), - resolution_wh=resolution_wh) - if image_annotation["iscrowd"] - else - polygon_to_mask(polygon= np.reshape(np.asarray(image_annotation["segmentation"], dtype=np.int32), (-1, 2)), - resolution_wh=resolution_wh) + rle_to_mask( + rle=np.array(image_annotation["segmentation"]["counts"]), + resolution_wh=resolution_wh, + ) + if image_annotation["iscrowd"] + else polygon_to_mask( + polygon=np.reshape( + np.asarray(image_annotation["segmentation"], dtype=np.int32), + (-1, 2), + ), + resolution_wh=resolution_wh, + ) for image_annotation in image_annotations ], dtype=bool, diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index d1981bfe..de2511e5 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -133,22 +133,24 @@ def train_test_split( return data[:split_index], data[split_index:] -def rle_to_mask(rle: np.ndarray, resolution_wh: Tuple[int, int]) -> npt.NDArray[np.bool_]: +def rle_to_mask( + rle: np.ndarray, resolution_wh: Tuple[int, int] +) -> npt.NDArray[np.bool_]: """ Converts run-length encoding (RLE) to a binary mask. Args: - rle (np.ndarray): The 1D RLE array, the format used in the COCO dataset (column-wise encoding, + rle (np.ndarray): The 1D RLE array, the format used in the COCO dataset (column-wise encoding, values of an array with even indices represent the number of pixels assigned as background, values of an array with odd indices represent the number of pixels assigned as foreground object). resolution_wh (Tuple[int, int]): The width (w) and height (h) of the desired binary mask resolution. Returns: - npt.NDArray[np.bool_]: The generated 2D Boolean mask of shape (h,w), where the foreground object - is marked with `True`'s and the rest is filled with `False`'s. + npt.NDArray[np.bool_]: The generated 2D Boolean mask of shape (h,w), where the foreground object + is marked with `True`'s and the rest is filled with `False`'s. Examples: - rle = [2, 2, 2], resolution_wh = [3, 2] -> mask = [[False, True, False], + rle = [2, 2, 2], resolution_wh = [3, 2] -> mask = [[False, True, False], [False, True, False]] """ width, height = resolution_wh @@ -157,7 +159,7 @@ def rle_to_mask(rle: np.ndarray, resolution_wh: Tuple[int, int]) -> npt.NDArray[ zero_one_values[1::2] = 1 decoded_rle = np.repeat(zero_one_values, rle) - return decoded_rle.reshape((height,width), order="F") + return decoded_rle.reshape((height, width), order="F") def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: @@ -165,7 +167,7 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: Converts a binary mask into a run-length encoding (RLE). Args: - mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground object + mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground object and `False` indicates background. Returns: @@ -174,10 +176,10 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: Examples: mask = [[False, True, True], -> rle = [2, 4] - [False, True, True]] + [False, True, True]] mask = [[True, True, True], -> rle = [0, 6] - [True, True, True]] + [True, True, True]] """ rle = [] if mask[0][0] == 1: diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 610b9350..bb4a6e64 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -1,5 +1,5 @@ from contextlib import ExitStack as DoesNotRaise -from typing import Dict, List, Tuple, Union +from typing import Dict, List, Tuple import numpy as np import pytest @@ -232,7 +232,10 @@ def test_group_coco_annotations_by_image_id( ( [ mock_cock_coco_annotation( - category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, segmentation = [[0,0, 4,0, 4,5, 9,5, 9,9, 0,9]], + category_id=0, + bbox=(0, 0, 10, 10), + area=10 * 10, + segmentation=[[0, 0, 4, 0, 4, 5, 9, 5, 9, 9, 0, 9]], ) ], (20, 20), @@ -240,19 +243,53 @@ def test_group_coco_annotations_by_image_id( Detections( xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32), class_id=np.array([0], dtype=int), - mask = np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((1,20,20)) + mask=np.array( + [ + 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 + for i in range(0, 20) + for j in range(0, 20) + ] + ).reshape((1, 20, 20)), ), DoesNotRaise(), ), # single image annotations with mask, segmentation mask in L-like shape, like below: - # 1 0 0 0 - # 1 1 0 0 - # 0 0 0 0 - # 0 0 0 0 + # 1 0 0 0 + # 1 1 0 0 + # 0 0 0 0 + # 0 0 0 0 ( [ mock_cock_coco_annotation( - category_id=0, bbox=(0, 0, 10, 10), area=10 * 10, - segmentation = {'size':[20,20], 'counts':[0, 10, 10, 10, 10, 10, 10, 10, 10, 10, 15, 5, 15, 5, 15, 5, 15, 5, 15, 5, 210]}, iscrowd = True + category_id=0, + bbox=(0, 0, 10, 10), + area=10 * 10, + segmentation={ + "size": [20, 20], + "counts": [ + 0, + 10, + 10, + 10, + 10, + 10, + 10, + 10, + 10, + 10, + 15, + 5, + 15, + 5, + 15, + 5, + 15, + 5, + 15, + 5, + 210, + ], + }, + iscrowd=True, ) ], (20, 20), @@ -260,14 +297,20 @@ def test_group_coco_annotations_by_image_id( Detections( xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32), class_id=np.array([0], dtype=int), - mask = np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((1,20,20)) + mask=np.array( + [ + 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 + for i in range(0, 20) + for j in range(0, 20) + ] + ).reshape((1, 20, 20)), ), DoesNotRaise(), ), # single image annotations with mask, RLE segmentation mask in L-like shape, like below: - # 1 0 0 0 - # 1 1 0 0 - # 0 0 0 0 - # 0 0 0 0 + # 1 0 0 0 + # 1 1 0 0 + # 0 0 0 0 + # 0 0 0 0 ( [ mock_cock_coco_annotation( @@ -317,36 +360,55 @@ def test_group_coco_annotations_by_image_id( # P P 0 0 # 0 0 0 0 # 0 0 0 0 - ( [ mock_cock_coco_annotation( - category_id=0, bbox=(5, 0, 5, 5), area=5 * 5, - segmentation = {'size':[20,20], 'counts':[100, 5, 15, 5, 15, 5, 15, 5, 15, 5, 215]}, iscrowd = True + category_id=0, + bbox=(5, 0, 5, 5), + area=5 * 5, + segmentation={ + "size": [20, 20], + "counts": [100, 5, 15, 5, 15, 5, 15, 5, 15, 5, 215], + }, + iscrowd=True, ), mock_cock_coco_annotation( - category_id=1, bbox=(0, 0, 10, 10), area=10 * 10, segmentation = [[0,0, 4,0, 4,5, 9,5, 9,9, 0,9]] + category_id=1, + bbox=(0, 0, 10, 10), + area=10 * 10, + segmentation=[[0, 0, 4, 0, 4, 5, 9, 5, 9, 9, 0, 9]], ), ], (20, 20), True, Detections( - xyxy=np.array( - [[5, 0, 10, 5], [0, 0, 10, 10]], dtype=np.float32 - ), + xyxy=np.array([[5, 0, 10, 5], [0, 0, 10, 10]], dtype=np.float32), class_id=np.array([0, 1], dtype=int), - mask = np.array([ - np.array([1 if j>4 and j<10 and i<5 else 0 for i in range(0,20) for j in range(0,20)]).reshape((20,20)), - np.array([0 if i>=10 or j>=10 or (i<5 and j >=5) else 1 for i in range(0,20) for j in range(0,20)]).reshape((20,20)), - ]) + mask=np.array( + [ + np.array( + [ + 1 if j > 4 and j < 10 and i < 5 else 0 + for i in range(0, 20) + for j in range(0, 20) + ] + ).reshape((20, 20)), + np.array( + [ + 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 + for i in range(0, 20) + for j in range(0, 20) + ] + ).reshape((20, 20)), + ] + ), ), DoesNotRaise(), ), # two image annotations with mask, first mask as RLE in shape of square, second as polygon in in L-like shape, like below (P = polygon, R = RLE): - # P R 0 0 - # P P 0 0 - # 0 0 0 0 - # 0 0 0 0 - + # P R 0 0 + # P P 0 0 + # 0 0 0 0 + # 0 0 0 0 ], ) def test_coco_annotations_to_detections( From 88a43cc795c840b57beb0d159b1bcbd9a9bc1259 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Tue, 7 May 2024 00:22:24 +0200 Subject: [PATCH 037/136] unit tests for rle_to_mask and mask_to_rle functions --- supervision/dataset/utils.py | 4 +- test/dataset/test_utils.py | 98 ++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index de2511e5..53593a35 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -134,13 +134,13 @@ def train_test_split( def rle_to_mask( - rle: np.ndarray, resolution_wh: Tuple[int, int] + rle: npt.NDArray[np.int_], resolution_wh: Tuple[int, int] ) -> npt.NDArray[np.bool_]: """ Converts run-length encoding (RLE) to a binary mask. Args: - rle (np.ndarray): The 1D RLE array, the format used in the COCO dataset (column-wise encoding, + rle (npt.NDArray[np.int_]): The 1D RLE array, the format used in the COCO dataset (column-wise encoding, values of an array with even indices represent the number of pixels assigned as background, values of an array with odd indices represent the number of pixels assigned as foreground object). resolution_wh (Tuple[int, int]): The width (w) and height (h) of the desired binary mask resolution. diff --git a/test/dataset/test_utils.py b/test/dataset/test_utils.py index 5ca96ca5..9cefa438 100644 --- a/test/dataset/test_utils.py +++ b/test/dataset/test_utils.py @@ -2,6 +2,8 @@ from contextlib import ExitStack as DoesNotRaise from test.test_utils import mock_detections from typing import Dict, List, Optional, Tuple, TypeVar +import numpy as np +import numpy.typing as npt import pytest from supervision import Detections @@ -10,6 +12,8 @@ from supervision.dataset.utils import ( map_detections_class_id, merge_class_lists, train_test_split, + mask_to_rle, + rle_to_mask, ) T = TypeVar("T") @@ -229,3 +233,97 @@ def test_map_detections_class_id( source_to_target_mapping=source_to_target_mapping, detections=detections ) assert result == expected_result + + +@pytest.mark.parametrize( + "mask, expected_rle, exception", + [ + ( + np.zeros((3,3)).astype(bool), + [9], + DoesNotRaise(), + ), # mask with background only (mask with only False values) + ( + np.ones((3,3)).astype(bool), + [0, 9], + DoesNotRaise(), + ), # mask with foreground only (mask with only True values) + ( + np.array( + [[0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 0, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]] + ).astype(bool), + [6, 3, 2, 1, 1, 1, 2, 3, 6], + DoesNotRaise(), + ), # mask where foreground object has hole + ( + np.array( + [[1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1]] + ).astype(bool), + [0, 5, 5, 5, 5, 5], + DoesNotRaise(), + ), # mask where foreground consists of 3 separate components + ], +) +def test_mask_to_rle_convertion( + mask: npt.NDArray[np.bool_], expected_rle: List[int], exception: Exception +) -> None: + with exception: + result = mask_to_rle(mask=mask) + assert result == expected_rle + + +@pytest.mark.parametrize( + "rle, resolution_wh, expected_mask, exception", + [ + ( + [9], + [3, 3], + np.zeros((3,3)).astype(bool), + DoesNotRaise(), + ), # mask with background only (mask with only False values) + ( + [0, 9], + [3, 3], + np.ones((3,3)).astype(bool), + DoesNotRaise(), + ), # mask with foreground only (mask with only True values) + ( + [6, 3, 2, 1, 1, 1, 2, 3, 6], + [5, 5], + np.array( + [[0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 0, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]] + ).astype(bool), + DoesNotRaise(), + ), # mask where foreground object has hole + ( + [0, 5, 5, 5, 5, 5], + [5, 5], + np.array( + [[1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1]] + ).astype(bool), + DoesNotRaise(), + ), # mask where foreground consists of 3 separate components + ], +) +def test_rle_to_mask_convertion( + rle: npt.NDArray[np.int_], resolution_wh: Tuple[int, int],expected_mask: npt.NDArray[np.bool_], exception: Exception +) -> None: + with exception: + result = rle_to_mask(rle=rle, resolution_wh=resolution_wh) + assert np.all(result == expected_mask) From 6b5dacd8e3d4bfe8742c193d68fe4bbf235a7966 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 22:22:47 +0000 Subject: [PATCH 038/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/dataset/test_utils.py | 65 ++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/test/dataset/test_utils.py b/test/dataset/test_utils.py index 9cefa438..58f2ec22 100644 --- a/test/dataset/test_utils.py +++ b/test/dataset/test_utils.py @@ -10,10 +10,10 @@ from supervision import Detections from supervision.dataset.utils import ( build_class_index_mapping, map_detections_class_id, - merge_class_lists, - train_test_split, mask_to_rle, + merge_class_lists, rle_to_mask, + train_test_split, ) T = TypeVar("T") @@ -239,33 +239,37 @@ def test_map_detections_class_id( "mask, expected_rle, exception", [ ( - np.zeros((3,3)).astype(bool), + np.zeros((3, 3)).astype(bool), [9], DoesNotRaise(), ), # mask with background only (mask with only False values) ( - np.ones((3,3)).astype(bool), + np.ones((3, 3)).astype(bool), [0, 9], DoesNotRaise(), ), # mask with foreground only (mask with only True values) ( np.array( - [[0, 0, 0, 0, 0], - [0, 1, 1, 1, 0], - [0, 1, 0, 1, 0], - [0, 1, 1, 1, 0], - [0, 0, 0, 0, 0]] + [ + [0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 0, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0], + ] ).astype(bool), [6, 3, 2, 1, 1, 1, 2, 3, 6], DoesNotRaise(), ), # mask where foreground object has hole ( np.array( - [[1, 0, 1, 0, 1], - [1, 0, 1, 0, 1], - [1, 0, 1, 0, 1], - [1, 0, 1, 0, 1], - [1, 0, 1, 0, 1]] + [ + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + ] ).astype(bool), [0, 5, 5, 5, 5, 5], DoesNotRaise(), @@ -286,24 +290,26 @@ def test_mask_to_rle_convertion( ( [9], [3, 3], - np.zeros((3,3)).astype(bool), + np.zeros((3, 3)).astype(bool), DoesNotRaise(), ), # mask with background only (mask with only False values) ( [0, 9], [3, 3], - np.ones((3,3)).astype(bool), + np.ones((3, 3)).astype(bool), DoesNotRaise(), ), # mask with foreground only (mask with only True values) ( [6, 3, 2, 1, 1, 1, 2, 3, 6], [5, 5], np.array( - [[0, 0, 0, 0, 0], - [0, 1, 1, 1, 0], - [0, 1, 0, 1, 0], - [0, 1, 1, 1, 0], - [0, 0, 0, 0, 0]] + [ + [0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 0, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0], + ] ).astype(bool), DoesNotRaise(), ), # mask where foreground object has hole @@ -311,18 +317,23 @@ def test_mask_to_rle_convertion( [0, 5, 5, 5, 5, 5], [5, 5], np.array( - [[1, 0, 1, 0, 1], - [1, 0, 1, 0, 1], - [1, 0, 1, 0, 1], - [1, 0, 1, 0, 1], - [1, 0, 1, 0, 1]] + [ + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + ] ).astype(bool), DoesNotRaise(), ), # mask where foreground consists of 3 separate components ], ) def test_rle_to_mask_convertion( - rle: npt.NDArray[np.int_], resolution_wh: Tuple[int, int],expected_mask: npt.NDArray[np.bool_], exception: Exception + rle: npt.NDArray[np.int_], + resolution_wh: Tuple[int, int], + expected_mask: npt.NDArray[np.bool_], + exception: Exception, ) -> None: with exception: result = rle_to_mask(rle=rle, resolution_wh=resolution_wh) From d7b6af84e6ed892ac27c535c623c83bae77cf9d7 Mon Sep 17 00:00:00 2001 From: Piotr Skalski Date: Tue, 7 May 2024 12:25:41 +0200 Subject: [PATCH 039/136] 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 040/136] 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 041/136] 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 a9f821c28a98430972441fd14bfe80051bdc6631 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Tue, 7 May 2024 17:34:13 +0200 Subject: [PATCH 042/136] Assertion error when number of pixel in RLE does not match the nuber of pixels computed as width*height --- supervision/dataset/utils.py | 6 ++++++ test/dataset/test_utils.py | 10 ++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index 53593a35..96f6a54b 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -149,12 +149,18 @@ def rle_to_mask( npt.NDArray[np.bool_]: The generated 2D Boolean mask of shape (h,w), where the foreground object is marked with `True`'s and the rest is filled with `False`'s. + Raises: + AssertionError: If the sum of pixels encoded in RLE differs from the + number of pixels in the expected mask (computed based on resolution_wh). + Examples: rle = [2, 2, 2], resolution_wh = [3, 2] -> mask = [[False, True, False], [False, True, False]] """ width, height = resolution_wh + assert width*height == np.sum(rle), "the sum of the number of pixels in the RLE must be the same as the number of pixels in the expected mask" + zero_one_values = np.zeros_like(rle) zero_one_values[1::2] = 1 diff --git a/test/dataset/test_utils.py b/test/dataset/test_utils.py index 9cefa438..d88e8349 100644 --- a/test/dataset/test_utils.py +++ b/test/dataset/test_utils.py @@ -272,7 +272,7 @@ def test_map_detections_class_id( ), # mask where foreground consists of 3 separate components ], ) -def test_mask_to_rle_convertion( +def test_mask_to_rle_conversion( mask: npt.NDArray[np.bool_], expected_rle: List[int], exception: Exception ) -> None: with exception: @@ -319,9 +319,15 @@ def test_mask_to_rle_convertion( ).astype(bool), DoesNotRaise(), ), # mask where foreground consists of 3 separate components + ( + [0, 5, 5, 5, 5, 5], + [5, 5], + None, + pytest.raises(AssertionError), + ), # mask where foreground consists of 3 separate components ], ) -def test_rle_to_mask_convertion( +def test_rle_to_mask_conversion( rle: npt.NDArray[np.int_], resolution_wh: Tuple[int, int],expected_mask: npt.NDArray[np.bool_], exception: Exception ) -> None: with exception: From d59ec0f2237616b188e1c30201904713385f9734 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 May 2024 15:36:45 +0000 Subject: [PATCH 043/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index 96f6a54b..0bb432f4 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -150,16 +150,18 @@ def rle_to_mask( is marked with `True`'s and the rest is filled with `False`'s. Raises: - AssertionError: If the sum of pixels encoded in RLE differs from the + AssertionError: If the sum of pixels encoded in RLE differs from the number of pixels in the expected mask (computed based on resolution_wh). - + Examples: rle = [2, 2, 2], resolution_wh = [3, 2] -> mask = [[False, True, False], [False, True, False]] """ width, height = resolution_wh - assert width*height == np.sum(rle), "the sum of the number of pixels in the RLE must be the same as the number of pixels in the expected mask" + assert ( + width * height == np.sum(rle) + ), "the sum of the number of pixels in the RLE must be the same as the number of pixels in the expected mask" zero_one_values = np.zeros_like(rle) zero_one_values[1::2] = 1 From aac1d8862a35c8c1b741b224cf5db8fed64a3cec Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Tue, 7 May 2024 18:01:14 +0200 Subject: [PATCH 044/136] assertion for valid mask in mask_to_rle function --- supervision/dataset/utils.py | 6 ++++++ test/dataset/test_utils.py | 19 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index 0bb432f4..0167dc39 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -182,6 +182,9 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: List[int]: the run-length encoded mask. Values of a list with even indices represent the number of pixels assigned as background (`False`), values of a list with odd indices represent the number of pixels assigned as foreground object (`True`). + Raises: + AssertionError: If imput mask is not 2D or is empty. + Examples: mask = [[False, True, True], -> rle = [2, 4] [False, True, True]] @@ -189,6 +192,9 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: mask = [[True, True, True], -> rle = [0, 6] [True, True, True]] """ + assert mask.ndim == 2, "Input mask must be 2D" + assert mask.size != 0, "Input mask cannot be empty" + rle = [] if mask[0][0] == 1: rle = [0] diff --git a/test/dataset/test_utils.py b/test/dataset/test_utils.py index b810f204..0bf8ecb4 100644 --- a/test/dataset/test_utils.py +++ b/test/dataset/test_utils.py @@ -274,6 +274,20 @@ def test_map_detections_class_id( [0, 5, 5, 5, 5, 5], DoesNotRaise(), ), # mask where foreground consists of 3 separate components + ( + np.array( + [[[]]] + ).astype(bool), + None, + pytest.raises(AssertionError), + ), # raises AssertionError because mask dimentionality is not 2D + ( + np.array( + [[]] + ).astype(bool), + None, + pytest.raises(AssertionError), + ), # raises AssertionError because mask is empty ], ) def test_mask_to_rle_conversion( @@ -329,10 +343,11 @@ def test_mask_to_rle_conversion( ), # mask where foreground consists of 3 separate components ( [0, 5, 5, 5, 5, 5], - [5, 5], + [2, 2], None, pytest.raises(AssertionError), - ), # mask where foreground consists of 3 separate components + ), # raises AssertionError because number of pixels in RLE does not match + # number of pixels in expected mask (width x height). ], ) def test_rle_to_mask_convertion( From 36a301e7fccbb1ffe78aef984046e70f56170279 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 May 2024 16:01:35 +0000 Subject: [PATCH 045/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/dataset/test_utils.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/test/dataset/test_utils.py b/test/dataset/test_utils.py index 0bf8ecb4..e269dde7 100644 --- a/test/dataset/test_utils.py +++ b/test/dataset/test_utils.py @@ -275,19 +275,15 @@ def test_map_detections_class_id( DoesNotRaise(), ), # mask where foreground consists of 3 separate components ( - np.array( - [[[]]] - ).astype(bool), + np.array([[[]]]).astype(bool), None, pytest.raises(AssertionError), - ), # raises AssertionError because mask dimentionality is not 2D + ), # raises AssertionError because mask dimentionality is not 2D ( - np.array( - [[]] - ).astype(bool), + np.array([[]]).astype(bool), None, pytest.raises(AssertionError), - ), # raises AssertionError because mask is empty + ), # raises AssertionError because mask is empty ], ) def test_mask_to_rle_conversion( @@ -346,8 +342,8 @@ def test_mask_to_rle_conversion( [2, 2], None, pytest.raises(AssertionError), - ), # raises AssertionError because number of pixels in RLE does not match - # number of pixels in expected mask (width x height). + ), # raises AssertionError because number of pixels in RLE does not match + # number of pixels in expected mask (width x height). ], ) def test_rle_to_mask_convertion( From 649e27323dbd8c282cc14c245a48b4f27d372357 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Wed, 8 May 2024 12:49:25 +0200 Subject: [PATCH 046/136] fix the line lengths --- supervision/dataset/formats/coco.py | 3 ++- supervision/dataset/utils.py | 29 ++++++++++++++++++----------- test/dataset/formats/test_coco.py | 12 ++++++++---- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index febeb8d6..0489cd08 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -123,7 +123,8 @@ def detections_to_coco_annotations( )[0].flatten() ) # todo: flag for when to use RLE? - # segmentation = {"counts": mask_to_rle(binary_mask=mask), "size": list(mask.shape[:2])} + # segmentation = {"counts": mask_to_rle(binary_mask=mask), + # "size": list(mask.shape[:2])} coco_annotation = { "id": annotation_id, "image_id": image_id, diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index 0167dc39..bd7d7b57 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -140,14 +140,18 @@ def rle_to_mask( Converts run-length encoding (RLE) to a binary mask. Args: - rle (npt.NDArray[np.int_]): The 1D RLE array, the format used in the COCO dataset (column-wise encoding, - values of an array with even indices represent the number of pixels assigned as background, - values of an array with odd indices represent the number of pixels assigned as foreground object). - resolution_wh (Tuple[int, int]): The width (w) and height (h) of the desired binary mask resolution. + rle (npt.NDArray[np.int_]): The 1D RLE array, the format used in the COCO + dataset (column-wise encoding, values of an array with even indices + represent the number of pixels assigned as background, + values of an array with odd indices represent the number of pixels + assigned as foreground object). + resolution_wh (Tuple[int, int]): The width (w) and height (h) + of the desired binary mask resolution. Returns: - npt.NDArray[np.bool_]: The generated 2D Boolean mask of shape (h,w), where the foreground object - is marked with `True`'s and the rest is filled with `False`'s. + npt.NDArray[np.bool_]: The generated 2D Boolean mask of shape (h,w), + where the foreground object is marked with `True`'s and the rest + is filled with `False`'s. Raises: AssertionError: If the sum of pixels encoded in RLE differs from the @@ -161,7 +165,8 @@ def rle_to_mask( assert ( width * height == np.sum(rle) - ), "the sum of the number of pixels in the RLE must be the same as the number of pixels in the expected mask" + ), ("the sum of the number of pixels in the RLE must be the same " + "as the number of pixels in the expected mask") zero_one_values = np.zeros_like(rle) zero_one_values[1::2] = 1 @@ -175,12 +180,14 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: Converts a binary mask into a run-length encoding (RLE). Args: - mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground object - and `False` indicates background. + mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground + object and `False` indicates background. Returns: - List[int]: the run-length encoded mask. Values of a list with even indices represent the number of pixels assigned as background (`False`), - values of a list with odd indices represent the number of pixels assigned as foreground object (`True`). + List[int]: the run-length encoded mask. Values of a list with even indices + represent the number of pixels assigned as background (`False`), values + of a list with odd indices represent the number of pixels assigned + as foreground object (`True`). Raises: AssertionError: If imput mask is not 2D or is empty. diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index bb4a6e64..12ff4f00 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -252,7 +252,8 @@ def test_group_coco_annotations_by_image_id( ).reshape((1, 20, 20)), ), DoesNotRaise(), - ), # single image annotations with mask, segmentation mask in L-like shape, like below: + ), # single image annotations with mask, segmentation mask in L-like shape, + # like below: # 1 0 0 0 # 1 1 0 0 # 0 0 0 0 @@ -306,7 +307,8 @@ def test_group_coco_annotations_by_image_id( ).reshape((1, 20, 20)), ), DoesNotRaise(), - ), # single image annotations with mask, RLE segmentation mask in L-like shape, like below: + ), # single image annotations with mask, RLE segmentation mask in L-like shape, + # like below: # 1 0 0 0 # 1 1 0 0 # 0 0 0 0 @@ -355,7 +357,8 @@ def test_group_coco_annotations_by_image_id( ), ), DoesNotRaise(), - ), # two image annotations with mask, one mask as polygon in in L-like shape, second as RLE in shape of square, like below (P = polygon, R = RLE): + ), # two image annotations with mask, one mask as polygon in in L-like shape, + # second as RLE in shape of square, like below (P = polygon, R = RLE): # P R 0 0 # P P 0 0 # 0 0 0 0 @@ -404,7 +407,8 @@ def test_group_coco_annotations_by_image_id( ), ), DoesNotRaise(), - ), # two image annotations with mask, first mask as RLE in shape of square, second as polygon in in L-like shape, like below (P = polygon, R = RLE): + ), # two image annotations with mask, first mask as RLE in shape of square, + # second as polygon in in L-like shape, like below (P = polygon, R = RLE): # P R 0 0 # P P 0 0 # 0 0 0 0 From 008711aac479453496b0ee9f7655d0b427670bdc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 May 2024 10:50:30 +0000 Subject: [PATCH 047/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/formats/coco.py | 2 +- supervision/dataset/utils.py | 28 ++++++++++++++-------------- test/dataset/formats/test_coco.py | 8 ++++---- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 0489cd08..4beb1dcd 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -123,7 +123,7 @@ def detections_to_coco_annotations( )[0].flatten() ) # todo: flag for when to use RLE? - # segmentation = {"counts": mask_to_rle(binary_mask=mask), + # segmentation = {"counts": mask_to_rle(binary_mask=mask), # "size": list(mask.shape[:2])} coco_annotation = { "id": annotation_id, diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index bd7d7b57..93242e2b 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -140,17 +140,17 @@ def rle_to_mask( Converts run-length encoding (RLE) to a binary mask. Args: - rle (npt.NDArray[np.int_]): The 1D RLE array, the format used in the COCO - dataset (column-wise encoding, values of an array with even indices + rle (npt.NDArray[np.int_]): The 1D RLE array, the format used in the COCO + dataset (column-wise encoding, values of an array with even indices represent the number of pixels assigned as background, - values of an array with odd indices represent the number of pixels + values of an array with odd indices represent the number of pixels assigned as foreground object). - resolution_wh (Tuple[int, int]): The width (w) and height (h) + resolution_wh (Tuple[int, int]): The width (w) and height (h) of the desired binary mask resolution. Returns: - npt.NDArray[np.bool_]: The generated 2D Boolean mask of shape (h,w), - where the foreground object is marked with `True`'s and the rest + npt.NDArray[np.bool_]: The generated 2D Boolean mask of shape (h,w), + where the foreground object is marked with `True`'s and the rest is filled with `False`'s. Raises: @@ -163,10 +163,10 @@ def rle_to_mask( """ width, height = resolution_wh - assert ( - width * height == np.sum(rle) - ), ("the sum of the number of pixels in the RLE must be the same " - "as the number of pixels in the expected mask") + assert width * height == np.sum(rle), ( + "the sum of the number of pixels in the RLE must be the same " + "as the number of pixels in the expected mask" + ) zero_one_values = np.zeros_like(rle) zero_one_values[1::2] = 1 @@ -180,13 +180,13 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: Converts a binary mask into a run-length encoding (RLE). Args: - mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground + mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground object and `False` indicates background. Returns: - List[int]: the run-length encoded mask. Values of a list with even indices - represent the number of pixels assigned as background (`False`), values - of a list with odd indices represent the number of pixels assigned + List[int]: the run-length encoded mask. Values of a list with even indices + represent the number of pixels assigned as background (`False`), values + of a list with odd indices represent the number of pixels assigned as foreground object (`True`). Raises: diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 12ff4f00..f47e796d 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -252,7 +252,7 @@ def test_group_coco_annotations_by_image_id( ).reshape((1, 20, 20)), ), DoesNotRaise(), - ), # single image annotations with mask, segmentation mask in L-like shape, + ), # single image annotations with mask, segmentation mask in L-like shape, # like below: # 1 0 0 0 # 1 1 0 0 @@ -307,7 +307,7 @@ def test_group_coco_annotations_by_image_id( ).reshape((1, 20, 20)), ), DoesNotRaise(), - ), # single image annotations with mask, RLE segmentation mask in L-like shape, + ), # single image annotations with mask, RLE segmentation mask in L-like shape, # like below: # 1 0 0 0 # 1 1 0 0 @@ -357,7 +357,7 @@ def test_group_coco_annotations_by_image_id( ), ), DoesNotRaise(), - ), # two image annotations with mask, one mask as polygon in in L-like shape, + ), # two image annotations with mask, one mask as polygon in in L-like shape, # second as RLE in shape of square, like below (P = polygon, R = RLE): # P R 0 0 # P P 0 0 @@ -407,7 +407,7 @@ def test_group_coco_annotations_by_image_id( ), ), DoesNotRaise(), - ), # two image annotations with mask, first mask as RLE in shape of square, + ), # two image annotations with mask, first mask as RLE in shape of square, # second as polygon in in L-like shape, like below (P = polygon, R = RLE): # P R 0 0 # P P 0 0 From 45563dfddff9b590398fbb5555179b0c1d2d5a42 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 8 May 2024 14:07:07 +0300 Subject: [PATCH 048/136] 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 c47b0ce9907fcc776a6a803d97acf9be7bd7770a Mon Sep 17 00:00:00 2001 From: Raif Olson Date: Wed, 8 May 2024 11:07:14 -0400 Subject: [PATCH 049/136] filter out detections with zero area. --- supervision/tracker/byte_tracker/core.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/supervision/tracker/byte_tracker/core.py b/supervision/tracker/byte_tracker/core.py index ce3bbbbf..132bf391 100644 --- a/supervision/tracker/byte_tracker/core.py +++ b/supervision/tracker/byte_tracker/core.py @@ -362,6 +362,13 @@ class ByteTrack: scores = tensors[:, 4] bboxes = tensors[:, :4] + bbox_areas = (bboxes[:, 2] - bboxes[:, 0]) * (bboxes[:, 3] - bboxes[:, 1]) + valid_box_inds = bbox_areas > 0 + + class_ids = class_ids[valid_box_inds] + scores = scores[valid_box_inds] + bboxes = bboxes[valid_box_inds] + remain_inds = scores > self.track_activation_threshold inds_low = scores > 0.1 inds_high = scores < self.track_activation_threshold From 1ebbe3a8d104dfc20cd52a5270cd983581861e74 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Thu, 9 May 2024 09:17:37 +0300 Subject: [PATCH 050/136] 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 051/136] 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 052/136] 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 053/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/detection/utils.py | 44 ++++++++++++---------------------- 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 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 054/136] 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 055/136] :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 e4e66f8a9a9ab132776285cde21107e4c69dba6e Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Fri, 10 May 2024 10:16:34 +0200 Subject: [PATCH 056/136] faster mask to rle --- supervision/dataset/utils.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index bd7d7b57..6789d17a 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -202,11 +202,18 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: assert mask.ndim == 2, "Input mask must be 2D" assert mask.size != 0, "Input mask cannot be empty" - rle = [] - if mask[0][0] == 1: - rle = [0] + on_value_change_indices = np.where(mask.ravel(order='F') != + np.roll(mask.ravel(order='F'),1))[0] + + on_value_change_indices = np.append(on_value_change_indices, mask.size) + # need to add 0 at the beginning when the same value is in the first and + # last element of the flattened mask + if on_value_change_indices[0] != 0: + on_value_change_indices = np.insert(on_value_change_indices, 0, 0) - for _, group in groupby(mask.ravel(order="F")): - rle.append(len(list(group))) + rle = np.diff(on_value_change_indices) - return rle + if mask[0][0]==1: + rle = np.insert(rle, 0, 0) + + return list(rle) From 7bd92b096fbe5a4a6377884ca7c78fd3a9fe556c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 10 May 2024 08:18:35 +0000 Subject: [PATCH 057/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/utils.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index 4fbd1230..9b5ae3cd 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -1,7 +1,6 @@ import copy import os import random -from itertools import groupby from pathlib import Path from typing import Dict, List, Optional, Tuple, TypeVar @@ -202,18 +201,19 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: assert mask.ndim == 2, "Input mask must be 2D" assert mask.size != 0, "Input mask cannot be empty" - on_value_change_indices = np.where(mask.ravel(order='F') != - np.roll(mask.ravel(order='F'),1))[0] - + on_value_change_indices = np.where( + mask.ravel(order="F") != np.roll(mask.ravel(order="F"), 1) + )[0] + on_value_change_indices = np.append(on_value_change_indices, mask.size) - # need to add 0 at the beginning when the same value is in the first and + # need to add 0 at the beginning when the same value is in the first and # last element of the flattened mask - if on_value_change_indices[0] != 0: - on_value_change_indices = np.insert(on_value_change_indices, 0, 0) + if on_value_change_indices[0] != 0: + on_value_change_indices = np.insert(on_value_change_indices, 0, 0) rle = np.diff(on_value_change_indices) - if mask[0][0]==1: - rle = np.insert(rle, 0, 0) + if mask[0][0] == 1: + rle = np.insert(rle, 0, 0) return list(rle) From 2364abf481bfea634d3066d7c31f561170097e00 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Fri, 10 May 2024 15:08:18 +0200 Subject: [PATCH 058/136] 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 059/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/detection/test_utils.py | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index 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 060/136] 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 061/136] 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 062/136] 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 063/136] 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 8c857dd9a61999b3d7d7c26568a855f617eb6f43 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Sun, 12 May 2024 23:02:22 +0200 Subject: [PATCH 064/136] speed up rle to mask --- supervision/dataset/utils.py | 6 ++++-- test/dataset/test_utils.py | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index 4fbd1230..82491871 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -168,10 +168,12 @@ def rle_to_mask( "as the number of pixels in the expected mask" ) - zero_one_values = np.zeros_like(rle) + zero_one_values = np.zeros(shape = (rle.size,1), dtype=np.uint8) zero_one_values[1::2] = 1 - decoded_rle = np.repeat(zero_one_values, rle) + decoded_rle = np.repeat(zero_one_values, rle, axis=0) + decoded_rle = np.append(decoded_rle, + np.zeros(width * height - len(decoded_rle), dtype=np.uint8)) return decoded_rle.reshape((height, width), order="F") diff --git a/test/dataset/test_utils.py b/test/dataset/test_utils.py index e269dde7..e30b4e5f 100644 --- a/test/dataset/test_utils.py +++ b/test/dataset/test_utils.py @@ -298,19 +298,19 @@ def test_mask_to_rle_conversion( "rle, resolution_wh, expected_mask, exception", [ ( - [9], + np.array([9]), [3, 3], np.zeros((3, 3)).astype(bool), DoesNotRaise(), ), # mask with background only (mask with only False values) ( - [0, 9], + np.array([0, 9]), [3, 3], np.ones((3, 3)).astype(bool), DoesNotRaise(), ), # mask with foreground only (mask with only True values) ( - [6, 3, 2, 1, 1, 1, 2, 3, 6], + np.array([6, 3, 2, 1, 1, 1, 2, 3, 6]), [5, 5], np.array( [ @@ -324,7 +324,7 @@ def test_mask_to_rle_conversion( DoesNotRaise(), ), # mask where foreground object has hole ( - [0, 5, 5, 5, 5, 5], + np.array([0, 5, 5, 5, 5, 5]), [5, 5], np.array( [ @@ -338,7 +338,7 @@ def test_mask_to_rle_conversion( DoesNotRaise(), ), # mask where foreground consists of 3 separate components ( - [0, 5, 5, 5, 5, 5], + np.array([0, 5, 5, 5, 5, 5]), [2, 2], None, pytest.raises(AssertionError), From 14b57345a763a0a4e414a64649882b6054571b78 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 12 May 2024 21:31:16 +0000 Subject: [PATCH 065/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index 61f3b1c8..b0b5577e 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -167,12 +167,13 @@ def rle_to_mask( "as the number of pixels in the expected mask" ) - zero_one_values = np.zeros(shape = (rle.size,1), dtype=np.uint8) + zero_one_values = np.zeros(shape=(rle.size, 1), dtype=np.uint8) zero_one_values[1::2] = 1 decoded_rle = np.repeat(zero_one_values, rle, axis=0) - decoded_rle = np.append(decoded_rle, - np.zeros(width * height - len(decoded_rle), dtype=np.uint8)) + decoded_rle = np.append( + decoded_rle, np.zeros(width * height - len(decoded_rle), dtype=np.uint8) + ) return decoded_rle.reshape((height, width), order="F") From 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 066/136] :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 067/136] 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 d21c98a4d2fda92300d4931f952fcc0e91669959 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Mon, 13 May 2024 12:06:38 +0200 Subject: [PATCH 068/136] docs updated; `rle_to_mask` and `mask_to_rle` added to `__init__.py` --- docs/{datasets.md => datasets/core.md} | 1 + docs/datasets/utils.md | 18 ++++++++ mkdocs.yml | 4 +- supervision/__init__.py | 4 ++ supervision/dataset/utils.py | 60 ++++++++++++++++++-------- test/dataset/test_utils.py | 12 ++++-- 6 files changed, 76 insertions(+), 23 deletions(-) rename docs/{datasets.md => datasets/core.md} (97%) create mode 100644 docs/datasets/utils.md diff --git a/docs/datasets.md b/docs/datasets/core.md similarity index 97% rename from docs/datasets.md rename to docs/datasets/core.md index 73931515..03d0c196 100644 --- a/docs/datasets.md +++ b/docs/datasets/core.md @@ -1,5 +1,6 @@ --- comments: true +status: new --- # Datasets diff --git a/docs/datasets/utils.md b/docs/datasets/utils.md new file mode 100644 index 00000000..6be56303 --- /dev/null +++ b/docs/datasets/utils.md @@ -0,0 +1,18 @@ +--- +comments: true +status: new +--- + +# Datasets Utils + + + +:::supervision.dataset.utils.rle_to_mask + + + +:::supervision.dataset.utils.mask_to_rle diff --git a/mkdocs.yml b/mkdocs.yml index cf206a82..281c40c9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -61,7 +61,9 @@ nav: - Detection Smoother: detection/tools/smoother.md - Save Detections: detection/tools/save_detections.md - Trackers: trackers.md - - Datasets: datasets.md + - Datasets: + - Core: datasets/core.md + - Utils: datasets/utils.md - Utils: - Video: utils/video.md - Image: utils/image.md diff --git a/supervision/__init__.py b/supervision/__init__.py index bb526514..b7f7a5e8 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -34,6 +34,10 @@ from supervision.dataset.core import ( ClassificationDataset, DetectionDataset, ) +from supervision.dataset.utils import ( + rle_to_mask, + mask_to_rle, +) from supervision.detection.annotate import BoxAnnotator from supervision.detection.core import Detections from supervision.detection.line_zone import LineZone, LineZoneAnnotator diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index b0b5577e..ed7062b9 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -2,7 +2,7 @@ import copy import os import random from pathlib import Path -from typing import Dict, List, Optional, Tuple, TypeVar +from typing import Dict, List, Optional, Tuple, Union, TypeVar import cv2 import numpy as np @@ -133,33 +133,42 @@ def train_test_split( def rle_to_mask( - rle: npt.NDArray[np.int_], resolution_wh: Tuple[int, int] + rle: Union[npt.NDArray[np.int_], List[int]], resolution_wh: Tuple[int, int] ) -> npt.NDArray[np.bool_]: """ Converts run-length encoding (RLE) to a binary mask. Args: - rle (npt.NDArray[np.int_]): The 1D RLE array, the format used in the COCO - dataset (column-wise encoding, values of an array with even indices - represent the number of pixels assigned as background, + rle (Union[npt.NDArray[np.int_], List[int]]): The 1D RLE array, the format + used in the COCO dataset (column-wise encoding, values of an array with + even indices represent the number of pixels assigned as background, values of an array with odd indices represent the number of pixels assigned as foreground object). resolution_wh (Tuple[int, int]): The width (w) and height (h) - of the desired binary mask resolution. + of the desired binary mask. Returns: - npt.NDArray[np.bool_]: The generated 2D Boolean mask of shape (h,w), - where the foreground object is marked with `True`'s and the rest - is filled with `False`'s. + The generated 2D Boolean mask of shape `(h, w)`, where the foreground object is + marked with `True`'s and the rest is filled with `False`'s. Raises: AssertionError: If the sum of pixels encoded in RLE differs from the - number of pixels in the expected mask (computed based on resolution_wh). + number of pixels in the expected mask (computed based on resolution_wh). Examples: - rle = [2, 2, 2], resolution_wh = [3, 2] -> mask = [[False, True, False], - [False, True, False]] + ```python + import supervision as sv + + sv.rle_to_mask([2, 2, 2], (3, 2)) + # array([ + # [False, True, False], + # [False, True, False] + # ]) + ``` """ + if isinstance(rle, list): + rle = np.array(rle, dtype=int) + width, height = resolution_wh assert width * height == np.sum(rle), ( @@ -186,20 +195,33 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: object and `False` indicates background. Returns: - List[int]: the run-length encoded mask. Values of a list with even indices + The run-length encoded mask. Values of a list with even indices represent the number of pixels assigned as background (`False`), values of a list with odd indices represent the number of pixels assigned as foreground object (`True`). Raises: - AssertionError: If imput mask is not 2D or is empty. + AssertionError: If input mask is not 2D or is empty. - Examples: - mask = [[False, True, True], -> rle = [2, 4] - [False, True, True]] + Examples: + ```python + import numpy as np + import supervision as sv - mask = [[True, True, True], -> rle = [0, 6] - [True, True, True]] + mask = np.array([ + [False, True, True], + [False, True, True] + ]) + sv.mask_to_rle(mask) + # [2, 4] + + mask = np.array([ + [True, True, True], + [True, True, True] + ]) + sv.mask_to_rle(mask) + # [0, 6] + ``` """ assert mask.ndim == 2, "Input mask must be 2D" assert mask.size != 0, "Input mask cannot be empty" diff --git a/test/dataset/test_utils.py b/test/dataset/test_utils.py index e30b4e5f..41e1da5b 100644 --- a/test/dataset/test_utils.py +++ b/test/dataset/test_utils.py @@ -286,7 +286,7 @@ def test_map_detections_class_id( ), # raises AssertionError because mask is empty ], ) -def test_mask_to_rle_conversion( +def test_mask_to_rle( mask: npt.NDArray[np.bool_], expected_rle: List[int], exception: Exception ) -> None: with exception: @@ -302,7 +302,13 @@ def test_mask_to_rle_conversion( [3, 3], np.zeros((3, 3)).astype(bool), DoesNotRaise(), - ), # mask with background only (mask with only False values) + ), # mask with background only (mask with only False values); rle as array + ( + [9], + [3, 3], + np.zeros((3, 3)).astype(bool), + DoesNotRaise(), + ), # mask with background only (mask with only False values); rle as list ( np.array([0, 9]), [3, 3], @@ -346,7 +352,7 @@ def test_mask_to_rle_conversion( # number of pixels in expected mask (width x height). ], ) -def test_rle_to_mask_convertion( +def test_rle_to_mask( rle: npt.NDArray[np.int_], resolution_wh: Tuple[int, int], expected_mask: npt.NDArray[np.bool_], From 9a1c11218c8c247a3203ba76c501fe2aac08e1ba Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 10:06:55 +0000 Subject: [PATCH 069/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/__init__.py | 5 +---- supervision/dataset/utils.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/supervision/__init__.py b/supervision/__init__.py index b7f7a5e8..2bc72944 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -34,10 +34,7 @@ from supervision.dataset.core import ( ClassificationDataset, DetectionDataset, ) -from supervision.dataset.utils import ( - rle_to_mask, - mask_to_rle, -) +from supervision.dataset.utils import mask_to_rle, rle_to_mask from supervision.detection.annotate import BoxAnnotator from supervision.detection.core import Detections from supervision.detection.line_zone import LineZone, LineZoneAnnotator diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index ed7062b9..4efc7194 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -2,7 +2,7 @@ import copy import os import random from pathlib import Path -from typing import Dict, List, Optional, Tuple, Union, TypeVar +from typing import Dict, List, Optional, Tuple, TypeVar, Union import cv2 import numpy as np From 59b273ba87e30e406e8036560bd0c3348a86287c Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Mon, 13 May 2024 12:25:25 +0200 Subject: [PATCH 070/136] small refactor --- supervision/dataset/formats/coco.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 4beb1dcd..fc62ac66 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -5,6 +5,7 @@ from typing import Dict, List, Tuple import cv2 import numpy as np +import numpy.typing as npt from supervision.dataset.utils import ( approximate_mask_with_polygons, @@ -58,7 +59,10 @@ def group_coco_annotations_by_image_id( return annotations -def _annotations_to_mask(image_annotations: List[dict], resolution_wh: Tuple[int, int]): +def coco_annotations_to_masks( + image_annotations: List[dict], + resolution_wh: Tuple[int, int] +) -> npt.NDArray[np.bool_]: return np.array( [ rle_to_mask( @@ -93,7 +97,10 @@ def coco_annotations_to_detections( xyxy[:, 2:4] += xyxy[:, 0:2] if with_masks: - mask = _annotations_to_mask(image_annotations, resolution_wh) + mask = coco_annotations_to_masks( + image_annotations=image_annotations, + resolution_wh=resolution_wh + ) return Detections( class_id=np.asarray(class_ids, dtype=int), xyxy=xyxy, mask=mask ) From cccacab62143010e75be4217a9af3c26e4ea3ffc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 10:25:46 +0000 Subject: [PATCH 071/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/formats/coco.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index fc62ac66..b6b2490a 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -60,8 +60,7 @@ def group_coco_annotations_by_image_id( def coco_annotations_to_masks( - image_annotations: List[dict], - resolution_wh: Tuple[int, int] + image_annotations: List[dict], resolution_wh: Tuple[int, int] ) -> npt.NDArray[np.bool_]: return np.array( [ @@ -98,8 +97,7 @@ def coco_annotations_to_detections( if with_masks: mask = coco_annotations_to_masks( - image_annotations=image_annotations, - resolution_wh=resolution_wh + image_annotations=image_annotations, resolution_wh=resolution_wh ) return Detections( class_id=np.asarray(class_ids, dtype=int), xyxy=xyxy, mask=mask From 82ab6c57cb8cb2f863e27c551e31e310e18fdb38 Mon Sep 17 00:00:00 2001 From: LinasKo Date: Mon, 13 May 2024 14:14:42 +0300 Subject: [PATCH 072/136] 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 073/136] 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 074/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/keypoint/annotators.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/supervision/keypoint/annotators.py b/supervision/keypoint/annotators.py index 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 075/136] 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 076/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/keypoint/annotators.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/keypoint/annotators.md b/docs/keypoint/annotators.md index 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 077/136] 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 078/136] 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 079/136] 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 080/136] 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 081/136] 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 082/136] 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 083/136] 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 084/136] =?UTF-8?q?chore(pre=5Fcommit):=20=E2=AC=86=20pre?= =?UTF-8?q?=5Fcommit=20autoupdate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.4.3 → v0.4.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.3...v0.4.4) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 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] From 5ef934e78099d29f507c29cfd9521d37db1d9886 Mon Sep 17 00:00:00 2001 From: Christoforos Aristeidou Date: Wed, 15 May 2024 09:51:55 +0300 Subject: [PATCH 085/136] fixed sentence spacing --- supervision/annotators/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/supervision/annotators/utils.py b/supervision/annotators/utils.py index e206c8cb..23e42752 100644 --- a/supervision/annotators/utils.py +++ b/supervision/annotators/utils.py @@ -34,14 +34,14 @@ def resolve_color_idx( ) -> int: if detection_idx >= len(detections): raise ValueError( - f"Detection index {detection_idx}" + f"Detection index {detection_idx} " f"is out of bounds for detections of length {len(detections)}" ) if isinstance(color_lookup, np.ndarray): if len(color_lookup) != len(detections): raise ValueError( - f"Length of color lookup {len(color_lookup)}" + f"Length of color lookup {len(color_lookup)} " f"does not match length of detections {len(detections)}" ) return color_lookup[detection_idx] @@ -50,14 +50,14 @@ def resolve_color_idx( elif color_lookup == ColorLookup.CLASS: if detections.class_id is None: raise ValueError( - "Could not resolve color by class because" + "Could not resolve color by class because " "Detections do not have class_id" ) return detections.class_id[detection_idx] elif color_lookup == ColorLookup.TRACK: if detections.tracker_id is None: raise ValueError( - "Could not resolve color by track because" + "Could not resolve color by track because " "Detections do not have tracker_id" ) return detections.tracker_id[detection_idx] From aab861c2db5ae2b641285a225fbeafe6206a6d02 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Wed, 15 May 2024 08:55:47 +0200 Subject: [PATCH 086/136] test_coco_annotations_to_detections result matrices defined in place --- test/dataset/formats/test_coco.py | 216 +++++++++++------------------- 1 file changed, 79 insertions(+), 137 deletions(-) diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index f47e796d..bfde8495 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -233,186 +233,128 @@ def test_group_coco_annotations_by_image_id( [ mock_cock_coco_annotation( category_id=0, - bbox=(0, 0, 10, 10), - area=10 * 10, - segmentation=[[0, 0, 4, 0, 4, 5, 9, 5, 9, 9, 0, 9]], + bbox=(0, 0, 5, 5), + area= 5 * 5, + segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]], ) ], - (20, 20), + (5, 5), True, Detections( - xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32), + xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), class_id=np.array([0], dtype=int), - mask=np.array( - [ - 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 - for i in range(0, 20) - for j in range(0, 20) - ] - ).reshape((1, 20, 20)), + mask=np.array([[[1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1]]]), ), DoesNotRaise(), - ), # single image annotations with mask, segmentation mask in L-like shape, - # like below: - # 1 0 0 0 - # 1 1 0 0 - # 0 0 0 0 - # 0 0 0 0 + ), # single image annotations with mask as polygon ( [ mock_cock_coco_annotation( category_id=0, - bbox=(0, 0, 10, 10), - area=10 * 10, - segmentation={ - "size": [20, 20], - "counts": [ - 0, - 10, - 10, - 10, - 10, - 10, - 10, - 10, - 10, - 10, - 15, - 5, - 15, - 5, - 15, - 5, - 15, - 5, - 15, - 5, - 210, - ], - }, - iscrowd=True, - ) - ], - (20, 20), - True, - Detections( - xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32), - class_id=np.array([0], dtype=int), - mask=np.array( - [ - 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 - for i in range(0, 20) - for j in range(0, 20) - ] - ).reshape((1, 20, 20)), - ), - DoesNotRaise(), - ), # single image annotations with mask, RLE segmentation mask in L-like shape, - # like below: - # 1 0 0 0 - # 1 1 0 0 - # 0 0 0 0 - # 0 0 0 0 - ( - [ - mock_cock_coco_annotation( - category_id=0, - bbox=(0, 0, 10, 10), - area=10 * 10, - segmentation=[[0, 0, 4, 0, 4, 5, 9, 5, 9, 9, 0, 9]], - ), - mock_cock_coco_annotation( - category_id=0, - bbox=(5, 0, 5, 5), + bbox=(0, 0, 5, 5), area=5 * 5, segmentation={ - "size": [20, 20], - "counts": [100, 5, 15, 5, 15, 5, 15, 5, 15, 5, 215], + "size": [5, 5], + "counts": [0, 15, 2, 3, 2, 3], + }, + iscrowd=True, + ) + ], + (5, 5), + True, + Detections( + xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), + class_id=np.array([0], dtype=int), + mask=np.array([[[1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1]]]), + ), + DoesNotRaise(), + ), # single image annotations with mask, RLE segmentation mask + ( + [ + mock_cock_coco_annotation( + category_id=0, + bbox=(0, 0, 5, 5), + area= 5 * 5, + segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]], + ), + mock_cock_coco_annotation( + category_id=0, + bbox=(3, 0, 2, 2), + area=2 * 2, + segmentation={ + "size": [5, 5], + "counts": [15, 2, 3, 2, 3], }, iscrowd=True, ), ], - (20, 20), + (5, 5), True, Detections( - xyxy=np.array([[0, 0, 10, 10], [5, 0, 10, 5]], dtype=np.float32), + xyxy=np.array([[0, 0, 5, 5], [3, 0, 5, 2]], dtype=np.float32), class_id=np.array([0, 0], dtype=int), - mask=np.array( - [ - np.array( - [ - 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 - for i in range(0, 20) - for j in range(0, 20) - ] - ).reshape((20, 20)), - np.array( - [ - 1 if j > 4 and j < 10 and i < 5 else 0 - for i in range(0, 20) - for j in range(0, 20) - ] - ).reshape((20, 20)), - ] - ), + mask=np.array([ [[1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1]], + [[0, 0, 0, 1, 1], + [0, 0, 0, 1, 1], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]]]) ), DoesNotRaise(), - ), # two image annotations with mask, one mask as polygon in in L-like shape, - # second as RLE in shape of square, like below (P = polygon, R = RLE): - # P R 0 0 - # P P 0 0 - # 0 0 0 0 - # 0 0 0 0 + ), # two image annotations with mask, one mask as polygon ans second as RLE ( [ mock_cock_coco_annotation( category_id=0, - bbox=(5, 0, 5, 5), - area=5 * 5, + bbox=(3, 0, 2, 2), + area=2 * 2, segmentation={ - "size": [20, 20], - "counts": [100, 5, 15, 5, 15, 5, 15, 5, 15, 5, 215], + "size": [5, 5], + "counts": [15, 2, 3, 2, 3], }, iscrowd=True, ), mock_cock_coco_annotation( category_id=1, - bbox=(0, 0, 10, 10), - area=10 * 10, - segmentation=[[0, 0, 4, 0, 4, 5, 9, 5, 9, 9, 0, 9]], + bbox=(0, 0, 5, 5), + area= 5 * 5, + segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]], ), ], - (20, 20), + (5, 5), True, Detections( - xyxy=np.array([[5, 0, 10, 5], [0, 0, 10, 10]], dtype=np.float32), + xyxy=np.array([[3, 0, 5, 2], [0, 0, 5, 5]], dtype=np.float32), class_id=np.array([0, 1], dtype=int), mask=np.array( [ - np.array( - [ - 1 if j > 4 and j < 10 and i < 5 else 0 - for i in range(0, 20) - for j in range(0, 20) - ] - ).reshape((20, 20)), - np.array( - [ - 0 if i >= 10 or j >= 10 or (i < 5 and j >= 5) else 1 - for i in range(0, 20) - for j in range(0, 20) - ] - ).reshape((20, 20)), + [[0, 0, 0, 1, 1], + [0, 0, 0, 1, 1], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]], + [[1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1]] ] ), ), DoesNotRaise(), - ), # two image annotations with mask, first mask as RLE in shape of square, - # second as polygon in in L-like shape, like below (P = polygon, R = RLE): - # P R 0 0 - # P P 0 0 - # 0 0 0 0 - # 0 0 0 0 + ), # two image annotations with mask, first mask as RLE and second as polygon ], ) def test_coco_annotations_to_detections( From f08404eb7d73b001e860c4b56ccc799cdfa6ce8e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 06:56:06 +0000 Subject: [PATCH 087/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/dataset/formats/test_coco.py | 92 +++++++++++++++++++------------ 1 file changed, 58 insertions(+), 34 deletions(-) diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index bfde8495..66e73c0e 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -234,7 +234,7 @@ def test_group_coco_annotations_by_image_id( mock_cock_coco_annotation( category_id=0, bbox=(0, 0, 5, 5), - area= 5 * 5, + area=5 * 5, segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]], ) ], @@ -243,14 +243,20 @@ def test_group_coco_annotations_by_image_id( Detections( xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), class_id=np.array([0], dtype=int), - mask=np.array([[[1, 1, 1, 0, 0], - [1, 1, 1, 0, 0], - [1, 1, 1, 1, 1], - [1, 1, 1, 1, 1], - [1, 1, 1, 1, 1]]]), + mask=np.array( + [ + [ + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + ] + ] + ), ), DoesNotRaise(), - ), # single image annotations with mask as polygon + ), # single image annotations with mask as polygon ( [ mock_cock_coco_annotation( @@ -269,11 +275,17 @@ def test_group_coco_annotations_by_image_id( Detections( xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), class_id=np.array([0], dtype=int), - mask=np.array([[[1, 1, 1, 0, 0], - [1, 1, 1, 0, 0], - [1, 1, 1, 1, 1], - [1, 1, 1, 1, 1], - [1, 1, 1, 1, 1]]]), + mask=np.array( + [ + [ + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + ] + ] + ), ), DoesNotRaise(), ), # single image annotations with mask, RLE segmentation mask @@ -282,7 +294,7 @@ def test_group_coco_annotations_by_image_id( mock_cock_coco_annotation( category_id=0, bbox=(0, 0, 5, 5), - area= 5 * 5, + area=5 * 5, segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]], ), mock_cock_coco_annotation( @@ -301,16 +313,24 @@ def test_group_coco_annotations_by_image_id( Detections( xyxy=np.array([[0, 0, 5, 5], [3, 0, 5, 2]], dtype=np.float32), class_id=np.array([0, 0], dtype=int), - mask=np.array([ [[1, 1, 1, 0, 0], - [1, 1, 1, 0, 0], - [1, 1, 1, 1, 1], - [1, 1, 1, 1, 1], - [1, 1, 1, 1, 1]], - [[0, 0, 0, 1, 1], - [0, 0, 0, 1, 1], - [0, 0, 0, 0, 0], - [0, 0, 0, 0, 0], - [0, 0, 0, 0, 0]]]) + mask=np.array( + [ + [ + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + ], + [ + [0, 0, 0, 1, 1], + [0, 0, 0, 1, 1], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + ] + ), ), DoesNotRaise(), ), # two image annotations with mask, one mask as polygon ans second as RLE @@ -329,7 +349,7 @@ def test_group_coco_annotations_by_image_id( mock_cock_coco_annotation( category_id=1, bbox=(0, 0, 5, 5), - area= 5 * 5, + area=5 * 5, segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]], ), ], @@ -340,16 +360,20 @@ def test_group_coco_annotations_by_image_id( class_id=np.array([0, 1], dtype=int), mask=np.array( [ - [[0, 0, 0, 1, 1], - [0, 0, 0, 1, 1], - [0, 0, 0, 0, 0], - [0, 0, 0, 0, 0], - [0, 0, 0, 0, 0]], - [[1, 1, 1, 0, 0], - [1, 1, 1, 0, 0], - [1, 1, 1, 1, 1], - [1, 1, 1, 1, 1], - [1, 1, 1, 1, 1]] + [ + [0, 0, 0, 1, 1], + [0, 0, 0, 1, 1], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + [ + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + ], ] ), ), From 24d1840ea1c8b6dab615b6d8c1b4913b131ddff8 Mon Sep 17 00:00:00 2001 From: Onuralp SEZER Date: Wed, 15 May 2024 18:35:36 +0300 Subject: [PATCH 088/136] =?UTF-8?q?docs:=20=F0=9F=93=9D=20RichLabelAnnotat?= =?UTF-8?q?or=20documentation=20added=20into=20docs=20and=20minor=20var=20?= =?UTF-8?q?fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onuralp SEZER --- docs/annotators.md | 37 ++++++++++++++++++++++++++++++++++ supervision/__init__.py | 1 + supervision/annotators/core.py | 4 ++-- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/docs/annotators.md b/docs/annotators.md index e1f4b115..958f2a74 100644 --- a/docs/annotators.md +++ b/docs/annotators.md @@ -285,6 +285,37 @@ status: new +=== "RichLabel" + + ```python + import supervision as sv + + image = ... + detections = sv.Detections(...) + + labels = [ + f"{class_name} {confidence:.2f}" + for class_name, confidence + in zip(detections['class_name'], detections.confidence) + ] + + rich_label_annotator = sv.RichLabelAnnotator( + font_path=".../font.ttf", + text_position=sv.Position.CENTER + ) + annotated_frame = label_annotator.annotate( + scene=image.copy(), + detections=detections, + labels=labels + ) + ``` + +
+ + ![label-annotator-example](https://media.roboflow.com/supervision-annotator-examples/label-annotator-example-purple.png){ align=center width="800" } + +
+ === "Crop" ```python @@ -492,6 +523,12 @@ status: new :::supervision.annotators.core.LabelAnnotator + + +:::supervision.annotators.core.RichLabelAnnotator + diff --git a/supervision/__init__.py b/supervision/__init__.py index f8e7a832..d2b502b9 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -18,6 +18,7 @@ from supervision.annotators.core import ( HaloAnnotator, HeatMapAnnotator, LabelAnnotator, + RichLabelAnnotator, MaskAnnotator, OrientedBoxAnnotator, PercentageBarAnnotator, diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index f2b552cd..75373e33 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -1177,7 +1177,7 @@ class RichLabelAnnotator: Example: ```python - import supervision as sv + import supervision as sv image = ... detections = sv.Detections(...) @@ -1188,7 +1188,7 @@ class RichLabelAnnotator: in zip(detections['class_name'], detections.confidence) ] - label_annotator = sv.RichLabelAnnotator(font_path="path/to/font.ttf") + rich_label_annotator = sv.RichLabelAnnotator(font_path="path/to/font.ttf") annotated_frame = label_annotator.annotate( scene=image.copy(), detections=detections, From d983177c3a3af42d88facff59a755f924c8933d9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 15:54:51 +0000 Subject: [PATCH 089/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/__init__.py | 2 +- supervision/annotators/core.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/supervision/__init__.py b/supervision/__init__.py index da754928..64680012 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -18,12 +18,12 @@ from supervision.annotators.core import ( HaloAnnotator, HeatMapAnnotator, LabelAnnotator, - RichLabelAnnotator, MaskAnnotator, OrientedBoxAnnotator, PercentageBarAnnotator, PixelateAnnotator, PolygonAnnotator, + RichLabelAnnotator, RoundBoxAnnotator, TraceAnnotator, TriangleAnnotator, diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index 75373e33..1c0ad8af 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -1125,8 +1125,8 @@ class RichLabelAnnotator: color (Union[Color, ColorPalette]): The color or color palette to use for annotating the text background. text_color (Color): The color to use for the text. - font_path (str): Path to the font file (e.g., ".ttf" or ".otf") to use for rendering text. - If `None`, the default PIL font will be used. + font_path (str): Path to the font file (e.g., ".ttf" or ".otf") to use for + rendering text. If `None`, the default PIL font will be used. font_size (int): Font size for the text. text_padding (int): Padding around the text within its background box. text_position (Position): Position of the text relative to the detection. From a615dfccb619c347f704d4e98682ddf01681ffb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 00:44:29 +0000 Subject: [PATCH 090/136] :arrow_up: Bump mkdocs-material from 9.5.22 to 9.5.23 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.5.22 to 9.5.23. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.22...9.5.23) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 71b6addb..2a689d76 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2198,13 +2198,13 @@ pygments = ">2.12.0" [[package]] name = "mkdocs-material" -version = "9.5.22" +version = "9.5.23" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.22-py3-none-any.whl", hash = "sha256:8c7a377d323567934e6cd46915e64dc209efceaec0dec1cf2202184f5649862c"}, - {file = "mkdocs_material-9.5.22.tar.gz", hash = "sha256:22a853a456ae8c581c4628159574d6fc7c71b2c7569dc9c3a82cc70432219599"}, + {file = "mkdocs_material-9.5.23-py3-none-any.whl", hash = "sha256:ffd08a5beaef3cd135aceb58ded8b98bbbbf2b70e5b656f6a14a63c917d9b001"}, + {file = "mkdocs_material-9.5.23.tar.gz", hash = "sha256:4627fc3f15de2cba2bde9debc2fd59b9888ef494beabfe67eb352e23d14bf288"}, ] [package.dependencies] From d8679d9c46cbf76fd86b64ea11bf2833a9794781 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Thu, 16 May 2024 08:40:28 +0200 Subject: [PATCH 091/136] automatic RLE for masks with holes or in multiple pieces --- supervision/dataset/formats/coco.py | 47 +++++++---- test/dataset/formats/test_coco.py | 116 +++++++++++++++++++++++++++- 2 files changed, 148 insertions(+), 15 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index b6b2490a..2cc442a9 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -11,6 +11,7 @@ from supervision.dataset.utils import ( approximate_mask_with_polygons, map_detections_class_id, rle_to_mask, + mask_to_rle ) from supervision.detection.core import Detections from supervision.detection.utils import polygon_to_mask @@ -106,6 +107,21 @@ def coco_annotations_to_detections( return Detections(xyxy=xyxy, class_id=np.asarray(class_ids, dtype=int)) +def _mask_has_holes(mask: np.ndarray)-> bool: + _, hierarchy = cv2.findContours(mask.astype(np.uint8), cv2.RETR_CCOMP, + cv2.CHAIN_APPROX_SIMPLE) + parent_countour_index = 3 + for h in hierarchy[0]: + if h[parent_countour_index] != -1: + return True + return False + + +def _mask_has_multiple_segments(mask: np.ndarray)-> bool: + number_of_labels, _ = cv2.connectedComponents(mask.astype(np.uint8), connectivity=4) + return number_of_labels > 2 + + def detections_to_coco_annotations( detections: Detections, image_id: int, @@ -118,26 +134,31 @@ def detections_to_coco_annotations( for xyxy, mask, _, class_id, _, _ in detections: box_width, box_height = xyxy[2] - xyxy[0], xyxy[3] - xyxy[1] segmentation = [] + iscrowd = 0 if mask is not None: - segmentation = list( - approximate_mask_with_polygons( - mask=mask, - min_image_area_percentage=min_image_area_percentage, - max_image_area_percentage=max_image_area_percentage, - approximation_percentage=approximation_percentage, - )[0].flatten() - ) - # todo: flag for when to use RLE? - # segmentation = {"counts": mask_to_rle(binary_mask=mask), - # "size": list(mask.shape[:2])} + iscrowd = _mask_has_holes(mask = mask) or \ + _mask_has_multiple_segments(mask = mask) + + if iscrowd: + segmentation = {"counts": mask_to_rle(mask=mask), + "size": list(mask.shape[:2])} + else: + segmentation = [list( + approximate_mask_with_polygons( + mask=mask, + min_image_area_percentage=min_image_area_percentage, + max_image_area_percentage=max_image_area_percentage, + approximation_percentage=approximation_percentage, + )[0].flatten() + )] # multicomponent masks supported only for rle format coco_annotation = { "id": annotation_id, "image_id": image_id, "category_id": int(class_id), "bbox": [xyxy[0], xyxy[1], box_width, box_height], "area": box_width * box_height, - "segmentation": [segmentation] if segmentation else [], - "iscrowd": 0, ## todo: iscrowd depends on flag 1 if RLE 0 if polygon + "segmentation": segmentation, + "iscrowd": iscrowd, } coco_annotations.append(coco_annotation) annotation_id += 1 diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 66e73c0e..cecb637d 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -1,5 +1,5 @@ from contextlib import ExitStack as DoesNotRaise -from typing import Dict, List, Tuple +from typing import Dict, List, Tuple, Union import numpy as np import pytest @@ -11,6 +11,7 @@ from supervision.dataset.formats.coco import ( coco_annotations_to_detections, coco_categories_to_classes, group_coco_annotations_by_image_id, + detections_to_coco_annotations ) @@ -20,9 +21,11 @@ def mock_cock_coco_annotation( category_id: int = 0, bbox: Tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0), area: float = 0.0, - segmentation: List[list] = None, + segmentation: Union[List[list], Dict] = None, iscrowd: bool = False, ) -> dict: + if not segmentation: + segmentation = [] return { "id": annotation_id, "image_id": image_id, @@ -454,3 +457,112 @@ def test_build_coco_class_index_mapping( coco_categories=coco_categories, target_classes=target_classes ) assert result == expected_result + + +@pytest.mark.parametrize( + "detections, image_id, annotation_id, expected_result, exception", + [ + ( + Detections(xyxy=np.array([[0, 0, 100, 100]], dtype=np.float32), + class_id=np.array([0], dtype=int)), + 0, + 0, + [mock_cock_coco_annotation(category_id=0, bbox=(0, 0, 100, 100), area=100 * 100)], + DoesNotRaise(), + ), # no segmentation mask + # ( + # Detections( + # xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), + # class_id=np.array([0], dtype=int), + # mask=np.array( + # [ + # [ + # [1, 1, 1, 0, 0], + # [1, 1, 1, 0, 0], + # [1, 1, 1, 1, 1], + # [1, 1, 1, 1, 1], + # [1, 1, 1, 1, 1], + # ] + # ] + # ), + # ), + # 0, + # 0, + # [mock_cock_coco_annotation( + # category_id=0, + # bbox=(0, 0, 5, 5), + # area=5 * 5, + # segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]])], + # DoesNotRaise(), + # ), # segmentation mask in single component,no holes in mask, expects polygon mask + ( + Detections( + xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), + class_id=np.array([0], dtype=int), + mask=np.array( + [ + [ + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [0, 0, 0, 1, 1], + [0, 0, 0, 1, 1], + ] + ] + ), + ), + 0, + 0, + [mock_cock_coco_annotation( + category_id=0, + bbox=(0, 0, 5, 5), + area=5 * 5, + segmentation={ + "size": [5, 5], + "counts": [0, 3, 2, 3, 2, 3, 5, 2, 3, 2], + }, + iscrowd=True, )], + DoesNotRaise(), + ), # segmentation mask with 2 components, no holes in mask, expects RLE mask + ( + Detections( + xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), + class_id=np.array([0], dtype=int), + mask=np.array( + [ + [ + [0, 1, 1, 1, 1], + [0, 1, 1, 1, 1], + [1, 1, 0, 0, 1], + [1, 1, 0, 0, 1], + [1, 1, 1, 1, 1], + ] + ] + ), + ), + 0, + 0, + [mock_cock_coco_annotation( + category_id=0, + bbox=(0, 0, 5, 5), + area=5 * 5, + segmentation={ + "size": [5, 5], + "counts": [2, 10, 2, 3, 2, 6], + }, + iscrowd=True, )], + DoesNotRaise(), + ) # segmentation mask in single component, with holes in mask, expects RLE mask + ], +) +def test_detections_to_coco_annotations( + detections: Detections, + image_id: int, + annotation_id: int, + expected_result: List[Dict], + exception: Exception) -> None: + with exception: + result, _ = detections_to_coco_annotations( + detections=detections, image_id=image_id, annotation_id=annotation_id + ) + assert result == expected_result From eefa05cf01c3875f7d21a2b40a71c1fdc4bdc591 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 06:40:55 +0000 Subject: [PATCH 092/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/formats/coco.py | 42 +++++---- test/dataset/formats/test_coco.py | 129 +++++++++++++++------------- 2 files changed, 95 insertions(+), 76 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 2cc442a9..19e7aa3e 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -10,8 +10,8 @@ import numpy.typing as npt from supervision.dataset.utils import ( approximate_mask_with_polygons, map_detections_class_id, + mask_to_rle, rle_to_mask, - mask_to_rle ) from supervision.detection.core import Detections from supervision.detection.utils import polygon_to_mask @@ -107,9 +107,10 @@ def coco_annotations_to_detections( return Detections(xyxy=xyxy, class_id=np.asarray(class_ids, dtype=int)) -def _mask_has_holes(mask: np.ndarray)-> bool: - _, hierarchy = cv2.findContours(mask.astype(np.uint8), cv2.RETR_CCOMP, - cv2.CHAIN_APPROX_SIMPLE) +def _mask_has_holes(mask: np.ndarray) -> bool: + _, hierarchy = cv2.findContours( + mask.astype(np.uint8), cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE + ) parent_countour_index = 3 for h in hierarchy[0]: if h[parent_countour_index] != -1: @@ -117,9 +118,9 @@ def _mask_has_holes(mask: np.ndarray)-> bool: return False -def _mask_has_multiple_segments(mask: np.ndarray)-> bool: +def _mask_has_multiple_segments(mask: np.ndarray) -> bool: number_of_labels, _ = cv2.connectedComponents(mask.astype(np.uint8), connectivity=4) - return number_of_labels > 2 + return number_of_labels > 2 def detections_to_coco_annotations( @@ -136,21 +137,26 @@ def detections_to_coco_annotations( segmentation = [] iscrowd = 0 if mask is not None: - iscrowd = _mask_has_holes(mask = mask) or \ - _mask_has_multiple_segments(mask = mask) + iscrowd = _mask_has_holes(mask=mask) or _mask_has_multiple_segments( + mask=mask + ) if iscrowd: - segmentation = {"counts": mask_to_rle(mask=mask), - "size": list(mask.shape[:2])} + segmentation = { + "counts": mask_to_rle(mask=mask), + "size": list(mask.shape[:2]), + } else: - segmentation = [list( - approximate_mask_with_polygons( - mask=mask, - min_image_area_percentage=min_image_area_percentage, - max_image_area_percentage=max_image_area_percentage, - approximation_percentage=approximation_percentage, - )[0].flatten() - )] # multicomponent masks supported only for rle format + segmentation = [ + list( + approximate_mask_with_polygons( + mask=mask, + min_image_area_percentage=min_image_area_percentage, + max_image_area_percentage=max_image_area_percentage, + approximation_percentage=approximation_percentage, + )[0].flatten() + ) + ] # multicomponent masks supported only for rle format coco_annotation = { "id": annotation_id, "image_id": image_id, diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index cecb637d..139c74f8 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -10,8 +10,8 @@ from supervision.dataset.formats.coco import ( classes_to_coco_categories, coco_annotations_to_detections, coco_categories_to_classes, + detections_to_coco_annotations, group_coco_annotations_by_image_id, - detections_to_coco_annotations ) @@ -463,40 +463,46 @@ def test_build_coco_class_index_mapping( "detections, image_id, annotation_id, expected_result, exception", [ ( - Detections(xyxy=np.array([[0, 0, 100, 100]], dtype=np.float32), - class_id=np.array([0], dtype=int)), - 0, - 0, - [mock_cock_coco_annotation(category_id=0, bbox=(0, 0, 100, 100), area=100 * 100)], - DoesNotRaise(), - ), # no segmentation mask - # ( - # Detections( - # xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), - # class_id=np.array([0], dtype=int), - # mask=np.array( - # [ - # [ - # [1, 1, 1, 0, 0], - # [1, 1, 1, 0, 0], - # [1, 1, 1, 1, 1], - # [1, 1, 1, 1, 1], - # [1, 1, 1, 1, 1], - # ] - # ] - # ), - # ), - # 0, - # 0, - # [mock_cock_coco_annotation( - # category_id=0, - # bbox=(0, 0, 5, 5), - # area=5 * 5, - # segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]])], - # DoesNotRaise(), - # ), # segmentation mask in single component,no holes in mask, expects polygon mask - ( - Detections( + Detections( + xyxy=np.array([[0, 0, 100, 100]], dtype=np.float32), + class_id=np.array([0], dtype=int), + ), + 0, + 0, + [ + mock_cock_coco_annotation( + category_id=0, bbox=(0, 0, 100, 100), area=100 * 100 + ) + ], + DoesNotRaise(), + ), # no segmentation mask + # ( + # Detections( + # xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), + # class_id=np.array([0], dtype=int), + # mask=np.array( + # [ + # [ + # [1, 1, 1, 0, 0], + # [1, 1, 1, 0, 0], + # [1, 1, 1, 1, 1], + # [1, 1, 1, 1, 1], + # [1, 1, 1, 1, 1], + # ] + # ] + # ), + # ), + # 0, + # 0, + # [mock_cock_coco_annotation( + # category_id=0, + # bbox=(0, 0, 5, 5), + # area=5 * 5, + # segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]])], + # DoesNotRaise(), + # ), # segmentation mask in single component,no holes in mask, expects polygon mask + ( + Detections( xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), class_id=np.array([0], dtype=int), mask=np.array( @@ -511,21 +517,24 @@ def test_build_coco_class_index_mapping( ] ), ), - 0, - 0, - [mock_cock_coco_annotation( - category_id=0, - bbox=(0, 0, 5, 5), - area=5 * 5, - segmentation={ + 0, + 0, + [ + mock_cock_coco_annotation( + category_id=0, + bbox=(0, 0, 5, 5), + area=5 * 5, + segmentation={ "size": [5, 5], "counts": [0, 3, 2, 3, 2, 3, 5, 2, 3, 2], }, - iscrowd=True, )], - DoesNotRaise(), - ), # segmentation mask with 2 components, no holes in mask, expects RLE mask - ( - Detections( + iscrowd=True, + ) + ], + DoesNotRaise(), + ), # segmentation mask with 2 components, no holes in mask, expects RLE mask + ( + Detections( xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), class_id=np.array([0], dtype=int), mask=np.array( @@ -540,19 +549,22 @@ def test_build_coco_class_index_mapping( ] ), ), - 0, - 0, - [mock_cock_coco_annotation( - category_id=0, - bbox=(0, 0, 5, 5), - area=5 * 5, - segmentation={ + 0, + 0, + [ + mock_cock_coco_annotation( + category_id=0, + bbox=(0, 0, 5, 5), + area=5 * 5, + segmentation={ "size": [5, 5], "counts": [2, 10, 2, 3, 2, 6], }, - iscrowd=True, )], - DoesNotRaise(), - ) # segmentation mask in single component, with holes in mask, expects RLE mask + iscrowd=True, + ) + ], + DoesNotRaise(), + ), # segmentation mask in single component, with holes in mask, expects RLE mask ], ) def test_detections_to_coco_annotations( @@ -560,7 +572,8 @@ def test_detections_to_coco_annotations( image_id: int, annotation_id: int, expected_result: List[Dict], - exception: Exception) -> None: + exception: Exception, +) -> None: with exception: result, _ = detections_to_coco_annotations( detections=detections, image_id=image_id, annotation_id=annotation_id From 1e8ee47db25b01ed09f413f88624a3f86e8eb55b Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Thu, 16 May 2024 08:56:58 +0200 Subject: [PATCH 093/136] craete copies of mask in _has_holes and _mask_has_multiple_segments --- supervision/dataset/formats/coco.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 19e7aa3e..db65ed57 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -108,9 +108,8 @@ def coco_annotations_to_detections( def _mask_has_holes(mask: np.ndarray) -> bool: - _, hierarchy = cv2.findContours( - mask.astype(np.uint8), cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE - ) + mask_uint8 = mask.astype(np.uint8) + _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) parent_countour_index = 3 for h in hierarchy[0]: if h[parent_countour_index] != -1: @@ -119,7 +118,8 @@ def _mask_has_holes(mask: np.ndarray) -> bool: def _mask_has_multiple_segments(mask: np.ndarray) -> bool: - number_of_labels, _ = cv2.connectedComponents(mask.astype(np.uint8), connectivity=4) + mask_uint8 = mask.astype(np.uint8) + number_of_labels, _ = cv2.connectedComponents(mask_uint8, connectivity=4) return number_of_labels > 2 From ce05a0c5441afb0a604ce9dc30997297952157c0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 06:57:24 +0000 Subject: [PATCH 094/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/formats/coco.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index db65ed57..5a55532c 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -108,7 +108,7 @@ def coco_annotations_to_detections( def _mask_has_holes(mask: np.ndarray) -> bool: - mask_uint8 = mask.astype(np.uint8) + mask_uint8 = mask.astype(np.uint8) _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) parent_countour_index = 3 for h in hierarchy[0]: @@ -118,7 +118,7 @@ def _mask_has_holes(mask: np.ndarray) -> bool: def _mask_has_multiple_segments(mask: np.ndarray) -> bool: - mask_uint8 = mask.astype(np.uint8) + mask_uint8 = mask.astype(np.uint8) number_of_labels, _ = cv2.connectedComponents(mask_uint8, connectivity=4) return number_of_labels > 2 From 2c769e2f720e198a6d784c9b977dd8ec7c1b17ae Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Thu, 16 May 2024 10:11:03 +0300 Subject: [PATCH 095/136] Missing return in another slicer callback --- docs/how_to/detect_small_objects.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how_to/detect_small_objects.md b/docs/how_to/detect_small_objects.md index 63801682..175b4f36 100644 --- a/docs/how_to/detect_small_objects.md +++ b/docs/how_to/detect_small_objects.md @@ -282,7 +282,7 @@ objects within each, and aggregating the results. def callback(image_slice: np.ndarray) -> sv.Detections: results = model.infer(image_slice)[0] - detections = sv.Detections.from_inference(results) + return sv.Detections.from_inference(results) slicer = sv.InferenceSlicer(callback = callback) detections = slicer(image) From 3ee72e38c35982983cd3fc971b31499a56d76f58 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Thu, 16 May 2024 09:20:26 +0200 Subject: [PATCH 096/136] check for empty mask --- supervision/dataset/formats/coco.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index db65ed57..91e11dcb 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -109,7 +109,8 @@ def coco_annotations_to_detections( def _mask_has_holes(mask: np.ndarray) -> bool: mask_uint8 = mask.astype(np.uint8) - _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) + _, hierarchy = cv2.findContours( + mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) parent_countour_index = 3 for h in hierarchy[0]: if h[parent_countour_index] != -1: @@ -118,6 +119,8 @@ def _mask_has_holes(mask: np.ndarray) -> bool: def _mask_has_multiple_segments(mask: np.ndarray) -> bool: + if mask.size == 0: + return False mask_uint8 = mask.astype(np.uint8) number_of_labels, _ = cv2.connectedComponents(mask_uint8, connectivity=4) return number_of_labels > 2 From b3b745508bf05895d4061fb8a8018a489e8dde2d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 07:23:53 +0000 Subject: [PATCH 097/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/formats/coco.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 73b5b32a..0e80afb5 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -120,7 +120,7 @@ def _mask_has_holes(mask: np.ndarray) -> bool: def _mask_has_multiple_segments(mask: np.ndarray) -> bool: if mask.size == 0: return False - mask_uint8 = mask.astype(np.uint8) + mask_uint8 = mask.astype(np.uint8) number_of_labels, _ = cv2.connectedComponents(mask_uint8, connectivity=4) return number_of_labels > 2 From 0252cf210c5fd78a0066f0b3c8399eab0df83bab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 May 2024 00:59:54 +0000 Subject: [PATCH 098/136] :arrow_up: Bump notebook from 7.1.3 to 7.2.0 Bumps [notebook](https://github.com/jupyter/notebook) from 7.1.3 to 7.2.0. - [Release notes](https://github.com/jupyter/notebook/releases) - [Changelog](https://github.com/jupyter/notebook/blob/main/CHANGELOG.md) - [Commits](https://github.com/jupyter/notebook/compare/@jupyter-notebook/tree@7.1.3...@jupyter-notebook/tree@7.2.0) --- updated-dependencies: - dependency-name: notebook dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- poetry.lock | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2a689d76..cf0350b1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1566,13 +1566,13 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (> [[package]] name = "jupyterlab" -version = "4.1.2" +version = "4.2.0" description = "JupyterLab computational environment" optional = false python-versions = ">=3.8" files = [ - {file = "jupyterlab-4.1.2-py3-none-any.whl", hash = "sha256:aa88193f03cf4d3555f6712f04d74112b5eb85edd7d222c588c7603a26d33c5b"}, - {file = "jupyterlab-4.1.2.tar.gz", hash = "sha256:5d6348b3ed4085181499f621b7dfb6eb0b1f57f3586857aadfc8e3bf4c4885f9"}, + {file = "jupyterlab-4.2.0-py3-none-any.whl", hash = "sha256:0dfe9278e25a145362289c555d9beb505697d269c10e99909766af7c440ad3cc"}, + {file = "jupyterlab-4.2.0.tar.gz", hash = "sha256:356e9205a6a2ab689c47c8fe4919dba6c076e376d03f26baadc05748c2435dd5"}, ] [package.dependencies] @@ -1580,23 +1580,24 @@ async-lru = ">=1.0.0" httpx = ">=0.25.0" importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} importlib-resources = {version = ">=1.4", markers = "python_version < \"3.9\""} -ipykernel = "*" +ipykernel = ">=6.5.0" jinja2 = ">=3.0.3" jupyter-core = "*" jupyter-lsp = ">=2.0.0" jupyter-server = ">=2.4.0,<3" -jupyterlab-server = ">=2.19.0,<3" +jupyterlab-server = ">=2.27.1,<3" notebook-shim = ">=0.2" packaging = "*" -tomli = {version = "*", markers = "python_version < \"3.11\""} +tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} tornado = ">=6.2.0" traitlets = "*" [package.extras] -dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.2.0)"] +dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.3.5)"] docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<7.3.0)", "sphinx-copybutton"] -docs-screenshots = ["altair (==5.2.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.1)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.0.post6)", "matplotlib (==3.8.2)", "nbconvert (>=7.0.0)", "pandas (==2.2.0)", "scipy (==1.12.0)", "vega-datasets (==0.9.0)"] +docs-screenshots = ["altair (==5.3.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.2)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.1.post2)", "matplotlib (==3.8.3)", "nbconvert (>=7.0.0)", "pandas (==2.2.1)", "scipy (==1.12.0)", "vega-datasets (==0.9.0)"] test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"] +upgrade-extension = ["copier (>=8,<10)", "jinja2-time (<0.3)", "pydantic (<2.0)", "pyyaml-include (<2.0)", "tomli-w (<2.0)"] [[package]] name = "jupyterlab-pygments" @@ -1611,13 +1612,13 @@ files = [ [[package]] name = "jupyterlab-server" -version = "2.25.3" +version = "2.27.1" description = "A set of server components for JupyterLab and JupyterLab like applications." optional = false python-versions = ">=3.8" files = [ - {file = "jupyterlab_server-2.25.3-py3-none-any.whl", hash = "sha256:c48862519fded9b418c71645d85a49b2f0ec50d032ba8316738e9276046088c1"}, - {file = "jupyterlab_server-2.25.3.tar.gz", hash = "sha256:846f125a8a19656611df5b03e5912c8393cea6900859baa64fa515eb64a8dc40"}, + {file = "jupyterlab_server-2.27.1-py3-none-any.whl", hash = "sha256:f5e26156e5258b24d532c84e7c74cc212e203bff93eb856f81c24c16daeecc75"}, + {file = "jupyterlab_server-2.27.1.tar.gz", hash = "sha256:097b5ac709b676c7284ac9c5e373f11930a561f52cd5a86e4fc7e5a9c8a8631d"}, ] [package.dependencies] @@ -1633,7 +1634,7 @@ requests = ">=2.31" [package.extras] docs = ["autodoc-traits", "jinja2 (<3.2.0)", "mistune (<4)", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinxcontrib-openapi (>0.8)"] openapi = ["openapi-core (>=0.18.0,<0.19.0)", "ruamel-yaml"] -test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-validator (>=0.6.0,<0.8.0)", "pytest (>=7.0)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter[server] (>=0.6.2)", "pytest-timeout", "requests-mock", "ruamel-yaml", "sphinxcontrib-spelling", "strict-rfc3339", "werkzeug"] +test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-validator (>=0.6.0,<0.8.0)", "pytest (>=7.0,<8)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter[server] (>=0.6.2)", "pytest-timeout", "requests-mock", "ruamel-yaml", "sphinxcontrib-spelling", "strict-rfc3339", "werkzeug"] [[package]] name = "jupyterlab-widgets" @@ -2484,26 +2485,26 @@ setuptools = "*" [[package]] name = "notebook" -version = "7.1.3" +version = "7.2.0" description = "Jupyter Notebook - A web-based notebook environment for interactive computing" optional = false python-versions = ">=3.8" files = [ - {file = "notebook-7.1.3-py3-none-any.whl", hash = "sha256:919b911e59f41f6e3857ce93c9d93535ba66bb090059712770e5968c07e1004d"}, - {file = "notebook-7.1.3.tar.gz", hash = "sha256:41fcebff44cf7bb9377180808bcbae066629b55d8c7722f1ebbe75ca44f9cfc1"}, + {file = "notebook-7.2.0-py3-none-any.whl", hash = "sha256:b4752d7407d6c8872fc505df0f00d3cae46e8efb033b822adacbaa3f1f3ce8f5"}, + {file = "notebook-7.2.0.tar.gz", hash = "sha256:34a2ba4b08ad5d19ec930db7484fb79746a1784be9e1a5f8218f9af8656a141f"}, ] [package.dependencies] jupyter-server = ">=2.4.0,<3" -jupyterlab = ">=4.1.1,<4.2" -jupyterlab-server = ">=2.22.1,<3" +jupyterlab = ">=4.2.0,<4.3" +jupyterlab-server = ">=2.27.1,<3" notebook-shim = ">=0.2,<0.3" tornado = ">=6.2.0" [package.extras] dev = ["hatch", "pre-commit"] docs = ["myst-parser", "nbsphinx", "pydata-sphinx-theme", "sphinx (>=1.3.6)", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.22.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"] +test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.27.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"] [[package]] name = "notebook-shim" From eb84d41c617710851aebf2f5e578bde1f7624511 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 May 2024 01:00:50 +0000 Subject: [PATCH 099/136] :arrow_up: Bump twine from 5.0.0 to 5.1.0 Bumps [twine](https://github.com/pypa/twine) from 5.0.0 to 5.1.0. - [Release notes](https://github.com/pypa/twine/releases) - [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst) - [Commits](https://github.com/pypa/twine/compare/5.0.0...5.1.0) --- updated-dependencies: - dependency-name: twine dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2a689d76..e72cb0b6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4009,13 +4009,13 @@ test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0, [[package]] name = "twine" -version = "5.0.0" +version = "5.1.0" description = "Collection of utilities for publishing packages on PyPI" optional = false python-versions = ">=3.8" files = [ - {file = "twine-5.0.0-py3-none-any.whl", hash = "sha256:a262933de0b484c53408f9edae2e7821c1c45a3314ff2df9bdd343aa7ab8edc0"}, - {file = "twine-5.0.0.tar.gz", hash = "sha256:89b0cc7d370a4b66421cc6102f269aa910fe0f1861c124f573cf2ddedbc10cf4"}, + {file = "twine-5.1.0-py3-none-any.whl", hash = "sha256:fe1d814395bfe50cfbe27783cb74efe93abeac3f66deaeb6c8390e4e92bacb43"}, + {file = "twine-5.1.0.tar.gz", hash = "sha256:4d74770c88c4fcaf8134d2a6a9d863e40f08255ff7d8e2acb3cbbd57d25f6e9d"}, ] [package.dependencies] From de5349ad6d02f9a69dfd419284291e23a52b602d Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Fri, 17 May 2024 09:17:38 +0300 Subject: [PATCH 100/136] docs: calculate_optimal_text_scale, not calculate_optimal_font_scale * Latter does not exist --- docs/utils/draw.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/utils/draw.md b/docs/utils/draw.md index 84758e06..f4b86a53 100644 --- a/docs/utils/draw.md +++ b/docs/utils/draw.md @@ -41,7 +41,7 @@ comments: true :::supervision.draw.utils.draw_image :::supervision.draw.utils.calculate_optimal_text_scale From 879ae9416a6598440c39fa8eaaf77f32916e4d51 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Fri, 17 May 2024 10:34:53 +0200 Subject: [PATCH 101/136] test polygon mask when mask in single component and no holes --- supervision/dataset/formats/coco.py | 2 -- test/dataset/formats/test_coco.py | 55 +++++++++++++++-------------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 73b5b32a..723ad463 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -118,8 +118,6 @@ def _mask_has_holes(mask: np.ndarray) -> bool: def _mask_has_multiple_segments(mask: np.ndarray) -> bool: - if mask.size == 0: - return False mask_uint8 = mask.astype(np.uint8) number_of_labels, _ = cv2.connectedComponents(mask_uint8, connectivity=4) return number_of_labels > 2 diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 139c74f8..11085a90 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -476,31 +476,32 @@ def test_build_coco_class_index_mapping( ], DoesNotRaise(), ), # no segmentation mask - # ( - # Detections( - # xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), - # class_id=np.array([0], dtype=int), - # mask=np.array( - # [ - # [ - # [1, 1, 1, 0, 0], - # [1, 1, 1, 0, 0], - # [1, 1, 1, 1, 1], - # [1, 1, 1, 1, 1], - # [1, 1, 1, 1, 1], - # ] - # ] - # ), - # ), - # 0, - # 0, - # [mock_cock_coco_annotation( - # category_id=0, - # bbox=(0, 0, 5, 5), - # area=5 * 5, - # segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]])], - # DoesNotRaise(), - # ), # segmentation mask in single component,no holes in mask, expects polygon mask + ( + Detections( + xyxy=np.array([[0, 0, 4, 5]], dtype=np.float32), + class_id=np.array([0], dtype=int), + mask=np.array( + [ + [ + [1, 1, 1, 1, 0], + [1, 1, 1, 1, 0], + [1, 1, 1, 1, 0], + [1, 1, 1, 1, 0], + [1, 1, 1, 1, 0], + ] + ] + ), + ), + 0, + 0, + [mock_cock_coco_annotation( + category_id=0, + bbox=(0, 0, 4, 5), + area=4 * 5, + segmentation=[[0, 0, 0, 4, 3, 4, 3, 0]])], + DoesNotRaise(), + ), # segmentation mask in single component,no holes in mask, + # expects polygon mask ( Detections( xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), @@ -564,7 +565,7 @@ def test_build_coco_class_index_mapping( ) ], DoesNotRaise(), - ), # segmentation mask in single component, with holes in mask, expects RLE mask + ), # segmentation mask in single component, with holes in mask, expects RLE mask ], ) def test_detections_to_coco_annotations( @@ -576,6 +577,6 @@ def test_detections_to_coco_annotations( ) -> None: with exception: result, _ = detections_to_coco_annotations( - detections=detections, image_id=image_id, annotation_id=annotation_id + detections=detections, image_id=image_id, annotation_id=annotation_id, ) assert result == expected_result From ab775d9f76c7487ffbf74b5dc978479a218b7341 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Fri, 17 May 2024 11:45:00 +0300 Subject: [PATCH 102/136] Attempt to fix SegFault * https://github.com/opencv/opencv-python/issues/706 --- supervision/dataset/formats/coco.py | 3 ++- test/dataset/formats/test_coco.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 0e80afb5..4f6c169e 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -121,7 +121,8 @@ def _mask_has_multiple_segments(mask: np.ndarray) -> bool: if mask.size == 0: return False mask_uint8 = mask.astype(np.uint8) - number_of_labels, _ = cv2.connectedComponents(mask_uint8, connectivity=4) + labels = np.zeros_like(mask_uint8, dtype=np.int32) + number_of_labels, _ = cv2.connectedComponents(mask_uint8, labels, connectivity=4) return number_of_labels > 2 diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 139c74f8..af6b31bc 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -500,7 +500,7 @@ def test_build_coco_class_index_mapping( # area=5 * 5, # segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]])], # DoesNotRaise(), - # ), # segmentation mask in single component,no holes in mask, expects polygon mask + # ), # seg mask in single component,no holes in mask, expects polygon ( Detections( xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), @@ -564,7 +564,7 @@ def test_build_coco_class_index_mapping( ) ], DoesNotRaise(), - ), # segmentation mask in single component, with holes in mask, expects RLE mask + ), # seg mask in single component, with holes in mask, expects RLE mask ], ) def test_detections_to_coco_annotations( From 0cd2c9d1464d2de77db97b7fc5550d05955477eb Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Fri, 17 May 2024 11:16:37 +0200 Subject: [PATCH 103/136] documentation change for as_coco --- supervision/dataset/core.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/supervision/dataset/core.py b/supervision/dataset/core.py index 551e96da..fbbbe6b7 100644 --- a/supervision/dataset/core.py +++ b/supervision/dataset/core.py @@ -430,6 +430,10 @@ class DetectionDataset(BaseDataset): """ Exports the dataset to COCO format. This method saves the images and their corresponding annotations in COCO format. + The format of the mask is determined automatically: + when a mask consists of multiple disconnected elements + or has holes the RLE format is used, + otherwise, the mask is encoded as a polygon. Args: images_directory_path (Optional[str]): The path to the directory From 8d432c78b1cb02d62a8051c972d92454a1cecd54 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 17 May 2024 09:16:57 +0000 Subject: [PATCH 104/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/core.py | 4 ++-- test/dataset/formats/test_coco.py | 37 ++++++++++++++++++------------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/supervision/dataset/core.py b/supervision/dataset/core.py index fbbbe6b7..6ae5844a 100644 --- a/supervision/dataset/core.py +++ b/supervision/dataset/core.py @@ -430,8 +430,8 @@ class DetectionDataset(BaseDataset): """ Exports the dataset to COCO format. This method saves the images and their corresponding annotations in COCO format. - The format of the mask is determined automatically: - when a mask consists of multiple disconnected elements + The format of the mask is determined automatically: + when a mask consists of multiple disconnected elements or has holes the RLE format is used, otherwise, the mask is encoded as a polygon. diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 6be0fa2c..8da8ccc8 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -478,30 +478,33 @@ def test_build_coco_class_index_mapping( ), # no segmentation mask ( Detections( - xyxy=np.array([[0, 0, 4, 5]], dtype=np.float32), - class_id=np.array([0], dtype=int), - mask=np.array( + xyxy=np.array([[0, 0, 4, 5]], dtype=np.float32), + class_id=np.array([0], dtype=int), + mask=np.array( + [ [ - [ - [1, 1, 1, 1, 0], - [1, 1, 1, 1, 0], - [1, 1, 1, 1, 0], - [1, 1, 1, 1, 0], - [1, 1, 1, 1, 0], - ] + [1, 1, 1, 1, 0], + [1, 1, 1, 1, 0], + [1, 1, 1, 1, 0], + [1, 1, 1, 1, 0], + [1, 1, 1, 1, 0], ] - ), + ] ), + ), 0, 0, - [mock_cock_coco_annotation( + [ + mock_cock_coco_annotation( category_id=0, bbox=(0, 0, 4, 5), area=4 * 5, - segmentation=[[0, 0, 0, 4, 3, 4, 3, 0]])], + segmentation=[[0, 0, 0, 4, 3, 4, 3, 0]], + ) + ], DoesNotRaise(), - ), # segmentation mask in single component,no holes in mask, - # expects polygon mask + ), # segmentation mask in single component,no holes in mask, + # expects polygon mask ( Detections( xyxy=np.array([[0, 0, 5, 5]], dtype=np.float32), @@ -577,6 +580,8 @@ def test_detections_to_coco_annotations( ) -> None: with exception: result, _ = detections_to_coco_annotations( - detections=detections, image_id=image_id, annotation_id=annotation_id, + detections=detections, + image_id=image_id, + annotation_id=annotation_id, ) assert result == expected_result From b761a5c32b6c04ec078c2b87bc2c1daecd343cbd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 00:53:40 +0000 Subject: [PATCH 105/136] :arrow_up: Bump pytest from 8.2.0 to 8.2.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.2.0 to 8.2.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.2.0...8.2.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index db11d029..9ed05cfb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3039,13 +3039,13 @@ tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [[package]] name = "pytest" -version = "8.2.0" +version = "8.2.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, - {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, + {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, + {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, ] [package.dependencies] From a40bf78bb20dcca49ad19c75b808a51d913da9f4 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Mon, 20 May 2024 18:17:03 +0200 Subject: [PATCH 106/136] coco mock method refactoring --- supervision/dataset/formats/coco.py | 2 +- test/dataset/formats/test_coco.py | 68 ++++++++++++++--------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index c8b73758..c07c2b52 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -157,7 +157,7 @@ def detections_to_coco_annotations( approximation_percentage=approximation_percentage, )[0].flatten() ) - ] # multicomponent masks supported only for rle format + ] coco_annotation = { "id": annotation_id, "image_id": image_id, diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 6be0fa2c..926a56e8 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -15,7 +15,7 @@ from supervision.dataset.formats.coco import ( ) -def mock_cock_coco_annotation( +def mock_coco_annotation( annotation_id: int = 0, image_id: int = 0, category_id: int = 0, @@ -107,10 +107,10 @@ def test_classes_to_coco_categories_and_back_to_classes( [ ([], {}, DoesNotRaise()), # empty coco annotations ( - [mock_cock_coco_annotation(annotation_id=0, image_id=0, category_id=0)], + [mock_coco_annotation(annotation_id=0, image_id=0, category_id=0)], { 0: [ - mock_cock_coco_annotation( + mock_coco_annotation( annotation_id=0, image_id=0, category_id=0 ) ] @@ -119,17 +119,17 @@ def test_classes_to_coco_categories_and_back_to_classes( ), # single coco annotation ( [ - mock_cock_coco_annotation(annotation_id=0, image_id=0, category_id=0), - mock_cock_coco_annotation(annotation_id=1, image_id=1, category_id=0), + mock_coco_annotation(annotation_id=0, image_id=0, category_id=0), + mock_coco_annotation(annotation_id=1, image_id=1, category_id=0), ], { 0: [ - mock_cock_coco_annotation( + mock_coco_annotation( annotation_id=0, image_id=0, category_id=0 ) ], 1: [ - mock_cock_coco_annotation( + mock_coco_annotation( annotation_id=1, image_id=1, category_id=0 ) ], @@ -138,41 +138,41 @@ def test_classes_to_coco_categories_and_back_to_classes( ), # two coco annotations ( [ - mock_cock_coco_annotation(annotation_id=0, image_id=0, category_id=0), - mock_cock_coco_annotation(annotation_id=1, image_id=1, category_id=1), - mock_cock_coco_annotation(annotation_id=2, image_id=1, category_id=2), - mock_cock_coco_annotation(annotation_id=3, image_id=2, category_id=3), - mock_cock_coco_annotation(annotation_id=4, image_id=3, category_id=1), - mock_cock_coco_annotation(annotation_id=5, image_id=3, category_id=2), - mock_cock_coco_annotation(annotation_id=5, image_id=3, category_id=3), + mock_coco_annotation(annotation_id=0, image_id=0, category_id=0), + mock_coco_annotation(annotation_id=1, image_id=1, category_id=1), + mock_coco_annotation(annotation_id=2, image_id=1, category_id=2), + mock_coco_annotation(annotation_id=3, image_id=2, category_id=3), + mock_coco_annotation(annotation_id=4, image_id=3, category_id=1), + mock_coco_annotation(annotation_id=5, image_id=3, category_id=2), + mock_coco_annotation(annotation_id=5, image_id=3, category_id=3), ], { 0: [ - mock_cock_coco_annotation( + mock_coco_annotation( annotation_id=0, image_id=0, category_id=0 ), ], 1: [ - mock_cock_coco_annotation( + mock_coco_annotation( annotation_id=1, image_id=1, category_id=1 ), - mock_cock_coco_annotation( + mock_coco_annotation( annotation_id=2, image_id=1, category_id=2 ), ], 2: [ - mock_cock_coco_annotation( + mock_coco_annotation( annotation_id=3, image_id=2, category_id=3 ), ], 3: [ - mock_cock_coco_annotation( + mock_coco_annotation( annotation_id=4, image_id=3, category_id=1 ), - mock_cock_coco_annotation( + mock_coco_annotation( annotation_id=5, image_id=3, category_id=2 ), - mock_cock_coco_annotation( + mock_coco_annotation( annotation_id=5, image_id=3, category_id=3 ), ], @@ -201,7 +201,7 @@ def test_group_coco_annotations_by_image_id( ), # empty image annotations ( [ - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(0, 0, 100, 100), area=100 * 100 ) ], @@ -215,10 +215,10 @@ def test_group_coco_annotations_by_image_id( ), # single image annotations ( [ - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(0, 0, 100, 100), area=100 * 100 ), - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(100, 100, 100, 100), area=100 * 100 ), ], @@ -234,7 +234,7 @@ def test_group_coco_annotations_by_image_id( ), # two image annotations ( [ - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(0, 0, 5, 5), area=5 * 5, @@ -262,7 +262,7 @@ def test_group_coco_annotations_by_image_id( ), # single image annotations with mask as polygon ( [ - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(0, 0, 5, 5), area=5 * 5, @@ -294,13 +294,13 @@ def test_group_coco_annotations_by_image_id( ), # single image annotations with mask, RLE segmentation mask ( [ - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(0, 0, 5, 5), area=5 * 5, segmentation=[[0, 0, 2, 0, 2, 2, 4, 2, 4, 4, 0, 4]], ), - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(3, 0, 2, 2), area=2 * 2, @@ -339,7 +339,7 @@ def test_group_coco_annotations_by_image_id( ), # two image annotations with mask, one mask as polygon ans second as RLE ( [ - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(3, 0, 2, 2), area=2 * 2, @@ -349,7 +349,7 @@ def test_group_coco_annotations_by_image_id( }, iscrowd=True, ), - mock_cock_coco_annotation( + mock_coco_annotation( category_id=1, bbox=(0, 0, 5, 5), area=5 * 5, @@ -470,7 +470,7 @@ def test_build_coco_class_index_mapping( 0, 0, [ - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(0, 0, 100, 100), area=100 * 100 ) ], @@ -494,7 +494,7 @@ def test_build_coco_class_index_mapping( ), 0, 0, - [mock_cock_coco_annotation( + [mock_coco_annotation( category_id=0, bbox=(0, 0, 4, 5), area=4 * 5, @@ -521,7 +521,7 @@ def test_build_coco_class_index_mapping( 0, 0, [ - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(0, 0, 5, 5), area=5 * 5, @@ -553,7 +553,7 @@ def test_build_coco_class_index_mapping( 0, 0, [ - mock_cock_coco_annotation( + mock_coco_annotation( category_id=0, bbox=(0, 0, 5, 5), area=5 * 5, From 2626263ff3f552634b301501b4824df160f2ee47 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Mon, 20 May 2024 18:54:10 +0200 Subject: [PATCH 107/136] move has_holes and mask_has_multiple_segments to detections utils --- supervision/dataset/formats/coco.py | 25 ++++----------- supervision/detection/utils.py | 47 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index c07c2b52..b1d67fff 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -14,7 +14,11 @@ from supervision.dataset.utils import ( rle_to_mask, ) from supervision.detection.core import Detections -from supervision.detection.utils import polygon_to_mask +from supervision.detection.utils import ( + polygon_to_mask, + mask_has_multiple_segments, + mask_has_holes +) from supervision.utils.file import read_json_file, save_json_file @@ -107,23 +111,6 @@ def coco_annotations_to_detections( return Detections(xyxy=xyxy, class_id=np.asarray(class_ids, dtype=int)) -def _mask_has_holes(mask: np.ndarray) -> bool: - mask_uint8 = mask.astype(np.uint8) - _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) - parent_countour_index = 3 - for h in hierarchy[0]: - if h[parent_countour_index] != -1: - return True - return False - - -def _mask_has_multiple_segments(mask: np.ndarray) -> bool: - mask_uint8 = mask.astype(np.uint8) - labels = np.zeros_like(mask_uint8, dtype=np.int32) - number_of_labels, _ = cv2.connectedComponents(mask_uint8, labels, connectivity=4) - return number_of_labels > 2 - - def detections_to_coco_annotations( detections: Detections, image_id: int, @@ -138,7 +125,7 @@ def detections_to_coco_annotations( segmentation = [] iscrowd = 0 if mask is not None: - iscrowd = _mask_has_holes(mask=mask) or _mask_has_multiple_segments( + iscrowd = mask_has_holes(mask=mask) or mask_has_multiple_segments( mask=mask ) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 3eeba5b4..1dd208bb 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -3,6 +3,7 @@ from typing import Dict, List, Optional, Tuple, Union import cv2 import numpy as np +import numpy.typing as npt from supervision.config import CLASS_NAME_DATA_FIELD @@ -766,3 +767,49 @@ def get_data_item( raise TypeError(f"Unsupported data type for key '{key}': {type(value)}") return subset_data + + +def mask_has_holes(mask: npt.NDArray[np.bool_]) -> bool: + """ + Checks if target objects in binary mask contain holes + (A hole is when background pixels are fully enclosed by foreground pixels) + + Args: + mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground + object and `False` indicates background. + Returns: + True when holes are detected, False otherwise. + """ + mask_uint8 = mask.astype(np.uint8) + _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) + parent_countour_index = 3 + for h in hierarchy[0]: + if h[parent_countour_index] != -1: + return True + return False + + +def mask_has_multiple_segments(mask: npt.NDArray[np.bool_], + connectivity:int = 4) -> bool: + """ + Checks if the binary mask consists of multiple not connected elements representing + the foreground objects. + Args: + mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground + object and `False` indicates background. + connectivity (int) : Default: 4 is 4-way connectivity, which means that + foreground pixels are the part of the same segment/component + if their edges touch. + Alternatively: 8 for 8-way connectivity, when foreground pixels are + connected by their edges or corners touch. + Returns: + True when the mask contains multiple not connected components, False otherwise. + """ + if connectivity!=4 and connectivity!=8: + raise ValueError('''Incorrect connectivity value,''' + ''' possible connectivity values: 4 or 8''') + mask_uint8 = mask.astype(np.uint8) + labels = np.zeros_like(mask_uint8, dtype=np.int32) + number_of_labels, _ = cv2.connectedComponents(mask_uint8, labels, + connectivity=connectivity) + return number_of_labels > 2 From 7ee84c48e02c22c6e51bb42f71372f5851cca65c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 16:56:46 +0000 Subject: [PATCH 108/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/formats/coco.py | 8 ++--- supervision/detection/utils.py | 30 +++++++++-------- test/dataset/formats/test_coco.py | 51 +++++++---------------------- 3 files changed, 32 insertions(+), 57 deletions(-) diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index b1d67fff..3c684871 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -15,9 +15,9 @@ from supervision.dataset.utils import ( ) from supervision.detection.core import Detections from supervision.detection.utils import ( - polygon_to_mask, + mask_has_holes, mask_has_multiple_segments, - mask_has_holes + polygon_to_mask, ) from supervision.utils.file import read_json_file, save_json_file @@ -125,9 +125,7 @@ def detections_to_coco_annotations( segmentation = [] iscrowd = 0 if mask is not None: - iscrowd = mask_has_holes(mask=mask) or mask_has_multiple_segments( - mask=mask - ) + iscrowd = mask_has_holes(mask=mask) or mask_has_multiple_segments(mask=mask) if iscrowd: segmentation = { diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 1dd208bb..f00641fb 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -771,7 +771,7 @@ def get_data_item( def mask_has_holes(mask: npt.NDArray[np.bool_]) -> bool: """ - Checks if target objects in binary mask contain holes + Checks if target objects in binary mask contain holes (A hole is when background pixels are fully enclosed by foreground pixels) Args: @@ -789,27 +789,31 @@ def mask_has_holes(mask: npt.NDArray[np.bool_]) -> bool: return False -def mask_has_multiple_segments(mask: npt.NDArray[np.bool_], - connectivity:int = 4) -> bool: +def mask_has_multiple_segments( + mask: npt.NDArray[np.bool_], connectivity: int = 4 +) -> bool: """ - Checks if the binary mask consists of multiple not connected elements representing + Checks if the binary mask consists of multiple not connected elements representing the foreground objects. Args: mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground object and `False` indicates background. - connectivity (int) : Default: 4 is 4-way connectivity, which means that - foreground pixels are the part of the same segment/component - if their edges touch. - Alternatively: 8 for 8-way connectivity, when foreground pixels are + connectivity (int) : Default: 4 is 4-way connectivity, which means that + foreground pixels are the part of the same segment/component + if their edges touch. + Alternatively: 8 for 8-way connectivity, when foreground pixels are connected by their edges or corners touch. Returns: True when the mask contains multiple not connected components, False otherwise. """ - if connectivity!=4 and connectivity!=8: - raise ValueError('''Incorrect connectivity value,''' - ''' possible connectivity values: 4 or 8''') + if connectivity != 4 and connectivity != 8: + raise ValueError( + """Incorrect connectivity value,""" + """ possible connectivity values: 4 or 8""" + ) mask_uint8 = mask.astype(np.uint8) labels = np.zeros_like(mask_uint8, dtype=np.int32) - number_of_labels, _ = cv2.connectedComponents(mask_uint8, labels, - connectivity=connectivity) + number_of_labels, _ = cv2.connectedComponents( + mask_uint8, labels, connectivity=connectivity + ) return number_of_labels > 2 diff --git a/test/dataset/formats/test_coco.py b/test/dataset/formats/test_coco.py index 9c89b964..7e269dae 100644 --- a/test/dataset/formats/test_coco.py +++ b/test/dataset/formats/test_coco.py @@ -108,13 +108,7 @@ def test_classes_to_coco_categories_and_back_to_classes( ([], {}, DoesNotRaise()), # empty coco annotations ( [mock_coco_annotation(annotation_id=0, image_id=0, category_id=0)], - { - 0: [ - mock_coco_annotation( - annotation_id=0, image_id=0, category_id=0 - ) - ] - }, + {0: [mock_coco_annotation(annotation_id=0, image_id=0, category_id=0)]}, DoesNotRaise(), ), # single coco annotation ( @@ -123,16 +117,8 @@ def test_classes_to_coco_categories_and_back_to_classes( mock_coco_annotation(annotation_id=1, image_id=1, category_id=0), ], { - 0: [ - mock_coco_annotation( - annotation_id=0, image_id=0, category_id=0 - ) - ], - 1: [ - mock_coco_annotation( - annotation_id=1, image_id=1, category_id=0 - ) - ], + 0: [mock_coco_annotation(annotation_id=0, image_id=0, category_id=0)], + 1: [mock_coco_annotation(annotation_id=1, image_id=1, category_id=0)], }, DoesNotRaise(), ), # two coco annotations @@ -148,33 +134,19 @@ def test_classes_to_coco_categories_and_back_to_classes( ], { 0: [ - mock_coco_annotation( - annotation_id=0, image_id=0, category_id=0 - ), + mock_coco_annotation(annotation_id=0, image_id=0, category_id=0), ], 1: [ - mock_coco_annotation( - annotation_id=1, image_id=1, category_id=1 - ), - mock_coco_annotation( - annotation_id=2, image_id=1, category_id=2 - ), + mock_coco_annotation(annotation_id=1, image_id=1, category_id=1), + mock_coco_annotation(annotation_id=2, image_id=1, category_id=2), ], 2: [ - mock_coco_annotation( - annotation_id=3, image_id=2, category_id=3 - ), + mock_coco_annotation(annotation_id=3, image_id=2, category_id=3), ], 3: [ - mock_coco_annotation( - annotation_id=4, image_id=3, category_id=1 - ), - mock_coco_annotation( - annotation_id=5, image_id=3, category_id=2 - ), - mock_coco_annotation( - annotation_id=5, image_id=3, category_id=3 - ), + mock_coco_annotation(annotation_id=4, image_id=3, category_id=1), + mock_coco_annotation(annotation_id=5, image_id=3, category_id=2), + mock_coco_annotation(annotation_id=5, image_id=3, category_id=3), ], }, DoesNotRaise(), @@ -494,7 +466,8 @@ def test_build_coco_class_index_mapping( ), 0, 0, - [mock_coco_annotation( + [ + mock_coco_annotation( category_id=0, bbox=(0, 0, 4, 5), area=4 * 5, From 68f07e18f38c208b5e9177b4a957d511a2eaf247 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Mon, 20 May 2024 19:23:22 +0200 Subject: [PATCH 109/136] tests for mask_has_holes --- supervision/detection/utils.py | 15 ++++++--- test/detection/test_utils.py | 59 ++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 1dd208bb..b39b7b38 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -782,10 +782,12 @@ def mask_has_holes(mask: npt.NDArray[np.bool_]) -> bool: """ mask_uint8 = mask.astype(np.uint8) _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) - parent_countour_index = 3 - for h in hierarchy[0]: - if h[parent_countour_index] != -1: - return True + + if hierarchy: # at least one contour was found + parent_countour_index = 3 + for h in hierarchy[0]: + if h[parent_countour_index] != -1: + return True return False @@ -794,6 +796,7 @@ def mask_has_multiple_segments(mask: npt.NDArray[np.bool_], """ Checks if the binary mask consists of multiple not connected elements representing the foreground objects. + Args: mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground object and `False` indicates background. @@ -802,8 +805,12 @@ def mask_has_multiple_segments(mask: npt.NDArray[np.bool_], if their edges touch. Alternatively: 8 for 8-way connectivity, when foreground pixels are connected by their edges or corners touch. + Returns: True when the mask contains multiple not connected components, False otherwise. + + Raises: + ValueError: If connectivity(int) parameter value is not 4 or 8. """ if connectivity!=4 and connectivity!=8: raise ValueError('''Incorrect connectivity value,''' diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index 1c4a1d34..1214cfed 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -2,6 +2,7 @@ from contextlib import ExitStack as DoesNotRaise from typing import Any, Dict, List, Optional, Tuple import numpy as np +import numpy.typing as npt import pytest from supervision.config import CLASS_NAME_DATA_FIELD @@ -16,6 +17,8 @@ from supervision.detection.utils import ( move_boxes, process_roboflow_result, scale_boxes, + mask_has_holes, + mask_has_multiple_segments, ) TEST_MASK = np.zeros((1, 1000, 1000), dtype=bool) @@ -1203,3 +1206,59 @@ def test_get_data_item( assert ( result[key] == expected_result[key] ), f"Mismatch in non-array data for key {key}" + + +@pytest.mark.parametrize( + "mask, expected_result, exception", + [ + (np.array([[0, 0, 0, 0], + [0, 1, 1, 0], + [0, 1, 0, 0], + [0, 1, 1, 0]]).astype(bool), + False, + DoesNotRaise(), + ), # foreground object in one continuous piece + (np.array([[1, 0, 0, 0], + [1, 0, 0, 0], + [0, 0, 0, 0], + [0, 1, 1, 0]]).astype(bool), + False, + DoesNotRaise(), + ), # foreground object in 2 seperate elements + (np.array([[0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]]).astype(bool), + False, + DoesNotRaise(), + ), # no foreground pixels in mask + (np.array([[1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1]]).astype(bool), + False, + DoesNotRaise(), + ), # only foreground pixels in mask + (np.array([[1, 1, 1, 0], + [1, 0, 1, 0], + [1, 1, 1, 0], + [0, 0, 0, 0]]).astype(bool), + True, + DoesNotRaise(), + ), # foreground object has 1 hole + (np.array([[1, 1, 1, 0], + [1, 0, 1, 1], + [1, 1, 0, 1], + [0, 1, 1, 1]]).astype(bool), + True, + DoesNotRaise(), + ), # foreground object has 2 holes + ], +) +def test_mask_has_holes( + mask: npt.NDArray[np.bool_], expected_result: bool, exception: Exception +) -> None: + with exception: + result = mask_has_holes(mask) + assert result == expected_result + \ No newline at end of file From c26eb1a70f4271930ca0f17ee3f13aa70892b0be Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Mon, 20 May 2024 19:52:48 +0200 Subject: [PATCH 110/136] unit tests for test_mask_has_multiple_segments --- test/detection/test_utils.py | 81 +++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index 1214cfed..a9472f85 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -1261,4 +1261,83 @@ def test_mask_has_holes( with exception: result = mask_has_holes(mask) assert result == expected_result - \ No newline at end of file + + +@pytest.mark.parametrize( + "mask, connectivity, expected_result, exception", + [ + (np.array([[0, 0, 0, 0], + [0, 1, 1, 0], + [0, 1, 0, 0], + [0, 1, 1, 0]]).astype(bool), + 4, + False, + DoesNotRaise(), + ), # foreground object in one continuous piece + (np.array([[1, 0, 0, 0], + [1, 0, 0, 0], + [0, 0, 0, 0], + [0, 1, 1, 0]]).astype(bool), + 4, + True, + DoesNotRaise(), + ), # foreground object in 2 seperate elements + (np.array([[0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]]).astype(bool), + 4, + False, + DoesNotRaise(), + ), # no foreground pixels in mask + (np.array([[1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1]]).astype(bool), + 4, + False, + DoesNotRaise(), + ), # only foreground pixels in mask + (np.array([[1, 1, 1, 0], + [1, 0, 1, 1], + [1, 1, 0, 1], + [0, 1, 1, 1]]).astype(bool), + 4, + False, + DoesNotRaise(), + ), # foreground object has 2 holes, but is in single piece + (np.array([[1, 1, 0, 0], + [1, 1, 0, 1], + [1, 0, 1, 1], + [0, 0, 1, 1]]).astype(bool), + 4, + True, + DoesNotRaise(), + ), # foreground object in 2 elements with respect to 4-way connectivity + (np.array([[1, 1, 0, 0], + [1, 1, 0, 1], + [1, 0, 1, 1], + [0, 0, 1, 1]]).astype(bool), + 8, + False, + DoesNotRaise(), + ), # foreground object in single piece with respect to 8-way connectivity + (np.array([[1, 1, 0, 0], + [1, 1, 0, 1], + [1, 0, 1, 1], + [0, 0, 1, 1]]).astype(bool), + 5, + None, + pytest.raises(ValueError), + ), # Incorrect connectivity parameter value, raises ValueError + ], +) +def test_mask_has_multiple_segments( + mask: npt.NDArray[np.bool_], + connectivity: int, + expected_result: bool, + exception: Exception +) -> None: + with exception: + result = mask_has_multiple_segments(mask = mask, connectivity=connectivity) + assert result == expected_result From dfac30cba7eb626236d0c60bf01091e42db82949 Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Mon, 20 May 2024 20:45:17 +0200 Subject: [PATCH 111/136] change docu for mask_has_multiple_segments and mask_has_holes and add to global __init__.py --- docs/detection/utils.md | 12 ++++++++++++ supervision/__init__.py | 2 ++ 2 files changed, 14 insertions(+) diff --git a/docs/detection/utils.md b/docs/detection/utils.md index abacdc21..9271c23d 100644 --- a/docs/detection/utils.md +++ b/docs/detection/utils.md @@ -70,3 +70,15 @@ status: new :::supervision.detection.utils.scale_boxes + + + +:::supervision.detection.utils.mask_has_holes + + + +:::supervision.detection.utils.mask_has_multiple_segments diff --git a/supervision/__init__.py b/supervision/__init__.py index 2bc72944..caa0e5ae 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -56,6 +56,8 @@ from supervision.detection.utils import ( polygon_to_mask, polygon_to_xyxy, scale_boxes, + mask_has_holes, + mask_has_multiple_segments, ) from supervision.draw.color import Color, ColorPalette from supervision.draw.utils import ( From ab55e810876749e4bc62397c9aa39f1e71d4812c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 19:41:32 +0000 Subject: [PATCH 112/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/detection/utils.md | 2 +- supervision/__init__.py | 4 +- supervision/detection/utils.py | 2 +- test/detection/test_utils.py | 228 ++++++++++++++++----------------- 4 files changed, 118 insertions(+), 118 deletions(-) diff --git a/docs/detection/utils.md b/docs/detection/utils.md index 8b9f91f7..0c42a69e 100644 --- a/docs/detection/utils.md +++ b/docs/detection/utils.md @@ -99,4 +99,4 @@ status: new

mask_has_multiple_segments

-:::supervision.detection.utils.mask_has_multiple_segments \ No newline at end of file +:::supervision.detection.utils.mask_has_multiple_segments diff --git a/supervision/__init__.py b/supervision/__init__.py index 7f50a7ad..51b43ef0 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -50,6 +50,8 @@ from supervision.detection.utils import ( calculate_masks_centroids, clip_boxes, filter_polygons_by_area, + mask_has_holes, + mask_has_multiple_segments, mask_iou_batch, mask_non_max_suppression, mask_to_polygons, @@ -60,8 +62,6 @@ from supervision.detection.utils import ( polygon_to_mask, polygon_to_xyxy, scale_boxes, - mask_has_holes, - mask_has_multiple_segments, ) from supervision.draw.color import Color, ColorPalette from supervision.draw.utils import ( diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 9dd3d734..35510060 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -857,7 +857,7 @@ def mask_has_holes(mask: npt.NDArray[np.bool_]) -> bool: mask_uint8 = mask.astype(np.uint8) _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) - if hierarchy: # at least one contour was found + if hierarchy: # at least one contour was found parent_countour_index = 3 for h in hierarchy[0]: if h[parent_countour_index] != -1: diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index b72ca4bf..2821aed2 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -12,13 +12,13 @@ from supervision.detection.utils import ( clip_boxes, filter_polygons_by_area, get_data_item, + mask_has_holes, + mask_has_multiple_segments, mask_non_max_suppression, merge_data, move_boxes, process_roboflow_result, scale_boxes, - mask_has_holes, - mask_has_multiple_segments, ) TEST_MASK = np.zeros((1, 1000, 1000), dtype=bool) @@ -1273,48 +1273,48 @@ def test_get_data_item( @pytest.mark.parametrize( "mask, expected_result, exception", [ - (np.array([[0, 0, 0, 0], - [0, 1, 1, 0], - [0, 1, 0, 0], - [0, 1, 1, 0]]).astype(bool), - False, - DoesNotRaise(), - ), # foreground object in one continuous piece - (np.array([[1, 0, 0, 0], - [1, 0, 0, 0], - [0, 0, 0, 0], - [0, 1, 1, 0]]).astype(bool), - False, - DoesNotRaise(), - ), # foreground object in 2 seperate elements - (np.array([[0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0]]).astype(bool), - False, - DoesNotRaise(), - ), # no foreground pixels in mask - (np.array([[1, 1, 1, 1], - [1, 1, 1, 1], - [1, 1, 1, 1], - [1, 1, 1, 1]]).astype(bool), - False, - DoesNotRaise(), - ), # only foreground pixels in mask - (np.array([[1, 1, 1, 0], - [1, 0, 1, 0], - [1, 1, 1, 0], - [0, 0, 0, 0]]).astype(bool), - True, - DoesNotRaise(), - ), # foreground object has 1 hole - (np.array([[1, 1, 1, 0], - [1, 0, 1, 1], - [1, 1, 0, 1], - [0, 1, 1, 1]]).astype(bool), - True, - DoesNotRaise(), - ), # foreground object has 2 holes + ( + np.array([[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 0, 0], [0, 1, 1, 0]]).astype( + bool + ), + False, + DoesNotRaise(), + ), # foreground object in one continuous piece + ( + np.array([[1, 0, 0, 0], [1, 0, 0, 0], [0, 0, 0, 0], [0, 1, 1, 0]]).astype( + bool + ), + False, + DoesNotRaise(), + ), # foreground object in 2 seperate elements + ( + np.array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]).astype( + bool + ), + False, + DoesNotRaise(), + ), # no foreground pixels in mask + ( + np.array([[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]).astype( + bool + ), + False, + DoesNotRaise(), + ), # only foreground pixels in mask + ( + np.array([[1, 1, 1, 0], [1, 0, 1, 0], [1, 1, 1, 0], [0, 0, 0, 0]]).astype( + bool + ), + True, + DoesNotRaise(), + ), # foreground object has 1 hole + ( + np.array([[1, 1, 1, 0], [1, 0, 1, 1], [1, 1, 0, 1], [0, 1, 1, 1]]).astype( + bool + ), + True, + DoesNotRaise(), + ), # foreground object has 2 holes ], ) def test_mask_has_holes( @@ -1322,84 +1322,84 @@ def test_mask_has_holes( ) -> None: with exception: result = mask_has_holes(mask) - assert result == expected_result + assert result == expected_result @pytest.mark.parametrize( "mask, connectivity, expected_result, exception", [ - (np.array([[0, 0, 0, 0], - [0, 1, 1, 0], - [0, 1, 0, 0], - [0, 1, 1, 0]]).astype(bool), - 4, - False, - DoesNotRaise(), - ), # foreground object in one continuous piece - (np.array([[1, 0, 0, 0], - [1, 0, 0, 0], - [0, 0, 0, 0], - [0, 1, 1, 0]]).astype(bool), - 4, - True, - DoesNotRaise(), - ), # foreground object in 2 seperate elements - (np.array([[0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0]]).astype(bool), - 4, - False, - DoesNotRaise(), - ), # no foreground pixels in mask - (np.array([[1, 1, 1, 1], - [1, 1, 1, 1], - [1, 1, 1, 1], - [1, 1, 1, 1]]).astype(bool), - 4, - False, - DoesNotRaise(), - ), # only foreground pixels in mask - (np.array([[1, 1, 1, 0], - [1, 0, 1, 1], - [1, 1, 0, 1], - [0, 1, 1, 1]]).astype(bool), - 4, - False, - DoesNotRaise(), - ), # foreground object has 2 holes, but is in single piece - (np.array([[1, 1, 0, 0], - [1, 1, 0, 1], - [1, 0, 1, 1], - [0, 0, 1, 1]]).astype(bool), - 4, - True, - DoesNotRaise(), - ), # foreground object in 2 elements with respect to 4-way connectivity - (np.array([[1, 1, 0, 0], - [1, 1, 0, 1], - [1, 0, 1, 1], - [0, 0, 1, 1]]).astype(bool), - 8, - False, - DoesNotRaise(), - ), # foreground object in single piece with respect to 8-way connectivity - (np.array([[1, 1, 0, 0], - [1, 1, 0, 1], - [1, 0, 1, 1], - [0, 0, 1, 1]]).astype(bool), - 5, - None, - pytest.raises(ValueError), - ), # Incorrect connectivity parameter value, raises ValueError + ( + np.array([[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 0, 0], [0, 1, 1, 0]]).astype( + bool + ), + 4, + False, + DoesNotRaise(), + ), # foreground object in one continuous piece + ( + np.array([[1, 0, 0, 0], [1, 0, 0, 0], [0, 0, 0, 0], [0, 1, 1, 0]]).astype( + bool + ), + 4, + True, + DoesNotRaise(), + ), # foreground object in 2 seperate elements + ( + np.array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]).astype( + bool + ), + 4, + False, + DoesNotRaise(), + ), # no foreground pixels in mask + ( + np.array([[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]).astype( + bool + ), + 4, + False, + DoesNotRaise(), + ), # only foreground pixels in mask + ( + np.array([[1, 1, 1, 0], [1, 0, 1, 1], [1, 1, 0, 1], [0, 1, 1, 1]]).astype( + bool + ), + 4, + False, + DoesNotRaise(), + ), # foreground object has 2 holes, but is in single piece + ( + np.array([[1, 1, 0, 0], [1, 1, 0, 1], [1, 0, 1, 1], [0, 0, 1, 1]]).astype( + bool + ), + 4, + True, + DoesNotRaise(), + ), # foreground object in 2 elements with respect to 4-way connectivity + ( + np.array([[1, 1, 0, 0], [1, 1, 0, 1], [1, 0, 1, 1], [0, 0, 1, 1]]).astype( + bool + ), + 8, + False, + DoesNotRaise(), + ), # foreground object in single piece with respect to 8-way connectivity + ( + np.array([[1, 1, 0, 0], [1, 1, 0, 1], [1, 0, 1, 1], [0, 0, 1, 1]]).astype( + bool + ), + 5, + None, + pytest.raises(ValueError), + ), # Incorrect connectivity parameter value, raises ValueError ], ) def test_mask_has_multiple_segments( mask: npt.NDArray[np.bool_], - connectivity: int, - expected_result: bool, - exception: Exception + connectivity: int, + expected_result: bool, + exception: Exception, ) -> None: with exception: - result = mask_has_multiple_segments(mask = mask, connectivity=connectivity) - assert result == expected_result + result = mask_has_multiple_segments(mask=mask, connectivity=connectivity) + assert result == expected_result From 6dbf3f61e3e764c56c6152f18088f7624354d1dd Mon Sep 17 00:00:00 2001 From: magda skoczen Date: Mon, 20 May 2024 21:47:04 +0200 Subject: [PATCH 113/136] fix for unit tests for test_mask_has_holes --- supervision/detection/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 35510060..89728c7a 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -857,7 +857,7 @@ def mask_has_holes(mask: npt.NDArray[np.bool_]) -> bool: mask_uint8 = mask.astype(np.uint8) _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) - if hierarchy: # at least one contour was found + if hierarchy is not None: # at least one contour was found parent_countour_index = 3 for h in hierarchy[0]: if h[parent_countour_index] != -1: From 414d3e9afbbd9326bb91f3e2eb58a426bce4e8c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 01:01:12 +0000 Subject: [PATCH 114/136] --- updated-dependencies: - dependency-name: requests dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9ed05cfb..38eae842 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3461,13 +3461,13 @@ files = [ [[package]] name = "requests" -version = "2.31.0" +version = "2.32.1" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, + {file = "requests-2.32.1-py3-none-any.whl", hash = "sha256:21ac9465cdf8c1650fe1ecde8a71669a93d4e6f147550483a2967d08396a56a5"}, + {file = "requests-2.32.1.tar.gz", hash = "sha256:eb97e87e64c79e64e5b8ac75cee9dd1f97f49e289b083ee6be96268930725685"}, ] [package.dependencies] @@ -4258,4 +4258,4 @@ desktop = ["opencv-python"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "29af5aa06f97e77a2dba94c5a6d77d7d1903448724df07416026a378d3c6a64d" +content-hash = "7ecea27cde915f67ee71e0ed55cabd890c6473e5f222434efd0b1f4392713312" diff --git a/pyproject.toml b/pyproject.toml index 8ceacbff..252bb49d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ pyyaml = ">=5.3" defusedxml = "^0.7.1" opencv-python = { version = ">=4.5.5.64", optional = true } opencv-python-headless = ">=4.5.5.64" -requests = { version = ">=2.26.0,<=2.31.0", optional = true } +requests = { version = ">=2.26.0,<=2.32.1", optional = true } tqdm = { version = ">=4.62.3,<=4.66.4", optional = true } pillow = ">=9.4" From 32f773548e6ce233c8fd0ae6afd1327861940e15 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 01:02:35 +0000 Subject: [PATCH 115/136] --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9ed05cfb..c9807aef 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2199,13 +2199,13 @@ pygments = ">2.12.0" [[package]] name = "mkdocs-material" -version = "9.5.23" +version = "9.5.24" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.23-py3-none-any.whl", hash = "sha256:ffd08a5beaef3cd135aceb58ded8b98bbbbf2b70e5b656f6a14a63c917d9b001"}, - {file = "mkdocs_material-9.5.23.tar.gz", hash = "sha256:4627fc3f15de2cba2bde9debc2fd59b9888ef494beabfe67eb352e23d14bf288"}, + {file = "mkdocs_material-9.5.24-py3-none-any.whl", hash = "sha256:e12cd75954c535b61e716f359cf2a5056bf4514889d17161fdebd5df4b0153c6"}, + {file = "mkdocs_material-9.5.24.tar.gz", hash = "sha256:02d5aaba0ee755e707c3ef6e748f9acb7b3011187c0ea766db31af8905078a34"}, ] [package.dependencies] From f2716252d59ac60da041f1e3bab1b1744726d79a Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Tue, 21 May 2024 11:43:09 +0200 Subject: [PATCH 116/136] small refactor + docs improvements --- docs/detection/utils.md | 10 +-- supervision/__init__.py | 4 +- supervision/dataset/formats/coco.py | 6 +- supervision/detection/utils.py | 106 ++++++++++++++++++++++------ test/detection/test_utils.py | 12 ++-- 5 files changed, 100 insertions(+), 38 deletions(-) diff --git a/docs/detection/utils.md b/docs/detection/utils.md index 0c42a69e..f9c9473b 100644 --- a/docs/detection/utils.md +++ b/docs/detection/utils.md @@ -78,7 +78,7 @@ status: new :::supervision.detection.utils.scale_boxes :::supervision.detection.utils.clip_boxes @@ -90,13 +90,13 @@ status: new :::supervision.detection.utils.pad_boxes -:::supervision.detection.utils.mask_has_holes +:::supervision.detection.utils.contains_holes -:::supervision.detection.utils.mask_has_multiple_segments +:::supervision.detection.utils.contains_multiple_segments diff --git a/supervision/__init__.py b/supervision/__init__.py index 51b43ef0..af5f8dcc 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -50,8 +50,8 @@ from supervision.detection.utils import ( calculate_masks_centroids, clip_boxes, filter_polygons_by_area, - mask_has_holes, - mask_has_multiple_segments, + contains_holes, + contains_multiple_segments, mask_iou_batch, mask_non_max_suppression, mask_to_polygons, diff --git a/supervision/dataset/formats/coco.py b/supervision/dataset/formats/coco.py index 3c684871..353e33f5 100644 --- a/supervision/dataset/formats/coco.py +++ b/supervision/dataset/formats/coco.py @@ -15,8 +15,8 @@ from supervision.dataset.utils import ( ) from supervision.detection.core import Detections from supervision.detection.utils import ( - mask_has_holes, - mask_has_multiple_segments, + contains_holes, + contains_multiple_segments, polygon_to_mask, ) from supervision.utils.file import read_json_file, save_json_file @@ -125,7 +125,7 @@ def detections_to_coco_annotations( segmentation = [] iscrowd = 0 if mask is not None: - iscrowd = mask_has_holes(mask=mask) or mask_has_multiple_segments(mask=mask) + iscrowd = contains_holes(mask=mask) or contains_multiple_segments(mask=mask) if iscrowd: segmentation = { diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 89728c7a..5a8e7ba2 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -609,13 +609,16 @@ def move_boxes(xyxy: np.ndarray, offset: np.ndarray) -> np.ndarray: import numpy as np import supervision as sv - boxes = np.array([[10, 10, 20, 20], [30, 30, 40, 40]]) + xyxy = np.array([ + [10, 10, 20, 20], + [30, 30, 40, 40] + ]) offset = np.array([5, 5]) - moved_box = sv.move_boxes(boxes, offset) - print(moved_box) - # np.array([ + + sv.move_boxes(xyxy=xyxy, offset=offset) + # array([ # [15, 15, 25, 25], - # [35, 35, 45, 45] + # [35, 35, 45, 45] # ]) ``` """ @@ -675,11 +678,13 @@ def scale_boxes(xyxy: np.ndarray, factor: float) -> np.ndarray: import numpy as np import supervision as sv - boxes = np.array([[10, 10, 20, 20], [30, 30, 40, 40]]) - factor = 1.5 - scaled_bb = sv.scale_boxes(boxes, factor) - print(scaled_bb) - # np.array([ + xyxy = np.array([ + [10, 10, 20, 20], + [30, 30, 40, 40] + ]) + + scaled_bb = sv.scale_boxes(xyxy=xyxy, factor=1.5) + # array([ # [ 7.5, 7.5, 22.5, 22.5], # [27.5, 27.5, 42.5, 42.5] # ]) @@ -843,34 +848,62 @@ def get_data_item( return subset_data -def mask_has_holes(mask: npt.NDArray[np.bool_]) -> bool: +def contains_holes(mask: npt.NDArray[np.bool_]) -> bool: """ - Checks if target objects in binary mask contain holes - (A hole is when background pixels are fully enclosed by foreground pixels) + Checks if the binary mask contains holes (background pixels fully enclosed by + foreground pixels). Args: mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground object and `False` indicates background. + Returns: - True when holes are detected, False otherwise. + True if holes are detected, False otherwise. + + Examples: + ```python + import numpy as np + import supervision as sv + + mask = np.array([ + [0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 0, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0] + ]).astype(bool) + + sv.contains_holes(mask=mask) + # True + + mask = np.array([ + [0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0] + ]).astype(bool) + + sv.contains_holes(mask=mask) + # False + ``` """ mask_uint8 = mask.astype(np.uint8) _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) - if hierarchy is not None: # at least one contour was found - parent_countour_index = 3 + if hierarchy is not None: + parent_contour_index = 3 for h in hierarchy[0]: - if h[parent_countour_index] != -1: + if h[parent_contour_index] != -1: return True return False -def mask_has_multiple_segments( +def contains_multiple_segments( mask: npt.NDArray[np.bool_], connectivity: int = 4 ) -> bool: """ - Checks if the binary mask consists of multiple not connected elements representing - the foreground objects. + Checks if the binary mask contains multiple unconnected foreground segments. Args: mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground @@ -886,11 +919,40 @@ def mask_has_multiple_segments( Raises: ValueError: If connectivity(int) parameter value is not 4 or 8. + + Examples: + ```python + import numpy as np + import supervision as sv + + mask = np.array([ + [0, 0, 0, 0, 0, 0], + [0, 1, 1, 0, 1, 1], + [0, 1, 1, 0, 1, 1], + [0, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 0, 0] + ]).astype(bool) + + sv.contains_multiple_segments(mask=mask, connectivity=4) + # True + + mask = np.array([ + [0, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0] + ]).astype(bool) + + sv.contains_multiple_segments(mask=mask, connectivity=4) + # False + ``` """ if connectivity != 4 and connectivity != 8: raise ValueError( - """Incorrect connectivity value,""" - """ possible connectivity values: 4 or 8""" + "Incorrect connectivity value. Possible connectivity values: 4 or 8." ) mask_uint8 = mask.astype(np.uint8) labels = np.zeros_like(mask_uint8, dtype=np.int32) diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index 2821aed2..c7ed674a 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -12,8 +12,8 @@ from supervision.detection.utils import ( clip_boxes, filter_polygons_by_area, get_data_item, - mask_has_holes, - mask_has_multiple_segments, + contains_holes, + contains_multiple_segments, mask_non_max_suppression, merge_data, move_boxes, @@ -1317,11 +1317,11 @@ def test_get_data_item( ), # foreground object has 2 holes ], ) -def test_mask_has_holes( +def test_contains_holes( mask: npt.NDArray[np.bool_], expected_result: bool, exception: Exception ) -> None: with exception: - result = mask_has_holes(mask) + result = contains_holes(mask) assert result == expected_result @@ -1394,12 +1394,12 @@ def test_mask_has_holes( ), # Incorrect connectivity parameter value, raises ValueError ], ) -def test_mask_has_multiple_segments( +def test_contains_multiple_segments( mask: npt.NDArray[np.bool_], connectivity: int, expected_result: bool, exception: Exception, ) -> None: with exception: - result = mask_has_multiple_segments(mask=mask, connectivity=connectivity) + result = contains_multiple_segments(mask=mask, connectivity=connectivity) assert result == expected_result From 996b0a3af346f15fb0e692b6d58bcbf98a2de3ba Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 09:43:28 +0000 Subject: [PATCH 117/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/__init__.py | 2 +- test/detection/test_utils.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/supervision/__init__.py b/supervision/__init__.py index af5f8dcc..abe63390 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -49,9 +49,9 @@ from supervision.detection.utils import ( box_non_max_suppression, calculate_masks_centroids, clip_boxes, - filter_polygons_by_area, contains_holes, contains_multiple_segments, + filter_polygons_by_area, mask_iou_batch, mask_non_max_suppression, mask_to_polygons, diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index c7ed674a..6a2070da 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -10,10 +10,10 @@ from supervision.detection.utils import ( box_non_max_suppression, calculate_masks_centroids, clip_boxes, - filter_polygons_by_area, - get_data_item, contains_holes, contains_multiple_segments, + filter_polygons_by_area, + get_data_item, mask_non_max_suppression, merge_data, move_boxes, From 1d43042c55418ecba8fcfbfe7973e65e15147f03 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Tue, 21 May 2024 12:01:05 +0200 Subject: [PATCH 118/136] small refactor + docs improvements --- supervision/dataset/core.py | 38 ++++++++++++++++++++-------------- supervision/detection/utils.py | 4 ++-- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/supervision/dataset/core.py b/supervision/dataset/core.py index 6ae5844a..c8863df3 100644 --- a/supervision/dataset/core.py +++ b/supervision/dataset/core.py @@ -116,13 +116,12 @@ class DetectionDataset(BaseDataset): Tuple[DetectionDataset, DetectionDataset]: A tuple containing the training and testing datasets. - Example: + Examples: ```python import supervision as sv ds = sv.DetectionDataset(...) - train_ds, test_ds = ds.split(split_ratio=0.7, - random_state=42, shuffle=True) + train_ds, test_ds = ds.split(split_ratio=0.7, random_state=42, shuffle=True) len(train_ds), len(test_ds) # (700, 300) ``` @@ -229,7 +228,7 @@ class DetectionDataset(BaseDataset): DetectionDataset: A DetectionDataset instance containing the loaded images and annotations. - Example: + Examples: ```python import roboflow from roboflow import Roboflow @@ -286,7 +285,7 @@ class DetectionDataset(BaseDataset): DetectionDataset: A DetectionDataset instance containing the loaded images and annotations. - Example: + Examples: ```python import roboflow from roboflow import Roboflow @@ -391,7 +390,7 @@ class DetectionDataset(BaseDataset): DetectionDataset: A DetectionDataset instance containing the loaded images and annotations. - Example: + Examples: ```python import roboflow from roboflow import Roboflow @@ -430,10 +429,20 @@ class DetectionDataset(BaseDataset): """ Exports the dataset to COCO format. This method saves the images and their corresponding annotations in COCO format. - The format of the mask is determined automatically: - when a mask consists of multiple disconnected elements - or has holes the RLE format is used, - otherwise, the mask is encoded as a polygon. + + !!! tip + + The format of the mask is determined automatically based on its structure: + + - If a mask contains multiple disconnected components or holes, it will be + saved using the Run-Length Encoding (RLE) format for efficient storage and + processing. + - If a mask consists of a single, contiguous region without any holes, it + will be encoded as a polygon, preserving the outline of the object. + + This automatic selection ensures that the masks are stored in the most + appropriate and space-efficient format, complying with COCO dataset + standards. Args: images_directory_path (Optional[str]): The path to the directory @@ -486,7 +495,7 @@ class DetectionDataset(BaseDataset): (DetectionDataset): A single `DetectionDataset` object containing the merged data from the input list. - Example: + Examples: ```python import supervision as sv @@ -571,13 +580,12 @@ class ClassificationDataset(BaseDataset): Tuple[ClassificationDataset, ClassificationDataset]: A tuple containing the training and testing datasets. - Example: + Examples: ```python import supervision as sv cd = sv.ClassificationDataset(...) - train_cd,test_cd = cd.split(split_ratio=0.7, - random_state=42,shuffle=True) + train_cd,test_cd = cd.split(split_ratio=0.7, random_state=42,shuffle=True) len(train_cd), len(test_cd) # (700, 300) ``` @@ -639,7 +647,7 @@ class ClassificationDataset(BaseDataset): Returns: ClassificationDataset: The dataset. - Example: + Examples: ```python import roboflow from roboflow import Roboflow diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 5a8e7ba2..f1e77883 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -604,7 +604,7 @@ def move_boxes(xyxy: np.ndarray, offset: np.ndarray) -> np.ndarray: Returns: np.ndarray: Repositioned bounding boxes. - Example: + Examples: ```python import numpy as np import supervision as sv @@ -673,7 +673,7 @@ def scale_boxes(xyxy: np.ndarray, factor: float) -> np.ndarray: Returns: np.ndarray: Scaled bounding boxes. - Example: + Examples: ```python import numpy as np import supervision as sv From 40b537c0bebe535e93ed7b7eef719c3a2a2e57e0 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Tue, 21 May 2024 13:58:20 +0300 Subject: [PATCH 119/136] tracker_id contents are never None --- supervision/detection/line_zone.py | 10 +++++++--- supervision/detection/tools/smoother.py | 2 -- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/supervision/detection/line_zone.py b/supervision/detection/line_zone.py index 53d762a0..fe850894 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -140,6 +140,13 @@ class LineZone: if len(detections) == 0: return crossed_in, crossed_out + if detections.tracker_id is None: + print( + "Line zone conting skipped. LineZone requires tracker_id. Refer to " + "https://supervision.roboflow.com/latest/trackers for more information." + ) + return crossed_in, crossed_out + all_anchors = np.array( [ detections.get_anchors_coordinates(anchor) @@ -148,9 +155,6 @@ class LineZone: ) for i, tracker_id in enumerate(detections.tracker_id): - if tracker_id is None: - continue - box_anchors = [Point(x=x, y=y) for x, y in all_anchors[:, i, :]] in_limits = all( diff --git a/supervision/detection/tools/smoother.py b/supervision/detection/tools/smoother.py index f58f3299..6b20bdd1 100644 --- a/supervision/detection/tools/smoother.py +++ b/supervision/detection/tools/smoother.py @@ -78,8 +78,6 @@ class DetectionsSmoother: for detection_idx in range(len(detections)): tracker_id = detections.tracker_id[detection_idx] - if tracker_id is None: - continue self.tracks[tracker_id].append(detections[detection_idx]) From c3855c982847f6a74d69da3062ee76ed98690997 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Tue, 21 May 2024 13:08:02 +0200 Subject: [PATCH 120/136] small refactor + docs improvements --- supervision/dataset/utils.py | 28 ++++++++++++++++++---------- supervision/detection/utils.py | 8 ++++++-- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index 4efc7194..a43fbe5a 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -159,10 +159,12 @@ def rle_to_mask( ```python import supervision as sv - sv.rle_to_mask([2, 2, 2], (3, 2)) + sv.rle_to_mask([5, 2, 2, 2, 5], (4, 4)) # array([ - # [False, True, False], - # [False, True, False] + # [False, False, False, False], + # [False, True, True, False], + # [False, True, True, False], + # [False, False, False, False], # ]) ``` """ @@ -209,20 +211,26 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: import supervision as sv mask = np.array([ - [False, True, True], - [False, True, True] + [True, True, True, True], + [True, True, True, True], + [True, True, True, True], + [True, True, True, True], ]) sv.mask_to_rle(mask) - # [2, 4] + # [0, 16] mask = np.array([ - [True, True, True], - [True, True, True] + [False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False], ]) sv.mask_to_rle(mask) - # [0, 6] + # [5, 2, 2, 2, 5] ``` - """ + + ![mask_to_rle](https://media.roboflow.com/supervision-docs/mask-to-rle.png){ align=center width="800" } + """ # noqa E501 // docs assert mask.ndim == 2, "Input mask must be 2D" assert mask.size != 0, "Input mask cannot be empty" diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index f1e77883..f9f71f98 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -887,7 +887,9 @@ def contains_holes(mask: npt.NDArray[np.bool_]) -> bool: sv.contains_holes(mask=mask) # False ``` - """ + + ![contains_holes](https://media.roboflow.com/supervision-docs/contains-holes.png){ align=center width="800" } + """ # noqa E501 // docs mask_uint8 = mask.astype(np.uint8) _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) @@ -949,7 +951,9 @@ def contains_multiple_segments( sv.contains_multiple_segments(mask=mask, connectivity=4) # False ``` - """ + + ![contains_multiple_segments](https://media.roboflow.com/supervision-docs/contains-multiple-segments.png){ align=center width="800" } + """ # noqa E501 // docs if connectivity != 4 and connectivity != 8: raise ValueError( "Incorrect connectivity value. Possible connectivity values: 4 or 8." From 58408898180f9dc5370c5a17e043aa31db44c7a5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 11:08:19 +0000 Subject: [PATCH 121/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/dataset/utils.py | 2 +- supervision/detection/utils.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/supervision/dataset/utils.py b/supervision/dataset/utils.py index a43fbe5a..32ece6bf 100644 --- a/supervision/dataset/utils.py +++ b/supervision/dataset/utils.py @@ -228,7 +228,7 @@ def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]: sv.mask_to_rle(mask) # [5, 2, 2, 2, 5] ``` - + ![mask_to_rle](https://media.roboflow.com/supervision-docs/mask-to-rle.png){ align=center width="800" } """ # noqa E501 // docs assert mask.ndim == 2, "Input mask must be 2D" diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index f9f71f98..742f94f6 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -887,7 +887,7 @@ def contains_holes(mask: npt.NDArray[np.bool_]) -> bool: sv.contains_holes(mask=mask) # False ``` - + ![contains_holes](https://media.roboflow.com/supervision-docs/contains-holes.png){ align=center width="800" } """ # noqa E501 // docs mask_uint8 = mask.astype(np.uint8) @@ -951,7 +951,7 @@ def contains_multiple_segments( sv.contains_multiple_segments(mask=mask, connectivity=4) # False ``` - + ![contains_multiple_segments](https://media.roboflow.com/supervision-docs/contains-multiple-segments.png){ align=center width="800" } """ # noqa E501 // docs if connectivity != 4 and connectivity != 8: From 6b7861d4d1d17abcf09c2d3df51ec5c643e71067 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 00:29:25 +0000 Subject: [PATCH 122/136] --- updated-dependencies: - dependency-name: requests dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index e672448a..85615d1c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3461,13 +3461,13 @@ files = [ [[package]] name = "requests" -version = "2.32.1" +version = "2.32.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" files = [ - {file = "requests-2.32.1-py3-none-any.whl", hash = "sha256:21ac9465cdf8c1650fe1ecde8a71669a93d4e6f147550483a2967d08396a56a5"}, - {file = "requests-2.32.1.tar.gz", hash = "sha256:eb97e87e64c79e64e5b8ac75cee9dd1f97f49e289b083ee6be96268930725685"}, + {file = "requests-2.32.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"}, + {file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"}, ] [package.dependencies] @@ -4258,4 +4258,4 @@ desktop = ["opencv-python"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "7ecea27cde915f67ee71e0ed55cabd890c6473e5f222434efd0b1f4392713312" +content-hash = "ad8402ec1767f9427ab38bad7dab54b302a30f9e08b6489fad224c8481745b37" diff --git a/pyproject.toml b/pyproject.toml index 252bb49d..ff83f5fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ pyyaml = ">=5.3" defusedxml = "^0.7.1" opencv-python = { version = ">=4.5.5.64", optional = true } opencv-python-headless = ">=4.5.5.64" -requests = { version = ">=2.26.0,<=2.32.1", optional = true } +requests = { version = ">=2.26.0,<=2.32.2", optional = true } tqdm = { version = ">=4.62.3,<=4.66.4", optional = true } pillow = ">=9.4" From a2195aa3789403e24ca44b941e0f14c3b49655ce Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Wed, 22 May 2024 14:24:52 +0200 Subject: [PATCH 123/136] initial commit adding support for `from_lmm` and specifically for PaliGemma --- supervision/detection/core.py | 47 ++++++++++++++ supervision/detection/lmm.py | 62 +++++++++++++++++++ test/detection/test_lmm.py | 113 ++++++++++++++++++++++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 supervision/detection/lmm.py create mode 100644 test/detection/test_lmm.py diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 0ba9e4f4..d6a04efb 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -7,6 +7,7 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple, Union import numpy as np from supervision.config import CLASS_NAME_DATA_FIELD, ORIENTED_BOX_COORDINATES +from supervision.detection.lmm import LMM, validate_lmm_and_kwargs, from_paligemma from supervision.detection.utils import ( box_non_max_suppression, calculate_masks_centroids, @@ -805,6 +806,52 @@ class Detections: class_id=paddledet_result["bbox"][:, 0].astype(int), ) + @classmethod + def from_lmm(cls, lmm: Union[LMM, str], result: str, **kwargs) -> Detections: + """ + Creates a Detections object from the given result string based on the specified + Large Multimodal Model (LMM). + + Args: + lmm (Union[LMM, str]): The type of LMM (Large Multimodal Model) to use. + result (str): The result string containing the detection data. + **kwargs: Additional keyword arguments required by the specified LMM. + + Returns: + Detections: A new Detections object. + + Raises: + ValueError: If the LMM is invalid, required arguments are missing, or + disallowed arguments are provided. + ValueError: If the specified LMM is not supported. + + Examples: + ```python + import supervision as sv + + paligemma_result = " cat" + detections = sv.Detections.from_lmm( + sv.LMM.PALIGEMMA, + paligemma_result, + resolution_wh=(1000, 1000), + classes=['cat', 'dog'] + ) + detections.xyxy + # array([[250., 250., 750., 750.]]) + + detections.class_id + # array([0]) + ``` + """ + lmm = validate_lmm_and_kwargs(lmm, kwargs) + + if lmm == LMM.PALIGEMMA: + xyxy, class_id, class_name = from_paligemma(result, **kwargs) + data = {CLASS_NAME_DATA_FIELD: class_name} + return cls(xyxy=xyxy, class_id=class_id, data=data) + + raise ValueError(f"Unsupported LMM: {lmm}") + @classmethod def empty(cls) -> Detections: """ diff --git a/supervision/detection/lmm.py b/supervision/detection/lmm.py new file mode 100644 index 00000000..1c4b90dc --- /dev/null +++ b/supervision/detection/lmm.py @@ -0,0 +1,62 @@ +import re +import numpy as np +from enum import Enum +from typing import Dict, List, Tuple, Optional, Union, Any + + +class LMM(Enum): + PALIGEMMA = 'paligemma' + + +REQUIRED_ARGUMENTS: Dict[LMM, List[str]] = { + LMM.PALIGEMMA: ['resolution_wh'] +} + +ALLOWED_ARGUMENTS: Dict[LMM, List[str]] = { + LMM.PALIGEMMA: ['resolution_wh', 'classes'] +} + + +def validate_lmm_and_kwargs(lmm: Union[LMM, str], kwargs: Dict[str, Any]) -> LMM: + if isinstance(lmm, str): + try: + lmm = LMM(lmm.lower()) + except ValueError: + raise ValueError( + f"Invalid lmm value: {lmm}. Must be one of {[e.value for e in LMM]}" + ) + + required_args = REQUIRED_ARGUMENTS.get(lmm, []) + for arg in required_args: + if arg not in kwargs: + raise ValueError(f"Missing required argument: {arg}") + + allowed_args = ALLOWED_ARGUMENTS.get(lmm, []) + for arg in kwargs: + if arg not in allowed_args: + raise ValueError(f"Argument {arg} is not allowed for {lmm.name}") + + return lmm + + +def from_paligemma( + result: str, + resolution_wh: Tuple[int, int], + classes: Optional[List[str]] = None +) -> Tuple[np.ndarray, Optional[np.ndarray], np.ndarray]: + w, h = resolution_wh + pattern = re.compile( + r'(?) (\w+)') + matches = pattern.findall(result) + matches = np.array(matches) if matches else np.empty((0, 5)) + + xyxy, class_name = matches[:, [1, 0, 3, 2]], matches[:, 4] + xyxy = xyxy.astype(int) / 1024 * np.array([w, h, w, h]) + class_id = None + + if classes is not None: + mask = np.array([name in classes for name in class_name]) + xyxy, class_name = xyxy[mask], class_name[mask] + class_id = np.array([classes.index(name) for name in class_name]) + + return xyxy, class_id, class_name.astype(np.dtype('U')) diff --git a/test/detection/test_lmm.py b/test/detection/test_lmm.py new file mode 100644 index 00000000..b7f7c5b4 --- /dev/null +++ b/test/detection/test_lmm.py @@ -0,0 +1,113 @@ +import numpy as np +from typing import Tuple, Optional, List + +import pytest + +from supervision.detection.lmm import from_paligemma + + +@pytest.mark.parametrize( + "result, resolution_wh, classes, expected_results", + [ + ( + "", + (1000, 1000), + None, + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + ), # empty response + ( + "\n", + (1000, 1000), + None, + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + ), # new line response + ( + "the quick brown fox jumps over the lazy dog.", + (1000, 1000), + None, + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + ), # response with no location + ( + " cat", + (1000, 1000), + None, + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + ), # response with missing location + ( + " cat", + (1000, 1000), + None, + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + ), # response with extra location + ( + "", + (1000, 1000), + None, + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + ), # response with no class + ( + " catt", + (1000, 1000), + ['cat', 'dog'], + (np.empty((0, 4)), np.empty(0), np.empty(0).astype(np.dtype('U'))) + ), # response with invalid class + ( + " cat", + (1000, 1000), + None, + ( + np.array([[250., 250., 750., 750.]]), + None, + np.array(['cat']).astype(np.dtype('U')) + ) + ), # correct response; no classes + ( + " cat ;", + (1000, 1000), + ['cat', 'dog'], + ( + np.array([[250., 250., 750., 750.]]), + np.array([0]), + np.array(['cat']).astype(np.dtype('U')) + ) + ), # correct response; with classes + ( + " cat ; cat", + (1000, 1000), + ['cat', 'dog'], + ( + np.array([[250., 250., 750., 750.]]), + np.array([0]), + np.array(['cat']).astype(np.dtype('U')) + ) + ), # partially correct response; with classes + ( + " cat ; cat", + (1000, 1000), + ['cat', 'dog'], + ( + np.array([[250., 250., 750., 750.]]), + np.array([0]), + np.array(['cat']).astype(np.dtype('U')) + ) + ), # partially correct response; with classes + ] +) +def test_from_paligemma( + result: str, + resolution_wh: Tuple[int, int], + classes: Optional[List[str]], + expected_results: Tuple[np.ndarray, Optional[np.ndarray], np.ndarray] +) -> None: + result = from_paligemma(result=result, resolution_wh=resolution_wh, classes=classes) + + print(result[0].dtype) + print(expected_results[0].dtype) + # print(result[1]) + # print(expected_results[1]) + print(result[2].dtype) + print(expected_results[2].dtype) + + np.testing.assert_array_equal(result[0], expected_results[0]) + np.testing.assert_array_equal(result[1], expected_results[1]) + np.testing.assert_array_equal(result[2], expected_results[2]) From 36a73f7d4912a56ab289d38d22b17b1538bd39ed Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Wed, 22 May 2024 14:26:13 +0200 Subject: [PATCH 124/136] clean up --- test/detection/test_lmm.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/detection/test_lmm.py b/test/detection/test_lmm.py index b7f7c5b4..f8ea91ef 100644 --- a/test/detection/test_lmm.py +++ b/test/detection/test_lmm.py @@ -100,14 +100,6 @@ def test_from_paligemma( expected_results: Tuple[np.ndarray, Optional[np.ndarray], np.ndarray] ) -> None: result = from_paligemma(result=result, resolution_wh=resolution_wh, classes=classes) - - print(result[0].dtype) - print(expected_results[0].dtype) - # print(result[1]) - # print(expected_results[1]) - print(result[2].dtype) - print(expected_results[2].dtype) - np.testing.assert_array_equal(result[0], expected_results[0]) np.testing.assert_array_equal(result[1], expected_results[1]) np.testing.assert_array_equal(result[2], expected_results[2]) From 7eb918282cf81c0c30e80ddad279265fe35528a9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 12:27:53 +0000 Subject: [PATCH 125/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/detection/core.py | 2 +- supervision/detection/lmm.py | 24 +++++++--------- test/detection/test_lmm.py | 54 +++++++++++++++++------------------ 3 files changed, 38 insertions(+), 42 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index d6a04efb..e8599817 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -7,7 +7,7 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple, Union import numpy as np from supervision.config import CLASS_NAME_DATA_FIELD, ORIENTED_BOX_COORDINATES -from supervision.detection.lmm import LMM, validate_lmm_and_kwargs, from_paligemma +from supervision.detection.lmm import LMM, from_paligemma, validate_lmm_and_kwargs from supervision.detection.utils import ( box_non_max_suppression, calculate_masks_centroids, diff --git a/supervision/detection/lmm.py b/supervision/detection/lmm.py index 1c4b90dc..67921328 100644 --- a/supervision/detection/lmm.py +++ b/supervision/detection/lmm.py @@ -1,20 +1,17 @@ import re -import numpy as np from enum import Enum -from typing import Dict, List, Tuple, Optional, Union, Any +from typing import Any, Dict, List, Optional, Tuple, Union + +import numpy as np class LMM(Enum): - PALIGEMMA = 'paligemma' + PALIGEMMA = "paligemma" -REQUIRED_ARGUMENTS: Dict[LMM, List[str]] = { - LMM.PALIGEMMA: ['resolution_wh'] -} +REQUIRED_ARGUMENTS: Dict[LMM, List[str]] = {LMM.PALIGEMMA: ["resolution_wh"]} -ALLOWED_ARGUMENTS: Dict[LMM, List[str]] = { - LMM.PALIGEMMA: ['resolution_wh', 'classes'] -} +ALLOWED_ARGUMENTS: Dict[LMM, List[str]] = {LMM.PALIGEMMA: ["resolution_wh", "classes"]} def validate_lmm_and_kwargs(lmm: Union[LMM, str], kwargs: Dict[str, Any]) -> LMM: @@ -40,13 +37,12 @@ def validate_lmm_and_kwargs(lmm: Union[LMM, str], kwargs: Dict[str, Any]) -> LMM def from_paligemma( - result: str, - resolution_wh: Tuple[int, int], - classes: Optional[List[str]] = None + result: str, resolution_wh: Tuple[int, int], classes: Optional[List[str]] = None ) -> Tuple[np.ndarray, Optional[np.ndarray], np.ndarray]: w, h = resolution_wh pattern = re.compile( - r'(?) (\w+)') + r"(?) (\w+)" + ) matches = pattern.findall(result) matches = np.array(matches) if matches else np.empty((0, 5)) @@ -59,4 +55,4 @@ def from_paligemma( xyxy, class_name = xyxy[mask], class_name[mask] class_id = np.array([classes.index(name) for name in class_name]) - return xyxy, class_id, class_name.astype(np.dtype('U')) + return xyxy, class_id, class_name.astype(np.dtype("U")) diff --git a/test/detection/test_lmm.py b/test/detection/test_lmm.py index f8ea91ef..5066a7a3 100644 --- a/test/detection/test_lmm.py +++ b/test/detection/test_lmm.py @@ -1,6 +1,6 @@ -import numpy as np -from typing import Tuple, Optional, List +from typing import List, Optional, Tuple +import numpy as np import pytest from supervision.detection.lmm import from_paligemma @@ -13,91 +13,91 @@ from supervision.detection.lmm import from_paligemma "", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype("U"))), ), # empty response ( "\n", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype("U"))), ), # new line response ( "the quick brown fox jumps over the lazy dog.", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype("U"))), ), # response with no location ( " cat", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype("U"))), ), # response with missing location ( " cat", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype("U"))), ), # response with extra location ( "", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(np.dtype("U"))), ), # response with no class ( " catt", (1000, 1000), - ['cat', 'dog'], - (np.empty((0, 4)), np.empty(0), np.empty(0).astype(np.dtype('U'))) + ["cat", "dog"], + (np.empty((0, 4)), np.empty(0), np.empty(0).astype(np.dtype("U"))), ), # response with invalid class ( " cat", (1000, 1000), None, ( - np.array([[250., 250., 750., 750.]]), + np.array([[250.0, 250.0, 750.0, 750.0]]), None, - np.array(['cat']).astype(np.dtype('U')) - ) + np.array(["cat"]).astype(np.dtype("U")), + ), ), # correct response; no classes ( " cat ;", (1000, 1000), - ['cat', 'dog'], + ["cat", "dog"], ( - np.array([[250., 250., 750., 750.]]), + np.array([[250.0, 250.0, 750.0, 750.0]]), np.array([0]), - np.array(['cat']).astype(np.dtype('U')) - ) + np.array(["cat"]).astype(np.dtype("U")), + ), ), # correct response; with classes ( " cat ; cat", (1000, 1000), - ['cat', 'dog'], + ["cat", "dog"], ( - np.array([[250., 250., 750., 750.]]), + np.array([[250.0, 250.0, 750.0, 750.0]]), np.array([0]), - np.array(['cat']).astype(np.dtype('U')) - ) + np.array(["cat"]).astype(np.dtype("U")), + ), ), # partially correct response; with classes ( " cat ; cat", (1000, 1000), - ['cat', 'dog'], + ["cat", "dog"], ( - np.array([[250., 250., 750., 750.]]), + np.array([[250.0, 250.0, 750.0, 750.0]]), np.array([0]), - np.array(['cat']).astype(np.dtype('U')) - ) + np.array(["cat"]).astype(np.dtype("U")), + ), ), # partially correct response; with classes - ] + ], ) def test_from_paligemma( result: str, resolution_wh: Tuple[int, int], classes: Optional[List[str]], - expected_results: Tuple[np.ndarray, Optional[np.ndarray], np.ndarray] + expected_results: Tuple[np.ndarray, Optional[np.ndarray], np.ndarray], ) -> None: result = from_paligemma(result=result, resolution_wh=resolution_wh, classes=classes) np.testing.assert_array_equal(result[0], expected_results[0]) From b81c5e84758487494f90ad67805f9ddba4564ebe Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Wed, 22 May 2024 16:28:24 +0200 Subject: [PATCH 126/136] update to allow multi-word class names --- supervision/detection/lmm.py | 5 ++-- test/detection/test_lmm.py | 45 +++++++++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/supervision/detection/lmm.py b/supervision/detection/lmm.py index 1c4b90dc..9cd7434e 100644 --- a/supervision/detection/lmm.py +++ b/supervision/detection/lmm.py @@ -46,12 +46,13 @@ def from_paligemma( ) -> Tuple[np.ndarray, Optional[np.ndarray], np.ndarray]: w, h = resolution_wh pattern = re.compile( - r'(?) (\w+)') + r'(?) ([\w\s]+)') matches = pattern.findall(result) matches = np.array(matches) if matches else np.empty((0, 5)) xyxy, class_name = matches[:, [1, 0, 3, 2]], matches[:, 4] xyxy = xyxy.astype(int) / 1024 * np.array([w, h, w, h]) + class_name = np.char.strip(class_name.astype(str)) class_id = None if classes is not None: @@ -59,4 +60,4 @@ def from_paligemma( xyxy, class_name = xyxy[mask], class_name[mask] class_id = np.array([classes.index(name) for name in class_name]) - return xyxy, class_id, class_name.astype(np.dtype('U')) + return xyxy, class_id, class_name diff --git a/test/detection/test_lmm.py b/test/detection/test_lmm.py index f8ea91ef..91840d88 100644 --- a/test/detection/test_lmm.py +++ b/test/detection/test_lmm.py @@ -13,43 +13,43 @@ from supervision.detection.lmm import from_paligemma "", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(str)) ), # empty response ( "\n", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(str)) ), # new line response ( "the quick brown fox jumps over the lazy dog.", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(str)) ), # response with no location ( " cat", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(str)) ), # response with missing location ( " cat", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(str)) ), # response with extra location ( "", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), None, np.empty(0).astype(str)) ), # response with no class ( " catt", (1000, 1000), ['cat', 'dog'], - (np.empty((0, 4)), np.empty(0), np.empty(0).astype(np.dtype('U'))) + (np.empty((0, 4)), np.empty(0), np.empty(0).astype(str)) ), # response with invalid class ( " cat", @@ -58,7 +58,17 @@ from supervision.detection.lmm import from_paligemma ( np.array([[250., 250., 750., 750.]]), None, - np.array(['cat']).astype(np.dtype('U')) + np.array(['cat']).astype(str) + ) + ), # correct response; no classes + ( + " black cat", + (1000, 1000), + None, + ( + np.array([[250., 250., 750., 750.]]), + None, + np.array(['black cat']).astype(np.dtype('U')) ) ), # correct response; no classes ( @@ -68,7 +78,20 @@ from supervision.detection.lmm import from_paligemma ( np.array([[250., 250., 750., 750.]]), np.array([0]), - np.array(['cat']).astype(np.dtype('U')) + np.array(['cat']).astype(str) + ) + ), # correct response; with classes + ( + " cat ; dog", + (1000, 1000), + ['cat', 'dog'], + ( + np.array([ + [250., 250., 750., 750.], + [250., 250., 750., 750.] + ]), + np.array([0, 1]), + np.array(['cat', 'dog']).astype(np.dtype('U')) ) ), # correct response; with classes ( @@ -78,7 +101,7 @@ from supervision.detection.lmm import from_paligemma ( np.array([[250., 250., 750., 750.]]), np.array([0]), - np.array(['cat']).astype(np.dtype('U')) + np.array(['cat']).astype(str) ) ), # partially correct response; with classes ( @@ -88,7 +111,7 @@ from supervision.detection.lmm import from_paligemma ( np.array([[250., 250., 750., 750.]]), np.array([0]), - np.array(['cat']).astype(np.dtype('U')) + np.array(['cat']).astype(str) ) ), # partially correct response; with classes ] From bf43d6566b8bfa9ac6c317bceb312c414268a60b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 14:29:20 +0000 Subject: [PATCH 127/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/detection/lmm.py | 22 +++++------ test/detection/test_lmm.py | 71 +++++++++++++++++------------------- 2 files changed, 43 insertions(+), 50 deletions(-) diff --git a/supervision/detection/lmm.py b/supervision/detection/lmm.py index 9cd7434e..3660fb68 100644 --- a/supervision/detection/lmm.py +++ b/supervision/detection/lmm.py @@ -1,20 +1,17 @@ import re -import numpy as np from enum import Enum -from typing import Dict, List, Tuple, Optional, Union, Any +from typing import Any, Dict, List, Optional, Tuple, Union + +import numpy as np class LMM(Enum): - PALIGEMMA = 'paligemma' + PALIGEMMA = "paligemma" -REQUIRED_ARGUMENTS: Dict[LMM, List[str]] = { - LMM.PALIGEMMA: ['resolution_wh'] -} +REQUIRED_ARGUMENTS: Dict[LMM, List[str]] = {LMM.PALIGEMMA: ["resolution_wh"]} -ALLOWED_ARGUMENTS: Dict[LMM, List[str]] = { - LMM.PALIGEMMA: ['resolution_wh', 'classes'] -} +ALLOWED_ARGUMENTS: Dict[LMM, List[str]] = {LMM.PALIGEMMA: ["resolution_wh", "classes"]} def validate_lmm_and_kwargs(lmm: Union[LMM, str], kwargs: Dict[str, Any]) -> LMM: @@ -40,13 +37,12 @@ def validate_lmm_and_kwargs(lmm: Union[LMM, str], kwargs: Dict[str, Any]) -> LMM def from_paligemma( - result: str, - resolution_wh: Tuple[int, int], - classes: Optional[List[str]] = None + result: str, resolution_wh: Tuple[int, int], classes: Optional[List[str]] = None ) -> Tuple[np.ndarray, Optional[np.ndarray], np.ndarray]: w, h = resolution_wh pattern = re.compile( - r'(?) ([\w\s]+)') + r"(?) ([\w\s]+)" + ) matches = pattern.findall(result) matches = np.array(matches) if matches else np.empty((0, 5)) diff --git a/test/detection/test_lmm.py b/test/detection/test_lmm.py index 91840d88..5b4f31ba 100644 --- a/test/detection/test_lmm.py +++ b/test/detection/test_lmm.py @@ -1,6 +1,6 @@ -import numpy as np -from typing import Tuple, Optional, List +from typing import List, Optional, Tuple +import numpy as np import pytest from supervision.detection.lmm import from_paligemma @@ -13,114 +13,111 @@ from supervision.detection.lmm import from_paligemma "", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(str)) + (np.empty((0, 4)), None, np.empty(0).astype(str)), ), # empty response ( "\n", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(str)) + (np.empty((0, 4)), None, np.empty(0).astype(str)), ), # new line response ( "the quick brown fox jumps over the lazy dog.", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(str)) + (np.empty((0, 4)), None, np.empty(0).astype(str)), ), # response with no location ( " cat", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(str)) + (np.empty((0, 4)), None, np.empty(0).astype(str)), ), # response with missing location ( " cat", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(str)) + (np.empty((0, 4)), None, np.empty(0).astype(str)), ), # response with extra location ( "", (1000, 1000), None, - (np.empty((0, 4)), None, np.empty(0).astype(str)) + (np.empty((0, 4)), None, np.empty(0).astype(str)), ), # response with no class ( " catt", (1000, 1000), - ['cat', 'dog'], - (np.empty((0, 4)), np.empty(0), np.empty(0).astype(str)) + ["cat", "dog"], + (np.empty((0, 4)), np.empty(0), np.empty(0).astype(str)), ), # response with invalid class ( " cat", (1000, 1000), None, ( - np.array([[250., 250., 750., 750.]]), + np.array([[250.0, 250.0, 750.0, 750.0]]), None, - np.array(['cat']).astype(str) - ) + np.array(["cat"]).astype(str), + ), ), # correct response; no classes ( " black cat", (1000, 1000), None, ( - np.array([[250., 250., 750., 750.]]), + np.array([[250.0, 250.0, 750.0, 750.0]]), None, - np.array(['black cat']).astype(np.dtype('U')) - ) + np.array(["black cat"]).astype(np.dtype("U")), + ), ), # correct response; no classes ( " cat ;", (1000, 1000), - ['cat', 'dog'], + ["cat", "dog"], ( - np.array([[250., 250., 750., 750.]]), + np.array([[250.0, 250.0, 750.0, 750.0]]), np.array([0]), - np.array(['cat']).astype(str) - ) + np.array(["cat"]).astype(str), + ), ), # correct response; with classes ( " cat ; dog", (1000, 1000), - ['cat', 'dog'], + ["cat", "dog"], ( - np.array([ - [250., 250., 750., 750.], - [250., 250., 750., 750.] - ]), + np.array([[250.0, 250.0, 750.0, 750.0], [250.0, 250.0, 750.0, 750.0]]), np.array([0, 1]), - np.array(['cat', 'dog']).astype(np.dtype('U')) - ) + np.array(["cat", "dog"]).astype(np.dtype("U")), + ), ), # correct response; with classes ( " cat ; cat", (1000, 1000), - ['cat', 'dog'], + ["cat", "dog"], ( - np.array([[250., 250., 750., 750.]]), + np.array([[250.0, 250.0, 750.0, 750.0]]), np.array([0]), - np.array(['cat']).astype(str) - ) + np.array(["cat"]).astype(str), + ), ), # partially correct response; with classes ( " cat ; cat", (1000, 1000), - ['cat', 'dog'], + ["cat", "dog"], ( - np.array([[250., 250., 750., 750.]]), + np.array([[250.0, 250.0, 750.0, 750.0]]), np.array([0]), - np.array(['cat']).astype(str) - ) + np.array(["cat"]).astype(str), + ), ), # partially correct response; with classes - ] + ], ) def test_from_paligemma( result: str, resolution_wh: Tuple[int, int], classes: Optional[List[str]], - expected_results: Tuple[np.ndarray, Optional[np.ndarray], np.ndarray] + expected_results: Tuple[np.ndarray, Optional[np.ndarray], np.ndarray], ) -> None: result = from_paligemma(result=result, resolution_wh=resolution_wh, classes=classes) np.testing.assert_array_equal(result[0], expected_results[0]) From 07c36c6223a49bbc496503ec3db34f2b0ac775f8 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Wed, 22 May 2024 17:47:52 +0200 Subject: [PATCH 128/136] make linter happy --- test/detection/test_lmm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/detection/test_lmm.py b/test/detection/test_lmm.py index 5b4f31ba..b6232bd8 100644 --- a/test/detection/test_lmm.py +++ b/test/detection/test_lmm.py @@ -82,7 +82,7 @@ from supervision.detection.lmm import from_paligemma ), ), # correct response; with classes ( - " cat ; dog", + " cat ; dog", # noqa: E501 (1000, 1000), ["cat", "dog"], ( @@ -92,7 +92,7 @@ from supervision.detection.lmm import from_paligemma ), ), # correct response; with classes ( - " cat ; cat", + " cat ; cat", # noqa: E501 (1000, 1000), ["cat", "dog"], ( @@ -102,7 +102,7 @@ from supervision.detection.lmm import from_paligemma ), ), # partially correct response; with classes ( - " cat ; cat", + " cat ; cat", # noqa: E501 (1000, 1000), ["cat", "dog"], ( From 8b4884ec9ed72c7c9d3203a0ca2a3ef6125fd624 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 22 May 2024 21:33:34 +0300 Subject: [PATCH 129/136] tracker, smoother: SupervisionWarnings --- supervision/detection/line_zone.py | 10 +++++++--- supervision/detection/tools/smoother.py | 8 ++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/supervision/detection/line_zone.py b/supervision/detection/line_zone.py index fe850894..cd4400e5 100644 --- a/supervision/detection/line_zone.py +++ b/supervision/detection/line_zone.py @@ -1,3 +1,4 @@ +import warnings from typing import Dict, Iterable, Optional, Tuple import cv2 @@ -7,6 +8,7 @@ from supervision.detection.core import Detections from supervision.draw.color import Color from supervision.draw.utils import draw_text from supervision.geometry.core import Point, Position, Vector +from supervision.utils.internal import SupervisionWarnings class LineZone: @@ -141,9 +143,11 @@ class LineZone: return crossed_in, crossed_out if detections.tracker_id is None: - print( - "Line zone conting skipped. LineZone requires tracker_id. Refer to " - "https://supervision.roboflow.com/latest/trackers for more information." + warnings.warn( + "Line zone counting skipped. LineZone requires tracker_id. Refer to " + "https://supervision.roboflow.com/latest/trackers for more " + "information.", + category=SupervisionWarnings, ) return crossed_in, crossed_out diff --git a/supervision/detection/tools/smoother.py b/supervision/detection/tools/smoother.py index 6b20bdd1..5768c3e8 100644 --- a/supervision/detection/tools/smoother.py +++ b/supervision/detection/tools/smoother.py @@ -1,3 +1,4 @@ +import warnings from collections import defaultdict, deque from copy import deepcopy from typing import Optional @@ -5,6 +6,7 @@ from typing import Optional import numpy as np from supervision.detection.core import Detections +from supervision.utils.internal import SupervisionWarnings class DetectionsSmoother: @@ -70,9 +72,11 @@ class DetectionsSmoother: """ if detections.tracker_id is None: - print( + warnings.warn( "Smoothing skipped. DetectionsSmoother requires tracker_id. Refer to " - "https://supervision.roboflow.com/latest/trackers for more information." + "https://supervision.roboflow.com/latest/trackers for more " + "information.", + category=SupervisionWarnings, ) return detections From 251185ba677d152fdbaaabe9d3d67bbd9425dbf1 Mon Sep 17 00:00:00 2001 From: Onuralp SEZER Date: Wed, 22 May 2024 19:48:39 +0300 Subject: [PATCH 130/136] =?UTF-8?q?fix:=20=F0=9F=90=9E=20yolo=20obb=20hass?= =?UTF-8?q?tr=20added=20into=20condition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onuralp SEZER --- supervision/detection/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 0ba9e4f4..6563de7d 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -240,7 +240,7 @@ class Detections: Class names values can be accessed using `detections["class_name"]`. """ # noqa: E501 // docs - if "obb" in ultralytics_results and ultralytics_results.obb is not None: + if hasattr(ultralytics_results, "obb") and ultralytics_results.obb is not None: class_id = ultralytics_results.obb.cls.cpu().numpy().astype(int) class_names = np.array([ultralytics_results.names[i] for i in class_id]) oriented_box_coordinates = ultralytics_results.obb.xyxyxyxy.cpu().numpy() From 99854a5b6693e7c70dd338df05839e9ff466c213 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 22 May 2024 23:14:17 +0300 Subject: [PATCH 131/136] minor docs change: rename How To - Track Objects --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 281c40c9..54f1eda2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -41,7 +41,7 @@ nav: - Save Detections: how_to/save_detections.md - Filter Detections: how_to/filter_detections.md - Detect Small Objects: how_to/detect_small_objects.md - - Track Objects: how_to/track_objects.md + - Detect and Track Objects on Video: how_to/track_objects.md - API: - Detection and Segmentation: From c9c2dad316a24f445e89357017113dc0ae708ebb Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Wed, 22 May 2024 23:47:45 +0300 Subject: [PATCH 132/136] Docs: shorter title for object tracking how-to --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 54f1eda2..f257238d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -41,7 +41,7 @@ nav: - Save Detections: how_to/save_detections.md - Filter Detections: how_to/filter_detections.md - Detect Small Objects: how_to/detect_small_objects.md - - Detect and Track Objects on Video: how_to/track_objects.md + - Track Objects on Video: how_to/track_objects.md - API: - Detection and Segmentation: From 0115ef8b7d40b9a242ed3799820b897b43b5da7e Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Thu, 23 May 2024 09:11:47 +0200 Subject: [PATCH 133/136] small fix when `mask` is empty --- supervision/detection/lmm.py | 2 +- test/detection/test_lmm.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/supervision/detection/lmm.py b/supervision/detection/lmm.py index 3660fb68..0278fc00 100644 --- a/supervision/detection/lmm.py +++ b/supervision/detection/lmm.py @@ -52,7 +52,7 @@ def from_paligemma( class_id = None if classes is not None: - mask = np.array([name in classes for name in class_name]) + mask = np.array([name in classes for name in class_name]).astype(bool) xyxy, class_name = xyxy[mask], class_name[mask] class_id = np.array([classes.index(name) for name in class_name]) diff --git a/test/detection/test_lmm.py b/test/detection/test_lmm.py index b6232bd8..e20b947d 100644 --- a/test/detection/test_lmm.py +++ b/test/detection/test_lmm.py @@ -15,6 +15,12 @@ from supervision.detection.lmm import from_paligemma None, (np.empty((0, 4)), None, np.empty(0).astype(str)), ), # empty response + ( + "", + (1000, 1000), + ['cat', 'dog'], + (np.empty((0, 4)), None, np.empty(0).astype(str)), + ), # empty response with classes ( "\n", (1000, 1000), From ad2220bc1da2e018d1ce08685359eb02ab3c5bd4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 23 May 2024 07:12:17 +0000 Subject: [PATCH 134/136] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20aut?= =?UTF-8?q?o=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/detection/test_lmm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/detection/test_lmm.py b/test/detection/test_lmm.py index e20b947d..129aa44b 100644 --- a/test/detection/test_lmm.py +++ b/test/detection/test_lmm.py @@ -18,7 +18,7 @@ from supervision.detection.lmm import from_paligemma ( "", (1000, 1000), - ['cat', 'dog'], + ["cat", "dog"], (np.empty((0, 4)), None, np.empty(0).astype(str)), ), # empty response with classes ( From 19296a73f612536e8b1b9f2f82ee9dee707dd225 Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Thu, 23 May 2024 18:44:23 +0300 Subject: [PATCH 135/136] ious: Replace 0-area check with nan conversion --- supervision/detection/utils.py | 4 +++- supervision/tracker/byte_tracker/core.py | 7 ------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 3eeba5b4..80742fd3 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -56,7 +56,9 @@ def box_iou_batch(boxes_true: np.ndarray, boxes_detection: np.ndarray) -> np.nda bottom_right = np.minimum(boxes_true[:, None, 2:], boxes_detection[:, 2:]) area_inter = np.prod(np.clip(bottom_right - top_left, a_min=0, a_max=None), 2) - return area_inter / (area_true[:, None] + area_detection - area_inter) + ious = area_inter / (area_true[:, None] + area_detection - area_inter) + ious = np.nan_to_num(ious) + return ious def _mask_iou_batch_split( diff --git a/supervision/tracker/byte_tracker/core.py b/supervision/tracker/byte_tracker/core.py index 132bf391..ce3bbbbf 100644 --- a/supervision/tracker/byte_tracker/core.py +++ b/supervision/tracker/byte_tracker/core.py @@ -362,13 +362,6 @@ class ByteTrack: scores = tensors[:, 4] bboxes = tensors[:, :4] - bbox_areas = (bboxes[:, 2] - bboxes[:, 0]) * (bboxes[:, 3] - bboxes[:, 1]) - valid_box_inds = bbox_areas > 0 - - class_ids = class_ids[valid_box_inds] - scores = scores[valid_box_inds] - bboxes = bboxes[valid_box_inds] - remain_inds = scores > self.track_activation_threshold inds_low = scores > 0.1 inds_high = scores < self.track_activation_threshold From 9afca17854236faf5c92296bfb95f796d614578c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 May 2024 00:13:26 +0000 Subject: [PATCH 136/136] :arrow_up: Bump ruff from 0.4.4 to 0.4.5 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.4 to 0.4.5. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.4.4...v0.4.5) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/poetry.lock b/poetry.lock index 85615d1c..0a9276f8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3662,28 +3662,28 @@ files = [ [[package]] name = "ruff" -version = "0.4.4" +version = "0.4.5" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29d44ef5bb6a08e235c8249294fa8d431adc1426bfda99ed493119e6f9ea1bf6"}, - {file = "ruff-0.4.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c4efe62b5bbb24178c950732ddd40712b878a9b96b1d02b0ff0b08a090cbd891"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c8e2f1e8fc12d07ab521a9005d68a969e167b589cbcaee354cb61e9d9de9c15"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60ed88b636a463214905c002fa3eaab19795679ed55529f91e488db3fe8976ab"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b90fc5e170fc71c712cc4d9ab0e24ea505c6a9e4ebf346787a67e691dfb72e85"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8e7e6ebc10ef16dcdc77fd5557ee60647512b400e4a60bdc4849468f076f6eef"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9ddb2c494fb79fc208cd15ffe08f32b7682519e067413dbaf5f4b01a6087bcd"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c51c928a14f9f0a871082603e25a1588059b7e08a920f2f9fa7157b5bf08cfe9"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5eb0a4bfd6400b7d07c09a7725e1a98c3b838be557fee229ac0f84d9aa49c36"}, - {file = "ruff-0.4.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b1867ee9bf3acc21778dcb293db504692eda5f7a11a6e6cc40890182a9f9e595"}, - {file = "ruff-0.4.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1aecced1269481ef2894cc495647392a34b0bf3e28ff53ed95a385b13aa45768"}, - {file = "ruff-0.4.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da73eb616b3241a307b837f32756dc20a0b07e2bcb694fec73699c93d04a69e"}, - {file = "ruff-0.4.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:958b4ea5589706a81065e2a776237de2ecc3e763342e5cc8e02a4a4d8a5e6f95"}, - {file = "ruff-0.4.4-py3-none-win32.whl", hash = "sha256:cb53473849f011bca6e754f2cdf47cafc9c4f4ff4570003a0dad0b9b6890e876"}, - {file = "ruff-0.4.4-py3-none-win_amd64.whl", hash = "sha256:424e5b72597482543b684c11def82669cc6b395aa8cc69acc1858b5ef3e5daae"}, - {file = "ruff-0.4.4-py3-none-win_arm64.whl", hash = "sha256:39df0537b47d3b597293edbb95baf54ff5b49589eb7ff41926d8243caa995ea6"}, - {file = "ruff-0.4.4.tar.gz", hash = "sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af"}, + {file = "ruff-0.4.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8f58e615dec58b1a6b291769b559e12fdffb53cc4187160a2fc83250eaf54e96"}, + {file = "ruff-0.4.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:84dd157474e16e3a82745d2afa1016c17d27cb5d52b12e3d45d418bcc6d49264"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25f483ad9d50b00e7fd577f6d0305aa18494c6af139bce7319c68a17180087f4"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63fde3bf6f3ad4e990357af1d30e8ba2730860a954ea9282c95fc0846f5f64af"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e3ba4620dee27f76bbcad97067766026c918ba0f2d035c2fc25cbdd04d9c97"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:441dab55c568e38d02bbda68a926a3d0b54f5510095c9de7f95e47a39e0168aa"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1169e47e9c4136c997f08f9857ae889d614c5035d87d38fda9b44b4338909cdf"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:755ac9ac2598a941512fc36a9070a13c88d72ff874a9781493eb237ab02d75df"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4b02a65985be2b34b170025a8b92449088ce61e33e69956ce4d316c0fe7cce0"}, + {file = "ruff-0.4.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:75a426506a183d9201e7e5664de3f6b414ad3850d7625764106f7b6d0486f0a1"}, + {file = "ruff-0.4.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6e1b139b45e2911419044237d90b60e472f57285950e1492c757dfc88259bb06"}, + {file = "ruff-0.4.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a6f29a8221d2e3d85ff0c7b4371c0e37b39c87732c969b4d90f3dad2e721c5b1"}, + {file = "ruff-0.4.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d6ef817124d72b54cc923f3444828ba24fa45c3164bc9e8f1813db2f3d3a8a11"}, + {file = "ruff-0.4.5-py3-none-win32.whl", hash = "sha256:aed8166c18b1a169a5d3ec28a49b43340949e400665555b51ee06f22813ef062"}, + {file = "ruff-0.4.5-py3-none-win_amd64.whl", hash = "sha256:b0b03c619d2b4350b4a27e34fd2ac64d0dabe1afbf43de57d0f9d8a05ecffa45"}, + {file = "ruff-0.4.5-py3-none-win_arm64.whl", hash = "sha256:9d15de3425f53161b3f5a5658d4522e4eee5ea002bf2ac7aa380743dd9ad5fba"}, + {file = "ruff-0.4.5.tar.gz", hash = "sha256:286eabd47e7d4d521d199cab84deca135557e6d1e0f0d01c29e757c3cb151b54"}, ] [[package]]