Skip to content

Commit 134917a

Browse files
authored
Merge pull request #24 from fastlabel/develop
Merge to main
2 parents 8a9b1b6 + d5f61d9 commit 134917a

File tree

5 files changed

+168
-16
lines changed

5 files changed

+168
-16
lines changed

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ client.delete_task(task_id="YOUR_TASK_ID")
164164
"annotations": [
165165
{
166166
"attributes": [
167-
{ "key": "kind", "name": "猫の種類", "type": "text", "value": "三毛猫" }
167+
{ "key": "kind", "name": "Kind", "type": "text", "value": "Scottish field" }
168168
],
169169
"color": "#b36d18",
170170
"points": [
@@ -297,6 +297,17 @@ tasks = client.get_multi_image_tasks(project="YOUR_PROJECT_SLUG")
297297
}
298298
```
299299

300+
## Converter
301+
302+
### COCO
303+
304+
- Get tasks and convert to [COCO format](https://cocodataset.org/#format-data) (supporting bbox or polygon annotation type).
305+
306+
```python
307+
tasks = client.get_tasks(project="YOUR_PROJECT_SLUG")
308+
pprint(client.to_coco(tasks))
309+
```
310+
300311
## API Docs
301312

302313
Check [this](https://api.fastlabel.ai/docs/) for further information.

examples/create_task.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
name = "YOUR_DATA_NAME"
1010
file_path = "YOUR_DATA_FILE_PATH" # e.g.) ./cat.jpg
1111
annotations = [{
12+
"type": "bbox",
1213
"value": "cat",
1314
"attributes": [
1415
{

fastlabel/__init__.py

Lines changed: 151 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import os
22
import glob
3+
from enum import Enum
34
from logging import getLogger
5+
from concurrent.futures import ThreadPoolExecutor
46

57
import requests
68
import base64
9+
import numpy as np
10+
import geojson
711

812
logger = getLogger(__name__)
913

@@ -20,7 +24,7 @@ def __init__(self) -> None:
2024
self.access_token = "Bearer " + \
2125
os.environ.get("FASTLABEL_ACCESS_TOKEN")
2226

23-
def _getrequest(self, endpoint: str, params=None) -> dict:
27+
def __getrequest(self, endpoint: str, params=None) -> dict:
2428
"""Makes a get request to an endpoint.
2529
If an error occurs, assumes that endpoint returns JSON as:
2630
{ 'statusCode': XXX,
@@ -46,7 +50,7 @@ def _getrequest(self, endpoint: str, params=None) -> dict:
4650
else:
4751
raise FastLabelException(error, r.status_code)
4852

49-
def _deleterequest(self, endpoint: str, params=None) -> dict:
53+
def __deleterequest(self, endpoint: str, params=None) -> dict:
5054
"""Makes a delete request to an endpoint.
5155
If an error occurs, assumes that endpoint returns JSON as:
5256
{ 'statusCode': XXX,
@@ -71,7 +75,7 @@ def _deleterequest(self, endpoint: str, params=None) -> dict:
7175
else:
7276
raise FastLabelException(error, r.status_code)
7377

74-
def _postrequest(self, endpoint, payload=None):
78+
def __postrequest(self, endpoint, payload=None):
7579
"""Makes a post request to an endpoint.
7680
If an error occurs, assumes that endpoint returns JSON as:
7781
{ 'statusCode': XXX,
@@ -97,7 +101,7 @@ def _postrequest(self, endpoint, payload=None):
97101
else:
98102
raise FastLabelException(error, r.status_code)
99103

100-
def _putrequest(self, endpoint, payload=None):
104+
def __putrequest(self, endpoint, payload=None):
101105
"""Makes a put request to an endpoint.
102106
If an error occurs, assumes that endpoint returns JSON as:
103107
{ 'statusCode': XXX,
@@ -128,14 +132,14 @@ def find_task(self, task_id: str) -> dict:
128132
Find a signle task.
129133
"""
130134
endpoint = "tasks/" + task_id
131-
return self._getrequest(endpoint)
135+
return self.__getrequest(endpoint)
132136

133137
def find_multi_image_task(self, task_id: str) -> dict:
134138
"""
135139
Find a signle multi image task.
136140
"""
137141
endpoint = "tasks/multi/image/" + task_id
138-
return self._getrequest(endpoint)
142+
return self.__getrequest(endpoint)
139143

140144
def get_tasks(
141145
self,
@@ -165,7 +169,7 @@ def get_tasks(
165169
params["offset"] = offset
166170
if limit:
167171
params["limit"] = limit
168-
return self._getrequest(endpoint, params=params)
172+
return self.__getrequest(endpoint, params=params)
169173

170174
def get_multi_image_tasks(
171175
self,
@@ -198,7 +202,7 @@ def get_multi_image_tasks(
198202
params["offset"] = offset
199203
if limit:
200204
params["limit"] = limit
201-
return self._getrequest(endpoint, params=params)
205+
return self.__getrequest(endpoint, params=params)
202206

203207
def create_task(
204208
self,
@@ -233,8 +237,8 @@ def create_task(
233237
payload["annotations"] = annotations
234238
if tags:
235239
payload["tags"] = tags
236-
return self._postrequest(endpoint, payload=payload)
237-
240+
return self.__postrequest(endpoint, payload=payload)
241+
238242
def create_multi_image_task(
239243
self,
240244
project: str,
@@ -277,7 +281,7 @@ def create_multi_image_task(
277281
payload["annotations"] = annotations
278282
if tags:
279283
payload["tags"] = tags
280-
return self._postrequest(endpoint, payload=payload)
284+
return self.__postrequest(endpoint, payload=payload)
281285

282286
def update_task(
283287
self,
@@ -298,14 +302,54 @@ def update_task(
298302
payload["status"] = status
299303
if tags:
300304
payload["tags"] = tags
301-
return self._putrequest(endpoint, payload=payload)
305+
return self.__putrequest(endpoint, payload=payload)
302306

303307
def delete_task(self, task_id: str) -> None:
304308
"""
305309
Delete a single task.
306310
"""
307311
endpoint = "tasks/" + task_id
308-
self._deleterequest(endpoint)
312+
self.__deleterequest(endpoint)
313+
314+
def to_coco(self, tasks: list) -> dict:
315+
# Get categories
316+
categories = self.__get_categories(tasks)
317+
318+
# Get images and annotations
319+
images = []
320+
annotations = []
321+
annotation_id = 0
322+
image_id = 0
323+
for task in tasks:
324+
if task["height"] == 0 or task["width"] == 0:
325+
continue
326+
327+
image_id += 1
328+
image = {
329+
"file_name": task["name"],
330+
"height": task["height"],
331+
"width": task["width"],
332+
"id": image_id,
333+
}
334+
images.append(image)
335+
336+
data = [{"annotation": annotation, "categories": categories,
337+
"image": image} for annotation in task["annotations"]]
338+
with ThreadPoolExecutor(max_workers=8) as executor:
339+
results = executor.map(self.__to_annotation, data)
340+
341+
for result in results:
342+
annotation_id += 1
343+
if not result:
344+
continue
345+
result["id"] = annotation_id
346+
annotations.append(result)
347+
348+
return {
349+
"images": images,
350+
"categories": categories,
351+
"annotations": annotations,
352+
}
309353

310354
def __base64_encode(self, file_path: str) -> str:
311355
with open(file_path, "rb") as f:
@@ -314,6 +358,100 @@ def __base64_encode(self, file_path: str) -> str:
314358
def __is_supported_ext(self, file_path: str) -> bool:
315359
return file_path.lower().endswith(('.png', '.jpg', '.jpeg'))
316360

361+
def __get_categories(self, tasks: list) -> list:
362+
values = []
363+
for task in tasks:
364+
for annotation in task["annotations"]:
365+
if annotation["type"] != AnnotationType.bbox.value and annotation["type"] != AnnotationType.polygon.value:
366+
continue
367+
values.append(annotation["value"])
368+
values = list(set(values))
369+
370+
categories = []
371+
for index, value in enumerate(values):
372+
category = {
373+
"supercategory": value,
374+
"id": index + 1,
375+
"name": value
376+
}
377+
categories.append(category)
378+
return categories
379+
380+
def __to_annotation(self, data: dict) -> dict:
381+
annotation = data["annotation"]
382+
categories = data["categories"]
383+
image = data["image"]
384+
points = annotation["points"]
385+
annotation_type = annotation["type"]
386+
annotation_id = 0
387+
388+
if annotation_type != AnnotationType.bbox.value and annotation_type != AnnotationType.polygon.value:
389+
return None
390+
if not points or len(points) == 0:
391+
return None
392+
if annotation_type == AnnotationType.bbox.value and (int(points[0]) == int(points[2]) or int(points[1]) == int(points[3])):
393+
return None
394+
395+
category = self.__get_category_by_name(categories, annotation["value"])
396+
397+
return self.__get_annotation(
398+
annotation_id, points, category["id"], image, annotation_type)
399+
400+
def __get_category_by_name(self, categories: list, name: str) -> str:
401+
category = [
402+
category for category in categories if category["name"] == name][0]
403+
return category
404+
405+
def __get_annotation(self, id_: int, points: list, category_id: int, image: dict, annotation_type: str) -> dict:
406+
annotation = {}
407+
annotation["segmentation"] = [points]
408+
annotation["iscrowd"] = 0
409+
annotation["area"] = self.__calc_area(annotation_type, points)
410+
annotation["image_id"] = image["id"]
411+
annotation["bbox"] = self.__to_bbox(points)
412+
annotation["category_id"] = category_id
413+
annotation["id"] = id_
414+
return annotation
415+
416+
def __to_bbox(self, points: list) -> list:
417+
points_splitted = [points[idx:idx + 2]
418+
for idx in range(0, len(points), 2)]
419+
polygon_geo = geojson.Polygon(points_splitted)
420+
coords = np.array(list(geojson.utils.coords(polygon_geo)))
421+
left_top_x = coords[:, 0].min()
422+
left_top_y = coords[:, 1].min()
423+
right_bottom_x = coords[:, 0].max()
424+
right_bottom_y = coords[:, 1].max()
425+
426+
return [
427+
left_top_x, # x
428+
left_top_y, # y
429+
right_bottom_x - left_top_x, # width
430+
right_bottom_y - left_top_y, # height
431+
]
432+
433+
def __calc_area(self, annotation_type: str, points: list) -> float:
434+
area = 0
435+
if annotation_type == AnnotationType.bbox.value:
436+
width = points[0] - points[2]
437+
height = points[1] - points[3]
438+
area = width * height
439+
elif annotation_type == AnnotationType.polygon.value:
440+
x = points[0::2]
441+
y = points[1::2]
442+
area = 0.5 * np.abs(np.dot(x, np.roll(y, 1)) -
443+
np.dot(y, np.roll(x, 1)))
444+
return area
445+
446+
447+
class AnnotationType(Enum):
448+
bbox = "bbox"
449+
polygon = "polygon"
450+
keypoint = "keypoint"
451+
classification = "classification"
452+
line = "line"
453+
segmentation = "segmentation"
454+
317455

318456
class FastLabelException(Exception):
319457
def __init__(self, message, errcode):

requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
requests==2.25.1
1+
requests==2.25.1
2+
numpy==1.20.2
3+
geojson==2.5.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.5.0",
11+
version="0.6.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)