66from pathlib import Path
77from typing import Any , Dict , List , Optional , Tuple
88
9+ import numpy as np
10+ from shapely .geometry import Polygon
11+ from shapely .ops import polygonize
12+ from shapely .validation import make_valid
13+
914from kili .domain .ontology import JobMLTask , JobTool
1015from kili .services .export .exceptions import (
1116 NoCompatibleJobError ,
@@ -412,12 +417,15 @@ def _get_coco_image_annotations(
412417 print ("continue" )
413418 continue
414419 bounding_poly = annotation ["boundingPoly" ]
415- bbox , poly = _get_coco_geometry_from_kili_bpoly (
420+ area , bbox , poly = _get_coco_geometry_from_kili_bpoly (
416421 bounding_poly , coco_image ["width" ], coco_image ["height" ]
417422 )
418423 if len (poly ) < 6 : # twice the number of vertices
419424 print ("A polygon must contain more than 2 points. Skipping this polygon..." )
420425 continue
426+ if bbox [2 ] == 0 and bbox [3 ] == 0 :
427+ print ("An annotation with zero dimensions has been ignored." )
428+ continue
421429
422430 categories = annotation ["categories" ]
423431 coco_annotation = CocoAnnotation (
@@ -429,7 +437,7 @@ def _get_coco_image_annotations(
429437 # But a type of object can appear several times on the same image.
430438 # The limitation of the single connected part comes from Kili.
431439 segmentation = [poly ],
432- area = coco_image [ "height" ] * coco_image [ "width" ] ,
440+ area = area ,
433441 iscrowd = 0 ,
434442 )
435443
@@ -442,6 +450,23 @@ def _get_coco_image_annotations(
442450 return coco_annotations , annotation_j
443451
444452
453+ # Shoelace formula, implementation from https://stackoverflow.com/a/30408825.
454+ def _get_shoelace_area (x : List [float ], y : List [float ]):
455+ # Split self intersecting polygon into multiple polygons to compute area safely.
456+ polygon = Polygon (np .c_ [x , y ])
457+ polygons = polygonize (make_valid (polygon ))
458+
459+ area = 0
460+
461+ for poly in polygons :
462+ p_xx , p_yy = poly .exterior .coords .xy
463+ p_x = p_xx .tolist ()
464+ p_y = p_yy .tolist ()
465+ area += 0.5 * np .abs (np .dot (p_x , np .roll (p_y , 1 )) - np .dot (p_y , np .roll (p_x , 1 )))
466+
467+ return area
468+
469+
445470def _get_coco_geometry_from_kili_bpoly (
446471 bounding_poly : List [Dict ], asset_width : int , asset_height : int
447472):
@@ -452,9 +477,18 @@ def _get_coco_geometry_from_kili_bpoly(
452477 x_min , y_min = min (p_x ), min (p_y )
453478 x_max , y_max = max (p_x ), max (p_y )
454479 bbox_width , bbox_height = x_max - x_min , y_max - y_min
480+ area = _get_shoelace_area (p_x , p_y )
481+
482+ # Compute and remove negative area
483+ if len (bounding_poly ) > 1 :
484+ negative_normalized_vertices = bounding_poly [1 ]["normalizedVertices" ]
485+ np_x = [float (vertice ["x" ]) * asset_width for vertice in negative_normalized_vertices ]
486+ np_y = [float (vertice ["y" ]) * asset_height for vertice in negative_normalized_vertices ]
487+ area -= _get_shoelace_area (np_x , np_y )
488+
455489 bbox = [int (x_min ), int (y_min ), int (bbox_width ), int (bbox_height )]
456490 poly = [p for vertice in poly_vertices for p in vertice ]
457- return bbox , poly
491+ return area , bbox , poly
458492
459493
460494def _get_coco_categories (cat_kili_id_to_coco_id , merged ) -> List [CocoCategory ]:
0 commit comments