megadetector 5.0.6__py3-none-any.whl → 5.0.8__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 (75) hide show
  1. api/batch_processing/data_preparation/manage_local_batch.py +297 -202
  2. api/batch_processing/data_preparation/manage_video_batch.py +7 -2
  3. api/batch_processing/postprocessing/add_max_conf.py +1 -0
  4. api/batch_processing/postprocessing/combine_api_outputs.py +2 -2
  5. api/batch_processing/postprocessing/compare_batch_results.py +111 -61
  6. api/batch_processing/postprocessing/convert_output_format.py +24 -6
  7. api/batch_processing/postprocessing/load_api_results.py +56 -72
  8. api/batch_processing/postprocessing/md_to_labelme.py +119 -51
  9. api/batch_processing/postprocessing/merge_detections.py +30 -5
  10. api/batch_processing/postprocessing/postprocess_batch_results.py +175 -55
  11. api/batch_processing/postprocessing/remap_detection_categories.py +163 -0
  12. api/batch_processing/postprocessing/render_detection_confusion_matrix.py +628 -0
  13. api/batch_processing/postprocessing/repeat_detection_elimination/find_repeat_detections.py +71 -23
  14. api/batch_processing/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +1 -1
  15. api/batch_processing/postprocessing/repeat_detection_elimination/repeat_detections_core.py +224 -76
  16. api/batch_processing/postprocessing/subset_json_detector_output.py +132 -5
  17. api/batch_processing/postprocessing/top_folders_to_bottom.py +1 -1
  18. classification/prepare_classification_script.py +191 -191
  19. data_management/cct_json_utils.py +7 -2
  20. data_management/coco_to_labelme.py +263 -0
  21. data_management/coco_to_yolo.py +72 -48
  22. data_management/databases/integrity_check_json_db.py +75 -64
  23. data_management/databases/subset_json_db.py +1 -1
  24. data_management/generate_crops_from_cct.py +1 -1
  25. data_management/get_image_sizes.py +44 -26
  26. data_management/importers/animl_results_to_md_results.py +3 -5
  27. data_management/importers/noaa_seals_2019.py +2 -2
  28. data_management/importers/zamba_results_to_md_results.py +2 -2
  29. data_management/labelme_to_coco.py +264 -127
  30. data_management/labelme_to_yolo.py +96 -53
  31. data_management/lila/create_lila_blank_set.py +557 -0
  32. data_management/lila/create_lila_test_set.py +2 -1
  33. data_management/lila/create_links_to_md_results_files.py +1 -1
  34. data_management/lila/download_lila_subset.py +138 -45
  35. data_management/lila/generate_lila_per_image_labels.py +23 -14
  36. data_management/lila/get_lila_annotation_counts.py +16 -10
  37. data_management/lila/lila_common.py +15 -42
  38. data_management/lila/test_lila_metadata_urls.py +116 -0
  39. data_management/read_exif.py +65 -16
  40. data_management/remap_coco_categories.py +84 -0
  41. data_management/resize_coco_dataset.py +14 -31
  42. data_management/wi_download_csv_to_coco.py +239 -0
  43. data_management/yolo_output_to_md_output.py +40 -13
  44. data_management/yolo_to_coco.py +313 -100
  45. detection/process_video.py +36 -14
  46. detection/pytorch_detector.py +1 -1
  47. detection/run_detector.py +73 -18
  48. detection/run_detector_batch.py +116 -27
  49. detection/run_inference_with_yolov5_val.py +135 -27
  50. detection/run_tiled_inference.py +153 -43
  51. detection/tf_detector.py +2 -1
  52. detection/video_utils.py +4 -2
  53. md_utils/ct_utils.py +101 -6
  54. md_utils/md_tests.py +264 -17
  55. md_utils/path_utils.py +326 -47
  56. md_utils/process_utils.py +26 -7
  57. md_utils/split_locations_into_train_val.py +215 -0
  58. md_utils/string_utils.py +10 -0
  59. md_utils/url_utils.py +66 -3
  60. md_utils/write_html_image_list.py +12 -2
  61. md_visualization/visualization_utils.py +380 -74
  62. md_visualization/visualize_db.py +41 -10
  63. md_visualization/visualize_detector_output.py +185 -104
  64. {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/METADATA +11 -13
  65. {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/RECORD +74 -67
  66. {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/WHEEL +1 -1
  67. taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +1 -1
  68. taxonomy_mapping/map_new_lila_datasets.py +43 -39
  69. taxonomy_mapping/prepare_lila_taxonomy_release.py +5 -2
  70. taxonomy_mapping/preview_lila_taxonomy.py +27 -27
  71. taxonomy_mapping/species_lookup.py +33 -13
  72. taxonomy_mapping/taxonomy_csv_checker.py +7 -5
  73. md_visualization/visualize_megadb.py +0 -183
  74. {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/LICENSE +0 -0
  75. {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/top_level.txt +0 -0
detection/run_detector.py CHANGED
@@ -10,12 +10,7 @@
10
10
  # This script is not a good way to process lots of images (tens of thousands,
11
11
  # say). It does not facilitate checkpointing the results so if it crashes you
12
12
  # would have to start from scratch. If you want to run a detector (e.g., ours)
13
- # on lots of images, you should check out:
14
- #
15
- # 1) run_detector_batch.py (for local execution)
16
- #
17
- # 2) https://github.com/agentmorris/MegaDetector/tree/master/api/batch_processing
18
- # (for running large jobs on Azure ML)
13
+ # on lots of images, you should check out run_detector_batch.py.
19
14
  #
20
15
  # To run this script, we recommend you set up a conda virtual environment
21
16
  # following instructions in the Installation section on the main README, using
@@ -136,6 +131,33 @@ downloadable_models = {
136
131
  'MDV5B':'https://github.com/agentmorris/MegaDetector/releases/download/v5.0/md_v5b.0.0.pt'
137
132
  }
138
133
 
134
+ model_string_to_model_version = {
135
+ 'v2':'v2.0.0',
136
+ 'v3':'v3.0.0',
137
+ 'v4.1':'v4.1.0',
138
+ 'v5a.0.0':'v5a.0.0',
139
+ 'v5b.0.0':'v5b.0.0',
140
+ 'mdv5a':'v5a.0.0',
141
+ 'mdv5b':'v5b.0.0',
142
+ 'mdv4':'v4.1.0',
143
+ 'mdv3':'v3.0.0'
144
+ }
145
+
146
+ # Approximate inference speeds (in images per second) for MDv5 based on
147
+ # benchmarks, only used for reporting very coarse expectations about inference time.
148
+ device_token_to_mdv5_inference_speed = {
149
+ '4090':17.6,
150
+ '3090':11.4,
151
+ '3080':9.5,
152
+ '3050':4.2,
153
+ 'P2000':2.1,
154
+ # These are written this way because they're MDv4 benchmarks, and MDv5
155
+ # is around 3.5x faster than MDv4.
156
+ 'V100':2.79*3.5,
157
+ '2080':2.3*3.5,
158
+ '2060':1.6*3.5
159
+ }
160
+
139
161
 
140
162
  #%% Utility functions
141
163
 
@@ -190,18 +212,9 @@ def get_detector_version_from_filename(detector_filename):
190
212
  "v4.1.0", "v5a.0.0", and "v5b.0.0", respectively.
191
213
  """
192
214
 
193
- fn = os.path.basename(detector_filename)
194
- known_model_versions = {'v2':'v2.0.0',
195
- 'v3':'v3.0.0',
196
- 'v4.1':'v4.1.0',
197
- 'v5a.0.0':'v5a.0.0',
198
- 'v5b.0.0':'v5b.0.0',
199
- 'MDV5A':'v5a.0.0',
200
- 'MDV5B':'v5b.0.0',
201
- 'MDV4':'v4.1.0',
202
- 'MDV3':'v3.0.0'}
215
+ fn = os.path.basename(detector_filename).lower()
203
216
  matches = []
204
- for s in known_model_versions.keys():
217
+ for s in model_string_to_model_version.keys():
205
218
  if s in fn:
206
219
  matches.append(s)
207
220
  if len(matches) == 0:
@@ -211,9 +224,51 @@ def get_detector_version_from_filename(detector_filename):
211
224
  print('Warning: multiple MegaDetector versions for model file {}'.format(detector_filename))
212
225
  return 'multiple'
213
226
  else:
214
- return known_model_versions[matches[0]]
227
+ return model_string_to_model_version[matches[0]]
215
228
 
216
229
 
230
+ def estimate_md_images_per_second(model_file, device_name=None):
231
+ """
232
+ Estimate how fast MegaDetector will run based on benchmarks. Defaults to querying
233
+ the current device. Returns None if no data is available for the current card/model.
234
+ Estimates only available for a small handful of GPUs.
235
+ """
236
+
237
+ if device_name is None:
238
+ try:
239
+ import torch
240
+ device_name = torch.cuda.get_device_name()
241
+ except Exception as e:
242
+ print('Error querying device name: {}'.format(e))
243
+ return None
244
+
245
+ model_file = model_file.lower().strip()
246
+ if model_file in model_string_to_model_version.values():
247
+ model_version = model_file
248
+ else:
249
+ model_version = get_detector_version_from_filename(model_file)
250
+ if model_version not in model_string_to_model_version.values():
251
+ print('Error determining model version for model file {}'.format(model_file))
252
+ return None
253
+
254
+ mdv5_inference_speed = None
255
+ for device_token in device_token_to_mdv5_inference_speed.keys():
256
+ if device_token in device_name:
257
+ mdv5_inference_speed = device_token_to_mdv5_inference_speed[device_token]
258
+ break
259
+
260
+ if mdv5_inference_speed is None:
261
+ print('No speed estimate available for {}'.format(device_name))
262
+
263
+ if 'v5' in model_version:
264
+ return mdv5_inference_speed
265
+ elif 'v2' in model_version or 'v3' in model_version or 'v4' in model_version:
266
+ return mdv5_inference_speed / 3.5
267
+ else:
268
+ print('Could not estimate inference speed for model file {}'.format(model_file))
269
+ return None
270
+
271
+
217
272
  def get_typical_confidence_threshold_from_results(results):
218
273
  """
219
274
  Given the .json data loaded from a MD results file, determine a typical confidence
@@ -245,7 +245,8 @@ def process_images(im_files, detector, confidence_threshold, use_image_queue=Fal
245
245
  quiet=False, image_size=None, checkpoint_queue=None, include_image_size=False,
246
246
  include_image_timestamp=False, include_exif_data=False):
247
247
  """
248
- Runs MegaDetector over a list of image files.
248
+ Runs MegaDetector over a list of image files. As of 3/2024, this entry point is used when the
249
+ image queue is enabled, but not in the standard inference path (which loops over process_image()).
249
250
 
250
251
  Args
251
252
  - im_files: list of str, paths to image files
@@ -269,7 +270,7 @@ def process_images(im_files, detector, confidence_threshold, use_image_queue=Fal
269
270
  include_image_size=include_image_size,
270
271
  include_image_timestamp=include_image_timestamp,
271
272
  include_exif_data=include_exif_data)
272
- else:
273
+ else:
273
274
  results = []
274
275
  for im_file in im_files:
275
276
  result = process_image(im_file, detector, confidence_threshold,
@@ -662,7 +663,7 @@ def get_image_datetime(image):
662
663
 
663
664
  def write_results_to_file(results, output_file, relative_path_base=None,
664
665
  detector_file=None, info=None, include_max_conf=False,
665
- custom_metadata=None):
666
+ custom_metadata=None, force_forward_slashes=True):
666
667
  """
667
668
  Writes list of detection results to JSON output file. Format matches:
668
669
 
@@ -692,6 +693,14 @@ def write_results_to_file(results, output_file, relative_path_base=None,
692
693
  results_relative.append(r_relative)
693
694
  results = results_relative
694
695
 
696
+ if force_forward_slashes:
697
+ results_converted = []
698
+ for r in results:
699
+ r_converted = copy.copy(r)
700
+ r_converted['file'] = r_converted['file'].replace('\\','/')
701
+ results_converted.append(r_converted)
702
+ results = results_converted
703
+
695
704
  # The typical case: we need to build the 'info' struct
696
705
  if info is None:
697
706
 
@@ -751,17 +760,75 @@ if False:
751
760
 
752
761
  #%%
753
762
 
763
+ model_file = 'MDV5A'
764
+ image_dir = r'g:\camera_traps\camera_trap_images'
765
+ output_file = r'g:\temp\md-test.json'
766
+
767
+ recursive = True
768
+ output_relative_filenames = True
769
+ include_max_conf = False
770
+ quiet = True
771
+ image_size = None
772
+ use_image_queue = False
773
+ confidence_threshold = 0.0001
774
+ checkpoint_frequency = 5
754
775
  checkpoint_path = None
755
- model_file = r'G:\temp\models\md_v4.1.0.pb'
756
- confidence_threshold = 0.1
757
- checkpoint_frequency = -1
758
- results = None
776
+ resume_from_checkpoint = 'auto'
777
+ allow_checkpoint_overwrite = False
759
778
  ncores = 1
760
- use_image_queue = False
761
- quiet = False
762
- image_dir = r'G:\temp\demo_images\ssmini'
763
- image_size = None
779
+ class_mapping_filename = None
780
+ include_image_size = True
781
+ include_image_timestamp = True
782
+ include_exif_data = True
783
+ overwrite_handling = None
784
+
785
+ # Generate a command line
786
+ cmd = 'python run_detector_batch.py "{}" "{}" "{}"'.format(
787
+ model_file,image_dir,output_file)
788
+
789
+ if recursive:
790
+ cmd += ' --recursive'
791
+ if output_relative_filenames:
792
+ cmd += ' --output_relative_filenames'
793
+ if include_max_conf:
794
+ cmd += ' --include_max_conf'
795
+ if quiet:
796
+ cmd += ' --quiet'
797
+ if image_size is not None:
798
+ cmd += ' --image_size {}'.format(image_size)
799
+ if use_image_queue:
800
+ cmd += ' --use_image_queue'
801
+ if confidence_threshold is not None:
802
+ cmd += ' --threshold {}'.format(confidence_threshold)
803
+ if checkpoint_frequency is not None:
804
+ cmd += ' --checkpoint_frequency {}'.format(checkpoint_frequency)
805
+ if checkpoint_path is not None:
806
+ cmd += ' --checkpoint_path "{}"'.format(checkpoint_path)
807
+ if resume_from_checkpoint is not None:
808
+ cmd += ' --resume_from_checkpoint "{}"'.format(resume_from_checkpoint)
809
+ if allow_checkpoint_overwrite:
810
+ cmd += ' --allow_checkpoint_overwrite'
811
+ if ncores is not None:
812
+ cmd += ' --ncores {}'.format(ncores)
813
+ if class_mapping_filename is not None:
814
+ cmd += ' --class_mapping_filename "{}"'.format(class_mapping_filename)
815
+ if include_image_size:
816
+ cmd += ' --include_image_size'
817
+ if include_image_timestamp:
818
+ cmd += ' --include_image_timestamp'
819
+ if include_exif_data:
820
+ cmd += ' --include_exif_data'
821
+ if overwrite_handling is not None:
822
+ cmd += ' --overwrite_handling {}'.format(overwrite_handling)
823
+
824
+ print(cmd)
825
+ import clipboard; clipboard.copy(cmd)
826
+
827
+
828
+ #%% Run inference interactively
829
+
764
830
  image_file_names = path_utils.find_images(image_dir, recursive=False)
831
+ results = None
765
832
 
766
833
  start_time = time.time()
767
834
 
@@ -840,12 +907,15 @@ def main():
840
907
  '--checkpoint_path',
841
908
  type=str,
842
909
  default=None,
843
- help='File name to which checkpoints will be written if checkpoint_frequency is > 0')
910
+ help='File name to which checkpoints will be written if checkpoint_frequency is > 0, ' + \
911
+ 'defaults to md_checkpoint_[date].json in the same folder as the output file')
844
912
  parser.add_argument(
845
913
  '--resume_from_checkpoint',
846
914
  type=str,
847
915
  default=None,
848
- help='Path to a JSON checkpoint file to resume from')
916
+ help='Path to a JSON checkpoint file to resume from, or "auto" to ' + \
917
+ 'find the most recent checkpoint in the same folder as the output file. "auto" uses' + \
918
+ 'checkpoint_path (rather than searching the output folder) if checkpoint_path is specified.')
849
919
  parser.add_argument(
850
920
  '--allow_checkpoint_overwrite',
851
921
  action='store_true',
@@ -897,7 +967,7 @@ def main():
897
967
 
898
968
  assert os.path.exists(args.detector_file), \
899
969
  'detector file {} does not exist'.format(args.detector_file)
900
- assert 0.0 < args.threshold <= 1.0, 'Confidence threshold needs to be between 0 and 1'
970
+ assert 0.0 <= args.threshold <= 1.0, 'Confidence threshold needs to be between 0 and 1'
901
971
  assert args.output_file.endswith('.json'), 'output_file specified needs to end with .json'
902
972
  if args.checkpoint_frequency != -1:
903
973
  assert args.checkpoint_frequency > 0, 'Checkpoint_frequency needs to be > 0 or == -1'
@@ -919,19 +989,42 @@ def main():
919
989
  else:
920
990
  raise ValueError('Illegal overwrite handling string {}'.format(args.overwrite_handling))
921
991
 
992
+ output_dir = os.path.dirname(args.output_file)
993
+
994
+ if len(output_dir) > 0:
995
+ os.makedirs(output_dir,exist_ok=True)
996
+
997
+ assert not os.path.isdir(args.output_file), 'Specified output file is a directory'
998
+
922
999
  if args.class_mapping_filename is not None:
923
1000
  load_custom_class_mapping(args.class_mapping_filename)
924
-
1001
+
925
1002
  # Load the checkpoint if available
926
1003
  #
927
1004
  # Relative file names are only output at the end; all file paths in the checkpoint are
928
- # still full paths.
1005
+ # still absolute paths.
929
1006
  if args.resume_from_checkpoint is not None:
930
- assert os.path.exists(args.resume_from_checkpoint), \
1007
+ if args.resume_from_checkpoint == 'auto':
1008
+ checkpoint_files = os.listdir(output_dir)
1009
+ checkpoint_files = [fn for fn in checkpoint_files if \
1010
+ (fn.startswith('md_checkpoint') and fn.endswith('.json'))]
1011
+ if len(checkpoint_files) == 0:
1012
+ raise ValueError('resume_from_checkpoint set to "auto", but no checkpoints found in {}'.format(
1013
+ output_dir))
1014
+ else:
1015
+ if len(checkpoint_files) > 1:
1016
+ print('Warning: found {} checkpoints in {}, using the latest'.format(
1017
+ len(checkpoint_files),output_dir))
1018
+ checkpoint_files = sorted(checkpoint_files)
1019
+ checkpoint_file_relative = checkpoint_files[-1]
1020
+ checkpoint_file = os.path.join(output_dir,checkpoint_file_relative)
1021
+ else:
1022
+ checkpoint_file = args.resume_from_checkpoint
1023
+ assert os.path.exists(checkpoint_file), \
931
1024
  'File at resume_from_checkpoint specified does not exist'
932
- with open(args.resume_from_checkpoint) as f:
1025
+ with open(checkpoint_file) as f:
933
1026
  print('Loading previous results from checkpoint file {}'.format(
934
- args.resume_from_checkpoint))
1027
+ checkpoint_file))
935
1028
  saved = json.load(f)
936
1029
  assert 'images' in saved, \
937
1030
  'The checkpoint file does not have the correct fields; cannot be restored'
@@ -982,13 +1075,6 @@ def main():
982
1075
  assert os.path.exists(image_file_names[0]), \
983
1076
  'The first image to be processed does not exist at {}'.format(image_file_names[0])
984
1077
 
985
- output_dir = os.path.dirname(args.output_file)
986
-
987
- if len(output_dir) > 0:
988
- os.makedirs(output_dir,exist_ok=True)
989
-
990
- assert not os.path.isdir(args.output_file), 'Specified output file is a directory'
991
-
992
1078
  # Test that we can write to the output_file's dir if checkpointing requested
993
1079
  if args.checkpoint_frequency != -1:
994
1080
 
@@ -996,7 +1082,7 @@ def main():
996
1082
  checkpoint_path = args.checkpoint_path
997
1083
  else:
998
1084
  checkpoint_path = os.path.join(output_dir,
999
- 'checkpoint_{}.json'.format(
1085
+ 'md_checkpoint_{}.json'.format(
1000
1086
  datetime.utcnow().strftime("%Y%m%d%H%M%S")))
1001
1087
 
1002
1088
  # Don't overwrite existing checkpoint files, this is a sure-fire way to eventually
@@ -1023,6 +1109,9 @@ def main():
1023
1109
 
1024
1110
  else:
1025
1111
 
1112
+ if args.checkpoint_path is not None:
1113
+ print('Warning: checkpointing disabled because checkpoint_frequency is -1, ' + \
1114
+ 'but a checkpoint path was specified')
1026
1115
  checkpoint_path = None
1027
1116
 
1028
1117
  start_time = time.time()
@@ -49,6 +49,7 @@ from tqdm import tqdm
49
49
 
50
50
  from md_utils import path_utils
51
51
  from md_utils import process_utils
52
+ from md_utils import string_utils
52
53
  from data_management import yolo_output_to_md_output
53
54
  from detection.run_detector import try_download_known_detector
54
55
 
@@ -68,17 +69,20 @@ class YoloInferenceOptions:
68
69
 
69
70
  ## Optional ##
70
71
 
71
- # Required for YOLOv5 models, not for YOLOv8 models
72
+ # Required for older YOLOv5 inference, not for newer ulytralytics inference
72
73
  yolo_working_folder = None
73
74
 
74
- model_type = 'yolov5' # currently 'yolov5' and 'yolov8' are supported
75
+ # Currently 'yolov5' and 'ultralytics' are supported, and really these are proxies for
76
+ # "the yolov5 repo" and "the ultralytics repo" (typically YOLOv8).
77
+ model_type = 'yolov5'
75
78
 
76
79
  image_size = default_image_size_with_augmentation
77
80
  conf_thres = '0.001'
78
81
  batch_size = 1
79
82
  device_string = '0'
80
83
  augment = True
81
-
84
+ half_precision_enabled = None
85
+
82
86
  symlink_folder = None
83
87
  use_symlinks = True
84
88
 
@@ -97,16 +101,30 @@ class YoloInferenceOptions:
97
101
  overwrite_handling = 'skip'
98
102
 
99
103
  preview_yolo_command_only = False
104
+
105
+ treat_copy_failures_as_warnings = False
106
+
107
+ save_yolo_debug_output = False
108
+
109
+ recursive = True
100
110
 
101
111
 
102
112
  #%% Main function
103
113
 
104
114
  def run_inference_with_yolo_val(options):
105
115
 
106
- ##%% Path handling
116
+ ##%% Input and path handling
117
+
118
+ if options.model_type == 'yolov8':
119
+
120
+ print('Warning: model type "yolov8" supplied, "ultralytics" is the preferred model type string for YOLOv8 models')
121
+ options.model_type = 'ultralytics'
122
+
123
+ if (options.model_type == 'yolov5') and ('yolov8' in options.model_filename.lower()):
124
+ print('\n\n*** Warning: model type set as "yolov5", but your model filename contains "yolov8"... did you mean to use --model_type yolov8?" ***\n\n')
107
125
 
108
126
  if options.yolo_working_folder is None:
109
- assert options.model_type == 'yolov8', \
127
+ assert options.model_type == 'ultralytics', \
110
128
  'A working folder is required to run YOLOv5 val.py'
111
129
  else:
112
130
  assert os.path.isdir(options.yolo_working_folder), \
@@ -115,6 +133,11 @@ def run_inference_with_yolo_val(options):
115
133
  assert os.path.isdir(options.input_folder) or os.path.isfile(options.input_folder), \
116
134
  'Could not find input {}'.format(options.input_folder)
117
135
 
136
+ if options.half_precision_enabled is not None:
137
+ assert options.half_precision_enabled in (0,1), \
138
+ 'Invalid value {} for --half_precision_enabled (should be 0 or 1)'.format(
139
+ options.half_precision_enabled)
140
+
118
141
  # If the model filename is a known model string (e.g. "MDv5A", download the model if necessary)
119
142
  model_filename = try_download_known_detector(options.model_filename)
120
143
 
@@ -182,7 +205,7 @@ def run_inference_with_yolo_val(options):
182
205
  ##%% Enumerate images
183
206
 
184
207
  if os.path.isdir(options.input_folder):
185
- image_files_absolute = path_utils.find_images(options.input_folder,recursive=True)
208
+ image_files_absolute = path_utils.find_images(options.input_folder,recursive=options.recursive)
186
209
  else:
187
210
  assert os.path.isfile(options.input_folder)
188
211
  with open(options.input_folder,'r') as f:
@@ -218,10 +241,20 @@ def run_inference_with_yolo_val(options):
218
241
  else:
219
242
  shutil.copyfile(image_fn,symlink_full_path)
220
243
  except Exception as e:
221
- image_id_to_error[image_id] = str(e)
222
- print('Warning: error copying/creating link for input file {}: {}'.format(
223
- image_fn,str(e)))
224
- continue
244
+ error_string = str(e)
245
+ image_id_to_error[image_id] = error_string
246
+ # Always break if the user is trying to create symlinks on Windows without
247
+ # permission, 100% of images will always fail in this case.
248
+ if ('a required privilege is not held by the client' in error_string.lower()) or \
249
+ (not options.treat_copy_failures_as_warnings):
250
+ print('\nError copying/creating link for input file {}: {}'.format(
251
+ image_fn,error_string))
252
+
253
+ raise
254
+ else:
255
+ print('Warning: error copying/creating link for input file {}: {}'.format(
256
+ image_fn,error_string))
257
+ continue
225
258
 
226
259
  # ...for each image
227
260
 
@@ -270,17 +303,34 @@ def run_inference_with_yolo_val(options):
270
303
  if options.augment:
271
304
  cmd += ' --augment'
272
305
 
273
- elif options.model_type == 'yolov8':
306
+ # --half is a store_true argument for YOLOv5's val.py
307
+ if (options.half_precision_enabled is not None) and (options.half_precision_enabled == 1):
308
+ cmd += ' --half'
274
309
 
310
+ # Sometimes useful for debugging
311
+ # cmd += ' --save_conf --save_txt'
312
+
313
+ elif options.model_type == 'ultralytics':
314
+
275
315
  if options.augment:
276
316
  augment_string = 'augment'
277
317
  else:
278
318
  augment_string = ''
279
319
 
280
- cmd = 'yolo val {} model="{}" imgsz={} batch={} data="{}" project="{}" name="{}"'.format(
281
- augment_string,model_filename,image_size_string,options.batch_size,yolo_dataset_file,
282
- yolo_results_folder,'yolo_results')
283
- cmd += ' save_hybrid save_json'
320
+ cmd = 'yolo val {} model="{}" imgsz={} batch={} data="{}" project="{}" name="{}" device="{}"'.\
321
+ format(augment_string,model_filename,image_size_string,options.batch_size,
322
+ yolo_dataset_file,yolo_results_folder,'yolo_results',options.device_string)
323
+ cmd += ' save_json exist_ok'
324
+
325
+ if (options.half_precision_enabled is not None):
326
+ if options.half_precision_enabled == 1:
327
+ cmd += ' --half=True'
328
+ else:
329
+ assert options.half_precision_enabled == 0
330
+ cmd += ' --half=False'
331
+
332
+ # Sometimes useful for debugging
333
+ # cmd += ' save_conf save_txt'
284
334
 
285
335
  else:
286
336
 
@@ -293,38 +343,84 @@ def run_inference_with_yolo_val(options):
293
343
 
294
344
  if options.yolo_working_folder is not None:
295
345
  current_dir = os.getcwd()
296
- os.chdir(options.yolo_working_folder)
346
+ os.chdir(options.yolo_working_folder)
347
+
297
348
  print('Running YOLO inference command:\n{}\n'.format(cmd))
298
349
 
299
350
  if options.preview_yolo_command_only:
351
+
300
352
  if options.remove_symlink_folder:
301
353
  try:
354
+ print('Removing YOLO symlink folder {}'.format(symlink_folder))
302
355
  shutil.rmtree(symlink_folder)
303
356
  except Exception:
304
357
  print('Warning: error removing symlink folder {}'.format(symlink_folder))
305
358
  pass
306
359
  if options.remove_yolo_results_folder:
307
360
  try:
361
+ print('Removing YOLO results folder {}'.format(yolo_results_folder))
308
362
  shutil.rmtree(yolo_results_folder)
309
363
  except Exception:
310
364
  print('Warning: error removing YOLO results folder {}'.format(yolo_results_folder))
311
365
  pass
312
366
 
313
367
  sys.exit()
314
-
315
- execution_result = process_utils.execute_and_print(cmd)
368
+
369
+ execution_result = process_utils.execute_and_print(cmd,encoding='utf-8',verbose=True)
316
370
  assert execution_result['status'] == 0, 'Error running {}'.format(options.model_type)
317
371
  yolo_console_output = execution_result['output']
372
+
373
+ if options.save_yolo_debug_output:
374
+ with open(os.path.join(yolo_results_folder,'yolo_console_output.txt'),'w') as f:
375
+ for s in yolo_console_output:
376
+ f.write(s + '\n')
377
+ with open(os.path.join(yolo_results_folder,'image_id_to_file.json'),'w') as f:
378
+ json.dump(image_id_to_file,f,indent=1)
379
+ with open(os.path.join(yolo_results_folder,'image_id_to_error.json'),'w') as f:
380
+ json.dump(image_id_to_error,f,indent=1)
381
+
382
+
383
+ # YOLO console output contains lots of ANSI escape codes, remove them for easier parsing
384
+ yolo_console_output = [string_utils.remove_ansi_codes(s) for s in yolo_console_output]
318
385
 
386
+ # Find errors that occurred during the initial corruption check; these will not be included in the
387
+ # output. Errors that occur during inference will be handled separately.
319
388
  yolo_read_failures = []
389
+
320
390
  for line in yolo_console_output:
391
+ # Lines look like:
392
+ #
393
+ # For ultralytics val:
394
+ #
395
+ # val: WARNING ⚠️ /a/b/c/d.jpg: ignoring corrupt image/label: [Errno 13] Permission denied: '/a/b/c/d.jpg'
396
+ # line = "val: WARNING ⚠️ /a/b/c/d.jpg: ignoring corrupt image/label: [Errno 13] Permission denied: '/a/b/c/d.jpg'"
397
+ #
398
+ # For yolov5 val.py:
399
+ #
400
+ # test: WARNING: a/b/c/d.jpg: ignoring corrupt image/label: cannot identify image file '/a/b/c/d.jpg'
401
+ # line = "test: WARNING: a/b/c/d.jpg: ignoring corrupt image/label: cannot identify image file '/a/b/c/d.jpg'"
321
402
  if 'cannot identify image file' in line:
322
403
  tokens = line.split('cannot identify image file')
323
404
  image_name = tokens[-1].strip()
324
405
  assert image_name[0] == "'" and image_name [-1] == "'"
325
406
  image_name = image_name[1:-1]
326
407
  yolo_read_failures.append(image_name)
327
-
408
+ elif 'ignoring corrupt image/label' in line:
409
+ assert 'WARNING' in line
410
+ if '⚠️' in line:
411
+ assert line.startswith('val'), \
412
+ 'Unrecognized line in YOLO output: {}'.format(line)
413
+ tokens = line.split('ignoring corrupt image/label')
414
+ image_name = tokens[0].split('⚠️')[-1].strip()
415
+ else:
416
+ assert line.startswith('test'), \
417
+ 'Unrecognized line in YOLO output: {}'.format(line)
418
+ tokens = line.split('ignoring corrupt image/label')
419
+ image_name = tokens[0].split('WARNING:')[-1].strip()
420
+ assert image_name.endswith(':')
421
+ image_name = image_name[0:-1]
422
+ yolo_read_failures.append(image_name)
423
+
328
424
  # image_file = yolo_read_failures[0]
329
425
  for image_file in yolo_read_failures:
330
426
  image_id = os.path.splitext(os.path.basename(image_file))[0]
@@ -338,7 +434,7 @@ def run_inference_with_yolo_val(options):
338
434
 
339
435
  ##%% Convert results to MD format
340
436
 
341
- json_files = glob.glob(yolo_results_folder+ '/yolo_results/*.json')
437
+ json_files = glob.glob(yolo_results_folder + '/yolo_results/*.json')
342
438
  assert len(json_files) == 1
343
439
  yolo_json_file = json_files[0]
344
440
 
@@ -390,7 +486,7 @@ def run_inference_with_yolo_val(options):
390
486
 
391
487
  #%% Command-line driver
392
488
 
393
- import argparse,sys
489
+ import argparse
394
490
  from md_utils.ct_utils import args_to_object
395
491
 
396
492
  def main():
@@ -422,9 +518,12 @@ def main():
422
518
  parser.add_argument(
423
519
  '--batch_size', default=options.batch_size, type=int,
424
520
  help='inference batch size (default {})'.format(options.batch_size))
521
+ parser.add_argument(
522
+ '--half_precision_enabled', default=None, type=int,
523
+ help='use half-precision-inference (1 or 0) (default is the underlying model\'s default, probably full for YOLOv8 and half for YOLOv5')
425
524
  parser.add_argument(
426
525
  '--device_string', default=options.device_string, type=str,
427
- help='CUDA device specifier, e.g. "0" or "cpu" (default {})'.format(options.device_string))
526
+ help='CUDA device specifier, typically "0" or "1" for CUDA devices, "mps" for M1/M2 devices, or "cpu" (default {})'.format(options.device_string))
428
527
  parser.add_argument(
429
528
  '--overwrite_handling', default=options.overwrite_handling, type=str,
430
529
  help='action to take if the output file exists (skip, error, overwrite) (default {})'.format(
@@ -435,7 +534,7 @@ def main():
435
534
  '(otherwise defaults to MD categories)')
436
535
  parser.add_argument(
437
536
  '--model_type', default=options.model_type, type=str,
438
- help='Model type (yolov5 or yolov8) (default {})'.format(options.model_type))
537
+ help='Model type ("yolov5" or "ultralytics" ("yolov8" behaves the same as "ultralytics")) (default {})'.format(options.model_type))
439
538
 
440
539
  parser.add_argument(
441
540
  '--symlink_folder', type=str,
@@ -452,6 +551,13 @@ def main():
452
551
  parser.add_argument(
453
552
  '--no_remove_yolo_results_folder', action='store_true',
454
553
  help='don\'t remove the temporary folder full of YOLO intermediate files')
554
+ parser.add_argument(
555
+ '--save_yolo_debug_output', action='store_true',
556
+ help='write yolo console output to a text file in the results folder, along with additional debug files')
557
+
558
+ parser.add_argument(
559
+ '--nonrecursive', action='store_true',
560
+ help='Disable recursive folder processing')
455
561
 
456
562
  parser.add_argument(
457
563
  '--preview_yolo_command_only', action='store_true',
@@ -474,14 +580,15 @@ def main():
474
580
 
475
581
  # If the caller hasn't specified an image size, choose one based on whether augmentation
476
582
  # is enabled.
477
- if args.image_size is None:
478
- assert options.augment in (0,1)
479
- if options.augment == 1:
583
+ if args.image_size is None:
584
+ assert args.augment_enabled in (0,1), \
585
+ 'Illegal augment_enabled value {}'.format(args.augment_enabled)
586
+ if args.augment_enabled == 1:
480
587
  args.image_size = default_image_size_with_augmentation
481
588
  else:
482
589
  args.image_size = default_image_size_with_no_augmentation
483
590
  augment_enabled_string = 'enabled'
484
- if not options.augment:
591
+ if not args.augment_enabled:
485
592
  augment_enabled_string = 'disabled'
486
593
  print('Augmentation is {}, using default image size {}'.format(
487
594
  augment_enabled_string,args.image_size))
@@ -491,6 +598,7 @@ def main():
491
598
  if args.yolo_dataset_file is not None:
492
599
  options.yolo_category_id_to_name = args.yolo_dataset_file
493
600
 
601
+ options.recursive = (not options.nonrecursive)
494
602
  options.remove_symlink_folder = (not options.no_remove_symlink_folder)
495
603
  options.remove_yolo_results_folder = (not options.no_remove_yolo_results_folder)
496
604
  options.use_symlinks = (not options.no_use_symlinks)