Skip to content

Commit bc12a8e

Browse files
authored
Merge pull request #56 from roboflow/feature/rafactor_of_functions_in_notebooks_package_and_intitial_version_of_voc_xml_support
feature/rafactor_of_functions_in_notebooks_package_and_intitial_version_of_voc_xml_support
2 parents 1fa4a1c + 2be4981 commit bc12a8e

File tree

9 files changed

+232
-18
lines changed

9 files changed

+232
-18
lines changed

docs/annotation/voc.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## detections_to_voc_xml
2+
3+
:::supervision.annotation.voc.detections_to_voc_xml

docs/changelog.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
### 0.4.0 <small>April 5, 2023</small>
2+
3+
- Added [[#46](https://github.com/roboflow/supervision/discussions/48)]: `Detections.empty` to allow easy creation of empty `Detections` objects.
4+
- Added [[#56](https://github.com/roboflow/supervision/pull/56)]: `Detections.from_roboflow` to allow easy creation of `Detections` objects from Roboflow API inference results.
5+
- Added [[#56](https://github.com/roboflow/supervision/pull/56)]: `plot_images_grid` to allow easy plotting of multiple images on single plot.
6+
- Added [[#56](https://github.com/roboflow/supervision/pull/56)]: initial support for Pascal VOC XML format with `detections_to_voc_xml` method.
7+
- Changed [[#56](https://github.com/roboflow/supervision/pull/56)]: `show_frame_in_notebook` refactored and renamed to `plot_image`.
8+
19
### 0.3.2 <small>March 23, 2023</small>
210

311
- Changed [[#50](https://github.com/roboflow/supervision/issues/50)]: Allow `Detections.class_id` to be `None`.

docs/notebook/utils.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1-
## show_frame_in_notebook
1+
## plot_image
22

3-
:::supervision.notebook.utils.show_frame_in_notebook
3+
:::supervision.notebook.utils.plot_image
4+
5+
## plot_images_grid
6+
7+
:::supervision.notebook.utils.plot_images_grid

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ nav:
3131
- Utils: detection/utils.md
3232
- Draw:
3333
- Utils: draw/utils.md
34+
- Annotations:
35+
- Pascal VOC XML: annotation/voc.md
3436
- Notebook:
3537
- Utils: notebook/utils.md
3638
- Changelog: changelog.md

supervision/__init__.py

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

3+
from supervision.annotation.voc import detections_to_voc_xml
34
from supervision.detection.annotate import BoxAnnotator
45
from supervision.detection.core import Detections
56
from supervision.detection.line_counter import LineZone, LineZoneAnnotator
@@ -9,7 +10,7 @@
910
from supervision.draw.utils import draw_filled_rectangle, draw_polygon, draw_text
1011
from supervision.geometry.core import Point, Position, Rect
1112
from supervision.geometry.utils import get_polygon_center
12-
from supervision.notebook.utils import show_frame_in_notebook
13+
from supervision.notebook.utils import plot_image, plot_images_grid
1314
from supervision.video import (
1415
VideoInfo,
1516
VideoSink,

supervision/annotation/__init__.py

Whitespace-only changes.

supervision/annotation/voc.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from typing import List
2+
from xml.dom.minidom import parseString
3+
from xml.etree.ElementTree import Element, SubElement, tostring
4+
5+
from supervision.detection.core import Detections
6+
7+
8+
def detections_to_voc_xml(
9+
detections: Detections,
10+
classes: List[str],
11+
filename: str,
12+
width: int,
13+
height: int,
14+
depth: int = 3,
15+
) -> str:
16+
"""
17+
Converts Detections object to Pascal VOC XML format.
18+
19+
Args:
20+
detections (Detections): A Detections object containing bounding boxes, class ids, and other relevant information.
21+
classes (List[str]): A list of class names corresponding to the class ids in the Detections object.
22+
filename (str): The name of the image file associated with the detections.
23+
width (int): The width of the image in pixels.
24+
height (int): The height of the image in pixels.
25+
depth (int, optional): The number of color channels in the image. Defaults to 3 for RGB images.
26+
27+
Returns:
28+
str: An XML string in Pascal VOC format representing the detections.
29+
30+
Examples:
31+
```python
32+
>>> import numpy as np
33+
>>> import supervision as sv
34+
35+
>>> xyxy = np.array([
36+
... [50, 30, 200, 180],
37+
... [20, 40, 150, 190]
38+
... ])
39+
>>> class_id = np.array([1, 0])
40+
>>> detections = Detections(xyxy=xyxy, class_id=class_id)
41+
42+
>>> classes = ["dog", "cat"]
43+
44+
>>> voc_xml = detections_to_voc_xml(
45+
... detections=detections,
46+
... classes=classes,
47+
... filename="image1.jpg",
48+
... width=500,
49+
... height=400
50+
... )
51+
```
52+
"""
53+
54+
# Create root element
55+
annotation = Element("annotation")
56+
57+
# Add folder element
58+
folder = SubElement(annotation, "folder")
59+
folder.text = "VOC"
60+
61+
# Add filename element
62+
fname = SubElement(annotation, "filename")
63+
fname.text = filename
64+
65+
# Add source element
66+
source = SubElement(annotation, "source")
67+
database = SubElement(source, "database")
68+
database.text = "roboflow.ai"
69+
70+
# Add size element
71+
size = SubElement(annotation, "size")
72+
w = SubElement(size, "width")
73+
w.text = str(width)
74+
h = SubElement(size, "height")
75+
h.text = str(height)
76+
d = SubElement(size, "depth")
77+
d.text = str(depth)
78+
79+
# Add segmented element
80+
segmented = SubElement(annotation, "segmented")
81+
segmented.text = "0"
82+
83+
# Add object elements
84+
for i in range(detections.xyxy.shape[0]):
85+
obj = SubElement(annotation, "object")
86+
87+
class_id = detections.class_id[i] if detections.class_id is not None else None
88+
name = SubElement(obj, "name")
89+
name.text = classes[class_id] if class_id is not None else "unknown"
90+
91+
bndbox = SubElement(obj, "bndbox")
92+
xmin = SubElement(bndbox, "xmin")
93+
xmin.text = str(int(detections.xyxy[i, 0]))
94+
ymin = SubElement(bndbox, "ymin")
95+
ymin.text = str(int(detections.xyxy[i, 1]))
96+
xmax = SubElement(bndbox, "xmax")
97+
xmax.text = str(int(detections.xyxy[i, 2]))
98+
ymax = SubElement(bndbox, "ymax")
99+
ymax.text = str(int(detections.xyxy[i, 3]))
100+
101+
# Generate XML string
102+
xml_string = parseString(tostring(annotation)).toprettyxml(indent=" ")
103+
104+
return xml_string

supervision/detection/core.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

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

66
import numpy as np
77

@@ -176,6 +176,31 @@ def from_detectron2(cls, detectron2_results) -> Detections:
176176
.astype(int),
177177
)
178178

179+
@classmethod
180+
def from_roboflow(cls, roboflow_result: dict, class_list: List[str]) -> Detections:
181+
xyxy = []
182+
confidence = []
183+
class_id = []
184+
185+
for prediction in roboflow_result["predictions"]:
186+
x = prediction["x"]
187+
y = prediction["y"]
188+
width = prediction["width"]
189+
height = prediction["height"]
190+
x_min = x - width / 2
191+
y_min = y - height / 2
192+
x_max = x_min + width
193+
y_max = y_min + height
194+
xyxy.append([x_min, y_min, x_max, y_max])
195+
class_id.append(class_list.index(prediction["class"]))
196+
confidence.append(prediction["confidence"])
197+
198+
return Detections(
199+
xyxy=np.array(xyxy),
200+
confidence=np.array(confidence),
201+
class_id=np.array(class_id).astype(int),
202+
)
203+
179204
@classmethod
180205
def from_coco_annotations(cls, coco_annotation: dict) -> Detections:
181206
xyxy, class_id = [], []
@@ -187,6 +212,14 @@ def from_coco_annotations(cls, coco_annotation: dict) -> Detections:
187212

188213
return cls(xyxy=np.array(xyxy), class_id=np.array(class_id))
189214

215+
@classmethod
216+
def empty(cls) -> Detections:
217+
return cls(
218+
xyxy=np.empty((0, 4), dtype=np.float32),
219+
confidence=np.array([], dtype=np.float32),
220+
class_id=np.array([], dtype=int),
221+
)
222+
190223
def get_anchor_coordinates(self, anchor: Position) -> np.ndarray:
191224
"""
192225
Returns the bounding box coordinates for a specific anchor.

supervision/notebook/utils.py

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,92 @@
1-
from typing import Tuple
1+
from typing import List, Optional, Tuple
22

33
import cv2
44
import matplotlib.pyplot as plt
55
import numpy as np
66

77

8-
def show_frame_in_notebook(
9-
frame: np.ndarray, size: Tuple[int, int] = (10, 10), cmap: str = "gray"
10-
):
8+
def plot_image(
9+
image: np.ndarray, size: Tuple[int, int] = (10, 10), cmap: Optional[str] = "gray"
10+
) -> None:
1111
"""
12-
Display a frame in Jupyter Notebook using Matplotlib
12+
Plots image using matplotlib.
1313
14-
Attributes:
15-
frame (np.ndarray): The frame to be displayed.
16-
size (Tuple[int, int]): The size of the plot. default:(10,10)
17-
cmap (str): the colormap to use for single channel images. default:gray
14+
Args:
15+
image (np.ndarray): The frame to be displayed.
16+
size (Tuple[int, int]): The size of the plot.
17+
cmap (str): the colormap to use for single channel images.
1818
1919
Examples:
2020
```python
21+
>>> import cv2
2122
>>> import supervision as sv
2223
24+
>>> image = cv2.imread("path/to/image.jpg")
25+
2326
%matplotlib inline
24-
>>> sv.show_frame_in_notebook(frame, (16, 16))
27+
>>> sv.plot_image(image, (16, 16))
2528
```
2629
"""
27-
if frame.ndim == 2:
30+
if image.ndim == 2:
2831
plt.figure(figsize=size)
29-
plt.imshow(frame, cmap=cmap)
32+
plt.imshow(image, cmap=cmap)
3033
else:
3134
plt.figure(figsize=size)
32-
plt.imshow(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
35+
plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
36+
plt.show()
37+
38+
39+
def plot_images_grid(
40+
images: List[np.ndarray],
41+
grid_size: Tuple[int, int],
42+
titles: Optional[List[str]] = None,
43+
size: Tuple[int, int] = (12, 12),
44+
) -> None:
45+
"""
46+
Plots images in a grid using matplotlib.
47+
48+
Args:
49+
images (List[np.ndarray]): A list of images as numpy arrays.
50+
grid_size (Tuple[int, int]): A tuple specifying the number of rows and columns for the grid.
51+
titles (Optional[List[str]]): A list of titles for each image. Defaults to None.
52+
size (Tuple[int, int]): A tuple specifying the width and height of the entire plot in inches.
53+
54+
Raises:
55+
ValueError: If the number of images exceeds the grid size.
56+
57+
Examples:
58+
```python
59+
>>> import cv2
60+
>>> import supervision as sv
61+
62+
>>> image1 = cv2.imread("path/to/image1.jpg")
63+
>>> image2 = cv2.imread("path/to/image2.jpg")
64+
>>> image3 = cv2.imread("path/to/image3.jpg")
65+
66+
>>> images = [image1, image2, image3]
67+
>>> titles = ["Image 1", "Image 2", "Image 3"]
68+
69+
%matplotlib inline
70+
>>> plot_images_grid(images, grid_size=(2, 2), titles=titles, figsize=(16, 16))
71+
```
72+
"""
73+
74+
nrows, ncols = grid_size
75+
76+
if len(images) > nrows * ncols:
77+
raise ValueError(
78+
"The number of images exceeds the grid size. Please increase the grid size or reduce the number of images."
79+
)
80+
81+
fig, axes = plt.subplots(nrows=nrows, ncols=ncols, figsize=size)
82+
83+
for idx, ax in enumerate(axes.flat):
84+
if idx < len(images):
85+
ax.imshow(cv2.cvtColor(images[idx], cv2.COLOR_BGR2RGB))
86+
if titles is not None and idx < len(titles):
87+
ax.set_title(titles[idx])
88+
ax.axis("off")
89+
else:
90+
ax.axis("off")
91+
3392
plt.show()

0 commit comments

Comments
 (0)