11import os
22import glob
3+ from enum import Enum
34from logging import getLogger
5+ from concurrent .futures import ThreadPoolExecutor
46
57import requests
68import base64
9+ import numpy as np
10+ import geojson
711
812logger = 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
318456class FastLabelException (Exception ):
319457 def __init__ (self , message , errcode ):
0 commit comments