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

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

Potentially problematic release.


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

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