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.

Files changed (201) 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 +98 -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 +152 -0
  11. megadetector/api/batch_processing/api_core/server_orchestration.py +360 -0
  12. megadetector/api/batch_processing/api_core/server_utils.py +92 -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 +126 -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 +266 -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 +610 -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 +239 -0
  58. megadetector/data_management/cct_json_utils.py +395 -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 +272 -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 +477 -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 +796 -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 +874 -0
  129. megadetector/data_management/read_exif.py +681 -0
  130. megadetector/data_management/remap_coco_categories.py +84 -0
  131. megadetector/data_management/remove_exif.py +66 -0
  132. megadetector/data_management/resize_coco_dataset.py +189 -0
  133. megadetector/data_management/wi_download_csv_to_coco.py +246 -0
  134. megadetector/data_management/yolo_output_to_md_output.py +441 -0
  135. megadetector/data_management/yolo_to_coco.py +676 -0
  136. megadetector/detection/__init__.py +0 -0
  137. megadetector/detection/detector_training/__init__.py +0 -0
  138. megadetector/detection/detector_training/model_main_tf2.py +114 -0
  139. megadetector/detection/process_video.py +702 -0
  140. megadetector/detection/pytorch_detector.py +341 -0
  141. megadetector/detection/run_detector.py +779 -0
  142. megadetector/detection/run_detector_batch.py +1219 -0
  143. megadetector/detection/run_inference_with_yolov5_val.py +917 -0
  144. megadetector/detection/run_tiled_inference.py +934 -0
  145. megadetector/detection/tf_detector.py +189 -0
  146. megadetector/detection/video_utils.py +606 -0
  147. megadetector/postprocessing/__init__.py +0 -0
  148. megadetector/postprocessing/add_max_conf.py +64 -0
  149. megadetector/postprocessing/categorize_detections_by_size.py +163 -0
  150. megadetector/postprocessing/combine_api_outputs.py +249 -0
  151. megadetector/postprocessing/compare_batch_results.py +958 -0
  152. megadetector/postprocessing/convert_output_format.py +396 -0
  153. megadetector/postprocessing/load_api_results.py +195 -0
  154. megadetector/postprocessing/md_to_coco.py +310 -0
  155. megadetector/postprocessing/md_to_labelme.py +330 -0
  156. megadetector/postprocessing/merge_detections.py +401 -0
  157. megadetector/postprocessing/postprocess_batch_results.py +1902 -0
  158. megadetector/postprocessing/remap_detection_categories.py +170 -0
  159. megadetector/postprocessing/render_detection_confusion_matrix.py +660 -0
  160. megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +211 -0
  161. megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +83 -0
  162. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +1631 -0
  163. megadetector/postprocessing/separate_detections_into_folders.py +730 -0
  164. megadetector/postprocessing/subset_json_detector_output.py +696 -0
  165. megadetector/postprocessing/top_folders_to_bottom.py +223 -0
  166. megadetector/taxonomy_mapping/__init__.py +0 -0
  167. megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +491 -0
  168. megadetector/taxonomy_mapping/map_new_lila_datasets.py +150 -0
  169. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +142 -0
  170. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +590 -0
  171. megadetector/taxonomy_mapping/retrieve_sample_image.py +71 -0
  172. megadetector/taxonomy_mapping/simple_image_download.py +219 -0
  173. megadetector/taxonomy_mapping/species_lookup.py +834 -0
  174. megadetector/taxonomy_mapping/taxonomy_csv_checker.py +159 -0
  175. megadetector/taxonomy_mapping/taxonomy_graph.py +346 -0
  176. megadetector/taxonomy_mapping/validate_lila_category_mappings.py +83 -0
  177. megadetector/utils/__init__.py +0 -0
  178. megadetector/utils/azure_utils.py +178 -0
  179. megadetector/utils/ct_utils.py +612 -0
  180. megadetector/utils/directory_listing.py +246 -0
  181. megadetector/utils/md_tests.py +968 -0
  182. megadetector/utils/path_utils.py +1044 -0
  183. megadetector/utils/process_utils.py +157 -0
  184. megadetector/utils/sas_blob_utils.py +509 -0
  185. megadetector/utils/split_locations_into_train_val.py +228 -0
  186. megadetector/utils/string_utils.py +92 -0
  187. megadetector/utils/url_utils.py +323 -0
  188. megadetector/utils/write_html_image_list.py +225 -0
  189. megadetector/visualization/__init__.py +0 -0
  190. megadetector/visualization/plot_utils.py +293 -0
  191. megadetector/visualization/render_images_with_thumbnails.py +275 -0
  192. megadetector/visualization/visualization_utils.py +1536 -0
  193. megadetector/visualization/visualize_db.py +550 -0
  194. megadetector/visualization/visualize_detector_output.py +405 -0
  195. {megadetector-5.0.11.dist-info → megadetector-5.0.12.dist-info}/METADATA +1 -1
  196. megadetector-5.0.12.dist-info/RECORD +199 -0
  197. megadetector-5.0.12.dist-info/top_level.txt +1 -0
  198. megadetector-5.0.11.dist-info/RECORD +0 -5
  199. megadetector-5.0.11.dist-info/top_level.txt +0 -1
  200. {megadetector-5.0.11.dist-info → megadetector-5.0.12.dist-info}/LICENSE +0 -0
  201. {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()