megadetector 5.0.10__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.

Files changed (226) hide show
  1. {megadetector-5.0.10.dist-info → megadetector-5.0.11.dist-info}/LICENSE +0 -0
  2. {megadetector-5.0.10.dist-info → megadetector-5.0.11.dist-info}/METADATA +12 -11
  3. megadetector-5.0.11.dist-info/RECORD +5 -0
  4. megadetector-5.0.11.dist-info/top_level.txt +1 -0
  5. api/__init__.py +0 -0
  6. api/batch_processing/__init__.py +0 -0
  7. api/batch_processing/api_core/__init__.py +0 -0
  8. api/batch_processing/api_core/batch_service/__init__.py +0 -0
  9. api/batch_processing/api_core/batch_service/score.py +0 -439
  10. api/batch_processing/api_core/server.py +0 -294
  11. api/batch_processing/api_core/server_api_config.py +0 -98
  12. api/batch_processing/api_core/server_app_config.py +0 -55
  13. api/batch_processing/api_core/server_batch_job_manager.py +0 -220
  14. api/batch_processing/api_core/server_job_status_table.py +0 -152
  15. api/batch_processing/api_core/server_orchestration.py +0 -360
  16. api/batch_processing/api_core/server_utils.py +0 -92
  17. api/batch_processing/api_core_support/__init__.py +0 -0
  18. api/batch_processing/api_core_support/aggregate_results_manually.py +0 -46
  19. api/batch_processing/api_support/__init__.py +0 -0
  20. api/batch_processing/api_support/summarize_daily_activity.py +0 -152
  21. api/batch_processing/data_preparation/__init__.py +0 -0
  22. api/batch_processing/data_preparation/manage_local_batch.py +0 -2391
  23. api/batch_processing/data_preparation/manage_video_batch.py +0 -327
  24. api/batch_processing/integration/digiKam/setup.py +0 -6
  25. api/batch_processing/integration/digiKam/xmp_integration.py +0 -465
  26. api/batch_processing/integration/eMammal/test_scripts/config_template.py +0 -5
  27. api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +0 -126
  28. api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +0 -55
  29. api/batch_processing/postprocessing/__init__.py +0 -0
  30. api/batch_processing/postprocessing/add_max_conf.py +0 -64
  31. api/batch_processing/postprocessing/categorize_detections_by_size.py +0 -163
  32. api/batch_processing/postprocessing/combine_api_outputs.py +0 -249
  33. api/batch_processing/postprocessing/compare_batch_results.py +0 -958
  34. api/batch_processing/postprocessing/convert_output_format.py +0 -397
  35. api/batch_processing/postprocessing/load_api_results.py +0 -195
  36. api/batch_processing/postprocessing/md_to_coco.py +0 -310
  37. api/batch_processing/postprocessing/md_to_labelme.py +0 -330
  38. api/batch_processing/postprocessing/merge_detections.py +0 -401
  39. api/batch_processing/postprocessing/postprocess_batch_results.py +0 -1904
  40. api/batch_processing/postprocessing/remap_detection_categories.py +0 -170
  41. api/batch_processing/postprocessing/render_detection_confusion_matrix.py +0 -661
  42. api/batch_processing/postprocessing/repeat_detection_elimination/find_repeat_detections.py +0 -211
  43. api/batch_processing/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +0 -82
  44. api/batch_processing/postprocessing/repeat_detection_elimination/repeat_detections_core.py +0 -1631
  45. api/batch_processing/postprocessing/separate_detections_into_folders.py +0 -731
  46. api/batch_processing/postprocessing/subset_json_detector_output.py +0 -696
  47. api/batch_processing/postprocessing/top_folders_to_bottom.py +0 -223
  48. api/synchronous/__init__.py +0 -0
  49. api/synchronous/api_core/animal_detection_api/__init__.py +0 -0
  50. api/synchronous/api_core/animal_detection_api/api_backend.py +0 -152
  51. api/synchronous/api_core/animal_detection_api/api_frontend.py +0 -266
  52. api/synchronous/api_core/animal_detection_api/config.py +0 -35
  53. api/synchronous/api_core/animal_detection_api/data_management/annotations/annotation_constants.py +0 -47
  54. api/synchronous/api_core/animal_detection_api/detection/detector_training/copy_checkpoints.py +0 -43
  55. api/synchronous/api_core/animal_detection_api/detection/detector_training/model_main_tf2.py +0 -114
  56. api/synchronous/api_core/animal_detection_api/detection/process_video.py +0 -543
  57. api/synchronous/api_core/animal_detection_api/detection/pytorch_detector.py +0 -304
  58. api/synchronous/api_core/animal_detection_api/detection/run_detector.py +0 -627
  59. api/synchronous/api_core/animal_detection_api/detection/run_detector_batch.py +0 -1029
  60. api/synchronous/api_core/animal_detection_api/detection/run_inference_with_yolov5_val.py +0 -581
  61. api/synchronous/api_core/animal_detection_api/detection/run_tiled_inference.py +0 -754
  62. api/synchronous/api_core/animal_detection_api/detection/tf_detector.py +0 -165
  63. api/synchronous/api_core/animal_detection_api/detection/video_utils.py +0 -495
  64. api/synchronous/api_core/animal_detection_api/md_utils/azure_utils.py +0 -174
  65. api/synchronous/api_core/animal_detection_api/md_utils/ct_utils.py +0 -262
  66. api/synchronous/api_core/animal_detection_api/md_utils/directory_listing.py +0 -251
  67. api/synchronous/api_core/animal_detection_api/md_utils/matlab_porting_tools.py +0 -97
  68. api/synchronous/api_core/animal_detection_api/md_utils/path_utils.py +0 -416
  69. api/synchronous/api_core/animal_detection_api/md_utils/process_utils.py +0 -110
  70. api/synchronous/api_core/animal_detection_api/md_utils/sas_blob_utils.py +0 -509
  71. api/synchronous/api_core/animal_detection_api/md_utils/string_utils.py +0 -59
  72. api/synchronous/api_core/animal_detection_api/md_utils/url_utils.py +0 -144
  73. api/synchronous/api_core/animal_detection_api/md_utils/write_html_image_list.py +0 -226
  74. api/synchronous/api_core/animal_detection_api/md_visualization/visualization_utils.py +0 -841
  75. api/synchronous/api_core/tests/__init__.py +0 -0
  76. api/synchronous/api_core/tests/load_test.py +0 -110
  77. classification/__init__.py +0 -0
  78. classification/aggregate_classifier_probs.py +0 -108
  79. classification/analyze_failed_images.py +0 -227
  80. classification/cache_batchapi_outputs.py +0 -198
  81. classification/create_classification_dataset.py +0 -627
  82. classification/crop_detections.py +0 -516
  83. classification/csv_to_json.py +0 -226
  84. classification/detect_and_crop.py +0 -855
  85. classification/efficientnet/__init__.py +0 -9
  86. classification/efficientnet/model.py +0 -415
  87. classification/efficientnet/utils.py +0 -610
  88. classification/evaluate_model.py +0 -520
  89. classification/identify_mislabeled_candidates.py +0 -152
  90. classification/json_to_azcopy_list.py +0 -63
  91. classification/json_validator.py +0 -695
  92. classification/map_classification_categories.py +0 -276
  93. classification/merge_classification_detection_output.py +0 -506
  94. classification/prepare_classification_script.py +0 -194
  95. classification/prepare_classification_script_mc.py +0 -228
  96. classification/run_classifier.py +0 -286
  97. classification/save_mislabeled.py +0 -110
  98. classification/train_classifier.py +0 -825
  99. classification/train_classifier_tf.py +0 -724
  100. classification/train_utils.py +0 -322
  101. data_management/__init__.py +0 -0
  102. data_management/annotations/__init__.py +0 -0
  103. data_management/annotations/annotation_constants.py +0 -34
  104. data_management/camtrap_dp_to_coco.py +0 -238
  105. data_management/cct_json_utils.py +0 -395
  106. data_management/cct_to_md.py +0 -176
  107. data_management/cct_to_wi.py +0 -289
  108. data_management/coco_to_labelme.py +0 -272
  109. data_management/coco_to_yolo.py +0 -662
  110. data_management/databases/__init__.py +0 -0
  111. data_management/databases/add_width_and_height_to_db.py +0 -33
  112. data_management/databases/combine_coco_camera_traps_files.py +0 -206
  113. data_management/databases/integrity_check_json_db.py +0 -477
  114. data_management/databases/subset_json_db.py +0 -115
  115. data_management/generate_crops_from_cct.py +0 -149
  116. data_management/get_image_sizes.py +0 -188
  117. data_management/importers/add_nacti_sizes.py +0 -52
  118. data_management/importers/add_timestamps_to_icct.py +0 -79
  119. data_management/importers/animl_results_to_md_results.py +0 -158
  120. data_management/importers/auckland_doc_test_to_json.py +0 -372
  121. data_management/importers/auckland_doc_to_json.py +0 -200
  122. data_management/importers/awc_to_json.py +0 -189
  123. data_management/importers/bellevue_to_json.py +0 -273
  124. data_management/importers/cacophony-thermal-importer.py +0 -796
  125. data_management/importers/carrizo_shrubfree_2018.py +0 -268
  126. data_management/importers/carrizo_trail_cam_2017.py +0 -287
  127. data_management/importers/cct_field_adjustments.py +0 -57
  128. data_management/importers/channel_islands_to_cct.py +0 -913
  129. data_management/importers/eMammal/copy_and_unzip_emammal.py +0 -180
  130. data_management/importers/eMammal/eMammal_helpers.py +0 -249
  131. data_management/importers/eMammal/make_eMammal_json.py +0 -223
  132. data_management/importers/ena24_to_json.py +0 -275
  133. data_management/importers/filenames_to_json.py +0 -385
  134. data_management/importers/helena_to_cct.py +0 -282
  135. data_management/importers/idaho-camera-traps.py +0 -1407
  136. data_management/importers/idfg_iwildcam_lila_prep.py +0 -294
  137. data_management/importers/jb_csv_to_json.py +0 -150
  138. data_management/importers/mcgill_to_json.py +0 -250
  139. data_management/importers/missouri_to_json.py +0 -489
  140. data_management/importers/nacti_fieldname_adjustments.py +0 -79
  141. data_management/importers/noaa_seals_2019.py +0 -181
  142. data_management/importers/pc_to_json.py +0 -365
  143. data_management/importers/plot_wni_giraffes.py +0 -123
  144. data_management/importers/prepare-noaa-fish-data-for-lila.py +0 -359
  145. data_management/importers/prepare_zsl_imerit.py +0 -131
  146. data_management/importers/rspb_to_json.py +0 -356
  147. data_management/importers/save_the_elephants_survey_A.py +0 -320
  148. data_management/importers/save_the_elephants_survey_B.py +0 -332
  149. data_management/importers/snapshot_safari_importer.py +0 -758
  150. data_management/importers/snapshot_safari_importer_reprise.py +0 -665
  151. data_management/importers/snapshot_serengeti_lila.py +0 -1067
  152. data_management/importers/snapshotserengeti/make_full_SS_json.py +0 -150
  153. data_management/importers/snapshotserengeti/make_per_season_SS_json.py +0 -153
  154. data_management/importers/sulross_get_exif.py +0 -65
  155. data_management/importers/timelapse_csv_set_to_json.py +0 -490
  156. data_management/importers/ubc_to_json.py +0 -399
  157. data_management/importers/umn_to_json.py +0 -507
  158. data_management/importers/wellington_to_json.py +0 -263
  159. data_management/importers/wi_to_json.py +0 -441
  160. data_management/importers/zamba_results_to_md_results.py +0 -181
  161. data_management/labelme_to_coco.py +0 -548
  162. data_management/labelme_to_yolo.py +0 -272
  163. data_management/lila/__init__.py +0 -0
  164. data_management/lila/add_locations_to_island_camera_traps.py +0 -97
  165. data_management/lila/add_locations_to_nacti.py +0 -147
  166. data_management/lila/create_lila_blank_set.py +0 -557
  167. data_management/lila/create_lila_test_set.py +0 -151
  168. data_management/lila/create_links_to_md_results_files.py +0 -106
  169. data_management/lila/download_lila_subset.py +0 -177
  170. data_management/lila/generate_lila_per_image_labels.py +0 -515
  171. data_management/lila/get_lila_annotation_counts.py +0 -170
  172. data_management/lila/get_lila_image_counts.py +0 -111
  173. data_management/lila/lila_common.py +0 -300
  174. data_management/lila/test_lila_metadata_urls.py +0 -132
  175. data_management/ocr_tools.py +0 -874
  176. data_management/read_exif.py +0 -681
  177. data_management/remap_coco_categories.py +0 -84
  178. data_management/remove_exif.py +0 -66
  179. data_management/resize_coco_dataset.py +0 -189
  180. data_management/wi_download_csv_to_coco.py +0 -246
  181. data_management/yolo_output_to_md_output.py +0 -441
  182. data_management/yolo_to_coco.py +0 -676
  183. detection/__init__.py +0 -0
  184. detection/detector_training/__init__.py +0 -0
  185. detection/detector_training/model_main_tf2.py +0 -114
  186. detection/process_video.py +0 -703
  187. detection/pytorch_detector.py +0 -337
  188. detection/run_detector.py +0 -779
  189. detection/run_detector_batch.py +0 -1219
  190. detection/run_inference_with_yolov5_val.py +0 -917
  191. detection/run_tiled_inference.py +0 -935
  192. detection/tf_detector.py +0 -188
  193. detection/video_utils.py +0 -606
  194. docs/source/conf.py +0 -43
  195. md_utils/__init__.py +0 -0
  196. md_utils/azure_utils.py +0 -174
  197. md_utils/ct_utils.py +0 -612
  198. md_utils/directory_listing.py +0 -246
  199. md_utils/md_tests.py +0 -968
  200. md_utils/path_utils.py +0 -1044
  201. md_utils/process_utils.py +0 -157
  202. md_utils/sas_blob_utils.py +0 -509
  203. md_utils/split_locations_into_train_val.py +0 -228
  204. md_utils/string_utils.py +0 -92
  205. md_utils/url_utils.py +0 -323
  206. md_utils/write_html_image_list.py +0 -225
  207. md_visualization/__init__.py +0 -0
  208. md_visualization/plot_utils.py +0 -293
  209. md_visualization/render_images_with_thumbnails.py +0 -275
  210. md_visualization/visualization_utils.py +0 -1537
  211. md_visualization/visualize_db.py +0 -551
  212. md_visualization/visualize_detector_output.py +0 -406
  213. megadetector-5.0.10.dist-info/RECORD +0 -224
  214. megadetector-5.0.10.dist-info/top_level.txt +0 -8
  215. taxonomy_mapping/__init__.py +0 -0
  216. taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +0 -491
  217. taxonomy_mapping/map_new_lila_datasets.py +0 -154
  218. taxonomy_mapping/prepare_lila_taxonomy_release.py +0 -142
  219. taxonomy_mapping/preview_lila_taxonomy.py +0 -591
  220. taxonomy_mapping/retrieve_sample_image.py +0 -71
  221. taxonomy_mapping/simple_image_download.py +0 -218
  222. taxonomy_mapping/species_lookup.py +0 -834
  223. taxonomy_mapping/taxonomy_csv_checker.py +0 -159
  224. taxonomy_mapping/taxonomy_graph.py +0 -346
  225. taxonomy_mapping/validate_lila_category_mappings.py +0 -83
  226. {megadetector-5.0.10.dist-info → megadetector-5.0.11.dist-info}/WHEEL +0 -0
@@ -1,1537 +0,0 @@
1
- """
2
-
3
- visualization_utils.py
4
-
5
- Rendering functions shared across visualization scripts
6
-
7
- """
8
-
9
- #%% Constants and imports
10
-
11
- import time
12
- import numpy as np
13
- import requests
14
- import os
15
- import cv2
16
-
17
- from io import BytesIO
18
- from PIL import Image, ImageFile, ImageFont, ImageDraw
19
- from multiprocessing.pool import ThreadPool
20
- from multiprocessing.pool import Pool
21
- from tqdm import tqdm
22
- from functools import partial
23
-
24
- from md_utils.path_utils import find_images
25
-
26
- from data_management.annotations import annotation_constants
27
- from data_management.annotations.annotation_constants import (
28
- detector_bbox_category_id_to_name)
29
-
30
- ImageFile.LOAD_TRUNCATED_IMAGES = True
31
-
32
- # Maps EXIF standard rotation identifiers to degrees. The value "1" indicates no
33
- # rotation; this will be ignored. The values 2, 4, 5, and 7 are mirrored rotations,
34
- # which are not supported (we'll assert() on this when we apply rotations).
35
- EXIF_IMAGE_NO_ROTATION = 1
36
- EXIF_IMAGE_ROTATIONS = {
37
- 3: 180,
38
- 6: 270,
39
- 8: 90
40
- }
41
-
42
- TEXTALIGN_LEFT = 0
43
- TEXTALIGN_RIGHT = 1
44
-
45
- # Convert category ID from int to str
46
- DEFAULT_DETECTOR_LABEL_MAP = {
47
- str(k): v for k, v in detector_bbox_category_id_to_name.items()
48
- }
49
-
50
- # Constants controlling retry behavior when fetching images from URLs
51
- n_retries = 10
52
- retry_sleep_time = 0.01
53
-
54
- # If we try to open an image from a URL, and we encounter any error in this list,
55
- # we'll retry, otherwise it's just an error.
56
- error_names_for_retry = ['ConnectionError']
57
-
58
- DEFAULT_BOX_THICKNESS = 4
59
- DEFAULT_LABEL_FONT_SIZE = 16
60
-
61
- # Default color map for mapping integer category IDs to colors when rendering bounding
62
- # boxes
63
- DEFAULT_COLORS = [
64
- 'AliceBlue', 'Red', 'RoyalBlue', 'Gold', 'Chartreuse', 'Aqua', 'Azure',
65
- 'Beige', 'Bisque', 'BlanchedAlmond', 'BlueViolet', 'BurlyWood', 'CadetBlue',
66
- 'AntiqueWhite', 'Chocolate', 'Coral', 'CornflowerBlue', 'Cornsilk', 'Crimson',
67
- 'Cyan', 'DarkCyan', 'DarkGoldenRod', 'DarkGrey', 'DarkKhaki', 'DarkOrange',
68
- 'DarkOrchid', 'DarkSalmon', 'DarkSeaGreen', 'DarkTurquoise', 'DarkViolet',
69
- 'DeepPink', 'DeepSkyBlue', 'DodgerBlue', 'FireBrick', 'FloralWhite',
70
- 'ForestGreen', 'Fuchsia', 'Gainsboro', 'GhostWhite', 'GoldenRod',
71
- 'Salmon', 'Tan', 'HoneyDew', 'HotPink', 'IndianRed', 'Ivory', 'Khaki',
72
- 'Lavender', 'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue',
73
- 'LightCoral', 'LightCyan', 'LightGoldenRodYellow', 'LightGray', 'LightGrey',
74
- 'LightGreen', 'LightPink', 'LightSalmon', 'LightSeaGreen', 'LightSkyBlue',
75
- 'LightSlateGray', 'LightSlateGrey', 'LightSteelBlue', 'LightYellow', 'Lime',
76
- 'LimeGreen', 'Linen', 'Magenta', 'MediumAquaMarine', 'MediumOrchid',
77
- 'MediumPurple', 'MediumSeaGreen', 'MediumSlateBlue', 'MediumSpringGreen',
78
- 'MediumTurquoise', 'MediumVioletRed', 'MintCream', 'MistyRose', 'Moccasin',
79
- 'NavajoWhite', 'OldLace', 'Olive', 'OliveDrab', 'Orange', 'OrangeRed',
80
- 'Orchid', 'PaleGoldenRod', 'PaleGreen', 'PaleTurquoise', 'PaleVioletRed',
81
- 'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', 'Plum', 'PowderBlue', 'Purple',
82
- 'RosyBrown', 'Aquamarine', 'SaddleBrown', 'Green', 'SandyBrown',
83
- 'SeaGreen', 'SeaShell', 'Sienna', 'Silver', 'SkyBlue', 'SlateBlue',
84
- 'SlateGray', 'SlateGrey', 'Snow', 'SpringGreen', 'SteelBlue', 'GreenYellow',
85
- 'Teal', 'Thistle', 'Tomato', 'Turquoise', 'Violet', 'Wheat', 'White',
86
- 'WhiteSmoke', 'Yellow', 'YellowGreen'
87
- ]
88
-
89
-
90
- #%% Functions
91
-
92
- def open_image(input_file, ignore_exif_rotation=False):
93
- """
94
- Opens an image in binary format using PIL.Image and converts to RGB mode.
95
-
96
- Supports local files or URLs.
97
-
98
- This operation is lazy; image will not be actually loaded until the first
99
- operation that needs to load it (for example, resizing), so file opening
100
- errors can show up later. load_image() is the non-lazy version of this function.
101
-
102
- Args:
103
- input_file (str or BytesIO): can be a path to an image file (anything
104
- that PIL can open), a URL, or an image as a stream of bytes
105
- ignore_exif_rotation (bool, optional): don't rotate the loaded pixels,
106
- even if we are loading a JPEG and that JPEG says it should be rotated
107
-
108
- Returns:
109
- PIL.Image.Image: A PIL Image object in RGB mode
110
- """
111
-
112
- if (isinstance(input_file, str)
113
- and input_file.startswith(('http://', 'https://'))):
114
- try:
115
- response = requests.get(input_file)
116
- except Exception as e:
117
- print(f'Error retrieving image {input_file}: {e}')
118
- success = False
119
- if e.__class__.__name__ in error_names_for_retry:
120
- for i_retry in range(0,n_retries):
121
- try:
122
- time.sleep(retry_sleep_time)
123
- response = requests.get(input_file)
124
- except Exception as e:
125
- print(f'Error retrieving image {input_file} on retry {i_retry}: {e}')
126
- continue
127
- print('Succeeded on retry {}'.format(i_retry))
128
- success = True
129
- break
130
- if not success:
131
- raise
132
- try:
133
- image = Image.open(BytesIO(response.content))
134
- except Exception as e:
135
- print(f'Error opening image {input_file}: {e}')
136
- raise
137
-
138
- else:
139
- image = Image.open(input_file)
140
-
141
- # Convert to RGB if necessary
142
- if image.mode not in ('RGBA', 'RGB', 'L', 'I;16'):
143
- raise AttributeError(
144
- f'Image {input_file} uses unsupported mode {image.mode}')
145
- if image.mode == 'RGBA' or image.mode == 'L':
146
- # PIL.Image.convert() returns a converted copy of this image
147
- image = image.convert(mode='RGB')
148
-
149
- if not ignore_exif_rotation:
150
- # Alter orientation as needed according to EXIF tag 0x112 (274) for Orientation
151
- #
152
- # https://gist.github.com/dangtrinhnt/a577ece4cbe5364aad28
153
- # https://www.media.mit.edu/pia/Research/deepview/exif.html
154
- #
155
- try:
156
- exif = image._getexif()
157
- orientation: int = exif.get(274, None)
158
- if (orientation is not None) and (orientation != EXIF_IMAGE_NO_ROTATION):
159
- assert orientation in EXIF_IMAGE_ROTATIONS, \
160
- 'Mirrored rotations are not supported'
161
- image = image.rotate(EXIF_IMAGE_ROTATIONS[orientation], expand=True)
162
- except Exception:
163
- pass
164
-
165
- return image
166
-
167
- # ...def open_image(...)
168
-
169
-
170
- def exif_preserving_save(pil_image,output_file,quality='keep',default_quality=85,verbose=False):
171
- """
172
- Saves [pil_image] to [output_file], making a moderate attempt to preserve EXIF
173
- data and JPEG quality. Neither is guaranteed.
174
-
175
- Also see:
176
-
177
- https://discuss.dizzycoding.com/determining-jpg-quality-in-python-pil/
178
-
179
- ...for more ways to preserve jpeg quality if quality='keep' doesn't do the trick.
180
-
181
- Args:
182
- pil_image (Image): the PIL Image objct to save
183
- output_file (str): the destination file
184
- quality (str or int, optional): can be "keep" (default), or an integer from 0 to 100.
185
- This is only used if PIL thinks the the source image is a JPEG. If you load a JPEG
186
- and resize it in memory, for example, it's no longer a JPEG.
187
- default_quality (int, optional): determines output quality when quality == 'keep' and we are
188
- saving a non-JPEG source to a JPEG file
189
- verbose (bool, optional): enable additional debug console output
190
- """
191
-
192
- # Read EXIF metadata
193
- exif = pil_image.info['exif'] if ('exif' in pil_image.info) else None
194
-
195
- # Quality preservation is only supported for JPEG sources.
196
- if pil_image.format != "JPEG":
197
- if quality == 'keep':
198
- if verbose:
199
- print('Warning: quality "keep" passed when saving a non-JPEG source (during save to {})'.format(
200
- output_file))
201
- quality = default_quality
202
-
203
- # Some output formats don't support the quality parameter, so we try once with,
204
- # and once without. This is a horrible cascade of if's, but it's a consequence of
205
- # the fact that "None" is not supported for either "exif" or "quality".
206
-
207
- try:
208
-
209
- if exif is not None:
210
- pil_image.save(output_file, exif=exif, quality=quality)
211
- else:
212
- pil_image.save(output_file, quality=quality)
213
-
214
- except Exception:
215
-
216
- if verbose:
217
- print('Warning: failed to write {}, trying again without quality parameter'.format(output_file))
218
- if exif is not None:
219
- pil_image.save(output_file, exif=exif)
220
- else:
221
- pil_image.save(output_file)
222
-
223
- # ...def exif_preserving_save(...)
224
-
225
-
226
- def load_image(input_file, ignore_exif_rotation=False):
227
- """
228
- Loads an image file. This is the non-lazy version of open_file(); i.e.,
229
- it forces image decoding before returning.
230
-
231
- Args:
232
- input_file (str or BytesIO): can be a path to an image file (anything
233
- that PIL can open), a URL, or an image as a stream of bytes
234
- ignore_exif_rotation (bool, optional): don't rotate the loaded pixels,
235
- even if we are loading a JPEG and that JPEG says it should be rotated
236
-
237
- Returns:
238
- PIL.Image.Image: a PIL Image object in RGB mode
239
- """
240
-
241
- image = open_image(input_file, ignore_exif_rotation=ignore_exif_rotation)
242
- image.load()
243
- return image
244
-
245
-
246
- def resize_image(image, target_width=-1, target_height=-1, output_file=None,
247
- no_enlarge_width=False, verbose=False, quality='keep'):
248
- """
249
- Resizes a PIL Image object to the specified width and height; does not resize
250
- in place. If either width or height are -1, resizes with aspect ratio preservation.
251
-
252
- If target_width and target_height are both -1, does not modify the image, but
253
- will write to output_file if supplied.
254
-
255
- If no resizing is required, and an Image object is supplied, returns the original Image
256
- object (i.e., does not copy).
257
-
258
- Args:
259
- image (Image or str): PIL Image object or a filename (local file or URL)
260
- target_width (int, optional): width to which we should resize this image, or -1
261
- to let target_height determine the size
262
- target_height (int, optional): height to which we should resize this image, or -1
263
- to let target_width determine the size
264
- output_file (str, optional): file to which we should save this image; if None,
265
- just returns the image without saving
266
- no_enlarge_width (bool, optional): if [no_enlarge_width] is True, and
267
- [target width] is larger than the original image width, does not modify the image,
268
- but will write to output_file if supplied
269
- verbose (bool, optional): enable additional debug output
270
- quality (str or int, optional): passed to exif_preserving_save, see docs for more detail
271
-
272
- returns:
273
- PIL.Image.Image: the resized image, which may be the original image if no resizing is
274
- required
275
- """
276
-
277
- image_fn = 'in_memory'
278
- if isinstance(image,str):
279
- image_fn = image
280
- image = load_image(image)
281
-
282
- if target_width is None:
283
- target_width = -1
284
-
285
- if target_height is None:
286
- target_height = -1
287
-
288
- resize_required = True
289
-
290
- # No resize was requested, this is always a no-op
291
- if target_width == -1 and target_height == -1:
292
-
293
- resize_required = False
294
-
295
- # Does either dimension need to scale according to the other?
296
- elif target_width == -1 or target_height == -1:
297
-
298
- # Aspect ratio as width over height
299
- # ar = w / h
300
- aspect_ratio = image.size[0] / image.size[1]
301
-
302
- if target_width != -1:
303
- # h = w / ar
304
- target_height = int(target_width / aspect_ratio)
305
- else:
306
- # w = ar * h
307
- target_width = int(aspect_ratio * target_height)
308
-
309
- # If we're not enlarging images and this would be an enlarge operation
310
- if (no_enlarge_width) and (target_width > image.size[0]):
311
-
312
- if verbose:
313
- print('Bypassing image enlarge for {} --> {}'.format(
314
- image_fn,str(output_file)))
315
- resize_required = False
316
-
317
- # If the target size is the same as the original size
318
- if (target_width == image.size[0]) and (target_height == image.size[1]):
319
-
320
- resize_required = False
321
-
322
- if not resize_required:
323
-
324
- if output_file is not None:
325
- if verbose:
326
- print('No resize required for resize {} --> {}'.format(
327
- image_fn,str(output_file)))
328
- exif_preserving_save(image,output_file,quality=quality,verbose=verbose)
329
- return image
330
-
331
- assert target_width > 0 and target_height > 0, \
332
- 'Invalid image resize target {},{}'.format(target_width,target_height)
333
-
334
- # The antialiasing parameter changed between Pillow versions 9 and 10, and for a bit,
335
- # I'd like to support both.
336
- try:
337
- resized_image = image.resize((target_width, target_height), Image.ANTIALIAS)
338
- except:
339
- resized_image = image.resize((target_width, target_height), Image.Resampling.LANCZOS)
340
-
341
- if output_file is not None:
342
- exif_preserving_save(resized_image,output_file,quality=quality,verbose=verbose)
343
-
344
- return resized_image
345
-
346
- # ...def resize_image(...)
347
-
348
-
349
- def crop_image(detections, image, confidence_threshold=0.15, expansion=0):
350
- """
351
- Crops detections above [confidence_threshold] from the PIL image [image],
352
- returning a list of PIL Images.
353
-
354
- Args:
355
- detections (list): a list of dictionaries with keys 'conf' and 'bbox';
356
- boxes are length-four arrays formatted as [x,y,w,h], normalized,
357
- upper-left origin (this is the standard MD detection format)
358
- image (Image): the PIL Image object from which we should crop detections
359
- confidence_threshold (float, optional): only crop detections above this threshold
360
- expansion (int, optional): a number of pixels to include on each side of a cropped
361
- detection
362
-
363
- Returns:
364
- list: a possibly-empty list of PIL Image objects
365
- """
366
-
367
- ret_images = []
368
-
369
- for detection in detections:
370
-
371
- score = float(detection['conf'])
372
-
373
- if score >= confidence_threshold:
374
-
375
- x1, y1, w_box, h_box = detection['bbox']
376
- ymin,xmin,ymax,xmax = y1, x1, y1 + h_box, x1 + w_box
377
-
378
- # Convert to pixels so we can use the PIL crop() function
379
- im_width, im_height = image.size
380
- (left, right, top, bottom) = (xmin * im_width, xmax * im_width,
381
- ymin * im_height, ymax * im_height)
382
-
383
- if expansion > 0:
384
- left -= expansion
385
- right += expansion
386
- top -= expansion
387
- bottom += expansion
388
-
389
- # PIL's crop() does surprising things if you provide values outside of
390
- # the image, clip inputs
391
- left = max(left,0); right = max(right,0)
392
- top = max(top,0); bottom = max(bottom,0)
393
-
394
- left = min(left,im_width-1); right = min(right,im_width-1)
395
- top = min(top,im_height-1); bottom = min(bottom,im_height-1)
396
-
397
- ret_images.append(image.crop((left, top, right, bottom)))
398
-
399
- # ...if this detection is above threshold
400
-
401
- # ...for each detection
402
-
403
- return ret_images
404
-
405
-
406
- def render_detection_bounding_boxes(detections,
407
- image,
408
- label_map='show_categories',
409
- classification_label_map=None,
410
- confidence_threshold=0.15,
411
- thickness=DEFAULT_BOX_THICKNESS,
412
- expansion=0,
413
- classification_confidence_threshold=0.3,
414
- max_classifications=3,
415
- colormap=None,
416
- textalign=TEXTALIGN_LEFT,
417
- label_font_size=DEFAULT_LABEL_FONT_SIZE,
418
- custom_strings=None):
419
- """
420
- Renders bounding boxes (with labels and confidence values) on an image for all
421
- detections above a threshold.
422
-
423
- Renders classification labels if present.
424
-
425
- [image] is modified in place.
426
-
427
- Args:
428
-
429
- detections (list): list of detections in the MD output format, for example:
430
-
431
- .. code-block::none
432
-
433
- [
434
- {
435
- "category": "2",
436
- "conf": 0.996,
437
- "bbox": [
438
- 0.0,
439
- 0.2762,
440
- 0.1234,
441
- 0.2458
442
- ]
443
- }
444
- ]
445
-
446
- ...where the bbox coordinates are [x, y, box_width, box_height].
447
-
448
- (0, 0) is the upper-left. Coordinates are normalized.
449
-
450
- Supports classification results, in the standard format:
451
-
452
- .. code-block::none
453
-
454
- [
455
- {
456
- "category": "2",
457
- "conf": 0.996,
458
- "bbox": [
459
- 0.0,
460
- 0.2762,
461
- 0.1234,
462
- 0.2458
463
- ]
464
- "classifications": [
465
- ["3", 0.901],
466
- ["1", 0.071],
467
- ["4", 0.025]
468
- ]
469
- }
470
- ]
471
-
472
- image (PIL.Image.Image): image on which we should render detections
473
-
474
- label_map (dict, optional): optional, mapping the numeric label to a string name. The type of the
475
- numeric label (typically strings) needs to be consistent with the keys in label_map; no casting is
476
- carried out. If [label_map] is None, no labels are shown (not even numbers and confidence values).
477
- If you want category numbers and confidence values without class labels, use the default value,
478
- the string 'show_categories'.
479
-
480
- classification_label_map (dict, optional): optional, mapping of the string class labels to the actual
481
- class names. The type of the numeric label (typically strings) needs to be consistent with the keys
482
- in label_map; no casting is carried out. If [label_map] is None, no labels are shown (not even numbers
483
- and confidence values).
484
-
485
- confidence_threshold (float or dict, optional), threshold above which boxes are rendered. Can also be a
486
- dictionary mapping category IDs to thresholds.
487
-
488
- thickness (int, optional): line thickness in pixels
489
-
490
- expansion (int, optional): number of pixels to expand bounding boxes on each side
491
-
492
- classification_confidence_threshold (float, optional): confidence above which classification results
493
- are displayed
494
-
495
- max_classifications (int, optional): maximum number of classification results rendered for one image
496
-
497
- colormap (list, optional): list of color names, used to choose colors for categories by
498
- indexing with the values in [classes]; defaults to a reasonable set of colors
499
-
500
- textalign (int, optional): TEXTALIGN_LEFT or TEXTALIGN_RIGHT
501
-
502
- label_font_size (float, optional): font size for labels
503
-
504
- custom_strings: optional set of strings to append to detection labels, should have the
505
- same length as [detections]. Appended before any classification labels.
506
- """
507
-
508
- # Input validation
509
- if (label_map is not None) and (isinstance(label_map,str)) and (label_map == 'show_categories'):
510
- label_map = {}
511
-
512
- if custom_strings is not None:
513
- assert len(custom_strings) == len(detections), \
514
- '{} custom strings provided for {} detections'.format(
515
- len(custom_strings),len(detections))
516
-
517
- display_boxes = []
518
-
519
- # list of lists, one list of strings for each bounding box (to accommodate multiple labels)
520
- display_strs = []
521
-
522
- # for color selection
523
- classes = []
524
-
525
- for i_detection,detection in enumerate(detections):
526
-
527
- score = detection['conf']
528
-
529
- if isinstance(confidence_threshold,dict):
530
- rendering_threshold = confidence_threshold[detection['category']]
531
- else:
532
- rendering_threshold = confidence_threshold
533
-
534
- # Always render objects with a confidence of "None", this is typically used
535
- # for ground truth data.
536
- if score is None or score >= rendering_threshold:
537
-
538
- x1, y1, w_box, h_box = detection['bbox']
539
- display_boxes.append([y1, x1, y1 + h_box, x1 + w_box])
540
- clss = detection['category']
541
-
542
- # {} is the default, which means "show labels with no mapping", so don't use "if label_map" here
543
- # if label_map:
544
- if label_map is not None:
545
- label = label_map[clss] if clss in label_map else clss
546
- if score is not None:
547
- displayed_label = ['{}: {}%'.format(label, round(100 * score))]
548
- else:
549
- displayed_label = ['{}'.format(label)]
550
- else:
551
- displayed_label = ''
552
-
553
- if custom_strings is not None:
554
- custom_string = custom_strings[i_detection]
555
- if custom_string is not None and len(custom_string) > 0:
556
- if isinstance(displayed_label,str):
557
- displayed_label += ' ' + custom_string
558
- else:
559
- assert len(displayed_label) == 1
560
- displayed_label[0] += ' ' + custom_string
561
-
562
- if 'classifications' in detection:
563
-
564
- # To avoid duplicate colors with detection-only visualization, offset
565
- # the classification class index by the number of detection classes
566
- clss = annotation_constants.NUM_DETECTOR_CATEGORIES + int(detection['classifications'][0][0])
567
- classifications = detection['classifications']
568
- if len(classifications) > max_classifications:
569
- classifications = classifications[0:max_classifications]
570
-
571
- for classification in classifications:
572
-
573
- classification_conf = classification[1]
574
- if classification_conf is not None and \
575
- classification_conf < classification_confidence_threshold:
576
- continue
577
- class_key = classification[0]
578
- if (classification_label_map is not None) and (class_key in classification_label_map):
579
- class_name = classification_label_map[class_key]
580
- else:
581
- class_name = class_key
582
- if classification_conf is not None:
583
- displayed_label += ['{}: {:5.1%}'.format(class_name.lower(), classification_conf)]
584
- else:
585
- displayed_label += ['{}'.format(class_name.lower())]
586
-
587
- # ...for each classification
588
-
589
- # ...if we have classification results
590
-
591
- display_strs.append(displayed_label)
592
- classes.append(clss)
593
-
594
- # ...if the confidence of this detection is above threshold
595
-
596
- # ...for each detection
597
-
598
- display_boxes = np.array(display_boxes)
599
-
600
- draw_bounding_boxes_on_image(image, display_boxes, classes,
601
- display_strs=display_strs, thickness=thickness,
602
- expansion=expansion, colormap=colormap, textalign=textalign,
603
- label_font_size=label_font_size)
604
-
605
- # ...render_detection_bounding_boxes(...)
606
-
607
-
608
- def draw_bounding_boxes_on_image(image,
609
- boxes,
610
- classes,
611
- thickness=DEFAULT_BOX_THICKNESS,
612
- expansion=0,
613
- display_strs=None,
614
- colormap=None,
615
- textalign=TEXTALIGN_LEFT,
616
- label_font_size=DEFAULT_LABEL_FONT_SIZE):
617
- """
618
- Draws bounding boxes on an image. Modifies the image in place.
619
-
620
- Args:
621
-
622
- image (PIL.Image): the image on which we should draw boxes
623
- boxes (np.array): a two-dimensional numpy array of size [N, 4], where N is the
624
- number of boxes, and each row is (ymin, xmin, ymax, xmax). Coordinates should be
625
- normalized to image height/width.
626
- classes (list): a list of ints or string-formatted ints corresponding to the
627
- class labels of the boxes. This is only used for color selection. Should have the same
628
- length as [boxes].
629
- thickness (int, optional): line thickness in pixels
630
- expansion (int, optional): number of pixels to expand bounding boxes on each side
631
- display_strs (list, optional): list of list of strings (the outer list should have the
632
- same length as [boxes]). Typically this is used to show (possibly multiple) detection
633
- or classification categories and/or confidence values.
634
- colormap (list, optional): list of color names, used to choose colors for categories by
635
- indexing with the values in [classes]; defaults to a reasonable set of colors
636
- textalign (int, optional): TEXTALIGN_LEFT or TEXTALIGN_RIGHT
637
- label_font_size (float, optional): font size for labels
638
- """
639
-
640
- boxes_shape = boxes.shape
641
- if not boxes_shape:
642
- return
643
- if len(boxes_shape) != 2 or boxes_shape[1] != 4:
644
- # print('Input must be of size [N, 4], but is ' + str(boxes_shape))
645
- return # no object detection on this image, return
646
- for i in range(boxes_shape[0]):
647
- if display_strs:
648
- display_str_list = display_strs[i]
649
- draw_bounding_box_on_image(image,
650
- boxes[i, 0], boxes[i, 1], boxes[i, 2], boxes[i, 3],
651
- classes[i],
652
- thickness=thickness, expansion=expansion,
653
- display_str_list=display_str_list,
654
- colormap=colormap,
655
- textalign=textalign,
656
- label_font_size=label_font_size)
657
-
658
- # ...draw_bounding_boxes_on_image(...)
659
-
660
-
661
- def draw_bounding_box_on_image(image,
662
- ymin,
663
- xmin,
664
- ymax,
665
- xmax,
666
- clss=None,
667
- thickness=DEFAULT_BOX_THICKNESS,
668
- expansion=0,
669
- display_str_list=None,
670
- use_normalized_coordinates=True,
671
- label_font_size=DEFAULT_LABEL_FONT_SIZE,
672
- colormap=None,
673
- textalign=TEXTALIGN_LEFT):
674
- """
675
- Adds a bounding box to an image. Modifies the image in place.
676
-
677
- Bounding box coordinates can be specified in either absolute (pixel) or
678
- normalized coordinates by setting the use_normalized_coordinates argument.
679
-
680
- Each string in display_str_list is displayed on a separate line above the
681
- bounding box in black text on a rectangle filled with the input 'color'.
682
- If the top of the bounding box extends to the edge of the image, the strings
683
- are displayed below the bounding box.
684
-
685
- Adapted from:
686
-
687
- https://github.com/tensorflow/models/blob/master/research/object_detection/utils/visualization_utils.py
688
-
689
- Args:
690
- image (PIL.Image.Image): the image on which we should draw a box
691
- ymin (float): ymin of bounding box
692
- xmin (float): xmin of bounding box
693
- ymax (float): ymax of bounding box
694
- xmax (float): xmax of bounding box
695
- clss (int, optional): the class index of the object in this bounding box, used for choosing
696
- a color; should be either an integer or a string-formatted integer
697
- thickness (int, optional): line thickness in pixels
698
- expansion (int, optional): number of pixels to expand bounding boxes on each side
699
- display_str_list (list, optional): list of strings to display above the box (each to be shown on its
700
- own line)
701
- use_normalized_coordinates (bool, optional): if True (default), treat coordinates
702
- ymin, xmin, ymax, xmax as relative to the image, otherwise coordinates as absolute pixel values
703
- label_font_size (float, optional): font size
704
- colormap (list, optional): list of color names, used to choose colors for categories by
705
- indexing with the values in [classes]; defaults to a reasonable set of colors
706
- textalign (int, optional): TEXTALIGN_LEFT or TEXTALIGN_RIGHT
707
- """
708
-
709
- if colormap is None:
710
- colormap = DEFAULT_COLORS
711
-
712
- if display_str_list is None:
713
- display_str_list = []
714
-
715
- if clss is None:
716
- # Default to the MegaDetector animal class ID (1)
717
- color = colormap[1]
718
- else:
719
- color = colormap[int(clss) % len(colormap)]
720
-
721
- draw = ImageDraw.Draw(image)
722
- im_width, im_height = image.size
723
- if use_normalized_coordinates:
724
- (left, right, top, bottom) = (xmin * im_width, xmax * im_width,
725
- ymin * im_height, ymax * im_height)
726
- else:
727
- (left, right, top, bottom) = (xmin, xmax, ymin, ymax)
728
-
729
- if expansion > 0:
730
-
731
- left -= expansion
732
- right += expansion
733
- top -= expansion
734
- bottom += expansion
735
-
736
- # Deliberately trimming to the width of the image only in the case where
737
- # box expansion is turned on. There's not an obvious correct behavior here,
738
- # but the thinking is that if the caller provided an out-of-range bounding
739
- # box, they meant to do that, but at least in the eyes of the person writing
740
- # this comment, if you expand a box for visualization reasons, you don't want
741
- # to end up with part of a box.
742
- #
743
- # A slightly more sophisticated might check whether it was in fact the expansion
744
- # that made this box larger than the image, but this is the case 99.999% of the time
745
- # here, so that doesn't seem necessary.
746
- left = max(left,0); right = max(right,0)
747
- top = max(top,0); bottom = max(bottom,0)
748
-
749
- left = min(left,im_width-1); right = min(right,im_width-1)
750
- top = min(top,im_height-1); bottom = min(bottom,im_height-1)
751
-
752
- # ...if we need to expand boxes
753
-
754
- draw.line([(left, top), (left, bottom), (right, bottom),
755
- (right, top), (left, top)], width=thickness, fill=color)
756
-
757
- try:
758
- font = ImageFont.truetype('arial.ttf', label_font_size)
759
- except IOError:
760
- font = ImageFont.load_default()
761
-
762
- def get_text_size(font,s):
763
-
764
- # This is what we did w/Pillow 9
765
- # w,h = font.getsize(s)
766
-
767
- # I would *think* this would be the equivalent for Pillow 10
768
- # l,t,r,b = font.getbbox(s); w = r-l; h=b-t
769
-
770
- # ...but this actually produces the most similar results to Pillow 9
771
- # l,t,r,b = font.getbbox(s); w = r; h=b
772
-
773
- try:
774
- l,t,r,b = font.getbbox(s); w = r; h=b
775
- except Exception:
776
- w,h = font.getsize(s)
777
-
778
- return w,h
779
-
780
- # If the total height of the display strings added to the top of the bounding
781
- # box exceeds the top of the image, stack the strings below the bounding box
782
- # instead of above.
783
- display_str_heights = [get_text_size(font,ds)[1] for ds in display_str_list]
784
-
785
- # Each display_str has a top and bottom margin of 0.05x.
786
- total_display_str_height = (1 + 2 * 0.05) * sum(display_str_heights)
787
-
788
- if top > total_display_str_height:
789
- text_bottom = top
790
- else:
791
- text_bottom = bottom + total_display_str_height
792
-
793
- # Reverse list and print from bottom to top.
794
- for display_str in display_str_list[::-1]:
795
-
796
- # Skip empty strings
797
- if len(display_str) == 0:
798
- continue
799
-
800
- text_width, text_height = get_text_size(font,display_str)
801
-
802
- text_left = left
803
-
804
- if textalign == TEXTALIGN_RIGHT:
805
- text_left = right - text_width
806
-
807
- margin = np.ceil(0.05 * text_height)
808
-
809
- draw.rectangle(
810
- [(text_left, text_bottom - text_height - 2 * margin), (text_left + text_width,
811
- text_bottom)],
812
- fill=color)
813
-
814
- draw.text(
815
- (text_left + margin, text_bottom - text_height - margin),
816
- display_str,
817
- fill='black',
818
- font=font)
819
-
820
- text_bottom -= (text_height + 2 * margin)
821
-
822
- # ...def draw_bounding_box_on_image(...)
823
-
824
-
825
- def render_megadb_bounding_boxes(boxes_info, image):
826
- """
827
- Render bounding boxes to an image, where those boxes are in the mostly-deprecated
828
- MegaDB format, which looks like:
829
-
830
- .. code-block::none
831
-
832
- {
833
- "category": "animal",
834
- "bbox": [
835
- 0.739,
836
- 0.448,
837
- 0.187,
838
- 0.198
839
- ]
840
- }
841
-
842
- Args:
843
- boxes_info (list): list of dicts, each dict represents a single detection
844
- where bbox coordinates are normalized [x_min, y_min, width, height]
845
- image (PIL.Image.Image): image to modify
846
-
847
- :meta private:
848
- """
849
-
850
- display_boxes = []
851
- display_strs = []
852
- classes = [] # ints, for selecting colors
853
-
854
- for b in boxes_info:
855
- x_min, y_min, w_rel, h_rel = b['bbox']
856
- y_max = y_min + h_rel
857
- x_max = x_min + w_rel
858
- display_boxes.append([y_min, x_min, y_max, x_max])
859
- display_strs.append([b['category']])
860
- classes.append(annotation_constants.detector_bbox_category_name_to_id[b['category']])
861
-
862
- display_boxes = np.array(display_boxes)
863
- draw_bounding_boxes_on_image(image, display_boxes, classes, display_strs=display_strs)
864
-
865
- # ...def render_iMerit_boxes(...)
866
-
867
-
868
- def render_db_bounding_boxes(boxes,
869
- classes,
870
- image,
871
- original_size=None,
872
- label_map=None,
873
- thickness=DEFAULT_BOX_THICKNESS,
874
- expansion=0):
875
- """
876
- Render bounding boxes (with class labels) on an image. This is a wrapper for
877
- draw_bounding_boxes_on_image, allowing the caller to operate on a resized image
878
- by providing the original size of the image; boxes will be scaled accordingly.
879
-
880
- This function assumes that bounding boxes are in absolute coordinates, typically
881
- because they come from COCO camera traps .json files.
882
-
883
- Args:
884
- boxes (list): list of length-4 tuples, foramtted as (x,y,w,h) (in pixels)
885
- classes (list): list of ints (or string-formatted ints), used to choose labels (either
886
- by literally rendering the class labels, or by indexing into [label_map])
887
- image (PIL.Image.Image): image object to modify
888
- original_size (tuple, optional): if this is not None, and the size is different than
889
- the size of [image], we assume that [boxes] refer to the original size, and we scale
890
- them accordingly before rendering
891
- label_map (dict, optional): int --> str dictionary, typically mapping category IDs to
892
- species labels; if None, category labels are rendered verbatim (typically as numbers)
893
- thickness (int, optional): line width
894
- expansion (int, optional): a number of pixels to include on each side of a cropped
895
- detection
896
- """
897
-
898
- display_boxes = []
899
- display_strs = []
900
-
901
- if original_size is not None:
902
- image_size = original_size
903
- else:
904
- image_size = image.size
905
-
906
- img_width, img_height = image_size
907
-
908
- for box, clss in zip(boxes, classes):
909
-
910
- x_min_abs, y_min_abs, width_abs, height_abs = box[0:4]
911
-
912
- ymin = y_min_abs / img_height
913
- ymax = ymin + height_abs / img_height
914
-
915
- xmin = x_min_abs / img_width
916
- xmax = xmin + width_abs / img_width
917
-
918
- display_boxes.append([ymin, xmin, ymax, xmax])
919
-
920
- if label_map:
921
- clss = label_map[int(clss)]
922
-
923
- # need to be a string here because PIL needs to iterate through chars
924
- display_strs.append([str(clss)])
925
-
926
- display_boxes = np.array(display_boxes)
927
-
928
- draw_bounding_boxes_on_image(image,
929
- display_boxes,
930
- classes,
931
- display_strs=display_strs,
932
- thickness=thickness,
933
- expansion=expansion)
934
-
935
- # ...def render_db_bounding_boxes(...)
936
-
937
-
938
- def draw_bounding_boxes_on_file(input_file,
939
- output_file,
940
- detections,
941
- confidence_threshold=0.0,
942
- detector_label_map=DEFAULT_DETECTOR_LABEL_MAP,
943
- thickness=DEFAULT_BOX_THICKNESS,
944
- expansion=0,
945
- colormap=None,
946
- label_font_size=DEFAULT_LABEL_FONT_SIZE,
947
- custom_strings=None,
948
- target_size=None,
949
- ignore_exif_rotation=False):
950
- """
951
- Renders detection bounding boxes on an image loaded from file, optionally writing the results to
952
- a new image file.
953
-
954
- Args:
955
- input_file (str): filename or URL to load
956
- output_file (str, optional): filename to which we should write the rendered image
957
- detections (list): a list of dictionaries with keys 'conf' and 'bbox';
958
- boxes are length-four arrays formatted as [x,y,w,h], normalized,
959
- upper-left origin (this is the standard MD detection format)
960
- detector_label_map (dict, optional): a dict mapping category IDs to strings. If this
961
- is None, no confidence values or identifiers are shown If this is {}, just category
962
- indices and confidence values are shown.
963
- thickness (int, optional): line width in pixels for box rendering
964
- expansion (int, optional): box expansion in pixels
965
- colormap (list, optional): list of color names, used to choose colors for categories by
966
- indexing with the values in [classes]; defaults to a reasonable set of colors
967
- label_font_size (float, optional): label font size
968
- custom_strings (list, optional): set of strings to append to detection labels, should have the
969
- same length as [detections]. Appended before any classification labels.
970
- target_size (tuple, optional): tuple of (target_width,target_height). Either or both can be -1,
971
- see resize_image() for documentation. If None or (-1,-1), uses the original image size.
972
- ignore_exif_rotation (bool, optional): don't rotate the loaded pixels,
973
- even if we are loading a JPEG and that JPEG says it should be rotated.
974
-
975
- Returns:
976
- PIL.Image.Image: loaded and modified image
977
- """
978
-
979
- image = open_image(input_file, ignore_exif_rotation=ignore_exif_rotation)
980
-
981
- if target_size is not None:
982
- image = resize_image(image,target_size[0],target_size[1])
983
-
984
- render_detection_bounding_boxes(
985
- detections, image, label_map=detector_label_map,
986
- confidence_threshold=confidence_threshold,
987
- thickness=thickness,expansion=expansion,colormap=colormap,
988
- custom_strings=custom_strings,label_font_size=label_font_size)
989
-
990
- if output_file is not None:
991
- image.save(output_file)
992
-
993
- return image
994
-
995
-
996
- def draw_db_boxes_on_file(input_file,
997
- output_file,
998
- boxes,
999
- classes=None,
1000
- label_map=None,
1001
- thickness=DEFAULT_BOX_THICKNESS,
1002
- expansion=0,
1003
- ignore_exif_rotation=False):
1004
- """
1005
- Render COCO-formatted bounding boxes (in absolute coordinates) on an image loaded from file,
1006
- writing the results to a new image file.
1007
-
1008
- Args:
1009
- input_file (str): image file to read
1010
- output_file (str): image file to write
1011
- boxes (list): list of length-4 tuples, foramtted as (x,y,w,h) (in pixels)
1012
- classes (list, optional): list of ints (or string-formatted ints), used to choose
1013
- labels (either by literally rendering the class labels, or by indexing into [label_map])
1014
- label_map (dict, optional): int --> str dictionary, typically mapping category IDs to
1015
- species labels; if None, category labels are rendered verbatim (typically as numbers)
1016
- thickness (int, optional): line width
1017
- expansion (int, optional): a number of pixels to include on each side of a cropped
1018
- detection
1019
- ignore_exif_rotation (bool, optional): don't rotate the loaded pixels,
1020
- even if we are loading a JPEG and that JPEG says it should be rotated
1021
-
1022
- Returns:
1023
- PIL.Image.Image: the loaded and modified image
1024
- """
1025
-
1026
- image = open_image(input_file, ignore_exif_rotation=ignore_exif_rotation)
1027
-
1028
- if classes is None:
1029
- classes = [0] * len(boxes)
1030
-
1031
- render_db_bounding_boxes(boxes, classes, image, original_size=None,
1032
- label_map=label_map, thickness=thickness, expansion=expansion)
1033
-
1034
- image.save(output_file)
1035
-
1036
- return image
1037
-
1038
- # ...def draw_bounding_boxes_on_file(...)
1039
-
1040
-
1041
- def gray_scale_fraction(image,crop_size=(0.1,0.1)):
1042
- """
1043
- Computes the fraction of the pixels in [image] that appear to be grayscale (R==G==B),
1044
- useful for approximating whether this is a night-time image when flash information is not
1045
- available in EXIF data (or for video frames, where this information is often not available
1046
- in structured metadata at all).
1047
-
1048
- Args:
1049
- image (str or PIL.Image.Image): Image, filename, or URL to analyze
1050
- crop_size (optional): a 2-element list/tuple, representing the fraction of the
1051
- image to crop at the top and bottom, respectively, before analyzing (to minimize
1052
- the possibility of including color elements in the image overlay)
1053
-
1054
- Returns:
1055
- float: the fraction of pixels in [image] that appear to be grayscale (R==G==B)
1056
- """
1057
-
1058
- if isinstance(image,str):
1059
- image = Image.open(image)
1060
-
1061
- if image.mode == 'L':
1062
- return 1.0
1063
-
1064
- if len(image.getbands()) == 1:
1065
- return 1.0
1066
-
1067
- # Crop if necessary
1068
- if crop_size[0] > 0 or crop_size[1] > 0:
1069
-
1070
- assert (crop_size[0] + crop_size[1]) < 1.0, \
1071
- print('Illegal crop size: {}'.format(str(crop_size)))
1072
-
1073
- top_crop_pixels = int(image.height * crop_size[0])
1074
- bottom_crop_pixels = int(image.height * crop_size[1])
1075
-
1076
- left = 0
1077
- right = image.width
1078
-
1079
- # Remove pixels from the top
1080
- first_crop_top = top_crop_pixels
1081
- first_crop_bottom = image.height
1082
- first_crop = image.crop((left, first_crop_top, right, first_crop_bottom))
1083
-
1084
- # Remove pixels from the bottom
1085
- second_crop_top = 0
1086
- second_crop_bottom = first_crop.height - bottom_crop_pixels
1087
- second_crop = first_crop.crop((left, second_crop_top, right, second_crop_bottom))
1088
-
1089
- image = second_crop
1090
-
1091
- # It doesn't matter if these are actually R/G/B, they're just names
1092
- r = np.array(image.getchannel(0))
1093
- g = np.array(image.getchannel(1))
1094
- b = np.array(image.getchannel(2))
1095
-
1096
- gray_pixels = np.logical_and(r == g, r == b)
1097
- n_pixels = gray_pixels.size
1098
- n_gray_pixels = gray_pixels.sum()
1099
-
1100
- return n_gray_pixels / n_pixels
1101
-
1102
- # Non-numpy way to do the same thing, briefly keeping this here for posterity
1103
- if False:
1104
-
1105
- w, h = image.size
1106
- n_pixels = w*h
1107
- n_gray_pixels = 0
1108
- for i in range(w):
1109
- for j in range(h):
1110
- r, g, b = image.getpixel((i,j))
1111
- if r == g and r == b and g == b:
1112
- n_gray_pixels += 1
1113
-
1114
-
1115
- # ...def gray_scale_fraction(...)
1116
-
1117
-
1118
- def _resize_relative_image(fn_relative,
1119
- input_folder,output_folder,
1120
- target_width,target_height,no_enlarge_width,verbose,quality):
1121
- """
1122
- Internal function for resizing an image from one folder to another,
1123
- maintaining relative path.
1124
- """
1125
-
1126
- input_fn_abs = os.path.join(input_folder,fn_relative)
1127
- output_fn_abs = os.path.join(output_folder,fn_relative)
1128
- os.makedirs(os.path.dirname(output_fn_abs),exist_ok=True)
1129
- try:
1130
- _ = resize_image(input_fn_abs,
1131
- output_file=output_fn_abs,
1132
- target_width=target_width, target_height=target_height,
1133
- no_enlarge_width=no_enlarge_width, verbose=verbose, quality=quality)
1134
- status = 'success'
1135
- error = None
1136
- except Exception as e:
1137
- if verbose:
1138
- print('Error resizing {}: {}'.format(fn_relative,str(e)))
1139
- status = 'error'
1140
- error = str(e)
1141
-
1142
- return {'fn_relative':fn_relative,'status':status,'error':error}
1143
-
1144
- # ...def _resize_relative_image(...)
1145
-
1146
-
1147
- def _resize_absolute_image(input_output_files,
1148
- target_width,target_height,no_enlarge_width,verbose,quality):
1149
-
1150
- """
1151
- Internal wrapper for resize_image used in the context of a batch resize operation.
1152
- """
1153
-
1154
- input_fn_abs = input_output_files[0]
1155
- output_fn_abs = input_output_files[1]
1156
- os.makedirs(os.path.dirname(output_fn_abs),exist_ok=True)
1157
- try:
1158
- _ = resize_image(input_fn_abs,
1159
- output_file=output_fn_abs,
1160
- target_width=target_width, target_height=target_height,
1161
- no_enlarge_width=no_enlarge_width, verbose=verbose, quality=quality)
1162
- status = 'success'
1163
- error = None
1164
- except Exception as e:
1165
- if verbose:
1166
- print('Error resizing {}: {}'.format(input_fn_abs,str(e)))
1167
- status = 'error'
1168
- error = str(e)
1169
-
1170
- return {'input_fn':input_fn_abs,'output_fn':output_fn_abs,status:'status',
1171
- 'error':error}
1172
-
1173
- # ..._resize_absolute_image(...)
1174
-
1175
-
1176
- def resize_images(input_file_to_output_file,
1177
- target_width=-1,
1178
- target_height=-1,
1179
- no_enlarge_width=False,
1180
- verbose=False,
1181
- quality='keep',
1182
- pool_type='process',
1183
- n_workers=10):
1184
- """
1185
- Resizes all images the dictionary [input_file_to_output_file].
1186
-
1187
- TODO: This is a little more redundant with resize_image_folder than I would like;
1188
- refactor resize_image_folder to call resize_images. Not doing that yet because
1189
- at the time I'm writing this comment, a lot of code depends on resize_image_folder
1190
- and I don't want to rock the boat yet.
1191
-
1192
- Args:
1193
- input_file_to_output_file (dict): dict mapping images that exist to the locations
1194
- where the resized versions should be written
1195
- target_width (int, optional): width to which we should resize this image, or -1
1196
- to let target_height determine the size
1197
- target_height (int, optional): height to which we should resize this image, or -1
1198
- to let target_width determine the size
1199
- no_enlarge_width (bool, optional): if [no_enlarge_width] is True, and
1200
- [target width] is larger than the original image width, does not modify the image,
1201
- but will write to output_file if supplied
1202
- verbose (bool, optional): enable additional debug output
1203
- quality (str or int, optional): passed to exif_preserving_save, see docs for more detail
1204
- pool_type (str, optional): whether use use processes ('process') or threads ('thread') for
1205
- parallelization; ignored if n_workers <= 1
1206
- n_workers (int, optional): number of workers to use for parallel resizing; set to <=1
1207
- to disable parallelization
1208
-
1209
- Returns:
1210
- list: a list of dicts with keys 'input_fn', 'output_fn', 'status', and 'error'.
1211
- 'status' will be 'success' or 'error'; 'error' will be None for successful cases,
1212
- otherwise will contain the image-specific error.
1213
- """
1214
-
1215
- assert pool_type in ('process','thread'), 'Illegal pool type {}'.format(pool_type)
1216
-
1217
- input_output_file_pairs = []
1218
-
1219
- # Reformat input files as (input,output) tuples
1220
- for input_fn in input_file_to_output_file:
1221
- input_output_file_pairs.append((input_fn,input_file_to_output_file[input_fn]))
1222
-
1223
- if n_workers == 1:
1224
-
1225
- results = []
1226
- for i_o_file_pair in tqdm(input_output_file_pairs):
1227
- results.append(_resize_absolute_image(i_o_file_pair,
1228
- target_width=target_width,
1229
- target_height=target_height,
1230
- no_enlarge_width=no_enlarge_width,
1231
- verbose=verbose,
1232
- quality=quality))
1233
-
1234
- else:
1235
-
1236
- if pool_type == 'thread':
1237
- pool = ThreadPool(n_workers); poolstring = 'threads'
1238
- else:
1239
- assert pool_type == 'process'
1240
- pool = Pool(n_workers); poolstring = 'processes'
1241
-
1242
- if verbose:
1243
- print('Starting resizing pool with {} {}'.format(n_workers,poolstring))
1244
-
1245
- p = partial(_resize_absolute_image,
1246
- target_width=target_width,
1247
- target_height=target_height,
1248
- no_enlarge_width=no_enlarge_width,
1249
- verbose=verbose,
1250
- quality=quality)
1251
-
1252
- results = list(tqdm(pool.imap(p, input_output_file_pairs),total=len(input_output_file_pairs)))
1253
-
1254
- return results
1255
-
1256
- # ...def resize_images(...)
1257
-
1258
-
1259
- def resize_image_folder(input_folder,
1260
- output_folder=None,
1261
- target_width=-1,
1262
- target_height=-1,
1263
- no_enlarge_width=False,
1264
- verbose=False,
1265
- quality='keep',
1266
- pool_type='process',
1267
- n_workers=10,
1268
- recursive=True,
1269
- image_files_relative=None):
1270
- """
1271
- Resize all images in a folder (defaults to recursive).
1272
-
1273
- Defaults to in-place resizing (output_folder is optional).
1274
-
1275
- Args:
1276
- input_folder (str): folder in which we should find images to resize
1277
- output_folder (str, optional): folder in which we should write resized images. If
1278
- None, resizes images in place. Otherwise, maintains relative paths in the target
1279
- folder.
1280
- target_width (int, optional): width to which we should resize this image, or -1
1281
- to let target_height determine the size
1282
- target_height (int, optional): height to which we should resize this image, or -1
1283
- to let target_width determine the size
1284
- no_enlarge_width (bool, optional): if [no_enlarge_width] is True, and
1285
- [target width] is larger than the original image width, does not modify the image,
1286
- but will write to output_file if supplied
1287
- verbose (bool, optional): enable additional debug output
1288
- quality (str or int, optional): passed to exif_preserving_save, see docs for more detail
1289
- pool_type (str, optional): whether use use processes ('process') or threads ('thread') for
1290
- parallelization; ignored if n_workers <= 1
1291
- n_workers (int, optional): number of workers to use for parallel resizing; set to <=1
1292
- to disable parallelization
1293
- recursive (bool, optional): whether to search [input_folder] recursively for images.
1294
- image_files_relative (list, optional): if not None, skips any relative paths not
1295
- in this list.
1296
-
1297
- Returns:
1298
- list: a list of dicts with keys 'input_fn', 'output_fn', 'status', and 'error'.
1299
- 'status' will be 'success' or 'error'; 'error' will be None for successful cases,
1300
- otherwise will contain the image-specific error.
1301
- """
1302
-
1303
- assert os.path.isdir(input_folder), '{} is not a folder'.format(input_folder)
1304
-
1305
- if output_folder is None:
1306
- output_folder = input_folder
1307
- else:
1308
- os.makedirs(output_folder,exist_ok=True)
1309
-
1310
- assert pool_type in ('process','thread'), 'Illegal pool type {}'.format(pool_type)
1311
-
1312
- if image_files_relative is None:
1313
-
1314
- if verbose:
1315
- print('Enumerating images')
1316
-
1317
- image_files_relative = find_images(input_folder,recursive=recursive,
1318
- return_relative_paths=True,convert_slashes=True)
1319
- if verbose:
1320
- print('Found {} images'.format(len(image_files_relative)))
1321
-
1322
- if n_workers == 1:
1323
-
1324
- if verbose:
1325
- print('Resizing images')
1326
-
1327
- results = []
1328
- for fn_relative in tqdm(image_files_relative):
1329
- results.append(_resize_relative_image(fn_relative,
1330
- input_folder=input_folder,
1331
- output_folder=output_folder,
1332
- target_width=target_width,
1333
- target_height=target_height,
1334
- no_enlarge_width=no_enlarge_width,
1335
- verbose=verbose,
1336
- quality=quality))
1337
-
1338
- else:
1339
-
1340
- if pool_type == 'thread':
1341
- pool = ThreadPool(n_workers); poolstring = 'threads'
1342
- else:
1343
- assert pool_type == 'process'
1344
- pool = Pool(n_workers); poolstring = 'processes'
1345
-
1346
- if verbose:
1347
- print('Starting resizing pool with {} {}'.format(n_workers,poolstring))
1348
-
1349
- p = partial(_resize_relative_image,
1350
- input_folder=input_folder,
1351
- output_folder=output_folder,
1352
- target_width=target_width,
1353
- target_height=target_height,
1354
- no_enlarge_width=no_enlarge_width,
1355
- verbose=verbose,
1356
- quality=quality)
1357
-
1358
- results = list(tqdm(pool.imap(p, image_files_relative),total=len(image_files_relative)))
1359
-
1360
- return results
1361
-
1362
- # ...def resize_image_folder(...)
1363
-
1364
-
1365
- #%% Image integrity checking functions
1366
-
1367
- def check_image_integrity(filename,modes=None):
1368
- """
1369
- Check whether we can successfully load an image via OpenCV and/or PIL.
1370
-
1371
- Args:
1372
- filename (str): the filename to evaluate
1373
- modes (list, optional): a list containing one or more of:
1374
-
1375
- - 'cv'
1376
- - 'pil'
1377
- - 'skimage'
1378
- - 'jpeg_trailer'
1379
-
1380
- 'jpeg_trailer' checks that the binary data ends with ffd9. It does not check whether
1381
- the image is actually a jpeg, and even if it is, there are lots of reasons the image might not
1382
- end with ffd9. It's also true the JPEGs that cause "premature end of jpeg segment" issues
1383
- don't end with ffd9, so this may be a useful diagnostic. High precision, very low recall
1384
- for corrupt jpegs.
1385
-
1386
- Set to None to use all modes.
1387
-
1388
- Returns:
1389
- dict: a dict with a key called 'file' (the value of [filename]), one key for each string in
1390
- [modes] (a success indicator for that mode, specifically a string starting with either
1391
- 'success' or 'error').
1392
- """
1393
-
1394
- if modes is None:
1395
- modes = ('cv','pil','skimage','jpeg_trailer')
1396
- else:
1397
- if isinstance(modes,str):
1398
- modes = [modes]
1399
- for mode in modes:
1400
- assert mode in ('cv','pil','skimage'), 'Unrecognized mode {}'.format(mode)
1401
-
1402
- assert os.path.isfile(filename), 'Could not find file {}'.format(filename)
1403
-
1404
- result = {}
1405
- result['file'] = filename
1406
-
1407
- for mode in modes:
1408
-
1409
- result[mode] = 'unknown'
1410
- if mode == 'pil':
1411
- try:
1412
- pil_im = load_image(filename) # noqa
1413
- assert pil_im is not None
1414
- result[mode] = 'success'
1415
- except Exception as e:
1416
- result[mode] = 'error: {}'.format(str(e))
1417
- elif mode == 'cv':
1418
- try:
1419
- cv_im = cv2.imread(filename)
1420
- assert cv_im is not None, 'Unknown opencv read failure'
1421
- numpy_im = np.asarray(cv_im) # noqa
1422
- result[mode] = 'success'
1423
- except Exception as e:
1424
- result[mode] = 'error: {}'.format(str(e))
1425
- elif mode == 'skimage':
1426
- try:
1427
- # This is not a standard dependency
1428
- from skimage import io as skimage_io # noqa
1429
- except Exception:
1430
- result[mode] = 'could not import skimage, run pip install scikit-image'
1431
- return result
1432
- try:
1433
- skimage_im = skimage_io.imread(filename) # noqa
1434
- assert skimage_im is not None
1435
- result[mode] = 'success'
1436
- except Exception as e:
1437
- result[mode] = 'error: {}'.format(str(e))
1438
- elif mode == 'jpeg_trailer':
1439
- # https://stackoverflow.com/a/48282863/16644970
1440
- try:
1441
- with open(filename, 'rb') as f:
1442
- check_chars = f.read()[-2:]
1443
- if check_chars != b'\xff\xd9':
1444
- result[mode] = 'invalid jpeg trailer: {}'.format(str(check_chars))
1445
- else:
1446
- result[mode] = 'success'
1447
- except Exception as e:
1448
- result[mode] = 'error: {}'.format(str(e))
1449
-
1450
- # ...for each mode
1451
-
1452
- return result
1453
-
1454
- # ...def check_image_integrity(...)
1455
-
1456
-
1457
- def parallel_check_image_integrity(filenames,
1458
- modes=None,
1459
- max_workers=16,
1460
- use_threads=True,
1461
- recursive=True):
1462
- """
1463
- Check whether we can successfully load a list of images via OpenCV and/or PIL.
1464
-
1465
- Args:
1466
- filenames (list or str): a list of image filenames or a folder
1467
- mode (list): see check_image_integrity() for documentation on the [modes] parameter
1468
- max_workers (int, optional): the number of parallel workers to use; set to <=1 to disable
1469
- parallelization
1470
- use_threads (bool, optional): whether to use threads (True) or processes (False) for
1471
- parallelization
1472
- recursive (bool, optional): if [filenames] is a folder, whether to search recursively for images.
1473
- Ignored if [filenames] is a list.
1474
-
1475
- Returns:
1476
- list: a list of dicts, each with a key called 'file' (the value of [filename]), one key for
1477
- each string in [modes] (a success indicator for that mode, specifically a string starting
1478
- with either 'success' or 'error').
1479
- """
1480
-
1481
- n_workers = min(max_workers,len(filenames))
1482
-
1483
- if isinstance(filenames,str) and os.path.isdir(filenames):
1484
- filenames = find_images(filenames,recursive=recursive,return_relative_paths=False)
1485
-
1486
- print('Checking image integrity for {} filenames'.format(len(filenames)))
1487
-
1488
- if n_workers <= 1:
1489
-
1490
- results = []
1491
- for filename in filenames:
1492
- results.append(check_image_integrity(filename,modes=modes))
1493
-
1494
- else:
1495
-
1496
- if use_threads:
1497
- pool = ThreadPool(n_workers)
1498
- else:
1499
- pool = Pool(n_workers)
1500
-
1501
- results = list(tqdm(pool.imap(
1502
- partial(check_image_integrity,modes=modes),filenames), total=len(filenames)))
1503
-
1504
- return results
1505
-
1506
-
1507
- #%% Test drivers
1508
-
1509
- if False:
1510
-
1511
- #%% Recursive resize test
1512
-
1513
- from md_visualization.visualization_utils import resize_image_folder # noqa
1514
-
1515
- input_folder = r"C:\temp\resize-test\in"
1516
- output_folder = r"C:\temp\resize-test\out"
1517
-
1518
- resize_results = resize_image_folder(input_folder,output_folder,
1519
- target_width=1280,verbose=True,quality=85,no_enlarge_width=True,
1520
- pool_type='process',n_workers=10)
1521
-
1522
-
1523
- #%% Integrity checking test
1524
-
1525
- from md_utils import md_tests
1526
- options = md_tests.download_test_data()
1527
- folder = options.scratch_dir
1528
-
1529
- results = parallel_check_image_integrity(folder,max_workers=8)
1530
-
1531
- modes = ['cv','pil','skimage','jpeg_trailer']
1532
-
1533
- for r in results:
1534
- for mode in modes:
1535
- if r[mode] != 'success':
1536
- s = r[mode]
1537
- print('Mode {} failed for {}:\n{}\n'.format(mode,r['file'],s))