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.
- api/batch_processing/data_preparation/manage_local_batch.py +297 -202
- api/batch_processing/data_preparation/manage_video_batch.py +7 -2
- api/batch_processing/postprocessing/add_max_conf.py +1 -0
- api/batch_processing/postprocessing/combine_api_outputs.py +2 -2
- api/batch_processing/postprocessing/compare_batch_results.py +111 -61
- api/batch_processing/postprocessing/convert_output_format.py +24 -6
- api/batch_processing/postprocessing/load_api_results.py +56 -72
- api/batch_processing/postprocessing/md_to_labelme.py +119 -51
- api/batch_processing/postprocessing/merge_detections.py +30 -5
- api/batch_processing/postprocessing/postprocess_batch_results.py +175 -55
- api/batch_processing/postprocessing/remap_detection_categories.py +163 -0
- api/batch_processing/postprocessing/render_detection_confusion_matrix.py +628 -0
- api/batch_processing/postprocessing/repeat_detection_elimination/find_repeat_detections.py +71 -23
- api/batch_processing/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +1 -1
- api/batch_processing/postprocessing/repeat_detection_elimination/repeat_detections_core.py +224 -76
- api/batch_processing/postprocessing/subset_json_detector_output.py +132 -5
- api/batch_processing/postprocessing/top_folders_to_bottom.py +1 -1
- classification/prepare_classification_script.py +191 -191
- data_management/cct_json_utils.py +7 -2
- data_management/coco_to_labelme.py +263 -0
- data_management/coco_to_yolo.py +72 -48
- data_management/databases/integrity_check_json_db.py +75 -64
- data_management/databases/subset_json_db.py +1 -1
- data_management/generate_crops_from_cct.py +1 -1
- data_management/get_image_sizes.py +44 -26
- data_management/importers/animl_results_to_md_results.py +3 -5
- data_management/importers/noaa_seals_2019.py +2 -2
- data_management/importers/zamba_results_to_md_results.py +2 -2
- data_management/labelme_to_coco.py +264 -127
- data_management/labelme_to_yolo.py +96 -53
- data_management/lila/create_lila_blank_set.py +557 -0
- data_management/lila/create_lila_test_set.py +2 -1
- data_management/lila/create_links_to_md_results_files.py +1 -1
- data_management/lila/download_lila_subset.py +138 -45
- data_management/lila/generate_lila_per_image_labels.py +23 -14
- data_management/lila/get_lila_annotation_counts.py +16 -10
- data_management/lila/lila_common.py +15 -42
- data_management/lila/test_lila_metadata_urls.py +116 -0
- data_management/read_exif.py +65 -16
- data_management/remap_coco_categories.py +84 -0
- data_management/resize_coco_dataset.py +14 -31
- data_management/wi_download_csv_to_coco.py +239 -0
- data_management/yolo_output_to_md_output.py +40 -13
- data_management/yolo_to_coco.py +313 -100
- detection/process_video.py +36 -14
- detection/pytorch_detector.py +1 -1
- detection/run_detector.py +73 -18
- detection/run_detector_batch.py +116 -27
- detection/run_inference_with_yolov5_val.py +135 -27
- detection/run_tiled_inference.py +153 -43
- detection/tf_detector.py +2 -1
- detection/video_utils.py +4 -2
- md_utils/ct_utils.py +101 -6
- md_utils/md_tests.py +264 -17
- md_utils/path_utils.py +326 -47
- md_utils/process_utils.py +26 -7
- md_utils/split_locations_into_train_val.py +215 -0
- md_utils/string_utils.py +10 -0
- md_utils/url_utils.py +66 -3
- md_utils/write_html_image_list.py +12 -2
- md_visualization/visualization_utils.py +380 -74
- md_visualization/visualize_db.py +41 -10
- md_visualization/visualize_detector_output.py +185 -104
- {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/METADATA +11 -13
- {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/RECORD +74 -67
- {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/WHEEL +1 -1
- taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +1 -1
- taxonomy_mapping/map_new_lila_datasets.py +43 -39
- taxonomy_mapping/prepare_lila_taxonomy_release.py +5 -2
- taxonomy_mapping/preview_lila_taxonomy.py +27 -27
- taxonomy_mapping/species_lookup.py +33 -13
- taxonomy_mapping/taxonomy_csv_checker.py +7 -5
- md_visualization/visualize_megadb.py +0 -183
- {megadetector-5.0.6.dist-info → megadetector-5.0.8.dist-info}/LICENSE +0 -0
- {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)
|
|
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(...)
|
|
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
|
-
#
|
|
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,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
199
|
-
|
|
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
|
|
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 >=
|
|
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
|
|
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
|
|
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)
|