megadetector 5.0.11__py3-none-any.whl → 5.0.12__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/__init__.py +0 -0
- megadetector/api/batch_processing/__init__.py +0 -0
- 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 +439 -0
- megadetector/api/batch_processing/api_core/server.py +294 -0
- megadetector/api/batch_processing/api_core/server_api_config.py +98 -0
- megadetector/api/batch_processing/api_core/server_app_config.py +55 -0
- megadetector/api/batch_processing/api_core/server_batch_job_manager.py +220 -0
- megadetector/api/batch_processing/api_core/server_job_status_table.py +152 -0
- megadetector/api/batch_processing/api_core/server_orchestration.py +360 -0
- megadetector/api/batch_processing/api_core/server_utils.py +92 -0
- megadetector/api/batch_processing/api_core_support/__init__.py +0 -0
- megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +46 -0
- megadetector/api/batch_processing/api_support/__init__.py +0 -0
- megadetector/api/batch_processing/api_support/summarize_daily_activity.py +152 -0
- megadetector/api/batch_processing/data_preparation/__init__.py +0 -0
- megadetector/api/batch_processing/integration/digiKam/setup.py +6 -0
- megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +465 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/config_template.py +5 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +126 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +55 -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 +152 -0
- megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +266 -0
- megadetector/api/synchronous/api_core/animal_detection_api/config.py +35 -0
- megadetector/api/synchronous/api_core/tests/__init__.py +0 -0
- megadetector/api/synchronous/api_core/tests/load_test.py +110 -0
- megadetector/classification/__init__.py +0 -0
- megadetector/classification/aggregate_classifier_probs.py +108 -0
- megadetector/classification/analyze_failed_images.py +227 -0
- megadetector/classification/cache_batchapi_outputs.py +198 -0
- megadetector/classification/create_classification_dataset.py +627 -0
- megadetector/classification/crop_detections.py +516 -0
- megadetector/classification/csv_to_json.py +226 -0
- megadetector/classification/detect_and_crop.py +855 -0
- megadetector/classification/efficientnet/__init__.py +9 -0
- megadetector/classification/efficientnet/model.py +415 -0
- megadetector/classification/efficientnet/utils.py +610 -0
- megadetector/classification/evaluate_model.py +520 -0
- megadetector/classification/identify_mislabeled_candidates.py +152 -0
- megadetector/classification/json_to_azcopy_list.py +63 -0
- megadetector/classification/json_validator.py +699 -0
- megadetector/classification/map_classification_categories.py +276 -0
- megadetector/classification/merge_classification_detection_output.py +506 -0
- megadetector/classification/prepare_classification_script.py +194 -0
- megadetector/classification/prepare_classification_script_mc.py +228 -0
- megadetector/classification/run_classifier.py +287 -0
- megadetector/classification/save_mislabeled.py +110 -0
- megadetector/classification/train_classifier.py +827 -0
- megadetector/classification/train_classifier_tf.py +725 -0
- megadetector/classification/train_utils.py +323 -0
- megadetector/data_management/__init__.py +0 -0
- megadetector/data_management/annotations/__init__.py +0 -0
- megadetector/data_management/annotations/annotation_constants.py +34 -0
- megadetector/data_management/camtrap_dp_to_coco.py +239 -0
- megadetector/data_management/cct_json_utils.py +395 -0
- megadetector/data_management/cct_to_md.py +176 -0
- megadetector/data_management/cct_to_wi.py +289 -0
- megadetector/data_management/coco_to_labelme.py +272 -0
- megadetector/data_management/coco_to_yolo.py +662 -0
- megadetector/data_management/databases/__init__.py +0 -0
- megadetector/data_management/databases/add_width_and_height_to_db.py +33 -0
- megadetector/data_management/databases/combine_coco_camera_traps_files.py +206 -0
- megadetector/data_management/databases/integrity_check_json_db.py +477 -0
- megadetector/data_management/databases/subset_json_db.py +115 -0
- megadetector/data_management/generate_crops_from_cct.py +149 -0
- megadetector/data_management/get_image_sizes.py +189 -0
- megadetector/data_management/importers/add_nacti_sizes.py +52 -0
- megadetector/data_management/importers/add_timestamps_to_icct.py +79 -0
- megadetector/data_management/importers/animl_results_to_md_results.py +158 -0
- megadetector/data_management/importers/auckland_doc_test_to_json.py +373 -0
- megadetector/data_management/importers/auckland_doc_to_json.py +201 -0
- megadetector/data_management/importers/awc_to_json.py +191 -0
- megadetector/data_management/importers/bellevue_to_json.py +273 -0
- megadetector/data_management/importers/cacophony-thermal-importer.py +796 -0
- megadetector/data_management/importers/carrizo_shrubfree_2018.py +269 -0
- megadetector/data_management/importers/carrizo_trail_cam_2017.py +289 -0
- megadetector/data_management/importers/cct_field_adjustments.py +58 -0
- megadetector/data_management/importers/channel_islands_to_cct.py +913 -0
- megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +180 -0
- megadetector/data_management/importers/eMammal/eMammal_helpers.py +249 -0
- megadetector/data_management/importers/eMammal/make_eMammal_json.py +223 -0
- megadetector/data_management/importers/ena24_to_json.py +276 -0
- megadetector/data_management/importers/filenames_to_json.py +386 -0
- megadetector/data_management/importers/helena_to_cct.py +283 -0
- megadetector/data_management/importers/idaho-camera-traps.py +1407 -0
- megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +294 -0
- megadetector/data_management/importers/jb_csv_to_json.py +150 -0
- megadetector/data_management/importers/mcgill_to_json.py +250 -0
- megadetector/data_management/importers/missouri_to_json.py +490 -0
- megadetector/data_management/importers/nacti_fieldname_adjustments.py +79 -0
- megadetector/data_management/importers/noaa_seals_2019.py +181 -0
- megadetector/data_management/importers/pc_to_json.py +365 -0
- megadetector/data_management/importers/plot_wni_giraffes.py +123 -0
- megadetector/data_management/importers/prepare-noaa-fish-data-for-lila.py +359 -0
- megadetector/data_management/importers/prepare_zsl_imerit.py +131 -0
- megadetector/data_management/importers/rspb_to_json.py +356 -0
- megadetector/data_management/importers/save_the_elephants_survey_A.py +320 -0
- megadetector/data_management/importers/save_the_elephants_survey_B.py +329 -0
- megadetector/data_management/importers/snapshot_safari_importer.py +758 -0
- megadetector/data_management/importers/snapshot_safari_importer_reprise.py +665 -0
- megadetector/data_management/importers/snapshot_serengeti_lila.py +1067 -0
- megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +150 -0
- megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +153 -0
- megadetector/data_management/importers/sulross_get_exif.py +65 -0
- megadetector/data_management/importers/timelapse_csv_set_to_json.py +490 -0
- megadetector/data_management/importers/ubc_to_json.py +399 -0
- megadetector/data_management/importers/umn_to_json.py +507 -0
- megadetector/data_management/importers/wellington_to_json.py +263 -0
- megadetector/data_management/importers/wi_to_json.py +442 -0
- megadetector/data_management/importers/zamba_results_to_md_results.py +181 -0
- megadetector/data_management/labelme_to_coco.py +547 -0
- megadetector/data_management/labelme_to_yolo.py +272 -0
- megadetector/data_management/lila/__init__.py +0 -0
- megadetector/data_management/lila/add_locations_to_island_camera_traps.py +97 -0
- megadetector/data_management/lila/add_locations_to_nacti.py +147 -0
- megadetector/data_management/lila/create_lila_blank_set.py +558 -0
- megadetector/data_management/lila/create_lila_test_set.py +152 -0
- megadetector/data_management/lila/create_links_to_md_results_files.py +106 -0
- megadetector/data_management/lila/download_lila_subset.py +178 -0
- megadetector/data_management/lila/generate_lila_per_image_labels.py +516 -0
- megadetector/data_management/lila/get_lila_annotation_counts.py +170 -0
- megadetector/data_management/lila/get_lila_image_counts.py +112 -0
- megadetector/data_management/lila/lila_common.py +300 -0
- megadetector/data_management/lila/test_lila_metadata_urls.py +132 -0
- megadetector/data_management/ocr_tools.py +874 -0
- megadetector/data_management/read_exif.py +681 -0
- megadetector/data_management/remap_coco_categories.py +84 -0
- megadetector/data_management/remove_exif.py +66 -0
- megadetector/data_management/resize_coco_dataset.py +189 -0
- megadetector/data_management/wi_download_csv_to_coco.py +246 -0
- megadetector/data_management/yolo_output_to_md_output.py +441 -0
- megadetector/data_management/yolo_to_coco.py +676 -0
- megadetector/detection/__init__.py +0 -0
- megadetector/detection/detector_training/__init__.py +0 -0
- megadetector/detection/detector_training/model_main_tf2.py +114 -0
- megadetector/detection/process_video.py +702 -0
- megadetector/detection/pytorch_detector.py +341 -0
- megadetector/detection/run_detector.py +779 -0
- megadetector/detection/run_detector_batch.py +1219 -0
- megadetector/detection/run_inference_with_yolov5_val.py +917 -0
- megadetector/detection/run_tiled_inference.py +934 -0
- megadetector/detection/tf_detector.py +189 -0
- megadetector/detection/video_utils.py +606 -0
- megadetector/postprocessing/__init__.py +0 -0
- megadetector/postprocessing/add_max_conf.py +64 -0
- megadetector/postprocessing/categorize_detections_by_size.py +163 -0
- megadetector/postprocessing/combine_api_outputs.py +249 -0
- megadetector/postprocessing/compare_batch_results.py +958 -0
- megadetector/postprocessing/convert_output_format.py +396 -0
- megadetector/postprocessing/load_api_results.py +195 -0
- megadetector/postprocessing/md_to_coco.py +310 -0
- megadetector/postprocessing/md_to_labelme.py +330 -0
- megadetector/postprocessing/merge_detections.py +401 -0
- megadetector/postprocessing/postprocess_batch_results.py +1902 -0
- megadetector/postprocessing/remap_detection_categories.py +170 -0
- megadetector/postprocessing/render_detection_confusion_matrix.py +660 -0
- megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +211 -0
- megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +83 -0
- megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +1631 -0
- megadetector/postprocessing/separate_detections_into_folders.py +730 -0
- megadetector/postprocessing/subset_json_detector_output.py +696 -0
- megadetector/postprocessing/top_folders_to_bottom.py +223 -0
- megadetector/taxonomy_mapping/__init__.py +0 -0
- megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +491 -0
- megadetector/taxonomy_mapping/map_new_lila_datasets.py +150 -0
- megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +142 -0
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +590 -0
- megadetector/taxonomy_mapping/retrieve_sample_image.py +71 -0
- megadetector/taxonomy_mapping/simple_image_download.py +219 -0
- megadetector/taxonomy_mapping/species_lookup.py +834 -0
- megadetector/taxonomy_mapping/taxonomy_csv_checker.py +159 -0
- megadetector/taxonomy_mapping/taxonomy_graph.py +346 -0
- megadetector/taxonomy_mapping/validate_lila_category_mappings.py +83 -0
- megadetector/utils/__init__.py +0 -0
- megadetector/utils/azure_utils.py +178 -0
- megadetector/utils/ct_utils.py +612 -0
- megadetector/utils/directory_listing.py +246 -0
- megadetector/utils/md_tests.py +968 -0
- megadetector/utils/path_utils.py +1044 -0
- megadetector/utils/process_utils.py +157 -0
- megadetector/utils/sas_blob_utils.py +509 -0
- megadetector/utils/split_locations_into_train_val.py +228 -0
- megadetector/utils/string_utils.py +92 -0
- megadetector/utils/url_utils.py +323 -0
- megadetector/utils/write_html_image_list.py +225 -0
- megadetector/visualization/__init__.py +0 -0
- megadetector/visualization/plot_utils.py +293 -0
- megadetector/visualization/render_images_with_thumbnails.py +275 -0
- megadetector/visualization/visualization_utils.py +1536 -0
- megadetector/visualization/visualize_db.py +550 -0
- megadetector/visualization/visualize_detector_output.py +405 -0
- {megadetector-5.0.11.dist-info → megadetector-5.0.12.dist-info}/METADATA +1 -1
- megadetector-5.0.12.dist-info/RECORD +199 -0
- megadetector-5.0.12.dist-info/top_level.txt +1 -0
- megadetector-5.0.11.dist-info/RECORD +0 -5
- megadetector-5.0.11.dist-info/top_level.txt +0 -1
- {megadetector-5.0.11.dist-info → megadetector-5.0.12.dist-info}/LICENSE +0 -0
- {megadetector-5.0.11.dist-info → megadetector-5.0.12.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
compare_batch_results.py
|
|
4
|
+
|
|
5
|
+
Compare sets of batch results; typically used to compare:
|
|
6
|
+
|
|
7
|
+
* Results from different MegaDetector versions
|
|
8
|
+
* Results before/after RDE
|
|
9
|
+
* Results with/without augmentation
|
|
10
|
+
|
|
11
|
+
Makes pairwise comparisons, but can take lists of results files (will perform
|
|
12
|
+
all pairwise comparisons). Results are written to an HTML page that shows the number
|
|
13
|
+
and nature of disagreements (in the sense of each image being a detection or non-detection),
|
|
14
|
+
with sample images for each category.
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
#%% Imports
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import random
|
|
23
|
+
import copy
|
|
24
|
+
import urllib
|
|
25
|
+
import itertools
|
|
26
|
+
|
|
27
|
+
from tqdm import tqdm
|
|
28
|
+
from functools import partial
|
|
29
|
+
|
|
30
|
+
from multiprocessing.pool import ThreadPool
|
|
31
|
+
from multiprocessing.pool import Pool
|
|
32
|
+
|
|
33
|
+
from megadetector.visualization import visualization_utils
|
|
34
|
+
from megadetector.utils.write_html_image_list import write_html_image_list
|
|
35
|
+
from megadetector.utils import path_utils
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
#%% Constants and support classes
|
|
39
|
+
|
|
40
|
+
class PairwiseBatchComparisonOptions:
|
|
41
|
+
"""
|
|
42
|
+
Defines the options used for a single pairwise comparison; a list of these
|
|
43
|
+
pairwise options sets is stored in the BatchComparisonsOptions class.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
#: First filename to compare
|
|
47
|
+
results_filename_a = None
|
|
48
|
+
|
|
49
|
+
#: Second filename to compare
|
|
50
|
+
results_filename_b = None
|
|
51
|
+
|
|
52
|
+
#: Description to use in the output HTML for filename A
|
|
53
|
+
results_description_a = None
|
|
54
|
+
|
|
55
|
+
#: Description to use in the output HTML for filename B
|
|
56
|
+
results_description_b = None
|
|
57
|
+
|
|
58
|
+
#: Per-class detection thresholds to use for filename A (including a 'default' threshold)
|
|
59
|
+
detection_thresholds_a = {'animal':0.15,'person':0.15,'vehicle':0.15,'default':0.15}
|
|
60
|
+
|
|
61
|
+
#: Per-class detection thresholds to use for filename B (including a 'default' threshold)
|
|
62
|
+
detection_thresholds_b = {'animal':0.15,'person':0.15,'vehicle':0.15,'default':0.15}
|
|
63
|
+
|
|
64
|
+
#: Rendering threshold to use for all categories for filename A
|
|
65
|
+
rendering_confidence_threshold_a = 0.1
|
|
66
|
+
|
|
67
|
+
#: Rendering threshold to use for all categories for filename B
|
|
68
|
+
rendering_confidence_threshold_b = 0.1
|
|
69
|
+
|
|
70
|
+
# ...class PairwiseBatchComparisonOptions
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class BatchComparisonOptions:
|
|
74
|
+
"""
|
|
75
|
+
Defines the options for a set of (possibly many) pairwise comparisons.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
#: Folder to which we should write HTML output
|
|
79
|
+
output_folder = None
|
|
80
|
+
|
|
81
|
+
#: Base folder for images (which are specified as relative files)
|
|
82
|
+
image_folder = None
|
|
83
|
+
|
|
84
|
+
#: Job name to use in the HTML output file
|
|
85
|
+
job_name = ''
|
|
86
|
+
|
|
87
|
+
#: Maximum number of images to render for each category, where a "category" here is
|
|
88
|
+
#: "detections_a_only", "detections_b_only", etc., or None to render all images.
|
|
89
|
+
max_images_per_category = 1000
|
|
90
|
+
|
|
91
|
+
#: Maximum number of images per HTML page (paginates if a category page goes beyond this),
|
|
92
|
+
#: or None to disable pagination.
|
|
93
|
+
max_images_per_page = None
|
|
94
|
+
|
|
95
|
+
#: Colormap to use for detections in file A (maps detection categories to colors)
|
|
96
|
+
colormap_a = ['Red']
|
|
97
|
+
|
|
98
|
+
#: Colormap to use for detections in file B (maps detection categories to colors)
|
|
99
|
+
colormap_b = ['RoyalBlue']
|
|
100
|
+
|
|
101
|
+
#: Process-based parallelization isn't supported yet; this must be "True"
|
|
102
|
+
parallelize_rendering_with_threads = True
|
|
103
|
+
|
|
104
|
+
#: List of filenames to include in the comparison, or None to use all files
|
|
105
|
+
filenames_to_include = None
|
|
106
|
+
|
|
107
|
+
#: Compare only detections/non-detections, ignore categories (still renders categories)
|
|
108
|
+
class_agnostic_comparison = False
|
|
109
|
+
|
|
110
|
+
#: Width of images to render in the output HTML
|
|
111
|
+
target_width = 800
|
|
112
|
+
|
|
113
|
+
#: Number of workers to use for rendering, or <=1 to disable parallelization
|
|
114
|
+
n_rendering_workers = 20
|
|
115
|
+
|
|
116
|
+
#: Random seed for image sampling (not used if max_images_per_category is None)
|
|
117
|
+
random_seed = 0
|
|
118
|
+
|
|
119
|
+
#: Whether to sort results by confidence; if this is False, sorts by filename
|
|
120
|
+
sort_by_confidence = False
|
|
121
|
+
|
|
122
|
+
#: The expectation is that all results sets being compared will refer to the same images; if this
|
|
123
|
+
#: is True (default), we'll error if that's not the case, otherwise non-matching lists will just be
|
|
124
|
+
#: a warning.
|
|
125
|
+
error_on_non_matching_lists = True
|
|
126
|
+
|
|
127
|
+
#: List of PairwiseBatchComparisonOptions that defines the comparisons we'll render.
|
|
128
|
+
pairwise_options = []
|
|
129
|
+
|
|
130
|
+
# ...class BatchComparisonOptions
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class PairwiseBatchComparisonResults:
|
|
134
|
+
"""
|
|
135
|
+
The results from a single pairwise comparison.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
#: String of HTML content suitable for rendering to an HTML file
|
|
139
|
+
html_content = None
|
|
140
|
+
|
|
141
|
+
#: Possibly-modified version of the PairwiseBatchComparisonOptions supplied as input.
|
|
142
|
+
pairwise_options = None
|
|
143
|
+
|
|
144
|
+
#: A dictionary with keys including:
|
|
145
|
+
#:
|
|
146
|
+
#: common_detections
|
|
147
|
+
#: common_non_detections
|
|
148
|
+
#: detections_a_only
|
|
149
|
+
#: detections_b_only
|
|
150
|
+
#: class_transitions
|
|
151
|
+
#
|
|
152
|
+
#: Each of these maps a filename to a two-element list (the image in set A, the image in set B).
|
|
153
|
+
categories_to_image_pairs = None
|
|
154
|
+
|
|
155
|
+
# ...class PairwiseBatchComparisonResults
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class BatchComparisonResults:
|
|
159
|
+
"""
|
|
160
|
+
The results from a set of pairwise comparisons
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
#: Filename containing HTML output
|
|
164
|
+
html_output_file = None
|
|
165
|
+
|
|
166
|
+
#: A list of PairwiseBatchComparisonResults
|
|
167
|
+
pairwise_results = None
|
|
168
|
+
|
|
169
|
+
# ...class BatchComparisonResults
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
main_page_style_header = """<head>
|
|
173
|
+
<style type="text/css">
|
|
174
|
+
a { text-decoration: none; }
|
|
175
|
+
body { font-family: segoe ui, calibri, "trebuchet ms", verdana, arial, sans-serif; }
|
|
176
|
+
div.contentdiv { margin-left: 20px; }
|
|
177
|
+
</style>
|
|
178
|
+
</head>"""
|
|
179
|
+
|
|
180
|
+
main_page_header = '<html>\n{}\n<body>\n'.format(main_page_style_header)
|
|
181
|
+
main_page_footer = '<br/><br/><br/></body></html>\n'
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
#%% Comparison functions
|
|
185
|
+
|
|
186
|
+
def _render_image_pair(fn,image_pairs,category_folder,options,pairwise_options):
|
|
187
|
+
"""
|
|
188
|
+
Render two sets of results (i.e., a comparison) for a single image.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
fn (str): image filename
|
|
192
|
+
image_pairs (dict): dict mapping filenames to pairs of image dicts
|
|
193
|
+
category_folder (str): folder to which to render this image, typically
|
|
194
|
+
"detections_a_only", "detections_b_only", etc.
|
|
195
|
+
options (BatchComparisonOptions): job options
|
|
196
|
+
pairwise_options (PairwiseBatchComparisonOptions): pairwise comparison options
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
str: rendered image filename
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
input_image_path = os.path.join(options.image_folder,fn)
|
|
203
|
+
assert os.path.isfile(input_image_path), 'Image {} does not exist'.format(input_image_path)
|
|
204
|
+
|
|
205
|
+
im = visualization_utils.open_image(input_image_path)
|
|
206
|
+
image_pair = image_pairs[fn]
|
|
207
|
+
detections_a = image_pair[0]['detections']
|
|
208
|
+
detections_b = image_pair[1]['detections']
|
|
209
|
+
|
|
210
|
+
custom_strings_a = [''] * len(detections_a)
|
|
211
|
+
custom_strings_b = [''] * len(detections_b)
|
|
212
|
+
|
|
213
|
+
# This function is often used to compare results before/after various merging
|
|
214
|
+
# steps, so we have some special-case formatting based on the "transferred_from"
|
|
215
|
+
# field generated in merge_detections.py.
|
|
216
|
+
for i_det,det in enumerate(detections_a):
|
|
217
|
+
if 'transferred_from' in det:
|
|
218
|
+
custom_strings_a[i_det] = '({})'.format(
|
|
219
|
+
det['transferred_from'].split('.')[0])
|
|
220
|
+
|
|
221
|
+
for i_det,det in enumerate(detections_b):
|
|
222
|
+
if 'transferred_from' in det:
|
|
223
|
+
custom_strings_b[i_det] = '({})'.format(
|
|
224
|
+
det['transferred_from'].split('.')[0])
|
|
225
|
+
|
|
226
|
+
if options.target_width is not None:
|
|
227
|
+
im = visualization_utils.resize_image(im, options.target_width)
|
|
228
|
+
|
|
229
|
+
visualization_utils.render_detection_bounding_boxes(detections_a,im,
|
|
230
|
+
confidence_threshold=pairwise_options.rendering_confidence_threshold_a,
|
|
231
|
+
thickness=4,expansion=0,
|
|
232
|
+
colormap=options.colormap_a,
|
|
233
|
+
textalign=visualization_utils.TEXTALIGN_LEFT,
|
|
234
|
+
custom_strings=custom_strings_a)
|
|
235
|
+
visualization_utils.render_detection_bounding_boxes(detections_b,im,
|
|
236
|
+
confidence_threshold=pairwise_options.rendering_confidence_threshold_b,
|
|
237
|
+
thickness=2,expansion=0,
|
|
238
|
+
colormap=options.colormap_b,
|
|
239
|
+
textalign=visualization_utils.TEXTALIGN_RIGHT,
|
|
240
|
+
custom_strings=custom_strings_b)
|
|
241
|
+
|
|
242
|
+
output_image_fn = path_utils.flatten_path(fn)
|
|
243
|
+
output_image_path = os.path.join(category_folder,output_image_fn)
|
|
244
|
+
im.save(output_image_path)
|
|
245
|
+
return output_image_path
|
|
246
|
+
|
|
247
|
+
# ...def _render_image_pair()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _pairwise_compare_batch_results(options,output_index,pairwise_options):
|
|
251
|
+
"""
|
|
252
|
+
The main entry point for this module is compare_batch_results(), which calls
|
|
253
|
+
this function for each pair of comparisons the caller has requested. Generates an
|
|
254
|
+
HTML page for this comparison. Returns a BatchComparisonResults object.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
options (BatchComparisonOptions): overall job options for this comparison group
|
|
258
|
+
output_index (int): a numeric index used for generating HTML titles
|
|
259
|
+
pairwise_options (PairwiseBatchComparisonOptions): job options for this comparison
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
PairwiseBatchComparisonResults: the results of this pairwise comparison
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
# pairwise_options is passed as a parameter here, and should not be specified
|
|
266
|
+
# in the options object.
|
|
267
|
+
assert options.pairwise_options is None
|
|
268
|
+
|
|
269
|
+
if options.random_seed is not None:
|
|
270
|
+
random.seed(options.random_seed)
|
|
271
|
+
|
|
272
|
+
# Warn the user if some "detections" might not get rendered
|
|
273
|
+
max_classification_threshold_a = max(list(pairwise_options.detection_thresholds_a.values()))
|
|
274
|
+
max_classification_threshold_b = max(list(pairwise_options.detection_thresholds_b.values()))
|
|
275
|
+
|
|
276
|
+
if pairwise_options.rendering_confidence_threshold_a > max_classification_threshold_a:
|
|
277
|
+
print('*** Warning: rendering threshold A ({}) is higher than max confidence threshold A ({}) ***'.format(
|
|
278
|
+
pairwise_options.rendering_confidence_threshold_a,max_classification_threshold_a))
|
|
279
|
+
|
|
280
|
+
if pairwise_options.rendering_confidence_threshold_b > max_classification_threshold_b:
|
|
281
|
+
print('*** Warning: rendering threshold B ({}) is higher than max confidence threshold B ({}) ***'.format(
|
|
282
|
+
pairwise_options.rendering_confidence_threshold_b,max_classification_threshold_b))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
##%% Validate inputs
|
|
286
|
+
|
|
287
|
+
assert os.path.isfile(pairwise_options.results_filename_a), \
|
|
288
|
+
"Can't find results file {}".format(pairwise_options.results_filename_a)
|
|
289
|
+
assert os.path.isfile(pairwise_options.results_filename_b), \
|
|
290
|
+
"Can't find results file {}".format(pairwise_options.results_filename_b)
|
|
291
|
+
assert os.path.isdir(options.image_folder), \
|
|
292
|
+
"Can't find image folder {}".format(pairwise_options.image_folder)
|
|
293
|
+
os.makedirs(options.output_folder,exist_ok=True)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
##%% Load both result sets
|
|
297
|
+
|
|
298
|
+
with open(pairwise_options.results_filename_a,'r') as f:
|
|
299
|
+
results_a = json.load(f)
|
|
300
|
+
|
|
301
|
+
with open(pairwise_options.results_filename_b,'r') as f:
|
|
302
|
+
results_b = json.load(f)
|
|
303
|
+
|
|
304
|
+
# Don't let path separators confuse things
|
|
305
|
+
for im in results_a['images']:
|
|
306
|
+
if 'file' in im:
|
|
307
|
+
im['file'] = im['file'].replace('\\','/')
|
|
308
|
+
for im in results_b['images']:
|
|
309
|
+
if 'file' in im:
|
|
310
|
+
im['file'] = im['file'].replace('\\','/')
|
|
311
|
+
|
|
312
|
+
if not options.class_agnostic_comparison:
|
|
313
|
+
assert results_a['detection_categories'] == results_b['detection_categories'], \
|
|
314
|
+
"Cannot perform a class-sensitive comparison across results with different categories"
|
|
315
|
+
|
|
316
|
+
detection_categories_a = results_a['detection_categories']
|
|
317
|
+
detection_categories_b = results_b['detection_categories']
|
|
318
|
+
|
|
319
|
+
if pairwise_options.results_description_a is None:
|
|
320
|
+
if 'detector' not in results_a['info']:
|
|
321
|
+
print('No model metadata supplied for results-A, assuming MDv4')
|
|
322
|
+
pairwise_options.results_description_a = 'MDv4 (assumed)'
|
|
323
|
+
else:
|
|
324
|
+
pairwise_options.results_description_a = results_a['info']['detector']
|
|
325
|
+
|
|
326
|
+
if pairwise_options.results_description_b is None:
|
|
327
|
+
if 'detector' not in results_b['info']:
|
|
328
|
+
print('No model metadata supplied for results-B, assuming MDv4')
|
|
329
|
+
pairwise_options.results_description_b = 'MDv4 (assumed)'
|
|
330
|
+
else:
|
|
331
|
+
pairwise_options.results_description_b = results_b['info']['detector']
|
|
332
|
+
|
|
333
|
+
images_a = results_a['images']
|
|
334
|
+
images_b = results_b['images']
|
|
335
|
+
|
|
336
|
+
filename_to_image_a = {im['file']:im for im in images_a}
|
|
337
|
+
filename_to_image_b = {im['file']:im for im in images_b}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
##%% Make sure they represent the same set of images
|
|
341
|
+
|
|
342
|
+
filenames_a = [im['file'] for im in images_a]
|
|
343
|
+
filenames_b_set = set([im['file'] for im in images_b])
|
|
344
|
+
|
|
345
|
+
if len(images_a) != len(images_b):
|
|
346
|
+
s = 'set A has {} images, set B has {}'.format(len(images_a),len(images_b))
|
|
347
|
+
if options.error_on_non_matching_lists:
|
|
348
|
+
raise ValueError(s)
|
|
349
|
+
else:
|
|
350
|
+
print('Warning: ' + s)
|
|
351
|
+
else:
|
|
352
|
+
if options.error_on_non_matching_lists:
|
|
353
|
+
for fn in filenames_a:
|
|
354
|
+
assert fn in filenames_b_set
|
|
355
|
+
|
|
356
|
+
assert len(filenames_a) == len(images_a)
|
|
357
|
+
assert len(filenames_b_set) == len(images_b)
|
|
358
|
+
|
|
359
|
+
if options.filenames_to_include is None:
|
|
360
|
+
filenames_to_compare = filenames_a
|
|
361
|
+
else:
|
|
362
|
+
filenames_to_compare = options.filenames_to_include
|
|
363
|
+
|
|
364
|
+
##%% Find differences
|
|
365
|
+
|
|
366
|
+
# Each of these maps a filename to a two-element list (the image in set A, the image in set B)
|
|
367
|
+
#
|
|
368
|
+
# Right now, we only handle a very simple notion of class transition, where the detection
|
|
369
|
+
# of maximum confidence changes class *and* both images have an above-threshold detection.
|
|
370
|
+
common_detections = {}
|
|
371
|
+
common_non_detections = {}
|
|
372
|
+
detections_a_only = {}
|
|
373
|
+
detections_b_only = {}
|
|
374
|
+
class_transitions = {}
|
|
375
|
+
|
|
376
|
+
# fn = filenames_to_compare[0]
|
|
377
|
+
for fn in tqdm(filenames_to_compare):
|
|
378
|
+
|
|
379
|
+
if fn not in filename_to_image_b:
|
|
380
|
+
|
|
381
|
+
# We shouldn't have gotten this far if error_on_non_matching_lists is set
|
|
382
|
+
assert not options.error_on_non_matching_lists
|
|
383
|
+
|
|
384
|
+
print('Skipping filename {}, not in image set B'.format(fn))
|
|
385
|
+
continue
|
|
386
|
+
|
|
387
|
+
im_a = filename_to_image_a[fn]
|
|
388
|
+
im_b = filename_to_image_b[fn]
|
|
389
|
+
|
|
390
|
+
categories_above_threshold_a = set()
|
|
391
|
+
|
|
392
|
+
if not 'detections' in im_a or im_a['detections'] is None:
|
|
393
|
+
assert 'failure' in im_a and im_a['failure'] is not None
|
|
394
|
+
continue
|
|
395
|
+
|
|
396
|
+
if not 'detections' in im_b or im_b['detections'] is None:
|
|
397
|
+
assert 'failure' in im_b and im_b['failure'] is not None
|
|
398
|
+
continue
|
|
399
|
+
|
|
400
|
+
invalid_category_error = False
|
|
401
|
+
|
|
402
|
+
# det = im_a['detections'][0]
|
|
403
|
+
for det in im_a['detections']:
|
|
404
|
+
|
|
405
|
+
category_id = det['category']
|
|
406
|
+
|
|
407
|
+
if category_id not in detection_categories_a:
|
|
408
|
+
print('Warning: unexpected category {} for model A on file {}'.format(category_id,fn))
|
|
409
|
+
invalid_category_error = True
|
|
410
|
+
break
|
|
411
|
+
|
|
412
|
+
conf = det['conf']
|
|
413
|
+
|
|
414
|
+
if detection_categories_a[category_id] in pairwise_options.detection_thresholds_a:
|
|
415
|
+
conf_thresh = pairwise_options.detection_thresholds_a[detection_categories_a[category_id]]
|
|
416
|
+
else:
|
|
417
|
+
conf_thresh = pairwise_options.detection_thresholds_a['default']
|
|
418
|
+
|
|
419
|
+
if conf >= conf_thresh:
|
|
420
|
+
categories_above_threshold_a.add(category_id)
|
|
421
|
+
|
|
422
|
+
if invalid_category_error:
|
|
423
|
+
continue
|
|
424
|
+
|
|
425
|
+
categories_above_threshold_b = set()
|
|
426
|
+
|
|
427
|
+
for det in im_b['detections']:
|
|
428
|
+
|
|
429
|
+
category_id = det['category']
|
|
430
|
+
|
|
431
|
+
if category_id not in detection_categories_b:
|
|
432
|
+
print('Warning: unexpected category {} for model B on file {}'.format(category_id,fn))
|
|
433
|
+
invalid_category_error = True
|
|
434
|
+
break
|
|
435
|
+
|
|
436
|
+
conf = det['conf']
|
|
437
|
+
|
|
438
|
+
if detection_categories_b[category_id] in pairwise_options.detection_thresholds_b:
|
|
439
|
+
conf_thresh = pairwise_options.detection_thresholds_b[detection_categories_b[category_id]]
|
|
440
|
+
else:
|
|
441
|
+
conf_thresh = pairwise_options.detection_thresholds_a['default']
|
|
442
|
+
|
|
443
|
+
if conf >= conf_thresh:
|
|
444
|
+
categories_above_threshold_b.add(category_id)
|
|
445
|
+
|
|
446
|
+
if invalid_category_error:
|
|
447
|
+
continue
|
|
448
|
+
|
|
449
|
+
im_pair = (im_a,im_b)
|
|
450
|
+
|
|
451
|
+
detection_a = (len(categories_above_threshold_a) > 0)
|
|
452
|
+
detection_b = (len(categories_above_threshold_b) > 0)
|
|
453
|
+
|
|
454
|
+
if detection_a and detection_b:
|
|
455
|
+
if (categories_above_threshold_a == categories_above_threshold_b) or \
|
|
456
|
+
options.class_agnostic_comparison:
|
|
457
|
+
common_detections[fn] = im_pair
|
|
458
|
+
else:
|
|
459
|
+
class_transitions[fn] = im_pair
|
|
460
|
+
elif (not detection_a) and (not detection_b):
|
|
461
|
+
common_non_detections[fn] = im_pair
|
|
462
|
+
elif detection_a and (not detection_b):
|
|
463
|
+
detections_a_only[fn] = im_pair
|
|
464
|
+
else:
|
|
465
|
+
assert detection_b and (not detection_a)
|
|
466
|
+
detections_b_only[fn] = im_pair
|
|
467
|
+
|
|
468
|
+
# ...for each filename
|
|
469
|
+
|
|
470
|
+
print('Of {} files:\n{} common detections\n{} common non-detections\n{} A only\n{} B only\n{} class transitions'.format(
|
|
471
|
+
len(filenames_to_compare),len(common_detections),
|
|
472
|
+
len(common_non_detections),len(detections_a_only),
|
|
473
|
+
len(detections_b_only),len(class_transitions)))
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
##%% Sample and plot differences
|
|
477
|
+
|
|
478
|
+
if options.n_rendering_workers > 1:
|
|
479
|
+
worker_type = 'processes'
|
|
480
|
+
if options.parallelize_rendering_with_threads:
|
|
481
|
+
worker_type = 'threads'
|
|
482
|
+
print('Rendering images with {} {}'.format(options.n_rendering_workers,worker_type))
|
|
483
|
+
if options.parallelize_rendering_with_threads:
|
|
484
|
+
pool = ThreadPool(options.n_rendering_workers)
|
|
485
|
+
else:
|
|
486
|
+
pool = Pool(options.n_rendering_workers)
|
|
487
|
+
|
|
488
|
+
categories_to_image_pairs = {
|
|
489
|
+
'common_detections':common_detections,
|
|
490
|
+
'common_non_detections':common_non_detections,
|
|
491
|
+
'detections_a_only':detections_a_only,
|
|
492
|
+
'detections_b_only':detections_b_only,
|
|
493
|
+
'class_transitions':class_transitions
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
categories_to_page_titles = {
|
|
497
|
+
'common_detections':'Detections common to both models',
|
|
498
|
+
'common_non_detections':'Non-detections common to both models',
|
|
499
|
+
'detections_a_only':'Detections reported by model A only',
|
|
500
|
+
'detections_b_only':'Detections reported by model B only',
|
|
501
|
+
'class_transitions':'Detections reported as different classes by models A and B'
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
local_output_folder = os.path.join(options.output_folder,'cmp_' + \
|
|
505
|
+
str(output_index).zfill(3))
|
|
506
|
+
|
|
507
|
+
def render_detection_comparisons(category,image_pairs,image_filenames):
|
|
508
|
+
|
|
509
|
+
print('Rendering detections for category {}'.format(category))
|
|
510
|
+
|
|
511
|
+
category_folder = os.path.join(local_output_folder,category)
|
|
512
|
+
os.makedirs(category_folder,exist_ok=True)
|
|
513
|
+
|
|
514
|
+
# fn = image_filenames[0]
|
|
515
|
+
if options.n_rendering_workers <= 1:
|
|
516
|
+
output_image_paths = []
|
|
517
|
+
for fn in tqdm(image_filenames):
|
|
518
|
+
output_image_paths.append(_render_image_pair(fn,image_pairs,category_folder,
|
|
519
|
+
options,pairwise_options))
|
|
520
|
+
else:
|
|
521
|
+
output_image_paths = list(tqdm(pool.imap(
|
|
522
|
+
partial(_render_image_pair, image_pairs=image_pairs,
|
|
523
|
+
category_folder=category_folder,options=options,
|
|
524
|
+
pairwise_options=pairwise_options),
|
|
525
|
+
image_filenames),
|
|
526
|
+
total=len(image_filenames)))
|
|
527
|
+
|
|
528
|
+
return output_image_paths
|
|
529
|
+
|
|
530
|
+
# ...def render_detection_comparisons()
|
|
531
|
+
|
|
532
|
+
# For each category, generate comparison images and the
|
|
533
|
+
# comparison HTML page.
|
|
534
|
+
#
|
|
535
|
+
# category = 'common_detections'
|
|
536
|
+
for category in categories_to_image_pairs.keys():
|
|
537
|
+
|
|
538
|
+
# Choose detection pairs we're going to render for this category
|
|
539
|
+
image_pairs = categories_to_image_pairs[category]
|
|
540
|
+
image_filenames = list(image_pairs.keys())
|
|
541
|
+
|
|
542
|
+
if options.max_images_per_category is not None and options.max_images_per_category > 0:
|
|
543
|
+
if len(image_filenames) > options.max_images_per_category:
|
|
544
|
+
print('Sampling {} of {} image pairs for category {}'.format(
|
|
545
|
+
options.max_images_per_category,
|
|
546
|
+
len(image_filenames),
|
|
547
|
+
category))
|
|
548
|
+
image_filenames = random.sample(image_filenames,
|
|
549
|
+
options.max_images_per_category)
|
|
550
|
+
assert len(image_filenames) <= options.max_images_per_category
|
|
551
|
+
|
|
552
|
+
input_image_absolute_paths = [os.path.join(options.image_folder,fn) for fn in image_filenames]
|
|
553
|
+
|
|
554
|
+
category_image_output_paths = render_detection_comparisons(category,
|
|
555
|
+
image_pairs,image_filenames)
|
|
556
|
+
|
|
557
|
+
category_html_filename = os.path.join(local_output_folder,
|
|
558
|
+
category + '.html')
|
|
559
|
+
category_image_output_paths_relative = [os.path.relpath(s,local_output_folder) \
|
|
560
|
+
for s in category_image_output_paths]
|
|
561
|
+
|
|
562
|
+
image_info = []
|
|
563
|
+
|
|
564
|
+
assert len(category_image_output_paths_relative) == len(input_image_absolute_paths)
|
|
565
|
+
|
|
566
|
+
for i_fn,fn in enumerate(category_image_output_paths_relative):
|
|
567
|
+
|
|
568
|
+
input_path_relative = image_filenames[i_fn]
|
|
569
|
+
image_pair = image_pairs[input_path_relative]
|
|
570
|
+
assert len(image_pair) == 2; image_a = image_pair[0]; image_b = image_pair[1]
|
|
571
|
+
|
|
572
|
+
def maxempty(L):
|
|
573
|
+
if len(L) == 0:
|
|
574
|
+
return 0
|
|
575
|
+
else:
|
|
576
|
+
return max(L)
|
|
577
|
+
|
|
578
|
+
max_conf_a = maxempty([det['conf'] for det in image_a['detections']])
|
|
579
|
+
max_conf_b = maxempty([det['conf'] for det in image_b['detections']])
|
|
580
|
+
|
|
581
|
+
title = input_path_relative + ' (max conf {:.2f},{:.2f})'.format(max_conf_a,max_conf_b)
|
|
582
|
+
|
|
583
|
+
# Only used if sort_by_confidence is True
|
|
584
|
+
if category == 'common_detections':
|
|
585
|
+
sort_conf = max(max_conf_a,max_conf_b)
|
|
586
|
+
elif category == 'common_non_detections':
|
|
587
|
+
sort_conf = max(max_conf_a,max_conf_b)
|
|
588
|
+
elif category == 'detections_a_only':
|
|
589
|
+
sort_conf = max_conf_a
|
|
590
|
+
elif category == 'detections_b_only':
|
|
591
|
+
sort_conf = max_conf_b
|
|
592
|
+
elif category == 'class_transitions':
|
|
593
|
+
sort_conf = max(max_conf_a,max_conf_b)
|
|
594
|
+
else:
|
|
595
|
+
print('Warning: unknown sort category {}'.format(category))
|
|
596
|
+
sort_conf = max(max_conf_a,max_conf_b)
|
|
597
|
+
|
|
598
|
+
info = {
|
|
599
|
+
'filename': fn,
|
|
600
|
+
'title': title,
|
|
601
|
+
'textStyle': 'font-family:verdana,arial,calibri;font-size:' + \
|
|
602
|
+
'80%;text-align:left;margin-top:20;margin-bottom:5',
|
|
603
|
+
'linkTarget': urllib.parse.quote(input_image_absolute_paths[i_fn]),
|
|
604
|
+
'sort_conf':sort_conf
|
|
605
|
+
}
|
|
606
|
+
image_info.append(info)
|
|
607
|
+
|
|
608
|
+
# ...for each image
|
|
609
|
+
|
|
610
|
+
category_page_header_string = '<h1>{}</h1>'.format(categories_to_page_titles[category])
|
|
611
|
+
category_page_header_string += '<p style="font-weight:bold;">\n'
|
|
612
|
+
category_page_header_string += 'Model A: {}<br/>\n'.format(
|
|
613
|
+
pairwise_options.results_description_a)
|
|
614
|
+
category_page_header_string += 'Model B: {}'.format(pairwise_options.results_description_b)
|
|
615
|
+
category_page_header_string += '</p>\n'
|
|
616
|
+
|
|
617
|
+
category_page_header_string += '<p>\n'
|
|
618
|
+
category_page_header_string += 'Detection thresholds for A ({}):\n{}<br/>'.format(
|
|
619
|
+
pairwise_options.results_description_a,str(pairwise_options.detection_thresholds_a))
|
|
620
|
+
category_page_header_string += 'Detection thresholds for B ({}):\n{}<br/>'.format(
|
|
621
|
+
pairwise_options.results_description_b,str(pairwise_options.detection_thresholds_b))
|
|
622
|
+
category_page_header_string += 'Rendering threshold for A ({}):\n{}<br/>'.format(
|
|
623
|
+
pairwise_options.results_description_a,
|
|
624
|
+
str(pairwise_options.rendering_confidence_threshold_a))
|
|
625
|
+
category_page_header_string += 'Rendering threshold for B ({}):\n{}<br/>'.format(
|
|
626
|
+
pairwise_options.results_description_b,
|
|
627
|
+
str(pairwise_options.rendering_confidence_threshold_b))
|
|
628
|
+
category_page_header_string += '</p>\n'
|
|
629
|
+
|
|
630
|
+
# Default to sorting by filename
|
|
631
|
+
if options.sort_by_confidence:
|
|
632
|
+
image_info = sorted(image_info, key=lambda d: d['sort_conf'], reverse=True)
|
|
633
|
+
else:
|
|
634
|
+
image_info = sorted(image_info, key=lambda d: d['filename'])
|
|
635
|
+
|
|
636
|
+
write_html_image_list(
|
|
637
|
+
category_html_filename,
|
|
638
|
+
images=image_info,
|
|
639
|
+
options={
|
|
640
|
+
'headerHtml': category_page_header_string,
|
|
641
|
+
'maxFiguresPerHtmlFile': options.max_images_per_page
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
# ...for each category
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
##%% Write the top-level HTML file content
|
|
648
|
+
|
|
649
|
+
html_output_string = ''
|
|
650
|
+
|
|
651
|
+
html_output_string += '<p>Comparing <b>{}</b> (A, red) to <b>{}</b> (B, blue)</p>'.format(
|
|
652
|
+
pairwise_options.results_description_a,pairwise_options.results_description_b)
|
|
653
|
+
html_output_string += '<div class="contentdiv">\n'
|
|
654
|
+
html_output_string += 'Detection thresholds for {}:\n{}<br/>'.format(
|
|
655
|
+
pairwise_options.results_description_a,
|
|
656
|
+
str(pairwise_options.detection_thresholds_a))
|
|
657
|
+
html_output_string += 'Detection thresholds for {}:\n{}<br/>'.format(
|
|
658
|
+
pairwise_options.results_description_b,
|
|
659
|
+
str(pairwise_options.detection_thresholds_b))
|
|
660
|
+
html_output_string += 'Rendering threshold for {}:\n{}<br/>'.format(
|
|
661
|
+
pairwise_options.results_description_a,
|
|
662
|
+
str(pairwise_options.rendering_confidence_threshold_a))
|
|
663
|
+
html_output_string += 'Rendering threshold for {}:\n{}<br/>'.format(
|
|
664
|
+
pairwise_options.results_description_b,
|
|
665
|
+
str(pairwise_options.rendering_confidence_threshold_b))
|
|
666
|
+
|
|
667
|
+
html_output_string += '<br/>'
|
|
668
|
+
|
|
669
|
+
html_output_string += 'Rendering a maximum of {} images per category<br/>'.format(
|
|
670
|
+
options.max_images_per_category)
|
|
671
|
+
|
|
672
|
+
html_output_string += '<br/>'
|
|
673
|
+
|
|
674
|
+
html_output_string += ('Of {} total files:<br/><br/><div style="margin-left:15px;">{} common detections<br/>{} common non-detections<br/>{} A only<br/>{} B only<br/>{} class transitions</div><br/>'.format(
|
|
675
|
+
len(filenames_to_compare),len(common_detections),
|
|
676
|
+
len(common_non_detections),len(detections_a_only),
|
|
677
|
+
len(detections_b_only),len(class_transitions)))
|
|
678
|
+
|
|
679
|
+
html_output_string += 'Comparison pages:<br/><br/>\n'
|
|
680
|
+
html_output_string += '<div style="margin-left:15px;">\n'
|
|
681
|
+
|
|
682
|
+
comparison_path_relative = os.path.relpath(local_output_folder,options.output_folder)
|
|
683
|
+
for category in categories_to_image_pairs.keys():
|
|
684
|
+
category_html_filename = os.path.join(comparison_path_relative,category + '.html')
|
|
685
|
+
html_output_string += '<a href="{}">{}</a><br/>\n'.format(
|
|
686
|
+
category_html_filename,category)
|
|
687
|
+
|
|
688
|
+
html_output_string += '</div>\n'
|
|
689
|
+
html_output_string += '</div>\n'
|
|
690
|
+
|
|
691
|
+
pairwise_results = PairwiseBatchComparisonResults()
|
|
692
|
+
|
|
693
|
+
pairwise_results.html_content = html_output_string
|
|
694
|
+
pairwise_results.pairwise_options = pairwise_options
|
|
695
|
+
pairwise_results.categories_to_image_pairs = categories_to_image_pairs
|
|
696
|
+
|
|
697
|
+
return pairwise_results
|
|
698
|
+
|
|
699
|
+
# ...def _pairwise_compare_batch_results()
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def compare_batch_results(options):
|
|
703
|
+
"""
|
|
704
|
+
The main entry point for this module. Runs one or more batch results comparisons,
|
|
705
|
+
writing results to an html page. Most of the work is deferred to _pairwise_compare_batch_results().
|
|
706
|
+
|
|
707
|
+
Args:
|
|
708
|
+
options (BatchComparisonOptions): job options to use for this comparison task, including the
|
|
709
|
+
list of specific pairswise comparisons to make (in the pairwise_options field)
|
|
710
|
+
|
|
711
|
+
Returns:
|
|
712
|
+
BatchComparisonResults: the results of this comparison task
|
|
713
|
+
"""
|
|
714
|
+
|
|
715
|
+
assert options.output_folder is not None
|
|
716
|
+
assert options.image_folder is not None
|
|
717
|
+
assert options.pairwise_options is not None
|
|
718
|
+
|
|
719
|
+
options = copy.deepcopy(options)
|
|
720
|
+
|
|
721
|
+
if not isinstance(options.pairwise_options,list):
|
|
722
|
+
options.pairwise_options = [options.pairwise_options]
|
|
723
|
+
|
|
724
|
+
pairwise_options_list = options.pairwise_options
|
|
725
|
+
n_comparisons = len(pairwise_options_list)
|
|
726
|
+
|
|
727
|
+
options.pairwise_options = None
|
|
728
|
+
|
|
729
|
+
html_content = ''
|
|
730
|
+
all_pairwise_results = []
|
|
731
|
+
|
|
732
|
+
# i_comparison = 0; pairwise_options = pairwise_options_list[i_comparison]
|
|
733
|
+
for i_comparison,pairwise_options in enumerate(pairwise_options_list):
|
|
734
|
+
print('Running comparison {} of {}'.format(i_comparison,n_comparisons))
|
|
735
|
+
pairwise_results = \
|
|
736
|
+
_pairwise_compare_batch_results(options,i_comparison,pairwise_options)
|
|
737
|
+
html_content += pairwise_results.html_content
|
|
738
|
+
all_pairwise_results.append(pairwise_results)
|
|
739
|
+
|
|
740
|
+
html_output_string = main_page_header
|
|
741
|
+
job_name_string = ''
|
|
742
|
+
if len(options.job_name) > 0:
|
|
743
|
+
job_name_string = ' for {}'.format(options.job_name)
|
|
744
|
+
html_output_string += '<h2>Comparison of results{}</h2>\n'.format(
|
|
745
|
+
job_name_string)
|
|
746
|
+
html_output_string += html_content
|
|
747
|
+
html_output_string += main_page_footer
|
|
748
|
+
|
|
749
|
+
html_output_file = os.path.join(options.output_folder,'index.html')
|
|
750
|
+
with open(html_output_file,'w') as f:
|
|
751
|
+
f.write(html_output_string)
|
|
752
|
+
|
|
753
|
+
results = BatchComparisonResults()
|
|
754
|
+
results.html_output_file = html_output_file
|
|
755
|
+
results.pairwise_results = all_pairwise_results
|
|
756
|
+
return results
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def n_way_comparison(filenames,options,detection_thresholds=None,rendering_thresholds=None):
|
|
760
|
+
"""
|
|
761
|
+
Performs N pairwise comparisons for the list of results files in [filenames], by generating
|
|
762
|
+
sets of pairwise options and calling compare_batch_results.
|
|
763
|
+
|
|
764
|
+
Args:
|
|
765
|
+
filenames (list): list of MD results filenames to compare
|
|
766
|
+
options (BatchComparisonOptions): task options set in which pairwise_options is still
|
|
767
|
+
empty; that will get populated from [filenames]
|
|
768
|
+
detection_thresholds (list, optional): list of detection thresholds with the same length
|
|
769
|
+
as [filenames], or None to use sensible defaults
|
|
770
|
+
rendering_thresholds (list, optional): list of rendering thresholds with the same length
|
|
771
|
+
as [filenames], or None to use sensible defaults
|
|
772
|
+
|
|
773
|
+
Returns:
|
|
774
|
+
BatchComparisonResults: the results of this comparison task
|
|
775
|
+
"""
|
|
776
|
+
|
|
777
|
+
if detection_thresholds is None:
|
|
778
|
+
detection_thresholds = [0.15] * len(filenames)
|
|
779
|
+
assert len(detection_thresholds) == len(filenames)
|
|
780
|
+
|
|
781
|
+
if rendering_thresholds is not None:
|
|
782
|
+
assert len(rendering_thresholds) == len(detection_thresholds)
|
|
783
|
+
else:
|
|
784
|
+
rendering_thresholds = [(x*0.6666) for x in detection_thresholds]
|
|
785
|
+
|
|
786
|
+
# Choose all pairwise combinations of the files in [filenames]
|
|
787
|
+
for i, j in itertools.combinations(list(range(0,len(filenames))),2):
|
|
788
|
+
|
|
789
|
+
pairwise_options = PairwiseBatchComparisonOptions()
|
|
790
|
+
|
|
791
|
+
pairwise_options.results_filename_a = filenames[i]
|
|
792
|
+
pairwise_options.results_filename_b = filenames[j]
|
|
793
|
+
|
|
794
|
+
pairwise_options.rendering_confidence_threshold_a = rendering_thresholds[i]
|
|
795
|
+
pairwise_options.rendering_confidence_threshold_b = rendering_thresholds[j]
|
|
796
|
+
|
|
797
|
+
pairwise_options.detection_thresholds_a = {'default':detection_thresholds[i]}
|
|
798
|
+
pairwise_options.detection_thresholds_b = {'default':detection_thresholds[j]}
|
|
799
|
+
|
|
800
|
+
options.pairwise_options.append(pairwise_options)
|
|
801
|
+
|
|
802
|
+
return compare_batch_results(options)
|
|
803
|
+
|
|
804
|
+
# ...n_way_comparison()
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
#%% Interactive driver
|
|
808
|
+
|
|
809
|
+
if False:
|
|
810
|
+
|
|
811
|
+
#%% Test two-way comparison
|
|
812
|
+
|
|
813
|
+
options = BatchComparisonOptions()
|
|
814
|
+
|
|
815
|
+
options.parallelize_rendering_with_threads = True
|
|
816
|
+
|
|
817
|
+
options.job_name = 'BCT'
|
|
818
|
+
options.output_folder = r'g:\temp\comparisons'
|
|
819
|
+
options.image_folder = r'g:\camera_traps\camera_trap_images'
|
|
820
|
+
options.max_images_per_category = 100
|
|
821
|
+
options.sort_by_confidence = True
|
|
822
|
+
|
|
823
|
+
options.pairwise_options = []
|
|
824
|
+
|
|
825
|
+
results_base = os.path.expanduser('~/postprocessing/bellevue-camera-traps')
|
|
826
|
+
filenames = [
|
|
827
|
+
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'),
|
|
828
|
+
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')
|
|
829
|
+
]
|
|
830
|
+
|
|
831
|
+
detection_thresholds = [0.15,0.15]
|
|
832
|
+
rendering_thresholds = None
|
|
833
|
+
|
|
834
|
+
results = n_way_comparison(filenames,options,detection_thresholds,rendering_thresholds=rendering_thresholds)
|
|
835
|
+
|
|
836
|
+
from megadetector.utils.path_utils import open_file
|
|
837
|
+
open_file(results.html_output_file)
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
#%% Test three-way comparison
|
|
841
|
+
|
|
842
|
+
options = BatchComparisonOptions()
|
|
843
|
+
|
|
844
|
+
options.parallelize_rendering_with_threads = False
|
|
845
|
+
|
|
846
|
+
options.job_name = 'KGA-test'
|
|
847
|
+
options.output_folder = os.path.expanduser('~/tmp/md-comparison-test')
|
|
848
|
+
options.image_folder = os.path.expanduser('~/data/KGA')
|
|
849
|
+
|
|
850
|
+
options.pairwise_options = []
|
|
851
|
+
|
|
852
|
+
filenames = [
|
|
853
|
+
os.path.expanduser('~/data/KGA-4.json'),
|
|
854
|
+
os.path.expanduser('~/data/KGA-5a.json'),
|
|
855
|
+
os.path.expanduser('~/data/KGA-5b.json')
|
|
856
|
+
]
|
|
857
|
+
|
|
858
|
+
detection_thresholds = [0.7,0.15,0.15]
|
|
859
|
+
|
|
860
|
+
results = n_way_comparison(filenames,options,detection_thresholds,rendering_thresholds=None)
|
|
861
|
+
|
|
862
|
+
from megadetector.utils.path_utils import open_file
|
|
863
|
+
open_file(results.html_output_file)
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
#%% Command-line driver
|
|
867
|
+
|
|
868
|
+
"""
|
|
869
|
+
python compare_batch_results.py ~/tmp/comparison-test ~/data/KGA ~/data/KGA-5a.json ~/data/KGA-5b.json ~/data/KGA-4.json --detection_thresholds 0.15 0.15 0.7 --rendering_thresholds 0.1 0.1 0.6 --use_processes
|
|
870
|
+
"""
|
|
871
|
+
|
|
872
|
+
import sys,argparse,textwrap
|
|
873
|
+
|
|
874
|
+
def main():
|
|
875
|
+
|
|
876
|
+
options = BatchComparisonOptions()
|
|
877
|
+
|
|
878
|
+
parser = argparse.ArgumentParser(
|
|
879
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
880
|
+
epilog=textwrap.dedent('''\
|
|
881
|
+
Example:
|
|
882
|
+
|
|
883
|
+
python compare_batch_results.py output_folder image_folder mdv5a.json mdv5b.json mdv4.json --detection_thresholds 0.15 0.15 0.7
|
|
884
|
+
'''))
|
|
885
|
+
|
|
886
|
+
parser.add_argument('output_folder', type=str, help='folder to which to write html results')
|
|
887
|
+
|
|
888
|
+
parser.add_argument('image_folder', type=str, help='image source folder')
|
|
889
|
+
|
|
890
|
+
parser.add_argument('results_files', nargs='*', type=str, help='list of .json files to be compared')
|
|
891
|
+
|
|
892
|
+
parser.add_argument('--detection_thresholds', nargs='*', type=float,
|
|
893
|
+
help='list of detection thresholds, same length as the number of .json files, ' + \
|
|
894
|
+
'defaults to 0.15 for all files')
|
|
895
|
+
|
|
896
|
+
parser.add_argument('--rendering_thresholds', nargs='*', type=float,
|
|
897
|
+
help='list of rendering thresholds, same length as the number of .json files, ' + \
|
|
898
|
+
'defaults to 0.10 for all files')
|
|
899
|
+
|
|
900
|
+
parser.add_argument('--max_images_per_category', type=int, default=options.max_images_per_category,
|
|
901
|
+
help='number of images to sample for each agreement category (common detections, etc.)')
|
|
902
|
+
|
|
903
|
+
parser.add_argument('--target_width', type=int, default=options.target_width,
|
|
904
|
+
help='output image width, defaults to {}'.format(options.target_width))
|
|
905
|
+
|
|
906
|
+
parser.add_argument('--use_processes', action='store_true',
|
|
907
|
+
help='use processes rather than threads for parallelization')
|
|
908
|
+
|
|
909
|
+
parser.add_argument('--open_results', action='store_true',
|
|
910
|
+
help='open the output html file when done')
|
|
911
|
+
|
|
912
|
+
parser.add_argument('--n_rendering_workers', type=int, default=options.n_rendering_workers,
|
|
913
|
+
help='number of workers for parallel rendering, defaults to {}'.format(
|
|
914
|
+
options.n_rendering_workers))
|
|
915
|
+
|
|
916
|
+
if len(sys.argv[1:])==0:
|
|
917
|
+
parser.print_help()
|
|
918
|
+
parser.exit()
|
|
919
|
+
|
|
920
|
+
args = parser.parse_args()
|
|
921
|
+
|
|
922
|
+
print('Output folder:')
|
|
923
|
+
print(args.output_folder)
|
|
924
|
+
|
|
925
|
+
print('\nResults files:')
|
|
926
|
+
print(args.results_files)
|
|
927
|
+
|
|
928
|
+
print('\nDetection thresholds:')
|
|
929
|
+
print(args.detection_thresholds)
|
|
930
|
+
|
|
931
|
+
print('\nRendering thresholds:')
|
|
932
|
+
print(args.rendering_thresholds)
|
|
933
|
+
|
|
934
|
+
# Convert to options objects
|
|
935
|
+
options = BatchComparisonOptions()
|
|
936
|
+
|
|
937
|
+
options.output_folder = args.output_folder
|
|
938
|
+
options.image_folder = args.image_folder
|
|
939
|
+
options.target_width = args.target_width
|
|
940
|
+
options.n_rendering_workers = args.n_rendering_workers
|
|
941
|
+
options.max_images_per_category = args.max_images_per_category
|
|
942
|
+
|
|
943
|
+
if args.use_processes:
|
|
944
|
+
options.parallelize_rendering_with_threads = False
|
|
945
|
+
|
|
946
|
+
results = n_way_comparison(args.results_files,options,args.detection_thresholds,args.rendering_thresholds)
|
|
947
|
+
|
|
948
|
+
if args.open_results:
|
|
949
|
+
path_utils.open_file(results.html_output_file)
|
|
950
|
+
|
|
951
|
+
print('Wrote results to {}'.format(results.html_output_file))
|
|
952
|
+
|
|
953
|
+
# ...main()
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
if __name__ == '__main__':
|
|
957
|
+
|
|
958
|
+
main()
|