megadetector 5.0.5__py3-none-any.whl → 5.0.7__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 (132) hide show
  1. api/batch_processing/data_preparation/manage_local_batch.py +302 -263
  2. api/batch_processing/data_preparation/manage_video_batch.py +81 -2
  3. api/batch_processing/postprocessing/add_max_conf.py +1 -0
  4. api/batch_processing/postprocessing/categorize_detections_by_size.py +50 -19
  5. api/batch_processing/postprocessing/compare_batch_results.py +110 -60
  6. api/batch_processing/postprocessing/load_api_results.py +56 -70
  7. api/batch_processing/postprocessing/md_to_coco.py +1 -1
  8. api/batch_processing/postprocessing/md_to_labelme.py +2 -1
  9. api/batch_processing/postprocessing/postprocess_batch_results.py +240 -81
  10. api/batch_processing/postprocessing/render_detection_confusion_matrix.py +625 -0
  11. api/batch_processing/postprocessing/repeat_detection_elimination/find_repeat_detections.py +71 -23
  12. api/batch_processing/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +1 -1
  13. api/batch_processing/postprocessing/repeat_detection_elimination/repeat_detections_core.py +227 -75
  14. api/batch_processing/postprocessing/subset_json_detector_output.py +132 -5
  15. api/batch_processing/postprocessing/top_folders_to_bottom.py +1 -1
  16. api/synchronous/api_core/animal_detection_api/detection/run_detector_batch.py +2 -2
  17. classification/prepare_classification_script.py +191 -191
  18. data_management/coco_to_yolo.py +68 -45
  19. data_management/databases/integrity_check_json_db.py +7 -5
  20. data_management/generate_crops_from_cct.py +3 -3
  21. data_management/get_image_sizes.py +8 -6
  22. data_management/importers/add_timestamps_to_icct.py +79 -0
  23. data_management/importers/animl_results_to_md_results.py +160 -0
  24. data_management/importers/auckland_doc_test_to_json.py +4 -4
  25. data_management/importers/auckland_doc_to_json.py +1 -1
  26. data_management/importers/awc_to_json.py +5 -5
  27. data_management/importers/bellevue_to_json.py +5 -5
  28. data_management/importers/carrizo_shrubfree_2018.py +5 -5
  29. data_management/importers/carrizo_trail_cam_2017.py +5 -5
  30. data_management/importers/cct_field_adjustments.py +2 -3
  31. data_management/importers/channel_islands_to_cct.py +4 -4
  32. data_management/importers/ena24_to_json.py +5 -5
  33. data_management/importers/helena_to_cct.py +10 -10
  34. data_management/importers/idaho-camera-traps.py +12 -12
  35. data_management/importers/idfg_iwildcam_lila_prep.py +8 -8
  36. data_management/importers/jb_csv_to_json.py +4 -4
  37. data_management/importers/missouri_to_json.py +1 -1
  38. data_management/importers/noaa_seals_2019.py +1 -1
  39. data_management/importers/pc_to_json.py +5 -5
  40. data_management/importers/prepare-noaa-fish-data-for-lila.py +4 -4
  41. data_management/importers/prepare_zsl_imerit.py +5 -5
  42. data_management/importers/rspb_to_json.py +4 -4
  43. data_management/importers/save_the_elephants_survey_A.py +5 -5
  44. data_management/importers/save_the_elephants_survey_B.py +6 -6
  45. data_management/importers/snapshot_safari_importer.py +9 -9
  46. data_management/importers/snapshot_serengeti_lila.py +9 -9
  47. data_management/importers/timelapse_csv_set_to_json.py +5 -7
  48. data_management/importers/ubc_to_json.py +4 -4
  49. data_management/importers/umn_to_json.py +4 -4
  50. data_management/importers/wellington_to_json.py +1 -1
  51. data_management/importers/wi_to_json.py +2 -2
  52. data_management/importers/zamba_results_to_md_results.py +181 -0
  53. data_management/labelme_to_coco.py +35 -7
  54. data_management/labelme_to_yolo.py +229 -0
  55. data_management/lila/add_locations_to_island_camera_traps.py +1 -1
  56. data_management/lila/add_locations_to_nacti.py +147 -0
  57. data_management/lila/create_lila_blank_set.py +474 -0
  58. data_management/lila/create_lila_test_set.py +2 -1
  59. data_management/lila/create_links_to_md_results_files.py +106 -0
  60. data_management/lila/download_lila_subset.py +46 -21
  61. data_management/lila/generate_lila_per_image_labels.py +23 -14
  62. data_management/lila/get_lila_annotation_counts.py +17 -11
  63. data_management/lila/lila_common.py +14 -11
  64. data_management/lila/test_lila_metadata_urls.py +116 -0
  65. data_management/ocr_tools.py +829 -0
  66. data_management/resize_coco_dataset.py +13 -11
  67. data_management/yolo_output_to_md_output.py +84 -12
  68. data_management/yolo_to_coco.py +38 -20
  69. detection/process_video.py +36 -14
  70. detection/pytorch_detector.py +23 -8
  71. detection/run_detector.py +76 -19
  72. detection/run_detector_batch.py +178 -63
  73. detection/run_inference_with_yolov5_val.py +326 -57
  74. detection/run_tiled_inference.py +153 -43
  75. detection/video_utils.py +34 -8
  76. md_utils/ct_utils.py +172 -1
  77. md_utils/md_tests.py +372 -51
  78. md_utils/path_utils.py +167 -39
  79. md_utils/process_utils.py +26 -7
  80. md_utils/split_locations_into_train_val.py +215 -0
  81. md_utils/string_utils.py +10 -0
  82. md_utils/url_utils.py +0 -2
  83. md_utils/write_html_image_list.py +9 -26
  84. md_visualization/plot_utils.py +12 -8
  85. md_visualization/visualization_utils.py +106 -7
  86. md_visualization/visualize_db.py +16 -8
  87. md_visualization/visualize_detector_output.py +208 -97
  88. {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/METADATA +3 -6
  89. {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/RECORD +98 -121
  90. {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/WHEEL +1 -1
  91. taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +1 -1
  92. taxonomy_mapping/map_new_lila_datasets.py +43 -39
  93. taxonomy_mapping/prepare_lila_taxonomy_release.py +5 -2
  94. taxonomy_mapping/preview_lila_taxonomy.py +27 -27
  95. taxonomy_mapping/species_lookup.py +33 -13
  96. taxonomy_mapping/taxonomy_csv_checker.py +7 -5
  97. api/synchronous/api_core/yolov5/detect.py +0 -252
  98. api/synchronous/api_core/yolov5/export.py +0 -607
  99. api/synchronous/api_core/yolov5/hubconf.py +0 -146
  100. api/synchronous/api_core/yolov5/models/__init__.py +0 -0
  101. api/synchronous/api_core/yolov5/models/common.py +0 -738
  102. api/synchronous/api_core/yolov5/models/experimental.py +0 -104
  103. api/synchronous/api_core/yolov5/models/tf.py +0 -574
  104. api/synchronous/api_core/yolov5/models/yolo.py +0 -338
  105. api/synchronous/api_core/yolov5/train.py +0 -670
  106. api/synchronous/api_core/yolov5/utils/__init__.py +0 -36
  107. api/synchronous/api_core/yolov5/utils/activations.py +0 -103
  108. api/synchronous/api_core/yolov5/utils/augmentations.py +0 -284
  109. api/synchronous/api_core/yolov5/utils/autoanchor.py +0 -170
  110. api/synchronous/api_core/yolov5/utils/autobatch.py +0 -66
  111. api/synchronous/api_core/yolov5/utils/aws/__init__.py +0 -0
  112. api/synchronous/api_core/yolov5/utils/aws/resume.py +0 -40
  113. api/synchronous/api_core/yolov5/utils/benchmarks.py +0 -148
  114. api/synchronous/api_core/yolov5/utils/callbacks.py +0 -71
  115. api/synchronous/api_core/yolov5/utils/dataloaders.py +0 -1087
  116. api/synchronous/api_core/yolov5/utils/downloads.py +0 -178
  117. api/synchronous/api_core/yolov5/utils/flask_rest_api/example_request.py +0 -19
  118. api/synchronous/api_core/yolov5/utils/flask_rest_api/restapi.py +0 -46
  119. api/synchronous/api_core/yolov5/utils/general.py +0 -1018
  120. api/synchronous/api_core/yolov5/utils/loggers/__init__.py +0 -187
  121. api/synchronous/api_core/yolov5/utils/loggers/wandb/__init__.py +0 -0
  122. api/synchronous/api_core/yolov5/utils/loggers/wandb/log_dataset.py +0 -27
  123. api/synchronous/api_core/yolov5/utils/loggers/wandb/sweep.py +0 -41
  124. api/synchronous/api_core/yolov5/utils/loggers/wandb/wandb_utils.py +0 -577
  125. api/synchronous/api_core/yolov5/utils/loss.py +0 -234
  126. api/synchronous/api_core/yolov5/utils/metrics.py +0 -355
  127. api/synchronous/api_core/yolov5/utils/plots.py +0 -489
  128. api/synchronous/api_core/yolov5/utils/torch_utils.py +0 -314
  129. api/synchronous/api_core/yolov5/val.py +0 -394
  130. md_utils/matlab_porting_tools.py +0 -97
  131. {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/LICENSE +0 -0
  132. {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/top_level.txt +0 -0
@@ -38,6 +38,7 @@
38
38
  #%% Imports
39
39
 
40
40
  import os
41
+ import sys
41
42
  import uuid
42
43
  import glob
43
44
  import tempfile
@@ -48,7 +49,12 @@ from tqdm import tqdm
48
49
 
49
50
  from md_utils import path_utils
50
51
  from md_utils import process_utils
52
+ from md_utils import string_utils
51
53
  from data_management import yolo_output_to_md_output
54
+ from detection.run_detector import try_download_known_detector
55
+
56
+ default_image_size_with_augmentation = int(1280 * 1.3)
57
+ default_image_size_with_no_augmentation = 1280
52
58
 
53
59
 
54
60
  #%% Options class
@@ -59,17 +65,24 @@ class YoloInferenceOptions:
59
65
 
60
66
  input_folder = None
61
67
  model_filename = None
62
- yolo_working_folder = None
63
68
  output_file = None
64
-
69
+
65
70
  ## Optional ##
66
71
 
67
- image_size = 1280 * 1.3
72
+ # Required for older YOLOv5 inference, not for newer ulytralytics inference
73
+ yolo_working_folder = None
74
+
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'
78
+
79
+ image_size = default_image_size_with_augmentation
68
80
  conf_thres = '0.001'
69
81
  batch_size = 1
70
82
  device_string = '0'
71
83
  augment = True
72
-
84
+ half_precision_enabled = None
85
+
73
86
  symlink_folder = None
74
87
  use_symlinks = True
75
88
 
@@ -80,24 +93,54 @@ class YoloInferenceOptions:
80
93
 
81
94
  # These are deliberately offset from the standard MD categories; YOLOv5
82
95
  # needs categories IDs to start at 0.
96
+ #
97
+ # This can also be a string that points to a YOLOv5 dataset.yaml file.
83
98
  yolo_category_id_to_name = {0:'animal',1:'person',2:'vehicle'}
84
99
 
85
100
  # 'error','skip','overwrite'
86
101
  overwrite_handling = 'skip'
102
+
103
+ preview_yolo_command_only = False
104
+
105
+ treat_copy_failures_as_warnings = False
106
+
107
+ save_yolo_debug_output = False
87
108
 
88
109
 
89
110
  #%% Main function
90
111
 
91
112
  def run_inference_with_yolo_val(options):
92
113
 
93
- ##%% Path handling
114
+ ##%% Input and path handling
115
+
116
+ if options.model_type == 'yolov8':
117
+
118
+ print('Warning: model type "yolov8" supplied, "ultralytics" is the preferred model type string for YOLOv8 models')
119
+ options.model_type = 'ultralytics'
120
+
121
+ if (options.model_type == 'yolov5') and ('yolov8' in options.model_filename.lower()):
122
+ 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')
94
123
 
124
+ if options.yolo_working_folder is None:
125
+ assert options.model_type == 'ultralytics', \
126
+ 'A working folder is required to run YOLOv5 val.py'
127
+ else:
128
+ assert os.path.isdir(options.yolo_working_folder), \
129
+ 'Could not find working folder {}'.format(options.yolo_working_folder)
130
+
95
131
  assert os.path.isdir(options.input_folder) or os.path.isfile(options.input_folder), \
96
132
  'Could not find input {}'.format(options.input_folder)
97
- assert os.path.isdir(options.yolo_working_folder), \
98
- 'Could not find working folder {}'.format(options.yolo_working_folder)
99
- assert os.path.isfile(options.model_filename), \
100
- 'Could not find model file {}'.format(options.model_filename)
133
+
134
+ if options.half_precision_enabled is not None:
135
+ assert options.half_precision_enabled in (0,1), \
136
+ 'Invalid value {} for --half_precision_enabled (should be 0 or 1)'.format(
137
+ options.half_precision_enabled)
138
+
139
+ # If the model filename is a known model string (e.g. "MDv5A", download the model if necessary)
140
+ model_filename = try_download_known_detector(options.model_filename)
141
+
142
+ assert os.path.isfile(model_filename), \
143
+ 'Could not find model file {}'.format(model_filename)
101
144
 
102
145
  if os.path.exists(options.output_file):
103
146
  if options.overwrite_handling == 'skip':
@@ -112,6 +155,17 @@ def run_inference_with_yolo_val(options):
112
155
 
113
156
  os.makedirs(os.path.dirname(options.output_file),exist_ok=True)
114
157
 
158
+
159
+ ##%% Other input handling
160
+
161
+ if isinstance(options.yolo_category_id_to_name,str):
162
+ assert os.path.isfile(options.yolo_category_id_to_name)
163
+ yolo_dataset_file = options.yolo_category_id_to_name
164
+ options.yolo_category_id_to_name = \
165
+ yolo_output_to_md_output.read_classes_from_yolo_dataset_file(yolo_dataset_file)
166
+ print('Loaded {} category mappings from {}'.format(
167
+ len(options.yolo_category_id_to_name),yolo_dataset_file))
168
+
115
169
  temporary_folder = None
116
170
  symlink_folder_is_temp_folder = False
117
171
  yolo_folder_is_temp_folder = False
@@ -185,24 +239,38 @@ def run_inference_with_yolo_val(options):
185
239
  else:
186
240
  shutil.copyfile(image_fn,symlink_full_path)
187
241
  except Exception as e:
188
- image_id_to_error[image_id] = str(e)
189
- print('Warning: error copying/creating link for input file {}: {}'.format(
190
- image_fn,str(e)))
191
- continue
242
+ error_string = str(e)
243
+ image_id_to_error[image_id] = error_string
244
+ # Always break if the user is trying to create symlinks on Windows without
245
+ # permission, 100% of images will always fail in this case.
246
+ if ('a required privilege is not held by the client' in error_string.lower()) or \
247
+ (not options.treat_copy_failures_as_warnings):
248
+ print('\nError copying/creating link for input file {}: {}'.format(
249
+ image_fn,error_string))
250
+
251
+ raise
252
+ else:
253
+ print('Warning: error copying/creating link for input file {}: {}'.format(
254
+ image_fn,error_string))
255
+ continue
192
256
 
193
257
  # ...for each image
194
258
 
195
259
 
196
- ##%% Create the dataset file
260
+ ##%% Create the dataset file if necessary
261
+
262
+ # This may have been passed in as a string, but at this point, we should have
263
+ # loaded the dataset file.
264
+ assert isinstance(options.yolo_category_id_to_name,dict)
197
265
 
198
266
  # Category IDs need to be continuous integers starting at 0
199
267
  category_ids = sorted(list(options.yolo_category_id_to_name.keys()))
200
268
  assert category_ids[0] == 0
201
269
  assert len(category_ids) == 1 + category_ids[-1]
202
270
 
203
- dataset_file = os.path.join(yolo_results_folder,'dataset.yaml')
271
+ yolo_dataset_file = os.path.join(yolo_results_folder,'dataset.yaml')
204
272
 
205
- with open(dataset_file,'w') as f:
273
+ with open(yolo_dataset_file,'w') as f:
206
274
  f.write('path: {}\n'.format(symlink_folder_inner))
207
275
  f.write('train: .\n')
208
276
  f.write('val: .\n')
@@ -217,50 +285,154 @@ def run_inference_with_yolo_val(options):
217
285
  options.yolo_category_id_to_name[category_id]))
218
286
 
219
287
 
220
- ##%% Prepare YOLOv5 command
288
+ ##%% Prepare Python command or YOLO CLI command
221
289
 
222
290
  image_size_string = str(round(options.image_size))
223
- cmd = 'python val.py --data "{}"'.format(dataset_file)
224
- cmd += ' --weights "{}"'.format(options.model_filename)
225
- cmd += ' --batch-size {} --imgsz {} --conf-thres {} --task test'.format(
226
- options.batch_size,image_size_string,options.conf_thres)
227
- cmd += ' --device "{}" --save-json'.format(options.device_string)
228
- cmd += ' --project "{}" --name "{}" --exist-ok'.format(yolo_results_folder,'yolo_results')
229
291
 
230
- if options.augment:
231
- cmd += ' --augment'
292
+ if options.model_type == 'yolov5':
293
+
294
+ cmd = 'python val.py --task test --data "{}"'.format(yolo_dataset_file)
295
+ cmd += ' --weights "{}"'.format(model_filename)
296
+ cmd += ' --batch-size {} --imgsz {} --conf-thres {}'.format(
297
+ options.batch_size,image_size_string,options.conf_thres)
298
+ cmd += ' --device "{}" --save-json'.format(options.device_string)
299
+ cmd += ' --project "{}" --name "{}" --exist-ok'.format(yolo_results_folder,'yolo_results')
300
+
301
+ if options.augment:
302
+ cmd += ' --augment'
303
+
304
+ # --half is a store_true argument for YOLOv5's val.py
305
+ if (options.half_precision_enabled is not None) and (options.half_precision_enabled == 1):
306
+ cmd += ' --half'
307
+
308
+ # Sometimes useful for debugging
309
+ # cmd += ' --save_conf --save_txt'
310
+
311
+ elif options.model_type == 'ultralytics':
312
+
313
+ if options.augment:
314
+ augment_string = 'augment'
315
+ else:
316
+ augment_string = ''
317
+
318
+ cmd = 'yolo val {} model="{}" imgsz={} batch={} data="{}" project="{}" name="{}" device="{}"'.\
319
+ format(augment_string,model_filename,image_size_string,options.batch_size,
320
+ yolo_dataset_file,yolo_results_folder,'yolo_results',options.device_string)
321
+ cmd += ' save_json exist_ok'
322
+
323
+ if (options.half_precision_enabled is not None):
324
+ if options.half_precision_enabled == 1:
325
+ cmd += ' --half=True'
326
+ else:
327
+ assert options.half_precision_enabled == 0
328
+ cmd += ' --half=False'
329
+
330
+ # Sometimes useful for debugging
331
+ # cmd += ' save_conf save_txt'
332
+
333
+ else:
334
+
335
+ raise ValueError('Unrecognized model type {}'.format(options.model_type))
336
+
337
+ # print(cmd); import clipboard; clipboard.copy(cmd)
338
+
232
339
 
340
+ ##%% Run YOLO command
341
+
342
+ if options.yolo_working_folder is not None:
343
+ current_dir = os.getcwd()
344
+ os.chdir(options.yolo_working_folder)
233
345
 
234
- ##%% Run YOLOv5 command
346
+ print('Running YOLO inference command:\n{}\n'.format(cmd))
235
347
 
236
- current_dir = os.getcwd()
237
- os.chdir(options.yolo_working_folder)
238
- execution_result = process_utils.execute_and_print(cmd)
239
- assert execution_result['status'] == 0, 'Error running YOLOv5'
348
+ if options.preview_yolo_command_only:
349
+
350
+ if options.remove_symlink_folder:
351
+ try:
352
+ print('Removing YOLO symlink folder {}'.format(symlink_folder))
353
+ shutil.rmtree(symlink_folder)
354
+ except Exception:
355
+ print('Warning: error removing symlink folder {}'.format(symlink_folder))
356
+ pass
357
+ if options.remove_yolo_results_folder:
358
+ try:
359
+ print('Removing YOLO results folder {}'.format(yolo_results_folder))
360
+ shutil.rmtree(yolo_results_folder)
361
+ except Exception:
362
+ print('Warning: error removing YOLO results folder {}'.format(yolo_results_folder))
363
+ pass
364
+
365
+ sys.exit()
366
+
367
+ execution_result = process_utils.execute_and_print(cmd,encoding='utf-8',verbose=True)
368
+ assert execution_result['status'] == 0, 'Error running {}'.format(options.model_type)
240
369
  yolo_console_output = execution_result['output']
370
+
371
+ if options.save_yolo_debug_output:
372
+ with open(os.path.join(yolo_results_folder,'yolo_console_output.txt'),'w') as f:
373
+ for s in yolo_console_output:
374
+ f.write(s + '\n')
375
+ with open(os.path.join(yolo_results_folder,'image_id_to_file.json'),'w') as f:
376
+ json.dump(image_id_to_file,f,indent=1)
377
+ with open(os.path.join(yolo_results_folder,'image_id_to_error.json'),'w') as f:
378
+ json.dump(image_id_to_error,f,indent=1)
379
+
380
+
381
+ # YOLO console output contains lots of ANSI escape codes, remove them for easier parsing
382
+ yolo_console_output = [string_utils.remove_ansi_codes(s) for s in yolo_console_output]
241
383
 
384
+ # Find errors that occrred during the initial corruption check; these will not be included in the
385
+ # output. Errors that occur during inference will be handled separately.
242
386
  yolo_read_failures = []
387
+
243
388
  for line in yolo_console_output:
389
+ # Lines look like:
390
+ #
391
+ # For ultralytics val:
392
+ #
393
+ # val: WARNING ⚠️ /a/b/c/d.jpg: ignoring corrupt image/label: [Errno 13] Permission denied: '/a/b/c/d.jpg'
394
+ # line = "val: WARNING ⚠️ /a/b/c/d.jpg: ignoring corrupt image/label: [Errno 13] Permission denied: '/a/b/c/d.jpg'"
395
+ #
396
+ # For yolov5 val.py:
397
+ #
398
+ # test: WARNING: a/b/c/d.jpg: ignoring corrupt image/label: cannot identify image file '/a/b/c/d.jpg'
399
+ # line = "test: WARNING: a/b/c/d.jpg: ignoring corrupt image/label: cannot identify image file '/a/b/c/d.jpg'"
244
400
  if 'cannot identify image file' in line:
245
401
  tokens = line.split('cannot identify image file')
246
402
  image_name = tokens[-1].strip()
247
403
  assert image_name[0] == "'" and image_name [-1] == "'"
248
404
  image_name = image_name[1:-1]
249
405
  yolo_read_failures.append(image_name)
250
-
406
+ elif 'ignoring corrupt image/label' in line:
407
+ assert 'WARNING' in line
408
+ if '⚠️' in line:
409
+ assert line.startswith('val'), \
410
+ 'Unrecognized line in YOLO output: {}'.format(line)
411
+ tokens = line.split('ignoring corrupt image/label')
412
+ image_name = tokens[0].split('⚠️')[-1].strip()
413
+ else:
414
+ assert line.startswith('test'), \
415
+ 'Unrecognized line in YOLO output: {}'.format(line)
416
+ tokens = line.split('ignoring corrupt image/label')
417
+ image_name = tokens[0].split('WARNING:')[-1].strip()
418
+ assert image_name.endswith(':')
419
+ image_name = image_name[0:-1]
420
+ yolo_read_failures.append(image_name)
421
+
251
422
  # image_file = yolo_read_failures[0]
252
423
  for image_file in yolo_read_failures:
253
424
  image_id = os.path.splitext(os.path.basename(image_file))[0]
254
425
  assert image_id in image_id_to_file
255
426
  if image_id not in image_id_to_error:
256
- image_id_to_error[image_id] = 'YOLOv5 read failure'
427
+ image_id_to_error[image_id] = 'YOLO read failure'
257
428
 
258
- os.chdir(current_dir)
429
+ if options.yolo_working_folder is not None:
430
+ os.chdir(current_dir)
259
431
 
260
432
 
261
433
  ##%% Convert results to MD format
262
434
 
263
- json_files = glob.glob(yolo_results_folder+ '/yolo_results/*.json')
435
+ json_files = glob.glob(yolo_results_folder + '/yolo_results/*.json')
264
436
  assert len(json_files) == 1
265
437
  yolo_json_file = json_files[0]
266
438
 
@@ -288,7 +460,7 @@ def run_inference_with_yolo_val(options):
288
460
  image_folder=image_base,
289
461
  output_file=options.output_file,
290
462
  yolo_category_id_to_name=options.yolo_category_id_to_name,
291
- detector_name=os.path.basename(options.model_filename),
463
+ detector_name=os.path.basename(model_filename),
292
464
  image_id_to_relative_path=image_id_to_relative_path,
293
465
  image_id_to_error=image_id_to_error)
294
466
 
@@ -312,7 +484,7 @@ def run_inference_with_yolo_val(options):
312
484
 
313
485
  #%% Command-line driver
314
486
 
315
- import argparse,sys
487
+ import argparse
316
488
  from md_utils.ct_utils import args_to_object
317
489
 
318
490
  def main():
@@ -329,14 +501,14 @@ def main():
329
501
  parser.add_argument(
330
502
  'output_file',type=str,
331
503
  help='.json file where output will be written')
332
- parser.add_argument(
333
- 'yolo_working_folder',type=str,
334
- help='folder in which to execute val.py')
335
504
 
336
505
  parser.add_argument(
337
- '--image_size', default=options.image_size, type=int,
338
- help='image size for model execution (default {})'.format(
339
- options.image_size))
506
+ '--yolo_working_folder',type=str,default=None,
507
+ help='folder in which to execute val.py (not necessary for YOLOv8 inference)')
508
+ parser.add_argument(
509
+ '--image_size', default=None, type=int,
510
+ help='image size for model execution (default {} when augmentation is enabled, else {})'.format(
511
+ default_image_size_with_augmentation,default_image_size_with_no_augmentation))
340
512
  parser.add_argument(
341
513
  '--conf_thres', default=options.conf_thres, type=float,
342
514
  help='confidence threshold for including detections in the output file (default {})'.format(
@@ -344,13 +516,23 @@ def main():
344
516
  parser.add_argument(
345
517
  '--batch_size', default=options.batch_size, type=int,
346
518
  help='inference batch size (default {})'.format(options.batch_size))
519
+ parser.add_argument(
520
+ '--half_precision_enabled', default=None, type=int,
521
+ help='use half-precision-inference (1 or 0) (default is the underlying model\'s default, probably half for YOLOv8 and full for YOLOv8')
347
522
  parser.add_argument(
348
523
  '--device_string', default=options.device_string, type=str,
349
- help='CUDA device specifier, e.g. "0" or "cpu" (default {})'.format(options.device_string))
524
+ help='CUDA device specifier, typically "0" or "1" for CUDA devices, "mps" for M1/M2 devices, or "cpu" (default {})'.format(options.device_string))
350
525
  parser.add_argument(
351
526
  '--overwrite_handling', default=options.overwrite_handling, type=str,
352
527
  help='action to take if the output file exists (skip, error, overwrite) (default {})'.format(
353
- options.overwrite_handling) )
528
+ options.overwrite_handling))
529
+ parser.add_argument(
530
+ '--yolo_dataset_file', default=None, type=str,
531
+ help='YOLOv5 dataset.yml file from which we should load category information ' + \
532
+ '(otherwise defaults to MD categories)')
533
+ parser.add_argument(
534
+ '--model_type', default=options.model_type, type=str,
535
+ help='Model type ("yolov5" or "ultralytics" ("yolov8" behaves the same as "ultralytics")) (default {})'.format(options.model_type))
354
536
 
355
537
  parser.add_argument(
356
538
  '--symlink_folder', type=str,
@@ -367,11 +549,19 @@ def main():
367
549
  parser.add_argument(
368
550
  '--no_remove_yolo_results_folder', action='store_true',
369
551
  help='don\'t remove the temporary folder full of YOLO intermediate files')
552
+ parser.add_argument(
553
+ '--save_yolo_debug_output', action='store_true',
554
+ help='write yolo console output to a text file in the results folder, along with additional debug files')
555
+
556
+ parser.add_argument(
557
+ '--preview_yolo_command_only', action='store_true',
558
+ help='don\'t run inference, just preview the YOLO inference command (still creates symlinks)')
370
559
 
371
560
  if options.augment:
372
561
  default_augment_enabled = 1
373
562
  else:
374
563
  default_augment_enabled = 0
564
+
375
565
  parser.add_argument(
376
566
  '--augment_enabled', default=default_augment_enabled, type=int,
377
567
  help='enable/disable augmentation (default {})'.format(default_augment_enabled))
@@ -381,8 +571,27 @@ def main():
381
571
  parser.exit()
382
572
 
383
573
  args = parser.parse_args()
384
-
574
+
575
+ # If the caller hasn't specified an image size, choose one based on whether augmentation
576
+ # is enabled.
577
+ if args.image_size is None:
578
+ assert args.augment_enabled in (0,1), \
579
+ 'Illegal augment_enabled value {}'.format(args.augment_enabled)
580
+ if args.augment_enabled == 1:
581
+ args.image_size = default_image_size_with_augmentation
582
+ else:
583
+ args.image_size = default_image_size_with_no_augmentation
584
+ augment_enabled_string = 'enabled'
585
+ if not args.augment_enabled:
586
+ augment_enabled_string = 'disabled'
587
+ print('Augmentation is {}, using default image size {}'.format(
588
+ augment_enabled_string,args.image_size))
589
+
385
590
  args_to_object(args, options)
591
+
592
+ if args.yolo_dataset_file is not None:
593
+ options.yolo_category_id_to_name = args.yolo_dataset_file
594
+
386
595
  options.remove_symlink_folder = (not options.no_remove_symlink_folder)
387
596
  options.remove_yolo_results_folder = (not options.no_remove_yolo_results_folder)
388
597
  options.use_symlinks = (not options.no_use_symlinks)
@@ -400,16 +609,6 @@ if __name__ == '__main__':
400
609
  #%% Scrap
401
610
 
402
611
  if False:
403
-
404
- #%% Run from a set of options
405
-
406
- options = YoloInferenceOptions()
407
-
408
- args = {'augment': 1, 'batch_size': 1, 'conf_thres': 0.005, 'device_string': '1', 'image_size': 1664.0, 'input_folder': '/home/user/postprocessing/usgs-kissel/usgs-kissel-2023-09-11-aug-v5a.0.0/chunk031.json', 'model_filename': '/home/user/models/camera_traps/megadetector/md_v5.0.0/md_v5a.0.0.pt', 'output_file': '/home/user/postprocessing/usgs-kissel/usgs-kissel-2023-09-11-aug-v5a.0.0/chunk031_results.json', 'overwrite_handling': 'skip', 'symlink_folder': '/home/user/postprocessing/usgs-kissel/usgs-kissel-2023-09-11-aug-v5a.0.0/symlinks/symlinks_031', 'yolo_results_folder': '/home/user/postprocessing/usgs-kissel/usgs-kissel-2023-09-11-aug-v5a.0.0/yolo_results/yolo_results_031', 'yolo_working_folder': '/home/user/git/yolov5', 'remove_symlink_folder': True, 'remove_yolo_results_folder': True, 'use_symlinks': False, 'augment': True}
409
-
410
- for k in args:
411
- setattr(options, k, args[k])
412
-
413
612
 
414
613
  #%% Test driver (folder)
415
614
 
@@ -452,8 +651,10 @@ if False:
452
651
  options.remove_temporary_symlink_folder = False
453
652
  options.remove_yolo_results_file = False
454
653
 
455
- cmd = f'python run_inference_with_yolov5_val.py {model_filename} {input_folder} {output_file} {yolo_working_folder} ' + \
456
- f' --image_size {options.image_size} --conf_thres {options.conf_thres} --batch_size {options.batch_size} ' + \
654
+ cmd = f'python run_inference_with_yolov5_val.py {model_filename} {input_folder} ' + \
655
+ f'{output_file} --yolo_working_folder {yolo_working_folder} ' + \
656
+ f' --image_size {options.image_size} --conf_thres {options.conf_thres} ' + \
657
+ f' --batch_size {options.batch_size} ' + \
457
658
  f' --symlink_folder {options.symlink_folder} --yolo_results_folder {options.yolo_results_folder} ' + \
458
659
  ' --no_remove_symlink_folder --no_remove_yolo_results_folder'
459
660
 
@@ -470,6 +671,74 @@ if False:
470
671
  import clipboard; clipboard.copy(cmd)
471
672
 
472
673
 
674
+ #%% Test driver (folder) (YOLOv8 model)
675
+
676
+ project_name = 'yolov8-inference-test'
677
+ input_folder = os.path.expanduser('~/data/usgs-kissel-training-resized/val')
678
+ dataset_file = os.path.expanduser('~/data/usgs-kissel-training-yolo/dataset.yaml')
679
+ output_folder = os.path.expanduser(f'~/tmp/{project_name}')
680
+ model_filename = os.path.expanduser(
681
+ '~/models/usgs-tegus/usgs-tegus-yolov8x-2023.10.25-b-1-img640-e200-best.pt')
682
+ model_name = os.path.splitext(os.path.basename(model_filename))[0]
683
+
684
+ assert os.path.isdir(input_folder)
685
+ assert os.path.isfile(dataset_file)
686
+ assert os.path.isfile(model_filename)
687
+
688
+ symlink_folder = os.path.join(output_folder,'symlinks')
689
+ yolo_results_folder = os.path.join(output_folder,'yolo_results')
690
+
691
+ output_file = os.path.join(output_folder,'{}_{}-md_format.json'.format(
692
+ project_name,model_name))
693
+
694
+ options = YoloInferenceOptions()
695
+
696
+ options.model_type = 'yolov8'
697
+ options.yolo_category_id_to_name = dataset_file
698
+ options.yolo_working_folder = None
699
+ options.output_file = output_file
700
+
701
+ options.augment = False
702
+ options.conf_thres = '0.001'
703
+ options.batch_size = 1
704
+ options.device_string = '0'
705
+
706
+ if options.augment:
707
+ options.image_size = round(640 * 1.3)
708
+ else:
709
+ options.image_size = 640
710
+
711
+ options.input_folder = input_folder
712
+ options.model_filename = model_filename
713
+
714
+ options.yolo_results_folder = yolo_results_folder
715
+ options.symlink_folder = symlink_folder
716
+ options.use_symlinks = False
717
+
718
+ options.remove_temporary_symlink_folder = False
719
+ options.remove_yolo_results_file = False
720
+
721
+ cmd = f'python run_inference_with_yolov5_val.py {model_filename} ' + \
722
+ f'{input_folder} {output_file}' + \
723
+ f' --image_size {options.image_size} --conf_thres {options.conf_thres} ' + \
724
+ f' --batch_size {options.batch_size} --symlink_folder {options.symlink_folder} ' + \
725
+ f'--yolo_results_folder {options.yolo_results_folder} --model_type {options.model_type}' + \
726
+ f' --yolo_dataset_file {options.yolo_category_id_to_name}' + \
727
+ ' --no_remove_symlink_folder --no_remove_yolo_results_folder'
728
+
729
+ if not options.use_symlinks:
730
+ cmd += ' --no_use_symlinks'
731
+ if not options.augment:
732
+ cmd += ' --augment_enabled 0'
733
+
734
+ print(cmd)
735
+ execute_in_python = False
736
+ if execute_in_python:
737
+ run_inference_with_yolo_val(options)
738
+ else:
739
+ import clipboard; clipboard.copy(cmd)
740
+
741
+
473
742
  #%% Preview results
474
743
 
475
744
  postprocessing_output_folder = os.path.join(output_folder,'yolo-val-preview')