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
|
@@ -8,17 +8,17 @@ Compare sets of batch results; typically used to compare:
|
|
|
8
8
|
* Results before/after RDE
|
|
9
9
|
* Results with/without augmentation
|
|
10
10
|
|
|
11
|
-
Makes pairwise comparisons between sets of results, but can take lists of results files
|
|
12
|
-
(will perform all pairwise comparisons). Results are written to an HTML page that shows the
|
|
13
|
-
number and nature of disagreements (in the sense of each image being a detection or non-detection),
|
|
11
|
+
Makes pairwise comparisons between sets of results, but can take lists of results files
|
|
12
|
+
(will perform all pairwise comparisons). Results are written to an HTML page that shows the
|
|
13
|
+
number and nature of disagreements (in the sense of each image being a detection or non-detection),
|
|
14
14
|
with sample images for each category.
|
|
15
15
|
|
|
16
16
|
Operates in one of three modes, depending on whether ground truth labels/boxes are available:
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
* The most common mode assumes no ground truth, just finds agreement/disagreement between
|
|
19
19
|
results files, or class discrepancies.
|
|
20
20
|
|
|
21
|
-
* If image-level ground truth is available, finds image-level agreements on TPs/TNs/FPs/FNs, but also
|
|
21
|
+
* If image-level ground truth is available, finds image-level agreements on TPs/TNs/FPs/FNs, but also
|
|
22
22
|
finds image-level TPs/TNs/FPs/FNs that are unique to each set of results (at the specified confidence
|
|
23
23
|
threshold).
|
|
24
24
|
|
|
@@ -36,6 +36,9 @@ import random
|
|
|
36
36
|
import copy
|
|
37
37
|
import urllib
|
|
38
38
|
import itertools
|
|
39
|
+
import sys
|
|
40
|
+
import argparse
|
|
41
|
+
import textwrap
|
|
39
42
|
|
|
40
43
|
import numpy as np
|
|
41
44
|
|
|
@@ -54,17 +57,17 @@ from megadetector.utils.ct_utils import invert_dictionary, get_iou
|
|
|
54
57
|
from megadetector.utils import path_utils
|
|
55
58
|
from megadetector.visualization.visualization_utils import get_text_size
|
|
56
59
|
|
|
57
|
-
def _maxempty(L):
|
|
60
|
+
def _maxempty(L): # noqa
|
|
58
61
|
"""
|
|
59
62
|
Return the maximum value in a list, or 0 if the list is empty
|
|
60
63
|
"""
|
|
61
|
-
|
|
64
|
+
|
|
62
65
|
if len(L) == 0:
|
|
63
66
|
return 0
|
|
64
67
|
else:
|
|
65
68
|
return max(L)
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
|
|
70
|
+
|
|
68
71
|
#%% Constants and support classes
|
|
69
72
|
|
|
70
73
|
class PairwiseBatchComparisonOptions:
|
|
@@ -72,32 +75,32 @@ class PairwiseBatchComparisonOptions:
|
|
|
72
75
|
Defines the options used for a single pairwise comparison; a list of these
|
|
73
76
|
pairwise options sets is stored in the BatchComparisonsOptions class.
|
|
74
77
|
"""
|
|
75
|
-
|
|
78
|
+
|
|
76
79
|
def __init__(self):
|
|
77
|
-
|
|
80
|
+
|
|
78
81
|
#: First filename to compare
|
|
79
82
|
self.results_filename_a = None
|
|
80
|
-
|
|
83
|
+
|
|
81
84
|
#: Second filename to compare
|
|
82
85
|
self.results_filename_b = None
|
|
83
|
-
|
|
86
|
+
|
|
84
87
|
#: Description to use in the output HTML for filename A
|
|
85
88
|
self.results_description_a = None
|
|
86
|
-
|
|
89
|
+
|
|
87
90
|
#: Description to use in the output HTML for filename B
|
|
88
91
|
self.results_description_b = None
|
|
89
|
-
|
|
92
|
+
|
|
90
93
|
#: Per-class detection thresholds to use for filename A (including a 'default' threshold)
|
|
91
94
|
self.detection_thresholds_a = {'animal':0.15,'person':0.15,'vehicle':0.15,'default':0.15}
|
|
92
|
-
|
|
95
|
+
|
|
93
96
|
#: Per-class detection thresholds to use for filename B (including a 'default' threshold)
|
|
94
97
|
self.detection_thresholds_b = {'animal':0.15,'person':0.15,'vehicle':0.15,'default':0.15}
|
|
95
|
-
|
|
98
|
+
|
|
96
99
|
#: Rendering threshold to use for all categories for filename A
|
|
97
100
|
self.rendering_confidence_threshold_a = 0.1
|
|
98
|
-
|
|
101
|
+
|
|
99
102
|
#: Rendering threshold to use for all categories for filename B
|
|
100
|
-
self.rendering_confidence_threshold_b = 0.1
|
|
103
|
+
self.rendering_confidence_threshold_b = 0.1
|
|
101
104
|
|
|
102
105
|
# ...class PairwiseBatchComparisonOptions
|
|
103
106
|
|
|
@@ -106,120 +109,120 @@ class BatchComparisonOptions:
|
|
|
106
109
|
"""
|
|
107
110
|
Defines the options for a set of (possibly many) pairwise comparisons.
|
|
108
111
|
"""
|
|
109
|
-
|
|
112
|
+
|
|
110
113
|
def __init__(self):
|
|
111
|
-
|
|
114
|
+
|
|
112
115
|
#: Folder to which we should write HTML output
|
|
113
116
|
self.output_folder = None
|
|
114
|
-
|
|
117
|
+
|
|
115
118
|
#: Base folder for images (which are specified as relative files)
|
|
116
119
|
self.image_folder = None
|
|
117
|
-
|
|
120
|
+
|
|
118
121
|
#: Job name to use in the HTML output file
|
|
119
122
|
self.job_name = ''
|
|
120
|
-
|
|
123
|
+
|
|
121
124
|
#: Maximum number of images to render for each category, where a "category" here is
|
|
122
125
|
#: "detections_a_only", "detections_b_only", etc., or None to render all images.
|
|
123
126
|
self.max_images_per_category = 1000
|
|
124
|
-
|
|
127
|
+
|
|
125
128
|
#: Maximum number of images per HTML page (paginates if a category page goes beyond this),
|
|
126
129
|
#: or None to disable pagination.
|
|
127
130
|
self.max_images_per_page = None
|
|
128
|
-
|
|
131
|
+
|
|
129
132
|
#: Colormap to use for detections in file A (maps detection categories to colors)
|
|
130
133
|
self.colormap_a = ['Red']
|
|
131
|
-
|
|
134
|
+
|
|
132
135
|
#: Colormap to use for detections in file B (maps detection categories to colors)
|
|
133
136
|
self.colormap_b = ['RoyalBlue']
|
|
134
|
-
|
|
137
|
+
|
|
135
138
|
#: Process-based parallelization isn't supported yet; this must be "True"
|
|
136
139
|
self.parallelize_rendering_with_threads = True
|
|
137
|
-
|
|
140
|
+
|
|
138
141
|
#: List of filenames to include in the comparison, or None to use all files
|
|
139
142
|
self.filenames_to_include = None
|
|
140
|
-
|
|
143
|
+
|
|
141
144
|
#: List of category names to include in the comparison, or None to use all categories
|
|
142
145
|
self.category_names_to_include = None
|
|
143
|
-
|
|
146
|
+
|
|
144
147
|
#: Compare only detections/non-detections, ignore categories (still renders categories)
|
|
145
148
|
self.class_agnostic_comparison = False
|
|
146
|
-
|
|
149
|
+
|
|
147
150
|
#: Width of images to render in the output HTML
|
|
148
151
|
self.target_width = 800
|
|
149
|
-
|
|
152
|
+
|
|
150
153
|
#: Number of workers to use for rendering, or <=1 to disable parallelization
|
|
151
154
|
self.n_rendering_workers = 20
|
|
152
|
-
|
|
155
|
+
|
|
153
156
|
#: Random seed for image sampling (not used if max_images_per_category is None)
|
|
154
157
|
self.random_seed = 0
|
|
155
|
-
|
|
158
|
+
|
|
156
159
|
#: Whether to sort results by confidence; if this is False, sorts by filename
|
|
157
160
|
self.sort_by_confidence = False
|
|
158
|
-
|
|
161
|
+
|
|
159
162
|
#: The expectation is that all results sets being compared will refer to the same images; if this
|
|
160
163
|
#: is True (default), we'll error if that's not the case, otherwise non-matching lists will just be
|
|
161
164
|
#: a warning.
|
|
162
165
|
self.error_on_non_matching_lists = True
|
|
163
|
-
|
|
166
|
+
|
|
164
167
|
#: Ground truth .json file in COCO Camera Traps format, or an already-loaded COCO dictionary
|
|
165
168
|
self.ground_truth_file = None
|
|
166
|
-
|
|
169
|
+
|
|
167
170
|
#: IoU threshold to use when comparing to ground truth with boxes
|
|
168
171
|
self.gt_iou_threshold = 0.5
|
|
169
|
-
|
|
172
|
+
|
|
170
173
|
#: Category names that refer to empty images when image-level ground truth is provided
|
|
171
174
|
self.gt_empty_categories = ['empty','blank','misfire']
|
|
172
|
-
|
|
175
|
+
|
|
173
176
|
#: Should we show image-level labels as text on each image when boxes are not available?
|
|
174
177
|
self.show_labels_for_image_level_gt = True
|
|
175
|
-
|
|
178
|
+
|
|
176
179
|
#: Should we show category names (instead of numbers) on GT boxes?
|
|
177
180
|
self.show_category_names_on_gt_boxes = True
|
|
178
|
-
|
|
181
|
+
|
|
179
182
|
#: Should we show category names (instead of numbers) on detected boxes?
|
|
180
183
|
self.show_category_names_on_detected_boxes = True
|
|
181
|
-
|
|
184
|
+
|
|
182
185
|
#: List of PairwiseBatchComparisonOptions that defines the comparisons we'll render.
|
|
183
186
|
self.pairwise_options = []
|
|
184
|
-
|
|
187
|
+
|
|
185
188
|
#: Only process images whose file names contain this token
|
|
186
189
|
#:
|
|
187
190
|
#: This can also be a pointer to a function that takes a string (filename)
|
|
188
|
-
#: and returns a bool (if the function returns True, the image will be
|
|
191
|
+
#: and returns a bool (if the function returns True, the image will be
|
|
189
192
|
#: included in the comparison).
|
|
190
193
|
self.required_token = None
|
|
191
|
-
|
|
194
|
+
|
|
192
195
|
#: Enable additional debug output
|
|
193
196
|
self.verbose = False
|
|
194
|
-
|
|
197
|
+
|
|
195
198
|
#: Separate out the "clean TP" and "clean TN" categories, only relevant when GT is
|
|
196
199
|
#: available.
|
|
197
200
|
self.include_clean_categories = True
|
|
198
|
-
|
|
201
|
+
|
|
199
202
|
#: When rendering to the output table, optionally write alternative strings
|
|
200
203
|
#: to describe images
|
|
201
204
|
self.fn_to_display_fn = None
|
|
202
|
-
|
|
205
|
+
|
|
203
206
|
#: Should we run urllib.parse.quote() on paths before using them as links in the
|
|
204
207
|
#: output page?
|
|
205
208
|
self.parse_link_paths = True
|
|
206
|
-
|
|
209
|
+
|
|
207
210
|
# ...class BatchComparisonOptions
|
|
208
|
-
|
|
211
|
+
|
|
209
212
|
|
|
210
213
|
class PairwiseBatchComparisonResults:
|
|
211
214
|
"""
|
|
212
215
|
The results from a single pairwise comparison.
|
|
213
216
|
"""
|
|
214
|
-
|
|
217
|
+
|
|
215
218
|
def __init__(self):
|
|
216
|
-
|
|
219
|
+
|
|
217
220
|
#: String of HTML content suitable for rendering to an HTML file
|
|
218
221
|
self.html_content = None
|
|
219
|
-
|
|
222
|
+
|
|
220
223
|
#: Possibly-modified version of the PairwiseBatchComparisonOptions supplied as input.
|
|
221
224
|
self.pairwise_options = None
|
|
222
|
-
|
|
225
|
+
|
|
223
226
|
#: A dictionary with keys representing category names; in the no-ground-truth case, for example,
|
|
224
227
|
#: category names are:
|
|
225
228
|
#:
|
|
@@ -233,22 +236,22 @@ class PairwiseBatchComparisonResults:
|
|
|
233
236
|
self.categories_to_image_pairs = None
|
|
234
237
|
|
|
235
238
|
# ...class PairwiseBatchComparisonResults
|
|
236
|
-
|
|
237
|
-
|
|
239
|
+
|
|
240
|
+
|
|
238
241
|
class BatchComparisonResults:
|
|
239
242
|
"""
|
|
240
243
|
The results from a set of pairwise comparisons
|
|
241
244
|
"""
|
|
242
|
-
|
|
245
|
+
|
|
243
246
|
def __init__(self):
|
|
244
|
-
|
|
247
|
+
|
|
245
248
|
#: Filename containing HTML output
|
|
246
249
|
self.html_output_file = None
|
|
247
|
-
|
|
250
|
+
|
|
248
251
|
#: A list of PairwiseBatchComparisonResults
|
|
249
252
|
self.pairwise_results = None
|
|
250
|
-
|
|
251
|
-
# ...class BatchComparisonResults
|
|
253
|
+
|
|
254
|
+
# ...class BatchComparisonResults
|
|
252
255
|
|
|
253
256
|
|
|
254
257
|
main_page_style_header = """<head>
|
|
@@ -268,30 +271,30 @@ main_page_footer = '<br/><br/><br/></body></html>\n'
|
|
|
268
271
|
def _render_image_pair(fn,image_pairs,category_folder,options,pairwise_options):
|
|
269
272
|
"""
|
|
270
273
|
Render two sets of results (i.e., a comparison) for a single image.
|
|
271
|
-
|
|
274
|
+
|
|
272
275
|
Args:
|
|
273
276
|
fn (str): image filename
|
|
274
277
|
image_pairs (dict): dict mapping filenames to pairs of image dicts
|
|
275
|
-
category_folder (str): folder to which to render this image, typically
|
|
278
|
+
category_folder (str): folder to which to render this image, typically
|
|
276
279
|
"detections_a_only", "detections_b_only", etc.
|
|
277
280
|
options (BatchComparisonOptions): job options
|
|
278
281
|
pairwise_options (PairwiseBatchComparisonOptions): pairwise comparison options
|
|
279
|
-
|
|
282
|
+
|
|
280
283
|
Returns:
|
|
281
|
-
str: rendered image filename
|
|
284
|
+
str: rendered image filename
|
|
282
285
|
"""
|
|
283
|
-
|
|
286
|
+
|
|
284
287
|
input_image_path = os.path.join(options.image_folder,fn)
|
|
285
288
|
assert os.path.isfile(input_image_path), 'Image {} does not exist'.format(input_image_path)
|
|
286
|
-
|
|
289
|
+
|
|
287
290
|
im = visualization_utils.open_image(input_image_path)
|
|
288
291
|
image_pair = image_pairs[fn]
|
|
289
292
|
detections_a = image_pair['im_a']['detections']
|
|
290
|
-
detections_b = image_pair['im_b']['detections']
|
|
291
|
-
|
|
293
|
+
detections_b = image_pair['im_b']['detections']
|
|
294
|
+
|
|
292
295
|
custom_strings_a = [''] * len(detections_a)
|
|
293
|
-
custom_strings_b = [''] * len(detections_b)
|
|
294
|
-
|
|
296
|
+
custom_strings_b = [''] * len(detections_b)
|
|
297
|
+
|
|
295
298
|
# This function is often used to compare results before/after various merging
|
|
296
299
|
# steps, so we have some special-case formatting based on the "transferred_from"
|
|
297
300
|
# field generated in merge_detections.py.
|
|
@@ -299,19 +302,19 @@ def _render_image_pair(fn,image_pairs,category_folder,options,pairwise_options):
|
|
|
299
302
|
if 'transferred_from' in det:
|
|
300
303
|
custom_strings_a[i_det] = '({})'.format(
|
|
301
304
|
det['transferred_from'].split('.')[0])
|
|
302
|
-
|
|
305
|
+
|
|
303
306
|
for i_det,det in enumerate(detections_b):
|
|
304
307
|
if 'transferred_from' in det:
|
|
305
308
|
custom_strings_b[i_det] = '({})'.format(
|
|
306
309
|
det['transferred_from'].split('.')[0])
|
|
307
|
-
|
|
310
|
+
|
|
308
311
|
if options.target_width is not None:
|
|
309
312
|
im = visualization_utils.resize_image(im, options.target_width)
|
|
310
|
-
|
|
313
|
+
|
|
311
314
|
label_map = None
|
|
312
315
|
if options.show_category_names_on_detected_boxes:
|
|
313
|
-
label_map=options.detection_category_id_to_name
|
|
314
|
-
|
|
316
|
+
label_map=options.detection_category_id_to_name
|
|
317
|
+
|
|
315
318
|
visualization_utils.render_detection_bounding_boxes(detections_a,im,
|
|
316
319
|
confidence_threshold=pairwise_options.rendering_confidence_threshold_a,
|
|
317
320
|
thickness=4,expansion=0,
|
|
@@ -331,7 +334,7 @@ def _render_image_pair(fn,image_pairs,category_folder,options,pairwise_options):
|
|
|
331
334
|
|
|
332
335
|
# Do we also need to render ground truth?
|
|
333
336
|
if 'im_gt' in image_pair and image_pair['im_gt'] is not None:
|
|
334
|
-
|
|
337
|
+
|
|
335
338
|
im_gt = image_pair['im_gt']
|
|
336
339
|
annotations_gt = image_pair['annotations_gt']
|
|
337
340
|
gt_boxes = []
|
|
@@ -339,60 +342,60 @@ def _render_image_pair(fn,image_pairs,category_folder,options,pairwise_options):
|
|
|
339
342
|
if 'bbox' in ann:
|
|
340
343
|
gt_boxes.append(ann['bbox'])
|
|
341
344
|
gt_categories = [ann['category_id'] for ann in annotations_gt]
|
|
342
|
-
|
|
345
|
+
|
|
343
346
|
if len(gt_boxes) > 0:
|
|
344
|
-
|
|
347
|
+
|
|
345
348
|
label_map = None
|
|
346
349
|
if options.show_category_names_on_gt_boxes:
|
|
347
350
|
label_map=options.gt_category_id_to_name
|
|
348
|
-
|
|
351
|
+
|
|
349
352
|
assert len(gt_boxes) == len(gt_categories)
|
|
350
353
|
gt_colormap = ['yellow']*(max(gt_categories)+1)
|
|
351
354
|
visualization_utils.render_db_bounding_boxes(boxes=gt_boxes,
|
|
352
|
-
classes=gt_categories,
|
|
353
|
-
image=im,
|
|
355
|
+
classes=gt_categories,
|
|
356
|
+
image=im,
|
|
354
357
|
original_size=(im_gt['width'],im_gt['height']),
|
|
355
|
-
label_map=label_map,
|
|
356
|
-
thickness=1,
|
|
358
|
+
label_map=label_map,
|
|
359
|
+
thickness=1,
|
|
357
360
|
expansion=0,
|
|
358
361
|
textalign=visualization_utils.TEXTALIGN_RIGHT,
|
|
359
362
|
vtextalign=visualization_utils.VTEXTALIGN_TOP,
|
|
360
363
|
text_rotation=-90,
|
|
361
364
|
colormap=gt_colormap)
|
|
362
|
-
|
|
365
|
+
|
|
363
366
|
else:
|
|
364
|
-
|
|
367
|
+
|
|
365
368
|
if options.show_labels_for_image_level_gt:
|
|
366
|
-
|
|
369
|
+
|
|
367
370
|
gt_categories_set = set([ann['category_id'] for ann in annotations_gt])
|
|
368
|
-
gt_category_names = [options.gt_category_id_to_name[category_name] for
|
|
371
|
+
gt_category_names = [options.gt_category_id_to_name[category_name] for
|
|
369
372
|
category_name in gt_categories_set]
|
|
370
373
|
category_string = ','.join(gt_category_names)
|
|
371
374
|
category_string = '(' + category_string + ')'
|
|
372
|
-
|
|
375
|
+
|
|
373
376
|
try:
|
|
374
377
|
font = ImageFont.truetype('arial.ttf', 25)
|
|
375
378
|
except IOError:
|
|
376
379
|
font = ImageFont.load_default()
|
|
377
|
-
|
|
380
|
+
|
|
378
381
|
draw = ImageDraw.Draw(im)
|
|
379
|
-
|
|
382
|
+
|
|
380
383
|
text_width, text_height = get_text_size(font,category_string)
|
|
381
|
-
|
|
384
|
+
|
|
382
385
|
text_left = 10
|
|
383
386
|
text_bottom = text_height + 10
|
|
384
387
|
margin = np.ceil(0.05 * text_height)
|
|
385
|
-
|
|
388
|
+
|
|
386
389
|
draw.text(
|
|
387
390
|
(text_left + margin, text_bottom - text_height - margin),
|
|
388
391
|
category_string,
|
|
389
392
|
fill='white',
|
|
390
393
|
font=font)
|
|
391
|
-
|
|
394
|
+
|
|
392
395
|
# ...if we have boxes in the GT
|
|
393
|
-
|
|
396
|
+
|
|
394
397
|
# ...if we need to render ground truth
|
|
395
|
-
|
|
398
|
+
|
|
396
399
|
output_image_fn = path_utils.flatten_path(fn)
|
|
397
400
|
output_image_path = os.path.join(category_folder,output_image_fn)
|
|
398
401
|
im.save(output_image_path)
|
|
@@ -409,47 +412,47 @@ def _result_types_to_comparison_category(result_types_present_a,
|
|
|
409
412
|
Given the set of result types (tp,tn,fp,fn) present in each of two sets of results
|
|
410
413
|
for an image, determine the category to which we want to assign this image.
|
|
411
414
|
"""
|
|
412
|
-
|
|
415
|
+
|
|
413
416
|
# The "common_tp" category is for the case where both models have *only* TPs
|
|
414
417
|
if ('tp' in result_types_present_a) and ('tp' in result_types_present_b) and \
|
|
415
418
|
(len(result_types_present_a) == 1) and (len(result_types_present_b) == 1):
|
|
416
419
|
return 'common_tp'
|
|
417
|
-
|
|
420
|
+
|
|
418
421
|
# The "common_tn" category is for the case where both models have *only* TNs
|
|
419
422
|
if ('tn' in result_types_present_a) and ('tn' in result_types_present_b) and \
|
|
420
423
|
(len(result_types_present_a) == 1) and (len(result_types_present_b) == 1):
|
|
421
424
|
return 'common_tn'
|
|
422
425
|
|
|
423
|
-
"""
|
|
426
|
+
"""
|
|
424
427
|
# The "common_fp" category is for the case where both models have *only* FPs
|
|
425
428
|
if ('fp' in result_types_present_a) and ('fp' in result_types_present_b) and \
|
|
426
429
|
(len(result_types_present_a) == 1) and (len(result_types_present_b) == 1):
|
|
427
430
|
return 'common_fp'
|
|
428
431
|
"""
|
|
429
|
-
|
|
432
|
+
|
|
430
433
|
# The "common_fp" category is for the case where both models have at least one FP,
|
|
431
434
|
# and no FNs.
|
|
432
435
|
if ('fp' in result_types_present_a) and ('fp' in result_types_present_b) and \
|
|
433
436
|
('fn' not in result_types_present_a) and ('fn' not in result_types_present_b):
|
|
434
437
|
return 'common_fp'
|
|
435
|
-
|
|
438
|
+
|
|
436
439
|
"""
|
|
437
440
|
# The "common_fn" category is for the case where both models have *only* FNs
|
|
438
441
|
if ('fn' in result_types_present_a) and ('fn' in result_types_present_b) and \
|
|
439
442
|
(len(result_types_present_a) == 1) and (len(result_types_present_b) == 1):
|
|
440
443
|
return 'common_fn'
|
|
441
444
|
"""
|
|
442
|
-
|
|
445
|
+
|
|
443
446
|
# The "common_fn" category is for the case where both models have at least one FN,
|
|
444
447
|
# and no FPs
|
|
445
448
|
if ('fn' in result_types_present_a) and ('fn' in result_types_present_b) and \
|
|
446
449
|
('fp' not in result_types_present_a) and ('fp' not in result_types_present_b):
|
|
447
450
|
return 'common_fn'
|
|
448
|
-
|
|
451
|
+
|
|
449
452
|
## The tp-only categories are for the case where one model has *only* TPs
|
|
450
|
-
|
|
453
|
+
|
|
451
454
|
if ('tp' in result_types_present_a) and (len(result_types_present_a) == 1):
|
|
452
|
-
# Clean TPs are cases where the other model has only FNs, no FPs
|
|
455
|
+
# Clean TPs are cases where the other model has only FNs, no FPs
|
|
453
456
|
if options.include_clean_categories:
|
|
454
457
|
if ('fn' in result_types_present_b) and \
|
|
455
458
|
('fp' not in result_types_present_b) and \
|
|
@@ -459,9 +462,9 @@ def _result_types_to_comparison_category(result_types_present_a,
|
|
|
459
462
|
# has any mistakse
|
|
460
463
|
if ('fn' in result_types_present_b) or ('fp' in result_types_present_b):
|
|
461
464
|
return 'tp_a_only'
|
|
462
|
-
|
|
465
|
+
|
|
463
466
|
if ('tp' in result_types_present_b) and (len(result_types_present_b) == 1):
|
|
464
|
-
# Clean TPs are cases where the other model has only FNs, no FPs
|
|
467
|
+
# Clean TPs are cases where the other model has only FNs, no FPs
|
|
465
468
|
if options.include_clean_categories:
|
|
466
469
|
if ('fn' in result_types_present_a) and \
|
|
467
470
|
('fp' not in result_types_present_a) and \
|
|
@@ -471,7 +474,7 @@ def _result_types_to_comparison_category(result_types_present_a,
|
|
|
471
474
|
# has any mistakse
|
|
472
475
|
if ('fn' in result_types_present_a) or ('fp' in result_types_present_a):
|
|
473
476
|
return 'tp_b_only'
|
|
474
|
-
|
|
477
|
+
|
|
475
478
|
# The tn-only categories are for the case where one model has a TN and the
|
|
476
479
|
# other has at least one fp
|
|
477
480
|
if 'tn' in result_types_present_a and 'fp' in result_types_present_b:
|
|
@@ -482,7 +485,7 @@ def _result_types_to_comparison_category(result_types_present_a,
|
|
|
482
485
|
assert len(result_types_present_a) == 1
|
|
483
486
|
assert len(result_types_present_b) == 1
|
|
484
487
|
return 'tn_b_only'
|
|
485
|
-
|
|
488
|
+
|
|
486
489
|
# The 'fpfn' category is for everything else
|
|
487
490
|
return 'fpfn'
|
|
488
491
|
|
|
@@ -491,18 +494,18 @@ def _result_types_to_comparison_category(result_types_present_a,
|
|
|
491
494
|
|
|
492
495
|
def _subset_md_results(results,options):
|
|
493
496
|
"""
|
|
494
|
-
Subset a set of MegaDetector results according to the rules defined in the
|
|
497
|
+
Subset a set of MegaDetector results according to the rules defined in the
|
|
495
498
|
BatchComparisonOptions object [options]. Typically used to filter for files
|
|
496
499
|
containing a particular string. Modifies [results] in place, also returns.
|
|
497
|
-
|
|
500
|
+
|
|
498
501
|
Args:
|
|
499
502
|
results (dict): MD results
|
|
500
503
|
options (BatchComparisonOptions): job options containing filtering rules
|
|
501
504
|
"""
|
|
502
|
-
|
|
505
|
+
|
|
503
506
|
if options.required_token is None:
|
|
504
507
|
return results
|
|
505
|
-
|
|
508
|
+
|
|
506
509
|
images_to_keep = []
|
|
507
510
|
for im in results['images']:
|
|
508
511
|
# Is [required_token] a string?
|
|
@@ -514,29 +517,29 @@ def _subset_md_results(results,options):
|
|
|
514
517
|
assert callable(options.required_token), 'Illegal value for required_token'
|
|
515
518
|
if options.required_token(im['file']):
|
|
516
519
|
images_to_keep.append(im)
|
|
517
|
-
|
|
518
|
-
|
|
520
|
+
|
|
521
|
+
|
|
519
522
|
if options.verbose:
|
|
520
523
|
print('Keeping {} of {} images in MD results'.format(
|
|
521
524
|
len(images_to_keep),len(results['images'])))
|
|
522
|
-
|
|
525
|
+
|
|
523
526
|
results['images'] = images_to_keep
|
|
524
527
|
return results
|
|
525
|
-
|
|
528
|
+
|
|
526
529
|
# ...def _subset_md_results(...)
|
|
527
530
|
|
|
528
531
|
|
|
529
532
|
def _subset_ground_truth(gt_data,options):
|
|
530
533
|
"""
|
|
531
|
-
Subset a set of COCO annotations according to the rules defined in the
|
|
534
|
+
Subset a set of COCO annotations according to the rules defined in the
|
|
532
535
|
BatchComparisonOptions object [options]. Typically used to filter for files
|
|
533
536
|
containing a particular string. Modifies [results] in place, also returns.
|
|
534
|
-
|
|
537
|
+
|
|
535
538
|
Args:
|
|
536
539
|
gt_data (dict): COCO-formatted annotations
|
|
537
540
|
options (BatchComparisonOptions): job options containing filtering rules
|
|
538
541
|
"""
|
|
539
|
-
|
|
542
|
+
|
|
540
543
|
if options.required_token is None:
|
|
541
544
|
return gt_data
|
|
542
545
|
|
|
@@ -548,22 +551,22 @@ def _subset_ground_truth(gt_data,options):
|
|
|
548
551
|
else:
|
|
549
552
|
if options.required_token(im['file_name']):
|
|
550
553
|
images_to_keep.append(im)
|
|
551
|
-
|
|
554
|
+
|
|
552
555
|
image_ids_to_keep_set = set([im['id'] for im in images_to_keep])
|
|
553
|
-
|
|
556
|
+
|
|
554
557
|
annotations_to_keep = []
|
|
555
558
|
for ann in gt_data['annotations']:
|
|
556
559
|
if ann['image_id'] in image_ids_to_keep_set:
|
|
557
560
|
annotations_to_keep.append(ann)
|
|
558
|
-
|
|
561
|
+
|
|
559
562
|
if options.verbose:
|
|
560
563
|
print('Keeping {} of {} images, {} of {} annotations in GT data'.format(
|
|
561
564
|
len(images_to_keep),len(gt_data['images']),
|
|
562
565
|
len(annotations_to_keep),len(gt_data['annotations'])))
|
|
563
|
-
|
|
566
|
+
|
|
564
567
|
gt_data['images'] = images_to_keep
|
|
565
568
|
gt_data['annotations'] = annotations_to_keep
|
|
566
|
-
|
|
569
|
+
|
|
567
570
|
return gt_data
|
|
568
571
|
|
|
569
572
|
# ...def _subset_ground_truth(...)
|
|
@@ -571,41 +574,41 @@ def _subset_ground_truth(gt_data,options):
|
|
|
571
574
|
|
|
572
575
|
def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
573
576
|
"""
|
|
574
|
-
The main entry point for this module is compare_batch_results(), which calls
|
|
577
|
+
The main entry point for this module is compare_batch_results(), which calls
|
|
575
578
|
this function for each pair of comparisons the caller has requested. Generates an
|
|
576
579
|
HTML page for this comparison. Returns a BatchComparisonResults object.
|
|
577
|
-
|
|
580
|
+
|
|
578
581
|
Args:
|
|
579
582
|
options (BatchComparisonOptions): overall job options for this comparison group
|
|
580
|
-
output_index (int): a numeric index used for generating HTML titles
|
|
583
|
+
output_index (int): a numeric index used for generating HTML titles
|
|
581
584
|
pairwise_options (PairwiseBatchComparisonOptions): job options for this comparison
|
|
582
|
-
|
|
585
|
+
|
|
583
586
|
Returns:
|
|
584
587
|
PairwiseBatchComparisonResults: the results of this pairwise comparison
|
|
585
588
|
"""
|
|
586
|
-
|
|
589
|
+
|
|
587
590
|
# pairwise_options is passed as a parameter here, and should not be specified
|
|
588
591
|
# in the options object.
|
|
589
592
|
assert options.pairwise_options is None
|
|
590
|
-
|
|
593
|
+
|
|
591
594
|
if options.random_seed is not None:
|
|
592
595
|
random.seed(options.random_seed)
|
|
593
596
|
|
|
594
597
|
# Warn the user if some "detections" might not get rendered
|
|
595
598
|
max_classification_threshold_a = max(list(pairwise_options.detection_thresholds_a.values()))
|
|
596
599
|
max_classification_threshold_b = max(list(pairwise_options.detection_thresholds_b.values()))
|
|
597
|
-
|
|
600
|
+
|
|
598
601
|
if pairwise_options.rendering_confidence_threshold_a > max_classification_threshold_a:
|
|
599
602
|
print('*** Warning: rendering threshold A ({}) is higher than max confidence threshold A ({}) ***'.format(
|
|
600
603
|
pairwise_options.rendering_confidence_threshold_a,max_classification_threshold_a))
|
|
601
|
-
|
|
604
|
+
|
|
602
605
|
if pairwise_options.rendering_confidence_threshold_b > max_classification_threshold_b:
|
|
603
606
|
print('*** Warning: rendering threshold B ({}) is higher than max confidence threshold B ({}) ***'.format(
|
|
604
607
|
pairwise_options.rendering_confidence_threshold_b,max_classification_threshold_b))
|
|
605
|
-
|
|
608
|
+
|
|
606
609
|
|
|
607
610
|
##%% Validate inputs
|
|
608
|
-
|
|
611
|
+
|
|
609
612
|
assert os.path.isfile(pairwise_options.results_filename_a), \
|
|
610
613
|
"Can't find results file {}".format(pairwise_options.results_filename_a)
|
|
611
614
|
assert os.path.isfile(pairwise_options.results_filename_b), \
|
|
@@ -613,16 +616,16 @@ def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
|
613
616
|
assert os.path.isdir(options.image_folder), \
|
|
614
617
|
"Can't find image folder {}".format(options.image_folder)
|
|
615
618
|
os.makedirs(options.output_folder,exist_ok=True)
|
|
616
|
-
|
|
617
|
-
|
|
619
|
+
|
|
620
|
+
|
|
618
621
|
##%% Load both result sets
|
|
619
|
-
|
|
622
|
+
|
|
620
623
|
with open(pairwise_options.results_filename_a,'r') as f:
|
|
621
624
|
results_a = json.load(f)
|
|
622
|
-
|
|
625
|
+
|
|
623
626
|
with open(pairwise_options.results_filename_b,'r') as f:
|
|
624
627
|
results_b = json.load(f)
|
|
625
|
-
|
|
628
|
+
|
|
626
629
|
# Don't let path separators confuse things
|
|
627
630
|
for im in results_a['images']:
|
|
628
631
|
if 'file' in im:
|
|
@@ -630,47 +633,47 @@ def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
|
630
633
|
for im in results_b['images']:
|
|
631
634
|
if 'file' in im:
|
|
632
635
|
im['file'] = im['file'].replace('\\','/')
|
|
633
|
-
|
|
636
|
+
|
|
634
637
|
if not options.class_agnostic_comparison:
|
|
635
638
|
assert results_a['detection_categories'] == results_b['detection_categories'], \
|
|
636
639
|
"Cannot perform a class-sensitive comparison across results with different categories"
|
|
637
|
-
|
|
640
|
+
|
|
638
641
|
detection_categories_a = results_a['detection_categories']
|
|
639
642
|
detection_categories_b = results_b['detection_categories']
|
|
640
|
-
detection_category_id_to_name = detection_categories_a
|
|
643
|
+
detection_category_id_to_name = detection_categories_a
|
|
641
644
|
detection_category_name_to_id = invert_dictionary(detection_categories_a)
|
|
642
645
|
options.detection_category_id_to_name = detection_category_id_to_name
|
|
643
|
-
|
|
646
|
+
|
|
644
647
|
if pairwise_options.results_description_a is None:
|
|
645
648
|
if 'detector' not in results_a['info']:
|
|
646
649
|
print('No model metadata supplied for results-A, assuming MDv4')
|
|
647
650
|
pairwise_options.results_description_a = 'MDv4 (assumed)'
|
|
648
|
-
else:
|
|
651
|
+
else:
|
|
649
652
|
pairwise_options.results_description_a = results_a['info']['detector']
|
|
650
|
-
|
|
653
|
+
|
|
651
654
|
if pairwise_options.results_description_b is None:
|
|
652
655
|
if 'detector' not in results_b['info']:
|
|
653
656
|
print('No model metadata supplied for results-B, assuming MDv4')
|
|
654
657
|
pairwise_options.results_description_b = 'MDv4 (assumed)'
|
|
655
|
-
else:
|
|
658
|
+
else:
|
|
656
659
|
pairwise_options.results_description_b = results_b['info']['detector']
|
|
657
|
-
|
|
660
|
+
|
|
658
661
|
# Restrict this comparison to specific files if requested
|
|
659
662
|
results_a = _subset_md_results(results_a, options)
|
|
660
663
|
results_b = _subset_md_results(results_b, options)
|
|
661
|
-
|
|
664
|
+
|
|
662
665
|
images_a = results_a['images']
|
|
663
666
|
images_b = results_b['images']
|
|
664
|
-
|
|
667
|
+
|
|
665
668
|
filename_to_image_a = {im['file']:im for im in images_a}
|
|
666
669
|
filename_to_image_b = {im['file']:im for im in images_b}
|
|
667
|
-
|
|
668
|
-
|
|
670
|
+
|
|
671
|
+
|
|
669
672
|
##%% Make sure they represent the same set of images
|
|
670
|
-
|
|
673
|
+
|
|
671
674
|
filenames_a = [im['file'] for im in images_a]
|
|
672
675
|
filenames_b_set = set([im['file'] for im in images_b])
|
|
673
|
-
|
|
676
|
+
|
|
674
677
|
if len(images_a) != len(images_b):
|
|
675
678
|
s = 'set A has {} images, set B has {}'.format(len(images_a),len(images_b))
|
|
676
679
|
if options.error_on_non_matching_lists:
|
|
@@ -683,57 +686,57 @@ def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
|
683
686
|
assert fn in filenames_b_set
|
|
684
687
|
|
|
685
688
|
assert len(filenames_a) == len(images_a)
|
|
686
|
-
assert len(filenames_b_set) == len(images_b)
|
|
687
|
-
|
|
689
|
+
assert len(filenames_b_set) == len(images_b)
|
|
690
|
+
|
|
688
691
|
if options.filenames_to_include is None:
|
|
689
692
|
filenames_to_compare = filenames_a
|
|
690
693
|
else:
|
|
691
694
|
filenames_to_compare = options.filenames_to_include
|
|
692
|
-
|
|
693
|
-
|
|
695
|
+
|
|
696
|
+
|
|
694
697
|
##%% Determine whether ground truth is available
|
|
695
|
-
|
|
698
|
+
|
|
696
699
|
# ...and determine what type of GT is available, boxes or image-level labels
|
|
697
|
-
|
|
700
|
+
|
|
698
701
|
gt_data = None
|
|
699
702
|
gt_category_id_to_detection_category_id = None
|
|
700
|
-
|
|
703
|
+
|
|
701
704
|
if options.ground_truth_file is None:
|
|
702
|
-
|
|
705
|
+
|
|
703
706
|
ground_truth_type = 'no_gt'
|
|
704
|
-
|
|
707
|
+
|
|
705
708
|
else:
|
|
706
|
-
|
|
709
|
+
|
|
707
710
|
# Read ground truth data if necessary
|
|
708
|
-
if isinstance(options.ground_truth_file,dict):
|
|
709
|
-
gt_data = options.ground_truth_file
|
|
710
|
-
else:
|
|
711
|
+
if isinstance(options.ground_truth_file,dict):
|
|
712
|
+
gt_data = options.ground_truth_file
|
|
713
|
+
else:
|
|
711
714
|
assert isinstance(options.ground_truth_file,str)
|
|
712
715
|
with open(options.ground_truth_file,'r') as f:
|
|
713
716
|
gt_data = json.load(f)
|
|
714
|
-
|
|
717
|
+
|
|
715
718
|
# Restrict this comparison to specific files if requested
|
|
716
719
|
gt_data = _subset_ground_truth(gt_data, options)
|
|
717
|
-
|
|
720
|
+
|
|
718
721
|
# Do we have box-level ground truth or image-level ground truth?
|
|
719
722
|
found_box = False
|
|
720
|
-
|
|
723
|
+
|
|
721
724
|
for ann in gt_data['annotations']:
|
|
722
725
|
if 'bbox' in ann:
|
|
723
726
|
found_box = True
|
|
724
727
|
break
|
|
725
|
-
|
|
728
|
+
|
|
726
729
|
if found_box:
|
|
727
730
|
ground_truth_type = 'bbox_gt'
|
|
728
731
|
else:
|
|
729
732
|
ground_truth_type = 'image_level_gt'
|
|
730
|
-
|
|
733
|
+
|
|
731
734
|
gt_category_name_to_id = {c['name']:c['id'] for c in gt_data['categories']}
|
|
732
735
|
gt_category_id_to_name = invert_dictionary(gt_category_name_to_id)
|
|
733
736
|
options.gt_category_id_to_name = gt_category_id_to_name
|
|
734
|
-
|
|
737
|
+
|
|
735
738
|
if ground_truth_type == 'bbox_gt':
|
|
736
|
-
|
|
739
|
+
|
|
737
740
|
if not options.class_agnostic_comparison:
|
|
738
741
|
assert set(gt_category_name_to_id.keys()) == set(detection_category_name_to_id.keys()), \
|
|
739
742
|
'Cannot compare detections to GT with different categories when class_agnostic_comparison is False'
|
|
@@ -742,50 +745,50 @@ def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
|
742
745
|
gt_category_id = gt_category_name_to_id[category_name]
|
|
743
746
|
detection_category_id = detection_category_name_to_id[category_name]
|
|
744
747
|
gt_category_id_to_detection_category_id[gt_category_id] = detection_category_id
|
|
745
|
-
|
|
748
|
+
|
|
746
749
|
elif ground_truth_type == 'image_level_gt':
|
|
747
|
-
|
|
750
|
+
|
|
748
751
|
if not options.class_agnostic_comparison:
|
|
749
752
|
for detection_category_name in detection_category_name_to_id:
|
|
750
753
|
if detection_category_name not in gt_category_name_to_id:
|
|
751
754
|
raise ValueError('Detection category {} not available in GT category list'.format(
|
|
752
|
-
detection_category_name))
|
|
755
|
+
detection_category_name))
|
|
753
756
|
for gt_category_name in gt_category_name_to_id:
|
|
754
757
|
if gt_category_name in options.gt_empty_categories:
|
|
755
758
|
continue
|
|
756
759
|
if (gt_category_name not in detection_category_name_to_id):
|
|
757
760
|
raise ValueError('GT category {} not available in detection category list'.format(
|
|
758
761
|
gt_category_name))
|
|
759
|
-
|
|
762
|
+
|
|
760
763
|
assert ground_truth_type in ('no_gt','bbox_gt','image_level_gt')
|
|
761
|
-
|
|
764
|
+
|
|
762
765
|
# Make sure ground truth data refers to at least *some* of the same files that are in our
|
|
763
|
-
# results files
|
|
766
|
+
# results files
|
|
764
767
|
if gt_data is not None:
|
|
765
|
-
|
|
768
|
+
|
|
766
769
|
filenames_to_compare_set = set(filenames_to_compare)
|
|
767
770
|
gt_filenames = [im['file_name'] for im in gt_data['images']]
|
|
768
771
|
gt_filenames_set = set(gt_filenames)
|
|
769
|
-
|
|
772
|
+
|
|
770
773
|
common_filenames = filenames_to_compare_set.intersection(gt_filenames_set)
|
|
771
774
|
assert len(common_filenames) > 0, 'MD results files and ground truth file have no images in common'
|
|
772
|
-
|
|
775
|
+
|
|
773
776
|
filenames_only_in_gt = gt_filenames_set.difference(filenames_to_compare_set)
|
|
774
777
|
if len(filenames_only_in_gt) > 0:
|
|
775
778
|
print('Warning: {} files are only available in the ground truth (not in MD results)'.format(
|
|
776
779
|
len(filenames_only_in_gt)))
|
|
777
|
-
|
|
780
|
+
|
|
778
781
|
filenames_only_in_results = gt_filenames_set.difference(gt_filenames)
|
|
779
782
|
if len(filenames_only_in_results) > 0:
|
|
780
783
|
print('Warning: {} files are only available in the MD results (not in ground truth)'.format(
|
|
781
784
|
len(filenames_only_in_results)))
|
|
782
|
-
|
|
785
|
+
|
|
783
786
|
if options.error_on_non_matching_lists:
|
|
784
787
|
if len(filenames_only_in_gt) > 0 or len(filenames_only_in_results) > 0:
|
|
785
|
-
raise ValueError('GT image set is not identical to result image sets')
|
|
786
|
-
|
|
788
|
+
raise ValueError('GT image set is not identical to result image sets')
|
|
789
|
+
|
|
787
790
|
filenames_to_compare = sorted(list(common_filenames))
|
|
788
|
-
|
|
791
|
+
|
|
789
792
|
# Map filenames to ground truth images and annotations
|
|
790
793
|
filename_to_image_gt = {im['file_name']:im for im in gt_data['images']}
|
|
791
794
|
gt_image_id_to_image = {}
|
|
@@ -793,39 +796,39 @@ def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
|
793
796
|
gt_image_id_to_image[im['id']] = im
|
|
794
797
|
gt_image_id_to_annotations = defaultdict(list)
|
|
795
798
|
for ann in gt_data['annotations']:
|
|
796
|
-
gt_image_id_to_annotations[ann['image_id']].append(ann)
|
|
797
|
-
|
|
799
|
+
gt_image_id_to_annotations[ann['image_id']].append(ann)
|
|
800
|
+
|
|
798
801
|
# Convert annotations to relative (MD) coordinates
|
|
799
|
-
|
|
802
|
+
|
|
800
803
|
# ann = gt_data['annotations'][0]
|
|
801
804
|
for ann in gt_data['annotations']:
|
|
802
805
|
gt_image = gt_image_id_to_image[ann['image_id']]
|
|
803
806
|
if 'bbox' not in ann:
|
|
804
807
|
continue
|
|
805
808
|
# COCO format: [x,y,width,height]
|
|
806
|
-
# normalized format: [x_min, y_min, width_of_box, height_of_box]
|
|
809
|
+
# normalized format: [x_min, y_min, width_of_box, height_of_box]
|
|
807
810
|
normalized_bbox = [ann['bbox'][0]/gt_image['width'],ann['bbox'][1]/gt_image['height'],
|
|
808
|
-
ann['bbox'][2]/gt_image['width'],ann['bbox'][3]/gt_image['height']]
|
|
811
|
+
ann['bbox'][2]/gt_image['width'],ann['bbox'][3]/gt_image['height']]
|
|
809
812
|
ann['normalized_bbox'] = normalized_bbox
|
|
810
|
-
|
|
811
|
-
|
|
813
|
+
|
|
814
|
+
|
|
812
815
|
##%% Find differences
|
|
813
|
-
|
|
816
|
+
|
|
814
817
|
# See PairwiseBatchComparisonResults for a description
|
|
815
818
|
categories_to_image_pairs = {}
|
|
816
|
-
|
|
817
|
-
# This will map category names that can be used in filenames (e.g. "common_non_detections" or
|
|
819
|
+
|
|
820
|
+
# This will map category names that can be used in filenames (e.g. "common_non_detections" or
|
|
818
821
|
# "false_positives_a_only" to friendly names (e.g. "Common non-detections")
|
|
819
822
|
categories_to_page_titles = None
|
|
820
|
-
|
|
823
|
+
|
|
821
824
|
if ground_truth_type == 'no_gt':
|
|
822
|
-
|
|
825
|
+
|
|
823
826
|
categories_to_image_pairs['common_detections'] = {}
|
|
824
827
|
categories_to_image_pairs['common_non_detections'] = {}
|
|
825
828
|
categories_to_image_pairs['detections_a_only'] = {}
|
|
826
829
|
categories_to_image_pairs['detections_b_only'] = {}
|
|
827
830
|
categories_to_image_pairs['class_transitions'] = {}
|
|
828
|
-
|
|
831
|
+
|
|
829
832
|
categories_to_page_titles = {
|
|
830
833
|
'common_detections':'Detections common to both models',
|
|
831
834
|
'common_non_detections':'Non-detections common to both models',
|
|
@@ -833,22 +836,22 @@ def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
|
833
836
|
'detections_b_only':'Detections reported by model B only',
|
|
834
837
|
'class_transitions':'Detections reported as different classes by models A and B'
|
|
835
838
|
}
|
|
836
|
-
|
|
837
|
-
|
|
839
|
+
|
|
840
|
+
|
|
838
841
|
elif (ground_truth_type == 'bbox_gt') or (ground_truth_type == 'image_level_gt'):
|
|
839
|
-
|
|
842
|
+
|
|
840
843
|
categories_to_image_pairs['common_tp'] = {}
|
|
841
844
|
categories_to_image_pairs['common_tn'] = {}
|
|
842
845
|
categories_to_image_pairs['common_fp'] = {}
|
|
843
846
|
categories_to_image_pairs['common_fn'] = {}
|
|
844
|
-
|
|
847
|
+
|
|
845
848
|
categories_to_image_pairs['tp_a_only'] = {}
|
|
846
849
|
categories_to_image_pairs['tp_b_only'] = {}
|
|
847
850
|
categories_to_image_pairs['tn_a_only'] = {}
|
|
848
851
|
categories_to_image_pairs['tn_b_only'] = {}
|
|
849
|
-
|
|
852
|
+
|
|
850
853
|
categories_to_image_pairs['fpfn'] = {}
|
|
851
|
-
|
|
854
|
+
|
|
852
855
|
categories_to_page_titles = {
|
|
853
856
|
'common_tp':'Common true positives',
|
|
854
857
|
'common_tn':'Common true negatives',
|
|
@@ -860,28 +863,28 @@ def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
|
860
863
|
'tn_b_only':'TN (B only)',
|
|
861
864
|
'fpfn':'More complicated discrepancies'
|
|
862
865
|
}
|
|
863
|
-
|
|
866
|
+
|
|
864
867
|
if options.include_clean_categories:
|
|
865
|
-
|
|
868
|
+
|
|
866
869
|
categories_to_image_pairs['clean_tp_a_only'] = {}
|
|
867
870
|
categories_to_image_pairs['clean_tp_b_only'] = {}
|
|
868
871
|
# categories_to_image_pairs['clean_tn_a_only'] = {}
|
|
869
872
|
# categories_to_image_pairs['clean_tn_b_only'] = {}
|
|
870
|
-
|
|
873
|
+
|
|
871
874
|
categories_to_page_titles['clean_tp_a_only'] = 'Clean TP wins for A'
|
|
872
875
|
categories_to_page_titles['clean_tp_b_only'] = 'Clean TP wins for B'
|
|
873
876
|
# categories_to_page_titles['clean_tn_a_only'] = 'Clean TN wins for A'
|
|
874
877
|
# categories_to_page_titles['clean_tn_b_only'] = 'Clean TN wins for B'
|
|
875
|
-
|
|
876
|
-
|
|
878
|
+
|
|
879
|
+
|
|
877
880
|
else:
|
|
878
|
-
|
|
881
|
+
|
|
879
882
|
raise Exception('Unknown ground truth type: {}'.format(ground_truth_type))
|
|
880
|
-
|
|
883
|
+
|
|
881
884
|
# Map category IDs to thresholds
|
|
882
885
|
category_id_to_threshold_a = {}
|
|
883
886
|
category_id_to_threshold_b = {}
|
|
884
|
-
|
|
887
|
+
|
|
885
888
|
for category_id in detection_categories_a:
|
|
886
889
|
category_name = detection_categories_a[category_id]
|
|
887
890
|
if category_name in pairwise_options.detection_thresholds_a:
|
|
@@ -890,7 +893,7 @@ def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
|
890
893
|
else:
|
|
891
894
|
category_id_to_threshold_a[category_id] = \
|
|
892
895
|
pairwise_options.detection_thresholds_a['default']
|
|
893
|
-
|
|
896
|
+
|
|
894
897
|
for category_id in detection_categories_b:
|
|
895
898
|
category_name = detection_categories_b[category_id]
|
|
896
899
|
if category_name in pairwise_options.detection_thresholds_b:
|
|
@@ -899,142 +902,142 @@ def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
|
899
902
|
else:
|
|
900
903
|
category_id_to_threshold_b[category_id] = \
|
|
901
904
|
pairwise_options.detection_thresholds_b['default']
|
|
902
|
-
|
|
905
|
+
|
|
903
906
|
# fn = filenames_to_compare[0]
|
|
904
907
|
for i_file,fn in tqdm(enumerate(filenames_to_compare),total=len(filenames_to_compare)):
|
|
905
|
-
|
|
908
|
+
|
|
906
909
|
if fn not in filename_to_image_b:
|
|
907
|
-
|
|
910
|
+
|
|
908
911
|
# We shouldn't have gotten this far if error_on_non_matching_lists is set
|
|
909
912
|
assert not options.error_on_non_matching_lists
|
|
910
|
-
|
|
913
|
+
|
|
911
914
|
print('Skipping filename {}, not in image set B'.format(fn))
|
|
912
915
|
continue
|
|
913
|
-
|
|
916
|
+
|
|
914
917
|
im_a = filename_to_image_a[fn]
|
|
915
918
|
im_b = filename_to_image_b[fn]
|
|
916
|
-
|
|
919
|
+
|
|
917
920
|
im_pair = {}
|
|
918
921
|
im_pair['im_a'] = im_a
|
|
919
922
|
im_pair['im_b'] = im_b
|
|
920
923
|
im_pair['im_gt'] = None
|
|
921
924
|
im_pair['annotations_gt'] = None
|
|
922
|
-
|
|
925
|
+
|
|
923
926
|
if gt_data is not None:
|
|
924
|
-
|
|
927
|
+
|
|
925
928
|
if fn not in filename_to_image_gt:
|
|
926
|
-
|
|
929
|
+
|
|
927
930
|
# We shouldn't have gotten this far if error_on_non_matching_lists is set
|
|
928
931
|
assert not options.error_on_non_matching_lists
|
|
929
|
-
|
|
932
|
+
|
|
930
933
|
print('Skipping filename {}, not in ground truth'.format(fn))
|
|
931
|
-
continue
|
|
932
|
-
|
|
934
|
+
continue
|
|
935
|
+
|
|
933
936
|
im_gt = filename_to_image_gt[fn]
|
|
934
937
|
annotations_gt = gt_image_id_to_annotations[im_gt['id']]
|
|
935
938
|
im_pair['im_gt'] = im_gt
|
|
936
939
|
im_pair['annotations_gt'] = annotations_gt
|
|
937
|
-
|
|
940
|
+
|
|
938
941
|
comparison_category = None
|
|
939
|
-
|
|
942
|
+
|
|
940
943
|
# Compare image A to image B, without ground truth
|
|
941
944
|
if ground_truth_type == 'no_gt':
|
|
942
|
-
|
|
945
|
+
|
|
943
946
|
categories_above_threshold_a = set()
|
|
944
947
|
|
|
945
|
-
if
|
|
948
|
+
if 'detections' not in im_a or im_a['detections'] is None:
|
|
946
949
|
assert 'failure' in im_a and im_a['failure'] is not None
|
|
947
950
|
continue
|
|
948
|
-
|
|
949
|
-
if
|
|
951
|
+
|
|
952
|
+
if 'detections' not in im_b or im_b['detections'] is None:
|
|
950
953
|
assert 'failure' in im_b and im_b['failure'] is not None
|
|
951
954
|
continue
|
|
952
|
-
|
|
955
|
+
|
|
953
956
|
invalid_category_error = False
|
|
954
957
|
|
|
955
958
|
# det = im_a['detections'][0]
|
|
956
959
|
for det in im_a['detections']:
|
|
957
|
-
|
|
960
|
+
|
|
958
961
|
category_id = det['category']
|
|
959
|
-
|
|
962
|
+
|
|
960
963
|
if category_id not in category_id_to_threshold_a:
|
|
961
964
|
print('Warning: unexpected category {} for model A on file {}'.format(category_id,fn))
|
|
962
965
|
invalid_category_error = True
|
|
963
966
|
break
|
|
964
|
-
|
|
965
|
-
conf = det['conf']
|
|
967
|
+
|
|
968
|
+
conf = det['conf']
|
|
966
969
|
conf_thresh = category_id_to_threshold_a[category_id]
|
|
967
970
|
if conf >= conf_thresh:
|
|
968
971
|
categories_above_threshold_a.add(category_id)
|
|
969
|
-
|
|
972
|
+
|
|
970
973
|
if invalid_category_error:
|
|
971
974
|
continue
|
|
972
|
-
|
|
975
|
+
|
|
973
976
|
categories_above_threshold_b = set()
|
|
974
|
-
|
|
977
|
+
|
|
975
978
|
for det in im_b['detections']:
|
|
976
|
-
|
|
979
|
+
|
|
977
980
|
category_id = det['category']
|
|
978
|
-
|
|
981
|
+
|
|
979
982
|
if category_id not in category_id_to_threshold_b:
|
|
980
983
|
print('Warning: unexpected category {} for model B on file {}'.format(category_id,fn))
|
|
981
984
|
invalid_category_error = True
|
|
982
985
|
break
|
|
983
|
-
|
|
984
|
-
conf = det['conf']
|
|
985
|
-
conf_thresh = category_id_to_threshold_b[category_id]
|
|
986
|
+
|
|
987
|
+
conf = det['conf']
|
|
988
|
+
conf_thresh = category_id_to_threshold_b[category_id]
|
|
986
989
|
if conf >= conf_thresh:
|
|
987
990
|
categories_above_threshold_b.add(category_id)
|
|
988
|
-
|
|
991
|
+
|
|
989
992
|
if invalid_category_error:
|
|
990
|
-
|
|
993
|
+
|
|
991
994
|
continue
|
|
992
|
-
|
|
995
|
+
|
|
993
996
|
# Should we be restricting the comparison to only certain categories?
|
|
994
997
|
if options.category_names_to_include is not None:
|
|
995
|
-
|
|
998
|
+
|
|
996
999
|
# Just in case the user provided a single category instead of a list
|
|
997
1000
|
if isinstance(options.category_names_to_include,str):
|
|
998
1001
|
options.category_names_to_include = [options.category_names_to_include]
|
|
999
|
-
|
|
1002
|
+
|
|
1000
1003
|
category_name_to_id_a = invert_dictionary(detection_categories_a)
|
|
1001
1004
|
category_name_to_id_b = invert_dictionary(detection_categories_b)
|
|
1002
1005
|
category_ids_to_include_a = []
|
|
1003
1006
|
category_ids_to_include_b = []
|
|
1004
|
-
|
|
1007
|
+
|
|
1005
1008
|
for category_name in options.category_names_to_include:
|
|
1006
1009
|
if category_name in category_name_to_id_a:
|
|
1007
1010
|
category_ids_to_include_a.append(category_name_to_id_a[category_name])
|
|
1008
1011
|
if category_name in category_name_to_id_b:
|
|
1009
1012
|
category_ids_to_include_b.append(category_name_to_id_b[category_name])
|
|
1010
|
-
|
|
1013
|
+
|
|
1011
1014
|
# Restrict the categories we treat as above-threshold to the set we're supposed
|
|
1012
1015
|
# to be using
|
|
1013
1016
|
categories_above_threshold_a = [category_id for category_id in categories_above_threshold_a if \
|
|
1014
1017
|
category_id in category_ids_to_include_a]
|
|
1015
1018
|
categories_above_threshold_b = [category_id for category_id in categories_above_threshold_b if \
|
|
1016
1019
|
category_id in category_ids_to_include_b]
|
|
1017
|
-
|
|
1020
|
+
|
|
1018
1021
|
detection_a = (len(categories_above_threshold_a) > 0)
|
|
1019
1022
|
detection_b = (len(categories_above_threshold_b) > 0)
|
|
1020
|
-
|
|
1021
|
-
if detection_a and detection_b:
|
|
1023
|
+
|
|
1024
|
+
if detection_a and detection_b:
|
|
1022
1025
|
if (categories_above_threshold_a == categories_above_threshold_b) or \
|
|
1023
|
-
options.class_agnostic_comparison:
|
|
1026
|
+
options.class_agnostic_comparison:
|
|
1024
1027
|
comparison_category = 'common_detections'
|
|
1025
|
-
else:
|
|
1028
|
+
else:
|
|
1026
1029
|
comparison_category = 'class_transitions'
|
|
1027
1030
|
elif (not detection_a) and (not detection_b):
|
|
1028
1031
|
comparison_category = 'common_non_detections'
|
|
1029
1032
|
elif detection_a and (not detection_b):
|
|
1030
|
-
comparison_category = 'detections_a_only'
|
|
1033
|
+
comparison_category = 'detections_a_only'
|
|
1031
1034
|
else:
|
|
1032
1035
|
assert detection_b and (not detection_a)
|
|
1033
|
-
comparison_category = 'detections_b_only'
|
|
1034
|
-
|
|
1036
|
+
comparison_category = 'detections_b_only'
|
|
1037
|
+
|
|
1035
1038
|
max_conf_a = _maxempty([det['conf'] for det in im_a['detections']])
|
|
1036
1039
|
max_conf_b = _maxempty([det['conf'] for det in im_b['detections']])
|
|
1037
|
-
|
|
1040
|
+
|
|
1038
1041
|
# Only used if sort_by_confidence is True
|
|
1039
1042
|
if comparison_category == 'common_detections':
|
|
1040
1043
|
sort_conf = max(max_conf_a,max_conf_b)
|
|
@@ -1049,11 +1052,11 @@ def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
|
1049
1052
|
else:
|
|
1050
1053
|
print('Warning: unknown comparison category {}'.format(comparison_category))
|
|
1051
1054
|
sort_conf = max(max_conf_a,max_conf_b)
|
|
1052
|
-
|
|
1055
|
+
|
|
1053
1056
|
elif ground_truth_type == 'bbox_gt':
|
|
1054
|
-
|
|
1057
|
+
|
|
1055
1058
|
def _boxes_match(det,gt_ann):
|
|
1056
|
-
|
|
1059
|
+
|
|
1057
1060
|
# if we're doing class-sensitive comparisons, only match same-category classes
|
|
1058
1061
|
if not options.class_agnostic_comparison:
|
|
1059
1062
|
detection_category_id = det['category']
|
|
@@ -1061,140 +1064,140 @@ def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
|
1061
1064
|
if detection_category_id != \
|
|
1062
1065
|
gt_category_id_to_detection_category_id[gt_category_id]:
|
|
1063
1066
|
return False
|
|
1064
|
-
|
|
1067
|
+
|
|
1065
1068
|
if 'bbox' not in gt_ann:
|
|
1066
1069
|
return False
|
|
1067
|
-
|
|
1070
|
+
|
|
1068
1071
|
assert 'normalized_bbox' in gt_ann
|
|
1069
1072
|
iou = get_iou(det['bbox'],gt_ann['normalized_bbox'])
|
|
1070
|
-
|
|
1073
|
+
|
|
1071
1074
|
return iou >= options.gt_iou_threshold
|
|
1072
|
-
|
|
1075
|
+
|
|
1073
1076
|
# ...def _boxes_match(...)
|
|
1074
|
-
|
|
1077
|
+
|
|
1075
1078
|
# Categorize each model into TP/TN/FP/FN
|
|
1076
1079
|
def _categorize_image_with_box_gt(im_detection,im_gt,annotations_gt,category_id_to_threshold):
|
|
1077
|
-
|
|
1080
|
+
|
|
1078
1081
|
annotations_gt = [ann for ann in annotations_gt if 'bbox' in ann]
|
|
1079
|
-
|
|
1082
|
+
|
|
1080
1083
|
assert im_detection['file'] == im_gt['file_name']
|
|
1081
|
-
|
|
1084
|
+
|
|
1082
1085
|
# List of result types - tn, tp, fp, fn - present in this image. tn is
|
|
1083
1086
|
# mutually exclusive with the others.
|
|
1084
1087
|
result_types_present = set()
|
|
1085
|
-
|
|
1088
|
+
|
|
1086
1089
|
# Find detections above threshold
|
|
1087
1090
|
detections_above_threshold = []
|
|
1088
|
-
|
|
1091
|
+
|
|
1089
1092
|
# det = im_detection['detections'][0]
|
|
1090
1093
|
for det in im_detection['detections']:
|
|
1091
1094
|
category_id = det['category']
|
|
1092
1095
|
threshold = category_id_to_threshold[category_id]
|
|
1093
1096
|
if det['conf'] > threshold:
|
|
1094
1097
|
detections_above_threshold.append(det)
|
|
1095
|
-
|
|
1098
|
+
|
|
1096
1099
|
if len(detections_above_threshold) == 0 and len(annotations_gt) == 0:
|
|
1097
1100
|
result_types_present.add('tn')
|
|
1098
1101
|
return result_types_present
|
|
1099
|
-
|
|
1102
|
+
|
|
1100
1103
|
# Look for a match for each detection
|
|
1101
1104
|
#
|
|
1102
1105
|
# det = detections_above_threshold[0]
|
|
1103
1106
|
for det in detections_above_threshold:
|
|
1104
|
-
|
|
1107
|
+
|
|
1105
1108
|
det_matches_annotation = False
|
|
1106
|
-
|
|
1109
|
+
|
|
1107
1110
|
# gt_ann = annotations_gt[0]
|
|
1108
1111
|
for gt_ann in annotations_gt:
|
|
1109
1112
|
if _boxes_match(det, gt_ann):
|
|
1110
1113
|
det_matches_annotation = True
|
|
1111
|
-
break
|
|
1112
|
-
|
|
1114
|
+
break
|
|
1115
|
+
|
|
1113
1116
|
if det_matches_annotation:
|
|
1114
1117
|
result_types_present.add('tp')
|
|
1115
1118
|
else:
|
|
1116
1119
|
result_types_present.add('fp')
|
|
1117
|
-
|
|
1120
|
+
|
|
1118
1121
|
# Look for a match for each GT bbox
|
|
1119
1122
|
#
|
|
1120
1123
|
# gt_ann = annotations_gt[0]
|
|
1121
1124
|
for gt_ann in annotations_gt:
|
|
1122
|
-
|
|
1125
|
+
|
|
1123
1126
|
annotation_matches_det = False
|
|
1124
|
-
|
|
1127
|
+
|
|
1125
1128
|
for det in detections_above_threshold:
|
|
1126
|
-
|
|
1129
|
+
|
|
1127
1130
|
if _boxes_match(det, gt_ann):
|
|
1128
1131
|
annotation_matches_det = True
|
|
1129
|
-
break
|
|
1130
|
-
|
|
1132
|
+
break
|
|
1133
|
+
|
|
1131
1134
|
if annotation_matches_det:
|
|
1132
1135
|
# We should have found this when we looped over detections
|
|
1133
|
-
assert 'tp' in result_types_present
|
|
1136
|
+
assert 'tp' in result_types_present
|
|
1134
1137
|
else:
|
|
1135
1138
|
result_types_present.add('fn')
|
|
1136
|
-
|
|
1139
|
+
|
|
1137
1140
|
# ...for each above-threshold detection
|
|
1138
|
-
|
|
1141
|
+
|
|
1139
1142
|
return result_types_present
|
|
1140
|
-
|
|
1143
|
+
|
|
1141
1144
|
# ...def _categorize_image_with_box_gt(...)
|
|
1142
|
-
|
|
1145
|
+
|
|
1143
1146
|
# im_detection = im_a; category_id_to_threshold = category_id_to_threshold_a
|
|
1144
1147
|
result_types_present_a = \
|
|
1145
1148
|
_categorize_image_with_box_gt(im_a,im_gt,annotations_gt,category_id_to_threshold_a)
|
|
1146
1149
|
result_types_present_b = \
|
|
1147
1150
|
_categorize_image_with_box_gt(im_b,im_gt,annotations_gt,category_id_to_threshold_b)
|
|
1148
1151
|
|
|
1149
|
-
|
|
1152
|
+
|
|
1150
1153
|
## Some combinations are nonsense
|
|
1151
|
-
|
|
1154
|
+
|
|
1152
1155
|
# TNs are mutually exclusive with other categories
|
|
1153
1156
|
if 'tn' in result_types_present_a or 'tn' in result_types_present_b:
|
|
1154
1157
|
assert len(result_types_present_a) == 1
|
|
1155
1158
|
assert len(result_types_present_b) == 1
|
|
1156
|
-
|
|
1157
|
-
# If either model has a TP or FN, the other has to have a TP or FN, since
|
|
1159
|
+
|
|
1160
|
+
# If either model has a TP or FN, the other has to have a TP or FN, since
|
|
1158
1161
|
# there was something in the GT
|
|
1159
1162
|
if ('tp' in result_types_present_a) or ('fn' in result_types_present_a):
|
|
1160
1163
|
assert 'tp' in result_types_present_b or 'fn' in result_types_present_b
|
|
1161
1164
|
if ('tp' in result_types_present_b) or ('fn' in result_types_present_b):
|
|
1162
1165
|
assert 'tp' in result_types_present_a or 'fn' in result_types_present_a
|
|
1163
|
-
|
|
1164
|
-
# If either model has a TP or FN, the other has to have a TP or FN, since
|
|
1166
|
+
|
|
1167
|
+
# If either model has a TP or FN, the other has to have a TP or FN, since
|
|
1165
1168
|
# there was something in the GT
|
|
1166
1169
|
if ('tp' in result_types_present_a) or ('fn' in result_types_present_a):
|
|
1167
1170
|
assert 'tp' in result_types_present_b or 'fn' in result_types_present_b
|
|
1168
1171
|
if ('tp' in result_types_present_b) or ('fn' in result_types_present_b):
|
|
1169
1172
|
assert 'tp' in result_types_present_a or 'fn' in result_types_present_a
|
|
1170
|
-
|
|
1171
|
-
|
|
1173
|
+
|
|
1174
|
+
|
|
1172
1175
|
## Choose a comparison category based on result types
|
|
1173
|
-
|
|
1176
|
+
|
|
1174
1177
|
comparison_category = _result_types_to_comparison_category(
|
|
1175
1178
|
result_types_present_a,result_types_present_b,ground_truth_type,options)
|
|
1176
|
-
|
|
1179
|
+
|
|
1177
1180
|
# TODO: this may or may not be the right way to interpret sorting
|
|
1178
1181
|
# by confidence in this case, e.g., we may want to sort by confidence
|
|
1179
1182
|
# of correct or incorrect matches. But this isn't *wrong*.
|
|
1180
1183
|
max_conf_a = _maxempty([det['conf'] for det in im_a['detections']])
|
|
1181
1184
|
max_conf_b = _maxempty([det['conf'] for det in im_b['detections']])
|
|
1182
1185
|
sort_conf = max(max_conf_a,max_conf_b)
|
|
1183
|
-
|
|
1186
|
+
|
|
1184
1187
|
else:
|
|
1185
|
-
|
|
1188
|
+
|
|
1186
1189
|
# Categorize each model into TP/TN/FP/FN
|
|
1187
1190
|
def _categorize_image_with_image_level_gt(im_detection,im_gt,annotations_gt,
|
|
1188
1191
|
category_id_to_threshold):
|
|
1189
|
-
|
|
1192
|
+
|
|
1190
1193
|
assert im_detection['file'] == im_gt['file_name']
|
|
1191
|
-
|
|
1194
|
+
|
|
1192
1195
|
# List of result types - tn, tp, fp, fn - present in this image.
|
|
1193
1196
|
result_types_present = set()
|
|
1194
|
-
|
|
1197
|
+
|
|
1195
1198
|
# Find detections above threshold
|
|
1196
1199
|
category_names_detected = set()
|
|
1197
|
-
|
|
1200
|
+
|
|
1198
1201
|
# det = im_detection['detections'][0]
|
|
1199
1202
|
for det in im_detection['detections']:
|
|
1200
1203
|
category_id = det['category']
|
|
@@ -1202,148 +1205,150 @@ def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
|
1202
1205
|
if det['conf'] > threshold:
|
|
1203
1206
|
category_name = detection_category_id_to_name[det['category']]
|
|
1204
1207
|
category_names_detected.add(category_name)
|
|
1205
|
-
|
|
1208
|
+
|
|
1206
1209
|
category_names_in_gt = set()
|
|
1207
|
-
|
|
1210
|
+
|
|
1208
1211
|
# ann = annotations_gt[0]
|
|
1209
1212
|
for ann in annotations_gt:
|
|
1210
1213
|
category_name = gt_category_id_to_name[ann['category_id']]
|
|
1211
1214
|
category_names_in_gt.add(category_name)
|
|
1212
|
-
|
|
1215
|
+
|
|
1213
1216
|
for category_name in category_names_detected:
|
|
1214
|
-
|
|
1217
|
+
|
|
1215
1218
|
if category_name in category_names_in_gt:
|
|
1216
1219
|
result_types_present.add('tp')
|
|
1217
1220
|
else:
|
|
1218
1221
|
result_types_present.add('fp')
|
|
1219
|
-
|
|
1222
|
+
|
|
1220
1223
|
for category_name in category_names_in_gt:
|
|
1221
|
-
|
|
1224
|
+
|
|
1222
1225
|
# Is this an empty image?
|
|
1223
1226
|
if category_name in options.gt_empty_categories:
|
|
1224
|
-
|
|
1227
|
+
|
|
1225
1228
|
assert all([cn in options.gt_empty_categories for cn in category_names_in_gt]), \
|
|
1226
1229
|
'Image {} has both empty and non-empty ground truth labels'.format(
|
|
1227
1230
|
im_detection['file'])
|
|
1228
|
-
if len(category_names_detected) > 0:
|
|
1231
|
+
if len(category_names_detected) > 0:
|
|
1229
1232
|
result_types_present.add('fp')
|
|
1230
1233
|
# If there is a false positive present in an empty image, there can't
|
|
1231
1234
|
# be any other result types present
|
|
1232
1235
|
assert len(result_types_present) == 1
|
|
1233
1236
|
else:
|
|
1234
1237
|
result_types_present.add('tn')
|
|
1235
|
-
|
|
1238
|
+
|
|
1236
1239
|
elif category_name in category_names_detected:
|
|
1237
|
-
|
|
1240
|
+
|
|
1238
1241
|
assert 'tp' in result_types_present
|
|
1239
|
-
|
|
1242
|
+
|
|
1240
1243
|
else:
|
|
1241
|
-
|
|
1244
|
+
|
|
1242
1245
|
result_types_present.add('fn')
|
|
1243
|
-
|
|
1246
|
+
|
|
1244
1247
|
return result_types_present
|
|
1245
|
-
|
|
1248
|
+
|
|
1246
1249
|
# ...def _categorize_image_with_image_level_gt(...)
|
|
1247
|
-
|
|
1250
|
+
|
|
1248
1251
|
# im_detection = im_a; category_id_to_threshold = category_id_to_threshold_a
|
|
1249
1252
|
result_types_present_a = \
|
|
1250
1253
|
_categorize_image_with_image_level_gt(im_a,im_gt,annotations_gt,category_id_to_threshold_a)
|
|
1251
1254
|
result_types_present_b = \
|
|
1252
1255
|
_categorize_image_with_image_level_gt(im_b,im_gt,annotations_gt,category_id_to_threshold_b)
|
|
1253
|
-
|
|
1254
|
-
|
|
1256
|
+
|
|
1257
|
+
|
|
1255
1258
|
## Some combinations are nonsense
|
|
1256
|
-
|
|
1257
|
-
# If either model has a TP or FN, the other has to have a TP or FN, since
|
|
1259
|
+
|
|
1260
|
+
# If either model has a TP or FN, the other has to have a TP or FN, since
|
|
1258
1261
|
# there was something in the GT
|
|
1259
1262
|
if ('tp' in result_types_present_a) or ('fn' in result_types_present_a):
|
|
1260
1263
|
assert 'tp' in result_types_present_b or 'fn' in result_types_present_b
|
|
1261
1264
|
if ('tp' in result_types_present_b) or ('fn' in result_types_present_b):
|
|
1262
1265
|
assert 'tp' in result_types_present_a or 'fn' in result_types_present_a
|
|
1263
|
-
|
|
1264
|
-
|
|
1266
|
+
|
|
1267
|
+
|
|
1265
1268
|
## Choose a comparison category based on result types
|
|
1266
|
-
|
|
1269
|
+
|
|
1267
1270
|
comparison_category = _result_types_to_comparison_category(
|
|
1268
1271
|
result_types_present_a,result_types_present_b,ground_truth_type,options)
|
|
1269
|
-
|
|
1272
|
+
|
|
1270
1273
|
# TODO: this may or may not be the right way to interpret sorting
|
|
1271
1274
|
# by confidence in this case, e.g., we may want to sort by confidence
|
|
1272
1275
|
# of correct or incorrect matches. But this isn't *wrong*.
|
|
1273
1276
|
max_conf_a = _maxempty([det['conf'] for det in im_a['detections']])
|
|
1274
1277
|
max_conf_b = _maxempty([det['conf'] for det in im_b['detections']])
|
|
1275
1278
|
sort_conf = max(max_conf_a,max_conf_b)
|
|
1276
|
-
|
|
1277
|
-
# ...what kind of ground truth (if any) do we have?
|
|
1278
|
-
|
|
1279
|
-
assert comparison_category is not None
|
|
1280
|
-
categories_to_image_pairs[comparison_category][fn] = im_pair
|
|
1279
|
+
|
|
1280
|
+
# ...what kind of ground truth (if any) do we have?
|
|
1281
|
+
|
|
1282
|
+
assert comparison_category is not None
|
|
1283
|
+
categories_to_image_pairs[comparison_category][fn] = im_pair
|
|
1281
1284
|
im_pair['sort_conf'] = sort_conf
|
|
1282
|
-
|
|
1285
|
+
|
|
1283
1286
|
# ...for each filename
|
|
1284
|
-
|
|
1285
|
-
|
|
1287
|
+
|
|
1288
|
+
|
|
1286
1289
|
##%% Sample and plot differences
|
|
1287
|
-
|
|
1290
|
+
|
|
1291
|
+
pool = None
|
|
1292
|
+
|
|
1288
1293
|
if options.n_rendering_workers > 1:
|
|
1289
1294
|
worker_type = 'processes'
|
|
1290
1295
|
if options.parallelize_rendering_with_threads:
|
|
1291
1296
|
worker_type = 'threads'
|
|
1292
1297
|
print('Rendering images with {} {}'.format(options.n_rendering_workers,worker_type))
|
|
1293
1298
|
if options.parallelize_rendering_with_threads:
|
|
1294
|
-
pool = ThreadPool(options.n_rendering_workers)
|
|
1299
|
+
pool = ThreadPool(options.n_rendering_workers)
|
|
1295
1300
|
else:
|
|
1296
|
-
pool = Pool(options.n_rendering_workers)
|
|
1297
|
-
|
|
1301
|
+
pool = Pool(options.n_rendering_workers)
|
|
1302
|
+
|
|
1298
1303
|
local_output_folder = os.path.join(options.output_folder,'cmp_' + \
|
|
1299
1304
|
str(output_index).zfill(3))
|
|
1300
|
-
|
|
1305
|
+
|
|
1301
1306
|
def render_detection_comparisons(category,image_pairs,image_filenames):
|
|
1302
|
-
|
|
1307
|
+
|
|
1303
1308
|
print('Rendering detections for category {}'.format(category))
|
|
1304
|
-
|
|
1309
|
+
|
|
1305
1310
|
category_folder = os.path.join(local_output_folder,category)
|
|
1306
1311
|
os.makedirs(category_folder,exist_ok=True)
|
|
1307
|
-
|
|
1312
|
+
|
|
1308
1313
|
# fn = image_filenames[0]
|
|
1309
1314
|
if options.n_rendering_workers <= 1:
|
|
1310
1315
|
output_image_paths = []
|
|
1311
|
-
for fn in tqdm(image_filenames):
|
|
1316
|
+
for fn in tqdm(image_filenames):
|
|
1312
1317
|
output_image_paths.append(_render_image_pair(fn,image_pairs,category_folder,
|
|
1313
1318
|
options,pairwise_options))
|
|
1314
|
-
else:
|
|
1319
|
+
else:
|
|
1315
1320
|
output_image_paths = list(tqdm(pool.imap(
|
|
1316
|
-
partial(_render_image_pair, image_pairs=image_pairs,
|
|
1321
|
+
partial(_render_image_pair, image_pairs=image_pairs,
|
|
1317
1322
|
category_folder=category_folder,options=options,
|
|
1318
1323
|
pairwise_options=pairwise_options),
|
|
1319
|
-
image_filenames),
|
|
1324
|
+
image_filenames),
|
|
1320
1325
|
total=len(image_filenames)))
|
|
1321
|
-
|
|
1326
|
+
|
|
1322
1327
|
return output_image_paths
|
|
1323
|
-
|
|
1328
|
+
|
|
1324
1329
|
# ...def render_detection_comparisons()
|
|
1325
|
-
|
|
1330
|
+
|
|
1326
1331
|
if len(options.colormap_a) > 1:
|
|
1327
1332
|
color_string_a = str(options.colormap_a)
|
|
1328
1333
|
else:
|
|
1329
1334
|
color_string_a = options.colormap_a[0]
|
|
1330
|
-
|
|
1335
|
+
|
|
1331
1336
|
if len(options.colormap_b) > 1:
|
|
1332
1337
|
color_string_b = str(options.colormap_b)
|
|
1333
1338
|
else:
|
|
1334
1339
|
color_string_b = options.colormap_b[0]
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
# For each category, generate comparison images and the
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
# For each category, generate comparison images and the
|
|
1338
1343
|
# comparison HTML page.
|
|
1339
1344
|
#
|
|
1340
1345
|
# category = 'common_detections'
|
|
1341
1346
|
for category in categories_to_image_pairs.keys():
|
|
1342
|
-
|
|
1347
|
+
|
|
1343
1348
|
# Choose detection pairs we're going to render for this category
|
|
1344
1349
|
image_pairs = categories_to_image_pairs[category]
|
|
1345
1350
|
image_filenames = list(image_pairs.keys())
|
|
1346
|
-
|
|
1351
|
+
|
|
1347
1352
|
if options.max_images_per_category is not None and options.max_images_per_category > 0:
|
|
1348
1353
|
if len(image_filenames) > options.max_images_per_category:
|
|
1349
1354
|
print('Sampling {} of {} image pairs for category {}'.format(
|
|
@@ -1355,45 +1360,45 @@ def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
|
1355
1360
|
assert len(image_filenames) <= options.max_images_per_category
|
|
1356
1361
|
|
|
1357
1362
|
input_image_absolute_paths = [os.path.join(options.image_folder,fn) for fn in image_filenames]
|
|
1358
|
-
|
|
1363
|
+
|
|
1359
1364
|
category_image_output_paths = render_detection_comparisons(category,
|
|
1360
1365
|
image_pairs,image_filenames)
|
|
1361
|
-
|
|
1366
|
+
|
|
1362
1367
|
category_html_filename = os.path.join(local_output_folder,
|
|
1363
1368
|
category + '.html')
|
|
1364
1369
|
category_image_output_paths_relative = [os.path.relpath(s,local_output_folder) \
|
|
1365
1370
|
for s in category_image_output_paths]
|
|
1366
|
-
|
|
1371
|
+
|
|
1367
1372
|
image_info = []
|
|
1368
|
-
|
|
1373
|
+
|
|
1369
1374
|
assert len(category_image_output_paths_relative) == len(input_image_absolute_paths)
|
|
1370
|
-
|
|
1371
|
-
for i_fn,fn in enumerate(category_image_output_paths_relative):
|
|
1372
|
-
|
|
1375
|
+
|
|
1376
|
+
for i_fn,fn in enumerate(category_image_output_paths_relative):
|
|
1377
|
+
|
|
1373
1378
|
input_path_relative = image_filenames[i_fn]
|
|
1374
1379
|
image_pair = image_pairs[input_path_relative]
|
|
1375
1380
|
image_a = image_pair['im_a']
|
|
1376
1381
|
image_b = image_pair['im_b']
|
|
1377
|
-
|
|
1378
|
-
if options.fn_to_display_fn is not None:
|
|
1382
|
+
|
|
1383
|
+
if options.fn_to_display_fn is not None:
|
|
1379
1384
|
assert input_path_relative in options.fn_to_display_fn, \
|
|
1380
1385
|
'fn_to_display_fn provided, but {} is not mapped'.format(input_path_relative)
|
|
1381
1386
|
display_path = options.fn_to_display_fn[input_path_relative]
|
|
1382
1387
|
else:
|
|
1383
1388
|
display_path = input_path_relative
|
|
1384
|
-
|
|
1389
|
+
|
|
1385
1390
|
sort_conf = image_pair['sort_conf']
|
|
1386
|
-
|
|
1391
|
+
|
|
1387
1392
|
max_conf_a = _maxempty([det['conf'] for det in image_a['detections']])
|
|
1388
1393
|
max_conf_b = _maxempty([det['conf'] for det in image_b['detections']])
|
|
1389
|
-
|
|
1394
|
+
|
|
1390
1395
|
title = display_path + ' (max conf {:.2f},{:.2f})'.format(max_conf_a,max_conf_b)
|
|
1391
|
-
|
|
1396
|
+
|
|
1392
1397
|
if options.parse_link_paths:
|
|
1393
1398
|
link_target_string = urllib.parse.quote(input_image_absolute_paths[i_fn])
|
|
1394
1399
|
else:
|
|
1395
1400
|
link_target_string = input_image_absolute_paths[i_fn]
|
|
1396
|
-
|
|
1401
|
+
|
|
1397
1402
|
info = {
|
|
1398
1403
|
'filename': fn,
|
|
1399
1404
|
'title': title,
|
|
@@ -1404,9 +1409,9 @@ def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
|
1404
1409
|
}
|
|
1405
1410
|
|
|
1406
1411
|
image_info.append(info)
|
|
1407
|
-
|
|
1412
|
+
|
|
1408
1413
|
# ...for each image
|
|
1409
|
-
|
|
1414
|
+
|
|
1410
1415
|
category_page_header_string = '<h1>{}</h1>\n'.format(categories_to_page_titles[category])
|
|
1411
1416
|
category_page_header_string += '<p style="font-weight:bold;">\n'
|
|
1412
1417
|
category_page_header_string += 'Model A: {} ({})<br/>\n'.format(
|
|
@@ -1414,7 +1419,7 @@ def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
|
1414
1419
|
category_page_header_string += 'Model B: {} ({})'.format(
|
|
1415
1420
|
pairwise_options.results_description_b,color_string_b)
|
|
1416
1421
|
category_page_header_string += '</p>\n'
|
|
1417
|
-
|
|
1422
|
+
|
|
1418
1423
|
category_page_header_string += '<p>\n'
|
|
1419
1424
|
category_page_header_string += 'Detection thresholds for A ({}):\n{}<br/>'.format(
|
|
1420
1425
|
pairwise_options.results_description_a,str(pairwise_options.detection_thresholds_a))
|
|
@@ -1426,16 +1431,16 @@ def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
|
1426
1431
|
category_page_header_string += 'Rendering threshold for B ({}):\n{}<br/>'.format(
|
|
1427
1432
|
pairwise_options.results_description_b,
|
|
1428
1433
|
str(pairwise_options.rendering_confidence_threshold_b))
|
|
1429
|
-
category_page_header_string += '</p>\n'
|
|
1430
|
-
|
|
1434
|
+
category_page_header_string += '</p>\n'
|
|
1435
|
+
|
|
1431
1436
|
subpage_header_string = '\n'.join(category_page_header_string.split('\n')[1:])
|
|
1432
|
-
|
|
1437
|
+
|
|
1433
1438
|
# Default to sorting by filename
|
|
1434
1439
|
if options.sort_by_confidence:
|
|
1435
1440
|
image_info = sorted(image_info, key=lambda d: d['sort_conf'], reverse=True)
|
|
1436
1441
|
else:
|
|
1437
1442
|
image_info = sorted(image_info, key=lambda d: d['filename'])
|
|
1438
|
-
|
|
1443
|
+
|
|
1439
1444
|
write_html_image_list(
|
|
1440
1445
|
category_html_filename,
|
|
1441
1446
|
images=image_info,
|
|
@@ -1444,14 +1449,20 @@ def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
|
1444
1449
|
'subPageHeaderHtml': subpage_header_string,
|
|
1445
1450
|
'maxFiguresPerHtmlFile': options.max_images_per_page
|
|
1446
1451
|
})
|
|
1447
|
-
|
|
1452
|
+
|
|
1448
1453
|
# ...for each category
|
|
1449
|
-
|
|
1450
|
-
|
|
1454
|
+
|
|
1455
|
+
if pool is not None:
|
|
1456
|
+
try:
|
|
1457
|
+
pool.close()
|
|
1458
|
+
pool.join()
|
|
1459
|
+
print("Pool closed and joined for comparisong rendering")
|
|
1460
|
+
except Exception:
|
|
1461
|
+
pass
|
|
1451
1462
|
##%% Write the top-level HTML file content
|
|
1452
1463
|
|
|
1453
1464
|
html_output_string = ''
|
|
1454
|
-
|
|
1465
|
+
|
|
1455
1466
|
html_output_string += '<p>Comparing <b>{}</b> (A, {}) to <b>{}</b> (B, {})</p>'.format(
|
|
1456
1467
|
pairwise_options.results_description_a,color_string_a.lower(),
|
|
1457
1468
|
pairwise_options.results_description_b,color_string_b.lower())
|
|
@@ -1468,83 +1479,83 @@ def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
|
1468
1479
|
html_output_string += 'Rendering threshold for {}:\n{}<br/>'.format(
|
|
1469
1480
|
pairwise_options.results_description_b,
|
|
1470
1481
|
str(pairwise_options.rendering_confidence_threshold_b))
|
|
1471
|
-
|
|
1482
|
+
|
|
1472
1483
|
html_output_string += '<br/>'
|
|
1473
|
-
|
|
1484
|
+
|
|
1474
1485
|
html_output_string += 'Rendering a maximum of {} images per category<br/>'.format(
|
|
1475
1486
|
options.max_images_per_category)
|
|
1476
|
-
|
|
1487
|
+
|
|
1477
1488
|
html_output_string += '<br/>'
|
|
1478
|
-
|
|
1489
|
+
|
|
1479
1490
|
category_summary = ''
|
|
1480
1491
|
for i_category,category_name in enumerate(categories_to_image_pairs):
|
|
1481
1492
|
if i_category > 0:
|
|
1482
|
-
category_summary += '<br/>'
|
|
1493
|
+
category_summary += '<br/>'
|
|
1483
1494
|
category_summary += '{} {}'.format(
|
|
1484
1495
|
len(categories_to_image_pairs[category_name]),
|
|
1485
1496
|
category_name.replace('_',' '))
|
|
1486
|
-
|
|
1497
|
+
|
|
1487
1498
|
category_summary = \
|
|
1488
1499
|
'Of {} total files:<br/><br/><div style="margin-left:15px;">{}</div><br/>'.format(
|
|
1489
1500
|
len(filenames_to_compare),category_summary)
|
|
1490
|
-
|
|
1491
|
-
html_output_string += category_summary
|
|
1492
|
-
|
|
1501
|
+
|
|
1502
|
+
html_output_string += category_summary
|
|
1503
|
+
|
|
1493
1504
|
html_output_string += 'Comparison pages:<br/><br/>\n'
|
|
1494
1505
|
html_output_string += '<div style="margin-left:15px;">\n'
|
|
1495
|
-
|
|
1496
|
-
comparison_path_relative = os.path.relpath(local_output_folder,options.output_folder)
|
|
1506
|
+
|
|
1507
|
+
comparison_path_relative = os.path.relpath(local_output_folder,options.output_folder)
|
|
1497
1508
|
for category in categories_to_image_pairs.keys():
|
|
1498
1509
|
category_html_filename = os.path.join(comparison_path_relative,category + '.html')
|
|
1499
1510
|
html_output_string += '<a href="{}">{}</a><br/>\n'.format(
|
|
1500
1511
|
category_html_filename,category)
|
|
1501
|
-
|
|
1512
|
+
|
|
1502
1513
|
html_output_string += '</div>\n'
|
|
1503
1514
|
html_output_string += '</div>\n'
|
|
1504
|
-
|
|
1515
|
+
|
|
1505
1516
|
pairwise_results = PairwiseBatchComparisonResults()
|
|
1506
|
-
|
|
1517
|
+
|
|
1507
1518
|
pairwise_results.html_content = html_output_string
|
|
1508
1519
|
pairwise_results.pairwise_options = pairwise_options
|
|
1509
1520
|
pairwise_results.categories_to_image_pairs = categories_to_image_pairs
|
|
1510
|
-
|
|
1521
|
+
|
|
1511
1522
|
return pairwise_results
|
|
1512
|
-
|
|
1523
|
+
|
|
1513
1524
|
# ...def _pairwise_compare_batch_results()
|
|
1514
1525
|
|
|
1515
1526
|
|
|
1516
1527
|
def compare_batch_results(options):
|
|
1517
1528
|
"""
|
|
1518
|
-
The main entry point for this module. Runs one or more batch results comparisons,
|
|
1529
|
+
The main entry point for this module. Runs one or more batch results comparisons,
|
|
1519
1530
|
writing results to an html page. Most of the work is deferred to _pairwise_compare_batch_results().
|
|
1520
|
-
|
|
1531
|
+
|
|
1521
1532
|
Args:
|
|
1522
1533
|
options (BatchComparisonOptions): job options to use for this comparison task, including the
|
|
1523
1534
|
list of specific pairswise comparisons to make (in the pairwise_options field)
|
|
1524
|
-
|
|
1535
|
+
|
|
1525
1536
|
Returns:
|
|
1526
1537
|
BatchComparisonResults: the results of this comparison task
|
|
1527
1538
|
"""
|
|
1528
|
-
|
|
1539
|
+
|
|
1529
1540
|
assert options.output_folder is not None
|
|
1530
1541
|
assert options.image_folder is not None
|
|
1531
1542
|
assert options.pairwise_options is not None
|
|
1532
1543
|
|
|
1533
1544
|
options = copy.deepcopy(options)
|
|
1534
|
-
|
|
1545
|
+
|
|
1535
1546
|
if not isinstance(options.pairwise_options,list):
|
|
1536
1547
|
options.pairwise_options = [options.pairwise_options]
|
|
1537
|
-
|
|
1548
|
+
|
|
1538
1549
|
pairwise_options_list = options.pairwise_options
|
|
1539
1550
|
n_comparisons = len(pairwise_options_list)
|
|
1540
|
-
|
|
1551
|
+
|
|
1541
1552
|
options.pairwise_options = None
|
|
1542
|
-
|
|
1553
|
+
|
|
1543
1554
|
html_content = ''
|
|
1544
1555
|
all_pairwise_results = []
|
|
1545
|
-
|
|
1556
|
+
|
|
1546
1557
|
# i_comparison = 0; pairwise_options = pairwise_options_list[i_comparison]
|
|
1547
|
-
|
|
1558
|
+
|
|
1548
1559
|
for i_comparison,pairwise_options in enumerate(pairwise_options_list):
|
|
1549
1560
|
print('Running comparison {} of {}'.format(i_comparison,n_comparisons))
|
|
1550
1561
|
pairwise_results = \
|
|
@@ -1560,11 +1571,11 @@ def compare_batch_results(options):
|
|
|
1560
1571
|
job_name_string)
|
|
1561
1572
|
html_output_string += html_content
|
|
1562
1573
|
html_output_string += main_page_footer
|
|
1563
|
-
|
|
1564
|
-
html_output_file = os.path.join(options.output_folder,'index.html')
|
|
1574
|
+
|
|
1575
|
+
html_output_file = os.path.join(options.output_folder,'index.html')
|
|
1565
1576
|
with open(html_output_file,'w') as f:
|
|
1566
|
-
f.write(html_output_string)
|
|
1567
|
-
|
|
1577
|
+
f.write(html_output_string)
|
|
1578
|
+
|
|
1568
1579
|
results = BatchComparisonResults()
|
|
1569
1580
|
results.html_output_file = html_output_file
|
|
1570
1581
|
results.pairwise_results = all_pairwise_results
|
|
@@ -1579,10 +1590,10 @@ def n_way_comparison(filenames,
|
|
|
1579
1590
|
"""
|
|
1580
1591
|
Performs N pairwise comparisons for the list of results files in [filenames], by generating
|
|
1581
1592
|
sets of pairwise options and calling compare_batch_results.
|
|
1582
|
-
|
|
1593
|
+
|
|
1583
1594
|
Args:
|
|
1584
1595
|
filenames (list): list of MD results filenames to compare
|
|
1585
|
-
options (BatchComparisonOptions): task options set in which pairwise_options is still
|
|
1596
|
+
options (BatchComparisonOptions): task options set in which pairwise_options is still
|
|
1586
1597
|
empty; that will get populated from [filenames]
|
|
1587
1598
|
detection_thresholds (list, optional): list of detection thresholds with the same length
|
|
1588
1599
|
as [filenames], or None to use sensible defaults
|
|
@@ -1590,11 +1601,11 @@ def n_way_comparison(filenames,
|
|
|
1590
1601
|
as [filenames], or None to use sensible defaults
|
|
1591
1602
|
model_names (list, optional): list of model names to use the output HTML file, with
|
|
1592
1603
|
the same length as [filenames], or None to use sensible defaults
|
|
1593
|
-
|
|
1604
|
+
|
|
1594
1605
|
Returns:
|
|
1595
1606
|
BatchComparisonResults: the results of this comparison task
|
|
1596
1607
|
"""
|
|
1597
|
-
|
|
1608
|
+
|
|
1598
1609
|
if detection_thresholds is None:
|
|
1599
1610
|
detection_thresholds = [0.15] * len(filenames)
|
|
1600
1611
|
assert len(detection_thresholds) == len(filenames), \
|
|
@@ -1609,27 +1620,27 @@ def n_way_comparison(filenames,
|
|
|
1609
1620
|
if model_names is not None:
|
|
1610
1621
|
assert len(model_names) == len(filenames), \
|
|
1611
1622
|
'[model_names] should be the same length as [filenames]'
|
|
1612
|
-
|
|
1623
|
+
|
|
1613
1624
|
options.pairwise_options = []
|
|
1614
|
-
|
|
1625
|
+
|
|
1615
1626
|
# Choose all pairwise combinations of the files in [filenames]
|
|
1616
1627
|
for i, j in itertools.combinations(list(range(0,len(filenames))),2):
|
|
1617
|
-
|
|
1628
|
+
|
|
1618
1629
|
pairwise_options = PairwiseBatchComparisonOptions()
|
|
1619
|
-
|
|
1630
|
+
|
|
1620
1631
|
pairwise_options.results_filename_a = filenames[i]
|
|
1621
1632
|
pairwise_options.results_filename_b = filenames[j]
|
|
1622
|
-
|
|
1633
|
+
|
|
1623
1634
|
pairwise_options.rendering_confidence_threshold_a = rendering_thresholds[i]
|
|
1624
1635
|
pairwise_options.rendering_confidence_threshold_b = rendering_thresholds[j]
|
|
1625
|
-
|
|
1636
|
+
|
|
1626
1637
|
pairwise_options.detection_thresholds_a = {'default':detection_thresholds[i]}
|
|
1627
1638
|
pairwise_options.detection_thresholds_b = {'default':detection_thresholds[j]}
|
|
1628
|
-
|
|
1639
|
+
|
|
1629
1640
|
if model_names is not None:
|
|
1630
1641
|
pairwise_options.results_description_a = model_names[i]
|
|
1631
1642
|
pairwise_options.results_description_b = model_names[j]
|
|
1632
|
-
|
|
1643
|
+
|
|
1633
1644
|
options.pairwise_options.append(pairwise_options)
|
|
1634
1645
|
|
|
1635
1646
|
return compare_batch_results(options)
|
|
@@ -1641,46 +1652,46 @@ def find_image_level_detections_above_threshold(results,threshold=0.2,category_n
|
|
|
1641
1652
|
"""
|
|
1642
1653
|
Returns images in the set of MD results [results] with detections above
|
|
1643
1654
|
a threshold confidence level, optionally only counting certain categories.
|
|
1644
|
-
|
|
1655
|
+
|
|
1645
1656
|
Args:
|
|
1646
1657
|
results (str or dict): the set of results, either a .json filename or a results
|
|
1647
1658
|
dict
|
|
1648
|
-
threshold (float, optional): the threshold used to determine the target number of
|
|
1659
|
+
threshold (float, optional): the threshold used to determine the target number of
|
|
1649
1660
|
detections in [results]
|
|
1650
1661
|
category_names (list or str, optional): the list of category names to consider (defaults
|
|
1651
|
-
to using all categories), or the name of a single category.
|
|
1652
|
-
|
|
1662
|
+
to using all categories), or the name of a single category.
|
|
1663
|
+
|
|
1653
1664
|
Returns:
|
|
1654
1665
|
list: the images with above-threshold detections
|
|
1655
1666
|
"""
|
|
1656
1667
|
if isinstance(results,str):
|
|
1657
1668
|
with open(results,'r') as f:
|
|
1658
1669
|
results = json.load(f)
|
|
1659
|
-
|
|
1670
|
+
|
|
1660
1671
|
category_ids_to_consider = None
|
|
1661
|
-
|
|
1672
|
+
|
|
1662
1673
|
if category_names is not None:
|
|
1663
|
-
|
|
1674
|
+
|
|
1664
1675
|
if isinstance(category_names,str):
|
|
1665
1676
|
category_names = [category_names]
|
|
1666
|
-
|
|
1677
|
+
|
|
1667
1678
|
category_id_to_name = results['detection_categories']
|
|
1668
1679
|
category_name_to_id = invert_dictionary(category_id_to_name)
|
|
1669
|
-
|
|
1680
|
+
|
|
1670
1681
|
category_ids_to_consider = []
|
|
1671
|
-
|
|
1682
|
+
|
|
1672
1683
|
# category_name = category_names[0]
|
|
1673
1684
|
for category_name in category_names:
|
|
1674
1685
|
category_id = category_name_to_id[category_name]
|
|
1675
1686
|
category_ids_to_consider.append(category_id)
|
|
1676
|
-
|
|
1687
|
+
|
|
1677
1688
|
assert len(category_ids_to_consider) > 0, \
|
|
1678
1689
|
'Category name list did not map to any category IDs'
|
|
1679
|
-
|
|
1690
|
+
|
|
1680
1691
|
images_above_threshold = []
|
|
1681
|
-
|
|
1692
|
+
|
|
1682
1693
|
for im in results['images']:
|
|
1683
|
-
|
|
1694
|
+
|
|
1684
1695
|
if ('detections' in im) and (im['detections'] is not None) and (len(im['detections']) > 0):
|
|
1685
1696
|
confidence_values_this_image = [0]
|
|
1686
1697
|
for det in im['detections']:
|
|
@@ -1690,9 +1701,9 @@ def find_image_level_detections_above_threshold(results,threshold=0.2,category_n
|
|
|
1690
1701
|
confidence_values_this_image.append(det['conf'])
|
|
1691
1702
|
if max(confidence_values_this_image) >= threshold:
|
|
1692
1703
|
images_above_threshold.append(im)
|
|
1693
|
-
|
|
1704
|
+
|
|
1694
1705
|
# ...for each image
|
|
1695
|
-
|
|
1706
|
+
|
|
1696
1707
|
return images_above_threshold
|
|
1697
1708
|
|
|
1698
1709
|
# ...def find_image_level_detections_above_threshold(...)
|
|
@@ -1705,73 +1716,73 @@ def find_equivalent_threshold(results_a,
|
|
|
1705
1716
|
verbose=False):
|
|
1706
1717
|
"""
|
|
1707
1718
|
Given two sets of detector results, finds the confidence threshold for results_b
|
|
1708
|
-
that produces the same fraction of *images* with detections as threshold_a does for
|
|
1719
|
+
that produces the same fraction of *images* with detections as threshold_a does for
|
|
1709
1720
|
results_a. Uses all categories.
|
|
1710
|
-
|
|
1721
|
+
|
|
1711
1722
|
Args:
|
|
1712
1723
|
results_a (str or dict): the first set of results, either a .json filename or a results
|
|
1713
1724
|
dict
|
|
1714
1725
|
results_b (str or dict): the second set of results, either a .json filename or a results
|
|
1715
1726
|
dict
|
|
1716
|
-
threshold_a (float, optional): the threshold used to determine the target number of
|
|
1727
|
+
threshold_a (float, optional): the threshold used to determine the target number of
|
|
1717
1728
|
detections in results_a
|
|
1718
1729
|
category_names (list or str, optional): the list of category names to consider (defaults
|
|
1719
1730
|
to using all categories), or the name of a single category.
|
|
1720
1731
|
verbose (bool, optional): enable additional debug output
|
|
1721
|
-
|
|
1732
|
+
|
|
1722
1733
|
Returns:
|
|
1723
1734
|
float: the threshold that - when applied to results_b - produces the same number
|
|
1724
|
-
|
|
1735
|
+
of image-level detections that results from applying threshold_a to results_a
|
|
1725
1736
|
"""
|
|
1726
|
-
|
|
1737
|
+
|
|
1727
1738
|
if isinstance(results_a,str):
|
|
1728
1739
|
if verbose:
|
|
1729
1740
|
print('Loading results from {}'.format(results_a))
|
|
1730
1741
|
with open(results_a,'r') as f:
|
|
1731
1742
|
results_a = json.load(f)
|
|
1732
|
-
|
|
1743
|
+
|
|
1733
1744
|
if isinstance(results_b,str):
|
|
1734
1745
|
if verbose:
|
|
1735
1746
|
print('Loading results from {}'.format(results_b))
|
|
1736
1747
|
with open(results_b,'r') as f:
|
|
1737
1748
|
results_b = json.load(f)
|
|
1738
|
-
|
|
1749
|
+
|
|
1739
1750
|
category_ids_to_consider_a = None
|
|
1740
1751
|
category_ids_to_consider_b = None
|
|
1741
|
-
|
|
1752
|
+
|
|
1742
1753
|
if category_names is not None:
|
|
1743
|
-
|
|
1754
|
+
|
|
1744
1755
|
if isinstance(category_names,str):
|
|
1745
1756
|
category_names = [category_names]
|
|
1746
|
-
|
|
1757
|
+
|
|
1747
1758
|
categories_a = results_a['detection_categories']
|
|
1748
1759
|
categories_b = results_b['detection_categories']
|
|
1749
1760
|
category_name_to_id_a = invert_dictionary(categories_a)
|
|
1750
1761
|
category_name_to_id_b = invert_dictionary(categories_b)
|
|
1751
|
-
|
|
1762
|
+
|
|
1752
1763
|
category_ids_to_consider_a = []
|
|
1753
1764
|
category_ids_to_consider_b = []
|
|
1754
|
-
|
|
1765
|
+
|
|
1755
1766
|
# category_name = category_names[0]
|
|
1756
1767
|
for category_name in category_names:
|
|
1757
1768
|
category_id_a = category_name_to_id_a[category_name]
|
|
1758
1769
|
category_id_b = category_name_to_id_b[category_name]
|
|
1759
1770
|
category_ids_to_consider_a.append(category_id_a)
|
|
1760
1771
|
category_ids_to_consider_b.append(category_id_b)
|
|
1761
|
-
|
|
1772
|
+
|
|
1762
1773
|
assert len(category_ids_to_consider_a) > 0 and len(category_ids_to_consider_b) > 0, \
|
|
1763
1774
|
'Category name list did not map to any category IDs in one or both detection sets'
|
|
1764
|
-
|
|
1775
|
+
|
|
1765
1776
|
def _get_confidence_values_for_results(images,category_ids_to_consider,threshold):
|
|
1766
1777
|
"""
|
|
1767
1778
|
Return a list of the maximum confidence value for each image in [images].
|
|
1768
1779
|
Returns zero confidence for images with no detections (or no detections
|
|
1769
1780
|
in the specified categories). Does not return anything for invalid images.
|
|
1770
1781
|
"""
|
|
1771
|
-
|
|
1782
|
+
|
|
1772
1783
|
confidence_values = []
|
|
1773
1784
|
images_above_threshold = []
|
|
1774
|
-
|
|
1785
|
+
|
|
1775
1786
|
for im in images:
|
|
1776
1787
|
if 'detections' in im and im['detections'] is not None:
|
|
1777
1788
|
if len(im['detections']) == 0:
|
|
@@ -1787,44 +1798,44 @@ def find_equivalent_threshold(results_a,
|
|
|
1787
1798
|
confidence_values.append(0)
|
|
1788
1799
|
else:
|
|
1789
1800
|
max_conf_value = max(confidence_values_this_image)
|
|
1790
|
-
|
|
1801
|
+
|
|
1791
1802
|
if threshold is not None and max_conf_value >= threshold:
|
|
1792
1803
|
images_above_threshold.append(im)
|
|
1793
1804
|
confidence_values.append(max_conf_value)
|
|
1794
1805
|
# ...for each image
|
|
1795
|
-
|
|
1806
|
+
|
|
1796
1807
|
return confidence_values, images_above_threshold
|
|
1797
|
-
|
|
1808
|
+
|
|
1798
1809
|
confidence_values_a,images_above_threshold_a = \
|
|
1799
1810
|
_get_confidence_values_for_results(results_a['images'],
|
|
1800
1811
|
category_ids_to_consider_a,
|
|
1801
1812
|
threshold_a)
|
|
1802
|
-
|
|
1813
|
+
|
|
1803
1814
|
# ...def _get_confidence_values_for_results(...)
|
|
1804
|
-
|
|
1815
|
+
|
|
1805
1816
|
if verbose:
|
|
1806
1817
|
print('For result set A, considering {} of {} images'.format(
|
|
1807
1818
|
len(confidence_values_a),len(results_a['images'])))
|
|
1808
1819
|
confidence_values_a_above_threshold = [c for c in confidence_values_a if c >= threshold_a]
|
|
1809
|
-
|
|
1820
|
+
|
|
1810
1821
|
confidence_values_b,_ = _get_confidence_values_for_results(results_b['images'],
|
|
1811
1822
|
category_ids_to_consider_b,
|
|
1812
1823
|
threshold=None)
|
|
1813
1824
|
if verbose:
|
|
1814
1825
|
print('For result set B, considering {} of {} images'.format(
|
|
1815
1826
|
len(confidence_values_b),len(results_b['images'])))
|
|
1816
|
-
confidence_values_b = sorted(confidence_values_b)
|
|
1817
|
-
|
|
1827
|
+
confidence_values_b = sorted(confidence_values_b)
|
|
1828
|
+
|
|
1818
1829
|
target_detection_fraction = len(confidence_values_a_above_threshold) / len(confidence_values_a)
|
|
1819
|
-
|
|
1820
|
-
detection_cutoff_index = round((1.0-target_detection_fraction) * len(confidence_values_b))
|
|
1830
|
+
|
|
1831
|
+
detection_cutoff_index = round((1.0-target_detection_fraction) * len(confidence_values_b))
|
|
1821
1832
|
threshold_b = confidence_values_b[detection_cutoff_index]
|
|
1822
|
-
|
|
1833
|
+
|
|
1823
1834
|
if verbose:
|
|
1824
1835
|
print('{} confidence values above threshold (A)'.format(len(confidence_values_a_above_threshold)))
|
|
1825
1836
|
confidence_values_b_above_threshold = [c for c in confidence_values_b if c >= threshold_b]
|
|
1826
1837
|
print('{} confidence values above threshold (B)'.format(len(confidence_values_b_above_threshold)))
|
|
1827
|
-
|
|
1838
|
+
|
|
1828
1839
|
return threshold_b
|
|
1829
1840
|
|
|
1830
1841
|
# ...def find_equivalent_threshold(...)
|
|
@@ -1833,38 +1844,38 @@ def find_equivalent_threshold(results_a,
|
|
|
1833
1844
|
#%% Interactive driver
|
|
1834
1845
|
|
|
1835
1846
|
if False:
|
|
1836
|
-
|
|
1847
|
+
|
|
1837
1848
|
#%% Test two-way comparison
|
|
1838
|
-
|
|
1849
|
+
|
|
1839
1850
|
options = BatchComparisonOptions()
|
|
1840
1851
|
|
|
1841
1852
|
options.parallelize_rendering_with_threads = True
|
|
1842
|
-
|
|
1853
|
+
|
|
1843
1854
|
options.job_name = 'BCT'
|
|
1844
1855
|
options.output_folder = r'g:\temp\comparisons'
|
|
1845
1856
|
options.image_folder = r'g:\camera_traps\camera_trap_images'
|
|
1846
1857
|
options.max_images_per_category = 100
|
|
1847
1858
|
options.sort_by_confidence = True
|
|
1848
|
-
|
|
1859
|
+
|
|
1849
1860
|
options.pairwise_options = []
|
|
1850
1861
|
|
|
1851
1862
|
results_base = os.path.expanduser('~/postprocessing/bellevue-camera-traps')
|
|
1852
|
-
filenames = [
|
|
1863
|
+
filenames = [
|
|
1853
1864
|
os.path.join(results_base,r'bellevue-camera-traps-2023-12-05-v5a.0.0\combined_api_outputs\bellevue-camera-traps-2023-12-05-v5a.0.0_detections.json'),
|
|
1854
1865
|
os.path.join(results_base,r'bellevue-camera-traps-2023-12-05-aug-v5a.0.0\combined_api_outputs\bellevue-camera-traps-2023-12-05-aug-v5a.0.0_detections.json')
|
|
1855
1866
|
]
|
|
1856
1867
|
|
|
1857
1868
|
detection_thresholds = [0.15,0.15]
|
|
1858
1869
|
rendering_thresholds = None
|
|
1859
|
-
|
|
1870
|
+
|
|
1860
1871
|
results = n_way_comparison(filenames,options,detection_thresholds,rendering_thresholds=rendering_thresholds)
|
|
1861
|
-
|
|
1872
|
+
|
|
1862
1873
|
from megadetector.utils.path_utils import open_file
|
|
1863
1874
|
open_file(results.html_output_file)
|
|
1864
1875
|
|
|
1865
|
-
|
|
1876
|
+
|
|
1866
1877
|
#%% Test three-way comparison
|
|
1867
|
-
|
|
1878
|
+
|
|
1868
1879
|
options = BatchComparisonOptions()
|
|
1869
1880
|
|
|
1870
1881
|
options.parallelize_rendering_with_threads = False
|
|
@@ -1884,7 +1895,7 @@ if False:
|
|
|
1884
1895
|
detection_thresholds = [0.7,0.15,0.15]
|
|
1885
1896
|
|
|
1886
1897
|
results = n_way_comparison(filenames,options,detection_thresholds,rendering_thresholds=None)
|
|
1887
|
-
|
|
1898
|
+
|
|
1888
1899
|
from megadetector.utils.path_utils import open_file
|
|
1889
1900
|
open_file(results.html_output_file)
|
|
1890
1901
|
|
|
@@ -1892,23 +1903,23 @@ if False:
|
|
|
1892
1903
|
#%% Command-line driver
|
|
1893
1904
|
|
|
1894
1905
|
"""
|
|
1895
|
-
python compare_batch_results.py ~/tmp/comparison-test ~/data/KGA
|
|
1906
|
+
python compare_batch_results.py ~/tmp/comparison-test ~/data/KGA \
|
|
1907
|
+
~/data/KGA-5a.json ~/data/KGA-5b.json ~/data/KGA-4.json \
|
|
1908
|
+
--detection_thresholds 0.15 0.15 0.7 --rendering_thresholds 0.1 0.1 0.6 --use_processes
|
|
1896
1909
|
"""
|
|
1897
1910
|
|
|
1898
|
-
|
|
1911
|
+
def main(): # noqa
|
|
1899
1912
|
|
|
1900
|
-
def main():
|
|
1901
|
-
|
|
1902
1913
|
options = BatchComparisonOptions()
|
|
1903
|
-
|
|
1914
|
+
|
|
1904
1915
|
parser = argparse.ArgumentParser(
|
|
1905
1916
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1906
1917
|
epilog=textwrap.dedent('''\
|
|
1907
1918
|
Example:
|
|
1908
|
-
|
|
1919
|
+
|
|
1909
1920
|
python compare_batch_results.py output_folder image_folder mdv5a.json mdv5b.json mdv4.json --detection_thresholds 0.15 0.15 0.7
|
|
1910
1921
|
'''))
|
|
1911
|
-
|
|
1922
|
+
|
|
1912
1923
|
parser.add_argument('output_folder', type=str, help='folder to which to write html results')
|
|
1913
1924
|
|
|
1914
1925
|
parser.add_argument('image_folder', type=str, help='image source folder')
|
|
@@ -1918,67 +1929,67 @@ def main():
|
|
|
1918
1929
|
parser.add_argument('--detection_thresholds', nargs='*', type=float,
|
|
1919
1930
|
help='list of detection thresholds, same length as the number of .json files, ' + \
|
|
1920
1931
|
'defaults to 0.15 for all files')
|
|
1921
|
-
|
|
1932
|
+
|
|
1922
1933
|
parser.add_argument('--rendering_thresholds', nargs='*', type=float,
|
|
1923
1934
|
help='list of rendering thresholds, same length as the number of .json files, ' + \
|
|
1924
1935
|
'defaults to 0.10 for all files')
|
|
1925
|
-
|
|
1936
|
+
|
|
1926
1937
|
parser.add_argument('--max_images_per_category', type=int, default=options.max_images_per_category,
|
|
1927
1938
|
help='number of images to sample for each agreement category (common detections, etc.)')
|
|
1928
|
-
|
|
1929
|
-
parser.add_argument('--target_width', type=int, default=options.target_width,
|
|
1939
|
+
|
|
1940
|
+
parser.add_argument('--target_width', type=int, default=options.target_width,
|
|
1930
1941
|
help='output image width, defaults to {}'.format(options.target_width))
|
|
1931
|
-
|
|
1932
|
-
parser.add_argument('--use_processes', action='store_true',
|
|
1942
|
+
|
|
1943
|
+
parser.add_argument('--use_processes', action='store_true',
|
|
1933
1944
|
help='use processes rather than threads for parallelization')
|
|
1934
|
-
|
|
1935
|
-
parser.add_argument('--open_results', action='store_true',
|
|
1945
|
+
|
|
1946
|
+
parser.add_argument('--open_results', action='store_true',
|
|
1936
1947
|
help='open the output html file when done')
|
|
1937
|
-
|
|
1948
|
+
|
|
1938
1949
|
parser.add_argument('--n_rendering_workers', type=int, default=options.n_rendering_workers,
|
|
1939
1950
|
help='number of workers for parallel rendering, defaults to {}'.format(
|
|
1940
1951
|
options.n_rendering_workers))
|
|
1941
|
-
|
|
1952
|
+
|
|
1942
1953
|
if len(sys.argv[1:])==0:
|
|
1943
1954
|
parser.print_help()
|
|
1944
1955
|
parser.exit()
|
|
1945
|
-
|
|
1956
|
+
|
|
1946
1957
|
args = parser.parse_args()
|
|
1947
|
-
|
|
1958
|
+
|
|
1948
1959
|
print('Output folder:')
|
|
1949
1960
|
print(args.output_folder)
|
|
1950
|
-
|
|
1961
|
+
|
|
1951
1962
|
print('\nResults files:')
|
|
1952
1963
|
print(args.results_files)
|
|
1953
|
-
|
|
1964
|
+
|
|
1954
1965
|
print('\nDetection thresholds:')
|
|
1955
1966
|
print(args.detection_thresholds)
|
|
1956
|
-
|
|
1967
|
+
|
|
1957
1968
|
print('\nRendering thresholds:')
|
|
1958
|
-
print(args.rendering_thresholds)
|
|
1959
|
-
|
|
1969
|
+
print(args.rendering_thresholds)
|
|
1970
|
+
|
|
1960
1971
|
# Convert to options objects
|
|
1961
1972
|
options = BatchComparisonOptions()
|
|
1962
|
-
|
|
1973
|
+
|
|
1963
1974
|
options.output_folder = args.output_folder
|
|
1964
1975
|
options.image_folder = args.image_folder
|
|
1965
1976
|
options.target_width = args.target_width
|
|
1966
|
-
options.n_rendering_workers = args.n_rendering_workers
|
|
1977
|
+
options.n_rendering_workers = args.n_rendering_workers
|
|
1967
1978
|
options.max_images_per_category = args.max_images_per_category
|
|
1968
|
-
|
|
1979
|
+
|
|
1969
1980
|
if args.use_processes:
|
|
1970
1981
|
options.parallelize_rendering_with_threads = False
|
|
1971
|
-
|
|
1982
|
+
|
|
1972
1983
|
results = n_way_comparison(args.results_files,options,args.detection_thresholds,args.rendering_thresholds)
|
|
1973
|
-
|
|
1984
|
+
|
|
1974
1985
|
if args.open_results:
|
|
1975
1986
|
path_utils.open_file(results.html_output_file)
|
|
1976
|
-
|
|
1987
|
+
|
|
1977
1988
|
print('Wrote results to {}'.format(results.html_output_file))
|
|
1978
|
-
|
|
1989
|
+
|
|
1979
1990
|
# ...main()
|
|
1980
1991
|
|
|
1981
|
-
|
|
1992
|
+
|
|
1982
1993
|
if __name__ == '__main__':
|
|
1983
|
-
|
|
1994
|
+
|
|
1984
1995
|
main()
|