megadetector 5.0.11__py3-none-any.whl → 5.0.13__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 (203) hide show
  1. megadetector/api/__init__.py +0 -0
  2. megadetector/api/batch_processing/__init__.py +0 -0
  3. megadetector/api/batch_processing/api_core/__init__.py +0 -0
  4. megadetector/api/batch_processing/api_core/batch_service/__init__.py +0 -0
  5. megadetector/api/batch_processing/api_core/batch_service/score.py +439 -0
  6. megadetector/api/batch_processing/api_core/server.py +294 -0
  7. megadetector/api/batch_processing/api_core/server_api_config.py +97 -0
  8. megadetector/api/batch_processing/api_core/server_app_config.py +55 -0
  9. megadetector/api/batch_processing/api_core/server_batch_job_manager.py +220 -0
  10. megadetector/api/batch_processing/api_core/server_job_status_table.py +149 -0
  11. megadetector/api/batch_processing/api_core/server_orchestration.py +360 -0
  12. megadetector/api/batch_processing/api_core/server_utils.py +88 -0
  13. megadetector/api/batch_processing/api_core_support/__init__.py +0 -0
  14. megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +46 -0
  15. megadetector/api/batch_processing/api_support/__init__.py +0 -0
  16. megadetector/api/batch_processing/api_support/summarize_daily_activity.py +152 -0
  17. megadetector/api/batch_processing/data_preparation/__init__.py +0 -0
  18. megadetector/api/batch_processing/integration/digiKam/setup.py +6 -0
  19. megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +465 -0
  20. megadetector/api/batch_processing/integration/eMammal/test_scripts/config_template.py +5 -0
  21. megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +125 -0
  22. megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +55 -0
  23. megadetector/api/synchronous/__init__.py +0 -0
  24. megadetector/api/synchronous/api_core/animal_detection_api/__init__.py +0 -0
  25. megadetector/api/synchronous/api_core/animal_detection_api/api_backend.py +152 -0
  26. megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +263 -0
  27. megadetector/api/synchronous/api_core/animal_detection_api/config.py +35 -0
  28. megadetector/api/synchronous/api_core/tests/__init__.py +0 -0
  29. megadetector/api/synchronous/api_core/tests/load_test.py +110 -0
  30. megadetector/classification/__init__.py +0 -0
  31. megadetector/classification/aggregate_classifier_probs.py +108 -0
  32. megadetector/classification/analyze_failed_images.py +227 -0
  33. megadetector/classification/cache_batchapi_outputs.py +198 -0
  34. megadetector/classification/create_classification_dataset.py +627 -0
  35. megadetector/classification/crop_detections.py +516 -0
  36. megadetector/classification/csv_to_json.py +226 -0
  37. megadetector/classification/detect_and_crop.py +855 -0
  38. megadetector/classification/efficientnet/__init__.py +9 -0
  39. megadetector/classification/efficientnet/model.py +415 -0
  40. megadetector/classification/efficientnet/utils.py +607 -0
  41. megadetector/classification/evaluate_model.py +520 -0
  42. megadetector/classification/identify_mislabeled_candidates.py +152 -0
  43. megadetector/classification/json_to_azcopy_list.py +63 -0
  44. megadetector/classification/json_validator.py +699 -0
  45. megadetector/classification/map_classification_categories.py +276 -0
  46. megadetector/classification/merge_classification_detection_output.py +506 -0
  47. megadetector/classification/prepare_classification_script.py +194 -0
  48. megadetector/classification/prepare_classification_script_mc.py +228 -0
  49. megadetector/classification/run_classifier.py +287 -0
  50. megadetector/classification/save_mislabeled.py +110 -0
  51. megadetector/classification/train_classifier.py +827 -0
  52. megadetector/classification/train_classifier_tf.py +725 -0
  53. megadetector/classification/train_utils.py +323 -0
  54. megadetector/data_management/__init__.py +0 -0
  55. megadetector/data_management/annotations/__init__.py +0 -0
  56. megadetector/data_management/annotations/annotation_constants.py +34 -0
  57. megadetector/data_management/camtrap_dp_to_coco.py +237 -0
  58. megadetector/data_management/cct_json_utils.py +404 -0
  59. megadetector/data_management/cct_to_md.py +176 -0
  60. megadetector/data_management/cct_to_wi.py +289 -0
  61. megadetector/data_management/coco_to_labelme.py +283 -0
  62. megadetector/data_management/coco_to_yolo.py +662 -0
  63. megadetector/data_management/databases/__init__.py +0 -0
  64. megadetector/data_management/databases/add_width_and_height_to_db.py +33 -0
  65. megadetector/data_management/databases/combine_coco_camera_traps_files.py +206 -0
  66. megadetector/data_management/databases/integrity_check_json_db.py +493 -0
  67. megadetector/data_management/databases/subset_json_db.py +115 -0
  68. megadetector/data_management/generate_crops_from_cct.py +149 -0
  69. megadetector/data_management/get_image_sizes.py +189 -0
  70. megadetector/data_management/importers/add_nacti_sizes.py +52 -0
  71. megadetector/data_management/importers/add_timestamps_to_icct.py +79 -0
  72. megadetector/data_management/importers/animl_results_to_md_results.py +158 -0
  73. megadetector/data_management/importers/auckland_doc_test_to_json.py +373 -0
  74. megadetector/data_management/importers/auckland_doc_to_json.py +201 -0
  75. megadetector/data_management/importers/awc_to_json.py +191 -0
  76. megadetector/data_management/importers/bellevue_to_json.py +273 -0
  77. megadetector/data_management/importers/cacophony-thermal-importer.py +793 -0
  78. megadetector/data_management/importers/carrizo_shrubfree_2018.py +269 -0
  79. megadetector/data_management/importers/carrizo_trail_cam_2017.py +289 -0
  80. megadetector/data_management/importers/cct_field_adjustments.py +58 -0
  81. megadetector/data_management/importers/channel_islands_to_cct.py +913 -0
  82. megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +180 -0
  83. megadetector/data_management/importers/eMammal/eMammal_helpers.py +249 -0
  84. megadetector/data_management/importers/eMammal/make_eMammal_json.py +223 -0
  85. megadetector/data_management/importers/ena24_to_json.py +276 -0
  86. megadetector/data_management/importers/filenames_to_json.py +386 -0
  87. megadetector/data_management/importers/helena_to_cct.py +283 -0
  88. megadetector/data_management/importers/idaho-camera-traps.py +1407 -0
  89. megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +294 -0
  90. megadetector/data_management/importers/jb_csv_to_json.py +150 -0
  91. megadetector/data_management/importers/mcgill_to_json.py +250 -0
  92. megadetector/data_management/importers/missouri_to_json.py +490 -0
  93. megadetector/data_management/importers/nacti_fieldname_adjustments.py +79 -0
  94. megadetector/data_management/importers/noaa_seals_2019.py +181 -0
  95. megadetector/data_management/importers/pc_to_json.py +365 -0
  96. megadetector/data_management/importers/plot_wni_giraffes.py +123 -0
  97. megadetector/data_management/importers/prepare-noaa-fish-data-for-lila.py +359 -0
  98. megadetector/data_management/importers/prepare_zsl_imerit.py +131 -0
  99. megadetector/data_management/importers/rspb_to_json.py +356 -0
  100. megadetector/data_management/importers/save_the_elephants_survey_A.py +320 -0
  101. megadetector/data_management/importers/save_the_elephants_survey_B.py +329 -0
  102. megadetector/data_management/importers/snapshot_safari_importer.py +758 -0
  103. megadetector/data_management/importers/snapshot_safari_importer_reprise.py +665 -0
  104. megadetector/data_management/importers/snapshot_serengeti_lila.py +1067 -0
  105. megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +150 -0
  106. megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +153 -0
  107. megadetector/data_management/importers/sulross_get_exif.py +65 -0
  108. megadetector/data_management/importers/timelapse_csv_set_to_json.py +490 -0
  109. megadetector/data_management/importers/ubc_to_json.py +399 -0
  110. megadetector/data_management/importers/umn_to_json.py +507 -0
  111. megadetector/data_management/importers/wellington_to_json.py +263 -0
  112. megadetector/data_management/importers/wi_to_json.py +442 -0
  113. megadetector/data_management/importers/zamba_results_to_md_results.py +181 -0
  114. megadetector/data_management/labelme_to_coco.py +547 -0
  115. megadetector/data_management/labelme_to_yolo.py +272 -0
  116. megadetector/data_management/lila/__init__.py +0 -0
  117. megadetector/data_management/lila/add_locations_to_island_camera_traps.py +97 -0
  118. megadetector/data_management/lila/add_locations_to_nacti.py +147 -0
  119. megadetector/data_management/lila/create_lila_blank_set.py +558 -0
  120. megadetector/data_management/lila/create_lila_test_set.py +152 -0
  121. megadetector/data_management/lila/create_links_to_md_results_files.py +106 -0
  122. megadetector/data_management/lila/download_lila_subset.py +178 -0
  123. megadetector/data_management/lila/generate_lila_per_image_labels.py +516 -0
  124. megadetector/data_management/lila/get_lila_annotation_counts.py +170 -0
  125. megadetector/data_management/lila/get_lila_image_counts.py +112 -0
  126. megadetector/data_management/lila/lila_common.py +300 -0
  127. megadetector/data_management/lila/test_lila_metadata_urls.py +132 -0
  128. megadetector/data_management/ocr_tools.py +870 -0
  129. megadetector/data_management/read_exif.py +809 -0
  130. megadetector/data_management/remap_coco_categories.py +84 -0
  131. megadetector/data_management/remove_exif.py +66 -0
  132. megadetector/data_management/rename_images.py +187 -0
  133. megadetector/data_management/resize_coco_dataset.py +189 -0
  134. megadetector/data_management/wi_download_csv_to_coco.py +247 -0
  135. megadetector/data_management/yolo_output_to_md_output.py +446 -0
  136. megadetector/data_management/yolo_to_coco.py +676 -0
  137. megadetector/detection/__init__.py +0 -0
  138. megadetector/detection/detector_training/__init__.py +0 -0
  139. megadetector/detection/detector_training/model_main_tf2.py +114 -0
  140. megadetector/detection/process_video.py +846 -0
  141. megadetector/detection/pytorch_detector.py +355 -0
  142. megadetector/detection/run_detector.py +779 -0
  143. megadetector/detection/run_detector_batch.py +1219 -0
  144. megadetector/detection/run_inference_with_yolov5_val.py +1087 -0
  145. megadetector/detection/run_tiled_inference.py +934 -0
  146. megadetector/detection/tf_detector.py +192 -0
  147. megadetector/detection/video_utils.py +698 -0
  148. megadetector/postprocessing/__init__.py +0 -0
  149. megadetector/postprocessing/add_max_conf.py +64 -0
  150. megadetector/postprocessing/categorize_detections_by_size.py +165 -0
  151. megadetector/postprocessing/classification_postprocessing.py +716 -0
  152. megadetector/postprocessing/combine_api_outputs.py +249 -0
  153. megadetector/postprocessing/compare_batch_results.py +966 -0
  154. megadetector/postprocessing/convert_output_format.py +396 -0
  155. megadetector/postprocessing/load_api_results.py +195 -0
  156. megadetector/postprocessing/md_to_coco.py +310 -0
  157. megadetector/postprocessing/md_to_labelme.py +330 -0
  158. megadetector/postprocessing/merge_detections.py +412 -0
  159. megadetector/postprocessing/postprocess_batch_results.py +1908 -0
  160. megadetector/postprocessing/remap_detection_categories.py +170 -0
  161. megadetector/postprocessing/render_detection_confusion_matrix.py +660 -0
  162. megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +211 -0
  163. megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +83 -0
  164. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +1635 -0
  165. megadetector/postprocessing/separate_detections_into_folders.py +730 -0
  166. megadetector/postprocessing/subset_json_detector_output.py +700 -0
  167. megadetector/postprocessing/top_folders_to_bottom.py +223 -0
  168. megadetector/taxonomy_mapping/__init__.py +0 -0
  169. megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +491 -0
  170. megadetector/taxonomy_mapping/map_new_lila_datasets.py +150 -0
  171. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +142 -0
  172. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +588 -0
  173. megadetector/taxonomy_mapping/retrieve_sample_image.py +71 -0
  174. megadetector/taxonomy_mapping/simple_image_download.py +219 -0
  175. megadetector/taxonomy_mapping/species_lookup.py +834 -0
  176. megadetector/taxonomy_mapping/taxonomy_csv_checker.py +159 -0
  177. megadetector/taxonomy_mapping/taxonomy_graph.py +346 -0
  178. megadetector/taxonomy_mapping/validate_lila_category_mappings.py +83 -0
  179. megadetector/utils/__init__.py +0 -0
  180. megadetector/utils/azure_utils.py +178 -0
  181. megadetector/utils/ct_utils.py +613 -0
  182. megadetector/utils/directory_listing.py +246 -0
  183. megadetector/utils/md_tests.py +1164 -0
  184. megadetector/utils/path_utils.py +1045 -0
  185. megadetector/utils/process_utils.py +160 -0
  186. megadetector/utils/sas_blob_utils.py +509 -0
  187. megadetector/utils/split_locations_into_train_val.py +228 -0
  188. megadetector/utils/string_utils.py +92 -0
  189. megadetector/utils/url_utils.py +323 -0
  190. megadetector/utils/write_html_image_list.py +225 -0
  191. megadetector/visualization/__init__.py +0 -0
  192. megadetector/visualization/plot_utils.py +293 -0
  193. megadetector/visualization/render_images_with_thumbnails.py +275 -0
  194. megadetector/visualization/visualization_utils.py +1536 -0
  195. megadetector/visualization/visualize_db.py +552 -0
  196. megadetector/visualization/visualize_detector_output.py +405 -0
  197. {megadetector-5.0.11.dist-info → megadetector-5.0.13.dist-info}/LICENSE +0 -0
  198. {megadetector-5.0.11.dist-info → megadetector-5.0.13.dist-info}/METADATA +2 -2
  199. megadetector-5.0.13.dist-info/RECORD +201 -0
  200. megadetector-5.0.13.dist-info/top_level.txt +1 -0
  201. megadetector-5.0.11.dist-info/RECORD +0 -5
  202. megadetector-5.0.11.dist-info/top_level.txt +0 -1
  203. {megadetector-5.0.11.dist-info → megadetector-5.0.13.dist-info}/WHEEL +0 -0
@@ -0,0 +1,552 @@
1
+ """
2
+
3
+ visualize_db.py
4
+
5
+ Outputs an HTML page visualizing annotations (class labels and/or bounding boxes)
6
+ on a sample of images in a database in the COCO Camera Traps format.
7
+
8
+ """
9
+
10
+ #%% Imports
11
+
12
+ import argparse
13
+ import inspect
14
+ import random
15
+ import json
16
+ import math
17
+ import os
18
+ import sys
19
+ import time
20
+
21
+ import pandas as pd
22
+ import numpy as np
23
+
24
+ import humanfriendly
25
+
26
+ from itertools import compress
27
+ from multiprocessing.pool import ThreadPool
28
+ from multiprocessing.pool import Pool
29
+ from tqdm import tqdm
30
+
31
+ from megadetector.utils.write_html_image_list import write_html_image_list
32
+ from megadetector.data_management.cct_json_utils import IndexedJsonDb
33
+ from megadetector.visualization import visualization_utils as vis_utils
34
+
35
+
36
+ #%% Settings
37
+
38
+ class DbVizOptions:
39
+ """
40
+ Parameters controlling the behavior of visualize_db().
41
+ """
42
+
43
+ def __init__(self):
44
+
45
+ #: Number of images to sample from the database, or None to visualize all images
46
+ self.num_to_visualize = None
47
+
48
+ #: Target size for rendering; set either dimension to -1 to preserve aspect ratio.
49
+ #:
50
+ #: If viz_size is None or (-1,-1), the original image size is used.
51
+ self.viz_size = (800, -1)
52
+
53
+ #: HTML rendering options; see write_html_image_list for details
54
+ #:
55
+ #:The most relevant option one might want to set here is:
56
+ #:
57
+ #: htmlOptions['maxFiguresPerHtmlFile']
58
+ #:
59
+ #: ...which can be used to paginate previews to a number of images that will load well
60
+ #: in a browser (5000 is a reasonable limit).
61
+ self.htmlOptions = write_html_image_list()
62
+
63
+ #: Whether to sort images by filename (True) or randomly (False)
64
+ self.sort_by_filename = True
65
+
66
+ #: Only show images that contain bounding boxes
67
+ self.trim_to_images_with_bboxes = False
68
+
69
+ #: Random seed to use for sampling images
70
+ self.random_seed = 0
71
+
72
+ #: Should we include Web search links for each category name?
73
+ self.add_search_links = False
74
+
75
+ #: Should each thumbnail image link back to the original image?
76
+ self.include_image_links = False
77
+
78
+ #: Should there be a text link back to each original image?
79
+ self.include_filename_links = False
80
+
81
+ #: Line width in pixels
82
+ self.box_thickness = 4
83
+
84
+ #: Number of pixels to expand each bounding box
85
+ self.box_expansion = 0
86
+
87
+ #: Only include images that contain annotations with these class names (not IDs)
88
+ #:
89
+ #: Mutually exclusive with classes_to_exclude
90
+ self.classes_to_include = None
91
+
92
+ #: Exclude images that contain annotations with these class names (not IDs)
93
+ #:
94
+ #: Mutually exclusive with classes_to_include
95
+ self.classes_to_exclude = None
96
+
97
+ #: Special tag used to say "show me all images with multiple categories"
98
+ #:
99
+ #: :meta private:
100
+ self.multiple_categories_tag = '*multiple*'
101
+
102
+ #: We sometimes flatten image directories by replacing a path separator with
103
+ #: another character. Leave blank for the typical case where this isn't necessary.
104
+ self.pathsep_replacement = '' # '~'
105
+
106
+ #: Parallelize rendering across multiple workers
107
+ self.parallelize_rendering = False
108
+
109
+ #: In theory, whether to parallelize with threads (True) or processes (False), but
110
+ #: process-based parallelization in this function is currently unsupported
111
+ self.parallelize_rendering_with_threads = True
112
+
113
+ #: Number of workers to use for parallelization; ignored if parallelize_rendering
114
+ #: is False
115
+ self.parallelize_rendering_n_cores = 25
116
+
117
+ #: Should we show absolute (True) or relative (False) paths for each image?
118
+ self.show_full_paths = False
119
+
120
+ #: Set to False to skip existing images
121
+ self.force_rendering = True
122
+
123
+ #: Enable additionald debug console output
124
+ self.verbose = False
125
+
126
+
127
+ #%% Helper functions
128
+
129
+ def _image_filename_to_path(image_file_name, image_base_dir, pathsep_replacement=''):
130
+ """
131
+ Translates the file name in an image entry in the json database to a path, possibly doing
132
+ some manipulation of path separators.
133
+ """
134
+
135
+ if len(pathsep_replacement) > 0:
136
+ image_file_name = os.path.normpath(image_file_name).replace(os.pathsep,pathsep_replacement)
137
+ return os.path.join(image_base_dir, image_file_name)
138
+
139
+
140
+ #%% Core functions
141
+
142
+ def visualize_db(db_path, output_dir, image_base_dir, options=None):
143
+ """
144
+ Writes images and html to output_dir to visualize the annotations in a .json file.
145
+
146
+ Args:
147
+ db_path (str or dict): the .json filename to load, or a previously-loaded database
148
+ image_base_dir (str): the folder where the images live; filenames in [db_path] should
149
+ be relative to this folder.
150
+ options (DbVizOptions, optional): See DbVizOptions for details
151
+
152
+ Returns:
153
+ tuple: A length-two tuple containing (the html filename) and (the loaded database).
154
+ """
155
+
156
+ if options is None:
157
+ options = DbVizOptions()
158
+
159
+ if not options.parallelize_rendering_with_threads:
160
+ print('Warning: process-based parallelization is not yet supported by visualize_db')
161
+ options.parallelize_rendering_with_threads = True
162
+
163
+ if image_base_dir.startswith('http'):
164
+ if not image_base_dir.endswith('/'):
165
+ image_base_dir += '/'
166
+ else:
167
+ assert(os.path.isdir(image_base_dir))
168
+
169
+ os.makedirs(os.path.join(output_dir, 'rendered_images'), exist_ok=True)
170
+
171
+ if isinstance(db_path,str):
172
+ assert(os.path.isfile(db_path))
173
+ print('Loading database from {}...'.format(db_path))
174
+ image_db = json.load(open(db_path))
175
+ print('...done')
176
+ elif isinstance(db_path,dict):
177
+ print('Using previously-loaded DB')
178
+ image_db = db_path
179
+ else:
180
+ raise ValueError('Illegal dictionary or filename')
181
+
182
+ annotations = image_db['annotations']
183
+ images = image_db['images']
184
+ categories = image_db['categories']
185
+
186
+ # Optionally remove all images without bounding boxes, *before* sampling
187
+ if options.trim_to_images_with_bboxes:
188
+
189
+ bHasBbox = [False] * len(annotations)
190
+ for iAnn,ann in enumerate(annotations):
191
+ if 'bbox' in ann:
192
+ assert isinstance(ann['bbox'],list)
193
+ bHasBbox[iAnn] = True
194
+ annotationsWithBboxes = list(compress(annotations, bHasBbox))
195
+
196
+ imageIDsWithBboxes = [x['image_id'] for x in annotationsWithBboxes]
197
+ imageIDsWithBboxes = set(imageIDsWithBboxes)
198
+
199
+ bImageHasBbox = [False] * len(images)
200
+ for iImage,image in enumerate(images):
201
+ imageID = image['id']
202
+ if imageID in imageIDsWithBboxes:
203
+ bImageHasBbox[iImage] = True
204
+ imagesWithBboxes = list(compress(images, bImageHasBbox))
205
+ images = imagesWithBboxes
206
+
207
+ # Optionally include/remove images with specific labels, *before* sampling
208
+
209
+ assert (not ((options.classes_to_exclude is not None) and \
210
+ (options.classes_to_include is not None))), \
211
+ 'Cannot specify an inclusion and exclusion list'
212
+
213
+ if (options.classes_to_exclude is not None) or (options.classes_to_include is not None):
214
+
215
+ print('Indexing database')
216
+ indexed_db = IndexedJsonDb(image_db)
217
+ bValidClass = [True] * len(images)
218
+ for iImage,image in enumerate(images):
219
+ classes = indexed_db.get_classes_for_image(image)
220
+ if options.classes_to_exclude is not None:
221
+ for excludedClass in options.classes_to_exclude:
222
+ if excludedClass in classes:
223
+ bValidClass[iImage] = False
224
+ break
225
+ elif options.classes_to_include is not None:
226
+ bValidClass[iImage] = False
227
+ if options.multiple_categories_tag in options.classes_to_include:
228
+ if len(classes) > 1:
229
+ bValidClass[iImage] = True
230
+ if not bValidClass[iImage]:
231
+ for c in classes:
232
+ if c in options.classes_to_include:
233
+ bValidClass[iImage] = True
234
+ break
235
+ else:
236
+ raise ValueError('Illegal include/exclude combination')
237
+
238
+ imagesWithValidClasses = list(compress(images, bValidClass))
239
+ images = imagesWithValidClasses
240
+
241
+ # ...if we need to include/exclude categories
242
+
243
+ # Put the annotations in a dataframe so we can select all annotations for a given image
244
+ print('Creating data frames')
245
+ df_anno = pd.DataFrame(annotations)
246
+ df_img = pd.DataFrame(images)
247
+
248
+ # Construct label map
249
+ label_map = {}
250
+ for cat in categories:
251
+ label_map[int(cat['id'])] = cat['name']
252
+
253
+ # Take a sample of images
254
+ if options.num_to_visualize is not None:
255
+ if options.num_to_visualize > len(df_img):
256
+ print('Warning: asked to visualize {} images, but only {} are available, keeping them all'.\
257
+ format(options.num_to_visualize,len(df_img)))
258
+ else:
259
+ df_img = df_img.sample(n=options.num_to_visualize,random_state=options.random_seed)
260
+
261
+ images_html = []
262
+
263
+ # Set of dicts representing inputs to render_db_bounding_boxes:
264
+ #
265
+ # bboxes, boxClasses, image_path
266
+ rendering_info = []
267
+
268
+ print('Preparing rendering list')
269
+
270
+ for iImage,img in tqdm(df_img.iterrows(),total=len(df_img)):
271
+
272
+ img_id = img['id']
273
+ assert img_id is not None
274
+
275
+ img_relative_path = img['file_name']
276
+
277
+ if image_base_dir.startswith('http'):
278
+ img_path = image_base_dir + img_relative_path
279
+ else:
280
+ img_path = os.path.join(image_base_dir,
281
+ _image_filename_to_path(img_relative_path, image_base_dir))
282
+
283
+ annos_i = df_anno.loc[df_anno['image_id'] == img_id, :] # all annotations on this image
284
+
285
+ bboxes = []
286
+ boxClasses = []
287
+
288
+ # All the class labels we've seen for this image (with out without bboxes)
289
+ imageCategories = set()
290
+
291
+ annotationLevelForImage = ''
292
+
293
+ # Iterate over annotations for this image
294
+ # iAnn = 0; anno = annos_i.iloc[iAnn]
295
+ for iAnn,anno in annos_i.iterrows():
296
+
297
+ if 'sequence_level_annotation' in anno:
298
+ bSequenceLevelAnnotation = anno['sequence_level_annotation']
299
+ if bSequenceLevelAnnotation:
300
+ annLevel = 'sequence'
301
+ else:
302
+ annLevel = 'image'
303
+ if annotationLevelForImage == '':
304
+ annotationLevelForImage = annLevel
305
+ elif annotationLevelForImage != annLevel:
306
+ annotationLevelForImage = 'mixed'
307
+
308
+ categoryID = anno['category_id']
309
+ categoryName = label_map[categoryID]
310
+ if options.add_search_links:
311
+ categoryName = categoryName.replace('"','')
312
+ categoryName = '<a href="https://www.google.com/search?tbm=isch&q={}">{}</a>'.format(
313
+ categoryName,categoryName)
314
+ imageCategories.add(categoryName)
315
+
316
+ if 'bbox' in anno:
317
+ bbox = anno['bbox']
318
+ if isinstance(bbox,float):
319
+ assert math.isnan(bbox), "I shouldn't see a bbox that's neither a box nor NaN"
320
+ continue
321
+ bboxes.append(bbox)
322
+ boxClasses.append(anno['category_id'])
323
+
324
+ # ...for each of this image's annotations
325
+
326
+ imageClasses = ', '.join(imageCategories)
327
+
328
+ img_id_string = str(img_id).lower()
329
+ file_name = '{}_gt.jpg'.format(os.path.splitext(img_id_string)[0])
330
+
331
+ # Replace characters that muck up image links
332
+ illegal_characters = ['/','\\',':','\t','#',' ','%']
333
+ for c in illegal_characters:
334
+ file_name = file_name.replace(c,'~')
335
+
336
+ rendering_info.append({'bboxes':bboxes, 'boxClasses':boxClasses, 'img_path':img_path,
337
+ 'output_file_name':file_name})
338
+
339
+ labelLevelString = ' '
340
+ if len(annotationLevelForImage) > 0:
341
+ labelLevelString = ' (annotation level: {})'.format(annotationLevelForImage)
342
+
343
+ if 'frame_num' in img and 'seq_num_frames' in img:
344
+ frameString = ' frame: {} of {}, '.format(img['frame_num'],img['seq_num_frames'])
345
+ elif 'frame_num' in img:
346
+ frameString = ' frame: {}, '.format(img['frame_num'])
347
+ else:
348
+ frameString = ' '
349
+
350
+ if options.show_full_paths:
351
+ filename_text = img_path
352
+ else:
353
+ filename_text = img_relative_path
354
+ if options.include_filename_links:
355
+ filename_text = '<a href="{}">{}</a>'.format(img_path,filename_text)
356
+
357
+ flagString = ''
358
+
359
+ def isnan(x):
360
+ return (isinstance(x,float) and np.isnan(x))
361
+
362
+ if ('flags' in img) and (not isnan(img['flags'])):
363
+ flagString = ', flags: {}'.format(str(img['flags']))
364
+
365
+ # We're adding html for an image before we render it, so it's possible this image will
366
+ # fail to render. For applications where this script is being used to debua a database
367
+ # (the common case?), this is useful behavior, for other applications, this is annoying.
368
+ image_dict = \
369
+ {
370
+ 'filename': '{}/{}'.format('rendered_images', file_name),
371
+ 'title': '{}<br/>{}, num boxes: {}, {}class labels: {}{}{}'.format(
372
+ filename_text, img_id, len(bboxes), frameString, imageClasses, labelLevelString, flagString),
373
+ 'textStyle': 'font-family:verdana,arial,calibri;font-size:80%;' + \
374
+ 'text-align:left;margin-top:20;margin-bottom:5'
375
+ }
376
+ if options.include_image_links:
377
+ image_dict['linkTarget'] = img_path
378
+
379
+ images_html.append(image_dict)
380
+
381
+ # ...for each image
382
+
383
+ def render_image_info(rendering_info):
384
+
385
+ img_path = rendering_info['img_path']
386
+ bboxes = rendering_info['bboxes']
387
+ bboxClasses = rendering_info['boxClasses']
388
+ output_file_name = rendering_info['output_file_name']
389
+ output_full_path = os.path.join(output_dir, 'rendered_images', output_file_name)
390
+
391
+ if (os.path.isfile(output_full_path)) and (not options.force_rendering):
392
+ if options.verbose:
393
+ print('Skipping existing image {}'.format(output_full_path))
394
+ return True
395
+
396
+ if not img_path.startswith('http'):
397
+ if not os.path.exists(img_path):
398
+ print('Image {} cannot be found'.format(img_path))
399
+ return False
400
+
401
+ try:
402
+ original_image = vis_utils.open_image(img_path)
403
+ original_size = original_image.size
404
+ if (options.viz_size is None) or (options.viz_size[0] == -1 and options.viz_size[1] == -1):
405
+ image = original_image
406
+ else:
407
+ image = vis_utils.resize_image(original_image, options.viz_size[0],
408
+ options.viz_size[1])
409
+ except Exception as e:
410
+ print('Image {} failed to open, error: {}'.format(img_path, e))
411
+ return False
412
+
413
+ vis_utils.render_db_bounding_boxes(boxes=bboxes, classes=bboxClasses,
414
+ image=image, original_size=original_size,
415
+ label_map=label_map,
416
+ thickness=options.box_thickness,
417
+ expansion=options.box_expansion)
418
+
419
+ image.save(output_full_path)
420
+
421
+ return True
422
+
423
+ # ...def render_image_info
424
+
425
+ print('Rendering images')
426
+ start_time = time.time()
427
+
428
+ if options.parallelize_rendering:
429
+
430
+ if options.parallelize_rendering_with_threads:
431
+ worker_string = 'threads'
432
+ else:
433
+ worker_string = 'processes'
434
+
435
+ if options.parallelize_rendering_n_cores is None:
436
+ if options.parallelize_rendering_with_threads:
437
+ pool = ThreadPool()
438
+ else:
439
+ pool = Pool()
440
+ else:
441
+ if options.parallelize_rendering_with_threads:
442
+ pool = ThreadPool(options.parallelize_rendering_n_cores)
443
+ else:
444
+ pool = Pool(options.parallelize_rendering_n_cores)
445
+ print('Rendering images with {} {}'.format(options.parallelize_rendering_n_cores,
446
+ worker_string))
447
+ rendering_success = list(tqdm(pool.imap(render_image_info, rendering_info),
448
+ total=len(rendering_info)))
449
+
450
+ else:
451
+
452
+ rendering_success = []
453
+ for file_info in tqdm(rendering_info):
454
+ rendering_success.append(render_image_info(file_info))
455
+
456
+ elapsed = time.time() - start_time
457
+
458
+ print('Rendered {} images in {} ({} successful)'.format(
459
+ len(rendering_info),humanfriendly.format_timespan(elapsed),sum(rendering_success)))
460
+
461
+ if options.sort_by_filename:
462
+ images_html = sorted(images_html, key=lambda x: x['filename'])
463
+ else:
464
+ random.shuffle(images_html)
465
+
466
+ htmlOutputFile = os.path.join(output_dir, 'index.html')
467
+
468
+ htmlOptions = options.htmlOptions
469
+ if isinstance(db_path,str):
470
+ htmlOptions['headerHtml'] = '<h1>Sample annotations from {}</h1>'.format(db_path)
471
+ else:
472
+ htmlOptions['headerHtml'] = '<h1>Sample annotations</h1>'
473
+
474
+ write_html_image_list(
475
+ filename=htmlOutputFile,
476
+ images=images_html,
477
+ options=htmlOptions)
478
+
479
+ print('Visualized {} images, wrote results to {}'.format(len(images_html),htmlOutputFile))
480
+
481
+ return htmlOutputFile,image_db
482
+
483
+ # def visualize_db(...)
484
+
485
+
486
+ #%% Command-line driver
487
+
488
+ # Copy all fields from a Namespace (i.e., the output from parse_args) to an object.
489
+ #
490
+ # Skips fields starting with _. Does not check existence in the target object.
491
+ def args_to_object(args, obj):
492
+
493
+ for n, v in inspect.getmembers(args):
494
+ if not n.startswith('_'):
495
+ setattr(obj, n, v)
496
+
497
+
498
+ def main():
499
+
500
+ parser = argparse.ArgumentParser()
501
+ parser.add_argument('db_path', action='store', type=str,
502
+ help='.json file to visualize')
503
+ parser.add_argument('output_dir', action='store', type=str,
504
+ help='Output directory for html and rendered images')
505
+ parser.add_argument('image_base_dir', action='store', type=str,
506
+ help='Base directory (or URL) for input images')
507
+
508
+ parser.add_argument('--num_to_visualize', action='store', type=int, default=None,
509
+ help='Number of images to visualize (randomly drawn) (defaults to all)')
510
+ parser.add_argument('--random_sort', action='store_true',
511
+ help='Sort randomly (rather than by filename) in output html')
512
+ parser.add_argument('--trim_to_images_with_bboxes', action='store_true',
513
+ help='Only include images with bounding boxes (defaults to false)')
514
+ parser.add_argument('--random_seed', action='store', type=int, default=None,
515
+ help='Random seed for image selection')
516
+ parser.add_argument('--pathsep_replacement', action='store', type=str, default='',
517
+ help='Replace path separators in relative filenames with another ' + \
518
+ 'character (frequently ~)')
519
+
520
+ if len(sys.argv[1:]) == 0:
521
+ parser.print_help()
522
+ parser.exit()
523
+
524
+ args = parser.parse_args()
525
+
526
+ # Convert to an options object
527
+ options = DbVizOptions()
528
+ args_to_object(args, options)
529
+ if options.random_sort:
530
+ options.sort_by_filename = False
531
+
532
+ visualize_db(options.db_path,options.output_dir,options.image_base_dir,options)
533
+
534
+ if __name__ == '__main__':
535
+ main()
536
+
537
+
538
+ #%% Interactive driver
539
+
540
+ if False:
541
+
542
+ #%%
543
+
544
+ db_path = r'e:\wildlife_data\missouri_camera_traps\missouri_camera_traps_set1.json'
545
+ output_dir = r'e:\wildlife_data\missouri_camera_traps\preview'
546
+ image_base_dir = r'e:\wildlife_data\missouri_camera_traps'
547
+
548
+ options = DbVizOptions()
549
+ options.num_to_visualize = 100
550
+
551
+ htmlOutputFile,db = visualize_db(db_path,output_dir,image_base_dir,options)
552
+ # os.startfile(htmlOutputFile)