megadetector 5.0.23__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 +304 -68
  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 +165 -45
  24. megadetector/utils/gpu_test.py +107 -0
  25. megadetector/utils/md_tests.py +355 -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.23.dist-info → megadetector-5.0.24.dist-info}/METADATA +10 -24
  32. {megadetector-5.0.23.dist-info → megadetector-5.0.24.dist-info}/RECORD +35 -33
  33. megadetector/detection/detector_training/__init__.py +0 -0
  34. megadetector/detection/detector_training/model_main_tf2.py +0 -114
  35. megadetector/utils/torch_test.py +0 -32
  36. {megadetector-5.0.23.dist-info → megadetector-5.0.24.dist-info}/LICENSE +0 -0
  37. {megadetector-5.0.23.dist-info → megadetector-5.0.24.dist-info}/WHEEL +0 -0
  38. {megadetector-5.0.23.dist-info → megadetector-5.0.24.dist-info}/top_level.txt +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,6 +714,12 @@ 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
 
@@ -663,16 +735,20 @@ def run_python_tests(options):
663
735
 
664
736
 
665
737
  ## Run inference on an image
666
-
738
+
667
739
  print('\n** Running MD on a single image (module) **\n')
668
740
 
669
741
  from megadetector.detection import run_detector
670
742
  from megadetector.visualization import visualization_utils as vis_utils
671
743
  image_fn = os.path.join(options.scratch_dir,options.test_images[0])
672
- model = run_detector.load_detector(options.default_model)
744
+ model = run_detector.load_detector(options.default_model,
745
+ detector_options=copy(options.detector_options))
673
746
  pil_im = vis_utils.load_image(image_fn)
674
747
  result = model.generate_detections_one_image(pil_im) # noqa
675
-
748
+
749
+ if options.python_test_depth <= 1:
750
+ return
751
+
676
752
 
677
753
  ## Run inference on a folder
678
754
 
@@ -685,13 +761,15 @@ def run_python_tests(options):
685
761
  assert os.path.isdir(image_folder), 'Test image folder {} is not available'.format(image_folder)
686
762
  inference_output_file = os.path.join(options.scratch_dir,'folder_inference_output.json')
687
763
  image_file_names = path_utils.find_images(image_folder,recursive=True)
688
- 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))
689
768
  _ = write_results_to_file(results,
690
769
  inference_output_file,
691
770
  relative_path_base=image_folder,
692
771
  detector_file=options.default_model)
693
772
 
694
-
695
773
  ## Verify results
696
774
 
697
775
  # Verify format correctness
@@ -706,6 +784,9 @@ def run_python_tests(options):
706
784
 
707
785
  # Make note of this filename, we will use it again later
708
786
  inference_output_file_standard_inference = inference_output_file
787
+
788
+ if options.python_test_depth <= 2:
789
+ return
709
790
 
710
791
 
711
792
  ## Run and verify again with augmentation enabled
@@ -715,7 +796,11 @@ def run_python_tests(options):
715
796
  from megadetector.utils.path_utils import insert_before_extension
716
797
 
717
798
  inference_output_file_augmented = insert_before_extension(inference_output_file,'augmented')
718
- 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))
719
804
  _ = write_results_to_file(results,
720
805
  inference_output_file_augmented,
721
806
  relative_path_base=image_folder,
@@ -855,7 +940,9 @@ def run_python_tests(options):
855
940
  video_options.output_video_file = os.path.join(options.scratch_dir,'video_scratch/rendered_video.mp4')
856
941
  video_options.frame_folder = os.path.join(options.scratch_dir,'video_scratch/frame_folder')
857
942
  video_options.frame_rendering_folder = os.path.join(options.scratch_dir,'video_scratch/rendered_frame_folder')
858
- video_options.render_output_video = True
943
+
944
+ video_options.render_output_video = (not options.skip_video_rendering_tests)
945
+
859
946
  # video_options.keep_rendered_frames = False
860
947
  # video_options.keep_extracted_frames = False
861
948
  video_options.force_extracted_frame_folder_deletion = True
@@ -871,6 +958,7 @@ def run_python_tests(options):
871
958
  video_options.n_cores = 5
872
959
  # video_options.debug_max_frames = -1
873
960
  # video_options.class_mapping_filename = None
961
+ video_options.detector_options = copy(options.detector_options)
874
962
 
875
963
  _ = process_video(video_options)
876
964
 
@@ -908,7 +996,7 @@ def run_python_tests(options):
908
996
  # video_options.rendering_confidence_threshold = None
909
997
  # video_options.json_confidence_threshold = 0.005
910
998
  video_options.frame_sample = 10
911
- video_options.n_cores = 5
999
+ video_options.n_cores = 5
912
1000
 
913
1001
  # Force frame extraction to disk, since that's how we generated our expected results file
914
1002
  video_options.force_on_disk_frame_extraction = True
@@ -918,13 +1006,15 @@ def run_python_tests(options):
918
1006
  # Use quality == None, because we can't control whether YOLOv5 has patched cm2.imread,
919
1007
  # and therefore can't rely on using the quality parameter
920
1008
  video_options.quality = None
921
- video_options.max_width = None
1009
+ video_options.max_width = None
1010
+ video_options.detector_options = copy(options.detector_options)
922
1011
 
1012
+ video_options.keep_extracted_frames = True
923
1013
  _ = process_video_folder(video_options)
924
1014
 
925
1015
  assert os.path.isfile(video_options.output_json_file), \
926
1016
  'Python video test failed to render output .json file'
927
-
1017
+
928
1018
  frame_output_file = insert_before_extension(video_options.output_json_file,'frames')
929
1019
  assert os.path.isfile(frame_output_file)
930
1020
 
@@ -934,6 +1024,7 @@ def run_python_tests(options):
934
1024
  expected_results_file = \
935
1025
  get_expected_results_filename(is_gpu_available(verbose=False),test_type='video',options=options)
936
1026
  assert os.path.isfile(expected_results_file)
1027
+
937
1028
  compare_results(frame_output_file,expected_results_file,options)
938
1029
 
939
1030
 
@@ -978,7 +1069,6 @@ def run_cli_tests(options):
978
1069
 
979
1070
  print('\n*** Starting CLI tests ***\n')
980
1071
 
981
-
982
1072
  ## Environment management
983
1073
 
984
1074
  if options.cli_test_pythonpath is not None:
@@ -996,6 +1086,12 @@ def run_cli_tests(options):
996
1086
  download_test_data(options)
997
1087
 
998
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
+
999
1095
  ## Run inference on an image
1000
1096
 
1001
1097
  print('\n** Running MD on a single image (CLI) **\n')
@@ -1008,6 +1104,7 @@ def run_cli_tests(options):
1008
1104
  cmd = 'python megadetector/detection/run_detector.py'
1009
1105
  cmd += ' "{}" --image_file "{}" --output_dir "{}"'.format(
1010
1106
  options.default_model,image_fn,output_dir)
1107
+ cmd += ' --detector_options {}'.format(dict_to_kvp_list(options.detector_options))
1011
1108
  cmd_results = execute_and_print(cmd)
1012
1109
 
1013
1110
  if options.cpu_execution_is_error:
@@ -1019,6 +1116,13 @@ def run_cli_tests(options):
1019
1116
  if not gpu_available_via_cli:
1020
1117
  raise Exception('GPU execution is required, but not available')
1021
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
+
1022
1126
 
1023
1127
  ## Run inference on a folder
1024
1128
 
@@ -1035,6 +1139,7 @@ def run_cli_tests(options):
1035
1139
  options.default_model,image_folder,inference_output_file)
1036
1140
  cmd += ' --output_relative_filenames --quiet --include_image_size'
1037
1141
  cmd += ' --include_image_timestamp --include_exif_data'
1142
+ cmd += ' --detector_options {}'.format(dict_to_kvp_list(options.detector_options))
1038
1143
  cmd_results = execute_and_print(cmd)
1039
1144
 
1040
1145
  base_cmd = cmd
@@ -1043,13 +1148,12 @@ def run_cli_tests(options):
1043
1148
  ## Run again with checkpointing enabled, make sure the results are the same
1044
1149
 
1045
1150
  print('\n** Running MD on a folder (with checkpoints) (CLI) **\n')
1046
-
1047
- from megadetector.utils.path_utils import insert_before_extension
1048
1151
 
1049
1152
  checkpoint_string = ' --checkpoint_frequency 5'
1050
1153
  cmd = base_cmd + checkpoint_string
1051
1154
  inference_output_file_checkpoint = insert_before_extension(inference_output_file,'_checkpoint')
1052
1155
  cmd = cmd.replace(inference_output_file,inference_output_file_checkpoint)
1156
+ cmd += ' --detector_options {}'.format(dict_to_kvp_list(options.detector_options))
1053
1157
  cmd_results = execute_and_print(cmd)
1054
1158
 
1055
1159
  assert output_files_are_identical(fn1=inference_output_file,
@@ -1059,12 +1163,12 @@ def run_cli_tests(options):
1059
1163
 
1060
1164
  ## Run again with the image queue enabled, make sure the results are the same
1061
1165
 
1062
- 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')
1063
1167
 
1064
1168
  cmd = base_cmd + ' --use_image_queue'
1065
- from megadetector.utils.path_utils import insert_before_extension
1066
1169
  inference_output_file_queue = insert_before_extension(inference_output_file,'_queue')
1067
1170
  cmd = cmd.replace(inference_output_file,inference_output_file_queue)
1171
+ cmd += ' --detector_options {}'.format(dict_to_kvp_list(options.detector_options))
1068
1172
  cmd_results = execute_and_print(cmd)
1069
1173
 
1070
1174
  assert output_files_are_identical(fn1=inference_output_file,
@@ -1072,48 +1176,66 @@ def run_cli_tests(options):
1072
1176
  verbose=True)
1073
1177
 
1074
1178
 
1075
- ## 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')
1076
1180
 
1077
- # First run again on the CPU on a single thread if necessary, so we get a file that
1078
- # *should* be identical to the multicore version.
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)
1079
1186
 
1080
- gpu_available = is_gpu_available(verbose=False)
1187
+ assert output_files_are_identical(fn1=inference_output_file,
1188
+ fn2=inference_output_file_queue,
1189
+ verbose=True)
1081
1190
 
1082
- cuda_visible_devices = None
1083
- if 'CUDA_VISIBLE_DEVICES' in os.environ:
1084
- cuda_visible_devices = os.environ['CUDA_VISIBLE_DEVICES']
1085
- os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
1191
+ ## Run again on multiple cores, make sure the results are the same
1086
1192
 
1087
- # If we already ran on the CPU, no need to run again
1088
- if not gpu_available:
1089
- inference_output_file_cpu = inference_output_file
1090
- else:
1193
+ if not options.skip_cpu_tests:
1194
+
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)
1091
1198
 
1092
- print('\n** Running MD on a folder (single CPU) (CLI) **\n')
1093
-
1094
- inference_output_file_cpu = insert_before_extension(inference_output_file,'cpu')
1095
- cmd = base_cmd
1096
- 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))
1097
1226
  cmd_results = execute_and_print(cmd)
1098
1227
 
1099
- print('\n** Running MD on a folder (multiple CPUs) (CLI) **\n')
1100
-
1101
- cpu_string = ' --ncores 4'
1102
- cmd = base_cmd + cpu_string
1103
- from megadetector.utils.path_utils import insert_before_extension
1104
- inference_output_file_cpu_multicore = insert_before_extension(inference_output_file,'multicore')
1105
- cmd = cmd.replace(inference_output_file,inference_output_file_cpu_multicore)
1106
- cmd_results = execute_and_print(cmd)
1107
-
1108
- if cuda_visible_devices is not None:
1109
- print('Restoring CUDA_VISIBLE_DEVICES')
1110
- os.environ['CUDA_VISIBLE_DEVICES'] = cuda_visible_devices
1111
- else:
1112
- 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)
1113
1237
 
1114
- assert output_files_are_identical(fn1=inference_output_file_cpu,
1115
- fn2=inference_output_file_cpu_multicore,
1116
- verbose=True)
1238
+ # ...if we're not skipping the force-cpu tests
1117
1239
 
1118
1240
 
1119
1241
  ## Postprocessing
@@ -1172,23 +1294,33 @@ def run_cli_tests(options):
1172
1294
 
1173
1295
  ## Run inference on a folder (tiled)
1174
1296
 
1175
- 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
1176
1301
 
1177
- image_folder = os.path.join(options.scratch_dir,'md-test-images')
1178
- tiling_folder = os.path.join(options.scratch_dir,'tiling-folder')
1179
- inference_output_file_tiled = os.path.join(options.scratch_dir,'folder_inference_output_tiled.json')
1180
- if options.cli_working_dir is None:
1181
- cmd = 'python -m megadetector.detection.run_tiled_inference'
1302
+ if skip_tiling_tests:
1303
+
1304
+ print('### DEBUG: skipping tiling tests ###')
1305
+
1182
1306
  else:
1183
- cmd = 'python megadetector/detection/run_tiled_inference.py'
1184
- cmd += ' "{}" "{}" "{}" "{}"'.format(
1185
- options.default_model,image_folder,tiling_folder,inference_output_file_tiled)
1186
- cmd += ' --overwrite_handling overwrite'
1187
- cmd_results = execute_and_print(cmd)
1188
-
1189
- with open(inference_output_file_tiled,'r') as f:
1190
- 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)
1191
1320
 
1321
+ with open(inference_output_file_tiled,'r') as f:
1322
+ results_from_file = json.load(f) # noqa
1323
+
1192
1324
 
1193
1325
  ## Run inference on a folder (augmented, w/YOLOv5 val script)
1194
1326
 
@@ -1252,9 +1384,14 @@ def run_cli_tests(options):
1252
1384
  cmd += ' "{}" "{}"'.format(options.default_model,video_fn)
1253
1385
  cmd += ' --frame_folder "{}" --frame_rendering_folder "{}" --output_json_file "{}" --output_video_file "{}"'.format(
1254
1386
  frame_folder,frame_rendering_folder,video_inference_output_file,output_video_file)
1255
- cmd += ' --render_output_video --fourcc {}'.format(options.video_fourcc)
1387
+ cmd += ' --fourcc {}'.format(options.video_fourcc)
1256
1388
  cmd += ' --force_extracted_frame_folder_deletion --force_rendered_frame_folder_deletion --n_cores 5 --frame_sample 3'
1257
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
+
1258
1395
  cmd_results = execute_and_print(cmd)
1259
1396
 
1260
1397
  # ...if we're not skipping video tests
@@ -1274,6 +1411,7 @@ def run_cli_tests(options):
1274
1411
  options.alt_model,image_folder,inference_output_file_alt)
1275
1412
  cmd += ' --output_relative_filenames --quiet --include_image_size'
1276
1413
  cmd += ' --include_image_timestamp --include_exif_data'
1414
+ cmd += ' --detector_options {}'.format(dict_to_kvp_list(options.detector_options))
1277
1415
  cmd_results = execute_and_print(cmd)
1278
1416
 
1279
1417
  with open(inference_output_file_alt,'r') as f:
@@ -1302,6 +1440,50 @@ def run_cli_tests(options):
1302
1440
  # ...def run_cli_tests(...)
1303
1441
 
1304
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
+
1305
1487
  #%% Main test wrapper
1306
1488
 
1307
1489
  def run_tests(options):
@@ -1315,6 +1497,9 @@ def run_tests(options):
1315
1497
  # Prepare data folder
1316
1498
  download_test_data(options)
1317
1499
 
1500
+ # Run download tests if necessary
1501
+ run_download_tests(options)
1502
+
1318
1503
  if options.disable_gpu:
1319
1504
  os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
1320
1505
 
@@ -1331,8 +1516,32 @@ def run_tests(options):
1331
1516
 
1332
1517
  # Run python tests
1333
1518
  if not options.skip_python_tests:
1334
- run_python_tests(options)
1335
-
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
+
1336
1545
  # Run CLI tests
1337
1546
  if not options.skip_cli_tests:
1338
1547
  run_cli_tests(options)
@@ -1360,16 +1569,26 @@ if False:
1360
1569
  options.warning_mode = False
1361
1570
  options.max_coord_error = 0.01 # 0.001
1362
1571
  options.max_conf_error = 0.01 # 0.005
1363
- # 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('~')
1364
1583
  # options.yolo_working_dir = r'c:\git\yolov5-md'
1365
- options.cli_working_dir = os.path.expanduser('~')
1366
1584
  # options.yolo_working_dir = '/mnt/c/git/yolov5-md'
1367
1585
  options = download_test_data(options)
1368
1586
 
1369
1587
  #%%
1370
1588
 
1371
1589
  import os
1372
- 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']):
1373
1592
  os.environ['PYTHONPATH'] += ';' + options.yolo_working_dir
1374
1593
 
1375
1594
  #%%
@@ -1448,6 +1667,11 @@ def main():
1448
1667
  action='store_true',
1449
1668
  help='Skip tests related to video (which can be slow)')
1450
1669
 
1670
+ parser.add_argument(
1671
+ '--skip_video_rendering_tests',
1672
+ action='store_true',
1673
+ help='Skip tests related to *rendering* video')
1674
+
1451
1675
  parser.add_argument(
1452
1676
  '--skip_python_tests',
1453
1677
  action='store_true',
@@ -1458,6 +1682,16 @@ def main():
1458
1682
  action='store_true',
1459
1683
  help='Skip CLI tests')
1460
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
+
1461
1695
  parser.add_argument(
1462
1696
  '--force_data_download',
1463
1697
  action='store_true',
@@ -1506,13 +1740,43 @@ def main():
1506
1740
  help='PYTHONPATH to set for CLI tests; if None, inherits from the parent process'
1507
1741
  )
1508
1742
 
1509
- # 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.
1510
1771
  #
1511
1772
  # no_arguments_required
1512
-
1773
+
1513
1774
  args = parser.parse_args()
1514
1775
 
1776
+ initial_detector_options = options.detector_options
1515
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)
1516
1780
 
1517
1781
  run_tests(options)
1518
1782
 
@@ -1520,23 +1784,6 @@ if __name__ == '__main__':
1520
1784
  main()
1521
1785
 
1522
1786
 
1523
- #%% Sample invocations
1524
-
1525
- r"""
1526
- # Windows
1527
- set PYTHONPATH=c:\git\MegaDetector;c:\git\yolov5-md
1528
- cd c:\git\MegaDetector\megadetector\utils
1529
- 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"
1530
-
1531
- # Linux
1532
- export PYTHONPATH=/mnt/c/git/MegaDetector:/mnt/c/git/yolov5-md
1533
- cd /mnt/c/git/MegaDetector/megadetector/utils
1534
- 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"
1535
-
1536
- python -c "import md_tests; print(md_tests.get_expected_results_filename(True))"
1537
- """
1538
-
1539
-
1540
1787
  #%% Scrap
1541
1788
 
1542
1789
  if False: