megadetector 5.0.7__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 (48) hide show
  1. api/batch_processing/data_preparation/manage_local_batch.py +28 -14
  2. api/batch_processing/postprocessing/combine_api_outputs.py +2 -2
  3. api/batch_processing/postprocessing/compare_batch_results.py +1 -1
  4. api/batch_processing/postprocessing/convert_output_format.py +24 -6
  5. api/batch_processing/postprocessing/load_api_results.py +1 -3
  6. api/batch_processing/postprocessing/md_to_labelme.py +118 -51
  7. api/batch_processing/postprocessing/merge_detections.py +30 -5
  8. api/batch_processing/postprocessing/postprocess_batch_results.py +24 -12
  9. api/batch_processing/postprocessing/remap_detection_categories.py +163 -0
  10. api/batch_processing/postprocessing/render_detection_confusion_matrix.py +15 -12
  11. api/batch_processing/postprocessing/repeat_detection_elimination/repeat_detections_core.py +2 -2
  12. data_management/cct_json_utils.py +7 -2
  13. data_management/coco_to_labelme.py +263 -0
  14. data_management/coco_to_yolo.py +7 -4
  15. data_management/databases/integrity_check_json_db.py +68 -59
  16. data_management/databases/subset_json_db.py +1 -1
  17. data_management/get_image_sizes.py +44 -26
  18. data_management/importers/animl_results_to_md_results.py +1 -3
  19. data_management/importers/noaa_seals_2019.py +1 -1
  20. data_management/labelme_to_coco.py +252 -143
  21. data_management/labelme_to_yolo.py +95 -52
  22. data_management/lila/create_lila_blank_set.py +106 -23
  23. data_management/lila/download_lila_subset.py +133 -65
  24. data_management/lila/generate_lila_per_image_labels.py +1 -1
  25. data_management/lila/lila_common.py +8 -38
  26. data_management/read_exif.py +65 -16
  27. data_management/remap_coco_categories.py +84 -0
  28. data_management/resize_coco_dataset.py +3 -22
  29. data_management/wi_download_csv_to_coco.py +239 -0
  30. data_management/yolo_to_coco.py +283 -83
  31. detection/run_detector_batch.py +12 -3
  32. detection/run_inference_with_yolov5_val.py +10 -3
  33. detection/run_tiled_inference.py +2 -2
  34. detection/tf_detector.py +2 -1
  35. detection/video_utils.py +1 -1
  36. md_utils/ct_utils.py +22 -3
  37. md_utils/md_tests.py +11 -2
  38. md_utils/path_utils.py +206 -32
  39. md_utils/url_utils.py +66 -1
  40. md_utils/write_html_image_list.py +12 -3
  41. md_visualization/visualization_utils.py +363 -72
  42. md_visualization/visualize_db.py +33 -10
  43. {megadetector-5.0.7.dist-info → megadetector-5.0.8.dist-info}/METADATA +10 -12
  44. {megadetector-5.0.7.dist-info → megadetector-5.0.8.dist-info}/RECORD +47 -44
  45. {megadetector-5.0.7.dist-info → megadetector-5.0.8.dist-info}/WHEEL +1 -1
  46. md_visualization/visualize_megadb.py +0 -183
  47. {megadetector-5.0.7.dist-info → megadetector-5.0.8.dist-info}/LICENSE +0 -0
  48. {megadetector-5.0.7.dist-info → megadetector-5.0.8.dist-info}/top_level.txt +0 -0
@@ -42,6 +42,7 @@ def write_html_image_list(filename=None,images=None,options=None):
42
42
  defaultImageStyle
43
43
  maxFiguresPerHtmlFile
44
44
  urlEncodeFilenames (default True, e.g. '#' will be replaced by '%23')
45
+ urlEncodeLinkTargets (default True, e.g. '#' will be replaced by '%23')
45
46
 
46
47
  """
47
48
 
@@ -68,7 +69,10 @@ def write_html_image_list(filename=None,images=None,options=None):
68
69
 
69
70
  if 'urlEncodeFilenames' not in options or options['urlEncodeFilenames'] is None:
70
71
  options['urlEncodeFilenames'] = True
71
-
72
+
73
+ if 'urlEncodeLinkTargets' not in options or options['urlEncodeLinkTargets'] is None:
74
+ options['urlEncodeLinkTargets'] = True
75
+
72
76
  # Possibly split the html output for figures into multiple files; Chrome gets sad with
73
77
  # thousands of images in a single tab.
74
78
  if 'maxFiguresPerHtmlFile' not in options or options['maxFiguresPerHtmlFile'] is None:
@@ -176,8 +180,8 @@ def write_html_image_list(filename=None,images=None,options=None):
176
180
  title = title.encode('ascii','ignore').decode('ascii')
177
181
  filename = filename.encode('ascii','ignore').decode('ascii')
178
182
 
179
- if options['urlEncodeFilenames']:
180
- filename = filename.replace('\\','/')
183
+ filename = filename.replace('\\','/')
184
+ if options['urlEncodeFilenames']:
181
185
  filename = urllib.parse.quote(filename)
182
186
 
183
187
  if len(title) > 0:
@@ -185,6 +189,11 @@ def write_html_image_list(filename=None,images=None,options=None):
185
189
  '<p style="{}">{}</p>\n'\
186
190
  .format(textStyle,title))
187
191
 
192
+ linkTarget = linkTarget.replace('\\','/')
193
+ if options['urlEncodeLinkTargets']:
194
+ # These are typically absolute paths, so we only want to mess with certain characters
195
+ linkTarget = urllib.parse.quote(linkTarget,safe=':/')
196
+
188
197
  if len(linkTarget) > 0:
189
198
  fHtml.write('<a href="{}">'.format(linkTarget))
190
199
  # imageStyle.append(';border:0px;')
@@ -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(...)
119
133
 
120
- def exif_preserving_save(pil_image,output_file):
134
+
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,23 +199,33 @@ 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).
174
212
 
175
213
  None is equivalent to -1 for target_width and target_height.
176
214
 
177
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.
178
224
  """
179
225
 
226
+ image_fn = 'in_memory'
180
227
  if isinstance(image,str):
228
+ image_fn = image
181
229
  image = load_image(image)
182
230
 
183
231
  if target_width is None:
@@ -185,11 +233,15 @@ def resize_image(image, target_width, target_height=-1, output_file=None):
185
233
 
186
234
  if target_height is None:
187
235
  target_height = -1
236
+
237
+ resize_required = True
188
238
 
189
- # Null operation
239
+ # No resize was requested, this is always a no-op
190
240
  if target_width == -1 and target_height == -1:
191
- return image
192
-
241
+
242
+ resize_required = False
243
+
244
+ # Does either dimension need to scale according to the other?
193
245
  elif target_width == -1 or target_height == -1:
194
246
 
195
247
  # Aspect ratio as width over height
@@ -202,39 +254,46 @@ def resize_image(image, target_width, target_height=-1, output_file=None):
202
254
  else:
203
255
  # w = ar * h
204
256
  target_width = int(aspect_ratio * target_height)
205
-
206
- # This parameter changed between Pillow versions 9 and 10, and for a bit, I'd like to
207
- # 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.
208
285
  try:
209
286
  resized_image = image.resize((target_width, target_height), Image.ANTIALIAS)
210
287
  except:
211
288
  resized_image = image.resize((target_width, target_height), Image.Resampling.LANCZOS)
212
289
 
213
290
  if output_file is not None:
214
- exif_preserving_save(resized_image,output_file)
291
+ exif_preserving_save(resized_image,output_file,quality=quality,verbose=verbose)
215
292
 
216
293
  return resized_image
217
294
 
295
+ # ...def resize_image(...)
218
296
 
219
- def show_images_in_a_row(images):
220
-
221
- num = len(images)
222
- assert num > 0
223
-
224
- if isinstance(images[0], str):
225
- images = [Image.open(img) for img in images]
226
-
227
- fig, axarr = plt.subplots(1, num, squeeze=False) # number of rows, number of columns
228
- fig.set_size_inches((num * 5, 25)) # each image is 2 inches wide
229
- for i, img in enumerate(images):
230
- axarr[0, i].set_axis_off()
231
- axarr[0, i].imshow(img)
232
- return fig
233
-
234
-
235
- # The following three functions are modified versions of those at:
236
- #
237
- # https://github.com/tensorflow/models/blob/master/research/object_detection/utils/visualization_utils.py
238
297
 
239
298
  DEFAULT_COLORS = [
240
299
  'AliceBlue', 'Red', 'RoyalBlue', 'Gold', 'Chartreuse', 'Aqua', 'Azure',
@@ -373,7 +432,8 @@ def render_detection_bounding_boxes(detections, image,
373
432
 
374
433
  label_map: optional, mapping the numerical label to a string name. The type of the numerical label
375
434
  (default string) needs to be consistent with the keys in label_map; no casting is carried out.
376
- 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 {}.
377
437
 
378
438
  classification_label_map: optional, mapping of the string class labels to the actual class names.
379
439
  The type of the numerical label (default string) needs to be consistent with the keys in
@@ -491,6 +551,8 @@ def render_detection_bounding_boxes(detections, image,
491
551
  expansion=expansion, colormap=colormap, textalign=textalign,
492
552
  label_font_size=label_font_size)
493
553
 
554
+ # ...render_detection_bounding_boxes(...)
555
+
494
556
 
495
557
  def draw_bounding_boxes_on_image(image,
496
558
  boxes,
@@ -537,6 +599,8 @@ def draw_bounding_boxes_on_image(image,
537
599
  textalign=textalign,
538
600
  label_font_size=label_font_size)
539
601
 
602
+ # ...draw_bounding_boxes_on_image(...)
603
+
540
604
 
541
605
  def draw_bounding_box_on_image(image,
542
606
  ymin,
@@ -567,8 +631,9 @@ def draw_bounding_box_on_image(image,
567
631
  ymin: ymin of bounding box - upper left.
568
632
  xmin: xmin of bounding box.
569
633
  ymax: ymax of bounding box.
570
- xmax: xmax of bounding box.
571
- 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.
572
637
  thickness: line thickness. Default value is 4.
573
638
  expansion: number of pixels to expand bounding boxes on each side. Default is 0.
574
639
  display_str_list: list of strings to display in box
@@ -576,10 +641,15 @@ def draw_bounding_box_on_image(image,
576
641
  use_normalized_coordinates: If True (default), treat coordinates
577
642
  ymin, xmin, ymax, xmax as relative to the image. Otherwise treat
578
643
  coordinates as absolute.
579
- 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
580
649
  """
581
650
 
582
651
  if clss is None:
652
+ # Default to the MegaDetector animal class ID (1)
583
653
  color = colormap[1]
584
654
  else:
585
655
  color = colormap[int(clss) % len(colormap)]
@@ -685,6 +755,8 @@ def draw_bounding_box_on_image(image,
685
755
 
686
756
  text_bottom -= (text_height + 2 * margin)
687
757
 
758
+ # ...def draw_bounding_box_on_image(...)
759
+
688
760
 
689
761
  def render_iMerit_boxes(boxes, classes, image,
690
762
  label_map=annotation_constants.annotation_bbox_category_id_to_name):
@@ -758,6 +830,8 @@ def render_megadb_bounding_boxes(boxes_info, image):
758
830
  display_boxes = np.array(display_boxes)
759
831
  draw_bounding_boxes_on_image(image, display_boxes, classes, display_strs=display_strs)
760
832
 
833
+ # ...def render_iMerit_boxes(...)
834
+
761
835
 
762
836
  def render_db_bounding_boxes(boxes, classes, image, original_size=None,
763
837
  label_map=None, thickness=DEFAULT_BOX_THICKNESS, expansion=0):
@@ -802,13 +876,16 @@ def render_db_bounding_boxes(boxes, classes, image, original_size=None,
802
876
  draw_bounding_boxes_on_image(image, display_boxes, classes, display_strs=display_strs,
803
877
  thickness=thickness, expansion=expansion)
804
878
 
879
+ # ...def render_db_bounding_boxes(...)
880
+
805
881
 
806
882
  def draw_bounding_boxes_on_file(input_file, output_file, detections, confidence_threshold=0.0,
807
883
  detector_label_map=DEFAULT_DETECTOR_LABEL_MAP,
808
884
  thickness=DEFAULT_BOX_THICKNESS, expansion=0,
809
885
  colormap=DEFAULT_COLORS,
810
886
  label_font_size=DEFAULT_LABEL_FONT_SIZE,
811
- custom_strings=None,target_size=None):
887
+ custom_strings=None,target_size=None,
888
+ ignore_exif_rotation=False):
812
889
  """
813
890
  Render detection bounding boxes on an image loaded from file, writing the results to a
814
891
  new image file.
@@ -823,7 +900,9 @@ def draw_bounding_boxes_on_file(input_file, output_file, detections, confidence_
823
900
 
824
901
  Normalized, with the origin at the upper-left.
825
902
 
826
- 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.
827
906
 
828
907
  custom_strings: optional set of strings to append to detection labels, should have the
829
908
  same length as [detections]. Appended before classification labels, if classification
@@ -833,7 +912,7 @@ def draw_bounding_boxes_on_file(input_file, output_file, detections, confidence_
833
912
  see resize_image for documentation. If None or (-1,-1), uses the original image size.
834
913
  """
835
914
 
836
- image = open_image(input_file)
915
+ image = open_image(input_file, ignore_exif_rotation=ignore_exif_rotation)
837
916
 
838
917
  if target_size is not None:
839
918
  image = resize_image(image,target_size[0],target_size[1])
@@ -848,7 +927,8 @@ def draw_bounding_boxes_on_file(input_file, output_file, detections, confidence_
848
927
 
849
928
 
850
929
  def draw_db_boxes_on_file(input_file, output_file, boxes, classes=None,
851
- label_map=None, thickness=DEFAULT_BOX_THICKNESS, expansion=0):
930
+ label_map=None, thickness=DEFAULT_BOX_THICKNESS, expansion=0,
931
+ ignore_exif_rotation=False):
852
932
  """
853
933
  Render COCO bounding boxes (in absolute coordinates) on an image loaded from file, writing the
854
934
  results to a new image file.
@@ -858,7 +938,7 @@ def draw_db_boxes_on_file(input_file, output_file, boxes, classes=None,
858
938
  detector_label_map is a dict mapping category IDs to strings.
859
939
  """
860
940
 
861
- image = open_image(input_file)
941
+ image = open_image(input_file, ignore_exif_rotation=ignore_exif_rotation)
862
942
 
863
943
  if classes is None:
864
944
  classes = [0] * len(boxes)
@@ -869,6 +949,9 @@ def draw_db_boxes_on_file(input_file, output_file, boxes, classes=None,
869
949
  image.save(output_file)
870
950
 
871
951
 
952
+ # ...def draw_bounding_boxes_on_file(...)
953
+
954
+
872
955
  def gray_scale_fraction(image,crop_size=(0.1,0.1)):
873
956
  """
874
957
  Returns the fraction of the pixels in [image] that appear to be grayscale (R==G==B),
@@ -938,3 +1021,211 @@ def gray_scale_fraction(image,crop_size=(0.1,0.1)):
938
1021
  r, g, b = image.getpixel((i,j))
939
1022
  if r == g and r == b and g == b:
940
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)