Skip to content

Commit 3880093

Browse files
authored
Merge pull request #97 from fastlabel/feature/export-coco-segmentation
support segmentation for coco export
2 parents 54e0ee4 + 54eafcc commit 3880093

File tree

2 files changed

+138
-61
lines changed

2 files changed

+138
-61
lines changed

fastlabel/converters.py

Lines changed: 137 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from concurrent.futures import ThreadPoolExecutor
2+
from datetime import datetime
3+
from decimal import Decimal
24
from typing import List
35

46
import copy
@@ -76,7 +78,7 @@ def __get_categories(tasks: list, annotations: list) -> list:
7678
values = []
7779
for task in tasks:
7880
for task_annotation in task["annotations"]:
79-
if task_annotation["type"] != AnnotationType.bbox.value and task_annotation["type"] != AnnotationType.polygon.value and task_annotation["type"] != AnnotationType.pose_estimation.value:
81+
if task_annotation["type"] not in [AnnotationType.bbox.value, AnnotationType.polygon.value, AnnotationType.segmentation.value, AnnotationType.pose_estimation.value]:
8082
continue
8183
values.append(task_annotation["value"])
8284
values = list(set(values))
@@ -131,7 +133,7 @@ def __to_annotation(data: dict) -> dict:
131133
annotation_type = annotation["type"]
132134
annotation_id = 0
133135

134-
if annotation_type != AnnotationType.bbox.value and annotation_type != AnnotationType.polygon.value and annotation_type != AnnotationType.pose_estimation.value:
136+
if annotation_type not in [AnnotationType.bbox.value, AnnotationType.polygon.value, AnnotationType.segmentation.value, AnnotationType.pose_estimation.value]:
135137
return None
136138
if annotation_type != AnnotationType.pose_estimation.value and (not points or len(points)) == 0:
137139
return None
@@ -144,7 +146,6 @@ def __to_annotation(data: dict) -> dict:
144146
annotation_id, points, keypoints, category["id"], image, annotation_type)
145147

146148

147-
148149
def __get_category_by_name(categories: list, name: str) -> str:
149150
category = [
150151
category for category in categories if category["name"] == name][0]
@@ -169,46 +170,104 @@ def __get_annotation(id_: int, points: list, keypoints: list, category_id: int,
169170
annotation["num_keypoints"] = len(keypoints) if keypoints else 0
170171
annotation["keypoints"] = __get_coco_annotation_keypoints(
171172
keypoints) if keypoints else []
172-
annotation["segmentation"] = [points] if points else []
173+
annotation["segmentation"] = __to_coco_segmentation(
174+
annotation_type, points)
173175
annotation["iscrowd"] = 0
174-
annotation["area"] = __calc_area(annotation_type, points) if points else 0
176+
annotation["area"] = __to_area(annotation_type, points)
175177
annotation["image_id"] = image["id"]
176-
annotation["bbox"] = __to_bbox(points) if points else []
178+
annotation["bbox"] = __to_bbox(annotation_type, points)
177179
annotation["category_id"] = category_id
178180
annotation["id"] = id_
179181
return annotation
180182

181183

182-
def __to_bbox(points: list) -> list:
183-
points_splitted = [points[idx:idx + 2]
184-
for idx in range(0, len(points), 2)]
184+
def __get_without_hollowed_points(points: list) -> list:
185+
return [region[0] for region in points]
186+
187+
188+
def __to_coco_segmentation(annotation_type: str, points: list) -> list:
189+
if not points:
190+
return []
191+
if annotation_type == AnnotationType.segmentation.value:
192+
# Remove hollowed points
193+
return __get_without_hollowed_points(points)
194+
return [points]
195+
196+
197+
def __to_bbox(annotation_type: str, points: list) -> list:
198+
if not points:
199+
return []
200+
base_points = []
201+
if annotation_type == AnnotationType.segmentation.value:
202+
base_points = sum(__get_without_hollowed_points(points), [])
203+
else:
204+
base_points = points
205+
points_splitted = [
206+
base_points[idx: idx + 2] for idx in range(0, len(base_points), 2)
207+
]
185208
polygon_geo = geojson.Polygon(points_splitted)
186209
coords = np.array(list(geojson.utils.coords(polygon_geo)))
187210
left_top_x = coords[:, 0].min()
188211
left_top_y = coords[:, 1].min()
189212
right_bottom_x = coords[:, 0].max()
190213
right_bottom_y = coords[:, 1].max()
214+
width = right_bottom_x - left_top_x
215+
height = right_bottom_y - left_top_y
216+
return [__serialize(point) for point in [left_top_x, left_top_y, width, height]]
191217

192-
return [
193-
left_top_x, # x
194-
left_top_y, # y
195-
right_bottom_x - left_top_x, # width
196-
right_bottom_y - left_top_y, # height
197-
]
218+
219+
def __to_area(annotation_type: str, points: list) -> float:
220+
if not points:
221+
return 0
222+
area = 0
223+
if annotation_type == AnnotationType.segmentation.value:
224+
for region in __get_without_hollowed_points(points):
225+
area += __calc_area(annotation_type, region)
226+
else:
227+
area = __calc_area(annotation_type, points)
228+
return __serialize(area)
198229

199230

200231
def __calc_area(annotation_type: str, points: list) -> float:
201-
area = 0
232+
if not points:
233+
return 0
202234
if annotation_type == AnnotationType.bbox.value:
203235
width = points[0] - points[2]
204236
height = points[1] - points[3]
205-
area = width * height
206-
elif annotation_type == AnnotationType.polygon.value:
237+
return width * height
238+
elif annotation_type in [AnnotationType.polygon.value, AnnotationType.segmentation.value]:
207239
x = points[0::2]
208240
y = points[1::2]
209-
area = 0.5 * np.abs(np.dot(x, np.roll(y, 1)) -
210-
np.dot(y, np.roll(x, 1)))
211-
return area
241+
return 0.5 * np.abs(
242+
np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1))
243+
)
244+
else:
245+
raise Exception(f"Unsupported annotation type: {annotation_type}")
246+
247+
248+
def __serialize(value: any) -> any:
249+
if isinstance(value, datetime):
250+
return value.isoformat()
251+
if isinstance(value, Decimal):
252+
float_value = float(value)
253+
if float_value.is_integer():
254+
return int(value)
255+
else:
256+
return float_value
257+
if isinstance(value, float):
258+
if value.is_integer():
259+
return int(value)
260+
if isinstance(value, np.integer):
261+
return int(value)
262+
if isinstance(value, np.floating):
263+
float_value = float(value)
264+
if float_value.is_integer():
265+
return int(value)
266+
else:
267+
return float_value
268+
if isinstance(value, np.ndarray):
269+
return value.tolist()
270+
return value
212271

213272

214273
# YOLO
@@ -373,6 +432,7 @@ def to_pascalvoc(tasks: list) -> list:
373432
pascalvoc.append(voc)
374433
return pascalvoc
375434

435+
376436
def __get_pascalvoc_obj(data: dict) -> dict:
377437
annotation = data["annotation"]
378438
points = annotation["points"]
@@ -403,6 +463,7 @@ def __get_pascalvoc_obj(data: dict) -> dict:
403463
},
404464
}
405465

466+
406467
def __get_pascalvoc_tag_value(annotation: dict, target_tag_name: str) -> int:
407468
attributes = annotation["attributes"]
408469
if not attributes:
@@ -416,44 +477,46 @@ def __get_pascalvoc_tag_value(annotation: dict, target_tag_name: str) -> int:
416477

417478

418479
def to_labelme(tasks: list) -> list:
419-
labelmes =[]
480+
labelmes = []
420481
for task in tasks:
421482
shapes = []
422483
for annotation in task["annotations"]:
423-
shape_type = __to_labelme_shape_type(annotation["type"])
424-
if not shape_type:
425-
continue
426-
points = annotation["points"]
427-
if len(points) == 0:
428-
continue
484+
shape_type = __to_labelme_shape_type(annotation["type"])
485+
if not shape_type:
486+
continue
487+
points = annotation["points"]
488+
if len(points) == 0:
489+
continue
429490

430-
shape_points = []
431-
if annotation["type"] == "segmentation":
432-
for i in range(int(len(points[0][0]) / 2)):
433-
shape_points.append([points[0][0][i * 2], points[0][0][(i * 2) + 1]])
434-
else:
435-
for i in range(int(len(points) / 2)):
436-
shape_points.append([points[i * 2], points[(i * 2) + 1]])
437-
438-
shape = {
439-
"label": annotation["value"],
440-
"points": shape_points,
441-
"group_id": None,
442-
"shape_type": shape_type,
443-
"flags": {}
444-
}
445-
shapes.append(shape)
491+
shape_points = []
492+
if annotation["type"] == "segmentation":
493+
for i in range(int(len(points[0][0]) / 2)):
494+
shape_points.append(
495+
[points[0][0][i * 2], points[0][0][(i * 2) + 1]])
496+
else:
497+
for i in range(int(len(points) / 2)):
498+
shape_points.append([points[i * 2], points[(i * 2) + 1]])
499+
500+
shape = {
501+
"label": annotation["value"],
502+
"points": shape_points,
503+
"group_id": None,
504+
"shape_type": shape_type,
505+
"flags": {}
506+
}
507+
shapes.append(shape)
446508
labelmes.append({
447-
"version": "4.5.9",
448-
"flags": {},
449-
"shapes": shapes,
450-
"imagePath": task["name"],
451-
"imageData": None,
452-
"imageHeight": task["height"],
453-
"imageWidth": task["width"],
509+
"version": "4.5.9",
510+
"flags": {},
511+
"shapes": shapes,
512+
"imagePath": task["name"],
513+
"imageData": None,
514+
"imageHeight": task["height"],
515+
"imageWidth": task["width"],
454516
})
455517
return labelmes
456518

519+
457520
def __to_labelme_shape_type(annotation_type: str) -> str:
458521
if annotation_type == "polygon" or annotation_type == "segmentation":
459522
return "polygon"
@@ -515,10 +578,12 @@ def to_pixel_coordinates(tasks: list) -> list:
515578
new_regions.append(new_region)
516579
annotation["points"] = new_regions
517580
elif annotation["type"] == AnnotationType.polygon.value:
518-
new_points = __remove_duplicated_coordinates(annotation["points"])
581+
new_points = __remove_duplicated_coordinates(
582+
annotation["points"])
519583
annotation["points"] = new_points
520584
return tasks
521585

586+
522587
def __remove_duplicated_coordinates(points: List[int]) -> List[int]:
523588
"""
524589
Remove duplicated coordinates.
@@ -554,6 +619,7 @@ def __remove_duplicated_coordinates(points: List[int]) -> List[int]:
554619
new_points.append(points[i*2 + 1])
555620
return new_points
556621

622+
557623
def __get_pixel_coordinates(points: List[int or float]) -> List[int]:
558624
"""
559625
Remove diagonal coordinates and return pixel outline coordinates.
@@ -632,6 +698,7 @@ def execute_coco_to_fastlabel(coco: dict) -> dict:
632698
results[coco_images[coco_image_key]] = annotations
633699
return results
634700

701+
635702
def execute_labelme_to_fastlabel(labelme: dict, file_path: str = None) -> tuple:
636703
file_name = ""
637704
if file_path:
@@ -658,6 +725,7 @@ def execute_labelme_to_fastlabel(labelme: dict, file_path: str = None) -> tuple:
658725

659726
return (file_name, annotations)
660727

728+
661729
def execute_pascalvoc_to_fastlabel(pascalvoc: dict, file_path: str = None) -> tuple:
662730
target_pascalvoc = pascalvoc["annotation"]
663731
file_name = "" # file_path if file_path else target_pascalvoc["filename"]
@@ -713,16 +781,24 @@ def execute_yolo_to_fastlabel(
713781

714782
classs_name = classes[str(yolo_class_id)]
715783

716-
yolo_center_x_point = float(image_width) * float(yolo_center_x_ratio)
717-
yolo_center_y_point = float(image_height) * float(yolo_center_y_ratio)
718-
yolo_anno_width_size = float(image_width) * float(yolo_anno_width_ratio)
719-
yolo_anno_height_size = float(image_height) * float(yolo_anno_height_ratio)
784+
yolo_center_x_point = float(
785+
image_width) * float(yolo_center_x_ratio)
786+
yolo_center_y_point = float(
787+
image_height) * float(yolo_center_y_ratio)
788+
yolo_anno_width_size = float(
789+
image_width) * float(yolo_anno_width_ratio)
790+
yolo_anno_height_size = float(
791+
image_height) * float(yolo_anno_height_ratio)
720792

721793
points = []
722-
points.append(yolo_center_x_point - (yolo_anno_width_size / 2)) # x1
723-
points.append(yolo_center_y_point - (yolo_anno_height_size / 2)) # y1
724-
points.append(yolo_center_x_point + (yolo_anno_width_size / 2)) # x2
725-
points.append(yolo_center_y_point + (yolo_anno_height_size / 2)) # y2
794+
points.append(yolo_center_x_point -
795+
(yolo_anno_width_size / 2)) # x1
796+
points.append(yolo_center_y_point -
797+
(yolo_anno_height_size / 2)) # y1
798+
points.append(yolo_center_x_point +
799+
(yolo_anno_width_size / 2)) # x2
800+
points.append(yolo_center_y_point +
801+
(yolo_anno_height_size / 2)) # y2
726802
annotations.append(
727803
{
728804
"value": classs_name,
@@ -742,6 +818,7 @@ def execute_yolo_to_fastlabel(
742818

743819
return results
744820

821+
745822
def __get_annotation_type_by_labelme(shape_type: str) -> str:
746823
if shape_type == "rectangle":
747824
return "bbox"

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
setuptools.setup(
1010
name="fastlabel",
11-
version="0.11.12",
11+
version="0.11.13",
1212
author="eisuke-ueta",
1313
author_email="eisuke.ueta@fastlabel.ai",
1414
description="The official Python SDK for FastLabel API, the Data Platform for AI",

0 commit comments

Comments
 (0)