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.
- megadetector/data_management/cct_json_utils.py +15 -2
- megadetector/data_management/coco_to_yolo.py +53 -31
- megadetector/data_management/databases/combine_coco_camera_traps_files.py +7 -3
- megadetector/data_management/databases/integrity_check_json_db.py +2 -2
- megadetector/data_management/lila/add_locations_to_island_camera_traps.py +73 -69
- megadetector/data_management/lila/add_locations_to_nacti.py +114 -110
- megadetector/data_management/lila/generate_lila_per_image_labels.py +2 -2
- megadetector/data_management/lila/test_lila_metadata_urls.py +21 -10
- megadetector/data_management/remap_coco_categories.py +60 -11
- megadetector/data_management/{wi_to_md.py → speciesnet_to_md.py} +2 -2
- megadetector/data_management/yolo_to_coco.py +45 -15
- megadetector/detection/run_detector.py +1 -0
- megadetector/detection/run_detector_batch.py +5 -4
- megadetector/postprocessing/classification_postprocessing.py +788 -524
- megadetector/postprocessing/compare_batch_results.py +176 -9
- megadetector/postprocessing/create_crop_folder.py +420 -0
- megadetector/postprocessing/load_api_results.py +4 -1
- megadetector/postprocessing/md_to_coco.py +1 -1
- megadetector/postprocessing/postprocess_batch_results.py +158 -44
- megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +3 -8
- megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +2 -2
- megadetector/postprocessing/separate_detections_into_folders.py +20 -4
- megadetector/postprocessing/subset_json_detector_output.py +180 -15
- megadetector/postprocessing/validate_batch_results.py +13 -5
- megadetector/taxonomy_mapping/map_new_lila_datasets.py +6 -6
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +3 -58
- megadetector/taxonomy_mapping/species_lookup.py +45 -2
- megadetector/utils/ct_utils.py +76 -3
- megadetector/utils/directory_listing.py +4 -4
- megadetector/utils/gpu_test.py +21 -3
- megadetector/utils/md_tests.py +142 -49
- megadetector/utils/path_utils.py +342 -19
- megadetector/utils/wi_utils.py +1286 -212
- megadetector/visualization/visualization_utils.py +16 -4
- megadetector/visualization/visualize_db.py +1 -1
- megadetector/visualization/visualize_detector_output.py +1 -4
- {megadetector-5.0.24.dist-info → megadetector-5.0.26.dist-info}/METADATA +6 -3
- {megadetector-5.0.24.dist-info → megadetector-5.0.26.dist-info}/RECORD +41 -40
- {megadetector-5.0.24.dist-info → megadetector-5.0.26.dist-info}/WHEEL +1 -1
- {megadetector-5.0.24.dist-info → megadetector-5.0.26.dist-info/licenses}/LICENSE +0 -0
- {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
|
|
52
|
-
from megadetector.
|
|
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
|
-
#
|
|
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,
|
|
690
|
-
|
|
691
|
-
|
|
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[
|
|
717
|
-
|
|
718
|
-
|
|
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>
|
|
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
|
|
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[
|
|
827
|
-
max_conf = file_info[
|
|
828
|
-
detections = file_info[
|
|
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(
|
|
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
|
-
#
|
|
1527
|
-
|
|
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
|
-
# ...
|
|
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(
|
|
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>
|
|
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>
|
|
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
|
-
|
|
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
|
|
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='
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
#:
|
|
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
|
-
|
|
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()
|