megadetector 5.0.12__py3-none-any.whl → 5.0.13__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.
- megadetector/api/batch_processing/api_core/server.py +1 -1
- megadetector/api/batch_processing/api_core/server_api_config.py +0 -1
- megadetector/api/batch_processing/api_core/server_job_status_table.py +0 -3
- megadetector/api/batch_processing/api_core/server_utils.py +0 -4
- megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +0 -1
- megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +0 -3
- megadetector/classification/efficientnet/utils.py +0 -3
- megadetector/data_management/camtrap_dp_to_coco.py +0 -2
- megadetector/data_management/cct_json_utils.py +15 -6
- megadetector/data_management/coco_to_labelme.py +12 -1
- megadetector/data_management/databases/integrity_check_json_db.py +43 -27
- megadetector/data_management/importers/cacophony-thermal-importer.py +1 -4
- megadetector/data_management/ocr_tools.py +0 -4
- megadetector/data_management/read_exif.py +171 -43
- megadetector/data_management/rename_images.py +187 -0
- megadetector/data_management/wi_download_csv_to_coco.py +3 -2
- megadetector/data_management/yolo_output_to_md_output.py +7 -2
- megadetector/detection/process_video.py +360 -216
- megadetector/detection/pytorch_detector.py +17 -3
- megadetector/detection/run_inference_with_yolov5_val.py +527 -357
- megadetector/detection/tf_detector.py +3 -0
- megadetector/detection/video_utils.py +122 -30
- megadetector/postprocessing/categorize_detections_by_size.py +16 -14
- megadetector/postprocessing/classification_postprocessing.py +716 -0
- megadetector/postprocessing/compare_batch_results.py +101 -93
- megadetector/postprocessing/merge_detections.py +18 -7
- megadetector/postprocessing/postprocess_batch_results.py +133 -127
- megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +236 -232
- megadetector/postprocessing/subset_json_detector_output.py +66 -62
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +0 -2
- megadetector/utils/ct_utils.py +5 -4
- megadetector/utils/md_tests.py +311 -115
- megadetector/utils/path_utils.py +1 -0
- megadetector/utils/process_utils.py +6 -3
- megadetector/visualization/visualize_db.py +79 -77
- {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/LICENSE +0 -0
- {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/METADATA +2 -2
- {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/RECORD +40 -38
- {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/top_level.txt +0 -0
- {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/WHEEL +0 -0
megadetector/utils/md_tests.py
CHANGED
|
@@ -25,63 +25,79 @@ import urllib.request
|
|
|
25
25
|
import zipfile
|
|
26
26
|
import subprocess
|
|
27
27
|
import argparse
|
|
28
|
+
import inspect
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
#%% Classes
|
|
31
32
|
|
|
32
33
|
class MDTestOptions:
|
|
33
34
|
"""
|
|
34
|
-
Options controlling test behavior
|
|
35
|
+
Options controlling test behavior
|
|
35
36
|
"""
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
def __init__(self):
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
disable_gpu = False
|
|
40
|
+
## Required ##
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
42
|
+
#: Force CPU execution
|
|
43
|
+
self.disable_gpu = False
|
|
44
|
+
|
|
45
|
+
#: If GPU execution is requested, but a GPU is not available, should we error?
|
|
46
|
+
self.cpu_execution_is_error = False
|
|
47
|
+
|
|
48
|
+
#: Skip tests related to video processing
|
|
49
|
+
self.skip_video_tests = False
|
|
50
|
+
|
|
51
|
+
#: Skip tests launched via Python functions (as opposed to CLIs)
|
|
52
|
+
self.skip_python_tests = False
|
|
53
|
+
|
|
54
|
+
#: Skip CLI tests
|
|
55
|
+
self.skip_cli_tests = False
|
|
56
|
+
|
|
57
|
+
#: Force a specific folder for temporary input/output
|
|
58
|
+
self.scratch_dir = None
|
|
59
|
+
|
|
60
|
+
#: Where does the test data live?
|
|
61
|
+
self.test_data_url = 'https://lila.science/public/md-test-package.zip'
|
|
62
|
+
|
|
63
|
+
#: Download test data even if it appears to have already been downloaded
|
|
64
|
+
self.force_data_download = False
|
|
65
|
+
|
|
66
|
+
#: Unzip test data even if it appears to have already been unzipped
|
|
67
|
+
self.force_data_unzip = False
|
|
68
|
+
|
|
69
|
+
#: By default, any unexpected behavior is an error; this forces most errors to
|
|
70
|
+
#: be treated as warnings.
|
|
71
|
+
self.warning_mode = False
|
|
72
|
+
|
|
73
|
+
#: How much deviation from the expected detection coordinates should we allow before
|
|
74
|
+
#: a disrepancy becomes an error?
|
|
75
|
+
self.max_coord_error = 0.001
|
|
76
|
+
|
|
77
|
+
#: How much deviation from the expected confidence values should we allow before
|
|
78
|
+
#: a disrepancy becomes an error?
|
|
79
|
+
self.max_conf_error = 0.005
|
|
80
|
+
|
|
81
|
+
#: Current working directory when running CLI tests
|
|
82
|
+
self.cli_working_dir = None
|
|
83
|
+
|
|
84
|
+
#: YOLOv5 installation, only relevant if we're testing run_inference_with_yolov5_val.
|
|
85
|
+
#:
|
|
86
|
+
#: If this is None, we'll skip that test.
|
|
87
|
+
self.yolo_working_dir = None
|
|
88
|
+
|
|
89
|
+
#: fourcc code to use for video tests that involve rendering video
|
|
90
|
+
self.video_fourcc = 'mp4v'
|
|
91
|
+
|
|
92
|
+
#: Default model to use for testing (filename, URL, or well-known model string)
|
|
93
|
+
self.default_model = 'MDV5A'
|
|
94
|
+
|
|
95
|
+
#: For comparison tests, use a model that produces slightly different output
|
|
96
|
+
self.alt_model = 'MDV5B'
|
|
97
|
+
|
|
98
|
+
#: PYTHONPATH to set for CLI tests; if None, inherits from the parent process. Only
|
|
99
|
+
#: impacts the called functions, not the parent process.
|
|
100
|
+
self.cli_test_pythonpath = None
|
|
85
101
|
|
|
86
102
|
# ...class MDTestOptions()
|
|
87
103
|
|
|
@@ -124,8 +140,9 @@ def get_expected_results_filename(gpu_is_available):
|
|
|
124
140
|
import torch
|
|
125
141
|
m1_inference = torch.backends.mps.is_built and torch.backends.mps.is_available()
|
|
126
142
|
if m1_inference:
|
|
143
|
+
print('I appear to be running on M1/M2 hardware')
|
|
127
144
|
hw_string = 'cpu'
|
|
128
|
-
|
|
145
|
+
pt_string = 'pt1.10.1'
|
|
129
146
|
except Exception:
|
|
130
147
|
pass
|
|
131
148
|
|
|
@@ -215,6 +232,8 @@ def download_test_data(options=None):
|
|
|
215
232
|
options.test_images = [fn for fn in test_files if os.path.splitext(fn.lower())[1] in ('.jpg','.jpeg','.png')]
|
|
216
233
|
options.test_videos = [fn for fn in test_files if os.path.splitext(fn.lower())[1] in ('.mp4','.avi')]
|
|
217
234
|
options.test_videos = [fn for fn in options.test_videos if 'rendered' not in fn]
|
|
235
|
+
options.test_videos = [fn for fn in options.test_videos if \
|
|
236
|
+
os.path.isfile(os.path.join(scratch_dir,fn))]
|
|
218
237
|
|
|
219
238
|
print('Finished unzipping and enumerating test data')
|
|
220
239
|
|
|
@@ -257,7 +276,81 @@ def is_gpu_available(verbose=True):
|
|
|
257
276
|
print('No GPU available')
|
|
258
277
|
|
|
259
278
|
return gpu_available
|
|
279
|
+
|
|
280
|
+
# ...def is_gpu_available(...)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def output_files_are_identical(fn1,fn2,verbose=False):
|
|
284
|
+
"""
|
|
285
|
+
Checks whether two MD-formatted output files are identical other than file sorting.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
fn1 (str): the first filename to compare
|
|
289
|
+
fn2 (str): the second filename to compare
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
bool: whether [fn1] and [fn2] are identical other than file sorting.
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
if verbose:
|
|
296
|
+
print('Comparing {} to {}'.format(fn1,fn2))
|
|
297
|
+
|
|
298
|
+
with open(fn1,'r') as f:
|
|
299
|
+
fn1_results = json.load(f)
|
|
300
|
+
fn1_results['images'] = \
|
|
301
|
+
sorted(fn1_results['images'], key=lambda d: d['file'])
|
|
302
|
+
|
|
303
|
+
with open(fn2,'r') as f:
|
|
304
|
+
fn2_results = json.load(f)
|
|
305
|
+
fn2_results['images'] = \
|
|
306
|
+
sorted(fn2_results['images'], key=lambda d: d['file'])
|
|
307
|
+
|
|
308
|
+
if len(fn1_results['images']) != len(fn1_results['images']):
|
|
309
|
+
if verbose:
|
|
310
|
+
print('{} images in {}, {} images in {}'.format(
|
|
311
|
+
len(fn1_results['images']),fn1,
|
|
312
|
+
len(fn2_results['images']),fn2))
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
for i_image,fn1_image in enumerate(fn1_results['images']):
|
|
260
316
|
|
|
317
|
+
fn2_image = fn2_results['images'][i_image]
|
|
318
|
+
|
|
319
|
+
if fn1_image['file'] != fn2_image['file']:
|
|
320
|
+
if verbose:
|
|
321
|
+
print('Filename difference: {} vs {} '.format(fn1_image['file'],fn1_image['file']))
|
|
322
|
+
return False
|
|
323
|
+
|
|
324
|
+
if fn1_image != fn2_image:
|
|
325
|
+
if verbose:
|
|
326
|
+
print('Image-level difference in image {}'.format(fn1_image['file']))
|
|
327
|
+
return False
|
|
328
|
+
|
|
329
|
+
return True
|
|
330
|
+
|
|
331
|
+
# ...def output_files_are_identical(...)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _args_to_object(args, obj):
|
|
335
|
+
"""
|
|
336
|
+
Copies all fields from a Namespace (typically the output from parse_args) to an
|
|
337
|
+
object. Skips fields starting with _. Does not check existence in the target
|
|
338
|
+
object.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
args (argparse.Namespace): the namespace to convert to an object
|
|
342
|
+
obj (object): object whose whose attributes will be updated
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
object: the modified object (modified in place, but also returned)
|
|
346
|
+
"""
|
|
347
|
+
|
|
348
|
+
for n, v in inspect.getmembers(args):
|
|
349
|
+
if not n.startswith('_'):
|
|
350
|
+
setattr(obj, n, v)
|
|
351
|
+
|
|
352
|
+
return obj
|
|
353
|
+
|
|
261
354
|
|
|
262
355
|
#%% CLI functions
|
|
263
356
|
|
|
@@ -289,7 +382,7 @@ def execute(cmd):
|
|
|
289
382
|
return return_code
|
|
290
383
|
|
|
291
384
|
|
|
292
|
-
def execute_and_print(cmd,print_output=True):
|
|
385
|
+
def execute_and_print(cmd,print_output=True,catch_exceptions=False):
|
|
293
386
|
"""
|
|
294
387
|
Runs [cmd] (a single string) in a shell, capturing (and optionally printing) output.
|
|
295
388
|
|
|
@@ -311,6 +404,8 @@ def execute_and_print(cmd,print_output=True):
|
|
|
311
404
|
print(s,end='',flush=True)
|
|
312
405
|
to_return['status'] = 0
|
|
313
406
|
except subprocess.CalledProcessError as cpe:
|
|
407
|
+
if not catch_exceptions:
|
|
408
|
+
raise
|
|
314
409
|
print('execute_and_print caught error: {}'.format(cpe.output))
|
|
315
410
|
to_return['status'] = cpe.returncode
|
|
316
411
|
to_return['output'] = output
|
|
@@ -339,9 +434,8 @@ def run_python_tests(options):
|
|
|
339
434
|
|
|
340
435
|
from megadetector.detection import run_detector
|
|
341
436
|
from megadetector.visualization import visualization_utils as vis_utils
|
|
342
|
-
model_file = 'MDV5A'
|
|
343
437
|
image_fn = os.path.join(options.scratch_dir,options.test_images[0])
|
|
344
|
-
model = run_detector.load_detector(
|
|
438
|
+
model = run_detector.load_detector(options.default_model)
|
|
345
439
|
pil_im = vis_utils.load_image(image_fn)
|
|
346
440
|
result = model.generate_detections_one_image(pil_im) # noqa
|
|
347
441
|
|
|
@@ -355,9 +449,9 @@ def run_python_tests(options):
|
|
|
355
449
|
assert os.path.isdir(image_folder), 'Test image folder {} is not available'.format(image_folder)
|
|
356
450
|
inference_output_file = os.path.join(options.scratch_dir,'folder_inference_output.json')
|
|
357
451
|
image_file_names = path_utils.find_images(image_folder,recursive=True)
|
|
358
|
-
results = load_and_run_detector_batch(
|
|
452
|
+
results = load_and_run_detector_batch(options.default_model, image_file_names, quiet=True)
|
|
359
453
|
_ = write_results_to_file(results,inference_output_file,
|
|
360
|
-
relative_path_base=image_folder,detector_file=
|
|
454
|
+
relative_path_base=image_folder,detector_file=options.default_model)
|
|
361
455
|
|
|
362
456
|
# Read results
|
|
363
457
|
with open(inference_output_file,'r') as f:
|
|
@@ -459,11 +553,11 @@ def run_python_tests(options):
|
|
|
459
553
|
assert os.path.isfile(postprocessing_results.output_html_file), \
|
|
460
554
|
'Postprocessing output file {} not found'.format(postprocessing_results.output_html_file)
|
|
461
555
|
|
|
462
|
-
|
|
556
|
+
|
|
463
557
|
## Partial RDE test
|
|
464
558
|
|
|
465
559
|
from megadetector.postprocessing.repeat_detection_elimination.repeat_detections_core import \
|
|
466
|
-
RepeatDetectionOptions,find_repeat_detections
|
|
560
|
+
RepeatDetectionOptions, find_repeat_detections
|
|
467
561
|
|
|
468
562
|
rde_options = RepeatDetectionOptions()
|
|
469
563
|
rde_options.occurrenceThreshold = 2
|
|
@@ -477,9 +571,69 @@ def run_python_tests(options):
|
|
|
477
571
|
'Could not find RDE output file {}'.format(rde_results.filterFile)
|
|
478
572
|
|
|
479
573
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
574
|
+
## Run inference on a folder (with YOLOv5 val script)
|
|
575
|
+
|
|
576
|
+
if options.yolo_working_dir is None:
|
|
577
|
+
|
|
578
|
+
print('Skipping YOLO val inference tests, no YOLO folder supplied')
|
|
579
|
+
|
|
580
|
+
else:
|
|
581
|
+
|
|
582
|
+
from megadetector.detection.run_inference_with_yolov5_val import \
|
|
583
|
+
YoloInferenceOptions, run_inference_with_yolo_val
|
|
584
|
+
|
|
585
|
+
inference_output_file_yolo_val = os.path.join(options.scratch_dir,'folder_inference_output_yolo_val.json')
|
|
586
|
+
|
|
587
|
+
yolo_inference_options = YoloInferenceOptions()
|
|
588
|
+
yolo_inference_options.input_folder = os.path.join(options.scratch_dir,'md-test-images')
|
|
589
|
+
yolo_inference_options.output_file = inference_output_file_yolo_val
|
|
590
|
+
yolo_inference_options.yolo_working_folder = options.yolo_working_dir
|
|
591
|
+
yolo_inference_options.model_filename = options.default_model
|
|
592
|
+
yolo_inference_options.augment = False
|
|
593
|
+
yolo_inference_options.overwrite_handling = 'overwrite'
|
|
594
|
+
|
|
595
|
+
run_inference_with_yolo_val(yolo_inference_options)
|
|
596
|
+
|
|
597
|
+
# Run again, without symlinks this time
|
|
598
|
+
|
|
599
|
+
from megadetector.utils.path_utils import insert_before_extension
|
|
600
|
+
inference_output_file_yolo_val_no_links = insert_before_extension(inference_output_file_yolo_val,
|
|
601
|
+
'no-links')
|
|
602
|
+
yolo_inference_options.output_file = inference_output_file_yolo_val_no_links
|
|
603
|
+
yolo_inference_options.use_symlinks = False
|
|
604
|
+
run_inference_with_yolo_val(yolo_inference_options)
|
|
605
|
+
|
|
606
|
+
# Run again, with chunked inference and symlinks
|
|
607
|
+
|
|
608
|
+
inference_output_file_yolo_val_checkpoints = insert_before_extension(inference_output_file_yolo_val,
|
|
609
|
+
'checkpoints')
|
|
610
|
+
yolo_inference_options.output_file = inference_output_file_yolo_val_checkpoints
|
|
611
|
+
yolo_inference_options.use_symlinks = True
|
|
612
|
+
yolo_inference_options.checkpoint_frequency = 5
|
|
613
|
+
run_inference_with_yolo_val(yolo_inference_options)
|
|
614
|
+
|
|
615
|
+
# Run again, with chunked inference and no symlinks
|
|
616
|
+
|
|
617
|
+
inference_output_file_yolo_val_checkpoints_no_links = \
|
|
618
|
+
insert_before_extension(inference_output_file_yolo_val,'checkpoints-no-links')
|
|
619
|
+
yolo_inference_options.output_file = inference_output_file_yolo_val_checkpoints_no_links
|
|
620
|
+
yolo_inference_options.use_symlinks = False
|
|
621
|
+
yolo_inference_options.checkpoint_frequency = 5
|
|
622
|
+
run_inference_with_yolo_val(yolo_inference_options)
|
|
623
|
+
|
|
624
|
+
fn1 = inference_output_file_yolo_val
|
|
625
|
+
|
|
626
|
+
output_files_to_compare = [
|
|
627
|
+
inference_output_file_yolo_val_no_links,
|
|
628
|
+
inference_output_file_yolo_val_checkpoints,
|
|
629
|
+
inference_output_file_yolo_val_checkpoints_no_links
|
|
630
|
+
]
|
|
631
|
+
|
|
632
|
+
for fn2 in output_files_to_compare:
|
|
633
|
+
assert output_files_are_identical(fn1, fn2, verbose=True)
|
|
634
|
+
|
|
635
|
+
# ...if we need to run the YOLO val inference tests
|
|
636
|
+
|
|
483
637
|
|
|
484
638
|
if not options.skip_video_tests:
|
|
485
639
|
|
|
@@ -488,7 +642,7 @@ def run_python_tests(options):
|
|
|
488
642
|
from megadetector.detection.process_video import ProcessVideoOptions, process_video
|
|
489
643
|
|
|
490
644
|
video_options = ProcessVideoOptions()
|
|
491
|
-
video_options.model_file =
|
|
645
|
+
video_options.model_file = options.default_model
|
|
492
646
|
video_options.input_video_file = os.path.join(options.scratch_dir,options.test_videos[0])
|
|
493
647
|
video_options.output_json_file = os.path.join(options.scratch_dir,'single_video_output.json')
|
|
494
648
|
video_options.output_video_file = os.path.join(options.scratch_dir,'video_scratch/rendered_video.mp4')
|
|
@@ -503,7 +657,7 @@ def run_python_tests(options):
|
|
|
503
657
|
# video_options.reuse_frames_if_available = False
|
|
504
658
|
video_options.recursive = True
|
|
505
659
|
video_options.verbose = False
|
|
506
|
-
video_options.fourcc =
|
|
660
|
+
video_options.fourcc = options.video_fourcc
|
|
507
661
|
# video_options.rendering_confidence_threshold = None
|
|
508
662
|
# video_options.json_confidence_threshold = 0.005
|
|
509
663
|
video_options.frame_sample = 5
|
|
@@ -524,7 +678,7 @@ def run_python_tests(options):
|
|
|
524
678
|
from megadetector.detection.process_video import ProcessVideoOptions, process_video_folder
|
|
525
679
|
|
|
526
680
|
video_options = ProcessVideoOptions()
|
|
527
|
-
video_options.model_file =
|
|
681
|
+
video_options.model_file = options.default_model
|
|
528
682
|
video_options.input_video_file = os.path.join(options.scratch_dir,
|
|
529
683
|
os.path.dirname(options.test_videos[0]))
|
|
530
684
|
video_options.output_json_file = os.path.join(options.scratch_dir,'video_folder_output.json')
|
|
@@ -539,8 +693,8 @@ def run_python_tests(options):
|
|
|
539
693
|
# video_options.reuse_results_if_available = False
|
|
540
694
|
# video_options.reuse_frames_if_available = False
|
|
541
695
|
video_options.recursive = True
|
|
542
|
-
video_options.verbose =
|
|
543
|
-
|
|
696
|
+
video_options.verbose = True
|
|
697
|
+
video_options.fourcc = options.video_fourcc
|
|
544
698
|
# video_options.rendering_confidence_threshold = None
|
|
545
699
|
# video_options.json_confidence_threshold = 0.005
|
|
546
700
|
video_options.frame_sample = 5
|
|
@@ -572,6 +726,13 @@ def run_cli_tests(options):
|
|
|
572
726
|
|
|
573
727
|
print('\n*** Starting CLI tests ***\n')
|
|
574
728
|
|
|
729
|
+
|
|
730
|
+
## Environment management
|
|
731
|
+
|
|
732
|
+
if options.cli_test_pythonpath is not None:
|
|
733
|
+
os.environ['PYTHONPATH'] = options.cli_test_pythonpath
|
|
734
|
+
|
|
735
|
+
|
|
575
736
|
## chdir if necessary
|
|
576
737
|
|
|
577
738
|
if options.cli_working_dir is not None:
|
|
@@ -585,15 +746,14 @@ def run_cli_tests(options):
|
|
|
585
746
|
|
|
586
747
|
## Run inference on an image
|
|
587
748
|
|
|
588
|
-
model_file = 'MDV5A'
|
|
589
749
|
image_fn = os.path.join(options.scratch_dir,options.test_images[0])
|
|
590
750
|
output_dir = os.path.join(options.scratch_dir,'single_image_test')
|
|
591
751
|
if options.cli_working_dir is None:
|
|
592
752
|
cmd = 'python -m megadetector.detection.run_detector'
|
|
593
753
|
else:
|
|
594
754
|
cmd = 'python megadetector/detection/run_detector.py'
|
|
595
|
-
cmd += ' {} --image_file {} --output_dir {}'.format(
|
|
596
|
-
|
|
755
|
+
cmd += ' "{}" --image_file "{}" --output_dir "{}"'.format(
|
|
756
|
+
options.default_model,image_fn,output_dir)
|
|
597
757
|
print('Running: {}'.format(cmd))
|
|
598
758
|
cmd_results = execute_and_print(cmd)
|
|
599
759
|
|
|
@@ -616,19 +776,28 @@ def run_cli_tests(options):
|
|
|
616
776
|
cmd = 'python -m megadetector.detection.run_detector_batch'
|
|
617
777
|
else:
|
|
618
778
|
cmd = 'python megadetector/detection/run_detector_batch.py'
|
|
619
|
-
cmd += ' {} {} {} --recursive'.format(
|
|
620
|
-
|
|
779
|
+
cmd += ' "{}" "{}" "{}" --recursive'.format(
|
|
780
|
+
options.default_model,image_folder,inference_output_file)
|
|
621
781
|
cmd += ' --output_relative_filenames --quiet --include_image_size'
|
|
622
782
|
cmd += ' --include_image_timestamp --include_exif_data'
|
|
623
783
|
print('Running: {}'.format(cmd))
|
|
624
784
|
cmd_results = execute_and_print(cmd)
|
|
625
785
|
|
|
626
|
-
# Make sure a coherent file got written out, but don't verify the results, leave that
|
|
627
|
-
# to the Python tests.
|
|
628
|
-
with open(inference_output_file,'r') as f:
|
|
629
|
-
results_from_file = json.load(f) # noqa
|
|
630
786
|
|
|
631
|
-
|
|
787
|
+
## Run again with checkpointing enabled, make sure the results are the same
|
|
788
|
+
|
|
789
|
+
cmd += ' --checkpoint_frequency 5'
|
|
790
|
+
from megadetector.utils.path_utils import insert_before_extension
|
|
791
|
+
inference_output_file_checkpoint = insert_before_extension(inference_output_file,'_checkpoint')
|
|
792
|
+
assert inference_output_file_checkpoint != inference_output_file
|
|
793
|
+
cmd = cmd.replace(inference_output_file,inference_output_file_checkpoint)
|
|
794
|
+
print('Running: {}'.format(cmd))
|
|
795
|
+
cmd_results = execute_and_print(cmd)
|
|
796
|
+
|
|
797
|
+
assert output_files_are_identical(fn1=inference_output_file,
|
|
798
|
+
fn2=inference_output_file_checkpoint,verbose=True)
|
|
799
|
+
|
|
800
|
+
|
|
632
801
|
## Postprocessing
|
|
633
802
|
|
|
634
803
|
postprocessing_output_dir = os.path.join(options.scratch_dir,'postprocessing_output_cli')
|
|
@@ -637,9 +806,9 @@ def run_cli_tests(options):
|
|
|
637
806
|
cmd = 'python -m megadetector.postprocessing.postprocess_batch_results'
|
|
638
807
|
else:
|
|
639
808
|
cmd = 'python megadetector/postprocessing/postprocess_batch_results.py'
|
|
640
|
-
cmd += ' {} {}'.format(
|
|
809
|
+
cmd += ' "{}" "{}"'.format(
|
|
641
810
|
inference_output_file,postprocessing_output_dir)
|
|
642
|
-
cmd += ' --image_base_dir {}'.format(image_folder)
|
|
811
|
+
cmd += ' --image_base_dir "{}"'.format(image_folder)
|
|
643
812
|
print('Running: {}'.format(cmd))
|
|
644
813
|
cmd_results = execute_and_print(cmd)
|
|
645
814
|
|
|
@@ -652,9 +821,9 @@ def run_cli_tests(options):
|
|
|
652
821
|
cmd = 'python -m megadetector.postprocessing.repeat_detection_elimination.find_repeat_detections'
|
|
653
822
|
else:
|
|
654
823
|
cmd = 'python megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py'
|
|
655
|
-
cmd += ' {}'.format(inference_output_file)
|
|
656
|
-
cmd += ' --imageBase {}'.format(image_folder)
|
|
657
|
-
cmd += ' --outputBase {}'.format(rde_output_dir)
|
|
824
|
+
cmd += ' "{}"'.format(inference_output_file)
|
|
825
|
+
cmd += ' --imageBase "{}"'.format(image_folder)
|
|
826
|
+
cmd += ' --outputBase "{}"'.format(rde_output_dir)
|
|
658
827
|
cmd += ' --occurrenceThreshold 1' # Use an absurd number here to make sure we get some suspicious detections
|
|
659
828
|
print('Running: {}'.format(cmd))
|
|
660
829
|
cmd_results = execute_and_print(cmd)
|
|
@@ -674,7 +843,7 @@ def run_cli_tests(options):
|
|
|
674
843
|
cmd = 'python -m megadetector.postprocessing.repeat_detection_elimination.remove_repeat_detections'
|
|
675
844
|
else:
|
|
676
845
|
cmd = 'python megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py'
|
|
677
|
-
cmd += ' {} {} {}'.format(inference_output_file,filtered_output_file,filtering_output_dir)
|
|
846
|
+
cmd += ' "{}" "{}" "{}"'.format(inference_output_file,filtered_output_file,filtering_output_dir)
|
|
678
847
|
print('Running: {}'.format(cmd))
|
|
679
848
|
cmd_results = execute_and_print(cmd)
|
|
680
849
|
|
|
@@ -691,8 +860,8 @@ def run_cli_tests(options):
|
|
|
691
860
|
cmd = 'python -m megadetector.detection.run_tiled_inference'
|
|
692
861
|
else:
|
|
693
862
|
cmd = 'python megadetector/detection/run_tiled_inference.py'
|
|
694
|
-
cmd += ' {} {} {} {}'.format(
|
|
695
|
-
|
|
863
|
+
cmd += ' "{}" "{}" "{}" "{}"'.format(
|
|
864
|
+
options.default_model,image_folder,tiling_folder,inference_output_file_tiled)
|
|
696
865
|
cmd += ' --overwrite_handling overwrite'
|
|
697
866
|
print('Running: {}'.format(cmd))
|
|
698
867
|
cmd_results = execute_and_print(cmd)
|
|
@@ -703,7 +872,7 @@ def run_cli_tests(options):
|
|
|
703
872
|
|
|
704
873
|
## Run inference on a folder (augmented)
|
|
705
874
|
|
|
706
|
-
if options.
|
|
875
|
+
if options.yolo_working_dir is None:
|
|
707
876
|
|
|
708
877
|
print('Bypassing YOLOv5 val tests, no yolo folder supplied')
|
|
709
878
|
|
|
@@ -717,59 +886,67 @@ def run_cli_tests(options):
|
|
|
717
886
|
cmd = 'python -m megadetector.detection.run_inference_with_yolov5_val'
|
|
718
887
|
else:
|
|
719
888
|
cmd = 'python megadetector/detection/run_inference_with_yolov5_val.py'
|
|
720
|
-
cmd += ' {} {} {}'.format(
|
|
721
|
-
|
|
722
|
-
cmd += ' --yolo_working_folder {}'.format(options.
|
|
723
|
-
cmd += ' --yolo_results_folder {}'.format(yolo_results_folder)
|
|
724
|
-
cmd += ' --symlink_folder {}'.format(yolo_symlink_folder)
|
|
889
|
+
cmd += ' "{}" "{}" "{}"'.format(
|
|
890
|
+
options.default_model,image_folder,inference_output_file_yolo_val)
|
|
891
|
+
cmd += ' --yolo_working_folder "{}"'.format(options.yolo_working_dir)
|
|
892
|
+
cmd += ' --yolo_results_folder "{}"'.format(yolo_results_folder)
|
|
893
|
+
cmd += ' --symlink_folder "{}"'.format(yolo_symlink_folder)
|
|
725
894
|
cmd += ' --augment_enabled 1'
|
|
726
895
|
# cmd += ' --no_use_symlinks'
|
|
727
896
|
cmd += ' --overwrite_handling overwrite'
|
|
728
897
|
print('Running: {}'.format(cmd))
|
|
729
898
|
cmd_results = execute_and_print(cmd)
|
|
730
899
|
|
|
731
|
-
with
|
|
732
|
-
|
|
900
|
+
# Run again with checkpointing, make sure the output are identical
|
|
901
|
+
cmd += ' --checkpoint_frequency 5'
|
|
902
|
+
inference_output_file_yolo_val_checkpoint = \
|
|
903
|
+
os.path.join(options.scratch_dir,'folder_inference_output_yolo_val_checkpoint.json')
|
|
904
|
+
assert inference_output_file_yolo_val_checkpoint != inference_output_file_yolo_val
|
|
905
|
+
cmd = cmd.replace(inference_output_file_yolo_val,inference_output_file_yolo_val_checkpoint)
|
|
906
|
+
cmd_results = execute_and_print(cmd)
|
|
733
907
|
|
|
908
|
+
assert output_files_are_identical(fn1=inference_output_file_yolo_val,
|
|
909
|
+
fn2=inference_output_file_yolo_val_checkpoint)
|
|
734
910
|
|
|
735
911
|
if not options.skip_video_tests:
|
|
736
912
|
|
|
737
913
|
## Video test
|
|
738
914
|
|
|
739
|
-
model_file = 'MDV5A'
|
|
740
915
|
video_inference_output_file = os.path.join(options.scratch_dir,'video_inference_output.json')
|
|
741
916
|
output_video_file = os.path.join(options.scratch_dir,'video_scratch/cli_rendered_video.mp4')
|
|
742
917
|
frame_folder = os.path.join(options.scratch_dir,'video_scratch/frame_folder_cli')
|
|
743
918
|
frame_rendering_folder = os.path.join(options.scratch_dir,'video_scratch/rendered_frame_folder_cli')
|
|
744
919
|
|
|
745
|
-
video_fn = os.path.join(options.scratch_dir,options.test_videos[-1])
|
|
920
|
+
video_fn = os.path.join(options.scratch_dir,options.test_videos[-1])
|
|
921
|
+
assert os.path.isfile(video_fn), 'Could not find video file {}'.format(video_fn)
|
|
922
|
+
|
|
746
923
|
output_dir = os.path.join(options.scratch_dir,'single_video_test_cli')
|
|
747
924
|
if options.cli_working_dir is None:
|
|
748
925
|
cmd = 'python -m megadetector.detection.process_video'
|
|
749
926
|
else:
|
|
750
927
|
cmd = 'python megadetector/detection/process_video.py'
|
|
751
|
-
cmd += ' {} {}'.format(
|
|
752
|
-
cmd += ' --frame_folder {} --frame_rendering_folder {} --output_json_file {} --output_video_file {}'.format(
|
|
928
|
+
cmd += ' "{}" "{}"'.format(options.default_model,video_fn)
|
|
929
|
+
cmd += ' --frame_folder "{}" --frame_rendering_folder "{}" --output_json_file "{}" --output_video_file "{}"'.format(
|
|
753
930
|
frame_folder,frame_rendering_folder,video_inference_output_file,output_video_file)
|
|
754
|
-
cmd += ' --render_output_video --fourcc
|
|
931
|
+
cmd += ' --render_output_video --fourcc {}'.format(options.video_fourcc)
|
|
755
932
|
cmd += ' --force_extracted_frame_folder_deletion --force_rendered_frame_folder_deletion --n_cores 5 --frame_sample 3'
|
|
933
|
+
cmd += ' --verbose'
|
|
756
934
|
print('Running: {}'.format(cmd))
|
|
757
935
|
cmd_results = execute_and_print(cmd)
|
|
758
936
|
|
|
759
937
|
# ...if we're not skipping video tests
|
|
760
938
|
|
|
761
939
|
|
|
762
|
-
## Run inference on a folder (
|
|
940
|
+
## Run inference on a folder (with MDV5B, so we can do a comparison)
|
|
763
941
|
|
|
764
942
|
image_folder = os.path.join(options.scratch_dir,'md-test-images')
|
|
765
|
-
model_file = 'MDV5B'
|
|
766
943
|
inference_output_file_alt = os.path.join(options.scratch_dir,'folder_inference_output_alt.json')
|
|
767
944
|
if options.cli_working_dir is None:
|
|
768
945
|
cmd = 'python -m megadetector.detection.run_detector_batch'
|
|
769
946
|
else:
|
|
770
947
|
cmd = 'python megadetector/detection/run_detector_batch.py'
|
|
771
|
-
cmd += ' {} {} {} --recursive'.format(
|
|
772
|
-
|
|
948
|
+
cmd += ' "{}" "{}" "{}" --recursive'.format(
|
|
949
|
+
options.alt_model,image_folder,inference_output_file_alt)
|
|
773
950
|
cmd += ' --output_relative_filenames --quiet --include_image_size'
|
|
774
951
|
cmd += ' --include_image_timestamp --include_exif_data'
|
|
775
952
|
print('Running: {}'.format(cmd))
|
|
@@ -789,7 +966,7 @@ def run_cli_tests(options):
|
|
|
789
966
|
cmd = 'python -m megadetector.postprocessing.compare_batch_results'
|
|
790
967
|
else:
|
|
791
968
|
cmd = 'python megadetector/postprocessing/compare_batch_results.py'
|
|
792
|
-
cmd += ' {} {} {}'.format(comparison_output_folder,image_folder,results_files_string)
|
|
969
|
+
cmd += ' "{}" "{}" {}'.format(comparison_output_folder,image_folder,results_files_string)
|
|
793
970
|
print('Running: {}'.format(cmd))
|
|
794
971
|
cmd_results = execute_and_print(cmd)
|
|
795
972
|
|
|
@@ -813,7 +990,7 @@ def run_tests(options):
|
|
|
813
990
|
"""
|
|
814
991
|
|
|
815
992
|
# Prepare data folder
|
|
816
|
-
download_test_data(options)
|
|
993
|
+
download_test_data(options)
|
|
817
994
|
|
|
818
995
|
if options.disable_gpu:
|
|
819
996
|
os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
|
|
@@ -861,7 +1038,7 @@ if False:
|
|
|
861
1038
|
options.max_coord_error = 0.001
|
|
862
1039
|
options.max_conf_error = 0.005
|
|
863
1040
|
options.cli_working_dir = r'c:\git\MegaDetector'
|
|
864
|
-
options.
|
|
1041
|
+
options.yolo_working_dir = r'c:\git\yolov5-md'
|
|
865
1042
|
|
|
866
1043
|
|
|
867
1044
|
#%%
|
|
@@ -943,26 +1120,45 @@ def main():
|
|
|
943
1120
|
type=str,
|
|
944
1121
|
default=None,
|
|
945
1122
|
help='Working directory for CLI tests')
|
|
1123
|
+
|
|
1124
|
+
parser.add_argument(
|
|
1125
|
+
'--yolo_working_dir',
|
|
1126
|
+
type=str,
|
|
1127
|
+
default=None,
|
|
1128
|
+
help='Working directory for yolo inference tests')
|
|
946
1129
|
|
|
1130
|
+
parser.add_argument(
|
|
1131
|
+
'--cli_test_pythonpath',
|
|
1132
|
+
type=str,
|
|
1133
|
+
default=None,
|
|
1134
|
+
help='PYTHONPATH to set for CLI tests; if None, inherits from the parent process'
|
|
1135
|
+
)
|
|
1136
|
+
|
|
947
1137
|
# token used for linting
|
|
948
1138
|
#
|
|
949
1139
|
# no_arguments_required
|
|
950
1140
|
|
|
951
1141
|
args = parser.parse_args()
|
|
952
|
-
|
|
953
|
-
options
|
|
954
|
-
|
|
955
|
-
options.skip_video_tests = args.skip_video_tests
|
|
956
|
-
options.skip_python_tests = args.skip_python_tests
|
|
957
|
-
options.skip_cli_tests = args.skip_cli_tests
|
|
958
|
-
options.scratch_dir = args.scratch_dir
|
|
959
|
-
options.warning_mode = args.warning_mode
|
|
960
|
-
options.force_data_download = args.force_data_download
|
|
961
|
-
options.max_conf_error = args.max_conf_error
|
|
962
|
-
options.max_coord_error = args.max_coord_error
|
|
963
|
-
options.cli_working_dir = args.cli_working_dir
|
|
964
|
-
|
|
1142
|
+
|
|
1143
|
+
_args_to_object(args,options)
|
|
1144
|
+
|
|
965
1145
|
run_tests(options)
|
|
966
1146
|
|
|
967
|
-
if __name__ == '__main__':
|
|
1147
|
+
if __name__ == '__main__':
|
|
968
1148
|
main()
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
#%% Sample invocations
|
|
1152
|
+
|
|
1153
|
+
"""
|
|
1154
|
+
# Windows
|
|
1155
|
+
set PYTHONPATH=c:\git\MegaDetector;c:\git\yolov5-md
|
|
1156
|
+
python md_tests.py --cli_working_dir "c:\git\MegaDetector" --yolo_working_dir "c:\git\yolov5-md" --cli_test_pythonpath "c:\git\MegaDetector;c:\git\yolov5-md"
|
|
1157
|
+
|
|
1158
|
+
# Linux
|
|
1159
|
+
export PYTHONPATH=/mnt/c/git/MegaDetector:/mnt/c/git/yolov5-md
|
|
1160
|
+
python md_tests.py --cli_working_dir "/mnt/c/git/MegaDetector" --yolo_working_dir "/mnt/c/git/yolov5-md" --cli_test_pythonpath "/mnt/c/git/MegaDetector:/mnt/c/git/yolov5-md"
|
|
1161
|
+
|
|
1162
|
+
python -c "import md_tests; print(md_tests.get_expected_results_filename(True))"
|
|
1163
|
+
"""
|
|
1164
|
+
|