megadetector 5.0.6__py3-none-any.whl → 5.0.7__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 +278 -197
- 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/compare_batch_results.py +110 -60
- api/batch_processing/postprocessing/load_api_results.py +55 -69
- api/batch_processing/postprocessing/md_to_labelme.py +1 -0
- api/batch_processing/postprocessing/postprocess_batch_results.py +158 -50
- api/batch_processing/postprocessing/render_detection_confusion_matrix.py +625 -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 +222 -74
- 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/coco_to_yolo.py +65 -44
- data_management/databases/integrity_check_json_db.py +7 -5
- data_management/generate_crops_from_cct.py +1 -1
- data_management/importers/animl_results_to_md_results.py +2 -2
- data_management/importers/noaa_seals_2019.py +1 -1
- data_management/importers/zamba_results_to_md_results.py +2 -2
- data_management/labelme_to_coco.py +34 -6
- data_management/labelme_to_yolo.py +1 -1
- data_management/lila/create_lila_blank_set.py +474 -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 +46 -21
- 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 +14 -11
- data_management/lila/test_lila_metadata_urls.py +116 -0
- data_management/resize_coco_dataset.py +12 -10
- data_management/yolo_output_to_md_output.py +40 -13
- data_management/yolo_to_coco.py +34 -21
- detection/process_video.py +36 -14
- detection/pytorch_detector.py +1 -1
- detection/run_detector.py +73 -18
- detection/run_detector_batch.py +104 -24
- detection/run_inference_with_yolov5_val.py +127 -26
- detection/run_tiled_inference.py +153 -43
- detection/video_utils.py +3 -1
- md_utils/ct_utils.py +79 -3
- md_utils/md_tests.py +253 -15
- md_utils/path_utils.py +129 -24
- 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 +0 -2
- md_utils/write_html_image_list.py +1 -0
- md_visualization/visualization_utils.py +17 -2
- md_visualization/visualize_db.py +8 -0
- md_visualization/visualize_detector_output.py +185 -104
- {megadetector-5.0.6.dist-info → megadetector-5.0.7.dist-info}/METADATA +2 -2
- {megadetector-5.0.6.dist-info → megadetector-5.0.7.dist-info}/RECORD +62 -58
- {megadetector-5.0.6.dist-info → megadetector-5.0.7.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
- {megadetector-5.0.6.dist-info → megadetector-5.0.7.dist-info}/LICENSE +0 -0
- {megadetector-5.0.6.dist-info → megadetector-5.0.7.dist-info}/top_level.txt +0 -0
|
@@ -49,6 +49,7 @@ from tqdm import tqdm
|
|
|
49
49
|
|
|
50
50
|
from md_utils import path_utils
|
|
51
51
|
from md_utils import process_utils
|
|
52
|
+
from md_utils import string_utils
|
|
52
53
|
from data_management import yolo_output_to_md_output
|
|
53
54
|
from detection.run_detector import try_download_known_detector
|
|
54
55
|
|
|
@@ -68,17 +69,20 @@ class YoloInferenceOptions:
|
|
|
68
69
|
|
|
69
70
|
## Optional ##
|
|
70
71
|
|
|
71
|
-
# Required for YOLOv5
|
|
72
|
+
# Required for older YOLOv5 inference, not for newer ulytralytics inference
|
|
72
73
|
yolo_working_folder = None
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
# Currently 'yolov5' and 'ultralytics' are supported, and really these are proxies for
|
|
76
|
+
# "the yolov5 repo" and "the ultralytics repo" (typically YOLOv8).
|
|
77
|
+
model_type = 'yolov5'
|
|
75
78
|
|
|
76
79
|
image_size = default_image_size_with_augmentation
|
|
77
80
|
conf_thres = '0.001'
|
|
78
81
|
batch_size = 1
|
|
79
82
|
device_string = '0'
|
|
80
83
|
augment = True
|
|
81
|
-
|
|
84
|
+
half_precision_enabled = None
|
|
85
|
+
|
|
82
86
|
symlink_folder = None
|
|
83
87
|
use_symlinks = True
|
|
84
88
|
|
|
@@ -97,16 +101,28 @@ class YoloInferenceOptions:
|
|
|
97
101
|
overwrite_handling = 'skip'
|
|
98
102
|
|
|
99
103
|
preview_yolo_command_only = False
|
|
104
|
+
|
|
105
|
+
treat_copy_failures_as_warnings = False
|
|
106
|
+
|
|
107
|
+
save_yolo_debug_output = False
|
|
100
108
|
|
|
101
109
|
|
|
102
110
|
#%% Main function
|
|
103
111
|
|
|
104
112
|
def run_inference_with_yolo_val(options):
|
|
105
113
|
|
|
106
|
-
##%%
|
|
114
|
+
##%% Input and path handling
|
|
115
|
+
|
|
116
|
+
if options.model_type == 'yolov8':
|
|
117
|
+
|
|
118
|
+
print('Warning: model type "yolov8" supplied, "ultralytics" is the preferred model type string for YOLOv8 models')
|
|
119
|
+
options.model_type = 'ultralytics'
|
|
120
|
+
|
|
121
|
+
if (options.model_type == 'yolov5') and ('yolov8' in options.model_filename.lower()):
|
|
122
|
+
print('\n\n*** Warning: model type set as "yolov5", but your model filename contains "yolov8"... did you mean to use --model_type yolov8?" ***\n\n')
|
|
107
123
|
|
|
108
124
|
if options.yolo_working_folder is None:
|
|
109
|
-
assert options.model_type == '
|
|
125
|
+
assert options.model_type == 'ultralytics', \
|
|
110
126
|
'A working folder is required to run YOLOv5 val.py'
|
|
111
127
|
else:
|
|
112
128
|
assert os.path.isdir(options.yolo_working_folder), \
|
|
@@ -115,6 +131,11 @@ def run_inference_with_yolo_val(options):
|
|
|
115
131
|
assert os.path.isdir(options.input_folder) or os.path.isfile(options.input_folder), \
|
|
116
132
|
'Could not find input {}'.format(options.input_folder)
|
|
117
133
|
|
|
134
|
+
if options.half_precision_enabled is not None:
|
|
135
|
+
assert options.half_precision_enabled in (0,1), \
|
|
136
|
+
'Invalid value {} for --half_precision_enabled (should be 0 or 1)'.format(
|
|
137
|
+
options.half_precision_enabled)
|
|
138
|
+
|
|
118
139
|
# If the model filename is a known model string (e.g. "MDv5A", download the model if necessary)
|
|
119
140
|
model_filename = try_download_known_detector(options.model_filename)
|
|
120
141
|
|
|
@@ -218,10 +239,20 @@ def run_inference_with_yolo_val(options):
|
|
|
218
239
|
else:
|
|
219
240
|
shutil.copyfile(image_fn,symlink_full_path)
|
|
220
241
|
except Exception as e:
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
242
|
+
error_string = str(e)
|
|
243
|
+
image_id_to_error[image_id] = error_string
|
|
244
|
+
# Always break if the user is trying to create symlinks on Windows without
|
|
245
|
+
# permission, 100% of images will always fail in this case.
|
|
246
|
+
if ('a required privilege is not held by the client' in error_string.lower()) or \
|
|
247
|
+
(not options.treat_copy_failures_as_warnings):
|
|
248
|
+
print('\nError copying/creating link for input file {}: {}'.format(
|
|
249
|
+
image_fn,error_string))
|
|
250
|
+
|
|
251
|
+
raise
|
|
252
|
+
else:
|
|
253
|
+
print('Warning: error copying/creating link for input file {}: {}'.format(
|
|
254
|
+
image_fn,error_string))
|
|
255
|
+
continue
|
|
225
256
|
|
|
226
257
|
# ...for each image
|
|
227
258
|
|
|
@@ -270,17 +301,34 @@ def run_inference_with_yolo_val(options):
|
|
|
270
301
|
if options.augment:
|
|
271
302
|
cmd += ' --augment'
|
|
272
303
|
|
|
273
|
-
|
|
304
|
+
# --half is a store_true argument for YOLOv5's val.py
|
|
305
|
+
if (options.half_precision_enabled is not None) and (options.half_precision_enabled == 1):
|
|
306
|
+
cmd += ' --half'
|
|
307
|
+
|
|
308
|
+
# Sometimes useful for debugging
|
|
309
|
+
# cmd += ' --save_conf --save_txt'
|
|
274
310
|
|
|
311
|
+
elif options.model_type == 'ultralytics':
|
|
312
|
+
|
|
275
313
|
if options.augment:
|
|
276
314
|
augment_string = 'augment'
|
|
277
315
|
else:
|
|
278
316
|
augment_string = ''
|
|
279
317
|
|
|
280
|
-
cmd = 'yolo val {} model="{}" imgsz={} batch={} data="{}" project="{}" name="{}"'
|
|
281
|
-
augment_string,model_filename,image_size_string,options.batch_size,
|
|
282
|
-
|
|
283
|
-
cmd += '
|
|
318
|
+
cmd = 'yolo val {} model="{}" imgsz={} batch={} data="{}" project="{}" name="{}" device="{}"'.\
|
|
319
|
+
format(augment_string,model_filename,image_size_string,options.batch_size,
|
|
320
|
+
yolo_dataset_file,yolo_results_folder,'yolo_results',options.device_string)
|
|
321
|
+
cmd += ' save_json exist_ok'
|
|
322
|
+
|
|
323
|
+
if (options.half_precision_enabled is not None):
|
|
324
|
+
if options.half_precision_enabled == 1:
|
|
325
|
+
cmd += ' --half=True'
|
|
326
|
+
else:
|
|
327
|
+
assert options.half_precision_enabled == 0
|
|
328
|
+
cmd += ' --half=False'
|
|
329
|
+
|
|
330
|
+
# Sometimes useful for debugging
|
|
331
|
+
# cmd += ' save_conf save_txt'
|
|
284
332
|
|
|
285
333
|
else:
|
|
286
334
|
|
|
@@ -293,38 +341,84 @@ def run_inference_with_yolo_val(options):
|
|
|
293
341
|
|
|
294
342
|
if options.yolo_working_folder is not None:
|
|
295
343
|
current_dir = os.getcwd()
|
|
296
|
-
os.chdir(options.yolo_working_folder)
|
|
344
|
+
os.chdir(options.yolo_working_folder)
|
|
345
|
+
|
|
297
346
|
print('Running YOLO inference command:\n{}\n'.format(cmd))
|
|
298
347
|
|
|
299
348
|
if options.preview_yolo_command_only:
|
|
349
|
+
|
|
300
350
|
if options.remove_symlink_folder:
|
|
301
351
|
try:
|
|
352
|
+
print('Removing YOLO symlink folder {}'.format(symlink_folder))
|
|
302
353
|
shutil.rmtree(symlink_folder)
|
|
303
354
|
except Exception:
|
|
304
355
|
print('Warning: error removing symlink folder {}'.format(symlink_folder))
|
|
305
356
|
pass
|
|
306
357
|
if options.remove_yolo_results_folder:
|
|
307
358
|
try:
|
|
359
|
+
print('Removing YOLO results folder {}'.format(yolo_results_folder))
|
|
308
360
|
shutil.rmtree(yolo_results_folder)
|
|
309
361
|
except Exception:
|
|
310
362
|
print('Warning: error removing YOLO results folder {}'.format(yolo_results_folder))
|
|
311
363
|
pass
|
|
312
364
|
|
|
313
365
|
sys.exit()
|
|
314
|
-
|
|
315
|
-
execution_result = process_utils.execute_and_print(cmd)
|
|
366
|
+
|
|
367
|
+
execution_result = process_utils.execute_and_print(cmd,encoding='utf-8',verbose=True)
|
|
316
368
|
assert execution_result['status'] == 0, 'Error running {}'.format(options.model_type)
|
|
317
369
|
yolo_console_output = execution_result['output']
|
|
370
|
+
|
|
371
|
+
if options.save_yolo_debug_output:
|
|
372
|
+
with open(os.path.join(yolo_results_folder,'yolo_console_output.txt'),'w') as f:
|
|
373
|
+
for s in yolo_console_output:
|
|
374
|
+
f.write(s + '\n')
|
|
375
|
+
with open(os.path.join(yolo_results_folder,'image_id_to_file.json'),'w') as f:
|
|
376
|
+
json.dump(image_id_to_file,f,indent=1)
|
|
377
|
+
with open(os.path.join(yolo_results_folder,'image_id_to_error.json'),'w') as f:
|
|
378
|
+
json.dump(image_id_to_error,f,indent=1)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# YOLO console output contains lots of ANSI escape codes, remove them for easier parsing
|
|
382
|
+
yolo_console_output = [string_utils.remove_ansi_codes(s) for s in yolo_console_output]
|
|
318
383
|
|
|
384
|
+
# Find errors that occrred during the initial corruption check; these will not be included in the
|
|
385
|
+
# output. Errors that occur during inference will be handled separately.
|
|
319
386
|
yolo_read_failures = []
|
|
387
|
+
|
|
320
388
|
for line in yolo_console_output:
|
|
389
|
+
# Lines look like:
|
|
390
|
+
#
|
|
391
|
+
# For ultralytics val:
|
|
392
|
+
#
|
|
393
|
+
# val: WARNING ⚠️ /a/b/c/d.jpg: ignoring corrupt image/label: [Errno 13] Permission denied: '/a/b/c/d.jpg'
|
|
394
|
+
# line = "val: WARNING ⚠️ /a/b/c/d.jpg: ignoring corrupt image/label: [Errno 13] Permission denied: '/a/b/c/d.jpg'"
|
|
395
|
+
#
|
|
396
|
+
# For yolov5 val.py:
|
|
397
|
+
#
|
|
398
|
+
# test: WARNING: a/b/c/d.jpg: ignoring corrupt image/label: cannot identify image file '/a/b/c/d.jpg'
|
|
399
|
+
# line = "test: WARNING: a/b/c/d.jpg: ignoring corrupt image/label: cannot identify image file '/a/b/c/d.jpg'"
|
|
321
400
|
if 'cannot identify image file' in line:
|
|
322
401
|
tokens = line.split('cannot identify image file')
|
|
323
402
|
image_name = tokens[-1].strip()
|
|
324
403
|
assert image_name[0] == "'" and image_name [-1] == "'"
|
|
325
404
|
image_name = image_name[1:-1]
|
|
326
405
|
yolo_read_failures.append(image_name)
|
|
327
|
-
|
|
406
|
+
elif 'ignoring corrupt image/label' in line:
|
|
407
|
+
assert 'WARNING' in line
|
|
408
|
+
if '⚠️' in line:
|
|
409
|
+
assert line.startswith('val'), \
|
|
410
|
+
'Unrecognized line in YOLO output: {}'.format(line)
|
|
411
|
+
tokens = line.split('ignoring corrupt image/label')
|
|
412
|
+
image_name = tokens[0].split('⚠️')[-1].strip()
|
|
413
|
+
else:
|
|
414
|
+
assert line.startswith('test'), \
|
|
415
|
+
'Unrecognized line in YOLO output: {}'.format(line)
|
|
416
|
+
tokens = line.split('ignoring corrupt image/label')
|
|
417
|
+
image_name = tokens[0].split('WARNING:')[-1].strip()
|
|
418
|
+
assert image_name.endswith(':')
|
|
419
|
+
image_name = image_name[0:-1]
|
|
420
|
+
yolo_read_failures.append(image_name)
|
|
421
|
+
|
|
328
422
|
# image_file = yolo_read_failures[0]
|
|
329
423
|
for image_file in yolo_read_failures:
|
|
330
424
|
image_id = os.path.splitext(os.path.basename(image_file))[0]
|
|
@@ -338,7 +432,7 @@ def run_inference_with_yolo_val(options):
|
|
|
338
432
|
|
|
339
433
|
##%% Convert results to MD format
|
|
340
434
|
|
|
341
|
-
json_files = glob.glob(yolo_results_folder+ '/yolo_results/*.json')
|
|
435
|
+
json_files = glob.glob(yolo_results_folder + '/yolo_results/*.json')
|
|
342
436
|
assert len(json_files) == 1
|
|
343
437
|
yolo_json_file = json_files[0]
|
|
344
438
|
|
|
@@ -390,7 +484,7 @@ def run_inference_with_yolo_val(options):
|
|
|
390
484
|
|
|
391
485
|
#%% Command-line driver
|
|
392
486
|
|
|
393
|
-
import argparse
|
|
487
|
+
import argparse
|
|
394
488
|
from md_utils.ct_utils import args_to_object
|
|
395
489
|
|
|
396
490
|
def main():
|
|
@@ -422,9 +516,12 @@ def main():
|
|
|
422
516
|
parser.add_argument(
|
|
423
517
|
'--batch_size', default=options.batch_size, type=int,
|
|
424
518
|
help='inference batch size (default {})'.format(options.batch_size))
|
|
519
|
+
parser.add_argument(
|
|
520
|
+
'--half_precision_enabled', default=None, type=int,
|
|
521
|
+
help='use half-precision-inference (1 or 0) (default is the underlying model\'s default, probably half for YOLOv8 and full for YOLOv8')
|
|
425
522
|
parser.add_argument(
|
|
426
523
|
'--device_string', default=options.device_string, type=str,
|
|
427
|
-
help='CUDA device specifier,
|
|
524
|
+
help='CUDA device specifier, typically "0" or "1" for CUDA devices, "mps" for M1/M2 devices, or "cpu" (default {})'.format(options.device_string))
|
|
428
525
|
parser.add_argument(
|
|
429
526
|
'--overwrite_handling', default=options.overwrite_handling, type=str,
|
|
430
527
|
help='action to take if the output file exists (skip, error, overwrite) (default {})'.format(
|
|
@@ -435,7 +532,7 @@ def main():
|
|
|
435
532
|
'(otherwise defaults to MD categories)')
|
|
436
533
|
parser.add_argument(
|
|
437
534
|
'--model_type', default=options.model_type, type=str,
|
|
438
|
-
help='Model type (yolov5 or yolov8) (default {})'.format(options.model_type))
|
|
535
|
+
help='Model type ("yolov5" or "ultralytics" ("yolov8" behaves the same as "ultralytics")) (default {})'.format(options.model_type))
|
|
439
536
|
|
|
440
537
|
parser.add_argument(
|
|
441
538
|
'--symlink_folder', type=str,
|
|
@@ -452,6 +549,9 @@ def main():
|
|
|
452
549
|
parser.add_argument(
|
|
453
550
|
'--no_remove_yolo_results_folder', action='store_true',
|
|
454
551
|
help='don\'t remove the temporary folder full of YOLO intermediate files')
|
|
552
|
+
parser.add_argument(
|
|
553
|
+
'--save_yolo_debug_output', action='store_true',
|
|
554
|
+
help='write yolo console output to a text file in the results folder, along with additional debug files')
|
|
455
555
|
|
|
456
556
|
parser.add_argument(
|
|
457
557
|
'--preview_yolo_command_only', action='store_true',
|
|
@@ -474,14 +574,15 @@ def main():
|
|
|
474
574
|
|
|
475
575
|
# If the caller hasn't specified an image size, choose one based on whether augmentation
|
|
476
576
|
# is enabled.
|
|
477
|
-
if args.image_size is None:
|
|
478
|
-
assert
|
|
479
|
-
|
|
577
|
+
if args.image_size is None:
|
|
578
|
+
assert args.augment_enabled in (0,1), \
|
|
579
|
+
'Illegal augment_enabled value {}'.format(args.augment_enabled)
|
|
580
|
+
if args.augment_enabled == 1:
|
|
480
581
|
args.image_size = default_image_size_with_augmentation
|
|
481
582
|
else:
|
|
482
583
|
args.image_size = default_image_size_with_no_augmentation
|
|
483
584
|
augment_enabled_string = 'enabled'
|
|
484
|
-
if not
|
|
585
|
+
if not args.augment_enabled:
|
|
485
586
|
augment_enabled_string = 'disabled'
|
|
486
587
|
print('Augmentation is {}, using default image size {}'.format(
|
|
487
588
|
augment_enabled_string,args.image_size))
|
detection/run_tiled_inference.py
CHANGED
|
@@ -29,6 +29,7 @@ from tqdm import tqdm
|
|
|
29
29
|
|
|
30
30
|
from detection.run_inference_with_yolov5_val import YoloInferenceOptions,run_inference_with_yolo_val
|
|
31
31
|
from detection.run_detector_batch import load_and_run_detector_batch,write_results_to_file
|
|
32
|
+
from detection.run_detector import try_download_known_detector
|
|
32
33
|
|
|
33
34
|
import torch
|
|
34
35
|
from torchvision import ops
|
|
@@ -234,7 +235,7 @@ def in_place_nms(md_results, iou_thres=0.45, verbose=True):
|
|
|
234
235
|
# i_image = 18; im = md_results['images'][i_image]
|
|
235
236
|
for i_image,im in tqdm(enumerate(md_results['images']),total=len(md_results['images'])):
|
|
236
237
|
|
|
237
|
-
if len(im['detections']) == 0:
|
|
238
|
+
if (im['detections'] is None) or (len(im['detections']) == 0):
|
|
238
239
|
continue
|
|
239
240
|
|
|
240
241
|
boxes = []
|
|
@@ -282,40 +283,52 @@ def in_place_nms(md_results, iou_thres=0.45, verbose=True):
|
|
|
282
283
|
|
|
283
284
|
def _extract_tiles_for_image(fn_relative,image_folder,tiling_folder,patch_size,patch_stride,overwrite):
|
|
284
285
|
"""
|
|
285
|
-
|
|
286
|
+
Private function to extract tiles for a single image.
|
|
286
287
|
|
|
287
|
-
|
|
288
|
-
|
|
288
|
+
Returns a dict with fields 'patches' (see extract_patch_from_image) and 'image_fn'.
|
|
289
|
+
|
|
290
|
+
If there is an error, 'patches' will be None and the 'error' field will contain
|
|
291
|
+
failure details. In that case, some tiles may still be generated.
|
|
289
292
|
"""
|
|
290
293
|
|
|
291
294
|
fn_abs = os.path.join(image_folder,fn_relative)
|
|
295
|
+
error = None
|
|
296
|
+
patches = []
|
|
292
297
|
|
|
293
298
|
image_name = path_utils.clean_filename(fn_relative,char_limit=None,force_lower=True)
|
|
294
299
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
300
|
+
try:
|
|
301
|
+
|
|
302
|
+
# Open the image
|
|
303
|
+
im = vis_utils.open_image(fn_abs)
|
|
304
|
+
image_size = [im.width,im.height]
|
|
305
|
+
|
|
306
|
+
# Generate patch boundaries (a list of [x,y] starting points)
|
|
307
|
+
patch_boundaries = get_patch_boundaries(image_size,patch_size,patch_stride)
|
|
308
|
+
|
|
309
|
+
# Extract patches
|
|
310
|
+
#
|
|
311
|
+
# patch_xy = patch_boundaries[0]
|
|
312
|
+
for patch_xy in patch_boundaries:
|
|
298
313
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
patches = []
|
|
306
|
-
|
|
307
|
-
for patch_xy in patch_boundaries:
|
|
314
|
+
patch_info = extract_patch_from_image(im,patch_xy,patch_size,
|
|
315
|
+
patch_folder=tiling_folder,
|
|
316
|
+
image_name=image_name,
|
|
317
|
+
overwrite=overwrite)
|
|
318
|
+
patch_info['source_fn'] = fn_relative
|
|
319
|
+
patches.append(patch_info)
|
|
308
320
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
321
|
+
except Exception as e:
|
|
322
|
+
|
|
323
|
+
s = 'Patch generation error for {}: \n{}'.format(fn_relative,str(e))
|
|
324
|
+
print(s)
|
|
325
|
+
# patches = None
|
|
326
|
+
error = s
|
|
315
327
|
|
|
316
328
|
image_patch_info = {}
|
|
317
329
|
image_patch_info['patches'] = patches
|
|
318
330
|
image_patch_info['image_fn'] = fn_relative
|
|
331
|
+
image_patch_info['error'] = error
|
|
319
332
|
|
|
320
333
|
return image_patch_info
|
|
321
334
|
|
|
@@ -327,7 +340,8 @@ def run_tiled_inference(model_file, image_folder, tiling_folder, output_file,
|
|
|
327
340
|
checkpoint_path=None, checkpoint_frequency=-1, remove_tiles=False,
|
|
328
341
|
yolo_inference_options=None,
|
|
329
342
|
n_patch_extraction_workers=default_n_patch_extraction_workers,
|
|
330
|
-
overwrite_tiles=True
|
|
343
|
+
overwrite_tiles=True,
|
|
344
|
+
image_list=None):
|
|
331
345
|
"""
|
|
332
346
|
Run inference using [model_file] on the images in [image_folder], fist splitting each image up
|
|
333
347
|
into tiles of size [tile_size_x] x [tile_size_y], writing those tiles to [tiling_folder],
|
|
@@ -337,7 +351,8 @@ def run_tiled_inference(model_file, image_folder, tiling_folder, output_file,
|
|
|
337
351
|
[tiling_folder] can be any folder, but this function reserves the right to do whatever it wants
|
|
338
352
|
within that folder, including deleting everything, so it's best if it's a new folder.
|
|
339
353
|
Conceptually this folder is temporary, it's just helpful in this case to not actually
|
|
340
|
-
use the system temp folder, because the tile cache may be very large,
|
|
354
|
+
use the system temp folder, because the tile cache may be very large, so the caller may
|
|
355
|
+
want it to be on a specific drive.
|
|
341
356
|
|
|
342
357
|
tile_overlap is the fraction of overlap between tiles.
|
|
343
358
|
|
|
@@ -346,25 +361,54 @@ def run_tiled_inference(model_file, image_folder, tiling_folder, output_file,
|
|
|
346
361
|
if yolo_inference_options is supplied, it should be an instance of YoloInferenceOptions; in
|
|
347
362
|
this case the model will be run with run_inference_with_yolov5_val. This is typically used to
|
|
348
363
|
run the model with test-time augmentation.
|
|
349
|
-
"""
|
|
364
|
+
"""
|
|
350
365
|
|
|
351
366
|
##%% Validate arguments
|
|
352
367
|
|
|
353
368
|
assert tile_overlap < 1 and tile_overlap >= 0, \
|
|
354
369
|
'Illegal tile overlap value {}'.format(tile_overlap)
|
|
355
370
|
|
|
371
|
+
if tile_size_x == -1:
|
|
372
|
+
tile_size_x = default_tile_size[0]
|
|
373
|
+
if tile_size_y == -1:
|
|
374
|
+
tile_size_y = default_tile_size[1]
|
|
375
|
+
|
|
356
376
|
patch_size = [tile_size_x,tile_size_y]
|
|
357
377
|
patch_stride = (round(patch_size[0]*(1.0-tile_overlap)),
|
|
358
378
|
round(patch_size[1]*(1.0-tile_overlap)))
|
|
359
379
|
|
|
360
380
|
os.makedirs(tiling_folder,exist_ok=True)
|
|
361
381
|
|
|
362
|
-
|
|
363
382
|
##%% List files
|
|
364
383
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
384
|
+
if image_list is None:
|
|
385
|
+
|
|
386
|
+
print('Enumerating images in {}'.format(image_folder))
|
|
387
|
+
image_files_relative = path_utils.find_images(image_folder, recursive=True, return_relative_paths=True)
|
|
388
|
+
assert len(image_files_relative) > 0, 'No images found in folder {}'.format(image_folder)
|
|
389
|
+
|
|
390
|
+
else:
|
|
391
|
+
|
|
392
|
+
print('Loading image list from {}'.format(image_list))
|
|
393
|
+
with open(image_list,'r') as f:
|
|
394
|
+
image_files_relative = json.load(f)
|
|
395
|
+
n_absolute_paths = 0
|
|
396
|
+
for i_fn,fn in enumerate(image_files_relative):
|
|
397
|
+
if os.path.isabs(fn):
|
|
398
|
+
n_absolute_paths += 1
|
|
399
|
+
try:
|
|
400
|
+
fn_relative = os.path.relpath(fn,image_folder)
|
|
401
|
+
except ValueError:
|
|
402
|
+
'Illegal absolute path supplied to run_tiled_inference, {} is outside of {}'.format(
|
|
403
|
+
fn,image_folder)
|
|
404
|
+
raise
|
|
405
|
+
assert not fn_relative.startswith('..'), \
|
|
406
|
+
'Illegal absolute path supplied to run_tiled_inference, {} is outside of {}'.format(
|
|
407
|
+
fn,image_folder)
|
|
408
|
+
image_files_relative[i_fn] = fn_relative
|
|
409
|
+
if (n_absolute_paths != 0) and (n_absolute_paths != len(image_files_relative)):
|
|
410
|
+
raise ValueError('Illegal file list: converted {} of {} paths to relative'.format(
|
|
411
|
+
n_absolute_paths,len(image_files_relative)))
|
|
368
412
|
|
|
369
413
|
##%% Generate tiles
|
|
370
414
|
|
|
@@ -414,7 +458,7 @@ def run_tiled_inference(model_file, image_folder, tiling_folder, output_file,
|
|
|
414
458
|
image_files_relative),total=len(image_files_relative)))
|
|
415
459
|
|
|
416
460
|
# ...for each image
|
|
417
|
-
|
|
461
|
+
|
|
418
462
|
# Write tile information to file; this is just a debugging convenience
|
|
419
463
|
folder_name = path_utils.clean_filename(image_folder,force_lower=True)
|
|
420
464
|
if folder_name.startswith('_'):
|
|
@@ -424,9 +468,16 @@ def run_tiled_inference(model_file, image_folder, tiling_folder, output_file,
|
|
|
424
468
|
with open(tile_cache_file,'w') as f:
|
|
425
469
|
json.dump(all_image_patch_info,f,indent=1)
|
|
426
470
|
|
|
471
|
+
# Keep track of patches that failed
|
|
472
|
+
images_with_patch_errors = {}
|
|
473
|
+
for patch_info in all_image_patch_info:
|
|
474
|
+
if patch_info['error'] is not None:
|
|
475
|
+
images_with_patch_errors[patch_info['image_fn']] = patch_info
|
|
476
|
+
|
|
427
477
|
|
|
428
478
|
##%% Run inference on tiles
|
|
429
479
|
|
|
480
|
+
# When running with run_inference_with_yolov5_val, we'll pass the folder
|
|
430
481
|
if yolo_inference_options is not None:
|
|
431
482
|
|
|
432
483
|
patch_level_output_file = os.path.join(tiling_folder,folder_name + '_patch_level_results.json')
|
|
@@ -444,11 +495,16 @@ def run_tiled_inference(model_file, image_folder, tiling_folder, output_file,
|
|
|
444
495
|
run_inference_with_yolo_val(yolo_inference_options)
|
|
445
496
|
with open(patch_level_output_file,'r') as f:
|
|
446
497
|
patch_level_results = json.load(f)
|
|
447
|
-
|
|
498
|
+
|
|
499
|
+
# For standard inference, we'll pass a list of files
|
|
448
500
|
else:
|
|
449
501
|
|
|
450
502
|
patch_file_names = []
|
|
451
503
|
for im in all_image_patch_info:
|
|
504
|
+
# If there was a patch generation error, don't run inference
|
|
505
|
+
if patch_info['error'] is not None:
|
|
506
|
+
assert im['image_fn'] in images_with_patch_errors
|
|
507
|
+
continue
|
|
452
508
|
for patch in im['patches']:
|
|
453
509
|
patch_file_names.append(patch['patch_fn'])
|
|
454
510
|
|
|
@@ -481,18 +537,44 @@ def run_tiled_inference(model_file, image_folder, tiling_folder, output_file,
|
|
|
481
537
|
image_fn_relative_to_patch_info = { x['image_fn']:x for x in all_image_patch_info }
|
|
482
538
|
|
|
483
539
|
# i_image = 0; image_fn_relative = image_files_relative[i_image]
|
|
484
|
-
for i_image,image_fn_relative in tqdm(enumerate(image_files_relative),
|
|
540
|
+
for i_image,image_fn_relative in tqdm(enumerate(image_files_relative),
|
|
541
|
+
total=len(image_files_relative)):
|
|
485
542
|
|
|
486
543
|
image_fn_abs = os.path.join(image_folder,image_fn_relative)
|
|
487
544
|
assert os.path.isfile(image_fn_abs)
|
|
488
545
|
|
|
489
546
|
output_im = {}
|
|
490
547
|
output_im['file'] = image_fn_relative
|
|
491
|
-
|
|
548
|
+
|
|
549
|
+
# If we had a patch generation error
|
|
550
|
+
if image_fn_relative in images_with_patch_errors:
|
|
492
551
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
552
|
+
patch_info = image_fn_relative_to_patch_info[image_fn_relative]
|
|
553
|
+
assert patch_info['error'] is not None
|
|
554
|
+
|
|
555
|
+
output_im['detections'] = None
|
|
556
|
+
output_im['failure'] = 'Patch generation error'
|
|
557
|
+
output_im['failure_details'] = patch_info['error']
|
|
558
|
+
image_level_results['images'].append(output_im)
|
|
559
|
+
continue
|
|
560
|
+
|
|
561
|
+
try:
|
|
562
|
+
pil_im = vis_utils.open_image(image_fn_abs)
|
|
563
|
+
image_w = pil_im.size[0]
|
|
564
|
+
image_h = pil_im.size[1]
|
|
565
|
+
|
|
566
|
+
# This would be a very unusual situation; we're reading back an image here that we already
|
|
567
|
+
# (successfully) read once during patch generation.
|
|
568
|
+
except Exception as e:
|
|
569
|
+
print('Warning: image read error after successful patch generation for {}:\n{}'.format(
|
|
570
|
+
image_fn_relative,str(e)))
|
|
571
|
+
output_im['detections'] = None
|
|
572
|
+
output_im['failure'] = 'Patch processing error'
|
|
573
|
+
output_im['failure_details'] = str(e)
|
|
574
|
+
image_level_results['images'].append(output_im)
|
|
575
|
+
continue
|
|
576
|
+
|
|
577
|
+
output_im['detections'] = []
|
|
496
578
|
|
|
497
579
|
image_patch_info = image_fn_relative_to_patch_info[image_fn_relative]
|
|
498
580
|
assert image_patch_info['patches'][0]['source_fn'] == image_fn_relative
|
|
@@ -520,6 +602,14 @@ def run_tiled_inference(model_file, image_folder, tiling_folder, output_file,
|
|
|
520
602
|
assert patch_w == patch_size[0]
|
|
521
603
|
assert patch_h == patch_size[1]
|
|
522
604
|
|
|
605
|
+
# If there was an inference failure on one patch, report the image
|
|
606
|
+
# as an inference failure
|
|
607
|
+
if 'detections' not in patch_results:
|
|
608
|
+
assert 'failure' in patch_results
|
|
609
|
+
output_im['detections'] = None
|
|
610
|
+
output_im['failure'] = patch_results['failure']
|
|
611
|
+
break
|
|
612
|
+
|
|
523
613
|
# det = patch_results['detections'][0]
|
|
524
614
|
for det in patch_results['detections']:
|
|
525
615
|
|
|
@@ -703,7 +793,7 @@ def main():
|
|
|
703
793
|
help='Path to detector model file (.pb or .pt)')
|
|
704
794
|
parser.add_argument(
|
|
705
795
|
'image_folder',
|
|
706
|
-
help='Folder containing images for inference (always recursive)')
|
|
796
|
+
help='Folder containing images for inference (always recursive, unless image_list is supplied)')
|
|
707
797
|
parser.add_argument(
|
|
708
798
|
'tiling_folder',
|
|
709
799
|
help='Temporary folder where tiles and intermediate results will be stored')
|
|
@@ -729,6 +819,16 @@ def main():
|
|
|
729
819
|
type=float,
|
|
730
820
|
default=default_patch_overlap,
|
|
731
821
|
help=('Overlap between tiles [0,1] (defaults to {})'.format(default_patch_overlap)))
|
|
822
|
+
parser.add_argument(
|
|
823
|
+
'--overwrite_handling',
|
|
824
|
+
type=str,
|
|
825
|
+
default='skip',
|
|
826
|
+
help=('behavior when the targt file exists (skip/overwrite/error) (default skip)'))
|
|
827
|
+
parser.add_argument(
|
|
828
|
+
'--image_list',
|
|
829
|
+
type=str,
|
|
830
|
+
default=None,
|
|
831
|
+
help=('a .json list of relative filenames (or absolute paths contained within image_folder) to include'))
|
|
732
832
|
|
|
733
833
|
if len(sys.argv[1:]) == 0:
|
|
734
834
|
parser.print_help()
|
|
@@ -736,19 +836,29 @@ def main():
|
|
|
736
836
|
|
|
737
837
|
args = parser.parse_args()
|
|
738
838
|
|
|
739
|
-
|
|
839
|
+
model_file = try_download_known_detector(args.model_file)
|
|
840
|
+
assert os.path.exists(model_file), \
|
|
740
841
|
'detector file {} does not exist'.format(args.model_file)
|
|
741
|
-
|
|
842
|
+
|
|
742
843
|
if os.path.exists(args.output_file):
|
|
743
|
-
|
|
744
|
-
args.output_file))
|
|
844
|
+
if args.overwrite_handling == 'skip':
|
|
845
|
+
print('Warning: output file {} exists, skipping'.format(args.output_file))
|
|
846
|
+
return
|
|
847
|
+
elif args.overwrite_handling == 'overwrite':
|
|
848
|
+
print('Warning: output file {} exists, overwriting'.format(args.output_file))
|
|
849
|
+
elif args.overwrite_handling == 'error':
|
|
850
|
+
raise ValueError('Output file {} exists'.format(args.output_file))
|
|
851
|
+
else:
|
|
852
|
+
raise ValueError('Unknown output handling method {}'.format(args.overwrite_handling))
|
|
853
|
+
|
|
745
854
|
|
|
746
855
|
remove_tiles = (not args.no_remove_tiles)
|
|
747
856
|
|
|
748
|
-
run_tiled_inference(
|
|
857
|
+
run_tiled_inference(model_file, args.image_folder, args.tiling_folder, args.output_file,
|
|
749
858
|
tile_size_x=args.tile_size_x, tile_size_y=args.tile_size_y,
|
|
750
859
|
tile_overlap=args.tile_overlap,
|
|
751
|
-
remove_tiles=remove_tiles
|
|
860
|
+
remove_tiles=remove_tiles,
|
|
861
|
+
image_list=args.image_list)
|
|
752
862
|
|
|
753
863
|
if __name__ == '__main__':
|
|
754
864
|
main()
|
detection/video_utils.py
CHANGED
|
@@ -24,6 +24,8 @@ from md_utils import path_utils
|
|
|
24
24
|
|
|
25
25
|
from md_visualization import visualization_utils as vis_utils
|
|
26
26
|
|
|
27
|
+
default_fourcc = 'h264'
|
|
28
|
+
|
|
27
29
|
|
|
28
30
|
#%% Path utilities
|
|
29
31
|
|
|
@@ -76,7 +78,7 @@ def find_videos(dirname: str, recursive: bool = False,
|
|
|
76
78
|
|
|
77
79
|
# http://tsaith.github.io/combine-images-into-a-video-with-python-3-and-opencv-3.html
|
|
78
80
|
|
|
79
|
-
def frames_to_video(images, Fs, output_file_name, codec_spec=
|
|
81
|
+
def frames_to_video(images, Fs, output_file_name, codec_spec=default_fourcc):
|
|
80
82
|
"""
|
|
81
83
|
Given a list of image files and a sample rate, concatenate those images into
|
|
82
84
|
a video and write to [output_file_name].
|