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
@@ -96,7 +96,7 @@ DEFAULT_COLORS = [
96
96
  def open_image(input_file, ignore_exif_rotation=False):
97
97
  """
98
98
  Opens an image in binary format using PIL.Image and converts to RGB mode.
99
-
99
+
100
100
  Supports local files or URLs.
101
101
 
102
102
  This operation is lazy; image will not be actually loaded until the first
@@ -112,7 +112,7 @@ def open_image(input_file, ignore_exif_rotation=False):
112
112
  Returns:
113
113
  PIL.Image.Image: A PIL Image object in RGB mode
114
114
  """
115
-
115
+
116
116
  if (isinstance(input_file, str)
117
117
  and input_file.startswith(('http://', 'https://'))):
118
118
  try:
@@ -124,7 +124,7 @@ def open_image(input_file, ignore_exif_rotation=False):
124
124
  for i_retry in range(0,n_retries):
125
125
  try:
126
126
  time.sleep(retry_sleep_time)
127
- response = requests.get(input_file)
127
+ response = requests.get(input_file)
128
128
  except Exception as e:
129
129
  print(f'Error retrieving image {input_file} on retry {i_retry}: {e}')
130
130
  continue
@@ -141,7 +141,7 @@ def open_image(input_file, ignore_exif_rotation=False):
141
141
 
142
142
  else:
143
143
  image = Image.open(input_file)
144
-
144
+
145
145
  # Convert to RGB if necessary
146
146
  if image.mode not in ('RGBA', 'RGB', 'L', 'I;16'):
147
147
  raise AttributeError(
@@ -158,11 +158,11 @@ def open_image(input_file, ignore_exif_rotation=False):
158
158
  #
159
159
  try:
160
160
  exif = image._getexif()
161
- orientation: int = exif.get(274, None)
161
+ orientation: int = exif.get(274, None)
162
162
  if (orientation is not None) and (orientation != EXIF_IMAGE_NO_ROTATION):
163
163
  assert orientation in EXIF_IMAGE_ROTATIONS, \
164
164
  'Mirrored rotations are not supported'
165
- image = image.rotate(EXIF_IMAGE_ROTATIONS[orientation], expand=True)
165
+ image = image.rotate(EXIF_IMAGE_ROTATIONS[orientation], expand=True)
166
166
  except Exception:
167
167
  pass
168
168
 
@@ -175,73 +175,73 @@ def exif_preserving_save(pil_image,output_file,quality='keep',default_quality=85
175
175
  """
176
176
  Saves [pil_image] to [output_file], making a moderate attempt to preserve EXIF
177
177
  data and JPEG quality. Neither is guaranteed.
178
-
178
+
179
179
  Also see:
180
-
180
+
181
181
  https://discuss.dizzycoding.com/determining-jpg-quality-in-python-pil/
182
-
182
+
183
183
  ...for more ways to preserve jpeg quality if quality='keep' doesn't do the trick.
184
184
 
185
185
  Args:
186
- pil_image (Image): the PIL Image objct to save
186
+ pil_image (Image): the PIL Image object to save
187
187
  output_file (str): the destination file
188
- quality (str or int, optional): can be "keep" (default), or an integer from 0 to 100.
188
+ quality (str or int, optional): can be "keep" (default), or an integer from 0 to 100.
189
189
  This is only used if PIL thinks the the source image is a JPEG. If you load a JPEG
190
190
  and resize it in memory, for example, it's no longer a JPEG.
191
- default_quality (int, optional): determines output quality when quality == 'keep' and we are
191
+ default_quality (int, optional): determines output quality when quality == 'keep' and we are
192
192
  saving a non-JPEG source to a JPEG file
193
193
  verbose (bool, optional): enable additional debug console output
194
194
  """
195
-
195
+
196
196
  # Read EXIF metadata
197
197
  exif = pil_image.info['exif'] if ('exif' in pil_image.info) else None
198
-
198
+
199
199
  # Quality preservation is only supported for JPEG sources.
200
200
  if pil_image.format != "JPEG":
201
201
  if quality == 'keep':
202
202
  if verbose:
203
203
  print('Warning: quality "keep" passed when saving a non-JPEG source (during save to {})'.format(
204
204
  output_file))
205
- quality = default_quality
206
-
207
- # Some output formats don't support the quality parameter, so we try once with,
205
+ quality = default_quality
206
+
207
+ # Some output formats don't support the quality parameter, so we try once with,
208
208
  # and once without. This is a horrible cascade of if's, but it's a consequence of
209
209
  # the fact that "None" is not supported for either "exif" or "quality".
210
-
210
+
211
211
  try:
212
-
212
+
213
213
  if exif is not None:
214
214
  pil_image.save(output_file, exif=exif, quality=quality)
215
215
  else:
216
216
  pil_image.save(output_file, quality=quality)
217
-
217
+
218
218
  except Exception:
219
-
219
+
220
220
  if verbose:
221
221
  print('Warning: failed to write {}, trying again without quality parameter'.format(output_file))
222
222
  if exif is not None:
223
- pil_image.save(output_file, exif=exif)
223
+ pil_image.save(output_file, exif=exif)
224
224
  else:
225
225
  pil_image.save(output_file)
226
-
226
+
227
227
  # ...def exif_preserving_save(...)
228
228
 
229
229
 
230
230
  def load_image(input_file, ignore_exif_rotation=False):
231
231
  """
232
- Loads an image file. This is the non-lazy version of open_file(); i.e.,
232
+ Loads an image file. This is the non-lazy version of open_file(); i.e.,
233
233
  it forces image decoding before returning.
234
-
234
+
235
235
  Args:
236
236
  input_file (str or BytesIO): can be a path to an image file (anything
237
237
  that PIL can open), a URL, or an image as a stream of bytes
238
238
  ignore_exif_rotation (bool, optional): don't rotate the loaded pixels,
239
239
  even if we are loading a JPEG and that JPEG says it should be rotated
240
240
 
241
- Returns:
241
+ Returns:
242
242
  PIL.Image.Image: a PIL Image object in RGB mode
243
243
  """
244
-
244
+
245
245
  image = open_image(input_file, ignore_exif_rotation=ignore_exif_rotation)
246
246
  image.load()
247
247
  return image
@@ -252,13 +252,13 @@ def resize_image(image, target_width=-1, target_height=-1, output_file=None,
252
252
  """
253
253
  Resizes a PIL Image object to the specified width and height; does not resize
254
254
  in place. If either width or height are -1, resizes with aspect ratio preservation.
255
-
256
- If target_width and target_height are both -1, does not modify the image, but
255
+
256
+ If target_width and target_height are both -1, does not modify the image, but
257
257
  will write to output_file if supplied.
258
-
259
- If no resizing is required, and an Image object is supplied, returns the original Image
258
+
259
+ If no resizing is required, and an Image object is supplied, returns the original Image
260
260
  object (i.e., does not copy).
261
-
261
+
262
262
  Args:
263
263
  image (Image or str): PIL Image object or a filename (local file or URL)
264
264
  target_width (int, optional): width to which we should resize this image, or -1
@@ -267,14 +267,14 @@ def resize_image(image, target_width=-1, target_height=-1, output_file=None,
267
267
  to let target_width determine the size
268
268
  output_file (str, optional): file to which we should save this image; if None,
269
269
  just returns the image without saving
270
- no_enlarge_width (bool, optional): if [no_enlarge_width] is True, and
271
- [target width] is larger than the original image width, does not modify the image,
270
+ no_enlarge_width (bool, optional): if [no_enlarge_width] is True, and
271
+ [target width] is larger than the original image width, does not modify the image,
272
272
  but will write to output_file if supplied
273
273
  verbose (bool, optional): enable additional debug output
274
274
  quality (str or int, optional): passed to exif_preserving_save, see docs for more detail
275
-
276
- returns:
277
- PIL.Image.Image: the resized image, which may be the original image if no resizing is
275
+
276
+ Returns:
277
+ PIL.Image.Image: the resized image, which may be the original image if no resizing is
278
278
  required
279
279
  """
280
280
 
@@ -282,20 +282,20 @@ def resize_image(image, target_width=-1, target_height=-1, output_file=None,
282
282
  if isinstance(image,str):
283
283
  image_fn = image
284
284
  image = load_image(image)
285
-
285
+
286
286
  if target_width is None:
287
287
  target_width = -1
288
-
288
+
289
289
  if target_height is None:
290
290
  target_height = -1
291
-
291
+
292
292
  resize_required = True
293
-
293
+
294
294
  # No resize was requested, this is always a no-op
295
295
  if target_width == -1 and target_height == -1:
296
-
296
+
297
297
  resize_required = False
298
-
298
+
299
299
  # Does either dimension need to scale according to the other?
300
300
  elif target_width == -1 or target_height == -1:
301
301
 
@@ -309,42 +309,42 @@ def resize_image(image, target_width=-1, target_height=-1, output_file=None,
309
309
  else:
310
310
  # w = ar * h
311
311
  target_width = int(aspect_ratio * target_height)
312
-
312
+
313
313
  # If we're not enlarging images and this would be an enlarge operation
314
314
  if (no_enlarge_width) and (target_width > image.size[0]):
315
-
315
+
316
316
  if verbose:
317
317
  print('Bypassing image enlarge for {} --> {}'.format(
318
318
  image_fn,str(output_file)))
319
319
  resize_required = False
320
-
320
+
321
321
  # If the target size is the same as the original size
322
322
  if (target_width == image.size[0]) and (target_height == image.size[1]):
323
-
324
- resize_required = False
325
-
323
+
324
+ resize_required = False
325
+
326
326
  if not resize_required:
327
-
327
+
328
328
  if output_file is not None:
329
329
  if verbose:
330
330
  print('No resize required for resize {} --> {}'.format(
331
331
  image_fn,str(output_file)))
332
332
  exif_preserving_save(image,output_file,quality=quality,verbose=verbose)
333
333
  return image
334
-
334
+
335
335
  assert target_width > 0 and target_height > 0, \
336
336
  'Invalid image resize target {},{}'.format(target_width,target_height)
337
-
338
- # The antialiasing parameter changed between Pillow versions 9 and 10, and for a bit,
337
+
338
+ # The antialiasing parameter changed between Pillow versions 9 and 10, and for a bit,
339
339
  # I'd like to support both.
340
340
  try:
341
341
  resized_image = image.resize((target_width, target_height), Image.ANTIALIAS)
342
- except:
342
+ except Exception:
343
343
  resized_image = image.resize((target_width, target_height), Image.Resampling.LANCZOS)
344
-
344
+
345
345
  if output_file is not None:
346
346
  exif_preserving_save(resized_image,output_file,quality=quality,verbose=verbose)
347
-
347
+
348
348
  return resized_image
349
349
 
350
350
  # ...def resize_image(...)
@@ -357,23 +357,23 @@ def crop_image(detections, image, confidence_threshold=0.15, expansion=0):
357
357
 
358
358
  Args:
359
359
  detections (list): a list of dictionaries with keys 'conf' and 'bbox';
360
- boxes are length-four arrays formatted as [x,y,w,h], normalized,
360
+ boxes are length-four arrays formatted as [x,y,w,h], normalized,
361
361
  upper-left origin (this is the standard MD detection format)
362
362
  image (Image or str): the PIL Image object from which we should crop detections,
363
363
  or an image filename
364
364
  confidence_threshold (float, optional): only crop detections above this threshold
365
365
  expansion (int, optional): a number of pixels to include on each side of a cropped
366
366
  detection
367
-
367
+
368
368
  Returns:
369
- list: a possibly-empty list of PIL Image objects
369
+ list: a possibly-empty list of PIL Image objects
370
370
  """
371
371
 
372
372
  ret_images = []
373
373
 
374
374
  if isinstance(image,str):
375
375
  image = load_image(image)
376
-
376
+
377
377
  for detection in detections:
378
378
 
379
379
  score = float(detection['conf'])
@@ -417,17 +417,17 @@ def blur_detections(image,detections,blur_radius=40):
417
417
  """
418
418
  Blur the regions in [image] corresponding to the MD-formatted list [detections].
419
419
  [image] is modified in place.
420
-
420
+
421
421
  Args:
422
422
  image (PIL.Image.Image): image in which we should blur specific regions
423
423
  detections (list): list of detections in the MD output format, see render
424
424
  detection_bounding_boxes for more detail.
425
425
  """
426
-
426
+
427
427
  img_width, img_height = image.size
428
-
428
+
429
429
  for d in detections:
430
-
430
+
431
431
  bbox = d['bbox']
432
432
  x_norm, y_norm, width_norm, height_norm = bbox
433
433
 
@@ -436,29 +436,29 @@ def blur_detections(image,detections,blur_radius=40):
436
436
  y = int(y_norm * img_height)
437
437
  width = int(width_norm * img_width)
438
438
  height = int(height_norm * img_height)
439
-
439
+
440
440
  # Calculate box boundaries
441
441
  left = max(0, x)
442
442
  top = max(0, y)
443
443
  right = min(img_width, x + width)
444
444
  bottom = min(img_height, y + height)
445
-
445
+
446
446
  # Crop the region, blur it, and paste it back
447
447
  region = image.crop((left, top, right, bottom))
448
448
  blurred_region = region.filter(ImageFilter.GaussianBlur(radius=blur_radius))
449
449
  image.paste(blurred_region, (left, top))
450
450
 
451
451
  # ...for each detection
452
-
452
+
453
453
  # ...def blur_detections(...)
454
454
 
455
-
456
- def render_detection_bounding_boxes(detections,
455
+
456
+ def render_detection_bounding_boxes(detections,
457
457
  image,
458
458
  label_map='show_categories',
459
- classification_label_map=None,
460
- confidence_threshold=0,
461
- thickness=DEFAULT_BOX_THICKNESS,
459
+ classification_label_map=None,
460
+ confidence_threshold=0,
461
+ thickness=DEFAULT_BOX_THICKNESS,
462
462
  expansion=0,
463
463
  classification_confidence_threshold=0.3,
464
464
  max_classifications=3,
@@ -472,17 +472,16 @@ def render_detection_bounding_boxes(detections,
472
472
  """
473
473
  Renders bounding boxes (with labels and confidence values) on an image for all
474
474
  detections above a threshold.
475
-
475
+
476
476
  Renders classification labels if present.
477
-
477
+
478
478
  [image] is modified in place.
479
479
 
480
480
  Args:
481
-
482
481
  detections (list): list of detections in the MD output format, for example:
483
-
482
+
484
483
  .. code-block::none
485
-
484
+
486
485
  [
487
486
  {
488
487
  "category": "2",
@@ -495,15 +494,15 @@ def render_detection_bounding_boxes(detections,
495
494
  ]
496
495
  }
497
496
  ]
498
-
497
+
499
498
  ...where the bbox coordinates are [x, y, box_width, box_height].
500
-
499
+
501
500
  (0, 0) is the upper-left. Coordinates are normalized.
502
-
501
+
503
502
  Supports classification results, in the standard format:
504
-
503
+
505
504
  .. code-block::none
506
-
505
+
507
506
  [
508
507
  {
509
508
  "category": "2",
@@ -523,30 +522,30 @@ def render_detection_bounding_boxes(detections,
523
522
  ]
524
523
 
525
524
  image (PIL.Image.Image): image on which we should render detections
526
- label_map (dict, optional): optional, mapping the numeric label to a string name. The type of the
527
- numeric label (typically strings) needs to be consistent with the keys in label_map; no casting is
528
- carried out. If [label_map] is None, no labels are shown (not even numbers and confidence values).
529
- If you want category numbers and confidence values without class labels, use the default value,
525
+ label_map (dict, optional): optional, mapping the numeric label to a string name. The type of the
526
+ numeric label (typically strings) needs to be consistent with the keys in label_map; no casting is
527
+ carried out. If [label_map] is None, no labels are shown (not even numbers and confidence values).
528
+ If you want category numbers and confidence values without class labels, use the default value,
530
529
  the string 'show_categories'.
531
- classification_label_map (dict, optional): optional, mapping of the string class labels to the actual
532
- class names. The type of the numeric label (typically strings) needs to be consistent with the keys
533
- in label_map; no casting is carried out. If [label_map] is None, no labels are shown (not even numbers
530
+ classification_label_map (dict, optional): optional, mapping of the string class labels to the actual
531
+ class names. The type of the numeric label (typically strings) needs to be consistent with the keys
532
+ in label_map; no casting is carried out. If [label_map] is None, no labels are shown (not even numbers
534
533
  and confidence values).
535
- confidence_threshold (float or dict, optional), threshold above which boxes are rendered. Can also be a
536
- dictionary mapping category IDs to thresholds.
537
- thickness (int, optional): line thickness in pixels
538
- expansion (int, optional): number of pixels to expand bounding boxes on each side
539
- classification_confidence_threshold (float, optional): confidence above which classification results
540
- are displayed
541
- max_classifications (int, optional): maximum number of classification results rendered for one image
534
+ confidence_threshold (float or dict, optional), threshold above which boxes are rendered. Can also be a
535
+ dictionary mapping category IDs to thresholds.
536
+ thickness (int, optional): line thickness in pixels
537
+ expansion (int, optional): number of pixels to expand bounding boxes on each side
538
+ classification_confidence_threshold (float, optional): confidence above which classification results
539
+ are displayed
540
+ max_classifications (int, optional): maximum number of classification results rendered for one image
542
541
  colormap (list, optional): list of color names, used to choose colors for categories by
543
- indexing with the values in [classes]; defaults to a reasonable set of colors
544
- textalign (int, optional): TEXTALIGN_LEFT, TEXTALIGN_CENTER, or TEXTALIGN_RIGHT
545
- vtextalign (int, optional): VTEXTALIGN_TOP or VTEXTALIGN_BOTTOM
546
- label_font_size (float, optional): font size for labels
542
+ indexing with the values in [classes]; defaults to a reasonable set of colors
543
+ textalign (int, optional): TEXTALIGN_LEFT, TEXTALIGN_CENTER, or TEXTALIGN_RIGHT
544
+ vtextalign (int, optional): VTEXTALIGN_TOP or VTEXTALIGN_BOTTOM
545
+ label_font_size (float, optional): font size for labels
547
546
  custom_strings: optional set of strings to append to detection labels, should have the
548
547
  same length as [detections]. Appended before any classification labels.
549
- box_sort_order (str, optional): sorting scheme for detection boxes, can be None, "confidence", or
548
+ box_sort_order (str, optional): sorting scheme for detection boxes, can be None, "confidence", or
550
549
  "reverse_confidence".
551
550
  verbose (bool, optional): enable additional debug output
552
551
  """
@@ -554,52 +553,52 @@ def render_detection_bounding_boxes(detections,
554
553
  # Input validation
555
554
  if (label_map is not None) and (isinstance(label_map,str)) and (label_map == 'show_categories'):
556
555
  label_map = {}
557
-
556
+
558
557
  if custom_strings is not None:
559
558
  assert len(custom_strings) == len(detections), \
560
559
  '{} custom strings provided for {} detections'.format(
561
560
  len(custom_strings),len(detections))
562
-
561
+
563
562
  display_boxes = []
564
-
563
+
565
564
  # list of lists, one list of strings for each bounding box (to accommodate multiple labels)
566
- display_strs = []
567
-
565
+ display_strs = []
566
+
568
567
  # for color selection
569
- classes = []
568
+ classes = []
570
569
 
571
570
  if box_sort_order is not None:
572
-
573
- if box_sort_order == 'confidence':
571
+
572
+ if box_sort_order == 'confidence':
574
573
  detections = sort_list_of_dicts_by_key(detections,k='conf',reverse=False)
575
574
  elif box_sort_order == 'reverse_confidence':
576
575
  detections = sort_list_of_dicts_by_key(detections,k='conf',reverse=True)
577
576
  else:
578
577
  raise ValueError('Unrecognized sorting scheme {}'.format(box_sort_order))
579
-
578
+
580
579
  for i_detection,detection in enumerate(detections):
581
580
 
582
581
  score = detection['conf']
583
-
582
+
584
583
  if isinstance(confidence_threshold,dict):
585
584
  rendering_threshold = confidence_threshold[detection['category']]
586
585
  else:
587
- rendering_threshold = confidence_threshold
588
-
586
+ rendering_threshold = confidence_threshold
587
+
589
588
  # Always render objects with a confidence of "None", this is typically used
590
- # for ground truth data.
589
+ # for ground truth data.
591
590
  if score is None or score >= rendering_threshold:
592
-
591
+
593
592
  x1, y1, w_box, h_box = detection['bbox']
594
593
  display_boxes.append([y1, x1, y1 + h_box, x1 + w_box])
595
-
594
+
596
595
  # The class index to use for coloring this box, which may be based on the detection
597
596
  # category or on the most confident classification category.
598
597
  clss = detection['category']
599
-
600
- # This will be a list of strings that should be rendered above/below this box
598
+
599
+ # This will be a list of strings that should be rendered above/below this box
601
600
  displayed_label = []
602
-
601
+
603
602
  if label_map is not None:
604
603
  label = label_map[clss] if clss in label_map else clss
605
604
  if score is not None:
@@ -618,27 +617,27 @@ def render_detection_bounding_boxes(detections,
618
617
  if ('classifications' in detection) and len(detection['classifications']) > 0:
619
618
 
620
619
  classifications = detection['classifications']
621
-
620
+
622
621
  if len(classifications) > max_classifications:
623
622
  classifications = classifications[0:max_classifications]
624
-
623
+
625
624
  max_classification_category = 0
626
625
  max_classification_conf = -100
627
-
626
+
628
627
  for classification in classifications:
629
-
628
+
630
629
  classification_conf = classification[1]
631
630
  if classification_conf is None or \
632
631
  classification_conf < classification_confidence_threshold:
633
632
  continue
634
-
633
+
635
634
  class_key = classification[0]
636
-
635
+
637
636
  # Is this the most confident classification for this detection?
638
637
  if classification_conf > max_classification_conf:
639
638
  max_classification_conf = classification_conf
640
639
  max_classification_category = int(class_key)
641
-
640
+
642
641
  if (classification_label_map is not None) and (class_key in classification_label_map):
643
642
  class_name = classification_label_map[class_key]
644
643
  else:
@@ -647,15 +646,15 @@ def render_detection_bounding_boxes(detections,
647
646
  displayed_label += ['{}: {:5.1%}'.format(class_name.lower(), classification_conf)]
648
647
  else:
649
648
  displayed_label += ['{}'.format(class_name.lower())]
650
-
649
+
651
650
  # ...for each classification
652
651
 
653
652
  # To avoid duplicate colors with detection-only visualization, offset
654
653
  # the classification class index by the number of detection classes
655
654
  clss = annotation_constants.NUM_DETECTOR_CATEGORIES + max_classification_category
656
-
655
+
657
656
  # ...if we have classification results
658
-
657
+
659
658
  # display_strs is a list of labels for each box
660
659
  display_strs.append(displayed_label)
661
660
  classes.append(clss)
@@ -663,15 +662,15 @@ def render_detection_bounding_boxes(detections,
663
662
  # ...if the confidence of this detection is above threshold
664
663
 
665
664
  # ...for each detection
666
-
665
+
667
666
  display_boxes = np.array(display_boxes)
668
667
 
669
668
  if verbose:
670
669
  print('Rendering {} of {} detections'.format(len(display_boxes),len(detections)))
671
-
670
+
672
671
  draw_bounding_boxes_on_image(image, display_boxes, classes,
673
- display_strs=display_strs, thickness=thickness,
674
- expansion=expansion, colormap=colormap,
672
+ display_strs=display_strs, thickness=thickness,
673
+ expansion=expansion, colormap=colormap,
675
674
  textalign=textalign, vtextalign=vtextalign,
676
675
  label_font_size=label_font_size)
677
676
 
@@ -693,13 +692,12 @@ def draw_bounding_boxes_on_image(image,
693
692
  Draws bounding boxes on an image. Modifies the image in place.
694
693
 
695
694
  Args:
696
-
697
695
  image (PIL.Image): the image on which we should draw boxes
698
- boxes (np.array): a two-dimensional numpy array of size [N, 4], where N is the
696
+ boxes (np.array): a two-dimensional numpy array of size [N, 4], where N is the
699
697
  number of boxes, and each row is (ymin, xmin, ymax, xmax). Coordinates should be
700
698
  normalized to image height/width.
701
699
  classes (list): a list of ints or string-formatted ints corresponding to the
702
- class labels of the boxes. This is only used for color selection. Should have the same
700
+ class labels of the boxes. This is only used for color selection. Should have the same
703
701
  length as [boxes].
704
702
  thickness (int, optional): line thickness in pixels
705
703
  expansion (int, optional): number of pixels to expand bounding boxes on each side
@@ -741,29 +739,29 @@ def get_text_size(font,s):
741
739
  """
742
740
  Get the expected width and height when rendering the string [s] in the font
743
741
  [font].
744
-
742
+
745
743
  Args:
746
744
  font (PIL.ImageFont): the font whose size we should query
747
745
  s (str): the string whose size we should query
748
-
746
+
749
747
  Returns:
750
- tuple: (w,h), both floats in pixel coordinates
748
+ tuple: (w,h), both floats in pixel coordinates
751
749
  """
752
-
750
+
753
751
  # This is what we did w/Pillow 9
754
752
  # w,h = font.getsize(s)
755
-
753
+
756
754
  # I would *think* this would be the equivalent for Pillow 10
757
755
  # l,t,r,b = font.getbbox(s); w = r-l; h=b-t
758
-
756
+
759
757
  # ...but this actually produces the most similar results to Pillow 9
760
758
  # l,t,r,b = font.getbbox(s); w = r; h=b
761
-
759
+
762
760
  try:
763
- l,t,r,b = font.getbbox(s); w = r; h=b
761
+ l,t,r,b = font.getbbox(s); w = r; h=b # noqa
764
762
  except Exception:
765
763
  w,h = font.getsize(s)
766
-
764
+
767
765
  return w,h
768
766
 
769
767
 
@@ -779,7 +777,7 @@ def draw_bounding_box_on_image(image,
779
777
  use_normalized_coordinates=True,
780
778
  label_font_size=DEFAULT_LABEL_FONT_SIZE,
781
779
  colormap=None,
782
- textalign=TEXTALIGN_LEFT,
780
+ textalign=TEXTALIGN_LEFT,
783
781
  vtextalign=VTEXTALIGN_TOP,
784
782
  text_rotation=None):
785
783
  """
@@ -794,9 +792,9 @@ def draw_bounding_box_on_image(image,
794
792
  are displayed below the bounding box.
795
793
 
796
794
  Adapted from:
797
-
795
+
798
796
  https://github.com/tensorflow/models/blob/master/research/object_detection/utils/visualization_utils.py
799
-
797
+
800
798
  Args:
801
799
  image (PIL.Image.Image): the image on which we should draw a box
802
800
  ymin (float): ymin of bounding box
@@ -807,24 +805,24 @@ def draw_bounding_box_on_image(image,
807
805
  a color; should be either an integer or a string-formatted integer
808
806
  thickness (int, optional): line thickness in pixels
809
807
  expansion (int, optional): number of pixels to expand bounding boxes on each side
810
- display_str_list (list, optional): list of strings to display above the box (each to be shown on its
808
+ display_str_list (list, optional): list of strings to display above the box (each to be shown on its
811
809
  own line)
812
- use_normalized_coordinates (bool, optional): if True (default), treat coordinates
810
+ use_normalized_coordinates (bool, optional): if True (default), treat coordinates
813
811
  ymin, xmin, ymax, xmax as relative to the image, otherwise coordinates as absolute pixel values
814
- label_font_size (float, optional): font size
812
+ label_font_size (float, optional): font size
815
813
  colormap (list, optional): list of color names, used to choose colors for categories by
816
814
  indexing with the values in [classes]; defaults to a reasonable set of colors
817
- textalign (int, optional): TEXTALIGN_LEFT, TEXTALIGN_CENTER, or TEXTALIGN_RIGHT
815
+ textalign (int, optional): TEXTALIGN_LEFT, TEXTALIGN_CENTER, or TEXTALIGN_RIGHT
818
816
  vtextalign (int, optional): VTEXTALIGN_TOP or VTEXTALIGN_BOTTOM
819
817
  text_rotation (float, optional): rotation to apply to text
820
818
  """
821
-
819
+
822
820
  if colormap is None:
823
821
  colormap = DEFAULT_COLORS
824
-
822
+
825
823
  if display_str_list is None:
826
824
  display_str_list = []
827
-
825
+
828
826
  if clss is None:
829
827
  # Default to the MegaDetector animal class ID (1)
830
828
  color = colormap[1]
@@ -840,12 +838,12 @@ def draw_bounding_box_on_image(image,
840
838
  (left, right, top, bottom) = (xmin, xmax, ymin, ymax)
841
839
 
842
840
  if expansion > 0:
843
-
841
+
844
842
  left -= expansion
845
843
  right += expansion
846
844
  top -= expansion
847
845
  bottom += expansion
848
-
846
+
849
847
  # Deliberately trimming to the width of the image only in the case where
850
848
  # box expansion is turned on. There's not an obvious correct behavior here,
851
849
  # but the thinking is that if the caller provided an out-of-range bounding
@@ -861,9 +859,9 @@ def draw_bounding_box_on_image(image,
861
859
 
862
860
  left = min(left,im_width-1); right = min(right,im_width-1)
863
861
  top = min(top,im_height-1); bottom = min(bottom,im_height-1)
864
-
862
+
865
863
  # ...if we need to expand boxes
866
-
864
+
867
865
  draw.line([(left, top), (left, bottom), (right, bottom),
868
866
  (right, top), (left, top)], width=thickness, fill=color)
869
867
 
@@ -873,50 +871,50 @@ def draw_bounding_box_on_image(image,
873
871
  font = ImageFont.truetype('arial.ttf', label_font_size)
874
872
  except IOError:
875
873
  font = ImageFont.load_default()
876
-
874
+
877
875
  display_str_heights = [get_text_size(font,ds)[1] for ds in display_str_list]
878
-
876
+
879
877
  # Each display_str has a top and bottom margin of 0.05x.
880
878
  total_display_str_height = (1 + 2 * 0.05) * sum(display_str_heights)
881
-
879
+
882
880
  # Reverse list and print from bottom to top
883
881
  for i_str,display_str in enumerate(display_str_list[::-1]):
884
-
882
+
885
883
  # Skip empty strings
886
884
  if len(display_str) == 0:
887
885
  continue
888
-
889
- text_width, text_height = get_text_size(font,display_str)
886
+
887
+ text_width, text_height = get_text_size(font,display_str)
890
888
  margin = int(np.ceil(0.05 * text_height))
891
-
889
+
892
890
  if text_rotation is not None and text_rotation != 0:
893
-
891
+
894
892
  assert text_rotation == -90, \
895
893
  'Only -90-degree text rotation is supported'
896
-
894
+
897
895
  image_tmp = Image.new('RGB',(text_width+2*margin,text_height+2*margin))
898
896
  image_tmp_draw = ImageDraw.Draw(image_tmp)
899
897
  image_tmp_draw.rectangle([0,0,text_width+2*margin,text_height+2*margin],fill=color)
900
898
  image_tmp_draw.text( (margin,margin), display_str, font=font, fill='black')
901
899
  rotated_text = image_tmp.rotate(text_rotation,expand=1)
902
-
900
+
903
901
  if textalign == TEXTALIGN_RIGHT:
904
902
  text_left = right
905
903
  else:
906
904
  text_left = left
907
905
  text_left = int(text_left + (text_height) * i_str)
908
-
906
+
909
907
  if vtextalign == VTEXTALIGN_BOTTOM:
910
908
  text_top = bottom - text_width
911
909
  else:
912
910
  text_top = top
913
911
  text_left = int(text_left)
914
- text_top = int(text_top)
915
-
912
+ text_top = int(text_top)
913
+
916
914
  image.paste(rotated_text,[text_left,text_top])
917
-
915
+
918
916
  else:
919
-
917
+
920
918
  # If the total height of the display strings added to the top of the bounding
921
919
  # box exceeds the top of the image, stack the strings below the bounding box
922
920
  # instead of above, and vice-versa if we're bottom-aligning.
@@ -933,32 +931,32 @@ def draw_bounding_box_on_image(image,
933
931
  text_bottom = bottom + total_display_str_height
934
932
  if (text_bottom + total_display_str_height) > im_height:
935
933
  text_bottom = top
936
-
934
+
937
935
  text_bottom = int(text_bottom) - i_str * (int(text_height + (2 * margin)))
938
-
936
+
939
937
  text_left = left
940
-
938
+
941
939
  if textalign == TEXTALIGN_RIGHT:
942
940
  text_left = right - text_width
943
941
  elif textalign == TEXTALIGN_CENTER:
944
942
  text_left = ((right + left) / 2.0) - (text_width / 2.0)
945
- text_left = int(text_left)
946
-
943
+ text_left = int(text_left)
944
+
947
945
  draw.rectangle(
948
946
  [(text_left, (text_bottom - text_height) - (2 * margin)),
949
947
  (text_left + text_width, text_bottom)],
950
948
  fill=color)
951
-
949
+
952
950
  draw.text(
953
951
  (text_left + margin, text_bottom - text_height - margin),
954
952
  display_str,
955
953
  fill='black',
956
954
  font=font)
957
-
958
- # ...if we're rotating text
955
+
956
+ # ...if we're rotating text
959
957
 
960
958
  # ...if we're rendering text
961
-
959
+
962
960
  # ...def draw_bounding_box_on_image(...)
963
961
 
964
962
 
@@ -966,9 +964,9 @@ def render_megadb_bounding_boxes(boxes_info, image):
966
964
  """
967
965
  Render bounding boxes to an image, where those boxes are in the mostly-deprecated
968
966
  MegaDB format, which looks like:
969
-
967
+
970
968
  .. code-block::none
971
-
969
+
972
970
  {
973
971
  "category": "animal",
974
972
  "bbox": [
@@ -977,16 +975,16 @@ def render_megadb_bounding_boxes(boxes_info, image):
977
975
  0.187,
978
976
  0.198
979
977
  ]
980
- }
981
-
978
+ }
979
+
982
980
  Args:
983
981
  boxes_info (list): list of dicts, each dict represents a single detection
984
982
  where bbox coordinates are normalized [x_min, y_min, width, height]
985
983
  image (PIL.Image.Image): image to modify
986
-
984
+
987
985
  :meta private:
988
986
  """
989
-
987
+
990
988
  display_boxes = []
991
989
  display_strs = []
992
990
  classes = [] # ints, for selecting colors
@@ -1006,11 +1004,11 @@ def render_megadb_bounding_boxes(boxes_info, image):
1006
1004
 
1007
1005
 
1008
1006
  def render_db_bounding_boxes(boxes,
1009
- classes,
1010
- image,
1007
+ classes,
1008
+ image,
1011
1009
  original_size=None,
1012
- label_map=None,
1013
- thickness=DEFAULT_BOX_THICKNESS,
1010
+ label_map=None,
1011
+ thickness=DEFAULT_BOX_THICKNESS,
1014
1012
  expansion=0,
1015
1013
  colormap=None,
1016
1014
  textalign=TEXTALIGN_LEFT,
@@ -1022,16 +1020,16 @@ def render_db_bounding_boxes(boxes,
1022
1020
  Render bounding boxes (with class labels) on an image. This is a wrapper for
1023
1021
  draw_bounding_boxes_on_image, allowing the caller to operate on a resized image
1024
1022
  by providing the original size of the image; boxes will be scaled accordingly.
1025
-
1023
+
1026
1024
  This function assumes that bounding boxes are in absolute coordinates, typically
1027
1025
  because they come from COCO camera traps .json files.
1028
-
1026
+
1029
1027
  Args:
1030
1028
  boxes (list): list of length-4 tuples, foramtted as (x,y,w,h) (in pixels)
1031
1029
  classes (list): list of ints (or string-formatted ints), used to choose labels (either
1032
1030
  by literally rendering the class labels, or by indexing into [label_map])
1033
1031
  image (PIL.Image.Image): image object to modify
1034
- original_size (tuple, optional): if this is not None, and the size is different than
1032
+ original_size (tuple, optional): if this is not None, and the size is different than
1035
1033
  the size of [image], we assume that [boxes] refer to the original size, and we scale
1036
1034
  them accordingly before rendering
1037
1035
  label_map (dict, optional): int --> str dictionary, typically mapping category IDs to
@@ -1063,7 +1061,7 @@ def render_db_bounding_boxes(boxes,
1063
1061
 
1064
1062
  box = boxes[i_box]
1065
1063
  clss = classes[i_box]
1066
-
1064
+
1067
1065
  x_min_abs, y_min_abs, width_abs, height_abs = box[0:4]
1068
1066
 
1069
1067
  ymin = y_min_abs / img_height
@@ -1076,25 +1074,25 @@ def render_db_bounding_boxes(boxes,
1076
1074
 
1077
1075
  if label_map:
1078
1076
  clss = label_map[int(clss)]
1079
-
1077
+
1080
1078
  display_str = str(clss)
1081
-
1079
+
1082
1080
  # Do we have a tag to append to the class string?
1083
1081
  if tags is not None and tags[i_box] is not None and len(tags[i_box]) > 0:
1084
1082
  display_str += ' ' + tags[i_box]
1085
-
1083
+
1086
1084
  # need to be a string here because PIL needs to iterate through chars
1087
1085
  display_strs.append([display_str])
1088
1086
 
1089
1087
  # ...for each box
1090
-
1088
+
1091
1089
  display_boxes = np.array(display_boxes)
1092
-
1093
- draw_bounding_boxes_on_image(image,
1094
- display_boxes,
1095
- classes,
1090
+
1091
+ draw_bounding_boxes_on_image(image,
1092
+ display_boxes,
1093
+ classes,
1096
1094
  display_strs=display_strs,
1097
- thickness=thickness,
1095
+ thickness=thickness,
1098
1096
  expansion=expansion,
1099
1097
  colormap=colormap,
1100
1098
  textalign=textalign,
@@ -1105,12 +1103,12 @@ def render_db_bounding_boxes(boxes,
1105
1103
  # ...def render_db_bounding_boxes(...)
1106
1104
 
1107
1105
 
1108
- def draw_bounding_boxes_on_file(input_file,
1109
- output_file,
1110
- detections,
1106
+ def draw_bounding_boxes_on_file(input_file,
1107
+ output_file,
1108
+ detections,
1111
1109
  confidence_threshold=0.0,
1112
1110
  detector_label_map=DEFAULT_DETECTOR_LABEL_MAP,
1113
- thickness=DEFAULT_BOX_THICKNESS,
1111
+ thickness=DEFAULT_BOX_THICKNESS,
1114
1112
  expansion=0,
1115
1113
  colormap=None,
1116
1114
  label_font_size=DEFAULT_LABEL_FONT_SIZE,
@@ -1118,17 +1116,17 @@ def draw_bounding_boxes_on_file(input_file,
1118
1116
  target_size=None,
1119
1117
  ignore_exif_rotation=False):
1120
1118
  """
1121
- Renders detection bounding boxes on an image loaded from file, optionally writing the results to
1119
+ Renders detection bounding boxes on an image loaded from file, optionally writing the results to
1122
1120
  a new image file.
1123
-
1121
+
1124
1122
  Args:
1125
1123
  input_file (str): filename or URL to load
1126
1124
  output_file (str, optional): filename to which we should write the rendered image
1127
1125
  detections (list): a list of dictionaries with keys 'conf', 'bbox', and 'category';
1128
- boxes are length-four arrays formatted as [x,y,w,h], normalized,
1126
+ boxes are length-four arrays formatted as [x,y,w,h], normalized,
1129
1127
  upper-left origin (this is the standard MD detection format). 'category' is a string-int.
1130
- detector_label_map (dict, optional): a dict mapping category IDs to strings. If this
1131
- is None, no confidence values or identifiers are shown. If this is {}, just category
1128
+ detector_label_map (dict, optional): a dict mapping category IDs to strings. If this
1129
+ is None, no confidence values or identifiers are shown. If this is {}, just category
1132
1130
  indices and confidence values are shown.
1133
1131
  thickness (int, optional): line width in pixels for box rendering
1134
1132
  expansion (int, optional): box expansion in pixels
@@ -1141,16 +1139,16 @@ def draw_bounding_boxes_on_file(input_file,
1141
1139
  see resize_image() for documentation. If None or (-1,-1), uses the original image size.
1142
1140
  ignore_exif_rotation (bool, optional): don't rotate the loaded pixels,
1143
1141
  even if we are loading a JPEG and that JPEG says it should be rotated.
1144
-
1142
+
1145
1143
  Returns:
1146
1144
  PIL.Image.Image: loaded and modified image
1147
1145
  """
1148
-
1146
+
1149
1147
  image = open_image(input_file, ignore_exif_rotation=ignore_exif_rotation)
1150
-
1148
+
1151
1149
  if target_size is not None:
1152
1150
  image = resize_image(image,target_size[0],target_size[1])
1153
-
1151
+
1154
1152
  render_detection_bounding_boxes(
1155
1153
  detections, image, label_map=detector_label_map,
1156
1154
  confidence_threshold=confidence_threshold,
@@ -1159,27 +1157,27 @@ def draw_bounding_boxes_on_file(input_file,
1159
1157
 
1160
1158
  if output_file is not None:
1161
1159
  image.save(output_file)
1162
-
1160
+
1163
1161
  return image
1164
1162
 
1165
1163
 
1166
- def draw_db_boxes_on_file(input_file,
1167
- output_file,
1168
- boxes,
1169
- classes=None,
1170
- label_map=None,
1171
- thickness=DEFAULT_BOX_THICKNESS,
1164
+ def draw_db_boxes_on_file(input_file,
1165
+ output_file,
1166
+ boxes,
1167
+ classes=None,
1168
+ label_map=None,
1169
+ thickness=DEFAULT_BOX_THICKNESS,
1172
1170
  expansion=0,
1173
1171
  ignore_exif_rotation=False):
1174
1172
  """
1175
- Render COCO-formatted bounding boxes (in absolute coordinates) on an image loaded from file,
1173
+ Render COCO-formatted bounding boxes (in absolute coordinates) on an image loaded from file,
1176
1174
  writing the results to a new image file.
1177
1175
 
1178
1176
  Args:
1179
1177
  input_file (str): image file to read
1180
1178
  output_file (str): image file to write
1181
1179
  boxes (list): list of length-4 tuples, foramtted as (x,y,w,h) (in pixels)
1182
- classes (list, optional): list of ints (or string-formatted ints), used to choose
1180
+ classes (list, optional): list of ints (or string-formatted ints), used to choose
1183
1181
  labels (either by literally rendering the class labels, or by indexing into [label_map])
1184
1182
  label_map (dict, optional): int --> str dictionary, typically mapping category IDs to
1185
1183
  species labels; if None, category labels are rendered verbatim (typically as numbers)
@@ -1188,90 +1186,90 @@ def draw_db_boxes_on_file(input_file,
1188
1186
  detection
1189
1187
  ignore_exif_rotation (bool, optional): don't rotate the loaded pixels,
1190
1188
  even if we are loading a JPEG and that JPEG says it should be rotated
1191
-
1189
+
1192
1190
  Returns:
1193
1191
  PIL.Image.Image: the loaded and modified image
1194
1192
  """
1195
-
1193
+
1196
1194
  image = open_image(input_file, ignore_exif_rotation=ignore_exif_rotation)
1197
1195
 
1198
1196
  if classes is None:
1199
1197
  classes = [0] * len(boxes)
1200
-
1198
+
1201
1199
  render_db_bounding_boxes(boxes, classes, image, original_size=None,
1202
1200
  label_map=label_map, thickness=thickness, expansion=expansion)
1203
-
1201
+
1204
1202
  image.save(output_file)
1205
-
1203
+
1206
1204
  return image
1207
-
1205
+
1208
1206
  # ...def draw_bounding_boxes_on_file(...)
1209
1207
 
1210
1208
 
1211
1209
  def gray_scale_fraction(image,crop_size=(0.1,0.1)):
1212
1210
  """
1213
- Computes the fraction of the pixels in [image] that appear to be grayscale (R==G==B),
1211
+ Computes the fraction of the pixels in [image] that appear to be grayscale (R==G==B),
1214
1212
  useful for approximating whether this is a night-time image when flash information is not
1215
1213
  available in EXIF data (or for video frames, where this information is often not available
1216
1214
  in structured metadata at all).
1217
-
1215
+
1218
1216
  Args:
1219
1217
  image (str or PIL.Image.Image): Image, filename, or URL to analyze
1220
- crop_size (optional): a 2-element list/tuple, representing the fraction of the
1221
- image to crop at the top and bottom, respectively, before analyzing (to minimize
1218
+ crop_size (optional): a 2-element list/tuple, representing the fraction of the
1219
+ image to crop at the top and bottom, respectively, before analyzing (to minimize
1222
1220
  the possibility of including color elements in the image overlay)
1223
-
1221
+
1224
1222
  Returns:
1225
1223
  float: the fraction of pixels in [image] that appear to be grayscale (R==G==B)
1226
1224
  """
1227
-
1225
+
1228
1226
  if isinstance(image,str):
1229
1227
  image = Image.open(image)
1230
-
1228
+
1231
1229
  if image.mode == 'L':
1232
1230
  return 1.0
1233
-
1231
+
1234
1232
  if len(image.getbands()) == 1:
1235
1233
  return 1.0
1236
-
1234
+
1237
1235
  # Crop if necessary
1238
1236
  if crop_size[0] > 0 or crop_size[1] > 0:
1239
-
1237
+
1240
1238
  assert (crop_size[0] + crop_size[1]) < 1.0, \
1241
1239
  print('Illegal crop size: {}'.format(str(crop_size)))
1242
-
1240
+
1243
1241
  top_crop_pixels = int(image.height * crop_size[0])
1244
1242
  bottom_crop_pixels = int(image.height * crop_size[1])
1245
-
1243
+
1246
1244
  left = 0
1247
1245
  right = image.width
1248
-
1246
+
1249
1247
  # Remove pixels from the top
1250
1248
  first_crop_top = top_crop_pixels
1251
- first_crop_bottom = image.height
1249
+ first_crop_bottom = image.height
1252
1250
  first_crop = image.crop((left, first_crop_top, right, first_crop_bottom))
1253
-
1251
+
1254
1252
  # Remove pixels from the bottom
1255
1253
  second_crop_top = 0
1256
1254
  second_crop_bottom = first_crop.height - bottom_crop_pixels
1257
1255
  second_crop = first_crop.crop((left, second_crop_top, right, second_crop_bottom))
1258
-
1256
+
1259
1257
  image = second_crop
1260
-
1258
+
1261
1259
  # It doesn't matter if these are actually R/G/B, they're just names
1262
1260
  r = np.array(image.getchannel(0))
1263
1261
  g = np.array(image.getchannel(1))
1264
1262
  b = np.array(image.getchannel(2))
1265
-
1263
+
1266
1264
  gray_pixels = np.logical_and(r == g, r == b)
1267
1265
  n_pixels = gray_pixels.size
1268
1266
  n_gray_pixels = gray_pixels.sum()
1269
-
1267
+
1270
1268
  return n_gray_pixels / n_pixels
1271
1269
 
1272
1270
  # Non-numpy way to do the same thing, briefly keeping this here for posterity
1273
1271
  if False:
1274
-
1272
+
1275
1273
  w, h = image.size
1276
1274
  n_pixels = w*h
1277
1275
  n_gray_pixels = 0
@@ -1279,26 +1277,38 @@ def gray_scale_fraction(image,crop_size=(0.1,0.1)):
1279
1277
  for j in range(h):
1280
1278
  r, g, b = image.getpixel((i,j))
1281
1279
  if r == g and r == b and g == b:
1282
- n_gray_pixels += 1
1280
+ n_gray_pixels += 1
1283
1281
 
1284
1282
  # ...def gray_scale_fraction(...)
1285
1283
 
1286
1284
 
1287
1285
  def _resize_relative_image(fn_relative,
1288
- input_folder,output_folder,
1289
- target_width,target_height,no_enlarge_width,verbose,quality):
1286
+ input_folder,
1287
+ output_folder,
1288
+ target_width,
1289
+ target_height,
1290
+ no_enlarge_width,
1291
+ verbose,
1292
+ quality,
1293
+ overwrite=True):
1290
1294
  """
1291
1295
  Internal function for resizing an image from one folder to another,
1292
1296
  maintaining relative path.
1293
1297
  """
1294
-
1298
+
1295
1299
  input_fn_abs = os.path.join(input_folder,fn_relative)
1296
1300
  output_fn_abs = os.path.join(output_folder,fn_relative)
1301
+
1302
+ if (not overwrite) and (os.path.isfile(output_fn_abs)):
1303
+ status = 'skipped'
1304
+ error = None
1305
+ return {'fn_relative':fn_relative,'status':status,'error':error}
1306
+
1297
1307
  os.makedirs(os.path.dirname(output_fn_abs),exist_ok=True)
1298
1308
  try:
1299
- _ = resize_image(input_fn_abs,
1300
- output_file=output_fn_abs,
1301
- target_width=target_width, target_height=target_height,
1309
+ _ = resize_image(input_fn_abs,
1310
+ output_file=output_fn_abs,
1311
+ target_width=target_width, target_height=target_height,
1302
1312
  no_enlarge_width=no_enlarge_width, verbose=verbose, quality=quality)
1303
1313
  status = 'success'
1304
1314
  error = None
@@ -1307,7 +1317,7 @@ def _resize_relative_image(fn_relative,
1307
1317
  print('Error resizing {}: {}'.format(fn_relative,str(e)))
1308
1318
  status = 'error'
1309
1319
  error = str(e)
1310
-
1320
+
1311
1321
  return {'fn_relative':fn_relative,'status':status,'error':error}
1312
1322
 
1313
1323
  # ...def _resize_relative_image(...)
@@ -1315,18 +1325,17 @@ def _resize_relative_image(fn_relative,
1315
1325
 
1316
1326
  def _resize_absolute_image(input_output_files,
1317
1327
  target_width,target_height,no_enlarge_width,verbose,quality):
1318
-
1319
1328
  """
1320
1329
  Internal wrapper for resize_image used in the context of a batch resize operation.
1321
1330
  """
1322
-
1331
+
1323
1332
  input_fn_abs = input_output_files[0]
1324
1333
  output_fn_abs = input_output_files[1]
1325
1334
  os.makedirs(os.path.dirname(output_fn_abs),exist_ok=True)
1326
1335
  try:
1327
- _ = resize_image(input_fn_abs,
1328
- output_file=output_fn_abs,
1329
- target_width=target_width, target_height=target_height,
1336
+ _ = resize_image(input_fn_abs,
1337
+ output_file=output_fn_abs,
1338
+ target_width=target_width, target_height=target_height,
1330
1339
  no_enlarge_width=no_enlarge_width, verbose=verbose, quality=quality)
1331
1340
  status = 'success'
1332
1341
  error = None
@@ -1335,7 +1344,7 @@ def _resize_absolute_image(input_output_files,
1335
1344
  print('Error resizing {}: {}'.format(input_fn_abs,str(e)))
1336
1345
  status = 'error'
1337
1346
  error = str(e)
1338
-
1347
+
1339
1348
  return {'input_fn':input_fn_abs,'output_fn':output_fn_abs,status:'status',
1340
1349
  'error':error}
1341
1350
 
@@ -1343,21 +1352,21 @@ def _resize_absolute_image(input_output_files,
1343
1352
 
1344
1353
 
1345
1354
  def resize_images(input_file_to_output_file,
1346
- target_width=-1,
1355
+ target_width=-1,
1347
1356
  target_height=-1,
1348
- no_enlarge_width=False,
1349
- verbose=False,
1357
+ no_enlarge_width=False,
1358
+ verbose=False,
1350
1359
  quality='keep',
1351
- pool_type='process',
1360
+ pool_type='process',
1352
1361
  n_workers=10):
1353
1362
  """
1354
1363
  Resizes all images the dictionary [input_file_to_output_file].
1355
1364
 
1356
1365
  TODO: This is a little more redundant with resize_image_folder than I would like;
1357
1366
  refactor resize_image_folder to call resize_images. Not doing that yet because
1358
- at the time I'm writing this comment, a lot of code depends on resize_image_folder
1367
+ at the time I'm writing this comment, a lot of code depends on resize_image_folder
1359
1368
  and I don't want to rock the boat yet.
1360
-
1369
+
1361
1370
  Args:
1362
1371
  input_file_to_output_file (dict): dict mapping images that exist to the locations
1363
1372
  where the resized versions should be written
@@ -1365,8 +1374,8 @@ def resize_images(input_file_to_output_file,
1365
1374
  to let target_height determine the size
1366
1375
  target_height (int, optional): height to which we should resize this image, or -1
1367
1376
  to let target_width determine the size
1368
- no_enlarge_width (bool, optional): if [no_enlarge_width] is True, and
1369
- [target width] is larger than the original image width, does not modify the image,
1377
+ no_enlarge_width (bool, optional): if [no_enlarge_width] is True, and
1378
+ [target width] is larger than the original image width, does not modify the image,
1370
1379
  but will write to output_file if supplied
1371
1380
  verbose (bool, optional): enable additional debug output
1372
1381
  quality (str or int, optional): passed to exif_preserving_save, see docs for more detail
@@ -1377,20 +1386,20 @@ def resize_images(input_file_to_output_file,
1377
1386
 
1378
1387
  Returns:
1379
1388
  list: a list of dicts with keys 'input_fn', 'output_fn', 'status', and 'error'.
1380
- 'status' will be 'success' or 'error'; 'error' will be None for successful cases,
1389
+ 'status' will be 'success' or 'error'; 'error' will be None for successful cases,
1381
1390
  otherwise will contain the image-specific error.
1382
1391
  """
1383
-
1392
+
1384
1393
  assert pool_type in ('process','thread'), 'Illegal pool type {}'.format(pool_type)
1385
-
1394
+
1386
1395
  input_output_file_pairs = []
1387
-
1396
+
1388
1397
  # Reformat input files as (input,output) tuples
1389
1398
  for input_fn in input_file_to_output_file:
1390
1399
  input_output_file_pairs.append((input_fn,input_file_to_output_file[input_fn]))
1391
-
1392
- if n_workers == 1:
1393
-
1400
+
1401
+ if n_workers == 1:
1402
+
1394
1403
  results = []
1395
1404
  for i_o_file_pair in tqdm(input_output_file_pairs):
1396
1405
  results.append(_resize_absolute_image(i_o_file_pair,
@@ -1401,46 +1410,54 @@ def resize_images(input_file_to_output_file,
1401
1410
  quality=quality))
1402
1411
 
1403
1412
  else:
1404
-
1405
- if pool_type == 'thread':
1406
- pool = ThreadPool(n_workers); poolstring = 'threads'
1407
- else:
1408
- assert pool_type == 'process'
1409
- pool = Pool(n_workers); poolstring = 'processes'
1410
-
1411
- if verbose:
1412
- print('Starting resizing pool with {} {}'.format(n_workers,poolstring))
1413
-
1414
- p = partial(_resize_absolute_image,
1415
- target_width=target_width,
1416
- target_height=target_height,
1417
- no_enlarge_width=no_enlarge_width,
1418
- verbose=verbose,
1419
- quality=quality)
1420
-
1421
- results = list(tqdm(pool.imap(p, input_output_file_pairs),total=len(input_output_file_pairs)))
1413
+
1414
+ pool = None
1415
+
1416
+ try:
1417
+ if pool_type == 'thread':
1418
+ pool = ThreadPool(n_workers); poolstring = 'threads'
1419
+ else:
1420
+ assert pool_type == 'process'
1421
+ pool = Pool(n_workers); poolstring = 'processes'
1422
+
1423
+ if verbose:
1424
+ print('Starting resizing pool with {} {}'.format(n_workers,poolstring))
1425
+
1426
+ p = partial(_resize_absolute_image,
1427
+ target_width=target_width,
1428
+ target_height=target_height,
1429
+ no_enlarge_width=no_enlarge_width,
1430
+ verbose=verbose,
1431
+ quality=quality)
1432
+
1433
+ results = list(tqdm(pool.imap(p, input_output_file_pairs),total=len(input_output_file_pairs)))
1434
+ finally:
1435
+ pool.close()
1436
+ pool.join()
1437
+ print("Pool closed and joined for image resizing")
1422
1438
 
1423
1439
  return results
1424
1440
 
1425
1441
  # ...def resize_images(...)
1426
1442
 
1427
1443
 
1428
- def resize_image_folder(input_folder,
1444
+ def resize_image_folder(input_folder,
1429
1445
  output_folder=None,
1430
- target_width=-1,
1446
+ target_width=-1,
1431
1447
  target_height=-1,
1432
- no_enlarge_width=False,
1433
- verbose=False,
1448
+ no_enlarge_width=False,
1449
+ verbose=False,
1434
1450
  quality='keep',
1435
- pool_type='process',
1436
- n_workers=10,
1451
+ pool_type='process',
1452
+ n_workers=10,
1437
1453
  recursive=True,
1438
- image_files_relative=None):
1454
+ image_files_relative=None,
1455
+ overwrite=True):
1439
1456
  """
1440
1457
  Resize all images in a folder (defaults to recursive).
1441
-
1458
+
1442
1459
  Defaults to in-place resizing (output_folder is optional).
1443
-
1460
+
1444
1461
  Args:
1445
1462
  input_folder (str): folder in which we should find images to resize
1446
1463
  output_folder (str, optional): folder in which we should write resized images. If
@@ -1450,8 +1467,8 @@ def resize_image_folder(input_folder,
1450
1467
  to let target_height determine the size
1451
1468
  target_height (int, optional): height to which we should resize this image, or -1
1452
1469
  to let target_width determine the size
1453
- no_enlarge_width (bool, optional): if [no_enlarge_width] is True, and
1454
- [target width] is larger than the original image width, does not modify the image,
1470
+ no_enlarge_width (bool, optional): if [no_enlarge_width] is True, and
1471
+ [target width] is larger than the original image width, does not modify the image,
1455
1472
  but will write to output_file if supplied
1456
1473
  verbose (bool, optional): enable additional debug output
1457
1474
  quality (str or int, optional): passed to exif_preserving_save, see docs for more detail
@@ -1461,35 +1478,36 @@ def resize_image_folder(input_folder,
1461
1478
  to disable parallelization
1462
1479
  recursive (bool, optional): whether to search [input_folder] recursively for images.
1463
1480
  image_files_relative (list, optional): if not None, skips any relative paths not
1464
- in this list.
1465
-
1481
+ in this list
1482
+ overwrite (bool, optional): whether to overwrite existing target images
1483
+
1466
1484
  Returns:
1467
1485
  list: a list of dicts with keys 'input_fn', 'output_fn', 'status', and 'error'.
1468
- 'status' will be 'success' or 'error'; 'error' will be None for successful cases,
1469
- otherwise will contain the image-specific error.
1486
+ 'status' will be 'success', 'skipped', or 'error'; 'error' will be None for successful
1487
+ cases, otherwise will contain the image-specific error.
1470
1488
  """
1471
1489
 
1472
1490
  assert os.path.isdir(input_folder), '{} is not a folder'.format(input_folder)
1473
-
1491
+
1474
1492
  if output_folder is None:
1475
1493
  output_folder = input_folder
1476
1494
  else:
1477
1495
  os.makedirs(output_folder,exist_ok=True)
1478
-
1496
+
1479
1497
  assert pool_type in ('process','thread'), 'Illegal pool type {}'.format(pool_type)
1480
-
1498
+
1481
1499
  if image_files_relative is None:
1482
-
1500
+
1483
1501
  if verbose:
1484
1502
  print('Enumerating images')
1485
-
1503
+
1486
1504
  image_files_relative = find_images(input_folder,recursive=recursive,
1487
1505
  return_relative_paths=True,convert_slashes=True)
1488
1506
  if verbose:
1489
1507
  print('Found {} images'.format(len(image_files_relative)))
1490
-
1491
- if n_workers == 1:
1492
-
1508
+
1509
+ if n_workers == 1:
1510
+
1493
1511
  if verbose:
1494
1512
  print('Resizing images')
1495
1513
 
@@ -1502,19 +1520,20 @@ def resize_image_folder(input_folder,
1502
1520
  target_height=target_height,
1503
1521
  no_enlarge_width=no_enlarge_width,
1504
1522
  verbose=verbose,
1505
- quality=quality))
1523
+ quality=quality,
1524
+ overwrite=overwrite))
1506
1525
 
1507
1526
  else:
1508
-
1527
+
1509
1528
  if pool_type == 'thread':
1510
- pool = ThreadPool(n_workers); poolstring = 'threads'
1529
+ pool = ThreadPool(n_workers); poolstring = 'threads'
1511
1530
  else:
1512
1531
  assert pool_type == 'process'
1513
1532
  pool = Pool(n_workers); poolstring = 'processes'
1514
-
1533
+
1515
1534
  if verbose:
1516
1535
  print('Starting resizing pool with {} {}'.format(n_workers,poolstring))
1517
-
1536
+
1518
1537
  p = partial(_resize_relative_image,
1519
1538
  input_folder=input_folder,
1520
1539
  output_folder=output_folder,
@@ -1522,8 +1541,9 @@ def resize_image_folder(input_folder,
1522
1541
  target_height=target_height,
1523
1542
  no_enlarge_width=no_enlarge_width,
1524
1543
  verbose=verbose,
1525
- quality=quality)
1526
-
1544
+ quality=quality,
1545
+ overwrite=overwrite)
1546
+
1527
1547
  results = list(tqdm(pool.imap(p, image_files_relative),total=len(image_files_relative)))
1528
1548
 
1529
1549
  return results
@@ -1534,17 +1554,17 @@ def resize_image_folder(input_folder,
1534
1554
  def get_image_size(im,verbose=False):
1535
1555
  """
1536
1556
  Retrieve the size of an image. Returns None if the image fails to load.
1537
-
1557
+
1538
1558
  Args:
1539
1559
  im (str or PIL.Image): filename or PIL image
1540
-
1560
+
1541
1561
  Returns:
1542
1562
  tuple (w,h), or None if the image fails to load.
1543
1563
  """
1544
-
1564
+
1545
1565
  image_name = '[in memory]'
1546
-
1547
- try:
1566
+
1567
+ try:
1548
1568
  if isinstance(im,str):
1549
1569
  image_name = im
1550
1570
  im = load_image(im)
@@ -1561,20 +1581,20 @@ def get_image_size(im,verbose=False):
1561
1581
  print('Error reading width from image {}: {}'.format(
1562
1582
  image_name,str(e)))
1563
1583
  return None
1564
-
1584
+
1565
1585
  # ...def get_image_size(...)
1566
1586
 
1567
1587
 
1568
1588
  def parallel_get_image_sizes(filenames,
1569
- max_workers=16,
1570
- use_threads=True,
1589
+ max_workers=16,
1590
+ use_threads=True,
1571
1591
  recursive=True,
1572
1592
  verbose=False):
1573
1593
  """
1574
1594
  Retrieve image sizes for a list or folder of images
1575
-
1595
+
1576
1596
  Args:
1577
- filenames (list or str): a list of image filenames or a folder. Non-image files and
1597
+ filenames (list or str): a list of image filenames or a folder. Non-image files and
1578
1598
  unreadable images will be returned with a file size of None.
1579
1599
  max_workers (int, optional): the number of parallel workers to use; set to <=1 to disable
1580
1600
  parallelization
@@ -1583,7 +1603,7 @@ def parallel_get_image_sizes(filenames,
1583
1603
  recursive (bool, optional): if [filenames] is a folder, whether to search recursively for images.
1584
1604
  Ignored if [filenames] is a list.
1585
1605
  verbose (bool, optional): enable additional debug output
1586
-
1606
+
1587
1607
  Returns:
1588
1608
  dict: a dict mapping filenames to (w,h) tuples; the value will be None for images that fail
1589
1609
  to load.
@@ -1593,34 +1613,34 @@ def parallel_get_image_sizes(filenames,
1593
1613
  if verbose:
1594
1614
  print('Enumerating images in {}'.format(filenames))
1595
1615
  filenames = find_images(filenames,recursive=recursive,return_relative_paths=False)
1596
-
1616
+
1597
1617
  n_workers = min(max_workers,len(filenames))
1598
-
1618
+
1599
1619
  if verbose:
1600
1620
  print('Getting image sizes for {} images'.format(len(filenames)))
1601
-
1621
+
1602
1622
  if n_workers <= 1:
1603
-
1623
+
1604
1624
  results = []
1605
1625
  for filename in filenames:
1606
1626
  results.append(get_image_size(filename,verbose=verbose))
1607
-
1627
+
1608
1628
  else:
1609
-
1629
+
1610
1630
  if use_threads:
1611
1631
  pool = ThreadPool(n_workers)
1612
1632
  else:
1613
1633
  pool = Pool(n_workers)
1614
-
1634
+
1615
1635
  results = list(tqdm(pool.imap(
1616
1636
  partial(get_image_size,verbose=verbose),filenames), total=len(filenames)))
1617
-
1637
+
1618
1638
  assert len(filenames) == len(results), 'Internal error in parallel_get_image_sizes'
1619
-
1639
+
1620
1640
  to_return = {}
1621
1641
  for i_file,filename in enumerate(filenames):
1622
1642
  to_return[filename] = results[i_file]
1623
-
1643
+
1624
1644
  return to_return
1625
1645
 
1626
1646
 
@@ -1629,30 +1649,30 @@ def parallel_get_image_sizes(filenames,
1629
1649
  def check_image_integrity(filename,modes=None):
1630
1650
  """
1631
1651
  Check whether we can successfully load an image via OpenCV and/or PIL.
1632
-
1633
- Args:
1652
+
1653
+ Args:
1634
1654
  filename (str): the filename to evaluate
1635
1655
  modes (list, optional): a list containing one or more of:
1636
-
1656
+
1637
1657
  - 'cv'
1638
1658
  - 'pil'
1639
1659
  - 'skimage'
1640
- - 'jpeg_trailer'
1641
-
1660
+ - 'jpeg_trailer'
1661
+
1642
1662
  'jpeg_trailer' checks that the binary data ends with ffd9. It does not check whether
1643
1663
  the image is actually a jpeg, and even if it is, there are lots of reasons the image might not
1644
1664
  end with ffd9. It's also true the JPEGs that cause "premature end of jpeg segment" issues
1645
1665
  don't end with ffd9, so this may be a useful diagnostic. High precision, very low recall
1646
1666
  for corrupt jpegs.
1647
-
1667
+
1648
1668
  Set to None to use all modes.
1649
-
1669
+
1650
1670
  Returns:
1651
1671
  dict: a dict with a key called 'file' (the value of [filename]), one key for each string in
1652
1672
  [modes] (a success indicator for that mode, specifically a string starting with either
1653
1673
  'success' or 'error').
1654
1674
  """
1655
-
1675
+
1656
1676
  if modes is None:
1657
1677
  modes = ('cv','pil','skimage','jpeg_trailer')
1658
1678
  else:
@@ -1660,14 +1680,14 @@ def check_image_integrity(filename,modes=None):
1660
1680
  modes = [modes]
1661
1681
  for mode in modes:
1662
1682
  assert mode in ('cv','pil','skimage'), 'Unrecognized mode {}'.format(mode)
1663
-
1683
+
1664
1684
  assert os.path.isfile(filename), 'Could not find file {}'.format(filename)
1665
-
1685
+
1666
1686
  result = {}
1667
1687
  result['file'] = filename
1668
-
1688
+
1669
1689
  for mode in modes:
1670
-
1690
+
1671
1691
  result[mode] = 'unknown'
1672
1692
  if mode == 'pil':
1673
1693
  try:
@@ -1684,7 +1704,7 @@ def check_image_integrity(filename,modes=None):
1684
1704
  result[mode] = 'success'
1685
1705
  except Exception as e:
1686
1706
  result[mode] = 'error: {}'.format(str(e))
1687
- elif mode == 'skimage':
1707
+ elif mode == 'skimage':
1688
1708
  try:
1689
1709
  # This is not a standard dependency
1690
1710
  from skimage import io as skimage_io # noqa
@@ -1708,23 +1728,23 @@ def check_image_integrity(filename,modes=None):
1708
1728
  result[mode] = 'success'
1709
1729
  except Exception as e:
1710
1730
  result[mode] = 'error: {}'.format(str(e))
1711
-
1712
- # ...for each mode
1713
-
1731
+
1732
+ # ...for each mode
1733
+
1714
1734
  return result
1715
1735
 
1716
1736
  # ...def check_image_integrity(...)
1717
1737
 
1718
1738
 
1719
1739
  def parallel_check_image_integrity(filenames,
1720
- modes=None,
1721
- max_workers=16,
1722
- use_threads=True,
1740
+ modes=None,
1741
+ max_workers=16,
1742
+ use_threads=True,
1723
1743
  recursive=True,
1724
1744
  verbose=False):
1725
1745
  """
1726
1746
  Check whether we can successfully load a list of images via OpenCV and/or PIL.
1727
-
1747
+
1728
1748
  Args:
1729
1749
  filenames (list or str): a list of image filenames or a folder
1730
1750
  mode (list): see check_image_integrity() for documentation on the [modes] parameter
@@ -1735,10 +1755,10 @@ def parallel_check_image_integrity(filenames,
1735
1755
  recursive (bool, optional): if [filenames] is a folder, whether to search recursively for images.
1736
1756
  Ignored if [filenames] is a list.
1737
1757
  verbose (bool, optional): enable additional debug output
1738
-
1758
+
1739
1759
  Returns:
1740
- list: a list of dicts, each with a key called 'file' (the value of [filename]), one key for
1741
- each string in [modes] (a success indicator for that mode, specifically a string starting
1760
+ list: a list of dicts, each with a key called 'file' (the value of [filename]), one key for
1761
+ each string in [modes] (a success indicator for that mode, specifically a string starting
1742
1762
  with either 'success' or 'error').
1743
1763
  """
1744
1764
 
@@ -1746,69 +1766,69 @@ def parallel_check_image_integrity(filenames,
1746
1766
  if verbose:
1747
1767
  print('Enumerating images in {}'.format(filenames))
1748
1768
  filenames = find_images(filenames,recursive=recursive,return_relative_paths=False)
1749
-
1769
+
1750
1770
  n_workers = min(max_workers,len(filenames))
1751
-
1771
+
1752
1772
  if verbose:
1753
1773
  print('Checking image integrity for {} filenames'.format(len(filenames)))
1754
-
1774
+
1755
1775
  if n_workers <= 1:
1756
-
1776
+
1757
1777
  results = []
1758
1778
  for filename in filenames:
1759
1779
  results.append(check_image_integrity(filename,modes=modes))
1760
-
1780
+
1761
1781
  else:
1762
-
1782
+
1763
1783
  if use_threads:
1764
1784
  pool = ThreadPool(n_workers)
1765
1785
  else:
1766
1786
  pool = Pool(n_workers)
1767
-
1787
+
1768
1788
  results = list(tqdm(pool.imap(
1769
1789
  partial(check_image_integrity,modes=modes),filenames), total=len(filenames)))
1770
-
1790
+
1771
1791
  return results
1772
1792
 
1773
1793
 
1774
1794
  #%% Test drivers
1775
1795
 
1776
1796
  if False:
1777
-
1797
+
1778
1798
  #%% Text rendering tests
1779
-
1799
+
1780
1800
  import os # noqa
1781
1801
  import numpy as np # noqa
1782
1802
  from megadetector.visualization.visualization_utils import \
1783
1803
  draw_bounding_boxes_on_image, exif_preserving_save, load_image, \
1784
1804
  TEXTALIGN_LEFT,TEXTALIGN_RIGHT,VTEXTALIGN_BOTTOM,VTEXTALIGN_TOP, \
1785
1805
  DEFAULT_LABEL_FONT_SIZE
1786
-
1806
+
1787
1807
  fn = os.path.expanduser('~/AppData/Local/Temp/md-tests/md-test-images/ena24_7904.jpg')
1788
1808
  output_fn = r'g:\temp\test.jpg'
1789
-
1809
+
1790
1810
  image = load_image(fn)
1791
-
1811
+
1792
1812
  w = 0.2; h = 0.2
1793
1813
  all_boxes = [[0.05, 0.05, 0.25, 0.25],
1794
1814
  [0.05, 0.35, 0.25, 0.6],
1795
1815
  [0.35, 0.05, 0.6, 0.25],
1796
1816
  [0.35, 0.35, 0.6, 0.6]]
1797
-
1817
+
1798
1818
  alignments = [
1799
1819
  [TEXTALIGN_LEFT,VTEXTALIGN_TOP],
1800
1820
  [TEXTALIGN_LEFT,VTEXTALIGN_BOTTOM],
1801
1821
  [TEXTALIGN_RIGHT,VTEXTALIGN_TOP],
1802
1822
  [TEXTALIGN_RIGHT,VTEXTALIGN_BOTTOM]
1803
1823
  ]
1804
-
1824
+
1805
1825
  labels = ['left_top','left_bottom','right_top','right_bottom']
1806
-
1826
+
1807
1827
  text_rotation = -90
1808
1828
  n_label_copies = 2
1809
-
1829
+
1810
1830
  for i_box,box in enumerate(all_boxes):
1811
-
1831
+
1812
1832
  boxes = [box]
1813
1833
  boxes = np.array(boxes)
1814
1834
  classes = [i_box]
@@ -1830,30 +1850,30 @@ if False:
1830
1850
  exif_preserving_save(image,output_fn)
1831
1851
  from megadetector.utils.path_utils import open_file
1832
1852
  open_file(output_fn)
1833
-
1834
-
1853
+
1854
+
1835
1855
  #%% Recursive resize test
1836
-
1856
+
1837
1857
  from megadetector.visualization.visualization_utils import resize_image_folder # noqa
1838
-
1858
+
1839
1859
  input_folder = r"C:\temp\resize-test\in"
1840
1860
  output_folder = r"C:\temp\resize-test\out"
1841
-
1861
+
1842
1862
  resize_results = resize_image_folder(input_folder,output_folder,
1843
1863
  target_width=1280,verbose=True,quality=85,no_enlarge_width=True,
1844
1864
  pool_type='process',n_workers=10)
1845
-
1846
-
1865
+
1866
+
1847
1867
  #%% Integrity checking test
1848
-
1868
+
1849
1869
  from megadetector.utils import md_tests
1850
1870
  options = md_tests.download_test_data()
1851
1871
  folder = options.scratch_dir
1852
-
1872
+
1853
1873
  results = parallel_check_image_integrity(folder,max_workers=8)
1854
-
1874
+
1855
1875
  modes = ['cv','pil','skimage','jpeg_trailer']
1856
-
1876
+
1857
1877
  for r in results:
1858
1878
  for mode in modes:
1859
1879
  if r[mode] != 'success':