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.

Files changed (38) hide show
  1. megadetector/api/synchronous/api_core/animal_detection_api/api_backend.py +2 -3
  2. megadetector/classification/merge_classification_detection_output.py +2 -2
  3. megadetector/data_management/coco_to_labelme.py +2 -1
  4. megadetector/data_management/databases/integrity_check_json_db.py +15 -14
  5. megadetector/data_management/databases/subset_json_db.py +49 -21
  6. megadetector/data_management/mewc_to_md.py +340 -0
  7. megadetector/data_management/wi_to_md.py +41 -0
  8. megadetector/data_management/yolo_output_to_md_output.py +15 -8
  9. megadetector/detection/process_video.py +24 -7
  10. megadetector/detection/pytorch_detector.py +841 -160
  11. megadetector/detection/run_detector.py +340 -146
  12. megadetector/detection/run_detector_batch.py +304 -68
  13. megadetector/detection/run_inference_with_yolov5_val.py +61 -4
  14. megadetector/detection/tf_detector.py +6 -1
  15. megadetector/postprocessing/{combine_api_outputs.py → combine_batch_outputs.py} +10 -13
  16. megadetector/postprocessing/compare_batch_results.py +68 -6
  17. megadetector/postprocessing/md_to_labelme.py +7 -7
  18. megadetector/postprocessing/md_to_wi.py +40 -0
  19. megadetector/postprocessing/merge_detections.py +1 -1
  20. megadetector/postprocessing/postprocess_batch_results.py +10 -3
  21. megadetector/postprocessing/separate_detections_into_folders.py +32 -4
  22. megadetector/postprocessing/validate_batch_results.py +9 -4
  23. megadetector/utils/ct_utils.py +165 -45
  24. megadetector/utils/gpu_test.py +107 -0
  25. megadetector/utils/md_tests.py +355 -108
  26. megadetector/utils/path_utils.py +9 -2
  27. megadetector/utils/wi_utils.py +1794 -0
  28. megadetector/visualization/visualization_utils.py +82 -16
  29. megadetector/visualization/visualize_db.py +25 -7
  30. megadetector/visualization/visualize_detector_output.py +60 -13
  31. {megadetector-5.0.23.dist-info → megadetector-5.0.24.dist-info}/METADATA +10 -24
  32. {megadetector-5.0.23.dist-info → megadetector-5.0.24.dist-info}/RECORD +35 -33
  33. megadetector/detection/detector_training/__init__.py +0 -0
  34. megadetector/detection/detector_training/model_main_tf2.py +0 -114
  35. megadetector/utils/torch_test.py +0 -32
  36. {megadetector-5.0.23.dist-info → megadetector-5.0.24.dist-info}/LICENSE +0 -0
  37. {megadetector-5.0.23.dist-info → megadetector-5.0.24.dist-info}/WHEEL +0 -0
  38. {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.combine_api_outputs import combine_api_output_files
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
- _ = combine_api_output_files(input_files=chunk_output_files,
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
- combine_api_outputs.py
3
+ combine_batch_outputs.py
4
4
 
5
- Merges two or more .json files in batch API output format, optionally
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
- combine_api_outputs input1.json input2.json ... inputN.json output.json
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 combine_api_output_files(input_files,
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 = combine_api_output_dictionaries(
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 combine_api_output_dictionaries(input_dicts, require_uniqueness=True):
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
- raise ValueError(f'Unrecognized API output field: {k}')
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 API output versions in merging')
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
- # ...combine_api_output_files()
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
- combine_api_output_files(args.input_paths, args.output_path)
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': urllib.parse.quote(input_image_absolute_paths[i_fn]),
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 truncate_float
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 = truncate_float(det['bbox'][0] * im['width'],output_precision)
96
- y0 = truncate_float(det['bbox'][1] * im['height'],output_precision)
97
- x1 = truncate_float(x0 + det['bbox'][2] * im['width'],output_precision)
98
- y1 = truncate_float(y0 + det['bbox'][3] * im['height'],output_precision)
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'] = 'Failure image access'
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 combine_api_outputs.py.
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') as f:
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') as f:
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 any boxes
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
- validation_results['errors'].append(str(e))
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