megadetector 5.0.28__py3-none-any.whl → 10.0.0__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/batch_processing/integration/digiKam/xmp_integration.py +2 -2
- megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +1 -1
- megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +1 -1
- megadetector/classification/aggregate_classifier_probs.py +3 -3
- megadetector/classification/analyze_failed_images.py +5 -5
- megadetector/classification/cache_batchapi_outputs.py +5 -5
- megadetector/classification/create_classification_dataset.py +11 -12
- megadetector/classification/crop_detections.py +10 -10
- megadetector/classification/csv_to_json.py +8 -8
- megadetector/classification/detect_and_crop.py +13 -15
- megadetector/classification/efficientnet/model.py +8 -8
- megadetector/classification/efficientnet/utils.py +6 -5
- megadetector/classification/evaluate_model.py +7 -7
- megadetector/classification/identify_mislabeled_candidates.py +6 -6
- megadetector/classification/json_to_azcopy_list.py +1 -1
- megadetector/classification/json_validator.py +29 -32
- megadetector/classification/map_classification_categories.py +9 -9
- megadetector/classification/merge_classification_detection_output.py +12 -9
- megadetector/classification/prepare_classification_script.py +19 -19
- megadetector/classification/prepare_classification_script_mc.py +26 -26
- megadetector/classification/run_classifier.py +4 -4
- megadetector/classification/save_mislabeled.py +6 -6
- megadetector/classification/train_classifier.py +1 -1
- megadetector/classification/train_classifier_tf.py +9 -9
- megadetector/classification/train_utils.py +10 -10
- megadetector/data_management/annotations/annotation_constants.py +1 -2
- megadetector/data_management/camtrap_dp_to_coco.py +79 -46
- megadetector/data_management/cct_json_utils.py +103 -103
- megadetector/data_management/cct_to_md.py +49 -49
- megadetector/data_management/cct_to_wi.py +33 -33
- megadetector/data_management/coco_to_labelme.py +75 -75
- megadetector/data_management/coco_to_yolo.py +210 -193
- megadetector/data_management/databases/add_width_and_height_to_db.py +86 -12
- megadetector/data_management/databases/combine_coco_camera_traps_files.py +40 -40
- megadetector/data_management/databases/integrity_check_json_db.py +228 -200
- megadetector/data_management/databases/subset_json_db.py +33 -33
- megadetector/data_management/generate_crops_from_cct.py +88 -39
- megadetector/data_management/get_image_sizes.py +54 -49
- megadetector/data_management/labelme_to_coco.py +133 -125
- megadetector/data_management/labelme_to_yolo.py +159 -73
- megadetector/data_management/lila/create_lila_blank_set.py +81 -83
- megadetector/data_management/lila/create_lila_test_set.py +32 -31
- megadetector/data_management/lila/create_links_to_md_results_files.py +18 -18
- megadetector/data_management/lila/download_lila_subset.py +21 -24
- megadetector/data_management/lila/generate_lila_per_image_labels.py +365 -107
- megadetector/data_management/lila/get_lila_annotation_counts.py +35 -33
- megadetector/data_management/lila/get_lila_image_counts.py +22 -22
- megadetector/data_management/lila/lila_common.py +73 -70
- megadetector/data_management/lila/test_lila_metadata_urls.py +28 -19
- megadetector/data_management/mewc_to_md.py +344 -340
- megadetector/data_management/ocr_tools.py +262 -255
- megadetector/data_management/read_exif.py +249 -227
- megadetector/data_management/remap_coco_categories.py +90 -28
- megadetector/data_management/remove_exif.py +81 -21
- megadetector/data_management/rename_images.py +187 -187
- megadetector/data_management/resize_coco_dataset.py +588 -120
- megadetector/data_management/speciesnet_to_md.py +41 -41
- megadetector/data_management/wi_download_csv_to_coco.py +55 -55
- megadetector/data_management/yolo_output_to_md_output.py +248 -122
- megadetector/data_management/yolo_to_coco.py +333 -191
- megadetector/detection/change_detection.py +832 -0
- megadetector/detection/process_video.py +340 -337
- megadetector/detection/pytorch_detector.py +358 -278
- megadetector/detection/run_detector.py +399 -186
- megadetector/detection/run_detector_batch.py +404 -377
- megadetector/detection/run_inference_with_yolov5_val.py +340 -327
- megadetector/detection/run_tiled_inference.py +257 -249
- megadetector/detection/tf_detector.py +24 -24
- megadetector/detection/video_utils.py +332 -295
- megadetector/postprocessing/add_max_conf.py +19 -11
- megadetector/postprocessing/categorize_detections_by_size.py +45 -45
- megadetector/postprocessing/classification_postprocessing.py +468 -433
- megadetector/postprocessing/combine_batch_outputs.py +23 -23
- megadetector/postprocessing/compare_batch_results.py +590 -525
- megadetector/postprocessing/convert_output_format.py +106 -102
- megadetector/postprocessing/create_crop_folder.py +347 -147
- megadetector/postprocessing/detector_calibration.py +173 -168
- megadetector/postprocessing/generate_csv_report.py +508 -499
- megadetector/postprocessing/load_api_results.py +48 -27
- megadetector/postprocessing/md_to_coco.py +133 -102
- megadetector/postprocessing/md_to_labelme.py +107 -90
- megadetector/postprocessing/md_to_wi.py +40 -40
- megadetector/postprocessing/merge_detections.py +92 -114
- megadetector/postprocessing/postprocess_batch_results.py +319 -301
- megadetector/postprocessing/remap_detection_categories.py +91 -38
- megadetector/postprocessing/render_detection_confusion_matrix.py +214 -205
- megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +57 -57
- megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +27 -28
- megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +704 -679
- megadetector/postprocessing/separate_detections_into_folders.py +226 -211
- megadetector/postprocessing/subset_json_detector_output.py +265 -262
- megadetector/postprocessing/top_folders_to_bottom.py +45 -45
- megadetector/postprocessing/validate_batch_results.py +70 -70
- megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +52 -52
- megadetector/taxonomy_mapping/map_new_lila_datasets.py +18 -19
- megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +54 -33
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +67 -67
- megadetector/taxonomy_mapping/retrieve_sample_image.py +16 -16
- megadetector/taxonomy_mapping/simple_image_download.py +8 -8
- megadetector/taxonomy_mapping/species_lookup.py +156 -74
- megadetector/taxonomy_mapping/taxonomy_csv_checker.py +14 -14
- megadetector/taxonomy_mapping/taxonomy_graph.py +10 -10
- megadetector/taxonomy_mapping/validate_lila_category_mappings.py +13 -13
- megadetector/utils/ct_utils.py +1049 -211
- megadetector/utils/directory_listing.py +21 -77
- megadetector/utils/gpu_test.py +22 -22
- megadetector/utils/md_tests.py +632 -529
- megadetector/utils/path_utils.py +1520 -431
- megadetector/utils/process_utils.py +41 -41
- megadetector/utils/split_locations_into_train_val.py +62 -62
- megadetector/utils/string_utils.py +148 -27
- megadetector/utils/url_utils.py +489 -176
- megadetector/utils/wi_utils.py +2658 -2526
- megadetector/utils/write_html_image_list.py +137 -137
- megadetector/visualization/plot_utils.py +34 -30
- megadetector/visualization/render_images_with_thumbnails.py +39 -74
- megadetector/visualization/visualization_utils.py +487 -435
- megadetector/visualization/visualize_db.py +232 -198
- megadetector/visualization/visualize_detector_output.py +82 -76
- {megadetector-5.0.28.dist-info → megadetector-10.0.0.dist-info}/METADATA +5 -2
- megadetector-10.0.0.dist-info/RECORD +139 -0
- {megadetector-5.0.28.dist-info → megadetector-10.0.0.dist-info}/WHEEL +1 -1
- megadetector/api/batch_processing/api_core/__init__.py +0 -0
- megadetector/api/batch_processing/api_core/batch_service/__init__.py +0 -0
- megadetector/api/batch_processing/api_core/batch_service/score.py +0 -439
- megadetector/api/batch_processing/api_core/server.py +0 -294
- megadetector/api/batch_processing/api_core/server_api_config.py +0 -97
- megadetector/api/batch_processing/api_core/server_app_config.py +0 -55
- megadetector/api/batch_processing/api_core/server_batch_job_manager.py +0 -220
- megadetector/api/batch_processing/api_core/server_job_status_table.py +0 -149
- megadetector/api/batch_processing/api_core/server_orchestration.py +0 -360
- megadetector/api/batch_processing/api_core/server_utils.py +0 -88
- megadetector/api/batch_processing/api_core_support/__init__.py +0 -0
- megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +0 -46
- megadetector/api/batch_processing/api_support/__init__.py +0 -0
- megadetector/api/batch_processing/api_support/summarize_daily_activity.py +0 -152
- megadetector/api/batch_processing/data_preparation/__init__.py +0 -0
- megadetector/api/synchronous/__init__.py +0 -0
- megadetector/api/synchronous/api_core/animal_detection_api/__init__.py +0 -0
- megadetector/api/synchronous/api_core/animal_detection_api/api_backend.py +0 -151
- megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +0 -263
- megadetector/api/synchronous/api_core/animal_detection_api/config.py +0 -35
- megadetector/api/synchronous/api_core/tests/__init__.py +0 -0
- megadetector/api/synchronous/api_core/tests/load_test.py +0 -110
- megadetector/data_management/importers/add_nacti_sizes.py +0 -52
- megadetector/data_management/importers/add_timestamps_to_icct.py +0 -79
- megadetector/data_management/importers/animl_results_to_md_results.py +0 -158
- megadetector/data_management/importers/auckland_doc_test_to_json.py +0 -373
- megadetector/data_management/importers/auckland_doc_to_json.py +0 -201
- megadetector/data_management/importers/awc_to_json.py +0 -191
- megadetector/data_management/importers/bellevue_to_json.py +0 -272
- megadetector/data_management/importers/cacophony-thermal-importer.py +0 -793
- megadetector/data_management/importers/carrizo_shrubfree_2018.py +0 -269
- megadetector/data_management/importers/carrizo_trail_cam_2017.py +0 -289
- megadetector/data_management/importers/cct_field_adjustments.py +0 -58
- megadetector/data_management/importers/channel_islands_to_cct.py +0 -913
- megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +0 -180
- megadetector/data_management/importers/eMammal/eMammal_helpers.py +0 -249
- megadetector/data_management/importers/eMammal/make_eMammal_json.py +0 -223
- megadetector/data_management/importers/ena24_to_json.py +0 -276
- megadetector/data_management/importers/filenames_to_json.py +0 -386
- megadetector/data_management/importers/helena_to_cct.py +0 -283
- megadetector/data_management/importers/idaho-camera-traps.py +0 -1407
- megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +0 -294
- megadetector/data_management/importers/import_desert_lion_conservation_camera_traps.py +0 -387
- megadetector/data_management/importers/jb_csv_to_json.py +0 -150
- megadetector/data_management/importers/mcgill_to_json.py +0 -250
- megadetector/data_management/importers/missouri_to_json.py +0 -490
- megadetector/data_management/importers/nacti_fieldname_adjustments.py +0 -79
- megadetector/data_management/importers/noaa_seals_2019.py +0 -181
- megadetector/data_management/importers/osu-small-animals-to-json.py +0 -364
- megadetector/data_management/importers/pc_to_json.py +0 -365
- megadetector/data_management/importers/plot_wni_giraffes.py +0 -123
- megadetector/data_management/importers/prepare_zsl_imerit.py +0 -131
- megadetector/data_management/importers/raic_csv_to_md_results.py +0 -416
- megadetector/data_management/importers/rspb_to_json.py +0 -356
- megadetector/data_management/importers/save_the_elephants_survey_A.py +0 -320
- megadetector/data_management/importers/save_the_elephants_survey_B.py +0 -329
- megadetector/data_management/importers/snapshot_safari_importer.py +0 -758
- megadetector/data_management/importers/snapshot_serengeti_lila.py +0 -1067
- megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +0 -150
- megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +0 -153
- megadetector/data_management/importers/sulross_get_exif.py +0 -65
- megadetector/data_management/importers/timelapse_csv_set_to_json.py +0 -490
- megadetector/data_management/importers/ubc_to_json.py +0 -399
- megadetector/data_management/importers/umn_to_json.py +0 -507
- megadetector/data_management/importers/wellington_to_json.py +0 -263
- megadetector/data_management/importers/wi_to_json.py +0 -442
- megadetector/data_management/importers/zamba_results_to_md_results.py +0 -180
- megadetector/data_management/lila/add_locations_to_island_camera_traps.py +0 -101
- megadetector/data_management/lila/add_locations_to_nacti.py +0 -151
- megadetector/utils/azure_utils.py +0 -178
- megadetector/utils/sas_blob_utils.py +0 -509
- megadetector-5.0.28.dist-info/RECORD +0 -209
- /megadetector/{api/batch_processing/__init__.py → __init__.py} +0 -0
- {megadetector-5.0.28.dist-info → megadetector-10.0.0.dist-info}/licenses/LICENSE +0 -0
- {megadetector-5.0.28.dist-info → megadetector-10.0.0.dist-info}/top_level.txt +0 -0
|
@@ -12,7 +12,7 @@ Given a .json or .csv file containing MD results, do one or more of the followin
|
|
|
12
12
|
truth)
|
|
13
13
|
|
|
14
14
|
Ground truth, if available, must be in COCO Camera Traps format:
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
https://github.com/agentmorris/MegaDetector/blob/main/megadetector/data_management/README.md#coco-camera-traps-format
|
|
17
17
|
|
|
18
18
|
"""
|
|
@@ -30,7 +30,6 @@ import time
|
|
|
30
30
|
import uuid
|
|
31
31
|
import warnings
|
|
32
32
|
import random
|
|
33
|
-
import json
|
|
34
33
|
|
|
35
34
|
from enum import IntEnum
|
|
36
35
|
from multiprocessing.pool import ThreadPool
|
|
@@ -76,40 +75,40 @@ class PostProcessingOptions:
|
|
|
76
75
|
"""
|
|
77
76
|
Options used to parameterize process_batch_results().
|
|
78
77
|
"""
|
|
79
|
-
|
|
78
|
+
|
|
80
79
|
def __init__(self):
|
|
81
|
-
|
|
80
|
+
|
|
82
81
|
### Required inputs
|
|
83
|
-
|
|
82
|
+
|
|
84
83
|
#: MD results .json file to process
|
|
85
84
|
self.md_results_file = ''
|
|
86
|
-
|
|
85
|
+
|
|
87
86
|
#: Folder to which we should write HTML output
|
|
88
87
|
self.output_dir = ''
|
|
89
|
-
|
|
88
|
+
|
|
90
89
|
### Options
|
|
91
|
-
|
|
90
|
+
|
|
92
91
|
#: Folder where images live (filenames in [md_results_file] should be relative to this folder)
|
|
93
92
|
#:
|
|
94
93
|
#: Can be '' if [md_results_file] uses absolute paths.
|
|
95
94
|
self.image_base_dir = ''
|
|
96
|
-
|
|
95
|
+
|
|
97
96
|
## These apply only when we're doing ground-truth comparisons
|
|
98
|
-
|
|
97
|
+
|
|
99
98
|
#: Optional .json file containing ground truth information
|
|
100
99
|
self.ground_truth_json_file = ''
|
|
101
|
-
|
|
102
|
-
#: List of classes we'll treat as negative (defaults to "empty", typically includes
|
|
100
|
+
|
|
101
|
+
#: List of classes we'll treat as negative (defaults to "empty", typically includes
|
|
103
102
|
#: classes like "blank", "misfire", etc.).
|
|
104
103
|
#:
|
|
105
104
|
#: Include the token "#NO_LABELS#" to indicate that an image with no annotations
|
|
106
105
|
#: should be considered empty.
|
|
107
106
|
self.negative_classes = DEFAULT_NEGATIVE_CLASSES
|
|
108
|
-
|
|
109
|
-
#: List of classes we'll treat as neither positive nor negative (defaults to
|
|
107
|
+
|
|
108
|
+
#: List of classes we'll treat as neither positive nor negative (defaults to
|
|
110
109
|
#: "unknown", typically includes classes like "unidentifiable").
|
|
111
110
|
self.unlabeled_classes = DEFAULT_UNKNOWN_CLASSES
|
|
112
|
-
|
|
111
|
+
|
|
113
112
|
#: List of output sets that we should count, but not render images for.
|
|
114
113
|
#:
|
|
115
114
|
#: Typically used to preview sets with lots of empties, where you don't want to
|
|
@@ -118,120 +117,122 @@ class PostProcessingOptions:
|
|
|
118
117
|
#: detections, non_detections
|
|
119
118
|
#: detections_animal, detections_person, detections_vehicle
|
|
120
119
|
self.rendering_bypass_sets = []
|
|
121
|
-
|
|
120
|
+
|
|
122
121
|
#: If this is None, choose a confidence threshold based on the detector version.
|
|
123
122
|
#:
|
|
124
|
-
#: This can either be a float or a dictionary mapping category names (not IDs) to
|
|
125
|
-
#: thresholds. The category "default" can be used to specify thresholds for
|
|
126
|
-
#: other categories. Currently the use of a dict here is not supported when
|
|
123
|
+
#: This can either be a float or a dictionary mapping category names (not IDs) to
|
|
124
|
+
#: thresholds. The category "default" can be used to specify thresholds for
|
|
125
|
+
#: other categories. Currently the use of a dict here is not supported when
|
|
127
126
|
#: ground truth is supplied.
|
|
128
127
|
self.confidence_threshold = None
|
|
129
|
-
|
|
128
|
+
|
|
130
129
|
#: Confidence threshold to apply to classification (not detection) results
|
|
131
130
|
#:
|
|
132
131
|
#: Only a float is supported here (unlike the "confidence_threshold" parameter, which
|
|
133
|
-
#: can be a dict).
|
|
132
|
+
#: can be a dict).
|
|
134
133
|
self.classification_confidence_threshold = 0.5
|
|
135
|
-
|
|
134
|
+
|
|
136
135
|
#: Used for summary statistics only
|
|
137
136
|
self.target_recall = 0.9
|
|
138
|
-
|
|
137
|
+
|
|
139
138
|
#: Number of images to sample, -1 for "all images"
|
|
140
139
|
self.num_images_to_sample = 500
|
|
141
|
-
|
|
140
|
+
|
|
142
141
|
#: Random seed for sampling, or None
|
|
143
142
|
self.sample_seed = 0 # None
|
|
144
|
-
|
|
143
|
+
|
|
145
144
|
#: Image width for images in the HTML output
|
|
146
145
|
self.viz_target_width = 800
|
|
147
|
-
|
|
146
|
+
|
|
148
147
|
#: Line width (in pixels) for rendering detections
|
|
149
148
|
self.line_thickness = 4
|
|
150
|
-
|
|
149
|
+
|
|
151
150
|
#: Box expansion (in pixels) for rendering detections
|
|
152
151
|
self.box_expansion = 0
|
|
153
|
-
|
|
152
|
+
|
|
154
153
|
#: Job name to include in big letters in the output HTML
|
|
155
154
|
self.job_name_string = None
|
|
156
|
-
|
|
155
|
+
|
|
157
156
|
#: Model version string to include in the output HTML
|
|
158
157
|
self.model_version_string = None
|
|
159
|
-
|
|
158
|
+
|
|
160
159
|
#: Sort order for the output, should be one of "filename", "confidence", or "random"
|
|
161
160
|
self.html_sort_order = 'filename'
|
|
162
|
-
|
|
161
|
+
|
|
163
162
|
#: If True, images in the output HTML will be links back to the original images
|
|
164
163
|
self.link_images_to_originals = True
|
|
165
|
-
|
|
164
|
+
|
|
166
165
|
#: Optionally separate detections into categories (animal/vehicle/human)
|
|
167
|
-
#:
|
|
166
|
+
#:
|
|
168
167
|
#: Currently only supported when ground truth is unavailable
|
|
169
168
|
self.separate_detections_by_category = True
|
|
170
|
-
|
|
169
|
+
|
|
171
170
|
#: Optionally replace one or more strings in filenames with other strings;
|
|
172
171
|
#: useful for taking a set of results generated for one folder structure
|
|
173
172
|
#: and applying them to a slightly different folder structure.
|
|
174
173
|
self.api_output_filename_replacements = {}
|
|
175
|
-
|
|
174
|
+
|
|
176
175
|
#: Optionally replace one or more strings in filenames with other strings;
|
|
177
176
|
#: useful for taking a set of results generated for one folder structure
|
|
178
177
|
#: and applying them to a slightly different folder structure.
|
|
179
178
|
self.ground_truth_filename_replacements = {}
|
|
180
|
-
|
|
179
|
+
|
|
181
180
|
#: Allow bypassing API output loading when operating on previously-loaded
|
|
182
181
|
#: results. If present, this is a Pandas DataFrame. Almost never useful.
|
|
183
182
|
self.api_detection_results = None
|
|
184
|
-
|
|
183
|
+
|
|
185
184
|
#: Allow bypassing API output loading when operating on previously-loaded
|
|
186
185
|
#: results. If present, this is a str --> obj dict. Almost never useful.
|
|
187
186
|
self.api_other_fields = None
|
|
188
|
-
|
|
187
|
+
|
|
189
188
|
#: Should we also split out a separate report about the detections that were
|
|
190
189
|
#: just below our main confidence threshold?
|
|
191
190
|
#:
|
|
192
191
|
#: Currently only supported when ground truth is unavailable.
|
|
193
192
|
self.include_almost_detections = False
|
|
194
|
-
|
|
193
|
+
|
|
195
194
|
#: Only a float is supported here (unlike the "confidence_threshold" parameter, which
|
|
196
195
|
#: can be a dict).
|
|
197
196
|
self.almost_detection_confidence_threshold = None
|
|
198
|
-
|
|
199
|
-
#: Enable/disable rendering parallelization
|
|
197
|
+
|
|
198
|
+
#: Enable/disable rendering parallelization
|
|
200
199
|
self.parallelize_rendering = False
|
|
201
|
-
|
|
200
|
+
|
|
202
201
|
#: Number of threads/processes to use for rendering parallelization
|
|
203
202
|
self.parallelize_rendering_n_cores = 25
|
|
204
|
-
|
|
203
|
+
|
|
205
204
|
#: Whether to use threads (True) or processes (False) for rendering parallelization
|
|
206
205
|
self.parallelize_rendering_with_threads = True
|
|
207
|
-
|
|
206
|
+
|
|
208
207
|
#: When classification results are present, should be sort alphabetically by class name (False)
|
|
209
208
|
#: or in descending order by frequency (True)?
|
|
210
209
|
self.sort_classification_results_by_count = False
|
|
211
|
-
|
|
210
|
+
|
|
212
211
|
#: Should we split individual pages up into smaller pages if there are more than
|
|
213
212
|
#: N images?
|
|
214
213
|
self.max_figures_per_html_file = None
|
|
215
|
-
|
|
214
|
+
|
|
216
215
|
#: Footer text for the index page
|
|
217
|
-
# self.footer_text =
|
|
216
|
+
# self.footer_text = \
|
|
217
|
+
# '<br/><p style="font-size:80%;">Preview page created with the ' + \
|
|
218
|
+
# <a href="{}">MegaDetector Python package</a>.</p>'.\
|
|
218
219
|
# format('https://megadetector.readthedocs.io')
|
|
219
220
|
self.footer_text = ''
|
|
220
221
|
|
|
221
222
|
#: Character encoding to use when writing the index HTML html
|
|
222
223
|
self.output_html_encoding = None
|
|
223
|
-
|
|
224
|
+
|
|
224
225
|
#: Additional image fields to display in image headers. If this is a list,
|
|
225
226
|
#: we'll include those fields; if this is a dict, we'll use that dict to choose
|
|
226
227
|
#: alternative display names for each field.
|
|
227
228
|
self.additional_image_fields_to_display = None
|
|
228
|
-
|
|
229
|
-
#: If classification results are present, should we include a summary of
|
|
229
|
+
|
|
230
|
+
#: If classification results are present, should we include a summary of
|
|
230
231
|
#: classification categories?
|
|
231
232
|
self.include_classification_category_report = True
|
|
232
|
-
|
|
233
|
+
|
|
233
234
|
# ...__init__()
|
|
234
|
-
|
|
235
|
+
|
|
235
236
|
# ...PostProcessingOptions
|
|
236
237
|
|
|
237
238
|
|
|
@@ -239,15 +240,15 @@ class PostProcessingResults:
|
|
|
239
240
|
"""
|
|
240
241
|
Return format from process_batch_results
|
|
241
242
|
"""
|
|
242
|
-
|
|
243
|
+
|
|
243
244
|
def __init__(self):
|
|
244
|
-
|
|
245
|
+
|
|
245
246
|
#: HTML file to which preview information was written
|
|
246
247
|
self.output_html_file = ''
|
|
247
|
-
|
|
248
|
+
|
|
248
249
|
#: Pandas Dataframe containing detection results
|
|
249
250
|
self.api_detection_results = None
|
|
250
|
-
|
|
251
|
+
|
|
251
252
|
#: str --> obj dictionary containing other information loaded from the results file
|
|
252
253
|
self.api_other_fields = None
|
|
253
254
|
|
|
@@ -258,10 +259,10 @@ class DetectionStatus(IntEnum):
|
|
|
258
259
|
"""
|
|
259
260
|
Flags used to mark images as positive or negative for P/R analysis
|
|
260
261
|
(according to ground truth and/or detector output)
|
|
261
|
-
|
|
262
|
+
|
|
262
263
|
:meta private:
|
|
263
264
|
"""
|
|
264
|
-
|
|
265
|
+
|
|
265
266
|
DS_NEGATIVE = 0
|
|
266
267
|
DS_POSITIVE = 1
|
|
267
268
|
|
|
@@ -294,7 +295,7 @@ def _mark_detection_status(indexed_db,
|
|
|
294
295
|
|
|
295
296
|
returns (n_negative, n_positive, n_unknown, n_ambiguous)
|
|
296
297
|
"""
|
|
297
|
-
|
|
298
|
+
|
|
298
299
|
negative_classes = set(negative_classes)
|
|
299
300
|
unknown_classes = set(unknown_classes)
|
|
300
301
|
|
|
@@ -323,7 +324,7 @@ def _mark_detection_status(indexed_db,
|
|
|
323
324
|
|
|
324
325
|
# If there are no image annotations...
|
|
325
326
|
if len(categories) == 0:
|
|
326
|
-
|
|
327
|
+
|
|
327
328
|
if '#NO_LABELS#' in negative_classes:
|
|
328
329
|
n_negative += 1
|
|
329
330
|
im['_detection_status'] = DetectionStatus.DS_NEGATIVE
|
|
@@ -379,10 +380,10 @@ def is_sas_url(s) -> bool:
|
|
|
379
380
|
"""
|
|
380
381
|
Placeholder for a more robust way to verify that a link is a SAS URL.
|
|
381
382
|
99.999% of the time this will suffice for what we're using it for right now.
|
|
382
|
-
|
|
383
|
+
|
|
383
384
|
:meta private:
|
|
384
385
|
"""
|
|
385
|
-
|
|
386
|
+
|
|
386
387
|
return (s.startswith(('http://', 'https://')) and ('core.windows.net' in s)
|
|
387
388
|
and ('?' in s))
|
|
388
389
|
|
|
@@ -391,10 +392,10 @@ def relative_sas_url(folder_url, relative_path):
|
|
|
391
392
|
"""
|
|
392
393
|
Given a container-level or folder-level SAS URL, create a SAS URL to the
|
|
393
394
|
specified relative path.
|
|
394
|
-
|
|
395
|
+
|
|
395
396
|
:meta private:
|
|
396
397
|
"""
|
|
397
|
-
|
|
398
|
+
|
|
398
399
|
relative_path = relative_path.replace('%','%25')
|
|
399
400
|
relative_path = relative_path.replace('#','%23')
|
|
400
401
|
relative_path = relative_path.replace(' ','%20')
|
|
@@ -422,8 +423,8 @@ def _render_bounding_boxes(
|
|
|
422
423
|
options=None):
|
|
423
424
|
"""
|
|
424
425
|
Renders detection bounding boxes on a single image.
|
|
425
|
-
|
|
426
|
-
This is an internal function; if you want tools for rendering boxes on images, see
|
|
426
|
+
|
|
427
|
+
This is an internal function; if you want tools for rendering boxes on images, see
|
|
427
428
|
visualization.visualization_utils.
|
|
428
429
|
|
|
429
430
|
The source image is:
|
|
@@ -432,18 +433,18 @@ def _render_bounding_boxes(
|
|
|
432
433
|
|
|
433
434
|
The target image is, for example:
|
|
434
435
|
|
|
435
|
-
[options.output_dir] /
|
|
436
|
-
['detections' or 'non_detections'] /
|
|
436
|
+
[options.output_dir] /
|
|
437
|
+
['detections' or 'non_detections'] /
|
|
437
438
|
[filename with slashes turned into tildes]
|
|
438
439
|
|
|
439
440
|
"res" is a result type, e.g. "detections", "non-detections"; this determines the
|
|
440
441
|
output folder for the rendered image.
|
|
441
|
-
|
|
442
|
+
|
|
442
443
|
Only very preliminary support is provided for ground truth box rendering.
|
|
443
|
-
|
|
444
|
+
|
|
444
445
|
Returns the html info struct for this image in the format that's used for
|
|
445
446
|
write_html_image_list.
|
|
446
|
-
|
|
447
|
+
|
|
447
448
|
:meta private:
|
|
448
449
|
"""
|
|
449
450
|
|
|
@@ -451,7 +452,7 @@ def _render_bounding_boxes(
|
|
|
451
452
|
options = PostProcessingOptions()
|
|
452
453
|
|
|
453
454
|
image_full_path = None
|
|
454
|
-
|
|
455
|
+
|
|
455
456
|
if res in options.rendering_bypass_sets:
|
|
456
457
|
|
|
457
458
|
sample_name = res + '_' + path_utils.flatten_path(image_relative_path)
|
|
@@ -467,26 +468,26 @@ def _render_bounding_boxes(
|
|
|
467
468
|
# to just try/except on the image open.
|
|
468
469
|
try:
|
|
469
470
|
image = vis_utils.open_image(image_full_path)
|
|
470
|
-
except:
|
|
471
|
-
print('Warning: could not open image file {}'.format(image_full_path))
|
|
471
|
+
except Exception as e:
|
|
472
|
+
print('Warning: could not open image file {}: {}'.format(image_full_path,str(e)))
|
|
472
473
|
image = None
|
|
473
474
|
# return ''
|
|
474
|
-
|
|
475
|
+
|
|
475
476
|
# Render images to a flat folder
|
|
476
477
|
sample_name = res + '_' + path_utils.flatten_path(image_relative_path)
|
|
477
478
|
fullpath = os.path.join(options.output_dir, res, sample_name)
|
|
478
479
|
|
|
479
480
|
if image is not None:
|
|
480
|
-
|
|
481
|
+
|
|
481
482
|
original_size = image.size
|
|
482
|
-
|
|
483
|
+
|
|
483
484
|
# Resize the image if necessary
|
|
484
485
|
if options.viz_target_width is not None:
|
|
485
486
|
image = vis_utils.resize_image(image, options.viz_target_width)
|
|
486
|
-
|
|
487
|
+
|
|
487
488
|
# Render ground truth boxes if necessary
|
|
488
489
|
if ground_truth_boxes is not None and len(ground_truth_boxes) > 0:
|
|
489
|
-
|
|
490
|
+
|
|
490
491
|
# Create class labels like "gt_1" or "gt_27"
|
|
491
492
|
gt_classes = [0] * len(ground_truth_boxes)
|
|
492
493
|
label_map = {0:'ground truth'}
|
|
@@ -495,7 +496,7 @@ def _render_bounding_boxes(
|
|
|
495
496
|
vis_utils.render_db_bounding_boxes(ground_truth_boxes, gt_classes, image,
|
|
496
497
|
original_size=original_size,label_map=label_map,
|
|
497
498
|
thickness=4,expansion=4)
|
|
498
|
-
|
|
499
|
+
|
|
499
500
|
# Prepare per-category confidence thresholds
|
|
500
501
|
if isinstance(options.confidence_threshold,float):
|
|
501
502
|
rendering_confidence_threshold = options.confidence_threshold
|
|
@@ -507,7 +508,7 @@ def _render_bounding_boxes(
|
|
|
507
508
|
for category_id in category_ids:
|
|
508
509
|
rendering_confidence_threshold[category_id] = \
|
|
509
510
|
_get_threshold_for_category_id(category_id, options, detection_categories)
|
|
510
|
-
|
|
511
|
+
|
|
511
512
|
# Render detection boxes
|
|
512
513
|
vis_utils.render_detection_bounding_boxes(
|
|
513
514
|
detections, image,
|
|
@@ -517,7 +518,7 @@ def _render_bounding_boxes(
|
|
|
517
518
|
classification_confidence_threshold=options.classification_confidence_threshold,
|
|
518
519
|
thickness=options.line_thickness,
|
|
519
520
|
expansion=options.box_expansion)
|
|
520
|
-
|
|
521
|
+
|
|
521
522
|
try:
|
|
522
523
|
image.save(fullpath)
|
|
523
524
|
except OSError as e:
|
|
@@ -539,18 +540,18 @@ def _render_bounding_boxes(
|
|
|
539
540
|
'textStyle':\
|
|
540
541
|
'font-family:verdana,arial,calibri;font-size:80%;text-align:left;margin-top:20;margin-bottom:5'
|
|
541
542
|
}
|
|
542
|
-
|
|
543
|
+
|
|
543
544
|
# Optionally add links back to the original images
|
|
544
545
|
if options.link_images_to_originals and (image_full_path is not None):
|
|
545
|
-
|
|
546
|
+
|
|
546
547
|
# Handling special characters in links has been pushed down into
|
|
547
548
|
# write_html_image_list
|
|
548
549
|
#
|
|
549
550
|
# link_target = image_full_path.replace('\\','/')
|
|
550
551
|
# link_target = urllib.parse.quote(link_target)
|
|
551
552
|
link_target = image_full_path
|
|
552
|
-
info['linkTarget'] = link_target
|
|
553
|
-
|
|
553
|
+
info['linkTarget'] = link_target
|
|
554
|
+
|
|
554
555
|
return info
|
|
555
556
|
|
|
556
557
|
# ..._render_bounding_boxes
|
|
@@ -561,12 +562,12 @@ def _prepare_html_subpages(images_html, output_dir, options=None):
|
|
|
561
562
|
Write out a series of html image lists, e.g. the "detections" or "non-detections"
|
|
562
563
|
pages.
|
|
563
564
|
|
|
564
|
-
image_html is a dictionary mapping an html page name (e.g. "detections_animal") to
|
|
565
|
+
image_html is a dictionary mapping an html page name (e.g. "detections_animal") to
|
|
565
566
|
a list of image structs friendly to write_html_image_list.
|
|
566
|
-
|
|
567
|
+
|
|
567
568
|
Returns a dictionary mapping category names to image counts.
|
|
568
569
|
"""
|
|
569
|
-
|
|
570
|
+
|
|
570
571
|
if options is None:
|
|
571
572
|
options = PostProcessingOptions()
|
|
572
573
|
|
|
@@ -582,19 +583,20 @@ def _prepare_html_subpages(images_html, output_dir, options=None):
|
|
|
582
583
|
sorted_array = sorted(array, key=lambda x: x['filename'])
|
|
583
584
|
images_html_sorted[res] = sorted_array
|
|
584
585
|
images_html = images_html_sorted
|
|
585
|
-
|
|
586
|
+
|
|
586
587
|
# Optionally sort by confidence before writing to html
|
|
587
588
|
elif options.html_sort_order == 'confidence':
|
|
588
589
|
images_html_sorted = {}
|
|
589
590
|
for res, array in images_html.items():
|
|
590
|
-
|
|
591
|
+
|
|
591
592
|
if not all(['max_conf' in d for d in array]):
|
|
592
|
-
print("Warning: some elements in the {} page don't have confidence
|
|
593
|
+
print(f"Warning: some elements in the {res} page don't have confidence " + \
|
|
594
|
+
"values, can't sort by confidence")
|
|
593
595
|
else:
|
|
594
596
|
sorted_array = sorted(array, key=lambda x: x['max_conf'], reverse=True)
|
|
595
597
|
images_html_sorted[res] = sorted_array
|
|
596
598
|
images_html = images_html_sorted
|
|
597
|
-
|
|
599
|
+
|
|
598
600
|
else:
|
|
599
601
|
assert options.html_sort_order == 'random',\
|
|
600
602
|
'Unrecognized sort order {}'.format(options.html_sort_order)
|
|
@@ -603,15 +605,15 @@ def _prepare_html_subpages(images_html, output_dir, options=None):
|
|
|
603
605
|
sorted_array = random.sample(array,len(array))
|
|
604
606
|
images_html_sorted[res] = sorted_array
|
|
605
607
|
images_html = images_html_sorted
|
|
606
|
-
|
|
608
|
+
|
|
607
609
|
# Write the individual HTML files
|
|
608
610
|
for res, array in images_html.items():
|
|
609
|
-
|
|
610
|
-
html_image_list_options = {}
|
|
611
|
+
|
|
612
|
+
html_image_list_options = {}
|
|
611
613
|
html_image_list_options['maxFiguresPerHtmlFile'] = options.max_figures_per_html_file
|
|
612
614
|
html_image_list_options['headerHtml'] = '<h1>{}</h1>'.format(res.upper())
|
|
613
615
|
html_image_list_options['pageTitle'] = '{}'.format(res.lower())
|
|
614
|
-
|
|
616
|
+
|
|
615
617
|
# Don't write empty pages
|
|
616
618
|
if len(array) == 0:
|
|
617
619
|
continue
|
|
@@ -630,49 +632,49 @@ def _get_threshold_for_category_name(category_name,options):
|
|
|
630
632
|
"""
|
|
631
633
|
Determines the confidence threshold we should use for a specific category name.
|
|
632
634
|
"""
|
|
633
|
-
|
|
635
|
+
|
|
634
636
|
if isinstance(options.confidence_threshold,float):
|
|
635
637
|
return options.confidence_threshold
|
|
636
638
|
else:
|
|
637
639
|
assert isinstance(options.confidence_threshold,dict), \
|
|
638
640
|
'confidence_threshold must either be a float or a dict'
|
|
639
|
-
|
|
641
|
+
|
|
640
642
|
if category_name in options.confidence_threshold:
|
|
641
|
-
|
|
643
|
+
|
|
642
644
|
return options.confidence_threshold[category_name]
|
|
643
|
-
|
|
645
|
+
|
|
644
646
|
else:
|
|
645
647
|
assert 'default' in options.confidence_threshold, \
|
|
646
648
|
'category {} not in confidence_threshold dict, and no default supplied'.format(
|
|
647
649
|
category_name)
|
|
648
650
|
return options.confidence_threshold['default']
|
|
649
651
|
|
|
650
|
-
|
|
652
|
+
|
|
651
653
|
def _get_threshold_for_category_id(category_id,options,detection_categories):
|
|
652
654
|
"""
|
|
653
655
|
Determines the confidence threshold we should use for a specific category ID.
|
|
654
|
-
|
|
656
|
+
|
|
655
657
|
[detection_categories] is a dict mapping category IDs to names.
|
|
656
658
|
"""
|
|
657
|
-
|
|
658
|
-
if isinstance(options.confidence_threshold,float):
|
|
659
|
+
|
|
660
|
+
if isinstance(options.confidence_threshold,float):
|
|
659
661
|
return options.confidence_threshold
|
|
660
|
-
|
|
662
|
+
|
|
661
663
|
assert category_id in detection_categories, \
|
|
662
664
|
'Invalid category ID {}'.format(category_id)
|
|
663
|
-
|
|
665
|
+
|
|
664
666
|
category_name = detection_categories[category_id]
|
|
665
|
-
|
|
667
|
+
|
|
666
668
|
return _get_threshold_for_category_name(category_name,options)
|
|
667
|
-
|
|
668
|
-
|
|
669
|
+
|
|
670
|
+
|
|
669
671
|
def _get_positive_categories(detections,options,detection_categories):
|
|
670
672
|
"""
|
|
671
673
|
Gets a sorted list of unique categories (as string IDs) above the threshold for this image
|
|
672
|
-
|
|
674
|
+
|
|
673
675
|
[detection_categories] is a dict mapping category IDs to names.
|
|
674
676
|
"""
|
|
675
|
-
|
|
677
|
+
|
|
676
678
|
positive_categories = set()
|
|
677
679
|
for d in detections:
|
|
678
680
|
threshold = _get_threshold_for_category_id(d['category'], options, detection_categories)
|
|
@@ -685,8 +687,8 @@ def _has_positive_detection(detections,options,detection_categories):
|
|
|
685
687
|
"""
|
|
686
688
|
Determines whether any positive detections are present in the detection list
|
|
687
689
|
[detections].
|
|
688
|
-
"""
|
|
689
|
-
|
|
690
|
+
"""
|
|
691
|
+
|
|
690
692
|
found_positive_detection = False
|
|
691
693
|
for d in detections:
|
|
692
694
|
threshold = _get_threshold_for_category_id(d['category'], options, detection_categories)
|
|
@@ -694,51 +696,51 @@ def _has_positive_detection(detections,options,detection_categories):
|
|
|
694
696
|
found_positive_detection = True
|
|
695
697
|
break
|
|
696
698
|
return found_positive_detection
|
|
697
|
-
|
|
699
|
+
|
|
698
700
|
|
|
699
701
|
def _render_image_no_gt(file_info,
|
|
700
702
|
detection_categories_to_results_name,
|
|
701
703
|
detection_categories,
|
|
702
704
|
classification_categories,
|
|
703
705
|
options):
|
|
704
|
-
"""
|
|
706
|
+
r"""
|
|
705
707
|
Renders an image (with no ground truth information)
|
|
706
|
-
|
|
707
|
-
Returns a list of rendering structs, where the first item is a category (e.g. "detections_animal"),
|
|
708
|
+
|
|
709
|
+
Returns a list of rendering structs, where the first item is a category (e.g. "detections_animal"),
|
|
708
710
|
and the second is a dict of information needed for rendering. E.g.:
|
|
709
|
-
|
|
710
|
-
[['detections_animal',
|
|
711
|
+
|
|
712
|
+
[['detections_animal',
|
|
711
713
|
{
|
|
712
|
-
'filename': 'detections_animal/detections_animal_blah~01060415.JPG',
|
|
713
|
-
'title': '<b>Result type</b>: detections_animal,
|
|
714
|
+
'filename': 'detections_animal/detections_animal_blah~01060415.JPG',
|
|
715
|
+
'title': '<b>Result type</b>: detections_animal,
|
|
714
716
|
<b>Image</b>: blah\\01060415.JPG,
|
|
715
717
|
<b>Max conf</b>: 0.897',
|
|
716
718
|
'textStyle': 'font-family:verdana,arial,calibri;font-size:80%;text-align:left;margin-top:20;margin-bottom:5',
|
|
717
719
|
'linkTarget': 'full_path_to_%5C01060415.JPG'
|
|
718
720
|
}]]
|
|
719
|
-
|
|
721
|
+
|
|
720
722
|
When no classification data is present, this list will always be length-1. When
|
|
721
723
|
classification data is present, an image may appear in multiple categories.
|
|
722
|
-
|
|
724
|
+
|
|
723
725
|
Populates the 'max_conf' field of the first element of the list.
|
|
724
|
-
|
|
726
|
+
|
|
725
727
|
Returns None if there are any errors.
|
|
726
728
|
"""
|
|
727
|
-
|
|
729
|
+
|
|
728
730
|
image_relative_path = file_info['file']
|
|
729
|
-
|
|
731
|
+
|
|
730
732
|
# Useful debug snippet
|
|
731
733
|
#
|
|
732
734
|
# if 'filename' in image_relative_path:
|
|
733
735
|
# import pdb; pdb.set_trace()
|
|
734
|
-
|
|
736
|
+
|
|
735
737
|
max_conf = file_info['max_detection_conf']
|
|
736
738
|
detections = file_info['detections']
|
|
737
739
|
|
|
738
740
|
# Determine whether any positive detections are present (using a threshold that
|
|
739
741
|
# may vary by category)
|
|
740
742
|
found_positive_detection = _has_positive_detection(detections,options,detection_categories)
|
|
741
|
-
|
|
743
|
+
|
|
742
744
|
detection_status = DetectionStatus.DS_UNASSIGNED
|
|
743
745
|
if found_positive_detection:
|
|
744
746
|
detection_status = DetectionStatus.DS_POSITIVE
|
|
@@ -772,17 +774,17 @@ def _render_image_no_gt(file_info,
|
|
|
772
774
|
|
|
773
775
|
# Are there any bonus fields we need to include in each image header?
|
|
774
776
|
if options.additional_image_fields_to_display is not None:
|
|
775
|
-
|
|
777
|
+
|
|
776
778
|
for field_name in options.additional_image_fields_to_display:
|
|
777
|
-
|
|
779
|
+
|
|
778
780
|
if field_name in file_info:
|
|
779
|
-
|
|
781
|
+
|
|
780
782
|
field_value = file_info[field_name]
|
|
781
|
-
|
|
783
|
+
|
|
782
784
|
if (field_value is None) or \
|
|
783
785
|
(isinstance(field_value,float) and np.isnan(field_value)):
|
|
784
786
|
continue
|
|
785
|
-
|
|
787
|
+
|
|
786
788
|
# Optionally use a display name that's different from the field name
|
|
787
789
|
if isinstance(options.additional_image_fields_to_display,dict):
|
|
788
790
|
field_display_name = \
|
|
@@ -791,12 +793,12 @@ def _render_image_no_gt(file_info,
|
|
|
791
793
|
field_display_name = field_name
|
|
792
794
|
field_string = '<b>{}</b>: {}'.format(field_display_name,field_value)
|
|
793
795
|
display_name += ', {}'.format(field_string)
|
|
794
|
-
|
|
796
|
+
|
|
795
797
|
rendering_options = copy.copy(options)
|
|
796
798
|
if detection_status == DetectionStatus.DS_ALMOST:
|
|
797
799
|
rendering_options.confidence_threshold = \
|
|
798
800
|
rendering_options.almost_detection_confidence_threshold
|
|
799
|
-
|
|
801
|
+
|
|
800
802
|
rendered_image_html_info = _render_bounding_boxes(
|
|
801
803
|
image_base_dir=options.image_base_dir,
|
|
802
804
|
image_relative_path=image_relative_path,
|
|
@@ -815,9 +817,9 @@ def _render_image_no_gt(file_info,
|
|
|
815
817
|
image_result = [[res, rendered_image_html_info]]
|
|
816
818
|
classes_rendered_this_image = set()
|
|
817
819
|
max_conf = 0
|
|
818
|
-
|
|
820
|
+
|
|
819
821
|
for det in detections:
|
|
820
|
-
|
|
822
|
+
|
|
821
823
|
if det['conf'] > max_conf:
|
|
822
824
|
max_conf = det['conf']
|
|
823
825
|
|
|
@@ -827,7 +829,7 @@ def _render_image_no_gt(file_info,
|
|
|
827
829
|
_get_threshold_for_category_id(det['category'], options, detection_categories)
|
|
828
830
|
if det['conf'] < detection_threshold:
|
|
829
831
|
continue
|
|
830
|
-
|
|
832
|
+
|
|
831
833
|
if ('classifications' in det) and (len(det['classifications']) > 0) and \
|
|
832
834
|
(res != 'non_detections'):
|
|
833
835
|
|
|
@@ -837,14 +839,14 @@ def _render_image_no_gt(file_info,
|
|
|
837
839
|
top1_class_name = classification_categories[top1_class_id]
|
|
838
840
|
top1_class_score = classifications[0][1]
|
|
839
841
|
|
|
840
|
-
# If we either don't have a classification confidence threshold, or
|
|
842
|
+
# If we either don't have a classification confidence threshold, or
|
|
841
843
|
# we've met our classification confidence threshold
|
|
842
844
|
if (options.classification_confidence_threshold < 0) or \
|
|
843
845
|
(top1_class_score >= options.classification_confidence_threshold):
|
|
844
|
-
class_string = 'class_{}'.format(top1_class_name)
|
|
846
|
+
class_string = 'class_{}'.format(top1_class_name)
|
|
845
847
|
else:
|
|
846
848
|
class_string = 'class_unreliable'
|
|
847
|
-
|
|
849
|
+
|
|
848
850
|
if class_string not in classes_rendered_this_image:
|
|
849
851
|
image_result.append([class_string,
|
|
850
852
|
rendered_image_html_info])
|
|
@@ -855,13 +857,13 @@ def _render_image_no_gt(file_info,
|
|
|
855
857
|
# ...for each detection
|
|
856
858
|
|
|
857
859
|
image_result[0][1]['max_conf'] = max_conf
|
|
858
|
-
|
|
860
|
+
|
|
859
861
|
# ...if we got valid rendering info back from _render_bounding_boxes()
|
|
860
|
-
|
|
862
|
+
|
|
861
863
|
return image_result
|
|
862
864
|
|
|
863
865
|
# ...def _render_image_no_gt()
|
|
864
|
-
|
|
866
|
+
|
|
865
867
|
|
|
866
868
|
def _render_image_with_gt(file_info,ground_truth_indexed_db,
|
|
867
869
|
detection_categories,classification_categories,options):
|
|
@@ -869,7 +871,7 @@ def _render_image_with_gt(file_info,ground_truth_indexed_db,
|
|
|
869
871
|
Render an image with ground truth information. See _render_image_no_gt for return
|
|
870
872
|
data format.
|
|
871
873
|
"""
|
|
872
|
-
|
|
874
|
+
|
|
873
875
|
image_relative_path = file_info['file']
|
|
874
876
|
max_conf = file_info['max_detection_conf']
|
|
875
877
|
detections = file_info['detections']
|
|
@@ -890,7 +892,7 @@ def _render_image_with_gt(file_info,ground_truth_indexed_db,
|
|
|
890
892
|
ground_truth_box = [x for x in ann['bbox']]
|
|
891
893
|
ground_truth_box.append(ann['category_id'])
|
|
892
894
|
ground_truth_boxes.append(ground_truth_box)
|
|
893
|
-
|
|
895
|
+
|
|
894
896
|
gt_status = image['_detection_status']
|
|
895
897
|
|
|
896
898
|
gt_presence = bool(gt_status)
|
|
@@ -920,7 +922,7 @@ def _render_image_with_gt(file_info,ground_truth_indexed_db,
|
|
|
920
922
|
else:
|
|
921
923
|
res = 'tn'
|
|
922
924
|
|
|
923
|
-
display_name = '<b>Result type</b>: {}, <b>Presence</b>: {}, <b>Class</b>: {}, <b>Max conf</b>: {:0.3f}%, <b>Image</b>: {}'.format(
|
|
925
|
+
display_name = '<b>Result type</b>: {}, <b>Presence</b>: {}, <b>Class</b>: {}, <b>Max conf</b>: {:0.3f}%, <b>Image</b>: {}'.format( # noqa
|
|
924
926
|
res.upper(), str(gt_presence), gt_class_summary,
|
|
925
927
|
max_conf * 100, image_relative_path)
|
|
926
928
|
|
|
@@ -945,7 +947,7 @@ def _render_image_with_gt(file_info,ground_truth_indexed_db,
|
|
|
945
947
|
|
|
946
948
|
# ...def _render_image_with_gt()
|
|
947
949
|
|
|
948
|
-
|
|
950
|
+
|
|
949
951
|
#%% Main function
|
|
950
952
|
|
|
951
953
|
def process_batch_results(options):
|
|
@@ -960,15 +962,15 @@ def process_batch_results(options):
|
|
|
960
962
|
truth)
|
|
961
963
|
|
|
962
964
|
Ground truth, if available, must be in COCO Camera Traps format:
|
|
963
|
-
|
|
965
|
+
|
|
964
966
|
https://github.com/agentmorris/MegaDetector/blob/main/megadetector/data_management/README.md#coco-camera-traps-format
|
|
965
967
|
|
|
966
968
|
Args:
|
|
967
969
|
options (PostProcessingOptions): everything we need to render a preview/analysis for
|
|
968
970
|
this set of results; see the PostProcessingOptions class for details.
|
|
969
|
-
|
|
971
|
+
|
|
970
972
|
Returns:
|
|
971
|
-
PostProcessingResults: information about the results/preview, most importantly the
|
|
973
|
+
PostProcessingResults: information about the results/preview, most importantly the
|
|
972
974
|
HTML filename of the output. See the PostProcessingResults class for details.
|
|
973
975
|
"""
|
|
974
976
|
ppresults = PostProcessingResults()
|
|
@@ -990,7 +992,7 @@ def process_batch_results(options):
|
|
|
990
992
|
if (options.ground_truth_json_file is not None) and (len(options.ground_truth_json_file) > 0):
|
|
991
993
|
assert (options.confidence_threshold is None) or (isinstance(options.confidence_threshold,float)), \
|
|
992
994
|
'Variable confidence thresholds are not supported when supplying ground truth'
|
|
993
|
-
|
|
995
|
+
|
|
994
996
|
if (options.ground_truth_json_file is not None) and (len(options.ground_truth_json_file) > 0):
|
|
995
997
|
|
|
996
998
|
if options.separate_detections_by_category:
|
|
@@ -999,13 +1001,16 @@ def process_batch_results(options):
|
|
|
999
1001
|
options.separate_detections_by_category = False
|
|
1000
1002
|
|
|
1001
1003
|
ground_truth_indexed_db = IndexedJsonDb(
|
|
1002
|
-
options.ground_truth_json_file,
|
|
1004
|
+
options.ground_truth_json_file,
|
|
1005
|
+
b_normalize_paths=True,
|
|
1003
1006
|
filename_replacements=options.ground_truth_filename_replacements)
|
|
1004
1007
|
|
|
1005
1008
|
# Mark images in the ground truth as positive or negative
|
|
1006
1009
|
n_negative, n_positive, n_unknown, n_ambiguous = _mark_detection_status(
|
|
1007
|
-
ground_truth_indexed_db,
|
|
1010
|
+
ground_truth_indexed_db,
|
|
1011
|
+
negative_classes=options.negative_classes,
|
|
1008
1012
|
unknown_classes=options.unlabeled_classes)
|
|
1013
|
+
|
|
1009
1014
|
print(f'Finished loading and indexing ground truth: {n_negative} '
|
|
1010
1015
|
f'negative, {n_positive} positive, {n_unknown} unknown, '
|
|
1011
1016
|
f'{n_ambiguous} ambiguous')
|
|
@@ -1027,7 +1032,7 @@ def process_batch_results(options):
|
|
|
1027
1032
|
options.md_results_file, force_forward_slashes=True,
|
|
1028
1033
|
filename_replacements=options.api_output_filename_replacements)
|
|
1029
1034
|
ppresults.api_detection_results = detections_df
|
|
1030
|
-
ppresults.api_other_fields = other_fields
|
|
1035
|
+
ppresults.api_other_fields = other_fields
|
|
1031
1036
|
|
|
1032
1037
|
else:
|
|
1033
1038
|
print('Bypassing detection results loading...')
|
|
@@ -1036,13 +1041,13 @@ def process_batch_results(options):
|
|
|
1036
1041
|
other_fields = options.api_other_fields
|
|
1037
1042
|
|
|
1038
1043
|
# Determine confidence thresholds if necessary
|
|
1039
|
-
|
|
1044
|
+
|
|
1040
1045
|
if options.confidence_threshold is None:
|
|
1041
1046
|
options.confidence_threshold = \
|
|
1042
1047
|
get_typical_confidence_threshold_from_results(other_fields)
|
|
1043
1048
|
print('Choosing default confidence threshold of {} based on MD version'.format(
|
|
1044
|
-
options.confidence_threshold))
|
|
1045
|
-
|
|
1049
|
+
options.confidence_threshold))
|
|
1050
|
+
|
|
1046
1051
|
if options.almost_detection_confidence_threshold is None and options.include_almost_detections:
|
|
1047
1052
|
assert isinstance(options.confidence_threshold,float), \
|
|
1048
1053
|
'If you are using a dictionary of confidence thresholds and almost-detections are enabled, ' + \
|
|
@@ -1050,7 +1055,7 @@ def process_batch_results(options):
|
|
|
1050
1055
|
options.almost_detection_confidence_threshold = options.confidence_threshold - 0.05
|
|
1051
1056
|
if options.almost_detection_confidence_threshold < 0:
|
|
1052
1057
|
options.almost_detection_confidence_threshold = 0
|
|
1053
|
-
|
|
1058
|
+
|
|
1054
1059
|
# Remove rows with inference failures (typically due to corrupt images)
|
|
1055
1060
|
n_failures = 0
|
|
1056
1061
|
if 'failure' in detections_df.columns:
|
|
@@ -1059,11 +1064,11 @@ def process_batch_results(options):
|
|
|
1059
1064
|
# Explicitly forcing a copy() operation here to suppress "trying to be set
|
|
1060
1065
|
# on a copy" warnings (and associated risks) below.
|
|
1061
1066
|
detections_df = detections_df[detections_df['failure'].isna()].copy()
|
|
1062
|
-
|
|
1067
|
+
|
|
1063
1068
|
assert other_fields is not None
|
|
1064
1069
|
|
|
1065
1070
|
detection_categories = other_fields['detection_categories']
|
|
1066
|
-
|
|
1071
|
+
|
|
1067
1072
|
# Convert keys and values to lowercase
|
|
1068
1073
|
classification_categories = other_fields.get('classification_categories', {})
|
|
1069
1074
|
if classification_categories is not None:
|
|
@@ -1075,19 +1080,19 @@ def process_batch_results(options):
|
|
|
1075
1080
|
# Count detections and almost-detections for reporting purposes
|
|
1076
1081
|
n_positives = 0
|
|
1077
1082
|
n_almosts = 0
|
|
1078
|
-
|
|
1083
|
+
|
|
1079
1084
|
print('Assigning images to rendering categories')
|
|
1080
|
-
|
|
1085
|
+
|
|
1081
1086
|
for i_row,row in tqdm(detections_df.iterrows(),total=len(detections_df)):
|
|
1082
|
-
|
|
1087
|
+
|
|
1083
1088
|
detections = row['detections']
|
|
1084
1089
|
max_conf = row['max_detection_conf']
|
|
1085
1090
|
if _has_positive_detection(detections, options, detection_categories):
|
|
1086
1091
|
n_positives += 1
|
|
1087
1092
|
elif (options.almost_detection_confidence_threshold is not None) and \
|
|
1088
1093
|
(max_conf >= options.almost_detection_confidence_threshold):
|
|
1089
|
-
n_almosts += 1
|
|
1090
|
-
|
|
1094
|
+
n_almosts += 1
|
|
1095
|
+
|
|
1091
1096
|
print(f'Finished loading and preprocessing {len(detections_df)} rows '
|
|
1092
1097
|
f'from detector output, predicted {n_positives} positives.')
|
|
1093
1098
|
|
|
@@ -1106,18 +1111,18 @@ def process_batch_results(options):
|
|
|
1106
1111
|
job_name_string = 'unknown'
|
|
1107
1112
|
else:
|
|
1108
1113
|
job_name_string = os.path.basename(options.md_results_file)
|
|
1109
|
-
|
|
1114
|
+
|
|
1110
1115
|
if options.model_version_string is not None:
|
|
1111
1116
|
model_version_string = options.model_version_string
|
|
1112
1117
|
else:
|
|
1113
|
-
|
|
1118
|
+
|
|
1114
1119
|
if 'info' not in other_fields or 'detector' not in other_fields['info']:
|
|
1115
1120
|
print('No model metadata supplied, assuming MDv4')
|
|
1116
1121
|
model_version_string = 'MDv4 (assumed)'
|
|
1117
|
-
else:
|
|
1122
|
+
else:
|
|
1118
1123
|
model_version_string = other_fields['info']['detector']
|
|
1119
|
-
|
|
1120
|
-
|
|
1124
|
+
|
|
1125
|
+
|
|
1121
1126
|
##%% If we have ground truth, remove images we can't match to ground truth
|
|
1122
1127
|
|
|
1123
1128
|
if ground_truth_indexed_db is not None:
|
|
@@ -1147,7 +1152,7 @@ def process_batch_results(options):
|
|
|
1147
1152
|
|
|
1148
1153
|
output_html_file = ''
|
|
1149
1154
|
|
|
1150
|
-
style_header = """<head>
|
|
1155
|
+
style_header = """<head>
|
|
1151
1156
|
<title>Detection results preview</title>
|
|
1152
1157
|
<style type="text/css">
|
|
1153
1158
|
a { text-decoration: none; }
|
|
@@ -1176,9 +1181,9 @@ def process_batch_results(options):
|
|
|
1176
1181
|
|
|
1177
1182
|
n_positive = 0
|
|
1178
1183
|
n_negative = 0
|
|
1179
|
-
|
|
1184
|
+
|
|
1180
1185
|
for i_detection, fn in enumerate(detector_files):
|
|
1181
|
-
|
|
1186
|
+
|
|
1182
1187
|
image_id = ground_truth_indexed_db.filename_to_id[fn]
|
|
1183
1188
|
image = ground_truth_indexed_db.image_id_to_image[image_id]
|
|
1184
1189
|
detection_status = image['_detection_status']
|
|
@@ -1194,7 +1199,7 @@ def process_batch_results(options):
|
|
|
1194
1199
|
|
|
1195
1200
|
print('Of {} ground truth values, found {} positives and {} negatives'.format(
|
|
1196
1201
|
len(detections_df),n_positive,n_negative))
|
|
1197
|
-
|
|
1202
|
+
|
|
1198
1203
|
# Don't include ambiguous/unknown ground truth in precision/recall analysis
|
|
1199
1204
|
b_valid_ground_truth = gt_detections >= 0.0
|
|
1200
1205
|
|
|
@@ -1221,16 +1226,16 @@ def process_batch_results(options):
|
|
|
1221
1226
|
|
|
1222
1227
|
# Thresholds go up throughout precisions/recalls/thresholds; find the last
|
|
1223
1228
|
# value where recall is at or above target. That's our precision @ target recall.
|
|
1224
|
-
|
|
1229
|
+
|
|
1225
1230
|
i_above_target_recall = (np.where(recalls >= options.target_recall))
|
|
1226
|
-
|
|
1227
|
-
# np.where returns a tuple of arrays, but in this syntax where we're
|
|
1231
|
+
|
|
1232
|
+
# np.where returns a tuple of arrays, but in this syntax where we're
|
|
1228
1233
|
# comparing an array with a scalar, there will only be one element.
|
|
1229
1234
|
assert len (i_above_target_recall) == 1
|
|
1230
|
-
|
|
1235
|
+
|
|
1231
1236
|
# Convert back to a list
|
|
1232
1237
|
i_above_target_recall = i_above_target_recall[0].tolist()
|
|
1233
|
-
|
|
1238
|
+
|
|
1234
1239
|
if len(i_above_target_recall) == 0:
|
|
1235
1240
|
precision_at_target_recall = 0.0
|
|
1236
1241
|
else:
|
|
@@ -1346,7 +1351,7 @@ def process_batch_results(options):
|
|
|
1346
1351
|
# Prepend class name on each line and add to the top
|
|
1347
1352
|
cm_str_lines = [' ' * 16 + ' '.join(classname_headers)]
|
|
1348
1353
|
cm_str_lines += ['{:>15}'.format(cn[:15]) + ' ' + cm_line for cn, cm_line in \
|
|
1349
|
-
zip(classname_list, cm_str.splitlines())]
|
|
1354
|
+
zip(classname_list, cm_str.splitlines(), strict=True)]
|
|
1350
1355
|
|
|
1351
1356
|
# Print formatted confusion matrix
|
|
1352
1357
|
if False:
|
|
@@ -1387,7 +1392,7 @@ def process_batch_results(options):
|
|
|
1387
1392
|
t = 'Precision-Recall curve: AP={:0.1%}, P@{:0.1%}={:0.1%}'.format(
|
|
1388
1393
|
average_precision, options.target_recall, precision_at_target_recall)
|
|
1389
1394
|
fig = plot_utils.plot_precision_recall_curve(precisions, recalls, t)
|
|
1390
|
-
|
|
1395
|
+
|
|
1391
1396
|
pr_figure_relative_filename = 'prec_recall.png'
|
|
1392
1397
|
pr_figure_filename = os.path.join(output_dir, pr_figure_relative_filename)
|
|
1393
1398
|
fig.savefig(pr_figure_filename)
|
|
@@ -1402,7 +1407,7 @@ def process_batch_results(options):
|
|
|
1402
1407
|
# Accumulate html image structs (in the format expected by write_html_image_lists)
|
|
1403
1408
|
# for each category, e.g. 'tp', 'fp', ..., 'class_bird', ...
|
|
1404
1409
|
images_html = collections.defaultdict(list)
|
|
1405
|
-
|
|
1410
|
+
|
|
1406
1411
|
# Add default entries by accessing them for the first time
|
|
1407
1412
|
[images_html[res] for res in ['tp', 'tpc', 'tpi', 'fp', 'tn', 'fn']]
|
|
1408
1413
|
for res in images_html.keys():
|
|
@@ -1426,28 +1431,35 @@ def process_batch_results(options):
|
|
|
1426
1431
|
|
|
1427
1432
|
start_time = time.time()
|
|
1428
1433
|
if options.parallelize_rendering:
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
pool = ThreadPool(options.parallelize_rendering_n_cores)
|
|
1437
|
-
worker_string = 'threads'
|
|
1434
|
+
pool = None
|
|
1435
|
+
try:
|
|
1436
|
+
if options.parallelize_rendering_n_cores is None:
|
|
1437
|
+
if options.parallelize_rendering_with_threads:
|
|
1438
|
+
pool = ThreadPool()
|
|
1439
|
+
else:
|
|
1440
|
+
pool = Pool()
|
|
1438
1441
|
else:
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1442
|
+
if options.parallelize_rendering_with_threads:
|
|
1443
|
+
pool = ThreadPool(options.parallelize_rendering_n_cores)
|
|
1444
|
+
worker_string = 'threads'
|
|
1445
|
+
else:
|
|
1446
|
+
pool = Pool(options.parallelize_rendering_n_cores)
|
|
1447
|
+
worker_string = 'processes'
|
|
1448
|
+
print('Rendering images with {} {}'.format(options.parallelize_rendering_n_cores,
|
|
1449
|
+
worker_string))
|
|
1450
|
+
|
|
1451
|
+
rendering_results = list(tqdm(pool.imap(
|
|
1452
|
+
partial(_render_image_with_gt,
|
|
1453
|
+
ground_truth_indexed_db=ground_truth_indexed_db,
|
|
1454
|
+
detection_categories=detection_categories,
|
|
1455
|
+
classification_categories=classification_categories,
|
|
1456
|
+
options=options),
|
|
1457
|
+
files_to_render), total=len(files_to_render)))
|
|
1458
|
+
finally:
|
|
1459
|
+
if pool is not None:
|
|
1460
|
+
pool.close()
|
|
1461
|
+
pool.join()
|
|
1462
|
+
print("Pool closed and joined for GT rendering")
|
|
1451
1463
|
else:
|
|
1452
1464
|
for file_info in tqdm(files_to_render):
|
|
1453
1465
|
rendering_results.append(_render_image_with_gt(
|
|
@@ -1488,19 +1500,19 @@ def process_batch_results(options):
|
|
|
1488
1500
|
confidence_threshold_string = '{:.2%}'.format(options.confidence_threshold)
|
|
1489
1501
|
else:
|
|
1490
1502
|
confidence_threshold_string = str(options.confidence_threshold)
|
|
1491
|
-
|
|
1492
|
-
index_page = """<html>
|
|
1503
|
+
|
|
1504
|
+
index_page = """<html>
|
|
1493
1505
|
{}
|
|
1494
1506
|
<body>
|
|
1495
1507
|
<h2>Evaluation</h2>
|
|
1496
1508
|
|
|
1497
1509
|
<h3>Job metadata</h3>
|
|
1498
|
-
|
|
1510
|
+
|
|
1499
1511
|
<div class="contentdiv">
|
|
1500
1512
|
<p>Job name: {}<br/>
|
|
1501
1513
|
<p>Model version: {}</p>
|
|
1502
1514
|
</div>
|
|
1503
|
-
|
|
1515
|
+
|
|
1504
1516
|
<h3>Sample images</h3>
|
|
1505
1517
|
<div class="contentdiv">
|
|
1506
1518
|
<p>A sample of {} images, annotated with detections above confidence {}.</p>
|
|
@@ -1576,12 +1588,12 @@ def process_batch_results(options):
|
|
|
1576
1588
|
# Write custom footer if it was provided
|
|
1577
1589
|
if (options.footer_text is not None) and (len(options.footer_text) > 0):
|
|
1578
1590
|
index_page += '{}\n'.format(options.footer_text)
|
|
1579
|
-
|
|
1591
|
+
|
|
1580
1592
|
# Close open html tags
|
|
1581
1593
|
index_page += '\n</body></html>\n'
|
|
1582
|
-
|
|
1594
|
+
|
|
1583
1595
|
output_html_file = os.path.join(output_dir, 'index.html')
|
|
1584
|
-
with open(output_html_file, 'w',
|
|
1596
|
+
with open(output_html_file, 'w',
|
|
1585
1597
|
encoding=options.output_html_encoding) as f:
|
|
1586
1598
|
f.write(index_page)
|
|
1587
1599
|
|
|
@@ -1599,34 +1611,34 @@ def process_batch_results(options):
|
|
|
1599
1611
|
# Accumulate html image structs (in the format expected by write_html_image_list)
|
|
1600
1612
|
# for each category
|
|
1601
1613
|
images_html = collections.defaultdict(list)
|
|
1602
|
-
|
|
1614
|
+
|
|
1603
1615
|
# Add default entries by accessing them for the first time
|
|
1604
1616
|
|
|
1605
|
-
# Maps sorted tuples of detection category IDs (string ints) - e.g. ("1"), ("1", "4", "7") - to
|
|
1617
|
+
# Maps sorted tuples of detection category IDs (string ints) - e.g. ("1"), ("1", "4", "7") - to
|
|
1606
1618
|
# result set names, e.g. "detections_human", "detections_cat_truck".
|
|
1607
1619
|
detection_categories_to_results_name = {}
|
|
1608
|
-
|
|
1620
|
+
|
|
1609
1621
|
# Keep track of which categories are single-class (e.g. "animal") and which are
|
|
1610
1622
|
# combinations (e.g. "animal_vehicle")
|
|
1611
1623
|
detection_categories_to_category_count = {}
|
|
1612
|
-
|
|
1624
|
+
|
|
1613
1625
|
# For the creation of a "non-detections" category
|
|
1614
1626
|
images_html['non_detections']
|
|
1615
1627
|
detection_categories_to_category_count['non_detections'] = 0
|
|
1616
|
-
|
|
1617
|
-
|
|
1628
|
+
|
|
1629
|
+
|
|
1618
1630
|
if not options.separate_detections_by_category:
|
|
1619
1631
|
# For the creation of a "detections" category
|
|
1620
1632
|
images_html['detections']
|
|
1621
|
-
detection_categories_to_category_count['detections'] = 0
|
|
1633
|
+
detection_categories_to_category_count['detections'] = 0
|
|
1622
1634
|
else:
|
|
1623
1635
|
# Add a set of results for each category and combination of categories, e.g.
|
|
1624
1636
|
# "detections_animal_vehicle". When we're using this script for non-MegaDetector
|
|
1625
1637
|
# results, this can generate lots of categories, e.g. detections_bear_bird_cat_dog_pig.
|
|
1626
1638
|
# We'll keep that huge set of combinations in this map, but we'll only write
|
|
1627
|
-
# out links for the ones that are non-empty.
|
|
1639
|
+
# out links for the ones that are non-empty.
|
|
1628
1640
|
used_combinations = set()
|
|
1629
|
-
|
|
1641
|
+
|
|
1630
1642
|
# row = images_to_visualize.iloc[0]
|
|
1631
1643
|
for i_row, row in images_to_visualize.iterrows():
|
|
1632
1644
|
detections_this_row = row['detections']
|
|
@@ -1639,7 +1651,7 @@ def process_batch_results(options):
|
|
|
1639
1651
|
continue
|
|
1640
1652
|
sorted_categories_this_row = tuple(sorted(above_threshold_category_ids_this_row))
|
|
1641
1653
|
used_combinations.add(sorted_categories_this_row)
|
|
1642
|
-
|
|
1654
|
+
|
|
1643
1655
|
for sorted_subset in used_combinations:
|
|
1644
1656
|
assert len(sorted_subset) > 0
|
|
1645
1657
|
results_name = 'detections'
|
|
@@ -1647,7 +1659,7 @@ def process_batch_results(options):
|
|
|
1647
1659
|
results_name = results_name + '_' + detection_categories[category_id]
|
|
1648
1660
|
images_html[results_name]
|
|
1649
1661
|
detection_categories_to_results_name[sorted_subset] = results_name
|
|
1650
|
-
detection_categories_to_category_count[results_name] = len(sorted_subset)
|
|
1662
|
+
detection_categories_to_category_count[results_name] = len(sorted_subset)
|
|
1651
1663
|
|
|
1652
1664
|
if options.include_almost_detections:
|
|
1653
1665
|
images_html['almost_detections']
|
|
@@ -1658,7 +1670,7 @@ def process_batch_results(options):
|
|
|
1658
1670
|
os.makedirs(os.path.join(output_dir, res), exist_ok=True)
|
|
1659
1671
|
|
|
1660
1672
|
image_count = len(images_to_visualize)
|
|
1661
|
-
|
|
1673
|
+
|
|
1662
1674
|
# Each element will be a list of 2-tuples, with elements [collection name,html info struct]
|
|
1663
1675
|
rendering_results = []
|
|
1664
1676
|
|
|
@@ -1671,38 +1683,44 @@ def process_batch_results(options):
|
|
|
1671
1683
|
for _, row in images_to_visualize.iterrows():
|
|
1672
1684
|
|
|
1673
1685
|
assert isinstance(row['detections'],list)
|
|
1674
|
-
|
|
1686
|
+
|
|
1675
1687
|
# Filenames should already have been normalized to either '/' or '\'
|
|
1676
1688
|
files_to_render.append(row.to_dict())
|
|
1677
1689
|
|
|
1678
1690
|
start_time = time.time()
|
|
1679
1691
|
if options.parallelize_rendering:
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
if options.
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
if options.parallelize_rendering_with_threads:
|
|
1688
|
-
pool = ThreadPool(options.parallelize_rendering_n_cores)
|
|
1689
|
-
worker_string = 'threads'
|
|
1692
|
+
pool = None
|
|
1693
|
+
try:
|
|
1694
|
+
if options.parallelize_rendering_n_cores is None:
|
|
1695
|
+
if options.parallelize_rendering_with_threads:
|
|
1696
|
+
pool = ThreadPool()
|
|
1697
|
+
else:
|
|
1698
|
+
pool = Pool()
|
|
1690
1699
|
else:
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1700
|
+
if options.parallelize_rendering_with_threads:
|
|
1701
|
+
pool = ThreadPool(options.parallelize_rendering_n_cores)
|
|
1702
|
+
worker_string = 'threads'
|
|
1703
|
+
else:
|
|
1704
|
+
pool = Pool(options.parallelize_rendering_n_cores)
|
|
1705
|
+
worker_string = 'processes'
|
|
1706
|
+
print('Rendering images with {} {}'.format(options.parallelize_rendering_n_cores,
|
|
1707
|
+
worker_string))
|
|
1708
|
+
|
|
1709
|
+
# _render_image_no_gt(file_info,detection_categories_to_results_name,
|
|
1710
|
+
# detection_categories,classification_categories)
|
|
1711
|
+
|
|
1712
|
+
rendering_results = list(tqdm(pool.imap(
|
|
1713
|
+
partial(_render_image_no_gt,
|
|
1714
|
+
detection_categories_to_results_name=detection_categories_to_results_name,
|
|
1715
|
+
detection_categories=detection_categories,
|
|
1716
|
+
classification_categories=classification_categories,
|
|
1717
|
+
options=options),
|
|
1718
|
+
files_to_render), total=len(files_to_render)))
|
|
1719
|
+
finally:
|
|
1720
|
+
if pool is not None:
|
|
1721
|
+
pool.close()
|
|
1722
|
+
pool.join()
|
|
1723
|
+
print("Pool closed and joined for non-GT rendering")
|
|
1706
1724
|
else:
|
|
1707
1725
|
for file_info in tqdm(files_to_render):
|
|
1708
1726
|
rendering_result = _render_image_no_gt(file_info,
|
|
@@ -1711,12 +1729,12 @@ def process_batch_results(options):
|
|
|
1711
1729
|
classification_categories,
|
|
1712
1730
|
options=options)
|
|
1713
1731
|
rendering_results.append(rendering_result)
|
|
1714
|
-
|
|
1715
|
-
elapsed = time.time() - start_time
|
|
1716
|
-
|
|
1732
|
+
|
|
1733
|
+
elapsed = time.time() - start_time
|
|
1734
|
+
|
|
1717
1735
|
# Do we have classification results in addition to detection results?
|
|
1718
1736
|
has_classification_info = False
|
|
1719
|
-
|
|
1737
|
+
|
|
1720
1738
|
# Map all the rendering results in the list rendering_results into the
|
|
1721
1739
|
# dictionary images_html
|
|
1722
1740
|
image_rendered_count = 0
|
|
@@ -1731,7 +1749,7 @@ def process_batch_results(options):
|
|
|
1731
1749
|
|
|
1732
1750
|
# Prepare the individual html image files
|
|
1733
1751
|
image_counts = _prepare_html_subpages(images_html, output_dir, options)
|
|
1734
|
-
|
|
1752
|
+
|
|
1735
1753
|
if image_rendered_count == 0:
|
|
1736
1754
|
seconds_per_image = 0.0
|
|
1737
1755
|
else:
|
|
@@ -1744,7 +1762,7 @@ def process_batch_results(options):
|
|
|
1744
1762
|
# Write index.html
|
|
1745
1763
|
|
|
1746
1764
|
# We can't just sum these, because image_counts includes images in both their
|
|
1747
|
-
# detection and classification classes
|
|
1765
|
+
# detection and classification classes
|
|
1748
1766
|
total_images = 0
|
|
1749
1767
|
for k in image_counts.keys():
|
|
1750
1768
|
v = image_counts[k]
|
|
@@ -1754,7 +1772,7 @@ def process_batch_results(options):
|
|
|
1754
1772
|
|
|
1755
1773
|
if total_images != image_count:
|
|
1756
1774
|
print('Warning, missing images: image_count is {}, total_images is {}'.format(total_images,image_count))
|
|
1757
|
-
|
|
1775
|
+
|
|
1758
1776
|
almost_detection_string = ''
|
|
1759
1777
|
if options.include_almost_detections:
|
|
1760
1778
|
almost_detection_string = ' (“almost detection” threshold at {:.1%})'.format(
|
|
@@ -1765,15 +1783,15 @@ def process_batch_results(options):
|
|
|
1765
1783
|
confidence_threshold_string = '{:.2%}'.format(options.confidence_threshold)
|
|
1766
1784
|
else:
|
|
1767
1785
|
confidence_threshold_string = str(options.confidence_threshold)
|
|
1768
|
-
|
|
1786
|
+
|
|
1769
1787
|
index_page = """<html>\n{}\n<body>\n
|
|
1770
1788
|
<h2>Visualization of results for {}</h2>\n
|
|
1771
1789
|
<p>A sample of {} images (of {} total)FAILURE_PLACEHOLDER, annotated with detections above confidence {}{}.</p>\n
|
|
1772
|
-
|
|
1790
|
+
|
|
1773
1791
|
<div class="contentdiv">
|
|
1774
1792
|
<p>Model version: {}</p>
|
|
1775
1793
|
</div>
|
|
1776
|
-
|
|
1794
|
+
|
|
1777
1795
|
<h3>Detection results</h3>\n
|
|
1778
1796
|
<div class="contentdiv">\n""".format(
|
|
1779
1797
|
style_header, job_name_string, image_count, len(detections_df), confidence_threshold_string,
|
|
@@ -1781,9 +1799,9 @@ def process_batch_results(options):
|
|
|
1781
1799
|
|
|
1782
1800
|
failure_string = ''
|
|
1783
1801
|
if n_failures is not None:
|
|
1784
|
-
failure_string = ' ({} failures)'.format(n_failures)
|
|
1802
|
+
failure_string = ' ({} failures)'.format(n_failures)
|
|
1785
1803
|
index_page = index_page.replace('FAILURE_PLACEHOLDER',failure_string)
|
|
1786
|
-
|
|
1804
|
+
|
|
1787
1805
|
def result_set_name_to_friendly_name(result_set_name):
|
|
1788
1806
|
friendly_name = ''
|
|
1789
1807
|
friendly_name = result_set_name.replace('_','-')
|
|
@@ -1793,7 +1811,7 @@ def process_batch_results(options):
|
|
|
1793
1811
|
return friendly_name
|
|
1794
1812
|
|
|
1795
1813
|
sorted_result_set_names = sorted(list(images_html.keys()))
|
|
1796
|
-
|
|
1814
|
+
|
|
1797
1815
|
result_set_name_to_count = {}
|
|
1798
1816
|
for result_set_name in sorted_result_set_names:
|
|
1799
1817
|
image_count = image_counts[result_set_name]
|
|
@@ -1801,7 +1819,7 @@ def process_batch_results(options):
|
|
|
1801
1819
|
sorted_result_set_names = sorted(sorted_result_set_names,
|
|
1802
1820
|
key=lambda x: result_set_name_to_count[x],
|
|
1803
1821
|
reverse=True)
|
|
1804
|
-
|
|
1822
|
+
|
|
1805
1823
|
for result_set_name in sorted_result_set_names:
|
|
1806
1824
|
|
|
1807
1825
|
# Don't print classification classes here; we'll do that later with a slightly
|
|
@@ -1812,17 +1830,17 @@ def process_batch_results(options):
|
|
|
1812
1830
|
filename = result_set_name + '.html'
|
|
1813
1831
|
label = result_set_name_to_friendly_name(result_set_name)
|
|
1814
1832
|
image_count = image_counts[result_set_name]
|
|
1815
|
-
|
|
1833
|
+
|
|
1816
1834
|
# Don't include line items for empty multi-category pages
|
|
1817
1835
|
if image_count == 0 and \
|
|
1818
1836
|
detection_categories_to_category_count[result_set_name] > 1:
|
|
1819
1837
|
continue
|
|
1820
|
-
|
|
1838
|
+
|
|
1821
1839
|
if total_images == 0:
|
|
1822
1840
|
image_fraction = -1
|
|
1823
1841
|
else:
|
|
1824
1842
|
image_fraction = image_count / total_images
|
|
1825
|
-
|
|
1843
|
+
|
|
1826
1844
|
# Write the line item for this category, including a link only if the
|
|
1827
1845
|
# category is non-empty
|
|
1828
1846
|
if image_count == 0:
|
|
@@ -1831,17 +1849,17 @@ def process_batch_results(options):
|
|
|
1831
1849
|
else:
|
|
1832
1850
|
index_page += '<a href="{}">{}</a> ({}, {:.1%})<br/>\n'.format(
|
|
1833
1851
|
filename,label,image_count,image_fraction)
|
|
1834
|
-
|
|
1852
|
+
|
|
1835
1853
|
# ...for each result set
|
|
1836
|
-
|
|
1854
|
+
|
|
1837
1855
|
index_page += '</div>\n'
|
|
1838
1856
|
|
|
1839
1857
|
# If classification information is present and we're supposed to create
|
|
1840
1858
|
# a summary of classifications, we'll put it here
|
|
1841
1859
|
category_count_footer = None
|
|
1842
|
-
|
|
1860
|
+
|
|
1843
1861
|
if has_classification_info:
|
|
1844
|
-
|
|
1862
|
+
|
|
1845
1863
|
index_page += '<h3>Species classification results</h3>'
|
|
1846
1864
|
index_page += '<p>The same image might appear under multiple classes ' + \
|
|
1847
1865
|
'if multiple species were detected.</p>\n'
|
|
@@ -1855,12 +1873,12 @@ def process_batch_results(options):
|
|
|
1855
1873
|
class_names.append('unreliable')
|
|
1856
1874
|
|
|
1857
1875
|
if options.sort_classification_results_by_count:
|
|
1858
|
-
class_name_to_count = {}
|
|
1876
|
+
class_name_to_count = {}
|
|
1859
1877
|
for cname in class_names:
|
|
1860
1878
|
ccount = len(images_html['class_{}'.format(cname)])
|
|
1861
1879
|
class_name_to_count[cname] = ccount
|
|
1862
|
-
class_names = sorted(class_names,key=lambda x: class_name_to_count[x],reverse=True)
|
|
1863
|
-
|
|
1880
|
+
class_names = sorted(class_names,key=lambda x: class_name_to_count[x],reverse=True)
|
|
1881
|
+
|
|
1864
1882
|
for cname in class_names:
|
|
1865
1883
|
ccount = len(images_html['class_{}'.format(cname)])
|
|
1866
1884
|
if ccount > 0:
|
|
@@ -1873,18 +1891,18 @@ def process_batch_results(options):
|
|
|
1873
1891
|
# TODO: it's only for silly historical reasons that we re-read
|
|
1874
1892
|
# the input file in this case; we're not currently carrying the json
|
|
1875
1893
|
# representation around, only the Pandas representation.
|
|
1876
|
-
|
|
1894
|
+
|
|
1877
1895
|
print('Generating classification category report')
|
|
1878
|
-
|
|
1896
|
+
|
|
1879
1897
|
d = load_md_or_speciesnet_file(options.md_results_file)
|
|
1880
|
-
|
|
1898
|
+
|
|
1881
1899
|
classification_category_to_count = {}
|
|
1882
1900
|
|
|
1883
1901
|
# im = d['images'][0]
|
|
1884
1902
|
for im in d['images']:
|
|
1885
1903
|
if 'detections' in im and im['detections'] is not None:
|
|
1886
1904
|
for det in im['detections']:
|
|
1887
|
-
if 'classifications' in det:
|
|
1905
|
+
if ('classifications' in det) and (len(det['classifications']) > 0):
|
|
1888
1906
|
class_id = det['classifications'][0][0]
|
|
1889
1907
|
if class_id not in classification_category_to_count:
|
|
1890
1908
|
classification_category_to_count[class_id] = 0
|
|
@@ -1910,31 +1928,31 @@ def process_batch_results(options):
|
|
|
1910
1928
|
|
|
1911
1929
|
for category_name in category_name_to_count.keys():
|
|
1912
1930
|
count = category_name_to_count[category_name]
|
|
1913
|
-
category_count_html = '{}: {}<br>\n'.format(category_name,count)
|
|
1931
|
+
category_count_html = '{}: {}<br>\n'.format(category_name,count)
|
|
1914
1932
|
category_count_footer += category_count_html
|
|
1915
1933
|
|
|
1916
1934
|
category_count_footer += '</div>\n'
|
|
1917
|
-
|
|
1935
|
+
|
|
1918
1936
|
# ...if we're generating a classification category report
|
|
1919
|
-
|
|
1937
|
+
|
|
1920
1938
|
# ...if classification info is present
|
|
1921
|
-
|
|
1939
|
+
|
|
1922
1940
|
if category_count_footer is not None:
|
|
1923
1941
|
index_page += category_count_footer + '\n'
|
|
1924
|
-
|
|
1942
|
+
|
|
1925
1943
|
# Write custom footer if it was provided
|
|
1926
1944
|
if (options.footer_text is not None) and (len(options.footer_text) > 0):
|
|
1927
1945
|
index_page += options.footer_text + '\n'
|
|
1928
|
-
|
|
1946
|
+
|
|
1929
1947
|
# Close open html tags
|
|
1930
1948
|
index_page += '\n</body></html>\n'
|
|
1931
|
-
|
|
1949
|
+
|
|
1932
1950
|
output_html_file = os.path.join(output_dir, 'index.html')
|
|
1933
|
-
with open(output_html_file, 'w',
|
|
1951
|
+
with open(output_html_file, 'w',
|
|
1934
1952
|
encoding=options.output_html_encoding) as f:
|
|
1935
1953
|
f.write(index_page)
|
|
1936
1954
|
|
|
1937
|
-
print('Finished writing html to {}'.format(output_html_file))
|
|
1955
|
+
print('Finished writing html to {}'.format(output_html_file))
|
|
1938
1956
|
|
|
1939
1957
|
# ...if we do/don't have ground truth
|
|
1940
1958
|
|
|
@@ -1965,8 +1983,8 @@ if False:
|
|
|
1965
1983
|
|
|
1966
1984
|
#%% Command-line driver
|
|
1967
1985
|
|
|
1968
|
-
def main():
|
|
1969
|
-
|
|
1986
|
+
def main(): # noqa
|
|
1987
|
+
|
|
1970
1988
|
options = PostProcessingOptions()
|
|
1971
1989
|
|
|
1972
1990
|
parser = argparse.ArgumentParser()
|
|
@@ -2015,42 +2033,42 @@ def main():
|
|
|
2015
2033
|
'--n_cores', type=int, default=1,
|
|
2016
2034
|
help='Number of threads to use for rendering (default: 1)')
|
|
2017
2035
|
parser.add_argument(
|
|
2018
|
-
'--parallelize_rendering_with_processes',
|
|
2036
|
+
'--parallelize_rendering_with_processes',
|
|
2019
2037
|
action='store_true',
|
|
2020
2038
|
help='Should we use processes (instead of threads) for parallelization?')
|
|
2021
2039
|
parser.add_argument(
|
|
2022
|
-
'--no_separate_detections_by_category',
|
|
2040
|
+
'--no_separate_detections_by_category',
|
|
2023
2041
|
action='store_true',
|
|
2024
|
-
help='Collapse all categories into just "detections" and "non-detections"')
|
|
2042
|
+
help='Collapse all categories into just "detections" and "non-detections"')
|
|
2025
2043
|
parser.add_argument(
|
|
2026
|
-
'--open_output_file',
|
|
2044
|
+
'--open_output_file',
|
|
2027
2045
|
action='store_true',
|
|
2028
|
-
help='Open the HTML output file when finished')
|
|
2046
|
+
help='Open the HTML output file when finished')
|
|
2029
2047
|
parser.add_argument(
|
|
2030
|
-
'--max_figures_per_html_file',
|
|
2048
|
+
'--max_figures_per_html_file',
|
|
2031
2049
|
type=int, default=None,
|
|
2032
2050
|
help='Maximum number of images to put on a single HTML page')
|
|
2033
|
-
|
|
2051
|
+
|
|
2034
2052
|
if len(sys.argv[1:]) == 0:
|
|
2035
2053
|
parser.print_help()
|
|
2036
2054
|
parser.exit()
|
|
2037
2055
|
|
|
2038
2056
|
args = parser.parse_args()
|
|
2039
|
-
|
|
2057
|
+
|
|
2040
2058
|
if args.n_cores != 1:
|
|
2041
2059
|
assert (args.n_cores > 1), 'Illegal number of cores: {}'.format(args.n_cores)
|
|
2042
2060
|
if args.parallelize_rendering_with_processes:
|
|
2043
2061
|
args.parallelize_rendering_with_threads = False
|
|
2044
2062
|
args.parallelize_rendering = True
|
|
2045
|
-
args.parallelize_rendering_n_cores = args.n_cores
|
|
2063
|
+
args.parallelize_rendering_n_cores = args.n_cores
|
|
2046
2064
|
|
|
2047
|
-
args_to_object(args, options)
|
|
2065
|
+
args_to_object(args, options)
|
|
2048
2066
|
|
|
2049
2067
|
if args.no_separate_detections_by_category:
|
|
2050
2068
|
options.separate_detections_by_category = False
|
|
2051
|
-
|
|
2069
|
+
|
|
2052
2070
|
ppresults = process_batch_results(options)
|
|
2053
|
-
|
|
2071
|
+
|
|
2054
2072
|
if options.open_output_file:
|
|
2055
2073
|
path_utils.open_file(ppresults.output_html_file)
|
|
2056
2074
|
|