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.
- api/batch_processing/data_preparation/manage_local_batch.py +28 -14
- api/batch_processing/postprocessing/combine_api_outputs.py +2 -2
- api/batch_processing/postprocessing/compare_batch_results.py +1 -1
- api/batch_processing/postprocessing/convert_output_format.py +24 -6
- api/batch_processing/postprocessing/load_api_results.py +1 -3
- api/batch_processing/postprocessing/md_to_labelme.py +118 -51
- api/batch_processing/postprocessing/merge_detections.py +30 -5
- api/batch_processing/postprocessing/postprocess_batch_results.py +24 -12
- api/batch_processing/postprocessing/remap_detection_categories.py +163 -0
- api/batch_processing/postprocessing/render_detection_confusion_matrix.py +15 -12
- api/batch_processing/postprocessing/repeat_detection_elimination/repeat_detections_core.py +2 -2
- data_management/cct_json_utils.py +7 -2
- data_management/coco_to_labelme.py +263 -0
- data_management/coco_to_yolo.py +7 -4
- data_management/databases/integrity_check_json_db.py +68 -59
- data_management/databases/subset_json_db.py +1 -1
- data_management/get_image_sizes.py +44 -26
- data_management/importers/animl_results_to_md_results.py +1 -3
- data_management/importers/noaa_seals_2019.py +1 -1
- data_management/labelme_to_coco.py +252 -143
- data_management/labelme_to_yolo.py +95 -52
- data_management/lila/create_lila_blank_set.py +106 -23
- data_management/lila/download_lila_subset.py +133 -65
- data_management/lila/generate_lila_per_image_labels.py +1 -1
- data_management/lila/lila_common.py +8 -38
- data_management/read_exif.py +65 -16
- data_management/remap_coco_categories.py +84 -0
- data_management/resize_coco_dataset.py +3 -22
- data_management/wi_download_csv_to_coco.py +239 -0
- data_management/yolo_to_coco.py +283 -83
- detection/run_detector_batch.py +12 -3
- detection/run_inference_with_yolov5_val.py +10 -3
- detection/run_tiled_inference.py +2 -2
- detection/tf_detector.py +2 -1
- detection/video_utils.py +1 -1
- md_utils/ct_utils.py +22 -3
- md_utils/md_tests.py +11 -2
- md_utils/path_utils.py +206 -32
- md_utils/url_utils.py +66 -1
- md_utils/write_html_image_list.py +12 -3
- md_visualization/visualization_utils.py +363 -72
- md_visualization/visualize_db.py +33 -10
- {megadetector-5.0.7.dist-info → megadetector-5.0.8.dist-info}/METADATA +10 -12
- {megadetector-5.0.7.dist-info → megadetector-5.0.8.dist-info}/RECORD +47 -44
- {megadetector-5.0.7.dist-info → megadetector-5.0.8.dist-info}/WHEEL +1 -1
- md_visualization/visualize_megadb.py +0 -183
- {megadetector-5.0.7.dist-info → megadetector-5.0.8.dist-info}/LICENSE +0 -0
- {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
|
-
|
|
180
|
-
|
|
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)
|
|
28
|
+
detector_bbox_category_id_to_name)
|
|
23
29
|
|
|
24
30
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
|
25
31
|
|
|
26
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
239
|
+
# No resize was requested, this is always a no-op
|
|
190
240
|
if target_width == -1 and target_height == -1:
|
|
191
|
-
|
|
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
|
-
#
|
|
207
|
-
|
|
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
|
|
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
|
|
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)
|