megadetector 5.0.6__py3-none-any.whl → 5.0.8__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 (75) hide show
  1. api/batch_processing/data_preparation/manage_local_batch.py +297 -202
  2. api/batch_processing/data_preparation/manage_video_batch.py +7 -2
  3. api/batch_processing/postprocessing/add_max_conf.py +1 -0
  4. api/batch_processing/postprocessing/combine_api_outputs.py +2 -2
  5. api/batch_processing/postprocessing/compare_batch_results.py +111 -61
  6. api/batch_processing/postprocessing/convert_output_format.py +24 -6
  7. api/batch_processing/postprocessing/load_api_results.py +56 -72
  8. api/batch_processing/postprocessing/md_to_labelme.py +119 -51
  9. api/batch_processing/postprocessing/merge_detections.py +30 -5
  10. api/batch_processing/postprocessing/postprocess_batch_results.py +175 -55
  11. api/batch_processing/postprocessing/remap_detection_categories.py +163 -0
  12. api/batch_processing/postprocessing/render_detection_confusion_matrix.py +628 -0
  13. api/batch_processing/postprocessing/repeat_detection_elimination/find_repeat_detections.py +71 -23
  14. api/batch_processing/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +1 -1
  15. api/batch_processing/postprocessing/repeat_detection_elimination/repeat_detections_core.py +224 -76
  16. api/batch_processing/postprocessing/subset_json_detector_output.py +132 -5
  17. api/batch_processing/postprocessing/top_folders_to_bottom.py +1 -1
  18. classification/prepare_classification_script.py +191 -191
  19. data_management/cct_json_utils.py +7 -2
  20. data_management/coco_to_labelme.py +263 -0
  21. data_management/coco_to_yolo.py +72 -48
  22. data_management/databases/integrity_check_json_db.py +75 -64
  23. data_management/databases/subset_json_db.py +1 -1
  24. data_management/generate_crops_from_cct.py +1 -1
  25. data_management/get_image_sizes.py +44 -26
  26. data_management/importers/animl_results_to_md_results.py +3 -5
  27. data_management/importers/noaa_seals_2019.py +2 -2
  28. data_management/importers/zamba_results_to_md_results.py +2 -2
  29. data_management/labelme_to_coco.py +264 -127
  30. data_management/labelme_to_yolo.py +96 -53
  31. data_management/lila/create_lila_blank_set.py +557 -0
  32. data_management/lila/create_lila_test_set.py +2 -1
  33. data_management/lila/create_links_to_md_results_files.py +1 -1
  34. data_management/lila/download_lila_subset.py +138 -45
  35. data_management/lila/generate_lila_per_image_labels.py +23 -14
  36. data_management/lila/get_lila_annotation_counts.py +16 -10
  37. data_management/lila/lila_common.py +15 -42
  38. data_management/lila/test_lila_metadata_urls.py +116 -0
  39. data_management/read_exif.py +65 -16
  40. data_management/remap_coco_categories.py +84 -0
  41. data_management/resize_coco_dataset.py +14 -31
  42. data_management/wi_download_csv_to_coco.py +239 -0
  43. data_management/yolo_output_to_md_output.py +40 -13
  44. data_management/yolo_to_coco.py +313 -100
  45. detection/process_video.py +36 -14
  46. detection/pytorch_detector.py +1 -1
  47. detection/run_detector.py +73 -18
  48. detection/run_detector_batch.py +116 -27
  49. detection/run_inference_with_yolov5_val.py +135 -27
  50. detection/run_tiled_inference.py +153 -43
  51. detection/tf_detector.py +2 -1
  52. detection/video_utils.py +4 -2
  53. md_utils/ct_utils.py +101 -6
  54. md_utils/md_tests.py +264 -17
  55. md_utils/path_utils.py +326 -47
  56. md_utils/process_utils.py +26 -7
  57. md_utils/split_locations_into_train_val.py +215 -0
  58. md_utils/string_utils.py +10 -0
  59. md_utils/url_utils.py +66 -3
  60. md_utils/write_html_image_list.py +12 -2
  61. md_visualization/visualization_utils.py +380 -74
  62. md_visualization/visualize_db.py +41 -10
  63. md_visualization/visualize_detector_output.py +185 -104
  64. {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/METADATA +11 -13
  65. {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/RECORD +74 -67
  66. {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/WHEEL +1 -1
  67. taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +1 -1
  68. taxonomy_mapping/map_new_lila_datasets.py +43 -39
  69. taxonomy_mapping/prepare_lila_taxonomy_release.py +5 -2
  70. taxonomy_mapping/preview_lila_taxonomy.py +27 -27
  71. taxonomy_mapping/species_lookup.py +33 -13
  72. taxonomy_mapping/taxonomy_csv_checker.py +7 -5
  73. md_visualization/visualize_megadb.py +0 -183
  74. {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/LICENSE +0 -0
  75. {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/top_level.txt +0 -0
@@ -8,22 +8,32 @@
8
8
 
9
9
  #%% Constants and imports
10
10
 
11
- from io import BytesIO
12
- from typing import Union
13
11
  import time
14
-
15
- import matplotlib.pyplot as plt
16
12
  import numpy as np
17
13
  import requests
14
+ import os
15
+
16
+ from io import BytesIO
17
+ from typing import Union
18
18
  from PIL import Image, ImageFile, ImageFont, ImageDraw
19
+ from multiprocessing.pool import ThreadPool
20
+ from multiprocessing.pool import Pool
21
+ from tqdm import tqdm
22
+ from functools import partial
23
+
24
+ from md_utils.path_utils import find_images
19
25
 
20
26
  from data_management.annotations import annotation_constants
21
27
  from data_management.annotations.annotation_constants import (
22
- detector_bbox_category_id_to_name) # here id is int
28
+ detector_bbox_category_id_to_name)
23
29
 
24
30
  ImageFile.LOAD_TRUNCATED_IMAGES = True
25
31
 
26
- IMAGE_ROTATIONS = {
32
+ # Maps EXIF standard rotation identifiers to degrees. The value "1" indicates no
33
+ # rotation; this will be ignored. The values 2, 4, 5, and 7 are mirrored rotations,
34
+ # which are not supported (we'll assert() on this when we apply rotations).
35
+ EXIF_IMAGE_NO_ROTATION = 1
36
+ EXIF_IMAGE_ROTATIONS = {
27
37
  3: 180,
28
38
  6: 270,
29
39
  8: 90
@@ -32,7 +42,7 @@ IMAGE_ROTATIONS = {
32
42
  TEXTALIGN_LEFT = 0
33
43
  TEXTALIGN_RIGHT = 1
34
44
 
35
- # convert category ID from int to str
45
+ # Convert category ID from int to str
36
46
  DEFAULT_DETECTOR_LABEL_MAP = {
37
47
  str(k): v for k, v in detector_bbox_category_id_to_name.items()
38
48
  }
@@ -48,7 +58,7 @@ DEFAULT_LABEL_FONT_SIZE = 16
48
58
 
49
59
  #%% Functions
50
60
 
51
- def open_image(input_file: Union[str, BytesIO]) -> Image:
61
+ def open_image(input_file: Union[str, BytesIO], ignore_exif_rotation=False) -> Image:
52
62
  """
53
63
  Opens an image in binary format using PIL.Image and converts to RGB mode.
54
64
 
@@ -56,7 +66,7 @@ def open_image(input_file: Union[str, BytesIO]) -> Image:
56
66
 
57
67
  This operation is lazy; image will not be actually loaded until the first
58
68
  operation that needs to load it (for example, resizing), so file opening
59
- errors can show up later.
69
+ errors can show up later. load_image() is the non-lazy version of this function.
60
70
 
61
71
  Args:
62
72
  input_file: str or BytesIO, either a path to an image file (anything
@@ -101,23 +111,28 @@ def open_image(input_file: Union[str, BytesIO]) -> Image:
101
111
  # PIL.Image.convert() returns a converted copy of this image
102
112
  image = image.convert(mode='RGB')
103
113
 
104
- # Alter orientation as needed according to EXIF tag 0x112 (274) for Orientation
105
- #
106
- # https://gist.github.com/dangtrinhnt/a577ece4cbe5364aad28
107
- # https://www.media.mit.edu/pia/Research/deepview/exif.html
108
- #
109
- try:
110
- exif = image._getexif()
111
- orientation: int = exif.get(274, None) # 274 is the key for the Orientation field
112
- if orientation is not None and orientation in IMAGE_ROTATIONS:
113
- image = image.rotate(IMAGE_ROTATIONS[orientation], expand=True) # returns a rotated copy
114
- except Exception:
115
- pass
114
+ if not ignore_exif_rotation:
115
+ # Alter orientation as needed according to EXIF tag 0x112 (274) for Orientation
116
+ #
117
+ # https://gist.github.com/dangtrinhnt/a577ece4cbe5364aad28
118
+ # https://www.media.mit.edu/pia/Research/deepview/exif.html
119
+ #
120
+ try:
121
+ exif = image._getexif()
122
+ orientation: int = exif.get(274, None)
123
+ if (orientation is not None) and (orientation != EXIF_IMAGE_NO_ROTATION):
124
+ assert orientation in EXIF_IMAGE_ROTATIONS, \
125
+ 'Mirrored rotations are not supported'
126
+ image = image.rotate(EXIF_IMAGE_ROTATIONS[orientation], expand=True)
127
+ except Exception:
128
+ pass
116
129
 
117
130
  return image
118
131
 
132
+ # ...def open_image(...)
133
+
119
134
 
120
- def exif_preserving_save(pil_image,output_file):
135
+ def exif_preserving_save(pil_image,output_file,quality='keep',default_quality=85,verbose=False):
121
136
  """
122
137
  Save [pil_image] to [output_file], making a moderate attempt to preserve EXIF
123
138
  data and JPEG quality. Neither is guaranteed.
@@ -127,27 +142,50 @@ def exif_preserving_save(pil_image,output_file):
127
142
  https://discuss.dizzycoding.com/determining-jpg-quality-in-python-pil/
128
143
 
129
144
  ...for more ways to preserve jpeg quality if quality='keep' doesn't do the trick.
145
+
146
+ The "quality" parameter should be "keep" (default), or an integer from 0 to 100.
147
+ This is only used if PIL thinks the the source image is a JPEG. If you load a JPEG
148
+ and resize it in memory, for example, it's no longer a JPEG.
149
+
150
+ 'default_quality' is used when quality == 'keep' and we are saving a non-JPEG source.
151
+ 'keep' is only supported for JPEG sources.
130
152
  """
131
153
 
132
154
  # Read EXIF metadata
133
155
  exif = pil_image.info['exif'] if ('exif' in pil_image.info) else None
134
156
 
135
- # Write output with EXIF metadata if available, and quality='keep' if this is a JPEG
136
- # image. Unfortunately, neither parameter likes "None", so we get a slightly
137
- # icky cascade of if's here.
138
- if exif is not None:
139
- if pil_image.format == "JPEG":
140
- pil_image.save(output_file, exif=exif, quality='keep')
157
+ # Quality preservation is only supported for JPEG sources.
158
+ if pil_image.format != "JPEG":
159
+ if quality == 'keep':
160
+ if verbose:
161
+ print('Warning: quality "keep" passed when saving a non-JPEG source (during save to {})'.format(
162
+ output_file))
163
+ quality = default_quality
164
+
165
+ # Some output formats don't support the quality parameter, so we try once with,
166
+ # and once without. This is a horrible cascade of if's, but it's a consequence of
167
+ # the fact that "None" is not supported for either "exif" or "quality".
168
+
169
+ try:
170
+
171
+ if exif is not None:
172
+ pil_image.save(output_file, exif=exif, quality=quality)
141
173
  else:
142
- pil_image.save(output_file, exif=exif)
143
- else:
144
- if pil_image.format == "JPEG":
145
- pil_image.save(output_file, quality='keep')
174
+ pil_image.save(output_file, quality=quality)
175
+
176
+ except Exception:
177
+
178
+ if verbose:
179
+ print('Warning: failed to write {}, trying again without quality parameter'.format(output_file))
180
+ if exif is not None:
181
+ pil_image.save(output_file, exif=exif)
146
182
  else:
147
183
  pil_image.save(output_file)
148
184
 
149
-
150
- def load_image(input_file: Union[str, BytesIO]) -> Image:
185
+ # ...def exif_preserving_save(...)
186
+
187
+
188
+ def load_image(input_file: Union[str, BytesIO], ignore_exif_rotation=False) -> Image:
151
189
  """
152
190
  Loads the image at input_file as a PIL Image into memory.
153
191
 
@@ -161,27 +199,49 @@ def load_image(input_file: Union[str, BytesIO]) -> Image:
161
199
  Returns: PIL.Image.Image, in RGB mode
162
200
  """
163
201
 
164
- image = open_image(input_file)
202
+ image = open_image(input_file, ignore_exif_rotation=ignore_exif_rotation)
165
203
  image.load()
166
204
  return image
167
205
 
168
206
 
169
- def resize_image(image, target_width, target_height=-1, output_file=None):
207
+ def resize_image(image, target_width, target_height=-1, output_file=None,
208
+ no_enlarge_width=False, verbose=False, quality='keep'):
170
209
  """
171
210
  Resizes a PIL image object to the specified width and height; does not resize
172
211
  in place. If either width or height are -1, resizes with aspect ratio preservation.
173
- If both are -1, returns the original image (does not copy in this case).
212
+
213
+ None is equivalent to -1 for target_width and target_height.
174
214
 
175
215
  [image] can be a PIL image or a filename.
216
+
217
+ If target_width and target_height are both -1, does not modify the image, but
218
+ will write to output_file if supplied.
219
+
220
+ If no_enlarge_width is True, and the target width is larger than the original
221
+ image width, does not modify the image, but will write to output_file if supplied.
222
+
223
+ 'quality' is passed to exif_preserving_save, see docs there.
176
224
  """
177
225
 
226
+ image_fn = 'in_memory'
178
227
  if isinstance(image,str):
228
+ image_fn = image
179
229
  image = load_image(image)
180
230
 
181
- # Null operation
231
+ if target_width is None:
232
+ target_width = -1
233
+
234
+ if target_height is None:
235
+ target_height = -1
236
+
237
+ resize_required = True
238
+
239
+ # No resize was requested, this is always a no-op
182
240
  if target_width == -1 and target_height == -1:
183
- return image
184
-
241
+
242
+ resize_required = False
243
+
244
+ # Does either dimension need to scale according to the other?
185
245
  elif target_width == -1 or target_height == -1:
186
246
 
187
247
  # Aspect ratio as width over height
@@ -194,39 +254,46 @@ def resize_image(image, target_width, target_height=-1, output_file=None):
194
254
  else:
195
255
  # w = ar * h
196
256
  target_width = int(aspect_ratio * target_height)
197
-
198
- # This parameter changed between Pillow versions 9 and 10, and for a bit, I'd like to
199
- # support both.
257
+
258
+ # If we're not enlarging images and this would be an enlarge operation
259
+ if (no_enlarge_width) and (target_width > image.size[0]):
260
+
261
+ if verbose:
262
+ print('Bypassing image enlarge for {} --> {}'.format(
263
+ image_fn,str(output_file)))
264
+ resize_required = False
265
+
266
+ # If the target size is the same as the original size
267
+ if (target_width == image.size[0]) and (target_height == image.size[1]):
268
+
269
+ resize_required = False
270
+
271
+ if not resize_required:
272
+
273
+ if output_file is not None:
274
+ if verbose:
275
+ print('No resize required for resize {} --> {}'.format(
276
+ image_fn,str(output_file)))
277
+ exif_preserving_save(image,output_file,quality=quality,verbose=verbose)
278
+ return image
279
+
280
+ assert target_width > 0 and target_height > 0, \
281
+ 'Invalid image resize target {},{}'.format(target_width,target_height)
282
+
283
+ # The antialiasing parameter changed between Pillow versions 9 and 10, and for a bit,
284
+ # I'd like to support both.
200
285
  try:
201
286
  resized_image = image.resize((target_width, target_height), Image.ANTIALIAS)
202
287
  except:
203
288
  resized_image = image.resize((target_width, target_height), Image.Resampling.LANCZOS)
204
289
 
205
290
  if output_file is not None:
206
- exif_preserving_save(resized_image,output_file)
291
+ exif_preserving_save(resized_image,output_file,quality=quality,verbose=verbose)
207
292
 
208
293
  return resized_image
209
294
 
295
+ # ...def resize_image(...)
210
296
 
211
- def show_images_in_a_row(images):
212
-
213
- num = len(images)
214
- assert num > 0
215
-
216
- if isinstance(images[0], str):
217
- images = [Image.open(img) for img in images]
218
-
219
- fig, axarr = plt.subplots(1, num, squeeze=False) # number of rows, number of columns
220
- fig.set_size_inches((num * 5, 25)) # each image is 2 inches wide
221
- for i, img in enumerate(images):
222
- axarr[0, i].set_axis_off()
223
- axarr[0, i].imshow(img)
224
- return fig
225
-
226
-
227
- # The following three functions are modified versions of those at:
228
- #
229
- # https://github.com/tensorflow/models/blob/master/research/object_detection/utils/visualization_utils.py
230
297
 
231
298
  DEFAULT_COLORS = [
232
299
  'AliceBlue', 'Red', 'RoyalBlue', 'Gold', 'Chartreuse', 'Aqua', 'Azure',
@@ -365,13 +432,15 @@ def render_detection_bounding_boxes(detections, image,
365
432
 
366
433
  label_map: optional, mapping the numerical label to a string name. The type of the numerical label
367
434
  (default string) needs to be consistent with the keys in label_map; no casting is carried out.
368
- If this is None, no labels are shown.
435
+ If this is None, no labels are shown (not even numbers and confidence values). If you want
436
+ category numbers and confidence values without class labels, use {}.
369
437
 
370
438
  classification_label_map: optional, mapping of the string class labels to the actual class names.
371
439
  The type of the numerical label (default string) needs to be consistent with the keys in
372
440
  label_map; no casting is carried out. If this is None, no classification labels are shown.
373
441
 
374
- confidence_threshold: optional, threshold above which the bounding box is rendered.
442
+ confidence_threshold: optional, threshold above which boxes are rendered. Can also be a dictionary
443
+ mapping category IDs to thresholds.
375
444
 
376
445
  thickness: line thickness in pixels. Default value is 4.
377
446
 
@@ -405,9 +474,15 @@ def render_detection_bounding_boxes(detections, image,
405
474
 
406
475
  score = detection['conf']
407
476
 
477
+ if isinstance(confidence_threshold,dict):
478
+ rendering_threshold = confidence_threshold[detection['category']]
479
+ else:
480
+ rendering_threshold = confidence_threshold
481
+
482
+
408
483
  # Always render objects with a confidence of "None", this is typically used
409
484
  # for ground truth data.
410
- if score is None or score >= confidence_threshold:
485
+ if score is None or score >= rendering_threshold:
411
486
 
412
487
  x1, y1, w_box, h_box = detection['bbox']
413
488
  display_boxes.append([y1, x1, y1 + h_box, x1 + w_box])
@@ -476,6 +551,8 @@ def render_detection_bounding_boxes(detections, image,
476
551
  expansion=expansion, colormap=colormap, textalign=textalign,
477
552
  label_font_size=label_font_size)
478
553
 
554
+ # ...render_detection_bounding_boxes(...)
555
+
479
556
 
480
557
  def draw_bounding_boxes_on_image(image,
481
558
  boxes,
@@ -522,6 +599,8 @@ def draw_bounding_boxes_on_image(image,
522
599
  textalign=textalign,
523
600
  label_font_size=label_font_size)
524
601
 
602
+ # ...draw_bounding_boxes_on_image(...)
603
+
525
604
 
526
605
  def draw_bounding_box_on_image(image,
527
606
  ymin,
@@ -552,8 +631,9 @@ def draw_bounding_box_on_image(image,
552
631
  ymin: ymin of bounding box - upper left.
553
632
  xmin: xmin of bounding box.
554
633
  ymax: ymax of bounding box.
555
- xmax: xmax of bounding box.
556
- clss: str, the class of the object in this bounding box - will be cast to an int.
634
+ xmax: xmax of bounding box.
635
+ clss: str, the class of the object in this bounding box; should be either an integer
636
+ or a string-formatted integer.
557
637
  thickness: line thickness. Default value is 4.
558
638
  expansion: number of pixels to expand bounding boxes on each side. Default is 0.
559
639
  display_str_list: list of strings to display in box
@@ -561,10 +641,15 @@ def draw_bounding_box_on_image(image,
561
641
  use_normalized_coordinates: If True (default), treat coordinates
562
642
  ymin, xmin, ymax, xmax as relative to the image. Otherwise treat
563
643
  coordinates as absolute.
564
- label_font_size: font size to attempt to load arial.ttf with
644
+ label_font_size: font size
645
+
646
+ Adapted from:
647
+
648
+ https://github.com/tensorflow/models/blob/master/research/object_detection/utils/visualization_utils.py
565
649
  """
566
650
 
567
651
  if clss is None:
652
+ # Default to the MegaDetector animal class ID (1)
568
653
  color = colormap[1]
569
654
  else:
570
655
  color = colormap[int(clss) % len(colormap)]
@@ -670,6 +755,8 @@ def draw_bounding_box_on_image(image,
670
755
 
671
756
  text_bottom -= (text_height + 2 * margin)
672
757
 
758
+ # ...def draw_bounding_box_on_image(...)
759
+
673
760
 
674
761
  def render_iMerit_boxes(boxes, classes, image,
675
762
  label_map=annotation_constants.annotation_bbox_category_id_to_name):
@@ -743,6 +830,8 @@ def render_megadb_bounding_boxes(boxes_info, image):
743
830
  display_boxes = np.array(display_boxes)
744
831
  draw_bounding_boxes_on_image(image, display_boxes, classes, display_strs=display_strs)
745
832
 
833
+ # ...def render_iMerit_boxes(...)
834
+
746
835
 
747
836
  def render_db_bounding_boxes(boxes, classes, image, original_size=None,
748
837
  label_map=None, thickness=DEFAULT_BOX_THICKNESS, expansion=0):
@@ -787,13 +876,16 @@ def render_db_bounding_boxes(boxes, classes, image, original_size=None,
787
876
  draw_bounding_boxes_on_image(image, display_boxes, classes, display_strs=display_strs,
788
877
  thickness=thickness, expansion=expansion)
789
878
 
879
+ # ...def render_db_bounding_boxes(...)
880
+
790
881
 
791
882
  def draw_bounding_boxes_on_file(input_file, output_file, detections, confidence_threshold=0.0,
792
883
  detector_label_map=DEFAULT_DETECTOR_LABEL_MAP,
793
884
  thickness=DEFAULT_BOX_THICKNESS, expansion=0,
794
885
  colormap=DEFAULT_COLORS,
795
886
  label_font_size=DEFAULT_LABEL_FONT_SIZE,
796
- custom_strings=None,target_size=None):
887
+ custom_strings=None,target_size=None,
888
+ ignore_exif_rotation=False):
797
889
  """
798
890
  Render detection bounding boxes on an image loaded from file, writing the results to a
799
891
  new image file.
@@ -808,7 +900,9 @@ def draw_bounding_boxes_on_file(input_file, output_file, detections, confidence_
808
900
 
809
901
  Normalized, with the origin at the upper-left.
810
902
 
811
- detector_label_map is a dict mapping category IDs to strings.
903
+ detector_label_map is a dict mapping category IDs to strings. If this is None,
904
+ no confidence values or identifiers are shown If this is {}, just category indices and
905
+ confidence values are shown.
812
906
 
813
907
  custom_strings: optional set of strings to append to detection labels, should have the
814
908
  same length as [detections]. Appended before classification labels, if classification
@@ -818,7 +912,7 @@ def draw_bounding_boxes_on_file(input_file, output_file, detections, confidence_
818
912
  see resize_image for documentation. If None or (-1,-1), uses the original image size.
819
913
  """
820
914
 
821
- image = open_image(input_file)
915
+ image = open_image(input_file, ignore_exif_rotation=ignore_exif_rotation)
822
916
 
823
917
  if target_size is not None:
824
918
  image = resize_image(image,target_size[0],target_size[1])
@@ -833,7 +927,8 @@ def draw_bounding_boxes_on_file(input_file, output_file, detections, confidence_
833
927
 
834
928
 
835
929
  def draw_db_boxes_on_file(input_file, output_file, boxes, classes=None,
836
- label_map=None, thickness=DEFAULT_BOX_THICKNESS, expansion=0):
930
+ label_map=None, thickness=DEFAULT_BOX_THICKNESS, expansion=0,
931
+ ignore_exif_rotation=False):
837
932
  """
838
933
  Render COCO bounding boxes (in absolute coordinates) on an image loaded from file, writing the
839
934
  results to a new image file.
@@ -843,7 +938,7 @@ def draw_db_boxes_on_file(input_file, output_file, boxes, classes=None,
843
938
  detector_label_map is a dict mapping category IDs to strings.
844
939
  """
845
940
 
846
- image = open_image(input_file)
941
+ image = open_image(input_file, ignore_exif_rotation=ignore_exif_rotation)
847
942
 
848
943
  if classes is None:
849
944
  classes = [0] * len(boxes)
@@ -854,6 +949,9 @@ def draw_db_boxes_on_file(input_file, output_file, boxes, classes=None,
854
949
  image.save(output_file)
855
950
 
856
951
 
952
+ # ...def draw_bounding_boxes_on_file(...)
953
+
954
+
857
955
  def gray_scale_fraction(image,crop_size=(0.1,0.1)):
858
956
  """
859
957
  Returns the fraction of the pixels in [image] that appear to be grayscale (R==G==B),
@@ -923,3 +1021,211 @@ def gray_scale_fraction(image,crop_size=(0.1,0.1)):
923
1021
  r, g, b = image.getpixel((i,j))
924
1022
  if r == g and r == b and g == b:
925
1023
  n_gray_pixels += 1
1024
+
1025
+
1026
+ # ...def gray_scale_fraction(...)
1027
+
1028
+
1029
+ def _resize_relative_image(fn_relative,
1030
+ input_folder,output_folder,
1031
+ target_width,target_height,no_enlarge_width,verbose,quality):
1032
+ """
1033
+ Internal function for resizing an image from one folder to another,
1034
+ maintaining relative path.
1035
+ """
1036
+
1037
+ input_fn_abs = os.path.join(input_folder,fn_relative)
1038
+ output_fn_abs = os.path.join(output_folder,fn_relative)
1039
+ os.makedirs(os.path.dirname(output_fn_abs),exist_ok=True)
1040
+ try:
1041
+ _ = resize_image(input_fn_abs,
1042
+ output_file=output_fn_abs,
1043
+ target_width=target_width, target_height=target_height,
1044
+ no_enlarge_width=no_enlarge_width, verbose=verbose, quality=quality)
1045
+ status = 'success'
1046
+ error = None
1047
+ except Exception as e:
1048
+ if verbose:
1049
+ print('Error resizing {}: {}'.format(fn_relative,str(e)))
1050
+ status = 'error'
1051
+ error = str(e)
1052
+
1053
+ return {'fn_relative':fn_relative,'status':status,'error':error}
1054
+
1055
+ # ...def _resize_relative_image(...)
1056
+
1057
+
1058
+ def _resize_absolute_image(input_output_files,
1059
+ target_width,target_height,no_enlarge_width,verbose,quality):
1060
+
1061
+ """
1062
+ Internal wrappter for resize_image used in the context of a batch resize operation.
1063
+ """
1064
+
1065
+ input_fn_abs = input_output_files[0]
1066
+ output_fn_abs = input_output_files[1]
1067
+ os.makedirs(os.path.dirname(output_fn_abs),exist_ok=True)
1068
+ try:
1069
+ _ = resize_image(input_fn_abs,
1070
+ output_file=output_fn_abs,
1071
+ target_width=target_width, target_height=target_height,
1072
+ no_enlarge_width=no_enlarge_width, verbose=verbose, quality=quality)
1073
+ status = 'success'
1074
+ error = None
1075
+ except Exception as e:
1076
+ if verbose:
1077
+ print('Error resizing {}: {}'.format(input_fn_abs,str(e)))
1078
+ status = 'error'
1079
+ error = str(e)
1080
+
1081
+ return {'input_fn':input_fn_abs,'output_fn':output_fn_abs,status:'status',
1082
+ 'error':error}
1083
+
1084
+ # ..._resize_absolute_image(...)
1085
+
1086
+
1087
+ def resize_images(input_file_to_output_file,
1088
+ target_width=-1, target_height=-1,
1089
+ no_enlarge_width=False, verbose=False, quality='keep',
1090
+ pool_type='process', n_workers=10):
1091
+ """
1092
+ Resize all images the dictionary [input_file_to_output_file].
1093
+
1094
+ Defaults to parallelizing across processes.
1095
+
1096
+ See resize_image() for parameter information.
1097
+
1098
+ TODO: This is a little more redundant with resize_image_folder than I would like;
1099
+ refactor resize_image_folder to call resize_images. Not doing that yet because
1100
+ at the time I'm writing this comment, a lot of code depends on resize_image_folder
1101
+ and I don't want to rock the boat yet.
1102
+ """
1103
+
1104
+
1105
+ assert pool_type in ('process','thread'), 'Illegal pool type {}'.format(pool_type)
1106
+
1107
+ input_output_file_pairs = []
1108
+
1109
+ # Reformat input files as (input,output) tuples
1110
+ for input_fn in input_file_to_output_file:
1111
+ input_output_file_pairs.append((input_fn,input_file_to_output_file[input_fn]))
1112
+
1113
+ if n_workers == 1:
1114
+
1115
+ results = []
1116
+ for i_o_file_pair in tqdm(input_output_file_pairs):
1117
+ results.append(_resize_absolute_image(i_o_file_pair,
1118
+ target_width=target_width,
1119
+ target_height=target_height,
1120
+ no_enlarge_width=no_enlarge_width,
1121
+ verbose=verbose,
1122
+ quality=quality))
1123
+
1124
+ else:
1125
+
1126
+ if pool_type == 'thread':
1127
+ pool = ThreadPool(n_workers); poolstring = 'threads'
1128
+ else:
1129
+ assert pool_type == 'process'
1130
+ pool = Pool(n_workers); poolstring = 'processes'
1131
+
1132
+ if verbose:
1133
+ print('Starting resizing pool with {} {}'.format(n_workers,poolstring))
1134
+
1135
+ p = partial(_resize_absolute_image,
1136
+ target_width=target_width,
1137
+ target_height=target_height,
1138
+ no_enlarge_width=no_enlarge_width,
1139
+ verbose=verbose,
1140
+ quality=quality)
1141
+
1142
+ results = list(tqdm(pool.imap(p, input_output_file_pairs),total=len(input_output_file_pairs)))
1143
+
1144
+ return results
1145
+
1146
+ # ...def resize_images(...)
1147
+
1148
+
1149
+ def resize_image_folder(input_folder, output_folder=None,
1150
+ target_width=-1, target_height=-1,
1151
+ no_enlarge_width=False, verbose=False, quality='keep',
1152
+ pool_type='process', n_workers=10, recursive=True,
1153
+ image_files_relative=None):
1154
+ """
1155
+ Resize all images in a folder (defaults to recursive)
1156
+
1157
+ Defaults to in-place resizing (output_folder is optional).
1158
+
1159
+ Defaults to parallelizing across processes.
1160
+
1161
+ See resize_image() for parameter information.
1162
+ """
1163
+
1164
+ assert os.path.isdir(input_folder), '{} is not a folder'.format(input_folder)
1165
+
1166
+ if output_folder is None:
1167
+ output_folder = input_folder
1168
+ else:
1169
+ os.makedirs(output_folder,exist_ok=True)
1170
+
1171
+ assert pool_type in ('process','thread'), 'Illegal pool type {}'.format(pool_type)
1172
+
1173
+ if image_files_relative is None:
1174
+ image_files_relative = find_images(input_folder,recursive=recursive,return_relative_paths=True)
1175
+ if verbose:
1176
+ print('Found {} images'.format(len(image_files_relative)))
1177
+
1178
+ if n_workers == 1:
1179
+
1180
+ results = []
1181
+ for fn_relative in tqdm(image_files_relative):
1182
+ results.append(_resize_relative_image(fn_relative,
1183
+ input_folder=input_folder,
1184
+ output_folder=output_folder,
1185
+ target_width=target_width,
1186
+ target_height=target_height,
1187
+ no_enlarge_width=no_enlarge_width,
1188
+ verbose=verbose,
1189
+ quality=quality))
1190
+
1191
+ else:
1192
+
1193
+ if pool_type == 'thread':
1194
+ pool = ThreadPool(n_workers); poolstring = 'threads'
1195
+ else:
1196
+ assert pool_type == 'process'
1197
+ pool = Pool(n_workers); poolstring = 'processes'
1198
+
1199
+ if verbose:
1200
+ print('Starting resizing pool with {} {}'.format(n_workers,poolstring))
1201
+
1202
+ p = partial(_resize_relative_image,
1203
+ input_folder=input_folder,
1204
+ output_folder=output_folder,
1205
+ target_width=target_width,
1206
+ target_height=target_height,
1207
+ no_enlarge_width=no_enlarge_width,
1208
+ verbose=verbose,
1209
+ quality=quality)
1210
+
1211
+ results = list(tqdm(pool.imap(p, image_files_relative),total=len(image_files_relative)))
1212
+
1213
+ return results
1214
+
1215
+ # ...def resize_image_folder(...)
1216
+
1217
+
1218
+ #%% Test drivers
1219
+
1220
+ if False:
1221
+
1222
+ #%% Recursive resize test
1223
+
1224
+ from md_visualization.visualization_utils import resize_image_folder # noqa
1225
+
1226
+ input_folder = r"C:\temp\resize-test\in"
1227
+ output_folder = r"C:\temp\resize-test\out"
1228
+
1229
+ resize_results = resize_image_folder(input_folder,output_folder,
1230
+ target_width=1280,verbose=True,quality=85,no_enlarge_width=True,
1231
+ pool_type='process',n_workers=10)