megadetector 5.0.28__py3-none-any.whl → 10.0.0__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (197) hide show
  1. megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +2 -2
  2. megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +1 -1
  3. megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +1 -1
  4. megadetector/classification/aggregate_classifier_probs.py +3 -3
  5. megadetector/classification/analyze_failed_images.py +5 -5
  6. megadetector/classification/cache_batchapi_outputs.py +5 -5
  7. megadetector/classification/create_classification_dataset.py +11 -12
  8. megadetector/classification/crop_detections.py +10 -10
  9. megadetector/classification/csv_to_json.py +8 -8
  10. megadetector/classification/detect_and_crop.py +13 -15
  11. megadetector/classification/efficientnet/model.py +8 -8
  12. megadetector/classification/efficientnet/utils.py +6 -5
  13. megadetector/classification/evaluate_model.py +7 -7
  14. megadetector/classification/identify_mislabeled_candidates.py +6 -6
  15. megadetector/classification/json_to_azcopy_list.py +1 -1
  16. megadetector/classification/json_validator.py +29 -32
  17. megadetector/classification/map_classification_categories.py +9 -9
  18. megadetector/classification/merge_classification_detection_output.py +12 -9
  19. megadetector/classification/prepare_classification_script.py +19 -19
  20. megadetector/classification/prepare_classification_script_mc.py +26 -26
  21. megadetector/classification/run_classifier.py +4 -4
  22. megadetector/classification/save_mislabeled.py +6 -6
  23. megadetector/classification/train_classifier.py +1 -1
  24. megadetector/classification/train_classifier_tf.py +9 -9
  25. megadetector/classification/train_utils.py +10 -10
  26. megadetector/data_management/annotations/annotation_constants.py +1 -2
  27. megadetector/data_management/camtrap_dp_to_coco.py +79 -46
  28. megadetector/data_management/cct_json_utils.py +103 -103
  29. megadetector/data_management/cct_to_md.py +49 -49
  30. megadetector/data_management/cct_to_wi.py +33 -33
  31. megadetector/data_management/coco_to_labelme.py +75 -75
  32. megadetector/data_management/coco_to_yolo.py +210 -193
  33. megadetector/data_management/databases/add_width_and_height_to_db.py +86 -12
  34. megadetector/data_management/databases/combine_coco_camera_traps_files.py +40 -40
  35. megadetector/data_management/databases/integrity_check_json_db.py +228 -200
  36. megadetector/data_management/databases/subset_json_db.py +33 -33
  37. megadetector/data_management/generate_crops_from_cct.py +88 -39
  38. megadetector/data_management/get_image_sizes.py +54 -49
  39. megadetector/data_management/labelme_to_coco.py +133 -125
  40. megadetector/data_management/labelme_to_yolo.py +159 -73
  41. megadetector/data_management/lila/create_lila_blank_set.py +81 -83
  42. megadetector/data_management/lila/create_lila_test_set.py +32 -31
  43. megadetector/data_management/lila/create_links_to_md_results_files.py +18 -18
  44. megadetector/data_management/lila/download_lila_subset.py +21 -24
  45. megadetector/data_management/lila/generate_lila_per_image_labels.py +365 -107
  46. megadetector/data_management/lila/get_lila_annotation_counts.py +35 -33
  47. megadetector/data_management/lila/get_lila_image_counts.py +22 -22
  48. megadetector/data_management/lila/lila_common.py +73 -70
  49. megadetector/data_management/lila/test_lila_metadata_urls.py +28 -19
  50. megadetector/data_management/mewc_to_md.py +344 -340
  51. megadetector/data_management/ocr_tools.py +262 -255
  52. megadetector/data_management/read_exif.py +249 -227
  53. megadetector/data_management/remap_coco_categories.py +90 -28
  54. megadetector/data_management/remove_exif.py +81 -21
  55. megadetector/data_management/rename_images.py +187 -187
  56. megadetector/data_management/resize_coco_dataset.py +588 -120
  57. megadetector/data_management/speciesnet_to_md.py +41 -41
  58. megadetector/data_management/wi_download_csv_to_coco.py +55 -55
  59. megadetector/data_management/yolo_output_to_md_output.py +248 -122
  60. megadetector/data_management/yolo_to_coco.py +333 -191
  61. megadetector/detection/change_detection.py +832 -0
  62. megadetector/detection/process_video.py +340 -337
  63. megadetector/detection/pytorch_detector.py +358 -278
  64. megadetector/detection/run_detector.py +399 -186
  65. megadetector/detection/run_detector_batch.py +404 -377
  66. megadetector/detection/run_inference_with_yolov5_val.py +340 -327
  67. megadetector/detection/run_tiled_inference.py +257 -249
  68. megadetector/detection/tf_detector.py +24 -24
  69. megadetector/detection/video_utils.py +332 -295
  70. megadetector/postprocessing/add_max_conf.py +19 -11
  71. megadetector/postprocessing/categorize_detections_by_size.py +45 -45
  72. megadetector/postprocessing/classification_postprocessing.py +468 -433
  73. megadetector/postprocessing/combine_batch_outputs.py +23 -23
  74. megadetector/postprocessing/compare_batch_results.py +590 -525
  75. megadetector/postprocessing/convert_output_format.py +106 -102
  76. megadetector/postprocessing/create_crop_folder.py +347 -147
  77. megadetector/postprocessing/detector_calibration.py +173 -168
  78. megadetector/postprocessing/generate_csv_report.py +508 -499
  79. megadetector/postprocessing/load_api_results.py +48 -27
  80. megadetector/postprocessing/md_to_coco.py +133 -102
  81. megadetector/postprocessing/md_to_labelme.py +107 -90
  82. megadetector/postprocessing/md_to_wi.py +40 -40
  83. megadetector/postprocessing/merge_detections.py +92 -114
  84. megadetector/postprocessing/postprocess_batch_results.py +319 -301
  85. megadetector/postprocessing/remap_detection_categories.py +91 -38
  86. megadetector/postprocessing/render_detection_confusion_matrix.py +214 -205
  87. megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +57 -57
  88. megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +27 -28
  89. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +704 -679
  90. megadetector/postprocessing/separate_detections_into_folders.py +226 -211
  91. megadetector/postprocessing/subset_json_detector_output.py +265 -262
  92. megadetector/postprocessing/top_folders_to_bottom.py +45 -45
  93. megadetector/postprocessing/validate_batch_results.py +70 -70
  94. megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +52 -52
  95. megadetector/taxonomy_mapping/map_new_lila_datasets.py +18 -19
  96. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +54 -33
  97. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +67 -67
  98. megadetector/taxonomy_mapping/retrieve_sample_image.py +16 -16
  99. megadetector/taxonomy_mapping/simple_image_download.py +8 -8
  100. megadetector/taxonomy_mapping/species_lookup.py +156 -74
  101. megadetector/taxonomy_mapping/taxonomy_csv_checker.py +14 -14
  102. megadetector/taxonomy_mapping/taxonomy_graph.py +10 -10
  103. megadetector/taxonomy_mapping/validate_lila_category_mappings.py +13 -13
  104. megadetector/utils/ct_utils.py +1049 -211
  105. megadetector/utils/directory_listing.py +21 -77
  106. megadetector/utils/gpu_test.py +22 -22
  107. megadetector/utils/md_tests.py +632 -529
  108. megadetector/utils/path_utils.py +1520 -431
  109. megadetector/utils/process_utils.py +41 -41
  110. megadetector/utils/split_locations_into_train_val.py +62 -62
  111. megadetector/utils/string_utils.py +148 -27
  112. megadetector/utils/url_utils.py +489 -176
  113. megadetector/utils/wi_utils.py +2658 -2526
  114. megadetector/utils/write_html_image_list.py +137 -137
  115. megadetector/visualization/plot_utils.py +34 -30
  116. megadetector/visualization/render_images_with_thumbnails.py +39 -74
  117. megadetector/visualization/visualization_utils.py +487 -435
  118. megadetector/visualization/visualize_db.py +232 -198
  119. megadetector/visualization/visualize_detector_output.py +82 -76
  120. {megadetector-5.0.28.dist-info → megadetector-10.0.0.dist-info}/METADATA +5 -2
  121. megadetector-10.0.0.dist-info/RECORD +139 -0
  122. {megadetector-5.0.28.dist-info → megadetector-10.0.0.dist-info}/WHEEL +1 -1
  123. megadetector/api/batch_processing/api_core/__init__.py +0 -0
  124. megadetector/api/batch_processing/api_core/batch_service/__init__.py +0 -0
  125. megadetector/api/batch_processing/api_core/batch_service/score.py +0 -439
  126. megadetector/api/batch_processing/api_core/server.py +0 -294
  127. megadetector/api/batch_processing/api_core/server_api_config.py +0 -97
  128. megadetector/api/batch_processing/api_core/server_app_config.py +0 -55
  129. megadetector/api/batch_processing/api_core/server_batch_job_manager.py +0 -220
  130. megadetector/api/batch_processing/api_core/server_job_status_table.py +0 -149
  131. megadetector/api/batch_processing/api_core/server_orchestration.py +0 -360
  132. megadetector/api/batch_processing/api_core/server_utils.py +0 -88
  133. megadetector/api/batch_processing/api_core_support/__init__.py +0 -0
  134. megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +0 -46
  135. megadetector/api/batch_processing/api_support/__init__.py +0 -0
  136. megadetector/api/batch_processing/api_support/summarize_daily_activity.py +0 -152
  137. megadetector/api/batch_processing/data_preparation/__init__.py +0 -0
  138. megadetector/api/synchronous/__init__.py +0 -0
  139. megadetector/api/synchronous/api_core/animal_detection_api/__init__.py +0 -0
  140. megadetector/api/synchronous/api_core/animal_detection_api/api_backend.py +0 -151
  141. megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +0 -263
  142. megadetector/api/synchronous/api_core/animal_detection_api/config.py +0 -35
  143. megadetector/api/synchronous/api_core/tests/__init__.py +0 -0
  144. megadetector/api/synchronous/api_core/tests/load_test.py +0 -110
  145. megadetector/data_management/importers/add_nacti_sizes.py +0 -52
  146. megadetector/data_management/importers/add_timestamps_to_icct.py +0 -79
  147. megadetector/data_management/importers/animl_results_to_md_results.py +0 -158
  148. megadetector/data_management/importers/auckland_doc_test_to_json.py +0 -373
  149. megadetector/data_management/importers/auckland_doc_to_json.py +0 -201
  150. megadetector/data_management/importers/awc_to_json.py +0 -191
  151. megadetector/data_management/importers/bellevue_to_json.py +0 -272
  152. megadetector/data_management/importers/cacophony-thermal-importer.py +0 -793
  153. megadetector/data_management/importers/carrizo_shrubfree_2018.py +0 -269
  154. megadetector/data_management/importers/carrizo_trail_cam_2017.py +0 -289
  155. megadetector/data_management/importers/cct_field_adjustments.py +0 -58
  156. megadetector/data_management/importers/channel_islands_to_cct.py +0 -913
  157. megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +0 -180
  158. megadetector/data_management/importers/eMammal/eMammal_helpers.py +0 -249
  159. megadetector/data_management/importers/eMammal/make_eMammal_json.py +0 -223
  160. megadetector/data_management/importers/ena24_to_json.py +0 -276
  161. megadetector/data_management/importers/filenames_to_json.py +0 -386
  162. megadetector/data_management/importers/helena_to_cct.py +0 -283
  163. megadetector/data_management/importers/idaho-camera-traps.py +0 -1407
  164. megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +0 -294
  165. megadetector/data_management/importers/import_desert_lion_conservation_camera_traps.py +0 -387
  166. megadetector/data_management/importers/jb_csv_to_json.py +0 -150
  167. megadetector/data_management/importers/mcgill_to_json.py +0 -250
  168. megadetector/data_management/importers/missouri_to_json.py +0 -490
  169. megadetector/data_management/importers/nacti_fieldname_adjustments.py +0 -79
  170. megadetector/data_management/importers/noaa_seals_2019.py +0 -181
  171. megadetector/data_management/importers/osu-small-animals-to-json.py +0 -364
  172. megadetector/data_management/importers/pc_to_json.py +0 -365
  173. megadetector/data_management/importers/plot_wni_giraffes.py +0 -123
  174. megadetector/data_management/importers/prepare_zsl_imerit.py +0 -131
  175. megadetector/data_management/importers/raic_csv_to_md_results.py +0 -416
  176. megadetector/data_management/importers/rspb_to_json.py +0 -356
  177. megadetector/data_management/importers/save_the_elephants_survey_A.py +0 -320
  178. megadetector/data_management/importers/save_the_elephants_survey_B.py +0 -329
  179. megadetector/data_management/importers/snapshot_safari_importer.py +0 -758
  180. megadetector/data_management/importers/snapshot_serengeti_lila.py +0 -1067
  181. megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +0 -150
  182. megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +0 -153
  183. megadetector/data_management/importers/sulross_get_exif.py +0 -65
  184. megadetector/data_management/importers/timelapse_csv_set_to_json.py +0 -490
  185. megadetector/data_management/importers/ubc_to_json.py +0 -399
  186. megadetector/data_management/importers/umn_to_json.py +0 -507
  187. megadetector/data_management/importers/wellington_to_json.py +0 -263
  188. megadetector/data_management/importers/wi_to_json.py +0 -442
  189. megadetector/data_management/importers/zamba_results_to_md_results.py +0 -180
  190. megadetector/data_management/lila/add_locations_to_island_camera_traps.py +0 -101
  191. megadetector/data_management/lila/add_locations_to_nacti.py +0 -151
  192. megadetector/utils/azure_utils.py +0 -178
  193. megadetector/utils/sas_blob_utils.py +0 -509
  194. megadetector-5.0.28.dist-info/RECORD +0 -209
  195. /megadetector/{api/batch_processing/__init__.py → __init__.py} +0 -0
  196. {megadetector-5.0.28.dist-info → megadetector-10.0.0.dist-info}/licenses/LICENSE +0 -0
  197. {megadetector-5.0.28.dist-info → megadetector-10.0.0.dist-info}/top_level.txt +0 -0
@@ -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,90 +175,95 @@ 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
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
248
248
 
249
249
 
250
- def resize_image(image, target_width=-1, target_height=-1, output_file=None,
251
- no_enlarge_width=False, verbose=False, quality='keep'):
250
+ def resize_image(image,
251
+ target_width=-1,
252
+ target_height=-1,
253
+ output_file=None,
254
+ no_enlarge_width=False,
255
+ verbose=False,
256
+ quality='keep'):
252
257
  """
253
258
  Resizes a PIL Image object to the specified width and height; does not resize
254
259
  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
260
+
261
+ If target_width and target_height are both -1, does not modify the image, but
257
262
  will write to output_file if supplied.
258
-
259
- If no resizing is required, and an Image object is supplied, returns the original Image
263
+
264
+ If no resizing is required, and an Image object is supplied, returns the original Image
260
265
  object (i.e., does not copy).
261
-
266
+
262
267
  Args:
263
268
  image (Image or str): PIL Image object or a filename (local file or URL)
264
269
  target_width (int, optional): width to which we should resize this image, or -1
@@ -267,14 +272,14 @@ def resize_image(image, target_width=-1, target_height=-1, output_file=None,
267
272
  to let target_width determine the size
268
273
  output_file (str, optional): file to which we should save this image; if None,
269
274
  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,
275
+ no_enlarge_width (bool, optional): if [no_enlarge_width] is True, and
276
+ [target width] is larger than the original image width, does not modify the image,
272
277
  but will write to output_file if supplied
273
278
  verbose (bool, optional): enable additional debug output
274
279
  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
280
+
281
+ Returns:
282
+ PIL.Image.Image: the resized image, which may be the original image if no resizing is
278
283
  required
279
284
  """
280
285
 
@@ -282,20 +287,20 @@ def resize_image(image, target_width=-1, target_height=-1, output_file=None,
282
287
  if isinstance(image,str):
283
288
  image_fn = image
284
289
  image = load_image(image)
285
-
290
+
286
291
  if target_width is None:
287
292
  target_width = -1
288
-
293
+
289
294
  if target_height is None:
290
295
  target_height = -1
291
-
296
+
292
297
  resize_required = True
293
-
298
+
294
299
  # No resize was requested, this is always a no-op
295
300
  if target_width == -1 and target_height == -1:
296
-
301
+
297
302
  resize_required = False
298
-
303
+
299
304
  # Does either dimension need to scale according to the other?
300
305
  elif target_width == -1 or target_height == -1:
301
306
 
@@ -309,42 +314,42 @@ def resize_image(image, target_width=-1, target_height=-1, output_file=None,
309
314
  else:
310
315
  # w = ar * h
311
316
  target_width = int(aspect_ratio * target_height)
312
-
317
+
313
318
  # If we're not enlarging images and this would be an enlarge operation
314
319
  if (no_enlarge_width) and (target_width > image.size[0]):
315
-
320
+
316
321
  if verbose:
317
322
  print('Bypassing image enlarge for {} --> {}'.format(
318
323
  image_fn,str(output_file)))
319
324
  resize_required = False
320
-
325
+
321
326
  # If the target size is the same as the original size
322
327
  if (target_width == image.size[0]) and (target_height == image.size[1]):
323
-
324
- resize_required = False
325
-
328
+
329
+ resize_required = False
330
+
326
331
  if not resize_required:
327
-
332
+
328
333
  if output_file is not None:
329
334
  if verbose:
330
335
  print('No resize required for resize {} --> {}'.format(
331
336
  image_fn,str(output_file)))
332
337
  exif_preserving_save(image,output_file,quality=quality,verbose=verbose)
333
338
  return image
334
-
339
+
335
340
  assert target_width > 0 and target_height > 0, \
336
341
  '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,
342
+
343
+ # The antialiasing parameter changed between Pillow versions 9 and 10, and for a bit,
339
344
  # I'd like to support both.
340
345
  try:
341
346
  resized_image = image.resize((target_width, target_height), Image.ANTIALIAS)
342
- except:
347
+ except Exception:
343
348
  resized_image = image.resize((target_width, target_height), Image.Resampling.LANCZOS)
344
-
349
+
345
350
  if output_file is not None:
346
351
  exif_preserving_save(resized_image,output_file,quality=quality,verbose=verbose)
347
-
352
+
348
353
  return resized_image
349
354
 
350
355
  # ...def resize_image(...)
@@ -357,28 +362,28 @@ def crop_image(detections, image, confidence_threshold=0.15, expansion=0):
357
362
 
358
363
  Args:
359
364
  detections (list): a list of dictionaries with keys 'conf' and 'bbox';
360
- boxes are length-four arrays formatted as [x,y,w,h], normalized,
365
+ boxes are length-four arrays formatted as [x,y,w,h], normalized,
361
366
  upper-left origin (this is the standard MD detection format)
362
367
  image (Image or str): the PIL Image object from which we should crop detections,
363
368
  or an image filename
364
369
  confidence_threshold (float, optional): only crop detections above this threshold
365
370
  expansion (int, optional): a number of pixels to include on each side of a cropped
366
371
  detection
367
-
372
+
368
373
  Returns:
369
- list: a possibly-empty list of PIL Image objects
374
+ list: a possibly-empty list of PIL Image objects
370
375
  """
371
376
 
372
377
  ret_images = []
373
378
 
374
379
  if isinstance(image,str):
375
380
  image = load_image(image)
376
-
381
+
377
382
  for detection in detections:
378
383
 
379
384
  score = float(detection['conf'])
380
385
 
381
- if score >= confidence_threshold:
386
+ if (confidence_threshold is None) or (score >= confidence_threshold):
382
387
 
383
388
  x1, y1, w_box, h_box = detection['bbox']
384
389
  ymin,xmin,ymax,xmax = y1, x1, y1 + h_box, x1 + w_box
@@ -417,17 +422,18 @@ def blur_detections(image,detections,blur_radius=40):
417
422
  """
418
423
  Blur the regions in [image] corresponding to the MD-formatted list [detections].
419
424
  [image] is modified in place.
420
-
425
+
421
426
  Args:
422
427
  image (PIL.Image.Image): image in which we should blur specific regions
423
428
  detections (list): list of detections in the MD output format, see render
424
429
  detection_bounding_boxes for more detail.
430
+ blur_radius (int, optional): radius of blur kernel in pixels
425
431
  """
426
-
432
+
427
433
  img_width, img_height = image.size
428
-
434
+
429
435
  for d in detections:
430
-
436
+
431
437
  bbox = d['bbox']
432
438
  x_norm, y_norm, width_norm, height_norm = bbox
433
439
 
@@ -436,29 +442,29 @@ def blur_detections(image,detections,blur_radius=40):
436
442
  y = int(y_norm * img_height)
437
443
  width = int(width_norm * img_width)
438
444
  height = int(height_norm * img_height)
439
-
445
+
440
446
  # Calculate box boundaries
441
447
  left = max(0, x)
442
448
  top = max(0, y)
443
449
  right = min(img_width, x + width)
444
450
  bottom = min(img_height, y + height)
445
-
451
+
446
452
  # Crop the region, blur it, and paste it back
447
453
  region = image.crop((left, top, right, bottom))
448
454
  blurred_region = region.filter(ImageFilter.GaussianBlur(radius=blur_radius))
449
455
  image.paste(blurred_region, (left, top))
450
456
 
451
457
  # ...for each detection
452
-
458
+
453
459
  # ...def blur_detections(...)
454
460
 
455
-
456
- def render_detection_bounding_boxes(detections,
461
+
462
+ def render_detection_bounding_boxes(detections,
457
463
  image,
458
464
  label_map='show_categories',
459
- classification_label_map=None,
460
- confidence_threshold=0,
461
- thickness=DEFAULT_BOX_THICKNESS,
465
+ classification_label_map=None,
466
+ confidence_threshold=0.0,
467
+ thickness=DEFAULT_BOX_THICKNESS,
462
468
  expansion=0,
463
469
  classification_confidence_threshold=0.3,
464
470
  max_classifications=3,
@@ -472,17 +478,16 @@ def render_detection_bounding_boxes(detections,
472
478
  """
473
479
  Renders bounding boxes (with labels and confidence values) on an image for all
474
480
  detections above a threshold.
475
-
481
+
476
482
  Renders classification labels if present.
477
-
483
+
478
484
  [image] is modified in place.
479
485
 
480
486
  Args:
481
-
482
487
  detections (list): list of detections in the MD output format, for example:
483
-
488
+
484
489
  .. code-block::none
485
-
490
+
486
491
  [
487
492
  {
488
493
  "category": "2",
@@ -495,15 +500,15 @@ def render_detection_bounding_boxes(detections,
495
500
  ]
496
501
  }
497
502
  ]
498
-
503
+
499
504
  ...where the bbox coordinates are [x, y, box_width, box_height].
500
-
505
+
501
506
  (0, 0) is the upper-left. Coordinates are normalized.
502
-
507
+
503
508
  Supports classification results, in the standard format:
504
-
509
+
505
510
  .. code-block::none
506
-
511
+
507
512
  [
508
513
  {
509
514
  "category": "2",
@@ -523,30 +528,30 @@ def render_detection_bounding_boxes(detections,
523
528
  ]
524
529
 
525
530
  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,
531
+ label_map (dict, optional): optional, mapping the numeric label to a string name. The type of the
532
+ numeric label (typically strings) needs to be consistent with the keys in label_map; no casting is
533
+ carried out. If [label_map] is None, no labels are shown (not even numbers and confidence values).
534
+ If you want category numbers and confidence values without class labels, use the default value,
530
535
  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
536
+ classification_label_map (dict, optional): optional, mapping of the string class labels to the actual
537
+ class names. The type of the numeric label (typically strings) needs to be consistent with the keys
538
+ in label_map; no casting is carried out. If [label_map] is None, no labels are shown (not even numbers
534
539
  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
540
+ confidence_threshold (float or dict, optional): threshold above which boxes are rendered. Can also be a
541
+ dictionary mapping category IDs to thresholds.
542
+ thickness (int, optional): line thickness in pixels
543
+ expansion (int, optional): number of pixels to expand bounding boxes on each side
544
+ classification_confidence_threshold (float, optional): confidence above which classification results
545
+ are displayed
546
+ max_classifications (int, optional): maximum number of classification results rendered for one image
542
547
  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
547
- custom_strings: optional set of strings to append to detection labels, should have the
548
- 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
+ indexing with the values in [classes]; defaults to a reasonable set of colors
549
+ textalign (int, optional): TEXTALIGN_LEFT, TEXTALIGN_CENTER, or TEXTALIGN_RIGHT
550
+ vtextalign (int, optional): VTEXTALIGN_TOP or VTEXTALIGN_BOTTOM
551
+ label_font_size (float, optional): font size for labels
552
+ custom_strings (list of str, optional): optional set of strings to append to detection labels, should
553
+ have the same length as [detections]. Appended before any classification labels.
554
+ box_sort_order (str, optional): sorting scheme for detection boxes, can be None, "confidence", or
550
555
  "reverse_confidence".
551
556
  verbose (bool, optional): enable additional debug output
552
557
  """
@@ -554,52 +559,52 @@ def render_detection_bounding_boxes(detections,
554
559
  # Input validation
555
560
  if (label_map is not None) and (isinstance(label_map,str)) and (label_map == 'show_categories'):
556
561
  label_map = {}
557
-
562
+
558
563
  if custom_strings is not None:
559
564
  assert len(custom_strings) == len(detections), \
560
565
  '{} custom strings provided for {} detections'.format(
561
566
  len(custom_strings),len(detections))
562
-
567
+
563
568
  display_boxes = []
564
-
569
+
565
570
  # list of lists, one list of strings for each bounding box (to accommodate multiple labels)
566
- display_strs = []
567
-
571
+ display_strs = []
572
+
568
573
  # for color selection
569
- classes = []
574
+ classes = []
570
575
 
571
576
  if box_sort_order is not None:
572
-
573
- if box_sort_order == 'confidence':
577
+
578
+ if box_sort_order == 'confidence':
574
579
  detections = sort_list_of_dicts_by_key(detections,k='conf',reverse=False)
575
580
  elif box_sort_order == 'reverse_confidence':
576
581
  detections = sort_list_of_dicts_by_key(detections,k='conf',reverse=True)
577
582
  else:
578
583
  raise ValueError('Unrecognized sorting scheme {}'.format(box_sort_order))
579
-
584
+
580
585
  for i_detection,detection in enumerate(detections):
581
586
 
582
587
  score = detection['conf']
583
-
588
+
584
589
  if isinstance(confidence_threshold,dict):
585
590
  rendering_threshold = confidence_threshold[detection['category']]
586
591
  else:
587
- rendering_threshold = confidence_threshold
588
-
592
+ rendering_threshold = confidence_threshold
593
+
589
594
  # Always render objects with a confidence of "None", this is typically used
590
- # for ground truth data.
591
- if score is None or score >= rendering_threshold:
592
-
595
+ # for ground truth data.
596
+ if (score is None) or (rendering_threshold is None) or (score >= rendering_threshold):
597
+
593
598
  x1, y1, w_box, h_box = detection['bbox']
594
599
  display_boxes.append([y1, x1, y1 + h_box, x1 + w_box])
595
-
600
+
596
601
  # The class index to use for coloring this box, which may be based on the detection
597
602
  # category or on the most confident classification category.
598
603
  clss = detection['category']
599
-
600
- # This will be a list of strings that should be rendered above/below this box
604
+
605
+ # This will be a list of strings that should be rendered above/below this box
601
606
  displayed_label = []
602
-
607
+
603
608
  if label_map is not None:
604
609
  label = label_map[clss] if clss in label_map else clss
605
610
  if score is not None:
@@ -618,27 +623,27 @@ def render_detection_bounding_boxes(detections,
618
623
  if ('classifications' in detection) and len(detection['classifications']) > 0:
619
624
 
620
625
  classifications = detection['classifications']
621
-
626
+
622
627
  if len(classifications) > max_classifications:
623
628
  classifications = classifications[0:max_classifications]
624
-
629
+
625
630
  max_classification_category = 0
626
631
  max_classification_conf = -100
627
-
632
+
628
633
  for classification in classifications:
629
-
634
+
630
635
  classification_conf = classification[1]
631
636
  if classification_conf is None or \
632
637
  classification_conf < classification_confidence_threshold:
633
638
  continue
634
-
639
+
635
640
  class_key = classification[0]
636
-
641
+
637
642
  # Is this the most confident classification for this detection?
638
643
  if classification_conf > max_classification_conf:
639
644
  max_classification_conf = classification_conf
640
645
  max_classification_category = int(class_key)
641
-
646
+
642
647
  if (classification_label_map is not None) and (class_key in classification_label_map):
643
648
  class_name = classification_label_map[class_key]
644
649
  else:
@@ -647,15 +652,15 @@ def render_detection_bounding_boxes(detections,
647
652
  displayed_label += ['{}: {:5.1%}'.format(class_name.lower(), classification_conf)]
648
653
  else:
649
654
  displayed_label += ['{}'.format(class_name.lower())]
650
-
655
+
651
656
  # ...for each classification
652
657
 
653
658
  # To avoid duplicate colors with detection-only visualization, offset
654
659
  # the classification class index by the number of detection classes
655
660
  clss = annotation_constants.NUM_DETECTOR_CATEGORIES + max_classification_category
656
-
661
+
657
662
  # ...if we have classification results
658
-
663
+
659
664
  # display_strs is a list of labels for each box
660
665
  display_strs.append(displayed_label)
661
666
  classes.append(clss)
@@ -663,16 +668,21 @@ def render_detection_bounding_boxes(detections,
663
668
  # ...if the confidence of this detection is above threshold
664
669
 
665
670
  # ...for each detection
666
-
671
+
667
672
  display_boxes = np.array(display_boxes)
668
673
 
669
674
  if verbose:
670
675
  print('Rendering {} of {} detections'.format(len(display_boxes),len(detections)))
671
-
672
- draw_bounding_boxes_on_image(image, display_boxes, classes,
673
- display_strs=display_strs, thickness=thickness,
674
- expansion=expansion, colormap=colormap,
675
- textalign=textalign, vtextalign=vtextalign,
676
+
677
+ draw_bounding_boxes_on_image(image,
678
+ display_boxes,
679
+ classes,
680
+ display_strs=display_strs,
681
+ thickness=thickness,
682
+ expansion=expansion,
683
+ colormap=colormap,
684
+ textalign=textalign,
685
+ vtextalign=vtextalign,
676
686
  label_font_size=label_font_size)
677
687
 
678
688
  # ...render_detection_bounding_boxes(...)
@@ -693,13 +703,12 @@ def draw_bounding_boxes_on_image(image,
693
703
  Draws bounding boxes on an image. Modifies the image in place.
694
704
 
695
705
  Args:
696
-
697
706
  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
707
+ boxes (np.array): a two-dimensional numpy array of size [N, 4], where N is the
699
708
  number of boxes, and each row is (ymin, xmin, ymax, xmax). Coordinates should be
700
709
  normalized to image height/width.
701
710
  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
711
+ class labels of the boxes. This is only used for color selection. Should have the same
703
712
  length as [boxes].
704
713
  thickness (int, optional): line thickness in pixels
705
714
  expansion (int, optional): number of pixels to expand bounding boxes on each side
@@ -741,29 +750,29 @@ def get_text_size(font,s):
741
750
  """
742
751
  Get the expected width and height when rendering the string [s] in the font
743
752
  [font].
744
-
753
+
745
754
  Args:
746
755
  font (PIL.ImageFont): the font whose size we should query
747
756
  s (str): the string whose size we should query
748
-
757
+
749
758
  Returns:
750
- tuple: (w,h), both floats in pixel coordinates
759
+ tuple: (w,h), both floats in pixel coordinates
751
760
  """
752
-
761
+
753
762
  # This is what we did w/Pillow 9
754
763
  # w,h = font.getsize(s)
755
-
764
+
756
765
  # I would *think* this would be the equivalent for Pillow 10
757
766
  # l,t,r,b = font.getbbox(s); w = r-l; h=b-t
758
-
767
+
759
768
  # ...but this actually produces the most similar results to Pillow 9
760
769
  # l,t,r,b = font.getbbox(s); w = r; h=b
761
-
770
+
762
771
  try:
763
- l,t,r,b = font.getbbox(s); w = r; h=b
772
+ l,t,r,b = font.getbbox(s); w = r; h=b # noqa
764
773
  except Exception:
765
774
  w,h = font.getsize(s)
766
-
775
+
767
776
  return w,h
768
777
 
769
778
 
@@ -779,7 +788,7 @@ def draw_bounding_box_on_image(image,
779
788
  use_normalized_coordinates=True,
780
789
  label_font_size=DEFAULT_LABEL_FONT_SIZE,
781
790
  colormap=None,
782
- textalign=TEXTALIGN_LEFT,
791
+ textalign=TEXTALIGN_LEFT,
783
792
  vtextalign=VTEXTALIGN_TOP,
784
793
  text_rotation=None):
785
794
  """
@@ -794,9 +803,9 @@ def draw_bounding_box_on_image(image,
794
803
  are displayed below the bounding box.
795
804
 
796
805
  Adapted from:
797
-
806
+
798
807
  https://github.com/tensorflow/models/blob/master/research/object_detection/utils/visualization_utils.py
799
-
808
+
800
809
  Args:
801
810
  image (PIL.Image.Image): the image on which we should draw a box
802
811
  ymin (float): ymin of bounding box
@@ -807,24 +816,24 @@ def draw_bounding_box_on_image(image,
807
816
  a color; should be either an integer or a string-formatted integer
808
817
  thickness (int, optional): line thickness in pixels
809
818
  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
819
+ display_str_list (list, optional): list of strings to display above the box (each to be shown on its
811
820
  own line)
812
- use_normalized_coordinates (bool, optional): if True (default), treat coordinates
821
+ use_normalized_coordinates (bool, optional): if True (default), treat coordinates
813
822
  ymin, xmin, ymax, xmax as relative to the image, otherwise coordinates as absolute pixel values
814
- label_font_size (float, optional): font size
823
+ label_font_size (float, optional): font size
815
824
  colormap (list, optional): list of color names, used to choose colors for categories by
816
825
  indexing with the values in [classes]; defaults to a reasonable set of colors
817
- textalign (int, optional): TEXTALIGN_LEFT, TEXTALIGN_CENTER, or TEXTALIGN_RIGHT
826
+ textalign (int, optional): TEXTALIGN_LEFT, TEXTALIGN_CENTER, or TEXTALIGN_RIGHT
818
827
  vtextalign (int, optional): VTEXTALIGN_TOP or VTEXTALIGN_BOTTOM
819
828
  text_rotation (float, optional): rotation to apply to text
820
829
  """
821
-
830
+
822
831
  if colormap is None:
823
832
  colormap = DEFAULT_COLORS
824
-
833
+
825
834
  if display_str_list is None:
826
835
  display_str_list = []
827
-
836
+
828
837
  if clss is None:
829
838
  # Default to the MegaDetector animal class ID (1)
830
839
  color = colormap[1]
@@ -840,12 +849,12 @@ def draw_bounding_box_on_image(image,
840
849
  (left, right, top, bottom) = (xmin, xmax, ymin, ymax)
841
850
 
842
851
  if expansion > 0:
843
-
852
+
844
853
  left -= expansion
845
854
  right += expansion
846
855
  top -= expansion
847
856
  bottom += expansion
848
-
857
+
849
858
  # Deliberately trimming to the width of the image only in the case where
850
859
  # box expansion is turned on. There's not an obvious correct behavior here,
851
860
  # but the thinking is that if the caller provided an out-of-range bounding
@@ -861,9 +870,9 @@ def draw_bounding_box_on_image(image,
861
870
 
862
871
  left = min(left,im_width-1); right = min(right,im_width-1)
863
872
  top = min(top,im_height-1); bottom = min(bottom,im_height-1)
864
-
873
+
865
874
  # ...if we need to expand boxes
866
-
875
+
867
876
  draw.line([(left, top), (left, bottom), (right, bottom),
868
877
  (right, top), (left, top)], width=thickness, fill=color)
869
878
 
@@ -871,52 +880,52 @@ def draw_bounding_box_on_image(image,
871
880
 
872
881
  try:
873
882
  font = ImageFont.truetype('arial.ttf', label_font_size)
874
- except IOError:
883
+ except OSError:
875
884
  font = ImageFont.load_default()
876
-
885
+
877
886
  display_str_heights = [get_text_size(font,ds)[1] for ds in display_str_list]
878
-
887
+
879
888
  # Each display_str has a top and bottom margin of 0.05x.
880
889
  total_display_str_height = (1 + 2 * 0.05) * sum(display_str_heights)
881
-
890
+
882
891
  # Reverse list and print from bottom to top
883
892
  for i_str,display_str in enumerate(display_str_list[::-1]):
884
-
893
+
885
894
  # Skip empty strings
886
895
  if len(display_str) == 0:
887
896
  continue
888
-
889
- text_width, text_height = get_text_size(font,display_str)
897
+
898
+ text_width, text_height = get_text_size(font,display_str)
890
899
  margin = int(np.ceil(0.05 * text_height))
891
-
900
+
892
901
  if text_rotation is not None and text_rotation != 0:
893
-
902
+
894
903
  assert text_rotation == -90, \
895
904
  'Only -90-degree text rotation is supported'
896
-
905
+
897
906
  image_tmp = Image.new('RGB',(text_width+2*margin,text_height+2*margin))
898
907
  image_tmp_draw = ImageDraw.Draw(image_tmp)
899
908
  image_tmp_draw.rectangle([0,0,text_width+2*margin,text_height+2*margin],fill=color)
900
909
  image_tmp_draw.text( (margin,margin), display_str, font=font, fill='black')
901
910
  rotated_text = image_tmp.rotate(text_rotation,expand=1)
902
-
911
+
903
912
  if textalign == TEXTALIGN_RIGHT:
904
913
  text_left = right
905
914
  else:
906
915
  text_left = left
907
916
  text_left = int(text_left + (text_height) * i_str)
908
-
917
+
909
918
  if vtextalign == VTEXTALIGN_BOTTOM:
910
919
  text_top = bottom - text_width
911
920
  else:
912
921
  text_top = top
913
922
  text_left = int(text_left)
914
- text_top = int(text_top)
915
-
923
+ text_top = int(text_top)
924
+
916
925
  image.paste(rotated_text,[text_left,text_top])
917
-
926
+
918
927
  else:
919
-
928
+
920
929
  # If the total height of the display strings added to the top of the bounding
921
930
  # box exceeds the top of the image, stack the strings below the bounding box
922
931
  # instead of above, and vice-versa if we're bottom-aligning.
@@ -933,32 +942,32 @@ def draw_bounding_box_on_image(image,
933
942
  text_bottom = bottom + total_display_str_height
934
943
  if (text_bottom + total_display_str_height) > im_height:
935
944
  text_bottom = top
936
-
945
+
937
946
  text_bottom = int(text_bottom) - i_str * (int(text_height + (2 * margin)))
938
-
947
+
939
948
  text_left = left
940
-
949
+
941
950
  if textalign == TEXTALIGN_RIGHT:
942
951
  text_left = right - text_width
943
952
  elif textalign == TEXTALIGN_CENTER:
944
953
  text_left = ((right + left) / 2.0) - (text_width / 2.0)
945
- text_left = int(text_left)
946
-
954
+ text_left = int(text_left)
955
+
947
956
  draw.rectangle(
948
957
  [(text_left, (text_bottom - text_height) - (2 * margin)),
949
958
  (text_left + text_width, text_bottom)],
950
959
  fill=color)
951
-
960
+
952
961
  draw.text(
953
962
  (text_left + margin, text_bottom - text_height - margin),
954
963
  display_str,
955
964
  fill='black',
956
965
  font=font)
957
-
958
- # ...if we're rotating text
966
+
967
+ # ...if we're rotating text
959
968
 
960
969
  # ...if we're rendering text
961
-
970
+
962
971
  # ...def draw_bounding_box_on_image(...)
963
972
 
964
973
 
@@ -966,9 +975,9 @@ def render_megadb_bounding_boxes(boxes_info, image):
966
975
  """
967
976
  Render bounding boxes to an image, where those boxes are in the mostly-deprecated
968
977
  MegaDB format, which looks like:
969
-
978
+
970
979
  .. code-block::none
971
-
980
+
972
981
  {
973
982
  "category": "animal",
974
983
  "bbox": [
@@ -977,16 +986,16 @@ def render_megadb_bounding_boxes(boxes_info, image):
977
986
  0.187,
978
987
  0.198
979
988
  ]
980
- }
981
-
989
+ }
990
+
982
991
  Args:
983
992
  boxes_info (list): list of dicts, each dict represents a single detection
984
993
  where bbox coordinates are normalized [x_min, y_min, width, height]
985
994
  image (PIL.Image.Image): image to modify
986
-
995
+
987
996
  :meta private:
988
997
  """
989
-
998
+
990
999
  display_boxes = []
991
1000
  display_strs = []
992
1001
  classes = [] # ints, for selecting colors
@@ -1006,32 +1015,34 @@ def render_megadb_bounding_boxes(boxes_info, image):
1006
1015
 
1007
1016
 
1008
1017
  def render_db_bounding_boxes(boxes,
1009
- classes,
1010
- image,
1018
+ classes,
1019
+ image,
1011
1020
  original_size=None,
1012
- label_map=None,
1013
- thickness=DEFAULT_BOX_THICKNESS,
1021
+ label_map=None,
1022
+ thickness=DEFAULT_BOX_THICKNESS,
1014
1023
  expansion=0,
1015
1024
  colormap=None,
1016
1025
  textalign=TEXTALIGN_LEFT,
1017
1026
  vtextalign=VTEXTALIGN_TOP,
1018
1027
  text_rotation=None,
1019
1028
  label_font_size=DEFAULT_LABEL_FONT_SIZE,
1020
- tags=None):
1029
+ tags=None,
1030
+ boxes_are_normalized=False):
1021
1031
  """
1022
1032
  Render bounding boxes (with class labels) on an image. This is a wrapper for
1023
1033
  draw_bounding_boxes_on_image, allowing the caller to operate on a resized image
1024
1034
  by providing the original size of the image; boxes will be scaled accordingly.
1025
-
1035
+
1026
1036
  This function assumes that bounding boxes are in absolute coordinates, typically
1027
- because they come from COCO camera traps .json files.
1028
-
1037
+ because they come from COCO camera traps .json files, unless boxes_are_normalized
1038
+ is True.
1039
+
1029
1040
  Args:
1030
1041
  boxes (list): list of length-4 tuples, foramtted as (x,y,w,h) (in pixels)
1031
1042
  classes (list): list of ints (or string-formatted ints), used to choose labels (either
1032
1043
  by literally rendering the class labels, or by indexing into [label_map])
1033
1044
  image (PIL.Image.Image): image object to modify
1034
- original_size (tuple, optional): if this is not None, and the size is different than
1045
+ original_size (tuple, optional): if this is not None, and the size is different than
1035
1046
  the size of [image], we assume that [boxes] refer to the original size, and we scale
1036
1047
  them accordingly before rendering
1037
1048
  label_map (dict, optional): int --> str dictionary, typically mapping category IDs to
@@ -1047,6 +1058,7 @@ def render_db_bounding_boxes(boxes,
1047
1058
  label_font_size (float, optional): font size for labels
1048
1059
  tags (list, optional): list of strings of length len(boxes) that should be appended
1049
1060
  after each class name (e.g. to show scores)
1061
+ boxes_are_normalized (bool, optional): whether boxes have already been normalized
1050
1062
  """
1051
1063
 
1052
1064
  display_boxes = []
@@ -1063,38 +1075,48 @@ def render_db_bounding_boxes(boxes,
1063
1075
 
1064
1076
  box = boxes[i_box]
1065
1077
  clss = classes[i_box]
1066
-
1078
+
1067
1079
  x_min_abs, y_min_abs, width_abs, height_abs = box[0:4]
1068
1080
 
1069
- ymin = y_min_abs / img_height
1070
- ymax = ymin + height_abs / img_height
1081
+ # Normalize boxes if necessary
1082
+ if boxes_are_normalized:
1071
1083
 
1072
- xmin = x_min_abs / img_width
1073
- xmax = xmin + width_abs / img_width
1084
+ xmin = x_min_abs
1085
+ xmax = x_min_abs + width_abs
1086
+ ymin = y_min_abs
1087
+ ymax = y_min_abs + height_abs
1088
+
1089
+ else:
1090
+
1091
+ ymin = y_min_abs / img_height
1092
+ ymax = ymin + height_abs / img_height
1093
+
1094
+ xmin = x_min_abs / img_width
1095
+ xmax = xmin + width_abs / img_width
1074
1096
 
1075
1097
  display_boxes.append([ymin, xmin, ymax, xmax])
1076
1098
 
1077
1099
  if label_map:
1078
1100
  clss = label_map[int(clss)]
1079
-
1101
+
1080
1102
  display_str = str(clss)
1081
-
1103
+
1082
1104
  # Do we have a tag to append to the class string?
1083
1105
  if tags is not None and tags[i_box] is not None and len(tags[i_box]) > 0:
1084
1106
  display_str += ' ' + tags[i_box]
1085
-
1107
+
1086
1108
  # need to be a string here because PIL needs to iterate through chars
1087
1109
  display_strs.append([display_str])
1088
1110
 
1089
1111
  # ...for each box
1090
-
1112
+
1091
1113
  display_boxes = np.array(display_boxes)
1092
-
1093
- draw_bounding_boxes_on_image(image,
1094
- display_boxes,
1095
- classes,
1114
+
1115
+ draw_bounding_boxes_on_image(image,
1116
+ display_boxes,
1117
+ classes,
1096
1118
  display_strs=display_strs,
1097
- thickness=thickness,
1119
+ thickness=thickness,
1098
1120
  expansion=expansion,
1099
1121
  colormap=colormap,
1100
1122
  textalign=textalign,
@@ -1105,12 +1127,12 @@ def render_db_bounding_boxes(boxes,
1105
1127
  # ...def render_db_bounding_boxes(...)
1106
1128
 
1107
1129
 
1108
- def draw_bounding_boxes_on_file(input_file,
1109
- output_file,
1110
- detections,
1130
+ def draw_bounding_boxes_on_file(input_file,
1131
+ output_file,
1132
+ detections,
1111
1133
  confidence_threshold=0.0,
1112
1134
  detector_label_map=DEFAULT_DETECTOR_LABEL_MAP,
1113
- thickness=DEFAULT_BOX_THICKNESS,
1135
+ thickness=DEFAULT_BOX_THICKNESS,
1114
1136
  expansion=0,
1115
1137
  colormap=None,
1116
1138
  label_font_size=DEFAULT_LABEL_FONT_SIZE,
@@ -1118,17 +1140,19 @@ def draw_bounding_boxes_on_file(input_file,
1118
1140
  target_size=None,
1119
1141
  ignore_exif_rotation=False):
1120
1142
  """
1121
- Renders detection bounding boxes on an image loaded from file, optionally writing the results to
1143
+ Renders detection bounding boxes on an image loaded from file, optionally writing the results to
1122
1144
  a new image file.
1123
-
1145
+
1124
1146
  Args:
1125
1147
  input_file (str): filename or URL to load
1126
- output_file (str, optional): filename to which we should write the rendered image
1148
+ output_file (str): filename to which we should write the rendered image
1127
1149
  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,
1150
+ boxes are length-four arrays formatted as [x,y,w,h], normalized,
1129
1151
  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
1152
+ confidence_threshold (float, optional): only render detections with confidence above this
1153
+ threshold
1154
+ detector_label_map (dict, optional): a dict mapping category IDs to strings. If this
1155
+ is None, no confidence values or identifiers are shown. If this is {}, just category
1132
1156
  indices and confidence values are shown.
1133
1157
  thickness (int, optional): line width in pixels for box rendering
1134
1158
  expansion (int, optional): box expansion in pixels
@@ -1141,45 +1165,50 @@ def draw_bounding_boxes_on_file(input_file,
1141
1165
  see resize_image() for documentation. If None or (-1,-1), uses the original image size.
1142
1166
  ignore_exif_rotation (bool, optional): don't rotate the loaded pixels,
1143
1167
  even if we are loading a JPEG and that JPEG says it should be rotated.
1144
-
1168
+
1145
1169
  Returns:
1146
1170
  PIL.Image.Image: loaded and modified image
1147
1171
  """
1148
-
1172
+
1149
1173
  image = open_image(input_file, ignore_exif_rotation=ignore_exif_rotation)
1150
-
1174
+
1151
1175
  if target_size is not None:
1152
1176
  image = resize_image(image,target_size[0],target_size[1])
1153
-
1177
+
1154
1178
  render_detection_bounding_boxes(
1155
- detections, image, label_map=detector_label_map,
1179
+ detections,
1180
+ image,
1181
+ label_map=detector_label_map,
1156
1182
  confidence_threshold=confidence_threshold,
1157
- thickness=thickness,expansion=expansion,colormap=colormap,
1158
- custom_strings=custom_strings,label_font_size=label_font_size)
1183
+ thickness=thickness,
1184
+ expansion=expansion,
1185
+ colormap=colormap,
1186
+ custom_strings=custom_strings,
1187
+ label_font_size=label_font_size)
1159
1188
 
1160
1189
  if output_file is not None:
1161
1190
  image.save(output_file)
1162
-
1191
+
1163
1192
  return image
1164
1193
 
1165
1194
 
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,
1195
+ def draw_db_boxes_on_file(input_file,
1196
+ output_file,
1197
+ boxes,
1198
+ classes=None,
1199
+ label_map=None,
1200
+ thickness=DEFAULT_BOX_THICKNESS,
1172
1201
  expansion=0,
1173
1202
  ignore_exif_rotation=False):
1174
1203
  """
1175
- Render COCO-formatted bounding boxes (in absolute coordinates) on an image loaded from file,
1204
+ Render COCO-formatted bounding boxes (in absolute coordinates) on an image loaded from file,
1176
1205
  writing the results to a new image file.
1177
1206
 
1178
1207
  Args:
1179
1208
  input_file (str): image file to read
1180
1209
  output_file (str): image file to write
1181
1210
  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
1211
+ classes (list, optional): list of ints (or string-formatted ints), used to choose
1183
1212
  labels (either by literally rendering the class labels, or by indexing into [label_map])
1184
1213
  label_map (dict, optional): int --> str dictionary, typically mapping category IDs to
1185
1214
  species labels; if None, category labels are rendered verbatim (typically as numbers)
@@ -1188,90 +1217,95 @@ def draw_db_boxes_on_file(input_file,
1188
1217
  detection
1189
1218
  ignore_exif_rotation (bool, optional): don't rotate the loaded pixels,
1190
1219
  even if we are loading a JPEG and that JPEG says it should be rotated
1191
-
1220
+
1192
1221
  Returns:
1193
1222
  PIL.Image.Image: the loaded and modified image
1194
1223
  """
1195
-
1224
+
1196
1225
  image = open_image(input_file, ignore_exif_rotation=ignore_exif_rotation)
1197
1226
 
1198
1227
  if classes is None:
1199
1228
  classes = [0] * len(boxes)
1200
-
1201
- render_db_bounding_boxes(boxes, classes, image, original_size=None,
1202
- label_map=label_map, thickness=thickness, expansion=expansion)
1203
-
1229
+
1230
+ render_db_bounding_boxes(boxes,
1231
+ classes,
1232
+ image,
1233
+ original_size=None,
1234
+ label_map=label_map,
1235
+ thickness=thickness,
1236
+ expansion=expansion)
1237
+
1204
1238
  image.save(output_file)
1205
-
1239
+
1206
1240
  return image
1207
-
1241
+
1208
1242
  # ...def draw_bounding_boxes_on_file(...)
1209
1243
 
1210
1244
 
1211
1245
  def gray_scale_fraction(image,crop_size=(0.1,0.1)):
1212
1246
  """
1213
- Computes the fraction of the pixels in [image] that appear to be grayscale (R==G==B),
1247
+ Computes the fraction of the pixels in [image] that appear to be grayscale (R==G==B),
1214
1248
  useful for approximating whether this is a night-time image when flash information is not
1215
1249
  available in EXIF data (or for video frames, where this information is often not available
1216
1250
  in structured metadata at all).
1217
-
1251
+
1218
1252
  Args:
1219
1253
  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
1254
+ crop_size (tuple of floats, optional): a 2-element list/tuple, representing the fraction of
1255
+ the image to crop at the top and bottom, respectively, before analyzing (to minimize
1222
1256
  the possibility of including color elements in the image overlay)
1223
-
1257
+
1224
1258
  Returns:
1225
1259
  float: the fraction of pixels in [image] that appear to be grayscale (R==G==B)
1226
1260
  """
1227
-
1261
+
1228
1262
  if isinstance(image,str):
1229
1263
  image = Image.open(image)
1230
-
1264
+
1231
1265
  if image.mode == 'L':
1232
1266
  return 1.0
1233
-
1267
+
1234
1268
  if len(image.getbands()) == 1:
1235
1269
  return 1.0
1236
-
1270
+
1237
1271
  # Crop if necessary
1238
1272
  if crop_size[0] > 0 or crop_size[1] > 0:
1239
-
1273
+
1240
1274
  assert (crop_size[0] + crop_size[1]) < 1.0, \
1241
1275
  print('Illegal crop size: {}'.format(str(crop_size)))
1242
-
1276
+
1243
1277
  top_crop_pixels = int(image.height * crop_size[0])
1244
1278
  bottom_crop_pixels = int(image.height * crop_size[1])
1245
-
1279
+
1246
1280
  left = 0
1247
1281
  right = image.width
1248
-
1282
+
1249
1283
  # Remove pixels from the top
1250
1284
  first_crop_top = top_crop_pixels
1251
- first_crop_bottom = image.height
1285
+ first_crop_bottom = image.height
1252
1286
  first_crop = image.crop((left, first_crop_top, right, first_crop_bottom))
1253
-
1287
+
1254
1288
  # Remove pixels from the bottom
1255
1289
  second_crop_top = 0
1256
1290
  second_crop_bottom = first_crop.height - bottom_crop_pixels
1257
1291
  second_crop = first_crop.crop((left, second_crop_top, right, second_crop_bottom))
1258
-
1292
+
1259
1293
  image = second_crop
1260
-
1294
+
1261
1295
  # It doesn't matter if these are actually R/G/B, they're just names
1262
1296
  r = np.array(image.getchannel(0))
1263
1297
  g = np.array(image.getchannel(1))
1264
1298
  b = np.array(image.getchannel(2))
1265
-
1299
+
1266
1300
  gray_pixels = np.logical_and(r == g, r == b)
1267
1301
  n_pixels = gray_pixels.size
1268
1302
  n_gray_pixels = gray_pixels.sum()
1269
-
1303
+
1270
1304
  return n_gray_pixels / n_pixels
1271
1305
 
1272
1306
  # Non-numpy way to do the same thing, briefly keeping this here for posterity
1273
1307
  if False:
1274
-
1308
+
1275
1309
  w, h = image.size
1276
1310
  n_pixels = w*h
1277
1311
  n_gray_pixels = 0
@@ -1279,25 +1313,25 @@ def gray_scale_fraction(image,crop_size=(0.1,0.1)):
1279
1313
  for j in range(h):
1280
1314
  r, g, b = image.getpixel((i,j))
1281
1315
  if r == g and r == b and g == b:
1282
- n_gray_pixels += 1
1316
+ n_gray_pixels += 1
1283
1317
 
1284
1318
  # ...def gray_scale_fraction(...)
1285
1319
 
1286
1320
 
1287
1321
  def _resize_relative_image(fn_relative,
1288
- input_folder,
1289
- output_folder,
1290
- target_width,
1291
- target_height,
1292
- no_enlarge_width,
1293
- verbose,
1294
- quality,
1295
- overwrite=True):
1322
+ input_folder,
1323
+ output_folder,
1324
+ target_width,
1325
+ target_height,
1326
+ no_enlarge_width,
1327
+ verbose,
1328
+ quality,
1329
+ overwrite=True):
1296
1330
  """
1297
1331
  Internal function for resizing an image from one folder to another,
1298
1332
  maintaining relative path.
1299
1333
  """
1300
-
1334
+
1301
1335
  input_fn_abs = os.path.join(input_folder,fn_relative)
1302
1336
  output_fn_abs = os.path.join(output_folder,fn_relative)
1303
1337
 
@@ -1305,13 +1339,16 @@ def _resize_relative_image(fn_relative,
1305
1339
  status = 'skipped'
1306
1340
  error = None
1307
1341
  return {'fn_relative':fn_relative,'status':status,'error':error}
1308
-
1342
+
1309
1343
  os.makedirs(os.path.dirname(output_fn_abs),exist_ok=True)
1310
1344
  try:
1311
- _ = resize_image(input_fn_abs,
1312
- output_file=output_fn_abs,
1313
- target_width=target_width, target_height=target_height,
1314
- no_enlarge_width=no_enlarge_width, verbose=verbose, quality=quality)
1345
+ _ = resize_image(input_fn_abs,
1346
+ output_file=output_fn_abs,
1347
+ target_width=target_width,
1348
+ target_height=target_height,
1349
+ no_enlarge_width=no_enlarge_width,
1350
+ verbose=verbose,
1351
+ quality=quality)
1315
1352
  status = 'success'
1316
1353
  error = None
1317
1354
  except Exception as e:
@@ -1319,27 +1356,33 @@ def _resize_relative_image(fn_relative,
1319
1356
  print('Error resizing {}: {}'.format(fn_relative,str(e)))
1320
1357
  status = 'error'
1321
1358
  error = str(e)
1322
-
1359
+
1323
1360
  return {'fn_relative':fn_relative,'status':status,'error':error}
1324
1361
 
1325
1362
  # ...def _resize_relative_image(...)
1326
1363
 
1327
1364
 
1328
1365
  def _resize_absolute_image(input_output_files,
1329
- target_width,target_height,no_enlarge_width,verbose,quality):
1330
-
1366
+ target_width,
1367
+ target_height,
1368
+ no_enlarge_width,
1369
+ verbose,
1370
+ quality):
1331
1371
  """
1332
1372
  Internal wrapper for resize_image used in the context of a batch resize operation.
1333
1373
  """
1334
-
1374
+
1335
1375
  input_fn_abs = input_output_files[0]
1336
1376
  output_fn_abs = input_output_files[1]
1337
1377
  os.makedirs(os.path.dirname(output_fn_abs),exist_ok=True)
1338
1378
  try:
1339
- _ = resize_image(input_fn_abs,
1340
- output_file=output_fn_abs,
1341
- target_width=target_width, target_height=target_height,
1342
- no_enlarge_width=no_enlarge_width, verbose=verbose, quality=quality)
1379
+ _ = resize_image(input_fn_abs,
1380
+ output_file=output_fn_abs,
1381
+ target_width=target_width,
1382
+ target_height=target_height,
1383
+ no_enlarge_width=no_enlarge_width,
1384
+ verbose=verbose,
1385
+ quality=quality)
1343
1386
  status = 'success'
1344
1387
  error = None
1345
1388
  except Exception as e:
@@ -1347,7 +1390,7 @@ def _resize_absolute_image(input_output_files,
1347
1390
  print('Error resizing {}: {}'.format(input_fn_abs,str(e)))
1348
1391
  status = 'error'
1349
1392
  error = str(e)
1350
-
1393
+
1351
1394
  return {'input_fn':input_fn_abs,'output_fn':output_fn_abs,status:'status',
1352
1395
  'error':error}
1353
1396
 
@@ -1355,21 +1398,21 @@ def _resize_absolute_image(input_output_files,
1355
1398
 
1356
1399
 
1357
1400
  def resize_images(input_file_to_output_file,
1358
- target_width=-1,
1401
+ target_width=-1,
1359
1402
  target_height=-1,
1360
- no_enlarge_width=False,
1361
- verbose=False,
1403
+ no_enlarge_width=False,
1404
+ verbose=False,
1362
1405
  quality='keep',
1363
- pool_type='process',
1406
+ pool_type='process',
1364
1407
  n_workers=10):
1365
1408
  """
1366
1409
  Resizes all images the dictionary [input_file_to_output_file].
1367
1410
 
1368
1411
  TODO: This is a little more redundant with resize_image_folder than I would like;
1369
1412
  refactor resize_image_folder to call resize_images. Not doing that yet because
1370
- at the time I'm writing this comment, a lot of code depends on resize_image_folder
1413
+ at the time I'm writing this comment, a lot of code depends on resize_image_folder
1371
1414
  and I don't want to rock the boat yet.
1372
-
1415
+
1373
1416
  Args:
1374
1417
  input_file_to_output_file (dict): dict mapping images that exist to the locations
1375
1418
  where the resized versions should be written
@@ -1377,8 +1420,8 @@ def resize_images(input_file_to_output_file,
1377
1420
  to let target_height determine the size
1378
1421
  target_height (int, optional): height to which we should resize this image, or -1
1379
1422
  to let target_width determine the size
1380
- no_enlarge_width (bool, optional): if [no_enlarge_width] is True, and
1381
- [target width] is larger than the original image width, does not modify the image,
1423
+ no_enlarge_width (bool, optional): if [no_enlarge_width] is True, and
1424
+ [target width] is larger than the original image width, does not modify the image,
1382
1425
  but will write to output_file if supplied
1383
1426
  verbose (bool, optional): enable additional debug output
1384
1427
  quality (str or int, optional): passed to exif_preserving_save, see docs for more detail
@@ -1389,20 +1432,20 @@ def resize_images(input_file_to_output_file,
1389
1432
 
1390
1433
  Returns:
1391
1434
  list: a list of dicts with keys 'input_fn', 'output_fn', 'status', and 'error'.
1392
- 'status' will be 'success' or 'error'; 'error' will be None for successful cases,
1435
+ 'status' will be 'success' or 'error'; 'error' will be None for successful cases,
1393
1436
  otherwise will contain the image-specific error.
1394
1437
  """
1395
-
1438
+
1396
1439
  assert pool_type in ('process','thread'), 'Illegal pool type {}'.format(pool_type)
1397
-
1440
+
1398
1441
  input_output_file_pairs = []
1399
-
1442
+
1400
1443
  # Reformat input files as (input,output) tuples
1401
1444
  for input_fn in input_file_to_output_file:
1402
1445
  input_output_file_pairs.append((input_fn,input_file_to_output_file[input_fn]))
1403
-
1404
- if n_workers == 1:
1405
-
1446
+
1447
+ if n_workers == 1:
1448
+
1406
1449
  results = []
1407
1450
  for i_o_file_pair in tqdm(input_output_file_pairs):
1408
1451
  results.append(_resize_absolute_image(i_o_file_pair,
@@ -1413,47 +1456,54 @@ def resize_images(input_file_to_output_file,
1413
1456
  quality=quality))
1414
1457
 
1415
1458
  else:
1416
-
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)))
1459
+
1460
+ pool = None
1461
+
1462
+ try:
1463
+ if pool_type == 'thread':
1464
+ pool = ThreadPool(n_workers); poolstring = 'threads'
1465
+ else:
1466
+ assert pool_type == 'process'
1467
+ pool = Pool(n_workers); poolstring = 'processes'
1468
+
1469
+ if verbose:
1470
+ print('Starting resizing pool with {} {}'.format(n_workers,poolstring))
1471
+
1472
+ p = partial(_resize_absolute_image,
1473
+ target_width=target_width,
1474
+ target_height=target_height,
1475
+ no_enlarge_width=no_enlarge_width,
1476
+ verbose=verbose,
1477
+ quality=quality)
1478
+
1479
+ results = list(tqdm(pool.imap(p, input_output_file_pairs),total=len(input_output_file_pairs)))
1480
+ finally:
1481
+ pool.close()
1482
+ pool.join()
1483
+ print("Pool closed and joined for image resizing")
1434
1484
 
1435
1485
  return results
1436
1486
 
1437
1487
  # ...def resize_images(...)
1438
1488
 
1439
1489
 
1440
- def resize_image_folder(input_folder,
1490
+ def resize_image_folder(input_folder,
1441
1491
  output_folder=None,
1442
- target_width=-1,
1492
+ target_width=-1,
1443
1493
  target_height=-1,
1444
- no_enlarge_width=False,
1445
- verbose=False,
1494
+ no_enlarge_width=False,
1495
+ verbose=False,
1446
1496
  quality='keep',
1447
- pool_type='process',
1448
- n_workers=10,
1497
+ pool_type='process',
1498
+ n_workers=10,
1449
1499
  recursive=True,
1450
1500
  image_files_relative=None,
1451
1501
  overwrite=True):
1452
1502
  """
1453
1503
  Resize all images in a folder (defaults to recursive).
1454
-
1504
+
1455
1505
  Defaults to in-place resizing (output_folder is optional).
1456
-
1506
+
1457
1507
  Args:
1458
1508
  input_folder (str): folder in which we should find images to resize
1459
1509
  output_folder (str, optional): folder in which we should write resized images. If
@@ -1463,8 +1513,8 @@ def resize_image_folder(input_folder,
1463
1513
  to let target_height determine the size
1464
1514
  target_height (int, optional): height to which we should resize this image, or -1
1465
1515
  to let target_width determine the size
1466
- no_enlarge_width (bool, optional): if [no_enlarge_width] is True, and
1467
- [target width] is larger than the original image width, does not modify the image,
1516
+ no_enlarge_width (bool, optional): if [no_enlarge_width] is True, and
1517
+ [target width] is larger than the original image width, does not modify the image,
1468
1518
  but will write to output_file if supplied
1469
1519
  verbose (bool, optional): enable additional debug output
1470
1520
  quality (str or int, optional): passed to exif_preserving_save, see docs for more detail
@@ -1476,34 +1526,34 @@ def resize_image_folder(input_folder,
1476
1526
  image_files_relative (list, optional): if not None, skips any relative paths not
1477
1527
  in this list
1478
1528
  overwrite (bool, optional): whether to overwrite existing target images
1479
-
1529
+
1480
1530
  Returns:
1481
1531
  list: a list of dicts with keys 'input_fn', 'output_fn', 'status', and 'error'.
1482
- 'status' will be 'success', 'skipped', or 'error'; 'error' will be None for successful
1532
+ 'status' will be 'success', 'skipped', or 'error'; 'error' will be None for successful
1483
1533
  cases, otherwise will contain the image-specific error.
1484
1534
  """
1485
1535
 
1486
1536
  assert os.path.isdir(input_folder), '{} is not a folder'.format(input_folder)
1487
-
1537
+
1488
1538
  if output_folder is None:
1489
1539
  output_folder = input_folder
1490
1540
  else:
1491
1541
  os.makedirs(output_folder,exist_ok=True)
1492
-
1542
+
1493
1543
  assert pool_type in ('process','thread'), 'Illegal pool type {}'.format(pool_type)
1494
-
1544
+
1495
1545
  if image_files_relative is None:
1496
-
1546
+
1497
1547
  if verbose:
1498
1548
  print('Enumerating images')
1499
-
1549
+
1500
1550
  image_files_relative = find_images(input_folder,recursive=recursive,
1501
1551
  return_relative_paths=True,convert_slashes=True)
1502
1552
  if verbose:
1503
1553
  print('Found {} images'.format(len(image_files_relative)))
1504
-
1505
- if n_workers == 1:
1506
-
1554
+
1555
+ if n_workers == 1:
1556
+
1507
1557
  if verbose:
1508
1558
  print('Resizing images')
1509
1559
 
@@ -1520,16 +1570,16 @@ def resize_image_folder(input_folder,
1520
1570
  overwrite=overwrite))
1521
1571
 
1522
1572
  else:
1523
-
1573
+
1524
1574
  if pool_type == 'thread':
1525
- pool = ThreadPool(n_workers); poolstring = 'threads'
1575
+ pool = ThreadPool(n_workers); poolstring = 'threads'
1526
1576
  else:
1527
1577
  assert pool_type == 'process'
1528
1578
  pool = Pool(n_workers); poolstring = 'processes'
1529
-
1579
+
1530
1580
  if verbose:
1531
1581
  print('Starting resizing pool with {} {}'.format(n_workers,poolstring))
1532
-
1582
+
1533
1583
  p = partial(_resize_relative_image,
1534
1584
  input_folder=input_folder,
1535
1585
  output_folder=output_folder,
@@ -1539,8 +1589,9 @@ def resize_image_folder(input_folder,
1539
1589
  verbose=verbose,
1540
1590
  quality=quality,
1541
1591
  overwrite=overwrite)
1542
-
1543
- results = list(tqdm(pool.imap(p, image_files_relative),total=len(image_files_relative)))
1592
+
1593
+ results = list(tqdm(pool.imap(p, image_files_relative),
1594
+ total=len(image_files_relative)))
1544
1595
 
1545
1596
  return results
1546
1597
 
@@ -1550,17 +1601,18 @@ def resize_image_folder(input_folder,
1550
1601
  def get_image_size(im,verbose=False):
1551
1602
  """
1552
1603
  Retrieve the size of an image. Returns None if the image fails to load.
1553
-
1604
+
1554
1605
  Args:
1555
1606
  im (str or PIL.Image): filename or PIL image
1556
-
1607
+ verbose (bool, optional): enable additional debug output
1608
+
1557
1609
  Returns:
1558
1610
  tuple (w,h), or None if the image fails to load.
1559
1611
  """
1560
-
1612
+
1561
1613
  image_name = '[in memory]'
1562
-
1563
- try:
1614
+
1615
+ try:
1564
1616
  if isinstance(im,str):
1565
1617
  image_name = im
1566
1618
  im = load_image(im)
@@ -1577,66 +1629,66 @@ def get_image_size(im,verbose=False):
1577
1629
  print('Error reading width from image {}: {}'.format(
1578
1630
  image_name,str(e)))
1579
1631
  return None
1580
-
1632
+
1581
1633
  # ...def get_image_size(...)
1582
1634
 
1583
1635
 
1584
1636
  def parallel_get_image_sizes(filenames,
1585
- max_workers=16,
1586
- use_threads=True,
1637
+ max_workers=16,
1638
+ use_threads=True,
1587
1639
  recursive=True,
1588
1640
  verbose=False):
1589
1641
  """
1590
1642
  Retrieve image sizes for a list or folder of images
1591
-
1643
+
1592
1644
  Args:
1593
- filenames (list or str): a list of image filenames or a folder. Non-image files and
1645
+ filenames (list or str): a list of image filenames or a folder. Non-image files and
1594
1646
  unreadable images will be returned with a file size of None.
1595
1647
  max_workers (int, optional): the number of parallel workers to use; set to <=1 to disable
1596
1648
  parallelization
1597
1649
  use_threads (bool, optional): whether to use threads (True) or processes (False) for
1598
1650
  parallelization
1599
- recursive (bool, optional): if [filenames] is a folder, whether to search recursively for images.
1600
- Ignored if [filenames] is a list.
1651
+ recursive (bool, optional): if [filenames] is a folder, whether to search recursively
1652
+ for images. Ignored if [filenames] is a list.
1601
1653
  verbose (bool, optional): enable additional debug output
1602
-
1654
+
1603
1655
  Returns:
1604
1656
  dict: a dict mapping filenames to (w,h) tuples; the value will be None for images that fail
1605
- to load.
1657
+ to load. Filenames will always be absolute.
1606
1658
  """
1607
1659
 
1608
1660
  if isinstance(filenames,str) and os.path.isdir(filenames):
1609
1661
  if verbose:
1610
1662
  print('Enumerating images in {}'.format(filenames))
1611
1663
  filenames = find_images(filenames,recursive=recursive,return_relative_paths=False)
1612
-
1664
+
1613
1665
  n_workers = min(max_workers,len(filenames))
1614
-
1666
+
1615
1667
  if verbose:
1616
1668
  print('Getting image sizes for {} images'.format(len(filenames)))
1617
-
1669
+
1618
1670
  if n_workers <= 1:
1619
-
1671
+
1620
1672
  results = []
1621
1673
  for filename in filenames:
1622
1674
  results.append(get_image_size(filename,verbose=verbose))
1623
-
1675
+
1624
1676
  else:
1625
-
1677
+
1626
1678
  if use_threads:
1627
1679
  pool = ThreadPool(n_workers)
1628
1680
  else:
1629
1681
  pool = Pool(n_workers)
1630
-
1682
+
1631
1683
  results = list(tqdm(pool.imap(
1632
1684
  partial(get_image_size,verbose=verbose),filenames), total=len(filenames)))
1633
-
1685
+
1634
1686
  assert len(filenames) == len(results), 'Internal error in parallel_get_image_sizes'
1635
-
1687
+
1636
1688
  to_return = {}
1637
1689
  for i_file,filename in enumerate(filenames):
1638
1690
  to_return[filename] = results[i_file]
1639
-
1691
+
1640
1692
  return to_return
1641
1693
 
1642
1694
 
@@ -1645,30 +1697,30 @@ def parallel_get_image_sizes(filenames,
1645
1697
  def check_image_integrity(filename,modes=None):
1646
1698
  """
1647
1699
  Check whether we can successfully load an image via OpenCV and/or PIL.
1648
-
1649
- Args:
1700
+
1701
+ Args:
1650
1702
  filename (str): the filename to evaluate
1651
1703
  modes (list, optional): a list containing one or more of:
1652
-
1704
+
1653
1705
  - 'cv'
1654
1706
  - 'pil'
1655
1707
  - 'skimage'
1656
- - 'jpeg_trailer'
1657
-
1708
+ - 'jpeg_trailer'
1709
+
1658
1710
  'jpeg_trailer' checks that the binary data ends with ffd9. It does not check whether
1659
1711
  the image is actually a jpeg, and even if it is, there are lots of reasons the image might not
1660
1712
  end with ffd9. It's also true the JPEGs that cause "premature end of jpeg segment" issues
1661
1713
  don't end with ffd9, so this may be a useful diagnostic. High precision, very low recall
1662
1714
  for corrupt jpegs.
1663
-
1715
+
1664
1716
  Set to None to use all modes.
1665
-
1717
+
1666
1718
  Returns:
1667
1719
  dict: a dict with a key called 'file' (the value of [filename]), one key for each string in
1668
1720
  [modes] (a success indicator for that mode, specifically a string starting with either
1669
1721
  'success' or 'error').
1670
1722
  """
1671
-
1723
+
1672
1724
  if modes is None:
1673
1725
  modes = ('cv','pil','skimage','jpeg_trailer')
1674
1726
  else:
@@ -1676,14 +1728,14 @@ def check_image_integrity(filename,modes=None):
1676
1728
  modes = [modes]
1677
1729
  for mode in modes:
1678
1730
  assert mode in ('cv','pil','skimage'), 'Unrecognized mode {}'.format(mode)
1679
-
1731
+
1680
1732
  assert os.path.isfile(filename), 'Could not find file {}'.format(filename)
1681
-
1733
+
1682
1734
  result = {}
1683
1735
  result['file'] = filename
1684
-
1736
+
1685
1737
  for mode in modes:
1686
-
1738
+
1687
1739
  result[mode] = 'unknown'
1688
1740
  if mode == 'pil':
1689
1741
  try:
@@ -1700,10 +1752,10 @@ def check_image_integrity(filename,modes=None):
1700
1752
  result[mode] = 'success'
1701
1753
  except Exception as e:
1702
1754
  result[mode] = 'error: {}'.format(str(e))
1703
- elif mode == 'skimage':
1755
+ elif mode == 'skimage':
1704
1756
  try:
1705
1757
  # This is not a standard dependency
1706
- from skimage import io as skimage_io # noqa
1758
+ from skimage import io as skimage_io # type: ignore # noqa
1707
1759
  except Exception:
1708
1760
  result[mode] = 'could not import skimage, run pip install scikit-image'
1709
1761
  return result
@@ -1724,26 +1776,26 @@ def check_image_integrity(filename,modes=None):
1724
1776
  result[mode] = 'success'
1725
1777
  except Exception as e:
1726
1778
  result[mode] = 'error: {}'.format(str(e))
1727
-
1728
- # ...for each mode
1729
-
1779
+
1780
+ # ...for each mode
1781
+
1730
1782
  return result
1731
1783
 
1732
1784
  # ...def check_image_integrity(...)
1733
1785
 
1734
1786
 
1735
1787
  def parallel_check_image_integrity(filenames,
1736
- modes=None,
1737
- max_workers=16,
1738
- use_threads=True,
1788
+ modes=None,
1789
+ max_workers=16,
1790
+ use_threads=True,
1739
1791
  recursive=True,
1740
1792
  verbose=False):
1741
1793
  """
1742
1794
  Check whether we can successfully load a list of images via OpenCV and/or PIL.
1743
-
1795
+
1744
1796
  Args:
1745
1797
  filenames (list or str): a list of image filenames or a folder
1746
- mode (list): see check_image_integrity() for documentation on the [modes] parameter
1798
+ modes (list, optional): see check_image_integrity() for documentation on the [modes] parameter
1747
1799
  max_workers (int, optional): the number of parallel workers to use; set to <=1 to disable
1748
1800
  parallelization
1749
1801
  use_threads (bool, optional): whether to use threads (True) or processes (False) for
@@ -1751,10 +1803,10 @@ def parallel_check_image_integrity(filenames,
1751
1803
  recursive (bool, optional): if [filenames] is a folder, whether to search recursively for images.
1752
1804
  Ignored if [filenames] is a list.
1753
1805
  verbose (bool, optional): enable additional debug output
1754
-
1806
+
1755
1807
  Returns:
1756
- list: a list of dicts, each with a key called 'file' (the value of [filename]), one key for
1757
- each string in [modes] (a success indicator for that mode, specifically a string starting
1808
+ list: a list of dicts, each with a key called 'file' (the value of [filename]), one key for
1809
+ each string in [modes] (a success indicator for that mode, specifically a string starting
1758
1810
  with either 'success' or 'error').
1759
1811
  """
1760
1812
 
@@ -1762,69 +1814,69 @@ def parallel_check_image_integrity(filenames,
1762
1814
  if verbose:
1763
1815
  print('Enumerating images in {}'.format(filenames))
1764
1816
  filenames = find_images(filenames,recursive=recursive,return_relative_paths=False)
1765
-
1817
+
1766
1818
  n_workers = min(max_workers,len(filenames))
1767
-
1819
+
1768
1820
  if verbose:
1769
1821
  print('Checking image integrity for {} filenames'.format(len(filenames)))
1770
-
1822
+
1771
1823
  if n_workers <= 1:
1772
-
1824
+
1773
1825
  results = []
1774
1826
  for filename in filenames:
1775
1827
  results.append(check_image_integrity(filename,modes=modes))
1776
-
1828
+
1777
1829
  else:
1778
-
1830
+
1779
1831
  if use_threads:
1780
1832
  pool = ThreadPool(n_workers)
1781
1833
  else:
1782
1834
  pool = Pool(n_workers)
1783
-
1835
+
1784
1836
  results = list(tqdm(pool.imap(
1785
1837
  partial(check_image_integrity,modes=modes),filenames), total=len(filenames)))
1786
-
1838
+
1787
1839
  return results
1788
1840
 
1789
1841
 
1790
1842
  #%% Test drivers
1791
1843
 
1792
1844
  if False:
1793
-
1845
+
1794
1846
  #%% Text rendering tests
1795
-
1847
+
1796
1848
  import os # noqa
1797
1849
  import numpy as np # noqa
1798
1850
  from megadetector.visualization.visualization_utils import \
1799
1851
  draw_bounding_boxes_on_image, exif_preserving_save, load_image, \
1800
1852
  TEXTALIGN_LEFT,TEXTALIGN_RIGHT,VTEXTALIGN_BOTTOM,VTEXTALIGN_TOP, \
1801
1853
  DEFAULT_LABEL_FONT_SIZE
1802
-
1854
+
1803
1855
  fn = os.path.expanduser('~/AppData/Local/Temp/md-tests/md-test-images/ena24_7904.jpg')
1804
1856
  output_fn = r'g:\temp\test.jpg'
1805
-
1857
+
1806
1858
  image = load_image(fn)
1807
-
1859
+
1808
1860
  w = 0.2; h = 0.2
1809
1861
  all_boxes = [[0.05, 0.05, 0.25, 0.25],
1810
1862
  [0.05, 0.35, 0.25, 0.6],
1811
1863
  [0.35, 0.05, 0.6, 0.25],
1812
1864
  [0.35, 0.35, 0.6, 0.6]]
1813
-
1865
+
1814
1866
  alignments = [
1815
1867
  [TEXTALIGN_LEFT,VTEXTALIGN_TOP],
1816
1868
  [TEXTALIGN_LEFT,VTEXTALIGN_BOTTOM],
1817
1869
  [TEXTALIGN_RIGHT,VTEXTALIGN_TOP],
1818
1870
  [TEXTALIGN_RIGHT,VTEXTALIGN_BOTTOM]
1819
1871
  ]
1820
-
1872
+
1821
1873
  labels = ['left_top','left_bottom','right_top','right_bottom']
1822
-
1874
+
1823
1875
  text_rotation = -90
1824
1876
  n_label_copies = 2
1825
-
1877
+
1826
1878
  for i_box,box in enumerate(all_boxes):
1827
-
1879
+
1828
1880
  boxes = [box]
1829
1881
  boxes = np.array(boxes)
1830
1882
  classes = [i_box]
@@ -1846,30 +1898,30 @@ if False:
1846
1898
  exif_preserving_save(image,output_fn)
1847
1899
  from megadetector.utils.path_utils import open_file
1848
1900
  open_file(output_fn)
1849
-
1850
-
1901
+
1902
+
1851
1903
  #%% Recursive resize test
1852
-
1904
+
1853
1905
  from megadetector.visualization.visualization_utils import resize_image_folder # noqa
1854
-
1906
+
1855
1907
  input_folder = r"C:\temp\resize-test\in"
1856
1908
  output_folder = r"C:\temp\resize-test\out"
1857
-
1909
+
1858
1910
  resize_results = resize_image_folder(input_folder,output_folder,
1859
1911
  target_width=1280,verbose=True,quality=85,no_enlarge_width=True,
1860
1912
  pool_type='process',n_workers=10)
1861
-
1862
-
1913
+
1914
+
1863
1915
  #%% Integrity checking test
1864
-
1916
+
1865
1917
  from megadetector.utils import md_tests
1866
1918
  options = md_tests.download_test_data()
1867
1919
  folder = options.scratch_dir
1868
-
1920
+
1869
1921
  results = parallel_check_image_integrity(folder,max_workers=8)
1870
-
1922
+
1871
1923
  modes = ['cv','pil','skimage','jpeg_trailer']
1872
-
1924
+
1873
1925
  for r in results:
1874
1926
  for mode in modes:
1875
1927
  if r[mode] != 'success':