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