megadetector 5.0.6__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 +297 -202
- api/batch_processing/data_preparation/manage_video_batch.py +7 -2
- api/batch_processing/postprocessing/add_max_conf.py +1 -0
- api/batch_processing/postprocessing/combine_api_outputs.py +2 -2
- api/batch_processing/postprocessing/compare_batch_results.py +111 -61
- api/batch_processing/postprocessing/convert_output_format.py +24 -6
- api/batch_processing/postprocessing/load_api_results.py +56 -72
- api/batch_processing/postprocessing/md_to_labelme.py +119 -51
- api/batch_processing/postprocessing/merge_detections.py +30 -5
- api/batch_processing/postprocessing/postprocess_batch_results.py +175 -55
- api/batch_processing/postprocessing/remap_detection_categories.py +163 -0
- api/batch_processing/postprocessing/render_detection_confusion_matrix.py +628 -0
- api/batch_processing/postprocessing/repeat_detection_elimination/find_repeat_detections.py +71 -23
- api/batch_processing/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +1 -1
- api/batch_processing/postprocessing/repeat_detection_elimination/repeat_detections_core.py +224 -76
- api/batch_processing/postprocessing/subset_json_detector_output.py +132 -5
- api/batch_processing/postprocessing/top_folders_to_bottom.py +1 -1
- classification/prepare_classification_script.py +191 -191
- data_management/cct_json_utils.py +7 -2
- data_management/coco_to_labelme.py +263 -0
- data_management/coco_to_yolo.py +72 -48
- data_management/databases/integrity_check_json_db.py +75 -64
- data_management/databases/subset_json_db.py +1 -1
- data_management/generate_crops_from_cct.py +1 -1
- data_management/get_image_sizes.py +44 -26
- data_management/importers/animl_results_to_md_results.py +3 -5
- data_management/importers/noaa_seals_2019.py +2 -2
- data_management/importers/zamba_results_to_md_results.py +2 -2
- data_management/labelme_to_coco.py +264 -127
- data_management/labelme_to_yolo.py +96 -53
- data_management/lila/create_lila_blank_set.py +557 -0
- data_management/lila/create_lila_test_set.py +2 -1
- data_management/lila/create_links_to_md_results_files.py +1 -1
- data_management/lila/download_lila_subset.py +138 -45
- data_management/lila/generate_lila_per_image_labels.py +23 -14
- data_management/lila/get_lila_annotation_counts.py +16 -10
- data_management/lila/lila_common.py +15 -42
- data_management/lila/test_lila_metadata_urls.py +116 -0
- data_management/read_exif.py +65 -16
- data_management/remap_coco_categories.py +84 -0
- data_management/resize_coco_dataset.py +14 -31
- data_management/wi_download_csv_to_coco.py +239 -0
- data_management/yolo_output_to_md_output.py +40 -13
- data_management/yolo_to_coco.py +313 -100
- detection/process_video.py +36 -14
- detection/pytorch_detector.py +1 -1
- detection/run_detector.py +73 -18
- detection/run_detector_batch.py +116 -27
- detection/run_inference_with_yolov5_val.py +135 -27
- detection/run_tiled_inference.py +153 -43
- detection/tf_detector.py +2 -1
- detection/video_utils.py +4 -2
- md_utils/ct_utils.py +101 -6
- md_utils/md_tests.py +264 -17
- md_utils/path_utils.py +326 -47
- md_utils/process_utils.py +26 -7
- md_utils/split_locations_into_train_val.py +215 -0
- md_utils/string_utils.py +10 -0
- md_utils/url_utils.py +66 -3
- md_utils/write_html_image_list.py +12 -2
- md_visualization/visualization_utils.py +380 -74
- md_visualization/visualize_db.py +41 -10
- md_visualization/visualize_detector_output.py +185 -104
- {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/METADATA +11 -13
- {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/RECORD +74 -67
- {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/WHEEL +1 -1
- taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +1 -1
- taxonomy_mapping/map_new_lila_datasets.py +43 -39
- taxonomy_mapping/prepare_lila_taxonomy_release.py +5 -2
- taxonomy_mapping/preview_lila_taxonomy.py +27 -27
- taxonomy_mapping/species_lookup.py +33 -13
- taxonomy_mapping/taxonomy_csv_checker.py +7 -5
- md_visualization/visualize_megadb.py +0 -183
- {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/LICENSE +0 -0
- {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/top_level.txt +0 -0
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,57 +11,241 @@
|
|
|
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
|
|
23
|
+
from data_management.yolo_output_to_md_output import read_classes_from_yolo_dataset_file
|
|
24
|
+
|
|
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
|
+
|
|
21
129
|
|
|
22
130
|
|
|
23
131
|
#%% Main conversion function
|
|
24
132
|
|
|
25
|
-
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):
|
|
26
145
|
"""
|
|
27
146
|
Convert the YOLO-formatted data in [input_folder] to a COCO-formatted dictionary,
|
|
28
|
-
reading class names from
|
|
29
|
-
dataset to [output_file].
|
|
147
|
+
reading class names from [class_name_file], which can be a flat list with a .txt
|
|
148
|
+
extension or a YOLO dataset.yml file. Optionally writes the output dataset to [output_file].
|
|
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
|
+
|
|
169
|
+
Returns a COCO-formatted dictionary.
|
|
30
170
|
"""
|
|
31
171
|
|
|
32
|
-
|
|
172
|
+
## Validate input
|
|
33
173
|
|
|
34
174
|
assert os.path.isdir(input_folder)
|
|
35
175
|
assert os.path.isfile(class_name_file)
|
|
36
176
|
|
|
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
|
|
37
183
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
lines = f.readlines()
|
|
42
|
-
assert len(lines) > 0, 'Empty class name file {}'.format(class_name_file)
|
|
43
|
-
lines = [s.strip() for s in lines]
|
|
44
|
-
assert len(lines[0]) > 0, 'Empty class name file {} (empty first line)'.format(class_name_file)
|
|
184
|
+
ext = os.path.splitext(class_name_file)[1][1:]
|
|
185
|
+
assert ext in ('yml','txt','yaml','data'), 'Unrecognized class name file type {}'.format(
|
|
186
|
+
class_name_file)
|
|
45
187
|
|
|
46
|
-
|
|
47
|
-
b_found_blank = False
|
|
48
|
-
for s in lines:
|
|
49
|
-
if len(s) == 0:
|
|
50
|
-
b_found_blank = True
|
|
51
|
-
elif b_found_blank:
|
|
52
|
-
raise ValueError('Invalid class name file {}, non-blank line after the last blank line'.format(
|
|
53
|
-
class_name_file))
|
|
54
|
-
|
|
55
|
-
category_id_to_name = {}
|
|
188
|
+
if ext in ('txt','data'):
|
|
56
189
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
190
|
+
with open(class_name_file,'r') as f:
|
|
191
|
+
lines = f.readlines()
|
|
192
|
+
assert len(lines) > 0, 'Empty class name file {}'.format(class_name_file)
|
|
193
|
+
class_names = [s.strip() for s in lines]
|
|
194
|
+
assert len(lines[0]) > 0, 'Empty class name file {} (empty first line)'.format(class_name_file)
|
|
60
195
|
|
|
196
|
+
# Blank lines should only appear at the end
|
|
197
|
+
b_found_blank = False
|
|
198
|
+
for s in lines:
|
|
199
|
+
if len(s) == 0:
|
|
200
|
+
b_found_blank = True
|
|
201
|
+
elif b_found_blank:
|
|
202
|
+
raise ValueError('Invalid class name file {}, non-blank line after the last blank line'.format(
|
|
203
|
+
class_name_file))
|
|
204
|
+
|
|
205
|
+
category_id_to_name = {}
|
|
206
|
+
for i_category_id,category_name in enumerate(class_names):
|
|
207
|
+
assert len(category_name) > 0
|
|
208
|
+
category_id_to_name[i_category_id] = category_name
|
|
209
|
+
|
|
210
|
+
else:
|
|
61
211
|
|
|
62
|
-
|
|
212
|
+
assert ext in ('yml','yaml')
|
|
213
|
+
category_id_to_name = read_classes_from_yolo_dataset_file(class_name_file)
|
|
63
214
|
|
|
64
|
-
|
|
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
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
## Enumerate images
|
|
232
|
+
|
|
233
|
+
print('Enumerating images...')
|
|
234
|
+
|
|
235
|
+
image_files_abs = find_images(input_folder,recursive=recursive,convert_slashes=True)
|
|
65
236
|
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
|
|
68
249
|
categories = []
|
|
69
250
|
|
|
70
251
|
for category_id in category_id_to_name:
|
|
@@ -74,79 +255,111 @@ def yolo_to_coco(input_folder,class_name_file,output_file=None):
|
|
|
74
255
|
info['version'] = '1.0'
|
|
75
256
|
info['description'] = 'Converted from YOLO format'
|
|
76
257
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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)
|
|
99
301
|
else:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
continue
|
|
109
|
-
tokens = s.split()
|
|
110
|
-
assert len(tokens) == 5
|
|
111
|
-
category_id = int(tokens[0])
|
|
112
|
-
assert category_id in category_id_to_name, \
|
|
113
|
-
'Unrecognized category ID {} in annotation file {}'.format(
|
|
114
|
-
category_id,annotation_file)
|
|
115
|
-
ann = {}
|
|
116
|
-
ann['id'] = im['id'] + '_' + str(annotation_number)
|
|
117
|
-
ann['image_id'] = im['id']
|
|
118
|
-
ann['category_id'] = category_id
|
|
119
|
-
ann['sequence_level_annotation'] = False
|
|
120
|
-
|
|
121
|
-
# COCO: [x_min, y_min, width, height] in absolute coordinates
|
|
122
|
-
# YOLO: [class, x_center, y_center, width, height] in normalized coordinates
|
|
123
|
-
|
|
124
|
-
yolo_bbox = [float(x) for x in tokens[1:]]
|
|
125
|
-
|
|
126
|
-
normalized_x_center = yolo_bbox[0]
|
|
127
|
-
normalized_y_center = yolo_bbox[1]
|
|
128
|
-
normalized_width = yolo_bbox[2]
|
|
129
|
-
normalized_height = yolo_bbox[3]
|
|
130
|
-
|
|
131
|
-
absolute_x_center = normalized_x_center * im_width
|
|
132
|
-
absolute_y_center = normalized_y_center * im_height
|
|
133
|
-
absolute_width = normalized_width * im_width
|
|
134
|
-
absolute_height = normalized_height * im_height
|
|
135
|
-
absolute_x_min = absolute_x_center - absolute_width / 2
|
|
136
|
-
absolute_y_min = absolute_y_center - absolute_height / 2
|
|
137
|
-
|
|
138
|
-
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)))
|
|
139
310
|
|
|
140
|
-
|
|
141
|
-
|
|
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)
|
|
142
333
|
|
|
143
|
-
|
|
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)
|
|
144
341
|
|
|
145
|
-
|
|
342
|
+
# If this image read successfully, but there are no annotations
|
|
343
|
+
else:
|
|
146
344
|
|
|
147
|
-
|
|
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)
|
|
148
361
|
|
|
149
|
-
# ...for each image
|
|
362
|
+
# ...for each image result
|
|
150
363
|
|
|
151
364
|
print('Read {} annotations for {} images'.format(len(annotations),
|
|
152
365
|
len(images)))
|
detection/process_video.py
CHANGED
|
@@ -26,12 +26,17 @@ from detection.video_utils import frame_results_to_video_results
|
|
|
26
26
|
from detection.video_utils import video_folder_to_frames
|
|
27
27
|
from uuid import uuid1
|
|
28
28
|
|
|
29
|
+
from detection.video_utils import default_fourcc
|
|
30
|
+
|
|
29
31
|
|
|
30
32
|
#%% Options classes
|
|
31
33
|
|
|
32
34
|
class ProcessVideoOptions:
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
# Can be a model filename (.pt or .pb) or a model name (e.g. "MDV5A")
|
|
37
|
+
model_file = 'MDV5A'
|
|
38
|
+
|
|
39
|
+
# Can be a file or a folder
|
|
35
40
|
input_video_file = ''
|
|
36
41
|
|
|
37
42
|
output_json_file = None
|
|
@@ -72,9 +77,10 @@ class ProcessVideoOptions:
|
|
|
72
77
|
|
|
73
78
|
recursive = False
|
|
74
79
|
verbose = False
|
|
80
|
+
|
|
75
81
|
fourcc = None
|
|
76
82
|
|
|
77
|
-
rendering_confidence_threshold =
|
|
83
|
+
rendering_confidence_threshold = None
|
|
78
84
|
json_confidence_threshold = 0.005
|
|
79
85
|
frame_sample = None
|
|
80
86
|
|
|
@@ -175,8 +181,14 @@ def process_video(options):
|
|
|
175
181
|
confidence_threshold=options.rendering_confidence_threshold)
|
|
176
182
|
|
|
177
183
|
# Combine into a video
|
|
178
|
-
|
|
179
|
-
|
|
184
|
+
if options.frame_sample is None:
|
|
185
|
+
rendering_fs = Fs
|
|
186
|
+
else:
|
|
187
|
+
rendering_fs = Fs / options.frame_sample
|
|
188
|
+
|
|
189
|
+
print('Rendering video to {} at {} fps (original video {} fps)'.format(
|
|
190
|
+
options.output_video_file,rendering_fs,Fs))
|
|
191
|
+
frames_to_video(detected_frame_files, rendering_fs, options.output_video_file, codec_spec=options.fourcc)
|
|
180
192
|
|
|
181
193
|
# Delete the temporary directory we used for detection images
|
|
182
194
|
if not options.keep_rendered_frames:
|
|
@@ -344,11 +356,19 @@ def process_video_folder(options):
|
|
|
344
356
|
output_video_folder = options.input_video_file
|
|
345
357
|
|
|
346
358
|
# For each video
|
|
359
|
+
#
|
|
360
|
+
# TODO: parallelize this loop
|
|
361
|
+
#
|
|
347
362
|
# i_video=0; input_video_file_abs = video_filenames[i_video]
|
|
348
363
|
for i_video,input_video_file_abs in enumerate(video_filenames):
|
|
349
364
|
|
|
350
365
|
video_fs = Fs[i_video]
|
|
351
366
|
|
|
367
|
+
if options.frame_sample is None:
|
|
368
|
+
rendering_fs = video_fs
|
|
369
|
+
else:
|
|
370
|
+
rendering_fs = video_fs / options.frame_sample
|
|
371
|
+
|
|
352
372
|
input_video_file_relative = os.path.relpath(input_video_file_abs,options.input_video_file)
|
|
353
373
|
video_frame_output_folder = os.path.join(frame_rendering_output_dir,input_video_file_relative)
|
|
354
374
|
assert os.path.isdir(video_frame_output_folder), \
|
|
@@ -371,11 +391,10 @@ def process_video_folder(options):
|
|
|
371
391
|
os.makedirs(os.path.dirname(video_output_file),exist_ok=True)
|
|
372
392
|
|
|
373
393
|
# Create the output video
|
|
374
|
-
print('Rendering detections for video {} to {} at {} fps'.format(
|
|
375
|
-
|
|
376
|
-
frames_to_video(video_frame_files,
|
|
377
|
-
|
|
378
|
-
|
|
394
|
+
print('Rendering detections for video {} to {} at {} fps (original video {} fps)'.format(
|
|
395
|
+
input_video_file_relative,video_output_file,rendering_fs,video_fs))
|
|
396
|
+
frames_to_video(video_frame_files, rendering_fs, video_output_file, codec_spec=options.fourcc)
|
|
397
|
+
|
|
379
398
|
# ...for each video
|
|
380
399
|
|
|
381
400
|
# Possibly clean up rendered frames
|
|
@@ -525,12 +544,14 @@ if False:
|
|
|
525
544
|
|
|
526
545
|
def main():
|
|
527
546
|
|
|
547
|
+
default_options = ProcessVideoOptions()
|
|
548
|
+
|
|
528
549
|
parser = argparse.ArgumentParser(description=(
|
|
529
550
|
'Run MegaDetector on each frame in a video (or every Nth frame), optionally '\
|
|
530
551
|
'producing a new video with detections annotated'))
|
|
531
552
|
|
|
532
553
|
parser.add_argument('model_file', type=str,
|
|
533
|
-
help='MegaDetector model file')
|
|
554
|
+
help='MegaDetector model file (.pt or .pb) or model name (e.g. "MDV5A")')
|
|
534
555
|
|
|
535
556
|
parser.add_argument('input_video_file', type=str,
|
|
536
557
|
help='video file (or folder) to process')
|
|
@@ -567,8 +588,8 @@ def main():
|
|
|
567
588
|
parser.add_argument('--render_output_video', action='store_true',
|
|
568
589
|
help='enable video output rendering (not rendered by default)')
|
|
569
590
|
|
|
570
|
-
parser.add_argument('--fourcc', default=
|
|
571
|
-
help='fourcc code to use for video encoding, only used if render_output_video is True')
|
|
591
|
+
parser.add_argument('--fourcc', default=default_fourcc,
|
|
592
|
+
help='fourcc code to use for video encoding (default {}), only used if render_output_video is True'.format(default_fourcc))
|
|
572
593
|
|
|
573
594
|
parser.add_argument('--keep_rendered_frames',
|
|
574
595
|
action='store_true', help='Disable the deletion of rendered (w/boxes) frames')
|
|
@@ -586,11 +607,12 @@ def main():
|
|
|
586
607
|
'whether other files were present in the folder.')
|
|
587
608
|
|
|
588
609
|
parser.add_argument('--rendering_confidence_threshold', type=float,
|
|
589
|
-
default=
|
|
610
|
+
default=None, help="don't render boxes with confidence below this threshold (defaults to choosing based on the MD version)")
|
|
590
611
|
|
|
591
612
|
parser.add_argument('--json_confidence_threshold', type=float,
|
|
592
613
|
default=0.0, help="don't include boxes in the .json file with confidence "\
|
|
593
|
-
'below this threshold'
|
|
614
|
+
'below this threshold (default {})'.format(
|
|
615
|
+
default_options.json_confidence_threshold))
|
|
594
616
|
|
|
595
617
|
parser.add_argument('--n_cores', type=int,
|
|
596
618
|
default=1, help='number of cores to use for frame separation and detection. '\
|
detection/pytorch_detector.py
CHANGED
|
@@ -234,7 +234,7 @@ class PTDetector:
|
|
|
234
234
|
if self.device == 'mps':
|
|
235
235
|
# As of v1.13.0.dev20220824, nms is not implemented for MPS.
|
|
236
236
|
#
|
|
237
|
-
# Send
|
|
237
|
+
# Send prediction back to the CPU to fix.
|
|
238
238
|
pred = non_max_suppression(prediction=pred.cpu(), conf_thres=detection_threshold)
|
|
239
239
|
else:
|
|
240
240
|
pred = non_max_suppression(prediction=pred, conf_thres=detection_threshold)
|