megadetector 5.0.28__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 +231 -224
  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 +340 -337
  65. megadetector/detection/pytorch_detector.py +304 -262
  66. megadetector/detection/run_detector.py +177 -164
  67. megadetector/detection/run_detector_batch.py +364 -363
  68. megadetector/detection/run_inference_with_yolov5_val.py +328 -325
  69. megadetector/detection/run_tiled_inference.py +256 -249
  70. megadetector/detection/tf_detector.py +24 -24
  71. megadetector/detection/video_utils.py +290 -282
  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 +415 -415
  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 +219 -146
  79. megadetector/postprocessing/detector_calibration.py +173 -168
  80. megadetector/postprocessing/generate_csv_report.py +508 -499
  81. megadetector/postprocessing/load_api_results.py +23 -20
  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 +313 -298
  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 -66
  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 +10 -10
  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 +1018 -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 +1457 -398
  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 +61 -61
  115. megadetector/utils/string_utils.py +147 -26
  116. megadetector/utils/url_utils.py +463 -173
  117. megadetector/utils/wi_utils.py +2629 -2526
  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 +401 -397
  122. megadetector/visualization/visualize_db.py +197 -190
  123. megadetector/visualization/visualize_detector_output.py +79 -73
  124. {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/METADATA +135 -132
  125. megadetector-5.0.29.dist-info/RECORD +163 -0
  126. {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/WHEEL +1 -1
  127. {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/licenses/LICENSE +0 -0
  128. {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/top_level.txt +0 -0
  129. megadetector/data_management/importers/add_nacti_sizes.py +0 -52
  130. megadetector/data_management/importers/add_timestamps_to_icct.py +0 -79
  131. megadetector/data_management/importers/animl_results_to_md_results.py +0 -158
  132. megadetector/data_management/importers/auckland_doc_test_to_json.py +0 -373
  133. megadetector/data_management/importers/auckland_doc_to_json.py +0 -201
  134. megadetector/data_management/importers/awc_to_json.py +0 -191
  135. megadetector/data_management/importers/bellevue_to_json.py +0 -272
  136. megadetector/data_management/importers/cacophony-thermal-importer.py +0 -793
  137. megadetector/data_management/importers/carrizo_shrubfree_2018.py +0 -269
  138. megadetector/data_management/importers/carrizo_trail_cam_2017.py +0 -289
  139. megadetector/data_management/importers/cct_field_adjustments.py +0 -58
  140. megadetector/data_management/importers/channel_islands_to_cct.py +0 -913
  141. megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +0 -180
  142. megadetector/data_management/importers/eMammal/eMammal_helpers.py +0 -249
  143. megadetector/data_management/importers/eMammal/make_eMammal_json.py +0 -223
  144. megadetector/data_management/importers/ena24_to_json.py +0 -276
  145. megadetector/data_management/importers/filenames_to_json.py +0 -386
  146. megadetector/data_management/importers/helena_to_cct.py +0 -283
  147. megadetector/data_management/importers/idaho-camera-traps.py +0 -1407
  148. megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +0 -294
  149. megadetector/data_management/importers/import_desert_lion_conservation_camera_traps.py +0 -387
  150. megadetector/data_management/importers/jb_csv_to_json.py +0 -150
  151. megadetector/data_management/importers/mcgill_to_json.py +0 -250
  152. megadetector/data_management/importers/missouri_to_json.py +0 -490
  153. megadetector/data_management/importers/nacti_fieldname_adjustments.py +0 -79
  154. megadetector/data_management/importers/noaa_seals_2019.py +0 -181
  155. megadetector/data_management/importers/osu-small-animals-to-json.py +0 -364
  156. megadetector/data_management/importers/pc_to_json.py +0 -365
  157. megadetector/data_management/importers/plot_wni_giraffes.py +0 -123
  158. megadetector/data_management/importers/prepare_zsl_imerit.py +0 -131
  159. megadetector/data_management/importers/raic_csv_to_md_results.py +0 -416
  160. megadetector/data_management/importers/rspb_to_json.py +0 -356
  161. megadetector/data_management/importers/save_the_elephants_survey_A.py +0 -320
  162. megadetector/data_management/importers/save_the_elephants_survey_B.py +0 -329
  163. megadetector/data_management/importers/snapshot_safari_importer.py +0 -758
  164. megadetector/data_management/importers/snapshot_serengeti_lila.py +0 -1067
  165. megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +0 -150
  166. megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +0 -153
  167. megadetector/data_management/importers/sulross_get_exif.py +0 -65
  168. megadetector/data_management/importers/timelapse_csv_set_to_json.py +0 -490
  169. megadetector/data_management/importers/ubc_to_json.py +0 -399
  170. megadetector/data_management/importers/umn_to_json.py +0 -507
  171. megadetector/data_management/importers/wellington_to_json.py +0 -263
  172. megadetector/data_management/importers/wi_to_json.py +0 -442
  173. megadetector/data_management/importers/zamba_results_to_md_results.py +0 -180
  174. megadetector/data_management/lila/add_locations_to_island_camera_traps.py +0 -101
  175. megadetector/data_management/lila/add_locations_to_nacti.py +0 -151
  176. megadetector-5.0.28.dist-info/RECORD +0 -209
@@ -19,6 +19,7 @@ from functools import partial
19
19
 
20
20
  from megadetector.utils.path_utils import insert_before_extension
21
21
  from megadetector.utils.ct_utils import invert_dictionary
22
+ from megadetector.utils.ct_utils import is_list_sorted
22
23
  from megadetector.visualization.visualization_utils import crop_image
23
24
  from megadetector.visualization.visualization_utils import exif_preserving_save
24
25
 
@@ -29,24 +30,24 @@ class CreateCropFolderOptions:
29
30
  """
30
31
  Options used to parameterize create_crop_folder().
31
32
  """
32
-
33
+
33
34
  def __init__(self):
34
-
35
+
35
36
  #: Confidence threshold determining which detections get written
36
37
  self.confidence_threshold = 0.1
37
-
38
+
38
39
  #: Number of pixels to expand each crop
39
40
  self.expansion = 0
40
-
41
+
41
42
  #: JPEG quality to use for saving crops (None for default)
42
43
  self.quality = 95
43
-
44
+
44
45
  #: Whether to overwrite existing images
45
46
  self.overwrite = True
46
-
47
+
47
48
  #: Number of concurrent workers
48
49
  self.n_workers = 8
49
-
50
+
50
51
  #: Whether to use processes ('process') or threads ('thread') for parallelization
51
52
  self.pool_type = 'thread'
52
53
 
@@ -54,8 +55,8 @@ class CreateCropFolderOptions:
54
55
  #:
55
56
  #: options.category_names_to_include = ['animal']
56
57
  self.category_names_to_include = None
57
-
58
-
58
+
59
+
59
60
  #%% Support functions
60
61
 
61
62
  def _get_crop_filename(image_fn,crop_id):
@@ -77,34 +78,34 @@ def _generate_crops_for_single_image(crops_this_image,
77
78
  """
78
79
  if len(crops_this_image) == 0:
79
80
  return
80
-
81
- image_fn_relative = crops_this_image[0]['image_fn_relative']
81
+
82
+ image_fn_relative = crops_this_image[0]['image_fn_relative']
82
83
  input_fn_abs = os.path.join(input_folder,image_fn_relative)
83
84
  assert os.path.isfile(input_fn_abs)
84
-
85
+
85
86
  detections_to_crop = [c['detection'] for c in crops_this_image]
86
-
87
+
87
88
  cropped_images = crop_image(detections_to_crop,
88
89
  input_fn_abs,
89
90
  confidence_threshold=0,
90
91
  expansion=options.expansion)
91
-
92
+
92
93
  assert len(cropped_images) == len(crops_this_image)
93
-
94
+
94
95
  # i_crop = 0; crop_info = crops_this_image[0]
95
96
  for i_crop,crop_info in enumerate(crops_this_image):
96
-
97
+
97
98
  assert crop_info['image_fn_relative'] == image_fn_relative
98
- crop_filename_relative = _get_crop_filename(image_fn_relative, crop_info['crop_id'])
99
+ crop_filename_relative = _get_crop_filename(image_fn_relative, crop_info['crop_id'])
99
100
  crop_filename_abs = os.path.join(output_folder,crop_filename_relative).replace('\\','/')
100
-
101
+
101
102
  if os.path.isfile(crop_filename_abs) and not options.overwrite:
102
103
  continue
103
-
104
- cropped_image = cropped_images[i_crop]
105
- os.makedirs(os.path.dirname(crop_filename_abs),exist_ok=True)
104
+
105
+ cropped_image = cropped_images[i_crop]
106
+ os.makedirs(os.path.dirname(crop_filename_abs),exist_ok=True)
106
107
  exif_preserving_save(cropped_image,crop_filename_abs,quality=options.quality)
107
-
108
+
108
109
  # ...for each crop
109
110
 
110
111
 
@@ -113,119 +114,185 @@ def _generate_crops_for_single_image(crops_this_image,
113
114
  def crop_results_to_image_results(image_results_file_with_crop_ids,
114
115
  crop_results_file,
115
116
  output_file,
116
- delete_crop_information=True):
117
+ delete_crop_information=True,
118
+ require_identical_detection_categories=True,
119
+ restrict_to_top_n=-1,
120
+ crop_results_prefix=None,
121
+ detections_without_classification_handling='error'):
117
122
  """
118
123
  This function is intended to be run after you have:
119
-
124
+
120
125
  1. Run MegaDetector on a folder
121
126
  2. Generated a crop folder using create_crop_folder
122
127
  3. Run a species classifier on those crops
123
-
128
+
124
129
  This function will take the crop-level results and transform them back
125
- to the original images. Classification categories, if available, are taken
130
+ to the original images. Classification categories, if available, are taken
126
131
  from [crop_results_file].
127
-
132
+
128
133
  Args:
129
134
  image_results_file_with_crop_ids (str): results file for the original images,
130
- containing crop IDs, likely generated via create_crop_folder. All
135
+ containing crop IDs, likely generated via create_crop_folder. All
131
136
  non-standard fields in this file will be passed along to [output_file].
132
137
  crop_results_file (str): results file for the crop folder
133
138
  output_file (str): output .json file, containing crop-level classifications
134
139
  mapped back to the image level.
135
140
  delete_crop_information (bool, optional): whether to delete the "crop_id" and
136
141
  "crop_filename_relative" fields from each detection, if present.
142
+ require_identical_detection_categories (bool, optional): if True, error if
143
+ the image-level and crop-level detection categories are different. If False,
144
+ ignore the crop-level detection categories.
145
+ restrict_to_top_n (int, optional): If >0, removes all but the top N classification
146
+ results for each detection.
147
+ crop_results_prefix (str, optional): if not None, removes this prefix from crop
148
+ results filenames. Intended to support the case where the crop results
149
+ use absolute paths.
150
+ detections_without_classification_handling (str, optional): what to do when we
151
+ encounter a crop that doesn't appear in classification results: 'error',
152
+ or 'include' ("include" means "leave the detection alone, without classifications"
137
153
  """
138
-
154
+
139
155
  ##%% Validate inputs
140
-
156
+
141
157
  assert os.path.isfile(image_results_file_with_crop_ids), \
142
158
  'Could not find image-level input file {}'.format(image_results_file_with_crop_ids)
143
159
  assert os.path.isfile(crop_results_file), \
144
160
  'Could not find crop results file {}'.format(crop_results_file)
145
161
  os.makedirs(os.path.dirname(output_file),exist_ok=True)
146
-
147
-
162
+
163
+
148
164
  ##%% Read input files
149
-
165
+
150
166
  print('Reading input...')
151
-
167
+
152
168
  with open(image_results_file_with_crop_ids,'r') as f:
153
169
  image_results_with_crop_ids = json.load(f)
154
170
  with open(crop_results_file,'r') as f:
155
171
  crop_results = json.load(f)
156
172
 
157
173
  # Find all the detection categories that need to be consistent
158
- used_category_ids = set()
174
+ used_detection_category_ids = set()
159
175
  for im in tqdm(image_results_with_crop_ids['images']):
160
176
  if 'detections' not in im or im['detections'] is None:
161
- continue
177
+ continue
162
178
  for det in im['detections']:
163
179
  if 'crop_id' in det:
164
- used_category_ids.add(det['category'])
165
-
166
- # Make sure the categories that matter are consistent across the two files
167
- for category_id in used_category_ids:
168
- category_name = image_results_with_crop_ids['detection_categories'][category_id]
169
- assert category_id in crop_results['detection_categories'] and \
170
- category_name == crop_results['detection_categories'][category_id], \
171
- 'Crop results and detection results use incompatible categories'
172
-
180
+ used_detection_category_ids.add(det['category'])
181
+
182
+ # Make sure the detection categories that matter are consistent across the two files
183
+ if require_identical_detection_categories:
184
+ for category_id in used_detection_category_ids:
185
+ category_name = image_results_with_crop_ids['detection_categories'][category_id]
186
+ assert category_id in crop_results['detection_categories'] and \
187
+ category_name == crop_results['detection_categories'][category_id], \
188
+ 'Crop results and detection results use incompatible categories'
189
+
173
190
  crop_filename_to_results = {}
174
-
191
+
175
192
  # im = crop_results['images'][0]
176
- for im in crop_results['images']:
177
- crop_filename_to_results[im['file']] = im
178
-
193
+ for im in crop_results['images']:
194
+ fn = im['file']
195
+ # Possibly remove a prefix from each filename
196
+ if (crop_results_prefix is not None) and (crop_results_prefix in fn):
197
+ if fn.startswith(crop_results_prefix):
198
+ fn = fn.replace(crop_results_prefix,'',1)
199
+ im['file'] = fn
200
+ crop_filename_to_results[fn] = im
201
+
179
202
  if 'classification_categories' in crop_results:
180
203
  image_results_with_crop_ids['classification_categories'] = \
181
204
  crop_results['classification_categories']
182
-
205
+
183
206
  if 'classification_category_descriptions' in crop_results:
184
207
  image_results_with_crop_ids['classification_category_descriptions'] = \
185
208
  crop_results['classification_category_descriptions']
186
-
187
-
209
+
210
+
188
211
  ##%% Read classifications from crop results, merge into image-level results
189
-
212
+
213
+ print('Reading classification results...')
214
+
215
+ n_skipped_detections = 0
216
+
217
+ # Loop over the original image-level detections
218
+ #
190
219
  # im = image_results_with_crop_ids['images'][0]
191
- for im in tqdm(image_results_with_crop_ids['images']):
192
-
220
+ for i_image,im in tqdm(enumerate(image_results_with_crop_ids['images']),
221
+ total=len(image_results_with_crop_ids['images'])):
222
+
193
223
  if 'detections' not in im or im['detections'] is None:
194
224
  continue
195
-
225
+
226
+ # i_det = 0; det = im['detections'][i_det]
196
227
  for det in im['detections']:
197
-
228
+
198
229
  if 'classifications' in det:
199
230
  del det['classifications']
200
-
231
+
201
232
  if 'crop_id' in det:
233
+
234
+ # We may be skipping detections with no classification results
235
+ skip_detection = False
236
+
237
+ # Find the corresponding crop in the classification results
202
238
  crop_filename_relative = det['crop_filename_relative']
203
- assert crop_filename_relative in crop_filename_to_results, \
204
- 'Crop lookup error'
205
- crop_results_this_detection = crop_filename_to_results[crop_filename_relative]
206
- assert crop_results_this_detection['file'] == crop_filename_relative
207
- assert len(crop_results_this_detection['detections']) == 1
208
- # Allow a slight confidence difference for the case where output precision was truncated
209
- assert abs(crop_results_this_detection['detections'][0]['conf'] - det['conf']) < 0.01
210
- assert crop_results_this_detection['detections'][0]['category'] == det['category']
211
- assert crop_results_this_detection['detections'][0]['bbox'] == [0,0,1,1]
212
- det['classifications'] = crop_results_this_detection['detections'][0]['classifications']
213
-
239
+ if crop_filename_relative not in crop_filename_to_results:
240
+ if detections_without_classification_handling == 'error':
241
+ raise ValueError('Crop lookup error: {}'.format(crop_filename_relative))
242
+ elif detections_without_classification_handling == 'include':
243
+ # Leave this detection unclassified
244
+ skip_detection = True
245
+ else:
246
+ raise ValueError(
247
+ 'Illegal value for detections_without_classification_handling: {}'.format(
248
+ detections_without_classification_handling
249
+ ))
250
+
251
+ if not skip_detection:
252
+
253
+ crop_results_this_detection = crop_filename_to_results[crop_filename_relative]
254
+
255
+ # Consistency checking
256
+ assert crop_results_this_detection['file'] == crop_filename_relative, \
257
+ 'Crop filename mismatch'
258
+ assert len(crop_results_this_detection['detections']) == 1, \
259
+ 'Multiple crop results for a single detection'
260
+ assert crop_results_this_detection['detections'][0]['bbox'] == [0,0,1,1], \
261
+ 'Invalid crop bounding box'
262
+
263
+ # This check was helpful for the case where crop-level results had already
264
+ # taken detection confidence values from detector output by construct, but this isn't
265
+ # really meaningful for most cases.
266
+ # assert abs(crop_results_this_detection['detections'][0]['conf'] - det['conf']) < 0.01
267
+
268
+ if require_identical_detection_categories:
269
+ assert crop_results_this_detection['detections'][0]['category'] == det['category']
270
+
271
+ # Copy the crop-level classifications
272
+ det['classifications'] = crop_results_this_detection['detections'][0]['classifications']
273
+ confidence_values = [x[1] for x in det['classifications']]
274
+ assert is_list_sorted(confidence_values,reverse=True)
275
+ if restrict_to_top_n > 0:
276
+ det['classifications'] = det['classifications'][0:restrict_to_top_n]
277
+
214
278
  if delete_crop_information:
215
279
  if 'crop_id' in det:
216
280
  del det['crop_id']
217
281
  if 'crop_filename_relative' in det:
218
282
  del det['crop_filename_relative']
219
-
283
+
220
284
  # ...for each detection
221
-
222
- # ...for each image
223
-
224
-
285
+
286
+ # ...for each image
287
+
288
+ if n_skipped_detections > 0:
289
+ print('Skipped {} detections'.format(n_skipped_detections))
290
+
291
+
225
292
  ##%% Write output file
226
-
293
+
227
294
  print('Writing output file...')
228
-
295
+
229
296
  with open(output_file,'w') as f:
230
297
  json.dump(image_results_with_crop_ids,f,indent=1)
231
298
 
@@ -241,9 +308,9 @@ def create_crop_folder(input_file,
241
308
  """
242
309
  Given a MegaDetector .json file and a folder of images, creates a new folder
243
310
  of images representing all above-threshold crops from the original folder.
244
-
311
+
245
312
  Optionally writes a new .json file that attaches unique IDs to each detection.
246
-
313
+
247
314
  Args:
248
315
  input_file (str): MD-formatted .json file to process
249
316
  input_folder (str): Input image folder
@@ -251,11 +318,11 @@ def create_crop_folder(input_file,
251
318
  output_file (str, optional): new .json file that attaches unique IDs to each detection.
252
319
  crops_output_file (str, optional): new .json file that includes whole-image detections
253
320
  for each of the crops, using confidence values from the original results
254
- options (CreateCropFolderOptions, optional): crop parameters
321
+ options (CreateCropFolderOptions, optional): crop parameters
255
322
  """
256
-
323
+
257
324
  ## Validate options, prepare output folders
258
-
325
+
259
326
  if options is None:
260
327
  options = CreateCropFolderOptions()
261
328
 
@@ -264,45 +331,45 @@ def create_crop_folder(input_file,
264
331
  os.makedirs(output_folder,exist_ok=True)
265
332
  if output_file is not None:
266
333
  os.makedirs(os.path.dirname(output_file),exist_ok=True)
267
-
268
-
334
+
335
+
269
336
  ##%% Read input
270
-
271
- print('Reading MD results file...')
337
+
338
+ print('Reading MD results file...')
272
339
  with open(input_file,'r') as f:
273
340
  detection_results = json.load(f)
274
-
341
+
275
342
  category_ids_to_include = None
276
-
277
- if options.category_names_to_include is not None:
343
+
344
+ if options.category_names_to_include is not None:
278
345
  category_id_to_name = detection_results['detection_categories']
279
- category_name_to_id = invert_dictionary(category_id_to_name)
346
+ category_name_to_id = invert_dictionary(category_id_to_name)
280
347
  category_ids_to_include = set()
281
348
  for category_name in options.category_names_to_include:
282
349
  assert category_name in category_name_to_id, \
283
350
  'Unrecognized category name {}'.format(category_name)
284
- category_ids_to_include.add(category_name_to_id[category_name])
351
+ category_ids_to_include.add(category_name_to_id[category_name])
285
352
 
286
353
  ##%% Make a list of crops that we need to create
287
-
354
+
288
355
  # Maps input images to list of dicts, with keys 'crop_id','detection'
289
356
  image_fn_relative_to_crops = defaultdict(list)
290
357
  n_crops = 0
291
-
358
+
292
359
  n_detections_excluded_by_category = 0
293
360
 
294
361
  # im = detection_results['images'][0]
295
362
  for i_image,im in enumerate(detection_results['images']):
296
-
363
+
297
364
  if 'detections' not in im or im['detections'] is None or len(im['detections']) == 0:
298
365
  continue
299
-
366
+
300
367
  detections_this_image = im['detections']
301
-
368
+
302
369
  image_fn_relative = im['file']
303
-
370
+
304
371
  for i_detection,det in enumerate(detections_this_image):
305
-
372
+
306
373
  if det['conf'] < options.confidence_threshold:
307
374
  continue
308
375
 
@@ -312,87 +379,93 @@ def create_crop_folder(input_file,
312
379
  continue
313
380
 
314
381
  det['crop_id'] = i_detection
315
-
382
+
316
383
  crop_info = {'image_fn_relative':image_fn_relative,
317
384
  'crop_id':i_detection,
318
385
  'detection':det}
319
-
320
- crop_filename_relative = _get_crop_filename(image_fn_relative,
386
+
387
+ crop_filename_relative = _get_crop_filename(image_fn_relative,
321
388
  crop_info['crop_id'])
322
389
  det['crop_filename_relative'] = crop_filename_relative
323
390
 
324
391
  image_fn_relative_to_crops[image_fn_relative].append(crop_info)
325
392
  n_crops += 1
326
-
327
- # ...for each input image
393
+
394
+ # ...for each input image
328
395
 
329
396
  print('Prepared a list of {} crops from {} of {} input images'.format(
330
397
  n_crops,len(image_fn_relative_to_crops),len(detection_results['images'])))
331
-
398
+
332
399
  if n_detections_excluded_by_category > 0:
333
400
  print('Excluded {} detections by category'.format(n_detections_excluded_by_category))
334
-
401
+
335
402
  ##%% Generate crops
336
-
403
+
337
404
  if options.n_workers <= 1:
338
-
405
+
339
406
  # image_fn_relative = next(iter(image_fn_relative_to_crops))
340
407
  for image_fn_relative in tqdm(image_fn_relative_to_crops.keys()):
341
- crops_this_image = image_fn_relative_to_crops[image_fn_relative]
408
+ crops_this_image = image_fn_relative_to_crops[image_fn_relative]
342
409
  _generate_crops_for_single_image(crops_this_image=crops_this_image,
343
410
  input_folder=input_folder,
344
411
  output_folder=output_folder,
345
412
  options=options)
346
-
413
+
347
414
  else:
348
-
415
+
349
416
  print('Creating a {} pool with {} workers'.format(options.pool_type,options.n_workers))
417
+ pool = None
418
+ try:
419
+ if options.pool_type == 'thread':
420
+ pool = ThreadPool(options.n_workers)
421
+ else:
422
+ assert options.pool_type == 'process'
423
+ pool = Pool(options.n_workers)
424
+
425
+ # Each element in this list is the list of crops for a single image
426
+ crop_lists = list(image_fn_relative_to_crops.values())
427
+
428
+ with tqdm(total=len(image_fn_relative_to_crops)) as pbar:
429
+ for i,_ in enumerate(pool.imap_unordered(partial(
430
+ _generate_crops_for_single_image,
431
+ input_folder=input_folder,
432
+ output_folder=output_folder,
433
+ options=options),
434
+ crop_lists)):
435
+ pbar.update()
436
+ finally:
437
+ if pool is not None:
438
+ pool.close()
439
+ pool.join()
440
+ print("Pool closed and joined for crop folder creation")
441
+
442
+ # ...if we're using parallel processing
443
+
350
444
 
351
- if options.pool_type == 'thread':
352
- pool = ThreadPool(options.n_workers)
353
- else:
354
- assert options.pool_type == 'process'
355
- pool = Pool(options.n_workers)
356
-
357
- # Each element in this list is the list of crops for a single image
358
- crop_lists = list(image_fn_relative_to_crops.values())
359
-
360
- with tqdm(total=len(image_fn_relative_to_crops)) as pbar:
361
- for i,_ in enumerate(pool.imap_unordered(partial(
362
- _generate_crops_for_single_image,
363
- input_folder=input_folder,
364
- output_folder=output_folder,
365
- options=options),
366
- crop_lists)):
367
- pbar.update()
368
-
369
- # ...if we're using parallel processing
370
-
371
-
372
445
  ##%% Write output file
373
-
446
+
374
447
  if output_file is not None:
375
448
  with open(output_file,'w') as f:
376
449
  json.dump(detection_results,f,indent=1)
377
-
450
+
378
451
  if crops_output_file is not None:
379
-
452
+
380
453
  original_images = detection_results['images']
381
-
454
+
382
455
  detection_results_cropped = detection_results
383
456
  detection_results_cropped['images'] = []
384
-
457
+
385
458
  # im = original_images[0]
386
459
  for im in original_images:
387
-
460
+
388
461
  if 'detections' not in im or im['detections'] is None or len(im['detections']) == 0:
389
462
  continue
390
-
391
- detections_this_image = im['detections']
463
+
464
+ detections_this_image = im['detections']
392
465
  image_fn_relative = im['file']
393
-
466
+
394
467
  for i_detection,det in enumerate(detections_this_image):
395
-
468
+
396
469
  if 'crop_id' in det:
397
470
  im_out = {}
398
471
  im_out['file'] = det['crop_filename_relative']
@@ -402,19 +475,19 @@ def create_crop_folder(input_file,
402
475
  det_out['bbox'] = [0, 0, 1, 1]
403
476
  im_out['detections'] = [det_out]
404
477
  detection_results_cropped['images'].append(im_out)
405
-
478
+
406
479
  # ...if we need to include this crop in the new .json file
407
-
480
+
408
481
  # ...for each crop
409
-
482
+
410
483
  # ...for each original image
411
-
484
+
412
485
  with open(crops_output_file,'w') as f:
413
486
  json.dump(detection_results_cropped,f,indent=1)
414
-
487
+
415
488
  # ...def create_crop_folder()
416
489
 
417
490
 
418
491
  #%% Command-line driver
419
492
 
420
- # TODO
493
+ # TODO