megadetector 5.0.23__py3-none-any.whl → 5.0.24__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/api/synchronous/api_core/animal_detection_api/api_backend.py +2 -3
- megadetector/classification/merge_classification_detection_output.py +2 -2
- megadetector/data_management/coco_to_labelme.py +2 -1
- megadetector/data_management/databases/integrity_check_json_db.py +15 -14
- megadetector/data_management/databases/subset_json_db.py +49 -21
- megadetector/data_management/mewc_to_md.py +340 -0
- megadetector/data_management/wi_to_md.py +41 -0
- megadetector/data_management/yolo_output_to_md_output.py +15 -8
- megadetector/detection/process_video.py +24 -7
- megadetector/detection/pytorch_detector.py +841 -160
- megadetector/detection/run_detector.py +340 -146
- megadetector/detection/run_detector_batch.py +304 -68
- megadetector/detection/run_inference_with_yolov5_val.py +61 -4
- megadetector/detection/tf_detector.py +6 -1
- megadetector/postprocessing/{combine_api_outputs.py → combine_batch_outputs.py} +10 -13
- megadetector/postprocessing/compare_batch_results.py +68 -6
- megadetector/postprocessing/md_to_labelme.py +7 -7
- megadetector/postprocessing/md_to_wi.py +40 -0
- megadetector/postprocessing/merge_detections.py +1 -1
- megadetector/postprocessing/postprocess_batch_results.py +10 -3
- megadetector/postprocessing/separate_detections_into_folders.py +32 -4
- megadetector/postprocessing/validate_batch_results.py +9 -4
- megadetector/utils/ct_utils.py +165 -45
- megadetector/utils/gpu_test.py +107 -0
- megadetector/utils/md_tests.py +355 -108
- megadetector/utils/path_utils.py +9 -2
- megadetector/utils/wi_utils.py +1794 -0
- megadetector/visualization/visualization_utils.py +82 -16
- megadetector/visualization/visualize_db.py +25 -7
- megadetector/visualization/visualize_detector_output.py +60 -13
- {megadetector-5.0.23.dist-info → megadetector-5.0.24.dist-info}/METADATA +10 -24
- {megadetector-5.0.23.dist-info → megadetector-5.0.24.dist-info}/RECORD +35 -33
- megadetector/detection/detector_training/__init__.py +0 -0
- megadetector/detection/detector_training/model_main_tf2.py +0 -114
- megadetector/utils/torch_test.py +0 -32
- {megadetector-5.0.23.dist-info → megadetector-5.0.24.dist-info}/LICENSE +0 -0
- {megadetector-5.0.23.dist-info → megadetector-5.0.24.dist-info}/WHEEL +0 -0
- {megadetector-5.0.23.dist-info → megadetector-5.0.24.dist-info}/top_level.txt +0 -0
|
@@ -57,7 +57,7 @@ from megadetector.utils.ct_utils import is_iterable, split_list_into_fixed_size_
|
|
|
57
57
|
from megadetector.utils.path_utils import path_is_abs
|
|
58
58
|
from megadetector.data_management import yolo_output_to_md_output
|
|
59
59
|
from megadetector.detection.run_detector import try_download_known_detector
|
|
60
|
-
from megadetector.postprocessing.
|
|
60
|
+
from megadetector.postprocessing.combine_batch_outputs import combine_batch_output_files
|
|
61
61
|
|
|
62
62
|
default_image_size_with_augmentation = int(1280 * 1.3)
|
|
63
63
|
default_image_size_with_no_augmentation = 1280
|
|
@@ -214,6 +214,64 @@ def _clean_up_temporary_folders(options,
|
|
|
214
214
|
print('Warning: using temporary YOLO results folder {}, but not removing it'.format(
|
|
215
215
|
yolo_results_folder))
|
|
216
216
|
|
|
217
|
+
|
|
218
|
+
def get_stats_for_category(filename,category='all'):
|
|
219
|
+
"""
|
|
220
|
+
Retrieve statistics for a category from the YOLO console output
|
|
221
|
+
stored in [filenam].
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
filename (str): a text file containing console output from a YOLO val run
|
|
225
|
+
category (optional, str): a category name
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
dict: a dict with fields n_images, n_labels, P, R, mAP50, and mAP50-95
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
with open(filename,'r',encoding='utf-8') as f:
|
|
232
|
+
lines = f.readlines()
|
|
233
|
+
|
|
234
|
+
# This is just a hedge to make sure there isn't some YOLO version floating
|
|
235
|
+
# around that used different IoU thresholds in the console output.
|
|
236
|
+
found_map50 = False
|
|
237
|
+
found_map5095 = False
|
|
238
|
+
|
|
239
|
+
for line in lines:
|
|
240
|
+
|
|
241
|
+
s = line.strip()
|
|
242
|
+
|
|
243
|
+
if ' map50 ' in s.lower() or ' map@.5 ' in s.lower():
|
|
244
|
+
found_map50 = True
|
|
245
|
+
if 'map50-95' in s.lower() or 'map@.5:.95' in s.lower():
|
|
246
|
+
found_map5095 = True
|
|
247
|
+
|
|
248
|
+
if not s.startswith(category):
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
tokens = s.split(' ')
|
|
252
|
+
tokens_filtered = list(filter(None,tokens))
|
|
253
|
+
|
|
254
|
+
if len(tokens_filtered) != 7:
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
assert found_map50 and found_map5095, \
|
|
258
|
+
'Parsing error in YOLO console output file {}'.format(filename)
|
|
259
|
+
|
|
260
|
+
to_return = {}
|
|
261
|
+
to_return['category'] = category
|
|
262
|
+
assert category == tokens_filtered[0]
|
|
263
|
+
to_return['n_images'] = int(tokens_filtered[1])
|
|
264
|
+
to_return['n_labels'] = int(tokens_filtered[2])
|
|
265
|
+
to_return['P'] = float(tokens_filtered[3])
|
|
266
|
+
to_return['R'] = float(tokens_filtered[4])
|
|
267
|
+
to_return['mAP50'] = float(tokens_filtered[5])
|
|
268
|
+
to_return['mAP50-95'] = float(tokens_filtered[6])
|
|
269
|
+
return to_return
|
|
270
|
+
|
|
271
|
+
# ...for each line
|
|
272
|
+
|
|
273
|
+
return None
|
|
274
|
+
|
|
217
275
|
|
|
218
276
|
#%% Main function
|
|
219
277
|
|
|
@@ -478,7 +536,7 @@ def run_inference_with_yolo_val(options):
|
|
|
478
536
|
# ...for each chunk
|
|
479
537
|
|
|
480
538
|
# Merge
|
|
481
|
-
_ =
|
|
539
|
+
_ = combine_batch_output_files(input_files=chunk_output_files,
|
|
482
540
|
output_file=options.output_file,
|
|
483
541
|
require_uniqueness=True,
|
|
484
542
|
verbose=True)
|
|
@@ -644,8 +702,7 @@ def run_inference_with_yolo_val(options):
|
|
|
644
702
|
assert len(category_ids) == 1 + category_ids[-1]
|
|
645
703
|
|
|
646
704
|
yolo_dataset_file = os.path.join(yolo_results_folder,'dataset.yaml')
|
|
647
|
-
yolo_image_list_file = os.path.join(yolo_results_folder,'images.txt')
|
|
648
|
-
|
|
705
|
+
yolo_image_list_file = os.path.join(yolo_results_folder,'images.txt')
|
|
649
706
|
|
|
650
707
|
with open(yolo_image_list_file,'w') as f:
|
|
651
708
|
|
|
@@ -36,10 +36,15 @@ class TFDetector:
|
|
|
36
36
|
BATCH_SIZE = 1
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def __init__(self, model_path):
|
|
39
|
+
def __init__(self, model_path, detector_options=None):
|
|
40
40
|
"""
|
|
41
41
|
Loads a model from [model_path] and starts a tf.Session with this graph. Obtains
|
|
42
42
|
input and output tensor handles.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
model_path (str): path to .pdb file
|
|
46
|
+
detector_options (dict, optional): key-value pairs that control detector
|
|
47
|
+
options; currently not used by TFDetector
|
|
43
48
|
"""
|
|
44
49
|
|
|
45
50
|
detection_graph = TFDetector.__load_model(model_path)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
combine_batch_outputs.py
|
|
4
4
|
|
|
5
|
-
Merges two or more .json files in
|
|
5
|
+
Merges two or more .json files in MD output format, optionally
|
|
6
6
|
writing the results to another .json file.
|
|
7
7
|
|
|
8
8
|
* Concatenates image lists, erroring if images are not unique.
|
|
@@ -15,10 +15,7 @@ https://github.com/agentmorris/MegaDetector/tree/main/megadetector/api/batch_pro
|
|
|
15
15
|
|
|
16
16
|
Command-line use:
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Also see combine_api_shard_files() (not exposed via the command line yet) to
|
|
21
|
-
combine the intermediate files created by the API.
|
|
18
|
+
combine_batch_outputs input1.json input2.json ... inputN.json output.json
|
|
22
19
|
|
|
23
20
|
This does no checking for redundancy; if you are looking to ensemble
|
|
24
21
|
the results of multiple model versions, see merge_detections.py.
|
|
@@ -34,7 +31,7 @@ import json
|
|
|
34
31
|
|
|
35
32
|
#%% Merge functions
|
|
36
33
|
|
|
37
|
-
def
|
|
34
|
+
def combine_batch_output_files(input_files,
|
|
38
35
|
output_file=None,
|
|
39
36
|
require_uniqueness=True,
|
|
40
37
|
verbose=True):
|
|
@@ -64,7 +61,7 @@ def combine_api_output_files(input_files,
|
|
|
64
61
|
input_dicts.append(json.load(f))
|
|
65
62
|
|
|
66
63
|
print_if_verbose('Merging results')
|
|
67
|
-
merged_dict =
|
|
64
|
+
merged_dict = combine_batch_output_dictionaries(
|
|
68
65
|
input_dicts, require_uniqueness=require_uniqueness)
|
|
69
66
|
|
|
70
67
|
print_if_verbose('Writing output to {}'.format(output_file))
|
|
@@ -75,7 +72,7 @@ def combine_api_output_files(input_files,
|
|
|
75
72
|
return merged_dict
|
|
76
73
|
|
|
77
74
|
|
|
78
|
-
def
|
|
75
|
+
def combine_batch_output_dictionaries(input_dicts, require_uniqueness=True):
|
|
79
76
|
"""
|
|
80
77
|
Merges the list of MD results dictionaries [input_dicts] into a single dict.
|
|
81
78
|
See module header comment for details on merge rules.
|
|
@@ -106,7 +103,7 @@ def combine_api_output_dictionaries(input_dicts, require_uniqueness=True):
|
|
|
106
103
|
|
|
107
104
|
for k in input_dict:
|
|
108
105
|
if k not in known_fields:
|
|
109
|
-
|
|
106
|
+
print(f'Warning: unrecognized batch output field: {k}')
|
|
110
107
|
|
|
111
108
|
# Check compatibility of detection categories
|
|
112
109
|
for cat_id in input_dict['detection_categories']:
|
|
@@ -157,7 +154,7 @@ def combine_api_output_dictionaries(input_dicts, require_uniqueness=True):
|
|
|
157
154
|
assert info_compare['detector'] == info['detector'], (
|
|
158
155
|
'Incompatible detection versions in merging')
|
|
159
156
|
assert info_compare['format_version'] == info['format_version'], (
|
|
160
|
-
'Incompatible
|
|
157
|
+
'Incompatible batch output versions in merging')
|
|
161
158
|
if 'classifier' in info_compare:
|
|
162
159
|
if 'classifier' in info:
|
|
163
160
|
assert info['classifier'] == info_compare['classifier']
|
|
@@ -179,7 +176,7 @@ def combine_api_output_dictionaries(input_dicts, require_uniqueness=True):
|
|
|
179
176
|
'images': sorted_images}
|
|
180
177
|
return merged_dict
|
|
181
178
|
|
|
182
|
-
# ...
|
|
179
|
+
# ...combine_batch_output_files()
|
|
183
180
|
|
|
184
181
|
|
|
185
182
|
def combine_api_shard_files(input_files, output_file=None):
|
|
@@ -243,7 +240,7 @@ def main():
|
|
|
243
240
|
parser.exit()
|
|
244
241
|
|
|
245
242
|
args = parser.parse_args()
|
|
246
|
-
|
|
243
|
+
combine_batch_output_files(args.input_paths, args.output_path)
|
|
247
244
|
|
|
248
245
|
if __name__ == '__main__':
|
|
249
246
|
main()
|
|
@@ -197,6 +197,10 @@ class BatchComparisonOptions:
|
|
|
197
197
|
#: to describe images
|
|
198
198
|
self.fn_to_display_fn = None
|
|
199
199
|
|
|
200
|
+
#: Should we run urllib.parse.quote() on paths before using them as links in the
|
|
201
|
+
#: output page?
|
|
202
|
+
self.parse_link_paths = True
|
|
203
|
+
|
|
200
204
|
# ...class BatchComparisonOptions
|
|
201
205
|
|
|
202
206
|
|
|
@@ -1213,9 +1217,6 @@ def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
|
1213
1217
|
|
|
1214
1218
|
# ...def _categorize_image_with_image_level_gt(...)
|
|
1215
1219
|
|
|
1216
|
-
# if 'val#human#human#HoSa#2021.006_na#2021#2021.006 (2021)#20210713' in im_a['file']:
|
|
1217
|
-
# import pdb; pdb.set_trace()
|
|
1218
|
-
|
|
1219
1220
|
# im_detection = im_a; category_id_to_threshold = category_id_to_threshold_a
|
|
1220
1221
|
result_types_present_a = \
|
|
1221
1222
|
_categorize_image_with_image_level_gt(im_a,im_gt,annotations_gt,category_id_to_threshold_a)
|
|
@@ -1360,12 +1361,17 @@ def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
|
1360
1361
|
|
|
1361
1362
|
title = display_path + ' (max conf {:.2f},{:.2f})'.format(max_conf_a,max_conf_b)
|
|
1362
1363
|
|
|
1364
|
+
if options.parse_link_paths:
|
|
1365
|
+
link_target_string = urllib.parse.quote(input_image_absolute_paths[i_fn])
|
|
1366
|
+
else:
|
|
1367
|
+
link_target_string = input_image_absolute_paths[i_fn]
|
|
1368
|
+
|
|
1363
1369
|
info = {
|
|
1364
1370
|
'filename': fn,
|
|
1365
1371
|
'title': title,
|
|
1366
1372
|
'textStyle': 'font-family:verdana,arial,calibri;font-size:' + \
|
|
1367
1373
|
'80%;text-align:left;margin-top:20;margin-bottom:5',
|
|
1368
|
-
'linkTarget':
|
|
1374
|
+
'linkTarget': link_target_string,
|
|
1369
1375
|
'sort_conf':sort_conf
|
|
1370
1376
|
}
|
|
1371
1377
|
|
|
@@ -1575,7 +1581,9 @@ def n_way_comparison(filenames,
|
|
|
1575
1581
|
if model_names is not None:
|
|
1576
1582
|
assert len(model_names) == len(filenames), \
|
|
1577
1583
|
'[model_names] should be the same length as [filenames]'
|
|
1578
|
-
|
|
1584
|
+
|
|
1585
|
+
options.pairwise_options = []
|
|
1586
|
+
|
|
1579
1587
|
# Choose all pairwise combinations of the files in [filenames]
|
|
1580
1588
|
for i, j in itertools.combinations(list(range(0,len(filenames))),2):
|
|
1581
1589
|
|
|
@@ -1598,7 +1606,61 @@ def n_way_comparison(filenames,
|
|
|
1598
1606
|
|
|
1599
1607
|
return compare_batch_results(options)
|
|
1600
1608
|
|
|
1601
|
-
# ...n_way_comparison()
|
|
1609
|
+
# ...def n_way_comparison(...)
|
|
1610
|
+
|
|
1611
|
+
|
|
1612
|
+
def find_equivalent_threshold(results_a,results_b,threshold_a=0.2):
|
|
1613
|
+
"""
|
|
1614
|
+
Given two sets of detector results, finds the confidence threshold for results_b
|
|
1615
|
+
that produces the same fraction of *images* with detections as threshold_a does for
|
|
1616
|
+
results_a. Uses all categories.
|
|
1617
|
+
|
|
1618
|
+
Args:
|
|
1619
|
+
results_a (str or dict): the first set of results, either a .json filename or a results
|
|
1620
|
+
dict
|
|
1621
|
+
results_b (str or dict): the second set of results, either a .json filename or a results
|
|
1622
|
+
dict
|
|
1623
|
+
threshold_a (float, optional): the threshold used to determine the target number of
|
|
1624
|
+
detections in results_a
|
|
1625
|
+
|
|
1626
|
+
Returns:
|
|
1627
|
+
float: the threshold that - when applied to results_b - produces the same number
|
|
1628
|
+
of image-level detections that results from applying threshold_a to results_a
|
|
1629
|
+
"""
|
|
1630
|
+
|
|
1631
|
+
if isinstance(results_a,str):
|
|
1632
|
+
with open(results_a,'r') as f:
|
|
1633
|
+
results_a = json.load(f)
|
|
1634
|
+
|
|
1635
|
+
if isinstance(results_b,str):
|
|
1636
|
+
with open(results_b,'r') as f:
|
|
1637
|
+
results_b = json.load(f)
|
|
1638
|
+
|
|
1639
|
+
def get_confidence_values_for_results(images):
|
|
1640
|
+
confidence_values = []
|
|
1641
|
+
for im in images:
|
|
1642
|
+
if 'detections' in im and im['detections'] is not None:
|
|
1643
|
+
if len(im['detections']) == 0:
|
|
1644
|
+
confidence_values.append(0)
|
|
1645
|
+
else:
|
|
1646
|
+
confidence_values_this_image = [det['conf'] for det in im['detections']]
|
|
1647
|
+
confidence_values.append(max(confidence_values_this_image))
|
|
1648
|
+
return confidence_values
|
|
1649
|
+
|
|
1650
|
+
confidence_values_a = get_confidence_values_for_results(results_a['images'])
|
|
1651
|
+
confidence_values_a_above_threshold = [c for c in confidence_values_a if c >= threshold_a]
|
|
1652
|
+
|
|
1653
|
+
confidence_values_b = get_confidence_values_for_results(results_b['images'])
|
|
1654
|
+
confidence_values_b = sorted(confidence_values_b)
|
|
1655
|
+
|
|
1656
|
+
target_detection_fraction = len(confidence_values_a_above_threshold) / len(confidence_values_a)
|
|
1657
|
+
|
|
1658
|
+
detection_cutoff_index = round((1.0-target_detection_fraction) * len(confidence_values_b))
|
|
1659
|
+
threshold_b = confidence_values_b[detection_cutoff_index]
|
|
1660
|
+
|
|
1661
|
+
return threshold_b
|
|
1662
|
+
|
|
1663
|
+
# ...def find_equivalent_threshold(...)
|
|
1602
1664
|
|
|
1603
1665
|
|
|
1604
1666
|
#%% Interactive driver
|
|
@@ -25,8 +25,8 @@ from multiprocessing.pool import ThreadPool
|
|
|
25
25
|
from functools import partial
|
|
26
26
|
|
|
27
27
|
from megadetector.visualization.visualization_utils import open_image
|
|
28
|
-
from megadetector.utils.ct_utils import
|
|
29
|
-
from megadetector.detection.run_detector import DEFAULT_DETECTOR_LABEL_MAP
|
|
28
|
+
from megadetector.utils.ct_utils import round_float
|
|
29
|
+
from megadetector.detection.run_detector import DEFAULT_DETECTOR_LABEL_MAP, FAILURE_IMAGE_OPEN
|
|
30
30
|
|
|
31
31
|
output_precision = 3
|
|
32
32
|
default_confidence_threshold = 0.15
|
|
@@ -92,10 +92,10 @@ def get_labelme_dict_for_image(im,image_base_name=None,category_id_to_name=None,
|
|
|
92
92
|
# MD boxes are [x_min, y_min, width_of_box, height_of_box] (relative)
|
|
93
93
|
#
|
|
94
94
|
# labelme boxes are [[x0,y0],[x1,y1]] (absolute)
|
|
95
|
-
x0 =
|
|
96
|
-
y0 =
|
|
97
|
-
x1 =
|
|
98
|
-
y1 =
|
|
95
|
+
x0 = round_float(det['bbox'][0] * im['width'],output_precision)
|
|
96
|
+
y0 = round_float(det['bbox'][1] * im['height'],output_precision)
|
|
97
|
+
x1 = round_float(x0 + det['bbox'][2] * im['width'],output_precision)
|
|
98
|
+
y1 = round_float(y0 + det['bbox'][3] * im['height'],output_precision)
|
|
99
99
|
shape['points'] = [[x0,y0],[x1,y1]]
|
|
100
100
|
output_dict['shapes'].append(shape)
|
|
101
101
|
|
|
@@ -210,7 +210,7 @@ def md_to_labelme(results_file,image_base,confidence_threshold=None,
|
|
|
210
210
|
print('Warning: cannot open image {}, treating as a failure during inference'.format(
|
|
211
211
|
im_full_path))
|
|
212
212
|
if 'failure' not in im:
|
|
213
|
-
im['failure'] =
|
|
213
|
+
im['failure'] = FAILURE_IMAGE_OPEN
|
|
214
214
|
|
|
215
215
|
# ...if we need to read w/h information
|
|
216
216
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
md_to_wi.py
|
|
4
|
+
|
|
5
|
+
Converts the MD .json format to the WI predictions.json format.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
#%% Imports and constants
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import argparse
|
|
13
|
+
from megadetector.utils.wi_utils import generate_predictions_json_from_md_results
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
#%% Command-line driver
|
|
17
|
+
|
|
18
|
+
def main():
|
|
19
|
+
|
|
20
|
+
parser = argparse.ArgumentParser()
|
|
21
|
+
parser.add_argument('md_results_file', action='store', type=str,
|
|
22
|
+
help='output file in MD format to convert')
|
|
23
|
+
parser.add_argument('predictions_json_file', action='store', type=str,
|
|
24
|
+
help='.json file to write in predictions.json format')
|
|
25
|
+
parser.add_argument('--base_folder', action='store', type=str, default=None,
|
|
26
|
+
help='folder name to prepend to each path in md_results_file, ' + \
|
|
27
|
+
'to convert relative paths to absolute paths.')
|
|
28
|
+
|
|
29
|
+
if len(sys.argv[1:]) == 0:
|
|
30
|
+
parser.print_help()
|
|
31
|
+
parser.exit()
|
|
32
|
+
|
|
33
|
+
args = parser.parse_args()
|
|
34
|
+
|
|
35
|
+
generate_predictions_json_from_md_results(args.md_results_file,
|
|
36
|
+
args.predictions_json_file,
|
|
37
|
+
base_folder=None)
|
|
38
|
+
|
|
39
|
+
if __name__ == '__main__':
|
|
40
|
+
main()
|
|
@@ -9,7 +9,7 @@ results file from MDv5a.
|
|
|
9
9
|
Detection categories must be the same in both files; if you want to first remap
|
|
10
10
|
one file's category mapping to be the same as another's, see remap_detection_categories.
|
|
11
11
|
|
|
12
|
-
If you want to literally merge two .json files, see
|
|
12
|
+
If you want to literally merge two .json files, see combine_batch_outputs.py.
|
|
13
13
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
@@ -211,6 +211,9 @@ class PostProcessingOptions:
|
|
|
211
211
|
# format('https://megadetector.readthedocs.io')
|
|
212
212
|
self.footer_text = ''
|
|
213
213
|
|
|
214
|
+
#: Character encoding to use when writing the index HTML html
|
|
215
|
+
self.output_html_encoding = None
|
|
216
|
+
|
|
214
217
|
# ...__init__()
|
|
215
218
|
|
|
216
219
|
# ...PostProcessingOptions
|
|
@@ -778,7 +781,8 @@ def _render_image_no_gt(file_info,detection_categories_to_results_name,
|
|
|
778
781
|
if det['conf'] > max_conf:
|
|
779
782
|
max_conf = det['conf']
|
|
780
783
|
|
|
781
|
-
if ('classifications' in det) and (len(det['classifications']) > 0)
|
|
784
|
+
if ('classifications' in det) and (len(det['classifications']) > 0) and \
|
|
785
|
+
(res != 'non_detections'):
|
|
782
786
|
|
|
783
787
|
# This is a list of [class,confidence] pairs, sorted by confidence
|
|
784
788
|
classifications = det['classifications']
|
|
@@ -1522,7 +1526,8 @@ def process_batch_results(options):
|
|
|
1522
1526
|
# Close body and html tags
|
|
1523
1527
|
index_page += '{}</body></html>'.format(options.footer_text)
|
|
1524
1528
|
output_html_file = os.path.join(output_dir, 'index.html')
|
|
1525
|
-
with open(output_html_file, 'w'
|
|
1529
|
+
with open(output_html_file, 'w',
|
|
1530
|
+
encoding=options.output_html_encoding) as f:
|
|
1526
1531
|
f.write(index_page)
|
|
1527
1532
|
|
|
1528
1533
|
print('Finished writing html to {}'.format(output_html_file))
|
|
@@ -1778,6 +1783,7 @@ def process_batch_results(options):
|
|
|
1778
1783
|
index_page += '</div>\n'
|
|
1779
1784
|
|
|
1780
1785
|
if has_classification_info:
|
|
1786
|
+
|
|
1781
1787
|
index_page += '<h3>Images of detected classes</h3>'
|
|
1782
1788
|
index_page += '<p>The same image might appear under multiple classes ' + \
|
|
1783
1789
|
'if multiple species were detected.</p>\n'
|
|
@@ -1806,7 +1812,8 @@ def process_batch_results(options):
|
|
|
1806
1812
|
|
|
1807
1813
|
index_page += '{}</body></html>'.format(options.footer_text)
|
|
1808
1814
|
output_html_file = os.path.join(output_dir, 'index.html')
|
|
1809
|
-
with open(output_html_file, 'w'
|
|
1815
|
+
with open(output_html_file, 'w',
|
|
1816
|
+
encoding=options.output_html_encoding) as f:
|
|
1810
1817
|
f.write(index_page)
|
|
1811
1818
|
|
|
1812
1819
|
print('Finished writing html to {}'.format(output_html_file))
|
|
@@ -88,6 +88,7 @@ from tqdm import tqdm
|
|
|
88
88
|
from megadetector.utils.ct_utils import args_to_object, is_float
|
|
89
89
|
from megadetector.detection.run_detector import get_typical_confidence_threshold_from_results
|
|
90
90
|
from megadetector.visualization import visualization_utils as vis_utils
|
|
91
|
+
from megadetector.visualization.visualization_utils import blur_detections
|
|
91
92
|
|
|
92
93
|
friendly_folder_names = {'animal':'animals','person':'people','vehicle':'vehicles'}
|
|
93
94
|
|
|
@@ -188,6 +189,11 @@ class SeparateDetectionsIntoFoldersOptions:
|
|
|
188
189
|
#: Do not set explicitly; this gets loaded from [results_file]
|
|
189
190
|
self.category_id_to_category_name = None
|
|
190
191
|
|
|
192
|
+
#: List of category names for which we should blur detections, most commonly ['person']
|
|
193
|
+
#:
|
|
194
|
+
#: Can also be a comma-separated list.
|
|
195
|
+
self.category_names_to_blur = None
|
|
196
|
+
|
|
191
197
|
# ...__init__()
|
|
192
198
|
|
|
193
199
|
# ...class SeparateDetectionsIntoFoldersOptions
|
|
@@ -369,10 +375,10 @@ def _process_detections(im,options):
|
|
|
369
375
|
return
|
|
370
376
|
|
|
371
377
|
# At this point, this image is getting copied; we may or may not also need to
|
|
372
|
-
# draw bounding boxes.
|
|
378
|
+
# draw bounding boxes or blur pixels.
|
|
373
379
|
|
|
374
|
-
# Do a simple copy operation if we don't need to render
|
|
375
|
-
if (not options.render_boxes) or \
|
|
380
|
+
# Do a simple copy operation if we don't need to manipulate the images (render boxes, blur pixels)
|
|
381
|
+
if (not options.render_boxes and (options.category_names_to_blur is None)) or \
|
|
376
382
|
(categories_above_threshold is None) or \
|
|
377
383
|
(len(categories_above_threshold) == 0):
|
|
378
384
|
|
|
@@ -386,6 +392,24 @@ def _process_detections(im,options):
|
|
|
386
392
|
# Open the source image
|
|
387
393
|
pil_image = vis_utils.load_image(source_path)
|
|
388
394
|
|
|
395
|
+
# Blur regions in the image if necessary
|
|
396
|
+
category_names_to_blur = options.category_names_to_blur
|
|
397
|
+
|
|
398
|
+
if category_names_to_blur is not None:
|
|
399
|
+
|
|
400
|
+
if isinstance(category_names_to_blur,str):
|
|
401
|
+
category_names_to_blur = category_names_to_blur.split(',')
|
|
402
|
+
category_names_to_blur = [s.strip() for s in category_names_to_blur]
|
|
403
|
+
|
|
404
|
+
detections_to_blur = []
|
|
405
|
+
for d in detections:
|
|
406
|
+
category_name = options.category_id_to_category_name[d['category']]
|
|
407
|
+
category_threshold = options.category_name_to_threshold[category_name]
|
|
408
|
+
if (d['conf'] >= category_threshold) and (category_name in category_names_to_blur):
|
|
409
|
+
detections_to_blur.append(d)
|
|
410
|
+
if len(detections_to_blur) > 0:
|
|
411
|
+
blur_detections(pil_image,detections_to_blur)
|
|
412
|
+
|
|
389
413
|
# Render bounding boxes for each category separately, because
|
|
390
414
|
# we allow different thresholds for each category.
|
|
391
415
|
|
|
@@ -447,9 +471,11 @@ def separate_detections_into_folders(options):
|
|
|
447
471
|
# Input validation
|
|
448
472
|
|
|
449
473
|
# Currently we don't support moving (instead of copying) when we're also rendering
|
|
450
|
-
# bounding boxes.
|
|
474
|
+
# bounding boxes or blurring humans.
|
|
451
475
|
assert not (options.render_boxes and options.move_images), \
|
|
452
476
|
'Cannot specify both render_boxes and move_images'
|
|
477
|
+
assert not ((options.category_names_to_blur is not None) and options.move_images), \
|
|
478
|
+
'Cannot specify both category_names_to_blur and move_images'
|
|
453
479
|
|
|
454
480
|
# Create output folder if necessary
|
|
455
481
|
if (os.path.isdir(options.base_output_folder)) and \
|
|
@@ -687,6 +713,8 @@ def main():
|
|
|
687
713
|
help='Box expansion (in pixels) for rendering, only meaningful if ' + \
|
|
688
714
|
'using render_boxes (defaults to {})'.format(
|
|
689
715
|
default_box_expansion))
|
|
716
|
+
parser.add_argument('--category_names_to_blur', type=str, default=None,
|
|
717
|
+
help='Comma-separated list of category names to blur (or a single category name, e.g. "person")')
|
|
690
718
|
|
|
691
719
|
if len(sys.argv[1:])==0:
|
|
692
720
|
parser.print_help()
|
|
@@ -50,6 +50,9 @@ class ValidateBatchResultsOptions:
|
|
|
50
50
|
|
|
51
51
|
#: Enable additional debug output
|
|
52
52
|
self.verbose = False
|
|
53
|
+
|
|
54
|
+
#: Should we raise errors immediately (vs. just catching and reporting)?
|
|
55
|
+
self.raise_errors = False
|
|
53
56
|
|
|
54
57
|
# ...class ValidateBatchResultsOptions
|
|
55
58
|
|
|
@@ -71,8 +74,7 @@ def validate_batch_results(json_filename,options=None):
|
|
|
71
74
|
the loaded data. The "validation_results" dict contains fields called "errors", "warnings",
|
|
72
75
|
and "filename". "errors" and "warnings" are lists of strings, although "errors" will never
|
|
73
76
|
be longer than N=1, since validation fails at the first error.
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
|
|
76
78
|
"""
|
|
77
79
|
|
|
78
80
|
if options is None:
|
|
@@ -223,8 +225,11 @@ def validate_batch_results(json_filename,options=None):
|
|
|
223
225
|
'Warning: non-standard key {} present at file level'.format(k))
|
|
224
226
|
|
|
225
227
|
except Exception as e:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
+
|
|
229
|
+
if options.raise_errors:
|
|
230
|
+
raise
|
|
231
|
+
else:
|
|
232
|
+
validation_results['errors'].append(str(e))
|
|
228
233
|
|
|
229
234
|
# ...try/except
|
|
230
235
|
|