megadetector 5.0.11__py3-none-any.whl → 5.0.13__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.

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