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,677 @@
1
+ """
2
+
3
+ render_detection_confusion_matrix.py
4
+
5
+ Given a CCT-formatted ground truth file and a MegaDetector-formatted results file,
6
+ render an HTML confusion matrix. Typically used for multi-class detectors. Currently
7
+ assumes a single class per image.
8
+
9
+ """
10
+
11
+ #%% Imports and constants
12
+
13
+ import os
14
+ import json
15
+
16
+ import matplotlib.pyplot as plt
17
+ import numpy as np
18
+
19
+ from tqdm import tqdm
20
+ from collections import defaultdict
21
+ from functools import partial
22
+
23
+ from megadetector.utils.path_utils import find_images
24
+ from megadetector.utils.path_utils import flatten_path
25
+ from megadetector.utils.write_html_image_list import write_html_image_list
26
+ from megadetector.visualization import visualization_utils as vis_utils
27
+ from megadetector.visualization import plot_utils
28
+
29
+ from multiprocessing.pool import ThreadPool
30
+ from multiprocessing.pool import Pool
31
+
32
+
33
+ #%% Support functions
34
+
35
+ def _image_to_output_file(im,preview_images_folder):
36
+ """
37
+ Produces a clean filename from im (if [im] is a str) or im['file'] (if [im] is a dict).
38
+ """
39
+
40
+ if isinstance(im,str):
41
+ filename_relative = im
42
+ else:
43
+ filename_relative = im['file']
44
+
45
+ fn_clean = flatten_path(filename_relative).replace(' ','_')
46
+ return os.path.join(preview_images_folder,fn_clean)
47
+
48
+
49
+ def _render_image(im,render_image_constants):
50
+ """
51
+ Internal function for rendering a single image to the confusion matrix preview folder.
52
+ """
53
+
54
+ filename_to_ground_truth_im = render_image_constants['filename_to_ground_truth_im']
55
+ image_folder = render_image_constants['image_folder']
56
+ preview_images_folder = render_image_constants['preview_images_folder']
57
+ force_render_images = render_image_constants['force_render_images']
58
+ results_category_id_to_name = render_image_constants['results_category_id_to_name']
59
+ rendering_confidence_thresholds = render_image_constants['rendering_confidence_thresholds']
60
+ target_image_size = render_image_constants['target_image_size']
61
+
62
+ assert im['file'] in filename_to_ground_truth_im
63
+
64
+ output_file = _image_to_output_file(im,preview_images_folder)
65
+ if os.path.isfile(output_file) and not force_render_images:
66
+ return output_file
67
+
68
+ input_file = os.path.join(image_folder,im['file'])
69
+ assert os.path.isfile(input_file)
70
+
71
+ detections_to_render = []
72
+
73
+ for det in im['detections']:
74
+ category_name = results_category_id_to_name[det['category']]
75
+ detection_threshold = rendering_confidence_thresholds['default']
76
+ if category_name in rendering_confidence_thresholds:
77
+ detection_threshold = rendering_confidence_thresholds[category_name]
78
+ if det['conf'] > detection_threshold:
79
+ detections_to_render.append(det)
80
+
81
+ vis_utils.draw_bounding_boxes_on_file(input_file, output_file, detections_to_render,
82
+ detector_label_map=results_category_id_to_name,
83
+ label_font_size=20,target_size=target_image_size)
84
+
85
+ return output_file
86
+
87
+
88
+ #%% Main function
89
+
90
+ def render_detection_confusion_matrix(ground_truth_file,
91
+ results_file,
92
+ image_folder,
93
+ preview_folder,
94
+ force_render_images=False,
95
+ confidence_thresholds=None,
96
+ rendering_confidence_thresholds=None,
97
+ target_image_size=(1280,-1),
98
+ parallelize_rendering=True,
99
+ parallelize_rendering_n_cores=None,
100
+ parallelize_rendering_with_threads=False,
101
+ job_name='unknown',
102
+ model_file=None,
103
+ empty_category_name='empty',
104
+ html_image_list_options=None):
105
+ """
106
+ Given a CCT-formatted ground truth file and a MegaDetector-formatted results file,
107
+ render an HTML confusion matrix in [preview_folder. Typically used for multi-class detectors.
108
+ Currently assumes a single class per image.
109
+
110
+ confidence_thresholds and rendering_confidence_thresholds are dictionaries mapping
111
+ class names to thresholds. "default" is a special token that will be used for all
112
+ classes not otherwise assigned thresholds.
113
+
114
+ Args:
115
+ ground_truth_file (str): the CCT-formatted .json file with ground truth information
116
+ results_file (str): the MegaDetector results .json file
117
+ image_folder (str): the folder where images live; filenames in [ground_truth_file] and
118
+ [results_file] should be relative to this folder.
119
+ preview_folder (str): the output folder, i.e. the folder in which we'll create our nifty
120
+ HTML stuff.
121
+ force_render_images (bool, optional): if False, skips images that already exist
122
+ confidence_thresholds (dict, optional): a dictionary mapping class names to thresholds;
123
+ all classes not explicitly named here will use the threshold for the "default" category.
124
+ rendering_confidence_thresholds (dict, optional): a dictionary mapping class names to thresholds;
125
+ all classes not explicitly named here will use the threshold for the "default" category.
126
+ target_image_size (tuple, optional): output image size, as a pair of ints (width,height). If one
127
+ value is -1 and the other is not, aspect ratio is preserved. If both are -1, the original image
128
+ sizes are preserved.
129
+ parallelize_rendering (bool, optional): enable (default) or disable parallelization when rendering
130
+ parallelize_rendering_n_cores (int, optional): number of threads or processes to use for rendering, only
131
+ used if parallelize_rendering is True
132
+ parallelize_rendering_with_threads (bool, optional): whether to use threads (True) or processes (False)
133
+ when rendering, only used if parallelize_rendering is True
134
+ job_name (str, optional): job name to include in big letters in the output file
135
+ model_file (str, optional): model filename to include in HTML output
136
+ empty_category_name (str, optional): special category name that we should treat as empty, typically
137
+ "empty"
138
+ html_image_list_options (dict, optional): options listed passed along to write_html_image_list;
139
+ see write_html_image_list for documentation.
140
+
141
+ Returns:
142
+ dict: confusion matrix information, containing at least the key "html_file"
143
+ """
144
+
145
+ ##%% Argument and path handling
146
+
147
+ preview_images_folder = os.path.join(preview_folder,'images')
148
+ os.makedirs(preview_images_folder,exist_ok=True)
149
+
150
+ if confidence_thresholds is None:
151
+ confidence_thresholds = {'default':0.5}
152
+ if rendering_confidence_thresholds is None:
153
+ rendering_confidence_thresholds = {'default':0.4}
154
+
155
+
156
+ ##%% Load ground truth
157
+
158
+ with open(ground_truth_file,'r') as f:
159
+ ground_truth_data_cct = json.load(f)
160
+
161
+ filename_to_ground_truth_im = {}
162
+ for im in ground_truth_data_cct['images']:
163
+ assert im['file_name'] not in filename_to_ground_truth_im
164
+ filename_to_ground_truth_im[im['file_name']] = im
165
+
166
+
167
+ ##%% Confirm that the ground truth images are present in the image folder
168
+
169
+ ground_truth_images = find_images(image_folder,return_relative_paths=True,recursive=True)
170
+ assert len(ground_truth_images) == len(ground_truth_data_cct['images'])
171
+ del ground_truth_images
172
+
173
+
174
+ ##%% Map images to categories
175
+
176
+ # gt_image_id_to_image = {im['id']:im for im in ground_truth_data_cct['images']}
177
+ gt_image_id_to_annotations = defaultdict(list)
178
+
179
+ ground_truth_category_id_to_name = {}
180
+ for c in ground_truth_data_cct['categories']:
181
+ ground_truth_category_id_to_name[c['id']] = c['name']
182
+
183
+ # Add the empty category if necessary
184
+ if empty_category_name not in ground_truth_category_id_to_name.values():
185
+ empty_category_id = max(ground_truth_category_id_to_name.keys())+1
186
+ ground_truth_category_id_to_name[empty_category_id] = empty_category_name
187
+
188
+ ground_truth_category_names = sorted(list(ground_truth_category_id_to_name.values()))
189
+
190
+ for ann in ground_truth_data_cct['annotations']:
191
+ gt_image_id_to_annotations[ann['image_id']].append(ann)
192
+
193
+ gt_filename_to_category_names = defaultdict(set)
194
+
195
+ for im in ground_truth_data_cct['images']:
196
+ annotations_this_image = gt_image_id_to_annotations[im['id']]
197
+ for ann in annotations_this_image:
198
+ category_name = ground_truth_category_id_to_name[ann['category_id']]
199
+ gt_filename_to_category_names[im['file_name']].add(category_name)
200
+
201
+ for filename in gt_filename_to_category_names:
202
+
203
+ category_names_this_file = gt_filename_to_category_names[filename]
204
+
205
+ # The empty category should be exclusive
206
+ if empty_category_name in category_names_this_file:
207
+ assert len(category_names_this_file) == 1, \
208
+ 'Empty category assigned along with another category for {}'.format(filename)
209
+ assert len(category_names_this_file) > 0, \
210
+ 'No ground truth category assigned to {}'.format(filename)
211
+
212
+
213
+ ##%% Load results
214
+
215
+ with open(results_file,'r') as f:
216
+ md_formatted_results = json.load(f)
217
+
218
+ results_category_id_to_name = md_formatted_results['detection_categories']
219
+
220
+
221
+ ##%% Render images with detections
222
+
223
+ render_image_constants = {}
224
+ render_image_constants['filename_to_ground_truth_im'] = filename_to_ground_truth_im
225
+ render_image_constants['image_folder'] = image_folder
226
+ render_image_constants['preview_images_folder'] = preview_images_folder
227
+ render_image_constants['force_render_images'] = force_render_images
228
+ render_image_constants['results_category_id_to_name'] = results_category_id_to_name
229
+ render_image_constants['rendering_confidence_thresholds'] = rendering_confidence_thresholds
230
+ render_image_constants['target_image_size'] = target_image_size
231
+
232
+ if parallelize_rendering:
233
+
234
+ pool = None
235
+ try:
236
+ if parallelize_rendering_n_cores is None:
237
+ if parallelize_rendering_with_threads:
238
+ pool = ThreadPool()
239
+ else:
240
+ pool = Pool()
241
+ else:
242
+ if parallelize_rendering_with_threads:
243
+ pool = ThreadPool(parallelize_rendering_n_cores)
244
+ worker_string = 'threads'
245
+ else:
246
+ pool = Pool(parallelize_rendering_n_cores)
247
+ worker_string = 'processes'
248
+ print('Rendering images with {} {}'.format(parallelize_rendering_n_cores,
249
+ worker_string))
250
+
251
+ _ = list(tqdm(pool.imap(partial(_render_image,render_image_constants=render_image_constants),
252
+ md_formatted_results['images']),
253
+ total=len(md_formatted_results['images'])))
254
+ finally:
255
+ if pool is not None:
256
+ pool.close()
257
+ pool.join()
258
+ print('Pool closed and joined for confusion matrix rendering')
259
+
260
+ else:
261
+
262
+ # im = md_formatted_results['images'][0]
263
+ for im in tqdm(md_formatted_results['images']):
264
+ _render_image(im,render_image_constants)
265
+
266
+
267
+ ##%% Map images to predicted categories, and vice-versa
268
+
269
+ filename_to_predicted_categories = defaultdict(set)
270
+ predicted_category_name_to_filenames = defaultdict(set)
271
+
272
+ # im = md_formatted_results['images'][0]
273
+ for im in tqdm(md_formatted_results['images']):
274
+
275
+ assert im['file'] in filename_to_ground_truth_im
276
+
277
+ # det = im['detections'][0]
278
+ for det in im['detections']:
279
+ category_name = results_category_id_to_name[det['category']]
280
+ detection_threshold = confidence_thresholds['default']
281
+ if category_name in confidence_thresholds:
282
+ detection_threshold = confidence_thresholds[category_name]
283
+ if det['conf'] > detection_threshold:
284
+ filename_to_predicted_categories[im['file']].add(category_name)
285
+ predicted_category_name_to_filenames[category_name].add(im['file'])
286
+
287
+ # ...for each detection
288
+
289
+ # ...for each image
290
+
291
+
292
+ ##%% Create TP/TN/FP/FN lists
293
+
294
+ category_name_to_image_lists = {}
295
+
296
+ sub_page_tokens = ['fn','tn','fp','tp']
297
+
298
+ for category_name in ground_truth_category_names:
299
+
300
+ category_name_to_image_lists[category_name] = {}
301
+ for sub_page_token in sub_page_tokens:
302
+ category_name_to_image_lists[category_name][sub_page_token] = []
303
+
304
+ # filename = next(iter(gt_filename_to_category_names))
305
+ for filename in gt_filename_to_category_names.keys():
306
+
307
+ ground_truth_categories_this_image = gt_filename_to_category_names[filename]
308
+ predicted_categories_this_image = filename_to_predicted_categories[filename]
309
+
310
+ for category_name in ground_truth_category_names:
311
+
312
+ assignment = None
313
+
314
+ if category_name == empty_category_name:
315
+ # If this is an empty image
316
+ if category_name in ground_truth_categories_this_image:
317
+ assert len(ground_truth_categories_this_image) == 1
318
+ if len(predicted_categories_this_image) == 0:
319
+ assignment = 'tp'
320
+ else:
321
+ assignment = 'fn'
322
+ # If this not an empty image
323
+ else:
324
+ if len(predicted_categories_this_image) == 0:
325
+ assignment = 'fp'
326
+ else:
327
+ assignment = 'tn'
328
+
329
+ else:
330
+ if category_name in ground_truth_categories_this_image:
331
+ if category_name in predicted_categories_this_image:
332
+ assignment = 'tp'
333
+ else:
334
+ assignment = 'fn'
335
+ else:
336
+ if category_name in predicted_categories_this_image:
337
+ assignment = 'fp'
338
+ else:
339
+ assignment = 'tn'
340
+
341
+ category_name_to_image_lists[category_name][assignment].append(filename)
342
+
343
+ # ...for each filename
344
+
345
+
346
+ ##%% Create confusion matrix
347
+
348
+ gt_category_name_to_category_index = {}
349
+
350
+ for i_category,category_name in enumerate(ground_truth_category_names):
351
+ gt_category_name_to_category_index[category_name] = i_category
352
+
353
+ n_categories = len(gt_category_name_to_category_index)
354
+
355
+ # indexed as [true,predicted]
356
+ confusion_matrix = np.zeros(shape=(n_categories,n_categories),dtype=int)
357
+
358
+ filename_to_results_im = {im['file']:im for im in md_formatted_results['images']}
359
+
360
+ true_predicted_to_file_list = defaultdict(list)
361
+
362
+ # filename = next(iter(gt_filename_to_category_names.keys()))
363
+ for filename in gt_filename_to_category_names.keys():
364
+
365
+ ground_truth_categories_this_image = gt_filename_to_category_names[filename]
366
+ assert len(ground_truth_categories_this_image) == 1
367
+ ground_truth_category_name = next(iter(ground_truth_categories_this_image))
368
+
369
+ results_im = filename_to_results_im[filename]
370
+
371
+ # If there were no detections at all, call this image empty
372
+ if len(results_im['detections']) == 0:
373
+
374
+ predicted_category_name = empty_category_name
375
+
376
+ # Otherwise look for above-threshold detections
377
+ else:
378
+
379
+ results_category_name_to_confidence = defaultdict(int)
380
+ for det in results_im['detections']:
381
+
382
+ category_name = results_category_id_to_name[det['category']]
383
+ detection_threshold = confidence_thresholds['default']
384
+ if category_name in confidence_thresholds:
385
+ detection_threshold = confidence_thresholds[category_name]
386
+ if det['conf'] > detection_threshold:
387
+ results_category_name_to_confidence[category_name] = max(
388
+ results_category_name_to_confidence[category_name],det['conf'])
389
+
390
+ # ...for each detection
391
+
392
+ # If there were no detections above threshold
393
+ if len(results_category_name_to_confidence) == 0:
394
+ predicted_category_name = empty_category_name
395
+ else:
396
+ predicted_category_name = max(results_category_name_to_confidence,
397
+ key=results_category_name_to_confidence.get)
398
+
399
+ ground_truth_category_index = gt_category_name_to_category_index[ground_truth_category_name]
400
+ predicted_category_index = gt_category_name_to_category_index[predicted_category_name]
401
+
402
+ true_predicted_token = ground_truth_category_name + '_' + predicted_category_name
403
+ true_predicted_to_file_list[true_predicted_token].append(filename)
404
+
405
+ confusion_matrix[ground_truth_category_index,predicted_category_index] += 1
406
+
407
+ # ...for each ground truth file
408
+
409
+ plt.ioff()
410
+
411
+ fig_h = 3 + 0.3 * n_categories
412
+ fig_w = fig_h
413
+ fig = plt.figure(figsize=(fig_w, fig_h),tight_layout=True)
414
+
415
+ plot_utils.plot_confusion_matrix(
416
+ matrix=confusion_matrix,
417
+ classes=ground_truth_category_names,
418
+ normalize=False,
419
+ title='Confusion matrix',
420
+ cmap=plt.cm.Blues,
421
+ vmax=1.0,
422
+ use_colorbar=False,
423
+ y_label=True,
424
+ fig=fig)
425
+
426
+ cm_figure_fn_relative = 'confusion_matrix.png'
427
+ cm_figure_fn_abs = os.path.join(preview_folder, cm_figure_fn_relative)
428
+ # fig.show()
429
+ fig.savefig(cm_figure_fn_abs,dpi=100)
430
+ plt.close(fig)
431
+
432
+ # open_file(cm_figure_fn_abs)
433
+
434
+
435
+ ##%% Create HTML confusion matrix
436
+
437
+ html_confusion_matrix = '<table class="result-table">\n'
438
+ html_confusion_matrix += '<tr>\n'
439
+ html_confusion_matrix += '<td>{}</td>\n'.format('True category')
440
+ for category_name in ground_truth_category_names:
441
+ html_confusion_matrix += '<td>{}</td>\n'.format('&nbsp;')
442
+ html_confusion_matrix += '</tr>\n'
443
+
444
+ for true_category in ground_truth_category_names:
445
+
446
+ html_confusion_matrix += '<tr>\n'
447
+ html_confusion_matrix += '<td>{}</td>\n'.format(true_category)
448
+
449
+ for predicted_category in ground_truth_category_names:
450
+
451
+ true_predicted_token = true_category + '_' + predicted_category
452
+ image_list = true_predicted_to_file_list[true_predicted_token]
453
+ if len(image_list) == 0:
454
+ td_content = '0'
455
+ else:
456
+ if html_image_list_options is None:
457
+ html_image_list_options = {}
458
+ title_string = 'true: {}, predicted {}'.format(
459
+ true_category,predicted_category)
460
+ html_image_list_options['headerHtml'] = '<h1>{}</h1>'.format(title_string)
461
+
462
+ html_image_info_list = []
463
+
464
+ for image_filename_relative in image_list:
465
+ html_image_info = {}
466
+ detections = filename_to_results_im[image_filename_relative]['detections']
467
+ if len(detections) == 0:
468
+ max_conf = 0
469
+ else:
470
+ max_conf = max([d['conf'] for d in detections])
471
+
472
+ title = '<b>Image</b>: {}, <b>Max conf</b>: {:0.3f}'.format(
473
+ image_filename_relative, max_conf)
474
+ image_link = 'images/' + os.path.basename(
475
+ _image_to_output_file(image_filename_relative,preview_images_folder))
476
+ html_image_info = {
477
+ 'filename': image_link,
478
+ 'title': title,
479
+ 'textStyle':\
480
+ 'font-family:verdana,arial,calibri;font-size:80%;' + \
481
+ 'text-align:left;margin-top:20;margin-bottom:5'
482
+ }
483
+
484
+ html_image_info_list.append(html_image_info)
485
+
486
+ target_html_file_relative = true_predicted_token + '.html'
487
+ target_html_file_abs = os.path.join(preview_folder,target_html_file_relative)
488
+ write_html_image_list(
489
+ filename=target_html_file_abs,
490
+ images=html_image_info_list,
491
+ options=html_image_list_options)
492
+
493
+ td_content = '<a href="{}">{}</a>'.format(target_html_file_relative,
494
+ len(image_list))
495
+
496
+ html_confusion_matrix += '<td>{}</td>\n'.format(td_content)
497
+
498
+ # ...for each predicted category
499
+
500
+ html_confusion_matrix += '</tr>\n'
501
+
502
+ # ...for each true category
503
+
504
+ html_confusion_matrix += '<tr>\n'
505
+ html_confusion_matrix += '<td>&nbsp;</td>\n'
506
+
507
+ for category_name in ground_truth_category_names:
508
+ html_confusion_matrix += '<td class="rotate"><p style="margin-left:20px;">{}</p></td>\n'.format(
509
+ category_name)
510
+ html_confusion_matrix += '</tr>\n'
511
+
512
+ html_confusion_matrix += '</table>'
513
+
514
+
515
+ ##%% Create HTML sub-pages and HTML table
516
+
517
+ html_table = '<table class="result-table">\n'
518
+
519
+ html_table += '<tr>\n'
520
+ html_table += '<td>{}</td>\n'.format('True category')
521
+ for sub_page_token in sub_page_tokens:
522
+ html_table += '<td>{}</td>'.format(sub_page_token)
523
+ html_table += '</tr>\n'
524
+
525
+ filename_to_results_im = {im['file']:im for im in md_formatted_results['images']}
526
+
527
+ sub_page_token_to_page_name = {
528
+ 'fp':'false positives',
529
+ 'tp':'true positives',
530
+ 'fn':'false negatives',
531
+ 'tn':'true negatives'
532
+ }
533
+
534
+ # category_name = ground_truth_category_names[0]
535
+ for category_name in ground_truth_category_names:
536
+
537
+ html_table += '<tr>\n'
538
+
539
+ html_table += '<td>{}</td>\n'.format(category_name)
540
+
541
+ # sub_page_token = sub_page_tokens[0]
542
+ for sub_page_token in sub_page_tokens:
543
+
544
+ html_table += '<td>\n'
545
+
546
+ image_list = category_name_to_image_lists[category_name][sub_page_token]
547
+
548
+ if len(image_list) == 0:
549
+
550
+ html_table += '0\n'
551
+
552
+ else:
553
+
554
+ html_image_list_options = {}
555
+ title_string = '{}: {}'.format(category_name,sub_page_token_to_page_name[sub_page_token])
556
+ html_image_list_options['headerHtml'] = '<h1>{}</h1>'.format(title_string)
557
+
558
+ target_html_file_relative = '{}_{}.html'.format(category_name,sub_page_token)
559
+ target_html_file_abs = os.path.join(preview_folder,target_html_file_relative)
560
+
561
+ html_image_info_list = []
562
+
563
+ # image_filename_relative = image_list[0]
564
+ for image_filename_relative in image_list:
565
+
566
+ source_file = os.path.join(image_folder,image_filename_relative)
567
+ assert os.path.isfile(source_file)
568
+
569
+ html_image_info = {}
570
+ detections = filename_to_results_im[image_filename_relative]['detections']
571
+ if len(detections) == 0:
572
+ max_conf = 0
573
+ else:
574
+ max_conf = max([d['conf'] for d in detections])
575
+
576
+ title = '<b>Image</b>: {}, <b>Max conf</b>: {:0.3f}'.format(
577
+ image_filename_relative, max_conf)
578
+ image_link = 'images/' + os.path.basename(
579
+ _image_to_output_file(image_filename_relative,preview_images_folder))
580
+ html_image_info = {
581
+ 'filename': image_link,
582
+ 'title': title,
583
+ 'linkTarget': source_file,
584
+ 'textStyle':\
585
+ 'font-family:verdana,arial,calibri;font-size:80%;' + \
586
+ 'text-align:left;margin-top:20;margin-bottom:5'
587
+ }
588
+
589
+ html_image_info_list.append(html_image_info)
590
+
591
+ # ...for each image
592
+
593
+ write_html_image_list(
594
+ filename=target_html_file_abs,
595
+ images=html_image_info_list,
596
+ options=html_image_list_options)
597
+
598
+ html_table += '<a href="{}">{}</a>\n'.format(target_html_file_relative,len(image_list))
599
+
600
+ html_table += '</td>\n'
601
+
602
+ # ...for each sub-page
603
+
604
+ html_table += '</tr>\n'
605
+
606
+ # ...for each category
607
+
608
+ html_table += '</table>'
609
+
610
+ html = '<html>\n'
611
+
612
+ style_header = """<head>
613
+ <style type="text/css">
614
+ a { text-decoration: none; }
615
+ body { font-family: segoe ui, calibri, "trebuchet ms", verdana, arial, sans-serif; }
616
+ div.contentdiv { margin-left: 20px; }
617
+ table.result-table { border:1px solid black; border-collapse: collapse; margin-left:50px;}
618
+ td,th { padding:10px; }
619
+ .rotate {
620
+ padding:0px;
621
+ writing-mode:vertical-lr;
622
+ -webkit-transform: rotate(-180deg);
623
+ -moz-transform: rotate(-180deg);
624
+ -ms-transform: rotate(-180deg);
625
+ -o-transform: rotate(-180deg);
626
+ transform: rotate(-180deg);
627
+ }
628
+ </style>
629
+ </head>"""
630
+
631
+ html += style_header + '\n'
632
+
633
+ html += '<body>\n'
634
+
635
+ html += '<h1>Results summary for {}</h1>\n'.format(job_name)
636
+
637
+ if model_file is not None and len(model_file) > 0:
638
+ html += '<p><b>Model file</b>: {}</p>'.format(os.path.basename(model_file))
639
+
640
+ html += '<p><b>Confidence thresholds</b></p>'
641
+
642
+ for c in confidence_thresholds.keys():
643
+ html += '<p style="margin-left:15px;">{}: {}</p>'.format(c,confidence_thresholds[c])
644
+
645
+ html += '<h2>Confusion matrix</h2>\n'
646
+
647
+ html += '<p>...assuming a single category per image.</p>\n'
648
+
649
+ html += '<img src="{}"/>\n'.format(cm_figure_fn_relative)
650
+
651
+ html += '<h2>Confusion matrix (with links)</h2>\n'
652
+
653
+ html += '<p>...assuming a single category per image.</p>\n'
654
+
655
+ html += html_confusion_matrix
656
+
657
+ html += '<h2>Per-class statistics</h2>\n'
658
+
659
+ html += html_table
660
+
661
+ html += '</body>\n'
662
+ html += '<html>\n'
663
+
664
+ target_html_file = os.path.join(preview_folder,'index.html')
665
+
666
+ with open(target_html_file,'w') as f:
667
+ f.write(html)
668
+
669
+
670
+ ##%% Prepare return data
671
+
672
+ confusion_matrix_info = {}
673
+ confusion_matrix_info['html_file'] = target_html_file
674
+
675
+ return confusion_matrix_info
676
+
677
+ # ...render_detection_confusion_matrix(...)