megadetector 5.0.22__py3-none-any.whl → 5.0.24__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 (38) hide show
  1. megadetector/api/synchronous/api_core/animal_detection_api/api_backend.py +2 -3
  2. megadetector/classification/merge_classification_detection_output.py +2 -2
  3. megadetector/data_management/coco_to_labelme.py +2 -1
  4. megadetector/data_management/databases/integrity_check_json_db.py +15 -14
  5. megadetector/data_management/databases/subset_json_db.py +49 -21
  6. megadetector/data_management/mewc_to_md.py +340 -0
  7. megadetector/data_management/wi_to_md.py +41 -0
  8. megadetector/data_management/yolo_output_to_md_output.py +15 -8
  9. megadetector/detection/process_video.py +24 -7
  10. megadetector/detection/pytorch_detector.py +841 -160
  11. megadetector/detection/run_detector.py +340 -146
  12. megadetector/detection/run_detector_batch.py +306 -70
  13. megadetector/detection/run_inference_with_yolov5_val.py +61 -4
  14. megadetector/detection/tf_detector.py +6 -1
  15. megadetector/postprocessing/{combine_api_outputs.py → combine_batch_outputs.py} +10 -13
  16. megadetector/postprocessing/compare_batch_results.py +68 -6
  17. megadetector/postprocessing/md_to_labelme.py +7 -7
  18. megadetector/postprocessing/md_to_wi.py +40 -0
  19. megadetector/postprocessing/merge_detections.py +1 -1
  20. megadetector/postprocessing/postprocess_batch_results.py +10 -3
  21. megadetector/postprocessing/separate_detections_into_folders.py +32 -4
  22. megadetector/postprocessing/validate_batch_results.py +9 -4
  23. megadetector/utils/ct_utils.py +172 -57
  24. megadetector/utils/gpu_test.py +107 -0
  25. megadetector/utils/md_tests.py +363 -108
  26. megadetector/utils/path_utils.py +9 -2
  27. megadetector/utils/wi_utils.py +1794 -0
  28. megadetector/visualization/visualization_utils.py +82 -16
  29. megadetector/visualization/visualize_db.py +25 -7
  30. megadetector/visualization/visualize_detector_output.py +60 -13
  31. {megadetector-5.0.22.dist-info → megadetector-5.0.24.dist-info}/LICENSE +0 -0
  32. {megadetector-5.0.22.dist-info → megadetector-5.0.24.dist-info}/METADATA +129 -143
  33. {megadetector-5.0.22.dist-info → megadetector-5.0.24.dist-info}/RECORD +35 -33
  34. {megadetector-5.0.22.dist-info → megadetector-5.0.24.dist-info}/top_level.txt +0 -0
  35. megadetector/detection/detector_training/__init__.py +0 -0
  36. megadetector/detection/detector_training/model_main_tf2.py +0 -114
  37. megadetector/utils/torch_test.py +0 -32
  38. {megadetector-5.0.22.dist-info → megadetector-5.0.24.dist-info}/WHEEL +0 -0
@@ -29,6 +29,8 @@ import subprocess
29
29
  import argparse
30
30
  import inspect
31
31
 
32
+ from copy import copy
33
+
32
34
 
33
35
  #%% Classes
34
36
 
@@ -50,12 +52,21 @@ class MDTestOptions:
50
52
  #: Skip tests related to video processing
51
53
  self.skip_video_tests = False
52
54
 
55
+ #: Skip tests related to video rendering
56
+ self.skip_video_rendering_tests = False
57
+
53
58
  #: Skip tests launched via Python functions (as opposed to CLIs)
54
59
  self.skip_python_tests = False
55
60
 
56
61
  #: Skip CLI tests
57
62
  self.skip_cli_tests = False
58
63
 
64
+ #: Skip download tests
65
+ self.skip_download_tests = False
66
+
67
+ #: Skip force-CPU tests
68
+ self.skip_cpu_tests = False
69
+
59
70
  #: Force a specific folder for temporary input/output
60
71
  self.scratch_dir = None
61
72
 
@@ -106,7 +117,22 @@ class MDTestOptions:
106
117
  #: IoU threshold used to determine whether boxes in two detection files likely correspond
107
118
  #: to the same box.
108
119
  self.iou_threshold_for_file_comparison = 0.85
109
-
120
+
121
+ #: Detector options passed to PTDetector
122
+ self.detector_options = {'compatibility_mode':'classic-test'}
123
+
124
+ #: Used to drive a series of tests (typically with a low value for
125
+ #: python_test_depth) over a folder of models.
126
+ self.model_folder = None
127
+
128
+ #: Used as a knob to control the level of Python tests, typically used when
129
+ #: we want to run a series of simple tests on a small number of models, rather
130
+ #: than a deep test of tests on a small number of models. The gestalt is that
131
+ #: this is a range from 0-100.
132
+ self.python_test_depth = 100
133
+
134
+ # ...def __init__()
135
+
110
136
  # ...class MDTestOptions()
111
137
 
112
138
 
@@ -171,7 +197,7 @@ def get_expected_results_filename(gpu_is_available,
171
197
 
172
198
  if options is not None and options.scratch_dir is not None:
173
199
  fn = os.path.join(options.scratch_dir,fn)
174
-
200
+
175
201
  return fn
176
202
 
177
203
 
@@ -268,7 +294,7 @@ def download_test_data(options=None):
268
294
  os.path.isfile(os.path.join(scratch_dir,fn))]
269
295
 
270
296
  print('Finished unzipping and enumerating test data')
271
-
297
+
272
298
  return options
273
299
 
274
300
  # ...def download_test_data(...)
@@ -276,7 +302,7 @@ def download_test_data(options=None):
276
302
 
277
303
  def is_gpu_available(verbose=True):
278
304
  """
279
- Checks whether a GPU (including M1/M2 MPS) is available.
305
+ Checks whether a GPU (including M1/M2 MPS) is available, according to PyTorch.
280
306
 
281
307
  Args:
282
308
  verbose (bool, optional): enable additional debug console output
@@ -384,15 +410,21 @@ def compare_detection_lists(detections_a,detections_b,options,bidirectional_comp
384
410
 
385
411
  max_conf_error = 0
386
412
  max_coord_error = 0
387
-
413
+
414
+ max_conf_error_det_a = None
415
+ max_conf_error_det_b = None
416
+
417
+ max_coord_error_det_a = None
418
+ max_coord_error_det_b = None
419
+
388
420
  # i_det_a = 0
389
421
  for i_det_a in range(0,len(detections_a)):
390
422
 
391
423
  det_a = detections_a[i_det_a]
392
424
 
393
425
  # Don't process very-low-confidence boxes
394
- if det_a['conf'] < options.max_conf_error:
395
- continue
426
+ # if det_a['conf'] < options.max_conf_error:
427
+ # continue
396
428
 
397
429
  matching_det_b = None
398
430
  highest_iou = -1
@@ -402,22 +434,23 @@ def compare_detection_lists(detections_a,detections_b,options,bidirectional_comp
402
434
  # i_det_b = 0
403
435
  for i_det_b in range(0,len(detections_b)):
404
436
 
405
- b_det = detections_b[i_det_b]
437
+ det_b = detections_b[i_det_b]
406
438
 
407
- if b_det['category'] != det_a['category']:
439
+ if det_b['category'] != det_a['category']:
408
440
  continue
409
441
 
410
- iou = get_iou(det_a['bbox'],b_det['bbox'])
442
+ iou = get_iou(det_a['bbox'],det_b['bbox'])
411
443
 
412
444
  # Is this likely the same detection as det_a?
413
445
  if iou >= options.iou_threshold_for_file_comparison and iou > highest_iou:
414
- matching_det_b = b_det
446
+ matching_det_b = det_b
415
447
  highest_iou = iou
416
448
 
417
449
  # If there are no detections in this category in detections_b
418
450
  if matching_det_b is None:
419
451
  if det_a['conf'] > max_conf_error:
420
452
  max_conf_error = det_a['conf']
453
+ max_conf_error_det_a = det_a
421
454
  # max_coord_error = 1.0
422
455
  continue
423
456
 
@@ -427,13 +460,17 @@ def compare_detection_lists(detections_a,detections_b,options,bidirectional_comp
427
460
  for i_coord in range(0,4):
428
461
  coord_differences.append(abs(det_a['bbox'][i_coord]-\
429
462
  matching_det_b['bbox'][i_coord]))
430
- coord_err = max(coord_differences)
463
+ coord_err = max(coord_differences)
431
464
 
432
465
  if conf_err >= max_conf_error:
433
466
  max_conf_error = conf_err
467
+ max_conf_error_det_a = det_a
468
+ max_conf_error_det_b = det_b
434
469
 
435
470
  if coord_err >= max_coord_error:
436
- max_coord_error = coord_err
471
+ max_coord_error = coord_err
472
+ max_coord_error_det_a = det_a
473
+ max_coord_error_det_b = det_b
437
474
 
438
475
  # ...for each detection in detections_a
439
476
 
@@ -446,19 +483,32 @@ def compare_detection_lists(detections_a,detections_b,options,bidirectional_comp
446
483
 
447
484
  if reverse_comparison_results['max_conf_error'] > max_conf_error:
448
485
  max_conf_error = reverse_comparison_results['max_conf_error']
486
+ max_conf_error_det_a = reverse_comparison_results['max_conf_error_det_b']
487
+ max_conf_error_det_b = reverse_comparison_results['max_conf_error_det_a']
449
488
  if reverse_comparison_results['max_coord_error'] > max_coord_error:
450
489
  max_coord_error = reverse_comparison_results['max_coord_error']
490
+ max_coord_error_det_a = reverse_comparison_results['max_coord_error_det_b']
491
+ max_coord_error_det_b = reverse_comparison_results['max_coord_error_det_a']
451
492
 
452
493
  list_comparison_results = {}
494
+
453
495
  list_comparison_results['max_coord_error'] = max_coord_error
496
+ list_comparison_results['max_coord_error_det_a'] = max_coord_error_det_a
497
+ list_comparison_results['max_coord_error_det_b'] = max_coord_error_det_b
498
+
454
499
  list_comparison_results['max_conf_error'] = max_conf_error
500
+ list_comparison_results['max_conf_error_det_a'] = max_conf_error_det_a
501
+ list_comparison_results['max_conf_error_det_b'] = max_conf_error_det_b
455
502
 
456
503
  return list_comparison_results
457
504
 
458
505
  # ...def compare_detection_lists(...)
459
506
 
460
507
 
461
- def compare_results(inference_output_file,expected_results_file,options):
508
+ def compare_results(inference_output_file,
509
+ expected_results_file,
510
+ options,
511
+ expected_results_file_is_absolute=False):
462
512
  """
463
513
  Compare two MD-formatted output files that should be nearly identical, allowing small
464
514
  changes (e.g. rounding differences). Generally used to compare a new results file to
@@ -468,6 +518,9 @@ def compare_results(inference_output_file,expected_results_file,options):
468
518
  inference_output_file (str): the first results file to compare
469
519
  expected_results_file (str): the second results file to compare
470
520
  options (MDTestOptions): options that determine tolerable differences between files
521
+ expected_results_file_is_absolute (str, optional): by default,
522
+ expected_results_file is appended to options.scratch_dir; this option
523
+ specifies that it's an absolute path.
471
524
 
472
525
  Returns:
473
526
  dict: dictionary with keys 'max_coord_error' and 'max_conf_error'
@@ -477,7 +530,10 @@ def compare_results(inference_output_file,expected_results_file,options):
477
530
  with open(inference_output_file,'r') as f:
478
531
  results_from_file = json.load(f) # noqa
479
532
 
480
- with open(os.path.join(options.scratch_dir,expected_results_file),'r') as f:
533
+ if not expected_results_file_is_absolute:
534
+ expected_results_file= os.path.join(options.scratch_dir,expected_results_file)
535
+
536
+ with open(expected_results_file,'r') as f:
481
537
  expected_results = json.load(f)
482
538
 
483
539
  filename_to_results = {im['file'].replace('\\','/'):im for im in results_from_file['images']}
@@ -488,11 +544,13 @@ def compare_results(inference_output_file,expected_results_file,options):
488
544
  len(filename_to_results_expected),
489
545
  len(filename_to_results))
490
546
 
491
- max_conf_error = 0
547
+ max_conf_error = -1
492
548
  max_conf_error_file = None
549
+ max_conf_error_comparison_results = None
493
550
 
494
- max_coord_error = 0
551
+ max_coord_error = -1
495
552
  max_coord_error_file = None
553
+ max_coord_error_comparison_results = None
496
554
 
497
555
  # fn = next(iter(filename_to_results.keys()))
498
556
  for fn in filename_to_results.keys():
@@ -518,10 +576,12 @@ def compare_results(inference_output_file,expected_results_file,options):
518
576
 
519
577
  if comparison_results_this_image['max_conf_error'] > max_conf_error:
520
578
  max_conf_error = comparison_results_this_image['max_conf_error']
579
+ max_conf_error_comparison_results = comparison_results_this_image
521
580
  max_conf_error_file = fn
522
581
 
523
582
  if comparison_results_this_image['max_coord_error'] > max_coord_error:
524
583
  max_coord_error = comparison_results_this_image['max_coord_error']
584
+ max_coord_error_comparison_results = comparison_results_this_image
525
585
  max_coord_error_file = fn
526
586
 
527
587
  # ...for each image
@@ -537,7 +597,7 @@ def compare_results(inference_output_file,expected_results_file,options):
537
597
  'Coord error {} is greater than allowable ({}), on file:\n{} ({},{})'.format(
538
598
  max_coord_error,options.max_coord_error,max_coord_error_file,
539
599
  inference_output_file,expected_results_file)
540
-
600
+
541
601
  print('Max conf error: {} (file {})'.format(
542
602
  max_conf_error,max_conf_error_file))
543
603
  print('Max coord error: {} (file {})'.format(
@@ -545,7 +605,9 @@ def compare_results(inference_output_file,expected_results_file,options):
545
605
 
546
606
  comparison_results = {}
547
607
  comparison_results['max_conf_error'] = max_conf_error
608
+ comparison_results['max_conf_error_comparison_results'] = max_conf_error_comparison_results
548
609
  comparison_results['max_coord_error'] = max_coord_error
610
+ comparison_results['max_coord_error_comparison_results'] = max_coord_error_comparison_results
549
611
 
550
612
  return comparison_results
551
613
 
@@ -580,6 +642,10 @@ def _args_to_object(args, obj):
580
642
 
581
643
  os.environ["PYTHONUNBUFFERED"] = "1"
582
644
 
645
+ # In some circumstances I want to allow CLI tests to "succeed" even when they return
646
+ # specific non-zero output values.
647
+ allowable_process_return_codes = [0]
648
+
583
649
  def execute(cmd):
584
650
  """
585
651
  Runs [cmd] (a single string) in a shell, yielding each line of output to the caller.
@@ -598,7 +664,7 @@ def execute(cmd):
598
664
  yield stdout_line
599
665
  popen.stdout.close()
600
666
  return_code = popen.wait()
601
- if return_code:
667
+ if return_code not in allowable_process_return_codes:
602
668
  raise subprocess.CalledProcessError(return_code, cmd)
603
669
  return return_code
604
670
 
@@ -628,7 +694,7 @@ def execute_and_print(cmd,print_output=True,catch_exceptions=False,echo_command=
628
694
  print(s,end='',flush=True)
629
695
  to_return['status'] = 0
630
696
  except subprocess.CalledProcessError as cpe:
631
- if not catch_exceptions:
697
+ if not catch_exceptions:
632
698
  raise
633
699
  print('execute_and_print caught error: {}'.format(cpe.output))
634
700
  to_return['status'] = cpe.returncode
@@ -648,23 +714,41 @@ def run_python_tests(options):
648
714
  """
649
715
 
650
716
  print('\n*** Starting module tests ***\n')
717
+
718
+
719
+ ## Make sure our tests are doing what we think they're doing
720
+
721
+ from megadetector.detection import pytorch_detector
722
+ pytorch_detector.require_non_default_compatibility_mode = True
651
723
 
652
724
  ## Prepare data
653
725
 
654
726
  download_test_data(options)
655
727
 
656
728
 
657
- ## Run inference on an image
729
+ ## Miscellaneous utility tests
730
+
731
+ print('\n** Running ct_utils module test **\n')
732
+
733
+ from megadetector.utils.ct_utils import __module_test__ as ct_utils_test
734
+ ct_utils_test()
658
735
 
736
+
737
+ ## Run inference on an image
738
+
659
739
  print('\n** Running MD on a single image (module) **\n')
660
740
 
661
741
  from megadetector.detection import run_detector
662
742
  from megadetector.visualization import visualization_utils as vis_utils
663
743
  image_fn = os.path.join(options.scratch_dir,options.test_images[0])
664
- model = run_detector.load_detector(options.default_model)
744
+ model = run_detector.load_detector(options.default_model,
745
+ detector_options=copy(options.detector_options))
665
746
  pil_im = vis_utils.load_image(image_fn)
666
747
  result = model.generate_detections_one_image(pil_im) # noqa
667
-
748
+
749
+ if options.python_test_depth <= 1:
750
+ return
751
+
668
752
 
669
753
  ## Run inference on a folder
670
754
 
@@ -677,13 +761,15 @@ def run_python_tests(options):
677
761
  assert os.path.isdir(image_folder), 'Test image folder {} is not available'.format(image_folder)
678
762
  inference_output_file = os.path.join(options.scratch_dir,'folder_inference_output.json')
679
763
  image_file_names = path_utils.find_images(image_folder,recursive=True)
680
- results = load_and_run_detector_batch(options.default_model, image_file_names, quiet=True)
764
+ results = load_and_run_detector_batch(options.default_model,
765
+ image_file_names,
766
+ quiet=True,
767
+ detector_options=copy(options.detector_options))
681
768
  _ = write_results_to_file(results,
682
769
  inference_output_file,
683
770
  relative_path_base=image_folder,
684
771
  detector_file=options.default_model)
685
772
 
686
-
687
773
  ## Verify results
688
774
 
689
775
  # Verify format correctness
@@ -698,6 +784,9 @@ def run_python_tests(options):
698
784
 
699
785
  # Make note of this filename, we will use it again later
700
786
  inference_output_file_standard_inference = inference_output_file
787
+
788
+ if options.python_test_depth <= 2:
789
+ return
701
790
 
702
791
 
703
792
  ## Run and verify again with augmentation enabled
@@ -707,7 +796,11 @@ def run_python_tests(options):
707
796
  from megadetector.utils.path_utils import insert_before_extension
708
797
 
709
798
  inference_output_file_augmented = insert_before_extension(inference_output_file,'augmented')
710
- results = load_and_run_detector_batch(options.default_model, image_file_names, quiet=True, augment=True)
799
+ results = load_and_run_detector_batch(options.default_model,
800
+ image_file_names,
801
+ quiet=True,
802
+ augment=True,
803
+ detector_options=copy(options.detector_options))
711
804
  _ = write_results_to_file(results,
712
805
  inference_output_file_augmented,
713
806
  relative_path_base=image_folder,
@@ -847,7 +940,9 @@ def run_python_tests(options):
847
940
  video_options.output_video_file = os.path.join(options.scratch_dir,'video_scratch/rendered_video.mp4')
848
941
  video_options.frame_folder = os.path.join(options.scratch_dir,'video_scratch/frame_folder')
849
942
  video_options.frame_rendering_folder = os.path.join(options.scratch_dir,'video_scratch/rendered_frame_folder')
850
- video_options.render_output_video = True
943
+
944
+ video_options.render_output_video = (not options.skip_video_rendering_tests)
945
+
851
946
  # video_options.keep_rendered_frames = False
852
947
  # video_options.keep_extracted_frames = False
853
948
  video_options.force_extracted_frame_folder_deletion = True
@@ -863,6 +958,7 @@ def run_python_tests(options):
863
958
  video_options.n_cores = 5
864
959
  # video_options.debug_max_frames = -1
865
960
  # video_options.class_mapping_filename = None
961
+ video_options.detector_options = copy(options.detector_options)
866
962
 
867
963
  _ = process_video(video_options)
868
964
 
@@ -900,7 +996,7 @@ def run_python_tests(options):
900
996
  # video_options.rendering_confidence_threshold = None
901
997
  # video_options.json_confidence_threshold = 0.005
902
998
  video_options.frame_sample = 10
903
- video_options.n_cores = 5
999
+ video_options.n_cores = 5
904
1000
 
905
1001
  # Force frame extraction to disk, since that's how we generated our expected results file
906
1002
  video_options.force_on_disk_frame_extraction = True
@@ -910,13 +1006,15 @@ def run_python_tests(options):
910
1006
  # Use quality == None, because we can't control whether YOLOv5 has patched cm2.imread,
911
1007
  # and therefore can't rely on using the quality parameter
912
1008
  video_options.quality = None
913
- video_options.max_width = None
1009
+ video_options.max_width = None
1010
+ video_options.detector_options = copy(options.detector_options)
914
1011
 
1012
+ video_options.keep_extracted_frames = True
915
1013
  _ = process_video_folder(video_options)
916
1014
 
917
1015
  assert os.path.isfile(video_options.output_json_file), \
918
1016
  'Python video test failed to render output .json file'
919
-
1017
+
920
1018
  frame_output_file = insert_before_extension(video_options.output_json_file,'frames')
921
1019
  assert os.path.isfile(frame_output_file)
922
1020
 
@@ -926,6 +1024,7 @@ def run_python_tests(options):
926
1024
  expected_results_file = \
927
1025
  get_expected_results_filename(is_gpu_available(verbose=False),test_type='video',options=options)
928
1026
  assert os.path.isfile(expected_results_file)
1027
+
929
1028
  compare_results(frame_output_file,expected_results_file,options)
930
1029
 
931
1030
 
@@ -970,7 +1069,6 @@ def run_cli_tests(options):
970
1069
 
971
1070
  print('\n*** Starting CLI tests ***\n')
972
1071
 
973
-
974
1072
  ## Environment management
975
1073
 
976
1074
  if options.cli_test_pythonpath is not None:
@@ -988,6 +1086,12 @@ def run_cli_tests(options):
988
1086
  download_test_data(options)
989
1087
 
990
1088
 
1089
+ ## Utility imports
1090
+
1091
+ from megadetector.utils.ct_utils import dict_to_kvp_list
1092
+ from megadetector.utils.path_utils import insert_before_extension
1093
+
1094
+
991
1095
  ## Run inference on an image
992
1096
 
993
1097
  print('\n** Running MD on a single image (CLI) **\n')
@@ -1000,6 +1104,7 @@ def run_cli_tests(options):
1000
1104
  cmd = 'python megadetector/detection/run_detector.py'
1001
1105
  cmd += ' "{}" --image_file "{}" --output_dir "{}"'.format(
1002
1106
  options.default_model,image_fn,output_dir)
1107
+ cmd += ' --detector_options {}'.format(dict_to_kvp_list(options.detector_options))
1003
1108
  cmd_results = execute_and_print(cmd)
1004
1109
 
1005
1110
  if options.cpu_execution_is_error:
@@ -1011,6 +1116,13 @@ def run_cli_tests(options):
1011
1116
  if not gpu_available_via_cli:
1012
1117
  raise Exception('GPU execution is required, but not available')
1013
1118
 
1119
+ # Make sure we can also pass an absolute path to a model file, instead of, e.g. "MDV5A"
1120
+
1121
+ from megadetector.detection.run_detector import try_download_known_detector
1122
+ model_file = try_download_known_detector(options.default_model,force_download=False,verbose=False)
1123
+ cmd = cmd.replace(options.default_model,model_file)
1124
+ cmd_results = execute_and_print(cmd)
1125
+
1014
1126
 
1015
1127
  ## Run inference on a folder
1016
1128
 
@@ -1027,6 +1139,7 @@ def run_cli_tests(options):
1027
1139
  options.default_model,image_folder,inference_output_file)
1028
1140
  cmd += ' --output_relative_filenames --quiet --include_image_size'
1029
1141
  cmd += ' --include_image_timestamp --include_exif_data'
1142
+ cmd += ' --detector_options {}'.format(dict_to_kvp_list(options.detector_options))
1030
1143
  cmd_results = execute_and_print(cmd)
1031
1144
 
1032
1145
  base_cmd = cmd
@@ -1035,13 +1148,12 @@ def run_cli_tests(options):
1035
1148
  ## Run again with checkpointing enabled, make sure the results are the same
1036
1149
 
1037
1150
  print('\n** Running MD on a folder (with checkpoints) (CLI) **\n')
1038
-
1039
- from megadetector.utils.path_utils import insert_before_extension
1040
1151
 
1041
1152
  checkpoint_string = ' --checkpoint_frequency 5'
1042
1153
  cmd = base_cmd + checkpoint_string
1043
1154
  inference_output_file_checkpoint = insert_before_extension(inference_output_file,'_checkpoint')
1044
1155
  cmd = cmd.replace(inference_output_file,inference_output_file_checkpoint)
1156
+ cmd += ' --detector_options {}'.format(dict_to_kvp_list(options.detector_options))
1045
1157
  cmd_results = execute_and_print(cmd)
1046
1158
 
1047
1159
  assert output_files_are_identical(fn1=inference_output_file,
@@ -1051,12 +1163,12 @@ def run_cli_tests(options):
1051
1163
 
1052
1164
  ## Run again with the image queue enabled, make sure the results are the same
1053
1165
 
1054
- print('\n** Running MD on a folder (with image queue) (CLI) **\n')
1166
+ print('\n** Running MD on a folder (with image queue but no preprocessing) (CLI) **\n')
1055
1167
 
1056
1168
  cmd = base_cmd + ' --use_image_queue'
1057
- from megadetector.utils.path_utils import insert_before_extension
1058
1169
  inference_output_file_queue = insert_before_extension(inference_output_file,'_queue')
1059
1170
  cmd = cmd.replace(inference_output_file,inference_output_file_queue)
1171
+ cmd += ' --detector_options {}'.format(dict_to_kvp_list(options.detector_options))
1060
1172
  cmd_results = execute_and_print(cmd)
1061
1173
 
1062
1174
  assert output_files_are_identical(fn1=inference_output_file,
@@ -1064,48 +1176,66 @@ def run_cli_tests(options):
1064
1176
  verbose=True)
1065
1177
 
1066
1178
 
1067
- ## Run again on multiple cores, make sure the results are the same
1179
+ print('\n** Running MD on a folder (with image queue and preprocessing) (CLI) **\n')
1180
+
1181
+ cmd = base_cmd + ' --use_image_queue --preprocess_on_image_queue'
1182
+ inference_output_file_queue = insert_before_extension(inference_output_file,'_queue')
1183
+ cmd = cmd.replace(inference_output_file,inference_output_file_queue)
1184
+ cmd += ' --detector_options {}'.format(dict_to_kvp_list(options.detector_options))
1185
+ cmd_results = execute_and_print(cmd)
1068
1186
 
1069
- # First run again on the CPU on a single thread if necessary, so we get a file that
1070
- # *should* be identical to the multicore version.
1187
+ assert output_files_are_identical(fn1=inference_output_file,
1188
+ fn2=inference_output_file_queue,
1189
+ verbose=True)
1071
1190
 
1072
- gpu_available = is_gpu_available(verbose=False)
1191
+ ## Run again on multiple cores, make sure the results are the same
1073
1192
 
1074
- cuda_visible_devices = None
1075
- if 'CUDA_VISIBLE_DEVICES' in os.environ:
1076
- cuda_visible_devices = os.environ['CUDA_VISIBLE_DEVICES']
1077
- os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
1193
+ if not options.skip_cpu_tests:
1078
1194
 
1079
- # If we already ran on the CPU, no need to run again
1080
- if not gpu_available:
1081
- inference_output_file_cpu = inference_output_file
1082
- else:
1195
+ # First run again on the CPU on a single thread if necessary, so we get a file that
1196
+ # *should* be identical to the multicore version.
1197
+ gpu_available = is_gpu_available(verbose=False)
1083
1198
 
1084
- print('\n** Running MD on a folder (single CPU) (CLI) **\n')
1085
-
1086
- inference_output_file_cpu = insert_before_extension(inference_output_file,'cpu')
1087
- cmd = base_cmd
1088
- cmd = cmd.replace(inference_output_file,inference_output_file_cpu)
1199
+ cuda_visible_devices = None
1200
+ if 'CUDA_VISIBLE_DEVICES' in os.environ:
1201
+ cuda_visible_devices = os.environ['CUDA_VISIBLE_DEVICES']
1202
+ os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
1203
+
1204
+ # If we already ran on the CPU, no need to run again
1205
+ if not gpu_available:
1206
+
1207
+ inference_output_file_cpu = inference_output_file
1208
+
1209
+ else:
1210
+
1211
+ print('\n** Running MD on a folder (single CPU) (CLI) **\n')
1212
+
1213
+ inference_output_file_cpu = insert_before_extension(inference_output_file,'cpu')
1214
+ cmd = base_cmd
1215
+ cmd = cmd.replace(inference_output_file,inference_output_file_cpu)
1216
+ cmd += ' --detector_options {}'.format(dict_to_kvp_list(options.detector_options))
1217
+ cmd_results = execute_and_print(cmd)
1218
+
1219
+ print('\n** Running MD on a folder (multiple CPUs) (CLI) **\n')
1220
+
1221
+ cpu_string = ' --ncores 4'
1222
+ cmd = base_cmd + cpu_string
1223
+ inference_output_file_cpu_multicore = insert_before_extension(inference_output_file,'multicore')
1224
+ cmd = cmd.replace(inference_output_file,inference_output_file_cpu_multicore)
1225
+ cmd += ' --detector_options {}'.format(dict_to_kvp_list(options.detector_options))
1089
1226
  cmd_results = execute_and_print(cmd)
1090
1227
 
1091
- print('\n** Running MD on a folder (multiple CPUs) (CLI) **\n')
1092
-
1093
- cpu_string = ' --ncores 4'
1094
- cmd = base_cmd + cpu_string
1095
- from megadetector.utils.path_utils import insert_before_extension
1096
- inference_output_file_cpu_multicore = insert_before_extension(inference_output_file,'multicore')
1097
- cmd = cmd.replace(inference_output_file,inference_output_file_cpu_multicore)
1098
- cmd_results = execute_and_print(cmd)
1099
-
1100
- if cuda_visible_devices is not None:
1101
- print('Restoring CUDA_VISIBLE_DEVICES')
1102
- os.environ['CUDA_VISIBLE_DEVICES'] = cuda_visible_devices
1103
- else:
1104
- del os.environ['CUDA_VISIBLE_DEVICES']
1228
+ if cuda_visible_devices is not None:
1229
+ print('Restoring CUDA_VISIBLE_DEVICES')
1230
+ os.environ['CUDA_VISIBLE_DEVICES'] = cuda_visible_devices
1231
+ else:
1232
+ del os.environ['CUDA_VISIBLE_DEVICES']
1233
+
1234
+ assert output_files_are_identical(fn1=inference_output_file_cpu,
1235
+ fn2=inference_output_file_cpu_multicore,
1236
+ verbose=True)
1105
1237
 
1106
- assert output_files_are_identical(fn1=inference_output_file_cpu,
1107
- fn2=inference_output_file_cpu_multicore,
1108
- verbose=True)
1238
+ # ...if we're not skipping the force-cpu tests
1109
1239
 
1110
1240
 
1111
1241
  ## Postprocessing
@@ -1164,23 +1294,33 @@ def run_cli_tests(options):
1164
1294
 
1165
1295
  ## Run inference on a folder (tiled)
1166
1296
 
1167
- print('\n** Running tiled inference (CLI) **\n')
1297
+ # This is a rather esoteric code path that I turn off when I'm testing some
1298
+ # features that it doesn't include yet, particularly compatibility mode
1299
+ # control.
1300
+ skip_tiling_tests = True
1168
1301
 
1169
- image_folder = os.path.join(options.scratch_dir,'md-test-images')
1170
- tiling_folder = os.path.join(options.scratch_dir,'tiling-folder')
1171
- inference_output_file_tiled = os.path.join(options.scratch_dir,'folder_inference_output_tiled.json')
1172
- if options.cli_working_dir is None:
1173
- cmd = 'python -m megadetector.detection.run_tiled_inference'
1302
+ if skip_tiling_tests:
1303
+
1304
+ print('### DEBUG: skipping tiling tests ###')
1305
+
1174
1306
  else:
1175
- cmd = 'python megadetector/detection/run_tiled_inference.py'
1176
- cmd += ' "{}" "{}" "{}" "{}"'.format(
1177
- options.default_model,image_folder,tiling_folder,inference_output_file_tiled)
1178
- cmd += ' --overwrite_handling overwrite'
1179
- cmd_results = execute_and_print(cmd)
1180
-
1181
- with open(inference_output_file_tiled,'r') as f:
1182
- results_from_file = json.load(f) # noqa
1307
+ print('\n** Running tiled inference (CLI) **\n')
1308
+
1309
+ image_folder = os.path.join(options.scratch_dir,'md-test-images')
1310
+ tiling_folder = os.path.join(options.scratch_dir,'tiling-folder')
1311
+ inference_output_file_tiled = os.path.join(options.scratch_dir,'folder_inference_output_tiled.json')
1312
+ if options.cli_working_dir is None:
1313
+ cmd = 'python -m megadetector.detection.run_tiled_inference'
1314
+ else:
1315
+ cmd = 'python megadetector/detection/run_tiled_inference.py'
1316
+ cmd += ' "{}" "{}" "{}" "{}"'.format(
1317
+ options.default_model,image_folder,tiling_folder,inference_output_file_tiled)
1318
+ cmd += ' --overwrite_handling overwrite'
1319
+ cmd_results = execute_and_print(cmd)
1183
1320
 
1321
+ with open(inference_output_file_tiled,'r') as f:
1322
+ results_from_file = json.load(f) # noqa
1323
+
1184
1324
 
1185
1325
  ## Run inference on a folder (augmented, w/YOLOv5 val script)
1186
1326
 
@@ -1244,9 +1384,14 @@ def run_cli_tests(options):
1244
1384
  cmd += ' "{}" "{}"'.format(options.default_model,video_fn)
1245
1385
  cmd += ' --frame_folder "{}" --frame_rendering_folder "{}" --output_json_file "{}" --output_video_file "{}"'.format(
1246
1386
  frame_folder,frame_rendering_folder,video_inference_output_file,output_video_file)
1247
- cmd += ' --render_output_video --fourcc {}'.format(options.video_fourcc)
1387
+ cmd += ' --fourcc {}'.format(options.video_fourcc)
1248
1388
  cmd += ' --force_extracted_frame_folder_deletion --force_rendered_frame_folder_deletion --n_cores 5 --frame_sample 3'
1249
1389
  cmd += ' --verbose'
1390
+ cmd += ' --detector_options {}'.format(dict_to_kvp_list(options.detector_options))
1391
+
1392
+ if not options.skip_video_rendering_tests:
1393
+ cmd += ' --render_output_video'
1394
+
1250
1395
  cmd_results = execute_and_print(cmd)
1251
1396
 
1252
1397
  # ...if we're not skipping video tests
@@ -1266,6 +1411,7 @@ def run_cli_tests(options):
1266
1411
  options.alt_model,image_folder,inference_output_file_alt)
1267
1412
  cmd += ' --output_relative_filenames --quiet --include_image_size'
1268
1413
  cmd += ' --include_image_timestamp --include_exif_data'
1414
+ cmd += ' --detector_options {}'.format(dict_to_kvp_list(options.detector_options))
1269
1415
  cmd_results = execute_and_print(cmd)
1270
1416
 
1271
1417
  with open(inference_output_file_alt,'r') as f:
@@ -1294,6 +1440,50 @@ def run_cli_tests(options):
1294
1440
  # ...def run_cli_tests(...)
1295
1441
 
1296
1442
 
1443
+ def run_download_tests(options):
1444
+ """
1445
+ Args:
1446
+ options (MDTestOptions): see MDTestOptions for details
1447
+ """
1448
+
1449
+ if not options.skip_download_tests:
1450
+
1451
+ from megadetector.detection.run_detector import known_models, \
1452
+ try_download_known_detector, \
1453
+ get_detector_version_from_model_file, \
1454
+ model_string_to_model_version
1455
+
1456
+ # Make sure we can download models based on canonical version numbers,
1457
+ # e.g. "v5a.0.0"
1458
+ for model_name in known_models:
1459
+ url = known_models[model_name]['url']
1460
+ if 'localhost' in url:
1461
+ continue
1462
+ print('Testing download for known model {}'.format(model_name))
1463
+ fn = try_download_known_detector(model_name,
1464
+ force_download=False,
1465
+ verbose=False)
1466
+ version_string = get_detector_version_from_model_file(fn, verbose=False)
1467
+ assert version_string == model_name
1468
+
1469
+ # Make sure we can download models based on short names, e.g. "MDV5A"
1470
+ for model_name in model_string_to_model_version:
1471
+ model_version = model_string_to_model_version[model_name]
1472
+ assert model_version in known_models
1473
+ url = known_models[model_version]['url']
1474
+ if 'localhost' in url:
1475
+ continue
1476
+ print('Testing download for model short name {}'.format(model_name))
1477
+ fn = try_download_known_detector(model_name,
1478
+ force_download=False,
1479
+ verbose=False)
1480
+ assert fn != model_name
1481
+
1482
+ # ...if we need to test model downloads
1483
+
1484
+ # ...def run_download_tests()
1485
+
1486
+
1297
1487
  #%% Main test wrapper
1298
1488
 
1299
1489
  def run_tests(options):
@@ -1307,6 +1497,9 @@ def run_tests(options):
1307
1497
  # Prepare data folder
1308
1498
  download_test_data(options)
1309
1499
 
1500
+ # Run download tests if necessary
1501
+ run_download_tests(options)
1502
+
1310
1503
  if options.disable_gpu:
1311
1504
  os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
1312
1505
 
@@ -1323,8 +1516,32 @@ def run_tests(options):
1323
1516
 
1324
1517
  # Run python tests
1325
1518
  if not options.skip_python_tests:
1326
- run_python_tests(options)
1327
-
1519
+
1520
+ if options.model_folder is not None:
1521
+
1522
+ assert os.path.isdir(options.model_folder), \
1523
+ 'Could not find model folder {}'.format(options.model_folder)
1524
+
1525
+ model_files = os.listdir(options.model_folder)
1526
+ model_files = [fn for fn in model_files if fn.endswith('.pt')]
1527
+ model_files = [os.path.join(options.model_folder,fn) for fn in model_files]
1528
+
1529
+ assert len(model_files) > 0, \
1530
+ 'Could not find any models in folder {}'.format(options.model_folder)
1531
+
1532
+ original_default_model = options.default_model
1533
+
1534
+ for model_file in model_files:
1535
+ print('Running Python tests for model {}'.format(model_file))
1536
+ options.default_model = model_file
1537
+ run_python_tests(options)
1538
+
1539
+ options.default_model = original_default_model
1540
+
1541
+ else:
1542
+
1543
+ run_python_tests(options)
1544
+
1328
1545
  # Run CLI tests
1329
1546
  if not options.skip_cli_tests:
1330
1547
  run_cli_tests(options)
@@ -1352,16 +1569,26 @@ if False:
1352
1569
  options.warning_mode = False
1353
1570
  options.max_coord_error = 0.01 # 0.001
1354
1571
  options.max_conf_error = 0.01 # 0.005
1355
- # options.cli_working_dir = r'c:\git\MegaDetector'
1572
+ options.skip_video_rendering_tests = True
1573
+ # options.iou_threshold_for_file_comparison = 0.7
1574
+
1575
+ options.cli_working_dir = r'c:\git\MegaDetector'
1576
+ # When running in the cameratraps-detector environment
1577
+ # options.cli_test_pythonpath = r'c:\git\MegaDetector;c:\git\yolov5-md'
1578
+
1579
+ # When running in the MegaDetector environment
1580
+ options.cli_test_pythonpath = r'c:\git\MegaDetector'
1581
+
1582
+ # options.cli_working_dir = os.path.expanduser('~')
1356
1583
  # options.yolo_working_dir = r'c:\git\yolov5-md'
1357
- options.cli_working_dir = os.path.expanduser('~')
1358
1584
  # options.yolo_working_dir = '/mnt/c/git/yolov5-md'
1359
1585
  options = download_test_data(options)
1360
1586
 
1361
1587
  #%%
1362
1588
 
1363
1589
  import os
1364
- if 'PYTHONPATH' not in os.environ or options.yolo_working_dir not in os.environ['PYTHONPATH']:
1590
+ if ('PYTHONPATH' not in os.environ) or \
1591
+ (options.yolo_working_dir is not None and options.yolo_working_dir not in os.environ['PYTHONPATH']):
1365
1592
  os.environ['PYTHONPATH'] += ';' + options.yolo_working_dir
1366
1593
 
1367
1594
  #%%
@@ -1440,6 +1667,11 @@ def main():
1440
1667
  action='store_true',
1441
1668
  help='Skip tests related to video (which can be slow)')
1442
1669
 
1670
+ parser.add_argument(
1671
+ '--skip_video_rendering_tests',
1672
+ action='store_true',
1673
+ help='Skip tests related to *rendering* video')
1674
+
1443
1675
  parser.add_argument(
1444
1676
  '--skip_python_tests',
1445
1677
  action='store_true',
@@ -1450,6 +1682,16 @@ def main():
1450
1682
  action='store_true',
1451
1683
  help='Skip CLI tests')
1452
1684
 
1685
+ parser.add_argument(
1686
+ '--skip_download_tests',
1687
+ action='store_true',
1688
+ help='Skip model download tests')
1689
+
1690
+ parser.add_argument(
1691
+ '--skip_cpu_tests',
1692
+ action='store_true',
1693
+ help='Skip force-CPU tests')
1694
+
1453
1695
  parser.add_argument(
1454
1696
  '--force_data_download',
1455
1697
  action='store_true',
@@ -1498,13 +1740,43 @@ def main():
1498
1740
  help='PYTHONPATH to set for CLI tests; if None, inherits from the parent process'
1499
1741
  )
1500
1742
 
1501
- # token used for linting
1743
+ parser.add_argument(
1744
+ '--python_test_depth',
1745
+ type=int,
1746
+ default=options.python_test_depth,
1747
+ help='Used as a knob to control the level of Python tests (0-100)'
1748
+ )
1749
+
1750
+ parser.add_argument(
1751
+ '--model_folder',
1752
+ type=str,
1753
+ default=None,
1754
+ help='Run Python tests on every model in this folder'
1755
+ )
1756
+
1757
+ parser.add_argument(
1758
+ '--detector_options',
1759
+ nargs='*',
1760
+ metavar='KEY=VALUE',
1761
+ default='',
1762
+ help='Detector-specific options, as a space-separated list of key-value pairs')
1763
+
1764
+ parser.add_argument(
1765
+ '--default_model',
1766
+ type=str,
1767
+ default=options.default_model,
1768
+ help='Default model file or well-known model name (used for most tests)')
1769
+
1770
+ # The following token is used for linting, do not remove.
1502
1771
  #
1503
1772
  # no_arguments_required
1504
-
1773
+
1505
1774
  args = parser.parse_args()
1506
1775
 
1776
+ initial_detector_options = options.detector_options
1507
1777
  _args_to_object(args,options)
1778
+ from megadetector.utils.ct_utils import parse_kvp_list
1779
+ options.detector_options = parse_kvp_list(args.detector_options,d=initial_detector_options)
1508
1780
 
1509
1781
  run_tests(options)
1510
1782
 
@@ -1512,23 +1784,6 @@ if __name__ == '__main__':
1512
1784
  main()
1513
1785
 
1514
1786
 
1515
- #%% Sample invocations
1516
-
1517
- r"""
1518
- # Windows
1519
- set PYTHONPATH=c:\git\MegaDetector;c:\git\yolov5-md
1520
- cd c:\git\MegaDetector\megadetector\utils
1521
- 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"
1522
-
1523
- # Linux
1524
- export PYTHONPATH=/mnt/c/git/MegaDetector:/mnt/c/git/yolov5-md
1525
- cd /mnt/c/git/MegaDetector/megadetector/utils
1526
- 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"
1527
-
1528
- python -c "import md_tests; print(md_tests.get_expected_results_filename(True))"
1529
- """
1530
-
1531
-
1532
1787
  #%% Scrap
1533
1788
 
1534
1789
  if False: