megadetector 5.0.20__py3-none-any.whl → 5.0.21__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.
- megadetector/data_management/importers/osu-small-animals-to-json.py +4 -4
- megadetector/data_management/yolo_output_to_md_output.py +18 -5
- megadetector/detection/video_utils.py +19 -7
- megadetector/postprocessing/combine_api_outputs.py +1 -1
- megadetector/postprocessing/detector_calibration.py +367 -0
- megadetector/postprocessing/md_to_coco.py +2 -1
- megadetector/postprocessing/postprocess_batch_results.py +32 -20
- megadetector/postprocessing/validate_batch_results.py +118 -58
- megadetector/utils/md_tests.py +14 -12
- megadetector/utils/path_utils.py +139 -30
- megadetector/utils/write_html_image_list.py +16 -5
- megadetector/visualization/visualization_utils.py +126 -23
- megadetector/visualization/visualize_db.py +104 -63
- {megadetector-5.0.20.dist-info → megadetector-5.0.21.dist-info}/METADATA +1 -1
- {megadetector-5.0.20.dist-info → megadetector-5.0.21.dist-info}/RECORD +18 -18
- {megadetector-5.0.20.dist-info → megadetector-5.0.21.dist-info}/WHEEL +1 -1
- megadetector/data_management/importers/prepare-noaa-fish-data-for-lila.py +0 -359
- {megadetector-5.0.20.dist-info → megadetector-5.0.21.dist-info}/LICENSE +0 -0
- {megadetector-5.0.20.dist-info → megadetector-5.0.21.dist-info}/top_level.txt +0 -0
|
@@ -12,17 +12,17 @@ Prepare the OSU Small Animals dataset for LILA release:
|
|
|
12
12
|
|
|
13
13
|
import os
|
|
14
14
|
|
|
15
|
-
input_folder =
|
|
15
|
+
input_folder = os.path.expanduser('~/osu-small-animals')
|
|
16
16
|
assert os.path.isdir(input_folder)
|
|
17
17
|
|
|
18
|
-
output_folder =
|
|
18
|
+
output_folder = os.path.expanduser('~/osu-small-animals-lila')
|
|
19
19
|
os.makedirs(output_folder,exist_ok=True)
|
|
20
20
|
output_file = os.path.join(output_folder,'osu-small-animals.json')
|
|
21
21
|
|
|
22
|
-
preview_folder =
|
|
22
|
+
preview_folder = os.path.expanduser('~/osu-small-animals-preview')
|
|
23
23
|
os.makedirs(preview_folder,exist_ok=True)
|
|
24
24
|
|
|
25
|
-
common_to_latin_file = r'
|
|
25
|
+
common_to_latin_file = r'osu-small-animals-common-to-latin.txt'
|
|
26
26
|
assert os.path.isfile(common_to_latin_file)
|
|
27
27
|
|
|
28
28
|
|
|
@@ -59,19 +59,21 @@ def read_classes_from_yolo_dataset_file(fn):
|
|
|
59
59
|
integer category IDs to string category names.
|
|
60
60
|
|
|
61
61
|
Args:
|
|
62
|
-
fn (str): YOLOv5/YOLOv8 dataset file with a .yml or .yaml extension,
|
|
63
|
-
mapping integer category IDs to category names.
|
|
62
|
+
fn (str): YOLOv5/YOLOv8 dataset file with a .yml or .yaml extension, a .json file
|
|
63
|
+
mapping integer category IDs to category names, or a .txt file with a flat
|
|
64
|
+
list of classes.
|
|
64
65
|
|
|
65
66
|
Returns:
|
|
66
67
|
dict: a mapping from integer category IDs to category names
|
|
67
68
|
"""
|
|
68
|
-
|
|
69
|
+
|
|
70
|
+
category_id_to_name = {}
|
|
71
|
+
|
|
69
72
|
if fn.endswith('.yml') or fn.endswith('.yaml'):
|
|
70
73
|
|
|
71
74
|
with open(fn,'r') as f:
|
|
72
75
|
lines = f.readlines()
|
|
73
76
|
|
|
74
|
-
category_id_to_name = {}
|
|
75
77
|
pat = '\d+:.+'
|
|
76
78
|
for s in lines:
|
|
77
79
|
if re.search(pat,s) is not None:
|
|
@@ -83,10 +85,21 @@ def read_classes_from_yolo_dataset_file(fn):
|
|
|
83
85
|
|
|
84
86
|
with open(fn,'r') as f:
|
|
85
87
|
d_in = json.load(f)
|
|
86
|
-
category_id_to_name = {}
|
|
87
88
|
for k in d_in.keys():
|
|
88
89
|
category_id_to_name[int(k)] = d_in[k]
|
|
89
90
|
|
|
91
|
+
elif fn.endswith('.txt'):
|
|
92
|
+
|
|
93
|
+
with open(fn,'r') as f:
|
|
94
|
+
lines = f.readlines()
|
|
95
|
+
next_category_id = 0
|
|
96
|
+
for line in lines:
|
|
97
|
+
s = line.strip()
|
|
98
|
+
if len(s) == 0:
|
|
99
|
+
continue
|
|
100
|
+
category_id_to_name[next_category_id] = s
|
|
101
|
+
next_category_id += 1
|
|
102
|
+
|
|
90
103
|
else:
|
|
91
104
|
|
|
92
105
|
raise ValueError('Unrecognized category file type: {}'.format(fn))
|
|
@@ -678,12 +678,18 @@ def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,
|
|
|
678
678
|
return frame_filenames,fs
|
|
679
679
|
|
|
680
680
|
|
|
681
|
-
def video_folder_to_frames(input_folder,
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
681
|
+
def video_folder_to_frames(input_folder,
|
|
682
|
+
output_folder_base,
|
|
683
|
+
recursive=True,
|
|
684
|
+
overwrite=True,
|
|
685
|
+
n_threads=1,
|
|
686
|
+
every_n_frames=None,
|
|
687
|
+
verbose=False,
|
|
688
|
+
parallelization_uses_threads=True,
|
|
689
|
+
quality=None,
|
|
690
|
+
max_width=None,
|
|
691
|
+
frames_to_extract=None,
|
|
692
|
+
allow_empty_videos=False):
|
|
687
693
|
"""
|
|
688
694
|
For every video file in input_folder, creates a folder within output_folder_base, and
|
|
689
695
|
renders frame of that video to images in that folder.
|
|
@@ -709,6 +715,8 @@ def video_folder_to_frames(input_folder, output_folder_base,
|
|
|
709
715
|
each video; mutually exclusive with every_n_frames. If all values are beyond
|
|
710
716
|
the length of a video, no frames are extracted. Can also be a single int,
|
|
711
717
|
specifying a single frame number.
|
|
718
|
+
allow_empty_videos (bool, optional): Just print a warning if a video appears to have no
|
|
719
|
+
frames (by default, this is an error).
|
|
712
720
|
|
|
713
721
|
Returns:
|
|
714
722
|
tuple: a length-3 tuple containing:
|
|
@@ -719,8 +727,11 @@ def video_folder_to_frames(input_folder, output_folder_base,
|
|
|
719
727
|
"""
|
|
720
728
|
|
|
721
729
|
# Recursively enumerate video files
|
|
730
|
+
if verbose:
|
|
731
|
+
print('Enumerating videos in {}'.format(input_folder))
|
|
722
732
|
input_files_full_paths = find_videos(input_folder,recursive=recursive)
|
|
723
|
-
|
|
733
|
+
if verbose:
|
|
734
|
+
print('Found {} videos in folder {}'.format(len(input_files_full_paths),input_folder))
|
|
724
735
|
if len(input_files_full_paths) == 0:
|
|
725
736
|
return [],[],[]
|
|
726
737
|
|
|
@@ -974,6 +985,7 @@ if False:
|
|
|
974
985
|
results_file = r'results.json'
|
|
975
986
|
confidence_threshold = 0.75
|
|
976
987
|
|
|
988
|
+
|
|
977
989
|
#%% Load detector output
|
|
978
990
|
|
|
979
991
|
with open(results_file,'r') as f:
|
|
@@ -192,7 +192,7 @@ def combine_api_shard_files(input_files, output_file=None):
|
|
|
192
192
|
|
|
193
193
|
Args:
|
|
194
194
|
input_files (list of str): files to merge
|
|
195
|
-
output_file (str,
|
|
195
|
+
output_file (str, optional): file to which we should write merged results
|
|
196
196
|
|
|
197
197
|
Returns:
|
|
198
198
|
dict: merged results
|
|
@@ -0,0 +1,367 @@
|
|
|
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
|
+
|
|
14
|
+
from tqdm import tqdm
|
|
15
|
+
from enum import IntEnum
|
|
16
|
+
from collections import defaultdict
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import matplotlib
|
|
20
|
+
import matplotlib.pyplot as plt
|
|
21
|
+
|
|
22
|
+
from megadetector.postprocessing.validate_batch_results import \
|
|
23
|
+
validate_batch_results, ValidateBatchResultsOptions
|
|
24
|
+
from megadetector.utils.ct_utils import get_iou
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
#%% Classes
|
|
28
|
+
|
|
29
|
+
class CalibrationOptions:
|
|
30
|
+
"""
|
|
31
|
+
Options controlling comparison/calibration behavior.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
|
|
36
|
+
#: IoU threshold used for determining whether two detections are the same
|
|
37
|
+
#:
|
|
38
|
+
#: When multiple detections match, we will only use the highest-matching IoU.
|
|
39
|
+
self.iou_threshold = 0.75
|
|
40
|
+
|
|
41
|
+
#: Minimum confidence threshold to consider for calibration (should be lower than
|
|
42
|
+
#: the lowest value you would use in realistic situations)
|
|
43
|
+
self.confidence_threshold = 0.025
|
|
44
|
+
|
|
45
|
+
#: Should we populate the data_a and data_b fields in the return value?
|
|
46
|
+
self.return_data = False
|
|
47
|
+
|
|
48
|
+
#: Model name to use in printouts and plots for result set A
|
|
49
|
+
self.model_name_a = 'model_a'
|
|
50
|
+
|
|
51
|
+
#: Model name to use in printouts and plots for result set B
|
|
52
|
+
self.model_name_b = 'model_b'
|
|
53
|
+
|
|
54
|
+
#: Maximum number of samples to use for plotting or calibration per category,
|
|
55
|
+
#: or None to use all paired values.
|
|
56
|
+
self.max_samples_per_category = None
|
|
57
|
+
|
|
58
|
+
#: List of category IDs to use for plotting comparisons, or None to plot
|
|
59
|
+
#: all categories.
|
|
60
|
+
self.categories_to_plot = None
|
|
61
|
+
|
|
62
|
+
#: Optionally map category ID to name in plot labels
|
|
63
|
+
self.category_id_to_name = None
|
|
64
|
+
|
|
65
|
+
# ...class CalibrationOptions
|
|
66
|
+
|
|
67
|
+
class ConfidenceMatchColumns(IntEnum):
|
|
68
|
+
|
|
69
|
+
COLUMN_CONF_A = 0
|
|
70
|
+
COLUMN_CONF_B = 1
|
|
71
|
+
COLUMN_CONF_IOU = 2
|
|
72
|
+
COLUMN_CONF_I_IMAGE = 3
|
|
73
|
+
COLUMN_CONF_CATEGORY_ID = 4
|
|
74
|
+
|
|
75
|
+
class CalibrationResults:
|
|
76
|
+
"""
|
|
77
|
+
Results of a model-to-model comparison.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self):
|
|
81
|
+
|
|
82
|
+
#: List of tuples: [conf_a, conf_b, iou, i_image, category_id]
|
|
83
|
+
self.confidence_matches = []
|
|
84
|
+
|
|
85
|
+
#: Populated with the data loaded from json_filename_a if options.return_data is True
|
|
86
|
+
self.data_a = None
|
|
87
|
+
|
|
88
|
+
#: Populated with the data loaded from json_filename_b if options.return_data is True
|
|
89
|
+
self.data_b = None
|
|
90
|
+
|
|
91
|
+
# ...class CalibrationResults
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
#%% Calibration functions
|
|
95
|
+
|
|
96
|
+
def compare_model_confidence_values(json_filename_a,json_filename_b,options=None):
|
|
97
|
+
"""
|
|
98
|
+
Compare confidence values across two .json results files. Compares only detections that
|
|
99
|
+
can be matched by IoU, i.e., does not do anything with detections that only appear in one file.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
json_filename_a (str or dict): filename containing results from the first model to be compared;
|
|
103
|
+
should refer to the same images as [json_filename_b]. Can also be a loaded results dict.
|
|
104
|
+
json_filename_b (str or dict): filename containing results from the second model to be compared;
|
|
105
|
+
should refer to the same images as [json_filename_a]. Can also be a loaded results dict.
|
|
106
|
+
options (CalibrationOptions, optional): all the parameters used to control this process, see
|
|
107
|
+
CalibrationOptions for details
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
CalibrationResults: description of the comparison results
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
## Option handling
|
|
114
|
+
|
|
115
|
+
if options is None:
|
|
116
|
+
options = CalibrationOptions()
|
|
117
|
+
|
|
118
|
+
validation_options = ValidateBatchResultsOptions()
|
|
119
|
+
validation_options.return_data = True
|
|
120
|
+
|
|
121
|
+
if isinstance(json_filename_a,str):
|
|
122
|
+
results_a = validate_batch_results(json_filename_a,options=validation_options)
|
|
123
|
+
assert len(results_a['validation_results']['errors']) == 0
|
|
124
|
+
else:
|
|
125
|
+
assert isinstance(json_filename_a,dict)
|
|
126
|
+
results_a = json_filename_a
|
|
127
|
+
|
|
128
|
+
if isinstance(json_filename_b,str):
|
|
129
|
+
results_b = validate_batch_results(json_filename_b,options=validation_options)
|
|
130
|
+
assert len(results_b['validation_results']['errors']) == 0
|
|
131
|
+
else:
|
|
132
|
+
assert isinstance(json_filename_b,dict)
|
|
133
|
+
results_b = json_filename_b
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
## Make sure these results sets are comparable
|
|
137
|
+
|
|
138
|
+
image_filenames_a = [im['file'] for im in results_a['images']]
|
|
139
|
+
image_filenames_b = [im['file'] for im in results_b['images']]
|
|
140
|
+
|
|
141
|
+
assert set(image_filenames_a) == set(image_filenames_b), \
|
|
142
|
+
'Cannot calibrate non-matching image sets'
|
|
143
|
+
|
|
144
|
+
categories_a = results_a['detection_categories']
|
|
145
|
+
categories_b = results_b['detection_categories']
|
|
146
|
+
assert set(categories_a.keys()) == set(categories_b.keys())
|
|
147
|
+
for k in categories_a.keys():
|
|
148
|
+
assert categories_a[k] == categories_b[k], 'Category mismatch'
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
## Compare detections
|
|
152
|
+
|
|
153
|
+
image_filename_b_to_im = {}
|
|
154
|
+
for im in results_b['images']:
|
|
155
|
+
image_filename_b_to_im[im['file']] = im
|
|
156
|
+
|
|
157
|
+
n_detections_a = 0
|
|
158
|
+
n_detections_a_queried = 0
|
|
159
|
+
n_detections_a_matched = 0
|
|
160
|
+
|
|
161
|
+
confidence_matches = []
|
|
162
|
+
|
|
163
|
+
# For each image
|
|
164
|
+
# im_a = results_a['images'][0]
|
|
165
|
+
for i_image,im_a in tqdm(enumerate(results_a['images']),total=len(results_a['images'])):
|
|
166
|
+
|
|
167
|
+
fn = im_a['file']
|
|
168
|
+
im_b = image_filename_b_to_im[fn]
|
|
169
|
+
|
|
170
|
+
if 'detections' not in im_a or im_a['detections'] is None:
|
|
171
|
+
continue
|
|
172
|
+
if 'detections' not in im_b or im_b['detections'] is None:
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
# For each detection in result set A...
|
|
176
|
+
#
|
|
177
|
+
# det_a = im_a['detections'][0]
|
|
178
|
+
for det_a in im_a['detections']:
|
|
179
|
+
|
|
180
|
+
n_detections_a += 1
|
|
181
|
+
|
|
182
|
+
conf_a = det_a['conf']
|
|
183
|
+
category_id = det_a['category']
|
|
184
|
+
|
|
185
|
+
# Is this above threshold?
|
|
186
|
+
if conf_a < options.confidence_threshold:
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
n_detections_a_queried += 1
|
|
190
|
+
|
|
191
|
+
bbox_a = det_a['bbox']
|
|
192
|
+
|
|
193
|
+
best_iou = None
|
|
194
|
+
best_iou_conf = None
|
|
195
|
+
|
|
196
|
+
# For each detection in result set B...
|
|
197
|
+
#
|
|
198
|
+
# det_b = im_b['detections'][0]
|
|
199
|
+
for det_b in im_b['detections']:
|
|
200
|
+
|
|
201
|
+
# Is this the same category?
|
|
202
|
+
if det_b['category'] != category_id:
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
conf_b = det_b['conf']
|
|
206
|
+
|
|
207
|
+
# Is this above threshold?
|
|
208
|
+
if conf_b < options.confidence_threshold:
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
bbox_b = det_b['bbox']
|
|
212
|
+
|
|
213
|
+
iou = get_iou(bbox_a,bbox_b)
|
|
214
|
+
|
|
215
|
+
# Is this an adequate IoU to consider?
|
|
216
|
+
if iou < options.iou_threshold:
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
# Is this the best match so far?
|
|
220
|
+
if best_iou is None or iou > best_iou:
|
|
221
|
+
best_iou = iou
|
|
222
|
+
best_iou_conf = conf_b
|
|
223
|
+
|
|
224
|
+
# ...for each detection in im_b
|
|
225
|
+
|
|
226
|
+
if best_iou is not None:
|
|
227
|
+
n_detections_a_matched += 1
|
|
228
|
+
conf_result = [conf_a,best_iou_conf,best_iou,i_image,category_id]
|
|
229
|
+
confidence_matches.append(conf_result)
|
|
230
|
+
|
|
231
|
+
# ...for each detection in im_a
|
|
232
|
+
|
|
233
|
+
# ...for each image in result set A
|
|
234
|
+
|
|
235
|
+
print('\nOf {} detections in result set A, queried {}, matched {}'.format(
|
|
236
|
+
n_detections_a,n_detections_a_queried,n_detections_a_matched))
|
|
237
|
+
assert len(confidence_matches) == n_detections_a_matched
|
|
238
|
+
|
|
239
|
+
calibration_results = CalibrationResults()
|
|
240
|
+
calibration_results.confidence_matches = confidence_matches
|
|
241
|
+
|
|
242
|
+
if options.return_data:
|
|
243
|
+
calibration_results.data_a = results_a
|
|
244
|
+
calibration_results.data_b = results_b
|
|
245
|
+
|
|
246
|
+
return calibration_results
|
|
247
|
+
|
|
248
|
+
# ...def compare_model_confidence_values(...)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
#%% Plotting functions
|
|
252
|
+
|
|
253
|
+
def plot_matched_confidence_values(calibration_results,output_filename,options=None):
|
|
254
|
+
"""
|
|
255
|
+
Given a set of paired confidence values for matching detections (from
|
|
256
|
+
compare_model_confidence_values), plot histograms of those pairs for each
|
|
257
|
+
detection category.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
calibration_results (CalibrationResults): output from a call to
|
|
261
|
+
compare_model_confidence_values, containing paired confidence
|
|
262
|
+
values for two sets of detection results.
|
|
263
|
+
output_filename (str): filename to write the plot (.png or .jpg)
|
|
264
|
+
options (CalibrationOptions, optional): plotting options, see
|
|
265
|
+
CalibrationOptions for details.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
fig_w = 12
|
|
269
|
+
fig_h = 8
|
|
270
|
+
n_hist_bins = 80
|
|
271
|
+
|
|
272
|
+
if options is None:
|
|
273
|
+
options = CalibrationOptions()
|
|
274
|
+
|
|
275
|
+
# Find matched confidence pairs for each category ID
|
|
276
|
+
category_to_matches = defaultdict(list)
|
|
277
|
+
|
|
278
|
+
confidence_matches = calibration_results.confidence_matches
|
|
279
|
+
for m in confidence_matches:
|
|
280
|
+
category_id = m[ConfidenceMatchColumns.COLUMN_CONF_CATEGORY_ID]
|
|
281
|
+
category_to_matches[category_id].append(m)
|
|
282
|
+
|
|
283
|
+
# Optionally sample matches
|
|
284
|
+
category_to_samples = defaultdict(list)
|
|
285
|
+
|
|
286
|
+
for i_category,category_id in enumerate(category_to_matches.keys()):
|
|
287
|
+
|
|
288
|
+
matches_this_category = category_to_matches[category_id]
|
|
289
|
+
|
|
290
|
+
if (options.max_samples_per_category is None) or \
|
|
291
|
+
(len(matches_this_category) <= options.max_samples_per_category):
|
|
292
|
+
category_to_samples[category_id] = matches_this_category
|
|
293
|
+
else:
|
|
294
|
+
assert len(matches_this_category) > options.max_samples_per_category
|
|
295
|
+
category_to_samples[category_id] = random.sample(matches_this_category,options.max_samples_per_category)
|
|
296
|
+
|
|
297
|
+
del category_to_matches
|
|
298
|
+
del confidence_matches
|
|
299
|
+
|
|
300
|
+
categories_to_plot = list(category_to_samples.keys())
|
|
301
|
+
|
|
302
|
+
if options.categories_to_plot is not None:
|
|
303
|
+
categories_to_plot = [category_id for category_id in categories_to_plot if\
|
|
304
|
+
category_id in options.categories_to_plot]
|
|
305
|
+
|
|
306
|
+
n_subplots = len(categories_to_plot)
|
|
307
|
+
|
|
308
|
+
plt.ioff()
|
|
309
|
+
|
|
310
|
+
fig = matplotlib.figure.Figure(figsize=(fig_w, fig_h), tight_layout=True)
|
|
311
|
+
# fig,axes = plt.subplots(nrows=n_subplots,ncols=1)
|
|
312
|
+
|
|
313
|
+
axes = fig.subplots(n_subplots, 1)
|
|
314
|
+
|
|
315
|
+
# i_category = 0; category_id = categories_to_plot[i_category]
|
|
316
|
+
for i_category,category_id in enumerate(categories_to_plot):
|
|
317
|
+
|
|
318
|
+
ax = axes[i_category]
|
|
319
|
+
|
|
320
|
+
category_string = category_id
|
|
321
|
+
if options.category_id_to_name is not None and \
|
|
322
|
+
category_id in options.category_id_to_name:
|
|
323
|
+
category_string = options.category_id_to_name[category_id]
|
|
324
|
+
|
|
325
|
+
samples_this_category = category_to_samples[category_id]
|
|
326
|
+
x = [m[0] for m in samples_this_category]
|
|
327
|
+
y = [m[1] for m in samples_this_category]
|
|
328
|
+
|
|
329
|
+
weights_a = np.ones_like(x)/float(len(x))
|
|
330
|
+
weights_b = np.ones_like(y)/float(len(y))
|
|
331
|
+
ax.hist(x,histtype='step',bins=n_hist_bins,density=False,color='red',weights=weights_a)
|
|
332
|
+
ax.hist(y,histtype='step',bins=n_hist_bins,density=False,color='blue',weights=weights_b)
|
|
333
|
+
ax.legend([options.model_name_a,options.model_name_b])
|
|
334
|
+
ax.set_ylabel(category_string)
|
|
335
|
+
# plt.tight_layout()
|
|
336
|
+
|
|
337
|
+
# I experimented with heat maps, but they weren't very informative.
|
|
338
|
+
# Leaving this code here in case I revisit. Note to self: scatter plots
|
|
339
|
+
# were a disaster.
|
|
340
|
+
if False:
|
|
341
|
+
heatmap, xedges, yedges = np.histogram2d(x, y, bins=30)
|
|
342
|
+
extent = [xedges[0], xedges[-1], yedges[0], yedges[-1]]
|
|
343
|
+
plt.imshow(heatmap.T, extent=extent, origin='lower', norm='log')
|
|
344
|
+
|
|
345
|
+
# ...for each category for which we need to generate a histogram
|
|
346
|
+
|
|
347
|
+
plt.close(fig)
|
|
348
|
+
fig.savefig(output_filename,dpi=100)
|
|
349
|
+
|
|
350
|
+
# ...def plot_matched_confidence_values(...)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
#%% Interactive driver(s)
|
|
354
|
+
|
|
355
|
+
if False:
|
|
356
|
+
|
|
357
|
+
#%%
|
|
358
|
+
|
|
359
|
+
options = ValidateBatchResultsOptions()
|
|
360
|
+
# json_filename = r'g:\temp\format.json'
|
|
361
|
+
# json_filename = r'g:\temp\test-videos\video_results.json'
|
|
362
|
+
json_filename = r'g:\temp\test-videos\image_results.json'
|
|
363
|
+
options.check_image_existence = True
|
|
364
|
+
options.relative_path_base = r'g:\temp\test-videos'
|
|
365
|
+
validate_batch_results(json_filename,options)
|
|
366
|
+
|
|
367
|
+
|
|
@@ -41,7 +41,8 @@ def md_to_coco(md_results_file,
|
|
|
41
41
|
The default confidence threshold is not 0; the assumption is that by default, you are
|
|
42
42
|
going to treat the resulting COCO file as a set of labels. If you are using the resulting COCO
|
|
43
43
|
file to evaluate a detector, you likely want a default confidence threshold of 0. Confidence
|
|
44
|
-
values will be written to the semi-standard "score" field for each image
|
|
44
|
+
values will be written to the semi-standard "score" field for each image if
|
|
45
|
+
preserve_nonstandard_metadata is True.
|
|
45
46
|
|
|
46
47
|
A folder of images is required if width and height information are not available
|
|
47
48
|
in the MD results file.
|
|
@@ -92,16 +92,18 @@ class PostProcessingOptions:
|
|
|
92
92
|
#: Optional .json file containing ground truth information
|
|
93
93
|
self.ground_truth_json_file = ''
|
|
94
94
|
|
|
95
|
-
#:
|
|
95
|
+
#: List of classes we'll treat as negative (defaults to "empty", typically includes
|
|
96
|
+
#: classes like "blank", "misfire", etc.).
|
|
96
97
|
#:
|
|
97
98
|
#: Include the token "#NO_LABELS#" to indicate that an image with no annotations
|
|
98
99
|
#: should be considered empty.
|
|
99
100
|
self.negative_classes = DEFAULT_NEGATIVE_CLASSES
|
|
100
101
|
|
|
101
|
-
#:
|
|
102
|
+
#: List of classes we'll treat as neither positive nor negative (defaults to
|
|
103
|
+
#: "unknown", typically includes classes like "unidentifiable").
|
|
102
104
|
self.unlabeled_classes = DEFAULT_UNKNOWN_CLASSES
|
|
103
105
|
|
|
104
|
-
#:
|
|
106
|
+
#: List of output sets that we should count, but not render images for.
|
|
105
107
|
#:
|
|
106
108
|
#: Typically used to preview sets with lots of empties, where you don't want to
|
|
107
109
|
#: subset but also don't want to render 100,000 empty images.
|
|
@@ -198,11 +200,16 @@ class PostProcessingOptions:
|
|
|
198
200
|
|
|
199
201
|
#: When classification results are present, should be sort alphabetically by class name (False)
|
|
200
202
|
#: or in descending order by frequency (True)?
|
|
201
|
-
self.sort_classification_results_by_count = False
|
|
203
|
+
self.sort_classification_results_by_count = False
|
|
202
204
|
|
|
203
205
|
#: Should we split individual pages up into smaller pages if there are more than
|
|
204
206
|
#: N images?
|
|
205
207
|
self.max_figures_per_html_file = None
|
|
208
|
+
|
|
209
|
+
#: Footer text for the index page
|
|
210
|
+
# self.footer_text = '<br/><p style="font-size:80%;">Preview page created with the <a href="{}">MegaDetector Python package</a>.</p>'.\
|
|
211
|
+
# format('https://megadetector.readthedocs.io')
|
|
212
|
+
self.footer_text = ''
|
|
206
213
|
|
|
207
214
|
# ...__init__()
|
|
208
215
|
|
|
@@ -590,6 +597,7 @@ def _prepare_html_subpages(images_html, output_dir, options=None):
|
|
|
590
597
|
html_image_list_options = {}
|
|
591
598
|
html_image_list_options['maxFiguresPerHtmlFile'] = options.max_figures_per_html_file
|
|
592
599
|
html_image_list_options['headerHtml'] = '<h1>{}</h1>'.format(res.upper())
|
|
600
|
+
html_image_list_options['pageTitle'] = '{}'.format(res.lower())
|
|
593
601
|
|
|
594
602
|
# Don't write empty pages
|
|
595
603
|
if len(array) == 0:
|
|
@@ -762,7 +770,7 @@ def _render_image_no_gt(file_info,detection_categories_to_results_name,
|
|
|
762
770
|
if len(rendered_image_html_info) > 0:
|
|
763
771
|
|
|
764
772
|
image_result = [[res, rendered_image_html_info]]
|
|
765
|
-
|
|
773
|
+
classes_rendered_this_image = set()
|
|
766
774
|
max_conf = 0
|
|
767
775
|
|
|
768
776
|
for det in detections:
|
|
@@ -782,11 +790,14 @@ def _render_image_no_gt(file_info,detection_categories_to_results_name,
|
|
|
782
790
|
# confidence threshold
|
|
783
791
|
if (options.classification_confidence_threshold < 0) or \
|
|
784
792
|
(top1_class_score >= options.classification_confidence_threshold):
|
|
785
|
-
|
|
786
|
-
rendered_image_html_info])
|
|
793
|
+
class_string = 'class_{}'.format(top1_class_name)
|
|
787
794
|
else:
|
|
788
|
-
|
|
795
|
+
class_string = 'class_unreliable'
|
|
796
|
+
|
|
797
|
+
if class_string not in classes_rendered_this_image:
|
|
798
|
+
image_result.append([class_string,
|
|
789
799
|
rendered_image_html_info])
|
|
800
|
+
classes_rendered_this_image.add(class_string)
|
|
790
801
|
|
|
791
802
|
# ...if this detection has classification info
|
|
792
803
|
|
|
@@ -1083,7 +1094,8 @@ def process_batch_results(options):
|
|
|
1083
1094
|
|
|
1084
1095
|
output_html_file = ''
|
|
1085
1096
|
|
|
1086
|
-
style_header = """<head>
|
|
1097
|
+
style_header = """<head>
|
|
1098
|
+
<title>Detection results preview</title>
|
|
1087
1099
|
<style type="text/css">
|
|
1088
1100
|
a { text-decoration: none; }
|
|
1089
1101
|
body { font-family: segoe ui, calibri, "trebuchet ms", verdana, arial, sans-serif; }
|
|
@@ -1424,7 +1436,7 @@ def process_batch_results(options):
|
|
|
1424
1436
|
else:
|
|
1425
1437
|
confidence_threshold_string = str(options.confidence_threshold)
|
|
1426
1438
|
|
|
1427
|
-
index_page = """<html>
|
|
1439
|
+
index_page = """<html>
|
|
1428
1440
|
{}
|
|
1429
1441
|
<body>
|
|
1430
1442
|
<h2>Evaluation</h2>
|
|
@@ -1509,7 +1521,7 @@ def process_batch_results(options):
|
|
|
1509
1521
|
index_page += '</div>'
|
|
1510
1522
|
|
|
1511
1523
|
# Close body and html tags
|
|
1512
|
-
index_page += '</body></html>'
|
|
1524
|
+
index_page += '{}</body></html>'.format(options.footer_text)
|
|
1513
1525
|
output_html_file = os.path.join(output_dir, 'index.html')
|
|
1514
1526
|
with open(output_html_file, 'w') as f:
|
|
1515
1527
|
f.write(index_page)
|
|
@@ -1529,7 +1541,6 @@ def process_batch_results(options):
|
|
|
1529
1541
|
# for each category
|
|
1530
1542
|
images_html = collections.defaultdict(list)
|
|
1531
1543
|
|
|
1532
|
-
|
|
1533
1544
|
# Add default entries by accessing them for the first time
|
|
1534
1545
|
|
|
1535
1546
|
# Maps sorted tuples of detection category IDs (string ints) - e.g. ("1"), ("1", "4", "7") - to
|
|
@@ -1637,14 +1648,15 @@ def process_batch_results(options):
|
|
|
1637
1648
|
files_to_render), total=len(files_to_render)))
|
|
1638
1649
|
else:
|
|
1639
1650
|
for file_info in tqdm(files_to_render):
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1651
|
+
rendering_result = _render_image_no_gt(file_info,
|
|
1652
|
+
detection_categories_to_results_name,
|
|
1653
|
+
detection_categories,
|
|
1654
|
+
classification_categories,
|
|
1655
|
+
options=options)
|
|
1656
|
+
rendering_results.append(rendering_result)
|
|
1645
1657
|
|
|
1646
|
-
elapsed = time.time() - start_time
|
|
1647
|
-
|
|
1658
|
+
elapsed = time.time() - start_time
|
|
1659
|
+
|
|
1648
1660
|
# Do we have classification results in addition to detection results?
|
|
1649
1661
|
has_classification_info = False
|
|
1650
1662
|
|
|
@@ -1793,7 +1805,7 @@ def process_batch_results(options):
|
|
|
1793
1805
|
cname, cname.lower(), ccount)
|
|
1794
1806
|
index_page += '</div>\n'
|
|
1795
1807
|
|
|
1796
|
-
index_page += '</body></html>'
|
|
1808
|
+
index_page += '{}</body></html>'.format(options.footer_text)
|
|
1797
1809
|
output_html_file = os.path.join(output_dir, 'index.html')
|
|
1798
1810
|
with open(output_html_file, 'w') as f:
|
|
1799
1811
|
f.write(index_page)
|