From d7a5624d602be04042b698ca532f359448ae1a36 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Wed, 4 Oct 2023 15:28:00 +0200 Subject: [PATCH 1/4] Refactor TraceAnnotator to separate utility class --- supervision/__init__.py | 3 +- supervision/annotators/base.py | 47 -------- supervision/annotators/core.py | 86 +++++++++++-- supervision/annotators/utils.py | 89 ++++++++++++++ supervision/detection/annotate.py | 114 ------------------ .../{test_base.py => test_utils.py} | 2 +- 6 files changed, 171 insertions(+), 170 deletions(-) create mode 100644 supervision/annotators/utils.py rename test/annotators/{test_base.py => test_utils.py} (97%) diff --git a/supervision/__init__.py b/supervision/__init__.py index 7ad76847..dc27c88d 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -13,6 +13,7 @@ from supervision.annotators.core import ( EllipseAnnotator, LabelAnnotator, MaskAnnotator, + TraceAnnotator ) from supervision.classification.core import Classifications from supervision.dataset.core import ( @@ -20,7 +21,7 @@ from supervision.dataset.core import ( ClassificationDataset, DetectionDataset, ) -from supervision.detection.annotate import BoxAnnotator, TraceAnnotator +from supervision.detection.annotate import BoxAnnotator from supervision.detection.core import Detections from supervision.detection.line_counter import LineZone, LineZoneAnnotator from supervision.detection.tools.inference_slicer import InferenceSlicer diff --git a/supervision/annotators/base.py b/supervision/annotators/base.py index d9a6e29a..515ee1c8 100644 --- a/supervision/annotators/base.py +++ b/supervision/annotators/base.py @@ -1,55 +1,8 @@ from abc import ABC, abstractmethod -from enum import Enum -from typing import Union import numpy as np from supervision.detection.core import Detections -from supervision.draw.color import Color, ColorPalette - - -class ColorMap(Enum): - """ - Enum for annotator color mapping. - """ - - INDEX = "index" - CLASS = "class" - TRACK = "track" - - -def resolve_color_idx( - detections: Detections, detection_idx: int, color_map: ColorMap = ColorMap.CLASS -) -> int: - if detection_idx >= len(detections): - raise ValueError( - f"Detection index {detection_idx}" - f"is out of bounds for detections of length {len(detections)}" - ) - - if color_map == ColorMap.INDEX: - return detection_idx - elif color_map == ColorMap.CLASS: - if detections.class_id is None: - raise ValueError( - "Could not resolve color by class because" - "Detections do not have class_id" - ) - return detections.class_id[detection_idx] - elif color_map == ColorMap.TRACK: - if detections.tracker_id is None: - raise ValueError( - "Could not resolve color by track because" - "Detections do not have tracker_id" - ) - return detections.tracker_id[detection_idx] - - -def resolve_color(color: Union[Color, ColorPalette], idx: int) -> Color: - if isinstance(color, ColorPalette): - return color.by_idx(idx) - else: - return color class BaseAnnotator(ABC): diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index d108af8e..c38fc699 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -1,15 +1,12 @@ from math import sqrt -from typing import List, Tuple, Union +from typing import List, Tuple, Union, Optional import cv2 import numpy as np -from supervision.annotators.base import ( - BaseAnnotator, - ColorMap, - resolve_color, - resolve_color_idx, -) +from supervision.annotators.base import BaseAnnotator +from supervision.annotators.utils import ColorMap, resolve_color, resolve_color_idx, \ + Trace from supervision.detection.core import Detections from supervision.draw.color import Color, ColorPalette from supervision.geometry.core import Position @@ -562,3 +559,78 @@ class LabelAnnotator: lineType=cv2.LINE_AA, ) return scene + + +class TraceAnnotator: + """ + A class for drawing trace paths on an image based on detection coordinates. + """ + def __init__( + self, + color: Union[Color, ColorPalette] = ColorPalette.default(), + position: Optional[Position] = Position.CENTER, + trace_length: int = 30, + thickness: int = 2, + ): + """ + Args: + color (Union[Color, ColorPalette]): The color to draw the trace, can be + a single color or a color palette. + position (Optional[Position]): The position of the trace. + Defaults to `CENTER`. + trace_length (int): The maximum length of the trace in terms of historical + points. Defaults to `30`. + thickness (int): The thickness of the trace lines. Defaults to `2`. + """ + self.color: Union[Color, ColorPalette] = color + self.position = position + self.trace = Trace(max_size=trace_length) + self.thickness = thickness + + def annotate(self, scene: np.ndarray, detections: Detections) -> np.ndarray: + """ + Draws trace paths on the frame based on the detection coordinates provided. + + Args: + scene (np.ndarray): The image on which the traces will be drawn. + detections (Detections): The detections which include coordinates for + which the traces will be drawn. + + Returns: + np.ndarray: The image with the trace paths drawn on it. + + Example: + ```python + >>> import supervision as sv + + >>> image = ... + >>> detections = sv.Detections(...) + + >>> trace_annotator = sv.TraceAnnotator() + >>> annotated_frame = trace_annotator.annotate( + ... scene=image.copy(), + ... detections=detections + ... ) + ``` + """ + self.trace.put(detections) + + for i, (xyxy, mask, confidence, class_id, tracker_id) in enumerate(detections): + class_id = detections.class_id[i] if class_id is not None else None + idx = class_id if class_id is not None else i + color = ( + self.color.by_idx(idx) + if isinstance(self.color, ColorPalette) + else self.color + ) + + xy = self.trace.get(tracker_id=tracker_id) + if len(xy) > 1: + scene = cv2.polylines( + scene, + [xy.astype(np.int32)], + False, + color=color.as_bgr(), + thickness=self.thickness, + ) + return scene diff --git a/supervision/annotators/utils.py b/supervision/annotators/utils.py new file mode 100644 index 00000000..51a001c5 --- /dev/null +++ b/supervision/annotators/utils.py @@ -0,0 +1,89 @@ +from enum import Enum +from typing import Union, Optional + +import numpy as np + +from supervision.detection.core import Detections +from supervision.draw.color import Color, ColorPalette +from supervision.geometry.core import Position + + +class ColorMap(Enum): + """ + Enum for annotator color mapping. + """ + + INDEX = "index" + CLASS = "class" + TRACK = "track" + + +def resolve_color_idx( + detections: Detections, detection_idx: int, color_map: ColorMap = ColorMap.CLASS +) -> int: + if detection_idx >= len(detections): + raise ValueError( + f"Detection index {detection_idx}" + f"is out of bounds for detections of length {len(detections)}" + ) + + if color_map == ColorMap.INDEX: + return detection_idx + elif color_map == ColorMap.CLASS: + if detections.class_id is None: + raise ValueError( + "Could not resolve color by class because" + "Detections do not have class_id" + ) + return detections.class_id[detection_idx] + elif color_map == ColorMap.TRACK: + if detections.tracker_id is None: + raise ValueError( + "Could not resolve color by track because" + "Detections do not have tracker_id" + ) + return detections.tracker_id[detection_idx] + + +def resolve_color(color: Union[Color, ColorPalette], idx: int) -> Color: + if isinstance(color, ColorPalette): + return color.by_idx(idx) + return color + + +class Trace: + def __init__( + self, + max_size: Optional[int] = None, + start_frame_id: int = 0, + anchor: Position = Position.CENTER, + ) -> None: + self.current_frame_id = start_frame_id + self.max_size = max_size + self.anchor = anchor + + self.frame_id = np.array([], dtype=int) + self.xy = np.empty((0, 2), dtype=np.float32) + self.tracker_id = np.array([], dtype=int) + + def put(self, detections: Detections) -> None: + frame_id = np.full(len(detections), self.current_frame_id, dtype=int) + self.frame_id = np.concatenate([self.frame_id, frame_id]) + self.xy = np.concatenate( + [self.xy, detections.get_anchor_coordinates(self.anchor)] + ) + self.tracker_id = np.concatenate([self.tracker_id, detections.tracker_id]) + + unique_frame_id = np.unique(self.frame_id) + + if 0 < self.max_size < len(unique_frame_id): + max_allowed_frame_id = self.current_frame_id - self.max_size + 1 + filtering_mask = self.frame_id >= max_allowed_frame_id + self.frame_id = self.frame_id[filtering_mask] + self.xy = self.xy[filtering_mask] + self.tracker_id = self.tracker_id[filtering_mask] + + self.current_frame_id += 1 + + def get(self, tracker_id: int) -> np.ndarray: + return self.xy[self.tracker_id == tracker_id] diff --git a/supervision/detection/annotate.py b/supervision/detection/annotate.py index 83b6e0f8..5e9b379a 100644 --- a/supervision/detection/annotate.py +++ b/supervision/detection/annotate.py @@ -5,7 +5,6 @@ import numpy as np from supervision.detection.core import Detections from supervision.draw.color import Color, ColorPalette -from supervision.geometry.core import Position class BoxAnnotator: @@ -145,116 +144,3 @@ class BoxAnnotator: lineType=cv2.LINE_AA, ) return scene - - -class Trace: - def __init__( - self, - max_size: Optional[int] = None, - start_frame_id: int = 0, - anchor: Position = Position.CENTER, - ) -> None: - self.current_frame_id = start_frame_id - self.max_size = max_size - self.anchor = anchor - - self.frame_id = np.array([], dtype=int) - self.xy = np.empty((0, 2), dtype=np.float32) - self.tracker_id = np.array([], dtype=int) - - def put(self, detections: Detections) -> None: - frame_id = np.full(len(detections), self.current_frame_id, dtype=int) - self.frame_id = np.concatenate([self.frame_id, frame_id]) - self.xy = np.concatenate( - [self.xy, detections.get_anchor_coordinates(self.anchor)] - ) - self.tracker_id = np.concatenate([self.tracker_id, detections.tracker_id]) - - unique_frame_id = np.unique(self.frame_id) - - if 0 < self.max_size < len(unique_frame_id): - max_allowed_frame_id = self.current_frame_id - self.max_size + 1 - filtering_mask = self.frame_id >= max_allowed_frame_id - self.frame_id = self.frame_id[filtering_mask] - self.xy = self.xy[filtering_mask] - self.tracker_id = self.tracker_id[filtering_mask] - - self.current_frame_id += 1 - - def get(self, tracker_id: int) -> np.ndarray: - return self.xy[self.tracker_id == tracker_id] - - -class TraceAnnotator: - """ - A class for drawing trace paths on an image based on detection coordinates. - - Attributes: - color (Union[Color, ColorPalette]): The color to draw the trace, can be - a single color or a color palette. - position (Optional[Position]): The position of the trace. Defaults to `CENTER`. - trace_length (int): The maximum length of the trace in terms of historical - points. Defaults to `30`. - thickness (int): The thickness of the trace lines. Defaults to `2`. - - """ - - def __init__( - self, - color: Union[Color, ColorPalette] = ColorPalette.default(), - position: Optional[Position] = Position.CENTER, - trace_length: int = 30, - thickness: int = 2, - ): - self.color: Union[Color, ColorPalette] = color - self.position = position - self.trace = Trace(max_size=trace_length) - self.thickness = thickness - - def annotate(self, scene: np.ndarray, detections: Detections) -> np.ndarray: - """ - Draws trace paths on the frame based on the detection coordinates provided. - - Args: - scene (np.ndarray): The image on which the traces will be drawn. - detections (Detections): The detections which include coordinates for - which the traces will be drawn. - - Returns: - np.ndarray: The image with the trace paths drawn on it. - - Example: - ```python - >>> import supervision as sv - - >>> image = ... - >>> detections = sv.Detections(...) - - >>> trace_annotator = sv.TraceAnnotator() - >>> annotated_frame = trace_annotator.annotate( - ... scene=image.copy(), - ... detections=detections - ... ) - ``` - """ - self.trace.put(detections) - - for i, (xyxy, mask, confidence, class_id, tracker_id) in enumerate(detections): - class_id = detections.class_id[i] if class_id is not None else None - idx = class_id if class_id is not None else i - color = ( - self.color.by_idx(idx) - if isinstance(self.color, ColorPalette) - else self.color - ) - - xy = self.trace.get(tracker_id=tracker_id) - if len(xy) > 1: - scene = cv2.polylines( - scene, - [xy.astype(np.int32)], - False, - color=color.as_bgr(), - thickness=self.thickness, - ) - return scene diff --git a/test/annotators/test_base.py b/test/annotators/test_utils.py similarity index 97% rename from test/annotators/test_base.py rename to test/annotators/test_utils.py index 73d5a147..7e883a73 100644 --- a/test/annotators/test_base.py +++ b/test/annotators/test_utils.py @@ -4,7 +4,7 @@ from typing import Optional import pytest -from supervision.annotators.core import ColorMap, resolve_color_idx +from supervision.annotators.utils import ColorMap, resolve_color_idx from supervision.detection.core import Detections From 87d830d3a1471df65887863dc3e58c047f6f57ba Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 13:31:16 +0000 Subject: [PATCH 2/4] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto=20?= =?UTF-8?q?format=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 | 11 ++++++++--- supervision/annotators/utils.py | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/supervision/__init__.py b/supervision/__init__.py index dc27c88d..aab1eda8 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -13,7 +13,7 @@ from supervision.annotators.core import ( EllipseAnnotator, LabelAnnotator, MaskAnnotator, - TraceAnnotator + TraceAnnotator, ) from supervision.classification.core import Classifications from supervision.dataset.core import ( diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index c38fc699..2b63717e 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -1,12 +1,16 @@ from math import sqrt -from typing import List, Tuple, Union, Optional +from typing import List, Optional, Tuple, Union import cv2 import numpy as np from supervision.annotators.base import BaseAnnotator -from supervision.annotators.utils import ColorMap, resolve_color, resolve_color_idx, \ - Trace +from supervision.annotators.utils import ( + ColorMap, + Trace, + resolve_color, + resolve_color_idx, +) from supervision.detection.core import Detections from supervision.draw.color import Color, ColorPalette from supervision.geometry.core import Position @@ -565,6 +569,7 @@ class TraceAnnotator: """ A class for drawing trace paths on an image based on detection coordinates. """ + def __init__( self, color: Union[Color, ColorPalette] = ColorPalette.default(), diff --git a/supervision/annotators/utils.py b/supervision/annotators/utils.py index 51a001c5..40a8b7c0 100644 --- a/supervision/annotators/utils.py +++ b/supervision/annotators/utils.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Union, Optional +from typing import Optional, Union import numpy as np From 1eb74ed88a4e947a712cc3333f5772f42c5bad50 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Wed, 4 Oct 2023 15:35:07 +0200 Subject: [PATCH 3/4] Add TraceAnnotator description to the documentation The documentation has been updated to include the `TraceAnnotator` class. --- docs/annotators.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/annotators.md b/docs/annotators.md index 2077e58d..59e44ebd 100644 --- a/docs/annotators.md +++ b/docs/annotators.md @@ -124,6 +124,27 @@ +=== "Trace" + + ```python + >>> import supervision as sv + + >>> image = ... + >>> detections = sv.Detections(...) + + >>> trace_annotator = sv.TraceAnnotator() + >>> annotated_frame = trace_annotator.annotate( + ... scene=image.copy(), + ... detections=detections + ... ) + ``` + +
+ + ![label-annotator-example](https://media.roboflow.com/supervision-annotator-examples/label-annotator-example-2.png){ align=center width="800" } + +
+ ## BoundingBoxAnnotator :::supervision.annotators.core.BoundingBoxAnnotator @@ -147,3 +168,7 @@ ## LabelAnnotator :::supervision.annotators.core.LabelAnnotator + +## TraceAnnotator + +:::supervision.annotators.core.TraceAnnotator From 8511bbb7621d630e19331ffad57d2318ec0119a0 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Wed, 4 Oct 2023 16:15:38 +0200 Subject: [PATCH 4/4] Add Tracker ID warning and image to TraceAnnotator doc Added a warning about the requirement of `tracker_id` for TraceAnnotator in the `core.py`. Also, included an example image in the TraceAnnotator initialization for better understanding in both `core.py` and `annotators.md`. --- docs/annotators.md | 2 +- supervision/annotators/core.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/annotators.md b/docs/annotators.md index 59e44ebd..11bdfc17 100644 --- a/docs/annotators.md +++ b/docs/annotators.md @@ -141,7 +141,7 @@
- ![label-annotator-example](https://media.roboflow.com/supervision-annotator-examples/label-annotator-example-2.png){ align=center width="800" } + ![trace-annotator-example](https://media.roboflow.com/supervision-annotator-examples/trace-annotator-example.png){ align=center width="800" }
diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py index 2b63717e..350ae06b 100644 --- a/supervision/annotators/core.py +++ b/supervision/annotators/core.py @@ -568,6 +568,12 @@ class LabelAnnotator: class TraceAnnotator: """ A class for drawing trace paths on an image based on detection coordinates. + + !!! warning + + This annotator utilizes the `tracker_id`. Read + [here](https://supervision.roboflow.com/trackers/) to learn how to plug + tracking into your inference pipeline. """ def __init__( @@ -617,6 +623,9 @@ class TraceAnnotator: ... detections=detections ... ) ``` + + ![trace-annotator-example](https://media.roboflow.com/ + supervision-annotator-examples/trace-annotator-example.png) """ self.trace.put(detections)