megadetector 5.0.27__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 +232 -223
- 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 +341 -338
- megadetector/detection/pytorch_detector.py +308 -266
- megadetector/detection/run_detector.py +186 -166
- megadetector/detection/run_detector_batch.py +366 -364
- megadetector/detection/run_inference_with_yolov5_val.py +328 -325
- megadetector/detection/run_tiled_inference.py +312 -253
- megadetector/detection/tf_detector.py +24 -24
- megadetector/detection/video_utils.py +291 -283
- megadetector/postprocessing/add_max_conf.py +15 -11
- megadetector/postprocessing/categorize_detections_by_size.py +44 -44
- megadetector/postprocessing/classification_postprocessing.py +808 -311
- 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 +220 -147
- megadetector/postprocessing/detector_calibration.py +173 -168
- megadetector/postprocessing/generate_csv_report.py +508 -0
- megadetector/postprocessing/load_api_results.py +25 -22
- 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 +319 -302
- 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 -69
- 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 +11 -11
- megadetector/taxonomy_mapping/validate_lila_category_mappings.py +13 -13
- megadetector/utils/azure_utils.py +22 -22
- megadetector/utils/ct_utils.py +1019 -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 +1511 -406
- megadetector/utils/process_utils.py +41 -41
- megadetector/utils/sas_blob_utils.py +53 -49
- megadetector/utils/split_locations_into_train_val.py +73 -60
- megadetector/utils/string_utils.py +147 -26
- megadetector/utils/url_utils.py +463 -173
- megadetector/utils/wi_utils.py +2629 -2868
- 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 +424 -404
- megadetector/visualization/visualize_db.py +197 -190
- megadetector/visualization/visualize_detector_output.py +126 -98
- {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/METADATA +6 -3
- megadetector-5.0.29.dist-info/RECORD +163 -0
- {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/WHEEL +1 -1
- 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.27.dist-info/RECORD +0 -208
- {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/licenses/LICENSE +0 -0
- {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
pil_image (Image): the PIL Image
|
|
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,26 +1277,38 @@ 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
|
|
|
1286
1284
|
|
|
1287
1285
|
def _resize_relative_image(fn_relative,
|
|
1288
|
-
input_folder,
|
|
1289
|
-
|
|
1286
|
+
input_folder,
|
|
1287
|
+
output_folder,
|
|
1288
|
+
target_width,
|
|
1289
|
+
target_height,
|
|
1290
|
+
no_enlarge_width,
|
|
1291
|
+
verbose,
|
|
1292
|
+
quality,
|
|
1293
|
+
overwrite=True):
|
|
1290
1294
|
"""
|
|
1291
1295
|
Internal function for resizing an image from one folder to another,
|
|
1292
1296
|
maintaining relative path.
|
|
1293
1297
|
"""
|
|
1294
|
-
|
|
1298
|
+
|
|
1295
1299
|
input_fn_abs = os.path.join(input_folder,fn_relative)
|
|
1296
1300
|
output_fn_abs = os.path.join(output_folder,fn_relative)
|
|
1301
|
+
|
|
1302
|
+
if (not overwrite) and (os.path.isfile(output_fn_abs)):
|
|
1303
|
+
status = 'skipped'
|
|
1304
|
+
error = None
|
|
1305
|
+
return {'fn_relative':fn_relative,'status':status,'error':error}
|
|
1306
|
+
|
|
1297
1307
|
os.makedirs(os.path.dirname(output_fn_abs),exist_ok=True)
|
|
1298
1308
|
try:
|
|
1299
|
-
_ = resize_image(input_fn_abs,
|
|
1300
|
-
output_file=output_fn_abs,
|
|
1301
|
-
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,
|
|
1302
1312
|
no_enlarge_width=no_enlarge_width, verbose=verbose, quality=quality)
|
|
1303
1313
|
status = 'success'
|
|
1304
1314
|
error = None
|
|
@@ -1307,7 +1317,7 @@ def _resize_relative_image(fn_relative,
|
|
|
1307
1317
|
print('Error resizing {}: {}'.format(fn_relative,str(e)))
|
|
1308
1318
|
status = 'error'
|
|
1309
1319
|
error = str(e)
|
|
1310
|
-
|
|
1320
|
+
|
|
1311
1321
|
return {'fn_relative':fn_relative,'status':status,'error':error}
|
|
1312
1322
|
|
|
1313
1323
|
# ...def _resize_relative_image(...)
|
|
@@ -1315,18 +1325,17 @@ def _resize_relative_image(fn_relative,
|
|
|
1315
1325
|
|
|
1316
1326
|
def _resize_absolute_image(input_output_files,
|
|
1317
1327
|
target_width,target_height,no_enlarge_width,verbose,quality):
|
|
1318
|
-
|
|
1319
1328
|
"""
|
|
1320
1329
|
Internal wrapper for resize_image used in the context of a batch resize operation.
|
|
1321
1330
|
"""
|
|
1322
|
-
|
|
1331
|
+
|
|
1323
1332
|
input_fn_abs = input_output_files[0]
|
|
1324
1333
|
output_fn_abs = input_output_files[1]
|
|
1325
1334
|
os.makedirs(os.path.dirname(output_fn_abs),exist_ok=True)
|
|
1326
1335
|
try:
|
|
1327
|
-
_ = resize_image(input_fn_abs,
|
|
1328
|
-
output_file=output_fn_abs,
|
|
1329
|
-
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,
|
|
1330
1339
|
no_enlarge_width=no_enlarge_width, verbose=verbose, quality=quality)
|
|
1331
1340
|
status = 'success'
|
|
1332
1341
|
error = None
|
|
@@ -1335,7 +1344,7 @@ def _resize_absolute_image(input_output_files,
|
|
|
1335
1344
|
print('Error resizing {}: {}'.format(input_fn_abs,str(e)))
|
|
1336
1345
|
status = 'error'
|
|
1337
1346
|
error = str(e)
|
|
1338
|
-
|
|
1347
|
+
|
|
1339
1348
|
return {'input_fn':input_fn_abs,'output_fn':output_fn_abs,status:'status',
|
|
1340
1349
|
'error':error}
|
|
1341
1350
|
|
|
@@ -1343,21 +1352,21 @@ def _resize_absolute_image(input_output_files,
|
|
|
1343
1352
|
|
|
1344
1353
|
|
|
1345
1354
|
def resize_images(input_file_to_output_file,
|
|
1346
|
-
target_width=-1,
|
|
1355
|
+
target_width=-1,
|
|
1347
1356
|
target_height=-1,
|
|
1348
|
-
no_enlarge_width=False,
|
|
1349
|
-
verbose=False,
|
|
1357
|
+
no_enlarge_width=False,
|
|
1358
|
+
verbose=False,
|
|
1350
1359
|
quality='keep',
|
|
1351
|
-
pool_type='process',
|
|
1360
|
+
pool_type='process',
|
|
1352
1361
|
n_workers=10):
|
|
1353
1362
|
"""
|
|
1354
1363
|
Resizes all images the dictionary [input_file_to_output_file].
|
|
1355
1364
|
|
|
1356
1365
|
TODO: This is a little more redundant with resize_image_folder than I would like;
|
|
1357
1366
|
refactor resize_image_folder to call resize_images. Not doing that yet because
|
|
1358
|
-
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
|
|
1359
1368
|
and I don't want to rock the boat yet.
|
|
1360
|
-
|
|
1369
|
+
|
|
1361
1370
|
Args:
|
|
1362
1371
|
input_file_to_output_file (dict): dict mapping images that exist to the locations
|
|
1363
1372
|
where the resized versions should be written
|
|
@@ -1365,8 +1374,8 @@ def resize_images(input_file_to_output_file,
|
|
|
1365
1374
|
to let target_height determine the size
|
|
1366
1375
|
target_height (int, optional): height to which we should resize this image, or -1
|
|
1367
1376
|
to let target_width determine the size
|
|
1368
|
-
no_enlarge_width (bool, optional): if [no_enlarge_width] is True, and
|
|
1369
|
-
[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,
|
|
1370
1379
|
but will write to output_file if supplied
|
|
1371
1380
|
verbose (bool, optional): enable additional debug output
|
|
1372
1381
|
quality (str or int, optional): passed to exif_preserving_save, see docs for more detail
|
|
@@ -1377,20 +1386,20 @@ def resize_images(input_file_to_output_file,
|
|
|
1377
1386
|
|
|
1378
1387
|
Returns:
|
|
1379
1388
|
list: a list of dicts with keys 'input_fn', 'output_fn', 'status', and 'error'.
|
|
1380
|
-
'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,
|
|
1381
1390
|
otherwise will contain the image-specific error.
|
|
1382
1391
|
"""
|
|
1383
|
-
|
|
1392
|
+
|
|
1384
1393
|
assert pool_type in ('process','thread'), 'Illegal pool type {}'.format(pool_type)
|
|
1385
|
-
|
|
1394
|
+
|
|
1386
1395
|
input_output_file_pairs = []
|
|
1387
|
-
|
|
1396
|
+
|
|
1388
1397
|
# Reformat input files as (input,output) tuples
|
|
1389
1398
|
for input_fn in input_file_to_output_file:
|
|
1390
1399
|
input_output_file_pairs.append((input_fn,input_file_to_output_file[input_fn]))
|
|
1391
|
-
|
|
1392
|
-
if n_workers == 1:
|
|
1393
|
-
|
|
1400
|
+
|
|
1401
|
+
if n_workers == 1:
|
|
1402
|
+
|
|
1394
1403
|
results = []
|
|
1395
1404
|
for i_o_file_pair in tqdm(input_output_file_pairs):
|
|
1396
1405
|
results.append(_resize_absolute_image(i_o_file_pair,
|
|
@@ -1401,46 +1410,54 @@ def resize_images(input_file_to_output_file,
|
|
|
1401
1410
|
quality=quality))
|
|
1402
1411
|
|
|
1403
1412
|
else:
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
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")
|
|
1422
1438
|
|
|
1423
1439
|
return results
|
|
1424
1440
|
|
|
1425
1441
|
# ...def resize_images(...)
|
|
1426
1442
|
|
|
1427
1443
|
|
|
1428
|
-
def resize_image_folder(input_folder,
|
|
1444
|
+
def resize_image_folder(input_folder,
|
|
1429
1445
|
output_folder=None,
|
|
1430
|
-
target_width=-1,
|
|
1446
|
+
target_width=-1,
|
|
1431
1447
|
target_height=-1,
|
|
1432
|
-
no_enlarge_width=False,
|
|
1433
|
-
verbose=False,
|
|
1448
|
+
no_enlarge_width=False,
|
|
1449
|
+
verbose=False,
|
|
1434
1450
|
quality='keep',
|
|
1435
|
-
pool_type='process',
|
|
1436
|
-
n_workers=10,
|
|
1451
|
+
pool_type='process',
|
|
1452
|
+
n_workers=10,
|
|
1437
1453
|
recursive=True,
|
|
1438
|
-
image_files_relative=None
|
|
1454
|
+
image_files_relative=None,
|
|
1455
|
+
overwrite=True):
|
|
1439
1456
|
"""
|
|
1440
1457
|
Resize all images in a folder (defaults to recursive).
|
|
1441
|
-
|
|
1458
|
+
|
|
1442
1459
|
Defaults to in-place resizing (output_folder is optional).
|
|
1443
|
-
|
|
1460
|
+
|
|
1444
1461
|
Args:
|
|
1445
1462
|
input_folder (str): folder in which we should find images to resize
|
|
1446
1463
|
output_folder (str, optional): folder in which we should write resized images. If
|
|
@@ -1450,8 +1467,8 @@ def resize_image_folder(input_folder,
|
|
|
1450
1467
|
to let target_height determine the size
|
|
1451
1468
|
target_height (int, optional): height to which we should resize this image, or -1
|
|
1452
1469
|
to let target_width determine the size
|
|
1453
|
-
no_enlarge_width (bool, optional): if [no_enlarge_width] is True, and
|
|
1454
|
-
[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,
|
|
1455
1472
|
but will write to output_file if supplied
|
|
1456
1473
|
verbose (bool, optional): enable additional debug output
|
|
1457
1474
|
quality (str or int, optional): passed to exif_preserving_save, see docs for more detail
|
|
@@ -1461,35 +1478,36 @@ def resize_image_folder(input_folder,
|
|
|
1461
1478
|
to disable parallelization
|
|
1462
1479
|
recursive (bool, optional): whether to search [input_folder] recursively for images.
|
|
1463
1480
|
image_files_relative (list, optional): if not None, skips any relative paths not
|
|
1464
|
-
in this list
|
|
1465
|
-
|
|
1481
|
+
in this list
|
|
1482
|
+
overwrite (bool, optional): whether to overwrite existing target images
|
|
1483
|
+
|
|
1466
1484
|
Returns:
|
|
1467
1485
|
list: a list of dicts with keys 'input_fn', 'output_fn', 'status', and 'error'.
|
|
1468
|
-
'status' will be 'success' or 'error'; 'error' will be None for successful
|
|
1469
|
-
otherwise will contain the image-specific error.
|
|
1486
|
+
'status' will be 'success', 'skipped', or 'error'; 'error' will be None for successful
|
|
1487
|
+
cases, otherwise will contain the image-specific error.
|
|
1470
1488
|
"""
|
|
1471
1489
|
|
|
1472
1490
|
assert os.path.isdir(input_folder), '{} is not a folder'.format(input_folder)
|
|
1473
|
-
|
|
1491
|
+
|
|
1474
1492
|
if output_folder is None:
|
|
1475
1493
|
output_folder = input_folder
|
|
1476
1494
|
else:
|
|
1477
1495
|
os.makedirs(output_folder,exist_ok=True)
|
|
1478
|
-
|
|
1496
|
+
|
|
1479
1497
|
assert pool_type in ('process','thread'), 'Illegal pool type {}'.format(pool_type)
|
|
1480
|
-
|
|
1498
|
+
|
|
1481
1499
|
if image_files_relative is None:
|
|
1482
|
-
|
|
1500
|
+
|
|
1483
1501
|
if verbose:
|
|
1484
1502
|
print('Enumerating images')
|
|
1485
|
-
|
|
1503
|
+
|
|
1486
1504
|
image_files_relative = find_images(input_folder,recursive=recursive,
|
|
1487
1505
|
return_relative_paths=True,convert_slashes=True)
|
|
1488
1506
|
if verbose:
|
|
1489
1507
|
print('Found {} images'.format(len(image_files_relative)))
|
|
1490
|
-
|
|
1491
|
-
if n_workers == 1:
|
|
1492
|
-
|
|
1508
|
+
|
|
1509
|
+
if n_workers == 1:
|
|
1510
|
+
|
|
1493
1511
|
if verbose:
|
|
1494
1512
|
print('Resizing images')
|
|
1495
1513
|
|
|
@@ -1502,19 +1520,20 @@ def resize_image_folder(input_folder,
|
|
|
1502
1520
|
target_height=target_height,
|
|
1503
1521
|
no_enlarge_width=no_enlarge_width,
|
|
1504
1522
|
verbose=verbose,
|
|
1505
|
-
quality=quality
|
|
1523
|
+
quality=quality,
|
|
1524
|
+
overwrite=overwrite))
|
|
1506
1525
|
|
|
1507
1526
|
else:
|
|
1508
|
-
|
|
1527
|
+
|
|
1509
1528
|
if pool_type == 'thread':
|
|
1510
|
-
pool = ThreadPool(n_workers); poolstring = 'threads'
|
|
1529
|
+
pool = ThreadPool(n_workers); poolstring = 'threads'
|
|
1511
1530
|
else:
|
|
1512
1531
|
assert pool_type == 'process'
|
|
1513
1532
|
pool = Pool(n_workers); poolstring = 'processes'
|
|
1514
|
-
|
|
1533
|
+
|
|
1515
1534
|
if verbose:
|
|
1516
1535
|
print('Starting resizing pool with {} {}'.format(n_workers,poolstring))
|
|
1517
|
-
|
|
1536
|
+
|
|
1518
1537
|
p = partial(_resize_relative_image,
|
|
1519
1538
|
input_folder=input_folder,
|
|
1520
1539
|
output_folder=output_folder,
|
|
@@ -1522,8 +1541,9 @@ def resize_image_folder(input_folder,
|
|
|
1522
1541
|
target_height=target_height,
|
|
1523
1542
|
no_enlarge_width=no_enlarge_width,
|
|
1524
1543
|
verbose=verbose,
|
|
1525
|
-
quality=quality
|
|
1526
|
-
|
|
1544
|
+
quality=quality,
|
|
1545
|
+
overwrite=overwrite)
|
|
1546
|
+
|
|
1527
1547
|
results = list(tqdm(pool.imap(p, image_files_relative),total=len(image_files_relative)))
|
|
1528
1548
|
|
|
1529
1549
|
return results
|
|
@@ -1534,17 +1554,17 @@ def resize_image_folder(input_folder,
|
|
|
1534
1554
|
def get_image_size(im,verbose=False):
|
|
1535
1555
|
"""
|
|
1536
1556
|
Retrieve the size of an image. Returns None if the image fails to load.
|
|
1537
|
-
|
|
1557
|
+
|
|
1538
1558
|
Args:
|
|
1539
1559
|
im (str or PIL.Image): filename or PIL image
|
|
1540
|
-
|
|
1560
|
+
|
|
1541
1561
|
Returns:
|
|
1542
1562
|
tuple (w,h), or None if the image fails to load.
|
|
1543
1563
|
"""
|
|
1544
|
-
|
|
1564
|
+
|
|
1545
1565
|
image_name = '[in memory]'
|
|
1546
|
-
|
|
1547
|
-
try:
|
|
1566
|
+
|
|
1567
|
+
try:
|
|
1548
1568
|
if isinstance(im,str):
|
|
1549
1569
|
image_name = im
|
|
1550
1570
|
im = load_image(im)
|
|
@@ -1561,20 +1581,20 @@ def get_image_size(im,verbose=False):
|
|
|
1561
1581
|
print('Error reading width from image {}: {}'.format(
|
|
1562
1582
|
image_name,str(e)))
|
|
1563
1583
|
return None
|
|
1564
|
-
|
|
1584
|
+
|
|
1565
1585
|
# ...def get_image_size(...)
|
|
1566
1586
|
|
|
1567
1587
|
|
|
1568
1588
|
def parallel_get_image_sizes(filenames,
|
|
1569
|
-
max_workers=16,
|
|
1570
|
-
use_threads=True,
|
|
1589
|
+
max_workers=16,
|
|
1590
|
+
use_threads=True,
|
|
1571
1591
|
recursive=True,
|
|
1572
1592
|
verbose=False):
|
|
1573
1593
|
"""
|
|
1574
1594
|
Retrieve image sizes for a list or folder of images
|
|
1575
|
-
|
|
1595
|
+
|
|
1576
1596
|
Args:
|
|
1577
|
-
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
|
|
1578
1598
|
unreadable images will be returned with a file size of None.
|
|
1579
1599
|
max_workers (int, optional): the number of parallel workers to use; set to <=1 to disable
|
|
1580
1600
|
parallelization
|
|
@@ -1583,7 +1603,7 @@ def parallel_get_image_sizes(filenames,
|
|
|
1583
1603
|
recursive (bool, optional): if [filenames] is a folder, whether to search recursively for images.
|
|
1584
1604
|
Ignored if [filenames] is a list.
|
|
1585
1605
|
verbose (bool, optional): enable additional debug output
|
|
1586
|
-
|
|
1606
|
+
|
|
1587
1607
|
Returns:
|
|
1588
1608
|
dict: a dict mapping filenames to (w,h) tuples; the value will be None for images that fail
|
|
1589
1609
|
to load.
|
|
@@ -1593,34 +1613,34 @@ def parallel_get_image_sizes(filenames,
|
|
|
1593
1613
|
if verbose:
|
|
1594
1614
|
print('Enumerating images in {}'.format(filenames))
|
|
1595
1615
|
filenames = find_images(filenames,recursive=recursive,return_relative_paths=False)
|
|
1596
|
-
|
|
1616
|
+
|
|
1597
1617
|
n_workers = min(max_workers,len(filenames))
|
|
1598
|
-
|
|
1618
|
+
|
|
1599
1619
|
if verbose:
|
|
1600
1620
|
print('Getting image sizes for {} images'.format(len(filenames)))
|
|
1601
|
-
|
|
1621
|
+
|
|
1602
1622
|
if n_workers <= 1:
|
|
1603
|
-
|
|
1623
|
+
|
|
1604
1624
|
results = []
|
|
1605
1625
|
for filename in filenames:
|
|
1606
1626
|
results.append(get_image_size(filename,verbose=verbose))
|
|
1607
|
-
|
|
1627
|
+
|
|
1608
1628
|
else:
|
|
1609
|
-
|
|
1629
|
+
|
|
1610
1630
|
if use_threads:
|
|
1611
1631
|
pool = ThreadPool(n_workers)
|
|
1612
1632
|
else:
|
|
1613
1633
|
pool = Pool(n_workers)
|
|
1614
|
-
|
|
1634
|
+
|
|
1615
1635
|
results = list(tqdm(pool.imap(
|
|
1616
1636
|
partial(get_image_size,verbose=verbose),filenames), total=len(filenames)))
|
|
1617
|
-
|
|
1637
|
+
|
|
1618
1638
|
assert len(filenames) == len(results), 'Internal error in parallel_get_image_sizes'
|
|
1619
|
-
|
|
1639
|
+
|
|
1620
1640
|
to_return = {}
|
|
1621
1641
|
for i_file,filename in enumerate(filenames):
|
|
1622
1642
|
to_return[filename] = results[i_file]
|
|
1623
|
-
|
|
1643
|
+
|
|
1624
1644
|
return to_return
|
|
1625
1645
|
|
|
1626
1646
|
|
|
@@ -1629,30 +1649,30 @@ def parallel_get_image_sizes(filenames,
|
|
|
1629
1649
|
def check_image_integrity(filename,modes=None):
|
|
1630
1650
|
"""
|
|
1631
1651
|
Check whether we can successfully load an image via OpenCV and/or PIL.
|
|
1632
|
-
|
|
1633
|
-
Args:
|
|
1652
|
+
|
|
1653
|
+
Args:
|
|
1634
1654
|
filename (str): the filename to evaluate
|
|
1635
1655
|
modes (list, optional): a list containing one or more of:
|
|
1636
|
-
|
|
1656
|
+
|
|
1637
1657
|
- 'cv'
|
|
1638
1658
|
- 'pil'
|
|
1639
1659
|
- 'skimage'
|
|
1640
|
-
- 'jpeg_trailer'
|
|
1641
|
-
|
|
1660
|
+
- 'jpeg_trailer'
|
|
1661
|
+
|
|
1642
1662
|
'jpeg_trailer' checks that the binary data ends with ffd9. It does not check whether
|
|
1643
1663
|
the image is actually a jpeg, and even if it is, there are lots of reasons the image might not
|
|
1644
1664
|
end with ffd9. It's also true the JPEGs that cause "premature end of jpeg segment" issues
|
|
1645
1665
|
don't end with ffd9, so this may be a useful diagnostic. High precision, very low recall
|
|
1646
1666
|
for corrupt jpegs.
|
|
1647
|
-
|
|
1667
|
+
|
|
1648
1668
|
Set to None to use all modes.
|
|
1649
|
-
|
|
1669
|
+
|
|
1650
1670
|
Returns:
|
|
1651
1671
|
dict: a dict with a key called 'file' (the value of [filename]), one key for each string in
|
|
1652
1672
|
[modes] (a success indicator for that mode, specifically a string starting with either
|
|
1653
1673
|
'success' or 'error').
|
|
1654
1674
|
"""
|
|
1655
|
-
|
|
1675
|
+
|
|
1656
1676
|
if modes is None:
|
|
1657
1677
|
modes = ('cv','pil','skimage','jpeg_trailer')
|
|
1658
1678
|
else:
|
|
@@ -1660,14 +1680,14 @@ def check_image_integrity(filename,modes=None):
|
|
|
1660
1680
|
modes = [modes]
|
|
1661
1681
|
for mode in modes:
|
|
1662
1682
|
assert mode in ('cv','pil','skimage'), 'Unrecognized mode {}'.format(mode)
|
|
1663
|
-
|
|
1683
|
+
|
|
1664
1684
|
assert os.path.isfile(filename), 'Could not find file {}'.format(filename)
|
|
1665
|
-
|
|
1685
|
+
|
|
1666
1686
|
result = {}
|
|
1667
1687
|
result['file'] = filename
|
|
1668
|
-
|
|
1688
|
+
|
|
1669
1689
|
for mode in modes:
|
|
1670
|
-
|
|
1690
|
+
|
|
1671
1691
|
result[mode] = 'unknown'
|
|
1672
1692
|
if mode == 'pil':
|
|
1673
1693
|
try:
|
|
@@ -1684,7 +1704,7 @@ def check_image_integrity(filename,modes=None):
|
|
|
1684
1704
|
result[mode] = 'success'
|
|
1685
1705
|
except Exception as e:
|
|
1686
1706
|
result[mode] = 'error: {}'.format(str(e))
|
|
1687
|
-
elif mode == 'skimage':
|
|
1707
|
+
elif mode == 'skimage':
|
|
1688
1708
|
try:
|
|
1689
1709
|
# This is not a standard dependency
|
|
1690
1710
|
from skimage import io as skimage_io # noqa
|
|
@@ -1708,23 +1728,23 @@ def check_image_integrity(filename,modes=None):
|
|
|
1708
1728
|
result[mode] = 'success'
|
|
1709
1729
|
except Exception as e:
|
|
1710
1730
|
result[mode] = 'error: {}'.format(str(e))
|
|
1711
|
-
|
|
1712
|
-
# ...for each mode
|
|
1713
|
-
|
|
1731
|
+
|
|
1732
|
+
# ...for each mode
|
|
1733
|
+
|
|
1714
1734
|
return result
|
|
1715
1735
|
|
|
1716
1736
|
# ...def check_image_integrity(...)
|
|
1717
1737
|
|
|
1718
1738
|
|
|
1719
1739
|
def parallel_check_image_integrity(filenames,
|
|
1720
|
-
modes=None,
|
|
1721
|
-
max_workers=16,
|
|
1722
|
-
use_threads=True,
|
|
1740
|
+
modes=None,
|
|
1741
|
+
max_workers=16,
|
|
1742
|
+
use_threads=True,
|
|
1723
1743
|
recursive=True,
|
|
1724
1744
|
verbose=False):
|
|
1725
1745
|
"""
|
|
1726
1746
|
Check whether we can successfully load a list of images via OpenCV and/or PIL.
|
|
1727
|
-
|
|
1747
|
+
|
|
1728
1748
|
Args:
|
|
1729
1749
|
filenames (list or str): a list of image filenames or a folder
|
|
1730
1750
|
mode (list): see check_image_integrity() for documentation on the [modes] parameter
|
|
@@ -1735,10 +1755,10 @@ def parallel_check_image_integrity(filenames,
|
|
|
1735
1755
|
recursive (bool, optional): if [filenames] is a folder, whether to search recursively for images.
|
|
1736
1756
|
Ignored if [filenames] is a list.
|
|
1737
1757
|
verbose (bool, optional): enable additional debug output
|
|
1738
|
-
|
|
1758
|
+
|
|
1739
1759
|
Returns:
|
|
1740
|
-
list: a list of dicts, each with a key called 'file' (the value of [filename]), one key for
|
|
1741
|
-
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
|
|
1742
1762
|
with either 'success' or 'error').
|
|
1743
1763
|
"""
|
|
1744
1764
|
|
|
@@ -1746,69 +1766,69 @@ def parallel_check_image_integrity(filenames,
|
|
|
1746
1766
|
if verbose:
|
|
1747
1767
|
print('Enumerating images in {}'.format(filenames))
|
|
1748
1768
|
filenames = find_images(filenames,recursive=recursive,return_relative_paths=False)
|
|
1749
|
-
|
|
1769
|
+
|
|
1750
1770
|
n_workers = min(max_workers,len(filenames))
|
|
1751
|
-
|
|
1771
|
+
|
|
1752
1772
|
if verbose:
|
|
1753
1773
|
print('Checking image integrity for {} filenames'.format(len(filenames)))
|
|
1754
|
-
|
|
1774
|
+
|
|
1755
1775
|
if n_workers <= 1:
|
|
1756
|
-
|
|
1776
|
+
|
|
1757
1777
|
results = []
|
|
1758
1778
|
for filename in filenames:
|
|
1759
1779
|
results.append(check_image_integrity(filename,modes=modes))
|
|
1760
|
-
|
|
1780
|
+
|
|
1761
1781
|
else:
|
|
1762
|
-
|
|
1782
|
+
|
|
1763
1783
|
if use_threads:
|
|
1764
1784
|
pool = ThreadPool(n_workers)
|
|
1765
1785
|
else:
|
|
1766
1786
|
pool = Pool(n_workers)
|
|
1767
|
-
|
|
1787
|
+
|
|
1768
1788
|
results = list(tqdm(pool.imap(
|
|
1769
1789
|
partial(check_image_integrity,modes=modes),filenames), total=len(filenames)))
|
|
1770
|
-
|
|
1790
|
+
|
|
1771
1791
|
return results
|
|
1772
1792
|
|
|
1773
1793
|
|
|
1774
1794
|
#%% Test drivers
|
|
1775
1795
|
|
|
1776
1796
|
if False:
|
|
1777
|
-
|
|
1797
|
+
|
|
1778
1798
|
#%% Text rendering tests
|
|
1779
|
-
|
|
1799
|
+
|
|
1780
1800
|
import os # noqa
|
|
1781
1801
|
import numpy as np # noqa
|
|
1782
1802
|
from megadetector.visualization.visualization_utils import \
|
|
1783
1803
|
draw_bounding_boxes_on_image, exif_preserving_save, load_image, \
|
|
1784
1804
|
TEXTALIGN_LEFT,TEXTALIGN_RIGHT,VTEXTALIGN_BOTTOM,VTEXTALIGN_TOP, \
|
|
1785
1805
|
DEFAULT_LABEL_FONT_SIZE
|
|
1786
|
-
|
|
1806
|
+
|
|
1787
1807
|
fn = os.path.expanduser('~/AppData/Local/Temp/md-tests/md-test-images/ena24_7904.jpg')
|
|
1788
1808
|
output_fn = r'g:\temp\test.jpg'
|
|
1789
|
-
|
|
1809
|
+
|
|
1790
1810
|
image = load_image(fn)
|
|
1791
|
-
|
|
1811
|
+
|
|
1792
1812
|
w = 0.2; h = 0.2
|
|
1793
1813
|
all_boxes = [[0.05, 0.05, 0.25, 0.25],
|
|
1794
1814
|
[0.05, 0.35, 0.25, 0.6],
|
|
1795
1815
|
[0.35, 0.05, 0.6, 0.25],
|
|
1796
1816
|
[0.35, 0.35, 0.6, 0.6]]
|
|
1797
|
-
|
|
1817
|
+
|
|
1798
1818
|
alignments = [
|
|
1799
1819
|
[TEXTALIGN_LEFT,VTEXTALIGN_TOP],
|
|
1800
1820
|
[TEXTALIGN_LEFT,VTEXTALIGN_BOTTOM],
|
|
1801
1821
|
[TEXTALIGN_RIGHT,VTEXTALIGN_TOP],
|
|
1802
1822
|
[TEXTALIGN_RIGHT,VTEXTALIGN_BOTTOM]
|
|
1803
1823
|
]
|
|
1804
|
-
|
|
1824
|
+
|
|
1805
1825
|
labels = ['left_top','left_bottom','right_top','right_bottom']
|
|
1806
|
-
|
|
1826
|
+
|
|
1807
1827
|
text_rotation = -90
|
|
1808
1828
|
n_label_copies = 2
|
|
1809
|
-
|
|
1829
|
+
|
|
1810
1830
|
for i_box,box in enumerate(all_boxes):
|
|
1811
|
-
|
|
1831
|
+
|
|
1812
1832
|
boxes = [box]
|
|
1813
1833
|
boxes = np.array(boxes)
|
|
1814
1834
|
classes = [i_box]
|
|
@@ -1830,30 +1850,30 @@ if False:
|
|
|
1830
1850
|
exif_preserving_save(image,output_fn)
|
|
1831
1851
|
from megadetector.utils.path_utils import open_file
|
|
1832
1852
|
open_file(output_fn)
|
|
1833
|
-
|
|
1834
|
-
|
|
1853
|
+
|
|
1854
|
+
|
|
1835
1855
|
#%% Recursive resize test
|
|
1836
|
-
|
|
1856
|
+
|
|
1837
1857
|
from megadetector.visualization.visualization_utils import resize_image_folder # noqa
|
|
1838
|
-
|
|
1858
|
+
|
|
1839
1859
|
input_folder = r"C:\temp\resize-test\in"
|
|
1840
1860
|
output_folder = r"C:\temp\resize-test\out"
|
|
1841
|
-
|
|
1861
|
+
|
|
1842
1862
|
resize_results = resize_image_folder(input_folder,output_folder,
|
|
1843
1863
|
target_width=1280,verbose=True,quality=85,no_enlarge_width=True,
|
|
1844
1864
|
pool_type='process',n_workers=10)
|
|
1845
|
-
|
|
1846
|
-
|
|
1865
|
+
|
|
1866
|
+
|
|
1847
1867
|
#%% Integrity checking test
|
|
1848
|
-
|
|
1868
|
+
|
|
1849
1869
|
from megadetector.utils import md_tests
|
|
1850
1870
|
options = md_tests.download_test_data()
|
|
1851
1871
|
folder = options.scratch_dir
|
|
1852
|
-
|
|
1872
|
+
|
|
1853
1873
|
results = parallel_check_image_integrity(folder,max_workers=8)
|
|
1854
|
-
|
|
1874
|
+
|
|
1855
1875
|
modes = ['cv','pil','skimage','jpeg_trailer']
|
|
1856
|
-
|
|
1876
|
+
|
|
1857
1877
|
for r in results:
|
|
1858
1878
|
for mode in modes:
|
|
1859
1879
|
if r[mode] != 'success':
|