megadetector 5.0.27__py3-none-any.whl → 5.0.29__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of megadetector might be problematic. Click here for more details.

Files changed (176) hide show
  1. megadetector/api/batch_processing/api_core/batch_service/score.py +4 -5
  2. megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +1 -1
  3. megadetector/api/batch_processing/api_support/summarize_daily_activity.py +1 -1
  4. megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +2 -2
  5. megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +1 -1
  6. megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +1 -1
  7. megadetector/api/synchronous/api_core/tests/load_test.py +2 -3
  8. megadetector/classification/aggregate_classifier_probs.py +3 -3
  9. megadetector/classification/analyze_failed_images.py +5 -5
  10. megadetector/classification/cache_batchapi_outputs.py +5 -5
  11. megadetector/classification/create_classification_dataset.py +11 -12
  12. megadetector/classification/crop_detections.py +10 -10
  13. megadetector/classification/csv_to_json.py +8 -8
  14. megadetector/classification/detect_and_crop.py +13 -15
  15. megadetector/classification/evaluate_model.py +7 -7
  16. megadetector/classification/identify_mislabeled_candidates.py +6 -6
  17. megadetector/classification/json_to_azcopy_list.py +1 -1
  18. megadetector/classification/json_validator.py +29 -32
  19. megadetector/classification/map_classification_categories.py +9 -9
  20. megadetector/classification/merge_classification_detection_output.py +12 -9
  21. megadetector/classification/prepare_classification_script.py +19 -19
  22. megadetector/classification/prepare_classification_script_mc.py +23 -23
  23. megadetector/classification/run_classifier.py +4 -4
  24. megadetector/classification/save_mislabeled.py +6 -6
  25. megadetector/classification/train_classifier.py +1 -1
  26. megadetector/classification/train_classifier_tf.py +9 -9
  27. megadetector/classification/train_utils.py +10 -10
  28. megadetector/data_management/annotations/annotation_constants.py +1 -1
  29. megadetector/data_management/camtrap_dp_to_coco.py +45 -45
  30. megadetector/data_management/cct_json_utils.py +101 -101
  31. megadetector/data_management/cct_to_md.py +49 -49
  32. megadetector/data_management/cct_to_wi.py +33 -33
  33. megadetector/data_management/coco_to_labelme.py +75 -75
  34. megadetector/data_management/coco_to_yolo.py +189 -189
  35. megadetector/data_management/databases/add_width_and_height_to_db.py +3 -2
  36. megadetector/data_management/databases/combine_coco_camera_traps_files.py +38 -38
  37. megadetector/data_management/databases/integrity_check_json_db.py +202 -188
  38. megadetector/data_management/databases/subset_json_db.py +33 -33
  39. megadetector/data_management/generate_crops_from_cct.py +38 -38
  40. megadetector/data_management/get_image_sizes.py +54 -49
  41. megadetector/data_management/labelme_to_coco.py +130 -124
  42. megadetector/data_management/labelme_to_yolo.py +78 -72
  43. megadetector/data_management/lila/create_lila_blank_set.py +81 -83
  44. megadetector/data_management/lila/create_lila_test_set.py +32 -31
  45. megadetector/data_management/lila/create_links_to_md_results_files.py +18 -18
  46. megadetector/data_management/lila/download_lila_subset.py +21 -24
  47. megadetector/data_management/lila/generate_lila_per_image_labels.py +91 -91
  48. megadetector/data_management/lila/get_lila_annotation_counts.py +30 -30
  49. megadetector/data_management/lila/get_lila_image_counts.py +22 -22
  50. megadetector/data_management/lila/lila_common.py +70 -70
  51. megadetector/data_management/lila/test_lila_metadata_urls.py +13 -14
  52. megadetector/data_management/mewc_to_md.py +339 -340
  53. megadetector/data_management/ocr_tools.py +258 -252
  54. megadetector/data_management/read_exif.py +232 -223
  55. megadetector/data_management/remap_coco_categories.py +26 -26
  56. megadetector/data_management/remove_exif.py +31 -20
  57. megadetector/data_management/rename_images.py +187 -187
  58. megadetector/data_management/resize_coco_dataset.py +41 -41
  59. megadetector/data_management/speciesnet_to_md.py +41 -41
  60. megadetector/data_management/wi_download_csv_to_coco.py +55 -55
  61. megadetector/data_management/yolo_output_to_md_output.py +117 -120
  62. megadetector/data_management/yolo_to_coco.py +195 -188
  63. megadetector/detection/change_detection.py +831 -0
  64. megadetector/detection/process_video.py +341 -338
  65. megadetector/detection/pytorch_detector.py +308 -266
  66. megadetector/detection/run_detector.py +186 -166
  67. megadetector/detection/run_detector_batch.py +366 -364
  68. megadetector/detection/run_inference_with_yolov5_val.py +328 -325
  69. megadetector/detection/run_tiled_inference.py +312 -253
  70. megadetector/detection/tf_detector.py +24 -24
  71. megadetector/detection/video_utils.py +291 -283
  72. megadetector/postprocessing/add_max_conf.py +15 -11
  73. megadetector/postprocessing/categorize_detections_by_size.py +44 -44
  74. megadetector/postprocessing/classification_postprocessing.py +808 -311
  75. megadetector/postprocessing/combine_batch_outputs.py +20 -21
  76. megadetector/postprocessing/compare_batch_results.py +528 -517
  77. megadetector/postprocessing/convert_output_format.py +97 -97
  78. megadetector/postprocessing/create_crop_folder.py +220 -147
  79. megadetector/postprocessing/detector_calibration.py +173 -168
  80. megadetector/postprocessing/generate_csv_report.py +508 -0
  81. megadetector/postprocessing/load_api_results.py +25 -22
  82. megadetector/postprocessing/md_to_coco.py +129 -98
  83. megadetector/postprocessing/md_to_labelme.py +89 -83
  84. megadetector/postprocessing/md_to_wi.py +40 -40
  85. megadetector/postprocessing/merge_detections.py +87 -114
  86. megadetector/postprocessing/postprocess_batch_results.py +319 -302
  87. megadetector/postprocessing/remap_detection_categories.py +36 -36
  88. megadetector/postprocessing/render_detection_confusion_matrix.py +205 -199
  89. megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +57 -57
  90. megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +27 -28
  91. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +702 -677
  92. megadetector/postprocessing/separate_detections_into_folders.py +226 -211
  93. megadetector/postprocessing/subset_json_detector_output.py +265 -262
  94. megadetector/postprocessing/top_folders_to_bottom.py +45 -45
  95. megadetector/postprocessing/validate_batch_results.py +70 -70
  96. megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +52 -52
  97. megadetector/taxonomy_mapping/map_new_lila_datasets.py +15 -15
  98. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +14 -14
  99. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +66 -69
  100. megadetector/taxonomy_mapping/retrieve_sample_image.py +16 -16
  101. megadetector/taxonomy_mapping/simple_image_download.py +8 -8
  102. megadetector/taxonomy_mapping/species_lookup.py +33 -33
  103. megadetector/taxonomy_mapping/taxonomy_csv_checker.py +14 -14
  104. megadetector/taxonomy_mapping/taxonomy_graph.py +11 -11
  105. megadetector/taxonomy_mapping/validate_lila_category_mappings.py +13 -13
  106. megadetector/utils/azure_utils.py +22 -22
  107. megadetector/utils/ct_utils.py +1019 -200
  108. megadetector/utils/directory_listing.py +21 -77
  109. megadetector/utils/gpu_test.py +22 -22
  110. megadetector/utils/md_tests.py +541 -518
  111. megadetector/utils/path_utils.py +1511 -406
  112. megadetector/utils/process_utils.py +41 -41
  113. megadetector/utils/sas_blob_utils.py +53 -49
  114. megadetector/utils/split_locations_into_train_val.py +73 -60
  115. megadetector/utils/string_utils.py +147 -26
  116. megadetector/utils/url_utils.py +463 -173
  117. megadetector/utils/wi_utils.py +2629 -2868
  118. megadetector/utils/write_html_image_list.py +137 -137
  119. megadetector/visualization/plot_utils.py +21 -21
  120. megadetector/visualization/render_images_with_thumbnails.py +37 -73
  121. megadetector/visualization/visualization_utils.py +424 -404
  122. megadetector/visualization/visualize_db.py +197 -190
  123. megadetector/visualization/visualize_detector_output.py +126 -98
  124. {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/METADATA +6 -3
  125. megadetector-5.0.29.dist-info/RECORD +163 -0
  126. {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/WHEEL +1 -1
  127. megadetector/data_management/importers/add_nacti_sizes.py +0 -52
  128. megadetector/data_management/importers/add_timestamps_to_icct.py +0 -79
  129. megadetector/data_management/importers/animl_results_to_md_results.py +0 -158
  130. megadetector/data_management/importers/auckland_doc_test_to_json.py +0 -373
  131. megadetector/data_management/importers/auckland_doc_to_json.py +0 -201
  132. megadetector/data_management/importers/awc_to_json.py +0 -191
  133. megadetector/data_management/importers/bellevue_to_json.py +0 -272
  134. megadetector/data_management/importers/cacophony-thermal-importer.py +0 -793
  135. megadetector/data_management/importers/carrizo_shrubfree_2018.py +0 -269
  136. megadetector/data_management/importers/carrizo_trail_cam_2017.py +0 -289
  137. megadetector/data_management/importers/cct_field_adjustments.py +0 -58
  138. megadetector/data_management/importers/channel_islands_to_cct.py +0 -913
  139. megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +0 -180
  140. megadetector/data_management/importers/eMammal/eMammal_helpers.py +0 -249
  141. megadetector/data_management/importers/eMammal/make_eMammal_json.py +0 -223
  142. megadetector/data_management/importers/ena24_to_json.py +0 -276
  143. megadetector/data_management/importers/filenames_to_json.py +0 -386
  144. megadetector/data_management/importers/helena_to_cct.py +0 -283
  145. megadetector/data_management/importers/idaho-camera-traps.py +0 -1407
  146. megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +0 -294
  147. megadetector/data_management/importers/import_desert_lion_conservation_camera_traps.py +0 -387
  148. megadetector/data_management/importers/jb_csv_to_json.py +0 -150
  149. megadetector/data_management/importers/mcgill_to_json.py +0 -250
  150. megadetector/data_management/importers/missouri_to_json.py +0 -490
  151. megadetector/data_management/importers/nacti_fieldname_adjustments.py +0 -79
  152. megadetector/data_management/importers/noaa_seals_2019.py +0 -181
  153. megadetector/data_management/importers/osu-small-animals-to-json.py +0 -364
  154. megadetector/data_management/importers/pc_to_json.py +0 -365
  155. megadetector/data_management/importers/plot_wni_giraffes.py +0 -123
  156. megadetector/data_management/importers/prepare_zsl_imerit.py +0 -131
  157. megadetector/data_management/importers/raic_csv_to_md_results.py +0 -416
  158. megadetector/data_management/importers/rspb_to_json.py +0 -356
  159. megadetector/data_management/importers/save_the_elephants_survey_A.py +0 -320
  160. megadetector/data_management/importers/save_the_elephants_survey_B.py +0 -329
  161. megadetector/data_management/importers/snapshot_safari_importer.py +0 -758
  162. megadetector/data_management/importers/snapshot_serengeti_lila.py +0 -1067
  163. megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +0 -150
  164. megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +0 -153
  165. megadetector/data_management/importers/sulross_get_exif.py +0 -65
  166. megadetector/data_management/importers/timelapse_csv_set_to_json.py +0 -490
  167. megadetector/data_management/importers/ubc_to_json.py +0 -399
  168. megadetector/data_management/importers/umn_to_json.py +0 -507
  169. megadetector/data_management/importers/wellington_to_json.py +0 -263
  170. megadetector/data_management/importers/wi_to_json.py +0 -442
  171. megadetector/data_management/importers/zamba_results_to_md_results.py +0 -180
  172. megadetector/data_management/lila/add_locations_to_island_camera_traps.py +0 -101
  173. megadetector/data_management/lila/add_locations_to_nacti.py +0 -151
  174. megadetector-5.0.27.dist-info/RECORD +0 -208
  175. {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/licenses/LICENSE +0 -0
  176. {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/top_level.txt +0 -0
@@ -6,15 +6,15 @@ run_tiled_inference.py
6
6
 
7
7
  Runs inference on a folder, fist splitting each image up into tiles of size
8
8
  MxN (typically the native inference size of your detector), writing those
9
- tiles out to a temporary folder, then de-duplicating the resulting detections before
9
+ tiles out to a temporary folder, then de-duplicating the resulting detections before
10
10
  merging them back into a set of detections that make sense on the original images.
11
11
 
12
- This approach will likely fail to detect very large animals, so if you expect both large
13
- and small animals (in terms of pixel size), this script is best used in
12
+ This approach will likely fail to detect very large animals, so if you expect both large
13
+ and small animals (in terms of pixel size), this script is best used in
14
14
  conjunction with a traditional inference pass that looks at whole images.
15
15
 
16
16
  Currently requires temporary storage at least as large as the input data, generally
17
- a lot more than that (depending on the overlap between adjacent tiles). This is
17
+ a lot more than that (depending on the overlap between adjacent tiles). This is
18
18
  inefficient, but easy to debug.
19
19
 
20
20
  Programmatic invocation supports using YOLOv5's inference scripts (and test-time
@@ -26,16 +26,24 @@ augmentation); the command-line interface only supports standard inference right
26
26
 
27
27
  import os
28
28
  import json
29
+ import tempfile
30
+ import uuid
31
+ import sys
32
+ import argparse
29
33
 
30
34
  from tqdm import tqdm
31
35
 
32
36
  import torch
33
37
  from torchvision import ops
34
38
 
35
- from megadetector.detection.run_inference_with_yolov5_val import YoloInferenceOptions,run_inference_with_yolo_val
36
- from megadetector.detection.run_detector_batch import load_and_run_detector_batch,write_results_to_file
37
- from megadetector.detection.run_detector import try_download_known_detector
39
+ from megadetector.detection.run_inference_with_yolov5_val import \
40
+ YoloInferenceOptions,run_inference_with_yolo_val
41
+ from megadetector.detection.run_detector_batch import \
42
+ load_and_run_detector_batch,write_results_to_file
43
+ from megadetector.detection.run_detector import \
44
+ try_download_known_detector, CONF_DIGITS, COORD_DIGITS
38
45
  from megadetector.utils import path_utils
46
+ from megadetector.utils.ct_utils import round_float_array, round_float
39
47
  from megadetector.visualization import visualization_utils as vis_utils
40
48
 
41
49
  default_patch_overlap = 0.5
@@ -57,59 +65,59 @@ def get_patch_boundaries(image_size,patch_size,patch_stride=None):
57
65
  """
58
66
  Computes a list of patch starting coordinates (x,y) given an image size (w,h)
59
67
  and a stride (x,y)
60
-
68
+
61
69
  Patch size is guaranteed, but the stride may deviate to make sure all pixels are covered.
62
70
  I.e., we move by regular strides until the current patch walks off the right/bottom,
63
71
  at which point it backs up to one patch from the end. So if your image is 15
64
- pixels wide and you have a stride of 10 pixels, you will get starting positions
72
+ pixels wide and you have a stride of 10 pixels, you will get starting positions
65
73
  of 0 (from 0 to 9) and 5 (from 5 to 14).
66
-
74
+
67
75
  Args:
68
76
  image_size (tuple): size of the image you want to divide into patches, as a length-2 tuple (w,h)
69
77
  patch_size (tuple): patch size into which you want to divide an image, as a length-2 tuple (w,h)
70
- patch_stride (tuple or float, optional): stride between patches, as a length-2 tuple (x,y), or a
71
- float; if this is a float, it's interpreted as the stride relative to the patch size
78
+ patch_stride (tuple or float, optional): stride between patches, as a length-2 tuple (x,y), or a
79
+ float; if this is a float, it's interpreted as the stride relative to the patch size
72
80
  (0.1 == 10% stride). Defaults to half the patch size.
73
81
 
74
82
  Returns:
75
- list: list of length-2 tuples, each representing the x/y start position of a patch
83
+ list: list of length-2 tuples, each representing the x/y start position of a patch
76
84
  """
77
-
85
+
78
86
  if patch_stride is None:
79
87
  patch_stride = (round(patch_size[0]*(1.0-default_patch_overlap)),
80
88
  round(patch_size[1]*(1.0-default_patch_overlap)))
81
89
  elif isinstance(patch_stride,float):
82
90
  patch_stride = (round(patch_size[0]*(patch_stride)),
83
91
  round(patch_size[1]*(patch_stride)))
84
-
92
+
85
93
  image_width = image_size[0]
86
94
  image_height = image_size[1]
87
-
95
+
88
96
  assert patch_size[0] <= image_size[0], 'Patch width {} is larger than image width {}'.format(
89
97
  patch_size[0],image_size[0])
90
98
  assert patch_size[1] <= image_size[1], 'Patch height {} is larger than image height {}'.format(
91
99
  patch_size[1],image_size[1])
92
-
100
+
93
101
  def add_patch_row(patch_start_positions,y_start):
94
102
  """
95
103
  Add one row to our list of patch start positions, i.e.
96
104
  loop over all columns.
97
105
  """
98
-
106
+
99
107
  x_start = 0; x_end = x_start + patch_size[0] - 1
100
-
108
+
101
109
  while(True):
102
-
110
+
103
111
  patch_start_positions.append([x_start,y_start])
104
-
112
+
105
113
  # If this patch put us right at the end of the last column, we're done
106
114
  if x_end == image_width - 1:
107
115
  break
108
-
116
+
109
117
  # Move one patch to the right
110
118
  x_start += patch_stride[0]
111
119
  x_end = x_start + patch_size[0] - 1
112
-
120
+
113
121
  # If this patch flows over the edge, add one more patch to cover
114
122
  # the pixels on the end, then we're done.
115
123
  if x_end > (image_width - 1):
@@ -118,27 +126,27 @@ def get_patch_boundaries(image_size,patch_size,patch_stride=None):
118
126
  x_end = x_start + patch_size[0] - 1
119
127
  patch_start_positions.append([x_start,y_start])
120
128
  break
121
-
129
+
122
130
  # ...for each column
123
-
131
+
124
132
  return patch_start_positions
125
-
133
+
126
134
  patch_start_positions = []
127
-
135
+
128
136
  y_start = 0; y_end = y_start + patch_size[1] - 1
129
-
137
+
130
138
  while(True):
131
-
139
+
132
140
  patch_start_positions = add_patch_row(patch_start_positions,y_start)
133
-
141
+
134
142
  # If this patch put us right at the bottom of the lats row, we're done
135
143
  if y_end == image_height - 1:
136
144
  break
137
-
145
+
138
146
  # Move one patch down
139
147
  y_start += patch_stride[1]
140
148
  y_end = y_start + patch_size[1] - 1
141
-
149
+
142
150
  # If this patch flows over the bottom, add one more patch to cover
143
151
  # the pixels at the bottom, then we're done
144
152
  if y_end > (image_height - 1):
@@ -147,24 +155,24 @@ def get_patch_boundaries(image_size,patch_size,patch_stride=None):
147
155
  y_end = y_start + patch_size[1] - 1
148
156
  patch_start_positions = add_patch_row(patch_start_positions,y_start)
149
157
  break
150
-
158
+
151
159
  # ...for each row
152
-
160
+
153
161
  for p in patch_start_positions:
154
162
  assert p[0] >= 0 and p[1] >= 0 and p[0] <= image_width and p[1] <= image_height, \
155
163
  'Patch generation error (illegal patch {})'.format(p)
156
-
164
+
157
165
  # The last patch should always end at the bottom-right of the image
158
166
  assert patch_start_positions[-1][0]+patch_size[0] == image_width, \
159
167
  'Patch generation error (last patch does not end on the right)'
160
168
  assert patch_start_positions[-1][1]+patch_size[1] == image_height, \
161
169
  'Patch generation error (last patch does not end at the bottom)'
162
-
170
+
163
171
  # All patches should be unique
164
172
  patch_start_positions_tuples = [tuple(x) for x in patch_start_positions]
165
173
  assert len(patch_start_positions_tuples) == len(set(patch_start_positions_tuples)), \
166
174
  'Patch generation error (duplicate start position)'
167
-
175
+
168
176
  return patch_start_positions
169
177
 
170
178
  # ...get_patch_boundaries()
@@ -174,12 +182,12 @@ def patch_info_to_patch_name(image_name,patch_x_min,patch_y_min):
174
182
  """
175
183
  Gives a unique string name to an x/y coordinate, e.g. turns ("a.jpg",10,20) into
176
184
  "a.jpg_0010_0020".
177
-
185
+
178
186
  Args:
179
187
  image_name (str): image identifier
180
188
  patch_x_min (int): x coordinate
181
189
  patch_y_min (int): y coordinate
182
-
190
+
183
191
  Returns:
184
192
  str: name for this patch, e.g. "a.jpg_0010_0020"
185
193
  """
@@ -197,13 +205,13 @@ def extract_patch_from_image(im,
197
205
  overwrite=True):
198
206
  """
199
207
  Extracts a patch from the provided image, and writes that patch out to a new file.
200
-
208
+
201
209
  Args:
202
210
  im (str or Image): image from which we should extract a patch, can be a filename or
203
211
  a PIL Image object.
204
- patch_xy (tuple): length-2 tuple of ints (x,y) representing the upper-left corner
212
+ patch_xy (tuple): length-2 tuple of ints (x,y) representing the upper-left corner
205
213
  of the patch to extract
206
- patch_size (tuple): length-2 tuple of ints (w,h) representing the size of the
214
+ patch_size (tuple): length-2 tuple of ints (w,h) representing the size of the
207
215
  patch to extract
208
216
  patch_image_fn (str, optional): image filename to write the patch to; if this is None
209
217
  the filename will be generated from [image_name] and the patch coordinates
@@ -212,16 +220,16 @@ def extract_patch_from_image(im,
212
220
  image_name (str, optional): the identifier of the source image; only used to generate
213
221
  a patch filename, so only required if [patch_image_fn] is None
214
222
  overwrite (bool, optional): whether to overwrite an existing patch image
215
-
223
+
216
224
  Returns:
217
225
  dict: a dictionary with fields xmin,xmax,ymin,ymax,patch_fn
218
226
  """
219
-
227
+
220
228
  if isinstance(im,str):
221
229
  pil_im = vis_utils.open_image(im)
222
230
  else:
223
231
  pil_im = im
224
-
232
+
225
233
  patch_x_min = patch_xy[0]
226
234
  patch_y_min = patch_xy[1]
227
235
  patch_x_max = patch_x_min + patch_size[0] - 1
@@ -243,19 +251,19 @@ def extract_patch_from_image(im,
243
251
  "If you don't supply a patch filename to extract_patch_from_image, you need to supply a folder name"
244
252
  patch_name = patch_info_to_patch_name(image_name,patch_x_min,patch_y_min)
245
253
  patch_image_fn = os.path.join(patch_folder,patch_name + '.jpg')
246
-
254
+
247
255
  if os.path.isfile(patch_image_fn) and (not overwrite):
248
256
  pass
249
- else:
257
+ else:
250
258
  patch_im.save(patch_image_fn,quality=patch_jpeg_quality)
251
-
259
+
252
260
  patch_info = {}
253
261
  patch_info['xmin'] = patch_x_min
254
262
  patch_info['xmax'] = patch_x_max
255
263
  patch_info['ymin'] = patch_y_min
256
264
  patch_info['ymax'] = patch_y_max
257
265
  patch_info['patch_fn'] = patch_image_fn
258
-
266
+
259
267
  return patch_info
260
268
 
261
269
  # ...def extract_patch_from_image(...)
@@ -264,33 +272,33 @@ def extract_patch_from_image(im,
264
272
  def in_place_nms(md_results, iou_thres=0.45, verbose=True):
265
273
  """
266
274
  Run torch.ops.nms in-place on MD-formatted detection results.
267
-
275
+
268
276
  Args:
269
- md_results (dict): detection results for a list of images, in MD results format (i.e.,
277
+ md_results (dict): detection results for a list of images, in MD results format (i.e.,
270
278
  containing a list of image dicts with the key 'images', each of which has a list
271
279
  of detections with the key 'detections')
272
280
  iou_thres (float, optional): IoU threshold above which we will treat two detections as
273
281
  redundant
274
282
  verbose (bool, optional): enable additional debug console output
275
283
  """
276
-
284
+
277
285
  n_detections_before = 0
278
286
  n_detections_after = 0
279
-
287
+
280
288
  # i_image = 18; im = md_results['images'][i_image]
281
289
  for i_image,im in tqdm(enumerate(md_results['images']),total=len(md_results['images'])):
282
-
290
+
283
291
  if (im['detections'] is None) or (len(im['detections']) == 0):
284
292
  continue
285
-
293
+
286
294
  boxes = []
287
295
  scores = []
288
-
296
+
289
297
  n_detections_before += len(im['detections'])
290
-
298
+
291
299
  # det = im['detections'][0]
292
300
  for det in im['detections']:
293
-
301
+
294
302
  # Using x1/x2 notation rather than x0/x1 notation to be consistent
295
303
  # with the Torch documentation.
296
304
  x1 = det['bbox'][0]
@@ -302,119 +310,133 @@ def in_place_nms(md_results, iou_thres=0.45, verbose=True):
302
310
  scores.append(det['conf'])
303
311
 
304
312
  # ...for each detection
305
-
313
+
306
314
  t_boxes = torch.tensor(boxes)
307
315
  t_scores = torch.tensor(scores)
308
-
316
+
309
317
  box_indices = ops.nms(t_boxes,t_scores,iou_thres).tolist()
310
-
318
+
311
319
  post_nms_detections = [im['detections'][x] for x in box_indices]
312
-
320
+
313
321
  assert len(post_nms_detections) <= len(im['detections'])
314
-
322
+
315
323
  im['detections'] = post_nms_detections
316
-
324
+
317
325
  n_detections_after += len(im['detections'])
318
-
326
+
319
327
  # ...for each image
320
-
328
+
321
329
  if verbose:
322
330
  print('NMS removed {} of {} detections'.format(
323
331
  n_detections_before-n_detections_after,
324
332
  n_detections_before))
325
-
333
+
326
334
  # ...in_place_nms()
327
335
 
328
336
 
329
337
  def _extract_tiles_for_image(fn_relative,image_folder,tiling_folder,patch_size,patch_stride,overwrite):
330
338
  """
331
339
  Private function to extract tiles for a single image.
332
-
340
+
333
341
  Returns a dict with fields 'patches' (see extract_patch_from_image) and 'image_fn'.
334
-
342
+
335
343
  If there is an error, 'patches' will be None and the 'error' field will contain
336
344
  failure details. In that case, some tiles may still be generated.
337
345
  """
338
-
346
+
339
347
  fn_abs = os.path.join(image_folder,fn_relative)
340
348
  error = None
341
- patches = []
342
-
349
+ patches = []
350
+
343
351
  image_name = path_utils.clean_filename(fn_relative,char_limit=None,force_lower=True)
344
-
352
+
345
353
  try:
346
-
354
+
347
355
  # Open the image
348
356
  im = vis_utils.open_image(fn_abs)
349
357
  image_size = [im.width,im.height]
350
-
358
+
351
359
  # Generate patch boundaries (a list of [x,y] starting points)
352
- patch_boundaries = get_patch_boundaries(image_size,patch_size,patch_stride)
353
-
360
+ patch_boundaries = get_patch_boundaries(image_size,patch_size,patch_stride)
361
+
354
362
  # Extract patches
355
363
  #
356
- # patch_xy = patch_boundaries[0]
364
+ # patch_xy = patch_boundaries[0]
357
365
  for patch_xy in patch_boundaries:
358
-
366
+
359
367
  patch_info = extract_patch_from_image(im,patch_xy,patch_size,
360
368
  patch_folder=tiling_folder,
361
369
  image_name=image_name,
362
370
  overwrite=overwrite)
363
371
  patch_info['source_fn'] = fn_relative
364
372
  patches.append(patch_info)
365
-
373
+
366
374
  except Exception as e:
367
-
375
+
368
376
  s = 'Patch generation error for {}: \n{}'.format(fn_relative,str(e))
369
377
  print(s)
370
378
  # patches = None
371
379
  error = s
372
-
380
+
373
381
  image_patch_info = {}
374
382
  image_patch_info['patches'] = patches
375
383
  image_patch_info['image_fn'] = fn_relative
376
384
  image_patch_info['error'] = error
377
-
385
+
378
386
  return image_patch_info
379
-
380
-
387
+
388
+
381
389
  #%% Main function
382
-
383
- def run_tiled_inference(model_file, image_folder, tiling_folder, output_file,
384
- tile_size_x=1280, tile_size_y=1280, tile_overlap=0.5,
385
- checkpoint_path=None, checkpoint_frequency=-1, remove_tiles=False,
390
+
391
+ def run_tiled_inference(model_file,
392
+ image_folder,
393
+ tiling_folder,
394
+ output_file,
395
+ tile_size_x=1280,
396
+ tile_size_y=1280,
397
+ tile_overlap=0.5,
398
+ checkpoint_path=None,
399
+ checkpoint_frequency=-1,
400
+ remove_tiles=False,
386
401
  yolo_inference_options=None,
387
402
  n_patch_extraction_workers=default_n_patch_extraction_workers,
388
403
  overwrite_tiles=True,
389
- image_list=None):
404
+ image_list=None,
405
+ augment=False,
406
+ detector_options=None,
407
+ use_image_queue=True,
408
+ preprocess_on_image_queue=True,
409
+ inference_size=None):
390
410
  """
391
- Runs inference using [model_file] on the images in [image_folder], fist splitting each image up
411
+ Runs inference using [model_file] on the images in [image_folder], fist splitting each image up
392
412
  into tiles of size [tile_size_x] x [tile_size_y], writing those tiles to [tiling_folder],
393
- then de-duplicating the results before merging them back into a set of detections that make
394
- sense on the original images and writing those results to [output_file].
395
-
413
+ then de-duplicating the results before merging them back into a set of detections that make
414
+ sense on the original images and writing those results to [output_file].
415
+
396
416
  [tiling_folder] can be any folder, but this function reserves the right to do whatever it wants
397
- within that folder, including deleting everything, so it's best if it's a new folder.
417
+ within that folder, including deleting everything, so it's best if it's a new folder.
398
418
  Conceptually this folder is temporary, it's just helpful in this case to not actually
399
- use the system temp folder, because the tile cache may be very large, so the caller may
400
- want it to be on a specific drive.
401
-
419
+ use the system temp folder, because the tile cache may be very large, so the caller may
420
+ want it to be on a specific drive. If this is None, a new folder will be created in
421
+ system temp space.
422
+
402
423
  tile_overlap is the fraction of overlap between tiles.
403
-
424
+
404
425
  Optionally removes the temporary tiles.
405
-
406
- if yolo_inference_options is supplied, it should be an instance of YoloInferenceOptions; in
407
- this case the model will be run with run_inference_with_yolov5_val. This is typically used to
426
+
427
+ if yolo_inference_options is supplied, it should be an instance of YoloInferenceOptions; in
428
+ this case the model will be run with run_inference_with_yolov5_val. This is typically used to
408
429
  run the model with test-time augmentation.
409
-
430
+
410
431
  Args:
411
432
  model_file (str): model filename (ending in .pt), or a well-known model name (e.g. "MDV5A")
412
433
  image_folder (str): the folder of images to proess (always recursive)
413
- tiling_folder (str): folder for temporary tile storage; see caveats above
434
+ tiling_folder (str): folder for temporary tile storage; see caveats above. Can be None
435
+ to use system temp space.
414
436
  output_file (str): .json file to which we should write MD-formatted results
415
437
  tile_size_x (int, optional): tile width
416
438
  tile_size_y (int, optional): tile height
417
- tile_overlap (float, optional): overlap between adjacenet tiles, as a fraction of the
439
+ tile_overlap (float, optional): overlap between adjacent tiles, as a fraction of the
418
440
  tile size
419
441
  checkpoint_path (str, optional): checkpoint path; passed directly to run_detector_batch; see
420
442
  run_detector_batch for details
@@ -425,40 +447,55 @@ def run_tiled_inference(model_file, image_folder, tiling_folder, output_file,
425
447
  run_inference_with_yolov5_val.py, rather than with run_detector_batch.py, using these options
426
448
  n_patch_extraction_workers (int, optional): number of workers to use for patch extraction;
427
449
  set to <= 1 to disable parallelization
428
- image_list (list, optional): .json file containing a list of specific images to process. If
450
+ image_list (list, optional): .json file containing a list of specific images to process. If
429
451
  this is supplied, and the paths are absolute, [image_folder] will be ignored. If this is supplied,
430
- and the paths are relative, they should be relative to [image_folder].
431
-
452
+ and the paths are relative, they should be relative to [image_folder]
453
+ augment (bool, optional): apply test-time augmentation, only relevant if yolo_inference_options
454
+ is None
455
+ detector_options (dict, optional): parameters to pass to run_detector, only relevant if
456
+ yolo_inference_options is None
457
+ use_image_queue (bool, optional): whether to use a loader worker queue, only relevant if
458
+ yolo_inference_options is None
459
+ preprocess_on_image_queue (bool, optional): whether the image queue should also be responsible
460
+ for preprocessing
461
+ inference_size (int, optional): override the default inference image size, only relevant if
462
+ yolo_inference_options is None
463
+
432
464
  Returns:
433
465
  dict: MD-formatted results dictionary, identical to what's written to [output_file]
434
466
  """
435
467
 
436
468
  ##%% Validate arguments
437
-
469
+
438
470
  assert tile_overlap < 1 and tile_overlap >= 0, \
439
471
  'Illegal tile overlap value {}'.format(tile_overlap)
440
-
472
+
441
473
  if tile_size_x == -1:
442
474
  tile_size_x = default_tile_size[0]
443
475
  if tile_size_y == -1:
444
476
  tile_size_y = default_tile_size[1]
445
-
477
+
446
478
  patch_size = [tile_size_x,tile_size_y]
447
479
  patch_stride = (round(patch_size[0]*(1.0-tile_overlap)),
448
480
  round(patch_size[1]*(1.0-tile_overlap)))
449
-
481
+
482
+ if tiling_folder is None:
483
+ tiling_folder = \
484
+ os.path.join(tempfile.gettempdir(), 'md-tiling', str(uuid.uuid1()))
485
+ print('Creating temporary tiling folder: {}'.format(tiling_folder))
486
+
450
487
  os.makedirs(tiling_folder,exist_ok=True)
451
-
488
+
452
489
  ##%% List files
453
-
490
+
454
491
  if image_list is None:
455
-
492
+
456
493
  print('Enumerating images in {}'.format(image_folder))
457
- image_files_relative = path_utils.find_images(image_folder, recursive=True, return_relative_paths=True)
494
+ image_files_relative = path_utils.find_images(image_folder, recursive=True, return_relative_paths=True)
458
495
  assert len(image_files_relative) > 0, 'No images found in folder {}'.format(image_folder)
459
-
496
+
460
497
  else:
461
-
498
+
462
499
  print('Loading image list from {}'.format(image_list))
463
500
  with open(image_list,'r') as f:
464
501
  image_files_relative = json.load(f)
@@ -479,121 +516,134 @@ def run_tiled_inference(model_file, image_folder, tiling_folder, output_file,
479
516
  if (n_absolute_paths != 0) and (n_absolute_paths != len(image_files_relative)):
480
517
  raise ValueError('Illegal file list: converted {} of {} paths to relative'.format(
481
518
  n_absolute_paths,len(image_files_relative)))
482
-
519
+
483
520
  ##%% Generate tiles
484
-
521
+
485
522
  all_image_patch_info = None
486
-
523
+
487
524
  print('Extracting patches from {} images'.format(len(image_files_relative)))
488
-
525
+
489
526
  n_workers = n_patch_extraction_workers
490
-
527
+
491
528
  if n_workers <= 1:
492
-
529
+
493
530
  all_image_patch_info = []
494
-
495
- # fn_relative = image_files_relative[0]
496
- for fn_relative in tqdm(image_files_relative):
531
+
532
+ # fn_relative = image_files_relative[0]
533
+ for fn_relative in tqdm(image_files_relative):
497
534
  image_patch_info = \
498
535
  _extract_tiles_for_image(fn_relative,image_folder,tiling_folder,patch_size,patch_stride,
499
536
  overwrite=overwrite_tiles)
500
537
  all_image_patch_info.append(image_patch_info)
501
-
538
+
502
539
  else:
503
-
540
+
504
541
  from multiprocessing.pool import ThreadPool
505
542
  from multiprocessing.pool import Pool
506
543
  from functools import partial
507
544
 
508
- if n_workers > len(image_files_relative):
509
-
510
- print('Pool of {} requested, but only {} images available, reducing pool to {}'.\
511
- format(n_workers,len(image_files_relative),len(image_files_relative)))
512
- n_workers = len(image_files_relative)
513
-
514
- if parallelization_uses_threads:
515
- pool = ThreadPool(n_workers); poolstring = 'threads'
516
- else:
517
- pool = Pool(n_workers); poolstring = 'processes'
518
-
519
- print('Starting patch extraction pool with {} {}'.format(n_workers,poolstring))
520
-
521
- all_image_patch_info = list(tqdm(pool.imap(
522
- partial(_extract_tiles_for_image,
523
- image_folder=image_folder,
524
- tiling_folder=tiling_folder,
525
- patch_size=patch_size,
526
- patch_stride=patch_stride,
527
- overwrite=overwrite_tiles),
528
- image_files_relative),total=len(image_files_relative)))
529
-
545
+ pool = None
546
+ try:
547
+ if n_workers > len(image_files_relative):
548
+
549
+ print('Pool of {} requested, but only {} images available, reducing pool to {}'.\
550
+ format(n_workers,len(image_files_relative),len(image_files_relative)))
551
+ n_workers = len(image_files_relative)
552
+
553
+ if parallelization_uses_threads:
554
+ pool = ThreadPool(n_workers); poolstring = 'threads'
555
+ else:
556
+ pool = Pool(n_workers); poolstring = 'processes'
557
+
558
+ print('Starting patch extraction pool with {} {}'.format(n_workers,poolstring))
559
+
560
+ all_image_patch_info = list(tqdm(pool.imap(
561
+ partial(_extract_tiles_for_image,
562
+ image_folder=image_folder,
563
+ tiling_folder=tiling_folder,
564
+ patch_size=patch_size,
565
+ patch_stride=patch_stride,
566
+ overwrite=overwrite_tiles),
567
+ image_files_relative),total=len(image_files_relative)))
568
+ finally:
569
+ if pool is not None:
570
+ pool.close()
571
+ pool.join()
572
+ print("Pool closed and joined for patch extraction")
573
+
530
574
  # ...for each image
531
-
575
+
532
576
  # Write tile information to file; this is just a debugging convenience
533
577
  folder_name = path_utils.clean_filename(image_folder,force_lower=True)
534
578
  if folder_name.startswith('_'):
535
579
  folder_name = folder_name[1:]
536
-
580
+
537
581
  tile_cache_file = os.path.join(tiling_folder,folder_name + '_patch_info.json')
538
582
  with open(tile_cache_file,'w') as f:
539
583
  json.dump(all_image_patch_info,f,indent=1)
540
-
584
+
541
585
  # Keep track of patches that failed
542
586
  images_with_patch_errors = {}
543
587
  for patch_info in all_image_patch_info:
544
588
  if patch_info['error'] is not None:
545
589
  images_with_patch_errors[patch_info['image_fn']] = patch_info
546
-
547
-
548
- ##%% Run inference on tiles
549
-
590
+
591
+
592
+ ##%% Run inference on the folder of tiles
593
+
550
594
  # When running with run_inference_with_yolov5_val, we'll pass the folder
551
595
  if yolo_inference_options is not None:
552
-
596
+
553
597
  patch_level_output_file = os.path.join(tiling_folder,folder_name + '_patch_level_results.json')
554
-
598
+
555
599
  if yolo_inference_options.model_filename is None:
556
600
  yolo_inference_options.model_filename = model_file
557
601
  else:
558
602
  assert yolo_inference_options.model_filename == model_file, \
559
603
  'Model file between yolo inference file ({}) and model file parameter ({})'.format(
560
604
  yolo_inference_options.model_filename,model_file)
561
-
605
+
562
606
  yolo_inference_options.input_folder = tiling_folder
563
607
  yolo_inference_options.output_file = patch_level_output_file
564
-
608
+
565
609
  run_inference_with_yolo_val(yolo_inference_options)
566
610
  with open(patch_level_output_file,'r') as f:
567
611
  patch_level_results = json.load(f)
568
-
612
+
569
613
  # For standard inference, we'll pass a list of files
570
614
  else:
571
-
615
+
572
616
  patch_file_names = []
573
617
  for im in all_image_patch_info:
574
- # If there was a patch generation error, don't run inference
618
+ # If there was a patch generation error, don't run inference
575
619
  if patch_info['error'] is not None:
576
620
  assert im['image_fn'] in images_with_patch_errors
577
621
  continue
578
622
  for patch in im['patches']:
579
623
  patch_file_names.append(patch['patch_fn'])
580
-
581
- inference_results = load_and_run_detector_batch(model_file,
582
- patch_file_names,
624
+
625
+ inference_results = load_and_run_detector_batch(model_file,
626
+ patch_file_names,
583
627
  checkpoint_path=checkpoint_path,
584
628
  checkpoint_frequency=checkpoint_frequency,
585
- quiet=True)
586
-
629
+ quiet=True,
630
+ augment=augment,
631
+ detector_options=detector_options,
632
+ use_image_queue=use_image_queue,
633
+ preprocess_on_image_queue=preprocess_on_image_queue,
634
+ image_size=inference_size)
635
+
587
636
  patch_level_output_file = os.path.join(tiling_folder,folder_name + '_patch_level_results.json')
588
-
589
- patch_level_results = write_results_to_file(inference_results,
590
- patch_level_output_file,
591
- relative_path_base=tiling_folder,
637
+
638
+ patch_level_results = write_results_to_file(inference_results,
639
+ patch_level_output_file,
640
+ relative_path_base=tiling_folder,
592
641
  detector_file=model_file)
593
-
594
-
595
- ##%% Map patch-level detections back to the original images
596
-
642
+
643
+ # ...if we are/aren't using run_inference_with_yolov5_val
644
+
645
+ ##%% Map patch-level detections back to the original images
646
+
597
647
  # Map relative paths for patches to detections
598
648
  patch_fn_relative_to_results = {}
599
649
  for im in tqdm(patch_level_results['images']):
@@ -603,36 +653,36 @@ def run_tiled_inference(model_file, image_folder, tiling_folder, output_file,
603
653
  image_level_results['info'] = patch_level_results['info']
604
654
  image_level_results['detection_categories'] = patch_level_results['detection_categories']
605
655
  image_level_results['images'] = []
606
-
656
+
607
657
  image_fn_relative_to_patch_info = { x['image_fn']:x for x in all_image_patch_info }
608
-
658
+
609
659
  # i_image = 0; image_fn_relative = image_files_relative[i_image]
610
660
  for i_image,image_fn_relative in tqdm(enumerate(image_files_relative),
611
661
  total=len(image_files_relative)):
612
-
662
+
613
663
  image_fn_abs = os.path.join(image_folder,image_fn_relative)
614
664
  assert os.path.isfile(image_fn_abs)
615
-
665
+
616
666
  output_im = {}
617
667
  output_im['file'] = image_fn_relative
618
-
668
+
619
669
  # If we had a patch generation error
620
670
  if image_fn_relative in images_with_patch_errors:
621
-
671
+
622
672
  patch_info = image_fn_relative_to_patch_info[image_fn_relative]
623
673
  assert patch_info['error'] is not None
624
-
674
+
625
675
  output_im['detections'] = None
626
676
  output_im['failure'] = 'Patch generation error'
627
677
  output_im['failure_details'] = patch_info['error']
628
678
  image_level_results['images'].append(output_im)
629
679
  continue
630
-
680
+
631
681
  try:
632
- pil_im = vis_utils.open_image(image_fn_abs)
682
+ pil_im = vis_utils.open_image(image_fn_abs)
633
683
  image_w = pil_im.size[0]
634
684
  image_h = pil_im.size[1]
635
-
685
+
636
686
  # This would be a very unusual situation; we're reading back an image here that we already
637
687
  # (successfully) read once during patch generation.
638
688
  except Exception as e:
@@ -642,36 +692,36 @@ def run_tiled_inference(model_file, image_folder, tiling_folder, output_file,
642
692
  output_im['failure'] = 'Patch processing error'
643
693
  output_im['failure_details'] = str(e)
644
694
  image_level_results['images'].append(output_im)
645
- continue
646
-
695
+ continue
696
+
647
697
  output_im['detections'] = []
648
-
698
+
649
699
  image_patch_info = image_fn_relative_to_patch_info[image_fn_relative]
650
700
  assert image_patch_info['patches'][0]['source_fn'] == image_fn_relative
651
-
701
+
652
702
  # Patches for this image
653
703
  patch_fn_abs_to_patch_info_this_image = {}
654
-
704
+
655
705
  for patch_info in image_patch_info['patches']:
656
706
  patch_fn_abs_to_patch_info_this_image[patch_info['patch_fn']] = patch_info
657
-
707
+
658
708
  # For each patch
659
709
  #
660
710
  # i_patch = 0; patch_fn_abs = list(patch_fn_abs_to_patch_info_this_image.keys())[i_patch]
661
711
  for i_patch,patch_fn_abs in enumerate(patch_fn_abs_to_patch_info_this_image.keys()):
662
-
712
+
663
713
  patch_fn_relative = os.path.relpath(patch_fn_abs,tiling_folder)
664
714
  patch_results = patch_fn_relative_to_results[patch_fn_relative]
665
715
  patch_info = patch_fn_abs_to_patch_info_this_image[patch_fn_abs]
666
-
716
+
667
717
  # patch_results['file'] is a relative path, and a subset of patch_info['patch_fn']
668
718
  assert patch_results['file'] in patch_info['patch_fn']
669
-
719
+
670
720
  patch_w = (patch_info['xmax'] - patch_info['xmin']) + 1
671
721
  patch_h = (patch_info['ymax'] - patch_info['ymin']) + 1
672
722
  assert patch_w == patch_size[0]
673
723
  assert patch_h == patch_size[1]
674
-
724
+
675
725
  # If there was an inference failure on one patch, report the image
676
726
  # as an inference failure
677
727
  if 'detections' not in patch_results:
@@ -679,16 +729,16 @@ def run_tiled_inference(model_file, image_folder, tiling_folder, output_file,
679
729
  output_im['detections'] = None
680
730
  output_im['failure'] = patch_results['failure']
681
731
  break
682
-
732
+
683
733
  # det = patch_results['detections'][0]
684
734
  for det in patch_results['detections']:
685
-
735
+
686
736
  bbox_patch_relative = det['bbox']
687
737
  xmin_patch_relative = bbox_patch_relative[0]
688
738
  ymin_patch_relative = bbox_patch_relative[1]
689
739
  w_patch_relative = bbox_patch_relative[2]
690
740
  h_patch_relative = bbox_patch_relative[3]
691
-
741
+
692
742
  # Convert from patch-relative normalized values to image-relative absolute values
693
743
  w_pixels = w_patch_relative * patch_w
694
744
  h_pixels = h_patch_relative * patch_h
@@ -696,78 +746,82 @@ def run_tiled_inference(model_file, image_folder, tiling_folder, output_file,
696
746
  ymin_patch_pixels = ymin_patch_relative * patch_h
697
747
  xmin_image_pixels = patch_info['xmin'] + xmin_patch_pixels
698
748
  ymin_image_pixels = patch_info['ymin'] + ymin_patch_pixels
699
-
749
+
700
750
  # ...and now to image-relative normalized values
701
751
  w_image_normalized = w_pixels / image_w
702
752
  h_image_normalized = h_pixels / image_h
703
753
  xmin_image_normalized = xmin_image_pixels / image_w
704
754
  ymin_image_normalized = ymin_image_pixels / image_h
705
-
755
+
706
756
  bbox_image_normalized = [xmin_image_normalized,
707
757
  ymin_image_normalized,
708
758
  w_image_normalized,
709
759
  h_image_normalized]
710
-
760
+
761
+ bbox_image_normalized = round_float_array(bbox_image_normalized,
762
+ precision=COORD_DIGITS)
763
+ det['conf'] = round_float(det['conf'], precision=CONF_DIGITS)
764
+
711
765
  output_det = {}
712
766
  output_det['bbox'] = bbox_image_normalized
713
767
  output_det['conf'] = det['conf']
714
768
  output_det['category'] = det['category']
715
-
769
+
716
770
  output_im['detections'].append(output_det)
717
-
771
+
718
772
  # ...for each detection
719
-
773
+
720
774
  # ...for each patch
721
775
 
722
776
  image_level_results['images'].append(output_im)
723
-
724
- # ...for each image
777
+
778
+ # ...for each image
725
779
 
726
780
  image_level_results_file_pre_nms = \
727
781
  os.path.join(tiling_folder,folder_name + '_image_level_results_pre_nms.json')
728
782
  with open(image_level_results_file_pre_nms,'w') as f:
729
783
  json.dump(image_level_results,f,indent=1)
730
-
784
+
731
785
 
732
786
  ##%% Run NMS
733
-
787
+
734
788
  in_place_nms(image_level_results,iou_thres=nms_iou_threshold)
735
789
 
736
-
790
+
737
791
  ##%% Write output file
738
-
792
+
739
793
  print('Saving image-level results (after NMS) to {}'.format(output_file))
740
-
794
+
741
795
  with open(output_file,'w') as f:
742
796
  json.dump(image_level_results,f,indent=1)
743
797
 
744
-
798
+
745
799
  ##%% Possibly remove tiles
746
-
800
+
747
801
  if remove_tiles:
748
-
802
+
749
803
  patch_file_names = []
750
804
  for im in all_image_patch_info:
751
805
  for patch in im['patches']:
752
806
  patch_file_names.append(patch['patch_fn'])
753
-
807
+
754
808
  for patch_fn_abs in patch_file_names:
755
809
  os.remove(patch_fn_abs)
756
-
757
-
810
+
811
+
758
812
  ##%% Return
759
-
813
+
760
814
  return image_level_results
761
815
 
762
816
 
763
817
  #%% Interactive driver
764
818
 
765
819
  if False:
766
-
820
+
767
821
  pass
768
822
 
769
823
  #%% Run tiled inference (in Python)
770
-
824
+
771
825
  model_file = os.path.expanduser('~/models/camera_traps/megadetector/md_v5.0.0/md_v5a.0.0.pt')
772
826
  image_folder = os.path.expanduser('~/data/KRU-test')
773
827
  tiling_folder = os.path.expanduser('~/tmp/tiling-test')
@@ -779,47 +833,47 @@ if False:
779
833
  checkpoint_path = None
780
834
  checkpoint_frequency = -1
781
835
  remove_tiles = False
782
-
836
+
783
837
  use_yolo_inference = False
784
-
838
+
785
839
  if not use_yolo_inference:
786
-
840
+
787
841
  yolo_inference_options = None
788
-
842
+
789
843
  else:
790
-
844
+
791
845
  yolo_inference_options = YoloInferenceOptions()
792
846
  yolo_inference_options.yolo_working_folder = os.path.expanduser('~/git/yolov5')
793
-
847
+
794
848
  run_tiled_inference(model_file, image_folder, tiling_folder, output_file,
795
- tile_size_x=tile_size_x, tile_size_y=tile_size_y,
849
+ tile_size_x=tile_size_x, tile_size_y=tile_size_y,
796
850
  tile_overlap=tile_overlap,
797
- checkpoint_path=checkpoint_path,
798
- checkpoint_frequency=checkpoint_frequency,
799
- remove_tiles=remove_tiles,
851
+ checkpoint_path=checkpoint_path,
852
+ checkpoint_frequency=checkpoint_frequency,
853
+ remove_tiles=remove_tiles,
800
854
  yolo_inference_options=yolo_inference_options)
801
-
802
-
855
+
856
+
803
857
  #%% Run tiled inference (generate a command)
804
-
858
+
805
859
  import os
806
-
860
+
807
861
  model_file = os.path.expanduser('~/models/camera_traps/megadetector/md_v5.0.0/md_v5a.0.0.pt')
808
862
  image_folder = os.path.expanduser('~/data/KRU-test')
809
863
  tiling_folder = os.path.expanduser('~/tmp/tiling-test')
810
864
  output_file = os.path.expanduser('~/tmp/KRU-test-tiled.json')
811
865
  tile_size = [5152,3968]
812
866
  tile_overlap = 0.8
813
-
867
+
814
868
  cmd = f'python run_tiled_inference.py {model_file} {image_folder} {tiling_folder} {output_file} ' + \
815
869
  f'--tile_overlap {tile_overlap} --no_remove_tiles --tile_size_x {tile_size[0]} --tile_size_y {tile_size[1]}'
816
-
870
+
817
871
  print(cmd)
818
872
  import clipboard; clipboard.copy(cmd)
819
-
820
-
873
+
874
+
821
875
  #%% Preview tiled inference
822
-
876
+
823
877
  from megadetector.postprocessing.postprocess_batch_results import \
824
878
  PostProcessingOptions, process_batch_results
825
879
 
@@ -848,14 +902,12 @@ if False:
848
902
  html_output_file = ppresults.output_html_file
849
903
 
850
904
  path_utils.open_file(html_output_file)
851
-
852
-
905
+
906
+
853
907
  #%% Command-line driver
854
908
 
855
- import sys,argparse
909
+ def main(): # noqa
856
910
 
857
- def main():
858
-
859
911
  parser = argparse.ArgumentParser(
860
912
  description='Chop a folder of images up into tiles, run MD on the tiles, and stitch the results together')
861
913
  parser.add_argument(
@@ -873,7 +925,7 @@ def main():
873
925
  parser.add_argument(
874
926
  '--no_remove_tiles',
875
927
  action='store_true',
876
- help='Tiles are removed by default; this option suppresses tile deletion')
928
+ help='Tiles are removed by default; this option suppresses tile deletion')
877
929
  parser.add_argument(
878
930
  '--tile_size_x',
879
931
  type=int,
@@ -899,7 +951,14 @@ def main():
899
951
  type=str,
900
952
  default=None,
901
953
  help=('A .json list of relative filenames (or absolute paths contained within image_folder) to include'))
902
-
954
+ parser.add_argument(
955
+ '--detector_options',
956
+ type=str,
957
+ default=None,
958
+ help=('A list of detector options (key-value pairs) to '))
959
+
960
+ # detector_options = parse_kvp_list(args.detector_options)
961
+
903
962
  if len(sys.argv[1:]) == 0:
904
963
  parser.print_help()
905
964
  parser.exit()
@@ -909,7 +968,7 @@ def main():
909
968
  model_file = try_download_known_detector(args.model_file)
910
969
  assert os.path.exists(model_file), \
911
970
  'detector file {} does not exist'.format(args.model_file)
912
-
971
+
913
972
  if os.path.exists(args.output_file):
914
973
  if args.overwrite_handling == 'skip':
915
974
  print('Warning: output file {} exists, skipping'.format(args.output_file))
@@ -920,15 +979,15 @@ def main():
920
979
  raise ValueError('Output file {} exists'.format(args.output_file))
921
980
  else:
922
981
  raise ValueError('Unknown output handling method {}'.format(args.overwrite_handling))
923
-
982
+
924
983
 
925
984
  remove_tiles = (not args.no_remove_tiles)
926
985
 
927
986
  run_tiled_inference(model_file, args.image_folder, args.tiling_folder, args.output_file,
928
- tile_size_x=args.tile_size_x, tile_size_y=args.tile_size_y,
987
+ tile_size_x=args.tile_size_x, tile_size_y=args.tile_size_y,
929
988
  tile_overlap=args.tile_overlap,
930
989
  remove_tiles=remove_tiles,
931
990
  image_list=args.image_list)
932
-
991
+
933
992
  if __name__ == '__main__':
934
993
  main()