megadetector 10.0.15__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.
Files changed (147) hide show
  1. megadetector/__init__.py +0 -0
  2. megadetector/api/__init__.py +0 -0
  3. megadetector/api/batch_processing/integration/digiKam/setup.py +6 -0
  4. megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +465 -0
  5. megadetector/api/batch_processing/integration/eMammal/test_scripts/config_template.py +5 -0
  6. megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +125 -0
  7. megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +55 -0
  8. megadetector/classification/__init__.py +0 -0
  9. megadetector/classification/aggregate_classifier_probs.py +108 -0
  10. megadetector/classification/analyze_failed_images.py +227 -0
  11. megadetector/classification/cache_batchapi_outputs.py +198 -0
  12. megadetector/classification/create_classification_dataset.py +626 -0
  13. megadetector/classification/crop_detections.py +516 -0
  14. megadetector/classification/csv_to_json.py +226 -0
  15. megadetector/classification/detect_and_crop.py +853 -0
  16. megadetector/classification/efficientnet/__init__.py +9 -0
  17. megadetector/classification/efficientnet/model.py +415 -0
  18. megadetector/classification/efficientnet/utils.py +608 -0
  19. megadetector/classification/evaluate_model.py +520 -0
  20. megadetector/classification/identify_mislabeled_candidates.py +152 -0
  21. megadetector/classification/json_to_azcopy_list.py +63 -0
  22. megadetector/classification/json_validator.py +696 -0
  23. megadetector/classification/map_classification_categories.py +276 -0
  24. megadetector/classification/merge_classification_detection_output.py +509 -0
  25. megadetector/classification/prepare_classification_script.py +194 -0
  26. megadetector/classification/prepare_classification_script_mc.py +228 -0
  27. megadetector/classification/run_classifier.py +287 -0
  28. megadetector/classification/save_mislabeled.py +110 -0
  29. megadetector/classification/train_classifier.py +827 -0
  30. megadetector/classification/train_classifier_tf.py +725 -0
  31. megadetector/classification/train_utils.py +323 -0
  32. megadetector/data_management/__init__.py +0 -0
  33. megadetector/data_management/animl_to_md.py +161 -0
  34. megadetector/data_management/annotations/__init__.py +0 -0
  35. megadetector/data_management/annotations/annotation_constants.py +33 -0
  36. megadetector/data_management/camtrap_dp_to_coco.py +270 -0
  37. megadetector/data_management/cct_json_utils.py +566 -0
  38. megadetector/data_management/cct_to_md.py +184 -0
  39. megadetector/data_management/cct_to_wi.py +293 -0
  40. megadetector/data_management/coco_to_labelme.py +284 -0
  41. megadetector/data_management/coco_to_yolo.py +701 -0
  42. megadetector/data_management/databases/__init__.py +0 -0
  43. megadetector/data_management/databases/add_width_and_height_to_db.py +107 -0
  44. megadetector/data_management/databases/combine_coco_camera_traps_files.py +210 -0
  45. megadetector/data_management/databases/integrity_check_json_db.py +563 -0
  46. megadetector/data_management/databases/subset_json_db.py +195 -0
  47. megadetector/data_management/generate_crops_from_cct.py +200 -0
  48. megadetector/data_management/get_image_sizes.py +164 -0
  49. megadetector/data_management/labelme_to_coco.py +559 -0
  50. megadetector/data_management/labelme_to_yolo.py +349 -0
  51. megadetector/data_management/lila/__init__.py +0 -0
  52. megadetector/data_management/lila/create_lila_blank_set.py +556 -0
  53. megadetector/data_management/lila/create_lila_test_set.py +192 -0
  54. megadetector/data_management/lila/create_links_to_md_results_files.py +106 -0
  55. megadetector/data_management/lila/download_lila_subset.py +182 -0
  56. megadetector/data_management/lila/generate_lila_per_image_labels.py +777 -0
  57. megadetector/data_management/lila/get_lila_annotation_counts.py +174 -0
  58. megadetector/data_management/lila/get_lila_image_counts.py +112 -0
  59. megadetector/data_management/lila/lila_common.py +319 -0
  60. megadetector/data_management/lila/test_lila_metadata_urls.py +164 -0
  61. megadetector/data_management/mewc_to_md.py +344 -0
  62. megadetector/data_management/ocr_tools.py +873 -0
  63. megadetector/data_management/read_exif.py +964 -0
  64. megadetector/data_management/remap_coco_categories.py +195 -0
  65. megadetector/data_management/remove_exif.py +156 -0
  66. megadetector/data_management/rename_images.py +194 -0
  67. megadetector/data_management/resize_coco_dataset.py +665 -0
  68. megadetector/data_management/speciesnet_to_md.py +41 -0
  69. megadetector/data_management/wi_download_csv_to_coco.py +247 -0
  70. megadetector/data_management/yolo_output_to_md_output.py +594 -0
  71. megadetector/data_management/yolo_to_coco.py +984 -0
  72. megadetector/data_management/zamba_to_md.py +188 -0
  73. megadetector/detection/__init__.py +0 -0
  74. megadetector/detection/change_detection.py +840 -0
  75. megadetector/detection/process_video.py +479 -0
  76. megadetector/detection/pytorch_detector.py +1451 -0
  77. megadetector/detection/run_detector.py +1267 -0
  78. megadetector/detection/run_detector_batch.py +2172 -0
  79. megadetector/detection/run_inference_with_yolov5_val.py +1314 -0
  80. megadetector/detection/run_md_and_speciesnet.py +1604 -0
  81. megadetector/detection/run_tiled_inference.py +1044 -0
  82. megadetector/detection/tf_detector.py +209 -0
  83. megadetector/detection/video_utils.py +1379 -0
  84. megadetector/postprocessing/__init__.py +0 -0
  85. megadetector/postprocessing/add_max_conf.py +72 -0
  86. megadetector/postprocessing/categorize_detections_by_size.py +166 -0
  87. megadetector/postprocessing/classification_postprocessing.py +1943 -0
  88. megadetector/postprocessing/combine_batch_outputs.py +249 -0
  89. megadetector/postprocessing/compare_batch_results.py +2110 -0
  90. megadetector/postprocessing/convert_output_format.py +403 -0
  91. megadetector/postprocessing/create_crop_folder.py +629 -0
  92. megadetector/postprocessing/detector_calibration.py +570 -0
  93. megadetector/postprocessing/generate_csv_report.py +522 -0
  94. megadetector/postprocessing/load_api_results.py +223 -0
  95. megadetector/postprocessing/md_to_coco.py +428 -0
  96. megadetector/postprocessing/md_to_labelme.py +351 -0
  97. megadetector/postprocessing/md_to_wi.py +41 -0
  98. megadetector/postprocessing/merge_detections.py +392 -0
  99. megadetector/postprocessing/postprocess_batch_results.py +2140 -0
  100. megadetector/postprocessing/remap_detection_categories.py +226 -0
  101. megadetector/postprocessing/render_detection_confusion_matrix.py +677 -0
  102. megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +206 -0
  103. megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +82 -0
  104. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +1665 -0
  105. megadetector/postprocessing/separate_detections_into_folders.py +795 -0
  106. megadetector/postprocessing/subset_json_detector_output.py +964 -0
  107. megadetector/postprocessing/top_folders_to_bottom.py +238 -0
  108. megadetector/postprocessing/validate_batch_results.py +332 -0
  109. megadetector/taxonomy_mapping/__init__.py +0 -0
  110. megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +491 -0
  111. megadetector/taxonomy_mapping/map_new_lila_datasets.py +211 -0
  112. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +165 -0
  113. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +543 -0
  114. megadetector/taxonomy_mapping/retrieve_sample_image.py +71 -0
  115. megadetector/taxonomy_mapping/simple_image_download.py +231 -0
  116. megadetector/taxonomy_mapping/species_lookup.py +1008 -0
  117. megadetector/taxonomy_mapping/taxonomy_csv_checker.py +159 -0
  118. megadetector/taxonomy_mapping/taxonomy_graph.py +346 -0
  119. megadetector/taxonomy_mapping/validate_lila_category_mappings.py +83 -0
  120. megadetector/tests/__init__.py +0 -0
  121. megadetector/tests/test_nms_synthetic.py +335 -0
  122. megadetector/utils/__init__.py +0 -0
  123. megadetector/utils/ct_utils.py +1857 -0
  124. megadetector/utils/directory_listing.py +199 -0
  125. megadetector/utils/extract_frames_from_video.py +307 -0
  126. megadetector/utils/gpu_test.py +125 -0
  127. megadetector/utils/md_tests.py +2072 -0
  128. megadetector/utils/path_utils.py +2872 -0
  129. megadetector/utils/process_utils.py +172 -0
  130. megadetector/utils/split_locations_into_train_val.py +237 -0
  131. megadetector/utils/string_utils.py +234 -0
  132. megadetector/utils/url_utils.py +825 -0
  133. megadetector/utils/wi_platform_utils.py +968 -0
  134. megadetector/utils/wi_taxonomy_utils.py +1766 -0
  135. megadetector/utils/write_html_image_list.py +239 -0
  136. megadetector/visualization/__init__.py +0 -0
  137. megadetector/visualization/plot_utils.py +309 -0
  138. megadetector/visualization/render_images_with_thumbnails.py +243 -0
  139. megadetector/visualization/visualization_utils.py +1973 -0
  140. megadetector/visualization/visualize_db.py +630 -0
  141. megadetector/visualization/visualize_detector_output.py +498 -0
  142. megadetector/visualization/visualize_video_output.py +705 -0
  143. megadetector-10.0.15.dist-info/METADATA +115 -0
  144. megadetector-10.0.15.dist-info/RECORD +147 -0
  145. megadetector-10.0.15.dist-info/WHEEL +5 -0
  146. megadetector-10.0.15.dist-info/licenses/LICENSE +19 -0
  147. megadetector-10.0.15.dist-info/top_level.txt +1 -0
@@ -0,0 +1,630 @@
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
+ def _isnan(x):
36
+ return (isinstance(x,float) and np.isnan(x))
37
+
38
+
39
+ #%% Settings
40
+
41
+ class DbVizOptions:
42
+ """
43
+ Parameters controlling the behavior of visualize_db().
44
+ """
45
+
46
+ def __init__(self):
47
+
48
+ #: Number of images to sample from the database, or None to visualize all images
49
+ self.num_to_visualize = None
50
+
51
+ #: Target size for rendering; set either dimension to -1 to preserve aspect ratio.
52
+ #:
53
+ #: If viz_size is None or (-1,-1), the original image size is used.
54
+ self.viz_size = (1000, -1)
55
+
56
+ #: HTML rendering options; see write_html_image_list for details
57
+ #:
58
+ #:The most relevant option one might want to set here is:
59
+ #:
60
+ #: html_options['maxFiguresPerHtmlFile']
61
+ #:
62
+ #: ...which can be used to paginate previews to a number of images that will load well
63
+ #: in a browser (5000 is a reasonable limit).
64
+ self.html_options = write_html_image_list()
65
+
66
+ #: Whether to sort images by filename (True) or randomly (False)
67
+ self.sort_by_filename = True
68
+
69
+ #: Only show images that contain bounding boxes
70
+ self.trim_to_images_with_bboxes = False
71
+
72
+ #: Random seed to use for sampling images
73
+ self.random_seed = 0
74
+
75
+ #: Should we include Web search links for each category name?
76
+ self.add_search_links = False
77
+
78
+ #: Should each thumbnail image link back to the original image?
79
+ self.include_image_links = False
80
+
81
+ #: Should there be a text link back to each original image?
82
+ self.include_filename_links = False
83
+
84
+ #: Line width in pixels
85
+ self.box_thickness = 4
86
+
87
+ #: Number of pixels to expand each bounding box
88
+ self.box_expansion = 0
89
+
90
+ #: Only include images that contain annotations with these class names (not IDs) (list)
91
+ #:
92
+ #: Mutually exclusive with classes_to_exclude
93
+ self.classes_to_include = None
94
+
95
+ #: Exclude images that contain annotations with these class names (not IDs) (list)
96
+ #:
97
+ #: Mutually exclusive with classes_to_include
98
+ self.classes_to_exclude = None
99
+
100
+ #: Special tag used to say "show me all images with multiple categories"
101
+ #:
102
+ #: :meta private:
103
+ self.multiple_categories_tag = '*multiple*'
104
+
105
+ #: Parallelize rendering across multiple workers
106
+ self.parallelize_rendering = False
107
+
108
+ #: In theory, whether to parallelize with threads (True) or processes (False), but
109
+ #: process-based parallelization in this function is currently unsupported
110
+ self.parallelize_rendering_with_threads = True
111
+
112
+ #: Number of workers to use for parallelization; ignored if parallelize_rendering
113
+ #: is False
114
+ self.parallelize_rendering_n_cores = 16
115
+
116
+ #: Should we show absolute (True) or relative (False) paths for each image?
117
+ self.show_full_paths = False
118
+
119
+ #: List of additional fields in the image struct that we should print in image headers
120
+ self.extra_image_fields_to_print = None
121
+
122
+ #: List of additional fields in the annotation struct that we should print in image headers
123
+ self.extra_annotation_fields_to_print = None
124
+
125
+ #: Set to False to skip existing images
126
+ self.force_rendering = True
127
+
128
+ #: Enable additionald debug console output
129
+ self.verbose = False
130
+
131
+ #: COCO files used for evaluation may contain confidence scores, this
132
+ #: determines the field name used for confidence scores
133
+ self.confidence_field_name = 'score'
134
+
135
+ #: Optionally apply a confidence threshold; this requires that [confidence_field_name]
136
+ #: be present in all detections.
137
+ self.confidence_threshold = None
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 images and annotations in a
145
+ COCO-formatted .json file.
146
+
147
+ Args:
148
+ db_path (str or dict): the .json filename to load, or a previously-loaded database
149
+ output_dir (str): the folder to which we should write annotated images
150
+ image_base_dir (str): the folder where the images live; filenames in [db_path] should
151
+ be relative to this folder.
152
+ options (DbVizOptions, optional): See DbVizOptions for details
153
+
154
+ Returns:
155
+ tuple: A length-two tuple containing (the html filename) and (the loaded database).
156
+ """
157
+
158
+ if options is None:
159
+ options = DbVizOptions()
160
+
161
+ # Consistency checking for fields with specific format requirements
162
+
163
+ # These should be a lists, but if someone specifies a string, do a reasonable thing
164
+ if isinstance(options.extra_image_fields_to_print,str):
165
+ options.extra_image_fields_to_print = [options.extra_image_fields_to_print]
166
+ if isinstance(options.extra_annotation_fields_to_print,str):
167
+ options.extra_annotation_fields_to_print = [options.extra_annotation_fields_to_print]
168
+
169
+ if not options.parallelize_rendering_with_threads:
170
+ print('Warning: process-based parallelization is not yet supported by visualize_db')
171
+ options.parallelize_rendering_with_threads = True
172
+
173
+ if image_base_dir.startswith('http'):
174
+ if not image_base_dir.endswith('/'):
175
+ image_base_dir += '/'
176
+ else:
177
+ assert(os.path.isdir(image_base_dir))
178
+
179
+ os.makedirs(os.path.join(output_dir, 'rendered_images'), exist_ok=True)
180
+
181
+ if isinstance(db_path,str):
182
+ assert(os.path.isfile(db_path))
183
+ print('Loading database from {}...'.format(db_path))
184
+ image_db = json.load(open(db_path))
185
+ print('...done, loaded {} images'.format(len(image_db['images'])))
186
+ elif isinstance(db_path,dict):
187
+ print('Using previously-loaded DB')
188
+ image_db = db_path
189
+ else:
190
+ raise ValueError('Illegal dictionary or filename')
191
+
192
+ annotations = image_db['annotations']
193
+ images = image_db['images']
194
+ categories = image_db['categories']
195
+
196
+ # Optionally remove all images without bounding boxes, *before* sampling
197
+ if options.trim_to_images_with_bboxes:
198
+
199
+ b_has_bbox = [False] * len(annotations)
200
+ for i_ann,ann in enumerate(annotations):
201
+ if 'bbox' in ann or 'bbox_relative' in ann:
202
+ if 'bbox' in ann:
203
+ assert isinstance(ann['bbox'],list)
204
+ else:
205
+ assert isinstance(ann['bbox_relative'],list)
206
+ b_has_bbox[i_ann] = True
207
+ annotations_with_boxes = list(compress(annotations, b_has_bbox))
208
+
209
+ image_ids_with_boxes = [x['image_id'] for x in annotations_with_boxes]
210
+ image_ids_with_boxes = set(image_ids_with_boxes)
211
+
212
+ image_has_box = [False] * len(images)
213
+ for i_image,image in enumerate(images):
214
+ image_id = image['id']
215
+ if image_id in image_ids_with_boxes:
216
+ image_has_box[i_image] = True
217
+ images_with_bboxes = list(compress(images, image_has_box))
218
+ images = images_with_bboxes
219
+
220
+ # Optionally include/remove images with specific labels, *before* sampling
221
+
222
+ assert (not ((options.classes_to_exclude is not None) and \
223
+ (options.classes_to_include is not None))), \
224
+ 'Cannot specify an inclusion and exclusion list'
225
+
226
+ if options.classes_to_exclude is not None:
227
+ assert isinstance(options.classes_to_exclude,list), \
228
+ 'If supplied, classes_to_exclude should be a list'
229
+
230
+ if options.classes_to_include is not None:
231
+ assert isinstance(options.classes_to_include,list), \
232
+ 'If supplied, classes_to_include should be a list'
233
+
234
+ if (options.classes_to_exclude is not None) or (options.classes_to_include is not None):
235
+
236
+ print('Indexing database')
237
+ indexed_db = IndexedJsonDb(image_db)
238
+ b_valid_class = [True] * len(images)
239
+ for i_image,image in enumerate(images):
240
+ classes = indexed_db.get_classes_for_image(image)
241
+ if options.classes_to_exclude is not None:
242
+ for excluded_class in options.classes_to_exclude:
243
+ if excluded_class in classes:
244
+ b_valid_class[i_image] = False
245
+ break
246
+ elif options.classes_to_include is not None:
247
+ b_valid_class[i_image] = False
248
+ if options.multiple_categories_tag in options.classes_to_include:
249
+ if len(classes) > 1:
250
+ b_valid_class[i_image] = True
251
+ if not b_valid_class[i_image]:
252
+ for c in classes:
253
+ if c in options.classes_to_include:
254
+ b_valid_class[i_image] = True
255
+ break
256
+ else:
257
+ raise ValueError('Illegal include/exclude combination')
258
+
259
+ images_with_valid_classes = list(compress(images, b_valid_class))
260
+ images = images_with_valid_classes
261
+
262
+ # ...if we need to include/exclude categories
263
+
264
+ # Put the annotations in a dataframe so we can select all annotations for a given image
265
+ print('Creating data frames')
266
+ df_anno = pd.DataFrame(annotations)
267
+ df_img = pd.DataFrame(images)
268
+
269
+ # Construct label map
270
+ label_map = {}
271
+ for cat in categories:
272
+ label_map[int(cat['id'])] = cat['name']
273
+
274
+ # Take a sample of images
275
+ if options.num_to_visualize is not None:
276
+ if options.num_to_visualize > len(df_img):
277
+ print('Warning: asked to visualize {} images, but only {} are available, keeping them all'.\
278
+ format(options.num_to_visualize,len(df_img)))
279
+ else:
280
+ df_img = df_img.sample(n=options.num_to_visualize,random_state=options.random_seed)
281
+
282
+ images_html = []
283
+
284
+ # Set of dicts representing inputs to render_db_bounding_boxes:
285
+ #
286
+ # bboxes, box_classes, image_path
287
+ rendering_info = []
288
+
289
+ print('Preparing rendering list')
290
+
291
+ for i_image,img in tqdm(df_img.iterrows(),total=len(df_img)):
292
+
293
+ img_id = img['id']
294
+ assert img_id is not None
295
+
296
+ img_relative_path = img['file_name']
297
+
298
+ if image_base_dir.startswith('http'):
299
+ img_path = image_base_dir + img_relative_path
300
+ else:
301
+ img_path = os.path.join(image_base_dir,img_relative_path).replace('\\','/')
302
+
303
+ annos_i = df_anno.loc[df_anno['image_id'] == img_id, :] # all annotations on this image
304
+
305
+ bboxes = []
306
+ box_classes = []
307
+ box_score_strings = []
308
+
309
+ # All the class labels we've seen for this image (with or without bboxes)
310
+ image_categories = set()
311
+
312
+ extra_annotation_field_string = ''
313
+ annotation_level_for_image = ''
314
+
315
+ # Did this image come with already-normalized bounding boxes?
316
+ boxes_are_normalized = None
317
+
318
+ # Iterate over annotations for this image
319
+ # i_ann = 0; anno = annos_i.iloc[i_ann]
320
+ for i_ann,anno in annos_i.iterrows():
321
+
322
+ if options.extra_annotation_fields_to_print is not None:
323
+ field_names = list(anno.index)
324
+ for field_name in field_names:
325
+ if field_name in options.extra_annotation_fields_to_print:
326
+ field_value = anno[field_name]
327
+ if (field_value is not None) and (not _isnan(field_value)):
328
+ extra_annotation_field_string += ' ({}:{})'.format(
329
+ field_name,field_value)
330
+
331
+ if options.confidence_threshold is not None:
332
+ assert options.confidence_field_name in anno, \
333
+ 'Error: confidence thresholding requested, ' + \
334
+ 'but at least one annotation does not have the {} field'.format(
335
+ options.confidence_field_name)
336
+ if anno[options.confidence_field_name] < options.confidence_threshold:
337
+ continue
338
+
339
+ if 'sequence_level_annotation' in anno:
340
+ b_sequence_level_annotation = anno['sequence_level_annotation']
341
+ if b_sequence_level_annotation:
342
+ annotation_level = 'sequence'
343
+ else:
344
+ annotation_level = 'image'
345
+ if annotation_level_for_image == '':
346
+ annotation_level_for_image = annotation_level
347
+ elif annotation_level_for_image != annotation_level:
348
+ annotation_level_for_image = 'mixed'
349
+
350
+ category_id = anno['category_id']
351
+ category_name = label_map[category_id]
352
+ if options.add_search_links:
353
+ category_name = category_name.replace('"','')
354
+ category_name = '<a href="https://www.google.com/search?tbm=isch&q={}">{}</a>'.format(
355
+ category_name,category_name)
356
+
357
+ image_categories.add(category_name)
358
+
359
+ assert not ('bbox' in anno and 'bbox_relative' in anno), \
360
+ "An annotation can't have both an absolute and a relative bounding box"
361
+
362
+ box_field = 'bbox'
363
+ if 'bbox_relative' in anno:
364
+ box_field = 'bbox_relative'
365
+ assert (boxes_are_normalized is None) or (boxes_are_normalized), \
366
+ "An image can't have both absolute and relative bounding boxes"
367
+ boxes_are_normalized = True
368
+ elif 'bbox' in anno:
369
+ assert (boxes_are_normalized is None) or (not boxes_are_normalized), \
370
+ "An image can't have both absolute and relative bounding boxes"
371
+ boxes_are_normalized = False
372
+
373
+ if box_field in anno:
374
+ bbox = anno[box_field]
375
+ if isinstance(bbox,float):
376
+ assert math.isnan(bbox), "I shouldn't see a bbox that's neither a box nor NaN"
377
+ continue
378
+ bboxes.append(bbox)
379
+ box_classes.append(anno['category_id'])
380
+
381
+ box_score_string = ''
382
+ if options.confidence_field_name is not None and \
383
+ options.confidence_field_name in anno:
384
+ score = anno[options.confidence_field_name]
385
+ box_score_string = '({}%)'.format(round(100 * score))
386
+ box_score_strings.append(box_score_string)
387
+
388
+ # ...for each of this image's annotations
389
+
390
+ image_classes = ', '.join(image_categories)
391
+
392
+ img_id_string = str(img_id).lower()
393
+ file_name = '{}_gt.jpg'.format(os.path.splitext(img_id_string)[0])
394
+
395
+ # Replace characters that muck up image links, including flattening file
396
+ # separators.
397
+ illegal_characters = ['/','\\',':','\t','#',' ','%']
398
+ for c in illegal_characters:
399
+ file_name = file_name.replace(c,'~')
400
+
401
+ rendering_info_this_image = {'bboxes':bboxes,
402
+ 'box_classes':box_classes,
403
+ 'tags':box_score_strings,
404
+ 'img_path':img_path,
405
+ 'output_file_name':file_name,
406
+ 'boxes_are_normalized':boxes_are_normalized}
407
+ rendering_info.append(rendering_info_this_image)
408
+
409
+ label_level_string = ''
410
+ if len(annotation_level_for_image) > 0:
411
+ label_level_string = ' (annotation level: {})'.format(annotation_level_for_image)
412
+
413
+ if 'frame_num' in img and 'seq_num_frames' in img:
414
+ frame_string = ' frame: {} of {},'.format(img['frame_num'],img['seq_num_frames'])
415
+ elif 'frame_num' in img:
416
+ frame_string = ' frame: {},'.format(img['frame_num'])
417
+ else:
418
+ frame_string = ''
419
+
420
+ if options.show_full_paths:
421
+ filename_text = img_path
422
+ else:
423
+ filename_text = img_relative_path
424
+ if options.include_filename_links:
425
+ filename_text = '<a href="{}">{}</a>'.format(img_path,filename_text)
426
+
427
+ flag_string = ''
428
+
429
+ if ('flags' in img) and (not _isnan(img['flags'])):
430
+ flag_string = ', flags: {}'.format(str(img['flags']))
431
+
432
+ extra_field_string = ''
433
+
434
+ if options.extra_image_fields_to_print is not None:
435
+ for field_name in options.extra_image_fields_to_print:
436
+ if field_name in img:
437
+ # Always include a leading comma; this either separates us from the
438
+ # previous field in [extra_fields_to_print] or from the rest of the string
439
+ extra_field_string += ', {}: {}'.format(
440
+ field_name,str(img[field_name]))
441
+
442
+ # We're adding html for an image before we render it, so it's possible this image will
443
+ # fail to render. For applications where this script is being used to debua a database
444
+ # (the common case?), this is useful behavior, for other applications, this is annoying.
445
+ image_dict = \
446
+ {
447
+ 'filename': '{}/{}'.format('rendered_images', file_name),
448
+ 'title': '{}<br/>{}, num boxes: {},{} class labels: {}{}{}{}{}'.format(
449
+ filename_text, img_id, len(bboxes), frame_string, image_classes,
450
+ label_level_string, flag_string, extra_field_string, extra_annotation_field_string),
451
+ 'textStyle': 'font-family:verdana,arial,calibri;font-size:80%;' + \
452
+ 'text-align:left;margin-top:20;margin-bottom:5'
453
+ }
454
+ if options.include_image_links:
455
+ image_dict['linkTarget'] = img_path
456
+
457
+ images_html.append(image_dict)
458
+
459
+ # ...for each image
460
+
461
+ def render_image_info(rendering_info):
462
+
463
+ img_path = rendering_info['img_path']
464
+ bboxes = rendering_info['bboxes']
465
+ bbox_classes = rendering_info['box_classes']
466
+ boxes_are_normalized = rendering_info['boxes_are_normalized']
467
+ bbox_tags = None
468
+ if 'tags' in rendering_info:
469
+ bbox_tags = rendering_info['tags']
470
+ output_file_name = rendering_info['output_file_name']
471
+ output_full_path = os.path.join(output_dir, 'rendered_images', output_file_name)
472
+
473
+ if (os.path.isfile(output_full_path)) and (not options.force_rendering):
474
+ if options.verbose:
475
+ print('Skipping existing image {}'.format(output_full_path))
476
+ return True
477
+
478
+ if not img_path.startswith('http'):
479
+ if not os.path.exists(img_path):
480
+ print('Image {} cannot be found'.format(img_path))
481
+ return False
482
+
483
+ try:
484
+ original_image = vis_utils.open_image(img_path)
485
+ original_size = original_image.size
486
+ if (options.viz_size is None) or \
487
+ (options.viz_size[0] == -1 and options.viz_size[1] == -1):
488
+ image = original_image
489
+ else:
490
+ image = vis_utils.resize_image(original_image,
491
+ options.viz_size[0],
492
+ options.viz_size[1],
493
+ no_enlarge_width=True)
494
+ except Exception as e:
495
+ print('Image {} failed to open, error: {}'.format(img_path, e))
496
+ return False
497
+
498
+ vis_utils.render_db_bounding_boxes(boxes=bboxes,
499
+ classes=bbox_classes,
500
+ image=image,
501
+ original_size=original_size,
502
+ label_map=label_map,
503
+ thickness=options.box_thickness,
504
+ expansion=options.box_expansion,
505
+ tags=bbox_tags,
506
+ boxes_are_normalized=boxes_are_normalized)
507
+
508
+ image.save(output_full_path)
509
+
510
+ return True
511
+
512
+ # ...def render_image_info(...)
513
+
514
+ print('Rendering images')
515
+ start_time = time.time()
516
+
517
+ if options.parallelize_rendering:
518
+
519
+ if options.parallelize_rendering_with_threads:
520
+ worker_string = 'threads'
521
+ else:
522
+ worker_string = 'processes'
523
+
524
+ pool = None
525
+ try:
526
+ if options.parallelize_rendering_n_cores is None:
527
+ if options.parallelize_rendering_with_threads:
528
+ pool = ThreadPool()
529
+ else:
530
+ pool = Pool()
531
+ else:
532
+ if options.parallelize_rendering_with_threads:
533
+ pool = ThreadPool(options.parallelize_rendering_n_cores)
534
+ else:
535
+ pool = Pool(options.parallelize_rendering_n_cores)
536
+ print('Rendering images with {} {}'.format(options.parallelize_rendering_n_cores,
537
+ worker_string))
538
+ rendering_success = list(tqdm(pool.imap(render_image_info, rendering_info),
539
+ total=len(rendering_info)))
540
+ finally:
541
+ if pool is not None:
542
+ pool.close()
543
+ pool.join()
544
+ print("Pool closed and joined for DB visualization")
545
+
546
+ else:
547
+
548
+ rendering_success = []
549
+ for file_info in tqdm(rendering_info):
550
+ rendering_success.append(render_image_info(file_info))
551
+
552
+ elapsed = time.time() - start_time
553
+
554
+ print('Rendered {} images in {} ({} successful)'.format(
555
+ len(rendering_info),humanfriendly.format_timespan(elapsed),sum(rendering_success)))
556
+
557
+ if options.sort_by_filename:
558
+ images_html = sorted(images_html, key=lambda x: x['filename'])
559
+ else:
560
+ random.shuffle(images_html)
561
+
562
+ html_output_file = os.path.join(output_dir, 'index.html')
563
+
564
+ html_options = options.html_options
565
+ if isinstance(db_path,str):
566
+ html_options['headerHtml'] = '<h1>Sample annotations from {}</h1>'.format(db_path)
567
+ else:
568
+ html_options['headerHtml'] = '<h1>Sample annotations</h1>'
569
+
570
+ write_html_image_list(
571
+ filename=html_output_file,
572
+ images=images_html,
573
+ options=html_options)
574
+
575
+ print('Visualized {} images, wrote results to {}'.format(len(images_html),html_output_file))
576
+
577
+ return html_output_file,image_db
578
+
579
+ # ...def visualize_db(...)
580
+
581
+
582
+ #%% Command-line driver
583
+
584
+ # Copy all fields from a Namespace (i.e., the output from parse_args) to an object.
585
+ #
586
+ # Skips fields starting with _. Does not check existence in the target object.
587
+ def _args_to_object(args, obj):
588
+
589
+ for n, v in inspect.getmembers(args):
590
+ if not n.startswith('_'):
591
+ setattr(obj, n, v)
592
+
593
+ def main():
594
+ """
595
+ Command-line driver for visualize_db
596
+ """
597
+
598
+ parser = argparse.ArgumentParser()
599
+ parser.add_argument('db_path', action='store', type=str,
600
+ help='.json file to visualize')
601
+ parser.add_argument('output_dir', action='store', type=str,
602
+ help='Output directory for html and rendered images')
603
+ parser.add_argument('image_base_dir', action='store', type=str,
604
+ help='Base directory (or URL) for input images')
605
+
606
+ parser.add_argument('--num_to_visualize', action='store', type=int, default=None,
607
+ help='Number of images to visualize (randomly drawn) (defaults to all)')
608
+ parser.add_argument('--random_sort', action='store_true',
609
+ help='Sort randomly (rather than by filename) in output html')
610
+ parser.add_argument('--trim_to_images_with_bboxes', action='store_true',
611
+ help='Only include images with bounding boxes (defaults to false)')
612
+ parser.add_argument('--random_seed', action='store', type=int, default=None,
613
+ help='Random seed for image selection')
614
+
615
+ if len(sys.argv[1:]) == 0:
616
+ parser.print_help()
617
+ parser.exit()
618
+
619
+ args = parser.parse_args()
620
+
621
+ # Convert to an options object
622
+ options = DbVizOptions()
623
+ _args_to_object(args, options)
624
+ if options.random_sort:
625
+ options.sort_by_filename = False
626
+
627
+ visualize_db(options.db_path,options.output_dir,options.image_base_dir,options)
628
+
629
+ if __name__ == '__main__':
630
+ main()