megadetector 5.0.5__py3-none-any.whl → 5.0.7__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 (132) hide show
  1. api/batch_processing/data_preparation/manage_local_batch.py +302 -263
  2. api/batch_processing/data_preparation/manage_video_batch.py +81 -2
  3. api/batch_processing/postprocessing/add_max_conf.py +1 -0
  4. api/batch_processing/postprocessing/categorize_detections_by_size.py +50 -19
  5. api/batch_processing/postprocessing/compare_batch_results.py +110 -60
  6. api/batch_processing/postprocessing/load_api_results.py +56 -70
  7. api/batch_processing/postprocessing/md_to_coco.py +1 -1
  8. api/batch_processing/postprocessing/md_to_labelme.py +2 -1
  9. api/batch_processing/postprocessing/postprocess_batch_results.py +240 -81
  10. api/batch_processing/postprocessing/render_detection_confusion_matrix.py +625 -0
  11. api/batch_processing/postprocessing/repeat_detection_elimination/find_repeat_detections.py +71 -23
  12. api/batch_processing/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +1 -1
  13. api/batch_processing/postprocessing/repeat_detection_elimination/repeat_detections_core.py +227 -75
  14. api/batch_processing/postprocessing/subset_json_detector_output.py +132 -5
  15. api/batch_processing/postprocessing/top_folders_to_bottom.py +1 -1
  16. api/synchronous/api_core/animal_detection_api/detection/run_detector_batch.py +2 -2
  17. classification/prepare_classification_script.py +191 -191
  18. data_management/coco_to_yolo.py +68 -45
  19. data_management/databases/integrity_check_json_db.py +7 -5
  20. data_management/generate_crops_from_cct.py +3 -3
  21. data_management/get_image_sizes.py +8 -6
  22. data_management/importers/add_timestamps_to_icct.py +79 -0
  23. data_management/importers/animl_results_to_md_results.py +160 -0
  24. data_management/importers/auckland_doc_test_to_json.py +4 -4
  25. data_management/importers/auckland_doc_to_json.py +1 -1
  26. data_management/importers/awc_to_json.py +5 -5
  27. data_management/importers/bellevue_to_json.py +5 -5
  28. data_management/importers/carrizo_shrubfree_2018.py +5 -5
  29. data_management/importers/carrizo_trail_cam_2017.py +5 -5
  30. data_management/importers/cct_field_adjustments.py +2 -3
  31. data_management/importers/channel_islands_to_cct.py +4 -4
  32. data_management/importers/ena24_to_json.py +5 -5
  33. data_management/importers/helena_to_cct.py +10 -10
  34. data_management/importers/idaho-camera-traps.py +12 -12
  35. data_management/importers/idfg_iwildcam_lila_prep.py +8 -8
  36. data_management/importers/jb_csv_to_json.py +4 -4
  37. data_management/importers/missouri_to_json.py +1 -1
  38. data_management/importers/noaa_seals_2019.py +1 -1
  39. data_management/importers/pc_to_json.py +5 -5
  40. data_management/importers/prepare-noaa-fish-data-for-lila.py +4 -4
  41. data_management/importers/prepare_zsl_imerit.py +5 -5
  42. data_management/importers/rspb_to_json.py +4 -4
  43. data_management/importers/save_the_elephants_survey_A.py +5 -5
  44. data_management/importers/save_the_elephants_survey_B.py +6 -6
  45. data_management/importers/snapshot_safari_importer.py +9 -9
  46. data_management/importers/snapshot_serengeti_lila.py +9 -9
  47. data_management/importers/timelapse_csv_set_to_json.py +5 -7
  48. data_management/importers/ubc_to_json.py +4 -4
  49. data_management/importers/umn_to_json.py +4 -4
  50. data_management/importers/wellington_to_json.py +1 -1
  51. data_management/importers/wi_to_json.py +2 -2
  52. data_management/importers/zamba_results_to_md_results.py +181 -0
  53. data_management/labelme_to_coco.py +35 -7
  54. data_management/labelme_to_yolo.py +229 -0
  55. data_management/lila/add_locations_to_island_camera_traps.py +1 -1
  56. data_management/lila/add_locations_to_nacti.py +147 -0
  57. data_management/lila/create_lila_blank_set.py +474 -0
  58. data_management/lila/create_lila_test_set.py +2 -1
  59. data_management/lila/create_links_to_md_results_files.py +106 -0
  60. data_management/lila/download_lila_subset.py +46 -21
  61. data_management/lila/generate_lila_per_image_labels.py +23 -14
  62. data_management/lila/get_lila_annotation_counts.py +17 -11
  63. data_management/lila/lila_common.py +14 -11
  64. data_management/lila/test_lila_metadata_urls.py +116 -0
  65. data_management/ocr_tools.py +829 -0
  66. data_management/resize_coco_dataset.py +13 -11
  67. data_management/yolo_output_to_md_output.py +84 -12
  68. data_management/yolo_to_coco.py +38 -20
  69. detection/process_video.py +36 -14
  70. detection/pytorch_detector.py +23 -8
  71. detection/run_detector.py +76 -19
  72. detection/run_detector_batch.py +178 -63
  73. detection/run_inference_with_yolov5_val.py +326 -57
  74. detection/run_tiled_inference.py +153 -43
  75. detection/video_utils.py +34 -8
  76. md_utils/ct_utils.py +172 -1
  77. md_utils/md_tests.py +372 -51
  78. md_utils/path_utils.py +167 -39
  79. md_utils/process_utils.py +26 -7
  80. md_utils/split_locations_into_train_val.py +215 -0
  81. md_utils/string_utils.py +10 -0
  82. md_utils/url_utils.py +0 -2
  83. md_utils/write_html_image_list.py +9 -26
  84. md_visualization/plot_utils.py +12 -8
  85. md_visualization/visualization_utils.py +106 -7
  86. md_visualization/visualize_db.py +16 -8
  87. md_visualization/visualize_detector_output.py +208 -97
  88. {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/METADATA +3 -6
  89. {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/RECORD +98 -121
  90. {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/WHEEL +1 -1
  91. taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +1 -1
  92. taxonomy_mapping/map_new_lila_datasets.py +43 -39
  93. taxonomy_mapping/prepare_lila_taxonomy_release.py +5 -2
  94. taxonomy_mapping/preview_lila_taxonomy.py +27 -27
  95. taxonomy_mapping/species_lookup.py +33 -13
  96. taxonomy_mapping/taxonomy_csv_checker.py +7 -5
  97. api/synchronous/api_core/yolov5/detect.py +0 -252
  98. api/synchronous/api_core/yolov5/export.py +0 -607
  99. api/synchronous/api_core/yolov5/hubconf.py +0 -146
  100. api/synchronous/api_core/yolov5/models/__init__.py +0 -0
  101. api/synchronous/api_core/yolov5/models/common.py +0 -738
  102. api/synchronous/api_core/yolov5/models/experimental.py +0 -104
  103. api/synchronous/api_core/yolov5/models/tf.py +0 -574
  104. api/synchronous/api_core/yolov5/models/yolo.py +0 -338
  105. api/synchronous/api_core/yolov5/train.py +0 -670
  106. api/synchronous/api_core/yolov5/utils/__init__.py +0 -36
  107. api/synchronous/api_core/yolov5/utils/activations.py +0 -103
  108. api/synchronous/api_core/yolov5/utils/augmentations.py +0 -284
  109. api/synchronous/api_core/yolov5/utils/autoanchor.py +0 -170
  110. api/synchronous/api_core/yolov5/utils/autobatch.py +0 -66
  111. api/synchronous/api_core/yolov5/utils/aws/__init__.py +0 -0
  112. api/synchronous/api_core/yolov5/utils/aws/resume.py +0 -40
  113. api/synchronous/api_core/yolov5/utils/benchmarks.py +0 -148
  114. api/synchronous/api_core/yolov5/utils/callbacks.py +0 -71
  115. api/synchronous/api_core/yolov5/utils/dataloaders.py +0 -1087
  116. api/synchronous/api_core/yolov5/utils/downloads.py +0 -178
  117. api/synchronous/api_core/yolov5/utils/flask_rest_api/example_request.py +0 -19
  118. api/synchronous/api_core/yolov5/utils/flask_rest_api/restapi.py +0 -46
  119. api/synchronous/api_core/yolov5/utils/general.py +0 -1018
  120. api/synchronous/api_core/yolov5/utils/loggers/__init__.py +0 -187
  121. api/synchronous/api_core/yolov5/utils/loggers/wandb/__init__.py +0 -0
  122. api/synchronous/api_core/yolov5/utils/loggers/wandb/log_dataset.py +0 -27
  123. api/synchronous/api_core/yolov5/utils/loggers/wandb/sweep.py +0 -41
  124. api/synchronous/api_core/yolov5/utils/loggers/wandb/wandb_utils.py +0 -577
  125. api/synchronous/api_core/yolov5/utils/loss.py +0 -234
  126. api/synchronous/api_core/yolov5/utils/metrics.py +0 -355
  127. api/synchronous/api_core/yolov5/utils/plots.py +0 -489
  128. api/synchronous/api_core/yolov5/utils/torch_utils.py +0 -314
  129. api/synchronous/api_core/yolov5/val.py +0 -394
  130. md_utils/matlab_porting_tools.py +0 -97
  131. {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/LICENSE +0 -0
  132. {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,625 @@
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 md_utils.path_utils import find_images
24
+ from md_utils.path_utils import flatten_path
25
+ from md_visualization import visualization_utils as vis_utils
26
+ from md_utils.write_html_image_list import write_html_image_list
27
+
28
+ import md_visualization.plot_utils as plot_utils
29
+
30
+ from multiprocessing.pool import ThreadPool
31
+ from multiprocessing.pool import Pool
32
+
33
+
34
+ #%% Support functions
35
+
36
+ def image_to_output_file(im,preview_images_folder):
37
+
38
+ if isinstance(im,str):
39
+ filename_relative = im
40
+ else:
41
+ filename_relative = im['file']
42
+
43
+ fn_clean = flatten_path(filename_relative).replace(' ','_')
44
+ return os.path.join(preview_images_folder,fn_clean)
45
+
46
+
47
+ def render_image(im,render_image_constants):
48
+
49
+ filename_to_ground_truth_im = render_image_constants['filename_to_ground_truth_im']
50
+ image_folder = render_image_constants['image_folder']
51
+ preview_images_folder = render_image_constants['preview_images_folder']
52
+ force_render_images = render_image_constants['force_render_images']
53
+ results_category_id_to_name = render_image_constants['results_category_id_to_name']
54
+ rendering_confidence_thresholds = render_image_constants['rendering_confidence_thresholds']
55
+ target_image_size = render_image_constants['target_image_size']
56
+
57
+ assert im['file'] in filename_to_ground_truth_im
58
+
59
+ input_file = os.path.join(image_folder,im['file'])
60
+ assert os.path.isfile(input_file)
61
+
62
+ output_file = image_to_output_file(im,preview_images_folder)
63
+ if os.path.isfile(output_file) and not force_render_images:
64
+ return output_file
65
+
66
+ detections_to_render = []
67
+
68
+ for det in im['detections']:
69
+ category_name = results_category_id_to_name[det['category']]
70
+ detection_threshold = rendering_confidence_thresholds['default']
71
+ if category_name in rendering_confidence_thresholds:
72
+ detection_threshold = rendering_confidence_thresholds[category_name]
73
+ if det['conf'] > detection_threshold:
74
+ detections_to_render.append(det)
75
+
76
+ vis_utils.draw_bounding_boxes_on_file(input_file, output_file, detections_to_render,
77
+ detector_label_map=results_category_id_to_name,
78
+ label_font_size=20,target_size=target_image_size)
79
+
80
+ return output_file
81
+
82
+
83
+ #%% Main function
84
+
85
+ def render_detection_confusion_matrix(ground_truth_file,results_file,image_folder,preview_folder,
86
+ force_render_images=False, confidence_thresholds=None,
87
+ rendering_confidence_thresholds=None,
88
+ target_image_size=(1280,-1),
89
+ parallelize_rendering=True,
90
+ parallelize_rendering_n_cores=None,
91
+ parallelize_rendering_with_threads=False,
92
+ job_name='unknown',
93
+ model_file=None,
94
+ empty_category_name='empty',
95
+ html_image_list_options=None):
96
+ """
97
+ Given a CCT-formatted ground truth file and a MegaDetector-formatted results file,
98
+ render an HTML confusion matrix in [preview_folder. Typically used for multi-class detectors.
99
+ Currently assumes a single class per image.
100
+
101
+ confidence_thresholds and rendering_confidence_thresholds are dictionaries mapping
102
+ class names to thresholds. "default" is a special token that will be used for all
103
+ classes not otherwise assigned thresholds.
104
+ """
105
+
106
+ #%% Argument and path handling
107
+
108
+ preview_images_folder = os.path.join(preview_folder,'images')
109
+ os.makedirs(preview_images_folder,exist_ok=True)
110
+
111
+ if confidence_thresholds is None:
112
+ confidence_thresholds = {'default':0.5}
113
+ if rendering_confidence_thresholds is None:
114
+ rendering_confidence_thresholds = {'default':0.4}
115
+
116
+
117
+ #%% Load ground truth
118
+
119
+ with open(ground_truth_file,'r') as f:
120
+ ground_truth_data_cct = json.load(f)
121
+
122
+ filename_to_ground_truth_im = {}
123
+ for im in ground_truth_data_cct['images']:
124
+ assert im['file_name'] not in filename_to_ground_truth_im
125
+ filename_to_ground_truth_im[im['file_name']] = im
126
+
127
+
128
+ #%% Confirm that the ground truth images are present in the image folder
129
+
130
+ ground_truth_images = find_images(image_folder,return_relative_paths=True,recursive=True)
131
+ assert len(ground_truth_images) == len(ground_truth_data_cct['images'])
132
+ del ground_truth_images
133
+
134
+
135
+ #%% Map images to categories
136
+
137
+ # gt_image_id_to_image = {im['id']:im for im in ground_truth_data_cct['images']}
138
+ gt_image_id_to_annotations = defaultdict(list)
139
+
140
+ ground_truth_category_id_to_name = {}
141
+ for c in ground_truth_data_cct['categories']:
142
+ ground_truth_category_id_to_name[c['id']] = c['name']
143
+
144
+ # Add the empty category if necessary
145
+ if empty_category_name not in ground_truth_category_id_to_name.values():
146
+ empty_category_id = max(ground_truth_category_id_to_name.keys())+1
147
+ ground_truth_category_id_to_name[empty_category_id] = empty_category_name
148
+
149
+ ground_truth_category_names = sorted(list(ground_truth_category_id_to_name.values()))
150
+
151
+ for ann in ground_truth_data_cct['annotations']:
152
+ gt_image_id_to_annotations[ann['image_id']].append(ann)
153
+
154
+ gt_filename_to_category_names = defaultdict(set)
155
+
156
+ for im in ground_truth_data_cct['images']:
157
+ annotations_this_image = gt_image_id_to_annotations[im['id']]
158
+ for ann in annotations_this_image:
159
+ category_name = ground_truth_category_id_to_name[ann['category_id']]
160
+ gt_filename_to_category_names[im['file_name']].add(category_name)
161
+
162
+ for filename in gt_filename_to_category_names:
163
+
164
+ category_names_this_file = gt_filename_to_category_names[filename]
165
+
166
+ # The empty category should be exclusive
167
+ if empty_category_name in category_names_this_file:
168
+ assert len(category_names_this_file) == 1, \
169
+ 'Empty category assigned along with another category for {}'.format(filename)
170
+ assert len(category_names_this_file) > 0, \
171
+ 'No ground truth category assigned to {}'.format(filename)
172
+
173
+
174
+ #%% Load results
175
+
176
+ with open(results_file,'r') as f:
177
+ md_formatted_results = json.load(f)
178
+
179
+ results_category_id_to_name = md_formatted_results['detection_categories']
180
+
181
+
182
+ #%% Render images with detections
183
+
184
+ render_image_constants = {}
185
+ render_image_constants['filename_to_ground_truth_im'] = filename_to_ground_truth_im
186
+ render_image_constants['image_folder'] = image_folder
187
+ render_image_constants['preview_images_folder'] = preview_images_folder
188
+ render_image_constants['force_render_images'] = force_render_images
189
+ render_image_constants['results_category_id_to_name'] = results_category_id_to_name
190
+ render_image_constants['rendering_confidence_thresholds'] = rendering_confidence_thresholds
191
+ render_image_constants['target_image_size'] = target_image_size
192
+
193
+ if parallelize_rendering:
194
+
195
+ if parallelize_rendering_n_cores is None:
196
+ if parallelize_rendering_with_threads:
197
+ pool = ThreadPool()
198
+ else:
199
+ pool = Pool()
200
+ else:
201
+ if parallelize_rendering_with_threads:
202
+ pool = ThreadPool(parallelize_rendering_n_cores)
203
+ worker_string = 'threads'
204
+ else:
205
+ pool = Pool(parallelize_rendering_n_cores)
206
+ worker_string = 'processes'
207
+ print('Rendering images with {} {}'.format(parallelize_rendering_n_cores,
208
+ worker_string))
209
+
210
+ _ = list(tqdm(pool.imap(partial(render_image,render_image_constants=render_image_constants),
211
+ md_formatted_results['images']),
212
+ total=len(md_formatted_results['images'])))
213
+
214
+ else:
215
+
216
+ # im = md_formatted_results['images'][0]
217
+ for im in tqdm(md_formatted_results['images']):
218
+ render_image(im,render_image_constants)
219
+
220
+
221
+ #%% Map images to predicted categories, and vice-versa
222
+
223
+ filename_to_predicted_categories = defaultdict(set)
224
+ predicted_category_name_to_filenames = defaultdict(set)
225
+
226
+ # im = md_results['images'][0]
227
+ for im in tqdm(md_formatted_results['images']):
228
+
229
+ assert im['file'] in filename_to_ground_truth_im
230
+
231
+ # det = im['detections'][0]
232
+ for det in im['detections']:
233
+ category_name = results_category_id_to_name[det['category']]
234
+ detection_threshold = confidence_thresholds['default']
235
+ if category_name in confidence_thresholds:
236
+ detection_threshold = confidence_thresholds[category_name]
237
+ if det['conf'] > detection_threshold:
238
+ filename_to_predicted_categories[im['file']].add(category_name)
239
+ predicted_category_name_to_filenames[category_name].add(im['file'])
240
+
241
+ # ...for each detection
242
+
243
+ # ...for each image
244
+
245
+
246
+ #%% Create TP/TN/FP/FN lists
247
+
248
+ category_name_to_image_lists = {}
249
+
250
+ # These may not be identical; currently the ground truth contains an "unknown" category
251
+ # results_category_names = sorted(list(results_category_id_to_name.values()))
252
+
253
+ sub_page_tokens = ['fn','tn','fp','tp']
254
+
255
+ for category_name in ground_truth_category_names:
256
+
257
+ category_name_to_image_lists[category_name] = {}
258
+ for sub_page_token in sub_page_tokens:
259
+ category_name_to_image_lists[category_name][sub_page_token] = []
260
+
261
+ # filename = next(iter(gt_filename_to_category_names))
262
+ for filename in gt_filename_to_category_names.keys():
263
+
264
+ ground_truth_categories_this_image = gt_filename_to_category_names[filename]
265
+ predicted_categories_this_image = filename_to_predicted_categories[filename]
266
+
267
+ for category_name in ground_truth_category_names:
268
+
269
+ assignment = None
270
+
271
+ if category_name == empty_category_name:
272
+ # If this is an empty image
273
+ if category_name in ground_truth_categories_this_image:
274
+ assert len(ground_truth_categories_this_image) == 1
275
+ if len(predicted_categories_this_image) == 0:
276
+ assignment = 'tp'
277
+ else:
278
+ assignment = 'fn'
279
+ # If this not an empty image
280
+ else:
281
+ if len(predicted_categories_this_image) == 0:
282
+ assignment = 'fp'
283
+ else:
284
+ assignment = 'tn'
285
+
286
+ else:
287
+ if category_name in ground_truth_categories_this_image:
288
+ if category_name in predicted_categories_this_image:
289
+ assignment = 'tp'
290
+ else:
291
+ assignment = 'fn'
292
+ else:
293
+ if category_name in predicted_categories_this_image:
294
+ assignment = 'fp'
295
+ else:
296
+ assignment = 'tn'
297
+
298
+ category_name_to_image_lists[category_name][assignment].append(filename)
299
+
300
+ # ...for each filename
301
+
302
+
303
+ #%% Create confusion matrix
304
+
305
+ gt_category_name_to_category_index = {}
306
+
307
+ for i_category,category_name in enumerate(ground_truth_category_names):
308
+ gt_category_name_to_category_index[category_name] = i_category
309
+
310
+ n_categories = len(gt_category_name_to_category_index)
311
+
312
+ # indexed as [true,predicted]
313
+ confusion_matrix = np.zeros(shape=(n_categories,n_categories),dtype=int)
314
+
315
+ filename_to_results_im = {im['file']:im for im in md_formatted_results['images']}
316
+
317
+ true_predicted_to_file_list = defaultdict(list)
318
+
319
+ # filename = next(iter(gt_filename_to_category_names.keys()))
320
+ for filename in gt_filename_to_category_names.keys():
321
+
322
+ ground_truth_categories_this_image = gt_filename_to_category_names[filename]
323
+ assert len(ground_truth_categories_this_image) == 1
324
+ ground_truth_category_name = next(iter(ground_truth_categories_this_image))
325
+
326
+ results_im = filename_to_results_im[filename]
327
+
328
+ # If there were no detections at all, call this image empty
329
+ if len(results_im['detections']) == 0:
330
+ predicted_category_name = empty_category_name
331
+ # Otherwise look for above-threshold detections
332
+ else:
333
+ results_category_name_to_confidence = defaultdict(int)
334
+ for det in results_im['detections']:
335
+ category_name = results_category_id_to_name[det['category']]
336
+ detection_threshold = rendering_confidence_thresholds['default']
337
+ if category_name in rendering_confidence_thresholds:
338
+ detection_threshold = confidence_thresholds[category_name]
339
+ if det['conf'] > detection_threshold:
340
+ results_category_name_to_confidence[category_name] = max(
341
+ results_category_name_to_confidence[category_name],det['conf'])
342
+ # If there were no detections above threshold
343
+ if len(results_category_name_to_confidence) == 0:
344
+ predicted_category_name = empty_category_name
345
+ else:
346
+ predicted_category_name = max(results_category_name_to_confidence,
347
+ key=results_category_name_to_confidence.get)
348
+
349
+ ground_truth_category_index = gt_category_name_to_category_index[ground_truth_category_name]
350
+ predicted_category_index = gt_category_name_to_category_index[predicted_category_name]
351
+
352
+ true_predicted_token = ground_truth_category_name + '_' + predicted_category_name
353
+ true_predicted_to_file_list[true_predicted_token].append(filename)
354
+
355
+ confusion_matrix[ground_truth_category_index,predicted_category_index] += 1
356
+
357
+ plt.ioff()
358
+
359
+ fig_h = 3 + 0.3 * n_categories
360
+ fig_w = fig_h
361
+ fig = plt.figure(figsize=(fig_w, fig_h),tight_layout=True)
362
+
363
+ plot_utils.plot_confusion_matrix(
364
+ matrix=confusion_matrix,
365
+ classes=ground_truth_category_names,
366
+ normalize=False,
367
+ title='Confusion matrix',
368
+ cmap=plt.cm.Blues,
369
+ vmax=1.0,
370
+ use_colorbar=False,
371
+ y_label=True,
372
+ fig=fig)
373
+
374
+ cm_figure_fn_relative = 'confusion_matrix.png'
375
+ cm_figure_fn_abs = os.path.join(preview_folder, cm_figure_fn_relative)
376
+ # fig.show()
377
+ fig.savefig(cm_figure_fn_abs,dpi=100)
378
+ plt.close(fig)
379
+
380
+ # open_file(cm_figure_fn_abs)
381
+
382
+
383
+ #%% Create HTML confusion matrix
384
+
385
+ html_confusion_matrix = '<table class="result-table">\n'
386
+ html_confusion_matrix += '<tr>\n'
387
+ html_confusion_matrix += '<td>{}</td>\n'.format('True category')
388
+ for category_name in ground_truth_category_names:
389
+ html_confusion_matrix += '<td>{}</td>\n'.format('&nbsp;')
390
+ html_confusion_matrix += '</tr>\n'
391
+
392
+ for true_category in ground_truth_category_names:
393
+
394
+ html_confusion_matrix += '<tr>\n'
395
+ html_confusion_matrix += '<td>{}</td>\n'.format(true_category)
396
+
397
+ for predicted_category in ground_truth_category_names:
398
+
399
+ true_predicted_token = true_category + '_' + predicted_category
400
+ image_list = true_predicted_to_file_list[true_predicted_token]
401
+ if len(image_list) == 0:
402
+ td_content = '0'
403
+ else:
404
+ if html_image_list_options is None:
405
+ html_image_list_options = {}
406
+ title_string = 'true: {}, predicted {}'.format(
407
+ true_category,predicted_category)
408
+ html_image_list_options['headerHtml'] = '<h1>{}</h1>'.format(title_string)
409
+
410
+ html_image_info_list = []
411
+
412
+ for image_filename_relative in image_list:
413
+ html_image_info = {}
414
+ detections = filename_to_results_im[image_filename_relative]['detections']
415
+ if len(detections) == 0:
416
+ max_conf = 0
417
+ else:
418
+ max_conf = max([d['conf'] for d in detections])
419
+
420
+ title = '<b>Image</b>: {}, <b>Max conf</b>: {:0.3f}'.format(
421
+ image_filename_relative, max_conf)
422
+ image_link = 'images/' + os.path.basename(
423
+ image_to_output_file(image_filename_relative,preview_images_folder))
424
+ html_image_info = {
425
+ 'filename': image_link,
426
+ 'title': title,
427
+ 'textStyle':\
428
+ 'font-family:verdana,arial,calibri;font-size:80%;' + \
429
+ 'text-align:left;margin-top:20;margin-bottom:5'
430
+ }
431
+
432
+ html_image_info_list.append(html_image_info)
433
+
434
+ target_html_file_relative = true_predicted_token + '.html'
435
+ target_html_file_abs = os.path.join(preview_folder,target_html_file_relative)
436
+ write_html_image_list(
437
+ filename=target_html_file_abs,
438
+ images=html_image_info_list,
439
+ options=html_image_list_options)
440
+
441
+ td_content = '<a href="{}">{}</a>'.format(target_html_file_relative,
442
+ len(image_list))
443
+
444
+ html_confusion_matrix += '<td>{}</td>\n'.format(td_content)
445
+
446
+ # ...for each predicted category
447
+
448
+ html_confusion_matrix += '</tr>\n'
449
+
450
+ # ...for each true category
451
+
452
+ html_confusion_matrix += '<tr>\n'
453
+ html_confusion_matrix += '<td>&nbsp;</td>\n'
454
+
455
+ for category_name in ground_truth_category_names:
456
+ html_confusion_matrix += '<td class="rotate"><p style="margin-left:20px;">{}</p></td>\n'.format(
457
+ category_name)
458
+ html_confusion_matrix += '</tr>\n'
459
+
460
+ html_confusion_matrix += '</table>'
461
+
462
+
463
+ ##%% Create HTML sub-pages and HTML table
464
+
465
+ html_table = '<table class="result-table">\n'
466
+
467
+ html_table += '<tr>\n'
468
+ html_table += '<td>{}</td>\n'.format('True category')
469
+ for sub_page_token in sub_page_tokens:
470
+ html_table += '<td>{}</td>'.format(sub_page_token)
471
+ html_table += '</tr>\n'
472
+
473
+ filename_to_results_im = {im['file']:im for im in md_formatted_results['images']}
474
+
475
+ sub_page_token_to_page_name = {
476
+ 'fp':'false positives',
477
+ 'tp':'true positives',
478
+ 'fn':'false negatives',
479
+ 'tn':'true negatives'
480
+ }
481
+
482
+ # category_name = ground_truth_category_names[0]
483
+ for category_name in ground_truth_category_names:
484
+
485
+ html_table += '<tr>\n'
486
+
487
+ html_table += '<td>{}</td>\n'.format(category_name)
488
+
489
+ # sub_page_token = sub_page_tokens[0]
490
+ for sub_page_token in sub_page_tokens:
491
+
492
+ html_table += '<td>\n'
493
+
494
+ image_list = category_name_to_image_lists[category_name][sub_page_token]
495
+
496
+ if len(image_list) == 0:
497
+
498
+ html_table += '0\n'
499
+
500
+ else:
501
+
502
+ html_image_list_options = {}
503
+ title_string = '{}: {}'.format(category_name,sub_page_token_to_page_name[sub_page_token])
504
+ html_image_list_options['headerHtml'] = '<h1>{}</h1>'.format(title_string)
505
+
506
+ target_html_file_relative = '{}_{}.html'.format(category_name,sub_page_token)
507
+ target_html_file_abs = os.path.join(preview_folder,target_html_file_relative)
508
+
509
+ html_image_info_list = []
510
+
511
+ # image_filename_relative = image_list[0]
512
+ for image_filename_relative in image_list:
513
+
514
+ source_file = os.path.join(image_folder,image_filename_relative)
515
+ assert os.path.isfile(source_file)
516
+
517
+ html_image_info = {}
518
+ detections = filename_to_results_im[image_filename_relative]['detections']
519
+ if len(detections) == 0:
520
+ max_conf = 0
521
+ else:
522
+ max_conf = max([d['conf'] for d in detections])
523
+
524
+ title = '<b>Image</b>: {}, <b>Max conf</b>: {:0.3f}'.format(
525
+ image_filename_relative, max_conf)
526
+ image_link = 'images/' + os.path.basename(
527
+ image_to_output_file(image_filename_relative,preview_images_folder))
528
+ html_image_info = {
529
+ 'filename': image_link,
530
+ 'title': title,
531
+ 'linkTarget': source_file,
532
+ 'textStyle':\
533
+ 'font-family:verdana,arial,calibri;font-size:80%;' + \
534
+ 'text-align:left;margin-top:20;margin-bottom:5'
535
+ }
536
+
537
+ html_image_info_list.append(html_image_info)
538
+
539
+ # ...for each image
540
+
541
+ write_html_image_list(
542
+ filename=target_html_file_abs,
543
+ images=html_image_info_list,
544
+ options=html_image_list_options)
545
+
546
+ html_table += '<a href="{}">{}</a>\n'.format(target_html_file_relative,len(image_list))
547
+
548
+ html_table += '</td>\n'
549
+
550
+ # ...for each sub-page
551
+
552
+ html_table += '</tr>\n'
553
+
554
+ # ...for each category
555
+
556
+ html_table += '</table>'
557
+
558
+ html = '<html>\n'
559
+
560
+ style_header = """<head>
561
+ <style type="text/css">
562
+ a { text-decoration: none; }
563
+ body { font-family: segoe ui, calibri, "trebuchet ms", verdana, arial, sans-serif; }
564
+ div.contentdiv { margin-left: 20px; }
565
+ table.result-table { border:1px solid black; border-collapse: collapse; margin-left:50px;}
566
+ td,th { padding:10px; }
567
+ .rotate {
568
+ padding:0px;
569
+ writing-mode:vertical-lr;
570
+ -webkit-transform: rotate(-180deg);
571
+ -moz-transform: rotate(-180deg);
572
+ -ms-transform: rotate(-180deg);
573
+ -o-transform: rotate(-180deg);
574
+ transform: rotate(-180deg);
575
+ }
576
+ </style>
577
+ </head>"""
578
+
579
+ html += style_header + '\n'
580
+
581
+ html += '<body>\n'
582
+
583
+ html += '<h1>Results summary for {}</h1>\n'.format(job_name)
584
+
585
+ if model_file is not None and len(model_file) > 0:
586
+ html += '<p><b>Model file</b>: {}</p>'.format(os.path.basename(model_file))
587
+
588
+ html += '<p><b>Confidence thresholds</b></p>'
589
+
590
+ for c in confidence_thresholds.keys():
591
+ html += '<p style="margin-left:15px;">{}: {}</p>'.format(c,confidence_thresholds[c])
592
+
593
+ html += '<h2>Confusion matrix</h2>\n'
594
+
595
+ html += '<p>...assuming a single category per image.</p>\n'
596
+
597
+ html += '<img src="{}"/>\n'.format(cm_figure_fn_relative)
598
+
599
+ html += '<h2>Confusion matrix (with links)</h2>\n'
600
+
601
+ html += '<p>...assuming a single category per image.</p>\n'
602
+
603
+ html += html_confusion_matrix
604
+
605
+ html += '<h2>Per-class statistics</h2>\n'
606
+
607
+ html += html_table
608
+
609
+ html += '</body>\n'
610
+ html += '<html>\n'
611
+
612
+ target_html_file = os.path.join(preview_folder,'index.html')
613
+
614
+ with open(target_html_file,'w') as f:
615
+ f.write(html)
616
+
617
+
618
+ #%% Prepare return data
619
+
620
+ confusion_matrix_info = {}
621
+ confusion_matrix_info['html_file'] = target_html_file
622
+
623
+ return confusion_matrix_info
624
+
625
+ # ...render_detection_confusion_matrix(...)