Files
supervision/supervision/annotators/core.py
T
Onuralp SEZER 2bc58e7938 feat(core.py): 🚀 Allow custom NumPy array for custom_values in PercentageBarAnnotator
fix(core.py): 🐞 Treat CENTER_OF_MASS the same as CENTER in PercentageBarAnnotator
refactor(core.py): Move border mapping logic to separate method in PercentageBarAnnotator

Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2024-01-23 04:34:59 +03:00

1679 lines
59 KiB
Python

from math import sqrt
from typing import List, Optional, Tuple, Union
import cv2
import numpy as np
from supervision.annotators.base import BaseAnnotator
from supervision.annotators.utils import ColorLookup, Trace, resolve_color
from supervision.config import CLASS_NAME_DATA_FIELD
from supervision.detection.core import Detections
from supervision.detection.utils import clip_boxes, mask_to_polygons
from supervision.draw.color import Color, ColorPalette
from supervision.draw.utils import draw_polygon
from supervision.geometry.core import Position
class BoundingBoxAnnotator(BaseAnnotator):
"""
A class for drawing bounding boxes on an image using provided detections.
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
thickness: int = 2,
color_lookup: ColorLookup = ColorLookup.CLASS,
):
"""
Args:
color (Union[Color, ColorPalette]): The color or color palette to use for
annotating detections.
thickness (int): Thickness of the bounding box lines.
color_lookup (str): Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Union[Color, ColorPalette] = color
self.thickness: int = thickness
self.color_lookup: ColorLookup = color_lookup
def annotate(
self,
scene: np.ndarray,
detections: Detections,
custom_color_lookup: Optional[np.ndarray] = None,
) -> np.ndarray:
"""
Annotates the given scene with bounding boxes based on the provided detections.
Args:
scene (np.ndarray): The image where bounding boxes will be drawn.
detections (Detections): Object detections to annotate.
custom_color_lookup (Optional[np.ndarray]): Custom color lookup array.
Allows to override the default color mapping strategy.
Returns:
The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> bounding_box_annotator = sv.BoundingBoxAnnotator()
>>> annotated_frame = bounding_box_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![bounding-box-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/bounding-box-annotator-example-purple.png)
"""
for detection_idx in range(len(detections)):
x1, y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
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,
)
cv2.rectangle(
img=scene,
pt1=(x1, y1),
pt2=(x2, y2),
color=color.as_bgr(),
thickness=self.thickness,
)
return scene
class MaskAnnotator(BaseAnnotator):
"""
A class for drawing masks on an image using provided detections.
!!! warning
This annotator utilizes the `sv.Detections.mask`.
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
opacity: float = 0.5,
color_lookup: ColorLookup = ColorLookup.CLASS,
):
"""
Args:
color (Union[Color, ColorPalette]): The color or color palette to use for
annotating detections.
opacity (float): Opacity of the overlay mask. Must be between `0` and `1`.
color_lookup (str): Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Union[Color, ColorPalette] = color
self.opacity = opacity
self.color_lookup: ColorLookup = color_lookup
def annotate(
self,
scene: np.ndarray,
detections: Detections,
custom_color_lookup: Optional[np.ndarray] = None,
) -> np.ndarray:
"""
Annotates the given scene with masks based on the provided detections.
Args:
scene (np.ndarray): The image where masks will be drawn.
detections (Detections): Object detections to annotate.
custom_color_lookup (Optional[np.ndarray]): Custom color lookup array.
Allows to override the default color mapping strategy.
Returns:
The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> mask_annotator = sv.MaskAnnotator()
>>> annotated_frame = mask_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![mask-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/mask-annotator-example-purple.png)
"""
if detections.mask is None:
return scene
colored_mask = np.array(scene, copy=True, dtype=np.uint8)
for detection_idx in np.flip(np.argsort(detections.area)):
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,
)
mask = detections.mask[detection_idx]
colored_mask[mask] = color.as_bgr()
scene = cv2.addWeighted(colored_mask, self.opacity, scene, 1 - self.opacity, 0)
return scene.astype(np.uint8)
class PolygonAnnotator(BaseAnnotator):
"""
A class for drawing polygons on an image using provided detections.
!!! warning
This annotator utilizes the `sv.Detections.mask`.
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
thickness: int = 2,
color_lookup: ColorLookup = ColorLookup.CLASS,
):
"""
Args:
color (Union[Color, ColorPalette]): The color or color palette to use for
annotating detections.
thickness (int): Thickness of the polygon lines.
color_lookup (str): Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Union[Color, ColorPalette] = color
self.thickness: int = thickness
self.color_lookup: ColorLookup = color_lookup
def annotate(
self,
scene: np.ndarray,
detections: Detections,
custom_color_lookup: Optional[np.ndarray] = None,
) -> np.ndarray:
"""
Annotates the given scene with polygons based on the provided detections.
Args:
scene (np.ndarray): The image where polygons will be drawn.
detections (Detections): Object detections to annotate.
custom_color_lookup (Optional[np.ndarray]): Custom color lookup array.
Allows to override the default color mapping strategy.
Returns:
The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> polygon_annotator = sv.PolygonAnnotator()
>>> annotated_frame = polygon_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![polygon-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/polygon-annotator-example-purple.png)
"""
if detections.mask is None:
return scene
for detection_idx in range(len(detections)):
mask = detections.mask[detection_idx]
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,
)
for polygon in mask_to_polygons(mask=mask):
scene = draw_polygon(
scene=scene,
polygon=polygon,
color=color,
thickness=self.thickness,
)
return scene
class ColorAnnotator(BaseAnnotator):
"""
A class for drawing box masks on an image using provided detections.
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
opacity: float = 0.5,
color_lookup: ColorLookup = ColorLookup.CLASS,
):
"""
Args:
color (Union[Color, ColorPalette]): The color or color palette to use for
annotating detections.
opacity (float): Opacity of the overlay mask. Must be between `0` and `1`.
color_lookup (str): Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Union[Color, ColorPalette] = color
self.color_lookup: ColorLookup = color_lookup
self.opacity = opacity
def annotate(
self,
scene: np.ndarray,
detections: Detections,
custom_color_lookup: Optional[np.ndarray] = None,
) -> np.ndarray:
"""
Annotates the given scene with box masks based on the provided detections.
Args:
scene (np.ndarray): The image where bounding boxes will be drawn.
detections (Detections): Object detections to annotate.
custom_color_lookup (Optional[np.ndarray]): Custom color lookup array.
Allows to override the default color mapping strategy.
Returns:
The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> color_annotator = sv.ColorAnnotator()
>>> annotated_frame = color_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![box-mask-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/box-mask-annotator-example-purple.png)
"""
mask_image = scene.copy()
for detection_idx in range(len(detections)):
x1, y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
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,
)
cv2.rectangle(
img=scene,
pt1=(x1, y1),
pt2=(x2, y2),
color=color.as_bgr(),
thickness=-1,
)
scene = cv2.addWeighted(
scene, self.opacity, mask_image, 1 - self.opacity, gamma=0
)
return scene
class HaloAnnotator(BaseAnnotator):
"""
A class for drawing Halos on an image using provided detections.
!!! warning
This annotator utilizes the `sv.Detections.mask`.
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
opacity: float = 0.8,
kernel_size: int = 40,
color_lookup: ColorLookup = ColorLookup.CLASS,
):
"""
Args:
color (Union[Color, ColorPalette]): The color or color palette to use for
annotating detections.
opacity (float): Opacity of the overlay mask. Must be between `0` and `1`.
kernel_size (int): The size of the average pooling kernel used for creating
the halo.
color_lookup (str): Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Union[Color, ColorPalette] = color
self.opacity = opacity
self.color_lookup: ColorLookup = color_lookup
self.kernel_size: int = kernel_size
def annotate(
self,
scene: np.ndarray,
detections: Detections,
custom_color_lookup: Optional[np.ndarray] = None,
) -> np.ndarray:
"""
Annotates the given scene with halos based on the provided detections.
Args:
scene (np.ndarray): The image where masks will be drawn.
detections (Detections): Object detections to annotate.
custom_color_lookup (Optional[np.ndarray]): Custom color lookup array.
Allows to override the default color mapping strategy.
Returns:
The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> halo_annotator = sv.HaloAnnotator()
>>> annotated_frame = halo_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![halo-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/halo-annotator-example-purple.png)
"""
if detections.mask is None:
return scene
colored_mask = np.zeros_like(scene, dtype=np.uint8)
fmask = np.array([False] * scene.shape[0] * scene.shape[1]).reshape(
scene.shape[0], scene.shape[1]
)
for detection_idx in np.flip(np.argsort(detections.area)):
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,
)
mask = detections.mask[detection_idx]
fmask = np.logical_or(fmask, mask)
color_bgr = color.as_bgr()
colored_mask[mask] = color_bgr
colored_mask = cv2.blur(colored_mask, (self.kernel_size, self.kernel_size))
colored_mask[fmask] = [0, 0, 0]
gray = cv2.cvtColor(colored_mask, cv2.COLOR_BGR2GRAY)
alpha = self.opacity * gray / gray.max()
alpha_mask = alpha[:, :, np.newaxis]
scene = np.uint8(scene * (1 - alpha_mask) + colored_mask * self.opacity)
return scene
class EllipseAnnotator(BaseAnnotator):
"""
A class for drawing ellipses on an image using provided detections.
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
thickness: int = 2,
start_angle: int = -45,
end_angle: int = 235,
color_lookup: ColorLookup = ColorLookup.CLASS,
):
"""
Args:
color (Union[Color, ColorPalette]): The color or color palette to use for
annotating detections.
thickness (int): Thickness of the ellipse lines.
start_angle (int): Starting angle of the ellipse.
end_angle (int): Ending angle of the ellipse.
color_lookup (str): Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Union[Color, ColorPalette] = color
self.thickness: int = thickness
self.start_angle: int = start_angle
self.end_angle: int = end_angle
self.color_lookup: ColorLookup = color_lookup
def annotate(
self,
scene: np.ndarray,
detections: Detections,
custom_color_lookup: Optional[np.ndarray] = None,
) -> np.ndarray:
"""
Annotates the given scene with ellipses based on the provided detections.
Args:
scene (np.ndarray): The image where ellipses will be drawn.
detections (Detections): Object detections to annotate.
custom_color_lookup (Optional[np.ndarray]): Custom color lookup array.
Allows to override the default color mapping strategy.
Returns:
The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> ellipse_annotator = sv.EllipseAnnotator()
>>> annotated_frame = ellipse_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![ellipse-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/ellipse-annotator-example-purple.png)
"""
for detection_idx in range(len(detections)):
x1, y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
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,
)
center = (int((x1 + x2) / 2), y2)
width = x2 - x1
cv2.ellipse(
scene,
center=center,
axes=(int(width), int(0.35 * width)),
angle=0.0,
startAngle=self.start_angle,
endAngle=self.end_angle,
color=color.as_bgr(),
thickness=self.thickness,
lineType=cv2.LINE_4,
)
return scene
class BoxCornerAnnotator(BaseAnnotator):
"""
A class for drawing box corners on an image using provided detections.
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
thickness: int = 4,
corner_length: int = 15,
color_lookup: ColorLookup = ColorLookup.CLASS,
):
"""
Args:
color (Union[Color, ColorPalette]): The color or color palette to use for
annotating detections.
thickness (int): Thickness of the corner lines.
corner_length (int): Length of each corner line.
color_lookup (str): Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Union[Color, ColorPalette] = color
self.thickness: int = thickness
self.corner_length: int = corner_length
self.color_lookup: ColorLookup = color_lookup
def annotate(
self,
scene: np.ndarray,
detections: Detections,
custom_color_lookup: Optional[np.ndarray] = None,
) -> np.ndarray:
"""
Annotates the given scene with box corners based on the provided detections.
Args:
scene (np.ndarray): The image where box corners will be drawn.
detections (Detections): Object detections to annotate.
custom_color_lookup (Optional[np.ndarray]): Custom color lookup array.
Allows to override the default color mapping strategy.
Returns:
The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> corner_annotator = sv.BoxCornerAnnotator()
>>> annotated_frame = corner_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![box-corner-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/box-corner-annotator-example-purple.png)
"""
for detection_idx in range(len(detections)):
x1, y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
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,
)
corners = [(x1, y1), (x2, y1), (x1, y2), (x2, y2)]
for x, y in corners:
x_end = x + self.corner_length if x == x1 else x - self.corner_length
cv2.line(
scene, (x, y), (x_end, y), color.as_bgr(), thickness=self.thickness
)
y_end = y + self.corner_length if y == y1 else y - self.corner_length
cv2.line(
scene, (x, y), (x, y_end), color.as_bgr(), thickness=self.thickness
)
return scene
class CircleAnnotator(BaseAnnotator):
"""
A class for drawing circle on an image using provided detections.
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
thickness: int = 2,
color_lookup: ColorLookup = ColorLookup.CLASS,
):
"""
Args:
color (Union[Color, ColorPalette]): The color or color palette to use for
annotating detections.
thickness (int): Thickness of the circle line.
color_lookup (str): Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Union[Color, ColorPalette] = color
self.thickness: int = thickness
self.color_lookup: ColorLookup = color_lookup
def annotate(
self,
scene: np.ndarray,
detections: Detections,
custom_color_lookup: Optional[np.ndarray] = None,
) -> np.ndarray:
"""
Annotates the given scene with circles based on the provided detections.
Args:
scene (np.ndarray): The image where box corners will be drawn.
detections (Detections): Object detections to annotate.
custom_color_lookup (Optional[np.ndarray]): Custom color lookup array.
Allows to override the default color mapping strategy.
Returns:
The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> circle_annotator = sv.CircleAnnotator()
>>> annotated_frame = circle_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![circle-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/circle-annotator-example-purple.png)
"""
for detection_idx in range(len(detections)):
x1, y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
center = ((x1 + x2) // 2, (y1 + y2) // 2)
distance = sqrt((x1 - center[0]) ** 2 + (y1 - center[1]) ** 2)
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,
)
cv2.circle(
img=scene,
center=center,
radius=int(distance),
color=color.as_bgr(),
thickness=self.thickness,
)
return scene
class DotAnnotator(BaseAnnotator):
"""
A class for drawing dots on an image at specific coordinates based on provided
detections.
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
radius: int = 4,
position: Position = Position.CENTER,
color_lookup: ColorLookup = ColorLookup.CLASS,
):
"""
Args:
color (Union[Color, ColorPalette]): The color or color palette to use for
annotating detections.
radius (int): Radius of the drawn dots.
position (Position): The anchor position for placing the dot.
color_lookup (ColorLookup): Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Union[Color, ColorPalette] = color
self.radius: int = radius
self.position: Position = position
self.color_lookup: ColorLookup = color_lookup
def annotate(
self,
scene: np.ndarray,
detections: Detections,
custom_color_lookup: Optional[np.ndarray] = None,
) -> np.ndarray:
"""
Annotates the given scene with dots based on the provided detections.
Args:
scene (np.ndarray): The image where dots will be drawn.
detections (Detections): Object detections to annotate.
custom_color_lookup (Optional[np.ndarray]): Custom color lookup array.
Allows to override the default color mapping strategy.
Returns:
The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> dot_annotator = sv.DotAnnotator()
>>> annotated_frame = dot_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![dot-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/dot-annotator-example-purple.png)
"""
xy = detections.get_anchors_coordinates(anchor=self.position)
for detection_idx in range(len(detections)):
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,
)
center = (int(xy[detection_idx, 0]), int(xy[detection_idx, 1]))
cv2.circle(scene, center, self.radius, color.as_bgr(), -1)
return scene
class LabelAnnotator:
"""
A class for annotating labels on an image using provided detections.
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
text_color: Color = Color.BLACK,
text_scale: float = 0.5,
text_thickness: int = 1,
text_padding: int = 10,
text_position: Position = Position.TOP_LEFT,
color_lookup: ColorLookup = ColorLookup.CLASS,
):
"""
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.
text_scale (float): Font scale for the text.
text_thickness (int): Thickness of the text characters.
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 (str): Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Union[Color, ColorPalette] = 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
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,
)
def annotate(
self,
scene: np.ndarray,
detections: Detections,
labels: List[str] = None,
custom_color_lookup: Optional[np.ndarray] = None,
) -> np.ndarray:
"""
Annotates the given scene with labels based on the provided detections.
Args:
scene (np.ndarray): The image where labels will be drawn.
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.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> label_annotator = sv.LabelAnnotator(text_position=sv.Position.CENTER)
>>> annotated_frame = label_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![label-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/label-annotator-example-purple.png)
"""
font = cv2.FONT_HERSHEY_SIMPLEX
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)
text_w, text_h = cv2.getTextSize(
text=text,
fontFace=font,
fontScale=self.text_scale,
thickness=self.text_thickness,
)[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(
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
text_y = text_background_xyxy[1] + self.text_padding + text_h
cv2.rectangle(
img=scene,
pt1=(text_background_xyxy[0], text_background_xyxy[1]),
pt2=(text_background_xyxy[2], text_background_xyxy[3]),
color=color.as_bgr(),
thickness=cv2.FILLED,
)
cv2.putText(
img=scene,
text=text,
org=(text_x, text_y),
fontFace=font,
fontScale=self.text_scale,
color=self.text_color.as_rgb(),
thickness=self.text_thickness,
lineType=cv2.LINE_AA,
)
return scene
class BlurAnnotator(BaseAnnotator):
"""
A class for blurring regions in an image using provided detections.
"""
def __init__(self, kernel_size: int = 15):
"""
Args:
kernel_size (int): The size of the average pooling kernel used for blurring.
"""
self.kernel_size: int = kernel_size
def annotate(
self,
scene: np.ndarray,
detections: Detections,
) -> np.ndarray:
"""
Annotates the given scene by blurring regions based on the provided detections.
Args:
scene (np.ndarray): The image where blurring will be applied.
detections (Detections): Object detections to annotate.
Returns:
The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> blur_annotator = sv.BlurAnnotator()
>>> annotated_frame = circle_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![blur-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/blur-annotator-example-purple.png)
"""
image_height, image_width = scene.shape[:2]
clipped_xyxy = clip_boxes(
xyxy=detections.xyxy, resolution_wh=(image_width, image_height)
).astype(int)
for x1, y1, x2, y2 in clipped_xyxy:
roi = scene[y1:y2, x1:x2]
roi = cv2.blur(roi, (self.kernel_size, self.kernel_size))
scene[y1:y2, x1:x2] = roi
return scene
class TraceAnnotator:
"""
A class for drawing trace paths on an image based on detection coordinates.
!!! warning
This annotator utilizes the `sv.Detections.tracker_id`. Read
[here](https://supervision.roboflow.com/trackers/) to learn how to plug
tracking into your inference pipeline.
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
position: Position = Position.CENTER,
trace_length: int = 30,
thickness: int = 2,
color_lookup: ColorLookup = ColorLookup.CLASS,
):
"""
Args:
color (Union[Color, ColorPalette]): The color to draw the trace, can be
a single color or a color palette.
position (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`.
color_lookup (str): Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Union[Color, ColorPalette] = color
self.trace = Trace(max_size=trace_length, anchor=position)
self.thickness = thickness
self.color_lookup: ColorLookup = color_lookup
def annotate(
self,
scene: np.ndarray,
detections: Detections,
custom_color_lookup: Optional[np.ndarray] = None,
) -> 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.
custom_color_lookup (Optional[np.ndarray]): Custom color lookup array.
Allows to override the default color mapping strategy.
Returns:
The annotated image.
Example:
```python
>>> import supervision as sv
>>> from ultralytics import YOLO
>>> model = YOLO('yolov8x.pt')
>>> trace_annotator = sv.TraceAnnotator()
>>> video_info = sv.VideoInfo.from_video_path(video_path='...')
>>> frames_generator = sv.get_video_frames_generator(source_path='...')
>>> tracker = sv.ByteTrack()
>>> with sv.VideoSink(target_path='...', video_info=video_info) as sink:
... for frame in frames_generator:
... result = model(frame)[0]
... detections = sv.Detections.from_ultralytics(result)
... detections = tracker.update_with_detections(detections)
... annotated_frame = trace_annotator.annotate(
... scene=frame.copy(),
... detections=detections)
... sink.write_frame(frame=annotated_frame)
```
![trace-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/trace-annotator-example-purple.png)
"""
self.trace.put(detections)
for detection_idx in range(len(detections)):
tracker_id = int(detections.tracker_id[detection_idx])
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,
)
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
class HeatMapAnnotator:
"""
A class for drawing heatmaps on an image based on provided detections.
Heat accumulates over time and is drawn as a semi-transparent overlay
of blurred circles.
"""
def __init__(
self,
position: Position = Position.BOTTOM_CENTER,
opacity: float = 0.2,
radius: int = 40,
kernel_size: int = 25,
top_hue: int = 0,
low_hue: int = 125,
):
"""
Args:
position (Position): The position of the heatmap. Defaults to
`BOTTOM_CENTER`.
opacity (float): Opacity of the overlay mask, between 0 and 1.
radius (int): Radius of the heat circle.
kernel_size (int): Kernel size for blurring the heatmap.
top_hue (int): Hue at the top of the heatmap. Defaults to 0 (red).
low_hue (int): Hue at the bottom of the heatmap. Defaults to 125 (blue).
"""
self.position = position
self.opacity = opacity
self.radius = radius
self.kernel_size = kernel_size
self.heat_mask = None
self.top_hue = top_hue
self.low_hue = low_hue
def annotate(self, scene: np.ndarray, detections: Detections) -> np.ndarray:
"""
Annotates the scene with a heatmap based on the provided detections.
Args:
scene (np.ndarray): The image where the heatmap will be drawn.
detections (Detections): Object detections to annotate.
Returns:
Annotated image.
Example:
```python
>>> import supervision as sv
>>> from ultralytics import YOLO
>>> model = YOLO('yolov8x.pt')
>>> heat_map_annotator = sv.HeatMapAnnotator()
>>> video_info = sv.VideoInfo.from_video_path(video_path='...')
>>> frames_generator = get_video_frames_generator(source_path='...')
>>> with sv.VideoSink(target_path='...', video_info=video_info) as sink:
... for frame in frames_generator:
... result = model(frame)[0]
... detections = sv.Detections.from_ultralytics(result)
... annotated_frame = heat_map_annotator.annotate(
... scene=frame.copy(),
... detections=detections)
... sink.write_frame(frame=annotated_frame)
```
![heatmap-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/heat-map-annotator-example-purple.png)
"""
if self.heat_mask is None:
self.heat_mask = np.zeros(scene.shape[:2])
mask = np.zeros(scene.shape[:2])
for xy in detections.get_anchors_coordinates(self.position):
cv2.circle(mask, (int(xy[0]), int(xy[1])), self.radius, 1, -1)
self.heat_mask = mask + self.heat_mask
temp = self.heat_mask.copy()
temp = self.low_hue - temp / temp.max() * (self.low_hue - self.top_hue)
temp = temp.astype(np.uint8)
if self.kernel_size is not None:
temp = cv2.blur(temp, (self.kernel_size, self.kernel_size))
hsv = np.zeros(scene.shape)
hsv[..., 0] = temp
hsv[..., 1] = 255
hsv[..., 2] = 255
temp = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR)
mask = cv2.cvtColor(self.heat_mask.astype(np.uint8), cv2.COLOR_GRAY2BGR) > 0
scene[mask] = cv2.addWeighted(temp, self.opacity, scene, 1 - self.opacity, 0)[
mask
]
return scene
class PixelateAnnotator(BaseAnnotator):
"""
A class for pixelating regions in an image using provided detections.
"""
def __init__(self, pixel_size: int = 20):
"""
Args:
pixel_size (int): The size of the pixelation.
"""
self.pixel_size: int = pixel_size
def annotate(
self,
scene: np.ndarray,
detections: Detections,
) -> np.ndarray:
"""
Annotates the given scene by pixelating regions based on the provided
detections.
Args:
scene (np.ndarray): The image where pixelating will be applied.
detections (Detections): Object detections to annotate.
Returns:
The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> pixelate_annotator = sv.PixelateAnnotator()
>>> annotated_frame = pixelate_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![pixelate-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/pixelate-annotator-example-10.png)
"""
image_height, image_width = scene.shape[:2]
clipped_xyxy = clip_boxes(
xyxy=detections.xyxy, resolution_wh=(image_width, image_height)
).astype(int)
for x1, y1, x2, y2 in clipped_xyxy:
roi = scene[y1:y2, x1:x2]
scaled_up_roi = cv2.resize(
src=roi, dsize=None, fx=1 / self.pixel_size, fy=1 / self.pixel_size
)
scaled_down_roi = cv2.resize(
src=scaled_up_roi,
dsize=(roi.shape[1], roi.shape[0]),
interpolation=cv2.INTER_NEAREST,
)
scene[y1:y2, x1:x2] = scaled_down_roi
return scene
class TriangleAnnotator(BaseAnnotator):
"""
A class for drawing triangle markers on an image at specific coordinates based on
provided detections.
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
base: int = 10,
height: int = 10,
position: Position = Position.TOP_CENTER,
color_lookup: ColorLookup = ColorLookup.CLASS,
):
"""
Args:
color (Union[Color, ColorPalette]): The color or color palette to use for
annotating detections.
base (int): The base width of the triangle.
height (int): The height of the triangle.
position (Position): The anchor position for placing the triangle.
color_lookup (ColorLookup): Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Union[Color, ColorPalette] = color
self.base: int = base
self.height: int = height
self.position: Position = position
self.color_lookup: ColorLookup = color_lookup
def annotate(
self,
scene: np.ndarray,
detections: Detections,
custom_color_lookup: Optional[np.ndarray] = None,
) -> np.ndarray:
"""
Annotates the given scene with triangles based on the provided detections.
Args:
scene (np.ndarray): The image where triangles will be drawn.
detections (Detections): Object detections to annotate.
custom_color_lookup (Optional[np.ndarray]): Custom color lookup array.
Allows to override the default color mapping strategy.
Returns:
np.ndarray: The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> triangle_annotator = sv.TriangleAnnotator()
>>> annotated_frame = triangle_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![triangle-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/triangle-annotator-example.png)
"""
xy = detections.get_anchors_coordinates(anchor=self.position)
for detection_idx in range(len(detections)):
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,
)
tip_x, tip_y = int(xy[detection_idx, 0]), int(xy[detection_idx, 1])
vertices = np.array(
[
[tip_x - self.base // 2, tip_y - self.height],
[tip_x + self.base // 2, tip_y - self.height],
[tip_x, tip_y],
],
np.int32,
)
cv2.fillPoly(scene, [vertices], color.as_bgr())
return scene
class RoundBoxAnnotator(BaseAnnotator):
"""
A class for drawing bounding boxes with round edges on an image
using provided detections.
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
thickness: int = 2,
color_lookup: ColorLookup = ColorLookup.CLASS,
roundness: float = 0.6,
):
"""
Args:
color (Union[Color, ColorPalette]): The color or color palette to use for
annotating detections.
thickness (int): Thickness of the bounding box lines.
color_lookup (str): Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
roundness (float): Percent of roundness for edges of bounding box.
Value must be float 0 < roundness <= 1.0
By default roundness percent is calculated based on smaller side
length (width or height).
"""
self.color: Union[Color, ColorPalette] = color
self.thickness: int = thickness
self.color_lookup: ColorLookup = color_lookup
if not 0 < roundness <= 1.0:
raise ValueError("roundness attribute must be float between (0, 1.0]")
self.roundness: float = roundness
def annotate(
self,
scene: np.ndarray,
detections: Detections,
custom_color_lookup: Optional[np.ndarray] = None,
) -> np.ndarray:
"""
Annotates the given scene with bounding boxes with rounded edges
based on the provided detections.
Args:
scene (np.ndarray): The image where rounded bounding boxes will be drawn.
detections (Detections): Object detections to annotate.
custom_color_lookup (Optional[np.ndarray]): Custom color lookup array.
Allows to override the default color mapping strategy.
Returns:
The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> round_box_annotator = sv.RoundBoxAnnotator()
>>> annotated_frame = round_box_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![round-box-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/round-box-annotator-example.png)
"""
for detection_idx in range(len(detections)):
x1, y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
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,
)
radius = (
int((x2 - x1) // 2 * self.roundness)
if abs(x1 - x2) < abs(y1 - y2)
else int((y2 - y1) // 2 * self.roundness)
)
circle_coordinates = [
((x1 + radius), (y1 + radius)),
((x2 - radius), (y1 + radius)),
((x2 - radius), (y2 - radius)),
((x1 + radius), (y2 - radius)),
]
line_coordinates = [
((x1 + radius, y1), (x2 - radius, y1)),
((x2, y1 + radius), (x2, y2 - radius)),
((x1 + radius, y2), (x2 - radius, y2)),
((x1, y1 + radius), (x1, y2 - radius)),
]
start_angles = (180, 270, 0, 90)
end_angles = (270, 360, 90, 180)
for center_coordinates, line, start_angle, end_angle in zip(
circle_coordinates, line_coordinates, start_angles, end_angles
):
cv2.ellipse(
img=scene,
center=center_coordinates,
axes=(radius, radius),
angle=0,
startAngle=start_angle,
endAngle=end_angle,
color=color.as_bgr(),
thickness=self.thickness,
)
cv2.line(
img=scene,
pt1=line[0],
pt2=line[1],
color=color.as_bgr(),
thickness=self.thickness,
)
return scene
class PercentageBarAnnotator(BaseAnnotator):
"""
A class for drawing percentage bars of confidence threshold on an image
using provided detections.
"""
def __init__(
self,
height: int = 16,
width: int = 80,
color: Union[Color, ColorPalette] = ColorPalette.default(),
border_color: Color = Color.black(),
position: Position = Position.TOP_CENTER,
color_lookup: ColorLookup = ColorLookup.CLASS,
border_thickness: int = None,
custom_values: Optional[
Union[List[float], List[np.float32], np.ndarray]
] = None,
):
"""
Args:
height (int): The height in pixels of the percentage bar.
width (int): The width in pixels of the percentage bar.
color (Union[Color, ColorPalette]): The color or color palette to use for
annotating detections.
border_color (Color): The color of the border lines.
position (Position): The anchor position of drawing the percentage bar.
color_lookup (str): Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
border_thickness (int): The thickness of the border lines.
"""
self.height: int = height
self.width: int = width
self.color: Union[Color, ColorPalette] = color
self.border_color: Color = border_color
self.position: Position = position
self.color_lookup: ColorLookup = color_lookup
self.border_thickness: int = border_thickness
self.__custom_value_check(custom_values)
self.custom_values = custom_values
def annotate(
self,
scene: np.ndarray,
detections: Detections,
custom_color_lookup: Optional[np.ndarray] = None,
) -> np.ndarray:
"""
Annotates the given scene with percentage bars based on the provided
detections.
Args:
scene (np.ndarray): The image where percentage bars will be drawn.
detections (Detections): Object detections to annotate.
custom_color_lookup (Optional[np.ndarray]): Custom color lookup array.
Allows to override the default color mapping strategy.
Returns:
The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> percentage_bar_annotator = sv.BoundingBoxAnnotator()
>>> annotated_frame = percentage_bar_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![percentage-bar-example](https://media.roboflow.com/
supervision-annotator-examples/percentage-bar-example.png)
"""
num_values = min(
len(detections),
len(self.custom_values) if self.custom_values else len(detections),
)
for detection_idx in range(num_values):
cx, cy = detections.get_anchors_coordinates(anchor=self.position)[
detection_idx
].astype(int)
value = (
self.custom_values[detection_idx]
if self.custom_values
else detections.confidence[detection_idx]
)
if self.border_thickness is None:
self.border_thickness = int(0.15 * self.height)
border_mapping = self.__border_position(cx, cy)
border_coords = border_mapping[self.position]
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,
)
cv2.rectangle(
img=scene,
pt1=border_coords[0],
pt2=(
border_coords[0][0]
+ int((border_coords[1][0] - border_coords[0][0]) * value),
border_coords[1][1],
),
color=color.as_bgr(),
thickness=-1,
)
cv2.rectangle(
img=scene,
pt1=border_coords[0],
pt2=border_coords[1],
color=self.border_color.as_bgr(),
thickness=self.border_thickness,
)
return scene
def __border_position(self, cx, cy):
border_mapping = {
Position.TOP_LEFT: ((cx - self.width, cy - self.height), (cx, cy)),
Position.TOP_CENTER: (
(cx - self.width // 2, cy),
(cx + self.width // 2, cy - self.height),
),
Position.TOP_RIGHT: ((cx, cy), (cx + self.width, cy - self.height)),
Position.CENTER_LEFT: (
(cx - self.width, cy - self.height // 2),
(cx, cy + self.height // 2),
),
Position.CENTER: (
(cx - self.width // 2, cy - self.height // 2),
(cx + self.width // 2, cy + self.height // 2),
),
Position.CENTER_OF_MASS: (
(cx - self.width // 2, cy - self.height // 2),
(cx + self.width // 2, cy + self.height // 2),
),
Position.CENTER_RIGHT: (
(cx, cy - self.height // 2),
(cx + self.width, cy + self.height // 2),
),
Position.BOTTOM_LEFT: ((cx - self.width, cy), (cx, cy + self.height)),
Position.BOTTOM_CENTER: (
(cx - self.width // 2, cy),
(cx + self.width // 2, cy + self.height),
),
Position.BOTTOM_RIGHT: ((cx, cy), (cx + self.width, cy + self.height)),
}
return border_mapping
def __custom_value_check(self, custom_values):
if custom_values is not None:
if isinstance(custom_values, np.ndarray):
if custom_values.ndim != 1:
raise ValueError("custom_values array must be one-dimensional")
if custom_values.dtype != np.float32:
raise ValueError(
"custom_values array elements must be of type np.float32"
)
else: # assuming list
if not all(isinstance(x, (float, np.float32)) for x in custom_values):
raise ValueError(
"All elements in custom_values list must be of "
"type float or np.float32"
)
if not (np.min(custom_values) >= 0 and np.max(custom_values) <= 1):
raise ValueError("All values in custom_values must be between 0 and 1")