@ -30,13 +30,17 @@ def parse_args():
help = ' input file with CVAT annotation in *.xml format '
help = ' input file with CVAT annotation in *.xml format '
)
)
parser . add_argument (
parser . add_argument (
' --output ' , required= Tru e,
' --output ' , default= Non e,
help = ' output annotation file '
help = ' output annotation file . If not defined, the output file name will be created from input file name '
)
)
parser . add_argument (
parser . add_argument (
' --image-dir ' , required = True ,
' --image-dir ' , required = True ,
help = ' directory with images from annotation '
help = ' directory with images from annotation '
)
)
parser . add_argument (
' --labels ' , default = None ,
help = ' path to file with labels '
)
parser . add_argument (
parser . add_argument (
' --draw ' , default = None ,
' --draw ' , default = None ,
help = ' directory to save images with its segments. By default is disabled '
help = ' directory to save images with its segments. By default is disabled '
@ -89,6 +93,7 @@ def mask_to_polygon(mask, tolerance=1.0, area_threshold=1):
polygons . append ( reshaped_contour )
polygons . append ( reshaped_contour )
return polygons
return polygons
def draw_polygons ( polygons , img_name , input_dir , output_dir , draw_labels ) :
def draw_polygons ( polygons , img_name , input_dir , output_dir , draw_labels ) :
""" Draw on image contours of its objects and save
""" Draw on image contours of its objects and save
Args :
Args :
@ -123,6 +128,7 @@ def draw_polygons(polygons, img_name, input_dir, output_dir, draw_labels):
cv2 . putText ( img , label , ( x , y ) , cv2 . FONT_HERSHEY_COMPLEX_SMALL , 1 , red , 1 )
cv2 . putText ( img , label , ( x , y ) , cv2 . FONT_HERSHEY_COMPLEX_SMALL , 1 , red , 1 )
cv2 . imwrite ( output_file , img )
cv2 . imwrite ( output_file , img )
def fix_segments_intersections ( polygons , height , width , img_name , use_background_label ,
def fix_segments_intersections ( polygons , height , width , img_name , use_background_label ,
threshold = 0.0 , ratio_tolerance = 0.001 , area_threshold = 1 ) :
threshold = 0.0 , ratio_tolerance = 0.001 , area_threshold = 1 ) :
""" Find all intersected regions and crop contour for back object by objects which
""" Find all intersected regions and crop contour for back object by objects which
@ -206,6 +212,7 @@ def fix_segments_intersections(polygons, height, width, img_name, use_background
return output_polygons
return output_polygons
def polygon_area_and_bbox ( polygon , height , width ) :
def polygon_area_and_bbox ( polygon , height , width ) :
""" Calculate area of object ' s polygon and bounding box around it
""" Calculate area of object ' s polygon and bounding box around it
Args :
Args :
@ -222,6 +229,7 @@ def polygon_area_and_bbox(polygon, height, width):
max ( bbox [ : , 1 ] + bbox [ : , 3 ] ) - min ( bbox [ : , 1 ] ) ]
max ( bbox [ : , 1 ] + bbox [ : , 3 ] ) - min ( bbox [ : , 1 ] ) ]
return area , bbox
return area , bbox
def insert_license_data ( result_annotation ) :
def insert_license_data ( result_annotation ) :
""" Fill license fields in annotation by blank data
""" Fill license fields in annotation by blank data
Args :
Args :
@ -233,6 +241,7 @@ def insert_license_data(result_annotation):
' url ' : ' '
' url ' : ' '
} )
} )
def insert_info_data ( xml_root , result_annotation ) :
def insert_info_data ( xml_root , result_annotation ) :
""" Fill available information of annotation
""" Fill available information of annotation
Args :
Args :
@ -266,56 +275,84 @@ def insert_info_data(xml_root, result_annotation):
}
}
log . info ( ' Found the next information data: {} ' . format ( result_annotation [ ' info ' ] ) )
log . info ( ' Found the next information data: {} ' . format ( result_annotation [ ' info ' ] ) )
def insert_categories_data ( xml_root , use_background_label , result_annotation , xml_dir ) :
def insert_categories_data ( xml_root , use_background_label , result_annotation , labels_file = None ) :
""" Get labels from input annotation and fill categories field in output annotation
""" Get labels from input annotation and fill categories field in output annotation
Args :
Args :
xml_root : root for xml parser
xml_root : root for xml parser
use_background_label : key to enable using label background
use_background_label : key to enable using label background
result_annotation : output annotation in COCO representation
result_annotation : output annotation in COCO representation
xml_dir : directory with input annotation
labels_file : path to file with labels names .
If not defined , parse annotation to get labels names
"""
"""
log . info ( ' Reading labels... ' )
def get_categories ( names , bg_found , use_background_label , sort = False ) :
bg_used = False
category_map = { }
categories = [ ]
# Sort labels by its names to make the same order of ids for different annotations
if sort :
names . sort ( )
# Always use id = 0 for background
if bg_found and use_background_label :
category_map [ ' background ' ] = 0
bg_used = True
cat_id = 1
# Define id for all labels beginning from 1
for name in names :
if name == ' background ' :
continue
category_map [ name ] = cat_id
categories . append ( { ' id ' : cat_id , ' name ' : name , ' supercategory ' : ' ' } )
cat_id + = 1
return category_map , categories , bg_used
categories = [ ]
categories = [ ]
category_map = { }
category_map = { }
label_names = [ ]
bg_found = False
bg_found = False
id = 0
bg_used = False
for label in xml_root . iter ( ' label ' ) :
for name in label . findall ( " ./name " ) :
if labels_file is None :
if not use_background_label and name . text == ' background ' :
log . info ( ' Reading labels from annotation... ' )
bg_found = True
for label in xml_root . iter ( ' label ' ) :
continue
for name in label . findall ( " ./name " ) :
category_map [ name . text ] = id
if name . text == ' background ' :
categories . append ( { ' id ' : id , ' name ' : name . text , ' supercategory ' : ' ' } )
bg_found = True
id + = 1
continue
label_names . append ( name . text )
if len ( label_names ) == 0 :
log . info ( ' Labels in annotation were not found. Please use \' --labels \' argument to define file with labels. ' )
else :
category_map , categories , bg_used = get_categories ( label_names , bg_found , use_background_label , sort = True )
else :
log . info ( ' Parsing labels from file < {} >... ' . format ( labels_file ) )
with open ( labels_file , ' r ' ) as file :
string = ' '
while string != ' ' and string != ' \n ' :
string = file . readline ( )
labels = string . split ( ' ' )
for label in labels :
if label == ' \n ' or label == ' ' :
continue
label = label . replace ( ' \n ' , ' ' )
if label == ' background ' :
bg_found = True
continue
label_names . append ( label )
category_map , categories , bg_used = get_categories ( label_names , bg_found , use_background_label )
if len ( categories ) == 0 :
if len ( categories ) == 0 :
log . info ( ' Labels in annotation were not found. Trying to find file <labels.txt> in < {} > ' . format ( xml_dir ) )
raise ValueError ( ' Categories list is empty. Something wrong. ' )
if osp . isfile ( osp . join ( xml_dir , ' labels.txt ' ) ) :
labels_file = osp . join ( xml_dir , ' labels.txt ' )
log . info ( ' File <labels.txt> was found in < {} >. Reading... ' . format ( xml_dir ) )
with open ( labels_file , ' r ' ) as file :
string = ' '
id = 0
while string != ' ' and string != ' \n ' :
string = file . readline ( )
labels = string . split ( ' ' )
for l in labels :
if l == ' \n ' :
continue
if not use_background_label and l == ' background ' :
bg_found = True
continue
category_map [ l ] = id
categories . append ( { ' id ' : id , ' name ' : l , ' supercategory ' : ' ' } )
id + = 1
result_annotation [ ' categories ' ] = categories
result_annotation [ ' categories ' ] = categories
log . info ( ' Found the next labels: {} ' . format ( category_map ) )
log . info ( ' Found the next labels: {} ' . format ( category_map ) )
if bg_found :
if bg_found and not bg_used :
log . warning ( ' Label <background> was found but not used. '
log . warning ( ' Label <background> was found but not used. '
' To enable it should use command line argument [--use_background_label] ' )
' To enable it should use command line argument [--use_background_label] ' )
return category_map
return category_map
def insert_image_data ( image , path_to_images , result_annotation ) :
def insert_image_data ( image , result_annotation ) :
""" Get data from input annotation for image and fill fields for this image in output annotation
""" Get data from input annotation for image and fill fields for this image in output annotation
Args :
Args :
image : dictionary with data for image from original annotation
image : dictionary with data for image from original annotation
@ -329,11 +366,11 @@ def insert_image_data(image, path_to_images, result_annotation):
new_img [ ' license ' ] = 0
new_img [ ' license ' ] = 0
new_img [ ' id ' ] = image [ ' id ' ]
new_img [ ' id ' ] = image [ ' id ' ]
new_img [ ' file_name ' ] = osp . basename ( image [ ' name ' ] )
new_img [ ' file_name ' ] = osp . basename ( image [ ' name ' ] )
pic = cv2 . imread ( osp . join ( path_to_images , new_img [ ' file_name ' ] ) )
new_img [ ' height ' ] = int ( image [ ' height ' ] )
new_img [ ' height ' ] = pic . shape [ 0 ]
new_img [ ' width ' ] = int ( image [ ' width ' ] )
new_img [ ' width ' ] = pic . shape [ 1 ]
result_annotation [ ' images ' ] . append ( new_img )
result_annotation [ ' images ' ] . append ( new_img )
def insert_annotation_data ( image , category_map , segm_id , object , img_dims , result_annotation ) :
def insert_annotation_data ( image , category_map , segm_id , object , img_dims , result_annotation ) :
""" Get data from input annotation for object and fill fields for this object in output annotation
""" Get data from input annotation for object and fill fields for this object in output annotation
Args :
Args :
@ -359,10 +396,14 @@ def insert_annotation_data(image, category_map, segm_id, object, img_dims, resul
def main ( ) :
def main ( ) :
args = parse_args ( )
args = parse_args ( )
xml_file_name = args . cvat_xml
xml_file_name = args . cvat_xml
output_file_name = args . output
if args . output is not None :
output_file_name = args . output
else :
output_file_name = args . cvat_xml . split ( ' .xml ' ) [ 0 ] + ' .json '
log . info ( ' Output file name set to: {} ' . format ( output_file_name ) )
root = etree . parse ( xml_file_name ) . getroot ( )
root = etree . parse ( xml_file_name ) . getroot ( )
if args . draw != None :
if args . draw is not None :
log . info ( ' Draw key was enabled. Images will be saved in directory < {} > ' . format ( args . draw ) )
log . info ( ' Draw key was enabled. Images will be saved in directory < {} > ' . format ( args . draw ) )
result_annotation = {
result_annotation = {
@ -375,7 +416,7 @@ def main():
insert_license_data ( result_annotation )
insert_license_data ( result_annotation )
insert_info_data ( root , result_annotation )
insert_info_data ( root , result_annotation )
category_map = insert_categories_data ( root , args . use_background_label , result_annotation , osp. dirname ( xml_file_name ) )
category_map = insert_categories_data ( root , args . use_background_label , result_annotation , labels_file= args . labels )
if len ( category_map ) == 0 :
if len ( category_map ) == 0 :
sys . exit ( ' Labels were not found. Be sure that annotation < {} > includes field <labels> or '
sys . exit ( ' Labels were not found. Be sure that annotation < {} > includes field <labels> or '
@ -388,6 +429,9 @@ def main():
image = { }
image = { }
for key , value in img . items ( ) :
for key , value in img . items ( ) :
image [ key ] = value
image [ key ] = value
img_name = osp . join ( args . image_dir , osp . basename ( image [ ' name ' ] ) )
if not osp . isfile ( img_name ) :
log . warning ( ' Image < {} > is not available ' . format ( img_name ) )
image [ ' polygon ' ] = [ ]
image [ ' polygon ' ] = [ ]
z_order_on_counter = 0
z_order_on_counter = 0
polygon_counter = 0
polygon_counter = 0
@ -407,7 +451,7 @@ def main():
# Create new image
# Create new image
image [ ' id ' ] = int ( image [ ' id ' ] )
image [ ' id ' ] = int ( image [ ' id ' ] )
insert_image_data ( image , args. image_dir , result_annotation)
insert_image_data ( image , result_annotation)
height = result_annotation [ ' images ' ] [ - 1 ] [ ' height ' ]
height = result_annotation [ ' images ' ] [ - 1 ] [ ' height ' ]
width = result_annotation [ ' images ' ] [ - 1 ] [ ' width ' ]
width = result_annotation [ ' images ' ] [ - 1 ] [ ' width ' ]
image [ ' polygon ' ] = fix_segments_intersections ( image [ ' polygon ' ] , height , width ,
image [ ' polygon ' ] = fix_segments_intersections ( image [ ' polygon ' ] , height , width ,
@ -420,7 +464,7 @@ def main():
segm_id + = 1
segm_id + = 1
# Draw contours of objects on image
# Draw contours of objects on image
if args . draw != None :
if args . draw is not None :
draw_polygons ( image [ ' polygon ' ] , image [ ' name ' ] , args . image_dir , args . draw , args . draw_labels )
draw_polygons ( image [ ' polygon ' ] , image [ ' name ' ] , args . image_dir , args . draw , args . draw_labels )
log . info ( ' Processed images: {} ' . format ( len ( result_annotation [ ' images ' ] ) ) )
log . info ( ' Processed images: {} ' . format ( len ( result_annotation [ ' images ' ] ) ) )
@ -438,12 +482,12 @@ def main():
# Try to load created annotation via cocoapi
# Try to load created annotation via cocoapi
try :
try :
log . info ( ' Trying to load annotation < {} > via cocoapi... ' . format ( output_file_name ) )
log . info ( ' Trying to load annotation < {} > via cocoapi... ' . format ( output_file_name ) )
anno = coco_loader. COCO ( output_file_name )
coco_loader. COCO ( output_file_name )
except :
except :
raise
raise
else :
else :
log . info ( ' Annotation in COCO representation <{} > created from < {} > successfully! '
log . info ( ' Conversion <{} > --> < {} > has finished successfully! ' . format ( xml_file_name , output_file_name ) )
. format ( output_file_name , xml_file_name ) )
if __name__ == " __main__ " :
if __name__ == " __main__ " :
main ( )
main ( )