megadetector 5.0.28__py3-none-any.whl → 10.0.0__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 (197) hide show
  1. megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +2 -2
  2. megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +1 -1
  3. megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +1 -1
  4. megadetector/classification/aggregate_classifier_probs.py +3 -3
  5. megadetector/classification/analyze_failed_images.py +5 -5
  6. megadetector/classification/cache_batchapi_outputs.py +5 -5
  7. megadetector/classification/create_classification_dataset.py +11 -12
  8. megadetector/classification/crop_detections.py +10 -10
  9. megadetector/classification/csv_to_json.py +8 -8
  10. megadetector/classification/detect_and_crop.py +13 -15
  11. megadetector/classification/efficientnet/model.py +8 -8
  12. megadetector/classification/efficientnet/utils.py +6 -5
  13. megadetector/classification/evaluate_model.py +7 -7
  14. megadetector/classification/identify_mislabeled_candidates.py +6 -6
  15. megadetector/classification/json_to_azcopy_list.py +1 -1
  16. megadetector/classification/json_validator.py +29 -32
  17. megadetector/classification/map_classification_categories.py +9 -9
  18. megadetector/classification/merge_classification_detection_output.py +12 -9
  19. megadetector/classification/prepare_classification_script.py +19 -19
  20. megadetector/classification/prepare_classification_script_mc.py +26 -26
  21. megadetector/classification/run_classifier.py +4 -4
  22. megadetector/classification/save_mislabeled.py +6 -6
  23. megadetector/classification/train_classifier.py +1 -1
  24. megadetector/classification/train_classifier_tf.py +9 -9
  25. megadetector/classification/train_utils.py +10 -10
  26. megadetector/data_management/annotations/annotation_constants.py +1 -2
  27. megadetector/data_management/camtrap_dp_to_coco.py +79 -46
  28. megadetector/data_management/cct_json_utils.py +103 -103
  29. megadetector/data_management/cct_to_md.py +49 -49
  30. megadetector/data_management/cct_to_wi.py +33 -33
  31. megadetector/data_management/coco_to_labelme.py +75 -75
  32. megadetector/data_management/coco_to_yolo.py +210 -193
  33. megadetector/data_management/databases/add_width_and_height_to_db.py +86 -12
  34. megadetector/data_management/databases/combine_coco_camera_traps_files.py +40 -40
  35. megadetector/data_management/databases/integrity_check_json_db.py +228 -200
  36. megadetector/data_management/databases/subset_json_db.py +33 -33
  37. megadetector/data_management/generate_crops_from_cct.py +88 -39
  38. megadetector/data_management/get_image_sizes.py +54 -49
  39. megadetector/data_management/labelme_to_coco.py +133 -125
  40. megadetector/data_management/labelme_to_yolo.py +159 -73
  41. megadetector/data_management/lila/create_lila_blank_set.py +81 -83
  42. megadetector/data_management/lila/create_lila_test_set.py +32 -31
  43. megadetector/data_management/lila/create_links_to_md_results_files.py +18 -18
  44. megadetector/data_management/lila/download_lila_subset.py +21 -24
  45. megadetector/data_management/lila/generate_lila_per_image_labels.py +365 -107
  46. megadetector/data_management/lila/get_lila_annotation_counts.py +35 -33
  47. megadetector/data_management/lila/get_lila_image_counts.py +22 -22
  48. megadetector/data_management/lila/lila_common.py +73 -70
  49. megadetector/data_management/lila/test_lila_metadata_urls.py +28 -19
  50. megadetector/data_management/mewc_to_md.py +344 -340
  51. megadetector/data_management/ocr_tools.py +262 -255
  52. megadetector/data_management/read_exif.py +249 -227
  53. megadetector/data_management/remap_coco_categories.py +90 -28
  54. megadetector/data_management/remove_exif.py +81 -21
  55. megadetector/data_management/rename_images.py +187 -187
  56. megadetector/data_management/resize_coco_dataset.py +588 -120
  57. megadetector/data_management/speciesnet_to_md.py +41 -41
  58. megadetector/data_management/wi_download_csv_to_coco.py +55 -55
  59. megadetector/data_management/yolo_output_to_md_output.py +248 -122
  60. megadetector/data_management/yolo_to_coco.py +333 -191
  61. megadetector/detection/change_detection.py +832 -0
  62. megadetector/detection/process_video.py +340 -337
  63. megadetector/detection/pytorch_detector.py +358 -278
  64. megadetector/detection/run_detector.py +399 -186
  65. megadetector/detection/run_detector_batch.py +404 -377
  66. megadetector/detection/run_inference_with_yolov5_val.py +340 -327
  67. megadetector/detection/run_tiled_inference.py +257 -249
  68. megadetector/detection/tf_detector.py +24 -24
  69. megadetector/detection/video_utils.py +332 -295
  70. megadetector/postprocessing/add_max_conf.py +19 -11
  71. megadetector/postprocessing/categorize_detections_by_size.py +45 -45
  72. megadetector/postprocessing/classification_postprocessing.py +468 -433
  73. megadetector/postprocessing/combine_batch_outputs.py +23 -23
  74. megadetector/postprocessing/compare_batch_results.py +590 -525
  75. megadetector/postprocessing/convert_output_format.py +106 -102
  76. megadetector/postprocessing/create_crop_folder.py +347 -147
  77. megadetector/postprocessing/detector_calibration.py +173 -168
  78. megadetector/postprocessing/generate_csv_report.py +508 -499
  79. megadetector/postprocessing/load_api_results.py +48 -27
  80. megadetector/postprocessing/md_to_coco.py +133 -102
  81. megadetector/postprocessing/md_to_labelme.py +107 -90
  82. megadetector/postprocessing/md_to_wi.py +40 -40
  83. megadetector/postprocessing/merge_detections.py +92 -114
  84. megadetector/postprocessing/postprocess_batch_results.py +319 -301
  85. megadetector/postprocessing/remap_detection_categories.py +91 -38
  86. megadetector/postprocessing/render_detection_confusion_matrix.py +214 -205
  87. megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +57 -57
  88. megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +27 -28
  89. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +704 -679
  90. megadetector/postprocessing/separate_detections_into_folders.py +226 -211
  91. megadetector/postprocessing/subset_json_detector_output.py +265 -262
  92. megadetector/postprocessing/top_folders_to_bottom.py +45 -45
  93. megadetector/postprocessing/validate_batch_results.py +70 -70
  94. megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +52 -52
  95. megadetector/taxonomy_mapping/map_new_lila_datasets.py +18 -19
  96. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +54 -33
  97. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +67 -67
  98. megadetector/taxonomy_mapping/retrieve_sample_image.py +16 -16
  99. megadetector/taxonomy_mapping/simple_image_download.py +8 -8
  100. megadetector/taxonomy_mapping/species_lookup.py +156 -74
  101. megadetector/taxonomy_mapping/taxonomy_csv_checker.py +14 -14
  102. megadetector/taxonomy_mapping/taxonomy_graph.py +10 -10
  103. megadetector/taxonomy_mapping/validate_lila_category_mappings.py +13 -13
  104. megadetector/utils/ct_utils.py +1049 -211
  105. megadetector/utils/directory_listing.py +21 -77
  106. megadetector/utils/gpu_test.py +22 -22
  107. megadetector/utils/md_tests.py +632 -529
  108. megadetector/utils/path_utils.py +1520 -431
  109. megadetector/utils/process_utils.py +41 -41
  110. megadetector/utils/split_locations_into_train_val.py +62 -62
  111. megadetector/utils/string_utils.py +148 -27
  112. megadetector/utils/url_utils.py +489 -176
  113. megadetector/utils/wi_utils.py +2658 -2526
  114. megadetector/utils/write_html_image_list.py +137 -137
  115. megadetector/visualization/plot_utils.py +34 -30
  116. megadetector/visualization/render_images_with_thumbnails.py +39 -74
  117. megadetector/visualization/visualization_utils.py +487 -435
  118. megadetector/visualization/visualize_db.py +232 -198
  119. megadetector/visualization/visualize_detector_output.py +82 -76
  120. {megadetector-5.0.28.dist-info → megadetector-10.0.0.dist-info}/METADATA +5 -2
  121. megadetector-10.0.0.dist-info/RECORD +139 -0
  122. {megadetector-5.0.28.dist-info → megadetector-10.0.0.dist-info}/WHEEL +1 -1
  123. megadetector/api/batch_processing/api_core/__init__.py +0 -0
  124. megadetector/api/batch_processing/api_core/batch_service/__init__.py +0 -0
  125. megadetector/api/batch_processing/api_core/batch_service/score.py +0 -439
  126. megadetector/api/batch_processing/api_core/server.py +0 -294
  127. megadetector/api/batch_processing/api_core/server_api_config.py +0 -97
  128. megadetector/api/batch_processing/api_core/server_app_config.py +0 -55
  129. megadetector/api/batch_processing/api_core/server_batch_job_manager.py +0 -220
  130. megadetector/api/batch_processing/api_core/server_job_status_table.py +0 -149
  131. megadetector/api/batch_processing/api_core/server_orchestration.py +0 -360
  132. megadetector/api/batch_processing/api_core/server_utils.py +0 -88
  133. megadetector/api/batch_processing/api_core_support/__init__.py +0 -0
  134. megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +0 -46
  135. megadetector/api/batch_processing/api_support/__init__.py +0 -0
  136. megadetector/api/batch_processing/api_support/summarize_daily_activity.py +0 -152
  137. megadetector/api/batch_processing/data_preparation/__init__.py +0 -0
  138. megadetector/api/synchronous/__init__.py +0 -0
  139. megadetector/api/synchronous/api_core/animal_detection_api/__init__.py +0 -0
  140. megadetector/api/synchronous/api_core/animal_detection_api/api_backend.py +0 -151
  141. megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +0 -263
  142. megadetector/api/synchronous/api_core/animal_detection_api/config.py +0 -35
  143. megadetector/api/synchronous/api_core/tests/__init__.py +0 -0
  144. megadetector/api/synchronous/api_core/tests/load_test.py +0 -110
  145. megadetector/data_management/importers/add_nacti_sizes.py +0 -52
  146. megadetector/data_management/importers/add_timestamps_to_icct.py +0 -79
  147. megadetector/data_management/importers/animl_results_to_md_results.py +0 -158
  148. megadetector/data_management/importers/auckland_doc_test_to_json.py +0 -373
  149. megadetector/data_management/importers/auckland_doc_to_json.py +0 -201
  150. megadetector/data_management/importers/awc_to_json.py +0 -191
  151. megadetector/data_management/importers/bellevue_to_json.py +0 -272
  152. megadetector/data_management/importers/cacophony-thermal-importer.py +0 -793
  153. megadetector/data_management/importers/carrizo_shrubfree_2018.py +0 -269
  154. megadetector/data_management/importers/carrizo_trail_cam_2017.py +0 -289
  155. megadetector/data_management/importers/cct_field_adjustments.py +0 -58
  156. megadetector/data_management/importers/channel_islands_to_cct.py +0 -913
  157. megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +0 -180
  158. megadetector/data_management/importers/eMammal/eMammal_helpers.py +0 -249
  159. megadetector/data_management/importers/eMammal/make_eMammal_json.py +0 -223
  160. megadetector/data_management/importers/ena24_to_json.py +0 -276
  161. megadetector/data_management/importers/filenames_to_json.py +0 -386
  162. megadetector/data_management/importers/helena_to_cct.py +0 -283
  163. megadetector/data_management/importers/idaho-camera-traps.py +0 -1407
  164. megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +0 -294
  165. megadetector/data_management/importers/import_desert_lion_conservation_camera_traps.py +0 -387
  166. megadetector/data_management/importers/jb_csv_to_json.py +0 -150
  167. megadetector/data_management/importers/mcgill_to_json.py +0 -250
  168. megadetector/data_management/importers/missouri_to_json.py +0 -490
  169. megadetector/data_management/importers/nacti_fieldname_adjustments.py +0 -79
  170. megadetector/data_management/importers/noaa_seals_2019.py +0 -181
  171. megadetector/data_management/importers/osu-small-animals-to-json.py +0 -364
  172. megadetector/data_management/importers/pc_to_json.py +0 -365
  173. megadetector/data_management/importers/plot_wni_giraffes.py +0 -123
  174. megadetector/data_management/importers/prepare_zsl_imerit.py +0 -131
  175. megadetector/data_management/importers/raic_csv_to_md_results.py +0 -416
  176. megadetector/data_management/importers/rspb_to_json.py +0 -356
  177. megadetector/data_management/importers/save_the_elephants_survey_A.py +0 -320
  178. megadetector/data_management/importers/save_the_elephants_survey_B.py +0 -329
  179. megadetector/data_management/importers/snapshot_safari_importer.py +0 -758
  180. megadetector/data_management/importers/snapshot_serengeti_lila.py +0 -1067
  181. megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +0 -150
  182. megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +0 -153
  183. megadetector/data_management/importers/sulross_get_exif.py +0 -65
  184. megadetector/data_management/importers/timelapse_csv_set_to_json.py +0 -490
  185. megadetector/data_management/importers/ubc_to_json.py +0 -399
  186. megadetector/data_management/importers/umn_to_json.py +0 -507
  187. megadetector/data_management/importers/wellington_to_json.py +0 -263
  188. megadetector/data_management/importers/wi_to_json.py +0 -442
  189. megadetector/data_management/importers/zamba_results_to_md_results.py +0 -180
  190. megadetector/data_management/lila/add_locations_to_island_camera_traps.py +0 -101
  191. megadetector/data_management/lila/add_locations_to_nacti.py +0 -151
  192. megadetector/utils/azure_utils.py +0 -178
  193. megadetector/utils/sas_blob_utils.py +0 -509
  194. megadetector-5.0.28.dist-info/RECORD +0 -209
  195. /megadetector/{api/batch_processing/__init__.py → __init__.py} +0 -0
  196. {megadetector-5.0.28.dist-info → megadetector-10.0.0.dist-info}/licenses/LICENSE +0 -0
  197. {megadetector-5.0.28.dist-info → megadetector-10.0.0.dist-info}/top_level.txt +0 -0
@@ -14,7 +14,7 @@ no detections are present.
14
14
 
15
15
  YOLOv5 output has one text file per image, like so:
16
16
 
17
- 0 0.0141693 0.469758 0.0283385 0.131552 0.761428
17
+ 0 0.0141693 0.469758 0.0283385 0.131552 0.761428
18
18
 
19
19
  That's [class, x_center, y_center, width_of_box, height_of_box, confidence]
20
20
 
@@ -40,6 +40,8 @@ import json
40
40
  import csv
41
41
  import os
42
42
  import re
43
+ import sys
44
+ import argparse
43
45
 
44
46
  from collections import defaultdict
45
47
  from tqdm import tqdm
@@ -57,39 +59,39 @@ def read_classes_from_yolo_dataset_file(fn):
57
59
  Reads a dictionary mapping integer class IDs to class names from a YOLOv5/YOLOv8
58
60
  dataset.yaml file or a .json file. A .json file should contain a dictionary mapping
59
61
  integer category IDs to string category names.
60
-
62
+
61
63
  Args:
62
- fn (str): YOLOv5/YOLOv8 dataset file with a .yml or .yaml extension, a .json file
64
+ fn (str): YOLOv5/YOLOv8 dataset file with a .yml or .yaml extension, a .json file
63
65
  mapping integer category IDs to category names, or a .txt file with a flat
64
66
  list of classes.
65
-
67
+
66
68
  Returns:
67
69
  dict: a mapping from integer category IDs to category names
68
70
  """
69
-
71
+
70
72
  category_id_to_name = {}
71
-
73
+
72
74
  if fn.endswith('.yml') or fn.endswith('.yaml'):
73
-
75
+
74
76
  with open(fn,'r') as f:
75
77
  lines = f.readlines()
76
-
78
+
77
79
  pat = r'\d+:.+'
78
80
  for s in lines:
79
81
  if re.search(pat,s) is not None:
80
82
  tokens = s.split(':')
81
83
  assert len(tokens) == 2, 'Invalid token in category file {}'.format(fn)
82
84
  category_id_to_name[int(tokens[0].strip())] = tokens[1].strip()
83
-
85
+
84
86
  elif fn.endswith('.json'):
85
-
87
+
86
88
  with open(fn,'r') as f:
87
89
  d_in = json.load(f)
88
90
  for k in d_in.keys():
89
91
  category_id_to_name[int(k)] = d_in[k]
90
-
92
+
91
93
  elif fn.endswith('.txt'):
92
-
94
+
93
95
  with open(fn,'r') as f:
94
96
  lines = f.readlines()
95
97
  next_category_id = 0
@@ -99,20 +101,20 @@ def read_classes_from_yolo_dataset_file(fn):
99
101
  continue
100
102
  category_id_to_name[next_category_id] = s
101
103
  next_category_id += 1
102
-
104
+
103
105
  else:
104
-
106
+
105
107
  raise ValueError('Unrecognized category file type: {}'.format(fn))
106
-
108
+
107
109
  assert len(category_id_to_name) > 0, 'Failed to read class mappings from {}'.format(fn)
108
-
110
+
109
111
  return category_id_to_name
110
-
111
112
 
112
- def yolo_json_output_to_md_output(yolo_json_file,
113
+
114
+ def yolo_json_output_to_md_output(yolo_json_file,
113
115
  image_folder,
114
- output_file,
115
- yolo_category_id_to_name,
116
+ output_file,
117
+ yolo_category_id_to_name,
116
118
  detector_name='unknown',
117
119
  image_id_to_relative_path=None,
118
120
  offset_yolo_class_ids=True,
@@ -121,56 +123,56 @@ def yolo_json_output_to_md_output(yolo_json_file,
121
123
  convert_slashes=True):
122
124
  """
123
125
  Converts a YOLOv5/YOLOv8 .json file to MD .json format.
124
-
126
+
125
127
  Args:
126
-
127
- yolo_json_file (str): the .json file to convert from YOLOv5 format to MD output format
128
+ yolo_json_file (str): the YOLO-formatted .json file to convert to MD output format
128
129
  image_folder (str): the .json file contains relative path names, this is the path base
129
- yolo_category_id_to_name (str or dict): the .json results file contains only numeric
130
- identifiers for categories, but we want names and numbers for the output format;
131
- yolo_category_id_to_name provides that mapping either as a dict or as a YOLOv5
130
+ output_file (str): the MD-formatted .json file to write
131
+ yolo_category_id_to_name (str or dict): the .json results file contains only numeric
132
+ identifiers for categories, but we want names and numbers for the output format;
133
+ yolo_category_id_to_name provides that mapping either as a dict or as a YOLOv5
132
134
  dataset.yaml file.
133
- detector_name (str, optional): a string that gets put in the output file, not otherwise
135
+ detector_name (str, optional): a string that gets put in the output file, not otherwise
134
136
  used within this function
135
- image_id_to_relative_path (dict, optional): YOLOv5 .json uses only basenames (e.g.
137
+ image_id_to_relative_path (dict, optional): YOLOv5 .json uses only basenames (e.g.
136
138
  abc1234.JPG); by default these will be appended to the input path to create pathnames.
137
139
  If you have a flat folder, this is fine. If you want to map base names to relative paths in
138
- a more complicated way, use this parameter.
139
- offset_yolo_class_ids (bool, optional): YOLOv5 class IDs always start at zero; if you want to
140
- make the output classes start at 1, set offset_yolo_class_ids to True.
141
- truncate_to_standard_md_precision (bool, optional): YOLOv5 .json includes lots of
140
+ a more complicated way, use this parameter.
141
+ offset_yolo_class_ids (bool, optional): YOLOv5 class IDs always start at zero; if you want to
142
+ make the output classes start at 1, set offset_yolo_class_ids to True.
143
+ truncate_to_standard_md_precision (bool, optional): YOLOv5 .json includes lots of
142
144
  (not-super-meaningful) precision, set this to truncate to COORD_DIGITS and CONF_DIGITS.
143
- image_id_to_error (dict, optional): if you want to include image IDs in the output file for which
145
+ image_id_to_error (dict, optional): if you want to include image IDs in the output file for which
144
146
  you couldn't prepare the input file in the first place due to errors, include them here.
145
147
  convert_slashes (bool, optional): force all slashes to be forward slashes in the output file
146
- """
147
-
148
+ """
149
+
148
150
  assert os.path.isfile(yolo_json_file), \
149
151
  'Could not find YOLO .json file {}'.format(yolo_json_file)
150
152
  assert os.path.isdir(image_folder), \
151
153
  'Could not find image folder {}'.format(image_folder)
152
-
154
+
153
155
  if image_id_to_error is None:
154
156
  image_id_to_error = {}
155
-
157
+
156
158
  print('Converting {} to MD format and writing results to {}'.format(
157
159
  yolo_json_file,output_file))
158
-
160
+
159
161
  if isinstance(yolo_category_id_to_name,str):
160
162
  assert os.path.isfile(yolo_category_id_to_name), \
161
163
  'YOLO category mapping specified as a string, but file does not exist: {}'.format(
162
164
  yolo_category_id_to_name)
163
165
  yolo_category_id_to_name = read_classes_from_yolo_dataset_file(yolo_category_id_to_name)
164
-
166
+
165
167
  if image_id_to_relative_path is None:
166
-
168
+
167
169
  image_files = path_utils.find_images(image_folder,recursive=True)
168
170
  image_files = [os.path.relpath(fn,image_folder) for fn in image_files]
169
-
171
+
170
172
  # YOLOv5 identifies images in .json output by ID, which is the filename without
171
173
  # extension. If a mapping is not provided, these need to be unique.
172
174
  image_id_to_relative_path = {}
173
-
175
+
174
176
  for fn in image_files:
175
177
  image_id = os.path.splitext(os.path.basename(fn))[0]
176
178
  if image_id in image_id_to_relative_path:
@@ -180,49 +182,49 @@ def yolo_json_output_to_md_output(yolo_json_file,
180
182
  image_id_to_relative_path[image_id] = fn
181
183
 
182
184
  image_files_relative = sorted(list(image_id_to_relative_path.values()))
183
-
185
+
184
186
  image_file_relative_to_image_id = {}
185
187
  for image_id in image_id_to_relative_path:
186
188
  relative_path = image_id_to_relative_path[image_id]
187
189
  assert relative_path not in image_file_relative_to_image_id, \
188
190
  'Duplication image IDs in YOLO output conversion for image {}'.format(relative_path)
189
191
  image_file_relative_to_image_id[relative_path] = image_id
190
-
192
+
191
193
  with open(yolo_json_file,'r') as f:
192
194
  detections = json.load(f)
193
195
  assert isinstance(detections,list)
194
-
196
+
195
197
  image_id_to_detections = defaultdict(list)
196
-
198
+
197
199
  int_formatted_image_ids = False
198
-
200
+
199
201
  # det = detections[0]
200
202
  for det in detections:
201
-
203
+
202
204
  # This could be a string, but if the YOLOv5 inference script sees that the strings
203
205
  # are really ints, it converts to ints.
204
206
  image_id = det['image_id']
205
207
  image_id_to_detections[image_id].append(det)
206
208
  if isinstance(image_id,int):
207
209
  int_formatted_image_ids = True
208
-
210
+
209
211
  # If there are any ints present, everything should be ints
210
212
  if int_formatted_image_ids:
211
213
  for det in detections:
212
214
  assert isinstance(det['image_id'],int), \
213
215
  'Found mixed int and string image IDs'
214
-
216
+
215
217
  # Convert the keys in image_id_to_error to ints
216
218
  #
217
- # This should error if we're given non-int-friendly IDs
218
- int_formatted_image_id_to_error = {}
219
+ # This should error if we're given non-int-friendly IDs
220
+ int_formatted_image_id_to_error = {}
219
221
  for image_id in image_id_to_error:
220
222
  int_formatted_image_id_to_error[int(image_id)] = \
221
223
  image_id_to_error[image_id]
222
- image_id_to_error = int_formatted_image_id_to_error
223
-
224
+ image_id_to_error = int_formatted_image_id_to_error
225
+
224
226
  # ...if image IDs are formatted as integers in YOLO output
225
-
227
+
226
228
  # In a modified version of val.py, we use negative category IDs to indicate an error
227
229
  # that happened during inference (typically truncated images with valid headers,
228
230
  # so corruption was not detected during val.py's initial corruption check pass.
@@ -232,17 +234,17 @@ def yolo_json_output_to_md_output(yolo_json_file,
232
234
  error_string = det['error']
233
235
  print('Caught inference-time failure {} for image {}'.format(error_string,det['image_id']))
234
236
  image_id_to_error[det['image_id']] = error_string
235
-
237
+
236
238
  output_images = []
237
-
239
+
238
240
  # image_file_relative = image_files_relative[10]
239
241
  for image_file_relative in tqdm(image_files_relative):
240
-
242
+
241
243
  im = {}
242
244
  im['file'] = image_file_relative
243
245
  if convert_slashes:
244
246
  im['file'] = im['file'].replace('\\','/')
245
-
247
+
246
248
  image_id = image_file_relative_to_image_id[image_file_relative]
247
249
  if int_formatted_image_ids:
248
250
  image_id = int(image_id)
@@ -254,7 +256,7 @@ def yolo_json_output_to_md_output(yolo_json_file,
254
256
  detections = []
255
257
  else:
256
258
  detections = image_id_to_detections[image_id]
257
-
259
+
258
260
  image_full_path = os.path.join(image_folder,image_file_relative)
259
261
  try:
260
262
  pil_im = vis_utils.open_image(image_full_path)
@@ -264,17 +266,17 @@ def yolo_json_output_to_md_output(yolo_json_file,
264
266
  im['failure'] = 'Conversion error: {}'.format(s)
265
267
  output_images.append(im)
266
268
  continue
267
-
269
+
268
270
  im['detections'] = []
269
-
271
+
270
272
  image_w = pil_im.size[0]
271
273
  image_h = pil_im.size[1]
272
-
274
+
273
275
  # det = detections[0]
274
276
  for det in detections:
275
-
277
+
276
278
  output_det = {}
277
-
279
+
278
280
  yolo_cat_id = int(det['category_id'])
279
281
  if offset_yolo_class_ids:
280
282
  yolo_cat_id += 1
@@ -284,63 +286,62 @@ def yolo_json_output_to_md_output(yolo_json_file,
284
286
  conf = ct_utils.round_float(conf,CONF_DIGITS)
285
287
  output_det['conf'] = conf
286
288
  input_bbox = det['bbox']
287
-
289
+
288
290
  # YOLO's COCO .json is not *that* COCO-like, but it is COCO-like in
289
291
  # that the boxes are already [xmin/ymin/w/h]
290
292
  box_xmin_absolute = input_bbox[0]
291
293
  box_ymin_absolute = input_bbox[1]
292
294
  box_width_absolute = input_bbox[2]
293
295
  box_height_absolute = input_bbox[3]
294
-
296
+
295
297
  box_xmin_relative = box_xmin_absolute / image_w
296
298
  box_ymin_relative = box_ymin_absolute / image_h
297
299
  box_width_relative = box_width_absolute / image_w
298
300
  box_height_relative = box_height_absolute / image_h
299
-
301
+
300
302
  output_bbox = [box_xmin_relative,box_ymin_relative,
301
303
  box_width_relative,box_height_relative]
302
-
304
+
303
305
  if truncate_to_standard_md_precision:
304
306
  output_bbox = ct_utils.round_float_array(output_bbox,COORD_DIGITS)
305
-
307
+
306
308
  output_det['bbox'] = output_bbox
307
309
  im['detections'].append(output_det)
308
-
309
- # ...for each detection
310
-
310
+
311
+ # ...for each detection
312
+
311
313
  output_images.append(im)
312
-
314
+
313
315
  # ...for each image file
314
-
316
+
315
317
  d = {}
316
318
  d['images'] = output_images
317
319
  d['info'] = {'format_version':'1.4','detector':detector_name}
318
320
  d['detection_categories'] = {}
319
-
321
+
320
322
  for cat_id in yolo_category_id_to_name:
321
323
  yolo_cat_id = int(cat_id)
322
324
  if offset_yolo_class_ids:
323
325
  yolo_cat_id += 1
324
326
  d['detection_categories'][str(yolo_cat_id)] = yolo_category_id_to_name[cat_id]
325
-
326
- with open(output_file,'w') as f:
327
- json.dump(d,f,indent=1)
328
-
327
+
328
+ ct_utils.write_json(output_file, d)
329
+
329
330
  # ...def yolo_json_output_to_md_output(...)
330
-
331
331
 
332
- def yolo_txt_output_to_md_output(input_results_folder,
332
+
333
+ def yolo_txt_output_to_md_output(input_results_folder,
333
334
  image_folder,
334
- output_file,
335
+ output_file,
335
336
  detector_tag=None,
336
337
  truncate_to_standard_md_precision=True):
337
338
  """
338
339
  Converts a folder of YOLO-output .txt files to MD .json format.
339
-
340
- Less finished than the .json conversion function; this .txt conversion assumes
341
- a hard-coded mapping representing the standard MD categories (in MD indexing,
340
+
341
+ Less finished than the .json conversion function; this .txt conversion assumes
342
+ a hard-coded mapping representing the standard MD categories (in MD indexing,
342
343
  1/2/3=animal/person/vehicle; in YOLO indexing, 0/1/2=animal/person/vehicle).
343
-
344
+
344
345
  Args:
345
346
  input_results_folder (str): the folder containing YOLO-output .txt files
346
347
  image_folder (str): the folder where images live, may be the same as
@@ -349,47 +350,47 @@ def yolo_txt_output_to_md_output(input_results_folder,
349
350
  results
350
351
  detector_tag (str, optional): string to put in the 'detector' field in the
351
352
  output file
352
- truncate_to_standard_md_precision (bool, optional): set this to truncate to
353
+ truncate_to_standard_md_precision (bool, optional): set this to truncate to
353
354
  COORD_DIGITS and CONF_DIGITS, like the standard MD pipeline does.
354
355
  """
355
-
356
+
356
357
  assert os.path.isdir(input_results_folder)
357
358
  assert os.path.isdir(image_folder)
358
-
359
+
359
360
  ## Enumerate results files and image files
360
-
361
+
361
362
  yolo_results_files = os.listdir(input_results_folder)
362
363
  yolo_results_files = [f for f in yolo_results_files if f.lower().endswith('.txt')]
363
364
  # print('Found {} results files'.format(len(yolo_results_files)))
364
-
365
+
365
366
  image_files = path_utils.find_images(image_folder,recursive=False)
366
367
  image_files_relative = [os.path.basename(f) for f in image_files]
367
368
  # print('Found {} images'.format(len(image_files)))
368
-
369
+
369
370
  image_files_relative_no_extension = [os.path.splitext(f)[0] for f in image_files_relative]
370
-
371
+
371
372
  ## Make sure that every results file corresponds to an image
372
-
373
+
373
374
  for f in yolo_results_files:
374
375
  result_no_extension = os.path.splitext(f)[0]
375
376
  assert result_no_extension in image_files_relative_no_extension
376
-
377
+
377
378
  ## Build MD output data
378
-
379
+
379
380
  # Map 0-indexed YOLO categories to 1-indexed MD categories
380
381
  yolo_cat_map = { 0: 1, 1: 2, 2: 3 }
381
-
382
+
382
383
  images_entries = []
383
384
 
384
385
  # image_fn = image_files_relative[0]
385
386
  for image_fn in image_files_relative:
386
-
387
- image_name, ext = os.path.splitext(image_fn)
387
+
388
+ image_name, ext = os.path.splitext(image_fn)
388
389
  label_fn = image_name + '.txt'
389
390
  label_path = os.path.join(input_results_folder, label_fn)
390
-
391
+
391
392
  detections = []
392
-
393
+
393
394
  if not os.path.exists(label_path):
394
395
  # This is assumed to be an image with no detections
395
396
  pass
@@ -397,36 +398,36 @@ def yolo_txt_output_to_md_output(input_results_folder,
397
398
  with open(label_path, newline='') as f:
398
399
  reader = csv.reader(f, delimiter=' ')
399
400
  for row in reader:
400
- category = yolo_cat_map[int(row[0])]
401
- api_box = ct_utils.convert_yolo_to_xywh([float(row[1]), float(row[2]),
401
+ category = yolo_cat_map[int(row[0])]
402
+ api_box = ct_utils.convert_yolo_to_xywh([float(row[1]), float(row[2]),
402
403
  float(row[3]), float(row[4])])
403
-
404
+
404
405
  conf = float(row[5])
405
-
406
+
406
407
  if truncate_to_standard_md_precision:
407
408
  conf = ct_utils.round_float(conf, precision=CONF_DIGITS)
408
409
  api_box = ct_utils.round_float_array(api_box, precision=COORD_DIGITS)
409
-
410
+
410
411
  detections.append({
411
412
  'category': str(category),
412
413
  'conf': conf,
413
414
  'bbox': api_box
414
415
  })
415
-
416
+
416
417
  images_entries.append({
417
418
  'file': image_fn,
418
419
  'detections': detections
419
420
  })
420
-
421
+
421
422
  # ...for each image
422
-
423
+
423
424
  ## Save output file
424
-
425
+
425
426
  detector_string = 'converted_from_yolo_format'
426
-
427
+
427
428
  if detector_tag is not None:
428
429
  detector_string = detector_tag
429
-
430
+
430
431
  output_content = {
431
432
  'info': {
432
433
  'detector': detector_string,
@@ -440,27 +441,152 @@ def yolo_txt_output_to_md_output(input_results_folder,
440
441
  },
441
442
  'images': images_entries
442
443
  }
443
-
444
- with open(output_file,'w') as f:
445
- json.dump(output_content,f,indent=1)
446
-
444
+
445
+ ct_utils.write_json(output_file, output_content)
446
+
447
447
  # ...def yolo_txt_output_to_md_output(...)
448
448
 
449
449
 
450
450
  #%% Interactive driver
451
451
 
452
452
  if False:
453
-
453
+
454
454
  pass
455
455
 
456
- #%%
457
-
456
+ #%%
457
+
458
458
  input_results_folder = os.path.expanduser('~/tmp/model-version-experiments/pt-test-kru/exp/labels')
459
459
  image_folder = os.path.expanduser('~/data/KRU-test')
460
- output_file = os.path.expanduser('~/data/mdv5a-yolo-pt-kru.json')
460
+ output_file = os.path.expanduser('~/data/mdv5a-yolo-pt-kru.json')
461
461
  yolo_txt_output_to_md_output(input_results_folder,image_folder,output_file)
462
462
 
463
463
 
464
464
  #%% Command-line driver
465
465
 
466
- # TODO
466
+ def main():
467
+ """
468
+ Command-line interface to convert YOLOv5/YOLOv8 output (.json or .txt)
469
+ to MegaDetector output format.
470
+ """
471
+
472
+ parser = argparse.ArgumentParser(
473
+ description='Converts YOLOv5 output (.json or .txt) to MD output format.'
474
+ )
475
+
476
+ # The first argument determines which series of additional arguments are supported, for
477
+ # json/txt input
478
+ subparsers = parser.add_subparsers(dest='mode', required=True,
479
+ help="Mode of operation: 'json' for YOLO JSON output, 'txt' for YOLO TXT output.")
480
+
481
+
482
+ ## 'json' mode subparser
483
+
484
+ parser_json = subparsers.add_parser('json', help='Convert YOLO-formatted .json results.')
485
+
486
+ parser_json.add_argument(
487
+ 'yolo_json_file', type=str,
488
+ help='Path to the input YOLO-formatted .json results file'
489
+ )
490
+ parser_json.add_argument(
491
+ 'image_folder', type=str,
492
+ help='Path to the image folder'
493
+ )
494
+ parser_json.add_argument(
495
+ 'output_file', type=str,
496
+ help='Path to the MD-formatted .json output file'
497
+ )
498
+ parser_json.add_argument(
499
+ 'yolo_category_id_to_name_file', type=str,
500
+ help='Path to the .yml, .yaml, .json, or .txt file mapping YOLO category IDs to names'
501
+ )
502
+ parser_json.add_argument(
503
+ '--detector_name', type=str, default='unknown',
504
+ help="Detector name to store in the output file (default: 'unknown')"
505
+ )
506
+ parser_json.add_argument(
507
+ '--image_id_to_relative_path_file', type=str, default=None,
508
+ help='Path to a .json file mapping image IDs to relative paths'
509
+ )
510
+ parser_json.add_argument(
511
+ '--offset_yolo_class_ids', type=str, default='true', choices=['true', 'false'],
512
+ help="Offset YOLO class IDs in the output (default: 'true')"
513
+ )
514
+ parser_json.add_argument(
515
+ '--truncate_to_standard_md_precision', type=str, default='true', choices=['true', 'false'],
516
+ help="Truncate coordinates and confidences to standard MD precision (default: 'true')"
517
+ )
518
+ parser_json.add_argument(
519
+ '--convert_slashes', type=str, default='true', choices=['true', 'false'],
520
+ help="Convert backslashes to forward slashes in output file paths (default: 'true')"
521
+ )
522
+
523
+
524
+ ## 'txt' mode subparser
525
+
526
+ parser_txt = subparsers.add_parser('txt', help='Convert YOLO-formatted .txt results from a folder')
527
+ parser_txt.add_argument(
528
+ 'input_results_folder', type=str,
529
+ help='Path to the folder containing YOLO .txt output files'
530
+ )
531
+ parser_txt.add_argument(
532
+ 'image_folder', type=str,
533
+ help='Path to the image folder'
534
+ )
535
+ parser_txt.add_argument(
536
+ 'output_file', type=str,
537
+ help='Path to the MD-formatted .json file output'
538
+ )
539
+ parser_txt.add_argument(
540
+ '--detector_tag', type=str, default=None,
541
+ help='Detector tag to store in the output file'
542
+ )
543
+ parser_txt.add_argument(
544
+ '--truncate_to_standard_md_precision', type=str, default='true', choices=['true', 'false'],
545
+ help="Truncate coordinates and confidences to standard MD precision (default: 'true')."
546
+ )
547
+
548
+ args = parser.parse_args()
549
+
550
+ if args.mode == 'json':
551
+
552
+ image_id_to_relative_path = None
553
+ if args.image_id_to_relative_path_file:
554
+ try:
555
+ with open(args.image_id_to_relative_path_file, 'r') as f:
556
+ image_id_to_relative_path = json.load(f)
557
+ except Exception as e:
558
+ print(f"Error loading image_id_to_relative_path_file: {e}")
559
+ sys.exit(1)
560
+
561
+ offset_yolo_class_ids = args.offset_yolo_class_ids.lower() == 'true'
562
+ truncate_json = args.truncate_to_standard_md_precision.lower() == 'true'
563
+ convert_slashes = args.convert_slashes.lower() == 'true'
564
+
565
+ yolo_json_output_to_md_output(
566
+ yolo_json_file=args.yolo_json_file,
567
+ image_folder=args.image_folder,
568
+ output_file=args.output_file,
569
+ yolo_category_id_to_name=args.yolo_category_id_to_name_file, # Function handles reading this file
570
+ detector_name=args.detector_name,
571
+ image_id_to_relative_path=image_id_to_relative_path,
572
+ offset_yolo_class_ids=offset_yolo_class_ids,
573
+ truncate_to_standard_md_precision=truncate_json,
574
+ convert_slashes=convert_slashes
575
+ )
576
+ print('Converted {} to {}'.format(args.yolo_json_file,args.output_file))
577
+
578
+ elif args.mode == 'txt':
579
+
580
+ truncate_txt = args.truncate_to_standard_md_precision.lower() == 'true'
581
+
582
+ yolo_txt_output_to_md_output(
583
+ input_results_folder=args.input_results_folder,
584
+ image_folder=args.image_folder,
585
+ output_file=args.output_file,
586
+ detector_tag=args.detector_tag,
587
+ truncate_to_standard_md_precision=truncate_txt
588
+ )
589
+ print('Converted results from {} to {}'.format(args.input_results_folder,args.output_file))
590
+
591
+ if __name__ == '__main__':
592
+ main()