Skip to content

Commit d07230c

Browse files
authored
Merge pull request #31 from fastlabel/develop
Merge to main
2 parents b7685c0 + 4c04812 commit d07230c

File tree

7 files changed

+231
-17
lines changed

7 files changed

+231
-17
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ annotation.json
1111
*.jpg
1212
*.jpeg
1313
*.png
14-
tests/
14+
tests/
15+
output/

README.md

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -501,10 +501,10 @@ map = client.get_task_id_name_map(project="YOUR_PROJECT_SLUG")
501501

502502
```python
503503
annotation_id = client.create_annotation(
504-
project="YOUR_PROJECT_SLUG", type="bbox", value="cat", title="Cat", color="#FF0000")
504+
project="YOUR_PROJECT_SLUG", type="bbox", value="cat", title="Cat")
505505
```
506506

507-
- Create a new annotation with attributes.
507+
- Create a new annotation with color and attributes.
508508

509509
```python
510510
attributes = [
@@ -663,13 +663,39 @@ client.delete_annotation(annotation_id="YOUR_ANNOTATIPN_ID")
663663

664664
## Converter
665665

666+
Supporting bbox or polygon annotation type.
667+
666668
### COCO
667669

668-
- Get tasks and convert to [COCO format](https://cocodataset.org/#format-data) (supporting bbox or polygon annotation type).
670+
- Get tasks and export as a [COCO format](https://cocodataset.org/#format-data) file.
671+
672+
```python
673+
tasks = client.get_image_tasks(project="YOUR_PROJECT_SLUG")
674+
client.export_coco(tasks)
675+
```
676+
677+
- Export with specifying output directory.
678+
679+
```python
680+
client.export_coco(tasks=tasks, output_dir="YOUR_DIRECTROY")
681+
```
682+
683+
### YOLO
684+
685+
- Get tasks and export as YOLO format files.
686+
687+
```python
688+
tasks = client.get_image_tasks(project="YOUR_PROJECT_SLUG")
689+
client.export_yolo(tasks)
690+
```
691+
692+
### Pascal VOC
693+
694+
- Get tasks and export as Pascal VOC format files.
669695

670696
```python
671697
tasks = client.get_image_tasks(project="YOUR_PROJECT_SLUG")
672-
pprint(client.to_coco(tasks))
698+
client.export_pascalvoc(tasks)
673699
```
674700

675701
## API Docs

fastlabel/__init__.py

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import os
22
import glob
3+
import json
34
from logging import getLogger
45

6+
import xmltodict
7+
58
from .exceptions import FastLabelInvalidException
69
from .api import Api
710
from fastlabel import converters, utils
@@ -246,7 +249,7 @@ def get_task_id_name_map(
246249
self, project: str,
247250
offset: int = None,
248251
limit: int = 1000,
249-
) -> list:
252+
) -> dict:
250253
"""
251254
Returns a map of task ids and names.
252255
e.g.) {
@@ -447,12 +450,58 @@ def delete_task(self, task_id: str) -> None:
447450

448451
# Task Convert
449452

450-
def to_coco(self, tasks: list) -> dict:
453+
def export_coco(self, tasks: list, output_dir: str = os.path.join("output", "coco")) -> None:
454+
"""
455+
Convert tasks to COCO format and export as a file.
456+
457+
tasks is a list of tasks. (Required)
458+
output_dir is output directory(default: output/coco). (Optional)
459+
"""
460+
coco = converters.to_coco(tasks)
461+
os.makedirs(output_dir, exist_ok=True)
462+
file_path = os.path.join(output_dir, "annotations.json")
463+
with open(file_path, 'w') as f:
464+
json.dump(coco, f, indent=4, ensure_ascii=False)
465+
466+
def export_yolo(self, tasks: list, output_dir: str = os.path.join("output", "yolo")) -> None:
467+
"""
468+
Convert tasks to YOLO format and export as files.
469+
470+
tasks is a list of tasks. (Required)
471+
output_dir is output directory(default: output/yolo). (Optional)
451472
"""
452-
Convert tasks to COCO format.
473+
annos, categories = converters.to_yolo(tasks)
474+
for anno in annos:
475+
file_name = anno["filename"]
476+
basename = utils.get_basename(file_name)
477+
file_path = os.path.join(
478+
output_dir, "annotations", basename + ".txt")
479+
os.makedirs(os.path.dirname(file_path), exist_ok=True)
480+
with open(file_path, 'w', encoding="utf8") as f:
481+
for obj in anno["object"]:
482+
f.write(obj)
483+
f.write("\n")
484+
with open(os.path.join(output_dir, "classes.txt"), 'w', encoding="utf8") as f:
485+
for category in categories:
486+
f.write(category["name"])
487+
f.write("\n")
488+
489+
def export_pascalvoc(self, tasks: list, output_dir: str = os.path.join("output", "pascalvoc")) -> None:
453490
"""
491+
Convert tasks to Pascal VOC format as files.
454492
455-
return converters.to_coco(tasks)
493+
tasks is a list of tasks. (Required)
494+
output_dir is output directory(default: output/pascalvoc). (Optional)
495+
"""
496+
pascalvoc = converters.to_pascalvoc(tasks)
497+
for voc in pascalvoc:
498+
file_name = voc["annotation"]["filename"]
499+
basename = utils.get_basename(file_name)
500+
file_path = os.path.join(output_dir, basename + ".xml")
501+
os.makedirs(os.path.dirname(file_path), exist_ok=True)
502+
xml = xmltodict.unparse(voc, pretty=True, full_document=False)
503+
with open(file_path, 'w', encoding="utf8") as f:
504+
f.write(xml)
456505

457506
# Annotation
458507

@@ -507,7 +556,7 @@ def create_annotation(
507556
type: str,
508557
value: str,
509558
title: str,
510-
color: str,
559+
color: str = None,
511560
attributes: list = []
512561
) -> str:
513562
"""
@@ -517,7 +566,7 @@ def create_annotation(
517566
type can be 'bbox', 'polygon', 'keypoint', 'classification', 'line', 'segmentation'. (Required)
518567
value is an unique identifier of annotation in your project. (Required)
519568
title is a display name of value. (Required)
520-
color is hex color code like #ffffff. (Required)
569+
color is hex color code like #ffffff. (Optional)
521570
attributes is a list of attribute. (Optional)
522571
"""
523572
endpoint = "annotations"
@@ -526,8 +575,9 @@ def create_annotation(
526575
"type": type,
527576
"value": value,
528577
"title": title,
529-
"color": color
530578
}
579+
if color:
580+
payload["color"] = color
531581
if attributes:
532582
payload["attributes"] = attributes
533583
return self.api.post_request(endpoint, payload=payload)

fastlabel/converters.py

Lines changed: 130 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ class AnnotationType(Enum):
1414
segmentation = "segmentation"
1515

1616

17+
# COCO
18+
19+
1720
def to_coco(tasks: list) -> dict:
18-
"""
19-
Convert tasks to COCO format.
20-
"""
2121
# Get categories
2222
categories = __get_categories(tasks)
2323

@@ -147,3 +147,130 @@ def __calc_area(annotation_type: str, points: list) -> float:
147147
area = 0.5 * np.abs(np.dot(x, np.roll(y, 1)) -
148148
np.dot(y, np.roll(x, 1)))
149149
return area
150+
151+
152+
# YOLO
153+
154+
155+
def to_yolo(tasks: list) -> tuple:
156+
coco = to_coco(tasks)
157+
yolo = __coco2yolo(coco)
158+
return yolo
159+
160+
161+
def __coco2yolo(coco: dict) -> tuple:
162+
categories = coco["categories"]
163+
164+
annos = []
165+
for image in coco["images"]:
166+
dw = 1. / image["width"]
167+
dh = 1. / image["height"]
168+
169+
# Get objects
170+
objs = []
171+
for annotation in coco["annotations"]:
172+
if image["id"] != annotation["image_id"]:
173+
continue
174+
175+
category_index = "0"
176+
for index, category in enumerate(categories):
177+
if category["id"] == annotation["category_id"]:
178+
category_index = str(index)
179+
break
180+
181+
xmin = annotation["bbox"][0]
182+
ymin = annotation["bbox"][1]
183+
xmax = annotation["bbox"][0] + annotation["bbox"][2]
184+
ymax = annotation["bbox"][1] + annotation["bbox"][3]
185+
186+
x = (xmin + xmax) / 2
187+
y = (ymin + ymax) / 2
188+
w = xmax - xmin
189+
h = ymax - ymin
190+
191+
x = str(_truncate(x * dw, 7))
192+
y = str(_truncate(y * dh, 7))
193+
w = str(_truncate(w * dw, 7))
194+
h = str(_truncate(h * dh, 7))
195+
196+
obj = [category_index, x, y, w, h]
197+
objs.append(" ".join(obj))
198+
199+
# get annotation
200+
anno = {
201+
"filename": image["file_name"],
202+
"object": objs
203+
}
204+
annos.append(anno)
205+
206+
return annos, categories
207+
208+
209+
def _truncate(n, decimals=0) -> float:
210+
multiplier = 10 ** decimals
211+
return int(n * multiplier) / multiplier
212+
213+
214+
# Pascal VOC
215+
216+
217+
def to_pascalvoc(tasks: list) -> list:
218+
coco = to_coco(tasks)
219+
pascalvoc = __coco2pascalvoc(coco)
220+
return pascalvoc
221+
222+
223+
def __coco2pascalvoc(coco: dict) -> list:
224+
pascalvoc = []
225+
226+
for image in coco["images"]:
227+
228+
# Get objects
229+
objs = []
230+
for annotation in coco["annotations"]:
231+
if image["id"] != annotation["image_id"]:
232+
continue
233+
category = _get_category_by_id(
234+
coco["categories"], annotation["category_id"])
235+
236+
x = annotation["bbox"][0]
237+
y = annotation["bbox"][1]
238+
w = annotation["bbox"][2]
239+
h = annotation["bbox"][3]
240+
241+
obj = {
242+
"name": category["name"],
243+
"pose": "Unspecified",
244+
"truncated": 0,
245+
"difficult": 0,
246+
"bndbox": {
247+
"xmin": x,
248+
"ymin": y,
249+
"xmax": x + w,
250+
"ymax": y + h,
251+
},
252+
}
253+
objs.append(obj)
254+
255+
# get annotation
256+
voc = {
257+
"annotation": {
258+
"filename": image["file_name"],
259+
"size": {
260+
"width": image["width"],
261+
"height": image["height"],
262+
"depth": 3,
263+
},
264+
"segmented": 0,
265+
"object": objs
266+
}
267+
}
268+
pascalvoc.append(voc)
269+
270+
return pascalvoc
271+
272+
273+
def _get_category_by_id(categories: list, id_: str) -> str:
274+
category = [
275+
category for category in categories if category["id"] == id_][0]
276+
return category

fastlabel/utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
import base64
23

34

@@ -12,3 +13,11 @@ def is_image_supported_ext(file_path: str) -> bool:
1213

1314
def is_video_supported_ext(file_path: str) -> bool:
1415
return file_path.lower().endswith(('.mp4'))
16+
17+
18+
def get_basename(file_path: str) -> str:
19+
"""
20+
e.g.) file.jpg -> file
21+
path/to/file.jpg -> path/to/file
22+
"""
23+
return os.path.splitext(file_path)[0]

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
requests==2.25.1
22
numpy==1.20.2
3-
geojson==2.5.0
3+
geojson==2.5.0
4+
xmltodict==0.12.0

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.7.1",
11+
version="0.8.0",
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)