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.

Files changed (40) 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 +171 -43
  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 +360 -216
  19. megadetector/detection/pytorch_detector.py +17 -3
  20. megadetector/detection/run_inference_with_yolov5_val.py +527 -357
  21. megadetector/detection/tf_detector.py +3 -0
  22. megadetector/detection/video_utils.py +122 -30
  23. megadetector/postprocessing/categorize_detections_by_size.py +16 -14
  24. megadetector/postprocessing/classification_postprocessing.py +716 -0
  25. megadetector/postprocessing/compare_batch_results.py +101 -93
  26. megadetector/postprocessing/merge_detections.py +18 -7
  27. megadetector/postprocessing/postprocess_batch_results.py +133 -127
  28. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +236 -232
  29. megadetector/postprocessing/subset_json_detector_output.py +66 -62
  30. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +0 -2
  31. megadetector/utils/ct_utils.py +5 -4
  32. megadetector/utils/md_tests.py +311 -115
  33. megadetector/utils/path_utils.py +1 -0
  34. megadetector/utils/process_utils.py +6 -3
  35. megadetector/visualization/visualize_db.py +79 -77
  36. {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/LICENSE +0 -0
  37. {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/METADATA +2 -2
  38. {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/RECORD +40 -38
  39. {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/top_level.txt +0 -0
  40. {megadetector-5.0.12.dist-info → megadetector-5.0.13.dist-info}/WHEEL +0 -0
@@ -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
- ## 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
+ 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
- pt_string = 'pt1.10.1'
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(model_file)
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('MDV5A', image_file_names, quiet=True)
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=model_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
- # TODO: add remove_repeat_detections test here
481
- #
482
- # It's already tested in the CLI tests, so this is not urgent.
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 = 'MDV5A'
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 = 'mp4v'
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 = 'MDV5A'
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 = False
543
- # video_options.fourcc = None
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
- model_file,image_fn,output_dir)
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
- model_file,image_folder,inference_output_file)
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
- model_file,image_folder,tiling_folder,inference_output_file_tiled)
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.yolo_working_folder is None:
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
- 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)
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 open(inference_output_file_yolo_val,'r') as f:
732
- results_from_file = json.load(f) # noqa
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(model_file,video_fn)
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 mp4v'
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 (again, so we can do a comparison)
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
- model_file,image_folder,inference_output_file_alt)
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.yolo_working_folder = r'c:\git\yolov5'
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.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
-
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
+