Skip to content

Commit 2df4261

Browse files
authored
Merge pull request #50 from roboflow/feature/drop_requirement_for_class_id_in_detections
feature/drop_requirement_for_class_id_in_detections
2 parents 4dcca53 + a88eff8 commit 2df4261

7 files changed

Lines changed: 144 additions & 124 deletions

File tree

docs/detection/annotate.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## BoxAnnotator
2+
3+
:::supervision.detection.annotate.BoxAnnotator
File renamed without changes.
File renamed without changes.

mkdocs.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,17 @@ nav:
2626
- API reference:
2727
- Video: video.md
2828
- Detection:
29-
- Core: detection_core.md
30-
- Utils: detection_utils.md
29+
- Core: detection/core.md
30+
- Annotate: detection/annotate.md
31+
- Utils: detection/utils.md
3132
- Draw:
3233
- Utils: draw_utils.md
3334
- Notebook: notebook.md
3435

3536
theme:
3637
name: 'material'
37-
logo: https://raw.githubusercontent.com/roboflow/supervision/main/docs/assets/roboflow_logomark_white.svg
38-
favicon: https://raw.githubusercontent.com/roboflow/supervision/main/docs/assets/roboflow_logomark_color.svg
38+
logo: https://media.roboflow.com/open-source/supervision/supervision-lenny.png?updatedAt=1678995918671
39+
favicon: https://media.roboflow.com/open-source/supervision/supervision-lenny.png?updatedAt=1678995918671
3940
palette:
4041
primary: 'deep purple'
4142
accent: 'teal'

supervision/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
__version__ = "0.3.1"
1+
__version__ = "0.3.2"
22

3-
from supervision.detection.core import BoxAnnotator, Detections
3+
from supervision.detection.annotate import BoxAnnotator
4+
from supervision.detection.core import Detections
45
from supervision.detection.line_counter import LineZone, LineZoneAnnotator
56
from supervision.detection.polygon_zone import PolygonZone, PolygonZoneAnnotator
67
from supervision.detection.utils import generate_2d_mask

supervision/detection/annotate.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
from typing import List, Optional, Union
2+
3+
import cv2
4+
import numpy as np
5+
6+
from supervision.detection.core import Detections
7+
from supervision.draw.color import Color, ColorPalette
8+
9+
10+
class BoxAnnotator:
11+
"""
12+
A class for drawing bounding boxes on an image using detections provided.
13+
14+
Attributes:
15+
color (Union[Color, ColorPalette]): The color to draw the bounding box, can be a single color or a color palette
16+
thickness (int): The thickness of the bounding box lines, default is 2
17+
text_color (Color): The color of the text on the bounding box, default is white
18+
text_scale (float): The scale of the text on the bounding box, default is 0.5
19+
text_thickness (int): The thickness of the text on the bounding box, default is 1
20+
text_padding (int): The padding around the text on the bounding box, default is 5
21+
22+
"""
23+
24+
def __init__(
25+
self,
26+
color: Union[Color, ColorPalette] = ColorPalette.default(),
27+
thickness: int = 2,
28+
text_color: Color = Color.black(),
29+
text_scale: float = 0.5,
30+
text_thickness: int = 1,
31+
text_padding: int = 10,
32+
):
33+
self.color: Union[Color, ColorPalette] = color
34+
self.thickness: int = thickness
35+
self.text_color: Color = text_color
36+
self.text_scale: float = text_scale
37+
self.text_thickness: int = text_thickness
38+
self.text_padding: int = text_padding
39+
40+
def annotate(
41+
self,
42+
scene: np.ndarray,
43+
detections: Detections,
44+
labels: Optional[List[str]] = None,
45+
skip_label: bool = False,
46+
) -> np.ndarray:
47+
"""
48+
Draws bounding boxes on the frame using the detections provided.
49+
50+
Parameters:
51+
scene (np.ndarray): The image on which the bounding boxes will be drawn
52+
detections (Detections): The detections for which the bounding boxes will be drawn
53+
labels (Optional[List[str]]): An optional list of labels corresponding to each detection. If labels is provided, the confidence score of the detection will be replaced with the label.
54+
skip_label (bool): Is set to True, skips bounding box label annotation.
55+
Returns:
56+
np.ndarray: The image with the bounding boxes drawn on it
57+
"""
58+
font = cv2.FONT_HERSHEY_SIMPLEX
59+
for i, (xyxy, confidence, class_id, tracker_id) in enumerate(detections):
60+
x1, y1, x2, y2 = xyxy.astype(int)
61+
idx = class_id if class_id is not None else i
62+
color = (
63+
self.color.by_idx(idx)
64+
if isinstance(self.color, ColorPalette)
65+
else self.color
66+
)
67+
cv2.rectangle(
68+
img=scene,
69+
pt1=(x1, y1),
70+
pt2=(x2, y2),
71+
color=color.as_bgr(),
72+
thickness=self.thickness,
73+
)
74+
if skip_label:
75+
continue
76+
77+
text = (
78+
f"{class_id}"
79+
if (labels is None or len(detections) != len(labels))
80+
else labels[i]
81+
)
82+
83+
text_width, text_height = cv2.getTextSize(
84+
text=text,
85+
fontFace=font,
86+
fontScale=self.text_scale,
87+
thickness=self.text_thickness,
88+
)[0]
89+
90+
text_x = x1 + self.text_padding
91+
text_y = y1 - self.text_padding
92+
93+
text_background_x1 = x1
94+
text_background_y1 = y1 - 2 * self.text_padding - text_height
95+
96+
text_background_x2 = x1 + 2 * self.text_padding + text_width
97+
text_background_y2 = y1
98+
99+
cv2.rectangle(
100+
img=scene,
101+
pt1=(text_background_x1, text_background_y1),
102+
pt2=(text_background_x2, text_background_y2),
103+
color=color.as_bgr(),
104+
thickness=cv2.FILLED,
105+
)
106+
cv2.putText(
107+
img=scene,
108+
text=text,
109+
org=(text_x, text_y),
110+
fontFace=font,
111+
fontScale=self.text_scale,
112+
color=self.text_color.as_rgb(),
113+
thickness=self.text_thickness,
114+
lineType=cv2.LINE_AA,
115+
)
116+
return scene

supervision/detection/core.py

Lines changed: 17 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
from __future__ import annotations
22

33
from dataclasses import dataclass
4-
from typing import Iterator, List, Optional, Tuple, Union
4+
from typing import Iterator, Optional, Tuple, Union
55

6-
import cv2
76
import numpy as np
87

98
from supervision.detection.utils import non_max_suppression
10-
from supervision.draw.color import Color, ColorPalette
119
from supervision.geometry.core import Position
1210

1311

@@ -18,21 +16,22 @@ class Detections:
1816
1917
Attributes:
2018
xyxy (np.ndarray): An array of shape `(n, 4)` containing the bounding boxes coordinates in format `[x1, y1, x2, y2]`
19+
class_id (Optional[np.ndarray]): An array of shape `(n,)` containing the class ids of the detections.
2120
confidence (Optional[np.ndarray]): An array of shape `(n,)` containing the confidence scores of the detections.
22-
class_id (np.ndarray): An array of shape `(n,)` containing the class ids of the detections.
2321
tracker_id (Optional[np.ndarray]): An array of shape `(n,)` containing the tracker ids of the detections.
2422
"""
2523

2624
xyxy: np.ndarray
27-
class_id: np.ndarray
25+
class_id: Optional[np.ndarray] = None
2826
confidence: Optional[np.ndarray] = None
2927
tracker_id: Optional[np.ndarray] = None
3028

3129
def __post_init__(self):
3230
n = len(self.xyxy)
3331
validators = [
3432
(isinstance(self.xyxy, np.ndarray) and self.xyxy.shape == (n, 4)),
35-
(isinstance(self.class_id, np.ndarray) and self.class_id.shape == (n,)),
33+
self.class_id is None
34+
or (isinstance(self.class_id, np.ndarray) and self.class_id.shape == (n,)),
3635
self.confidence is None
3736
or (
3837
isinstance(self.confidence, np.ndarray)
@@ -47,8 +46,8 @@ def __post_init__(self):
4746
if not all(validators):
4847
raise ValueError(
4948
"xyxy must be 2d np.ndarray with (n, 4) shape, "
49+
"class_id must be None or 1d np.ndarray with (n,) shape, "
5050
"confidence must be None or 1d np.ndarray with (n,) shape, "
51-
"class_id must be 1d np.ndarray with (n,) shape, "
5251
"tracker_id must be None or 1d np.ndarray with (n,) shape"
5352
)
5453

@@ -68,21 +67,26 @@ def __iter__(
6867
yield (
6968
self.xyxy[i],
7069
self.confidence[i] if self.confidence is not None else None,
71-
self.class_id[i],
70+
self.class_id[i] if self.class_id is not None else None,
7271
self.tracker_id[i] if self.tracker_id is not None else None,
7372
)
7473

7574
def __eq__(self, other: Detections):
7675
return all(
7776
[
7877
np.array_equal(self.xyxy, other.xyxy),
78+
any(
79+
[
80+
self.class_id is None and other.class_id is None,
81+
np.array_equal(self.class_id, other.class_id),
82+
]
83+
),
7984
any(
8085
[
8186
self.confidence is None and other.confidence is None,
8287
np.array_equal(self.confidence, other.confidence),
8388
]
8489
),
85-
np.array_equal(self.class_id, other.class_id),
8690
any(
8791
[
8892
self.tracker_id is None and other.tracker_id is None,
@@ -213,8 +217,10 @@ def __getitem__(self, index: np.ndarray) -> Detections:
213217
):
214218
return Detections(
215219
xyxy=self.xyxy[index],
216-
confidence=self.confidence[index],
217-
class_id=self.class_id[index],
220+
confidence=self.confidence[index]
221+
if self.confidence is not None
222+
else None,
223+
class_id=self.class_id[index] if self.class_id is not None else None,
218224
tracker_id=self.tracker_id[index]
219225
if self.tracker_id is not None
220226
else None,
@@ -273,110 +279,3 @@ def with_nms(
273279
)
274280
indices = non_max_suppression(predictions=predictions, iou_threshold=threshold)
275281
return self[indices]
276-
277-
278-
class BoxAnnotator:
279-
def __init__(
280-
self,
281-
color: Union[Color, ColorPalette] = ColorPalette.default(),
282-
thickness: int = 2,
283-
text_color: Color = Color.black(),
284-
text_scale: float = 0.5,
285-
text_thickness: int = 1,
286-
text_padding: int = 10,
287-
):
288-
"""
289-
A class for drawing bounding boxes on an image using detections provided.
290-
291-
Attributes:
292-
color (Union[Color, ColorPalette]): The color to draw the bounding box, can be a single color or a color palette
293-
thickness (int): The thickness of the bounding box lines, default is 2
294-
text_color (Color): The color of the text on the bounding box, default is white
295-
text_scale (float): The scale of the text on the bounding box, default is 0.5
296-
text_thickness (int): The thickness of the text on the bounding box, default is 1
297-
text_padding (int): The padding around the text on the bounding box, default is 5
298-
299-
"""
300-
self.color: Union[Color, ColorPalette] = color
301-
self.thickness: int = thickness
302-
self.text_color: Color = text_color
303-
self.text_scale: float = text_scale
304-
self.text_thickness: int = text_thickness
305-
self.text_padding: int = text_padding
306-
307-
def annotate(
308-
self,
309-
scene: np.ndarray,
310-
detections: Detections,
311-
labels: Optional[List[str]] = None,
312-
skip_label: bool = False,
313-
) -> np.ndarray:
314-
"""
315-
Draws bounding boxes on the frame using the detections provided.
316-
317-
Parameters:
318-
scene (np.ndarray): The image on which the bounding boxes will be drawn
319-
detections (Detections): The detections for which the bounding boxes will be drawn
320-
labels (Optional[List[str]]): An optional list of labels corresponding to each detection. If labels is provided, the confidence score of the detection will be replaced with the label.
321-
skip_label (bool): Is set to True, skips bounding box label annotation.
322-
Returns:
323-
np.ndarray: The image with the bounding boxes drawn on it
324-
"""
325-
font = cv2.FONT_HERSHEY_SIMPLEX
326-
for i, (xyxy, confidence, class_id, tracker_id) in enumerate(detections):
327-
x1, y1, x2, y2 = xyxy.astype(int)
328-
color = (
329-
self.color.by_idx(class_id)
330-
if isinstance(self.color, ColorPalette)
331-
else self.color
332-
)
333-
cv2.rectangle(
334-
img=scene,
335-
pt1=(x1, y1),
336-
pt2=(x2, y2),
337-
color=color.as_bgr(),
338-
thickness=self.thickness,
339-
)
340-
if skip_label:
341-
continue
342-
343-
text = (
344-
f"{class_id}"
345-
if (labels is None or len(detections) != len(labels))
346-
else labels[i]
347-
)
348-
349-
text_width, text_height = cv2.getTextSize(
350-
text=text,
351-
fontFace=font,
352-
fontScale=self.text_scale,
353-
thickness=self.text_thickness,
354-
)[0]
355-
356-
text_x = x1 + self.text_padding
357-
text_y = y1 - self.text_padding
358-
359-
text_background_x1 = x1
360-
text_background_y1 = y1 - 2 * self.text_padding - text_height
361-
362-
text_background_x2 = x1 + 2 * self.text_padding + text_width
363-
text_background_y2 = y1
364-
365-
cv2.rectangle(
366-
img=scene,
367-
pt1=(text_background_x1, text_background_y1),
368-
pt2=(text_background_x2, text_background_y2),
369-
color=color.as_bgr(),
370-
thickness=cv2.FILLED,
371-
)
372-
cv2.putText(
373-
img=scene,
374-
text=text,
375-
org=(text_x, text_y),
376-
fontFace=font,
377-
fontScale=self.text_scale,
378-
color=self.text_color.as_rgb(),
379-
thickness=self.text_thickness,
380-
lineType=cv2.LINE_AA,
381-
)
382-
return scene

0 commit comments

Comments
 (0)