megadetector 5.0.9__py3-none-any.whl → 5.0.11__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-5.0.9.dist-info → megadetector-5.0.11.dist-info}/LICENSE +0 -0
- {megadetector-5.0.9.dist-info → megadetector-5.0.11.dist-info}/METADATA +12 -11
- megadetector-5.0.11.dist-info/RECORD +5 -0
- megadetector-5.0.11.dist-info/top_level.txt +1 -0
- api/__init__.py +0 -0
- api/batch_processing/__init__.py +0 -0
- api/batch_processing/api_core/__init__.py +0 -0
- api/batch_processing/api_core/batch_service/__init__.py +0 -0
- api/batch_processing/api_core/batch_service/score.py +0 -439
- api/batch_processing/api_core/server.py +0 -294
- api/batch_processing/api_core/server_api_config.py +0 -98
- api/batch_processing/api_core/server_app_config.py +0 -55
- api/batch_processing/api_core/server_batch_job_manager.py +0 -220
- api/batch_processing/api_core/server_job_status_table.py +0 -152
- api/batch_processing/api_core/server_orchestration.py +0 -360
- api/batch_processing/api_core/server_utils.py +0 -92
- api/batch_processing/api_core_support/__init__.py +0 -0
- api/batch_processing/api_core_support/aggregate_results_manually.py +0 -46
- api/batch_processing/api_support/__init__.py +0 -0
- api/batch_processing/api_support/summarize_daily_activity.py +0 -152
- api/batch_processing/data_preparation/__init__.py +0 -0
- api/batch_processing/data_preparation/manage_local_batch.py +0 -2391
- api/batch_processing/data_preparation/manage_video_batch.py +0 -327
- api/batch_processing/integration/digiKam/setup.py +0 -6
- api/batch_processing/integration/digiKam/xmp_integration.py +0 -465
- api/batch_processing/integration/eMammal/test_scripts/config_template.py +0 -5
- api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +0 -126
- api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +0 -55
- api/batch_processing/postprocessing/__init__.py +0 -0
- api/batch_processing/postprocessing/add_max_conf.py +0 -64
- api/batch_processing/postprocessing/categorize_detections_by_size.py +0 -163
- api/batch_processing/postprocessing/combine_api_outputs.py +0 -249
- api/batch_processing/postprocessing/compare_batch_results.py +0 -958
- api/batch_processing/postprocessing/convert_output_format.py +0 -397
- api/batch_processing/postprocessing/load_api_results.py +0 -195
- api/batch_processing/postprocessing/md_to_coco.py +0 -310
- api/batch_processing/postprocessing/md_to_labelme.py +0 -330
- api/batch_processing/postprocessing/merge_detections.py +0 -401
- api/batch_processing/postprocessing/postprocess_batch_results.py +0 -1904
- api/batch_processing/postprocessing/remap_detection_categories.py +0 -170
- api/batch_processing/postprocessing/render_detection_confusion_matrix.py +0 -661
- api/batch_processing/postprocessing/repeat_detection_elimination/find_repeat_detections.py +0 -211
- api/batch_processing/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +0 -82
- api/batch_processing/postprocessing/repeat_detection_elimination/repeat_detections_core.py +0 -1631
- api/batch_processing/postprocessing/separate_detections_into_folders.py +0 -731
- api/batch_processing/postprocessing/subset_json_detector_output.py +0 -696
- api/batch_processing/postprocessing/top_folders_to_bottom.py +0 -223
- api/synchronous/__init__.py +0 -0
- api/synchronous/api_core/animal_detection_api/__init__.py +0 -0
- api/synchronous/api_core/animal_detection_api/api_backend.py +0 -152
- api/synchronous/api_core/animal_detection_api/api_frontend.py +0 -266
- api/synchronous/api_core/animal_detection_api/config.py +0 -35
- api/synchronous/api_core/animal_detection_api/data_management/annotations/annotation_constants.py +0 -47
- api/synchronous/api_core/animal_detection_api/detection/detector_training/copy_checkpoints.py +0 -43
- api/synchronous/api_core/animal_detection_api/detection/detector_training/model_main_tf2.py +0 -114
- api/synchronous/api_core/animal_detection_api/detection/process_video.py +0 -543
- api/synchronous/api_core/animal_detection_api/detection/pytorch_detector.py +0 -304
- api/synchronous/api_core/animal_detection_api/detection/run_detector.py +0 -627
- api/synchronous/api_core/animal_detection_api/detection/run_detector_batch.py +0 -1029
- api/synchronous/api_core/animal_detection_api/detection/run_inference_with_yolov5_val.py +0 -581
- api/synchronous/api_core/animal_detection_api/detection/run_tiled_inference.py +0 -754
- api/synchronous/api_core/animal_detection_api/detection/tf_detector.py +0 -165
- api/synchronous/api_core/animal_detection_api/detection/video_utils.py +0 -495
- api/synchronous/api_core/animal_detection_api/md_utils/azure_utils.py +0 -174
- api/synchronous/api_core/animal_detection_api/md_utils/ct_utils.py +0 -262
- api/synchronous/api_core/animal_detection_api/md_utils/directory_listing.py +0 -251
- api/synchronous/api_core/animal_detection_api/md_utils/matlab_porting_tools.py +0 -97
- api/synchronous/api_core/animal_detection_api/md_utils/path_utils.py +0 -416
- api/synchronous/api_core/animal_detection_api/md_utils/process_utils.py +0 -110
- api/synchronous/api_core/animal_detection_api/md_utils/sas_blob_utils.py +0 -509
- api/synchronous/api_core/animal_detection_api/md_utils/string_utils.py +0 -59
- api/synchronous/api_core/animal_detection_api/md_utils/url_utils.py +0 -144
- api/synchronous/api_core/animal_detection_api/md_utils/write_html_image_list.py +0 -226
- api/synchronous/api_core/animal_detection_api/md_visualization/visualization_utils.py +0 -841
- api/synchronous/api_core/tests/__init__.py +0 -0
- api/synchronous/api_core/tests/load_test.py +0 -110
- classification/__init__.py +0 -0
- classification/aggregate_classifier_probs.py +0 -108
- classification/analyze_failed_images.py +0 -227
- classification/cache_batchapi_outputs.py +0 -198
- classification/create_classification_dataset.py +0 -627
- classification/crop_detections.py +0 -516
- classification/csv_to_json.py +0 -226
- classification/detect_and_crop.py +0 -855
- classification/efficientnet/__init__.py +0 -9
- classification/efficientnet/model.py +0 -415
- classification/efficientnet/utils.py +0 -610
- classification/evaluate_model.py +0 -520
- classification/identify_mislabeled_candidates.py +0 -152
- classification/json_to_azcopy_list.py +0 -63
- classification/json_validator.py +0 -695
- classification/map_classification_categories.py +0 -276
- classification/merge_classification_detection_output.py +0 -506
- classification/prepare_classification_script.py +0 -194
- classification/prepare_classification_script_mc.py +0 -228
- classification/run_classifier.py +0 -286
- classification/save_mislabeled.py +0 -110
- classification/train_classifier.py +0 -825
- classification/train_classifier_tf.py +0 -724
- classification/train_utils.py +0 -322
- data_management/__init__.py +0 -0
- data_management/annotations/__init__.py +0 -0
- data_management/annotations/annotation_constants.py +0 -34
- data_management/camtrap_dp_to_coco.py +0 -238
- data_management/cct_json_utils.py +0 -395
- data_management/cct_to_md.py +0 -176
- data_management/cct_to_wi.py +0 -289
- data_management/coco_to_labelme.py +0 -272
- data_management/coco_to_yolo.py +0 -662
- data_management/databases/__init__.py +0 -0
- data_management/databases/add_width_and_height_to_db.py +0 -33
- data_management/databases/combine_coco_camera_traps_files.py +0 -206
- data_management/databases/integrity_check_json_db.py +0 -477
- data_management/databases/subset_json_db.py +0 -115
- data_management/generate_crops_from_cct.py +0 -149
- data_management/get_image_sizes.py +0 -188
- data_management/importers/add_nacti_sizes.py +0 -52
- data_management/importers/add_timestamps_to_icct.py +0 -79
- data_management/importers/animl_results_to_md_results.py +0 -158
- data_management/importers/auckland_doc_test_to_json.py +0 -372
- data_management/importers/auckland_doc_to_json.py +0 -200
- data_management/importers/awc_to_json.py +0 -189
- data_management/importers/bellevue_to_json.py +0 -273
- data_management/importers/cacophony-thermal-importer.py +0 -796
- data_management/importers/carrizo_shrubfree_2018.py +0 -268
- data_management/importers/carrizo_trail_cam_2017.py +0 -287
- data_management/importers/cct_field_adjustments.py +0 -57
- data_management/importers/channel_islands_to_cct.py +0 -913
- data_management/importers/eMammal/copy_and_unzip_emammal.py +0 -180
- data_management/importers/eMammal/eMammal_helpers.py +0 -249
- data_management/importers/eMammal/make_eMammal_json.py +0 -223
- data_management/importers/ena24_to_json.py +0 -275
- data_management/importers/filenames_to_json.py +0 -385
- data_management/importers/helena_to_cct.py +0 -282
- data_management/importers/idaho-camera-traps.py +0 -1407
- data_management/importers/idfg_iwildcam_lila_prep.py +0 -294
- data_management/importers/jb_csv_to_json.py +0 -150
- data_management/importers/mcgill_to_json.py +0 -250
- data_management/importers/missouri_to_json.py +0 -489
- data_management/importers/nacti_fieldname_adjustments.py +0 -79
- data_management/importers/noaa_seals_2019.py +0 -181
- data_management/importers/pc_to_json.py +0 -365
- data_management/importers/plot_wni_giraffes.py +0 -123
- data_management/importers/prepare-noaa-fish-data-for-lila.py +0 -359
- data_management/importers/prepare_zsl_imerit.py +0 -131
- data_management/importers/rspb_to_json.py +0 -356
- data_management/importers/save_the_elephants_survey_A.py +0 -320
- data_management/importers/save_the_elephants_survey_B.py +0 -332
- data_management/importers/snapshot_safari_importer.py +0 -758
- data_management/importers/snapshot_safari_importer_reprise.py +0 -665
- data_management/importers/snapshot_serengeti_lila.py +0 -1067
- data_management/importers/snapshotserengeti/make_full_SS_json.py +0 -150
- data_management/importers/snapshotserengeti/make_per_season_SS_json.py +0 -153
- data_management/importers/sulross_get_exif.py +0 -65
- data_management/importers/timelapse_csv_set_to_json.py +0 -490
- data_management/importers/ubc_to_json.py +0 -399
- data_management/importers/umn_to_json.py +0 -507
- data_management/importers/wellington_to_json.py +0 -263
- data_management/importers/wi_to_json.py +0 -441
- data_management/importers/zamba_results_to_md_results.py +0 -181
- data_management/labelme_to_coco.py +0 -548
- data_management/labelme_to_yolo.py +0 -272
- data_management/lila/__init__.py +0 -0
- data_management/lila/add_locations_to_island_camera_traps.py +0 -97
- data_management/lila/add_locations_to_nacti.py +0 -147
- data_management/lila/create_lila_blank_set.py +0 -557
- data_management/lila/create_lila_test_set.py +0 -151
- data_management/lila/create_links_to_md_results_files.py +0 -106
- data_management/lila/download_lila_subset.py +0 -177
- data_management/lila/generate_lila_per_image_labels.py +0 -515
- data_management/lila/get_lila_annotation_counts.py +0 -170
- data_management/lila/get_lila_image_counts.py +0 -111
- data_management/lila/lila_common.py +0 -300
- data_management/lila/test_lila_metadata_urls.py +0 -132
- data_management/ocr_tools.py +0 -874
- data_management/read_exif.py +0 -681
- data_management/remap_coco_categories.py +0 -84
- data_management/remove_exif.py +0 -66
- data_management/resize_coco_dataset.py +0 -189
- data_management/wi_download_csv_to_coco.py +0 -246
- data_management/yolo_output_to_md_output.py +0 -441
- data_management/yolo_to_coco.py +0 -676
- detection/__init__.py +0 -0
- detection/detector_training/__init__.py +0 -0
- detection/detector_training/model_main_tf2.py +0 -114
- detection/process_video.py +0 -703
- detection/pytorch_detector.py +0 -337
- detection/run_detector.py +0 -779
- detection/run_detector_batch.py +0 -1219
- detection/run_inference_with_yolov5_val.py +0 -917
- detection/run_tiled_inference.py +0 -935
- detection/tf_detector.py +0 -188
- detection/video_utils.py +0 -606
- docs/source/conf.py +0 -43
- md_utils/__init__.py +0 -0
- md_utils/azure_utils.py +0 -174
- md_utils/ct_utils.py +0 -612
- md_utils/directory_listing.py +0 -246
- md_utils/md_tests.py +0 -968
- md_utils/path_utils.py +0 -1044
- md_utils/process_utils.py +0 -157
- md_utils/sas_blob_utils.py +0 -509
- md_utils/split_locations_into_train_val.py +0 -228
- md_utils/string_utils.py +0 -92
- md_utils/url_utils.py +0 -323
- md_utils/write_html_image_list.py +0 -225
- md_visualization/__init__.py +0 -0
- md_visualization/plot_utils.py +0 -293
- md_visualization/render_images_with_thumbnails.py +0 -275
- md_visualization/visualization_utils.py +0 -1537
- md_visualization/visualize_db.py +0 -551
- md_visualization/visualize_detector_output.py +0 -406
- megadetector-5.0.9.dist-info/RECORD +0 -224
- megadetector-5.0.9.dist-info/top_level.txt +0 -8
- taxonomy_mapping/__init__.py +0 -0
- taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +0 -491
- taxonomy_mapping/map_new_lila_datasets.py +0 -154
- taxonomy_mapping/prepare_lila_taxonomy_release.py +0 -142
- taxonomy_mapping/preview_lila_taxonomy.py +0 -591
- taxonomy_mapping/retrieve_sample_image.py +0 -71
- taxonomy_mapping/simple_image_download.py +0 -218
- taxonomy_mapping/species_lookup.py +0 -834
- taxonomy_mapping/taxonomy_csv_checker.py +0 -159
- taxonomy_mapping/taxonomy_graph.py +0 -346
- taxonomy_mapping/validate_lila_category_mappings.py +0 -83
- {megadetector-5.0.9.dist-info → megadetector-5.0.11.dist-info}/WHEEL +0 -0
|
@@ -1,841 +0,0 @@
|
|
|
1
|
-
########
|
|
2
|
-
#
|
|
3
|
-
# visualization_utils.py
|
|
4
|
-
#
|
|
5
|
-
# Core rendering functions shared across visualization scripts
|
|
6
|
-
#
|
|
7
|
-
########
|
|
8
|
-
|
|
9
|
-
#%% Constants and imports
|
|
10
|
-
|
|
11
|
-
from io import BytesIO
|
|
12
|
-
from typing import Union
|
|
13
|
-
import time
|
|
14
|
-
|
|
15
|
-
import matplotlib.pyplot as plt
|
|
16
|
-
import numpy as np
|
|
17
|
-
import requests
|
|
18
|
-
from PIL import Image, ImageFile, ImageFont, ImageDraw
|
|
19
|
-
|
|
20
|
-
from data_management.annotations import annotation_constants
|
|
21
|
-
from data_management.annotations.annotation_constants import (
|
|
22
|
-
detector_bbox_category_id_to_name) # here id is int
|
|
23
|
-
|
|
24
|
-
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
|
25
|
-
|
|
26
|
-
IMAGE_ROTATIONS = {
|
|
27
|
-
3: 180,
|
|
28
|
-
6: 270,
|
|
29
|
-
8: 90
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
TEXTALIGN_LEFT = 0
|
|
33
|
-
TEXTALIGN_RIGHT = 1
|
|
34
|
-
|
|
35
|
-
# convert category ID from int to str
|
|
36
|
-
DEFAULT_DETECTOR_LABEL_MAP = {
|
|
37
|
-
str(k): v for k, v in detector_bbox_category_id_to_name.items()
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
# Retry on blob storage read failures
|
|
41
|
-
n_retries = 10
|
|
42
|
-
retry_sleep_time = 0.01
|
|
43
|
-
error_names_for_retry = ['ConnectionError']
|
|
44
|
-
|
|
45
|
-
DEFAULT_BOX_THICKNESS = 4
|
|
46
|
-
DEFAULT_LABEL_FONT_SIZE = 16
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
#%% Functions
|
|
50
|
-
|
|
51
|
-
def open_image(input_file: Union[str, BytesIO]) -> Image:
|
|
52
|
-
"""
|
|
53
|
-
Opens an image in binary format using PIL.Image and converts to RGB mode.
|
|
54
|
-
|
|
55
|
-
Supports local files or URLs.
|
|
56
|
-
|
|
57
|
-
This operation is lazy; image will not be actually loaded until the first
|
|
58
|
-
operation that needs to load it (for example, resizing), so file opening
|
|
59
|
-
errors can show up later.
|
|
60
|
-
|
|
61
|
-
Args:
|
|
62
|
-
input_file: str or BytesIO, either a path to an image file (anything
|
|
63
|
-
that PIL can open), or an image as a stream of bytes
|
|
64
|
-
|
|
65
|
-
Returns:
|
|
66
|
-
A PIL image object in RGB mode
|
|
67
|
-
"""
|
|
68
|
-
|
|
69
|
-
if (isinstance(input_file, str)
|
|
70
|
-
and input_file.startswith(('http://', 'https://'))):
|
|
71
|
-
try:
|
|
72
|
-
response = requests.get(input_file)
|
|
73
|
-
except Exception as e:
|
|
74
|
-
print(f'Error retrieving image {input_file}: {e}')
|
|
75
|
-
success = False
|
|
76
|
-
if e.__class__.__name__ in error_names_for_retry:
|
|
77
|
-
for i_retry in range(0,n_retries):
|
|
78
|
-
try:
|
|
79
|
-
time.sleep(retry_sleep_time)
|
|
80
|
-
response = requests.get(input_file)
|
|
81
|
-
except Exception as e:
|
|
82
|
-
print(f'Error retrieving image {input_file} on retry {i_retry}: {e}')
|
|
83
|
-
continue
|
|
84
|
-
print('Succeeded on retry {}'.format(i_retry))
|
|
85
|
-
success = True
|
|
86
|
-
break
|
|
87
|
-
if not success:
|
|
88
|
-
raise
|
|
89
|
-
try:
|
|
90
|
-
image = Image.open(BytesIO(response.content))
|
|
91
|
-
except Exception as e:
|
|
92
|
-
print(f'Error opening image {input_file}: {e}')
|
|
93
|
-
raise
|
|
94
|
-
|
|
95
|
-
else:
|
|
96
|
-
image = Image.open(input_file)
|
|
97
|
-
if image.mode not in ('RGBA', 'RGB', 'L', 'I;16'):
|
|
98
|
-
raise AttributeError(
|
|
99
|
-
f'Image {input_file} uses unsupported mode {image.mode}')
|
|
100
|
-
if image.mode == 'RGBA' or image.mode == 'L':
|
|
101
|
-
# PIL.Image.convert() returns a converted copy of this image
|
|
102
|
-
image = image.convert(mode='RGB')
|
|
103
|
-
|
|
104
|
-
# Alter orientation as needed according to EXIF tag 0x112 (274) for Orientation
|
|
105
|
-
#
|
|
106
|
-
# https://gist.github.com/dangtrinhnt/a577ece4cbe5364aad28
|
|
107
|
-
# https://www.media.mit.edu/pia/Research/deepview/exif.html
|
|
108
|
-
#
|
|
109
|
-
try:
|
|
110
|
-
exif = image._getexif()
|
|
111
|
-
orientation: int = exif.get(274, None) # 274 is the key for the Orientation field
|
|
112
|
-
if orientation is not None and orientation in IMAGE_ROTATIONS:
|
|
113
|
-
image = image.rotate(IMAGE_ROTATIONS[orientation], expand=True) # returns a rotated copy
|
|
114
|
-
except Exception:
|
|
115
|
-
pass
|
|
116
|
-
|
|
117
|
-
return image
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def exif_preserving_save(pil_image,output_file):
|
|
121
|
-
"""
|
|
122
|
-
Save [pil_image] to [output_file], making a moderate attempt to preserve EXIF
|
|
123
|
-
data and JPEG quality. Neither is guaranteed.
|
|
124
|
-
|
|
125
|
-
Also see:
|
|
126
|
-
|
|
127
|
-
https://discuss.dizzycoding.com/determining-jpg-quality-in-python-pil/
|
|
128
|
-
|
|
129
|
-
...for more ways to preserve jpeg quality if quality='keep' doesn't do the trick.
|
|
130
|
-
"""
|
|
131
|
-
|
|
132
|
-
# Read EXIF metadata
|
|
133
|
-
exif = pil_image.info['exif'] if ('exif' in pil_image.info) else None
|
|
134
|
-
|
|
135
|
-
# Write output with EXIF metadata if available, and quality='keep' if this is a JPEG
|
|
136
|
-
# image. Unfortunately, neither parameter likes "None", so we get a slightly
|
|
137
|
-
# icky cascade of if's here.
|
|
138
|
-
if exif is not None:
|
|
139
|
-
if pil_image.format == "JPEG":
|
|
140
|
-
pil_image.save(output_file, exif=exif, quality='keep')
|
|
141
|
-
else:
|
|
142
|
-
pil_image.save(output_file, exif=exif)
|
|
143
|
-
else:
|
|
144
|
-
if pil_image.format == "JPEG":
|
|
145
|
-
pil_image.save(output_file, quality='keep')
|
|
146
|
-
else:
|
|
147
|
-
pil_image.save(output_file)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def load_image(input_file: Union[str, BytesIO]) -> Image:
|
|
151
|
-
"""
|
|
152
|
-
Loads the image at input_file as a PIL Image into memory.
|
|
153
|
-
|
|
154
|
-
Image.open() used in open_image() is lazy and errors will occur downstream
|
|
155
|
-
if not explicitly loaded.
|
|
156
|
-
|
|
157
|
-
Args:
|
|
158
|
-
input_file: str or BytesIO, either a path to an image file (anything
|
|
159
|
-
that PIL can open), or an image as a stream of bytes
|
|
160
|
-
|
|
161
|
-
Returns: PIL.Image.Image, in RGB mode
|
|
162
|
-
"""
|
|
163
|
-
|
|
164
|
-
image = open_image(input_file)
|
|
165
|
-
image.load()
|
|
166
|
-
return image
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def resize_image(image, target_width, target_height=-1):
|
|
170
|
-
"""
|
|
171
|
-
Resizes a PIL image object to the specified width and height; does not resize
|
|
172
|
-
in place. If either width or height are -1, resizes with aspect ratio preservation.
|
|
173
|
-
If both are -1, returns the original image (does not copy in this case).
|
|
174
|
-
"""
|
|
175
|
-
|
|
176
|
-
# Null operation
|
|
177
|
-
if target_width == -1 and target_height == -1:
|
|
178
|
-
return image
|
|
179
|
-
|
|
180
|
-
elif target_width == -1 or target_height == -1:
|
|
181
|
-
|
|
182
|
-
# Aspect ratio as width over height
|
|
183
|
-
# ar = w / h
|
|
184
|
-
aspect_ratio = image.size[0] / image.size[1]
|
|
185
|
-
|
|
186
|
-
if target_width != -1:
|
|
187
|
-
# h = w / ar
|
|
188
|
-
target_height = int(target_width / aspect_ratio)
|
|
189
|
-
else:
|
|
190
|
-
# w = ar * h
|
|
191
|
-
target_width = int(aspect_ratio * target_height)
|
|
192
|
-
|
|
193
|
-
# This parameter changed between Pillow versions 9 and 10, and for a bit, I'd like to
|
|
194
|
-
# support both.
|
|
195
|
-
try:
|
|
196
|
-
resized_image = image.resize((target_width, target_height), Image.ANTIALIAS)
|
|
197
|
-
except:
|
|
198
|
-
resized_image = image.resize((target_width, target_height), Image.Resampling.LANCZOS)
|
|
199
|
-
|
|
200
|
-
return resized_image
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
def show_images_in_a_row(images):
|
|
204
|
-
|
|
205
|
-
num = len(images)
|
|
206
|
-
assert num > 0
|
|
207
|
-
|
|
208
|
-
if isinstance(images[0], str):
|
|
209
|
-
images = [Image.open(img) for img in images]
|
|
210
|
-
|
|
211
|
-
fig, axarr = plt.subplots(1, num, squeeze=False) # number of rows, number of columns
|
|
212
|
-
fig.set_size_inches((num * 5, 25)) # each image is 2 inches wide
|
|
213
|
-
for i, img in enumerate(images):
|
|
214
|
-
axarr[0, i].set_axis_off()
|
|
215
|
-
axarr[0, i].imshow(img)
|
|
216
|
-
return fig
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
# The following three functions are modified versions of those at:
|
|
220
|
-
#
|
|
221
|
-
# https://github.com/tensorflow/models/blob/master/research/object_detection/utils/visualization_utils.py
|
|
222
|
-
|
|
223
|
-
DEFAULT_COLORS = [
|
|
224
|
-
'AliceBlue', 'Red', 'RoyalBlue', 'Gold', 'Chartreuse', 'Aqua', 'Azure',
|
|
225
|
-
'Beige', 'Bisque', 'BlanchedAlmond', 'BlueViolet', 'BurlyWood', 'CadetBlue',
|
|
226
|
-
'AntiqueWhite', 'Chocolate', 'Coral', 'CornflowerBlue', 'Cornsilk', 'Crimson',
|
|
227
|
-
'Cyan', 'DarkCyan', 'DarkGoldenRod', 'DarkGrey', 'DarkKhaki', 'DarkOrange',
|
|
228
|
-
'DarkOrchid', 'DarkSalmon', 'DarkSeaGreen', 'DarkTurquoise', 'DarkViolet',
|
|
229
|
-
'DeepPink', 'DeepSkyBlue', 'DodgerBlue', 'FireBrick', 'FloralWhite',
|
|
230
|
-
'ForestGreen', 'Fuchsia', 'Gainsboro', 'GhostWhite', 'GoldenRod',
|
|
231
|
-
'Salmon', 'Tan', 'HoneyDew', 'HotPink', 'IndianRed', 'Ivory', 'Khaki',
|
|
232
|
-
'Lavender', 'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue',
|
|
233
|
-
'LightCoral', 'LightCyan', 'LightGoldenRodYellow', 'LightGray', 'LightGrey',
|
|
234
|
-
'LightGreen', 'LightPink', 'LightSalmon', 'LightSeaGreen', 'LightSkyBlue',
|
|
235
|
-
'LightSlateGray', 'LightSlateGrey', 'LightSteelBlue', 'LightYellow', 'Lime',
|
|
236
|
-
'LimeGreen', 'Linen', 'Magenta', 'MediumAquaMarine', 'MediumOrchid',
|
|
237
|
-
'MediumPurple', 'MediumSeaGreen', 'MediumSlateBlue', 'MediumSpringGreen',
|
|
238
|
-
'MediumTurquoise', 'MediumVioletRed', 'MintCream', 'MistyRose', 'Moccasin',
|
|
239
|
-
'NavajoWhite', 'OldLace', 'Olive', 'OliveDrab', 'Orange', 'OrangeRed',
|
|
240
|
-
'Orchid', 'PaleGoldenRod', 'PaleGreen', 'PaleTurquoise', 'PaleVioletRed',
|
|
241
|
-
'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', 'Plum', 'PowderBlue', 'Purple',
|
|
242
|
-
'RosyBrown', 'Aquamarine', 'SaddleBrown', 'Green', 'SandyBrown',
|
|
243
|
-
'SeaGreen', 'SeaShell', 'Sienna', 'Silver', 'SkyBlue', 'SlateBlue',
|
|
244
|
-
'SlateGray', 'SlateGrey', 'Snow', 'SpringGreen', 'SteelBlue', 'GreenYellow',
|
|
245
|
-
'Teal', 'Thistle', 'Tomato', 'Turquoise', 'Violet', 'Wheat', 'White',
|
|
246
|
-
'WhiteSmoke', 'Yellow', 'YellowGreen'
|
|
247
|
-
]
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
def crop_image(detections, image, confidence_threshold=0.15, expansion=0):
|
|
251
|
-
"""
|
|
252
|
-
Crops detections above *confidence_threshold* from the PIL image *image*,
|
|
253
|
-
returning a list of PIL images.
|
|
254
|
-
|
|
255
|
-
*detections* should be a list of dictionaries with keys 'conf' and 'bbox';
|
|
256
|
-
see bbox format description below. Normalized, [x,y,w,h], upper-left-origin.
|
|
257
|
-
|
|
258
|
-
*expansion* specifies a number of pixels to include on each side of the box.
|
|
259
|
-
"""
|
|
260
|
-
|
|
261
|
-
ret_images = []
|
|
262
|
-
|
|
263
|
-
for detection in detections:
|
|
264
|
-
|
|
265
|
-
score = float(detection['conf'])
|
|
266
|
-
|
|
267
|
-
if score >= confidence_threshold:
|
|
268
|
-
|
|
269
|
-
x1, y1, w_box, h_box = detection['bbox']
|
|
270
|
-
ymin,xmin,ymax,xmax = y1, x1, y1 + h_box, x1 + w_box
|
|
271
|
-
|
|
272
|
-
# Convert to pixels so we can use the PIL crop() function
|
|
273
|
-
im_width, im_height = image.size
|
|
274
|
-
(left, right, top, bottom) = (xmin * im_width, xmax * im_width,
|
|
275
|
-
ymin * im_height, ymax * im_height)
|
|
276
|
-
|
|
277
|
-
if expansion > 0:
|
|
278
|
-
left -= expansion
|
|
279
|
-
right += expansion
|
|
280
|
-
top -= expansion
|
|
281
|
-
bottom += expansion
|
|
282
|
-
|
|
283
|
-
# PIL's crop() does surprising things if you provide values outside of
|
|
284
|
-
# the image, clip inputs
|
|
285
|
-
left = max(left,0); right = max(right,0)
|
|
286
|
-
top = max(top,0); bottom = max(bottom,0)
|
|
287
|
-
|
|
288
|
-
left = min(left,im_width-1); right = min(right,im_width-1)
|
|
289
|
-
top = min(top,im_height-1); bottom = min(bottom,im_height-1)
|
|
290
|
-
|
|
291
|
-
ret_images.append(image.crop((left, top, right, bottom)))
|
|
292
|
-
|
|
293
|
-
# ...if this detection is above threshold
|
|
294
|
-
|
|
295
|
-
# ...for each detection
|
|
296
|
-
|
|
297
|
-
return ret_images
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
def render_detection_bounding_boxes(detections, image,
|
|
301
|
-
label_map={},
|
|
302
|
-
classification_label_map=None,
|
|
303
|
-
confidence_threshold=0.15, thickness=DEFAULT_BOX_THICKNESS, expansion=0,
|
|
304
|
-
classification_confidence_threshold=0.3,
|
|
305
|
-
max_classifications=3,
|
|
306
|
-
colormap=DEFAULT_COLORS,
|
|
307
|
-
textalign=TEXTALIGN_LEFT,
|
|
308
|
-
label_font_size=DEFAULT_LABEL_FONT_SIZE,
|
|
309
|
-
custom_strings=None):
|
|
310
|
-
"""
|
|
311
|
-
Renders bounding boxes, label, and confidence on an image if confidence is above the threshold.
|
|
312
|
-
|
|
313
|
-
Boxes are in the format that's output from the batch processing API.
|
|
314
|
-
|
|
315
|
-
Renders classification labels if present.
|
|
316
|
-
|
|
317
|
-
Args:
|
|
318
|
-
|
|
319
|
-
detections: detections on the image, example content:
|
|
320
|
-
[
|
|
321
|
-
{
|
|
322
|
-
"category": "2",
|
|
323
|
-
"conf": 0.996,
|
|
324
|
-
"bbox": [
|
|
325
|
-
0.0,
|
|
326
|
-
0.2762,
|
|
327
|
-
0.1234,
|
|
328
|
-
0.2458
|
|
329
|
-
]
|
|
330
|
-
}
|
|
331
|
-
]
|
|
332
|
-
|
|
333
|
-
...where the bbox coordinates are [x, y, box_width, box_height].
|
|
334
|
-
|
|
335
|
-
(0, 0) is the upper-left. Coordinates are normalized.
|
|
336
|
-
|
|
337
|
-
Supports classification results, if *detections* has the format
|
|
338
|
-
[
|
|
339
|
-
{
|
|
340
|
-
"category": "2",
|
|
341
|
-
"conf": 0.996,
|
|
342
|
-
"bbox": [
|
|
343
|
-
0.0,
|
|
344
|
-
0.2762,
|
|
345
|
-
0.1234,
|
|
346
|
-
0.2458
|
|
347
|
-
]
|
|
348
|
-
"classifications": [
|
|
349
|
-
["3", 0.901],
|
|
350
|
-
["1", 0.071],
|
|
351
|
-
["4", 0.025]
|
|
352
|
-
]
|
|
353
|
-
}
|
|
354
|
-
]
|
|
355
|
-
|
|
356
|
-
image: PIL.Image object
|
|
357
|
-
|
|
358
|
-
label_map: optional, mapping the numerical label to a string name. The type of the numerical label
|
|
359
|
-
(default string) needs to be consistent with the keys in label_map; no casting is carried out.
|
|
360
|
-
If this is None, no labels are shown.
|
|
361
|
-
|
|
362
|
-
classification_label_map: optional, mapping of the string class labels to the actual class names.
|
|
363
|
-
The type of the numerical label (default string) needs to be consistent with the keys in
|
|
364
|
-
label_map; no casting is carried out. If this is None, no classification labels are shown.
|
|
365
|
-
|
|
366
|
-
confidence_threshold: optional, threshold above which the bounding box is rendered.
|
|
367
|
-
|
|
368
|
-
thickness: line thickness in pixels. Default value is 4.
|
|
369
|
-
|
|
370
|
-
expansion: number of pixels to expand bounding boxes on each side. Default is 0.
|
|
371
|
-
|
|
372
|
-
classification_confidence_threshold: confidence above which classification result is retained.
|
|
373
|
-
|
|
374
|
-
max_classifications: maximum number of classification results retained for one image.
|
|
375
|
-
|
|
376
|
-
custom_strings: optional set of strings to append to detection labels, should have the
|
|
377
|
-
same length as [detections]. Appended before classification labels, if classification
|
|
378
|
-
data is provided.
|
|
379
|
-
|
|
380
|
-
image is modified in place.
|
|
381
|
-
"""
|
|
382
|
-
|
|
383
|
-
if custom_strings is not None:
|
|
384
|
-
assert len(custom_strings) == len(detections), \
|
|
385
|
-
'{} custom strings provided for {} detections'.format(
|
|
386
|
-
len(custom_strings),len(detections))
|
|
387
|
-
|
|
388
|
-
display_boxes = []
|
|
389
|
-
|
|
390
|
-
# list of lists, one list of strings for each bounding box (to accommodate multiple labels)
|
|
391
|
-
display_strs = []
|
|
392
|
-
|
|
393
|
-
# for color selection
|
|
394
|
-
classes = []
|
|
395
|
-
|
|
396
|
-
for i_detection,detection in enumerate(detections):
|
|
397
|
-
|
|
398
|
-
score = detection['conf']
|
|
399
|
-
|
|
400
|
-
# Always render objects with a confidence of "None", this is typically used
|
|
401
|
-
# for ground truth data.
|
|
402
|
-
if score is None or score >= confidence_threshold:
|
|
403
|
-
|
|
404
|
-
x1, y1, w_box, h_box = detection['bbox']
|
|
405
|
-
display_boxes.append([y1, x1, y1 + h_box, x1 + w_box])
|
|
406
|
-
clss = detection['category']
|
|
407
|
-
|
|
408
|
-
# {} is the default, which means "show labels with no mapping", so don't use "if label_map" here
|
|
409
|
-
# if label_map:
|
|
410
|
-
if label_map is not None:
|
|
411
|
-
label = label_map[clss] if clss in label_map else clss
|
|
412
|
-
if score is not None:
|
|
413
|
-
displayed_label = ['{}: {}%'.format(label, round(100 * score))]
|
|
414
|
-
else:
|
|
415
|
-
displayed_label = ['{}'.format(label)]
|
|
416
|
-
else:
|
|
417
|
-
displayed_label = ''
|
|
418
|
-
|
|
419
|
-
if custom_strings is not None:
|
|
420
|
-
custom_string = custom_strings[i_detection]
|
|
421
|
-
if custom_string is not None and len(custom_string) > 0:
|
|
422
|
-
if isinstance(displayed_label,str):
|
|
423
|
-
displayed_label += ' ' + custom_string
|
|
424
|
-
else:
|
|
425
|
-
assert len(displayed_label) == 1
|
|
426
|
-
displayed_label[0] += ' ' + custom_string
|
|
427
|
-
|
|
428
|
-
if 'classifications' in detection:
|
|
429
|
-
|
|
430
|
-
# To avoid duplicate colors with detection-only visualization, offset
|
|
431
|
-
# the classification class index by the number of detection classes
|
|
432
|
-
clss = annotation_constants.NUM_DETECTOR_CATEGORIES + int(detection['classifications'][0][0])
|
|
433
|
-
classifications = detection['classifications']
|
|
434
|
-
if len(classifications) > max_classifications:
|
|
435
|
-
classifications = classifications[0:max_classifications]
|
|
436
|
-
|
|
437
|
-
for classification in classifications:
|
|
438
|
-
|
|
439
|
-
classification_conf = classification[1]
|
|
440
|
-
if classification_conf is not None and \
|
|
441
|
-
classification_conf < classification_confidence_threshold:
|
|
442
|
-
continue
|
|
443
|
-
class_key = classification[0]
|
|
444
|
-
if (classification_label_map is not None) and (class_key in classification_label_map):
|
|
445
|
-
class_name = classification_label_map[class_key]
|
|
446
|
-
else:
|
|
447
|
-
class_name = class_key
|
|
448
|
-
if classification_conf is not None:
|
|
449
|
-
displayed_label += ['{}: {:5.1%}'.format(class_name.lower(), classification_conf)]
|
|
450
|
-
else:
|
|
451
|
-
displayed_label += ['{}'.format(class_name.lower())]
|
|
452
|
-
|
|
453
|
-
# ...for each classification
|
|
454
|
-
|
|
455
|
-
# ...if we have classification results
|
|
456
|
-
|
|
457
|
-
display_strs.append(displayed_label)
|
|
458
|
-
classes.append(clss)
|
|
459
|
-
|
|
460
|
-
# ...if the confidence of this detection is above threshold
|
|
461
|
-
|
|
462
|
-
# ...for each detection
|
|
463
|
-
|
|
464
|
-
display_boxes = np.array(display_boxes)
|
|
465
|
-
|
|
466
|
-
draw_bounding_boxes_on_image(image, display_boxes, classes,
|
|
467
|
-
display_strs=display_strs, thickness=thickness,
|
|
468
|
-
expansion=expansion, colormap=colormap, textalign=textalign,
|
|
469
|
-
label_font_size=label_font_size)
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
def draw_bounding_boxes_on_image(image,
|
|
473
|
-
boxes,
|
|
474
|
-
classes,
|
|
475
|
-
thickness=DEFAULT_BOX_THICKNESS,
|
|
476
|
-
expansion=0,
|
|
477
|
-
display_strs=None,
|
|
478
|
-
colormap=DEFAULT_COLORS,
|
|
479
|
-
textalign=TEXTALIGN_LEFT,
|
|
480
|
-
label_font_size=DEFAULT_LABEL_FONT_SIZE):
|
|
481
|
-
"""
|
|
482
|
-
Draws bounding boxes on an image.
|
|
483
|
-
|
|
484
|
-
Args:
|
|
485
|
-
image: a PIL.Image object.
|
|
486
|
-
boxes: a 2 dimensional numpy array of [N, 4]: (ymin, xmin, ymax, xmax).
|
|
487
|
-
The coordinates are in normalized format between [0, 1].
|
|
488
|
-
classes: a list of ints or strings (that can be cast to ints) corresponding to the
|
|
489
|
-
class labels of the boxes. This is only used for color selection.
|
|
490
|
-
thickness: line thickness in pixels. Default value is 4.
|
|
491
|
-
expansion: number of pixels to expand bounding boxes on each side. Default is 0.
|
|
492
|
-
display_strs: list of list of strings.
|
|
493
|
-
a list of strings for each bounding box.
|
|
494
|
-
The reason to pass a list of strings for a
|
|
495
|
-
bounding box is that it might contain
|
|
496
|
-
multiple labels.
|
|
497
|
-
"""
|
|
498
|
-
|
|
499
|
-
boxes_shape = boxes.shape
|
|
500
|
-
if not boxes_shape:
|
|
501
|
-
return
|
|
502
|
-
if len(boxes_shape) != 2 or boxes_shape[1] != 4:
|
|
503
|
-
# print('Input must be of size [N, 4], but is ' + str(boxes_shape))
|
|
504
|
-
return # no object detection on this image, return
|
|
505
|
-
for i in range(boxes_shape[0]):
|
|
506
|
-
if display_strs:
|
|
507
|
-
display_str_list = display_strs[i]
|
|
508
|
-
draw_bounding_box_on_image(image,
|
|
509
|
-
boxes[i, 0], boxes[i, 1], boxes[i, 2], boxes[i, 3],
|
|
510
|
-
classes[i],
|
|
511
|
-
thickness=thickness, expansion=expansion,
|
|
512
|
-
display_str_list=display_str_list,
|
|
513
|
-
colormap=colormap,
|
|
514
|
-
textalign=textalign,
|
|
515
|
-
label_font_size=label_font_size)
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
def draw_bounding_box_on_image(image,
|
|
519
|
-
ymin,
|
|
520
|
-
xmin,
|
|
521
|
-
ymax,
|
|
522
|
-
xmax,
|
|
523
|
-
clss=None,
|
|
524
|
-
thickness=DEFAULT_BOX_THICKNESS,
|
|
525
|
-
expansion=0,
|
|
526
|
-
display_str_list=(),
|
|
527
|
-
use_normalized_coordinates=True,
|
|
528
|
-
label_font_size=DEFAULT_LABEL_FONT_SIZE,
|
|
529
|
-
colormap=DEFAULT_COLORS,
|
|
530
|
-
textalign=TEXTALIGN_LEFT):
|
|
531
|
-
"""
|
|
532
|
-
Adds a bounding box to an image.
|
|
533
|
-
|
|
534
|
-
Bounding box coordinates can be specified in either absolute (pixel) or
|
|
535
|
-
normalized coordinates by setting the use_normalized_coordinates argument.
|
|
536
|
-
|
|
537
|
-
Each string in display_str_list is displayed on a separate line above the
|
|
538
|
-
bounding box in black text on a rectangle filled with the input 'color'.
|
|
539
|
-
If the top of the bounding box extends to the edge of the image, the strings
|
|
540
|
-
are displayed below the bounding box.
|
|
541
|
-
|
|
542
|
-
Args:
|
|
543
|
-
image: a PIL.Image object.
|
|
544
|
-
ymin: ymin of bounding box - upper left.
|
|
545
|
-
xmin: xmin of bounding box.
|
|
546
|
-
ymax: ymax of bounding box.
|
|
547
|
-
xmax: xmax of bounding box.
|
|
548
|
-
clss: str, the class of the object in this bounding box - will be cast to an int.
|
|
549
|
-
thickness: line thickness. Default value is 4.
|
|
550
|
-
expansion: number of pixels to expand bounding boxes on each side. Default is 0.
|
|
551
|
-
display_str_list: list of strings to display in box
|
|
552
|
-
(each to be shown on its own line).
|
|
553
|
-
use_normalized_coordinates: If True (default), treat coordinates
|
|
554
|
-
ymin, xmin, ymax, xmax as relative to the image. Otherwise treat
|
|
555
|
-
coordinates as absolute.
|
|
556
|
-
label_font_size: font size to attempt to load arial.ttf with
|
|
557
|
-
"""
|
|
558
|
-
|
|
559
|
-
if clss is None:
|
|
560
|
-
color = colormap[1]
|
|
561
|
-
else:
|
|
562
|
-
color = colormap[int(clss) % len(colormap)]
|
|
563
|
-
|
|
564
|
-
draw = ImageDraw.Draw(image)
|
|
565
|
-
im_width, im_height = image.size
|
|
566
|
-
if use_normalized_coordinates:
|
|
567
|
-
(left, right, top, bottom) = (xmin * im_width, xmax * im_width,
|
|
568
|
-
ymin * im_height, ymax * im_height)
|
|
569
|
-
else:
|
|
570
|
-
(left, right, top, bottom) = (xmin, xmax, ymin, ymax)
|
|
571
|
-
|
|
572
|
-
if expansion > 0:
|
|
573
|
-
|
|
574
|
-
left -= expansion
|
|
575
|
-
right += expansion
|
|
576
|
-
top -= expansion
|
|
577
|
-
bottom += expansion
|
|
578
|
-
|
|
579
|
-
# Deliberately trimming to the width of the image only in the case where
|
|
580
|
-
# box expansion is turned on. There's not an obvious correct behavior here,
|
|
581
|
-
# but the thinking is that if the caller provided an out-of-range bounding
|
|
582
|
-
# box, they meant to do that, but at least in the eyes of the person writing
|
|
583
|
-
# this comment, if you expand a box for visualization reasons, you don't want
|
|
584
|
-
# to end up with part of a box.
|
|
585
|
-
#
|
|
586
|
-
# A slightly more sophisticated might check whether it was in fact the expansion
|
|
587
|
-
# that made this box larger than the image, but this is the case 99.999% of the time
|
|
588
|
-
# here, so that doesn't seem necessary.
|
|
589
|
-
left = max(left,0); right = max(right,0)
|
|
590
|
-
top = max(top,0); bottom = max(bottom,0)
|
|
591
|
-
|
|
592
|
-
left = min(left,im_width-1); right = min(right,im_width-1)
|
|
593
|
-
top = min(top,im_height-1); bottom = min(bottom,im_height-1)
|
|
594
|
-
|
|
595
|
-
# ...if we need to expand boxes
|
|
596
|
-
|
|
597
|
-
draw.line([(left, top), (left, bottom), (right, bottom),
|
|
598
|
-
(right, top), (left, top)], width=thickness, fill=color)
|
|
599
|
-
|
|
600
|
-
try:
|
|
601
|
-
font = ImageFont.truetype('arial.ttf', label_font_size)
|
|
602
|
-
except IOError:
|
|
603
|
-
font = ImageFont.load_default()
|
|
604
|
-
|
|
605
|
-
def get_text_size(font,s):
|
|
606
|
-
|
|
607
|
-
# This is what we did w/Pillow 9
|
|
608
|
-
# w,h = font.getsize(s)
|
|
609
|
-
|
|
610
|
-
# I would *think* this would be the equivalent for Pillow 10
|
|
611
|
-
# l,t,r,b = font.getbbox(s); w = r-l; h=b-t
|
|
612
|
-
|
|
613
|
-
# ...but this actually produces the most similar results to Pillow 9
|
|
614
|
-
# l,t,r,b = font.getbbox(s); w = r; h=b
|
|
615
|
-
|
|
616
|
-
try:
|
|
617
|
-
l,t,r,b = font.getbbox(s); w = r; h=b
|
|
618
|
-
except Exception:
|
|
619
|
-
w,h = font.getsize(s)
|
|
620
|
-
|
|
621
|
-
return w,h
|
|
622
|
-
|
|
623
|
-
# If the total height of the display strings added to the top of the bounding
|
|
624
|
-
# box exceeds the top of the image, stack the strings below the bounding box
|
|
625
|
-
# instead of above.
|
|
626
|
-
display_str_heights = [get_text_size(font,ds)[1] for ds in display_str_list]
|
|
627
|
-
|
|
628
|
-
# Each display_str has a top and bottom margin of 0.05x.
|
|
629
|
-
total_display_str_height = (1 + 2 * 0.05) * sum(display_str_heights)
|
|
630
|
-
|
|
631
|
-
if top > total_display_str_height:
|
|
632
|
-
text_bottom = top
|
|
633
|
-
else:
|
|
634
|
-
text_bottom = bottom + total_display_str_height
|
|
635
|
-
|
|
636
|
-
# Reverse list and print from bottom to top.
|
|
637
|
-
for display_str in display_str_list[::-1]:
|
|
638
|
-
|
|
639
|
-
# Skip empty strings
|
|
640
|
-
if len(display_str) == 0:
|
|
641
|
-
continue
|
|
642
|
-
|
|
643
|
-
text_width, text_height = get_text_size(font,display_str)
|
|
644
|
-
|
|
645
|
-
text_left = left
|
|
646
|
-
|
|
647
|
-
if textalign == TEXTALIGN_RIGHT:
|
|
648
|
-
text_left = right - text_width
|
|
649
|
-
|
|
650
|
-
margin = np.ceil(0.05 * text_height)
|
|
651
|
-
|
|
652
|
-
draw.rectangle(
|
|
653
|
-
[(text_left, text_bottom - text_height - 2 * margin), (text_left + text_width,
|
|
654
|
-
text_bottom)],
|
|
655
|
-
fill=color)
|
|
656
|
-
|
|
657
|
-
draw.text(
|
|
658
|
-
(text_left + margin, text_bottom - text_height - margin),
|
|
659
|
-
display_str,
|
|
660
|
-
fill='black',
|
|
661
|
-
font=font)
|
|
662
|
-
|
|
663
|
-
text_bottom -= (text_height + 2 * margin)
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
def render_iMerit_boxes(boxes, classes, image,
|
|
667
|
-
label_map=annotation_constants.annotation_bbox_category_id_to_name):
|
|
668
|
-
"""
|
|
669
|
-
Renders bounding boxes and their category labels on a PIL image.
|
|
670
|
-
|
|
671
|
-
Args:
|
|
672
|
-
boxes: bounding box annotations from iMerit, format is:
|
|
673
|
-
[x_rel, y_rel, w_rel, h_rel] (rel = relative coords)
|
|
674
|
-
classes: the class IDs of the predicted class of each box/object
|
|
675
|
-
image: PIL.Image object to annotate on
|
|
676
|
-
label_map: optional dict mapping classes to a string for display
|
|
677
|
-
|
|
678
|
-
Returns:
|
|
679
|
-
image will be altered in place
|
|
680
|
-
"""
|
|
681
|
-
|
|
682
|
-
display_boxes = []
|
|
683
|
-
|
|
684
|
-
# list of lists, one list of strings for each bounding box (to accommodate multiple labels)
|
|
685
|
-
display_strs = []
|
|
686
|
-
|
|
687
|
-
for box, clss in zip(boxes, classes):
|
|
688
|
-
if len(box) == 0:
|
|
689
|
-
assert clss == 5
|
|
690
|
-
continue
|
|
691
|
-
x_rel, y_rel, w_rel, h_rel = box
|
|
692
|
-
ymin, xmin = y_rel, x_rel
|
|
693
|
-
ymax = ymin + h_rel
|
|
694
|
-
xmax = xmin + w_rel
|
|
695
|
-
|
|
696
|
-
display_boxes.append([ymin, xmin, ymax, xmax])
|
|
697
|
-
|
|
698
|
-
if label_map:
|
|
699
|
-
clss = label_map[int(clss)]
|
|
700
|
-
display_strs.append([clss])
|
|
701
|
-
|
|
702
|
-
display_boxes = np.array(display_boxes)
|
|
703
|
-
draw_bounding_boxes_on_image(image, display_boxes, classes, display_strs=display_strs)
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
def render_megadb_bounding_boxes(boxes_info, image):
|
|
707
|
-
"""
|
|
708
|
-
Args:
|
|
709
|
-
boxes_info: list of dict, each dict represents a single detection
|
|
710
|
-
{
|
|
711
|
-
"category": "animal",
|
|
712
|
-
"bbox": [
|
|
713
|
-
0.739,
|
|
714
|
-
0.448,
|
|
715
|
-
0.187,
|
|
716
|
-
0.198
|
|
717
|
-
]
|
|
718
|
-
}
|
|
719
|
-
where bbox coordinates are normalized [x_min, y_min, width, height]
|
|
720
|
-
image: PIL.Image.Image, opened image
|
|
721
|
-
"""
|
|
722
|
-
|
|
723
|
-
display_boxes = []
|
|
724
|
-
display_strs = []
|
|
725
|
-
classes = [] # ints, for selecting colors
|
|
726
|
-
|
|
727
|
-
for b in boxes_info:
|
|
728
|
-
x_min, y_min, w_rel, h_rel = b['bbox']
|
|
729
|
-
y_max = y_min + h_rel
|
|
730
|
-
x_max = x_min + w_rel
|
|
731
|
-
display_boxes.append([y_min, x_min, y_max, x_max])
|
|
732
|
-
display_strs.append([b['category']])
|
|
733
|
-
classes.append(annotation_constants.detector_bbox_category_name_to_id[b['category']])
|
|
734
|
-
|
|
735
|
-
display_boxes = np.array(display_boxes)
|
|
736
|
-
draw_bounding_boxes_on_image(image, display_boxes, classes, display_strs=display_strs)
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
def render_db_bounding_boxes(boxes, classes, image, original_size=None,
|
|
740
|
-
label_map=None, thickness=DEFAULT_BOX_THICKNESS, expansion=0):
|
|
741
|
-
"""
|
|
742
|
-
Render bounding boxes (with class labels) on [image]. This is a wrapper for
|
|
743
|
-
draw_bounding_boxes_on_image, allowing the caller to operate on a resized image
|
|
744
|
-
by providing the original size of the image; bboxes will be scaled accordingly.
|
|
745
|
-
|
|
746
|
-
This function assumes that bounding boxes are in the COCO camera traps format,
|
|
747
|
-
with absolute coordinates.
|
|
748
|
-
"""
|
|
749
|
-
|
|
750
|
-
display_boxes = []
|
|
751
|
-
display_strs = []
|
|
752
|
-
|
|
753
|
-
if original_size is not None:
|
|
754
|
-
image_size = original_size
|
|
755
|
-
else:
|
|
756
|
-
image_size = image.size
|
|
757
|
-
|
|
758
|
-
img_width, img_height = image_size
|
|
759
|
-
|
|
760
|
-
for box, clss in zip(boxes, classes):
|
|
761
|
-
|
|
762
|
-
x_min_abs, y_min_abs, width_abs, height_abs = box[0:4]
|
|
763
|
-
|
|
764
|
-
ymin = y_min_abs / img_height
|
|
765
|
-
ymax = ymin + height_abs / img_height
|
|
766
|
-
|
|
767
|
-
xmin = x_min_abs / img_width
|
|
768
|
-
xmax = xmin + width_abs / img_width
|
|
769
|
-
|
|
770
|
-
display_boxes.append([ymin, xmin, ymax, xmax])
|
|
771
|
-
|
|
772
|
-
if label_map:
|
|
773
|
-
clss = label_map[int(clss)]
|
|
774
|
-
|
|
775
|
-
# need to be a string here because PIL needs to iterate through chars
|
|
776
|
-
display_strs.append([str(clss)])
|
|
777
|
-
|
|
778
|
-
display_boxes = np.array(display_boxes)
|
|
779
|
-
draw_bounding_boxes_on_image(image, display_boxes, classes, display_strs=display_strs,
|
|
780
|
-
thickness=thickness, expansion=expansion)
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
def draw_bounding_boxes_on_file(input_file, output_file, detections, confidence_threshold=0.0,
|
|
784
|
-
detector_label_map=DEFAULT_DETECTOR_LABEL_MAP,
|
|
785
|
-
thickness=DEFAULT_BOX_THICKNESS, expansion=0,
|
|
786
|
-
colormap=DEFAULT_COLORS,
|
|
787
|
-
custom_strings=None):
|
|
788
|
-
"""
|
|
789
|
-
Render detection bounding boxes on an image loaded from file, writing the results to a
|
|
790
|
-
new image file.
|
|
791
|
-
|
|
792
|
-
"detections" is in the API results format:
|
|
793
|
-
|
|
794
|
-
[{"category": "2","conf": 0.996,"bbox": [0.0,0.2762,0.1234,0.2458]}]
|
|
795
|
-
|
|
796
|
-
...where the bbox is:
|
|
797
|
-
|
|
798
|
-
[x_min, y_min, width_of_box, height_of_box]
|
|
799
|
-
|
|
800
|
-
Normalized, with the origin at the upper-left.
|
|
801
|
-
|
|
802
|
-
detector_label_map is a dict mapping category IDs to strings.
|
|
803
|
-
|
|
804
|
-
custom_strings: optional set of strings to append to detection labels, should have the
|
|
805
|
-
same length as [detections]. Appended before classification labels, if classification
|
|
806
|
-
data is provided.
|
|
807
|
-
"""
|
|
808
|
-
|
|
809
|
-
image = open_image(input_file)
|
|
810
|
-
|
|
811
|
-
render_detection_bounding_boxes(
|
|
812
|
-
detections, image, label_map=detector_label_map,
|
|
813
|
-
confidence_threshold=confidence_threshold,
|
|
814
|
-
thickness=thickness,expansion=expansion,colormap=colormap,
|
|
815
|
-
custom_strings=custom_strings)
|
|
816
|
-
|
|
817
|
-
image.save(output_file)
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
def draw_db_boxes_on_file(input_file, output_file, boxes, classes=None,
|
|
821
|
-
label_map=None, thickness=DEFAULT_BOX_THICKNESS, expansion=0):
|
|
822
|
-
"""
|
|
823
|
-
Render COCO bounding boxes (in absolute coordinates) on an image loaded from file, writing the
|
|
824
|
-
results to a new image file.
|
|
825
|
-
|
|
826
|
-
classes is a list of integer category IDs.
|
|
827
|
-
|
|
828
|
-
detector_label_map is a dict mapping category IDs to strings.
|
|
829
|
-
"""
|
|
830
|
-
|
|
831
|
-
image = open_image(input_file)
|
|
832
|
-
|
|
833
|
-
if classes is None:
|
|
834
|
-
classes = [0] * len(boxes)
|
|
835
|
-
|
|
836
|
-
render_db_bounding_boxes(boxes, classes, image, original_size=None,
|
|
837
|
-
label_map=label_map, thickness=thickness, expansion=expansion)
|
|
838
|
-
|
|
839
|
-
image.save(output_file)
|
|
840
|
-
|
|
841
|
-
|