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