eye-cv 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eye/__init__.py +115 -0
- eye/__init___supervision_original.py +120 -0
- eye/annotators/__init__.py +0 -0
- eye/annotators/base.py +22 -0
- eye/annotators/core.py +2699 -0
- eye/annotators/line.py +107 -0
- eye/annotators/modern.py +529 -0
- eye/annotators/trace.py +142 -0
- eye/annotators/utils.py +177 -0
- eye/assets/__init__.py +2 -0
- eye/assets/downloader.py +95 -0
- eye/assets/list.py +83 -0
- eye/classification/__init__.py +0 -0
- eye/classification/core.py +188 -0
- eye/config.py +2 -0
- eye/core/__init__.py +0 -0
- eye/core/trackers/__init__.py +1 -0
- eye/core/trackers/botsort_tracker.py +336 -0
- eye/core/trackers/bytetrack_tracker.py +284 -0
- eye/core/trackers/sort_tracker.py +200 -0
- eye/core/tracking.py +146 -0
- eye/dataset/__init__.py +0 -0
- eye/dataset/core.py +919 -0
- eye/dataset/formats/__init__.py +0 -0
- eye/dataset/formats/coco.py +258 -0
- eye/dataset/formats/pascal_voc.py +279 -0
- eye/dataset/formats/yolo.py +272 -0
- eye/dataset/utils.py +259 -0
- eye/detection/__init__.py +0 -0
- eye/detection/auto_convert.py +155 -0
- eye/detection/core.py +1529 -0
- eye/detection/detections_enhanced.py +392 -0
- eye/detection/line_zone.py +859 -0
- eye/detection/lmm.py +184 -0
- eye/detection/overlap_filter.py +270 -0
- eye/detection/tools/__init__.py +0 -0
- eye/detection/tools/csv_sink.py +181 -0
- eye/detection/tools/inference_slicer.py +288 -0
- eye/detection/tools/json_sink.py +142 -0
- eye/detection/tools/polygon_zone.py +202 -0
- eye/detection/tools/smoother.py +123 -0
- eye/detection/tools/smoothing.py +179 -0
- eye/detection/tools/smoothing_config.py +202 -0
- eye/detection/tools/transformers.py +247 -0
- eye/detection/utils.py +1175 -0
- eye/draw/__init__.py +0 -0
- eye/draw/color.py +154 -0
- eye/draw/utils.py +374 -0
- eye/filters.py +112 -0
- eye/geometry/__init__.py +0 -0
- eye/geometry/core.py +128 -0
- eye/geometry/utils.py +47 -0
- eye/keypoint/__init__.py +0 -0
- eye/keypoint/annotators.py +442 -0
- eye/keypoint/core.py +687 -0
- eye/keypoint/skeletons.py +2647 -0
- eye/metrics/__init__.py +21 -0
- eye/metrics/core.py +72 -0
- eye/metrics/detection.py +843 -0
- eye/metrics/f1_score.py +648 -0
- eye/metrics/mean_average_precision.py +628 -0
- eye/metrics/mean_average_recall.py +697 -0
- eye/metrics/precision.py +653 -0
- eye/metrics/recall.py +652 -0
- eye/metrics/utils/__init__.py +0 -0
- eye/metrics/utils/object_size.py +158 -0
- eye/metrics/utils/utils.py +9 -0
- eye/py.typed +0 -0
- eye/quick.py +104 -0
- eye/tracker/__init__.py +0 -0
- eye/tracker/byte_tracker/__init__.py +0 -0
- eye/tracker/byte_tracker/core.py +386 -0
- eye/tracker/byte_tracker/kalman_filter.py +205 -0
- eye/tracker/byte_tracker/matching.py +69 -0
- eye/tracker/byte_tracker/single_object_track.py +178 -0
- eye/tracker/byte_tracker/utils.py +18 -0
- eye/utils/__init__.py +0 -0
- eye/utils/conversion.py +132 -0
- eye/utils/file.py +159 -0
- eye/utils/image.py +794 -0
- eye/utils/internal.py +200 -0
- eye/utils/iterables.py +84 -0
- eye/utils/notebook.py +114 -0
- eye/utils/video.py +307 -0
- eye/utils_eye/__init__.py +1 -0
- eye/utils_eye/geometry.py +71 -0
- eye/utils_eye/nms.py +55 -0
- eye/validators/__init__.py +140 -0
- eye/web.py +271 -0
- eye_cv-1.0.0.dist-info/METADATA +319 -0
- eye_cv-1.0.0.dist-info/RECORD +94 -0
- eye_cv-1.0.0.dist-info/WHEEL +5 -0
- eye_cv-1.0.0.dist-info/licenses/LICENSE +21 -0
- eye_cv-1.0.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING, Dict, List, Tuple
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import numpy.typing as npt
|
|
8
|
+
|
|
9
|
+
from eye.dataset.utils import (
|
|
10
|
+
approximate_mask_with_polygons,
|
|
11
|
+
map_detections_class_id,
|
|
12
|
+
mask_to_rle,
|
|
13
|
+
rle_to_mask,
|
|
14
|
+
)
|
|
15
|
+
from eye.detection.core import Detections
|
|
16
|
+
from eye.detection.utils import (
|
|
17
|
+
contains_holes,
|
|
18
|
+
contains_multiple_segments,
|
|
19
|
+
polygon_to_mask,
|
|
20
|
+
)
|
|
21
|
+
from eye.utils.file import read_json_file, save_json_file
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from eye.dataset.core import DetectionDataset
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def coco_categories_to_classes(coco_categories: List[dict]) -> List[str]:
|
|
28
|
+
return [
|
|
29
|
+
category["name"]
|
|
30
|
+
for category in sorted(coco_categories, key=lambda category: category["id"])
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_coco_class_index_mapping(
|
|
35
|
+
coco_categories: List[dict], target_classes: List[str]
|
|
36
|
+
) -> Dict[int, int]:
|
|
37
|
+
source_class_to_index = {
|
|
38
|
+
category["name"]: category["id"] for category in coco_categories
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
source_class_to_index[target_class_name]: target_class_index
|
|
42
|
+
for target_class_index, target_class_name in enumerate(target_classes)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def classes_to_coco_categories(classes: List[str]) -> List[dict]:
|
|
47
|
+
return [
|
|
48
|
+
{
|
|
49
|
+
"id": class_id,
|
|
50
|
+
"name": class_name,
|
|
51
|
+
"supercategory": "common-objects",
|
|
52
|
+
}
|
|
53
|
+
for class_id, class_name in enumerate(classes)
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def group_coco_annotations_by_image_id(
|
|
58
|
+
coco_annotations: List[dict],
|
|
59
|
+
) -> Dict[int, List[dict]]:
|
|
60
|
+
annotations = {}
|
|
61
|
+
for annotation in coco_annotations:
|
|
62
|
+
image_id = annotation["image_id"]
|
|
63
|
+
if image_id not in annotations:
|
|
64
|
+
annotations[image_id] = []
|
|
65
|
+
annotations[image_id].append(annotation)
|
|
66
|
+
return annotations
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def coco_annotations_to_masks(
|
|
70
|
+
image_annotations: List[dict], resolution_wh: Tuple[int, int]
|
|
71
|
+
) -> npt.NDArray[np.bool_]:
|
|
72
|
+
return np.array(
|
|
73
|
+
[
|
|
74
|
+
rle_to_mask(
|
|
75
|
+
rle=np.array(image_annotation["segmentation"]["counts"]),
|
|
76
|
+
resolution_wh=resolution_wh,
|
|
77
|
+
)
|
|
78
|
+
if image_annotation["iscrowd"]
|
|
79
|
+
else polygon_to_mask(
|
|
80
|
+
polygon=np.reshape(
|
|
81
|
+
np.asarray(image_annotation["segmentation"], dtype=np.int32),
|
|
82
|
+
(-1, 2),
|
|
83
|
+
),
|
|
84
|
+
resolution_wh=resolution_wh,
|
|
85
|
+
)
|
|
86
|
+
for image_annotation in image_annotations
|
|
87
|
+
],
|
|
88
|
+
dtype=bool,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def coco_annotations_to_detections(
|
|
93
|
+
image_annotations: List[dict], resolution_wh: Tuple[int, int], with_masks: bool
|
|
94
|
+
) -> Detections:
|
|
95
|
+
if not image_annotations:
|
|
96
|
+
return Detections.empty()
|
|
97
|
+
|
|
98
|
+
class_ids = [
|
|
99
|
+
image_annotation["category_id"] for image_annotation in image_annotations
|
|
100
|
+
]
|
|
101
|
+
xyxy = [image_annotation["bbox"] for image_annotation in image_annotations]
|
|
102
|
+
xyxy = np.asarray(xyxy)
|
|
103
|
+
xyxy[:, 2:4] += xyxy[:, 0:2]
|
|
104
|
+
|
|
105
|
+
if with_masks:
|
|
106
|
+
mask = coco_annotations_to_masks(
|
|
107
|
+
image_annotations=image_annotations, resolution_wh=resolution_wh
|
|
108
|
+
)
|
|
109
|
+
return Detections(
|
|
110
|
+
class_id=np.asarray(class_ids, dtype=int), xyxy=xyxy, mask=mask
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return Detections(xyxy=xyxy, class_id=np.asarray(class_ids, dtype=int))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def detections_to_coco_annotations(
|
|
117
|
+
detections: Detections,
|
|
118
|
+
image_id: int,
|
|
119
|
+
annotation_id: int,
|
|
120
|
+
min_image_area_percentage: float = 0.0,
|
|
121
|
+
max_image_area_percentage: float = 1.0,
|
|
122
|
+
approximation_percentage: float = 0.75,
|
|
123
|
+
) -> Tuple[List[Dict], int]:
|
|
124
|
+
coco_annotations = []
|
|
125
|
+
for xyxy, mask, _, class_id, _, _ in detections:
|
|
126
|
+
box_width, box_height = xyxy[2] - xyxy[0], xyxy[3] - xyxy[1]
|
|
127
|
+
segmentation = []
|
|
128
|
+
iscrowd = 0
|
|
129
|
+
if mask is not None:
|
|
130
|
+
iscrowd = contains_holes(mask=mask) or contains_multiple_segments(mask=mask)
|
|
131
|
+
|
|
132
|
+
if iscrowd:
|
|
133
|
+
segmentation = {
|
|
134
|
+
"counts": mask_to_rle(mask=mask),
|
|
135
|
+
"size": list(mask.shape[:2]),
|
|
136
|
+
}
|
|
137
|
+
else:
|
|
138
|
+
segmentation = [
|
|
139
|
+
list(
|
|
140
|
+
approximate_mask_with_polygons(
|
|
141
|
+
mask=mask,
|
|
142
|
+
min_image_area_percentage=min_image_area_percentage,
|
|
143
|
+
max_image_area_percentage=max_image_area_percentage,
|
|
144
|
+
approximation_percentage=approximation_percentage,
|
|
145
|
+
)[0].flatten()
|
|
146
|
+
)
|
|
147
|
+
]
|
|
148
|
+
coco_annotation = {
|
|
149
|
+
"id": annotation_id,
|
|
150
|
+
"image_id": image_id,
|
|
151
|
+
"category_id": int(class_id),
|
|
152
|
+
"bbox": [xyxy[0], xyxy[1], box_width, box_height],
|
|
153
|
+
"area": box_width * box_height,
|
|
154
|
+
"segmentation": segmentation,
|
|
155
|
+
"iscrowd": iscrowd,
|
|
156
|
+
}
|
|
157
|
+
coco_annotations.append(coco_annotation)
|
|
158
|
+
annotation_id += 1
|
|
159
|
+
return coco_annotations, annotation_id
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def load_coco_annotations(
|
|
163
|
+
images_directory_path: str,
|
|
164
|
+
annotations_path: str,
|
|
165
|
+
force_masks: bool = False,
|
|
166
|
+
) -> Tuple[List[str], List[str], Dict[str, Detections]]:
|
|
167
|
+
coco_data = read_json_file(file_path=annotations_path)
|
|
168
|
+
classes = coco_categories_to_classes(coco_categories=coco_data["categories"])
|
|
169
|
+
class_index_mapping = build_coco_class_index_mapping(
|
|
170
|
+
coco_categories=coco_data["categories"], target_classes=classes
|
|
171
|
+
)
|
|
172
|
+
coco_images = coco_data["images"]
|
|
173
|
+
coco_annotations_groups = group_coco_annotations_by_image_id(
|
|
174
|
+
coco_annotations=coco_data["annotations"]
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
images = []
|
|
178
|
+
annotations = {}
|
|
179
|
+
|
|
180
|
+
for coco_image in coco_images:
|
|
181
|
+
image_name, image_width, image_height = (
|
|
182
|
+
coco_image["file_name"],
|
|
183
|
+
coco_image["width"],
|
|
184
|
+
coco_image["height"],
|
|
185
|
+
)
|
|
186
|
+
image_annotations = coco_annotations_groups.get(coco_image["id"], [])
|
|
187
|
+
image_path = os.path.join(images_directory_path, image_name)
|
|
188
|
+
|
|
189
|
+
annotation = coco_annotations_to_detections(
|
|
190
|
+
image_annotations=image_annotations,
|
|
191
|
+
resolution_wh=(image_width, image_height),
|
|
192
|
+
with_masks=force_masks,
|
|
193
|
+
)
|
|
194
|
+
annotation = map_detections_class_id(
|
|
195
|
+
source_to_target_mapping=class_index_mapping,
|
|
196
|
+
detections=annotation,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
images.append(image_path)
|
|
200
|
+
annotations[image_path] = annotation
|
|
201
|
+
|
|
202
|
+
return classes, images, annotations
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def save_coco_annotations(
|
|
206
|
+
dataset: "DetectionDataset",
|
|
207
|
+
annotation_path: str,
|
|
208
|
+
min_image_area_percentage: float = 0.0,
|
|
209
|
+
max_image_area_percentage: float = 1.0,
|
|
210
|
+
approximation_percentage: float = 0.75,
|
|
211
|
+
) -> None:
|
|
212
|
+
Path(annotation_path).parent.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
licenses = [
|
|
214
|
+
{
|
|
215
|
+
"id": 1,
|
|
216
|
+
"url": "https://creativecommons.org/licenses/by/4.0/",
|
|
217
|
+
"name": "CC BY 4.0",
|
|
218
|
+
}
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
coco_annotations = []
|
|
222
|
+
coco_images = []
|
|
223
|
+
coco_categories = classes_to_coco_categories(classes=dataset.classes)
|
|
224
|
+
|
|
225
|
+
image_id, annotation_id = 1, 1
|
|
226
|
+
for image_path, image, annotation in dataset:
|
|
227
|
+
image_height, image_width, _ = image.shape
|
|
228
|
+
image_name = f"{Path(image_path).stem}{Path(image_path).suffix}"
|
|
229
|
+
coco_image = {
|
|
230
|
+
"id": image_id,
|
|
231
|
+
"license": 1,
|
|
232
|
+
"file_name": image_name,
|
|
233
|
+
"height": image_height,
|
|
234
|
+
"width": image_width,
|
|
235
|
+
"date_captured": datetime.now().strftime("%m/%d/%Y,%H:%M:%S"),
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
coco_images.append(coco_image)
|
|
239
|
+
coco_annotation, annotation_id = detections_to_coco_annotations(
|
|
240
|
+
detections=annotation,
|
|
241
|
+
image_id=image_id,
|
|
242
|
+
annotation_id=annotation_id,
|
|
243
|
+
min_image_area_percentage=min_image_area_percentage,
|
|
244
|
+
max_image_area_percentage=max_image_area_percentage,
|
|
245
|
+
approximation_percentage=approximation_percentage,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
coco_annotations.extend(coco_annotation)
|
|
249
|
+
image_id += 1
|
|
250
|
+
|
|
251
|
+
annotation_dict = {
|
|
252
|
+
"info": {},
|
|
253
|
+
"licenses": licenses,
|
|
254
|
+
"categories": coco_categories,
|
|
255
|
+
"images": coco_images,
|
|
256
|
+
"annotations": coco_annotations,
|
|
257
|
+
}
|
|
258
|
+
save_json_file(annotation_dict, file_path=annotation_path)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Dict, List, Optional, Tuple
|
|
4
|
+
from xml.etree.ElementTree import Element, SubElement
|
|
5
|
+
|
|
6
|
+
import cv2
|
|
7
|
+
import numpy as np
|
|
8
|
+
from defusedxml.ElementTree import parse, tostring
|
|
9
|
+
from defusedxml.minidom import parseString
|
|
10
|
+
|
|
11
|
+
from eye.dataset.utils import approximate_mask_with_polygons
|
|
12
|
+
from eye.detection.core import Detections
|
|
13
|
+
from eye.detection.utils import polygon_to_mask, polygon_to_xyxy
|
|
14
|
+
from eye.utils.file import list_files_with_extensions
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def object_to_pascal_voc(
|
|
18
|
+
xyxy: np.ndarray, name: str, polygon: Optional[np.ndarray] = None
|
|
19
|
+
) -> Element:
|
|
20
|
+
root = Element("object")
|
|
21
|
+
|
|
22
|
+
object_name = SubElement(root, "name")
|
|
23
|
+
object_name.text = name
|
|
24
|
+
|
|
25
|
+
# https://github.com/roboflow/eye/issues/144
|
|
26
|
+
xyxy += 1
|
|
27
|
+
|
|
28
|
+
bndbox = SubElement(root, "bndbox")
|
|
29
|
+
xmin = SubElement(bndbox, "xmin")
|
|
30
|
+
xmin.text = str(int(xyxy[0]))
|
|
31
|
+
ymin = SubElement(bndbox, "ymin")
|
|
32
|
+
ymin.text = str(int(xyxy[1]))
|
|
33
|
+
xmax = SubElement(bndbox, "xmax")
|
|
34
|
+
xmax.text = str(int(xyxy[2]))
|
|
35
|
+
ymax = SubElement(bndbox, "ymax")
|
|
36
|
+
ymax.text = str(int(xyxy[3]))
|
|
37
|
+
|
|
38
|
+
if polygon is not None:
|
|
39
|
+
# https://github.com/roboflow/eye/issues/144
|
|
40
|
+
polygon += 1
|
|
41
|
+
object_polygon = SubElement(root, "polygon")
|
|
42
|
+
for index, point in enumerate(polygon, start=1):
|
|
43
|
+
x_coordinate, y_coordinate = point
|
|
44
|
+
x = SubElement(object_polygon, f"x{index}")
|
|
45
|
+
x.text = str(x_coordinate)
|
|
46
|
+
y = SubElement(object_polygon, f"y{index}")
|
|
47
|
+
y.text = str(y_coordinate)
|
|
48
|
+
|
|
49
|
+
return root
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def detections_to_pascal_voc(
|
|
53
|
+
detections: Detections,
|
|
54
|
+
classes: List[str],
|
|
55
|
+
filename: str,
|
|
56
|
+
image_shape: Tuple[int, int, int],
|
|
57
|
+
min_image_area_percentage: float = 0.0,
|
|
58
|
+
max_image_area_percentage: float = 1.0,
|
|
59
|
+
approximation_percentage: float = 0.75,
|
|
60
|
+
) -> str:
|
|
61
|
+
"""
|
|
62
|
+
Converts Detections object to Pascal VOC XML format.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
detections (Detections): A Detections object containing bounding boxes,
|
|
66
|
+
class ids, and other relevant information.
|
|
67
|
+
classes (List[str]): A list of class names corresponding to the
|
|
68
|
+
class ids in the Detections object.
|
|
69
|
+
filename (str): The name of the image file associated with the detections.
|
|
70
|
+
image_shape (Tuple[int, int, int]): The shape of the image
|
|
71
|
+
file associated with the detections.
|
|
72
|
+
min_image_area_percentage (float): Minimum detection area
|
|
73
|
+
relative to area of image associated with it.
|
|
74
|
+
max_image_area_percentage (float): Maximum detection area
|
|
75
|
+
relative to area of image associated with it.
|
|
76
|
+
approximation_percentage (float): The percentage of
|
|
77
|
+
polygon points to be removed from the input polygon, in the range [0, 1).
|
|
78
|
+
Returns:
|
|
79
|
+
str: An XML string in Pascal VOC format representing the detections.
|
|
80
|
+
"""
|
|
81
|
+
height, width, depth = image_shape
|
|
82
|
+
|
|
83
|
+
# Create root element
|
|
84
|
+
annotation = Element("annotation")
|
|
85
|
+
|
|
86
|
+
# Add folder element
|
|
87
|
+
folder = SubElement(annotation, "folder")
|
|
88
|
+
folder.text = "VOC"
|
|
89
|
+
|
|
90
|
+
# Add filename element
|
|
91
|
+
file_name = SubElement(annotation, "filename")
|
|
92
|
+
file_name.text = filename
|
|
93
|
+
|
|
94
|
+
# Add source element
|
|
95
|
+
source = SubElement(annotation, "source")
|
|
96
|
+
database = SubElement(source, "database")
|
|
97
|
+
database.text = "roboflow.ai"
|
|
98
|
+
|
|
99
|
+
# Add size element
|
|
100
|
+
size = SubElement(annotation, "size")
|
|
101
|
+
w = SubElement(size, "width")
|
|
102
|
+
w.text = str(width)
|
|
103
|
+
h = SubElement(size, "height")
|
|
104
|
+
h.text = str(height)
|
|
105
|
+
d = SubElement(size, "depth")
|
|
106
|
+
d.text = str(depth)
|
|
107
|
+
|
|
108
|
+
# Add segmented element
|
|
109
|
+
segmented = SubElement(annotation, "segmented")
|
|
110
|
+
segmented.text = "0"
|
|
111
|
+
|
|
112
|
+
# Add object elements
|
|
113
|
+
for xyxy, mask, _, class_id, _, _ in detections:
|
|
114
|
+
name = classes[class_id]
|
|
115
|
+
if mask is not None:
|
|
116
|
+
polygons = approximate_mask_with_polygons(
|
|
117
|
+
mask=mask,
|
|
118
|
+
min_image_area_percentage=min_image_area_percentage,
|
|
119
|
+
max_image_area_percentage=max_image_area_percentage,
|
|
120
|
+
approximation_percentage=approximation_percentage,
|
|
121
|
+
)
|
|
122
|
+
for polygon in polygons:
|
|
123
|
+
xyxy = polygon_to_xyxy(polygon=polygon)
|
|
124
|
+
next_object = object_to_pascal_voc(
|
|
125
|
+
xyxy=xyxy, name=name, polygon=polygon
|
|
126
|
+
)
|
|
127
|
+
annotation.append(next_object)
|
|
128
|
+
else:
|
|
129
|
+
next_object = object_to_pascal_voc(xyxy=xyxy, name=name)
|
|
130
|
+
annotation.append(next_object)
|
|
131
|
+
|
|
132
|
+
# Generate XML string
|
|
133
|
+
xml_string = parseString(tostring(annotation)).toprettyxml(indent=" ")
|
|
134
|
+
return xml_string
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def load_pascal_voc_annotations(
|
|
138
|
+
images_directory_path: str,
|
|
139
|
+
annotations_directory_path: str,
|
|
140
|
+
force_masks: bool = False,
|
|
141
|
+
) -> Tuple[List[str], List[str], Dict[str, Detections]]:
|
|
142
|
+
"""
|
|
143
|
+
Loads PASCAL VOC XML annotations and returns the image name,
|
|
144
|
+
a Detections instance, and a list of class names.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
images_directory_path (str): The path to the directory containing the images.
|
|
148
|
+
annotations_directory_path (str): The path to the directory containing the
|
|
149
|
+
PASCAL VOC annotation files.
|
|
150
|
+
force_masks (bool): If True, forces masks to be loaded for all
|
|
151
|
+
annotations, regardless of whether they are present.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Tuple[List[str], List[str], Dict[str, Detections]]: A tuple with a list
|
|
155
|
+
of class names, a list of paths to images, and a dictionary with image
|
|
156
|
+
paths as keys and corresponding Detections instances as values.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
image_paths = [
|
|
160
|
+
str(path)
|
|
161
|
+
for path in list_files_with_extensions(
|
|
162
|
+
directory=images_directory_path, extensions=["jpg", "jpeg", "png"]
|
|
163
|
+
)
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
classes: List[str] = []
|
|
167
|
+
annotations = {}
|
|
168
|
+
|
|
169
|
+
for image_path in image_paths:
|
|
170
|
+
image_stem = Path(image_path).stem
|
|
171
|
+
annotation_path = os.path.join(annotations_directory_path, f"{image_stem}.xml")
|
|
172
|
+
if not os.path.exists(annotation_path):
|
|
173
|
+
annotations[image_path] = Detections.empty()
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
tree = parse(annotation_path)
|
|
177
|
+
root = tree.getroot()
|
|
178
|
+
|
|
179
|
+
image = cv2.imread(image_path)
|
|
180
|
+
resolution_wh = (image.shape[1], image.shape[0])
|
|
181
|
+
annotation, classes = detections_from_xml_obj(
|
|
182
|
+
root, classes, resolution_wh, force_masks
|
|
183
|
+
)
|
|
184
|
+
annotations[image_path] = annotation
|
|
185
|
+
|
|
186
|
+
return classes, image_paths, annotations
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def detections_from_xml_obj(
|
|
190
|
+
root: Element, classes: List[str], resolution_wh, force_masks: bool = False
|
|
191
|
+
) -> Tuple[Detections, List[str]]:
|
|
192
|
+
"""
|
|
193
|
+
Converts an XML object in Pascal VOC format to a Detections object.
|
|
194
|
+
Expected XML format:
|
|
195
|
+
<annotation>
|
|
196
|
+
...
|
|
197
|
+
<object>
|
|
198
|
+
<name>dog</name>
|
|
199
|
+
<bndbox>
|
|
200
|
+
<xmin>48</xmin>
|
|
201
|
+
<ymin>240</ymin>
|
|
202
|
+
<xmax>195</xmax>
|
|
203
|
+
<ymax>371</ymax>
|
|
204
|
+
</bndbox>
|
|
205
|
+
<polygon>
|
|
206
|
+
<x1>48</x1>
|
|
207
|
+
<y1>240</y1>
|
|
208
|
+
<x2>195</x2>
|
|
209
|
+
<y2>240</y2>
|
|
210
|
+
<x3>195</x3>
|
|
211
|
+
<y3>371</y3>
|
|
212
|
+
<x4>48</x4>
|
|
213
|
+
<y4>371</y4>
|
|
214
|
+
</polygon>
|
|
215
|
+
</object>
|
|
216
|
+
</annotation>
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Tuple[Detections, List[str]]: A tuple containing a Detections object and an
|
|
220
|
+
updated list of class names, extended with the class names
|
|
221
|
+
from the XML object.
|
|
222
|
+
"""
|
|
223
|
+
xyxy = []
|
|
224
|
+
class_names = []
|
|
225
|
+
masks = []
|
|
226
|
+
with_masks = False
|
|
227
|
+
extended_classes = classes[:]
|
|
228
|
+
for obj in root.findall("object"):
|
|
229
|
+
class_name = obj.find("name").text
|
|
230
|
+
class_names.append(class_name)
|
|
231
|
+
|
|
232
|
+
bbox = obj.find("bndbox")
|
|
233
|
+
x1 = int(bbox.find("xmin").text)
|
|
234
|
+
y1 = int(bbox.find("ymin").text)
|
|
235
|
+
x2 = int(bbox.find("xmax").text)
|
|
236
|
+
y2 = int(bbox.find("ymax").text)
|
|
237
|
+
|
|
238
|
+
xyxy.append([x1, y1, x2, y2])
|
|
239
|
+
|
|
240
|
+
with_masks = obj.find("polygon") is not None
|
|
241
|
+
with_masks = force_masks if force_masks else with_masks
|
|
242
|
+
|
|
243
|
+
for polygon in obj.findall("polygon"):
|
|
244
|
+
polygon = parse_polygon_points(polygon)
|
|
245
|
+
# https://github.com/roboflow/eye/issues/144
|
|
246
|
+
polygon -= 1
|
|
247
|
+
|
|
248
|
+
mask_from_polygon = polygon_to_mask(
|
|
249
|
+
polygon=polygon,
|
|
250
|
+
resolution_wh=resolution_wh,
|
|
251
|
+
)
|
|
252
|
+
masks.append(mask_from_polygon)
|
|
253
|
+
|
|
254
|
+
xyxy = np.array(xyxy) if len(xyxy) > 0 else np.empty((0, 4))
|
|
255
|
+
|
|
256
|
+
# https://github.com/roboflow/eye/issues/144
|
|
257
|
+
xyxy -= 1
|
|
258
|
+
|
|
259
|
+
for k in set(class_names):
|
|
260
|
+
if k not in extended_classes:
|
|
261
|
+
extended_classes.append(k)
|
|
262
|
+
class_id = np.array(
|
|
263
|
+
[extended_classes.index(class_name) for class_name in class_names]
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
annotation = Detections(
|
|
267
|
+
xyxy=xyxy.astype(np.float32),
|
|
268
|
+
mask=np.array(masks).astype(bool) if with_masks else None,
|
|
269
|
+
class_id=class_id,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return annotation, extended_classes
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def parse_polygon_points(polygon: Element) -> np.ndarray:
|
|
276
|
+
coordinates = [int(coord.text) for coord in polygon.findall(".//*")]
|
|
277
|
+
return np.array(
|
|
278
|
+
[(coordinates[i], coordinates[i + 1]) for i in range(0, len(coordinates), 2)]
|
|
279
|
+
)
|