megadetector 5.0.28__py3-none-any.whl → 5.0.29__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of megadetector might be problematic. Click here for more details.

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