megadetector 5.0.7__py3-none-any.whl → 5.0.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of megadetector might be problematic. Click here for more details.
- api/batch_processing/data_preparation/manage_local_batch.py +28 -14
- api/batch_processing/postprocessing/combine_api_outputs.py +2 -2
- api/batch_processing/postprocessing/compare_batch_results.py +1 -1
- api/batch_processing/postprocessing/convert_output_format.py +24 -6
- api/batch_processing/postprocessing/load_api_results.py +1 -3
- api/batch_processing/postprocessing/md_to_labelme.py +118 -51
- api/batch_processing/postprocessing/merge_detections.py +30 -5
- api/batch_processing/postprocessing/postprocess_batch_results.py +24 -12
- api/batch_processing/postprocessing/remap_detection_categories.py +163 -0
- api/batch_processing/postprocessing/render_detection_confusion_matrix.py +15 -12
- api/batch_processing/postprocessing/repeat_detection_elimination/repeat_detections_core.py +2 -2
- data_management/cct_json_utils.py +7 -2
- data_management/coco_to_labelme.py +263 -0
- data_management/coco_to_yolo.py +7 -4
- data_management/databases/integrity_check_json_db.py +68 -59
- data_management/databases/subset_json_db.py +1 -1
- data_management/get_image_sizes.py +44 -26
- data_management/importers/animl_results_to_md_results.py +1 -3
- data_management/importers/noaa_seals_2019.py +1 -1
- data_management/labelme_to_coco.py +252 -143
- data_management/labelme_to_yolo.py +95 -52
- data_management/lila/create_lila_blank_set.py +106 -23
- data_management/lila/download_lila_subset.py +133 -65
- data_management/lila/generate_lila_per_image_labels.py +1 -1
- data_management/lila/lila_common.py +8 -38
- data_management/read_exif.py +65 -16
- data_management/remap_coco_categories.py +84 -0
- data_management/resize_coco_dataset.py +3 -22
- data_management/wi_download_csv_to_coco.py +239 -0
- data_management/yolo_to_coco.py +283 -83
- detection/run_detector_batch.py +12 -3
- detection/run_inference_with_yolov5_val.py +10 -3
- detection/run_tiled_inference.py +2 -2
- detection/tf_detector.py +2 -1
- detection/video_utils.py +1 -1
- md_utils/ct_utils.py +22 -3
- md_utils/md_tests.py +11 -2
- md_utils/path_utils.py +206 -32
- md_utils/url_utils.py +66 -1
- md_utils/write_html_image_list.py +12 -3
- md_visualization/visualization_utils.py +363 -72
- md_visualization/visualize_db.py +33 -10
- {megadetector-5.0.7.dist-info → megadetector-5.0.8.dist-info}/METADATA +10 -12
- {megadetector-5.0.7.dist-info → megadetector-5.0.8.dist-info}/RECORD +47 -44
- {megadetector-5.0.7.dist-info → megadetector-5.0.8.dist-info}/WHEEL +1 -1
- md_visualization/visualize_megadb.py +0 -183
- {megadetector-5.0.7.dist-info → megadetector-5.0.8.dist-info}/LICENSE +0 -0
- {megadetector-5.0.7.dist-info → megadetector-5.0.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
########
|
|
2
|
+
#
|
|
3
|
+
# wi_download_csv_to_coco.py
|
|
4
|
+
#
|
|
5
|
+
# Convert a .csv file from a Wildlife Insights project export to a COCO camera traps .json file.
|
|
6
|
+
#
|
|
7
|
+
# Currently assumes that common names are unique identifiers, which is convenient but unreliable.
|
|
8
|
+
#
|
|
9
|
+
########
|
|
10
|
+
|
|
11
|
+
#%% Imports and constants
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import json
|
|
15
|
+
import pandas as pd
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from tqdm import tqdm
|
|
19
|
+
from collections import defaultdict
|
|
20
|
+
|
|
21
|
+
from md_visualization import visualization_utils as vis_utils
|
|
22
|
+
|
|
23
|
+
wi_extra_annotation_columns = \
|
|
24
|
+
('is_blank','identified_by','wi_taxon_id','class','order','family','genus','species','uncertainty',
|
|
25
|
+
'number_of_objects','age','sex','animal_recognizable','individual_id','individual_animal_notes',
|
|
26
|
+
'behavior','highlighted','markings')
|
|
27
|
+
|
|
28
|
+
wi_extra_image_columns = ('project_id','deployment_id')
|
|
29
|
+
|
|
30
|
+
def make_location_id(project_id,deployment_id):
|
|
31
|
+
return 'project_' + str(project_id) + '_deployment_' + deployment_id
|
|
32
|
+
|
|
33
|
+
def isnan(v):
|
|
34
|
+
try:
|
|
35
|
+
return np.isnan(v)
|
|
36
|
+
except Exception:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
default_category_remappings = {
|
|
40
|
+
'Homo Species':'Human',
|
|
41
|
+
'Human-Camera Trapper':'Human',
|
|
42
|
+
'No CV Result':'Unknown'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
#%%
|
|
47
|
+
|
|
48
|
+
def wi_download_csv_to_coco(csv_file_in,
|
|
49
|
+
coco_file_out=None,
|
|
50
|
+
image_folder=None,
|
|
51
|
+
validate_images=False,
|
|
52
|
+
gs_prefix=None,
|
|
53
|
+
verbose=True,
|
|
54
|
+
category_remappings=default_category_remappings):
|
|
55
|
+
"""
|
|
56
|
+
Convert a .csv file from a Wildlife Insights project export to a COCO
|
|
57
|
+
camera traps .json file.
|
|
58
|
+
|
|
59
|
+
If [coco_file_out] is None, uses [csv_file_in].json
|
|
60
|
+
|
|
61
|
+
gs_prefix is a string to remove from GS URLs to convert to path names... for example, if
|
|
62
|
+
your gs:// URLs look like:
|
|
63
|
+
|
|
64
|
+
gs://11234134_xyz/deployment/55554/dfadfasdfs.jpg
|
|
65
|
+
|
|
66
|
+
...and you specify gs_prefix='11234134_xyz/deployment/', the filenames in
|
|
67
|
+
the .json file will look like:
|
|
68
|
+
|
|
69
|
+
55554/dfadfasdfs.jpg
|
|
70
|
+
|
|
71
|
+
exclude_re discards matching images; typically use to omit thumbnail images.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
#%% Create COCO dictionaries
|
|
75
|
+
|
|
76
|
+
category_name_to_id = {}
|
|
77
|
+
category_name_to_id['empty'] = 0
|
|
78
|
+
|
|
79
|
+
df = pd.read_csv(csv_file_in)
|
|
80
|
+
|
|
81
|
+
print('Read {} rows from {}'.format(len(df),csv_file_in))
|
|
82
|
+
|
|
83
|
+
image_id_to_image = {}
|
|
84
|
+
image_id_to_annotations = defaultdict(list)
|
|
85
|
+
|
|
86
|
+
# i_row = 0; row = df.iloc[i_row]
|
|
87
|
+
for i_row,row in df.iterrows():
|
|
88
|
+
|
|
89
|
+
image_id = row['image_id']
|
|
90
|
+
|
|
91
|
+
if image_id not in image_id_to_image:
|
|
92
|
+
|
|
93
|
+
im = {}
|
|
94
|
+
image_id_to_image[image_id] = im
|
|
95
|
+
|
|
96
|
+
im['id'] = image_id
|
|
97
|
+
|
|
98
|
+
gs_url = row['location']
|
|
99
|
+
assert gs_url.startswith('gs://')
|
|
100
|
+
|
|
101
|
+
file_name = gs_url.replace('gs://','')
|
|
102
|
+
if gs_prefix is not None:
|
|
103
|
+
file_name = file_name.replace(gs_prefix,'')
|
|
104
|
+
|
|
105
|
+
location_id = make_location_id(row['project_id'],row['deployment_id'])
|
|
106
|
+
im['file_name'] = file_name
|
|
107
|
+
im['location'] = location_id
|
|
108
|
+
im['datetime'] = row['timestamp']
|
|
109
|
+
|
|
110
|
+
im['wi_image_info'] = {}
|
|
111
|
+
for s in wi_extra_image_columns:
|
|
112
|
+
im['wi_image_info'][s] = str(row[s])
|
|
113
|
+
|
|
114
|
+
else:
|
|
115
|
+
|
|
116
|
+
im = image_id_to_image[image_id]
|
|
117
|
+
assert im['datetime'] == row['timestamp']
|
|
118
|
+
location_id = make_location_id(row['project_id'],row['deployment_id'])
|
|
119
|
+
assert im['location'] == location_id
|
|
120
|
+
|
|
121
|
+
category_name = row['common_name']
|
|
122
|
+
if category_remappings is not None and category_name in category_remappings:
|
|
123
|
+
category_name = category_remappings[category_name]
|
|
124
|
+
|
|
125
|
+
if category_name == 'Blank':
|
|
126
|
+
category_name = 'empty'
|
|
127
|
+
assert row['is_blank'] == 1
|
|
128
|
+
else:
|
|
129
|
+
assert row['is_blank'] == 0
|
|
130
|
+
assert isinstance(category_name,str)
|
|
131
|
+
if category_name in category_name_to_id:
|
|
132
|
+
category_id = category_name_to_id[category_name]
|
|
133
|
+
else:
|
|
134
|
+
category_id = len(category_name_to_id)
|
|
135
|
+
category_name_to_id[category_name] = category_id
|
|
136
|
+
|
|
137
|
+
ann = {}
|
|
138
|
+
ann['image_id'] = image_id
|
|
139
|
+
annotations_this_image = image_id_to_annotations[image_id]
|
|
140
|
+
annotation_number = len(annotations_this_image)
|
|
141
|
+
ann['id'] = image_id + '_' + str(annotation_number).zfill(2)
|
|
142
|
+
ann['category_id'] = category_id
|
|
143
|
+
annotations_this_image.append(ann)
|
|
144
|
+
|
|
145
|
+
extra_info = {}
|
|
146
|
+
for s in wi_extra_annotation_columns:
|
|
147
|
+
v = row[s]
|
|
148
|
+
if not isnan(v):
|
|
149
|
+
extra_info[s] = v
|
|
150
|
+
ann['wi_extra_info'] = extra_info
|
|
151
|
+
|
|
152
|
+
# ...for each row
|
|
153
|
+
|
|
154
|
+
images = list(image_id_to_image.values())
|
|
155
|
+
categories = []
|
|
156
|
+
for category_name in category_name_to_id:
|
|
157
|
+
category_id = category_name_to_id[category_name]
|
|
158
|
+
categories.append({'id':category_id,'name':category_name})
|
|
159
|
+
annotations = []
|
|
160
|
+
for image_id in image_id_to_annotations:
|
|
161
|
+
annotations_this_image = image_id_to_annotations[image_id]
|
|
162
|
+
for ann in annotations_this_image:
|
|
163
|
+
annotations.append(ann)
|
|
164
|
+
info = {'version':'1.00','description':'converted from WI export'}
|
|
165
|
+
info['source_file'] = csv_file_in
|
|
166
|
+
coco_data = {}
|
|
167
|
+
coco_data['info'] = info
|
|
168
|
+
coco_data['images'] = images
|
|
169
|
+
coco_data['annotations'] = annotations
|
|
170
|
+
coco_data['categories'] = categories
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
##%% Validate images, add sizes
|
|
174
|
+
|
|
175
|
+
if validate_images:
|
|
176
|
+
|
|
177
|
+
print('Validating images')
|
|
178
|
+
# TODO: trivially parallelizable
|
|
179
|
+
|
|
180
|
+
assert os.path.isdir(image_folder), \
|
|
181
|
+
'Must specify a valid image folder if you specify validate_images=True'
|
|
182
|
+
|
|
183
|
+
# im = images[0]
|
|
184
|
+
for im in tqdm(images):
|
|
185
|
+
file_name_relative = im['file_name']
|
|
186
|
+
file_name_abs = os.path.join(image_folder,file_name_relative)
|
|
187
|
+
assert os.path.isfile(file_name_abs)
|
|
188
|
+
|
|
189
|
+
im['corrupt'] = False
|
|
190
|
+
try:
|
|
191
|
+
pil_im = vis_utils.load_image(file_name_abs)
|
|
192
|
+
except Exception:
|
|
193
|
+
im['corrupt'] = True
|
|
194
|
+
if not im['corrupt']:
|
|
195
|
+
im['width'] = pil_im.width
|
|
196
|
+
im['height'] = pil_im.height
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
##%% Write output json
|
|
200
|
+
|
|
201
|
+
if coco_file_out is None:
|
|
202
|
+
|
|
203
|
+
coco_file_out = csv_file_in + '.json'
|
|
204
|
+
|
|
205
|
+
with open(coco_file_out,'w') as f:
|
|
206
|
+
json.dump(coco_data,f,indent=1)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
##%% Validate output
|
|
210
|
+
|
|
211
|
+
from data_management.databases.integrity_check_json_db import \
|
|
212
|
+
IntegrityCheckOptions,integrity_check_json_db
|
|
213
|
+
options = IntegrityCheckOptions()
|
|
214
|
+
options.baseDir = image_folder
|
|
215
|
+
options.bCheckImageExistence = True
|
|
216
|
+
options.verbose = verbose
|
|
217
|
+
_ = integrity_check_json_db(coco_file_out,options)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
#%% Interactive driver
|
|
222
|
+
|
|
223
|
+
if False:
|
|
224
|
+
|
|
225
|
+
#%%
|
|
226
|
+
|
|
227
|
+
base_folder = r'a/b/c'
|
|
228
|
+
csv_file_in = os.path.join(base_folder,'images.csv')
|
|
229
|
+
coco_file_out = None
|
|
230
|
+
gs_prefix = 'a_b_c_main/'
|
|
231
|
+
image_folder = os.path.join(base_folder,'images')
|
|
232
|
+
validate_images = False
|
|
233
|
+
verbose = True
|
|
234
|
+
category_remappings = default_category_remappings
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
#%% Command-line driver
|
|
238
|
+
|
|
239
|
+
# TODO
|
data_management/yolo_to_coco.py
CHANGED
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
#
|
|
3
3
|
# yolo_to_coco.py
|
|
4
4
|
#
|
|
5
|
-
# Converts a YOLO-formatted
|
|
6
|
-
#
|
|
7
|
-
# Currently supports only a single folder (i.e., no recursion). Treats images without
|
|
8
|
-
# corresponding .txt files as empty.
|
|
5
|
+
# Converts a folder of YOLO-formatted annotation files to a COCO-formatted dataset.
|
|
9
6
|
#
|
|
10
7
|
########
|
|
11
8
|
|
|
@@ -14,37 +11,181 @@
|
|
|
14
11
|
import json
|
|
15
12
|
import os
|
|
16
13
|
|
|
17
|
-
from
|
|
14
|
+
from multiprocessing.pool import ThreadPool
|
|
15
|
+
from multiprocessing.pool import Pool
|
|
16
|
+
from functools import partial
|
|
17
|
+
|
|
18
18
|
from tqdm import tqdm
|
|
19
19
|
|
|
20
20
|
from md_utils.path_utils import find_images
|
|
21
|
+
from md_utils.ct_utils import invert_dictionary
|
|
22
|
+
from md_visualization.visualization_utils import open_image
|
|
21
23
|
from data_management.yolo_output_to_md_output import read_classes_from_yolo_dataset_file
|
|
22
24
|
|
|
23
25
|
|
|
26
|
+
#%% Support functions
|
|
27
|
+
|
|
28
|
+
def filename_to_image_id(fn):
|
|
29
|
+
return fn.replace(' ','_')
|
|
30
|
+
|
|
31
|
+
def _process_image(fn_abs,input_folder,category_id_to_name):
|
|
32
|
+
"""
|
|
33
|
+
Internal support function for processing one image's labels.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
# Create the image object for this image
|
|
37
|
+
fn_relative = os.path.relpath(fn_abs,input_folder)
|
|
38
|
+
image_id = filename_to_image_id(fn_relative)
|
|
39
|
+
|
|
40
|
+
# This is done in a separate loop now
|
|
41
|
+
#
|
|
42
|
+
# assert image_id not in image_ids, \
|
|
43
|
+
# 'Oops, you have hit a very esoteric case where you have the same filename ' + \
|
|
44
|
+
# 'with both spaces and underscores, this is not currently handled.'
|
|
45
|
+
# image_ids.add(image_id)
|
|
46
|
+
|
|
47
|
+
im = {}
|
|
48
|
+
im['file_name'] = fn_relative
|
|
49
|
+
im['id'] = image_id
|
|
50
|
+
|
|
51
|
+
annotations_this_image = []
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
pil_im = open_image(fn_abs)
|
|
55
|
+
im_width, im_height = pil_im.size
|
|
56
|
+
im['width'] = im_width
|
|
57
|
+
im['height'] = im_height
|
|
58
|
+
im['error'] = None
|
|
59
|
+
except Exception as e:
|
|
60
|
+
print('Warning: error reading {}:\n{}'.format(fn_relative,str(e)))
|
|
61
|
+
im['width'] = -1
|
|
62
|
+
im['height'] = -1
|
|
63
|
+
im['error'] = str(e)
|
|
64
|
+
return (im,annotations_this_image)
|
|
65
|
+
|
|
66
|
+
# Is there an annotation file for this image?
|
|
67
|
+
annotation_file = os.path.splitext(fn_abs)[0] + '.txt'
|
|
68
|
+
if not os.path.isfile(annotation_file):
|
|
69
|
+
annotation_file = os.path.splitext(fn_abs)[0] + '.TXT'
|
|
70
|
+
|
|
71
|
+
if os.path.isfile(annotation_file):
|
|
72
|
+
|
|
73
|
+
with open(annotation_file,'r') as f:
|
|
74
|
+
lines = f.readlines()
|
|
75
|
+
lines = [s.strip() for s in lines]
|
|
76
|
+
|
|
77
|
+
# s = lines[0]
|
|
78
|
+
annotation_number = 0
|
|
79
|
+
|
|
80
|
+
for s in lines:
|
|
81
|
+
|
|
82
|
+
if len(s.strip()) == 0:
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
tokens = s.split()
|
|
86
|
+
assert len(tokens) == 5
|
|
87
|
+
category_id = int(tokens[0])
|
|
88
|
+
assert category_id in category_id_to_name, \
|
|
89
|
+
'Unrecognized category ID {} in annotation file {}'.format(
|
|
90
|
+
category_id,annotation_file)
|
|
91
|
+
ann = {}
|
|
92
|
+
ann['id'] = im['id'] + '_' + str(annotation_number)
|
|
93
|
+
ann['image_id'] = im['id']
|
|
94
|
+
ann['category_id'] = category_id
|
|
95
|
+
ann['sequence_level_annotation'] = False
|
|
96
|
+
|
|
97
|
+
# COCO: [x_min, y_min, width, height] in absolute coordinates
|
|
98
|
+
# YOLO: [class, x_center, y_center, width, height] in normalized coordinates
|
|
99
|
+
|
|
100
|
+
yolo_bbox = [float(x) for x in tokens[1:]]
|
|
101
|
+
|
|
102
|
+
normalized_x_center = yolo_bbox[0]
|
|
103
|
+
normalized_y_center = yolo_bbox[1]
|
|
104
|
+
normalized_width = yolo_bbox[2]
|
|
105
|
+
normalized_height = yolo_bbox[3]
|
|
106
|
+
|
|
107
|
+
absolute_x_center = normalized_x_center * im_width
|
|
108
|
+
absolute_y_center = normalized_y_center * im_height
|
|
109
|
+
absolute_width = normalized_width * im_width
|
|
110
|
+
absolute_height = normalized_height * im_height
|
|
111
|
+
absolute_x_min = absolute_x_center - absolute_width / 2
|
|
112
|
+
absolute_y_min = absolute_y_center - absolute_height / 2
|
|
113
|
+
|
|
114
|
+
coco_bbox = [absolute_x_min, absolute_y_min, absolute_width, absolute_height]
|
|
115
|
+
|
|
116
|
+
ann['bbox'] = coco_bbox
|
|
117
|
+
annotation_number += 1
|
|
118
|
+
|
|
119
|
+
annotations_this_image.append(ann)
|
|
120
|
+
|
|
121
|
+
# ...for each annotation
|
|
122
|
+
|
|
123
|
+
# ...if this image has annotations
|
|
124
|
+
|
|
125
|
+
return (im,annotations_this_image)
|
|
126
|
+
|
|
127
|
+
# ...def _process_image(...)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
24
131
|
#%% Main conversion function
|
|
25
132
|
|
|
26
|
-
def yolo_to_coco(input_folder,
|
|
133
|
+
def yolo_to_coco(input_folder,
|
|
134
|
+
class_name_file,
|
|
135
|
+
output_file=None,
|
|
136
|
+
empty_image_handling='no_annotations',
|
|
137
|
+
empty_image_category_name='empty',
|
|
138
|
+
error_image_handling='no_annotations',
|
|
139
|
+
allow_images_without_label_files=True,
|
|
140
|
+
n_workers=1,
|
|
141
|
+
pool_type='thread',
|
|
142
|
+
recursive=True,
|
|
143
|
+
exclude_string=None,
|
|
144
|
+
include_string=None):
|
|
27
145
|
"""
|
|
28
146
|
Convert the YOLO-formatted data in [input_folder] to a COCO-formatted dictionary,
|
|
29
147
|
reading class names from [class_name_file], which can be a flat list with a .txt
|
|
30
148
|
extension or a YOLO dataset.yml file. Optionally writes the output dataset to [output_file].
|
|
31
149
|
|
|
150
|
+
empty_image_handling can be:
|
|
151
|
+
|
|
152
|
+
* 'no_annotations': include the image in the image list, with no annotations
|
|
153
|
+
|
|
154
|
+
* 'empty_annotations': include the image in the image list, and add an annotation without
|
|
155
|
+
any bounding boxes, using a category called [empty_image_category_name].
|
|
156
|
+
|
|
157
|
+
* 'skip': don't include the image in the image list
|
|
158
|
+
|
|
159
|
+
* 'error': there shouldn't be any empty images
|
|
160
|
+
|
|
161
|
+
error_image_handling can be:
|
|
162
|
+
|
|
163
|
+
* 'skip': don't include the image at all
|
|
164
|
+
|
|
165
|
+
* 'no_annotations': include with no annotations
|
|
166
|
+
|
|
167
|
+
All images will be assigned an "error" value, usually None.
|
|
168
|
+
|
|
32
169
|
Returns a COCO-formatted dictionary.
|
|
33
170
|
"""
|
|
34
171
|
|
|
35
|
-
|
|
172
|
+
## Validate input
|
|
36
173
|
|
|
37
174
|
assert os.path.isdir(input_folder)
|
|
38
175
|
assert os.path.isfile(class_name_file)
|
|
39
176
|
|
|
40
|
-
|
|
41
|
-
|
|
177
|
+
assert empty_image_handling in \
|
|
178
|
+
('no_annotations','empty_annotations','skip','error'), \
|
|
179
|
+
'Unrecognized empty image handling spec: {}'.format(empty_image_handling)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
## Read class names
|
|
42
183
|
|
|
43
184
|
ext = os.path.splitext(class_name_file)[1][1:]
|
|
44
|
-
assert ext in ('yml','txt','yaml'), 'Unrecognized class name file type {}'.format(
|
|
185
|
+
assert ext in ('yml','txt','yaml','data'), 'Unrecognized class name file type {}'.format(
|
|
45
186
|
class_name_file)
|
|
46
187
|
|
|
47
|
-
if ext
|
|
188
|
+
if ext in ('txt','data'):
|
|
48
189
|
|
|
49
190
|
with open(class_name_file,'r') as f:
|
|
50
191
|
lines = f.readlines()
|
|
@@ -70,14 +211,41 @@ def yolo_to_coco(input_folder,class_name_file,output_file=None):
|
|
|
70
211
|
|
|
71
212
|
assert ext in ('yml','yaml')
|
|
72
213
|
category_id_to_name = read_classes_from_yolo_dataset_file(class_name_file)
|
|
214
|
+
|
|
215
|
+
# Find or create the empty image category, if necessary
|
|
216
|
+
empty_category_id = None
|
|
217
|
+
|
|
218
|
+
if (empty_image_handling == 'empty_annotations'):
|
|
219
|
+
category_name_to_id = invert_dictionary(category_id_to_name)
|
|
220
|
+
if empty_image_category_name in category_name_to_id:
|
|
221
|
+
empty_category_id = category_name_to_id[empty_image_category_name]
|
|
222
|
+
print('Using existing empty image category with name {}, ID {}'.format(
|
|
223
|
+
empty_image_category_name,empty_category_id))
|
|
224
|
+
else:
|
|
225
|
+
empty_category_id = len(category_id_to_name)
|
|
226
|
+
print('Adding an empty category with name {}, ID {}'.format(
|
|
227
|
+
empty_image_category_name,empty_category_id))
|
|
228
|
+
category_id_to_name[empty_category_id] = empty_image_category_name
|
|
73
229
|
|
|
74
|
-
|
|
75
|
-
|
|
230
|
+
|
|
231
|
+
## Enumerate images
|
|
232
|
+
|
|
233
|
+
print('Enumerating images...')
|
|
76
234
|
|
|
77
|
-
|
|
235
|
+
image_files_abs = find_images(input_folder,recursive=recursive,convert_slashes=True)
|
|
78
236
|
|
|
79
|
-
|
|
80
|
-
|
|
237
|
+
n_files_original = len(image_files_abs)
|
|
238
|
+
|
|
239
|
+
# Optionally include/exclude images matching specific strings
|
|
240
|
+
if exclude_string is not None:
|
|
241
|
+
image_files_abs = [fn for fn in image_files_abs if exclude_string not in fn]
|
|
242
|
+
if include_string is not None:
|
|
243
|
+
image_files_abs = [fn for fn in image_files_abs if include_string in fn]
|
|
244
|
+
|
|
245
|
+
if len(image_files_abs) != n_files_original or exclude_string is not None or include_string is not None:
|
|
246
|
+
n_excluded = n_files_original - len(image_files_abs)
|
|
247
|
+
print('Excluded {} of {} images based on filenames'.format(n_excluded,n_files_original))
|
|
248
|
+
|
|
81
249
|
categories = []
|
|
82
250
|
|
|
83
251
|
for category_id in category_id_to_name:
|
|
@@ -87,79 +255,111 @@ def yolo_to_coco(input_folder,class_name_file,output_file=None):
|
|
|
87
255
|
info['version'] = '1.0'
|
|
88
256
|
info['description'] = 'Converted from YOLO format'
|
|
89
257
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
258
|
+
image_ids = set()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
## If we're expected to have labels for every image, check before we process all the images
|
|
262
|
+
|
|
263
|
+
if not allow_images_without_label_files:
|
|
264
|
+
print('Verifying that label files exist')
|
|
265
|
+
for image_file_abs in tqdm(image_files_abs):
|
|
266
|
+
label_file_abs = os.path.splitext(image_file_abs)[0] + '.txt'
|
|
267
|
+
assert os.path.isfile(label_file_abs), \
|
|
268
|
+
'No annotation file for {}'.format(image_file_abs)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
## Initial loop to make sure image IDs will be unique
|
|
272
|
+
|
|
273
|
+
print('Validating image IDs...')
|
|
274
|
+
|
|
275
|
+
for fn_abs in tqdm(image_files_abs):
|
|
276
|
+
|
|
277
|
+
fn_relative = os.path.relpath(fn_abs,input_folder)
|
|
278
|
+
image_id = filename_to_image_id(fn_relative)
|
|
279
|
+
assert image_id not in image_ids, \
|
|
280
|
+
'Oops, you have hit a very esoteric case where you have the same filename ' + \
|
|
281
|
+
'with both spaces and underscores, this is not currently handled.'
|
|
282
|
+
image_ids.add(image_id)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
## Main loop to process labels
|
|
286
|
+
|
|
287
|
+
print('Processing labels...')
|
|
288
|
+
|
|
289
|
+
if n_workers <= 1:
|
|
290
|
+
|
|
291
|
+
image_results = []
|
|
292
|
+
for fn_abs in tqdm(image_files_abs):
|
|
293
|
+
image_results.append(_process_image(fn_abs,input_folder,category_id_to_name))
|
|
294
|
+
|
|
295
|
+
else:
|
|
296
|
+
|
|
297
|
+
assert pool_type in ('process','thread'), 'Illegal pool type {}'.format(pool_type)
|
|
298
|
+
|
|
299
|
+
if pool_type == 'thread':
|
|
300
|
+
pool = ThreadPool(n_workers)
|
|
112
301
|
else:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
continue
|
|
122
|
-
tokens = s.split()
|
|
123
|
-
assert len(tokens) == 5
|
|
124
|
-
category_id = int(tokens[0])
|
|
125
|
-
assert category_id in category_id_to_name, \
|
|
126
|
-
'Unrecognized category ID {} in annotation file {}'.format(
|
|
127
|
-
category_id,annotation_file)
|
|
128
|
-
ann = {}
|
|
129
|
-
ann['id'] = im['id'] + '_' + str(annotation_number)
|
|
130
|
-
ann['image_id'] = im['id']
|
|
131
|
-
ann['category_id'] = category_id
|
|
132
|
-
ann['sequence_level_annotation'] = False
|
|
133
|
-
|
|
134
|
-
# COCO: [x_min, y_min, width, height] in absolute coordinates
|
|
135
|
-
# YOLO: [class, x_center, y_center, width, height] in normalized coordinates
|
|
136
|
-
|
|
137
|
-
yolo_bbox = [float(x) for x in tokens[1:]]
|
|
138
|
-
|
|
139
|
-
normalized_x_center = yolo_bbox[0]
|
|
140
|
-
normalized_y_center = yolo_bbox[1]
|
|
141
|
-
normalized_width = yolo_bbox[2]
|
|
142
|
-
normalized_height = yolo_bbox[3]
|
|
143
|
-
|
|
144
|
-
absolute_x_center = normalized_x_center * im_width
|
|
145
|
-
absolute_y_center = normalized_y_center * im_height
|
|
146
|
-
absolute_width = normalized_width * im_width
|
|
147
|
-
absolute_height = normalized_height * im_height
|
|
148
|
-
absolute_x_min = absolute_x_center - absolute_width / 2
|
|
149
|
-
absolute_y_min = absolute_y_center - absolute_height / 2
|
|
150
|
-
|
|
151
|
-
coco_bbox = [absolute_x_min, absolute_y_min, absolute_width, absolute_height]
|
|
302
|
+
pool = Pool(n_workers)
|
|
303
|
+
|
|
304
|
+
print('Starting a {} pool of {} workers'.format(pool_type,n_workers))
|
|
305
|
+
|
|
306
|
+
p = partial(_process_image,input_folder=input_folder,
|
|
307
|
+
category_id_to_name=category_id_to_name)
|
|
308
|
+
image_results = list(tqdm(pool.imap(p, image_files_abs),
|
|
309
|
+
total=len(image_files_abs)))
|
|
152
310
|
|
|
153
|
-
|
|
154
|
-
|
|
311
|
+
|
|
312
|
+
assert len(image_results) == len(image_files_abs)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
## Re-assembly of results into a COCO dict
|
|
316
|
+
|
|
317
|
+
print('Assembling labels...')
|
|
318
|
+
|
|
319
|
+
images = []
|
|
320
|
+
annotations = []
|
|
321
|
+
|
|
322
|
+
for image_result in tqdm(image_results):
|
|
323
|
+
|
|
324
|
+
im = image_result[0]
|
|
325
|
+
annotations_this_image = image_result[1]
|
|
326
|
+
|
|
327
|
+
# If we have annotations for this image
|
|
328
|
+
if len(annotations_this_image) > 0:
|
|
329
|
+
assert im['error'] is None
|
|
330
|
+
images.append(im)
|
|
331
|
+
for ann in annotations_this_image:
|
|
332
|
+
annotations.append(ann)
|
|
155
333
|
|
|
156
|
-
|
|
334
|
+
# If this image failed to read
|
|
335
|
+
elif im['error'] is not None:
|
|
336
|
+
|
|
337
|
+
if error_image_handling == 'skip':
|
|
338
|
+
pass
|
|
339
|
+
elif error_image_handling == 'no_annotations':
|
|
340
|
+
images.append(im)
|
|
157
341
|
|
|
158
|
-
|
|
342
|
+
# If this image read successfully, but there are no annotations
|
|
343
|
+
else:
|
|
159
344
|
|
|
160
|
-
|
|
345
|
+
if empty_image_handling == 'skip':
|
|
346
|
+
pass
|
|
347
|
+
elif empty_image_handling == 'no_annotations':
|
|
348
|
+
images.append(im)
|
|
349
|
+
elif empty_image_handling == 'empty_annotations':
|
|
350
|
+
assert empty_category_id is not None
|
|
351
|
+
ann = {}
|
|
352
|
+
ann['id'] = im['id'] + '_0'
|
|
353
|
+
ann['image_id'] = im['id']
|
|
354
|
+
ann['category_id'] = empty_category_id
|
|
355
|
+
ann['sequence_level_annotation'] = False
|
|
356
|
+
# This would also be a reasonable thing to do, but it's not the convention
|
|
357
|
+
# we're adopting.
|
|
358
|
+
# ann['bbox'] = [0,0,0,0]
|
|
359
|
+
annotations.append(ann)
|
|
360
|
+
images.append(im)
|
|
161
361
|
|
|
162
|
-
# ...for each image
|
|
362
|
+
# ...for each image result
|
|
163
363
|
|
|
164
364
|
print('Read {} annotations for {} images'.format(len(annotations),
|
|
165
365
|
len(images)))
|