1+ # adapted from https://blog.roboflow.com/how-to-convert-annotations-from-voc-xml-to-coco-json/
2+
3+ import os
4+ import argparse
5+ import json
6+ import xml .etree .ElementTree as ET
7+ from typing import Dict , List
8+ from tqdm import tqdm
9+ import re
10+
11+
12+ def get_label2id (labels_path : str ) -> Dict [str , int ]:
13+ """id is 1 start"""
14+ with open (labels_path , 'r' ) as f :
15+ labels_str = f .read ().split ()
16+ labels_ids = list (range (0 , len (labels_str )))
17+ return dict (zip (labels_str , labels_ids ))
18+
19+
20+ def get_annpaths (ann_dir_path : str = None ,
21+ ann_ids_path : str = None ,
22+ ext : str = '' ,
23+ annpaths_list_path : str = None ) -> List [str ]:
24+ # If use annotation paths list
25+ if annpaths_list_path is not None :
26+ with open (annpaths_list_path , 'r' ) as f :
27+ ann_paths = f .read ().split ()
28+ return ann_paths
29+
30+ # If use annotaion ids list
31+ ext_with_dot = '.' + ext if ext != '' else ''
32+ with open (ann_ids_path , 'r' ) as f :
33+ ann_ids = f .read ().split ()
34+ ann_paths = [os .path .join (ann_dir_path , aid + ext_with_dot ) for aid in ann_ids ]
35+ return ann_paths
36+
37+
38+ def get_image_info (annotation_root , extract_num_from_imgid = True ):
39+ path = annotation_root .findtext ('path' )
40+ if path is None :
41+ filename = annotation_root .findtext ('filename' )
42+ else :
43+ filename = os .path .basename (path )
44+ img_name = os .path .basename (filename )
45+ img_id = os .path .splitext (img_name )[0 ]
46+ if extract_num_from_imgid and isinstance (img_id , str ):
47+ img_id = int (re .findall (r'\d+' , img_id )[0 ])
48+
49+ size = annotation_root .find ('size' )
50+ width = int (size .findtext ('width' ))
51+ height = int (size .findtext ('height' ))
52+
53+ image_info = {
54+ 'file_name' : filename ,
55+ 'height' : height ,
56+ 'width' : width ,
57+ 'id' : img_id
58+ }
59+ return image_info
60+
61+
62+ def get_coco_annotation_from_obj (obj , label2id ):
63+ label = obj .findtext ('name' )
64+ assert label in label2id , f"Error: { label } is not in label2id !"
65+ category_id = label2id [label ]
66+ bndbox = obj .find ('bndbox' )
67+ xmin = int (bndbox .findtext ('xmin' )) - 1
68+ ymin = int (bndbox .findtext ('ymin' )) - 1
69+ xmax = int (bndbox .findtext ('xmax' ))
70+ ymax = int (bndbox .findtext ('ymax' ))
71+ assert xmax > xmin and ymax > ymin , f"Box size error !: (xmin, ymin, xmax, ymax): { xmin , ymin , xmax , ymax } "
72+ o_width = xmax - xmin
73+ o_height = ymax - ymin
74+ ann = {
75+ 'area' : o_width * o_height ,
76+ 'iscrowd' : 0 ,
77+ 'bbox' : [xmin , ymin , o_width , o_height ],
78+ 'category_id' : category_id ,
79+ 'ignore' : 0 ,
80+ 'segmentation' : [] # This script is not for segmentation
81+ }
82+ return ann
83+
84+
85+ def convert_xmls_to_cocojson (annotation_paths : List [str ],
86+ label2id : Dict [str , int ],
87+ output_jsonpath : str ,
88+ extract_num_from_imgid : bool = True ):
89+ output_json_dict = {
90+ "images" : [],
91+ "type" : "instances" ,
92+ "annotations" : [],
93+ "categories" : []
94+ }
95+ bnd_id = 1 # START_BOUNDING_BOX_ID, TODO input as args ?
96+ print ('Start converting !' )
97+ for a_path in tqdm (annotation_paths ):
98+ # Read annotation xml
99+ ann_tree = ET .parse (a_path )
100+ ann_root = ann_tree .getroot ()
101+
102+ img_info = get_image_info (annotation_root = ann_root ,
103+ extract_num_from_imgid = extract_num_from_imgid )
104+ img_id = img_info ['id' ]
105+ output_json_dict ['images' ].append (img_info )
106+
107+ for obj in ann_root .findall ('object' ):
108+ ann = get_coco_annotation_from_obj (obj = obj , label2id = label2id )
109+ ann .update ({'image_id' : img_id , 'id' : bnd_id })
110+ output_json_dict ['annotations' ].append (ann )
111+ bnd_id = bnd_id + 1
112+
113+ for label , label_id in label2id .items ():
114+ category_info = {'supercategory' : 'none' , 'id' : label_id , 'name' : label }
115+ output_json_dict ['categories' ].append (category_info )
116+
117+ with open (output_jsonpath , 'w' ) as f :
118+ output_json = json .dumps (output_json_dict )
119+ f .write (output_json )
120+
121+
122+ def main ():
123+ parser = argparse .ArgumentParser (
124+ description = 'This script support converting voc format xmls to coco format json' )
125+ parser .add_argument ('--ann_dir' , type = str , default = None ,
126+ help = 'path to annotation files directory. It is not need when use --ann_paths_list' )
127+ parser .add_argument ('--ann_ids' , type = str , default = None ,
128+ help = 'path to annotation files ids list. It is not need when use --ann_paths_list' )
129+ parser .add_argument ('--ann_paths_list' , type = str , default = None ,
130+ help = 'path of annotation paths list. It is not need when use --ann_dir and --ann_ids' )
131+ parser .add_argument ('--labels' , type = str , default = None ,
132+ help = 'path to label list.' )
133+ parser .add_argument ('--output' , type = str , default = 'output.json' , help = 'path to output json file' )
134+ parser .add_argument ('--ext' , type = str , default = '' , help = 'additional extension of annotation file' )
135+ args = parser .parse_args ()
136+ label2id = get_label2id (labels_path = args .labels )
137+ ann_paths = get_annpaths (
138+ ann_dir_path = args .ann_dir ,
139+ ann_ids_path = args .ann_ids ,
140+ ext = args .ext ,
141+ annpaths_list_path = args .ann_paths_list
142+ )
143+ convert_xmls_to_cocojson (
144+ annotation_paths = ann_paths ,
145+ label2id = label2id ,
146+ output_jsonpath = args .output ,
147+ extract_num_from_imgid = True
148+ )
149+
150+
151+ if __name__ == '__main__' :
152+ main ()
0 commit comments