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.
- megadetector/__init__.py +0 -0
- megadetector/api/__init__.py +0 -0
- megadetector/api/batch_processing/integration/digiKam/setup.py +6 -0
- megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +465 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/config_template.py +5 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +125 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +55 -0
- megadetector/classification/__init__.py +0 -0
- megadetector/classification/aggregate_classifier_probs.py +108 -0
- megadetector/classification/analyze_failed_images.py +227 -0
- megadetector/classification/cache_batchapi_outputs.py +198 -0
- megadetector/classification/create_classification_dataset.py +626 -0
- megadetector/classification/crop_detections.py +516 -0
- megadetector/classification/csv_to_json.py +226 -0
- megadetector/classification/detect_and_crop.py +853 -0
- megadetector/classification/efficientnet/__init__.py +9 -0
- megadetector/classification/efficientnet/model.py +415 -0
- megadetector/classification/efficientnet/utils.py +608 -0
- megadetector/classification/evaluate_model.py +520 -0
- megadetector/classification/identify_mislabeled_candidates.py +152 -0
- megadetector/classification/json_to_azcopy_list.py +63 -0
- megadetector/classification/json_validator.py +696 -0
- megadetector/classification/map_classification_categories.py +276 -0
- megadetector/classification/merge_classification_detection_output.py +509 -0
- megadetector/classification/prepare_classification_script.py +194 -0
- megadetector/classification/prepare_classification_script_mc.py +228 -0
- megadetector/classification/run_classifier.py +287 -0
- megadetector/classification/save_mislabeled.py +110 -0
- megadetector/classification/train_classifier.py +827 -0
- megadetector/classification/train_classifier_tf.py +725 -0
- megadetector/classification/train_utils.py +323 -0
- megadetector/data_management/__init__.py +0 -0
- megadetector/data_management/animl_to_md.py +161 -0
- megadetector/data_management/annotations/__init__.py +0 -0
- megadetector/data_management/annotations/annotation_constants.py +33 -0
- megadetector/data_management/camtrap_dp_to_coco.py +270 -0
- megadetector/data_management/cct_json_utils.py +566 -0
- megadetector/data_management/cct_to_md.py +184 -0
- megadetector/data_management/cct_to_wi.py +293 -0
- megadetector/data_management/coco_to_labelme.py +284 -0
- megadetector/data_management/coco_to_yolo.py +701 -0
- megadetector/data_management/databases/__init__.py +0 -0
- megadetector/data_management/databases/add_width_and_height_to_db.py +107 -0
- megadetector/data_management/databases/combine_coco_camera_traps_files.py +210 -0
- megadetector/data_management/databases/integrity_check_json_db.py +563 -0
- megadetector/data_management/databases/subset_json_db.py +195 -0
- megadetector/data_management/generate_crops_from_cct.py +200 -0
- megadetector/data_management/get_image_sizes.py +164 -0
- megadetector/data_management/labelme_to_coco.py +559 -0
- megadetector/data_management/labelme_to_yolo.py +349 -0
- megadetector/data_management/lila/__init__.py +0 -0
- megadetector/data_management/lila/create_lila_blank_set.py +556 -0
- megadetector/data_management/lila/create_lila_test_set.py +192 -0
- megadetector/data_management/lila/create_links_to_md_results_files.py +106 -0
- megadetector/data_management/lila/download_lila_subset.py +182 -0
- megadetector/data_management/lila/generate_lila_per_image_labels.py +777 -0
- megadetector/data_management/lila/get_lila_annotation_counts.py +174 -0
- megadetector/data_management/lila/get_lila_image_counts.py +112 -0
- megadetector/data_management/lila/lila_common.py +319 -0
- megadetector/data_management/lila/test_lila_metadata_urls.py +164 -0
- megadetector/data_management/mewc_to_md.py +344 -0
- megadetector/data_management/ocr_tools.py +873 -0
- megadetector/data_management/read_exif.py +964 -0
- megadetector/data_management/remap_coco_categories.py +195 -0
- megadetector/data_management/remove_exif.py +156 -0
- megadetector/data_management/rename_images.py +194 -0
- megadetector/data_management/resize_coco_dataset.py +665 -0
- megadetector/data_management/speciesnet_to_md.py +41 -0
- megadetector/data_management/wi_download_csv_to_coco.py +247 -0
- megadetector/data_management/yolo_output_to_md_output.py +594 -0
- megadetector/data_management/yolo_to_coco.py +984 -0
- megadetector/data_management/zamba_to_md.py +188 -0
- megadetector/detection/__init__.py +0 -0
- megadetector/detection/change_detection.py +840 -0
- megadetector/detection/process_video.py +479 -0
- megadetector/detection/pytorch_detector.py +1451 -0
- megadetector/detection/run_detector.py +1267 -0
- megadetector/detection/run_detector_batch.py +2172 -0
- megadetector/detection/run_inference_with_yolov5_val.py +1314 -0
- megadetector/detection/run_md_and_speciesnet.py +1604 -0
- megadetector/detection/run_tiled_inference.py +1044 -0
- megadetector/detection/tf_detector.py +209 -0
- megadetector/detection/video_utils.py +1379 -0
- megadetector/postprocessing/__init__.py +0 -0
- megadetector/postprocessing/add_max_conf.py +72 -0
- megadetector/postprocessing/categorize_detections_by_size.py +166 -0
- megadetector/postprocessing/classification_postprocessing.py +1943 -0
- megadetector/postprocessing/combine_batch_outputs.py +249 -0
- megadetector/postprocessing/compare_batch_results.py +2110 -0
- megadetector/postprocessing/convert_output_format.py +403 -0
- megadetector/postprocessing/create_crop_folder.py +629 -0
- megadetector/postprocessing/detector_calibration.py +570 -0
- megadetector/postprocessing/generate_csv_report.py +522 -0
- megadetector/postprocessing/load_api_results.py +223 -0
- megadetector/postprocessing/md_to_coco.py +428 -0
- megadetector/postprocessing/md_to_labelme.py +351 -0
- megadetector/postprocessing/md_to_wi.py +41 -0
- megadetector/postprocessing/merge_detections.py +392 -0
- megadetector/postprocessing/postprocess_batch_results.py +2140 -0
- megadetector/postprocessing/remap_detection_categories.py +226 -0
- megadetector/postprocessing/render_detection_confusion_matrix.py +677 -0
- megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +206 -0
- megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +82 -0
- megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +1665 -0
- megadetector/postprocessing/separate_detections_into_folders.py +795 -0
- megadetector/postprocessing/subset_json_detector_output.py +964 -0
- megadetector/postprocessing/top_folders_to_bottom.py +238 -0
- megadetector/postprocessing/validate_batch_results.py +332 -0
- megadetector/taxonomy_mapping/__init__.py +0 -0
- megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +491 -0
- megadetector/taxonomy_mapping/map_new_lila_datasets.py +211 -0
- megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +165 -0
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +543 -0
- megadetector/taxonomy_mapping/retrieve_sample_image.py +71 -0
- megadetector/taxonomy_mapping/simple_image_download.py +231 -0
- megadetector/taxonomy_mapping/species_lookup.py +1008 -0
- megadetector/taxonomy_mapping/taxonomy_csv_checker.py +159 -0
- megadetector/taxonomy_mapping/taxonomy_graph.py +346 -0
- megadetector/taxonomy_mapping/validate_lila_category_mappings.py +83 -0
- megadetector/tests/__init__.py +0 -0
- megadetector/tests/test_nms_synthetic.py +335 -0
- megadetector/utils/__init__.py +0 -0
- megadetector/utils/ct_utils.py +1857 -0
- megadetector/utils/directory_listing.py +199 -0
- megadetector/utils/extract_frames_from_video.py +307 -0
- megadetector/utils/gpu_test.py +125 -0
- megadetector/utils/md_tests.py +2072 -0
- megadetector/utils/path_utils.py +2872 -0
- megadetector/utils/process_utils.py +172 -0
- megadetector/utils/split_locations_into_train_val.py +237 -0
- megadetector/utils/string_utils.py +234 -0
- megadetector/utils/url_utils.py +825 -0
- megadetector/utils/wi_platform_utils.py +968 -0
- megadetector/utils/wi_taxonomy_utils.py +1766 -0
- megadetector/utils/write_html_image_list.py +239 -0
- megadetector/visualization/__init__.py +0 -0
- megadetector/visualization/plot_utils.py +309 -0
- megadetector/visualization/render_images_with_thumbnails.py +243 -0
- megadetector/visualization/visualization_utils.py +1973 -0
- megadetector/visualization/visualize_db.py +630 -0
- megadetector/visualization/visualize_detector_output.py +498 -0
- megadetector/visualization/visualize_video_output.py +705 -0
- megadetector-10.0.15.dist-info/METADATA +115 -0
- megadetector-10.0.15.dist-info/RECORD +147 -0
- megadetector-10.0.15.dist-info/WHEEL +5 -0
- megadetector-10.0.15.dist-info/licenses/LICENSE +19 -0
- 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(' ')
|
|
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> </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(...)
|