megadetector 5.0.12__py3-none-any.whl → 5.0.14__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.

Files changed (45) hide show
  1. megadetector/api/batch_processing/api_core/server.py +1 -1
  2. megadetector/api/batch_processing/api_core/server_api_config.py +0 -1
  3. megadetector/api/batch_processing/api_core/server_job_status_table.py +0 -3
  4. megadetector/api/batch_processing/api_core/server_utils.py +0 -4
  5. megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +0 -1
  6. megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +0 -3
  7. megadetector/classification/efficientnet/utils.py +0 -3
  8. megadetector/data_management/camtrap_dp_to_coco.py +0 -2
  9. megadetector/data_management/cct_json_utils.py +15 -6
  10. megadetector/data_management/coco_to_labelme.py +12 -1
  11. megadetector/data_management/databases/integrity_check_json_db.py +43 -27
  12. megadetector/data_management/importers/cacophony-thermal-importer.py +1 -4
  13. megadetector/data_management/ocr_tools.py +0 -4
  14. megadetector/data_management/read_exif.py +178 -44
  15. megadetector/data_management/rename_images.py +187 -0
  16. megadetector/data_management/wi_download_csv_to_coco.py +3 -2
  17. megadetector/data_management/yolo_output_to_md_output.py +7 -2
  18. megadetector/detection/process_video.py +548 -244
  19. megadetector/detection/pytorch_detector.py +33 -14
  20. megadetector/detection/run_detector.py +17 -5
  21. megadetector/detection/run_detector_batch.py +179 -65
  22. megadetector/detection/run_inference_with_yolov5_val.py +527 -357
  23. megadetector/detection/tf_detector.py +14 -3
  24. megadetector/detection/video_utils.py +284 -61
  25. megadetector/postprocessing/categorize_detections_by_size.py +16 -14
  26. megadetector/postprocessing/classification_postprocessing.py +716 -0
  27. megadetector/postprocessing/compare_batch_results.py +101 -93
  28. megadetector/postprocessing/convert_output_format.py +12 -5
  29. megadetector/postprocessing/merge_detections.py +18 -7
  30. megadetector/postprocessing/postprocess_batch_results.py +133 -127
  31. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +236 -232
  32. megadetector/postprocessing/subset_json_detector_output.py +66 -62
  33. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +0 -2
  34. megadetector/utils/ct_utils.py +5 -4
  35. megadetector/utils/md_tests.py +380 -128
  36. megadetector/utils/path_utils.py +39 -6
  37. megadetector/utils/process_utils.py +13 -4
  38. megadetector/visualization/visualization_utils.py +7 -2
  39. megadetector/visualization/visualize_db.py +79 -77
  40. megadetector/visualization/visualize_detector_output.py +0 -1
  41. {megadetector-5.0.12.dist-info → megadetector-5.0.14.dist-info}/LICENSE +0 -0
  42. {megadetector-5.0.12.dist-info → megadetector-5.0.14.dist-info}/METADATA +2 -2
  43. {megadetector-5.0.12.dist-info → megadetector-5.0.14.dist-info}/RECORD +45 -43
  44. {megadetector-5.0.12.dist-info → megadetector-5.0.14.dist-info}/top_level.txt +0 -0
  45. {megadetector-5.0.12.dist-info → megadetector-5.0.14.dist-info}/WHEEL +0 -0
@@ -25,70 +25,90 @@ 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
- ## Required ##
38
+ def __init__(self):
38
39
 
39
- #: Force CPU execution
40
- disable_gpu = False
40
+ ## Required ##
41
41
 
42
- #: If GPU execution is requested, but a GPU is not available, should we error?
43
- cpu_execution_is_error = False
44
-
45
- #: Skip tests related to video processing
46
- skip_video_tests = False
47
-
48
- #: Skip tests launched via Python functions (as opposed to CLIs)
49
- skip_python_tests = False
50
-
51
- #: Skip CLI tests
52
- skip_cli_tests = False
53
-
54
- #: Force a specific folder for temporary input/output
55
- scratch_dir = None
56
-
57
- #: Where does the test data live?
58
- test_data_url = 'https://lila.science/public/md-test-package.zip'
59
-
60
- #: Download test data even if it appears to have already been downloaded
61
- force_data_download = False
62
-
63
- #: Unzip test data even if it appears to have already been unzipped
64
- force_data_unzip = False
65
-
66
- #: By default, any unexpected behavior is an error; this forces most errors to
67
- #: be treated as warnings.
68
- warning_mode = False
69
-
70
- #: How much deviation from the expected detection coordinates should we allow before
71
- #: a disrepancy becomes an error?
72
- max_coord_error = 0.001
73
-
74
- #: How much deviation from the expected confidence values should we allow before
75
- #: a disrepancy becomes an error?
76
- max_conf_error = 0.005
77
-
78
- #: Current working directory when running CLI tests
79
- cli_working_dir = None
80
-
81
- #: YOLOv5 installation, only relevant if we're testing run_inference_with_yolov5_val.
82
- #:
83
- #: If this is None, we'll skip that test.
84
- yolo_working_folder = None
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
+ #:
83
+ #: If this is None, we won't mess with the inherited working directory.
84
+ self.cli_working_dir = None
85
+
86
+ #: YOLOv5 installation, only relevant if we're testing run_inference_with_yolov5_val.
87
+ #:
88
+ #: If this is None, we'll skip that test.
89
+ self.yolo_working_dir = None
90
+
91
+ #: fourcc code to use for video tests that involve rendering video
92
+ self.video_fourcc = 'mp4v'
93
+
94
+ #: Default model to use for testing (filename, URL, or well-known model string)
95
+ self.default_model = 'MDV5A'
96
+
97
+ #: For comparison tests, use a model that produces slightly different output
98
+ self.alt_model = 'MDV5B'
99
+
100
+ #: PYTHONPATH to set for CLI tests; if None, inherits from the parent process. Only
101
+ #: impacts the called functions, not the parent process.
102
+ self.cli_test_pythonpath = None
85
103
 
86
104
  # ...class MDTestOptions()
87
105
 
88
106
 
89
107
  #%% Support functions
90
108
 
91
- def get_expected_results_filename(gpu_is_available):
109
+ def get_expected_results_filename(gpu_is_available,
110
+ model_string='mdv5a',
111
+ test_type='images'):
92
112
  """
93
113
  Expected results vary just a little across inference environments, particularly
94
114
  between PT 1.x and 2.x, so when making sure things are working acceptably, we
@@ -124,12 +144,13 @@ def get_expected_results_filename(gpu_is_available):
124
144
  import torch
125
145
  m1_inference = torch.backends.mps.is_built and torch.backends.mps.is_available()
126
146
  if m1_inference:
147
+ print('I appear to be running on M1/M2 hardware, using pt1/cpu as the reference results')
127
148
  hw_string = 'cpu'
128
- pt_string = 'pt1.10.1'
149
+ pt_string = 'pt1.10.1'
129
150
  except Exception:
130
151
  pass
131
152
 
132
- return 'md-test-results-{}-{}.json'.format(hw_string,pt_string)
153
+ return '{}-{}-results-{}-{}.json'.format(model_string,test_type,hw_string,pt_string)
133
154
 
134
155
 
135
156
  def download_test_data(options=None):
@@ -215,6 +236,8 @@ def download_test_data(options=None):
215
236
  options.test_images = [fn for fn in test_files if os.path.splitext(fn.lower())[1] in ('.jpg','.jpeg','.png')]
216
237
  options.test_videos = [fn for fn in test_files if os.path.splitext(fn.lower())[1] in ('.mp4','.avi')]
217
238
  options.test_videos = [fn for fn in options.test_videos if 'rendered' not in fn]
239
+ options.test_videos = [fn for fn in options.test_videos if \
240
+ os.path.isfile(os.path.join(scratch_dir,fn))]
218
241
 
219
242
  print('Finished unzipping and enumerating test data')
220
243
 
@@ -257,7 +280,82 @@ def is_gpu_available(verbose=True):
257
280
  print('No GPU available')
258
281
 
259
282
  return gpu_available
283
+
284
+ # ...def is_gpu_available(...)
285
+
286
+
287
+ def output_files_are_identical(fn1,fn2,verbose=False):
288
+ """
289
+ Checks whether two MD-formatted output files are identical other than file sorting.
290
+
291
+ Args:
292
+ fn1 (str): the first filename to compare
293
+ fn2 (str): the second filename to compare
294
+
295
+ Returns:
296
+ bool: whether [fn1] and [fn2] are identical other than file sorting.
297
+ """
298
+
299
+ if verbose:
300
+ print('Comparing {} to {}'.format(fn1,fn2))
301
+
302
+ with open(fn1,'r') as f:
303
+ fn1_results = json.load(f)
304
+ fn1_results['images'] = \
305
+ sorted(fn1_results['images'], key=lambda d: d['file'])
306
+
307
+ with open(fn2,'r') as f:
308
+ fn2_results = json.load(f)
309
+ fn2_results['images'] = \
310
+ sorted(fn2_results['images'], key=lambda d: d['file'])
311
+
312
+ if len(fn1_results['images']) != len(fn1_results['images']):
313
+ if verbose:
314
+ print('{} images in {}, {} images in {}'.format(
315
+ len(fn1_results['images']),fn1,
316
+ len(fn2_results['images']),fn2))
317
+ return False
318
+
319
+ # i_image = 0; fn1_image = fn1_results['images'][i_image]
320
+ for i_image,fn1_image in enumerate(fn1_results['images']):
321
+
322
+ fn2_image = fn2_results['images'][i_image]
323
+
324
+ if fn1_image['file'] != fn2_image['file']:
325
+ if verbose:
326
+ print('Filename difference at {}: {} vs {} '.format(i_image,fn1_image['file'],fn1_image['file']))
327
+ return False
328
+
329
+ if fn1_image != fn2_image:
330
+ if verbose:
331
+ print('Image-level difference in image {}: {}'.format(i_image,fn1_image['file']))
332
+ return False
333
+
334
+ return True
335
+
336
+ # ...def output_files_are_identical(...)
337
+
338
+
339
+ def _args_to_object(args, obj):
340
+ """
341
+ Copies all fields from a Namespace (typically the output from parse_args) to an
342
+ object. Skips fields starting with _. Does not check existence in the target
343
+ object.
344
+
345
+ Args:
346
+ args (argparse.Namespace): the namespace to convert to an object
347
+ obj (object): object whose whose attributes will be updated
260
348
 
349
+ Returns:
350
+ object: the modified object (modified in place, but also returned)
351
+ """
352
+
353
+ for n, v in inspect.getmembers(args):
354
+ if not n.startswith('_'):
355
+ setattr(obj, n, v)
356
+
357
+ return obj
358
+
261
359
 
262
360
  #%% CLI functions
263
361
 
@@ -289,7 +387,7 @@ def execute(cmd):
289
387
  return return_code
290
388
 
291
389
 
292
- def execute_and_print(cmd,print_output=True):
390
+ def execute_and_print(cmd,print_output=True,catch_exceptions=False,echo_command=True):
293
391
  """
294
392
  Runs [cmd] (a single string) in a shell, capturing (and optionally printing) output.
295
393
 
@@ -302,8 +400,11 @@ def execute_and_print(cmd,print_output=True):
302
400
  (the content of stdout)
303
401
  """
304
402
 
403
+ if echo_command:
404
+ print('Running command:\n{}\n'.format(cmd))
405
+
305
406
  to_return = {'status':'unknown','output':''}
306
- output=[]
407
+ output = []
307
408
  try:
308
409
  for s in execute(cmd):
309
410
  output.append(s)
@@ -311,6 +412,8 @@ def execute_and_print(cmd,print_output=True):
311
412
  print(s,end='',flush=True)
312
413
  to_return['status'] = 0
313
414
  except subprocess.CalledProcessError as cpe:
415
+ if not catch_exceptions:
416
+ raise
314
417
  print('execute_and_print caught error: {}'.format(cpe.output))
315
418
  to_return['status'] = cpe.returncode
316
419
  to_return['output'] = output
@@ -339,9 +442,8 @@ def run_python_tests(options):
339
442
 
340
443
  from megadetector.detection import run_detector
341
444
  from megadetector.visualization import visualization_utils as vis_utils
342
- model_file = 'MDV5A'
343
445
  image_fn = os.path.join(options.scratch_dir,options.test_images[0])
344
- model = run_detector.load_detector(model_file)
446
+ model = run_detector.load_detector(options.default_model)
345
447
  pil_im = vis_utils.load_image(image_fn)
346
448
  result = model.generate_detections_one_image(pil_im) # noqa
347
449
 
@@ -355,9 +457,9 @@ def run_python_tests(options):
355
457
  assert os.path.isdir(image_folder), 'Test image folder {} is not available'.format(image_folder)
356
458
  inference_output_file = os.path.join(options.scratch_dir,'folder_inference_output.json')
357
459
  image_file_names = path_utils.find_images(image_folder,recursive=True)
358
- results = load_and_run_detector_batch('MDV5A', image_file_names, quiet=True)
460
+ results = load_and_run_detector_batch(options.default_model, image_file_names, quiet=True)
359
461
  _ = write_results_to_file(results,inference_output_file,
360
- relative_path_base=image_folder,detector_file=model_file)
462
+ relative_path_base=image_folder,detector_file=options.default_model)
361
463
 
362
464
  # Read results
363
465
  with open(inference_output_file,'r') as f:
@@ -459,11 +561,11 @@ def run_python_tests(options):
459
561
  assert os.path.isfile(postprocessing_results.output_html_file), \
460
562
  'Postprocessing output file {} not found'.format(postprocessing_results.output_html_file)
461
563
 
462
-
564
+
463
565
  ## Partial RDE test
464
566
 
465
567
  from megadetector.postprocessing.repeat_detection_elimination.repeat_detections_core import \
466
- RepeatDetectionOptions,find_repeat_detections
568
+ RepeatDetectionOptions, find_repeat_detections
467
569
 
468
570
  rde_options = RepeatDetectionOptions()
469
571
  rde_options.occurrenceThreshold = 2
@@ -477,9 +579,69 @@ def run_python_tests(options):
477
579
  'Could not find RDE output file {}'.format(rde_results.filterFile)
478
580
 
479
581
 
480
- # TODO: add remove_repeat_detections test here
481
- #
482
- # It's already tested in the CLI tests, so this is not urgent.
582
+ ## Run inference on a folder (with YOLOv5 val script)
583
+
584
+ if options.yolo_working_dir is None:
585
+
586
+ print('Skipping YOLO val inference tests, no YOLO folder supplied')
587
+
588
+ else:
589
+
590
+ from megadetector.detection.run_inference_with_yolov5_val import \
591
+ YoloInferenceOptions, run_inference_with_yolo_val
592
+
593
+ inference_output_file_yolo_val = os.path.join(options.scratch_dir,'folder_inference_output_yolo_val.json')
594
+
595
+ yolo_inference_options = YoloInferenceOptions()
596
+ yolo_inference_options.input_folder = os.path.join(options.scratch_dir,'md-test-images')
597
+ yolo_inference_options.output_file = inference_output_file_yolo_val
598
+ yolo_inference_options.yolo_working_folder = options.yolo_working_dir
599
+ yolo_inference_options.model_filename = options.default_model
600
+ yolo_inference_options.augment = False
601
+ yolo_inference_options.overwrite_handling = 'overwrite'
602
+
603
+ run_inference_with_yolo_val(yolo_inference_options)
604
+
605
+ # Run again, without symlinks this time
606
+
607
+ from megadetector.utils.path_utils import insert_before_extension
608
+ inference_output_file_yolo_val_no_links = insert_before_extension(inference_output_file_yolo_val,
609
+ 'no-links')
610
+ yolo_inference_options.output_file = inference_output_file_yolo_val_no_links
611
+ yolo_inference_options.use_symlinks = False
612
+ run_inference_with_yolo_val(yolo_inference_options)
613
+
614
+ # Run again, with chunked inference and symlinks
615
+
616
+ inference_output_file_yolo_val_checkpoints = insert_before_extension(inference_output_file_yolo_val,
617
+ 'checkpoints')
618
+ yolo_inference_options.output_file = inference_output_file_yolo_val_checkpoints
619
+ yolo_inference_options.use_symlinks = True
620
+ yolo_inference_options.checkpoint_frequency = 5
621
+ run_inference_with_yolo_val(yolo_inference_options)
622
+
623
+ # Run again, with chunked inference and no symlinks
624
+
625
+ inference_output_file_yolo_val_checkpoints_no_links = \
626
+ insert_before_extension(inference_output_file_yolo_val,'checkpoints-no-links')
627
+ yolo_inference_options.output_file = inference_output_file_yolo_val_checkpoints_no_links
628
+ yolo_inference_options.use_symlinks = False
629
+ yolo_inference_options.checkpoint_frequency = 5
630
+ run_inference_with_yolo_val(yolo_inference_options)
631
+
632
+ fn1 = inference_output_file_yolo_val
633
+
634
+ output_files_to_compare = [
635
+ inference_output_file_yolo_val_no_links,
636
+ inference_output_file_yolo_val_checkpoints,
637
+ inference_output_file_yolo_val_checkpoints_no_links
638
+ ]
639
+
640
+ for fn2 in output_files_to_compare:
641
+ assert output_files_are_identical(fn1, fn2, verbose=True)
642
+
643
+ # ...if we need to run the YOLO val inference tests
644
+
483
645
 
484
646
  if not options.skip_video_tests:
485
647
 
@@ -488,7 +650,7 @@ def run_python_tests(options):
488
650
  from megadetector.detection.process_video import ProcessVideoOptions, process_video
489
651
 
490
652
  video_options = ProcessVideoOptions()
491
- video_options.model_file = 'MDV5A'
653
+ video_options.model_file = options.default_model
492
654
  video_options.input_video_file = os.path.join(options.scratch_dir,options.test_videos[0])
493
655
  video_options.output_json_file = os.path.join(options.scratch_dir,'single_video_output.json')
494
656
  video_options.output_video_file = os.path.join(options.scratch_dir,'video_scratch/rendered_video.mp4')
@@ -503,7 +665,7 @@ def run_python_tests(options):
503
665
  # video_options.reuse_frames_if_available = False
504
666
  video_options.recursive = True
505
667
  video_options.verbose = False
506
- video_options.fourcc = 'mp4v'
668
+ video_options.fourcc = options.video_fourcc
507
669
  # video_options.rendering_confidence_threshold = None
508
670
  # video_options.json_confidence_threshold = 0.005
509
671
  video_options.frame_sample = 5
@@ -524,7 +686,7 @@ def run_python_tests(options):
524
686
  from megadetector.detection.process_video import ProcessVideoOptions, process_video_folder
525
687
 
526
688
  video_options = ProcessVideoOptions()
527
- video_options.model_file = 'MDV5A'
689
+ video_options.model_file = options.default_model
528
690
  video_options.input_video_file = os.path.join(options.scratch_dir,
529
691
  os.path.dirname(options.test_videos[0]))
530
692
  video_options.output_json_file = os.path.join(options.scratch_dir,'video_folder_output.json')
@@ -539,8 +701,8 @@ def run_python_tests(options):
539
701
  # video_options.reuse_results_if_available = False
540
702
  # video_options.reuse_frames_if_available = False
541
703
  video_options.recursive = True
542
- video_options.verbose = False
543
- # video_options.fourcc = None
704
+ video_options.verbose = True
705
+ video_options.fourcc = options.video_fourcc
544
706
  # video_options.rendering_confidence_threshold = None
545
707
  # video_options.json_confidence_threshold = 0.005
546
708
  video_options.frame_sample = 5
@@ -572,6 +734,13 @@ def run_cli_tests(options):
572
734
 
573
735
  print('\n*** Starting CLI tests ***\n')
574
736
 
737
+
738
+ ## Environment management
739
+
740
+ if options.cli_test_pythonpath is not None:
741
+ os.environ['PYTHONPATH'] = options.cli_test_pythonpath
742
+
743
+
575
744
  ## chdir if necessary
576
745
 
577
746
  if options.cli_working_dir is not None:
@@ -585,16 +754,14 @@ def run_cli_tests(options):
585
754
 
586
755
  ## Run inference on an image
587
756
 
588
- model_file = 'MDV5A'
589
757
  image_fn = os.path.join(options.scratch_dir,options.test_images[0])
590
758
  output_dir = os.path.join(options.scratch_dir,'single_image_test')
591
759
  if options.cli_working_dir is None:
592
760
  cmd = 'python -m megadetector.detection.run_detector'
593
761
  else:
594
762
  cmd = 'python megadetector/detection/run_detector.py'
595
- cmd += ' {} --image_file {} --output_dir {}'.format(
596
- model_file,image_fn,output_dir)
597
- print('Running: {}'.format(cmd))
763
+ cmd += ' "{}" --image_file "{}" --output_dir "{}"'.format(
764
+ options.default_model,image_fn,output_dir)
598
765
  cmd_results = execute_and_print(cmd)
599
766
 
600
767
  if options.cpu_execution_is_error:
@@ -609,6 +776,7 @@ def run_cli_tests(options):
609
776
 
610
777
  ## Run inference on a folder
611
778
 
779
+
612
780
  image_folder = os.path.join(options.scratch_dir,'md-test-images')
613
781
  assert os.path.isdir(image_folder), 'Test image folder {} is not available'.format(image_folder)
614
782
  inference_output_file = os.path.join(options.scratch_dir,'folder_inference_output.json')
@@ -616,19 +784,79 @@ def run_cli_tests(options):
616
784
  cmd = 'python -m megadetector.detection.run_detector_batch'
617
785
  else:
618
786
  cmd = 'python megadetector/detection/run_detector_batch.py'
619
- cmd += ' {} {} {} --recursive'.format(
620
- model_file,image_folder,inference_output_file)
787
+ cmd += ' "{}" "{}" "{}" --recursive'.format(
788
+ options.default_model,image_folder,inference_output_file)
621
789
  cmd += ' --output_relative_filenames --quiet --include_image_size'
622
790
  cmd += ' --include_image_timestamp --include_exif_data'
623
- print('Running: {}'.format(cmd))
624
791
  cmd_results = execute_and_print(cmd)
625
792
 
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
793
+ base_cmd = cmd
794
+
795
+
796
+ ## Run again with checkpointing enabled, make sure the results are the same
797
+
798
+ from megadetector.utils.path_utils import insert_before_extension
799
+
800
+ checkpoint_string = ' --checkpoint_frequency 5'
801
+ cmd = base_cmd + checkpoint_string
802
+ inference_output_file_checkpoint = insert_before_extension(inference_output_file,'_checkpoint')
803
+ cmd = cmd.replace(inference_output_file,inference_output_file_checkpoint)
804
+ cmd_results = execute_and_print(cmd)
805
+
806
+ assert output_files_are_identical(fn1=inference_output_file,
807
+ fn2=inference_output_file_checkpoint,verbose=True)
808
+
809
+
810
+ ## Run again with the image queue enabled, make sure the results are the same
811
+
812
+ cmd = base_cmd + ' --use_image_queue'
813
+ from megadetector.utils.path_utils import insert_before_extension
814
+ inference_output_file_queue = insert_before_extension(inference_output_file,'_queue')
815
+ cmd = cmd.replace(inference_output_file,inference_output_file_queue)
816
+ cmd_results = execute_and_print(cmd)
817
+
818
+ assert output_files_are_identical(fn1=inference_output_file,
819
+ fn2=inference_output_file_queue,verbose=True)
820
+
821
+
822
+ ## Run again on multiple cores, make sure the results are the same
823
+
824
+ # First run again on the CPU on a single thread if necessary, so we get a file that
825
+ # *should* be identical to the multicore version.
826
+
827
+ gpu_available = is_gpu_available(verbose=False)
828
+
829
+ cuda_visible_devices = None
830
+ if 'CUDA_VISIBLE_DEVICES' in os.environ:
831
+ cuda_visible_devices = os.environ['CUDA_VISIBLE_DEVICES']
832
+ os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
833
+
834
+ # If we already ran on the CPU, no need to run again
835
+ if not gpu_available:
836
+ inference_output_file_cpu = inference_output_file
837
+ else:
838
+ inference_output_file_cpu = insert_before_extension(inference_output_file,'cpu')
839
+ cmd = base_cmd
840
+ cmd = cmd.replace(inference_output_file,inference_output_file_cpu)
841
+ cmd_results = execute_and_print(cmd)
842
+
843
+ cpu_string = ' --ncores 4'
844
+ cmd = base_cmd + cpu_string
845
+ from megadetector.utils.path_utils import insert_before_extension
846
+ inference_output_file_cpu_multicore = insert_before_extension(inference_output_file,'multicore')
847
+ cmd = cmd.replace(inference_output_file,inference_output_file_cpu_multicore)
848
+ cmd_results = execute_and_print(cmd)
849
+
850
+ if cuda_visible_devices is not None:
851
+ print('Restoring CUDA_VISIBLE_DEVICES')
852
+ os.environ['CUDA_VISIBLE_DEVICES'] = cuda_visible_devices
853
+ else:
854
+ del os.environ['CUDA_VISIBLE_DEVICES']
855
+
856
+ assert output_files_are_identical(fn1=inference_output_file_cpu,
857
+ fn2=inference_output_file_cpu_multicore,verbose=True)
858
+
630
859
 
631
-
632
860
  ## Postprocessing
633
861
 
634
862
  postprocessing_output_dir = os.path.join(options.scratch_dir,'postprocessing_output_cli')
@@ -637,10 +865,9 @@ def run_cli_tests(options):
637
865
  cmd = 'python -m megadetector.postprocessing.postprocess_batch_results'
638
866
  else:
639
867
  cmd = 'python megadetector/postprocessing/postprocess_batch_results.py'
640
- cmd += ' {} {}'.format(
868
+ cmd += ' "{}" "{}"'.format(
641
869
  inference_output_file,postprocessing_output_dir)
642
- cmd += ' --image_base_dir {}'.format(image_folder)
643
- print('Running: {}'.format(cmd))
870
+ cmd += ' --image_base_dir "{}"'.format(image_folder)
644
871
  cmd_results = execute_and_print(cmd)
645
872
 
646
873
 
@@ -652,11 +879,10 @@ def run_cli_tests(options):
652
879
  cmd = 'python -m megadetector.postprocessing.repeat_detection_elimination.find_repeat_detections'
653
880
  else:
654
881
  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)
882
+ cmd += ' "{}"'.format(inference_output_file)
883
+ cmd += ' --imageBase "{}"'.format(image_folder)
884
+ cmd += ' --outputBase "{}"'.format(rde_output_dir)
658
885
  cmd += ' --occurrenceThreshold 1' # Use an absurd number here to make sure we get some suspicious detections
659
- print('Running: {}'.format(cmd))
660
886
  cmd_results = execute_and_print(cmd)
661
887
 
662
888
  # Find the latest filtering folder
@@ -674,8 +900,7 @@ def run_cli_tests(options):
674
900
  cmd = 'python -m megadetector.postprocessing.repeat_detection_elimination.remove_repeat_detections'
675
901
  else:
676
902
  cmd = 'python megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py'
677
- cmd += ' {} {} {}'.format(inference_output_file,filtered_output_file,filtering_output_dir)
678
- print('Running: {}'.format(cmd))
903
+ cmd += ' "{}" "{}" "{}"'.format(inference_output_file,filtered_output_file,filtering_output_dir)
679
904
  cmd_results = execute_and_print(cmd)
680
905
 
681
906
  assert os.path.isfile(filtered_output_file), \
@@ -691,10 +916,9 @@ def run_cli_tests(options):
691
916
  cmd = 'python -m megadetector.detection.run_tiled_inference'
692
917
  else:
693
918
  cmd = 'python megadetector/detection/run_tiled_inference.py'
694
- cmd += ' {} {} {} {}'.format(
695
- model_file,image_folder,tiling_folder,inference_output_file_tiled)
919
+ cmd += ' "{}" "{}" "{}" "{}"'.format(
920
+ options.default_model,image_folder,tiling_folder,inference_output_file_tiled)
696
921
  cmd += ' --overwrite_handling overwrite'
697
- print('Running: {}'.format(cmd))
698
922
  cmd_results = execute_and_print(cmd)
699
923
 
700
924
  with open(inference_output_file_tiled,'r') as f:
@@ -703,7 +927,7 @@ def run_cli_tests(options):
703
927
 
704
928
  ## Run inference on a folder (augmented)
705
929
 
706
- if options.yolo_working_folder is None:
930
+ if options.yolo_working_dir is None:
707
931
 
708
932
  print('Bypassing YOLOv5 val tests, no yolo folder supplied')
709
933
 
@@ -717,62 +941,67 @@ def run_cli_tests(options):
717
941
  cmd = 'python -m megadetector.detection.run_inference_with_yolov5_val'
718
942
  else:
719
943
  cmd = 'python megadetector/detection/run_inference_with_yolov5_val.py'
720
- cmd += ' {} {} {}'.format(
721
- model_file,image_folder,inference_output_file_yolo_val)
722
- cmd += ' --yolo_working_folder {}'.format(options.yolo_working_folder)
723
- cmd += ' --yolo_results_folder {}'.format(yolo_results_folder)
724
- cmd += ' --symlink_folder {}'.format(yolo_symlink_folder)
944
+ cmd += ' "{}" "{}" "{}"'.format(
945
+ options.default_model,image_folder,inference_output_file_yolo_val)
946
+ cmd += ' --yolo_working_folder "{}"'.format(options.yolo_working_dir)
947
+ cmd += ' --yolo_results_folder "{}"'.format(yolo_results_folder)
948
+ cmd += ' --symlink_folder "{}"'.format(yolo_symlink_folder)
725
949
  cmd += ' --augment_enabled 1'
726
950
  # cmd += ' --no_use_symlinks'
727
951
  cmd += ' --overwrite_handling overwrite'
728
- print('Running: {}'.format(cmd))
729
952
  cmd_results = execute_and_print(cmd)
730
953
 
731
- with open(inference_output_file_yolo_val,'r') as f:
732
- results_from_file = json.load(f) # noqa
954
+ # Run again with checkpointing, make sure the output are identical
955
+ cmd += ' --checkpoint_frequency 5'
956
+ inference_output_file_yolo_val_checkpoint = \
957
+ os.path.join(options.scratch_dir,'folder_inference_output_yolo_val_checkpoint.json')
958
+ assert inference_output_file_yolo_val_checkpoint != inference_output_file_yolo_val
959
+ cmd = cmd.replace(inference_output_file_yolo_val,inference_output_file_yolo_val_checkpoint)
960
+ cmd_results = execute_and_print(cmd)
733
961
 
962
+ assert output_files_are_identical(fn1=inference_output_file_yolo_val,
963
+ fn2=inference_output_file_yolo_val_checkpoint)
734
964
 
735
965
  if not options.skip_video_tests:
736
966
 
737
967
  ## Video test
738
968
 
739
- model_file = 'MDV5A'
740
969
  video_inference_output_file = os.path.join(options.scratch_dir,'video_inference_output.json')
741
970
  output_video_file = os.path.join(options.scratch_dir,'video_scratch/cli_rendered_video.mp4')
742
971
  frame_folder = os.path.join(options.scratch_dir,'video_scratch/frame_folder_cli')
743
972
  frame_rendering_folder = os.path.join(options.scratch_dir,'video_scratch/rendered_frame_folder_cli')
744
973
 
745
- video_fn = os.path.join(options.scratch_dir,options.test_videos[-1])
974
+ video_fn = os.path.join(options.scratch_dir,options.test_videos[-1])
975
+ assert os.path.isfile(video_fn), 'Could not find video file {}'.format(video_fn)
976
+
746
977
  output_dir = os.path.join(options.scratch_dir,'single_video_test_cli')
747
978
  if options.cli_working_dir is None:
748
979
  cmd = 'python -m megadetector.detection.process_video'
749
980
  else:
750
981
  cmd = 'python megadetector/detection/process_video.py'
751
- cmd += ' {} {}'.format(model_file,video_fn)
752
- cmd += ' --frame_folder {} --frame_rendering_folder {} --output_json_file {} --output_video_file {}'.format(
982
+ cmd += ' "{}" "{}"'.format(options.default_model,video_fn)
983
+ cmd += ' --frame_folder "{}" --frame_rendering_folder "{}" --output_json_file "{}" --output_video_file "{}"'.format(
753
984
  frame_folder,frame_rendering_folder,video_inference_output_file,output_video_file)
754
- cmd += ' --render_output_video --fourcc mp4v'
985
+ cmd += ' --render_output_video --fourcc {}'.format(options.video_fourcc)
755
986
  cmd += ' --force_extracted_frame_folder_deletion --force_rendered_frame_folder_deletion --n_cores 5 --frame_sample 3'
756
- print('Running: {}'.format(cmd))
987
+ cmd += ' --verbose'
757
988
  cmd_results = execute_and_print(cmd)
758
989
 
759
990
  # ...if we're not skipping video tests
760
991
 
761
992
 
762
- ## Run inference on a folder (again, so we can do a comparison)
993
+ ## Run inference on a folder (with MDV5B, so we can do a comparison)
763
994
 
764
995
  image_folder = os.path.join(options.scratch_dir,'md-test-images')
765
- model_file = 'MDV5B'
766
996
  inference_output_file_alt = os.path.join(options.scratch_dir,'folder_inference_output_alt.json')
767
997
  if options.cli_working_dir is None:
768
998
  cmd = 'python -m megadetector.detection.run_detector_batch'
769
999
  else:
770
1000
  cmd = 'python megadetector/detection/run_detector_batch.py'
771
- cmd += ' {} {} {} --recursive'.format(
772
- model_file,image_folder,inference_output_file_alt)
1001
+ cmd += ' "{}" "{}" "{}" --recursive'.format(
1002
+ options.alt_model,image_folder,inference_output_file_alt)
773
1003
  cmd += ' --output_relative_filenames --quiet --include_image_size'
774
1004
  cmd += ' --include_image_timestamp --include_exif_data'
775
- print('Running: {}'.format(cmd))
776
1005
  cmd_results = execute_and_print(cmd)
777
1006
 
778
1007
  with open(inference_output_file_alt,'r') as f:
@@ -789,8 +1018,7 @@ def run_cli_tests(options):
789
1018
  cmd = 'python -m megadetector.postprocessing.compare_batch_results'
790
1019
  else:
791
1020
  cmd = 'python megadetector/postprocessing/compare_batch_results.py'
792
- cmd += ' {} {} {}'.format(comparison_output_folder,image_folder,results_files_string)
793
- print('Running: {}'.format(cmd))
1021
+ cmd += ' "{}" "{}" {}'.format(comparison_output_folder,image_folder,results_files_string)
794
1022
  cmd_results = execute_and_print(cmd)
795
1023
 
796
1024
  assert cmd_results['status'] == 0, 'Error generating comparison HTML'
@@ -813,7 +1041,7 @@ def run_tests(options):
813
1041
  """
814
1042
 
815
1043
  # Prepare data folder
816
- download_test_data(options)
1044
+ download_test_data(options)
817
1045
 
818
1046
  if options.disable_gpu:
819
1047
  os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
@@ -861,8 +1089,12 @@ if False:
861
1089
  options.max_coord_error = 0.001
862
1090
  options.max_conf_error = 0.005
863
1091
  options.cli_working_dir = r'c:\git\MegaDetector'
864
- options.yolo_working_folder = r'c:\git\yolov5'
1092
+ options.yolo_working_dir = r'c:\git\yolov5-md'
865
1093
 
1094
+ import os
1095
+
1096
+ if 'PYTHONPATH' not in os.environ or options.yolo_working_dir not in os.environ['PYTHONPATH']:
1097
+ os.environ['PYTHONPATH'] += ';' + options.yolo_working_dir
866
1098
 
867
1099
  #%%
868
1100
 
@@ -943,26 +1175,46 @@ def main():
943
1175
  type=str,
944
1176
  default=None,
945
1177
  help='Working directory for CLI tests')
1178
+
1179
+ parser.add_argument(
1180
+ '--yolo_working_dir',
1181
+ type=str,
1182
+ default=None,
1183
+ help='Working directory for yolo inference tests')
946
1184
 
1185
+ parser.add_argument(
1186
+ '--cli_test_pythonpath',
1187
+ type=str,
1188
+ default=None,
1189
+ help='PYTHONPATH to set for CLI tests; if None, inherits from the parent process'
1190
+ )
1191
+
947
1192
  # token used for linting
948
1193
  #
949
1194
  # no_arguments_required
950
1195
 
951
1196
  args = parser.parse_args()
952
-
953
- options.disable_gpu = args.disable_gpu
954
- options.cpu_execution_is_error = args.cpu_execution_is_error
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
-
1197
+
1198
+ _args_to_object(args,options)
1199
+
965
1200
  run_tests(options)
966
1201
 
967
- if __name__ == '__main__':
1202
+ if __name__ == '__main__':
968
1203
  main()
1204
+
1205
+
1206
+ #%% Sample invocations
1207
+
1208
+ r"""
1209
+ # Windows
1210
+ set PYTHONPATH=c:\git\MegaDetector;c:\git\yolov5-md
1211
+ cd c:\git\MegaDetector\megadetector\utils
1212
+ 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"
1213
+
1214
+ # Linux
1215
+ export PYTHONPATH=/mnt/c/git/MegaDetector:/mnt/c/git/yolov5-md
1216
+ cd /mnt/c/git/MegaDetector/megadetector/utils
1217
+ 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"
1218
+
1219
+ python -c "import md_tests; print(md_tests.get_expected_results_filename(True))"
1220
+ """