megadetector 5.0.24__py3-none-any.whl → 5.0.26__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 (41) hide show
  1. megadetector/data_management/cct_json_utils.py +15 -2
  2. megadetector/data_management/coco_to_yolo.py +53 -31
  3. megadetector/data_management/databases/combine_coco_camera_traps_files.py +7 -3
  4. megadetector/data_management/databases/integrity_check_json_db.py +2 -2
  5. megadetector/data_management/lila/add_locations_to_island_camera_traps.py +73 -69
  6. megadetector/data_management/lila/add_locations_to_nacti.py +114 -110
  7. megadetector/data_management/lila/generate_lila_per_image_labels.py +2 -2
  8. megadetector/data_management/lila/test_lila_metadata_urls.py +21 -10
  9. megadetector/data_management/remap_coco_categories.py +60 -11
  10. megadetector/data_management/{wi_to_md.py → speciesnet_to_md.py} +2 -2
  11. megadetector/data_management/yolo_to_coco.py +45 -15
  12. megadetector/detection/run_detector.py +1 -0
  13. megadetector/detection/run_detector_batch.py +5 -4
  14. megadetector/postprocessing/classification_postprocessing.py +788 -524
  15. megadetector/postprocessing/compare_batch_results.py +176 -9
  16. megadetector/postprocessing/create_crop_folder.py +420 -0
  17. megadetector/postprocessing/load_api_results.py +4 -1
  18. megadetector/postprocessing/md_to_coco.py +1 -1
  19. megadetector/postprocessing/postprocess_batch_results.py +158 -44
  20. megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +3 -8
  21. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +2 -2
  22. megadetector/postprocessing/separate_detections_into_folders.py +20 -4
  23. megadetector/postprocessing/subset_json_detector_output.py +180 -15
  24. megadetector/postprocessing/validate_batch_results.py +13 -5
  25. megadetector/taxonomy_mapping/map_new_lila_datasets.py +6 -6
  26. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +3 -58
  27. megadetector/taxonomy_mapping/species_lookup.py +45 -2
  28. megadetector/utils/ct_utils.py +76 -3
  29. megadetector/utils/directory_listing.py +4 -4
  30. megadetector/utils/gpu_test.py +21 -3
  31. megadetector/utils/md_tests.py +142 -49
  32. megadetector/utils/path_utils.py +342 -19
  33. megadetector/utils/wi_utils.py +1286 -212
  34. megadetector/visualization/visualization_utils.py +16 -4
  35. megadetector/visualization/visualize_db.py +1 -1
  36. megadetector/visualization/visualize_detector_output.py +1 -4
  37. {megadetector-5.0.24.dist-info → megadetector-5.0.26.dist-info}/METADATA +6 -3
  38. {megadetector-5.0.24.dist-info → megadetector-5.0.26.dist-info}/RECORD +41 -40
  39. {megadetector-5.0.24.dist-info → megadetector-5.0.26.dist-info}/WHEEL +1 -1
  40. {megadetector-5.0.24.dist-info → megadetector-5.0.26.dist-info/licenses}/LICENSE +0 -0
  41. {megadetector-5.0.24.dist-info → megadetector-5.0.26.dist-info}/top_level.txt +0 -0
@@ -30,6 +30,7 @@ import time
30
30
  import uuid
31
31
  import warnings
32
32
  import random
33
+ import json
33
34
 
34
35
  from enum import IntEnum
35
36
  from multiprocessing.pool import ThreadPool
@@ -48,8 +49,11 @@ from megadetector.visualization import visualization_utils as vis_utils
48
49
  from megadetector.visualization import plot_utils
49
50
  from megadetector.utils.write_html_image_list import write_html_image_list
50
51
  from megadetector.utils import path_utils
51
- from megadetector.utils.ct_utils import args_to_object, sets_overlap
52
- from megadetector.data_management.cct_json_utils import (CameraTrapJsonUtils, IndexedJsonDb)
52
+ from megadetector.utils.ct_utils import args_to_object
53
+ from megadetector.utils.ct_utils import sets_overlap
54
+ from megadetector.utils.ct_utils import sort_dictionary_by_value
55
+ from megadetector.data_management.cct_json_utils import CameraTrapJsonUtils
56
+ from megadetector.data_management.cct_json_utils import IndexedJsonDb
53
57
  from megadetector.postprocessing.load_api_results import load_api_results
54
58
  from megadetector.detection.run_detector import get_typical_confidence_threshold_from_results
55
59
 
@@ -214,6 +218,15 @@ class PostProcessingOptions:
214
218
  #: Character encoding to use when writing the index HTML html
215
219
  self.output_html_encoding = None
216
220
 
221
+ #: Additional image fields to display in image headers. If this is a list,
222
+ #: we'll include those fields; if this is a dict, we'll use that dict to choose
223
+ #: alternative display names for each field.
224
+ self.additional_image_fields_to_display = None
225
+
226
+ #: If classification results are present, should we include a summary of
227
+ #: classification categories?
228
+ self.include_classification_category_report = True
229
+
217
230
  # ...__init__()
218
231
 
219
232
  # ...PostProcessingOptions
@@ -434,15 +447,6 @@ def _render_bounding_boxes(
434
447
  if options is None:
435
448
  options = PostProcessingOptions()
436
449
 
437
- # Leaving code in place for reading from blob storage, may support this
438
- # in the future.
439
- """
440
- stream = io.BytesIO()
441
- _ = blob_service.get_blob_to_stream(container_name, image_id, stream)
442
- # resize is to display them in this notebook or in the HTML more quickly
443
- image = Image.open(stream).resize(viz_size)
444
- """
445
-
446
450
  image_full_path = None
447
451
 
448
452
  if res in options.rendering_bypass_sets:
@@ -472,10 +476,12 @@ def _render_bounding_boxes(
472
476
  if image is not None:
473
477
 
474
478
  original_size = image.size
475
-
479
+
480
+ # Resize the image if necessary
476
481
  if options.viz_target_width is not None:
477
482
  image = vis_utils.resize_image(image, options.viz_target_width)
478
483
 
484
+ # Render ground truth boxes if necessary
479
485
  if ground_truth_boxes is not None and len(ground_truth_boxes) > 0:
480
486
 
481
487
  # Create class labels like "gt_1" or "gt_27"
@@ -487,8 +493,7 @@ def _render_bounding_boxes(
487
493
  original_size=original_size,label_map=label_map,
488
494
  thickness=4,expansion=4)
489
495
 
490
- # render_detection_bounding_boxes expects either a float or a dict mapping
491
- # category IDs to names.
496
+ # Preprare per-category confidence thresholds
492
497
  if isinstance(options.confidence_threshold,float):
493
498
  rendering_confidence_threshold = options.confidence_threshold
494
499
  else:
@@ -499,12 +504,14 @@ def _render_bounding_boxes(
499
504
  for category_id in category_ids:
500
505
  rendering_confidence_threshold[category_id] = \
501
506
  _get_threshold_for_category_id(category_id, options, detection_categories)
502
-
507
+
508
+ # Render detection boxes
503
509
  vis_utils.render_detection_bounding_boxes(
504
510
  detections, image,
505
511
  label_map=detection_categories,
506
512
  classification_label_map=classification_categories,
507
513
  confidence_threshold=rendering_confidence_threshold,
514
+ classification_confidence_threshold=options.classification_confidence_threshold,
508
515
  thickness=options.line_thickness,
509
516
  expansion=options.box_expansion)
510
517
 
@@ -686,9 +693,11 @@ def _has_positive_detection(detections,options,detection_categories):
686
693
  return found_positive_detection
687
694
 
688
695
 
689
- def _render_image_no_gt(file_info,detection_categories_to_results_name,
690
- detection_categories,classification_categories,
691
- options):
696
+ def _render_image_no_gt(file_info,
697
+ detection_categories_to_results_name,
698
+ detection_categories,
699
+ classification_categories,
700
+ options):
692
701
  """
693
702
  Renders an image (with no ground truth information)
694
703
 
@@ -713,9 +722,15 @@ def _render_image_no_gt(file_info,detection_categories_to_results_name,
713
722
  Returns None if there are any errors.
714
723
  """
715
724
 
716
- image_relative_path = file_info[0]
717
- max_conf = file_info[1]
718
- detections = file_info[2]
725
+ image_relative_path = file_info['file']
726
+
727
+ # Useful debug snippet
728
+ #
729
+ # if 'filename' in image_relative_path:
730
+ # import pdb; pdb.set_trace()
731
+
732
+ max_conf = file_info['max_detection_conf']
733
+ detections = file_info['detections']
719
734
 
720
735
  # Determine whether any positive detections are present (using a threshold that
721
736
  # may vary by category)
@@ -749,9 +764,31 @@ def _render_image_no_gt(file_info,detection_categories_to_results_name,
749
764
  assert detection_status == DetectionStatus.DS_ALMOST
750
765
  res = 'almost_detections'
751
766
 
752
- display_name = '<b>Result type</b>: {}, <b>Image</b>: {}, <b>Max conf</b>: {:0.3f}'.format(
767
+ display_name = '<b>Result type</b>: {}, <b>image</b>: {}, <b>max conf</b>: {:0.3f}'.format(
753
768
  res, image_relative_path, max_conf)
754
769
 
770
+ # Are there any bonus fields we need to include in each image header?
771
+ if options.additional_image_fields_to_display is not None:
772
+
773
+ for field_name in options.additional_image_fields_to_display:
774
+
775
+ if field_name in file_info:
776
+
777
+ field_value = file_info[field_name]
778
+
779
+ if (field_value is None) or \
780
+ (isinstance(field_value,float) and np.isnan(field_value)):
781
+ continue
782
+
783
+ # Optionally use a display name that's different from the field name
784
+ if isinstance(options.additional_image_fields_to_display,dict):
785
+ field_display_name = \
786
+ options.additional_image_fields_to_display[field_name]
787
+ else:
788
+ field_display_name = field_name
789
+ field_string = '<b>{}</b>: {}'.format(field_display_name,field_value)
790
+ display_name += ', {}'.format(field_string)
791
+
755
792
  rendering_options = copy.copy(options)
756
793
  if detection_status == DetectionStatus.DS_ALMOST:
757
794
  rendering_options.confidence_threshold = \
@@ -781,17 +818,24 @@ def _render_image_no_gt(file_info,detection_categories_to_results_name,
781
818
  if det['conf'] > max_conf:
782
819
  max_conf = det['conf']
783
820
 
821
+ # We make the decision here that only "detections" (not "almost-detections")
822
+ # will appear on the classification category pages
823
+ detection_threshold = \
824
+ _get_threshold_for_category_id(det['category'], options, detection_categories)
825
+ if det['conf'] < detection_threshold:
826
+ continue
827
+
784
828
  if ('classifications' in det) and (len(det['classifications']) > 0) and \
785
829
  (res != 'non_detections'):
786
830
 
787
- # This is a list of [class,confidence] pairs, sorted by confidence
831
+ # This is a list of [class,confidence] pairs, sorted by classification confidence
788
832
  classifications = det['classifications']
789
833
  top1_class_id = classifications[0][0]
790
834
  top1_class_name = classification_categories[top1_class_id]
791
835
  top1_class_score = classifications[0][1]
792
836
 
793
- # If we either don't have a confidence threshold, or we've met our
794
- # confidence threshold
837
+ # If we either don't have a classification confidence threshold, or
838
+ # we've met our classification confidence threshold
795
839
  if (options.classification_confidence_threshold < 0) or \
796
840
  (top1_class_score >= options.classification_confidence_threshold):
797
841
  class_string = 'class_{}'.format(top1_class_name)
@@ -823,9 +867,9 @@ def _render_image_with_gt(file_info,ground_truth_indexed_db,
823
867
  data format.
824
868
  """
825
869
 
826
- image_relative_path = file_info[0]
827
- max_conf = file_info[1]
828
- detections = file_info[2]
870
+ image_relative_path = file_info['file']
871
+ max_conf = file_info['max_detection_conf']
872
+ detections = file_info['detections']
829
873
 
830
874
  # This should already have been normalized to either '/' or '\'
831
875
 
@@ -971,6 +1015,7 @@ def process_batch_results(options):
971
1015
  print('\n*** Warning: {} images with ambiguous positive/negative status found in ground truth ***\n'.format(
972
1016
  n_ambiguous))
973
1017
 
1018
+
974
1019
  ##%% Load detection (and possibly classification) results
975
1020
 
976
1021
  # If the caller hasn't supplied results, load them
@@ -1028,6 +1073,8 @@ def process_batch_results(options):
1028
1073
  n_positives = 0
1029
1074
  n_almosts = 0
1030
1075
 
1076
+ print('Assigning images to rendering categories')
1077
+
1031
1078
  for i_row,row in tqdm(detections_df.iterrows(),total=len(detections_df)):
1032
1079
 
1033
1080
  detections = row['detections']
@@ -1372,7 +1419,7 @@ def process_batch_results(options):
1372
1419
  for _, row in images_to_visualize.iterrows():
1373
1420
 
1374
1421
  # Filenames should already have been normalized to either '/' or '\'
1375
- files_to_render.append([row['file'], row['max_detection_conf'], row['detections']])
1422
+ files_to_render.append(row.to_dict())
1376
1423
 
1377
1424
  start_time = time.time()
1378
1425
  if options.parallelize_rendering:
@@ -1523,8 +1570,13 @@ def process_batch_results(options):
1523
1570
  len(images_html['class_{}'.format(cname)]))
1524
1571
  index_page += '</div>'
1525
1572
 
1526
- # Close body and html tags
1527
- index_page += '{}</body></html>'.format(options.footer_text)
1573
+ # Write custom footer if it was provided
1574
+ if (options.footer_text is not None) and (len(options.footer_text) > 0):
1575
+ index_page += '{}\n'.format(options.footer_text)
1576
+
1577
+ # Close open html tags
1578
+ index_page += '\n</body></html>\n'
1579
+
1528
1580
  output_html_file = os.path.join(output_dir, 'index.html')
1529
1581
  with open(output_html_file, 'w',
1530
1582
  encoding=options.output_html_encoding) as f:
@@ -1532,7 +1584,7 @@ def process_batch_results(options):
1532
1584
 
1533
1585
  print('Finished writing html to {}'.format(output_html_file))
1534
1586
 
1535
- # ...for each image
1587
+ # ...if we have ground truth
1536
1588
 
1537
1589
 
1538
1590
  ##%% Otherwise, if we don't have ground truth...
@@ -1618,9 +1670,7 @@ def process_batch_results(options):
1618
1670
  assert isinstance(row['detections'],list)
1619
1671
 
1620
1672
  # Filenames should already have been normalized to either '/' or '\'
1621
- files_to_render.append([row['file'],
1622
- row['max_detection_conf'],
1623
- row['detections']])
1673
+ files_to_render.append(row.to_dict())
1624
1674
 
1625
1675
  start_time = time.time()
1626
1676
  if options.parallelize_rendering:
@@ -1691,8 +1741,7 @@ def process_batch_results(options):
1691
1741
  # Write index.html
1692
1742
 
1693
1743
  # We can't just sum these, because image_counts includes images in both their
1694
- # detection and classification classes
1695
- # total_images = sum(image_counts.values())
1744
+ # detection and classification classes
1696
1745
  total_images = 0
1697
1746
  for k in image_counts.keys():
1698
1747
  v = image_counts[k]
@@ -1722,7 +1771,7 @@ def process_batch_results(options):
1722
1771
  <p>Model version: {}</p>
1723
1772
  </div>
1724
1773
 
1725
- <h3>Sample images</h3>\n
1774
+ <h3>Detection results</h3>\n
1726
1775
  <div class="contentdiv">\n""".format(
1727
1776
  style_header, job_name_string, image_count, len(detections_df), confidence_threshold_string,
1728
1777
  almost_detection_string, model_version_string)
@@ -1779,12 +1828,18 @@ def process_batch_results(options):
1779
1828
  else:
1780
1829
  index_page += '<a href="{}">{}</a> ({}, {:.1%})<br/>\n'.format(
1781
1830
  filename,label,image_count,image_fraction)
1782
-
1831
+
1832
+ # ...for each result set
1833
+
1783
1834
  index_page += '</div>\n'
1784
1835
 
1836
+ # If classification information is present and we're supposed to create
1837
+ # a summary of classifications, we'll put it here
1838
+ category_count_footer = None
1839
+
1785
1840
  if has_classification_info:
1786
1841
 
1787
- index_page += '<h3>Images of detected classes</h3>'
1842
+ index_page += '<h3>Species classification results</h3>'
1788
1843
  index_page += '<p>The same image might appear under multiple classes ' + \
1789
1844
  'if multiple species were detected.</p>\n'
1790
1845
  index_page += '<p>Classifications with confidence less than {:.1%} confidence are considered "unreliable".</p>\n'.format(
@@ -1810,15 +1865,74 @@ def process_batch_results(options):
1810
1865
  cname, cname.lower(), ccount)
1811
1866
  index_page += '</div>\n'
1812
1867
 
1813
- index_page += '{}</body></html>'.format(options.footer_text)
1868
+ if options.include_classification_category_report:
1869
+
1870
+ # TODO: it's only for silly historical reasons that we re-read
1871
+ # the input file in this case; we're not currently carrying the json
1872
+ # representation around, only the Pandas representation.
1873
+
1874
+ print('Generating classification category report')
1875
+
1876
+ with open(options.md_results_file,'r') as f:
1877
+ d = json.load(f)
1878
+
1879
+ classification_category_to_count = {}
1880
+
1881
+ # im = d['images'][0]
1882
+ for im in d['images']:
1883
+ if 'detections' in im and im['detections'] is not None:
1884
+ for det in im['detections']:
1885
+ if 'classifications' in det:
1886
+ class_id = det['classifications'][0][0]
1887
+ if class_id not in classification_category_to_count:
1888
+ classification_category_to_count[class_id] = 0
1889
+ else:
1890
+ classification_category_to_count[class_id] = \
1891
+ classification_category_to_count[class_id] + 1
1892
+
1893
+ category_name_to_count = {}
1894
+
1895
+ for class_id in classification_category_to_count:
1896
+ category_name = d['classification_categories'][class_id]
1897
+ category_name_to_count[category_name] = \
1898
+ classification_category_to_count[class_id]
1899
+
1900
+ category_name_to_count = sort_dictionary_by_value(
1901
+ category_name_to_count,reverse=True)
1902
+
1903
+ category_count_footer = ''
1904
+ category_count_footer += '<br/>\n'
1905
+ category_count_footer += \
1906
+ '<h3>Category counts (for the whole dataset, not just the sample used for this page)</h3>\n'
1907
+ category_count_footer += '<div class="contentdiv">\n'
1908
+
1909
+ for category_name in category_name_to_count.keys():
1910
+ count = category_name_to_count[category_name]
1911
+ category_count_html = '{}: {}<br>\n'.format(category_name,count)
1912
+ category_count_footer += category_count_html
1913
+
1914
+ category_count_footer += '</div>\n'
1915
+
1916
+ # ...if we're generating a classification category report
1917
+
1918
+ # ...if classification info is present
1919
+
1920
+ if category_count_footer is not None:
1921
+ index_page += category_count_footer + '\n'
1922
+
1923
+ # Write custom footer if it was provided
1924
+ if (options.footer_text is not None) and (len(options.footer_text) > 0):
1925
+ index_page += options.footer_text + '\n'
1926
+
1927
+ # Close open html tags
1928
+ index_page += '\n</body></html>\n'
1929
+
1814
1930
  output_html_file = os.path.join(output_dir, 'index.html')
1815
1931
  with open(output_html_file, 'w',
1816
1932
  encoding=options.output_html_encoding) as f:
1817
1933
  f.write(index_page)
1818
1934
 
1819
- print('Finished writing html to {}'.format(output_html_file))
1820
-
1821
- # os.startfile(output_html_file)
1935
+ print('Finished writing html to {}'.format(output_html_file))
1822
1936
 
1823
1937
  # ...if we do/don't have ground truth
1824
1938
 
@@ -41,7 +41,6 @@ if False:
41
41
  baseDir = ''
42
42
 
43
43
  options = repeat_detections_core.RepeatDetectionOptions()
44
- options.bRenderHtml = True
45
44
  options.imageBase = baseDir
46
45
  options.outputBase = os.path.join(baseDir, 'repeat_detections')
47
46
  options.filenameReplacements = {} # E.g., {'20190430cameratraps\\':''}
@@ -85,11 +84,10 @@ def main():
85
84
  'do manual review of the repeat detection images (which you should)')
86
85
 
87
86
  parser.add_argument('--imageBase', action='store', type=str, default='',
88
- help='Image base dir, relevant if renderHtml is True or if ' + \
89
- '"omitFilteringFolder" is not set')
87
+ help='Image base dir')
90
88
 
91
89
  parser.add_argument('--outputBase', action='store', type=str, default='',
92
- help='HTML or filtering folder output dir')
90
+ help='filtering folder output dir')
93
91
 
94
92
  parser.add_argument('--confidenceMin', action='store', type=float,
95
93
  default=defaultOptions.confidenceMin,
@@ -146,7 +144,7 @@ def main():
146
144
 
147
145
  parser.add_argument('--omitFilteringFolder', action='store_false',
148
146
  dest='bWriteFilteringFolder',
149
- help='Should we create a folder of rendered detections for post-filtering?')
147
+ help='Should we skip creating the folder of rendered detections filtering?')
150
148
 
151
149
  parser.add_argument('--debugMaxDir', action='store', type=int, default=-1,
152
150
  help='For debugging only, limit the number of directories we process')
@@ -191,9 +189,6 @@ def main():
191
189
  default=defaultOptions.detectionTilesPrimaryImageWidth,
192
190
  help='The width of the main image when rendering images with detection tiles')
193
191
 
194
- parser.add_argument('--renderHtml', action='store_true',
195
- dest='bRenderHtml', help='Should we render HTML output?')
196
-
197
192
  if len(sys.argv[1:]) == 0:
198
193
  parser.print_help()
199
194
  parser.exit()
@@ -181,7 +181,7 @@ class RepeatDetectionOptions:
181
181
  #: Original size is preserved if this is None.
182
182
  #:
183
183
  #: This does *not* include the tile image grid.
184
- self.maxOutputImageWidth = None
184
+ self.maxOutputImageWidth = 2000
185
185
 
186
186
  #: Line thickness (in pixels) for box rendering
187
187
  self.lineThickness = 10
@@ -256,7 +256,7 @@ class RepeatDetectionOptions:
256
256
  self.detectionTilesPrimaryImageLocation = 'right'
257
257
 
258
258
  #: Maximum number of individual detection instances to include in the mosaic
259
- self.detectionTilesMaxCrops = 250
259
+ self.detectionTilesMaxCrops = 150
260
260
 
261
261
  #: If bRenderOtherDetections is True, what color should we use to render the
262
262
  #: (hopefully pretty subtle) non-target detections?
@@ -86,6 +86,7 @@ from functools import partial
86
86
  from tqdm import tqdm
87
87
 
88
88
  from megadetector.utils.ct_utils import args_to_object, is_float
89
+ from megadetector.utils.path_utils import remove_empty_folders
89
90
  from megadetector.detection.run_detector import get_typical_confidence_threshold_from_results
90
91
  from megadetector.visualization import visualization_utils as vis_utils
91
92
  from megadetector.visualization.visualization_utils import blur_detections
@@ -167,7 +168,7 @@ class SeparateDetectionsIntoFoldersOptions:
167
168
  #:
168
169
  #: deer=0.75,cow=0.75
169
170
  #:
170
- #: Converted internally to a dict mapping name:threshold
171
+ #: String, converted internally to a dict mapping name:threshold
171
172
  self.classification_thresholds = None
172
173
 
173
174
  ## Debug or internal attributes
@@ -194,6 +195,10 @@ class SeparateDetectionsIntoFoldersOptions:
194
195
  #: Can also be a comma-separated list.
195
196
  self.category_names_to_blur = None
196
197
 
198
+ #: Remove all empty folders from the target folder at the end of the process,
199
+ #: whether or not they were created by this script
200
+ self.remove_empty_folders = False
201
+
197
202
  # ...__init__()
198
203
 
199
204
  # ...class SeparateDetectionsIntoFoldersOptions
@@ -319,7 +324,7 @@ def _process_detections(im,options):
319
324
 
320
325
  classification_category_id = classification[0]
321
326
  classification_confidence = classification[1]
322
-
327
+
323
328
  # Do we have a threshold for this category, and if so, is
324
329
  # this classification above threshold?
325
330
  assert options.classification_category_id_to_name is not None
@@ -521,7 +526,11 @@ def separate_detections_into_folders(options):
521
526
  for category_name in category_names:
522
527
 
523
528
  # Do we have a custom threshold for this category?
524
- assert category_name in options.category_name_to_threshold
529
+ if category_name not in options.category_name_to_threshold:
530
+ print('Warning: category {} in detection file, but not in threshold mapping'.format(
531
+ category_name))
532
+ options.category_name_to_threshold[category_name] = None
533
+
525
534
  if options.category_name_to_threshold[category_name] is None:
526
535
  options.category_name_to_threshold[category_name] = default_threshold
527
536
 
@@ -584,7 +593,7 @@ def separate_detections_into_folders(options):
584
593
 
585
594
  # ...for each token
586
595
 
587
- options.classification_thresholds = classification_thresholds
596
+ options.classification_thresholds = classification_thresholds
588
597
 
589
598
  # ...if classification thresholds are still in string format
590
599
 
@@ -611,6 +620,10 @@ def separate_detections_into_folders(options):
611
620
  pool = ThreadPool(options.n_threads)
612
621
  process_detections_with_options = partial(_process_detections, options=options)
613
622
  _ = list(tqdm(pool.imap(process_detections_with_options, images), total=len(images)))
623
+
624
+ if options.remove_empty_folders:
625
+ print('Removing empty folders from {}'.format(options.base_output_folder))
626
+ remove_empty_folders(options.base_output_folder)
614
627
 
615
628
  # ...def separate_detections_into_folders
616
629
 
@@ -715,6 +728,9 @@ def main():
715
728
  default_box_expansion))
716
729
  parser.add_argument('--category_names_to_blur', type=str, default=None,
717
730
  help='Comma-separated list of category names to blur (or a single category name, e.g. "person")')
731
+ parser.add_argument('--remove_empty_folders', action='store_true',
732
+ help='Remove all empty folders from the target folder at the end of the process, ' + \
733
+ 'whether or not they were created by this script')
718
734
 
719
735
  if len(sys.argv[1:])==0:
720
736
  parser.print_help()