megadetector 5.0.28__py3-none-any.whl → 10.0.0__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/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/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/efficientnet/model.py +8 -8
- megadetector/classification/efficientnet/utils.py +6 -5
- 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 +26 -26
- 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 -2
- megadetector/data_management/camtrap_dp_to_coco.py +79 -46
- megadetector/data_management/cct_json_utils.py +103 -103
- 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 +210 -193
- megadetector/data_management/databases/add_width_and_height_to_db.py +86 -12
- megadetector/data_management/databases/combine_coco_camera_traps_files.py +40 -40
- megadetector/data_management/databases/integrity_check_json_db.py +228 -200
- megadetector/data_management/databases/subset_json_db.py +33 -33
- megadetector/data_management/generate_crops_from_cct.py +88 -39
- megadetector/data_management/get_image_sizes.py +54 -49
- megadetector/data_management/labelme_to_coco.py +133 -125
- megadetector/data_management/labelme_to_yolo.py +159 -73
- 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 +365 -107
- megadetector/data_management/lila/get_lila_annotation_counts.py +35 -33
- megadetector/data_management/lila/get_lila_image_counts.py +22 -22
- megadetector/data_management/lila/lila_common.py +73 -70
- megadetector/data_management/lila/test_lila_metadata_urls.py +28 -19
- megadetector/data_management/mewc_to_md.py +344 -340
- megadetector/data_management/ocr_tools.py +262 -255
- megadetector/data_management/read_exif.py +249 -227
- megadetector/data_management/remap_coco_categories.py +90 -28
- megadetector/data_management/remove_exif.py +81 -21
- megadetector/data_management/rename_images.py +187 -187
- megadetector/data_management/resize_coco_dataset.py +588 -120
- 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 +248 -122
- megadetector/data_management/yolo_to_coco.py +333 -191
- megadetector/detection/change_detection.py +832 -0
- megadetector/detection/process_video.py +340 -337
- megadetector/detection/pytorch_detector.py +358 -278
- megadetector/detection/run_detector.py +399 -186
- megadetector/detection/run_detector_batch.py +404 -377
- megadetector/detection/run_inference_with_yolov5_val.py +340 -327
- megadetector/detection/run_tiled_inference.py +257 -249
- megadetector/detection/tf_detector.py +24 -24
- megadetector/detection/video_utils.py +332 -295
- megadetector/postprocessing/add_max_conf.py +19 -11
- megadetector/postprocessing/categorize_detections_by_size.py +45 -45
- megadetector/postprocessing/classification_postprocessing.py +468 -433
- megadetector/postprocessing/combine_batch_outputs.py +23 -23
- megadetector/postprocessing/compare_batch_results.py +590 -525
- megadetector/postprocessing/convert_output_format.py +106 -102
- megadetector/postprocessing/create_crop_folder.py +347 -147
- megadetector/postprocessing/detector_calibration.py +173 -168
- megadetector/postprocessing/generate_csv_report.py +508 -499
- megadetector/postprocessing/load_api_results.py +48 -27
- megadetector/postprocessing/md_to_coco.py +133 -102
- megadetector/postprocessing/md_to_labelme.py +107 -90
- megadetector/postprocessing/md_to_wi.py +40 -40
- megadetector/postprocessing/merge_detections.py +92 -114
- megadetector/postprocessing/postprocess_batch_results.py +319 -301
- megadetector/postprocessing/remap_detection_categories.py +91 -38
- megadetector/postprocessing/render_detection_confusion_matrix.py +214 -205
- 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 +704 -679
- 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 +18 -19
- megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +54 -33
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +67 -67
- megadetector/taxonomy_mapping/retrieve_sample_image.py +16 -16
- megadetector/taxonomy_mapping/simple_image_download.py +8 -8
- megadetector/taxonomy_mapping/species_lookup.py +156 -74
- 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/ct_utils.py +1049 -211
- megadetector/utils/directory_listing.py +21 -77
- megadetector/utils/gpu_test.py +22 -22
- megadetector/utils/md_tests.py +632 -529
- megadetector/utils/path_utils.py +1520 -431
- megadetector/utils/process_utils.py +41 -41
- megadetector/utils/split_locations_into_train_val.py +62 -62
- megadetector/utils/string_utils.py +148 -27
- megadetector/utils/url_utils.py +489 -176
- megadetector/utils/wi_utils.py +2658 -2526
- megadetector/utils/write_html_image_list.py +137 -137
- megadetector/visualization/plot_utils.py +34 -30
- megadetector/visualization/render_images_with_thumbnails.py +39 -74
- megadetector/visualization/visualization_utils.py +487 -435
- megadetector/visualization/visualize_db.py +232 -198
- megadetector/visualization/visualize_detector_output.py +82 -76
- {megadetector-5.0.28.dist-info → megadetector-10.0.0.dist-info}/METADATA +5 -2
- megadetector-10.0.0.dist-info/RECORD +139 -0
- {megadetector-5.0.28.dist-info → megadetector-10.0.0.dist-info}/WHEEL +1 -1
- megadetector/api/batch_processing/api_core/__init__.py +0 -0
- megadetector/api/batch_processing/api_core/batch_service/__init__.py +0 -0
- megadetector/api/batch_processing/api_core/batch_service/score.py +0 -439
- megadetector/api/batch_processing/api_core/server.py +0 -294
- megadetector/api/batch_processing/api_core/server_api_config.py +0 -97
- megadetector/api/batch_processing/api_core/server_app_config.py +0 -55
- megadetector/api/batch_processing/api_core/server_batch_job_manager.py +0 -220
- megadetector/api/batch_processing/api_core/server_job_status_table.py +0 -149
- megadetector/api/batch_processing/api_core/server_orchestration.py +0 -360
- megadetector/api/batch_processing/api_core/server_utils.py +0 -88
- megadetector/api/batch_processing/api_core_support/__init__.py +0 -0
- megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +0 -46
- megadetector/api/batch_processing/api_support/__init__.py +0 -0
- megadetector/api/batch_processing/api_support/summarize_daily_activity.py +0 -152
- megadetector/api/batch_processing/data_preparation/__init__.py +0 -0
- megadetector/api/synchronous/__init__.py +0 -0
- megadetector/api/synchronous/api_core/animal_detection_api/__init__.py +0 -0
- megadetector/api/synchronous/api_core/animal_detection_api/api_backend.py +0 -151
- megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +0 -263
- megadetector/api/synchronous/api_core/animal_detection_api/config.py +0 -35
- megadetector/api/synchronous/api_core/tests/__init__.py +0 -0
- megadetector/api/synchronous/api_core/tests/load_test.py +0 -110
- 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/utils/azure_utils.py +0 -178
- megadetector/utils/sas_blob_utils.py +0 -509
- megadetector-5.0.28.dist-info/RECORD +0 -209
- /megadetector/{api/batch_processing/__init__.py → __init__.py} +0 -0
- {megadetector-5.0.28.dist-info → megadetector-10.0.0.dist-info}/licenses/LICENSE +0 -0
- {megadetector-5.0.28.dist-info → megadetector-10.0.0.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,90 +175,95 @@ 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
|
|
248
248
|
|
|
249
249
|
|
|
250
|
-
def resize_image(image,
|
|
251
|
-
|
|
250
|
+
def resize_image(image,
|
|
251
|
+
target_width=-1,
|
|
252
|
+
target_height=-1,
|
|
253
|
+
output_file=None,
|
|
254
|
+
no_enlarge_width=False,
|
|
255
|
+
verbose=False,
|
|
256
|
+
quality='keep'):
|
|
252
257
|
"""
|
|
253
258
|
Resizes a PIL Image object to the specified width and height; does not resize
|
|
254
259
|
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
|
|
260
|
+
|
|
261
|
+
If target_width and target_height are both -1, does not modify the image, but
|
|
257
262
|
will write to output_file if supplied.
|
|
258
|
-
|
|
259
|
-
If no resizing is required, and an Image object is supplied, returns the original Image
|
|
263
|
+
|
|
264
|
+
If no resizing is required, and an Image object is supplied, returns the original Image
|
|
260
265
|
object (i.e., does not copy).
|
|
261
|
-
|
|
266
|
+
|
|
262
267
|
Args:
|
|
263
268
|
image (Image or str): PIL Image object or a filename (local file or URL)
|
|
264
269
|
target_width (int, optional): width to which we should resize this image, or -1
|
|
@@ -267,14 +272,14 @@ def resize_image(image, target_width=-1, target_height=-1, output_file=None,
|
|
|
267
272
|
to let target_width determine the size
|
|
268
273
|
output_file (str, optional): file to which we should save this image; if None,
|
|
269
274
|
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,
|
|
275
|
+
no_enlarge_width (bool, optional): if [no_enlarge_width] is True, and
|
|
276
|
+
[target width] is larger than the original image width, does not modify the image,
|
|
272
277
|
but will write to output_file if supplied
|
|
273
278
|
verbose (bool, optional): enable additional debug output
|
|
274
279
|
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
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
PIL.Image.Image: the resized image, which may be the original image if no resizing is
|
|
278
283
|
required
|
|
279
284
|
"""
|
|
280
285
|
|
|
@@ -282,20 +287,20 @@ def resize_image(image, target_width=-1, target_height=-1, output_file=None,
|
|
|
282
287
|
if isinstance(image,str):
|
|
283
288
|
image_fn = image
|
|
284
289
|
image = load_image(image)
|
|
285
|
-
|
|
290
|
+
|
|
286
291
|
if target_width is None:
|
|
287
292
|
target_width = -1
|
|
288
|
-
|
|
293
|
+
|
|
289
294
|
if target_height is None:
|
|
290
295
|
target_height = -1
|
|
291
|
-
|
|
296
|
+
|
|
292
297
|
resize_required = True
|
|
293
|
-
|
|
298
|
+
|
|
294
299
|
# No resize was requested, this is always a no-op
|
|
295
300
|
if target_width == -1 and target_height == -1:
|
|
296
|
-
|
|
301
|
+
|
|
297
302
|
resize_required = False
|
|
298
|
-
|
|
303
|
+
|
|
299
304
|
# Does either dimension need to scale according to the other?
|
|
300
305
|
elif target_width == -1 or target_height == -1:
|
|
301
306
|
|
|
@@ -309,42 +314,42 @@ def resize_image(image, target_width=-1, target_height=-1, output_file=None,
|
|
|
309
314
|
else:
|
|
310
315
|
# w = ar * h
|
|
311
316
|
target_width = int(aspect_ratio * target_height)
|
|
312
|
-
|
|
317
|
+
|
|
313
318
|
# If we're not enlarging images and this would be an enlarge operation
|
|
314
319
|
if (no_enlarge_width) and (target_width > image.size[0]):
|
|
315
|
-
|
|
320
|
+
|
|
316
321
|
if verbose:
|
|
317
322
|
print('Bypassing image enlarge for {} --> {}'.format(
|
|
318
323
|
image_fn,str(output_file)))
|
|
319
324
|
resize_required = False
|
|
320
|
-
|
|
325
|
+
|
|
321
326
|
# If the target size is the same as the original size
|
|
322
327
|
if (target_width == image.size[0]) and (target_height == image.size[1]):
|
|
323
|
-
|
|
324
|
-
resize_required = False
|
|
325
|
-
|
|
328
|
+
|
|
329
|
+
resize_required = False
|
|
330
|
+
|
|
326
331
|
if not resize_required:
|
|
327
|
-
|
|
332
|
+
|
|
328
333
|
if output_file is not None:
|
|
329
334
|
if verbose:
|
|
330
335
|
print('No resize required for resize {} --> {}'.format(
|
|
331
336
|
image_fn,str(output_file)))
|
|
332
337
|
exif_preserving_save(image,output_file,quality=quality,verbose=verbose)
|
|
333
338
|
return image
|
|
334
|
-
|
|
339
|
+
|
|
335
340
|
assert target_width > 0 and target_height > 0, \
|
|
336
341
|
'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,
|
|
342
|
+
|
|
343
|
+
# The antialiasing parameter changed between Pillow versions 9 and 10, and for a bit,
|
|
339
344
|
# I'd like to support both.
|
|
340
345
|
try:
|
|
341
346
|
resized_image = image.resize((target_width, target_height), Image.ANTIALIAS)
|
|
342
|
-
except:
|
|
347
|
+
except Exception:
|
|
343
348
|
resized_image = image.resize((target_width, target_height), Image.Resampling.LANCZOS)
|
|
344
|
-
|
|
349
|
+
|
|
345
350
|
if output_file is not None:
|
|
346
351
|
exif_preserving_save(resized_image,output_file,quality=quality,verbose=verbose)
|
|
347
|
-
|
|
352
|
+
|
|
348
353
|
return resized_image
|
|
349
354
|
|
|
350
355
|
# ...def resize_image(...)
|
|
@@ -357,28 +362,28 @@ def crop_image(detections, image, confidence_threshold=0.15, expansion=0):
|
|
|
357
362
|
|
|
358
363
|
Args:
|
|
359
364
|
detections (list): a list of dictionaries with keys 'conf' and 'bbox';
|
|
360
|
-
boxes are length-four arrays formatted as [x,y,w,h], normalized,
|
|
365
|
+
boxes are length-four arrays formatted as [x,y,w,h], normalized,
|
|
361
366
|
upper-left origin (this is the standard MD detection format)
|
|
362
367
|
image (Image or str): the PIL Image object from which we should crop detections,
|
|
363
368
|
or an image filename
|
|
364
369
|
confidence_threshold (float, optional): only crop detections above this threshold
|
|
365
370
|
expansion (int, optional): a number of pixels to include on each side of a cropped
|
|
366
371
|
detection
|
|
367
|
-
|
|
372
|
+
|
|
368
373
|
Returns:
|
|
369
|
-
list: a possibly-empty list of PIL Image objects
|
|
374
|
+
list: a possibly-empty list of PIL Image objects
|
|
370
375
|
"""
|
|
371
376
|
|
|
372
377
|
ret_images = []
|
|
373
378
|
|
|
374
379
|
if isinstance(image,str):
|
|
375
380
|
image = load_image(image)
|
|
376
|
-
|
|
381
|
+
|
|
377
382
|
for detection in detections:
|
|
378
383
|
|
|
379
384
|
score = float(detection['conf'])
|
|
380
385
|
|
|
381
|
-
if score >= confidence_threshold:
|
|
386
|
+
if (confidence_threshold is None) or (score >= confidence_threshold):
|
|
382
387
|
|
|
383
388
|
x1, y1, w_box, h_box = detection['bbox']
|
|
384
389
|
ymin,xmin,ymax,xmax = y1, x1, y1 + h_box, x1 + w_box
|
|
@@ -417,17 +422,18 @@ def blur_detections(image,detections,blur_radius=40):
|
|
|
417
422
|
"""
|
|
418
423
|
Blur the regions in [image] corresponding to the MD-formatted list [detections].
|
|
419
424
|
[image] is modified in place.
|
|
420
|
-
|
|
425
|
+
|
|
421
426
|
Args:
|
|
422
427
|
image (PIL.Image.Image): image in which we should blur specific regions
|
|
423
428
|
detections (list): list of detections in the MD output format, see render
|
|
424
429
|
detection_bounding_boxes for more detail.
|
|
430
|
+
blur_radius (int, optional): radius of blur kernel in pixels
|
|
425
431
|
"""
|
|
426
|
-
|
|
432
|
+
|
|
427
433
|
img_width, img_height = image.size
|
|
428
|
-
|
|
434
|
+
|
|
429
435
|
for d in detections:
|
|
430
|
-
|
|
436
|
+
|
|
431
437
|
bbox = d['bbox']
|
|
432
438
|
x_norm, y_norm, width_norm, height_norm = bbox
|
|
433
439
|
|
|
@@ -436,29 +442,29 @@ def blur_detections(image,detections,blur_radius=40):
|
|
|
436
442
|
y = int(y_norm * img_height)
|
|
437
443
|
width = int(width_norm * img_width)
|
|
438
444
|
height = int(height_norm * img_height)
|
|
439
|
-
|
|
445
|
+
|
|
440
446
|
# Calculate box boundaries
|
|
441
447
|
left = max(0, x)
|
|
442
448
|
top = max(0, y)
|
|
443
449
|
right = min(img_width, x + width)
|
|
444
450
|
bottom = min(img_height, y + height)
|
|
445
|
-
|
|
451
|
+
|
|
446
452
|
# Crop the region, blur it, and paste it back
|
|
447
453
|
region = image.crop((left, top, right, bottom))
|
|
448
454
|
blurred_region = region.filter(ImageFilter.GaussianBlur(radius=blur_radius))
|
|
449
455
|
image.paste(blurred_region, (left, top))
|
|
450
456
|
|
|
451
457
|
# ...for each detection
|
|
452
|
-
|
|
458
|
+
|
|
453
459
|
# ...def blur_detections(...)
|
|
454
460
|
|
|
455
|
-
|
|
456
|
-
def render_detection_bounding_boxes(detections,
|
|
461
|
+
|
|
462
|
+
def render_detection_bounding_boxes(detections,
|
|
457
463
|
image,
|
|
458
464
|
label_map='show_categories',
|
|
459
|
-
classification_label_map=None,
|
|
460
|
-
confidence_threshold=0,
|
|
461
|
-
thickness=DEFAULT_BOX_THICKNESS,
|
|
465
|
+
classification_label_map=None,
|
|
466
|
+
confidence_threshold=0.0,
|
|
467
|
+
thickness=DEFAULT_BOX_THICKNESS,
|
|
462
468
|
expansion=0,
|
|
463
469
|
classification_confidence_threshold=0.3,
|
|
464
470
|
max_classifications=3,
|
|
@@ -472,17 +478,16 @@ def render_detection_bounding_boxes(detections,
|
|
|
472
478
|
"""
|
|
473
479
|
Renders bounding boxes (with labels and confidence values) on an image for all
|
|
474
480
|
detections above a threshold.
|
|
475
|
-
|
|
481
|
+
|
|
476
482
|
Renders classification labels if present.
|
|
477
|
-
|
|
483
|
+
|
|
478
484
|
[image] is modified in place.
|
|
479
485
|
|
|
480
486
|
Args:
|
|
481
|
-
|
|
482
487
|
detections (list): list of detections in the MD output format, for example:
|
|
483
|
-
|
|
488
|
+
|
|
484
489
|
.. code-block::none
|
|
485
|
-
|
|
490
|
+
|
|
486
491
|
[
|
|
487
492
|
{
|
|
488
493
|
"category": "2",
|
|
@@ -495,15 +500,15 @@ def render_detection_bounding_boxes(detections,
|
|
|
495
500
|
]
|
|
496
501
|
}
|
|
497
502
|
]
|
|
498
|
-
|
|
503
|
+
|
|
499
504
|
...where the bbox coordinates are [x, y, box_width, box_height].
|
|
500
|
-
|
|
505
|
+
|
|
501
506
|
(0, 0) is the upper-left. Coordinates are normalized.
|
|
502
|
-
|
|
507
|
+
|
|
503
508
|
Supports classification results, in the standard format:
|
|
504
|
-
|
|
509
|
+
|
|
505
510
|
.. code-block::none
|
|
506
|
-
|
|
511
|
+
|
|
507
512
|
[
|
|
508
513
|
{
|
|
509
514
|
"category": "2",
|
|
@@ -523,30 +528,30 @@ def render_detection_bounding_boxes(detections,
|
|
|
523
528
|
]
|
|
524
529
|
|
|
525
530
|
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,
|
|
531
|
+
label_map (dict, optional): optional, mapping the numeric label to a string name. The type of the
|
|
532
|
+
numeric label (typically strings) needs to be consistent with the keys in label_map; no casting is
|
|
533
|
+
carried out. If [label_map] is None, no labels are shown (not even numbers and confidence values).
|
|
534
|
+
If you want category numbers and confidence values without class labels, use the default value,
|
|
530
535
|
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
|
|
536
|
+
classification_label_map (dict, optional): optional, mapping of the string class labels to the actual
|
|
537
|
+
class names. The type of the numeric label (typically strings) needs to be consistent with the keys
|
|
538
|
+
in label_map; no casting is carried out. If [label_map] is None, no labels are shown (not even numbers
|
|
534
539
|
and confidence values).
|
|
535
|
-
confidence_threshold (float or dict, optional)
|
|
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
|
|
540
|
+
confidence_threshold (float or dict, optional): threshold above which boxes are rendered. Can also be a
|
|
541
|
+
dictionary mapping category IDs to thresholds.
|
|
542
|
+
thickness (int, optional): line thickness in pixels
|
|
543
|
+
expansion (int, optional): number of pixels to expand bounding boxes on each side
|
|
544
|
+
classification_confidence_threshold (float, optional): confidence above which classification results
|
|
545
|
+
are displayed
|
|
546
|
+
max_classifications (int, optional): maximum number of classification results rendered for one image
|
|
542
547
|
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
|
|
547
|
-
custom_strings: optional set of strings to append to detection labels, should
|
|
548
|
-
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
|
+
indexing with the values in [classes]; defaults to a reasonable set of colors
|
|
549
|
+
textalign (int, optional): TEXTALIGN_LEFT, TEXTALIGN_CENTER, or TEXTALIGN_RIGHT
|
|
550
|
+
vtextalign (int, optional): VTEXTALIGN_TOP or VTEXTALIGN_BOTTOM
|
|
551
|
+
label_font_size (float, optional): font size for labels
|
|
552
|
+
custom_strings (list of str, optional): optional set of strings to append to detection labels, should
|
|
553
|
+
have the same length as [detections]. Appended before any classification labels.
|
|
554
|
+
box_sort_order (str, optional): sorting scheme for detection boxes, can be None, "confidence", or
|
|
550
555
|
"reverse_confidence".
|
|
551
556
|
verbose (bool, optional): enable additional debug output
|
|
552
557
|
"""
|
|
@@ -554,52 +559,52 @@ def render_detection_bounding_boxes(detections,
|
|
|
554
559
|
# Input validation
|
|
555
560
|
if (label_map is not None) and (isinstance(label_map,str)) and (label_map == 'show_categories'):
|
|
556
561
|
label_map = {}
|
|
557
|
-
|
|
562
|
+
|
|
558
563
|
if custom_strings is not None:
|
|
559
564
|
assert len(custom_strings) == len(detections), \
|
|
560
565
|
'{} custom strings provided for {} detections'.format(
|
|
561
566
|
len(custom_strings),len(detections))
|
|
562
|
-
|
|
567
|
+
|
|
563
568
|
display_boxes = []
|
|
564
|
-
|
|
569
|
+
|
|
565
570
|
# list of lists, one list of strings for each bounding box (to accommodate multiple labels)
|
|
566
|
-
display_strs = []
|
|
567
|
-
|
|
571
|
+
display_strs = []
|
|
572
|
+
|
|
568
573
|
# for color selection
|
|
569
|
-
classes = []
|
|
574
|
+
classes = []
|
|
570
575
|
|
|
571
576
|
if box_sort_order is not None:
|
|
572
|
-
|
|
573
|
-
if box_sort_order == 'confidence':
|
|
577
|
+
|
|
578
|
+
if box_sort_order == 'confidence':
|
|
574
579
|
detections = sort_list_of_dicts_by_key(detections,k='conf',reverse=False)
|
|
575
580
|
elif box_sort_order == 'reverse_confidence':
|
|
576
581
|
detections = sort_list_of_dicts_by_key(detections,k='conf',reverse=True)
|
|
577
582
|
else:
|
|
578
583
|
raise ValueError('Unrecognized sorting scheme {}'.format(box_sort_order))
|
|
579
|
-
|
|
584
|
+
|
|
580
585
|
for i_detection,detection in enumerate(detections):
|
|
581
586
|
|
|
582
587
|
score = detection['conf']
|
|
583
|
-
|
|
588
|
+
|
|
584
589
|
if isinstance(confidence_threshold,dict):
|
|
585
590
|
rendering_threshold = confidence_threshold[detection['category']]
|
|
586
591
|
else:
|
|
587
|
-
rendering_threshold = confidence_threshold
|
|
588
|
-
|
|
592
|
+
rendering_threshold = confidence_threshold
|
|
593
|
+
|
|
589
594
|
# Always render objects with a confidence of "None", this is typically used
|
|
590
|
-
# for ground truth data.
|
|
591
|
-
if score is None or score >= rendering_threshold:
|
|
592
|
-
|
|
595
|
+
# for ground truth data.
|
|
596
|
+
if (score is None) or (rendering_threshold is None) or (score >= rendering_threshold):
|
|
597
|
+
|
|
593
598
|
x1, y1, w_box, h_box = detection['bbox']
|
|
594
599
|
display_boxes.append([y1, x1, y1 + h_box, x1 + w_box])
|
|
595
|
-
|
|
600
|
+
|
|
596
601
|
# The class index to use for coloring this box, which may be based on the detection
|
|
597
602
|
# category or on the most confident classification category.
|
|
598
603
|
clss = detection['category']
|
|
599
|
-
|
|
600
|
-
# This will be a list of strings that should be rendered above/below this box
|
|
604
|
+
|
|
605
|
+
# This will be a list of strings that should be rendered above/below this box
|
|
601
606
|
displayed_label = []
|
|
602
|
-
|
|
607
|
+
|
|
603
608
|
if label_map is not None:
|
|
604
609
|
label = label_map[clss] if clss in label_map else clss
|
|
605
610
|
if score is not None:
|
|
@@ -618,27 +623,27 @@ def render_detection_bounding_boxes(detections,
|
|
|
618
623
|
if ('classifications' in detection) and len(detection['classifications']) > 0:
|
|
619
624
|
|
|
620
625
|
classifications = detection['classifications']
|
|
621
|
-
|
|
626
|
+
|
|
622
627
|
if len(classifications) > max_classifications:
|
|
623
628
|
classifications = classifications[0:max_classifications]
|
|
624
|
-
|
|
629
|
+
|
|
625
630
|
max_classification_category = 0
|
|
626
631
|
max_classification_conf = -100
|
|
627
|
-
|
|
632
|
+
|
|
628
633
|
for classification in classifications:
|
|
629
|
-
|
|
634
|
+
|
|
630
635
|
classification_conf = classification[1]
|
|
631
636
|
if classification_conf is None or \
|
|
632
637
|
classification_conf < classification_confidence_threshold:
|
|
633
638
|
continue
|
|
634
|
-
|
|
639
|
+
|
|
635
640
|
class_key = classification[0]
|
|
636
|
-
|
|
641
|
+
|
|
637
642
|
# Is this the most confident classification for this detection?
|
|
638
643
|
if classification_conf > max_classification_conf:
|
|
639
644
|
max_classification_conf = classification_conf
|
|
640
645
|
max_classification_category = int(class_key)
|
|
641
|
-
|
|
646
|
+
|
|
642
647
|
if (classification_label_map is not None) and (class_key in classification_label_map):
|
|
643
648
|
class_name = classification_label_map[class_key]
|
|
644
649
|
else:
|
|
@@ -647,15 +652,15 @@ def render_detection_bounding_boxes(detections,
|
|
|
647
652
|
displayed_label += ['{}: {:5.1%}'.format(class_name.lower(), classification_conf)]
|
|
648
653
|
else:
|
|
649
654
|
displayed_label += ['{}'.format(class_name.lower())]
|
|
650
|
-
|
|
655
|
+
|
|
651
656
|
# ...for each classification
|
|
652
657
|
|
|
653
658
|
# To avoid duplicate colors with detection-only visualization, offset
|
|
654
659
|
# the classification class index by the number of detection classes
|
|
655
660
|
clss = annotation_constants.NUM_DETECTOR_CATEGORIES + max_classification_category
|
|
656
|
-
|
|
661
|
+
|
|
657
662
|
# ...if we have classification results
|
|
658
|
-
|
|
663
|
+
|
|
659
664
|
# display_strs is a list of labels for each box
|
|
660
665
|
display_strs.append(displayed_label)
|
|
661
666
|
classes.append(clss)
|
|
@@ -663,16 +668,21 @@ def render_detection_bounding_boxes(detections,
|
|
|
663
668
|
# ...if the confidence of this detection is above threshold
|
|
664
669
|
|
|
665
670
|
# ...for each detection
|
|
666
|
-
|
|
671
|
+
|
|
667
672
|
display_boxes = np.array(display_boxes)
|
|
668
673
|
|
|
669
674
|
if verbose:
|
|
670
675
|
print('Rendering {} of {} detections'.format(len(display_boxes),len(detections)))
|
|
671
|
-
|
|
672
|
-
draw_bounding_boxes_on_image(image,
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
+
|
|
677
|
+
draw_bounding_boxes_on_image(image,
|
|
678
|
+
display_boxes,
|
|
679
|
+
classes,
|
|
680
|
+
display_strs=display_strs,
|
|
681
|
+
thickness=thickness,
|
|
682
|
+
expansion=expansion,
|
|
683
|
+
colormap=colormap,
|
|
684
|
+
textalign=textalign,
|
|
685
|
+
vtextalign=vtextalign,
|
|
676
686
|
label_font_size=label_font_size)
|
|
677
687
|
|
|
678
688
|
# ...render_detection_bounding_boxes(...)
|
|
@@ -693,13 +703,12 @@ def draw_bounding_boxes_on_image(image,
|
|
|
693
703
|
Draws bounding boxes on an image. Modifies the image in place.
|
|
694
704
|
|
|
695
705
|
Args:
|
|
696
|
-
|
|
697
706
|
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
|
|
707
|
+
boxes (np.array): a two-dimensional numpy array of size [N, 4], where N is the
|
|
699
708
|
number of boxes, and each row is (ymin, xmin, ymax, xmax). Coordinates should be
|
|
700
709
|
normalized to image height/width.
|
|
701
710
|
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
|
|
711
|
+
class labels of the boxes. This is only used for color selection. Should have the same
|
|
703
712
|
length as [boxes].
|
|
704
713
|
thickness (int, optional): line thickness in pixels
|
|
705
714
|
expansion (int, optional): number of pixels to expand bounding boxes on each side
|
|
@@ -741,29 +750,29 @@ def get_text_size(font,s):
|
|
|
741
750
|
"""
|
|
742
751
|
Get the expected width and height when rendering the string [s] in the font
|
|
743
752
|
[font].
|
|
744
|
-
|
|
753
|
+
|
|
745
754
|
Args:
|
|
746
755
|
font (PIL.ImageFont): the font whose size we should query
|
|
747
756
|
s (str): the string whose size we should query
|
|
748
|
-
|
|
757
|
+
|
|
749
758
|
Returns:
|
|
750
|
-
tuple: (w,h), both floats in pixel coordinates
|
|
759
|
+
tuple: (w,h), both floats in pixel coordinates
|
|
751
760
|
"""
|
|
752
|
-
|
|
761
|
+
|
|
753
762
|
# This is what we did w/Pillow 9
|
|
754
763
|
# w,h = font.getsize(s)
|
|
755
|
-
|
|
764
|
+
|
|
756
765
|
# I would *think* this would be the equivalent for Pillow 10
|
|
757
766
|
# l,t,r,b = font.getbbox(s); w = r-l; h=b-t
|
|
758
|
-
|
|
767
|
+
|
|
759
768
|
# ...but this actually produces the most similar results to Pillow 9
|
|
760
769
|
# l,t,r,b = font.getbbox(s); w = r; h=b
|
|
761
|
-
|
|
770
|
+
|
|
762
771
|
try:
|
|
763
|
-
l,t,r,b = font.getbbox(s); w = r; h=b
|
|
772
|
+
l,t,r,b = font.getbbox(s); w = r; h=b # noqa
|
|
764
773
|
except Exception:
|
|
765
774
|
w,h = font.getsize(s)
|
|
766
|
-
|
|
775
|
+
|
|
767
776
|
return w,h
|
|
768
777
|
|
|
769
778
|
|
|
@@ -779,7 +788,7 @@ def draw_bounding_box_on_image(image,
|
|
|
779
788
|
use_normalized_coordinates=True,
|
|
780
789
|
label_font_size=DEFAULT_LABEL_FONT_SIZE,
|
|
781
790
|
colormap=None,
|
|
782
|
-
textalign=TEXTALIGN_LEFT,
|
|
791
|
+
textalign=TEXTALIGN_LEFT,
|
|
783
792
|
vtextalign=VTEXTALIGN_TOP,
|
|
784
793
|
text_rotation=None):
|
|
785
794
|
"""
|
|
@@ -794,9 +803,9 @@ def draw_bounding_box_on_image(image,
|
|
|
794
803
|
are displayed below the bounding box.
|
|
795
804
|
|
|
796
805
|
Adapted from:
|
|
797
|
-
|
|
806
|
+
|
|
798
807
|
https://github.com/tensorflow/models/blob/master/research/object_detection/utils/visualization_utils.py
|
|
799
|
-
|
|
808
|
+
|
|
800
809
|
Args:
|
|
801
810
|
image (PIL.Image.Image): the image on which we should draw a box
|
|
802
811
|
ymin (float): ymin of bounding box
|
|
@@ -807,24 +816,24 @@ def draw_bounding_box_on_image(image,
|
|
|
807
816
|
a color; should be either an integer or a string-formatted integer
|
|
808
817
|
thickness (int, optional): line thickness in pixels
|
|
809
818
|
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
|
|
819
|
+
display_str_list (list, optional): list of strings to display above the box (each to be shown on its
|
|
811
820
|
own line)
|
|
812
|
-
use_normalized_coordinates (bool, optional): if True (default), treat coordinates
|
|
821
|
+
use_normalized_coordinates (bool, optional): if True (default), treat coordinates
|
|
813
822
|
ymin, xmin, ymax, xmax as relative to the image, otherwise coordinates as absolute pixel values
|
|
814
|
-
label_font_size (float, optional): font size
|
|
823
|
+
label_font_size (float, optional): font size
|
|
815
824
|
colormap (list, optional): list of color names, used to choose colors for categories by
|
|
816
825
|
indexing with the values in [classes]; defaults to a reasonable set of colors
|
|
817
|
-
textalign (int, optional): TEXTALIGN_LEFT, TEXTALIGN_CENTER, or TEXTALIGN_RIGHT
|
|
826
|
+
textalign (int, optional): TEXTALIGN_LEFT, TEXTALIGN_CENTER, or TEXTALIGN_RIGHT
|
|
818
827
|
vtextalign (int, optional): VTEXTALIGN_TOP or VTEXTALIGN_BOTTOM
|
|
819
828
|
text_rotation (float, optional): rotation to apply to text
|
|
820
829
|
"""
|
|
821
|
-
|
|
830
|
+
|
|
822
831
|
if colormap is None:
|
|
823
832
|
colormap = DEFAULT_COLORS
|
|
824
|
-
|
|
833
|
+
|
|
825
834
|
if display_str_list is None:
|
|
826
835
|
display_str_list = []
|
|
827
|
-
|
|
836
|
+
|
|
828
837
|
if clss is None:
|
|
829
838
|
# Default to the MegaDetector animal class ID (1)
|
|
830
839
|
color = colormap[1]
|
|
@@ -840,12 +849,12 @@ def draw_bounding_box_on_image(image,
|
|
|
840
849
|
(left, right, top, bottom) = (xmin, xmax, ymin, ymax)
|
|
841
850
|
|
|
842
851
|
if expansion > 0:
|
|
843
|
-
|
|
852
|
+
|
|
844
853
|
left -= expansion
|
|
845
854
|
right += expansion
|
|
846
855
|
top -= expansion
|
|
847
856
|
bottom += expansion
|
|
848
|
-
|
|
857
|
+
|
|
849
858
|
# Deliberately trimming to the width of the image only in the case where
|
|
850
859
|
# box expansion is turned on. There's not an obvious correct behavior here,
|
|
851
860
|
# but the thinking is that if the caller provided an out-of-range bounding
|
|
@@ -861,9 +870,9 @@ def draw_bounding_box_on_image(image,
|
|
|
861
870
|
|
|
862
871
|
left = min(left,im_width-1); right = min(right,im_width-1)
|
|
863
872
|
top = min(top,im_height-1); bottom = min(bottom,im_height-1)
|
|
864
|
-
|
|
873
|
+
|
|
865
874
|
# ...if we need to expand boxes
|
|
866
|
-
|
|
875
|
+
|
|
867
876
|
draw.line([(left, top), (left, bottom), (right, bottom),
|
|
868
877
|
(right, top), (left, top)], width=thickness, fill=color)
|
|
869
878
|
|
|
@@ -871,52 +880,52 @@ def draw_bounding_box_on_image(image,
|
|
|
871
880
|
|
|
872
881
|
try:
|
|
873
882
|
font = ImageFont.truetype('arial.ttf', label_font_size)
|
|
874
|
-
except
|
|
883
|
+
except OSError:
|
|
875
884
|
font = ImageFont.load_default()
|
|
876
|
-
|
|
885
|
+
|
|
877
886
|
display_str_heights = [get_text_size(font,ds)[1] for ds in display_str_list]
|
|
878
|
-
|
|
887
|
+
|
|
879
888
|
# Each display_str has a top and bottom margin of 0.05x.
|
|
880
889
|
total_display_str_height = (1 + 2 * 0.05) * sum(display_str_heights)
|
|
881
|
-
|
|
890
|
+
|
|
882
891
|
# Reverse list and print from bottom to top
|
|
883
892
|
for i_str,display_str in enumerate(display_str_list[::-1]):
|
|
884
|
-
|
|
893
|
+
|
|
885
894
|
# Skip empty strings
|
|
886
895
|
if len(display_str) == 0:
|
|
887
896
|
continue
|
|
888
|
-
|
|
889
|
-
text_width, text_height = get_text_size(font,display_str)
|
|
897
|
+
|
|
898
|
+
text_width, text_height = get_text_size(font,display_str)
|
|
890
899
|
margin = int(np.ceil(0.05 * text_height))
|
|
891
|
-
|
|
900
|
+
|
|
892
901
|
if text_rotation is not None and text_rotation != 0:
|
|
893
|
-
|
|
902
|
+
|
|
894
903
|
assert text_rotation == -90, \
|
|
895
904
|
'Only -90-degree text rotation is supported'
|
|
896
|
-
|
|
905
|
+
|
|
897
906
|
image_tmp = Image.new('RGB',(text_width+2*margin,text_height+2*margin))
|
|
898
907
|
image_tmp_draw = ImageDraw.Draw(image_tmp)
|
|
899
908
|
image_tmp_draw.rectangle([0,0,text_width+2*margin,text_height+2*margin],fill=color)
|
|
900
909
|
image_tmp_draw.text( (margin,margin), display_str, font=font, fill='black')
|
|
901
910
|
rotated_text = image_tmp.rotate(text_rotation,expand=1)
|
|
902
|
-
|
|
911
|
+
|
|
903
912
|
if textalign == TEXTALIGN_RIGHT:
|
|
904
913
|
text_left = right
|
|
905
914
|
else:
|
|
906
915
|
text_left = left
|
|
907
916
|
text_left = int(text_left + (text_height) * i_str)
|
|
908
|
-
|
|
917
|
+
|
|
909
918
|
if vtextalign == VTEXTALIGN_BOTTOM:
|
|
910
919
|
text_top = bottom - text_width
|
|
911
920
|
else:
|
|
912
921
|
text_top = top
|
|
913
922
|
text_left = int(text_left)
|
|
914
|
-
text_top = int(text_top)
|
|
915
|
-
|
|
923
|
+
text_top = int(text_top)
|
|
924
|
+
|
|
916
925
|
image.paste(rotated_text,[text_left,text_top])
|
|
917
|
-
|
|
926
|
+
|
|
918
927
|
else:
|
|
919
|
-
|
|
928
|
+
|
|
920
929
|
# If the total height of the display strings added to the top of the bounding
|
|
921
930
|
# box exceeds the top of the image, stack the strings below the bounding box
|
|
922
931
|
# instead of above, and vice-versa if we're bottom-aligning.
|
|
@@ -933,32 +942,32 @@ def draw_bounding_box_on_image(image,
|
|
|
933
942
|
text_bottom = bottom + total_display_str_height
|
|
934
943
|
if (text_bottom + total_display_str_height) > im_height:
|
|
935
944
|
text_bottom = top
|
|
936
|
-
|
|
945
|
+
|
|
937
946
|
text_bottom = int(text_bottom) - i_str * (int(text_height + (2 * margin)))
|
|
938
|
-
|
|
947
|
+
|
|
939
948
|
text_left = left
|
|
940
|
-
|
|
949
|
+
|
|
941
950
|
if textalign == TEXTALIGN_RIGHT:
|
|
942
951
|
text_left = right - text_width
|
|
943
952
|
elif textalign == TEXTALIGN_CENTER:
|
|
944
953
|
text_left = ((right + left) / 2.0) - (text_width / 2.0)
|
|
945
|
-
text_left = int(text_left)
|
|
946
|
-
|
|
954
|
+
text_left = int(text_left)
|
|
955
|
+
|
|
947
956
|
draw.rectangle(
|
|
948
957
|
[(text_left, (text_bottom - text_height) - (2 * margin)),
|
|
949
958
|
(text_left + text_width, text_bottom)],
|
|
950
959
|
fill=color)
|
|
951
|
-
|
|
960
|
+
|
|
952
961
|
draw.text(
|
|
953
962
|
(text_left + margin, text_bottom - text_height - margin),
|
|
954
963
|
display_str,
|
|
955
964
|
fill='black',
|
|
956
965
|
font=font)
|
|
957
|
-
|
|
958
|
-
# ...if we're rotating text
|
|
966
|
+
|
|
967
|
+
# ...if we're rotating text
|
|
959
968
|
|
|
960
969
|
# ...if we're rendering text
|
|
961
|
-
|
|
970
|
+
|
|
962
971
|
# ...def draw_bounding_box_on_image(...)
|
|
963
972
|
|
|
964
973
|
|
|
@@ -966,9 +975,9 @@ def render_megadb_bounding_boxes(boxes_info, image):
|
|
|
966
975
|
"""
|
|
967
976
|
Render bounding boxes to an image, where those boxes are in the mostly-deprecated
|
|
968
977
|
MegaDB format, which looks like:
|
|
969
|
-
|
|
978
|
+
|
|
970
979
|
.. code-block::none
|
|
971
|
-
|
|
980
|
+
|
|
972
981
|
{
|
|
973
982
|
"category": "animal",
|
|
974
983
|
"bbox": [
|
|
@@ -977,16 +986,16 @@ def render_megadb_bounding_boxes(boxes_info, image):
|
|
|
977
986
|
0.187,
|
|
978
987
|
0.198
|
|
979
988
|
]
|
|
980
|
-
}
|
|
981
|
-
|
|
989
|
+
}
|
|
990
|
+
|
|
982
991
|
Args:
|
|
983
992
|
boxes_info (list): list of dicts, each dict represents a single detection
|
|
984
993
|
where bbox coordinates are normalized [x_min, y_min, width, height]
|
|
985
994
|
image (PIL.Image.Image): image to modify
|
|
986
|
-
|
|
995
|
+
|
|
987
996
|
:meta private:
|
|
988
997
|
"""
|
|
989
|
-
|
|
998
|
+
|
|
990
999
|
display_boxes = []
|
|
991
1000
|
display_strs = []
|
|
992
1001
|
classes = [] # ints, for selecting colors
|
|
@@ -1006,32 +1015,34 @@ def render_megadb_bounding_boxes(boxes_info, image):
|
|
|
1006
1015
|
|
|
1007
1016
|
|
|
1008
1017
|
def render_db_bounding_boxes(boxes,
|
|
1009
|
-
classes,
|
|
1010
|
-
image,
|
|
1018
|
+
classes,
|
|
1019
|
+
image,
|
|
1011
1020
|
original_size=None,
|
|
1012
|
-
label_map=None,
|
|
1013
|
-
thickness=DEFAULT_BOX_THICKNESS,
|
|
1021
|
+
label_map=None,
|
|
1022
|
+
thickness=DEFAULT_BOX_THICKNESS,
|
|
1014
1023
|
expansion=0,
|
|
1015
1024
|
colormap=None,
|
|
1016
1025
|
textalign=TEXTALIGN_LEFT,
|
|
1017
1026
|
vtextalign=VTEXTALIGN_TOP,
|
|
1018
1027
|
text_rotation=None,
|
|
1019
1028
|
label_font_size=DEFAULT_LABEL_FONT_SIZE,
|
|
1020
|
-
tags=None
|
|
1029
|
+
tags=None,
|
|
1030
|
+
boxes_are_normalized=False):
|
|
1021
1031
|
"""
|
|
1022
1032
|
Render bounding boxes (with class labels) on an image. This is a wrapper for
|
|
1023
1033
|
draw_bounding_boxes_on_image, allowing the caller to operate on a resized image
|
|
1024
1034
|
by providing the original size of the image; boxes will be scaled accordingly.
|
|
1025
|
-
|
|
1035
|
+
|
|
1026
1036
|
This function assumes that bounding boxes are in absolute coordinates, typically
|
|
1027
|
-
because they come from COCO camera traps .json files
|
|
1028
|
-
|
|
1037
|
+
because they come from COCO camera traps .json files, unless boxes_are_normalized
|
|
1038
|
+
is True.
|
|
1039
|
+
|
|
1029
1040
|
Args:
|
|
1030
1041
|
boxes (list): list of length-4 tuples, foramtted as (x,y,w,h) (in pixels)
|
|
1031
1042
|
classes (list): list of ints (or string-formatted ints), used to choose labels (either
|
|
1032
1043
|
by literally rendering the class labels, or by indexing into [label_map])
|
|
1033
1044
|
image (PIL.Image.Image): image object to modify
|
|
1034
|
-
original_size (tuple, optional): if this is not None, and the size is different than
|
|
1045
|
+
original_size (tuple, optional): if this is not None, and the size is different than
|
|
1035
1046
|
the size of [image], we assume that [boxes] refer to the original size, and we scale
|
|
1036
1047
|
them accordingly before rendering
|
|
1037
1048
|
label_map (dict, optional): int --> str dictionary, typically mapping category IDs to
|
|
@@ -1047,6 +1058,7 @@ def render_db_bounding_boxes(boxes,
|
|
|
1047
1058
|
label_font_size (float, optional): font size for labels
|
|
1048
1059
|
tags (list, optional): list of strings of length len(boxes) that should be appended
|
|
1049
1060
|
after each class name (e.g. to show scores)
|
|
1061
|
+
boxes_are_normalized (bool, optional): whether boxes have already been normalized
|
|
1050
1062
|
"""
|
|
1051
1063
|
|
|
1052
1064
|
display_boxes = []
|
|
@@ -1063,38 +1075,48 @@ def render_db_bounding_boxes(boxes,
|
|
|
1063
1075
|
|
|
1064
1076
|
box = boxes[i_box]
|
|
1065
1077
|
clss = classes[i_box]
|
|
1066
|
-
|
|
1078
|
+
|
|
1067
1079
|
x_min_abs, y_min_abs, width_abs, height_abs = box[0:4]
|
|
1068
1080
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1081
|
+
# Normalize boxes if necessary
|
|
1082
|
+
if boxes_are_normalized:
|
|
1071
1083
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1084
|
+
xmin = x_min_abs
|
|
1085
|
+
xmax = x_min_abs + width_abs
|
|
1086
|
+
ymin = y_min_abs
|
|
1087
|
+
ymax = y_min_abs + height_abs
|
|
1088
|
+
|
|
1089
|
+
else:
|
|
1090
|
+
|
|
1091
|
+
ymin = y_min_abs / img_height
|
|
1092
|
+
ymax = ymin + height_abs / img_height
|
|
1093
|
+
|
|
1094
|
+
xmin = x_min_abs / img_width
|
|
1095
|
+
xmax = xmin + width_abs / img_width
|
|
1074
1096
|
|
|
1075
1097
|
display_boxes.append([ymin, xmin, ymax, xmax])
|
|
1076
1098
|
|
|
1077
1099
|
if label_map:
|
|
1078
1100
|
clss = label_map[int(clss)]
|
|
1079
|
-
|
|
1101
|
+
|
|
1080
1102
|
display_str = str(clss)
|
|
1081
|
-
|
|
1103
|
+
|
|
1082
1104
|
# Do we have a tag to append to the class string?
|
|
1083
1105
|
if tags is not None and tags[i_box] is not None and len(tags[i_box]) > 0:
|
|
1084
1106
|
display_str += ' ' + tags[i_box]
|
|
1085
|
-
|
|
1107
|
+
|
|
1086
1108
|
# need to be a string here because PIL needs to iterate through chars
|
|
1087
1109
|
display_strs.append([display_str])
|
|
1088
1110
|
|
|
1089
1111
|
# ...for each box
|
|
1090
|
-
|
|
1112
|
+
|
|
1091
1113
|
display_boxes = np.array(display_boxes)
|
|
1092
|
-
|
|
1093
|
-
draw_bounding_boxes_on_image(image,
|
|
1094
|
-
display_boxes,
|
|
1095
|
-
classes,
|
|
1114
|
+
|
|
1115
|
+
draw_bounding_boxes_on_image(image,
|
|
1116
|
+
display_boxes,
|
|
1117
|
+
classes,
|
|
1096
1118
|
display_strs=display_strs,
|
|
1097
|
-
thickness=thickness,
|
|
1119
|
+
thickness=thickness,
|
|
1098
1120
|
expansion=expansion,
|
|
1099
1121
|
colormap=colormap,
|
|
1100
1122
|
textalign=textalign,
|
|
@@ -1105,12 +1127,12 @@ def render_db_bounding_boxes(boxes,
|
|
|
1105
1127
|
# ...def render_db_bounding_boxes(...)
|
|
1106
1128
|
|
|
1107
1129
|
|
|
1108
|
-
def draw_bounding_boxes_on_file(input_file,
|
|
1109
|
-
output_file,
|
|
1110
|
-
detections,
|
|
1130
|
+
def draw_bounding_boxes_on_file(input_file,
|
|
1131
|
+
output_file,
|
|
1132
|
+
detections,
|
|
1111
1133
|
confidence_threshold=0.0,
|
|
1112
1134
|
detector_label_map=DEFAULT_DETECTOR_LABEL_MAP,
|
|
1113
|
-
thickness=DEFAULT_BOX_THICKNESS,
|
|
1135
|
+
thickness=DEFAULT_BOX_THICKNESS,
|
|
1114
1136
|
expansion=0,
|
|
1115
1137
|
colormap=None,
|
|
1116
1138
|
label_font_size=DEFAULT_LABEL_FONT_SIZE,
|
|
@@ -1118,17 +1140,19 @@ def draw_bounding_boxes_on_file(input_file,
|
|
|
1118
1140
|
target_size=None,
|
|
1119
1141
|
ignore_exif_rotation=False):
|
|
1120
1142
|
"""
|
|
1121
|
-
Renders detection bounding boxes on an image loaded from file, optionally writing the results to
|
|
1143
|
+
Renders detection bounding boxes on an image loaded from file, optionally writing the results to
|
|
1122
1144
|
a new image file.
|
|
1123
|
-
|
|
1145
|
+
|
|
1124
1146
|
Args:
|
|
1125
1147
|
input_file (str): filename or URL to load
|
|
1126
|
-
output_file (str
|
|
1148
|
+
output_file (str): filename to which we should write the rendered image
|
|
1127
1149
|
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,
|
|
1150
|
+
boxes are length-four arrays formatted as [x,y,w,h], normalized,
|
|
1129
1151
|
upper-left origin (this is the standard MD detection format). 'category' is a string-int.
|
|
1130
|
-
|
|
1131
|
-
|
|
1152
|
+
confidence_threshold (float, optional): only render detections with confidence above this
|
|
1153
|
+
threshold
|
|
1154
|
+
detector_label_map (dict, optional): a dict mapping category IDs to strings. If this
|
|
1155
|
+
is None, no confidence values or identifiers are shown. If this is {}, just category
|
|
1132
1156
|
indices and confidence values are shown.
|
|
1133
1157
|
thickness (int, optional): line width in pixels for box rendering
|
|
1134
1158
|
expansion (int, optional): box expansion in pixels
|
|
@@ -1141,45 +1165,50 @@ def draw_bounding_boxes_on_file(input_file,
|
|
|
1141
1165
|
see resize_image() for documentation. If None or (-1,-1), uses the original image size.
|
|
1142
1166
|
ignore_exif_rotation (bool, optional): don't rotate the loaded pixels,
|
|
1143
1167
|
even if we are loading a JPEG and that JPEG says it should be rotated.
|
|
1144
|
-
|
|
1168
|
+
|
|
1145
1169
|
Returns:
|
|
1146
1170
|
PIL.Image.Image: loaded and modified image
|
|
1147
1171
|
"""
|
|
1148
|
-
|
|
1172
|
+
|
|
1149
1173
|
image = open_image(input_file, ignore_exif_rotation=ignore_exif_rotation)
|
|
1150
|
-
|
|
1174
|
+
|
|
1151
1175
|
if target_size is not None:
|
|
1152
1176
|
image = resize_image(image,target_size[0],target_size[1])
|
|
1153
|
-
|
|
1177
|
+
|
|
1154
1178
|
render_detection_bounding_boxes(
|
|
1155
|
-
detections,
|
|
1179
|
+
detections,
|
|
1180
|
+
image,
|
|
1181
|
+
label_map=detector_label_map,
|
|
1156
1182
|
confidence_threshold=confidence_threshold,
|
|
1157
|
-
thickness=thickness,
|
|
1158
|
-
|
|
1183
|
+
thickness=thickness,
|
|
1184
|
+
expansion=expansion,
|
|
1185
|
+
colormap=colormap,
|
|
1186
|
+
custom_strings=custom_strings,
|
|
1187
|
+
label_font_size=label_font_size)
|
|
1159
1188
|
|
|
1160
1189
|
if output_file is not None:
|
|
1161
1190
|
image.save(output_file)
|
|
1162
|
-
|
|
1191
|
+
|
|
1163
1192
|
return image
|
|
1164
1193
|
|
|
1165
1194
|
|
|
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,
|
|
1195
|
+
def draw_db_boxes_on_file(input_file,
|
|
1196
|
+
output_file,
|
|
1197
|
+
boxes,
|
|
1198
|
+
classes=None,
|
|
1199
|
+
label_map=None,
|
|
1200
|
+
thickness=DEFAULT_BOX_THICKNESS,
|
|
1172
1201
|
expansion=0,
|
|
1173
1202
|
ignore_exif_rotation=False):
|
|
1174
1203
|
"""
|
|
1175
|
-
Render COCO-formatted bounding boxes (in absolute coordinates) on an image loaded from file,
|
|
1204
|
+
Render COCO-formatted bounding boxes (in absolute coordinates) on an image loaded from file,
|
|
1176
1205
|
writing the results to a new image file.
|
|
1177
1206
|
|
|
1178
1207
|
Args:
|
|
1179
1208
|
input_file (str): image file to read
|
|
1180
1209
|
output_file (str): image file to write
|
|
1181
1210
|
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
|
|
1211
|
+
classes (list, optional): list of ints (or string-formatted ints), used to choose
|
|
1183
1212
|
labels (either by literally rendering the class labels, or by indexing into [label_map])
|
|
1184
1213
|
label_map (dict, optional): int --> str dictionary, typically mapping category IDs to
|
|
1185
1214
|
species labels; if None, category labels are rendered verbatim (typically as numbers)
|
|
@@ -1188,90 +1217,95 @@ def draw_db_boxes_on_file(input_file,
|
|
|
1188
1217
|
detection
|
|
1189
1218
|
ignore_exif_rotation (bool, optional): don't rotate the loaded pixels,
|
|
1190
1219
|
even if we are loading a JPEG and that JPEG says it should be rotated
|
|
1191
|
-
|
|
1220
|
+
|
|
1192
1221
|
Returns:
|
|
1193
1222
|
PIL.Image.Image: the loaded and modified image
|
|
1194
1223
|
"""
|
|
1195
|
-
|
|
1224
|
+
|
|
1196
1225
|
image = open_image(input_file, ignore_exif_rotation=ignore_exif_rotation)
|
|
1197
1226
|
|
|
1198
1227
|
if classes is None:
|
|
1199
1228
|
classes = [0] * len(boxes)
|
|
1200
|
-
|
|
1201
|
-
render_db_bounding_boxes(boxes,
|
|
1202
|
-
|
|
1203
|
-
|
|
1229
|
+
|
|
1230
|
+
render_db_bounding_boxes(boxes,
|
|
1231
|
+
classes,
|
|
1232
|
+
image,
|
|
1233
|
+
original_size=None,
|
|
1234
|
+
label_map=label_map,
|
|
1235
|
+
thickness=thickness,
|
|
1236
|
+
expansion=expansion)
|
|
1237
|
+
|
|
1204
1238
|
image.save(output_file)
|
|
1205
|
-
|
|
1239
|
+
|
|
1206
1240
|
return image
|
|
1207
|
-
|
|
1241
|
+
|
|
1208
1242
|
# ...def draw_bounding_boxes_on_file(...)
|
|
1209
1243
|
|
|
1210
1244
|
|
|
1211
1245
|
def gray_scale_fraction(image,crop_size=(0.1,0.1)):
|
|
1212
1246
|
"""
|
|
1213
|
-
Computes the fraction of the pixels in [image] that appear to be grayscale (R==G==B),
|
|
1247
|
+
Computes the fraction of the pixels in [image] that appear to be grayscale (R==G==B),
|
|
1214
1248
|
useful for approximating whether this is a night-time image when flash information is not
|
|
1215
1249
|
available in EXIF data (or for video frames, where this information is often not available
|
|
1216
1250
|
in structured metadata at all).
|
|
1217
|
-
|
|
1251
|
+
|
|
1218
1252
|
Args:
|
|
1219
1253
|
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
|
|
1221
|
-
image to crop at the top and bottom, respectively, before analyzing (to minimize
|
|
1254
|
+
crop_size (tuple of floats, optional): a 2-element list/tuple, representing the fraction of
|
|
1255
|
+
the image to crop at the top and bottom, respectively, before analyzing (to minimize
|
|
1222
1256
|
the possibility of including color elements in the image overlay)
|
|
1223
|
-
|
|
1257
|
+
|
|
1224
1258
|
Returns:
|
|
1225
1259
|
float: the fraction of pixels in [image] that appear to be grayscale (R==G==B)
|
|
1226
1260
|
"""
|
|
1227
|
-
|
|
1261
|
+
|
|
1228
1262
|
if isinstance(image,str):
|
|
1229
1263
|
image = Image.open(image)
|
|
1230
|
-
|
|
1264
|
+
|
|
1231
1265
|
if image.mode == 'L':
|
|
1232
1266
|
return 1.0
|
|
1233
|
-
|
|
1267
|
+
|
|
1234
1268
|
if len(image.getbands()) == 1:
|
|
1235
1269
|
return 1.0
|
|
1236
|
-
|
|
1270
|
+
|
|
1237
1271
|
# Crop if necessary
|
|
1238
1272
|
if crop_size[0] > 0 or crop_size[1] > 0:
|
|
1239
|
-
|
|
1273
|
+
|
|
1240
1274
|
assert (crop_size[0] + crop_size[1]) < 1.0, \
|
|
1241
1275
|
print('Illegal crop size: {}'.format(str(crop_size)))
|
|
1242
|
-
|
|
1276
|
+
|
|
1243
1277
|
top_crop_pixels = int(image.height * crop_size[0])
|
|
1244
1278
|
bottom_crop_pixels = int(image.height * crop_size[1])
|
|
1245
|
-
|
|
1279
|
+
|
|
1246
1280
|
left = 0
|
|
1247
1281
|
right = image.width
|
|
1248
|
-
|
|
1282
|
+
|
|
1249
1283
|
# Remove pixels from the top
|
|
1250
1284
|
first_crop_top = top_crop_pixels
|
|
1251
|
-
first_crop_bottom = image.height
|
|
1285
|
+
first_crop_bottom = image.height
|
|
1252
1286
|
first_crop = image.crop((left, first_crop_top, right, first_crop_bottom))
|
|
1253
|
-
|
|
1287
|
+
|
|
1254
1288
|
# Remove pixels from the bottom
|
|
1255
1289
|
second_crop_top = 0
|
|
1256
1290
|
second_crop_bottom = first_crop.height - bottom_crop_pixels
|
|
1257
1291
|
second_crop = first_crop.crop((left, second_crop_top, right, second_crop_bottom))
|
|
1258
|
-
|
|
1292
|
+
|
|
1259
1293
|
image = second_crop
|
|
1260
|
-
|
|
1294
|
+
|
|
1261
1295
|
# It doesn't matter if these are actually R/G/B, they're just names
|
|
1262
1296
|
r = np.array(image.getchannel(0))
|
|
1263
1297
|
g = np.array(image.getchannel(1))
|
|
1264
1298
|
b = np.array(image.getchannel(2))
|
|
1265
|
-
|
|
1299
|
+
|
|
1266
1300
|
gray_pixels = np.logical_and(r == g, r == b)
|
|
1267
1301
|
n_pixels = gray_pixels.size
|
|
1268
1302
|
n_gray_pixels = gray_pixels.sum()
|
|
1269
|
-
|
|
1303
|
+
|
|
1270
1304
|
return n_gray_pixels / n_pixels
|
|
1271
1305
|
|
|
1272
1306
|
# Non-numpy way to do the same thing, briefly keeping this here for posterity
|
|
1273
1307
|
if False:
|
|
1274
|
-
|
|
1308
|
+
|
|
1275
1309
|
w, h = image.size
|
|
1276
1310
|
n_pixels = w*h
|
|
1277
1311
|
n_gray_pixels = 0
|
|
@@ -1279,25 +1313,25 @@ def gray_scale_fraction(image,crop_size=(0.1,0.1)):
|
|
|
1279
1313
|
for j in range(h):
|
|
1280
1314
|
r, g, b = image.getpixel((i,j))
|
|
1281
1315
|
if r == g and r == b and g == b:
|
|
1282
|
-
n_gray_pixels += 1
|
|
1316
|
+
n_gray_pixels += 1
|
|
1283
1317
|
|
|
1284
1318
|
# ...def gray_scale_fraction(...)
|
|
1285
1319
|
|
|
1286
1320
|
|
|
1287
1321
|
def _resize_relative_image(fn_relative,
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1322
|
+
input_folder,
|
|
1323
|
+
output_folder,
|
|
1324
|
+
target_width,
|
|
1325
|
+
target_height,
|
|
1326
|
+
no_enlarge_width,
|
|
1327
|
+
verbose,
|
|
1328
|
+
quality,
|
|
1329
|
+
overwrite=True):
|
|
1296
1330
|
"""
|
|
1297
1331
|
Internal function for resizing an image from one folder to another,
|
|
1298
1332
|
maintaining relative path.
|
|
1299
1333
|
"""
|
|
1300
|
-
|
|
1334
|
+
|
|
1301
1335
|
input_fn_abs = os.path.join(input_folder,fn_relative)
|
|
1302
1336
|
output_fn_abs = os.path.join(output_folder,fn_relative)
|
|
1303
1337
|
|
|
@@ -1305,13 +1339,16 @@ def _resize_relative_image(fn_relative,
|
|
|
1305
1339
|
status = 'skipped'
|
|
1306
1340
|
error = None
|
|
1307
1341
|
return {'fn_relative':fn_relative,'status':status,'error':error}
|
|
1308
|
-
|
|
1342
|
+
|
|
1309
1343
|
os.makedirs(os.path.dirname(output_fn_abs),exist_ok=True)
|
|
1310
1344
|
try:
|
|
1311
|
-
_ = resize_image(input_fn_abs,
|
|
1312
|
-
output_file=output_fn_abs,
|
|
1313
|
-
target_width=target_width,
|
|
1314
|
-
|
|
1345
|
+
_ = resize_image(input_fn_abs,
|
|
1346
|
+
output_file=output_fn_abs,
|
|
1347
|
+
target_width=target_width,
|
|
1348
|
+
target_height=target_height,
|
|
1349
|
+
no_enlarge_width=no_enlarge_width,
|
|
1350
|
+
verbose=verbose,
|
|
1351
|
+
quality=quality)
|
|
1315
1352
|
status = 'success'
|
|
1316
1353
|
error = None
|
|
1317
1354
|
except Exception as e:
|
|
@@ -1319,27 +1356,33 @@ def _resize_relative_image(fn_relative,
|
|
|
1319
1356
|
print('Error resizing {}: {}'.format(fn_relative,str(e)))
|
|
1320
1357
|
status = 'error'
|
|
1321
1358
|
error = str(e)
|
|
1322
|
-
|
|
1359
|
+
|
|
1323
1360
|
return {'fn_relative':fn_relative,'status':status,'error':error}
|
|
1324
1361
|
|
|
1325
1362
|
# ...def _resize_relative_image(...)
|
|
1326
1363
|
|
|
1327
1364
|
|
|
1328
1365
|
def _resize_absolute_image(input_output_files,
|
|
1329
|
-
|
|
1330
|
-
|
|
1366
|
+
target_width,
|
|
1367
|
+
target_height,
|
|
1368
|
+
no_enlarge_width,
|
|
1369
|
+
verbose,
|
|
1370
|
+
quality):
|
|
1331
1371
|
"""
|
|
1332
1372
|
Internal wrapper for resize_image used in the context of a batch resize operation.
|
|
1333
1373
|
"""
|
|
1334
|
-
|
|
1374
|
+
|
|
1335
1375
|
input_fn_abs = input_output_files[0]
|
|
1336
1376
|
output_fn_abs = input_output_files[1]
|
|
1337
1377
|
os.makedirs(os.path.dirname(output_fn_abs),exist_ok=True)
|
|
1338
1378
|
try:
|
|
1339
|
-
_ = resize_image(input_fn_abs,
|
|
1340
|
-
output_file=output_fn_abs,
|
|
1341
|
-
target_width=target_width,
|
|
1342
|
-
|
|
1379
|
+
_ = resize_image(input_fn_abs,
|
|
1380
|
+
output_file=output_fn_abs,
|
|
1381
|
+
target_width=target_width,
|
|
1382
|
+
target_height=target_height,
|
|
1383
|
+
no_enlarge_width=no_enlarge_width,
|
|
1384
|
+
verbose=verbose,
|
|
1385
|
+
quality=quality)
|
|
1343
1386
|
status = 'success'
|
|
1344
1387
|
error = None
|
|
1345
1388
|
except Exception as e:
|
|
@@ -1347,7 +1390,7 @@ def _resize_absolute_image(input_output_files,
|
|
|
1347
1390
|
print('Error resizing {}: {}'.format(input_fn_abs,str(e)))
|
|
1348
1391
|
status = 'error'
|
|
1349
1392
|
error = str(e)
|
|
1350
|
-
|
|
1393
|
+
|
|
1351
1394
|
return {'input_fn':input_fn_abs,'output_fn':output_fn_abs,status:'status',
|
|
1352
1395
|
'error':error}
|
|
1353
1396
|
|
|
@@ -1355,21 +1398,21 @@ def _resize_absolute_image(input_output_files,
|
|
|
1355
1398
|
|
|
1356
1399
|
|
|
1357
1400
|
def resize_images(input_file_to_output_file,
|
|
1358
|
-
target_width=-1,
|
|
1401
|
+
target_width=-1,
|
|
1359
1402
|
target_height=-1,
|
|
1360
|
-
no_enlarge_width=False,
|
|
1361
|
-
verbose=False,
|
|
1403
|
+
no_enlarge_width=False,
|
|
1404
|
+
verbose=False,
|
|
1362
1405
|
quality='keep',
|
|
1363
|
-
pool_type='process',
|
|
1406
|
+
pool_type='process',
|
|
1364
1407
|
n_workers=10):
|
|
1365
1408
|
"""
|
|
1366
1409
|
Resizes all images the dictionary [input_file_to_output_file].
|
|
1367
1410
|
|
|
1368
1411
|
TODO: This is a little more redundant with resize_image_folder than I would like;
|
|
1369
1412
|
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
|
|
1413
|
+
at the time I'm writing this comment, a lot of code depends on resize_image_folder
|
|
1371
1414
|
and I don't want to rock the boat yet.
|
|
1372
|
-
|
|
1415
|
+
|
|
1373
1416
|
Args:
|
|
1374
1417
|
input_file_to_output_file (dict): dict mapping images that exist to the locations
|
|
1375
1418
|
where the resized versions should be written
|
|
@@ -1377,8 +1420,8 @@ def resize_images(input_file_to_output_file,
|
|
|
1377
1420
|
to let target_height determine the size
|
|
1378
1421
|
target_height (int, optional): height to which we should resize this image, or -1
|
|
1379
1422
|
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,
|
|
1423
|
+
no_enlarge_width (bool, optional): if [no_enlarge_width] is True, and
|
|
1424
|
+
[target width] is larger than the original image width, does not modify the image,
|
|
1382
1425
|
but will write to output_file if supplied
|
|
1383
1426
|
verbose (bool, optional): enable additional debug output
|
|
1384
1427
|
quality (str or int, optional): passed to exif_preserving_save, see docs for more detail
|
|
@@ -1389,20 +1432,20 @@ def resize_images(input_file_to_output_file,
|
|
|
1389
1432
|
|
|
1390
1433
|
Returns:
|
|
1391
1434
|
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,
|
|
1435
|
+
'status' will be 'success' or 'error'; 'error' will be None for successful cases,
|
|
1393
1436
|
otherwise will contain the image-specific error.
|
|
1394
1437
|
"""
|
|
1395
|
-
|
|
1438
|
+
|
|
1396
1439
|
assert pool_type in ('process','thread'), 'Illegal pool type {}'.format(pool_type)
|
|
1397
|
-
|
|
1440
|
+
|
|
1398
1441
|
input_output_file_pairs = []
|
|
1399
|
-
|
|
1442
|
+
|
|
1400
1443
|
# Reformat input files as (input,output) tuples
|
|
1401
1444
|
for input_fn in input_file_to_output_file:
|
|
1402
1445
|
input_output_file_pairs.append((input_fn,input_file_to_output_file[input_fn]))
|
|
1403
|
-
|
|
1404
|
-
if n_workers == 1:
|
|
1405
|
-
|
|
1446
|
+
|
|
1447
|
+
if n_workers == 1:
|
|
1448
|
+
|
|
1406
1449
|
results = []
|
|
1407
1450
|
for i_o_file_pair in tqdm(input_output_file_pairs):
|
|
1408
1451
|
results.append(_resize_absolute_image(i_o_file_pair,
|
|
@@ -1413,47 +1456,54 @@ def resize_images(input_file_to_output_file,
|
|
|
1413
1456
|
quality=quality))
|
|
1414
1457
|
|
|
1415
1458
|
else:
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1459
|
+
|
|
1460
|
+
pool = None
|
|
1461
|
+
|
|
1462
|
+
try:
|
|
1463
|
+
if pool_type == 'thread':
|
|
1464
|
+
pool = ThreadPool(n_workers); poolstring = 'threads'
|
|
1465
|
+
else:
|
|
1466
|
+
assert pool_type == 'process'
|
|
1467
|
+
pool = Pool(n_workers); poolstring = 'processes'
|
|
1468
|
+
|
|
1469
|
+
if verbose:
|
|
1470
|
+
print('Starting resizing pool with {} {}'.format(n_workers,poolstring))
|
|
1471
|
+
|
|
1472
|
+
p = partial(_resize_absolute_image,
|
|
1473
|
+
target_width=target_width,
|
|
1474
|
+
target_height=target_height,
|
|
1475
|
+
no_enlarge_width=no_enlarge_width,
|
|
1476
|
+
verbose=verbose,
|
|
1477
|
+
quality=quality)
|
|
1478
|
+
|
|
1479
|
+
results = list(tqdm(pool.imap(p, input_output_file_pairs),total=len(input_output_file_pairs)))
|
|
1480
|
+
finally:
|
|
1481
|
+
pool.close()
|
|
1482
|
+
pool.join()
|
|
1483
|
+
print("Pool closed and joined for image resizing")
|
|
1434
1484
|
|
|
1435
1485
|
return results
|
|
1436
1486
|
|
|
1437
1487
|
# ...def resize_images(...)
|
|
1438
1488
|
|
|
1439
1489
|
|
|
1440
|
-
def resize_image_folder(input_folder,
|
|
1490
|
+
def resize_image_folder(input_folder,
|
|
1441
1491
|
output_folder=None,
|
|
1442
|
-
target_width=-1,
|
|
1492
|
+
target_width=-1,
|
|
1443
1493
|
target_height=-1,
|
|
1444
|
-
no_enlarge_width=False,
|
|
1445
|
-
verbose=False,
|
|
1494
|
+
no_enlarge_width=False,
|
|
1495
|
+
verbose=False,
|
|
1446
1496
|
quality='keep',
|
|
1447
|
-
pool_type='process',
|
|
1448
|
-
n_workers=10,
|
|
1497
|
+
pool_type='process',
|
|
1498
|
+
n_workers=10,
|
|
1449
1499
|
recursive=True,
|
|
1450
1500
|
image_files_relative=None,
|
|
1451
1501
|
overwrite=True):
|
|
1452
1502
|
"""
|
|
1453
1503
|
Resize all images in a folder (defaults to recursive).
|
|
1454
|
-
|
|
1504
|
+
|
|
1455
1505
|
Defaults to in-place resizing (output_folder is optional).
|
|
1456
|
-
|
|
1506
|
+
|
|
1457
1507
|
Args:
|
|
1458
1508
|
input_folder (str): folder in which we should find images to resize
|
|
1459
1509
|
output_folder (str, optional): folder in which we should write resized images. If
|
|
@@ -1463,8 +1513,8 @@ def resize_image_folder(input_folder,
|
|
|
1463
1513
|
to let target_height determine the size
|
|
1464
1514
|
target_height (int, optional): height to which we should resize this image, or -1
|
|
1465
1515
|
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,
|
|
1516
|
+
no_enlarge_width (bool, optional): if [no_enlarge_width] is True, and
|
|
1517
|
+
[target width] is larger than the original image width, does not modify the image,
|
|
1468
1518
|
but will write to output_file if supplied
|
|
1469
1519
|
verbose (bool, optional): enable additional debug output
|
|
1470
1520
|
quality (str or int, optional): passed to exif_preserving_save, see docs for more detail
|
|
@@ -1476,34 +1526,34 @@ def resize_image_folder(input_folder,
|
|
|
1476
1526
|
image_files_relative (list, optional): if not None, skips any relative paths not
|
|
1477
1527
|
in this list
|
|
1478
1528
|
overwrite (bool, optional): whether to overwrite existing target images
|
|
1479
|
-
|
|
1529
|
+
|
|
1480
1530
|
Returns:
|
|
1481
1531
|
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
|
|
1532
|
+
'status' will be 'success', 'skipped', or 'error'; 'error' will be None for successful
|
|
1483
1533
|
cases, otherwise will contain the image-specific error.
|
|
1484
1534
|
"""
|
|
1485
1535
|
|
|
1486
1536
|
assert os.path.isdir(input_folder), '{} is not a folder'.format(input_folder)
|
|
1487
|
-
|
|
1537
|
+
|
|
1488
1538
|
if output_folder is None:
|
|
1489
1539
|
output_folder = input_folder
|
|
1490
1540
|
else:
|
|
1491
1541
|
os.makedirs(output_folder,exist_ok=True)
|
|
1492
|
-
|
|
1542
|
+
|
|
1493
1543
|
assert pool_type in ('process','thread'), 'Illegal pool type {}'.format(pool_type)
|
|
1494
|
-
|
|
1544
|
+
|
|
1495
1545
|
if image_files_relative is None:
|
|
1496
|
-
|
|
1546
|
+
|
|
1497
1547
|
if verbose:
|
|
1498
1548
|
print('Enumerating images')
|
|
1499
|
-
|
|
1549
|
+
|
|
1500
1550
|
image_files_relative = find_images(input_folder,recursive=recursive,
|
|
1501
1551
|
return_relative_paths=True,convert_slashes=True)
|
|
1502
1552
|
if verbose:
|
|
1503
1553
|
print('Found {} images'.format(len(image_files_relative)))
|
|
1504
|
-
|
|
1505
|
-
if n_workers == 1:
|
|
1506
|
-
|
|
1554
|
+
|
|
1555
|
+
if n_workers == 1:
|
|
1556
|
+
|
|
1507
1557
|
if verbose:
|
|
1508
1558
|
print('Resizing images')
|
|
1509
1559
|
|
|
@@ -1520,16 +1570,16 @@ def resize_image_folder(input_folder,
|
|
|
1520
1570
|
overwrite=overwrite))
|
|
1521
1571
|
|
|
1522
1572
|
else:
|
|
1523
|
-
|
|
1573
|
+
|
|
1524
1574
|
if pool_type == 'thread':
|
|
1525
|
-
pool = ThreadPool(n_workers); poolstring = 'threads'
|
|
1575
|
+
pool = ThreadPool(n_workers); poolstring = 'threads'
|
|
1526
1576
|
else:
|
|
1527
1577
|
assert pool_type == 'process'
|
|
1528
1578
|
pool = Pool(n_workers); poolstring = 'processes'
|
|
1529
|
-
|
|
1579
|
+
|
|
1530
1580
|
if verbose:
|
|
1531
1581
|
print('Starting resizing pool with {} {}'.format(n_workers,poolstring))
|
|
1532
|
-
|
|
1582
|
+
|
|
1533
1583
|
p = partial(_resize_relative_image,
|
|
1534
1584
|
input_folder=input_folder,
|
|
1535
1585
|
output_folder=output_folder,
|
|
@@ -1539,8 +1589,9 @@ def resize_image_folder(input_folder,
|
|
|
1539
1589
|
verbose=verbose,
|
|
1540
1590
|
quality=quality,
|
|
1541
1591
|
overwrite=overwrite)
|
|
1542
|
-
|
|
1543
|
-
results = list(tqdm(pool.imap(p, image_files_relative),
|
|
1592
|
+
|
|
1593
|
+
results = list(tqdm(pool.imap(p, image_files_relative),
|
|
1594
|
+
total=len(image_files_relative)))
|
|
1544
1595
|
|
|
1545
1596
|
return results
|
|
1546
1597
|
|
|
@@ -1550,17 +1601,18 @@ def resize_image_folder(input_folder,
|
|
|
1550
1601
|
def get_image_size(im,verbose=False):
|
|
1551
1602
|
"""
|
|
1552
1603
|
Retrieve the size of an image. Returns None if the image fails to load.
|
|
1553
|
-
|
|
1604
|
+
|
|
1554
1605
|
Args:
|
|
1555
1606
|
im (str or PIL.Image): filename or PIL image
|
|
1556
|
-
|
|
1607
|
+
verbose (bool, optional): enable additional debug output
|
|
1608
|
+
|
|
1557
1609
|
Returns:
|
|
1558
1610
|
tuple (w,h), or None if the image fails to load.
|
|
1559
1611
|
"""
|
|
1560
|
-
|
|
1612
|
+
|
|
1561
1613
|
image_name = '[in memory]'
|
|
1562
|
-
|
|
1563
|
-
try:
|
|
1614
|
+
|
|
1615
|
+
try:
|
|
1564
1616
|
if isinstance(im,str):
|
|
1565
1617
|
image_name = im
|
|
1566
1618
|
im = load_image(im)
|
|
@@ -1577,66 +1629,66 @@ def get_image_size(im,verbose=False):
|
|
|
1577
1629
|
print('Error reading width from image {}: {}'.format(
|
|
1578
1630
|
image_name,str(e)))
|
|
1579
1631
|
return None
|
|
1580
|
-
|
|
1632
|
+
|
|
1581
1633
|
# ...def get_image_size(...)
|
|
1582
1634
|
|
|
1583
1635
|
|
|
1584
1636
|
def parallel_get_image_sizes(filenames,
|
|
1585
|
-
max_workers=16,
|
|
1586
|
-
use_threads=True,
|
|
1637
|
+
max_workers=16,
|
|
1638
|
+
use_threads=True,
|
|
1587
1639
|
recursive=True,
|
|
1588
1640
|
verbose=False):
|
|
1589
1641
|
"""
|
|
1590
1642
|
Retrieve image sizes for a list or folder of images
|
|
1591
|
-
|
|
1643
|
+
|
|
1592
1644
|
Args:
|
|
1593
|
-
filenames (list or str): a list of image filenames or a folder. Non-image files and
|
|
1645
|
+
filenames (list or str): a list of image filenames or a folder. Non-image files and
|
|
1594
1646
|
unreadable images will be returned with a file size of None.
|
|
1595
1647
|
max_workers (int, optional): the number of parallel workers to use; set to <=1 to disable
|
|
1596
1648
|
parallelization
|
|
1597
1649
|
use_threads (bool, optional): whether to use threads (True) or processes (False) for
|
|
1598
1650
|
parallelization
|
|
1599
|
-
recursive (bool, optional): if [filenames] is a folder, whether to search recursively
|
|
1600
|
-
Ignored if [filenames] is a list.
|
|
1651
|
+
recursive (bool, optional): if [filenames] is a folder, whether to search recursively
|
|
1652
|
+
for images. Ignored if [filenames] is a list.
|
|
1601
1653
|
verbose (bool, optional): enable additional debug output
|
|
1602
|
-
|
|
1654
|
+
|
|
1603
1655
|
Returns:
|
|
1604
1656
|
dict: a dict mapping filenames to (w,h) tuples; the value will be None for images that fail
|
|
1605
|
-
to load.
|
|
1657
|
+
to load. Filenames will always be absolute.
|
|
1606
1658
|
"""
|
|
1607
1659
|
|
|
1608
1660
|
if isinstance(filenames,str) and os.path.isdir(filenames):
|
|
1609
1661
|
if verbose:
|
|
1610
1662
|
print('Enumerating images in {}'.format(filenames))
|
|
1611
1663
|
filenames = find_images(filenames,recursive=recursive,return_relative_paths=False)
|
|
1612
|
-
|
|
1664
|
+
|
|
1613
1665
|
n_workers = min(max_workers,len(filenames))
|
|
1614
|
-
|
|
1666
|
+
|
|
1615
1667
|
if verbose:
|
|
1616
1668
|
print('Getting image sizes for {} images'.format(len(filenames)))
|
|
1617
|
-
|
|
1669
|
+
|
|
1618
1670
|
if n_workers <= 1:
|
|
1619
|
-
|
|
1671
|
+
|
|
1620
1672
|
results = []
|
|
1621
1673
|
for filename in filenames:
|
|
1622
1674
|
results.append(get_image_size(filename,verbose=verbose))
|
|
1623
|
-
|
|
1675
|
+
|
|
1624
1676
|
else:
|
|
1625
|
-
|
|
1677
|
+
|
|
1626
1678
|
if use_threads:
|
|
1627
1679
|
pool = ThreadPool(n_workers)
|
|
1628
1680
|
else:
|
|
1629
1681
|
pool = Pool(n_workers)
|
|
1630
|
-
|
|
1682
|
+
|
|
1631
1683
|
results = list(tqdm(pool.imap(
|
|
1632
1684
|
partial(get_image_size,verbose=verbose),filenames), total=len(filenames)))
|
|
1633
|
-
|
|
1685
|
+
|
|
1634
1686
|
assert len(filenames) == len(results), 'Internal error in parallel_get_image_sizes'
|
|
1635
|
-
|
|
1687
|
+
|
|
1636
1688
|
to_return = {}
|
|
1637
1689
|
for i_file,filename in enumerate(filenames):
|
|
1638
1690
|
to_return[filename] = results[i_file]
|
|
1639
|
-
|
|
1691
|
+
|
|
1640
1692
|
return to_return
|
|
1641
1693
|
|
|
1642
1694
|
|
|
@@ -1645,30 +1697,30 @@ def parallel_get_image_sizes(filenames,
|
|
|
1645
1697
|
def check_image_integrity(filename,modes=None):
|
|
1646
1698
|
"""
|
|
1647
1699
|
Check whether we can successfully load an image via OpenCV and/or PIL.
|
|
1648
|
-
|
|
1649
|
-
Args:
|
|
1700
|
+
|
|
1701
|
+
Args:
|
|
1650
1702
|
filename (str): the filename to evaluate
|
|
1651
1703
|
modes (list, optional): a list containing one or more of:
|
|
1652
|
-
|
|
1704
|
+
|
|
1653
1705
|
- 'cv'
|
|
1654
1706
|
- 'pil'
|
|
1655
1707
|
- 'skimage'
|
|
1656
|
-
- 'jpeg_trailer'
|
|
1657
|
-
|
|
1708
|
+
- 'jpeg_trailer'
|
|
1709
|
+
|
|
1658
1710
|
'jpeg_trailer' checks that the binary data ends with ffd9. It does not check whether
|
|
1659
1711
|
the image is actually a jpeg, and even if it is, there are lots of reasons the image might not
|
|
1660
1712
|
end with ffd9. It's also true the JPEGs that cause "premature end of jpeg segment" issues
|
|
1661
1713
|
don't end with ffd9, so this may be a useful diagnostic. High precision, very low recall
|
|
1662
1714
|
for corrupt jpegs.
|
|
1663
|
-
|
|
1715
|
+
|
|
1664
1716
|
Set to None to use all modes.
|
|
1665
|
-
|
|
1717
|
+
|
|
1666
1718
|
Returns:
|
|
1667
1719
|
dict: a dict with a key called 'file' (the value of [filename]), one key for each string in
|
|
1668
1720
|
[modes] (a success indicator for that mode, specifically a string starting with either
|
|
1669
1721
|
'success' or 'error').
|
|
1670
1722
|
"""
|
|
1671
|
-
|
|
1723
|
+
|
|
1672
1724
|
if modes is None:
|
|
1673
1725
|
modes = ('cv','pil','skimage','jpeg_trailer')
|
|
1674
1726
|
else:
|
|
@@ -1676,14 +1728,14 @@ def check_image_integrity(filename,modes=None):
|
|
|
1676
1728
|
modes = [modes]
|
|
1677
1729
|
for mode in modes:
|
|
1678
1730
|
assert mode in ('cv','pil','skimage'), 'Unrecognized mode {}'.format(mode)
|
|
1679
|
-
|
|
1731
|
+
|
|
1680
1732
|
assert os.path.isfile(filename), 'Could not find file {}'.format(filename)
|
|
1681
|
-
|
|
1733
|
+
|
|
1682
1734
|
result = {}
|
|
1683
1735
|
result['file'] = filename
|
|
1684
|
-
|
|
1736
|
+
|
|
1685
1737
|
for mode in modes:
|
|
1686
|
-
|
|
1738
|
+
|
|
1687
1739
|
result[mode] = 'unknown'
|
|
1688
1740
|
if mode == 'pil':
|
|
1689
1741
|
try:
|
|
@@ -1700,10 +1752,10 @@ def check_image_integrity(filename,modes=None):
|
|
|
1700
1752
|
result[mode] = 'success'
|
|
1701
1753
|
except Exception as e:
|
|
1702
1754
|
result[mode] = 'error: {}'.format(str(e))
|
|
1703
|
-
elif mode == 'skimage':
|
|
1755
|
+
elif mode == 'skimage':
|
|
1704
1756
|
try:
|
|
1705
1757
|
# This is not a standard dependency
|
|
1706
|
-
from skimage import io as skimage_io # noqa
|
|
1758
|
+
from skimage import io as skimage_io # type: ignore # noqa
|
|
1707
1759
|
except Exception:
|
|
1708
1760
|
result[mode] = 'could not import skimage, run pip install scikit-image'
|
|
1709
1761
|
return result
|
|
@@ -1724,26 +1776,26 @@ def check_image_integrity(filename,modes=None):
|
|
|
1724
1776
|
result[mode] = 'success'
|
|
1725
1777
|
except Exception as e:
|
|
1726
1778
|
result[mode] = 'error: {}'.format(str(e))
|
|
1727
|
-
|
|
1728
|
-
# ...for each mode
|
|
1729
|
-
|
|
1779
|
+
|
|
1780
|
+
# ...for each mode
|
|
1781
|
+
|
|
1730
1782
|
return result
|
|
1731
1783
|
|
|
1732
1784
|
# ...def check_image_integrity(...)
|
|
1733
1785
|
|
|
1734
1786
|
|
|
1735
1787
|
def parallel_check_image_integrity(filenames,
|
|
1736
|
-
modes=None,
|
|
1737
|
-
max_workers=16,
|
|
1738
|
-
use_threads=True,
|
|
1788
|
+
modes=None,
|
|
1789
|
+
max_workers=16,
|
|
1790
|
+
use_threads=True,
|
|
1739
1791
|
recursive=True,
|
|
1740
1792
|
verbose=False):
|
|
1741
1793
|
"""
|
|
1742
1794
|
Check whether we can successfully load a list of images via OpenCV and/or PIL.
|
|
1743
|
-
|
|
1795
|
+
|
|
1744
1796
|
Args:
|
|
1745
1797
|
filenames (list or str): a list of image filenames or a folder
|
|
1746
|
-
|
|
1798
|
+
modes (list, optional): see check_image_integrity() for documentation on the [modes] parameter
|
|
1747
1799
|
max_workers (int, optional): the number of parallel workers to use; set to <=1 to disable
|
|
1748
1800
|
parallelization
|
|
1749
1801
|
use_threads (bool, optional): whether to use threads (True) or processes (False) for
|
|
@@ -1751,10 +1803,10 @@ def parallel_check_image_integrity(filenames,
|
|
|
1751
1803
|
recursive (bool, optional): if [filenames] is a folder, whether to search recursively for images.
|
|
1752
1804
|
Ignored if [filenames] is a list.
|
|
1753
1805
|
verbose (bool, optional): enable additional debug output
|
|
1754
|
-
|
|
1806
|
+
|
|
1755
1807
|
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
|
|
1808
|
+
list: a list of dicts, each with a key called 'file' (the value of [filename]), one key for
|
|
1809
|
+
each string in [modes] (a success indicator for that mode, specifically a string starting
|
|
1758
1810
|
with either 'success' or 'error').
|
|
1759
1811
|
"""
|
|
1760
1812
|
|
|
@@ -1762,69 +1814,69 @@ def parallel_check_image_integrity(filenames,
|
|
|
1762
1814
|
if verbose:
|
|
1763
1815
|
print('Enumerating images in {}'.format(filenames))
|
|
1764
1816
|
filenames = find_images(filenames,recursive=recursive,return_relative_paths=False)
|
|
1765
|
-
|
|
1817
|
+
|
|
1766
1818
|
n_workers = min(max_workers,len(filenames))
|
|
1767
|
-
|
|
1819
|
+
|
|
1768
1820
|
if verbose:
|
|
1769
1821
|
print('Checking image integrity for {} filenames'.format(len(filenames)))
|
|
1770
|
-
|
|
1822
|
+
|
|
1771
1823
|
if n_workers <= 1:
|
|
1772
|
-
|
|
1824
|
+
|
|
1773
1825
|
results = []
|
|
1774
1826
|
for filename in filenames:
|
|
1775
1827
|
results.append(check_image_integrity(filename,modes=modes))
|
|
1776
|
-
|
|
1828
|
+
|
|
1777
1829
|
else:
|
|
1778
|
-
|
|
1830
|
+
|
|
1779
1831
|
if use_threads:
|
|
1780
1832
|
pool = ThreadPool(n_workers)
|
|
1781
1833
|
else:
|
|
1782
1834
|
pool = Pool(n_workers)
|
|
1783
|
-
|
|
1835
|
+
|
|
1784
1836
|
results = list(tqdm(pool.imap(
|
|
1785
1837
|
partial(check_image_integrity,modes=modes),filenames), total=len(filenames)))
|
|
1786
|
-
|
|
1838
|
+
|
|
1787
1839
|
return results
|
|
1788
1840
|
|
|
1789
1841
|
|
|
1790
1842
|
#%% Test drivers
|
|
1791
1843
|
|
|
1792
1844
|
if False:
|
|
1793
|
-
|
|
1845
|
+
|
|
1794
1846
|
#%% Text rendering tests
|
|
1795
|
-
|
|
1847
|
+
|
|
1796
1848
|
import os # noqa
|
|
1797
1849
|
import numpy as np # noqa
|
|
1798
1850
|
from megadetector.visualization.visualization_utils import \
|
|
1799
1851
|
draw_bounding_boxes_on_image, exif_preserving_save, load_image, \
|
|
1800
1852
|
TEXTALIGN_LEFT,TEXTALIGN_RIGHT,VTEXTALIGN_BOTTOM,VTEXTALIGN_TOP, \
|
|
1801
1853
|
DEFAULT_LABEL_FONT_SIZE
|
|
1802
|
-
|
|
1854
|
+
|
|
1803
1855
|
fn = os.path.expanduser('~/AppData/Local/Temp/md-tests/md-test-images/ena24_7904.jpg')
|
|
1804
1856
|
output_fn = r'g:\temp\test.jpg'
|
|
1805
|
-
|
|
1857
|
+
|
|
1806
1858
|
image = load_image(fn)
|
|
1807
|
-
|
|
1859
|
+
|
|
1808
1860
|
w = 0.2; h = 0.2
|
|
1809
1861
|
all_boxes = [[0.05, 0.05, 0.25, 0.25],
|
|
1810
1862
|
[0.05, 0.35, 0.25, 0.6],
|
|
1811
1863
|
[0.35, 0.05, 0.6, 0.25],
|
|
1812
1864
|
[0.35, 0.35, 0.6, 0.6]]
|
|
1813
|
-
|
|
1865
|
+
|
|
1814
1866
|
alignments = [
|
|
1815
1867
|
[TEXTALIGN_LEFT,VTEXTALIGN_TOP],
|
|
1816
1868
|
[TEXTALIGN_LEFT,VTEXTALIGN_BOTTOM],
|
|
1817
1869
|
[TEXTALIGN_RIGHT,VTEXTALIGN_TOP],
|
|
1818
1870
|
[TEXTALIGN_RIGHT,VTEXTALIGN_BOTTOM]
|
|
1819
1871
|
]
|
|
1820
|
-
|
|
1872
|
+
|
|
1821
1873
|
labels = ['left_top','left_bottom','right_top','right_bottom']
|
|
1822
|
-
|
|
1874
|
+
|
|
1823
1875
|
text_rotation = -90
|
|
1824
1876
|
n_label_copies = 2
|
|
1825
|
-
|
|
1877
|
+
|
|
1826
1878
|
for i_box,box in enumerate(all_boxes):
|
|
1827
|
-
|
|
1879
|
+
|
|
1828
1880
|
boxes = [box]
|
|
1829
1881
|
boxes = np.array(boxes)
|
|
1830
1882
|
classes = [i_box]
|
|
@@ -1846,30 +1898,30 @@ if False:
|
|
|
1846
1898
|
exif_preserving_save(image,output_fn)
|
|
1847
1899
|
from megadetector.utils.path_utils import open_file
|
|
1848
1900
|
open_file(output_fn)
|
|
1849
|
-
|
|
1850
|
-
|
|
1901
|
+
|
|
1902
|
+
|
|
1851
1903
|
#%% Recursive resize test
|
|
1852
|
-
|
|
1904
|
+
|
|
1853
1905
|
from megadetector.visualization.visualization_utils import resize_image_folder # noqa
|
|
1854
|
-
|
|
1906
|
+
|
|
1855
1907
|
input_folder = r"C:\temp\resize-test\in"
|
|
1856
1908
|
output_folder = r"C:\temp\resize-test\out"
|
|
1857
|
-
|
|
1909
|
+
|
|
1858
1910
|
resize_results = resize_image_folder(input_folder,output_folder,
|
|
1859
1911
|
target_width=1280,verbose=True,quality=85,no_enlarge_width=True,
|
|
1860
1912
|
pool_type='process',n_workers=10)
|
|
1861
|
-
|
|
1862
|
-
|
|
1913
|
+
|
|
1914
|
+
|
|
1863
1915
|
#%% Integrity checking test
|
|
1864
|
-
|
|
1916
|
+
|
|
1865
1917
|
from megadetector.utils import md_tests
|
|
1866
1918
|
options = md_tests.download_test_data()
|
|
1867
1919
|
folder = options.scratch_dir
|
|
1868
|
-
|
|
1920
|
+
|
|
1869
1921
|
results = parallel_check_image_integrity(folder,max_workers=8)
|
|
1870
|
-
|
|
1922
|
+
|
|
1871
1923
|
modes = ['cv','pil','skimage','jpeg_trailer']
|
|
1872
|
-
|
|
1924
|
+
|
|
1873
1925
|
for r in results:
|
|
1874
1926
|
for mode in modes:
|
|
1875
1927
|
if r[mode] != 'success':
|