megadetector 5.0.27__py3-none-any.whl → 5.0.29__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 (176) hide show
  1. megadetector/api/batch_processing/api_core/batch_service/score.py +4 -5
  2. megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +1 -1
  3. megadetector/api/batch_processing/api_support/summarize_daily_activity.py +1 -1
  4. megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +2 -2
  5. megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +1 -1
  6. megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +1 -1
  7. megadetector/api/synchronous/api_core/tests/load_test.py +2 -3
  8. megadetector/classification/aggregate_classifier_probs.py +3 -3
  9. megadetector/classification/analyze_failed_images.py +5 -5
  10. megadetector/classification/cache_batchapi_outputs.py +5 -5
  11. megadetector/classification/create_classification_dataset.py +11 -12
  12. megadetector/classification/crop_detections.py +10 -10
  13. megadetector/classification/csv_to_json.py +8 -8
  14. megadetector/classification/detect_and_crop.py +13 -15
  15. megadetector/classification/evaluate_model.py +7 -7
  16. megadetector/classification/identify_mislabeled_candidates.py +6 -6
  17. megadetector/classification/json_to_azcopy_list.py +1 -1
  18. megadetector/classification/json_validator.py +29 -32
  19. megadetector/classification/map_classification_categories.py +9 -9
  20. megadetector/classification/merge_classification_detection_output.py +12 -9
  21. megadetector/classification/prepare_classification_script.py +19 -19
  22. megadetector/classification/prepare_classification_script_mc.py +23 -23
  23. megadetector/classification/run_classifier.py +4 -4
  24. megadetector/classification/save_mislabeled.py +6 -6
  25. megadetector/classification/train_classifier.py +1 -1
  26. megadetector/classification/train_classifier_tf.py +9 -9
  27. megadetector/classification/train_utils.py +10 -10
  28. megadetector/data_management/annotations/annotation_constants.py +1 -1
  29. megadetector/data_management/camtrap_dp_to_coco.py +45 -45
  30. megadetector/data_management/cct_json_utils.py +101 -101
  31. megadetector/data_management/cct_to_md.py +49 -49
  32. megadetector/data_management/cct_to_wi.py +33 -33
  33. megadetector/data_management/coco_to_labelme.py +75 -75
  34. megadetector/data_management/coco_to_yolo.py +189 -189
  35. megadetector/data_management/databases/add_width_and_height_to_db.py +3 -2
  36. megadetector/data_management/databases/combine_coco_camera_traps_files.py +38 -38
  37. megadetector/data_management/databases/integrity_check_json_db.py +202 -188
  38. megadetector/data_management/databases/subset_json_db.py +33 -33
  39. megadetector/data_management/generate_crops_from_cct.py +38 -38
  40. megadetector/data_management/get_image_sizes.py +54 -49
  41. megadetector/data_management/labelme_to_coco.py +130 -124
  42. megadetector/data_management/labelme_to_yolo.py +78 -72
  43. megadetector/data_management/lila/create_lila_blank_set.py +81 -83
  44. megadetector/data_management/lila/create_lila_test_set.py +32 -31
  45. megadetector/data_management/lila/create_links_to_md_results_files.py +18 -18
  46. megadetector/data_management/lila/download_lila_subset.py +21 -24
  47. megadetector/data_management/lila/generate_lila_per_image_labels.py +91 -91
  48. megadetector/data_management/lila/get_lila_annotation_counts.py +30 -30
  49. megadetector/data_management/lila/get_lila_image_counts.py +22 -22
  50. megadetector/data_management/lila/lila_common.py +70 -70
  51. megadetector/data_management/lila/test_lila_metadata_urls.py +13 -14
  52. megadetector/data_management/mewc_to_md.py +339 -340
  53. megadetector/data_management/ocr_tools.py +258 -252
  54. megadetector/data_management/read_exif.py +232 -223
  55. megadetector/data_management/remap_coco_categories.py +26 -26
  56. megadetector/data_management/remove_exif.py +31 -20
  57. megadetector/data_management/rename_images.py +187 -187
  58. megadetector/data_management/resize_coco_dataset.py +41 -41
  59. megadetector/data_management/speciesnet_to_md.py +41 -41
  60. megadetector/data_management/wi_download_csv_to_coco.py +55 -55
  61. megadetector/data_management/yolo_output_to_md_output.py +117 -120
  62. megadetector/data_management/yolo_to_coco.py +195 -188
  63. megadetector/detection/change_detection.py +831 -0
  64. megadetector/detection/process_video.py +341 -338
  65. megadetector/detection/pytorch_detector.py +308 -266
  66. megadetector/detection/run_detector.py +186 -166
  67. megadetector/detection/run_detector_batch.py +366 -364
  68. megadetector/detection/run_inference_with_yolov5_val.py +328 -325
  69. megadetector/detection/run_tiled_inference.py +312 -253
  70. megadetector/detection/tf_detector.py +24 -24
  71. megadetector/detection/video_utils.py +291 -283
  72. megadetector/postprocessing/add_max_conf.py +15 -11
  73. megadetector/postprocessing/categorize_detections_by_size.py +44 -44
  74. megadetector/postprocessing/classification_postprocessing.py +808 -311
  75. megadetector/postprocessing/combine_batch_outputs.py +20 -21
  76. megadetector/postprocessing/compare_batch_results.py +528 -517
  77. megadetector/postprocessing/convert_output_format.py +97 -97
  78. megadetector/postprocessing/create_crop_folder.py +220 -147
  79. megadetector/postprocessing/detector_calibration.py +173 -168
  80. megadetector/postprocessing/generate_csv_report.py +508 -0
  81. megadetector/postprocessing/load_api_results.py +25 -22
  82. megadetector/postprocessing/md_to_coco.py +129 -98
  83. megadetector/postprocessing/md_to_labelme.py +89 -83
  84. megadetector/postprocessing/md_to_wi.py +40 -40
  85. megadetector/postprocessing/merge_detections.py +87 -114
  86. megadetector/postprocessing/postprocess_batch_results.py +319 -302
  87. megadetector/postprocessing/remap_detection_categories.py +36 -36
  88. megadetector/postprocessing/render_detection_confusion_matrix.py +205 -199
  89. megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +57 -57
  90. megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +27 -28
  91. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +702 -677
  92. megadetector/postprocessing/separate_detections_into_folders.py +226 -211
  93. megadetector/postprocessing/subset_json_detector_output.py +265 -262
  94. megadetector/postprocessing/top_folders_to_bottom.py +45 -45
  95. megadetector/postprocessing/validate_batch_results.py +70 -70
  96. megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +52 -52
  97. megadetector/taxonomy_mapping/map_new_lila_datasets.py +15 -15
  98. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +14 -14
  99. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +66 -69
  100. megadetector/taxonomy_mapping/retrieve_sample_image.py +16 -16
  101. megadetector/taxonomy_mapping/simple_image_download.py +8 -8
  102. megadetector/taxonomy_mapping/species_lookup.py +33 -33
  103. megadetector/taxonomy_mapping/taxonomy_csv_checker.py +14 -14
  104. megadetector/taxonomy_mapping/taxonomy_graph.py +11 -11
  105. megadetector/taxonomy_mapping/validate_lila_category_mappings.py +13 -13
  106. megadetector/utils/azure_utils.py +22 -22
  107. megadetector/utils/ct_utils.py +1019 -200
  108. megadetector/utils/directory_listing.py +21 -77
  109. megadetector/utils/gpu_test.py +22 -22
  110. megadetector/utils/md_tests.py +541 -518
  111. megadetector/utils/path_utils.py +1511 -406
  112. megadetector/utils/process_utils.py +41 -41
  113. megadetector/utils/sas_blob_utils.py +53 -49
  114. megadetector/utils/split_locations_into_train_val.py +73 -60
  115. megadetector/utils/string_utils.py +147 -26
  116. megadetector/utils/url_utils.py +463 -173
  117. megadetector/utils/wi_utils.py +2629 -2868
  118. megadetector/utils/write_html_image_list.py +137 -137
  119. megadetector/visualization/plot_utils.py +21 -21
  120. megadetector/visualization/render_images_with_thumbnails.py +37 -73
  121. megadetector/visualization/visualization_utils.py +424 -404
  122. megadetector/visualization/visualize_db.py +197 -190
  123. megadetector/visualization/visualize_detector_output.py +126 -98
  124. {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/METADATA +6 -3
  125. megadetector-5.0.29.dist-info/RECORD +163 -0
  126. {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/WHEEL +1 -1
  127. megadetector/data_management/importers/add_nacti_sizes.py +0 -52
  128. megadetector/data_management/importers/add_timestamps_to_icct.py +0 -79
  129. megadetector/data_management/importers/animl_results_to_md_results.py +0 -158
  130. megadetector/data_management/importers/auckland_doc_test_to_json.py +0 -373
  131. megadetector/data_management/importers/auckland_doc_to_json.py +0 -201
  132. megadetector/data_management/importers/awc_to_json.py +0 -191
  133. megadetector/data_management/importers/bellevue_to_json.py +0 -272
  134. megadetector/data_management/importers/cacophony-thermal-importer.py +0 -793
  135. megadetector/data_management/importers/carrizo_shrubfree_2018.py +0 -269
  136. megadetector/data_management/importers/carrizo_trail_cam_2017.py +0 -289
  137. megadetector/data_management/importers/cct_field_adjustments.py +0 -58
  138. megadetector/data_management/importers/channel_islands_to_cct.py +0 -913
  139. megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +0 -180
  140. megadetector/data_management/importers/eMammal/eMammal_helpers.py +0 -249
  141. megadetector/data_management/importers/eMammal/make_eMammal_json.py +0 -223
  142. megadetector/data_management/importers/ena24_to_json.py +0 -276
  143. megadetector/data_management/importers/filenames_to_json.py +0 -386
  144. megadetector/data_management/importers/helena_to_cct.py +0 -283
  145. megadetector/data_management/importers/idaho-camera-traps.py +0 -1407
  146. megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +0 -294
  147. megadetector/data_management/importers/import_desert_lion_conservation_camera_traps.py +0 -387
  148. megadetector/data_management/importers/jb_csv_to_json.py +0 -150
  149. megadetector/data_management/importers/mcgill_to_json.py +0 -250
  150. megadetector/data_management/importers/missouri_to_json.py +0 -490
  151. megadetector/data_management/importers/nacti_fieldname_adjustments.py +0 -79
  152. megadetector/data_management/importers/noaa_seals_2019.py +0 -181
  153. megadetector/data_management/importers/osu-small-animals-to-json.py +0 -364
  154. megadetector/data_management/importers/pc_to_json.py +0 -365
  155. megadetector/data_management/importers/plot_wni_giraffes.py +0 -123
  156. megadetector/data_management/importers/prepare_zsl_imerit.py +0 -131
  157. megadetector/data_management/importers/raic_csv_to_md_results.py +0 -416
  158. megadetector/data_management/importers/rspb_to_json.py +0 -356
  159. megadetector/data_management/importers/save_the_elephants_survey_A.py +0 -320
  160. megadetector/data_management/importers/save_the_elephants_survey_B.py +0 -329
  161. megadetector/data_management/importers/snapshot_safari_importer.py +0 -758
  162. megadetector/data_management/importers/snapshot_serengeti_lila.py +0 -1067
  163. megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +0 -150
  164. megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +0 -153
  165. megadetector/data_management/importers/sulross_get_exif.py +0 -65
  166. megadetector/data_management/importers/timelapse_csv_set_to_json.py +0 -490
  167. megadetector/data_management/importers/ubc_to_json.py +0 -399
  168. megadetector/data_management/importers/umn_to_json.py +0 -507
  169. megadetector/data_management/importers/wellington_to_json.py +0 -263
  170. megadetector/data_management/importers/wi_to_json.py +0 -442
  171. megadetector/data_management/importers/zamba_results_to_md_results.py +0 -180
  172. megadetector/data_management/lila/add_locations_to_island_camera_traps.py +0 -101
  173. megadetector/data_management/lila/add_locations_to_nacti.py +0 -151
  174. megadetector-5.0.27.dist-info/RECORD +0 -208
  175. {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/licenses/LICENSE +0 -0
  176. {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/top_level.txt +0 -0
@@ -5,30 +5,30 @@ run_inference_with_yolov5_val.py
5
5
  Runs a folder of images through MegaDetector (or another YOLOv5/YOLOv8 model) with YOLO's
6
6
  val.py, converting the output to the standard MD format. The reasons this script exists,
7
7
  as an alternative to the standard run_detector_batch.py are:
8
-
8
+
9
9
  * This script provides access to YOLO's test-time augmentation tools.
10
10
  * This script serves a reference implementation: by any reasonable definition, YOLOv5's
11
- val.py produces the "correct" result for any image, since it matches what was used in
11
+ val.py produces the "correct" result for any image, since it matches what was used in
12
12
  training.
13
- * This script works for any Ultralytics detection model, including YOLOv8 models
13
+ * This script works for any Ultralytics detection model, including YOLOv8 models
14
14
 
15
- YOLOv5's val.py uses each file's base name as a unique identifier, which doesn't work
15
+ YOLOv5's val.py uses each file's base name as a unique identifier, which doesn't work
16
16
  when you have typical camera trap images like:
17
17
 
18
18
  * a/b/c/RECONYX0001.JPG
19
19
  * d/e/f/RECONYX0001.JPG
20
20
 
21
- ...both of which would just be "RECONYX0001.JPG". So this script jumps through a bunch of
22
- hoops to put a symlinks in a flat folder, run YOLOv5 on that folder, and map the results back
21
+ ...both of which would just be "RECONYX0001.JPG". So this script jumps through a bunch of
22
+ hoops to put a symlinks in a flat folder, run YOLOv5 on that folder, and map the results back
23
23
  to the real files.
24
24
 
25
25
  If you are running a YOLOv5 model, this script currently requires the caller to supply the path
26
- where a working YOLOv5 install lives, and assumes that the current conda environment is all set up for
26
+ where a working YOLOv5 install lives, and assumes that the current conda environment is all set up for
27
27
  YOLOv5. If you are running a YOLOv8 model, the folder doesn't matter, but it assumes that ultralytics
28
28
  tools are available in the current environment.
29
29
 
30
- By default, this script uses symlinks to format the input images in a way that YOLO's
31
- val.py likes, as per above. This requires admin privileges on Windows... actually technically this
30
+ By default, this script uses symlinks to format the input images in a way that YOLO's
31
+ val.py likes, as per above. This requires admin privileges on Windows... actually technically this
32
32
  only requires permissions to create symbolic links, but I've never seen a case where someone has
33
33
  that permission and *doesn't* have admin privileges. If you are running this script on
34
34
  Windows and you don't have admin privileges, use --no_use_symlinks, which will make copies of images,
@@ -46,14 +46,17 @@ import tempfile
46
46
  import shutil
47
47
  import json
48
48
  import copy
49
+ import argparse
49
50
 
50
51
  from tqdm import tqdm
51
52
 
52
53
  from megadetector.utils import path_utils
53
54
  from megadetector.utils import process_utils
54
55
  from megadetector.utils import string_utils
56
+ from megadetector.utils.ct_utils import args_to_object
55
57
 
56
58
  from megadetector.utils.ct_utils import is_iterable, split_list_into_fixed_size_chunks
59
+ from megadetector.utils import ct_utils
57
60
  from megadetector.utils.path_utils import path_is_abs
58
61
  from megadetector.data_management import yolo_output_to_md_output
59
62
  from megadetector.detection.run_detector import try_download_known_detector
@@ -67,129 +70,129 @@ default_image_size_with_no_augmentation = 1280
67
70
 
68
71
  class YoloInferenceOptions:
69
72
  """
70
- Parameters that control the behavior of run_inference_with_yolov5_val(), including
73
+ Parameters that control the behavior of run_inference_with_yolov5_val(), including
71
74
  the input/output filenames.
72
75
  """
73
-
76
+
74
77
  def __init__(self):
75
-
78
+
76
79
  ## Required-ish ##
77
-
80
+
78
81
  #: Folder of images to process (can be None if image_filename_list contains absolute paths)
79
82
  self.input_folder = None
80
-
83
+
81
84
  #: If this is None, [input_folder] can't be None, we'll process all images in [input_folder].
82
- #:
83
- #: If this is not None, and [input_folder] is not None, this should be a list of relative image
84
- #: paths within [input_folder] to process, or a .txt or .json file containing a list of
85
+ #:
86
+ #: If this is not None, and [input_folder] is not None, this should be a list of relative image
87
+ #: paths within [input_folder] to process, or a .txt or .json file containing a list of
85
88
  #: relative image paths.
86
89
  #:
87
90
  #: If this is not None, and [input_folder] is None, this should be a list of absolute image
88
- #: paths, or a .txt or .json file containing a list of absolute image paths.
91
+ #: paths, or a .txt or .json file containing a list of absolute image paths.
89
92
  self.image_filename_list = None
90
-
93
+
91
94
  #: Model filename (ending in .pt), or a well-known model name (e.g. "MDV5A")
92
95
  self.model_filename = None
93
-
96
+
94
97
  #: .json output file, in MD results format
95
98
  self.output_file = None
96
-
97
-
99
+
100
+
98
101
  ## Optional ##
99
-
102
+
100
103
  #: Required for older YOLOv5 inference, not for newer ulytralytics/YOLOv8 inference
101
104
  self.yolo_working_folder = None
102
-
105
+
103
106
  #: Currently 'yolov5' and 'ultralytics' are supported, and really these are proxies for
104
107
  #: "the yolov5 repo" and "the ultralytics repo".
105
- self.model_type = 'yolov5'
106
-
108
+ self.model_type = 'yolov5'
109
+
107
110
  #: Image size to use; this is a single int, which in ultralytics's terminology means
108
111
  #: "scale the long side of the image to this size, and preserve aspect ratio".
109
112
  #:
110
113
  #: If None, will choose based on whether augmentation is enabled.
111
114
  self.image_size = None
112
-
115
+
113
116
  #: Detections below this threshold will not be included in the output file
114
117
  self.conf_thres = '0.001'
115
-
118
+
116
119
  #: Batch size... has no impact on results, but may create memory issues if you set
117
120
  #: this to large values
118
121
  self.batch_size = 1
119
-
122
+
120
123
  #: Device string: typically '0' for GPU 0, '1' for GPU 1, etc., or 'cpu'
121
124
  self.device_string = '0'
122
-
125
+
123
126
  #: Should we enable test-time augmentation?
124
127
  self.augment = False
125
-
128
+
126
129
  #: Should we enable half-precision inference?
127
130
  self.half_precision_enabled = None
128
-
129
- #: Where should we stash the temporary symlinks (or copies) used to give unique identifiers to image
131
+
132
+ #: Where should we stash the temporary symlinks (or copies) used to give unique identifiers to image
130
133
  # files?
131
134
  #:
132
135
  #: If this is None, we'll create a folder in system temp space.
133
136
  self.symlink_folder = None
134
-
137
+
135
138
  #: Should we use symlinks to give unique identifiers to image files (vs. copies)?
136
139
  self.use_symlinks = True
137
-
140
+
138
141
  #: How should we guarantee that YOLO IDs (base filenames) are unique? Choices are:
139
142
  #:
140
143
  #: * 'verify': assume image IDs are unique, but verify and error if they're not
141
144
  #: * 'links': create symlinks (or copies, depending on use_symlinks) to enforce uniqueness
142
145
  #: * 'auto': check whether IDs are unique, create links if necessary
143
146
  self.unique_id_strategy = 'links'
144
-
147
+
145
148
  #: Temporary folder to stash intermediate YOLO results.
146
149
  #:
147
- #: If this is None, we'll create a folder in system temp space.
150
+ #: If this is None, we'll create a folder in system temp space.
148
151
  self.yolo_results_folder = None
149
-
152
+
150
153
  #: Should we remove the symlink folder when we're done?
151
154
  self.remove_symlink_folder = True
152
-
155
+
153
156
  #: Should we remove the intermediate results folder when we're done?
154
157
  self.remove_yolo_results_folder = True
155
-
158
+
156
159
  #: These are deliberately offset from the standard MD categories; YOLOv5
157
160
  #: needs categories IDs to start at 0.
158
161
  #:
159
162
  #: This can also be a string that points to a YOLO dataset.yaml file.
160
163
  self.yolo_category_id_to_name = {0:'animal',1:'person',2:'vehicle'}
161
-
164
+
162
165
  #: What should we do if the output file already exists?
163
166
  #:
164
167
  #: Can be 'error', 'skip', or 'overwrite'.
165
168
  self.overwrite_handling = 'skip'
166
-
169
+
167
170
  #: If True, we'll do a dry run that lets you preview the YOLO val command, without
168
171
  #: actually running it.
169
172
  self.preview_yolo_command_only = False
170
-
173
+
171
174
  #: By default, if any errors occur while we're copying images or creating symlinks, it's
172
175
  #: game over. If this is True, those errors become warnings, and we plow ahead.
173
176
  self.treat_copy_failures_as_warnings = False
174
-
177
+
175
178
  #: Save YOLO console output
176
179
  self.save_yolo_debug_output = False
177
-
180
+
178
181
  #: Whether to search for images recursively within [input_folder]
179
182
  #:
180
183
  #: Ignored if a list of files is provided.
181
184
  self.recursive = True
182
-
185
+
183
186
  #: Maximum number of images to run in a single chunk
184
187
  self.checkpoint_frequency = None
185
-
186
- #: By default, if we're creating symlinks to images, we append a unique job ID to the
188
+
189
+ #: By default, if we're creating symlinks to images, we append a unique job ID to the
187
190
  #: symlink folder. If the caller is 100% sure that the symlink folder can be re-used
188
191
  #: across calls, this can be set to False.
189
192
  self.append_job_id_to_symlink_folder = True
190
-
193
+
191
194
  # ...def __init__()
192
-
195
+
193
196
  # ...YoloInferenceOptions()
194
197
 
195
198
 
@@ -201,13 +204,13 @@ def _clean_up_temporary_folders(options,
201
204
  """
202
205
  Remove temporary symlink/results folders, unless the caller requested that we leave them in place.
203
206
  """
204
-
207
+
205
208
  if options.remove_symlink_folder:
206
209
  shutil.rmtree(symlink_folder)
207
210
  elif symlink_folder_is_temp_folder:
208
211
  print('Warning: using temporary symlink folder {}, but not removing it'.format(
209
212
  symlink_folder))
210
-
213
+
211
214
  if options.remove_yolo_results_folder:
212
215
  shutil.rmtree(yolo_results_folder)
213
216
  elif yolo_folder_is_temp_folder:
@@ -219,44 +222,44 @@ def get_stats_for_category(filename,category='all'):
219
222
  """
220
223
  Retrieve statistics for a category from the YOLO console output
221
224
  stored in [filenam].
222
-
225
+
223
226
  Args:
224
227
  filename (str): a text file containing console output from a YOLO val run
225
228
  category (optional, str): a category name
226
-
229
+
227
230
  Returns:
228
231
  dict: a dict with fields n_images, n_labels, P, R, mAP50, and mAP50-95
229
232
  """
230
-
233
+
231
234
  with open(filename,'r',encoding='utf-8') as f:
232
235
  lines = f.readlines()
233
-
236
+
234
237
  # This is just a hedge to make sure there isn't some YOLO version floating
235
238
  # around that used different IoU thresholds in the console output.
236
239
  found_map50 = False
237
240
  found_map5095 = False
238
-
241
+
239
242
  for line in lines:
240
-
243
+
241
244
  s = line.strip()
242
-
245
+
243
246
  if ' map50 ' in s.lower() or ' map@.5 ' in s.lower():
244
247
  found_map50 = True
245
248
  if 'map50-95' in s.lower() or 'map@.5:.95' in s.lower():
246
249
  found_map5095 = True
247
-
250
+
248
251
  if not s.startswith(category):
249
252
  continue
250
-
253
+
251
254
  tokens = s.split(' ')
252
255
  tokens_filtered = list(filter(None,tokens))
253
-
256
+
254
257
  if len(tokens_filtered) != 7:
255
258
  continue
256
-
259
+
257
260
  assert found_map50 and found_map5095, \
258
261
  'Parsing error in YOLO console output file {}'.format(filename)
259
-
262
+
260
263
  to_return = {}
261
264
  to_return['category'] = category
262
265
  assert category == tokens_filtered[0]
@@ -267,68 +270,70 @@ def get_stats_for_category(filename,category='all'):
267
270
  to_return['mAP50'] = float(tokens_filtered[5])
268
271
  to_return['mAP50-95'] = float(tokens_filtered[6])
269
272
  return to_return
270
-
273
+
271
274
  # ...for each line
272
-
275
+
273
276
  return None
274
277
 
275
-
278
+
276
279
  #%% Main function
277
280
 
278
281
  def run_inference_with_yolo_val(options):
279
282
  """
280
283
  Runs a folder of images through MegaDetector (or another YOLOv5/YOLOv8 model) with YOLO's
281
284
  val.py, converting the output to the standard MD format.
282
-
283
- Args:
285
+
286
+ Args:
284
287
  options (YoloInferenceOptions): all the parameters used to control this process,
285
- including filenames; see YoloInferenceOptions for details
288
+ including filenames; see YoloInferenceOptions for details
286
289
  """
287
-
290
+
288
291
  ##%% Input and path handling
289
-
292
+
290
293
  default_options = YoloInferenceOptions()
291
-
294
+
292
295
  for k in options.__dict__.keys():
293
296
  if k not in default_options.__dict__:
294
297
  # Print warnings about unexpected variables, except for things like
295
298
  # "no_append_job_id_to_symlink_folder", which just negate existing objects
296
299
  if not k.startswith('no_'):
297
300
  print('Warning: unexpected variable {} in options object'.format(k))
298
-
301
+
299
302
  if options.model_type == 'yolov8':
300
-
301
- print('Warning: model type "yolov8" supplied, "ultralytics" is the preferred model type string for YOLOv8 models')
303
+
304
+ print('Warning: model type "yolov8" supplied, "ultralytics" is the preferred model ' + \
305
+ 'type string for YOLOv8 models')
302
306
  options.model_type = 'ultralytics'
303
-
307
+
304
308
  if (options.model_type == 'yolov5') and ('yolov8' in options.model_filename.lower()):
305
- 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')
306
-
309
+ print('\n\n*** Warning: model type set as "yolov5", but your model filename contains "yolov8"... ' + \
310
+ 'did you mean to use --model_type yolov8?" ***\n\n')
311
+
307
312
  if options.yolo_working_folder is None:
308
313
  assert options.model_type == 'ultralytics', \
309
314
  'A working folder is required to run YOLOv5 val.py'
310
315
  else:
311
316
  assert os.path.isdir(options.yolo_working_folder), \
312
317
  'Could not find working folder {}'.format(options.yolo_working_folder)
313
-
318
+
314
319
  if options.half_precision_enabled is not None:
315
320
  assert options.half_precision_enabled in (0,1), \
316
321
  'Invalid value {} for --half_precision_enabled (should be 0 or 1)'.format(
317
322
  options.half_precision_enabled)
318
-
323
+
319
324
  # If the model filename is a known model string (e.g. "MDv5A", download the model if necessary)
320
325
  model_filename = try_download_known_detector(options.model_filename)
321
-
326
+
322
327
  assert os.path.isfile(model_filename), \
323
328
  'Could not find model file {}'.format(model_filename)
324
-
329
+
325
330
  assert (options.input_folder is not None) or (options.image_filename_list is not None), \
326
331
  'You must specify a folder and/or a file list'
327
-
332
+
328
333
  if options.input_folder is not None:
329
334
  assert os.path.isdir(options.input_folder), 'Could not find input folder {}'.format(
330
335
  options.input_folder)
331
-
336
+
332
337
  if os.path.exists(options.output_file):
333
338
  if options.overwrite_handling == 'skip':
334
339
  print('Warning: output file {} exists, skipping'.format(options.output_file))
@@ -339,17 +344,17 @@ def run_inference_with_yolo_val(options):
339
344
  raise ValueError('Output file {} exists'.format(options.output_file))
340
345
  else:
341
346
  raise ValueError('Unknown output handling method {}'.format(options.overwrite_handling))
342
-
347
+
343
348
  os.makedirs(os.path.dirname(options.output_file),exist_ok=True)
344
-
349
+
345
350
  if options.input_folder is not None:
346
351
  options.input_folder = options.input_folder.replace('\\','/')
347
-
348
-
352
+
353
+
349
354
  ##%% Other input handling
350
-
355
+
351
356
  if isinstance(options.yolo_category_id_to_name,str):
352
-
357
+
353
358
  assert os.path.isfile(options.yolo_category_id_to_name)
354
359
  yolo_dataset_file = options.yolo_category_id_to_name
355
360
  options.yolo_category_id_to_name = \
@@ -360,9 +365,9 @@ def run_inference_with_yolo_val(options):
360
365
  temporary_folder = None
361
366
  symlink_folder_is_temp_folder = False
362
367
  yolo_folder_is_temp_folder = False
363
-
368
+
364
369
  job_id = str(uuid.uuid1())
365
-
370
+
366
371
  def get_job_temporary_folder(tf):
367
372
  if tf is not None:
368
373
  return tf
@@ -370,39 +375,39 @@ def run_inference_with_yolo_val(options):
370
375
  tf = os.path.join(tempdir_base,'md_to_yolo','md_to_yolo_' + job_id)
371
376
  os.makedirs(tf,exist_ok=True)
372
377
  return tf
373
-
378
+
374
379
  symlink_folder = options.symlink_folder
375
380
  yolo_results_folder = options.yolo_results_folder
376
-
381
+
377
382
  if symlink_folder is None:
378
383
  temporary_folder = get_job_temporary_folder(temporary_folder)
379
384
  symlink_folder = os.path.join(temporary_folder,'symlinks')
380
385
  symlink_folder_is_temp_folder = True
381
-
386
+
382
387
  if yolo_results_folder is None:
383
388
  temporary_folder = get_job_temporary_folder(temporary_folder)
384
389
  yolo_results_folder = os.path.join(temporary_folder,'yolo_results')
385
390
  yolo_folder_is_temp_folder = True
386
-
391
+
387
392
  if options.append_job_id_to_symlink_folder:
388
- # Attach a GUID to the symlink folder, regardless of whether we created it
393
+ # Attach a GUID to the symlink folder, regardless of whether we created it
389
394
  symlink_folder_inner = os.path.join(symlink_folder,job_id)
390
395
  else:
391
396
  print('Re-using existing symlink folder {}'.format(symlink_folder))
392
397
  symlink_folder_inner = symlink_folder
393
-
398
+
394
399
  os.makedirs(symlink_folder_inner,exist_ok=True)
395
400
  os.makedirs(yolo_results_folder,exist_ok=True)
396
-
401
+
397
402
 
398
403
  ##%% Enumerate images
399
-
404
+
400
405
  image_files_relative = None
401
406
  image_files_absolute = None
402
-
407
+
403
408
  # If the caller just provided a folder, not a list of files...
404
409
  if options.image_filename_list is None:
405
-
410
+
406
411
  assert options.input_folder is not None and os.path.isdir(options.input_folder), \
407
412
  'Could not find input folder {}'.format(options.input_folder)
408
413
  image_files_relative = path_utils.find_images(options.input_folder,
@@ -411,18 +416,18 @@ def run_inference_with_yolo_val(options):
411
416
  convert_slashes=True)
412
417
  image_files_absolute = [os.path.join(options.input_folder,fn) for \
413
418
  fn in image_files_relative]
414
-
419
+
415
420
  else:
416
-
417
- # If the caller provided a list of image files (rather than a filename pointing
421
+
422
+ # If the caller provided a list of image files (rather than a filename pointing
418
423
  # to a list of image files)...
419
424
  if is_iterable(options.image_filename_list) and not isinstance(options.image_filename_list,str):
420
-
425
+
421
426
  image_files_relative = options.image_filename_list
422
-
427
+
423
428
  # If the caller provided a filename pointing to a list of image files...
424
429
  else:
425
-
430
+
426
431
  assert isinstance(options.image_filename_list,str), \
427
432
  'Unrecognized image filename list object type: {}'.format(options.image_filename_list)
428
433
  assert os.path.isfile(options.image_filename_list), \
@@ -439,152 +444,152 @@ def run_inference_with_yolo_val(options):
439
444
  with open(options.image_filename_list,'r') as f:
440
445
  image_files_relative = f.readlines()
441
446
  image_files_relative = [s.strip() for s in image_files_relative]
442
-
447
+
443
448
  # ...whether the image filename list was supplied as list vs. a filename
444
-
449
+
445
450
  if options.input_folder is None:
446
-
451
+
447
452
  image_files_absolute = image_files_relative
448
-
453
+
449
454
  else:
450
-
455
+
451
456
  # The list should be relative filenames
452
457
  for fn in image_files_relative:
453
458
  assert not path_is_abs(fn), \
454
459
  'When providing a folder and a list, paths in the list should be relative'
455
-
460
+
456
461
  image_files_absolute = \
457
462
  [os.path.join(options.input_folder,fn) for fn in image_files_relative]
458
-
463
+
459
464
  for fn in image_files_absolute:
460
465
  assert os.path.isfile(fn), 'Could not find image file {}'.format(fn)
461
-
466
+
462
467
  # ...whether the caller supplied a list of filenames
463
-
468
+
464
469
  image_files_absolute = [fn.replace('\\','/') for fn in image_files_absolute]
465
-
470
+
466
471
  del image_files_relative
467
-
468
-
472
+
473
+
469
474
  ##%% Recurse if necessary to handle checkpoints
470
-
475
+
471
476
  if options.checkpoint_frequency is not None and options.checkpoint_frequency > 0:
472
-
477
+
473
478
  chunks = split_list_into_fixed_size_chunks(image_files_absolute,options.checkpoint_frequency)
474
-
479
+
475
480
  chunk_output_files = []
476
-
481
+
477
482
  # i_chunk = 0; chunk_files_abs = chunks[i_chunk]
478
483
  for i_chunk,chunk_files_abs in enumerate(chunks):
479
-
484
+
480
485
  print('Processing {} images from chunk {} of {}'.format(
481
486
  len(chunk_files_abs),i_chunk,len(chunks)))
482
-
487
+
483
488
  chunk_options = copy.deepcopy(options)
484
-
489
+
485
490
  # Run each chunk without checkpointing
486
491
  chunk_options.checkpoint_frequency = None
487
-
492
+
488
493
  if options.input_folder is not None:
489
494
  chunk_files_relative = \
490
495
  [os.path.relpath(fn,options.input_folder) for fn in chunk_files_abs]
491
496
  chunk_options.image_filename_list = chunk_files_relative
492
497
  else:
493
498
  chunk_options.image_filename_list = chunk_files_abs
494
-
499
+
495
500
  chunk_options.image_filename_list = \
496
501
  [fn.replace('\\','/') for fn in chunk_options.image_filename_list]
497
-
502
+
498
503
  chunk_string = 'chunk_{}'.format(str(i_chunk).zfill(5))
499
504
  chunk_options.yolo_results_folder = yolo_results_folder + '_' + chunk_string
500
505
  chunk_options.symlink_folder = symlink_folder + '_' + chunk_string
501
-
506
+
502
507
  # Put the output file in the parent job's scratch folder
503
508
  chunk_output_file = os.path.join(yolo_results_folder,chunk_string + '_results_md_format.json')
504
509
  chunk_output_files.append(chunk_output_file)
505
510
  chunk_options.output_file = chunk_output_file
506
-
511
+
507
512
  if os.path.isfile(chunk_output_file):
508
-
513
+
509
514
  print('Chunk output file {} exists, checking completeness'.format(chunk_output_file))
510
-
515
+
511
516
  with open(chunk_output_file,'r') as f:
512
517
  chunk_results = json.load(f)
513
- images_in_this_chunk_results_file = [im['file'] for im in chunk_results['images']]
518
+ images_in_this_chunk_results_file = [im['file'] for im in chunk_results['images']]
514
519
  assert len(images_in_this_chunk_results_file) == len(chunk_options.image_filename_list), \
515
- 'Expected {} images in chunk results file {}, found {}, possibly this is left over from a previous job?'.format(
516
- len(chunk_options.image_filename_list),chunk_output_file,
517
- len(images_in_this_chunk_results_file))
520
+ f'Expected {len(chunk_options.image_filename_list)} images in ' + \
521
+ f'chunk results file {chunk_output_file}, found {len(images_in_this_chunk_results_file)}, ' + \
522
+ 'possibly this is left over from a previous job?'
518
523
  for fn in images_in_this_chunk_results_file:
519
524
  assert fn in chunk_options.image_filename_list, \
520
- 'Unexpected image {} in chunk results file {}, possibly this is left over from a previous job?'.format(
521
- fn,chunk_output_file)
522
-
525
+ f'Unexpected image {fn} in chunk results file {chunk_output_file}, ' + \
526
+ 'possibly this is left over from a previous job?'
527
+
523
528
  print('Chunk output file {} exists and is complete, skipping this chunk'.format(
524
529
  chunk_output_file))
525
-
530
+
526
531
  # ...if the outptut file exists
527
-
532
+
528
533
  else:
529
-
534
+
530
535
  run_inference_with_yolo_val(chunk_options)
531
-
536
+
532
537
  # ...if we do/don't have to run this chunk
533
-
538
+
534
539
  assert os.path.isfile(chunk_options.output_file)
535
-
540
+
536
541
  # ...for each chunk
537
-
542
+
538
543
  # Merge
539
544
  _ = combine_batch_output_files(input_files=chunk_output_files,
540
545
  output_file=options.output_file,
541
546
  require_uniqueness=True,
542
547
  verbose=True)
543
-
548
+
544
549
  # Validate
545
550
  with open(options.output_file,'r') as f:
546
551
  combined_results = json.load(f)
547
552
  assert len(combined_results['images']) == len(image_files_absolute), \
548
553
  'Expected {} images in merged output file, found {}'.format(
549
554
  len(image_files_absolute),len(combined_results['images']))
550
-
555
+
551
556
  # Clean up
552
557
  _clean_up_temporary_folders(options,
553
558
  symlink_folder,yolo_results_folder,
554
559
  symlink_folder_is_temp_folder,yolo_folder_is_temp_folder)
555
-
560
+
556
561
  return
557
-
562
+
558
563
  # ...if we need to make recursive calls for file chunks
559
-
560
-
564
+
565
+
561
566
  ##%% Create symlinks (or copy images) to give a unique ID to each image
562
-
567
+
563
568
  # Maps YOLO image IDs (base filename without extension as it will appear in YOLO .json output)
564
569
  # to the *original full path* for each image (not the symlink path).
565
- image_id_to_file = {}
566
-
570
+ image_id_to_file = {}
571
+
567
572
  # Maps YOLO image IDs (base filename without extension as it will appear in YOLO .json output)
568
573
  # to errors, including errors that happen before we run the model at all (e.g. file access errors).
569
574
  image_id_to_error = {}
570
-
575
+
571
576
  create_links = True
572
-
577
+
573
578
  if options.unique_id_strategy == 'links':
574
-
579
+
575
580
  create_links = True
576
-
581
+
577
582
  else:
578
-
583
+
579
584
  assert options.unique_id_strategy in ('auto','verify'), \
580
585
  'Unknown unique ID strategy {}'.format(options.unique_id_strategy)
581
-
586
+
582
587
  image_ids_are_unique = True
583
-
584
- for i_image,image_fn in tqdm(enumerate(image_files_absolute),total=len(image_files_absolute)):
585
-
588
+
589
+ for i_image,image_fn in tqdm(enumerate(image_files_absolute),total=len(image_files_absolute)):
590
+
586
591
  image_id = os.path.splitext(os.path.basename(image_fn))[0]
587
-
592
+
588
593
  # Is this image ID unique?
589
594
  if image_id in image_id_to_file:
590
595
  if options.unique_id_strategy == 'verify':
@@ -596,20 +601,20 @@ def run_inference_with_yolo_val(options):
596
601
  image_ids_are_unique = False
597
602
  image_id_to_file = {}
598
603
  break
599
-
604
+
600
605
  image_id_to_file[image_id] = image_fn
601
-
606
+
602
607
  # ...for each image
603
-
608
+
604
609
  if image_ids_are_unique:
605
-
610
+
606
611
  print('"{}" specified for image uniqueness and images are unique, skipping links'.format(
607
612
  options.unique_id_strategy))
608
613
  assert len(image_id_to_file) == len(image_files_absolute)
609
614
  create_links = False
610
-
615
+
611
616
  else:
612
-
617
+
613
618
  assert options.unique_id_strategy == 'auto'
614
619
  create_links = True
615
620
  link_type = 'copies'
@@ -617,31 +622,31 @@ def run_inference_with_yolo_val(options):
617
622
  link_type = 'links'
618
623
  print('"auto" specified for image uniqueness and images are not unique, defaulting to {}'.format(
619
624
  link_type))
620
-
625
+
621
626
  # ...which unique ID strategy?
622
-
627
+
623
628
  if create_links:
624
-
629
+
625
630
  if options.use_symlinks:
626
631
  print('Creating {} symlinks in {}'.format(len(image_files_absolute),symlink_folder_inner))
627
632
  else:
628
633
  print('Symlinks disabled, copying {} images to {}'.format(len(image_files_absolute),symlink_folder_inner))
629
-
634
+
630
635
  link_full_paths = []
631
-
636
+
632
637
  # i_image = 0; image_fn = image_files_absolute[i_image]
633
638
  for i_image,image_fn in tqdm(enumerate(image_files_absolute),total=len(image_files_absolute)):
634
-
639
+
635
640
  ext = os.path.splitext(image_fn)[1]
636
641
  image_fn_without_extension = os.path.splitext(image_fn)[0]
637
-
642
+
638
643
  # YOLO .json output identifies images by the base filename without the extension
639
644
  image_id = str(i_image).zfill(10)
640
645
  image_id_to_file[image_id] = image_fn
641
646
  symlink_name = image_id + ext
642
647
  symlink_full_path = os.path.join(symlink_folder_inner,symlink_name)
643
648
  link_full_paths.append(symlink_full_path)
644
-
649
+
645
650
  # If annotation files exist, link those too; only useful if we're reading the computed
646
651
  # mAP value, but it doesn't hurt.
647
652
  annotation_fn = image_fn_without_extension + '.txt'
@@ -649,10 +654,10 @@ def run_inference_with_yolo_val(options):
649
654
  if os.path.isfile(annotation_fn):
650
655
  annotation_file_exists = True
651
656
  annotation_symlink_name = image_id + '.txt'
652
- annotation_symlink_full_path = os.path.join(symlink_folder_inner,annotation_symlink_name)
653
-
657
+ annotation_symlink_full_path = os.path.join(symlink_folder_inner,annotation_symlink_name)
658
+
654
659
  try:
655
-
660
+
656
661
  if options.use_symlinks:
657
662
  path_utils.safe_create_link(image_fn,symlink_full_path)
658
663
  if annotation_file_exists:
@@ -661,74 +666,74 @@ def run_inference_with_yolo_val(options):
661
666
  shutil.copyfile(image_fn,symlink_full_path)
662
667
  if annotation_file_exists:
663
668
  shutil.copyfile(annotation_fn,annotation_symlink_full_path)
664
-
669
+
665
670
  except Exception as e:
666
-
671
+
667
672
  error_string = str(e)
668
673
  image_id_to_error[image_id] = error_string
669
-
674
+
670
675
  # Always break if the user is trying to create symlinks on Windows without
671
676
  # permission, 100% of images will always fail in this case.
672
677
  if ('a required privilege is not held by the client' in error_string.lower()) or \
673
678
  (not options.treat_copy_failures_as_warnings):
674
-
679
+
675
680
  print('\nError copying/creating link for input file {}: {}'.format(
676
681
  image_fn,error_string))
677
-
682
+
678
683
  raise
679
-
684
+
680
685
  else:
681
-
686
+
682
687
  print('Warning: error copying/creating link for input file {}: {}'.format(
683
688
  image_fn,error_string))
684
689
  continue
685
-
690
+
686
691
  # ...except
687
-
692
+
688
693
  # ...for each image
689
-
694
+
690
695
  # ...if we need to create links/copies
691
696
 
692
-
697
+
693
698
  ##%% Create the dataset file if necessary
694
-
699
+
695
700
  # This may have been passed in as a string, but at this point, we should have
696
701
  # loaded the dataset file.
697
702
  assert isinstance(options.yolo_category_id_to_name,dict)
698
-
703
+
699
704
  # Category IDs need to be continuous integers starting at 0
700
705
  category_ids = sorted(list(options.yolo_category_id_to_name.keys()))
701
706
  assert category_ids[0] == 0
702
707
  assert len(category_ids) == 1 + category_ids[-1]
703
-
708
+
704
709
  yolo_dataset_file = os.path.join(yolo_results_folder,'dataset.yaml')
705
- yolo_image_list_file = os.path.join(yolo_results_folder,'images.txt')
706
-
710
+ yolo_image_list_file = os.path.join(yolo_results_folder,'images.txt')
711
+
707
712
  with open(yolo_image_list_file,'w') as f:
708
-
713
+
709
714
  if create_links:
710
715
  image_files_to_write = link_full_paths
711
716
  else:
712
717
  image_files_to_write = image_files_absolute
713
-
718
+
714
719
  for fn_abs in image_files_to_write:
715
- # At least in YOLOv5 val (need to verify for YOLOv8 val), filenames in this
720
+ # At least in YOLOv5 val (need to verify for YOLOv8 val), filenames in this
716
721
  # text file are treated as relative to the text file itself if they start with
717
722
  # "./", otherwise they're treated as absolute paths. Since we don't want to put this
718
723
  # text file in the image folder, we'll use absolute paths.
719
724
  # fn_relative = os.path.relpath(fn_abs,options.input_folder)
720
725
  # f.write(fn_relative + '\n')
721
726
  f.write(fn_abs + '\n')
722
-
727
+
723
728
  if create_links:
724
729
  inference_folder = symlink_folder_inner
725
730
  else:
726
731
  # This doesn't matter, but it has to be a valid path
727
732
  inference_folder = options.yolo_results_folder
728
-
733
+
729
734
  with open(yolo_dataset_file,'w') as f:
730
-
731
- f.write('path: {}\n'.format(inference_folder))
735
+
736
+ f.write('path: {}\n'.format(inference_folder))
732
737
  # These need to be valid paths, even if you're not using them, and "." is always safe
733
738
  f.write('train: .\n')
734
739
  f.write('val: .\n')
@@ -744,7 +749,7 @@ def run_inference_with_yolo_val(options):
744
749
 
745
750
 
746
751
  ##%% Prepare Python command or YOLO CLI command
747
-
752
+
748
753
  if options.image_size is None:
749
754
  if options.augment:
750
755
  image_size = default_image_size_with_augmentation
@@ -752,70 +757,70 @@ def run_inference_with_yolo_val(options):
752
757
  image_size = default_image_size_with_no_augmentation
753
758
  else:
754
759
  image_size = options.image_size
755
-
760
+
756
761
  image_size_string = str(round(image_size))
757
-
762
+
758
763
  if options.model_type == 'yolov5':
759
-
764
+
760
765
  cmd = 'python val.py --task test --data "{}"'.format(yolo_dataset_file)
761
766
  cmd += ' --weights "{}"'.format(model_filename)
762
767
  cmd += ' --batch-size {} --imgsz {} --conf-thres {}'.format(
763
768
  options.batch_size,image_size_string,options.conf_thres)
764
769
  cmd += ' --device "{}" --save-json'.format(options.device_string)
765
770
  cmd += ' --project "{}" --name "{}" --exist-ok'.format(yolo_results_folder,'yolo_results')
766
-
771
+
767
772
  # This is the NMS IoU threshold
768
773
  # cmd += ' --iou-thres 0.6'
769
-
774
+
770
775
  if options.augment:
771
776
  cmd += ' --augment'
772
-
777
+
773
778
  # --half is a store_true argument for YOLOv5's val.py
774
779
  if (options.half_precision_enabled is not None) and (options.half_precision_enabled == 1):
775
780
  cmd += ' --half'
776
-
781
+
777
782
  # Sometimes useful for debugging
778
783
  # cmd += ' --save_conf --save_txt'
779
-
784
+
780
785
  elif options.model_type == 'ultralytics':
781
-
786
+
782
787
  if options.augment:
783
788
  augment_string = 'augment'
784
789
  else:
785
790
  augment_string = ''
786
-
791
+
787
792
  cmd = 'yolo val {} model="{}" imgsz={} batch={} data="{}" project="{}" name="{}" device="{}"'.\
788
793
  format(augment_string,model_filename,image_size_string,options.batch_size,
789
794
  yolo_dataset_file,yolo_results_folder,'yolo_results',options.device_string)
790
795
  cmd += ' save_json exist_ok'
791
-
796
+
792
797
  if (options.half_precision_enabled is not None):
793
798
  if options.half_precision_enabled == 1:
794
799
  cmd += ' --half=True'
795
800
  else:
796
801
  assert options.half_precision_enabled == 0
797
802
  cmd += ' --half=False'
798
-
803
+
799
804
  # Sometimes useful for debugging
800
805
  # cmd += ' save_conf save_txt'
801
-
806
+
802
807
  else:
803
-
808
+
804
809
  raise ValueError('Unrecognized model type {}'.format(options.model_type))
805
-
810
+
806
811
  # print(cmd); import clipboard; clipboard.copy(cmd)
807
812
 
808
-
813
+
809
814
  ##%% Run YOLO command
810
-
815
+
811
816
  if options.yolo_working_folder is not None:
812
817
  current_dir = os.getcwd()
813
818
  os.chdir(options.yolo_working_folder)
814
819
 
815
820
  print('Running YOLO inference command:\n{}\n'.format(cmd))
816
-
821
+
817
822
  if options.preview_yolo_command_only:
818
-
823
+
819
824
  if options.remove_symlink_folder:
820
825
  try:
821
826
  print('Removing YOLO symlink folder {}'.format(symlink_folder))
@@ -830,34 +835,32 @@ def run_inference_with_yolo_val(options):
830
835
  except Exception:
831
836
  print('Warning: error removing YOLO results folder {}'.format(yolo_results_folder))
832
837
  pass
833
-
838
+
834
839
  # sys.exit()
835
840
  return
836
-
841
+
837
842
  execution_result = process_utils.execute_and_print(cmd,encoding='utf-8',verbose=True)
838
843
  assert execution_result['status'] == 0, 'Error running {}'.format(options.model_type)
839
844
  yolo_console_output = execution_result['output']
840
-
845
+
841
846
  if options.save_yolo_debug_output:
842
-
847
+
843
848
  with open(os.path.join(yolo_results_folder,'yolo_console_output.txt'),'w',encoding='utf-8') as f:
844
849
  for s in yolo_console_output:
845
850
  f.write(s + '\n')
846
- with open(os.path.join(yolo_results_folder,'image_id_to_file.json'),'w') as f:
847
- json.dump(image_id_to_file,f,indent=1)
848
- with open(os.path.join(yolo_results_folder,'image_id_to_error.json'),'w') as f:
849
- json.dump(image_id_to_error,f,indent=1)
850
-
851
-
851
+ ct_utils.write_json(os.path.join(yolo_results_folder,'image_id_to_file.json'), image_id_to_file)
852
+ ct_utils.write_json(os.path.join(yolo_results_folder,'image_id_to_error.json'), image_id_to_error)
853
+
854
+
852
855
  # YOLO console output contains lots of ANSI escape codes, remove them for easier parsing
853
856
  yolo_console_output = [string_utils.remove_ansi_codes(s) for s in yolo_console_output]
854
-
857
+
855
858
  # Find errors that occurred during the initial corruption check; these will not be included in the
856
859
  # output. Errors that occur during inference will be handled separately.
857
860
  yolo_read_failures = []
858
-
861
+
859
862
  for line in yolo_console_output:
860
-
863
+
861
864
  #
862
865
  # Lines indicating read failures look like:
863
866
  #
@@ -869,30 +872,30 @@ def run_inference_with_yolo_val(options):
869
872
  #
870
873
  # line = "test: WARNING: a/b/c/d.jpg: ignoring corrupt image/label: cannot identify image file '/a/b/c/d.jpg'"
871
874
  #
872
- # In both cases, when we are using symlinks, the first filename is the symlink name, the
875
+ # In both cases, when we are using symlinks, the first filename is the symlink name, the
873
876
  # second filename is the target, e.g.:
874
- #
877
+ #
875
878
  # line = "test: WARNING: /tmp/md_to_yolo/md_to_yolo_xyz/symlinks/xyz/0000000004.jpg: ignoring corrupt image/label: cannot identify image file '/tmp/md-tests/md-test-images/corrupt-images/real-file.jpg'"
876
879
  #
877
880
  # Windows example:
878
881
  #
879
882
  # line = "test: WARNING: g:\\temp\\md-test-images\\corrupt-images\\irfanview-can-still-read-me-caltech_camera_traps_5a0e37cc-23d2-11e8-a6a3-ec086b02610b.jpg: ignoring corrupt image/label: cannot identify image file 'g:\\\\temp\\\\md-test-images\\\\corrupt-images\\\\irfanview-can-still-read-me-caltech_camera_traps_5a0e37cc-23d2-11e8-a6a3-ec086b02610b.jpg'"
880
883
  #
881
-
884
+
882
885
  line = line.replace('⚠️',':')
883
886
  if 'ignoring corrupt image/label' in line:
884
-
887
+
885
888
  line_tokens = line.split('ignoring corrupt image/label')
886
889
  assert len(line_tokens) == 2
887
-
890
+
888
891
  tokens = line_tokens[0].split(':') # ,maxsplit=3)
889
892
  tokens = [s.strip() for s in tokens]
890
-
893
+
891
894
  # ['test', ' WARNING', ' a/b/c/d.jpg', ' ']
892
895
  assert len(tokens[-1]) == 0
893
896
  tokens = tokens[:-1]
894
897
  assert 'warning' in tokens[1].lower()
895
-
898
+
896
899
  if len(tokens) == 3:
897
900
  image_name = tokens[2].strip()
898
901
  else:
@@ -900,28 +903,28 @@ def run_inference_with_yolo_val(options):
900
903
  assert len(tokens) == 4
901
904
  assert len(tokens[2]) == 1
902
905
  image_name = ':'.join(tokens[2:4])
903
-
906
+
904
907
  yolo_read_failures.append(image_name)
905
-
908
+
906
909
  # ...if this line indicated a corrupt image
907
-
910
+
908
911
  # ...for each line in the console output
909
-
912
+
910
913
  # image_file = yolo_read_failures[0]
911
914
  for image_file in yolo_read_failures:
912
915
  image_id = os.path.splitext(os.path.basename(image_file))[0]
913
916
  assert image_id in image_id_to_file, 'Unexpected image ID {}'.format(image_id)
914
917
  if image_id not in image_id_to_error:
915
918
  image_id_to_error[image_id] = 'YOLO read failure'
916
-
919
+
917
920
  if options.yolo_working_folder is not None:
918
921
  os.chdir(current_dir)
919
-
920
-
922
+
923
+
921
924
  ##%% Convert results to MD format
922
-
925
+
923
926
  json_files = glob.glob(yolo_results_folder + '/yolo_results/*.json')
924
- assert len(json_files) == 1
927
+ assert len(json_files) == 1
925
928
  yolo_json_file = json_files[0]
926
929
 
927
930
  # Map YOLO image IDs to paths
@@ -939,14 +942,14 @@ def run_inference_with_yolo_val(options):
939
942
  # as the base path in this case.
940
943
  relative_path = fn
941
944
  image_id_to_relative_path[image_id] = relative_path
942
-
945
+
943
946
  # Are we working with a base folder?
944
947
  if options.input_folder is not None:
945
948
  assert os.path.isdir(options.input_folder)
946
949
  image_base = options.input_folder
947
950
  else:
948
951
  image_base = '/'
949
-
952
+
950
953
  yolo_output_to_md_output.yolo_json_output_to_md_output(
951
954
  yolo_json_file=yolo_json_file,
952
955
  image_folder=image_base,
@@ -958,34 +961,32 @@ def run_inference_with_yolo_val(options):
958
961
 
959
962
 
960
963
  ##%% Clean up
961
-
964
+
962
965
  _clean_up_temporary_folders(options,
963
966
  symlink_folder,yolo_results_folder,
964
967
  symlink_folder_is_temp_folder,yolo_folder_is_temp_folder)
965
-
968
+
966
969
  # ...def run_inference_with_yolo_val()
967
970
 
968
971
 
969
972
  #%% Command-line driver
970
973
 
971
- import argparse
972
- from megadetector.utils.ct_utils import args_to_object
974
+ def main(): # noqa
973
975
 
974
- def main():
975
-
976
976
  options = YoloInferenceOptions()
977
-
977
+
978
978
  parser = argparse.ArgumentParser()
979
979
  parser.add_argument(
980
980
  'model_filename',type=str,
981
981
  help='model file name')
982
982
  parser.add_argument(
983
983
  'input_folder',type=str,
984
- help='folder on which to recursively run the model, or a .json or .txt file containing a list of absolute image paths')
984
+ help='folder on which to recursively run the model, or a .json or .txt file ' + \
985
+ 'containing a list of absolute image paths')
985
986
  parser.add_argument(
986
987
  'output_file',type=str,
987
988
  help='.json file where output will be written')
988
-
989
+
989
990
  parser.add_argument(
990
991
  '--image_filename_list',type=str,default=None,
991
992
  help='.json or .txt file containing a list of relative image filenames within [input_folder]')
@@ -1005,10 +1006,12 @@ def main():
1005
1006
  help='inference batch size (default {})'.format(options.batch_size))
1006
1007
  parser.add_argument(
1007
1008
  '--half_precision_enabled', default=None, type=int,
1008
- help='use half-precision-inference (1 or 0) (default is the underlying model\'s default, probably full for YOLOv8 and half for YOLOv5')
1009
+ help='use half-precision-inference (1 or 0) (default is the underlying model\'s default, ' + \
1010
+ 'probably full for YOLOv8 and half for YOLOv5')
1009
1011
  parser.add_argument(
1010
1012
  '--device_string', default=options.device_string, type=str,
1011
- help='CUDA device specifier, typically "0" or "1" for CUDA devices, "mps" for M1/M2 devices, or "cpu" (default {})'.format(
1013
+ help='CUDA device specifier, typically "0" or "1" for CUDA devices, "mps" for ' + \
1014
+ 'M1/M2 devices, or "cpu" (default {})'.format(
1012
1015
  options.device_string))
1013
1016
  parser.add_argument(
1014
1017
  '--overwrite_handling', default=options.overwrite_handling, type=str,
@@ -1048,36 +1051,36 @@ def main():
1048
1051
  parser.add_argument(
1049
1052
  '--checkpoint_frequency', default=options.checkpoint_frequency, type=int,
1050
1053
  help='break the job into chunks with no more than this many images (default {})'.format(
1051
- options.checkpoint_frequency))
1054
+ options.checkpoint_frequency))
1052
1055
  parser.add_argument(
1053
1056
  '--no_append_job_id_to_symlink_folder', action='store_true',
1054
1057
  help="don't append a unique job ID to the symlink folder name")
1055
1058
  parser.add_argument(
1056
1059
  '--nonrecursive', action='store_true',
1057
1060
  help='disable recursive folder processing')
1058
-
1061
+
1059
1062
  parser.add_argument(
1060
1063
  '--preview_yolo_command_only', action='store_true',
1061
1064
  help='don\'t run inference, just preview the YOLO inference command (still creates symlinks)')
1062
-
1065
+
1063
1066
  if options.augment:
1064
1067
  default_augment_enabled = 1
1065
1068
  else:
1066
1069
  default_augment_enabled = 0
1067
-
1070
+
1068
1071
  parser.add_argument(
1069
1072
  '--augment_enabled', default=default_augment_enabled, type=int,
1070
1073
  help='enable/disable augmentation (default {})'.format(default_augment_enabled))
1071
-
1074
+
1072
1075
  if len(sys.argv[1:]) == 0:
1073
1076
  parser.print_help()
1074
1077
  parser.exit()
1075
1078
 
1076
1079
  args = parser.parse_args()
1077
-
1080
+
1078
1081
  # If the caller hasn't specified an image size, choose one based on whether augmentation
1079
1082
  # is enabled.
1080
- if args.image_size is None:
1083
+ if args.image_size is None:
1081
1084
  assert args.augment_enabled in (0,1), \
1082
1085
  'Illegal augment_enabled value {}'.format(args.augment_enabled)
1083
1086
  if args.augment_enabled == 1:
@@ -1089,38 +1092,38 @@ def main():
1089
1092
  augment_enabled_string = 'disabled'
1090
1093
  print('Augmentation is {}, using default image size {}'.format(
1091
1094
  augment_enabled_string,args.image_size))
1092
-
1095
+
1093
1096
  args_to_object(args, options)
1094
-
1097
+
1095
1098
  if args.yolo_dataset_file is not None:
1096
1099
  options.yolo_category_id_to_name = args.yolo_dataset_file
1097
-
1098
- # The function convention is that input_folder should be None when we want to use a list of
1099
- # absolute paths, but the CLI convention is that the required argument is always valid, whether
1100
+
1101
+ # The function convention is that input_folder should be None when we want to use a list of
1102
+ # absolute paths, but the CLI convention is that the required argument is always valid, whether
1100
1103
  # it's a folder or a list of absolute paths.
1101
1104
  if os.path.isfile(options.input_folder):
1102
1105
  assert options.image_filename_list is None, \
1103
1106
  'image_filename_list should not be specified when input_folder is a file'
1104
1107
  options.image_filename_list = options.input_folder
1105
- options.input_folder = None
1106
-
1108
+ options.input_folder = None
1109
+
1107
1110
  options.recursive = (not options.nonrecursive)
1108
1111
  options.append_job_id_to_symlink_folder = (not options.no_append_job_id_to_symlink_folder)
1109
1112
  options.remove_symlink_folder = (not options.no_remove_symlink_folder)
1110
1113
  options.remove_yolo_results_folder = (not options.no_remove_yolo_results_folder)
1111
1114
  options.use_symlinks = (not options.no_use_symlinks)
1112
- options.augment = (options.augment_enabled > 0)
1113
-
1115
+ options.augment = (options.augment_enabled > 0)
1116
+
1114
1117
  del options.nonrecursive
1115
1118
  del options.no_remove_symlink_folder
1116
1119
  del options.no_remove_yolo_results_folder
1117
1120
  del options.no_use_symlinks
1118
1121
  del options.augment_enabled
1119
1122
  del options.yolo_dataset_file
1120
-
1123
+
1121
1124
  print(options.__dict__)
1122
-
1123
- run_inference_with_yolo_val(options)
1125
+
1126
+ run_inference_with_yolo_val(options)
1124
1127
 
1125
1128
  if __name__ == '__main__':
1126
1129
  main()
@@ -1133,7 +1136,7 @@ if False:
1133
1136
 
1134
1137
  #%% Debugging
1135
1138
 
1136
- input_folder = r'g:\temp\md-test-images'
1139
+ input_folder = r'g:\temp\md-test-images'
1137
1140
  model_filename = 'MDV5A'
1138
1141
  output_folder = r'g:\temp\yolo-test-out'
1139
1142
  yolo_working_folder = r'c:\git\yolov5-md'
@@ -1142,16 +1145,16 @@ if False:
1142
1145
  symlink_folder = os.path.join(output_folder,'symlinks')
1143
1146
  yolo_results_folder = os.path.join(output_folder,'yolo_results')
1144
1147
  model_name = os.path.splitext(os.path.basename(model_filename))[0]
1145
-
1148
+
1146
1149
  output_file = os.path.join(output_folder,'{}_{}-md_format.json'.format(
1147
1150
  job_name,model_name))
1148
-
1151
+
1149
1152
  options = YoloInferenceOptions()
1150
-
1153
+
1151
1154
  options.yolo_working_folder = yolo_working_folder
1152
1155
  options.input_folder = input_folder
1153
1156
  options.output_file = output_file
1154
-
1157
+
1155
1158
  options.yolo_category_id_to_name = dataset_file
1156
1159
  options.augment = False
1157
1160
  options.conf_thres = '0.001'
@@ -1164,18 +1167,18 @@ if False:
1164
1167
  options.image_size = round(1280 * 1.3)
1165
1168
  else:
1166
1169
  options.image_size = 1280
1167
-
1170
+
1168
1171
  options.model_filename = model_filename
1169
-
1170
- options.yolo_results_folder = yolo_results_folder # os.path.join(output_folder + 'yolo_results')
1172
+
1173
+ options.yolo_results_folder = yolo_results_folder # os.path.join(output_folder + 'yolo_results')
1171
1174
  options.symlink_folder = symlink_folder # os.path.join(output_folder,'symlinks')
1172
1175
  options.use_symlinks = False
1173
-
1176
+
1174
1177
  options.remove_symlink_folder = True
1175
1178
  options.remove_yolo_results_folder = True
1176
-
1179
+
1177
1180
  options.checkpoint_frequency = None
1178
-
1181
+
1179
1182
  cmd = f'python run_inference_with_yolov5_val.py {model_filename} {input_folder} ' + \
1180
1183
  f'{output_file} --yolo_working_folder {yolo_working_folder} ' + \
1181
1184
  f' --image_size {options.image_size} --conf_thres {options.conf_thres} ' + \
@@ -1183,10 +1186,10 @@ if False:
1183
1186
  f' --symlink_folder {options.symlink_folder} --yolo_results_folder {options.yolo_results_folder} ' + \
1184
1187
  f' --yolo_dataset_file {options.yolo_category_id_to_name} ' + \
1185
1188
  f' --unique_id_strategy {options.unique_id_strategy} --overwrite_handling {options.overwrite_handling}'
1186
-
1189
+
1187
1190
  if not options.remove_symlink_folder:
1188
1191
  cmd += ' --no_remove_symlink_folder'
1189
- if not options.remove_yolo_results_folder:
1192
+ if not options.remove_yolo_results_folder:
1190
1193
  cmd += ' --no_remove_yolo_results_folder'
1191
1194
  if options.checkpoint_frequency is not None:
1192
1195
  cmd += f' --checkpoint_frequency {options.checkpoint_frequency}'
@@ -1194,7 +1197,7 @@ if False:
1194
1197
  cmd += ' --no_use_symlinks'
1195
1198
  if not options.augment:
1196
1199
  cmd += ' --augment_enabled 0'
1197
-
1200
+
1198
1201
  print(cmd)
1199
1202
  execute_in_python = False
1200
1203
  if execute_in_python:
@@ -1205,47 +1208,47 @@ if False:
1205
1208
 
1206
1209
 
1207
1210
  #%% Run inference on a folder
1208
-
1211
+
1209
1212
  input_folder = r'g:\temp\tegu-val-mini'.replace('\\','/')
1210
1213
  model_filename = r'g:\temp\usgs-tegus-yolov5x-231003-b8-img1280-e3002-best.pt'
1211
1214
  output_folder = r'g:\temp\tegu-scratch'
1212
1215
  yolo_working_folder = r'c:\git\yolov5-tegus'
1213
1216
  dataset_file = r'g:\temp\dataset.yaml'
1214
-
1217
+
1215
1218
  # This only impacts the output file name, it's not passed to the inference function
1216
1219
  job_name = 'yolo-inference-test'
1217
-
1220
+
1218
1221
  model_name = os.path.splitext(os.path.basename(model_filename))[0]
1219
-
1222
+
1220
1223
  symlink_folder = os.path.join(output_folder,'symlinks')
1221
1224
  yolo_results_folder = os.path.join(output_folder,'yolo_results')
1222
-
1225
+
1223
1226
  output_file = os.path.join(output_folder,'{}_{}-md_format.json'.format(
1224
1227
  job_name,model_name))
1225
-
1228
+
1226
1229
  options = YoloInferenceOptions()
1227
-
1230
+
1228
1231
  options.yolo_working_folder = yolo_working_folder
1229
1232
  options.input_folder = input_folder
1230
1233
  options.output_file = output_file
1231
-
1232
- pass_image_filename_list = False
1234
+
1235
+ pass_image_filename_list = False
1233
1236
  pass_relative_paths = True
1234
-
1237
+
1235
1238
  if pass_image_filename_list:
1236
1239
  if pass_relative_paths:
1237
1240
  options.image_filename_list = [
1238
1241
  r"val#american_cardinal#american_cardinal#CaCa#31W.01_C83#2017-2019#C90 and C83_31W.01#(05) 18AUG17 - 05SEP17 FTC AEG#MFDC1949_000065.JPG",
1239
1242
  r"val#american_cardinal#american_cardinal#CaCa#31W.01_C83#2017-2019#C90 and C83_31W.01#(04) 27JUL17 - 18AUG17 FTC AEG#MFDC1902_000064.JPG"
1240
- ]
1243
+ ]
1241
1244
  else:
1242
1245
  options.image_filename_list = [
1243
1246
  r"g:/temp/tegu-val-mini/val#american_cardinal#american_cardinal#CaCa#31W.01_C83#2017-2019#C90 and C83_31W.01#(05) 18AUG17 - 05SEP17 FTC AEG#MFDC1949_000065.JPG",
1244
1247
  r"g:/temp/tegu-val-mini/val#american_cardinal#american_cardinal#CaCa#31W.01_C83#2017-2019#C90 and C83_31W.01#(04) 27JUL17 - 18AUG17 FTC AEG#MFDC1902_000064.JPG"
1245
1248
  ]
1246
1249
  else:
1247
- options.image_filename_list = None
1248
-
1250
+ options.image_filename_list = None
1251
+
1249
1252
  options.yolo_category_id_to_name = dataset_file
1250
1253
  options.augment = False
1251
1254
  options.conf_thres = '0.001'
@@ -1258,18 +1261,18 @@ if False:
1258
1261
  options.image_size = round(1280 * 1.3)
1259
1262
  else:
1260
1263
  options.image_size = 1280
1261
-
1264
+
1262
1265
  options.model_filename = model_filename
1263
-
1264
- options.yolo_results_folder = yolo_results_folder # os.path.join(output_folder + 'yolo_results')
1266
+
1267
+ options.yolo_results_folder = yolo_results_folder # os.path.join(output_folder + 'yolo_results')
1265
1268
  options.symlink_folder = symlink_folder # os.path.join(output_folder,'symlinks')
1266
1269
  options.use_symlinks = False
1267
-
1270
+
1268
1271
  options.remove_symlink_folder = True
1269
1272
  options.remove_yolo_results_folder = True
1270
-
1273
+
1271
1274
  options.checkpoint_frequency = 5
1272
-
1275
+
1273
1276
  cmd = f'python run_inference_with_yolov5_val.py {model_filename} {input_folder} ' + \
1274
1277
  f'{output_file} --yolo_working_folder {yolo_working_folder} ' + \
1275
1278
  f' --image_size {options.image_size} --conf_thres {options.conf_thres} ' + \
@@ -1277,10 +1280,10 @@ if False:
1277
1280
  f' --symlink_folder {options.symlink_folder} --yolo_results_folder {options.yolo_results_folder} ' + \
1278
1281
  f' --yolo_dataset_file {options.yolo_category_id_to_name} ' + \
1279
1282
  f' --unique_id_strategy {options.unique_id_strategy} --overwrite_handling {options.overwrite_handling}'
1280
-
1283
+
1281
1284
  if not options.remove_symlink_folder:
1282
1285
  cmd += ' --no_remove_symlink_folder'
1283
- if not options.remove_yolo_results_folder:
1286
+ if not options.remove_yolo_results_folder:
1284
1287
  cmd += ' --no_remove_yolo_results_folder'
1285
1288
  if options.checkpoint_frequency is not None:
1286
1289
  cmd += f' --checkpoint_frequency {options.checkpoint_frequency}'
@@ -1288,7 +1291,7 @@ if False:
1288
1291
  cmd += ' --no_use_symlinks'
1289
1292
  if not options.augment:
1290
1293
  cmd += ' --augment_enabled 0'
1291
-
1294
+
1292
1295
  print(cmd)
1293
1296
  execute_in_python = False
1294
1297
  if execute_in_python: