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,570 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
detector_calibration.py
|
|
4
|
+
|
|
5
|
+
Tools for comparing/calibrating confidence values from detectors, particularly different
|
|
6
|
+
versions of MegaDetector.
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
#%% Constants and imports
|
|
11
|
+
|
|
12
|
+
import random
|
|
13
|
+
import copy
|
|
14
|
+
|
|
15
|
+
from tqdm import tqdm
|
|
16
|
+
from enum import IntEnum
|
|
17
|
+
from collections import defaultdict
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
import matplotlib
|
|
21
|
+
import matplotlib.pyplot as plt
|
|
22
|
+
|
|
23
|
+
from megadetector.postprocessing.validate_batch_results import \
|
|
24
|
+
validate_batch_results, ValidateBatchResultsOptions
|
|
25
|
+
from megadetector.utils.ct_utils import get_iou, max_none, is_iterable
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
#%% Classes
|
|
29
|
+
|
|
30
|
+
class CalibrationOptions:
|
|
31
|
+
"""
|
|
32
|
+
Options controlling comparison/calibration behavior.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self):
|
|
36
|
+
|
|
37
|
+
#: IoU threshold used for determining whether two detections are the same
|
|
38
|
+
#:
|
|
39
|
+
#: When multiple detections match, we will only use the highest-matching IoU.
|
|
40
|
+
self.iou_threshold = 0.6
|
|
41
|
+
|
|
42
|
+
#: Minimum confidence threshold to consider for calibration (should be lower than
|
|
43
|
+
#: the lowest value you would use in realistic situations)
|
|
44
|
+
self.confidence_threshold = 0.025
|
|
45
|
+
|
|
46
|
+
#: Should we populate the data_a and data_b fields in the return value?
|
|
47
|
+
self.return_data = False
|
|
48
|
+
|
|
49
|
+
#: Model name to use in printouts and plots for result set A
|
|
50
|
+
self.model_name_a = 'model_a'
|
|
51
|
+
|
|
52
|
+
#: Model name to use in printouts and plots for result set B
|
|
53
|
+
self.model_name_b = 'model_b'
|
|
54
|
+
|
|
55
|
+
#: Maximum number of samples to use for plotting or calibration per category,
|
|
56
|
+
#: or None to use all paired values. If separate_plots_by_category is False,
|
|
57
|
+
#: this is the overall number of points sampled.
|
|
58
|
+
self.max_samples_per_category = None
|
|
59
|
+
|
|
60
|
+
#: Should we make separate plots for each category? Mutually exclusive with
|
|
61
|
+
#: separate_plots_by_correctness.
|
|
62
|
+
self.separate_plots_by_category = True
|
|
63
|
+
|
|
64
|
+
#: Should we make separate plots for TPs/FPs? Mutually exclusive with
|
|
65
|
+
#: separate_plots_by_category.
|
|
66
|
+
self.separate_plots_by_correctness = False
|
|
67
|
+
|
|
68
|
+
#: List of category IDs to use for plotting comparisons, or None to plot
|
|
69
|
+
#: all categories.
|
|
70
|
+
self.categories_to_plot = None
|
|
71
|
+
|
|
72
|
+
#: Optionally map category ID to name in plot labels
|
|
73
|
+
self.category_id_to_name = None
|
|
74
|
+
|
|
75
|
+
#: Enable additional debug output
|
|
76
|
+
self.verbose = True
|
|
77
|
+
|
|
78
|
+
# ...class CalibrationOptions
|
|
79
|
+
|
|
80
|
+
class CalibrationMatchColumns(IntEnum):
|
|
81
|
+
"""
|
|
82
|
+
Enumeration defining columns in the calibration_matches list we'll assemble below.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
COLUMN_CONF_A = 0
|
|
86
|
+
COLUMN_CONF_B = 1
|
|
87
|
+
COLUMN_IOU = 2
|
|
88
|
+
COLUMN_I_IMAGE = 3
|
|
89
|
+
COLUMN_CATEGORY_ID = 4
|
|
90
|
+
COLUMN_MATCHES_GT = 5
|
|
91
|
+
|
|
92
|
+
class CalibrationResults:
|
|
93
|
+
"""
|
|
94
|
+
Results of a model-to-model comparison.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self):
|
|
98
|
+
|
|
99
|
+
#: List of tuples: [conf_a, conf_b, iou, i_image, category_id, matches_gt]
|
|
100
|
+
#:
|
|
101
|
+
#: If ground truth is supplied, [matches_gt] is a bool indicating whether either
|
|
102
|
+
#: of the detected boxes matches a ground truth box of the same category. If
|
|
103
|
+
#: ground truth is not supplied, [matches_gt] is None.
|
|
104
|
+
self.calibration_matches = []
|
|
105
|
+
|
|
106
|
+
#: Populated with the data loaded from json_filename_a if options.return_data is True
|
|
107
|
+
self.data_a = None
|
|
108
|
+
|
|
109
|
+
#: Populated with the data loaded from json_filename_b if options.return_data is True
|
|
110
|
+
self.data_b = None
|
|
111
|
+
|
|
112
|
+
# ...class CalibrationResults
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
#%% Calibration functions
|
|
116
|
+
|
|
117
|
+
def compare_model_confidence_values(json_filename_a,json_filename_b,json_filename_gt=None,options=None):
|
|
118
|
+
"""
|
|
119
|
+
Compare confidence values across two .json results files. Compares only detections that
|
|
120
|
+
can be matched by IoU, i.e., does not do anything with detections that only appear in one file.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
json_filename_a (str or dict): filename containing results from the first model to be compared;
|
|
124
|
+
should refer to the same images as [json_filename_b]. Can also be a loaded results dict.
|
|
125
|
+
json_filename_b (str or dict): filename containing results from the second model to be compared;
|
|
126
|
+
should refer to the same images as [json_filename_a]. Can also be a loaded results dict.
|
|
127
|
+
json_filename_gt (str or dict, optional): filename containing ground truth; should refer to the
|
|
128
|
+
same images as [json_filename_a] and [json_filename_b]. Can also be a loaded results dict.
|
|
129
|
+
Should be in COCO format.
|
|
130
|
+
options (CalibrationOptions, optional): all the parameters used to control this process, see
|
|
131
|
+
CalibrationOptions for details
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
CalibrationResults: description of the comparison results
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
## Option handling
|
|
138
|
+
|
|
139
|
+
if options is None:
|
|
140
|
+
options = CalibrationOptions()
|
|
141
|
+
|
|
142
|
+
validation_options = ValidateBatchResultsOptions()
|
|
143
|
+
validation_options.return_data = True
|
|
144
|
+
|
|
145
|
+
if isinstance(json_filename_a,str):
|
|
146
|
+
results_a = validate_batch_results(json_filename_a,options=validation_options)
|
|
147
|
+
assert len(results_a['validation_results']['errors']) == 0
|
|
148
|
+
else:
|
|
149
|
+
assert isinstance(json_filename_a,dict)
|
|
150
|
+
results_a = json_filename_a
|
|
151
|
+
|
|
152
|
+
if isinstance(json_filename_b,str):
|
|
153
|
+
results_b = validate_batch_results(json_filename_b,options=validation_options)
|
|
154
|
+
assert len(results_b['validation_results']['errors']) == 0
|
|
155
|
+
else:
|
|
156
|
+
assert isinstance(json_filename_b,dict)
|
|
157
|
+
results_b = json_filename_b
|
|
158
|
+
|
|
159
|
+
# Load ground truth, if supplied
|
|
160
|
+
gt_data = None
|
|
161
|
+
|
|
162
|
+
if json_filename_gt is not None:
|
|
163
|
+
if isinstance(json_filename_gt,str):
|
|
164
|
+
gt_data = validate_batch_results(json_filename_gt,
|
|
165
|
+
options=validation_options)
|
|
166
|
+
else:
|
|
167
|
+
assert isinstance(json_filename_gt,dict)
|
|
168
|
+
gt_data = json_filename_gt
|
|
169
|
+
|
|
170
|
+
## Make sure these results sets are comparable
|
|
171
|
+
|
|
172
|
+
image_filenames_a = [im['file'] for im in results_a['images']]
|
|
173
|
+
image_filenames_b = [im['file'] for im in results_b['images']]
|
|
174
|
+
|
|
175
|
+
assert set(image_filenames_a) == set(image_filenames_b), \
|
|
176
|
+
'Cannot calibrate non-matching image sets'
|
|
177
|
+
|
|
178
|
+
categories_a = results_a['detection_categories']
|
|
179
|
+
categories_b = results_b['detection_categories']
|
|
180
|
+
assert set(categories_a.keys()) == set(categories_b.keys())
|
|
181
|
+
for k in categories_a.keys():
|
|
182
|
+
assert categories_a[k] == categories_b[k], 'Category mismatch'
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
## Load ground truth if necessary
|
|
186
|
+
|
|
187
|
+
gt_category_name_to_id = None
|
|
188
|
+
gt_image_id_to_annotations = None
|
|
189
|
+
image_filename_to_gt_im = None
|
|
190
|
+
|
|
191
|
+
if gt_data is not None:
|
|
192
|
+
|
|
193
|
+
gt_category_name_to_id = {}
|
|
194
|
+
for c in gt_data['categories']:
|
|
195
|
+
gt_category_name_to_id[c['name']] = c['id']
|
|
196
|
+
|
|
197
|
+
image_filename_to_gt_im = {}
|
|
198
|
+
for im in gt_data['images']:
|
|
199
|
+
assert 'width' in im and 'height' in im, \
|
|
200
|
+
'I can only compare against GT that has "width" and "height" fields'
|
|
201
|
+
image_filename_to_gt_im[im['file_name']] = im
|
|
202
|
+
|
|
203
|
+
assert set(image_filename_to_gt_im.keys()) == set(image_filenames_a), \
|
|
204
|
+
'Ground truth filename list does not match image filename list'
|
|
205
|
+
|
|
206
|
+
gt_image_id_to_annotations = defaultdict(list)
|
|
207
|
+
for ann in gt_data['annotations']:
|
|
208
|
+
gt_image_id_to_annotations[ann['image_id']].append(ann)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
## Compare detections
|
|
212
|
+
|
|
213
|
+
image_filename_b_to_im = {}
|
|
214
|
+
for im in results_b['images']:
|
|
215
|
+
image_filename_b_to_im[im['file']] = im
|
|
216
|
+
|
|
217
|
+
n_detections_a = 0
|
|
218
|
+
n_detections_a_queried = 0
|
|
219
|
+
n_detections_a_matched = 0
|
|
220
|
+
|
|
221
|
+
calibration_matches = []
|
|
222
|
+
|
|
223
|
+
# For each image
|
|
224
|
+
# im_a = results_a['images'][0]
|
|
225
|
+
for i_image,im_a in tqdm(enumerate(results_a['images']),total=len(results_a['images'])):
|
|
226
|
+
|
|
227
|
+
fn = im_a['file']
|
|
228
|
+
im_b = image_filename_b_to_im[fn]
|
|
229
|
+
|
|
230
|
+
if 'detections' not in im_a or im_a['detections'] is None:
|
|
231
|
+
continue
|
|
232
|
+
if 'detections' not in im_b or im_b['detections'] is None:
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
im_gt = None
|
|
236
|
+
if gt_data is not None:
|
|
237
|
+
im_gt = image_filename_to_gt_im[fn]
|
|
238
|
+
|
|
239
|
+
# For each detection in result set A...
|
|
240
|
+
#
|
|
241
|
+
# det_a = im_a['detections'][0]
|
|
242
|
+
for det_a in im_a['detections']:
|
|
243
|
+
|
|
244
|
+
n_detections_a += 1
|
|
245
|
+
|
|
246
|
+
conf_a = det_a['conf']
|
|
247
|
+
category_id = det_a['category']
|
|
248
|
+
|
|
249
|
+
# Is this above threshold?
|
|
250
|
+
if conf_a < options.confidence_threshold:
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
n_detections_a_queried += 1
|
|
254
|
+
|
|
255
|
+
bbox_a = det_a['bbox']
|
|
256
|
+
|
|
257
|
+
best_iou = None
|
|
258
|
+
best_iou_conf = None
|
|
259
|
+
best_bbox_b = None
|
|
260
|
+
|
|
261
|
+
# For each detection in result set B...
|
|
262
|
+
#
|
|
263
|
+
# det_b = im_b['detections'][0]
|
|
264
|
+
for det_b in im_b['detections']:
|
|
265
|
+
|
|
266
|
+
# Is this the same category?
|
|
267
|
+
if det_b['category'] != category_id:
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
conf_b = det_b['conf']
|
|
271
|
+
|
|
272
|
+
# Is this above threshold?
|
|
273
|
+
if conf_b < options.confidence_threshold:
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
bbox_b = det_b['bbox']
|
|
277
|
+
|
|
278
|
+
iou = get_iou(bbox_a,bbox_b)
|
|
279
|
+
|
|
280
|
+
# Is this an adequate IoU to consider?
|
|
281
|
+
if iou < options.iou_threshold:
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
# Is this the best match so far?
|
|
285
|
+
if best_iou is None or iou > best_iou:
|
|
286
|
+
best_iou = iou
|
|
287
|
+
best_iou_conf = conf_b
|
|
288
|
+
best_bbox_b = bbox_b
|
|
289
|
+
|
|
290
|
+
# ...for each detection in im_b
|
|
291
|
+
|
|
292
|
+
# If we found a match between A and B
|
|
293
|
+
if best_iou is not None:
|
|
294
|
+
|
|
295
|
+
n_detections_a_matched += 1
|
|
296
|
+
|
|
297
|
+
# Does this pair of matched detections also match a ground truth box?
|
|
298
|
+
matches_gt = None
|
|
299
|
+
|
|
300
|
+
if im_gt is not None:
|
|
301
|
+
|
|
302
|
+
def max_iou_between_detection_and_gt(detection_box,category_name,im_gt,gt_annotations):
|
|
303
|
+
|
|
304
|
+
max_iou = None
|
|
305
|
+
|
|
306
|
+
# Which category ID are we looking for?
|
|
307
|
+
gt_category_id_for_detected_category_name = \
|
|
308
|
+
gt_category_name_to_id[category_name]
|
|
309
|
+
|
|
310
|
+
# For each GT annotation
|
|
311
|
+
#
|
|
312
|
+
# ann = gt_annotations[0]
|
|
313
|
+
for ann in gt_annotations:
|
|
314
|
+
|
|
315
|
+
# Only match against boxes in the same category
|
|
316
|
+
if ann['category_id'] != gt_category_id_for_detected_category_name:
|
|
317
|
+
continue
|
|
318
|
+
if 'bbox' not in ann:
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
# Normalize this box
|
|
322
|
+
#
|
|
323
|
+
# COCO format: [x,y,width,height]
|
|
324
|
+
# normalized format: [x_min, y_min, width_of_box, height_of_box]
|
|
325
|
+
normalized_gt_box = [ann['bbox'][0]/im_gt['width'],ann['bbox'][1]/im_gt['height'],
|
|
326
|
+
ann['bbox'][2]/im_gt['width'],ann['bbox'][3]/im_gt['height']]
|
|
327
|
+
|
|
328
|
+
iou = get_iou(detection_box, normalized_gt_box)
|
|
329
|
+
if max_iou is None or iou > max_iou:
|
|
330
|
+
max_iou = iou
|
|
331
|
+
|
|
332
|
+
# ...for each gt box
|
|
333
|
+
|
|
334
|
+
return max_iou
|
|
335
|
+
|
|
336
|
+
# ...def min_iou_between_detections_and_gt(...)
|
|
337
|
+
|
|
338
|
+
gt_annotations = gt_image_id_to_annotations[im_gt['id']]
|
|
339
|
+
|
|
340
|
+
# If they matched, the A and B boxes have the same category by definition
|
|
341
|
+
category_name = categories_a[det_a['category']]
|
|
342
|
+
|
|
343
|
+
max_iou_with_bbox_a = \
|
|
344
|
+
max_iou_between_detection_and_gt(bbox_a,category_name,im_gt,gt_annotations)
|
|
345
|
+
max_iou_with_bbox_b = \
|
|
346
|
+
max_iou_between_detection_and_gt(best_bbox_b,category_name,im_gt,gt_annotations)
|
|
347
|
+
|
|
348
|
+
max_iou_with_either_detection_set = max_none(max_iou_with_bbox_a,
|
|
349
|
+
max_iou_with_bbox_b)
|
|
350
|
+
|
|
351
|
+
matches_gt = False
|
|
352
|
+
if (max_iou_with_either_detection_set is not None) and \
|
|
353
|
+
(max_iou_with_either_detection_set >= options.iou_threshold):
|
|
354
|
+
matches_gt = True
|
|
355
|
+
|
|
356
|
+
# ...if we have ground truth
|
|
357
|
+
|
|
358
|
+
conf_result = [conf_a,best_iou_conf,best_iou,i_image,category_id,matches_gt]
|
|
359
|
+
calibration_matches.append(conf_result)
|
|
360
|
+
|
|
361
|
+
# ...if we had a match between A and B
|
|
362
|
+
# ...for each detection in im_a
|
|
363
|
+
|
|
364
|
+
# ...for each image in result set A
|
|
365
|
+
|
|
366
|
+
if options.verbose:
|
|
367
|
+
|
|
368
|
+
print('\nOf {} detections in result set A, queried {}, matched {}'.format(
|
|
369
|
+
n_detections_a,n_detections_a_queried,n_detections_a_matched))
|
|
370
|
+
|
|
371
|
+
if gt_data is not None:
|
|
372
|
+
n_matches = 0
|
|
373
|
+
for m in calibration_matches:
|
|
374
|
+
assert m[CalibrationMatchColumns.COLUMN_MATCHES_GT] is not None
|
|
375
|
+
if m[CalibrationMatchColumns.COLUMN_MATCHES_GT]:
|
|
376
|
+
n_matches += 1
|
|
377
|
+
print('{} matches also matched ground truth'.format(n_matches))
|
|
378
|
+
|
|
379
|
+
assert len(calibration_matches) == n_detections_a_matched
|
|
380
|
+
|
|
381
|
+
calibration_results = CalibrationResults()
|
|
382
|
+
calibration_results.calibration_matches = calibration_matches
|
|
383
|
+
|
|
384
|
+
if options.return_data:
|
|
385
|
+
calibration_results.data_a = results_a
|
|
386
|
+
calibration_results.data_b = results_b
|
|
387
|
+
|
|
388
|
+
return calibration_results
|
|
389
|
+
|
|
390
|
+
# ...def compare_model_confidence_values(...)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
#%% Plotting functions
|
|
394
|
+
|
|
395
|
+
def plot_matched_confidence_values(calibration_results,output_filename,options=None):
|
|
396
|
+
"""
|
|
397
|
+
Given a set of paired confidence values for matching detections (from
|
|
398
|
+
compare_model_confidence_values), plot histograms of those pairs for each
|
|
399
|
+
detection category.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
calibration_results (CalibrationResults): output from a call to
|
|
403
|
+
compare_model_confidence_values, containing paired confidence
|
|
404
|
+
values for two sets of detection results.
|
|
405
|
+
output_filename (str): filename to write the plot (.png or .jpg)
|
|
406
|
+
options (CalibrationOptions, optional): plotting options, see
|
|
407
|
+
CalibrationOptions for details.
|
|
408
|
+
"""
|
|
409
|
+
|
|
410
|
+
fig_w = 12
|
|
411
|
+
fig_h = 8
|
|
412
|
+
n_hist_bins = 80
|
|
413
|
+
|
|
414
|
+
if options is None:
|
|
415
|
+
options = CalibrationOptions()
|
|
416
|
+
|
|
417
|
+
assert not (options.separate_plots_by_category and \
|
|
418
|
+
options.separate_plots_by_correctness), \
|
|
419
|
+
'separate_plots_by_category and separate_plots_by_correctness are mutually exclusive'
|
|
420
|
+
|
|
421
|
+
category_id_to_name = None
|
|
422
|
+
category_to_samples = None
|
|
423
|
+
|
|
424
|
+
calibration_matches = calibration_results.calibration_matches
|
|
425
|
+
|
|
426
|
+
# If we're just lumping everything into one plot
|
|
427
|
+
if (not options.separate_plots_by_category) and (not options.separate_plots_by_correctness):
|
|
428
|
+
|
|
429
|
+
category_id_to_name = {'0':'all_categories'}
|
|
430
|
+
category_to_samples = {'0': []}
|
|
431
|
+
|
|
432
|
+
# Make everything category "0" (arbitrary)
|
|
433
|
+
calibration_matches = copy.deepcopy(calibration_matches)
|
|
434
|
+
for m in calibration_matches:
|
|
435
|
+
m[CalibrationMatchColumns.COLUMN_CATEGORY_ID] = '0'
|
|
436
|
+
if (options.max_samples_per_category is not None) and \
|
|
437
|
+
(len(calibration_matches) > options.max_samples_per_category):
|
|
438
|
+
calibration_matches = \
|
|
439
|
+
random.sample(calibration_matches,options.max_samples_per_category)
|
|
440
|
+
category_to_samples['0'] = calibration_matches
|
|
441
|
+
|
|
442
|
+
# If we're separating into lines for FPs and TPs (but not separating by category)
|
|
443
|
+
elif options.separate_plots_by_correctness:
|
|
444
|
+
|
|
445
|
+
assert not options.separate_plots_by_category
|
|
446
|
+
|
|
447
|
+
category_id_tp = '0'
|
|
448
|
+
category_id_fp = '1'
|
|
449
|
+
|
|
450
|
+
category_id_to_name = {category_id_tp:'TP', category_id_fp:'FP'}
|
|
451
|
+
category_to_samples = {category_id_tp: [], category_id_fp: []}
|
|
452
|
+
|
|
453
|
+
for m in calibration_matches:
|
|
454
|
+
assert m[CalibrationMatchColumns.COLUMN_MATCHES_GT] is not None, \
|
|
455
|
+
"Can't plot by correctness when GT status is not available for every match"
|
|
456
|
+
if m[CalibrationMatchColumns.COLUMN_MATCHES_GT]:
|
|
457
|
+
category_to_samples[category_id_tp].append(m)
|
|
458
|
+
else:
|
|
459
|
+
category_to_samples[category_id_fp].append(m)
|
|
460
|
+
|
|
461
|
+
# If we're separating by category
|
|
462
|
+
else:
|
|
463
|
+
|
|
464
|
+
assert options.separate_plots_by_category
|
|
465
|
+
|
|
466
|
+
category_to_samples = defaultdict(list)
|
|
467
|
+
|
|
468
|
+
category_to_matches = defaultdict(list)
|
|
469
|
+
for m in calibration_matches:
|
|
470
|
+
category_id = m[CalibrationMatchColumns.COLUMN_CATEGORY_ID]
|
|
471
|
+
category_to_matches[category_id].append(m)
|
|
472
|
+
|
|
473
|
+
category_id_to_name = None
|
|
474
|
+
if options.category_id_to_name is not None:
|
|
475
|
+
category_id_to_name = options.category_id_to_name
|
|
476
|
+
|
|
477
|
+
for i_category,category_id in enumerate(category_to_matches.keys()):
|
|
478
|
+
|
|
479
|
+
matches_this_category = category_to_matches[category_id]
|
|
480
|
+
|
|
481
|
+
if (options.max_samples_per_category is None) or \
|
|
482
|
+
(len(matches_this_category) <= options.max_samples_per_category):
|
|
483
|
+
category_to_samples[category_id] = matches_this_category
|
|
484
|
+
else:
|
|
485
|
+
assert len(matches_this_category) > options.max_samples_per_category
|
|
486
|
+
category_to_samples[category_id] = random.sample(matches_this_category,options.max_samples_per_category)
|
|
487
|
+
|
|
488
|
+
del category_to_matches
|
|
489
|
+
|
|
490
|
+
del calibration_matches
|
|
491
|
+
|
|
492
|
+
if options.verbose:
|
|
493
|
+
n_samples_for_histogram = 0
|
|
494
|
+
for c in category_to_samples:
|
|
495
|
+
n_samples_for_histogram += len(category_to_samples[c])
|
|
496
|
+
print('Creating a histogram based on {} samples'.format(n_samples_for_histogram))
|
|
497
|
+
|
|
498
|
+
categories_to_plot = list(category_to_samples.keys())
|
|
499
|
+
|
|
500
|
+
if options.categories_to_plot is not None:
|
|
501
|
+
categories_to_plot = [category_id for category_id in categories_to_plot if\
|
|
502
|
+
category_id in options.categories_to_plot]
|
|
503
|
+
|
|
504
|
+
n_subplots = len(categories_to_plot)
|
|
505
|
+
|
|
506
|
+
plt.ioff()
|
|
507
|
+
|
|
508
|
+
fig = matplotlib.figure.Figure(figsize=(fig_w, fig_h), tight_layout=True)
|
|
509
|
+
# fig,axes = plt.subplots(nrows=n_subplots,ncols=1)
|
|
510
|
+
|
|
511
|
+
axes = fig.subplots(n_subplots, 1)
|
|
512
|
+
|
|
513
|
+
if not is_iterable(axes):
|
|
514
|
+
assert n_subplots == 1
|
|
515
|
+
axes = [axes]
|
|
516
|
+
|
|
517
|
+
# i_category = 0; category_id = categories_to_plot[i_category]
|
|
518
|
+
for i_category,category_id in enumerate(categories_to_plot):
|
|
519
|
+
|
|
520
|
+
ax = axes[i_category]
|
|
521
|
+
|
|
522
|
+
category_string = str(category_id)
|
|
523
|
+
if (category_id_to_name is not None) and (category_id in category_id_to_name):
|
|
524
|
+
category_string = category_id_to_name[category_id]
|
|
525
|
+
|
|
526
|
+
samples_this_category = category_to_samples[category_id]
|
|
527
|
+
x = [m[0] for m in samples_this_category]
|
|
528
|
+
y = [m[1] for m in samples_this_category]
|
|
529
|
+
|
|
530
|
+
weights_a = np.ones_like(x)/float(len(x))
|
|
531
|
+
weights_b = np.ones_like(y)/float(len(y))
|
|
532
|
+
|
|
533
|
+
# Plot the first lie a little thicker so the second line will always show up
|
|
534
|
+
ax.hist(x,histtype='step',bins=n_hist_bins,density=False,color='red',weights=weights_a,linewidth=3.0)
|
|
535
|
+
ax.hist(y,histtype='step',bins=n_hist_bins,density=False,color='blue',weights=weights_b,linewidth=1.5)
|
|
536
|
+
|
|
537
|
+
ax.legend([options.model_name_a,options.model_name_b])
|
|
538
|
+
ax.set_ylabel(category_string)
|
|
539
|
+
# plt.tight_layout()
|
|
540
|
+
|
|
541
|
+
# I experimented with heat maps, but they weren't very informative.
|
|
542
|
+
# Leaving this code here in case I revisit. Note to self: scatter plots
|
|
543
|
+
# were a disaster.
|
|
544
|
+
if False:
|
|
545
|
+
heatmap, xedges, yedges = np.histogram2d(x, y, bins=30)
|
|
546
|
+
extent = [xedges[0], xedges[-1], yedges[0], yedges[-1]]
|
|
547
|
+
plt.imshow(heatmap.T, extent=extent, origin='lower', norm='log')
|
|
548
|
+
|
|
549
|
+
# ...for each category for which we need to generate a histogram
|
|
550
|
+
|
|
551
|
+
plt.close(fig)
|
|
552
|
+
fig.savefig(output_filename,dpi=100)
|
|
553
|
+
|
|
554
|
+
# ...def plot_matched_confidence_values(...)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
#%% Interactive driver(s)
|
|
558
|
+
|
|
559
|
+
if False:
|
|
560
|
+
|
|
561
|
+
#%%
|
|
562
|
+
|
|
563
|
+
options = ValidateBatchResultsOptions()
|
|
564
|
+
# json_filename = r'g:\temp\format.json'
|
|
565
|
+
# json_filename = r'g:\temp\test-videos\video_results.json'
|
|
566
|
+
json_filename = r'g:\temp\test-videos\image_results.json'
|
|
567
|
+
options.check_image_existence = True
|
|
568
|
+
options.relative_path_base = r'g:\temp\test-videos'
|
|
569
|
+
validate_batch_results(json_filename,options)
|
|
570
|
+
|