megadetector 5.0.11__py3-none-any.whl → 5.0.13__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 (203) hide show
  1. megadetector/api/__init__.py +0 -0
  2. megadetector/api/batch_processing/__init__.py +0 -0
  3. megadetector/api/batch_processing/api_core/__init__.py +0 -0
  4. megadetector/api/batch_processing/api_core/batch_service/__init__.py +0 -0
  5. megadetector/api/batch_processing/api_core/batch_service/score.py +439 -0
  6. megadetector/api/batch_processing/api_core/server.py +294 -0
  7. megadetector/api/batch_processing/api_core/server_api_config.py +97 -0
  8. megadetector/api/batch_processing/api_core/server_app_config.py +55 -0
  9. megadetector/api/batch_processing/api_core/server_batch_job_manager.py +220 -0
  10. megadetector/api/batch_processing/api_core/server_job_status_table.py +149 -0
  11. megadetector/api/batch_processing/api_core/server_orchestration.py +360 -0
  12. megadetector/api/batch_processing/api_core/server_utils.py +88 -0
  13. megadetector/api/batch_processing/api_core_support/__init__.py +0 -0
  14. megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +46 -0
  15. megadetector/api/batch_processing/api_support/__init__.py +0 -0
  16. megadetector/api/batch_processing/api_support/summarize_daily_activity.py +152 -0
  17. megadetector/api/batch_processing/data_preparation/__init__.py +0 -0
  18. megadetector/api/batch_processing/integration/digiKam/setup.py +6 -0
  19. megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +465 -0
  20. megadetector/api/batch_processing/integration/eMammal/test_scripts/config_template.py +5 -0
  21. megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +125 -0
  22. megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +55 -0
  23. megadetector/api/synchronous/__init__.py +0 -0
  24. megadetector/api/synchronous/api_core/animal_detection_api/__init__.py +0 -0
  25. megadetector/api/synchronous/api_core/animal_detection_api/api_backend.py +152 -0
  26. megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +263 -0
  27. megadetector/api/synchronous/api_core/animal_detection_api/config.py +35 -0
  28. megadetector/api/synchronous/api_core/tests/__init__.py +0 -0
  29. megadetector/api/synchronous/api_core/tests/load_test.py +110 -0
  30. megadetector/classification/__init__.py +0 -0
  31. megadetector/classification/aggregate_classifier_probs.py +108 -0
  32. megadetector/classification/analyze_failed_images.py +227 -0
  33. megadetector/classification/cache_batchapi_outputs.py +198 -0
  34. megadetector/classification/create_classification_dataset.py +627 -0
  35. megadetector/classification/crop_detections.py +516 -0
  36. megadetector/classification/csv_to_json.py +226 -0
  37. megadetector/classification/detect_and_crop.py +855 -0
  38. megadetector/classification/efficientnet/__init__.py +9 -0
  39. megadetector/classification/efficientnet/model.py +415 -0
  40. megadetector/classification/efficientnet/utils.py +607 -0
  41. megadetector/classification/evaluate_model.py +520 -0
  42. megadetector/classification/identify_mislabeled_candidates.py +152 -0
  43. megadetector/classification/json_to_azcopy_list.py +63 -0
  44. megadetector/classification/json_validator.py +699 -0
  45. megadetector/classification/map_classification_categories.py +276 -0
  46. megadetector/classification/merge_classification_detection_output.py +506 -0
  47. megadetector/classification/prepare_classification_script.py +194 -0
  48. megadetector/classification/prepare_classification_script_mc.py +228 -0
  49. megadetector/classification/run_classifier.py +287 -0
  50. megadetector/classification/save_mislabeled.py +110 -0
  51. megadetector/classification/train_classifier.py +827 -0
  52. megadetector/classification/train_classifier_tf.py +725 -0
  53. megadetector/classification/train_utils.py +323 -0
  54. megadetector/data_management/__init__.py +0 -0
  55. megadetector/data_management/annotations/__init__.py +0 -0
  56. megadetector/data_management/annotations/annotation_constants.py +34 -0
  57. megadetector/data_management/camtrap_dp_to_coco.py +237 -0
  58. megadetector/data_management/cct_json_utils.py +404 -0
  59. megadetector/data_management/cct_to_md.py +176 -0
  60. megadetector/data_management/cct_to_wi.py +289 -0
  61. megadetector/data_management/coco_to_labelme.py +283 -0
  62. megadetector/data_management/coco_to_yolo.py +662 -0
  63. megadetector/data_management/databases/__init__.py +0 -0
  64. megadetector/data_management/databases/add_width_and_height_to_db.py +33 -0
  65. megadetector/data_management/databases/combine_coco_camera_traps_files.py +206 -0
  66. megadetector/data_management/databases/integrity_check_json_db.py +493 -0
  67. megadetector/data_management/databases/subset_json_db.py +115 -0
  68. megadetector/data_management/generate_crops_from_cct.py +149 -0
  69. megadetector/data_management/get_image_sizes.py +189 -0
  70. megadetector/data_management/importers/add_nacti_sizes.py +52 -0
  71. megadetector/data_management/importers/add_timestamps_to_icct.py +79 -0
  72. megadetector/data_management/importers/animl_results_to_md_results.py +158 -0
  73. megadetector/data_management/importers/auckland_doc_test_to_json.py +373 -0
  74. megadetector/data_management/importers/auckland_doc_to_json.py +201 -0
  75. megadetector/data_management/importers/awc_to_json.py +191 -0
  76. megadetector/data_management/importers/bellevue_to_json.py +273 -0
  77. megadetector/data_management/importers/cacophony-thermal-importer.py +793 -0
  78. megadetector/data_management/importers/carrizo_shrubfree_2018.py +269 -0
  79. megadetector/data_management/importers/carrizo_trail_cam_2017.py +289 -0
  80. megadetector/data_management/importers/cct_field_adjustments.py +58 -0
  81. megadetector/data_management/importers/channel_islands_to_cct.py +913 -0
  82. megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +180 -0
  83. megadetector/data_management/importers/eMammal/eMammal_helpers.py +249 -0
  84. megadetector/data_management/importers/eMammal/make_eMammal_json.py +223 -0
  85. megadetector/data_management/importers/ena24_to_json.py +276 -0
  86. megadetector/data_management/importers/filenames_to_json.py +386 -0
  87. megadetector/data_management/importers/helena_to_cct.py +283 -0
  88. megadetector/data_management/importers/idaho-camera-traps.py +1407 -0
  89. megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +294 -0
  90. megadetector/data_management/importers/jb_csv_to_json.py +150 -0
  91. megadetector/data_management/importers/mcgill_to_json.py +250 -0
  92. megadetector/data_management/importers/missouri_to_json.py +490 -0
  93. megadetector/data_management/importers/nacti_fieldname_adjustments.py +79 -0
  94. megadetector/data_management/importers/noaa_seals_2019.py +181 -0
  95. megadetector/data_management/importers/pc_to_json.py +365 -0
  96. megadetector/data_management/importers/plot_wni_giraffes.py +123 -0
  97. megadetector/data_management/importers/prepare-noaa-fish-data-for-lila.py +359 -0
  98. megadetector/data_management/importers/prepare_zsl_imerit.py +131 -0
  99. megadetector/data_management/importers/rspb_to_json.py +356 -0
  100. megadetector/data_management/importers/save_the_elephants_survey_A.py +320 -0
  101. megadetector/data_management/importers/save_the_elephants_survey_B.py +329 -0
  102. megadetector/data_management/importers/snapshot_safari_importer.py +758 -0
  103. megadetector/data_management/importers/snapshot_safari_importer_reprise.py +665 -0
  104. megadetector/data_management/importers/snapshot_serengeti_lila.py +1067 -0
  105. megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +150 -0
  106. megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +153 -0
  107. megadetector/data_management/importers/sulross_get_exif.py +65 -0
  108. megadetector/data_management/importers/timelapse_csv_set_to_json.py +490 -0
  109. megadetector/data_management/importers/ubc_to_json.py +399 -0
  110. megadetector/data_management/importers/umn_to_json.py +507 -0
  111. megadetector/data_management/importers/wellington_to_json.py +263 -0
  112. megadetector/data_management/importers/wi_to_json.py +442 -0
  113. megadetector/data_management/importers/zamba_results_to_md_results.py +181 -0
  114. megadetector/data_management/labelme_to_coco.py +547 -0
  115. megadetector/data_management/labelme_to_yolo.py +272 -0
  116. megadetector/data_management/lila/__init__.py +0 -0
  117. megadetector/data_management/lila/add_locations_to_island_camera_traps.py +97 -0
  118. megadetector/data_management/lila/add_locations_to_nacti.py +147 -0
  119. megadetector/data_management/lila/create_lila_blank_set.py +558 -0
  120. megadetector/data_management/lila/create_lila_test_set.py +152 -0
  121. megadetector/data_management/lila/create_links_to_md_results_files.py +106 -0
  122. megadetector/data_management/lila/download_lila_subset.py +178 -0
  123. megadetector/data_management/lila/generate_lila_per_image_labels.py +516 -0
  124. megadetector/data_management/lila/get_lila_annotation_counts.py +170 -0
  125. megadetector/data_management/lila/get_lila_image_counts.py +112 -0
  126. megadetector/data_management/lila/lila_common.py +300 -0
  127. megadetector/data_management/lila/test_lila_metadata_urls.py +132 -0
  128. megadetector/data_management/ocr_tools.py +870 -0
  129. megadetector/data_management/read_exif.py +809 -0
  130. megadetector/data_management/remap_coco_categories.py +84 -0
  131. megadetector/data_management/remove_exif.py +66 -0
  132. megadetector/data_management/rename_images.py +187 -0
  133. megadetector/data_management/resize_coco_dataset.py +189 -0
  134. megadetector/data_management/wi_download_csv_to_coco.py +247 -0
  135. megadetector/data_management/yolo_output_to_md_output.py +446 -0
  136. megadetector/data_management/yolo_to_coco.py +676 -0
  137. megadetector/detection/__init__.py +0 -0
  138. megadetector/detection/detector_training/__init__.py +0 -0
  139. megadetector/detection/detector_training/model_main_tf2.py +114 -0
  140. megadetector/detection/process_video.py +846 -0
  141. megadetector/detection/pytorch_detector.py +355 -0
  142. megadetector/detection/run_detector.py +779 -0
  143. megadetector/detection/run_detector_batch.py +1219 -0
  144. megadetector/detection/run_inference_with_yolov5_val.py +1087 -0
  145. megadetector/detection/run_tiled_inference.py +934 -0
  146. megadetector/detection/tf_detector.py +192 -0
  147. megadetector/detection/video_utils.py +698 -0
  148. megadetector/postprocessing/__init__.py +0 -0
  149. megadetector/postprocessing/add_max_conf.py +64 -0
  150. megadetector/postprocessing/categorize_detections_by_size.py +165 -0
  151. megadetector/postprocessing/classification_postprocessing.py +716 -0
  152. megadetector/postprocessing/combine_api_outputs.py +249 -0
  153. megadetector/postprocessing/compare_batch_results.py +966 -0
  154. megadetector/postprocessing/convert_output_format.py +396 -0
  155. megadetector/postprocessing/load_api_results.py +195 -0
  156. megadetector/postprocessing/md_to_coco.py +310 -0
  157. megadetector/postprocessing/md_to_labelme.py +330 -0
  158. megadetector/postprocessing/merge_detections.py +412 -0
  159. megadetector/postprocessing/postprocess_batch_results.py +1908 -0
  160. megadetector/postprocessing/remap_detection_categories.py +170 -0
  161. megadetector/postprocessing/render_detection_confusion_matrix.py +660 -0
  162. megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +211 -0
  163. megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +83 -0
  164. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +1635 -0
  165. megadetector/postprocessing/separate_detections_into_folders.py +730 -0
  166. megadetector/postprocessing/subset_json_detector_output.py +700 -0
  167. megadetector/postprocessing/top_folders_to_bottom.py +223 -0
  168. megadetector/taxonomy_mapping/__init__.py +0 -0
  169. megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +491 -0
  170. megadetector/taxonomy_mapping/map_new_lila_datasets.py +150 -0
  171. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +142 -0
  172. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +588 -0
  173. megadetector/taxonomy_mapping/retrieve_sample_image.py +71 -0
  174. megadetector/taxonomy_mapping/simple_image_download.py +219 -0
  175. megadetector/taxonomy_mapping/species_lookup.py +834 -0
  176. megadetector/taxonomy_mapping/taxonomy_csv_checker.py +159 -0
  177. megadetector/taxonomy_mapping/taxonomy_graph.py +346 -0
  178. megadetector/taxonomy_mapping/validate_lila_category_mappings.py +83 -0
  179. megadetector/utils/__init__.py +0 -0
  180. megadetector/utils/azure_utils.py +178 -0
  181. megadetector/utils/ct_utils.py +613 -0
  182. megadetector/utils/directory_listing.py +246 -0
  183. megadetector/utils/md_tests.py +1164 -0
  184. megadetector/utils/path_utils.py +1045 -0
  185. megadetector/utils/process_utils.py +160 -0
  186. megadetector/utils/sas_blob_utils.py +509 -0
  187. megadetector/utils/split_locations_into_train_val.py +228 -0
  188. megadetector/utils/string_utils.py +92 -0
  189. megadetector/utils/url_utils.py +323 -0
  190. megadetector/utils/write_html_image_list.py +225 -0
  191. megadetector/visualization/__init__.py +0 -0
  192. megadetector/visualization/plot_utils.py +293 -0
  193. megadetector/visualization/render_images_with_thumbnails.py +275 -0
  194. megadetector/visualization/visualization_utils.py +1536 -0
  195. megadetector/visualization/visualize_db.py +552 -0
  196. megadetector/visualization/visualize_detector_output.py +405 -0
  197. {megadetector-5.0.11.dist-info → megadetector-5.0.13.dist-info}/LICENSE +0 -0
  198. {megadetector-5.0.11.dist-info → megadetector-5.0.13.dist-info}/METADATA +2 -2
  199. megadetector-5.0.13.dist-info/RECORD +201 -0
  200. megadetector-5.0.13.dist-info/top_level.txt +1 -0
  201. megadetector-5.0.11.dist-info/RECORD +0 -5
  202. megadetector-5.0.11.dist-info/top_level.txt +0 -1
  203. {megadetector-5.0.11.dist-info → megadetector-5.0.13.dist-info}/WHEEL +0 -0
@@ -0,0 +1,1087 @@
1
+ """
2
+
3
+ run_inference_with_yolov5_val.py
4
+
5
+ Runs a folder of images through MegaDetector (or another YOLOv5/YOLOv8 model) with YOLO's
6
+ val.py, converting the output to the standard MD format. The reasons this script exists,
7
+ as an alternative to the standard run_detector_batch.py are:
8
+
9
+ * This script provides access to YOLO's test-time augmentation tools.
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
12
+ training.
13
+ * This script works for any Ultralytics detection model, including YOLOv8 models
14
+
15
+ YOLOv5's val.py uses each file's base name as a unique identifier, which doesn't work
16
+ when you have typical camera trap images like:
17
+
18
+ * a/b/c/RECONYX0001.JPG
19
+ * d/e/f/RECONYX0001.JPG
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
23
+ to the real files.
24
+
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
27
+ YOLOv5. If you are running a YOLOv8 model, the folder doesn't matter, but it assumes that ultralytics
28
+ tools are available in the current environment.
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
32
+ only requires permissions to create symbolic links, but I've never seen a case where someone has
33
+ that permission and *doesn't* have admin privileges. If you are running this script on
34
+ Windows and you don't have admin privileges, use --no_use_symlinks, which will make copies of images,
35
+ rather than using symlinks.
36
+
37
+ """
38
+
39
+ #%% Imports
40
+
41
+ import os
42
+ import sys
43
+ import uuid
44
+ import glob
45
+ import tempfile
46
+ import shutil
47
+ import json
48
+ import copy
49
+
50
+ from tqdm import tqdm
51
+
52
+ from megadetector.utils import path_utils
53
+ from megadetector.utils import process_utils
54
+ from megadetector.utils import string_utils
55
+
56
+ from megadetector.utils.ct_utils import is_iterable, split_list_into_fixed_size_chunks
57
+ from megadetector.utils.path_utils import path_is_abs
58
+ from megadetector.data_management import yolo_output_to_md_output
59
+ from megadetector.detection.run_detector import try_download_known_detector
60
+ from megadetector.postprocessing.combine_api_outputs import combine_api_output_files
61
+
62
+ default_image_size_with_augmentation = int(1280 * 1.3)
63
+ default_image_size_with_no_augmentation = 1280
64
+
65
+
66
+ #%% Options class
67
+
68
+ class YoloInferenceOptions:
69
+ """
70
+ Parameters that control the behavior of run_inference_with_yolov5_val(), including
71
+ the input/output filenames.
72
+ """
73
+
74
+ def __init__(self):
75
+
76
+ ## Required-ish ##
77
+
78
+ #: Folder of images to process (can be None if image_filename_list contains absolute paths)
79
+ self.input_folder = None
80
+
81
+ #: 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
+ #: relative image paths.
86
+ #:
87
+ #: 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.
89
+ self.image_filename_list = None
90
+
91
+ #: Model filename (ending in .pt), or a well-known model name (e.g. "MDV5A")
92
+ self.model_filename = None
93
+
94
+ #: .json output file, in MD results format
95
+ self.output_file = None
96
+
97
+
98
+ ## Optional ##
99
+
100
+ #: Required for older YOLOv5 inference, not for newer ulytralytics/YOLOv8 inference
101
+ self.yolo_working_folder = None
102
+
103
+ #: Currently 'yolov5' and 'ultralytics' are supported, and really these are proxies for
104
+ #: "the yolov5 repo" and "the ultralytics repo".
105
+ self.model_type = 'yolov5'
106
+
107
+ #: Image size to use; this is a single int, which in ultralytics's terminology means
108
+ #: "scale the long side of the image to this size, and preserve aspect ratio".
109
+ self.image_size = default_image_size_with_augmentation
110
+
111
+ #: Detections below this threshold will not be included in the output file
112
+ self.conf_thres = '0.001'
113
+
114
+ #: Batch size... has no impact on results, but may create memory issues if you set
115
+ #: this to large values
116
+ self.batch_size = 1
117
+
118
+ #: Device string: typically '0' for GPU 0, '1' for GPU 1, etc., or 'cpu'
119
+ self.device_string = '0'
120
+
121
+ #: Should we enable test-time augmentation?
122
+ self.augment = True
123
+
124
+ #: Should we enable half-precision inference?
125
+ self.half_precision_enabled = None
126
+
127
+ #: Where should we stash the temporary symlinks (or copies) used to give unique identifiers to image
128
+ # files?
129
+ #:
130
+ #: If this is None, we'll create a folder in system temp space.
131
+ self.symlink_folder = None
132
+
133
+ #: Should we use symlinks to give unique identifiers to image files (vs. copies)?
134
+ self.use_symlinks = True
135
+
136
+ #: How should we guarantee that YOLO IDs (base filenames) are unique? Choices are:
137
+ #:
138
+ #: * 'verify': assume image IDs are unique, but verify and error if they're not
139
+ #: * 'links': create symlinks (or copies, depending on use_symlinks) to enforce uniqueness
140
+ #: * 'auto': check whether IDs are unique, create links if necessary
141
+ self.unique_id_strategy = 'links'
142
+
143
+ #: Temporary folder to stash intermediate YOLO results.
144
+ #:
145
+ #: If this is None, we'll create a folder in system temp space.
146
+ self.yolo_results_folder = None
147
+
148
+ #: Should we remove the symlink folder when we're done?
149
+ self.remove_symlink_folder = True
150
+
151
+ #: Should we remove the intermediate results folder when we're done?
152
+ self.remove_yolo_results_folder = True
153
+
154
+ #: These are deliberately offset from the standard MD categories; YOLOv5
155
+ #: needs categories IDs to start at 0.
156
+ #:
157
+ #: This can also be a string that points to a YOLO dataset.yaml file.
158
+ self.yolo_category_id_to_name = {0:'animal',1:'person',2:'vehicle'}
159
+
160
+ #: What should we do if the output file already exists?
161
+ #:
162
+ #: Can be 'error', 'skip', or 'overwrite'.
163
+ self.overwrite_handling = 'skip'
164
+
165
+ #: If True, we'll do a dry run that lets you preview the YOLO val command, without
166
+ #: actually running it.
167
+ self.preview_yolo_command_only = False
168
+
169
+ #: By default, if any errors occur while we're copying images or creating symlinks, it's
170
+ #: game over. If this is True, those errors become warnings, and we plow ahead.
171
+ self.treat_copy_failures_as_warnings = False
172
+
173
+ #: Save YOLO console output
174
+ self.save_yolo_debug_output = False
175
+
176
+ #: Whether to search for images recursively within [input_folder]
177
+ #:
178
+ #: Ignored if a list of files is provided.
179
+ self.recursive = True
180
+
181
+ #: Maximum number of images to run in a single chunk
182
+ self.checkpoint_frequency = None
183
+
184
+ # ...def __init__()
185
+
186
+ # ...YoloInferenceOptions()
187
+
188
+
189
+ #%% Support functions
190
+
191
+ def _clean_up_temporary_folders(options,
192
+ symlink_folder,yolo_results_folder,
193
+ symlink_folder_is_temp_folder,yolo_folder_is_temp_folder):
194
+ """
195
+ Remove temporary symlink/results folders, unless the caller requested that we leave them in place.
196
+ """
197
+
198
+ if options.remove_symlink_folder:
199
+ shutil.rmtree(symlink_folder)
200
+ elif symlink_folder_is_temp_folder:
201
+ print('Warning: using temporary symlink folder {}, but not removing it'.format(
202
+ symlink_folder))
203
+
204
+ if options.remove_yolo_results_folder:
205
+ shutil.rmtree(yolo_results_folder)
206
+ elif yolo_folder_is_temp_folder:
207
+ print('Warning: using temporary YOLO results folder {}, but not removing it'.format(
208
+ yolo_results_folder))
209
+
210
+
211
+ #%% Main function
212
+
213
+ def run_inference_with_yolo_val(options):
214
+ """
215
+ Runs a folder of images through MegaDetector (or another YOLOv5/YOLOv8 model) with YOLO's
216
+ val.py, converting the output to the standard MD format.
217
+
218
+ Args:
219
+ options (YoloInferenceOptions): all the parameters used to control this process,
220
+ including filenames; see YoloInferenceOptions for details
221
+ """
222
+
223
+ ##%% Input and path handling
224
+
225
+ default_options = YoloInferenceOptions()
226
+
227
+ for k in options.__dict__.keys():
228
+ if k not in default_options.__dict__:
229
+ print('Warning: unexpected variable {} in options object'.format(k))
230
+
231
+ if options.model_type == 'yolov8':
232
+
233
+ print('Warning: model type "yolov8" supplied, "ultralytics" is the preferred model type string for YOLOv8 models')
234
+ options.model_type = 'ultralytics'
235
+
236
+ if (options.model_type == 'yolov5') and ('yolov8' in options.model_filename.lower()):
237
+ 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')
238
+
239
+ if options.yolo_working_folder is None:
240
+ assert options.model_type == 'ultralytics', \
241
+ 'A working folder is required to run YOLOv5 val.py'
242
+ else:
243
+ assert os.path.isdir(options.yolo_working_folder), \
244
+ 'Could not find working folder {}'.format(options.yolo_working_folder)
245
+
246
+ if options.half_precision_enabled is not None:
247
+ assert options.half_precision_enabled in (0,1), \
248
+ 'Invalid value {} for --half_precision_enabled (should be 0 or 1)'.format(
249
+ options.half_precision_enabled)
250
+
251
+ # If the model filename is a known model string (e.g. "MDv5A", download the model if necessary)
252
+ model_filename = try_download_known_detector(options.model_filename)
253
+
254
+ assert os.path.isfile(model_filename), \
255
+ 'Could not find model file {}'.format(model_filename)
256
+
257
+ assert (options.input_folder is not None) or (options.image_filename_list is not None), \
258
+ 'You must specify a folder and/or a file list'
259
+
260
+ if options.input_folder is not None:
261
+ assert os.path.isdir(options.input_folder), 'Could not find input folder {}'.format(
262
+ options.input_folder)
263
+
264
+ if os.path.exists(options.output_file):
265
+ if options.overwrite_handling == 'skip':
266
+ print('Warning: output file {} exists, skipping'.format(options.output_file))
267
+ return
268
+ elif options.overwrite_handling == 'overwrite':
269
+ print('Warning: output file {} exists, overwriting'.format(options.output_file))
270
+ elif options.overwrite_handling == 'error':
271
+ raise ValueError('Output file {} exists'.format(options.output_file))
272
+ else:
273
+ raise ValueError('Unknown output handling method {}'.format(options.overwrite_handling))
274
+
275
+ os.makedirs(os.path.dirname(options.output_file),exist_ok=True)
276
+
277
+ if options.input_folder is not None:
278
+ options.input_folder = options.input_folder.replace('\\','/')
279
+
280
+
281
+ ##%% Other input handling
282
+
283
+ if isinstance(options.yolo_category_id_to_name,str):
284
+
285
+ assert os.path.isfile(options.yolo_category_id_to_name)
286
+ yolo_dataset_file = options.yolo_category_id_to_name
287
+ options.yolo_category_id_to_name = \
288
+ yolo_output_to_md_output.read_classes_from_yolo_dataset_file(yolo_dataset_file)
289
+ print('Loaded {} category mappings from {}'.format(
290
+ len(options.yolo_category_id_to_name),yolo_dataset_file))
291
+
292
+ temporary_folder = None
293
+ symlink_folder_is_temp_folder = False
294
+ yolo_folder_is_temp_folder = False
295
+
296
+ job_id = str(uuid.uuid1())
297
+
298
+ def get_job_temporary_folder(tf):
299
+ if tf is not None:
300
+ return tf
301
+ tempdir_base = tempfile.gettempdir()
302
+ tf = os.path.join(tempdir_base,'md_to_yolo','md_to_yolo_' + job_id)
303
+ os.makedirs(tf,exist_ok=True)
304
+ return tf
305
+
306
+ symlink_folder = options.symlink_folder
307
+ yolo_results_folder = options.yolo_results_folder
308
+
309
+ if symlink_folder is None:
310
+ temporary_folder = get_job_temporary_folder(temporary_folder)
311
+ symlink_folder = os.path.join(temporary_folder,'symlinks')
312
+ symlink_folder_is_temp_folder = True
313
+
314
+ if yolo_results_folder is None:
315
+ temporary_folder = get_job_temporary_folder(temporary_folder)
316
+ yolo_results_folder = os.path.join(temporary_folder,'yolo_results')
317
+ yolo_folder_is_temp_folder = True
318
+
319
+ # Attach a GUID to the symlink folder, regardless of whether we created it
320
+ symlink_folder_inner = os.path.join(symlink_folder,job_id)
321
+
322
+ os.makedirs(symlink_folder_inner,exist_ok=True)
323
+ os.makedirs(yolo_results_folder,exist_ok=True)
324
+
325
+
326
+ ##%% Enumerate images
327
+
328
+ image_files_relative = None
329
+ image_files_absolute = None
330
+
331
+ if options.image_filename_list is None:
332
+ assert options.input_folder is not None and os.path.isdir(options.input_folder), \
333
+ 'Could not find input folder {}'.format(options.input_folder)
334
+ image_files_relative = path_utils.find_images(options.input_folder,
335
+ recursive=options.recursive,
336
+ return_relative_paths=True,
337
+ convert_slashes=True)
338
+ image_files_absolute = [os.path.join(options.input_folder,fn) for \
339
+ fn in image_files_relative]
340
+ else:
341
+
342
+ if is_iterable(options.image_filename_list):
343
+
344
+ image_files_relative = options.image_filename_list
345
+
346
+ else:
347
+ assert isinstance(options.image_filename_list,str), \
348
+ 'Unrecognized image filename list object type: {}'.format(options.image_filename_list)
349
+ assert os.path.isfile(options.image_filename_list), \
350
+ 'Could not find image filename list file: {}'.format(options.image_filename_list)
351
+ ext = os.path.splitext(options.image_filename_list).lower()
352
+ assert ext in ('.json','.txt'), \
353
+ 'Unrecognized image filename list file extension: {}'.format(options.image_filename_list)
354
+ if ext == '.json':
355
+ with open(options.image_filename_list,'r') as f:
356
+ image_files_relative = json.load(f)
357
+ assert is_iterable(image_files_relative)
358
+ else:
359
+ assert ext == '.txt'
360
+ with open(options.image_filename_list,'r') as f:
361
+ image_files_relative = f.readlines()
362
+ image_files_relative = [s.strip() for s in image_files_relative]
363
+
364
+ # ...whether the image filename list was supplied as list vs. a filename
365
+
366
+ if options.input_folder is None:
367
+ image_files_absolute = image_files_relative
368
+ else:
369
+ # The list should be relative filenames
370
+ for fn in image_files_relative:
371
+ assert not path_is_abs(fn), \
372
+ 'When providing a folder and a list, paths in the list should be relative'
373
+
374
+ image_files_absolute = \
375
+ [os.path.join(options.input_folder,fn) for fn in image_files_relative]
376
+ for fn in image_files_absolute:
377
+ assert os.path.isfile(fn), 'Could not find image file {}'.format(fn)
378
+
379
+ # ...whether the caller supplied a list of filenames
380
+
381
+ image_files_absolute = [fn.replace('\\','/') for fn in image_files_absolute]
382
+ del image_files_relative
383
+
384
+
385
+ ##%% Recurse if necessary to handle checkpoints
386
+
387
+ if options.checkpoint_frequency is not None and options.checkpoint_frequency > 0:
388
+
389
+ chunks = split_list_into_fixed_size_chunks(image_files_absolute,options.checkpoint_frequency)
390
+
391
+ chunk_output_files = []
392
+
393
+ # i_chunk = 0; chunk_files_abs = chunks[i_chunk]
394
+ for i_chunk,chunk_files_abs in enumerate(chunks):
395
+
396
+ print('Processing {} images from chunk {} of {}'.format(
397
+ len(chunk_files_abs),i_chunk,len(chunks)))
398
+
399
+ chunk_options = copy.deepcopy(options)
400
+
401
+ # Run each chunk without checkpointing
402
+ chunk_options.checkpoint_frequency = None
403
+
404
+ if options.input_folder is not None:
405
+ chunk_files_relative = \
406
+ [os.path.relpath(fn,options.input_folder) for fn in chunk_files_abs]
407
+ chunk_options.image_filename_list = chunk_files_relative
408
+ else:
409
+ chunk_options.image_filename_list = chunk_files_abs
410
+
411
+ chunk_options.image_filename_list = \
412
+ [fn.replace('\\','/') for fn in chunk_options.image_filename_list]
413
+
414
+ chunk_string = 'chunk_{}'.format(str(i_chunk).zfill(5))
415
+ chunk_options.yolo_results_folder = yolo_results_folder + '_' + chunk_string
416
+ chunk_options.symlink_folder = symlink_folder + '_' + chunk_string
417
+
418
+ # Put the output file in the parent job's scratch folder
419
+ chunk_output_file = os.path.join(yolo_results_folder,chunk_string + '_results_md_format.json')
420
+ chunk_output_files.append(chunk_output_file)
421
+ chunk_options.output_file = chunk_output_file
422
+
423
+ if os.path.isfile(chunk_output_file):
424
+
425
+ print('Chunk output file {} exists, checking completeness'.format(chunk_output_file))
426
+
427
+ with open(chunk_output_file,'r') as f:
428
+ chunk_results = json.load(f)
429
+ images_in_this_chunk_results_file = [im['file'] for im in chunk_results['images']]
430
+ assert len(images_in_this_chunk_results_file) == len(chunk_options.image_filename_list), \
431
+ 'Expected {} images in chunk results file {}, found {}, possibly this is left over from a previous job?'.format(
432
+ len(chunk_options.image_filename_list),chunk_output_file,
433
+ len(images_in_this_chunk_results_file))
434
+ for fn in images_in_this_chunk_results_file:
435
+ assert fn in chunk_options.image_filename_list, \
436
+ 'Unexpected image {} in chunk results file {}, possibly this is left over from a previous job?'.format(
437
+ fn,chunk_output_file)
438
+
439
+ print('Chunk output file {} exists and is complete, skipping this chunk'.format(
440
+ chunk_output_file))
441
+
442
+ # ...if the outptut file exists
443
+
444
+ else:
445
+
446
+ run_inference_with_yolo_val(chunk_options)
447
+
448
+ # ...if we do/don't have to run this chunk
449
+
450
+ assert os.path.isfile(chunk_options.output_file)
451
+
452
+ # ...for each chunk
453
+
454
+ # Merge
455
+ _ = combine_api_output_files(input_files=chunk_output_files,
456
+ output_file=options.output_file,
457
+ require_uniqueness=True,
458
+ verbose=True)
459
+
460
+ # Validate
461
+ with open(options.output_file,'r') as f:
462
+ combined_results = json.load(f)
463
+ assert len(combined_results['images']) == len(image_files_absolute), \
464
+ 'Expected {} images in merged output file, found {}'.format(
465
+ len(image_files_absolute),len(combined_results['images']))
466
+
467
+ # Clean up
468
+ _clean_up_temporary_folders(options,
469
+ symlink_folder,yolo_results_folder,
470
+ symlink_folder_is_temp_folder,yolo_folder_is_temp_folder)
471
+
472
+ return
473
+
474
+ # ...if we need to make recursive calls for file chunks
475
+
476
+
477
+ ##%% Create symlinks (or copy images) to give a unique ID to each image
478
+
479
+ # Maps YOLO image IDs (base filename without extension as it will appear in YOLO .json output)
480
+ # to the *original full path* for each image (not the symlink path).
481
+ image_id_to_file = {}
482
+
483
+ # Maps YOLO image IDs (base filename without extension as it will appear in YOLO .json output)
484
+ # to errors, including errors that happen before we run the model at all (e.g. file access errors).
485
+ image_id_to_error = {}
486
+
487
+ create_links = True
488
+
489
+ if options.unique_id_strategy == 'links':
490
+
491
+ create_links = True
492
+
493
+ else:
494
+
495
+ assert options.unique_id_strategy in ('auto','verify'), \
496
+ 'Unknown unique ID strategy {}'.format(options.unique_id_strategy)
497
+
498
+ image_ids_are_unique = True
499
+
500
+ for i_image,image_fn in tqdm(enumerate(image_files_absolute),total=len(image_files_absolute)):
501
+
502
+ image_id = os.path.splitext(os.path.basename(image_fn))[0]
503
+
504
+ # Is this image ID unique?
505
+ if image_id in image_id_to_file:
506
+ if options.unique_id_strategy == 'verify':
507
+ raise ValueError('"verify" specified for image uniqueness, but ' +
508
+ 'image ID {} occurs more than once:\n\n{}\n\n{}'.format(
509
+ image_id,image_fn,image_id_to_file[image_id]))
510
+ else:
511
+ assert options.unique_id_strategy == 'auto'
512
+ image_ids_are_unique = False
513
+ image_id_to_file = {}
514
+ break
515
+
516
+ image_id_to_file[image_id] = image_fn
517
+
518
+ # ...for each image
519
+
520
+ if image_ids_are_unique:
521
+
522
+ print('"{}" specified for image uniqueness and images are unique, skipping links'.format(
523
+ options.unique_id_strategy))
524
+ assert len(image_id_to_file) == len(image_files_absolute)
525
+ create_links = False
526
+
527
+ else:
528
+
529
+ assert options.unique_id_strategy == 'auto'
530
+ create_links = True
531
+ link_type = 'copies'
532
+ if options.use_symlinks:
533
+ link_type = 'links'
534
+ print('"auto" specified for image uniqueness and images are not unique, defaulting to {}'.format(
535
+ link_type))
536
+
537
+ # ...which unique ID strategy?
538
+
539
+ if create_links:
540
+
541
+ if options.use_symlinks:
542
+ print('Creating {} symlinks in {}'.format(len(image_files_absolute),symlink_folder_inner))
543
+ else:
544
+ print('Symlinks disabled, copying {} images to {}'.format(len(image_files_absolute),symlink_folder_inner))
545
+
546
+ link_full_paths = []
547
+
548
+ # i_image = 0; image_fn = image_files_absolute[i_image]
549
+ for i_image,image_fn in tqdm(enumerate(image_files_absolute),total=len(image_files_absolute)):
550
+
551
+ ext = os.path.splitext(image_fn)[1]
552
+
553
+ # YOLO .json output identifies images by the base filename without the extension
554
+ image_id = str(i_image).zfill(10)
555
+ image_id_to_file[image_id] = image_fn
556
+ symlink_name = image_id + ext
557
+ symlink_full_path = os.path.join(symlink_folder_inner,symlink_name)
558
+ link_full_paths.append(symlink_full_path)
559
+
560
+ try:
561
+
562
+ if options.use_symlinks:
563
+ path_utils.safe_create_link(image_fn,symlink_full_path)
564
+ else:
565
+ shutil.copyfile(image_fn,symlink_full_path)
566
+
567
+ except Exception as e:
568
+
569
+ error_string = str(e)
570
+ image_id_to_error[image_id] = error_string
571
+
572
+ # Always break if the user is trying to create symlinks on Windows without
573
+ # permission, 100% of images will always fail in this case.
574
+ if ('a required privilege is not held by the client' in error_string.lower()) or \
575
+ (not options.treat_copy_failures_as_warnings):
576
+
577
+ print('\nError copying/creating link for input file {}: {}'.format(
578
+ image_fn,error_string))
579
+
580
+ raise
581
+
582
+ else:
583
+
584
+ print('Warning: error copying/creating link for input file {}: {}'.format(
585
+ image_fn,error_string))
586
+ continue
587
+
588
+ # ...except
589
+
590
+ # ...for each image
591
+
592
+ # ...if we need to create links/copies
593
+
594
+
595
+ ##%% Create the dataset file if necessary
596
+
597
+ # This may have been passed in as a string, but at this point, we should have
598
+ # loaded the dataset file.
599
+ assert isinstance(options.yolo_category_id_to_name,dict)
600
+
601
+ # Category IDs need to be continuous integers starting at 0
602
+ category_ids = sorted(list(options.yolo_category_id_to_name.keys()))
603
+ assert category_ids[0] == 0
604
+ assert len(category_ids) == 1 + category_ids[-1]
605
+
606
+ yolo_dataset_file = os.path.join(yolo_results_folder,'dataset.yaml')
607
+ yolo_image_list_file = os.path.join(yolo_results_folder,'images.txt')
608
+
609
+
610
+ with open(yolo_image_list_file,'w') as f:
611
+
612
+ if create_links:
613
+ image_files_to_write = link_full_paths
614
+ else:
615
+ image_files_to_write = image_files_absolute
616
+
617
+ for fn_abs in image_files_to_write:
618
+ # At least in YOLOv5 val (need to verify for YOLOv8 val), filenames in this
619
+ # text file are treated as relative to the text file itself if they start with
620
+ # "./", otherwise they're treated as absolute paths. Since we don't want to put this
621
+ # text file in the image folder, we'll use absolute paths.
622
+ # fn_relative = os.path.relpath(fn_abs,options.input_folder)
623
+ # f.write(fn_relative + '\n')
624
+ f.write(fn_abs + '\n')
625
+
626
+ if create_links:
627
+ inference_folder = symlink_folder_inner
628
+ else:
629
+ # This doesn't matter, but it has to be a valid path
630
+ inference_folder = options.yolo_results_folder
631
+
632
+ with open(yolo_dataset_file,'w') as f:
633
+
634
+ f.write('path: {}\n'.format(inference_folder))
635
+ # These need to be valid paths, even if you're not using them, and "." is always safe
636
+ f.write('train: .\n')
637
+ f.write('val: .\n')
638
+ f.write('test: {}\n'.format(yolo_image_list_file))
639
+ f.write('\n')
640
+ f.write('nc: {}\n'.format(len(options.yolo_category_id_to_name)))
641
+ f.write('\n')
642
+ f.write('names:\n')
643
+ for category_id in category_ids:
644
+ assert isinstance(category_id,int)
645
+ f.write(' {}: {}\n'.format(category_id,
646
+ options.yolo_category_id_to_name[category_id]))
647
+
648
+
649
+ ##%% Prepare Python command or YOLO CLI command
650
+
651
+ image_size_string = str(round(options.image_size))
652
+
653
+ if options.model_type == 'yolov5':
654
+
655
+ cmd = 'python val.py --task test --data "{}"'.format(yolo_dataset_file)
656
+ cmd += ' --weights "{}"'.format(model_filename)
657
+ cmd += ' --batch-size {} --imgsz {} --conf-thres {}'.format(
658
+ options.batch_size,image_size_string,options.conf_thres)
659
+ cmd += ' --device "{}" --save-json'.format(options.device_string)
660
+ cmd += ' --project "{}" --name "{}" --exist-ok'.format(yolo_results_folder,'yolo_results')
661
+
662
+ if options.augment:
663
+ cmd += ' --augment'
664
+
665
+ # --half is a store_true argument for YOLOv5's val.py
666
+ if (options.half_precision_enabled is not None) and (options.half_precision_enabled == 1):
667
+ cmd += ' --half'
668
+
669
+ # Sometimes useful for debugging
670
+ # cmd += ' --save_conf --save_txt'
671
+
672
+ elif options.model_type == 'ultralytics':
673
+
674
+ if options.augment:
675
+ augment_string = 'augment'
676
+ else:
677
+ augment_string = ''
678
+
679
+ cmd = 'yolo val {} model="{}" imgsz={} batch={} data="{}" project="{}" name="{}" device="{}"'.\
680
+ format(augment_string,model_filename,image_size_string,options.batch_size,
681
+ yolo_dataset_file,yolo_results_folder,'yolo_results',options.device_string)
682
+ cmd += ' save_json exist_ok'
683
+
684
+ if (options.half_precision_enabled is not None):
685
+ if options.half_precision_enabled == 1:
686
+ cmd += ' --half=True'
687
+ else:
688
+ assert options.half_precision_enabled == 0
689
+ cmd += ' --half=False'
690
+
691
+ # Sometimes useful for debugging
692
+ # cmd += ' save_conf save_txt'
693
+
694
+ else:
695
+
696
+ raise ValueError('Unrecognized model type {}'.format(options.model_type))
697
+
698
+ # print(cmd); import clipboard; clipboard.copy(cmd)
699
+
700
+
701
+ ##%% Run YOLO command
702
+
703
+ if options.yolo_working_folder is not None:
704
+ current_dir = os.getcwd()
705
+ os.chdir(options.yolo_working_folder)
706
+
707
+ print('Running YOLO inference command:\n{}\n'.format(cmd))
708
+
709
+ if options.preview_yolo_command_only:
710
+
711
+ if options.remove_symlink_folder:
712
+ try:
713
+ print('Removing YOLO symlink folder {}'.format(symlink_folder))
714
+ shutil.rmtree(symlink_folder)
715
+ except Exception:
716
+ print('Warning: error removing symlink folder {}'.format(symlink_folder))
717
+ pass
718
+ if options.remove_yolo_results_folder:
719
+ try:
720
+ print('Removing YOLO results folder {}'.format(yolo_results_folder))
721
+ shutil.rmtree(yolo_results_folder)
722
+ except Exception:
723
+ print('Warning: error removing YOLO results folder {}'.format(yolo_results_folder))
724
+ pass
725
+
726
+ # sys.exit()
727
+ return
728
+
729
+ execution_result = process_utils.execute_and_print(cmd,encoding='utf-8',verbose=True)
730
+ assert execution_result['status'] == 0, 'Error running {}'.format(options.model_type)
731
+ yolo_console_output = execution_result['output']
732
+
733
+ if options.save_yolo_debug_output:
734
+
735
+ with open(os.path.join(yolo_results_folder,'yolo_console_output.txt'),'w') as f:
736
+ for s in yolo_console_output:
737
+ f.write(s + '\n')
738
+ with open(os.path.join(yolo_results_folder,'image_id_to_file.json'),'w') as f:
739
+ json.dump(image_id_to_file,f,indent=1)
740
+ with open(os.path.join(yolo_results_folder,'image_id_to_error.json'),'w') as f:
741
+ json.dump(image_id_to_error,f,indent=1)
742
+
743
+
744
+ # YOLO console output contains lots of ANSI escape codes, remove them for easier parsing
745
+ yolo_console_output = [string_utils.remove_ansi_codes(s) for s in yolo_console_output]
746
+
747
+ # Find errors that occurred during the initial corruption check; these will not be included in the
748
+ # output. Errors that occur during inference will be handled separately.
749
+ yolo_read_failures = []
750
+
751
+ for line in yolo_console_output:
752
+ # Lines look like:
753
+ #
754
+ # For ultralytics val:
755
+ #
756
+ # val: WARNING ⚠️ /a/b/c/d.jpg: ignoring corrupt image/label: [Errno 13] Permission denied: '/a/b/c/d.jpg'
757
+ # line = "val: WARNING ⚠️ /a/b/c/d.jpg: ignoring corrupt image/label: [Errno 13] Permission denied: '/a/b/c/d.jpg'"
758
+ #
759
+ # For yolov5 val.py:
760
+ #
761
+ # test: WARNING: a/b/c/d.jpg: ignoring corrupt image/label: cannot identify image file '/a/b/c/d.jpg'
762
+ # line = "test: WARNING: a/b/c/d.jpg: ignoring corrupt image/label: cannot identify image file '/a/b/c/d.jpg'"
763
+ if 'cannot identify image file' in line:
764
+ tokens = line.split('cannot identify image file')
765
+ image_name = tokens[-1].strip()
766
+ assert image_name[0] == "'" and image_name [-1] == "'"
767
+ image_name = image_name[1:-1]
768
+ yolo_read_failures.append(image_name)
769
+ elif 'ignoring corrupt image/label' in line:
770
+ assert 'WARNING' in line
771
+ if '⚠️' in line:
772
+ assert line.startswith('val'), \
773
+ 'Unrecognized line in YOLO output: {}'.format(line)
774
+ tokens = line.split('ignoring corrupt image/label')
775
+ image_name = tokens[0].split('⚠️')[-1].strip()
776
+ else:
777
+ assert line.startswith('test'), \
778
+ 'Unrecognized line in YOLO output: {}'.format(line)
779
+ tokens = line.split('ignoring corrupt image/label')
780
+ image_name = tokens[0].split('WARNING:')[-1].strip()
781
+ assert image_name.endswith(':')
782
+ image_name = image_name[0:-1]
783
+ yolo_read_failures.append(image_name)
784
+
785
+ # image_file = yolo_read_failures[0]
786
+ for image_file in yolo_read_failures:
787
+ image_id = os.path.splitext(os.path.basename(image_file))[0]
788
+ assert image_id in image_id_to_file, 'Unexpected image ID {}'.format(image_id)
789
+ if image_id not in image_id_to_error:
790
+ image_id_to_error[image_id] = 'YOLO read failure'
791
+
792
+ if options.yolo_working_folder is not None:
793
+ os.chdir(current_dir)
794
+
795
+
796
+ ##%% Convert results to MD format
797
+
798
+ json_files = glob.glob(yolo_results_folder + '/yolo_results/*.json')
799
+ assert len(json_files) == 1
800
+ yolo_json_file = json_files[0]
801
+
802
+ # Map YOLO image IDs to paths
803
+ image_id_to_relative_path = {}
804
+ for image_id in image_id_to_file:
805
+ fn = image_id_to_file[image_id].replace('\\','/')
806
+ assert path_is_abs(fn)
807
+ if options.input_folder is not None:
808
+ assert os.path.isdir(options.input_folder)
809
+ assert options.input_folder in fn, 'Internal error: base folder {} not in file {}'.format(
810
+ options.input_folder,fn)
811
+ relative_path = os.path.relpath(fn,options.input_folder)
812
+ else:
813
+ # We'll use the absolute path as a relative path, and pass '/'
814
+ # as the base path in this case.
815
+ relative_path = fn
816
+ image_id_to_relative_path[image_id] = relative_path
817
+
818
+ # Are we working with a base folder?
819
+ if options.input_folder is not None:
820
+ assert os.path.isdir(options.input_folder)
821
+ image_base = options.input_folder
822
+ else:
823
+ image_base = '/'
824
+
825
+ yolo_output_to_md_output.yolo_json_output_to_md_output(
826
+ yolo_json_file=yolo_json_file,
827
+ image_folder=image_base,
828
+ output_file=options.output_file,
829
+ yolo_category_id_to_name=options.yolo_category_id_to_name,
830
+ detector_name=os.path.basename(model_filename),
831
+ image_id_to_relative_path=image_id_to_relative_path,
832
+ image_id_to_error=image_id_to_error)
833
+
834
+
835
+ ##%% Clean up
836
+
837
+ _clean_up_temporary_folders(options,
838
+ symlink_folder,yolo_results_folder,
839
+ symlink_folder_is_temp_folder,yolo_folder_is_temp_folder)
840
+
841
+ # ...def run_inference_with_yolo_val()
842
+
843
+
844
+ #%% Command-line driver
845
+
846
+ import argparse
847
+ from megadetector.utils.ct_utils import args_to_object
848
+
849
+ def main():
850
+
851
+ options = YoloInferenceOptions()
852
+
853
+ parser = argparse.ArgumentParser()
854
+ parser.add_argument(
855
+ 'model_filename',type=str,
856
+ help='model file name')
857
+ parser.add_argument(
858
+ 'input_folder',type=str,
859
+ help='folder on which to recursively run the model')
860
+ parser.add_argument(
861
+ 'output_file',type=str,
862
+ help='.json file where output will be written')
863
+
864
+ parser.add_argument(
865
+ '--image_filename_list',type=str,default=None,
866
+ help='.json or .txt file containing a list of relative image filenames within [input_folder]')
867
+ parser.add_argument(
868
+ '--yolo_working_folder',type=str,default=None,
869
+ help='folder in which to execute val.py (not necessary for YOLOv8 inference)')
870
+ parser.add_argument(
871
+ '--image_size', default=None, type=int,
872
+ help='image size for model execution (default {} when augmentation is enabled, else {})'.format(
873
+ default_image_size_with_augmentation,default_image_size_with_no_augmentation))
874
+ parser.add_argument(
875
+ '--conf_thres', default=options.conf_thres, type=float,
876
+ help='confidence threshold for including detections in the output file (default {})'.format(
877
+ options.conf_thres))
878
+ parser.add_argument(
879
+ '--batch_size', default=options.batch_size, type=int,
880
+ help='inference batch size (default {})'.format(options.batch_size))
881
+ parser.add_argument(
882
+ '--half_precision_enabled', default=None, type=int,
883
+ help='use half-precision-inference (1 or 0) (default is the underlying model\'s default, probably full for YOLOv8 and half for YOLOv5')
884
+ parser.add_argument(
885
+ '--device_string', default=options.device_string, type=str,
886
+ help='CUDA device specifier, typically "0" or "1" for CUDA devices, "mps" for M1/M2 devices, or "cpu" (default {})'.format(
887
+ options.device_string))
888
+ parser.add_argument(
889
+ '--overwrite_handling', default=options.overwrite_handling, type=str,
890
+ help='action to take if the output file exists (skip, error, overwrite) (default {})'.format(
891
+ options.overwrite_handling))
892
+ parser.add_argument(
893
+ '--yolo_dataset_file', default=None, type=str,
894
+ help='YOLOv5 dataset.yaml file from which we should load category information ' + \
895
+ '(otherwise defaults to MD categories)')
896
+ parser.add_argument(
897
+ '--model_type', default=options.model_type, type=str,
898
+ help='model type ("yolov5" or "ultralytics" ("yolov8" behaves the same as "ultralytics")) (default {})'.format(
899
+ options.model_type))
900
+
901
+ parser.add_argument('--unique_id_strategy', default=options.unique_id_strategy, type=str,
902
+ help='how should we ensure that unique filenames are passed to the YOLO val script, ' + \
903
+ 'can be "verify", "auto", or "links", see options class docs for details (default {})'.format(
904
+ options.unique_id_strategy))
905
+ parser.add_argument(
906
+ '--symlink_folder', default=None, type=str,
907
+ help='temporary folder for symlinks (defaults to a folder in the system temp dir)')
908
+ parser.add_argument(
909
+ '--yolo_results_folder', default=None, type=str,
910
+ help='temporary folder for YOLO intermediate output (defaults to a folder in the system temp dir)')
911
+ parser.add_argument(
912
+ '--no_use_symlinks', action='store_true',
913
+ help='copy files instead of creating symlinks when preparing the yolo input folder')
914
+ parser.add_argument(
915
+ '--no_remove_symlink_folder', action='store_true',
916
+ help='don\'t remove the temporary folder full of symlinks')
917
+ parser.add_argument(
918
+ '--no_remove_yolo_results_folder', action='store_true',
919
+ help='don\'t remove the temporary folder full of YOLO intermediate files')
920
+ parser.add_argument(
921
+ '--save_yolo_debug_output', action='store_true',
922
+ help='write yolo console output to a text file in the results folder, along with additional debug files')
923
+ parser.add_argument(
924
+ '--checkpoint_frequency', default=options.checkpoint_frequency, type=int,
925
+ help='break the job into chunks with no more than this many images (default {})'.format(
926
+ options.checkpoint_frequency))
927
+
928
+ parser.add_argument(
929
+ '--nonrecursive', action='store_true',
930
+ help='Disable recursive folder processing')
931
+
932
+ parser.add_argument(
933
+ '--preview_yolo_command_only', action='store_true',
934
+ help='don\'t run inference, just preview the YOLO inference command (still creates symlinks)')
935
+
936
+ if options.augment:
937
+ default_augment_enabled = 1
938
+ else:
939
+ default_augment_enabled = 0
940
+
941
+ parser.add_argument(
942
+ '--augment_enabled', default=default_augment_enabled, type=int,
943
+ help='enable/disable augmentation (default {})'.format(default_augment_enabled))
944
+
945
+ if len(sys.argv[1:]) == 0:
946
+ parser.print_help()
947
+ parser.exit()
948
+
949
+ args = parser.parse_args()
950
+
951
+ # If the caller hasn't specified an image size, choose one based on whether augmentation
952
+ # is enabled.
953
+ if args.image_size is None:
954
+ assert args.augment_enabled in (0,1), \
955
+ 'Illegal augment_enabled value {}'.format(args.augment_enabled)
956
+ if args.augment_enabled == 1:
957
+ args.image_size = default_image_size_with_augmentation
958
+ else:
959
+ args.image_size = default_image_size_with_no_augmentation
960
+ augment_enabled_string = 'enabled'
961
+ if not args.augment_enabled:
962
+ augment_enabled_string = 'disabled'
963
+ print('Augmentation is {}, using default image size {}'.format(
964
+ augment_enabled_string,args.image_size))
965
+
966
+ args_to_object(args, options)
967
+
968
+ if args.yolo_dataset_file is not None:
969
+ options.yolo_category_id_to_name = args.yolo_dataset_file
970
+ del options.yolo_dataset_file
971
+
972
+ options.recursive = (not options.nonrecursive)
973
+ options.remove_symlink_folder = (not options.no_remove_symlink_folder)
974
+ options.remove_yolo_results_folder = (not options.no_remove_yolo_results_folder)
975
+ options.use_symlinks = (not options.no_use_symlinks)
976
+ options.augment = (options.augment_enabled > 0)
977
+
978
+ del options.nonrecursive
979
+ del options.no_remove_symlink_folder
980
+ del options.no_remove_yolo_results_folder
981
+ del options.no_use_symlinks
982
+ del options.augment_enabled
983
+
984
+ print(options.__dict__)
985
+
986
+ run_inference_with_yolo_val(options)
987
+
988
+ if __name__ == '__main__':
989
+ main()
990
+
991
+
992
+ #%% Interactive driver
993
+
994
+ if False:
995
+
996
+ #%% Run inference on a folder
997
+
998
+ input_folder = r'g:\temp\tegu-val-mini'.replace('\\','/')
999
+ model_filename = r'g:\temp\usgs-tegus-yolov5x-231003-b8-img1280-e3002-best.pt'
1000
+ output_folder = r'g:\temp\tegu-scratch'
1001
+ yolo_working_folder = r'c:\git\yolov5-tegus'
1002
+ dataset_file = r'g:\temp\dataset.yaml'
1003
+
1004
+ # This only impacts the output file name, it's not passed to the inference functio
1005
+ job_name = 'yolo-inference-test'
1006
+
1007
+ model_name = os.path.splitext(os.path.basename(model_filename))[0]
1008
+
1009
+ symlink_folder = os.path.join(output_folder,'symlinks')
1010
+ yolo_results_folder = os.path.join(output_folder,'yolo_results')
1011
+
1012
+ output_file = os.path.join(output_folder,'{}_{}-md_format.json'.format(
1013
+ job_name,model_name))
1014
+
1015
+ options = YoloInferenceOptions()
1016
+
1017
+ options.yolo_working_folder = yolo_working_folder
1018
+ options.input_folder = input_folder
1019
+ options.output_file = output_file
1020
+
1021
+ pass_image_filename_list = False
1022
+ pass_relative_paths = True
1023
+
1024
+ if pass_image_filename_list:
1025
+ if pass_relative_paths:
1026
+ options.image_filename_list = [
1027
+ 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",
1028
+ 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"
1029
+ ]
1030
+ else:
1031
+ options.image_filename_list = [
1032
+ 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",
1033
+ 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"
1034
+ ]
1035
+ else:
1036
+ options.image_filename_list = None
1037
+
1038
+ options.yolo_category_id_to_name = dataset_file
1039
+ options.augment = False
1040
+ options.conf_thres = '0.001'
1041
+ options.batch_size = 1
1042
+ options.device_string = '0'
1043
+ options.unique_id_strategy = 'auto'
1044
+ options.overwrite_handling = 'overwrite'
1045
+
1046
+ if options.augment:
1047
+ options.image_size = round(1280 * 1.3)
1048
+ else:
1049
+ options.image_size = 1280
1050
+
1051
+ options.model_filename = model_filename
1052
+
1053
+ options.yolo_results_folder = yolo_results_folder # os.path.join(output_folder + 'yolo_results')
1054
+ options.symlink_folder = symlink_folder # os.path.join(output_folder,'symlinks')
1055
+ options.use_symlinks = False
1056
+
1057
+ options.remove_symlink_folder = True
1058
+ options.remove_yolo_results_folder = True
1059
+
1060
+ options.checkpoint_frequency = 5
1061
+
1062
+ cmd = f'python run_inference_with_yolov5_val.py {model_filename} {input_folder} ' + \
1063
+ f'{output_file} --yolo_working_folder {yolo_working_folder} ' + \
1064
+ f' --image_size {options.image_size} --conf_thres {options.conf_thres} ' + \
1065
+ f' --batch_size {options.batch_size} ' + \
1066
+ f' --symlink_folder {options.symlink_folder} --yolo_results_folder {options.yolo_results_folder} ' + \
1067
+ f' --yolo_dataset_file {options.yolo_category_id_to_name} ' + \
1068
+ f' --unique_id_strategy {options.unique_id_strategy} --overwrite_handling {options.overwrite_handling}'
1069
+
1070
+ if not options.remove_symlink_folder:
1071
+ cmd += ' --no_remove_symlink_folder'
1072
+ if not options.remove_yolo_results_folder:
1073
+ cmd += ' --no_remove_yolo_results_folder'
1074
+ if options.checkpoint_frequency is not None:
1075
+ cmd += f' --checkpoint_frequency {options.checkpoint_frequency}'
1076
+ if not options.use_symlinks:
1077
+ cmd += ' --no_use_symlinks'
1078
+ if not options.augment:
1079
+ cmd += ' --augment_enabled 0'
1080
+
1081
+ print(cmd)
1082
+ execute_in_python = False
1083
+ if execute_in_python:
1084
+ run_inference_with_yolo_val(options)
1085
+ else:
1086
+ import clipboard; clipboard.copy(cmd)
1087
+