megadetector 5.0.28__py3-none-any.whl → 5.0.29__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/api_core/batch_service/score.py +4 -5
- megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +1 -1
- megadetector/api/batch_processing/api_support/summarize_daily_activity.py +1 -1
- 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/api/synchronous/api_core/tests/load_test.py +2 -3
- 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/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 +23 -23
- 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 -1
- megadetector/data_management/camtrap_dp_to_coco.py +45 -45
- megadetector/data_management/cct_json_utils.py +101 -101
- 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 +189 -189
- megadetector/data_management/databases/add_width_and_height_to_db.py +3 -2
- megadetector/data_management/databases/combine_coco_camera_traps_files.py +38 -38
- megadetector/data_management/databases/integrity_check_json_db.py +202 -188
- megadetector/data_management/databases/subset_json_db.py +33 -33
- megadetector/data_management/generate_crops_from_cct.py +38 -38
- megadetector/data_management/get_image_sizes.py +54 -49
- megadetector/data_management/labelme_to_coco.py +130 -124
- megadetector/data_management/labelme_to_yolo.py +78 -72
- 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 +91 -91
- megadetector/data_management/lila/get_lila_annotation_counts.py +30 -30
- megadetector/data_management/lila/get_lila_image_counts.py +22 -22
- megadetector/data_management/lila/lila_common.py +70 -70
- megadetector/data_management/lila/test_lila_metadata_urls.py +13 -14
- megadetector/data_management/mewc_to_md.py +339 -340
- megadetector/data_management/ocr_tools.py +258 -252
- megadetector/data_management/read_exif.py +231 -224
- megadetector/data_management/remap_coco_categories.py +26 -26
- megadetector/data_management/remove_exif.py +31 -20
- megadetector/data_management/rename_images.py +187 -187
- megadetector/data_management/resize_coco_dataset.py +41 -41
- 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 +117 -120
- megadetector/data_management/yolo_to_coco.py +195 -188
- megadetector/detection/change_detection.py +831 -0
- megadetector/detection/process_video.py +340 -337
- megadetector/detection/pytorch_detector.py +304 -262
- megadetector/detection/run_detector.py +177 -164
- megadetector/detection/run_detector_batch.py +364 -363
- megadetector/detection/run_inference_with_yolov5_val.py +328 -325
- megadetector/detection/run_tiled_inference.py +256 -249
- megadetector/detection/tf_detector.py +24 -24
- megadetector/detection/video_utils.py +290 -282
- megadetector/postprocessing/add_max_conf.py +15 -11
- megadetector/postprocessing/categorize_detections_by_size.py +44 -44
- megadetector/postprocessing/classification_postprocessing.py +415 -415
- megadetector/postprocessing/combine_batch_outputs.py +20 -21
- megadetector/postprocessing/compare_batch_results.py +528 -517
- megadetector/postprocessing/convert_output_format.py +97 -97
- megadetector/postprocessing/create_crop_folder.py +219 -146
- megadetector/postprocessing/detector_calibration.py +173 -168
- megadetector/postprocessing/generate_csv_report.py +508 -499
- megadetector/postprocessing/load_api_results.py +23 -20
- megadetector/postprocessing/md_to_coco.py +129 -98
- megadetector/postprocessing/md_to_labelme.py +89 -83
- megadetector/postprocessing/md_to_wi.py +40 -40
- megadetector/postprocessing/merge_detections.py +87 -114
- megadetector/postprocessing/postprocess_batch_results.py +313 -298
- megadetector/postprocessing/remap_detection_categories.py +36 -36
- megadetector/postprocessing/render_detection_confusion_matrix.py +205 -199
- 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 +702 -677
- 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 +15 -15
- megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +14 -14
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +66 -66
- megadetector/taxonomy_mapping/retrieve_sample_image.py +16 -16
- megadetector/taxonomy_mapping/simple_image_download.py +8 -8
- megadetector/taxonomy_mapping/species_lookup.py +33 -33
- 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/azure_utils.py +22 -22
- megadetector/utils/ct_utils.py +1018 -200
- megadetector/utils/directory_listing.py +21 -77
- megadetector/utils/gpu_test.py +22 -22
- megadetector/utils/md_tests.py +541 -518
- megadetector/utils/path_utils.py +1457 -398
- megadetector/utils/process_utils.py +41 -41
- megadetector/utils/sas_blob_utils.py +53 -49
- megadetector/utils/split_locations_into_train_val.py +61 -61
- megadetector/utils/string_utils.py +147 -26
- megadetector/utils/url_utils.py +463 -173
- megadetector/utils/wi_utils.py +2629 -2526
- megadetector/utils/write_html_image_list.py +137 -137
- megadetector/visualization/plot_utils.py +21 -21
- megadetector/visualization/render_images_with_thumbnails.py +37 -73
- megadetector/visualization/visualization_utils.py +401 -397
- megadetector/visualization/visualize_db.py +197 -190
- megadetector/visualization/visualize_detector_output.py +79 -73
- {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/METADATA +135 -132
- megadetector-5.0.29.dist-info/RECORD +163 -0
- {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/WHEEL +1 -1
- {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/licenses/LICENSE +0 -0
- {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/top_level.txt +0 -0
- 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-5.0.28.dist-info/RECORD +0 -209
|
@@ -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:
|
|
@@ -1027,7 +1029,7 @@ def process_batch_results(options):
|
|
|
1027
1029
|
options.md_results_file, force_forward_slashes=True,
|
|
1028
1030
|
filename_replacements=options.api_output_filename_replacements)
|
|
1029
1031
|
ppresults.api_detection_results = detections_df
|
|
1030
|
-
ppresults.api_other_fields = other_fields
|
|
1032
|
+
ppresults.api_other_fields = other_fields
|
|
1031
1033
|
|
|
1032
1034
|
else:
|
|
1033
1035
|
print('Bypassing detection results loading...')
|
|
@@ -1036,13 +1038,13 @@ def process_batch_results(options):
|
|
|
1036
1038
|
other_fields = options.api_other_fields
|
|
1037
1039
|
|
|
1038
1040
|
# Determine confidence thresholds if necessary
|
|
1039
|
-
|
|
1041
|
+
|
|
1040
1042
|
if options.confidence_threshold is None:
|
|
1041
1043
|
options.confidence_threshold = \
|
|
1042
1044
|
get_typical_confidence_threshold_from_results(other_fields)
|
|
1043
1045
|
print('Choosing default confidence threshold of {} based on MD version'.format(
|
|
1044
|
-
options.confidence_threshold))
|
|
1045
|
-
|
|
1046
|
+
options.confidence_threshold))
|
|
1047
|
+
|
|
1046
1048
|
if options.almost_detection_confidence_threshold is None and options.include_almost_detections:
|
|
1047
1049
|
assert isinstance(options.confidence_threshold,float), \
|
|
1048
1050
|
'If you are using a dictionary of confidence thresholds and almost-detections are enabled, ' + \
|
|
@@ -1050,7 +1052,7 @@ def process_batch_results(options):
|
|
|
1050
1052
|
options.almost_detection_confidence_threshold = options.confidence_threshold - 0.05
|
|
1051
1053
|
if options.almost_detection_confidence_threshold < 0:
|
|
1052
1054
|
options.almost_detection_confidence_threshold = 0
|
|
1053
|
-
|
|
1055
|
+
|
|
1054
1056
|
# Remove rows with inference failures (typically due to corrupt images)
|
|
1055
1057
|
n_failures = 0
|
|
1056
1058
|
if 'failure' in detections_df.columns:
|
|
@@ -1059,11 +1061,11 @@ def process_batch_results(options):
|
|
|
1059
1061
|
# Explicitly forcing a copy() operation here to suppress "trying to be set
|
|
1060
1062
|
# on a copy" warnings (and associated risks) below.
|
|
1061
1063
|
detections_df = detections_df[detections_df['failure'].isna()].copy()
|
|
1062
|
-
|
|
1064
|
+
|
|
1063
1065
|
assert other_fields is not None
|
|
1064
1066
|
|
|
1065
1067
|
detection_categories = other_fields['detection_categories']
|
|
1066
|
-
|
|
1068
|
+
|
|
1067
1069
|
# Convert keys and values to lowercase
|
|
1068
1070
|
classification_categories = other_fields.get('classification_categories', {})
|
|
1069
1071
|
if classification_categories is not None:
|
|
@@ -1075,19 +1077,19 @@ def process_batch_results(options):
|
|
|
1075
1077
|
# Count detections and almost-detections for reporting purposes
|
|
1076
1078
|
n_positives = 0
|
|
1077
1079
|
n_almosts = 0
|
|
1078
|
-
|
|
1080
|
+
|
|
1079
1081
|
print('Assigning images to rendering categories')
|
|
1080
|
-
|
|
1082
|
+
|
|
1081
1083
|
for i_row,row in tqdm(detections_df.iterrows(),total=len(detections_df)):
|
|
1082
|
-
|
|
1084
|
+
|
|
1083
1085
|
detections = row['detections']
|
|
1084
1086
|
max_conf = row['max_detection_conf']
|
|
1085
1087
|
if _has_positive_detection(detections, options, detection_categories):
|
|
1086
1088
|
n_positives += 1
|
|
1087
1089
|
elif (options.almost_detection_confidence_threshold is not None) and \
|
|
1088
1090
|
(max_conf >= options.almost_detection_confidence_threshold):
|
|
1089
|
-
n_almosts += 1
|
|
1090
|
-
|
|
1091
|
+
n_almosts += 1
|
|
1092
|
+
|
|
1091
1093
|
print(f'Finished loading and preprocessing {len(detections_df)} rows '
|
|
1092
1094
|
f'from detector output, predicted {n_positives} positives.')
|
|
1093
1095
|
|
|
@@ -1106,18 +1108,18 @@ def process_batch_results(options):
|
|
|
1106
1108
|
job_name_string = 'unknown'
|
|
1107
1109
|
else:
|
|
1108
1110
|
job_name_string = os.path.basename(options.md_results_file)
|
|
1109
|
-
|
|
1111
|
+
|
|
1110
1112
|
if options.model_version_string is not None:
|
|
1111
1113
|
model_version_string = options.model_version_string
|
|
1112
1114
|
else:
|
|
1113
|
-
|
|
1115
|
+
|
|
1114
1116
|
if 'info' not in other_fields or 'detector' not in other_fields['info']:
|
|
1115
1117
|
print('No model metadata supplied, assuming MDv4')
|
|
1116
1118
|
model_version_string = 'MDv4 (assumed)'
|
|
1117
|
-
else:
|
|
1119
|
+
else:
|
|
1118
1120
|
model_version_string = other_fields['info']['detector']
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
+
|
|
1122
|
+
|
|
1121
1123
|
##%% If we have ground truth, remove images we can't match to ground truth
|
|
1122
1124
|
|
|
1123
1125
|
if ground_truth_indexed_db is not None:
|
|
@@ -1147,7 +1149,7 @@ def process_batch_results(options):
|
|
|
1147
1149
|
|
|
1148
1150
|
output_html_file = ''
|
|
1149
1151
|
|
|
1150
|
-
style_header = """<head>
|
|
1152
|
+
style_header = """<head>
|
|
1151
1153
|
<title>Detection results preview</title>
|
|
1152
1154
|
<style type="text/css">
|
|
1153
1155
|
a { text-decoration: none; }
|
|
@@ -1176,9 +1178,9 @@ def process_batch_results(options):
|
|
|
1176
1178
|
|
|
1177
1179
|
n_positive = 0
|
|
1178
1180
|
n_negative = 0
|
|
1179
|
-
|
|
1181
|
+
|
|
1180
1182
|
for i_detection, fn in enumerate(detector_files):
|
|
1181
|
-
|
|
1183
|
+
|
|
1182
1184
|
image_id = ground_truth_indexed_db.filename_to_id[fn]
|
|
1183
1185
|
image = ground_truth_indexed_db.image_id_to_image[image_id]
|
|
1184
1186
|
detection_status = image['_detection_status']
|
|
@@ -1194,7 +1196,7 @@ def process_batch_results(options):
|
|
|
1194
1196
|
|
|
1195
1197
|
print('Of {} ground truth values, found {} positives and {} negatives'.format(
|
|
1196
1198
|
len(detections_df),n_positive,n_negative))
|
|
1197
|
-
|
|
1199
|
+
|
|
1198
1200
|
# Don't include ambiguous/unknown ground truth in precision/recall analysis
|
|
1199
1201
|
b_valid_ground_truth = gt_detections >= 0.0
|
|
1200
1202
|
|
|
@@ -1221,16 +1223,16 @@ def process_batch_results(options):
|
|
|
1221
1223
|
|
|
1222
1224
|
# Thresholds go up throughout precisions/recalls/thresholds; find the last
|
|
1223
1225
|
# value where recall is at or above target. That's our precision @ target recall.
|
|
1224
|
-
|
|
1226
|
+
|
|
1225
1227
|
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
|
|
1228
|
+
|
|
1229
|
+
# np.where returns a tuple of arrays, but in this syntax where we're
|
|
1228
1230
|
# comparing an array with a scalar, there will only be one element.
|
|
1229
1231
|
assert len (i_above_target_recall) == 1
|
|
1230
|
-
|
|
1232
|
+
|
|
1231
1233
|
# Convert back to a list
|
|
1232
1234
|
i_above_target_recall = i_above_target_recall[0].tolist()
|
|
1233
|
-
|
|
1235
|
+
|
|
1234
1236
|
if len(i_above_target_recall) == 0:
|
|
1235
1237
|
precision_at_target_recall = 0.0
|
|
1236
1238
|
else:
|
|
@@ -1387,7 +1389,7 @@ def process_batch_results(options):
|
|
|
1387
1389
|
t = 'Precision-Recall curve: AP={:0.1%}, P@{:0.1%}={:0.1%}'.format(
|
|
1388
1390
|
average_precision, options.target_recall, precision_at_target_recall)
|
|
1389
1391
|
fig = plot_utils.plot_precision_recall_curve(precisions, recalls, t)
|
|
1390
|
-
|
|
1392
|
+
|
|
1391
1393
|
pr_figure_relative_filename = 'prec_recall.png'
|
|
1392
1394
|
pr_figure_filename = os.path.join(output_dir, pr_figure_relative_filename)
|
|
1393
1395
|
fig.savefig(pr_figure_filename)
|
|
@@ -1402,7 +1404,7 @@ def process_batch_results(options):
|
|
|
1402
1404
|
# Accumulate html image structs (in the format expected by write_html_image_lists)
|
|
1403
1405
|
# for each category, e.g. 'tp', 'fp', ..., 'class_bird', ...
|
|
1404
1406
|
images_html = collections.defaultdict(list)
|
|
1405
|
-
|
|
1407
|
+
|
|
1406
1408
|
# Add default entries by accessing them for the first time
|
|
1407
1409
|
[images_html[res] for res in ['tp', 'tpc', 'tpi', 'fp', 'tn', 'fn']]
|
|
1408
1410
|
for res in images_html.keys():
|
|
@@ -1426,28 +1428,35 @@ def process_batch_results(options):
|
|
|
1426
1428
|
|
|
1427
1429
|
start_time = time.time()
|
|
1428
1430
|
if options.parallelize_rendering:
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
pool = ThreadPool(options.parallelize_rendering_n_cores)
|
|
1437
|
-
worker_string = 'threads'
|
|
1431
|
+
pool = None
|
|
1432
|
+
try:
|
|
1433
|
+
if options.parallelize_rendering_n_cores is None:
|
|
1434
|
+
if options.parallelize_rendering_with_threads:
|
|
1435
|
+
pool = ThreadPool()
|
|
1436
|
+
else:
|
|
1437
|
+
pool = Pool()
|
|
1438
1438
|
else:
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1439
|
+
if options.parallelize_rendering_with_threads:
|
|
1440
|
+
pool = ThreadPool(options.parallelize_rendering_n_cores)
|
|
1441
|
+
worker_string = 'threads'
|
|
1442
|
+
else:
|
|
1443
|
+
pool = Pool(options.parallelize_rendering_n_cores)
|
|
1444
|
+
worker_string = 'processes'
|
|
1445
|
+
print('Rendering images with {} {}'.format(options.parallelize_rendering_n_cores,
|
|
1446
|
+
worker_string))
|
|
1447
|
+
|
|
1448
|
+
rendering_results = list(tqdm(pool.imap(
|
|
1449
|
+
partial(_render_image_with_gt,
|
|
1450
|
+
ground_truth_indexed_db=ground_truth_indexed_db,
|
|
1451
|
+
detection_categories=detection_categories,
|
|
1452
|
+
classification_categories=classification_categories,
|
|
1453
|
+
options=options),
|
|
1454
|
+
files_to_render), total=len(files_to_render)))
|
|
1455
|
+
finally:
|
|
1456
|
+
if pool is not None:
|
|
1457
|
+
pool.close()
|
|
1458
|
+
pool.join()
|
|
1459
|
+
print("Pool closed and joined for GT rendering")
|
|
1451
1460
|
else:
|
|
1452
1461
|
for file_info in tqdm(files_to_render):
|
|
1453
1462
|
rendering_results.append(_render_image_with_gt(
|
|
@@ -1488,19 +1497,19 @@ def process_batch_results(options):
|
|
|
1488
1497
|
confidence_threshold_string = '{:.2%}'.format(options.confidence_threshold)
|
|
1489
1498
|
else:
|
|
1490
1499
|
confidence_threshold_string = str(options.confidence_threshold)
|
|
1491
|
-
|
|
1492
|
-
index_page = """<html>
|
|
1500
|
+
|
|
1501
|
+
index_page = """<html>
|
|
1493
1502
|
{}
|
|
1494
1503
|
<body>
|
|
1495
1504
|
<h2>Evaluation</h2>
|
|
1496
1505
|
|
|
1497
1506
|
<h3>Job metadata</h3>
|
|
1498
|
-
|
|
1507
|
+
|
|
1499
1508
|
<div class="contentdiv">
|
|
1500
1509
|
<p>Job name: {}<br/>
|
|
1501
1510
|
<p>Model version: {}</p>
|
|
1502
1511
|
</div>
|
|
1503
|
-
|
|
1512
|
+
|
|
1504
1513
|
<h3>Sample images</h3>
|
|
1505
1514
|
<div class="contentdiv">
|
|
1506
1515
|
<p>A sample of {} images, annotated with detections above confidence {}.</p>
|
|
@@ -1576,12 +1585,12 @@ def process_batch_results(options):
|
|
|
1576
1585
|
# Write custom footer if it was provided
|
|
1577
1586
|
if (options.footer_text is not None) and (len(options.footer_text) > 0):
|
|
1578
1587
|
index_page += '{}\n'.format(options.footer_text)
|
|
1579
|
-
|
|
1588
|
+
|
|
1580
1589
|
# Close open html tags
|
|
1581
1590
|
index_page += '\n</body></html>\n'
|
|
1582
|
-
|
|
1591
|
+
|
|
1583
1592
|
output_html_file = os.path.join(output_dir, 'index.html')
|
|
1584
|
-
with open(output_html_file, 'w',
|
|
1593
|
+
with open(output_html_file, 'w',
|
|
1585
1594
|
encoding=options.output_html_encoding) as f:
|
|
1586
1595
|
f.write(index_page)
|
|
1587
1596
|
|
|
@@ -1599,34 +1608,34 @@ def process_batch_results(options):
|
|
|
1599
1608
|
# Accumulate html image structs (in the format expected by write_html_image_list)
|
|
1600
1609
|
# for each category
|
|
1601
1610
|
images_html = collections.defaultdict(list)
|
|
1602
|
-
|
|
1611
|
+
|
|
1603
1612
|
# Add default entries by accessing them for the first time
|
|
1604
1613
|
|
|
1605
|
-
# Maps sorted tuples of detection category IDs (string ints) - e.g. ("1"), ("1", "4", "7") - to
|
|
1614
|
+
# Maps sorted tuples of detection category IDs (string ints) - e.g. ("1"), ("1", "4", "7") - to
|
|
1606
1615
|
# result set names, e.g. "detections_human", "detections_cat_truck".
|
|
1607
1616
|
detection_categories_to_results_name = {}
|
|
1608
|
-
|
|
1617
|
+
|
|
1609
1618
|
# Keep track of which categories are single-class (e.g. "animal") and which are
|
|
1610
1619
|
# combinations (e.g. "animal_vehicle")
|
|
1611
1620
|
detection_categories_to_category_count = {}
|
|
1612
|
-
|
|
1621
|
+
|
|
1613
1622
|
# For the creation of a "non-detections" category
|
|
1614
1623
|
images_html['non_detections']
|
|
1615
1624
|
detection_categories_to_category_count['non_detections'] = 0
|
|
1616
|
-
|
|
1617
|
-
|
|
1625
|
+
|
|
1626
|
+
|
|
1618
1627
|
if not options.separate_detections_by_category:
|
|
1619
1628
|
# For the creation of a "detections" category
|
|
1620
1629
|
images_html['detections']
|
|
1621
|
-
detection_categories_to_category_count['detections'] = 0
|
|
1630
|
+
detection_categories_to_category_count['detections'] = 0
|
|
1622
1631
|
else:
|
|
1623
1632
|
# Add a set of results for each category and combination of categories, e.g.
|
|
1624
1633
|
# "detections_animal_vehicle". When we're using this script for non-MegaDetector
|
|
1625
1634
|
# results, this can generate lots of categories, e.g. detections_bear_bird_cat_dog_pig.
|
|
1626
1635
|
# 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.
|
|
1636
|
+
# out links for the ones that are non-empty.
|
|
1628
1637
|
used_combinations = set()
|
|
1629
|
-
|
|
1638
|
+
|
|
1630
1639
|
# row = images_to_visualize.iloc[0]
|
|
1631
1640
|
for i_row, row in images_to_visualize.iterrows():
|
|
1632
1641
|
detections_this_row = row['detections']
|
|
@@ -1639,7 +1648,7 @@ def process_batch_results(options):
|
|
|
1639
1648
|
continue
|
|
1640
1649
|
sorted_categories_this_row = tuple(sorted(above_threshold_category_ids_this_row))
|
|
1641
1650
|
used_combinations.add(sorted_categories_this_row)
|
|
1642
|
-
|
|
1651
|
+
|
|
1643
1652
|
for sorted_subset in used_combinations:
|
|
1644
1653
|
assert len(sorted_subset) > 0
|
|
1645
1654
|
results_name = 'detections'
|
|
@@ -1647,7 +1656,7 @@ def process_batch_results(options):
|
|
|
1647
1656
|
results_name = results_name + '_' + detection_categories[category_id]
|
|
1648
1657
|
images_html[results_name]
|
|
1649
1658
|
detection_categories_to_results_name[sorted_subset] = results_name
|
|
1650
|
-
detection_categories_to_category_count[results_name] = len(sorted_subset)
|
|
1659
|
+
detection_categories_to_category_count[results_name] = len(sorted_subset)
|
|
1651
1660
|
|
|
1652
1661
|
if options.include_almost_detections:
|
|
1653
1662
|
images_html['almost_detections']
|
|
@@ -1658,7 +1667,7 @@ def process_batch_results(options):
|
|
|
1658
1667
|
os.makedirs(os.path.join(output_dir, res), exist_ok=True)
|
|
1659
1668
|
|
|
1660
1669
|
image_count = len(images_to_visualize)
|
|
1661
|
-
|
|
1670
|
+
|
|
1662
1671
|
# Each element will be a list of 2-tuples, with elements [collection name,html info struct]
|
|
1663
1672
|
rendering_results = []
|
|
1664
1673
|
|
|
@@ -1671,38 +1680,44 @@ def process_batch_results(options):
|
|
|
1671
1680
|
for _, row in images_to_visualize.iterrows():
|
|
1672
1681
|
|
|
1673
1682
|
assert isinstance(row['detections'],list)
|
|
1674
|
-
|
|
1683
|
+
|
|
1675
1684
|
# Filenames should already have been normalized to either '/' or '\'
|
|
1676
1685
|
files_to_render.append(row.to_dict())
|
|
1677
1686
|
|
|
1678
1687
|
start_time = time.time()
|
|
1679
1688
|
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'
|
|
1689
|
+
pool = None
|
|
1690
|
+
try:
|
|
1691
|
+
if options.parallelize_rendering_n_cores is None:
|
|
1692
|
+
if options.parallelize_rendering_with_threads:
|
|
1693
|
+
pool = ThreadPool()
|
|
1694
|
+
else:
|
|
1695
|
+
pool = Pool()
|
|
1690
1696
|
else:
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1697
|
+
if options.parallelize_rendering_with_threads:
|
|
1698
|
+
pool = ThreadPool(options.parallelize_rendering_n_cores)
|
|
1699
|
+
worker_string = 'threads'
|
|
1700
|
+
else:
|
|
1701
|
+
pool = Pool(options.parallelize_rendering_n_cores)
|
|
1702
|
+
worker_string = 'processes'
|
|
1703
|
+
print('Rendering images with {} {}'.format(options.parallelize_rendering_n_cores,
|
|
1704
|
+
worker_string))
|
|
1705
|
+
|
|
1706
|
+
# _render_image_no_gt(file_info,detection_categories_to_results_name,
|
|
1707
|
+
# detection_categories,classification_categories)
|
|
1708
|
+
|
|
1709
|
+
rendering_results = list(tqdm(pool.imap(
|
|
1710
|
+
partial(_render_image_no_gt,
|
|
1711
|
+
detection_categories_to_results_name=detection_categories_to_results_name,
|
|
1712
|
+
detection_categories=detection_categories,
|
|
1713
|
+
classification_categories=classification_categories,
|
|
1714
|
+
options=options),
|
|
1715
|
+
files_to_render), total=len(files_to_render)))
|
|
1716
|
+
finally:
|
|
1717
|
+
if pool is not None:
|
|
1718
|
+
pool.close()
|
|
1719
|
+
pool.join()
|
|
1720
|
+
print("Pool closed and joined for non-GT rendering")
|
|
1706
1721
|
else:
|
|
1707
1722
|
for file_info in tqdm(files_to_render):
|
|
1708
1723
|
rendering_result = _render_image_no_gt(file_info,
|
|
@@ -1711,12 +1726,12 @@ def process_batch_results(options):
|
|
|
1711
1726
|
classification_categories,
|
|
1712
1727
|
options=options)
|
|
1713
1728
|
rendering_results.append(rendering_result)
|
|
1714
|
-
|
|
1715
|
-
elapsed = time.time() - start_time
|
|
1716
|
-
|
|
1729
|
+
|
|
1730
|
+
elapsed = time.time() - start_time
|
|
1731
|
+
|
|
1717
1732
|
# Do we have classification results in addition to detection results?
|
|
1718
1733
|
has_classification_info = False
|
|
1719
|
-
|
|
1734
|
+
|
|
1720
1735
|
# Map all the rendering results in the list rendering_results into the
|
|
1721
1736
|
# dictionary images_html
|
|
1722
1737
|
image_rendered_count = 0
|
|
@@ -1731,7 +1746,7 @@ def process_batch_results(options):
|
|
|
1731
1746
|
|
|
1732
1747
|
# Prepare the individual html image files
|
|
1733
1748
|
image_counts = _prepare_html_subpages(images_html, output_dir, options)
|
|
1734
|
-
|
|
1749
|
+
|
|
1735
1750
|
if image_rendered_count == 0:
|
|
1736
1751
|
seconds_per_image = 0.0
|
|
1737
1752
|
else:
|
|
@@ -1744,7 +1759,7 @@ def process_batch_results(options):
|
|
|
1744
1759
|
# Write index.html
|
|
1745
1760
|
|
|
1746
1761
|
# We can't just sum these, because image_counts includes images in both their
|
|
1747
|
-
# detection and classification classes
|
|
1762
|
+
# detection and classification classes
|
|
1748
1763
|
total_images = 0
|
|
1749
1764
|
for k in image_counts.keys():
|
|
1750
1765
|
v = image_counts[k]
|
|
@@ -1754,7 +1769,7 @@ def process_batch_results(options):
|
|
|
1754
1769
|
|
|
1755
1770
|
if total_images != image_count:
|
|
1756
1771
|
print('Warning, missing images: image_count is {}, total_images is {}'.format(total_images,image_count))
|
|
1757
|
-
|
|
1772
|
+
|
|
1758
1773
|
almost_detection_string = ''
|
|
1759
1774
|
if options.include_almost_detections:
|
|
1760
1775
|
almost_detection_string = ' (“almost detection” threshold at {:.1%})'.format(
|
|
@@ -1765,15 +1780,15 @@ def process_batch_results(options):
|
|
|
1765
1780
|
confidence_threshold_string = '{:.2%}'.format(options.confidence_threshold)
|
|
1766
1781
|
else:
|
|
1767
1782
|
confidence_threshold_string = str(options.confidence_threshold)
|
|
1768
|
-
|
|
1783
|
+
|
|
1769
1784
|
index_page = """<html>\n{}\n<body>\n
|
|
1770
1785
|
<h2>Visualization of results for {}</h2>\n
|
|
1771
1786
|
<p>A sample of {} images (of {} total)FAILURE_PLACEHOLDER, annotated with detections above confidence {}{}.</p>\n
|
|
1772
|
-
|
|
1787
|
+
|
|
1773
1788
|
<div class="contentdiv">
|
|
1774
1789
|
<p>Model version: {}</p>
|
|
1775
1790
|
</div>
|
|
1776
|
-
|
|
1791
|
+
|
|
1777
1792
|
<h3>Detection results</h3>\n
|
|
1778
1793
|
<div class="contentdiv">\n""".format(
|
|
1779
1794
|
style_header, job_name_string, image_count, len(detections_df), confidence_threshold_string,
|
|
@@ -1781,9 +1796,9 @@ def process_batch_results(options):
|
|
|
1781
1796
|
|
|
1782
1797
|
failure_string = ''
|
|
1783
1798
|
if n_failures is not None:
|
|
1784
|
-
failure_string = ' ({} failures)'.format(n_failures)
|
|
1799
|
+
failure_string = ' ({} failures)'.format(n_failures)
|
|
1785
1800
|
index_page = index_page.replace('FAILURE_PLACEHOLDER',failure_string)
|
|
1786
|
-
|
|
1801
|
+
|
|
1787
1802
|
def result_set_name_to_friendly_name(result_set_name):
|
|
1788
1803
|
friendly_name = ''
|
|
1789
1804
|
friendly_name = result_set_name.replace('_','-')
|
|
@@ -1793,7 +1808,7 @@ def process_batch_results(options):
|
|
|
1793
1808
|
return friendly_name
|
|
1794
1809
|
|
|
1795
1810
|
sorted_result_set_names = sorted(list(images_html.keys()))
|
|
1796
|
-
|
|
1811
|
+
|
|
1797
1812
|
result_set_name_to_count = {}
|
|
1798
1813
|
for result_set_name in sorted_result_set_names:
|
|
1799
1814
|
image_count = image_counts[result_set_name]
|
|
@@ -1801,7 +1816,7 @@ def process_batch_results(options):
|
|
|
1801
1816
|
sorted_result_set_names = sorted(sorted_result_set_names,
|
|
1802
1817
|
key=lambda x: result_set_name_to_count[x],
|
|
1803
1818
|
reverse=True)
|
|
1804
|
-
|
|
1819
|
+
|
|
1805
1820
|
for result_set_name in sorted_result_set_names:
|
|
1806
1821
|
|
|
1807
1822
|
# Don't print classification classes here; we'll do that later with a slightly
|
|
@@ -1812,17 +1827,17 @@ def process_batch_results(options):
|
|
|
1812
1827
|
filename = result_set_name + '.html'
|
|
1813
1828
|
label = result_set_name_to_friendly_name(result_set_name)
|
|
1814
1829
|
image_count = image_counts[result_set_name]
|
|
1815
|
-
|
|
1830
|
+
|
|
1816
1831
|
# Don't include line items for empty multi-category pages
|
|
1817
1832
|
if image_count == 0 and \
|
|
1818
1833
|
detection_categories_to_category_count[result_set_name] > 1:
|
|
1819
1834
|
continue
|
|
1820
|
-
|
|
1835
|
+
|
|
1821
1836
|
if total_images == 0:
|
|
1822
1837
|
image_fraction = -1
|
|
1823
1838
|
else:
|
|
1824
1839
|
image_fraction = image_count / total_images
|
|
1825
|
-
|
|
1840
|
+
|
|
1826
1841
|
# Write the line item for this category, including a link only if the
|
|
1827
1842
|
# category is non-empty
|
|
1828
1843
|
if image_count == 0:
|
|
@@ -1831,17 +1846,17 @@ def process_batch_results(options):
|
|
|
1831
1846
|
else:
|
|
1832
1847
|
index_page += '<a href="{}">{}</a> ({}, {:.1%})<br/>\n'.format(
|
|
1833
1848
|
filename,label,image_count,image_fraction)
|
|
1834
|
-
|
|
1849
|
+
|
|
1835
1850
|
# ...for each result set
|
|
1836
|
-
|
|
1851
|
+
|
|
1837
1852
|
index_page += '</div>\n'
|
|
1838
1853
|
|
|
1839
1854
|
# If classification information is present and we're supposed to create
|
|
1840
1855
|
# a summary of classifications, we'll put it here
|
|
1841
1856
|
category_count_footer = None
|
|
1842
|
-
|
|
1857
|
+
|
|
1843
1858
|
if has_classification_info:
|
|
1844
|
-
|
|
1859
|
+
|
|
1845
1860
|
index_page += '<h3>Species classification results</h3>'
|
|
1846
1861
|
index_page += '<p>The same image might appear under multiple classes ' + \
|
|
1847
1862
|
'if multiple species were detected.</p>\n'
|
|
@@ -1855,12 +1870,12 @@ def process_batch_results(options):
|
|
|
1855
1870
|
class_names.append('unreliable')
|
|
1856
1871
|
|
|
1857
1872
|
if options.sort_classification_results_by_count:
|
|
1858
|
-
class_name_to_count = {}
|
|
1873
|
+
class_name_to_count = {}
|
|
1859
1874
|
for cname in class_names:
|
|
1860
1875
|
ccount = len(images_html['class_{}'.format(cname)])
|
|
1861
1876
|
class_name_to_count[cname] = ccount
|
|
1862
|
-
class_names = sorted(class_names,key=lambda x: class_name_to_count[x],reverse=True)
|
|
1863
|
-
|
|
1877
|
+
class_names = sorted(class_names,key=lambda x: class_name_to_count[x],reverse=True)
|
|
1878
|
+
|
|
1864
1879
|
for cname in class_names:
|
|
1865
1880
|
ccount = len(images_html['class_{}'.format(cname)])
|
|
1866
1881
|
if ccount > 0:
|
|
@@ -1873,18 +1888,18 @@ def process_batch_results(options):
|
|
|
1873
1888
|
# TODO: it's only for silly historical reasons that we re-read
|
|
1874
1889
|
# the input file in this case; we're not currently carrying the json
|
|
1875
1890
|
# representation around, only the Pandas representation.
|
|
1876
|
-
|
|
1891
|
+
|
|
1877
1892
|
print('Generating classification category report')
|
|
1878
|
-
|
|
1893
|
+
|
|
1879
1894
|
d = load_md_or_speciesnet_file(options.md_results_file)
|
|
1880
|
-
|
|
1895
|
+
|
|
1881
1896
|
classification_category_to_count = {}
|
|
1882
1897
|
|
|
1883
1898
|
# im = d['images'][0]
|
|
1884
1899
|
for im in d['images']:
|
|
1885
1900
|
if 'detections' in im and im['detections'] is not None:
|
|
1886
1901
|
for det in im['detections']:
|
|
1887
|
-
if 'classifications' in det:
|
|
1902
|
+
if ('classifications' in det) and (len(det['classifications']) > 0):
|
|
1888
1903
|
class_id = det['classifications'][0][0]
|
|
1889
1904
|
if class_id not in classification_category_to_count:
|
|
1890
1905
|
classification_category_to_count[class_id] = 0
|
|
@@ -1910,31 +1925,31 @@ def process_batch_results(options):
|
|
|
1910
1925
|
|
|
1911
1926
|
for category_name in category_name_to_count.keys():
|
|
1912
1927
|
count = category_name_to_count[category_name]
|
|
1913
|
-
category_count_html = '{}: {}<br>\n'.format(category_name,count)
|
|
1928
|
+
category_count_html = '{}: {}<br>\n'.format(category_name,count)
|
|
1914
1929
|
category_count_footer += category_count_html
|
|
1915
1930
|
|
|
1916
1931
|
category_count_footer += '</div>\n'
|
|
1917
|
-
|
|
1932
|
+
|
|
1918
1933
|
# ...if we're generating a classification category report
|
|
1919
|
-
|
|
1934
|
+
|
|
1920
1935
|
# ...if classification info is present
|
|
1921
|
-
|
|
1936
|
+
|
|
1922
1937
|
if category_count_footer is not None:
|
|
1923
1938
|
index_page += category_count_footer + '\n'
|
|
1924
|
-
|
|
1939
|
+
|
|
1925
1940
|
# Write custom footer if it was provided
|
|
1926
1941
|
if (options.footer_text is not None) and (len(options.footer_text) > 0):
|
|
1927
1942
|
index_page += options.footer_text + '\n'
|
|
1928
|
-
|
|
1943
|
+
|
|
1929
1944
|
# Close open html tags
|
|
1930
1945
|
index_page += '\n</body></html>\n'
|
|
1931
|
-
|
|
1946
|
+
|
|
1932
1947
|
output_html_file = os.path.join(output_dir, 'index.html')
|
|
1933
|
-
with open(output_html_file, 'w',
|
|
1948
|
+
with open(output_html_file, 'w',
|
|
1934
1949
|
encoding=options.output_html_encoding) as f:
|
|
1935
1950
|
f.write(index_page)
|
|
1936
1951
|
|
|
1937
|
-
print('Finished writing html to {}'.format(output_html_file))
|
|
1952
|
+
print('Finished writing html to {}'.format(output_html_file))
|
|
1938
1953
|
|
|
1939
1954
|
# ...if we do/don't have ground truth
|
|
1940
1955
|
|
|
@@ -1965,8 +1980,8 @@ if False:
|
|
|
1965
1980
|
|
|
1966
1981
|
#%% Command-line driver
|
|
1967
1982
|
|
|
1968
|
-
def main():
|
|
1969
|
-
|
|
1983
|
+
def main(): # noqa
|
|
1984
|
+
|
|
1970
1985
|
options = PostProcessingOptions()
|
|
1971
1986
|
|
|
1972
1987
|
parser = argparse.ArgumentParser()
|
|
@@ -2015,42 +2030,42 @@ def main():
|
|
|
2015
2030
|
'--n_cores', type=int, default=1,
|
|
2016
2031
|
help='Number of threads to use for rendering (default: 1)')
|
|
2017
2032
|
parser.add_argument(
|
|
2018
|
-
'--parallelize_rendering_with_processes',
|
|
2033
|
+
'--parallelize_rendering_with_processes',
|
|
2019
2034
|
action='store_true',
|
|
2020
2035
|
help='Should we use processes (instead of threads) for parallelization?')
|
|
2021
2036
|
parser.add_argument(
|
|
2022
|
-
'--no_separate_detections_by_category',
|
|
2037
|
+
'--no_separate_detections_by_category',
|
|
2023
2038
|
action='store_true',
|
|
2024
|
-
help='Collapse all categories into just "detections" and "non-detections"')
|
|
2039
|
+
help='Collapse all categories into just "detections" and "non-detections"')
|
|
2025
2040
|
parser.add_argument(
|
|
2026
|
-
'--open_output_file',
|
|
2041
|
+
'--open_output_file',
|
|
2027
2042
|
action='store_true',
|
|
2028
|
-
help='Open the HTML output file when finished')
|
|
2043
|
+
help='Open the HTML output file when finished')
|
|
2029
2044
|
parser.add_argument(
|
|
2030
|
-
'--max_figures_per_html_file',
|
|
2045
|
+
'--max_figures_per_html_file',
|
|
2031
2046
|
type=int, default=None,
|
|
2032
2047
|
help='Maximum number of images to put on a single HTML page')
|
|
2033
|
-
|
|
2048
|
+
|
|
2034
2049
|
if len(sys.argv[1:]) == 0:
|
|
2035
2050
|
parser.print_help()
|
|
2036
2051
|
parser.exit()
|
|
2037
2052
|
|
|
2038
2053
|
args = parser.parse_args()
|
|
2039
|
-
|
|
2054
|
+
|
|
2040
2055
|
if args.n_cores != 1:
|
|
2041
2056
|
assert (args.n_cores > 1), 'Illegal number of cores: {}'.format(args.n_cores)
|
|
2042
2057
|
if args.parallelize_rendering_with_processes:
|
|
2043
2058
|
args.parallelize_rendering_with_threads = False
|
|
2044
2059
|
args.parallelize_rendering = True
|
|
2045
|
-
args.parallelize_rendering_n_cores = args.n_cores
|
|
2060
|
+
args.parallelize_rendering_n_cores = args.n_cores
|
|
2046
2061
|
|
|
2047
|
-
args_to_object(args, options)
|
|
2062
|
+
args_to_object(args, options)
|
|
2048
2063
|
|
|
2049
2064
|
if args.no_separate_detections_by_category:
|
|
2050
2065
|
options.separate_detections_by_category = False
|
|
2051
|
-
|
|
2066
|
+
|
|
2052
2067
|
ppresults = process_batch_results(options)
|
|
2053
|
-
|
|
2068
|
+
|
|
2054
2069
|
if options.open_output_file:
|
|
2055
2070
|
path_utils.open_file(ppresults.output_html_file)
|
|
2056
2071
|
|