megadetector 5.0.5__py3-none-any.whl → 5.0.7__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 (132) hide show
  1. api/batch_processing/data_preparation/manage_local_batch.py +302 -263
  2. api/batch_processing/data_preparation/manage_video_batch.py +81 -2
  3. api/batch_processing/postprocessing/add_max_conf.py +1 -0
  4. api/batch_processing/postprocessing/categorize_detections_by_size.py +50 -19
  5. api/batch_processing/postprocessing/compare_batch_results.py +110 -60
  6. api/batch_processing/postprocessing/load_api_results.py +56 -70
  7. api/batch_processing/postprocessing/md_to_coco.py +1 -1
  8. api/batch_processing/postprocessing/md_to_labelme.py +2 -1
  9. api/batch_processing/postprocessing/postprocess_batch_results.py +240 -81
  10. api/batch_processing/postprocessing/render_detection_confusion_matrix.py +625 -0
  11. api/batch_processing/postprocessing/repeat_detection_elimination/find_repeat_detections.py +71 -23
  12. api/batch_processing/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +1 -1
  13. api/batch_processing/postprocessing/repeat_detection_elimination/repeat_detections_core.py +227 -75
  14. api/batch_processing/postprocessing/subset_json_detector_output.py +132 -5
  15. api/batch_processing/postprocessing/top_folders_to_bottom.py +1 -1
  16. api/synchronous/api_core/animal_detection_api/detection/run_detector_batch.py +2 -2
  17. classification/prepare_classification_script.py +191 -191
  18. data_management/coco_to_yolo.py +68 -45
  19. data_management/databases/integrity_check_json_db.py +7 -5
  20. data_management/generate_crops_from_cct.py +3 -3
  21. data_management/get_image_sizes.py +8 -6
  22. data_management/importers/add_timestamps_to_icct.py +79 -0
  23. data_management/importers/animl_results_to_md_results.py +160 -0
  24. data_management/importers/auckland_doc_test_to_json.py +4 -4
  25. data_management/importers/auckland_doc_to_json.py +1 -1
  26. data_management/importers/awc_to_json.py +5 -5
  27. data_management/importers/bellevue_to_json.py +5 -5
  28. data_management/importers/carrizo_shrubfree_2018.py +5 -5
  29. data_management/importers/carrizo_trail_cam_2017.py +5 -5
  30. data_management/importers/cct_field_adjustments.py +2 -3
  31. data_management/importers/channel_islands_to_cct.py +4 -4
  32. data_management/importers/ena24_to_json.py +5 -5
  33. data_management/importers/helena_to_cct.py +10 -10
  34. data_management/importers/idaho-camera-traps.py +12 -12
  35. data_management/importers/idfg_iwildcam_lila_prep.py +8 -8
  36. data_management/importers/jb_csv_to_json.py +4 -4
  37. data_management/importers/missouri_to_json.py +1 -1
  38. data_management/importers/noaa_seals_2019.py +1 -1
  39. data_management/importers/pc_to_json.py +5 -5
  40. data_management/importers/prepare-noaa-fish-data-for-lila.py +4 -4
  41. data_management/importers/prepare_zsl_imerit.py +5 -5
  42. data_management/importers/rspb_to_json.py +4 -4
  43. data_management/importers/save_the_elephants_survey_A.py +5 -5
  44. data_management/importers/save_the_elephants_survey_B.py +6 -6
  45. data_management/importers/snapshot_safari_importer.py +9 -9
  46. data_management/importers/snapshot_serengeti_lila.py +9 -9
  47. data_management/importers/timelapse_csv_set_to_json.py +5 -7
  48. data_management/importers/ubc_to_json.py +4 -4
  49. data_management/importers/umn_to_json.py +4 -4
  50. data_management/importers/wellington_to_json.py +1 -1
  51. data_management/importers/wi_to_json.py +2 -2
  52. data_management/importers/zamba_results_to_md_results.py +181 -0
  53. data_management/labelme_to_coco.py +35 -7
  54. data_management/labelme_to_yolo.py +229 -0
  55. data_management/lila/add_locations_to_island_camera_traps.py +1 -1
  56. data_management/lila/add_locations_to_nacti.py +147 -0
  57. data_management/lila/create_lila_blank_set.py +474 -0
  58. data_management/lila/create_lila_test_set.py +2 -1
  59. data_management/lila/create_links_to_md_results_files.py +106 -0
  60. data_management/lila/download_lila_subset.py +46 -21
  61. data_management/lila/generate_lila_per_image_labels.py +23 -14
  62. data_management/lila/get_lila_annotation_counts.py +17 -11
  63. data_management/lila/lila_common.py +14 -11
  64. data_management/lila/test_lila_metadata_urls.py +116 -0
  65. data_management/ocr_tools.py +829 -0
  66. data_management/resize_coco_dataset.py +13 -11
  67. data_management/yolo_output_to_md_output.py +84 -12
  68. data_management/yolo_to_coco.py +38 -20
  69. detection/process_video.py +36 -14
  70. detection/pytorch_detector.py +23 -8
  71. detection/run_detector.py +76 -19
  72. detection/run_detector_batch.py +178 -63
  73. detection/run_inference_with_yolov5_val.py +326 -57
  74. detection/run_tiled_inference.py +153 -43
  75. detection/video_utils.py +34 -8
  76. md_utils/ct_utils.py +172 -1
  77. md_utils/md_tests.py +372 -51
  78. md_utils/path_utils.py +167 -39
  79. md_utils/process_utils.py +26 -7
  80. md_utils/split_locations_into_train_val.py +215 -0
  81. md_utils/string_utils.py +10 -0
  82. md_utils/url_utils.py +0 -2
  83. md_utils/write_html_image_list.py +9 -26
  84. md_visualization/plot_utils.py +12 -8
  85. md_visualization/visualization_utils.py +106 -7
  86. md_visualization/visualize_db.py +16 -8
  87. md_visualization/visualize_detector_output.py +208 -97
  88. {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/METADATA +3 -6
  89. {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/RECORD +98 -121
  90. {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/WHEEL +1 -1
  91. taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +1 -1
  92. taxonomy_mapping/map_new_lila_datasets.py +43 -39
  93. taxonomy_mapping/prepare_lila_taxonomy_release.py +5 -2
  94. taxonomy_mapping/preview_lila_taxonomy.py +27 -27
  95. taxonomy_mapping/species_lookup.py +33 -13
  96. taxonomy_mapping/taxonomy_csv_checker.py +7 -5
  97. api/synchronous/api_core/yolov5/detect.py +0 -252
  98. api/synchronous/api_core/yolov5/export.py +0 -607
  99. api/synchronous/api_core/yolov5/hubconf.py +0 -146
  100. api/synchronous/api_core/yolov5/models/__init__.py +0 -0
  101. api/synchronous/api_core/yolov5/models/common.py +0 -738
  102. api/synchronous/api_core/yolov5/models/experimental.py +0 -104
  103. api/synchronous/api_core/yolov5/models/tf.py +0 -574
  104. api/synchronous/api_core/yolov5/models/yolo.py +0 -338
  105. api/synchronous/api_core/yolov5/train.py +0 -670
  106. api/synchronous/api_core/yolov5/utils/__init__.py +0 -36
  107. api/synchronous/api_core/yolov5/utils/activations.py +0 -103
  108. api/synchronous/api_core/yolov5/utils/augmentations.py +0 -284
  109. api/synchronous/api_core/yolov5/utils/autoanchor.py +0 -170
  110. api/synchronous/api_core/yolov5/utils/autobatch.py +0 -66
  111. api/synchronous/api_core/yolov5/utils/aws/__init__.py +0 -0
  112. api/synchronous/api_core/yolov5/utils/aws/resume.py +0 -40
  113. api/synchronous/api_core/yolov5/utils/benchmarks.py +0 -148
  114. api/synchronous/api_core/yolov5/utils/callbacks.py +0 -71
  115. api/synchronous/api_core/yolov5/utils/dataloaders.py +0 -1087
  116. api/synchronous/api_core/yolov5/utils/downloads.py +0 -178
  117. api/synchronous/api_core/yolov5/utils/flask_rest_api/example_request.py +0 -19
  118. api/synchronous/api_core/yolov5/utils/flask_rest_api/restapi.py +0 -46
  119. api/synchronous/api_core/yolov5/utils/general.py +0 -1018
  120. api/synchronous/api_core/yolov5/utils/loggers/__init__.py +0 -187
  121. api/synchronous/api_core/yolov5/utils/loggers/wandb/__init__.py +0 -0
  122. api/synchronous/api_core/yolov5/utils/loggers/wandb/log_dataset.py +0 -27
  123. api/synchronous/api_core/yolov5/utils/loggers/wandb/sweep.py +0 -41
  124. api/synchronous/api_core/yolov5/utils/loggers/wandb/wandb_utils.py +0 -577
  125. api/synchronous/api_core/yolov5/utils/loss.py +0 -234
  126. api/synchronous/api_core/yolov5/utils/metrics.py +0 -355
  127. api/synchronous/api_core/yolov5/utils/plots.py +0 -489
  128. api/synchronous/api_core/yolov5/utils/torch_utils.py +0 -314
  129. api/synchronous/api_core/yolov5/val.py +0 -394
  130. md_utils/matlab_porting_tools.py +0 -97
  131. {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/LICENSE +0 -0
  132. {megadetector-5.0.5.dist-info → megadetector-5.0.7.dist-info}/top_level.txt +0 -0
@@ -151,6 +151,80 @@ if False:
151
151
 
152
152
  pass
153
153
 
154
+ #%% Render one or more sample videos...
155
+
156
+ # ...while we still have the frames and detections around
157
+
158
+ ## Imports
159
+
160
+ from md_visualization import visualize_detector_output
161
+ from detection.video_utils import frames_to_video
162
+
163
+
164
+ ## Constants and paths
165
+
166
+ confidence_threshold = 0.2
167
+ input_fs = 30
168
+
169
+ filtered_output_filename = '/a/b/c/blah_detections.filtered_rde_0.150_0.850_10_1.000.json'
170
+ video_fn_relative = '4.10cam6/IMG_0022.MP4'
171
+ output_video_base = os.path.expanduser('~/tmp/video_preview')
172
+
173
+
174
+ ## Filename handling
175
+
176
+ video_fn_relative = video_fn_relative.replace('\\','/')
177
+ video_fn_flat = video_fn_relative.replace('/','#')
178
+ video_name = os.path.splitext(video_fn_flat)[0]
179
+ output_video = os.path.join(output_video_base,'{}_detections.mp4'.format(video_name))
180
+ output_fs = input_fs / every_n_frames
181
+
182
+ rendered_detections_folder = os.path.join(output_video_base,'rendered_detections_{}'.format(video_name))
183
+ os.makedirs(rendered_detections_folder,exist_ok=True)
184
+
185
+
186
+ ## Find frames corresponding to this video
187
+
188
+ with open(filtered_output_filename,'r') as f:
189
+ frame_results = json.load(f)
190
+
191
+ frame_results_this_video = []
192
+
193
+ # im = frame_results['images'][0]
194
+ for im in frame_results['images']:
195
+ if im['file'].replace('\\','/').startswith(video_fn_relative):
196
+ frame_results_this_video.append(im)
197
+
198
+ assert len(frame_results_this_video) > 0, \
199
+ 'No frame results matched {}'.format(video_fn_relative)
200
+ print('Found {} matching frame results'.format(len(frame_results_this_video)))
201
+
202
+ frame_results['images'] = frame_results_this_video
203
+
204
+ frames_json = os.path.join(rendered_detections_folder,video_fn_flat + '.json')
205
+
206
+ with open(frames_json,'w') as f:
207
+ json.dump(frame_results,f,indent=1)
208
+
209
+
210
+ ## Render detections on those frames
211
+
212
+ detected_frame_files = visualize_detector_output.visualize_detector_output(
213
+ detector_output_path=frames_json,
214
+ out_dir=rendered_detections_folder,
215
+ images_dir=output_folder_base,
216
+ confidence_threshold=confidence_threshold,
217
+ preserve_path_structure=True,
218
+ output_image_width=-1)
219
+
220
+
221
+ ## Render the output video
222
+
223
+ frames_to_video(detected_frame_files, output_fs, output_video, codec_spec='h264')
224
+
225
+ # from md_utils.path_utils import open_file; open_file(output_video)
226
+
227
+
154
228
  #%% Test a possibly-broken video
155
229
 
156
230
  fn = '/datadrive/tmp/video.AVI'
@@ -175,8 +249,12 @@ if False:
175
249
  import os
176
250
  import nbformat as nbf
177
251
 
178
- input_py_file = os.path.expanduser(
179
- '~/git/MegaDetector/api/batch_processing/data_preparation/manage_video_batch.py')
252
+ if os.name == 'nt':
253
+ git_base = r'c:\git'
254
+ else:
255
+ git_base = os.path.expanduer('~/git')
256
+
257
+ input_py_file = git_base + '/MegaDetector/api/batch_processing/data_preparation/manage_video_batch.py'
180
258
  assert os.path.isfile(input_py_file)
181
259
  output_ipynb_file = input_py_file.replace('.py','.ipynb')
182
260
 
@@ -246,3 +324,4 @@ while(True):
246
324
  write_code_cell(current_cell)
247
325
 
248
326
  nbf.write(nb,output_ipynb_file)
327
+
@@ -61,3 +61,4 @@ def main():
61
61
 
62
62
  if __name__ == '__main__':
63
63
  main()
64
+
@@ -3,7 +3,7 @@
3
3
  # categorize_detections_by_size.py
4
4
  #
5
5
  # Given an API output .json file, creates a separate category for bounding boxes
6
- # above a size threshold.
6
+ # above one or more size thresholds.
7
7
  #
8
8
  ########
9
9
 
@@ -11,12 +11,16 @@
11
11
 
12
12
  import json
13
13
 
14
+ from collections import defaultdict
15
+ from tqdm import tqdm
16
+
14
17
 
15
18
  #%% Support classes
16
19
 
17
20
  class SizeCategorizationOptions:
18
21
 
19
- threshold = 0.95
22
+ # Should be sorted from smallest to largest
23
+ size_thresholds = [0.95]
20
24
 
21
25
  # List of category numbers to use in separation; uses all categories if None
22
26
  categories_to_separate = None
@@ -24,12 +28,13 @@ class SizeCategorizationOptions:
24
28
  # Can be "size", "width", or "height"
25
29
  measurement = 'size'
26
30
 
27
- output_category_name = 'large_detection'
31
+ # Should have the same length as "size_thresholds"
32
+ size_category_names = ['large_detection']
28
33
 
29
-
34
+
30
35
  #%% Main functions
31
36
 
32
- def categorize_detections_by_size(input_file,output_file,options=None):
37
+ def categorize_detections_by_size(input_file,output_file=None,options=None):
33
38
 
34
39
  if options is None:
35
40
  options = SizeCategorizationOptions()
@@ -38,6 +43,14 @@ def categorize_detections_by_size(input_file,output_file,options=None):
38
43
  options.categories_to_separate = \
39
44
  [str(c) for c in options.categories_to_separate]
40
45
 
46
+ assert len(options.size_thresholds) == len(options.size_category_names), \
47
+ 'Options struct should have the same number of category names and size thresholds'
48
+
49
+ # Sort size thresholds and names from largest to smallest
50
+ options.size_category_names = [x for _,x in sorted(zip(options.size_thresholds,
51
+ options.size_category_names),reverse=True)]
52
+ options.size_thresholds = sorted(options.size_thresholds,reverse=True)
53
+
41
54
  with open(input_file) as f:
42
55
  data = json.load(f)
43
56
 
@@ -45,12 +58,18 @@ def categorize_detections_by_size(input_file,output_file,options=None):
45
58
  category_keys = list(detection_categories.keys())
46
59
  category_keys = [int(k) for k in category_keys]
47
60
  max_key = max(category_keys)
48
- large_detection_category_id = str(max_key+1)
49
- detection_categories[large_detection_category_id] = options.output_category_name
50
-
51
- print('Creating large-box category for {} with ID {}'.format(
52
- options.output_category_name,large_detection_category_id))
53
61
 
62
+ threshold_to_category_id = {}
63
+ for i_threshold,threshold in enumerate(options.size_thresholds):
64
+
65
+ category_id = str(max_key+1)
66
+ max_key += 1
67
+ detection_categories[category_id] = options.size_category_names[i_threshold]
68
+ threshold_to_category_id[i_threshold] = category_id
69
+
70
+ print('Creating category for {} with ID {}'.format(
71
+ options.size_category_names[i_threshold],category_id))
72
+
54
73
  images = data['images']
55
74
 
56
75
  print('Loaded {} images'.format(len(images)))
@@ -58,10 +77,10 @@ def categorize_detections_by_size(input_file,output_file,options=None):
58
77
  # For each image...
59
78
  #
60
79
  # im = images[0]
80
+
81
+ category_id_to_count = defaultdict(int)
61
82
 
62
- n_large_detections = 0
63
-
64
- for im in images:
83
+ for im in tqdm(images):
65
84
 
66
85
  if im['detections'] is None:
67
86
  assert im['failure'] is not None and len(im['failure']) > 0
@@ -95,19 +114,31 @@ def categorize_detections_by_size(input_file,output_file,options=None):
95
114
  metric = h
96
115
  assert metric is not None
97
116
 
98
- if metric >= options.threshold:
117
+ for i_threshold,threshold in enumerate(options.size_thresholds):
99
118
 
100
- d['category'] = large_detection_category_id
101
- n_large_detections += 1
119
+ if metric >= threshold:
120
+
121
+ category_id = threshold_to_category_id[i_threshold]
122
+
123
+ category_id_to_count[category_id] += 1
124
+ d['category'] = category_id
125
+
126
+ break
102
127
 
128
+ # ...for each threshold
103
129
  # ...for each detection
104
130
 
105
131
  # ...for each image
106
132
 
107
- print('Found {} large detections'.format(n_large_detections))
133
+ for i_threshold in range(0,len(options.size_thresholds)):
134
+ category_name = options.size_category_names[i_threshold]
135
+ category_id = threshold_to_category_id[i_threshold]
136
+ category_count = category_id_to_count[category_id]
137
+ print('Found {} detections in category {}'.format(category_count,category_name))
108
138
 
109
- with open(output_file,'w') as f:
110
- json.dump(data,f,indent=1)
139
+ if output_file is not None:
140
+ with open(output_file,'w') as f:
141
+ json.dump(data,f,indent=1)
111
142
 
112
143
  return data
113
144
 
@@ -4,7 +4,7 @@
4
4
  #
5
5
  # Compare sets of batch results; typically used to compare:
6
6
  #
7
- # * MegaDetector versions
7
+ # * Results from different MegaDetector versions
8
8
  # * Results before/after RDE
9
9
  # * Results with/without augmentation
10
10
  #
@@ -36,9 +36,6 @@ from md_utils import path_utils
36
36
 
37
37
 
38
38
  #%% Constants and support classes
39
-
40
- # We will confirm that this matches what we load from each file
41
- default_detection_categories = {'1': 'animal', '2': 'person', '3': 'vehicle'}
42
39
 
43
40
  class PairwiseBatchComparisonOptions:
44
41
  """
@@ -52,8 +49,8 @@ class PairwiseBatchComparisonOptions:
52
49
  results_description_a = None
53
50
  results_description_b = None
54
51
 
55
- detection_thresholds_a = {'animal':0.15,'person':0.15,'vehicle':0.15}
56
- detection_thresholds_b = {'animal':0.15,'person':0.15,'vehicle':0.15}
52
+ detection_thresholds_a = {'animal':0.15,'person':0.15,'vehicle':0.15,'default':0.15}
53
+ detection_thresholds_b = {'animal':0.15,'person':0.15,'vehicle':0.15,'default':0.15}
57
54
 
58
55
  rendering_confidence_threshold_a = 0.1
59
56
  rendering_confidence_threshold_b = 0.1
@@ -71,16 +68,26 @@ class BatchComparisonOptions:
71
68
  job_name = ''
72
69
 
73
70
  max_images_per_category = 1000
71
+ max_images_per_page = None
74
72
  colormap_a = ['Red']
75
73
  colormap_b = ['RoyalBlue']
76
74
 
77
75
  # Process-based parallelization isn't supported yet; this must be "True"
78
76
  parallelize_rendering_with_threads = True
79
77
 
78
+ # List of filenames to include in the comparison, or None to use all files
79
+ filenames_to_include = None
80
+
81
+ # Compare only detections/non-detections, ignore categories (still renders categories)
82
+ class_agnostic_comparison = False
83
+
80
84
  target_width = 800
81
85
  n_rendering_workers = 20
82
86
  random_seed = 0
83
87
 
88
+ # Default to sorting by filename
89
+ sort_by_confidence = False
90
+
84
91
  error_on_non_matching_lists = True
85
92
 
86
93
  pairwise_options = []
@@ -90,7 +97,7 @@ class BatchComparisonOptions:
90
97
 
91
98
  class PairwiseBatchComparisonResults:
92
99
  """
93
- The results from a single pairwise comparison
100
+ The results from a single pairwise comparison.
94
101
  """
95
102
 
96
103
  html_content = None
@@ -98,7 +105,7 @@ class PairwiseBatchComparisonResults:
98
105
 
99
106
  # A dictionary with keys including:
100
107
  #
101
- # "common_detections"
108
+ # common_detections
102
109
  # common_non_detections
103
110
  # detections_a_only
104
111
  # detections_b_only
@@ -207,7 +214,8 @@ def pairwise_compare_batch_results(options,output_index,pairwise_options):
207
214
  # in the options object.
208
215
  assert options.pairwise_options is None
209
216
 
210
- random.seed(options.random_seed)
217
+ if options.random_seed is not None:
218
+ random.seed(options.random_seed)
211
219
 
212
220
  # Warn the user if some "detections" might not get rendered
213
221
  max_classification_threshold_a = max(list(pairwise_options.detection_thresholds_a.values()))
@@ -241,10 +249,20 @@ def pairwise_compare_batch_results(options,output_index,pairwise_options):
241
249
  with open(pairwise_options.results_filename_b,'r') as f:
242
250
  results_b = json.load(f)
243
251
 
244
- # assert results_a['detection_categories'] == default_detection_categories
245
- # assert results_b['detection_categories'] == default_detection_categories
246
- assert results_a['detection_categories'] == results_b['detection_categories']
247
- detection_categories = results_a['detection_categories']
252
+ # Don't let path separators confuse things
253
+ for im in results_a['images']:
254
+ if 'file' in im:
255
+ im['file'] = im['file'].replace('\\','/')
256
+ for im in results_b['images']:
257
+ if 'file' in im:
258
+ im['file'] = im['file'].replace('\\','/')
259
+
260
+ if not options.class_agnostic_comparison:
261
+ assert results_a['detection_categories'] == results_b['detection_categories'], \
262
+ "Cannot perform a class-sensitive comparison across results with different categories"
263
+
264
+ detection_categories_a = results_a['detection_categories']
265
+ detection_categories_b = results_b['detection_categories']
248
266
 
249
267
  if pairwise_options.results_description_a is None:
250
268
  if 'detector' not in results_a['info']:
@@ -286,6 +304,10 @@ def pairwise_compare_batch_results(options,output_index,pairwise_options):
286
304
  assert len(filenames_a) == len(images_a)
287
305
  assert len(filenames_b_set) == len(images_b)
288
306
 
307
+ if options.filenames_to_include is None:
308
+ filenames_to_compare = filenames_a
309
+ else:
310
+ filenames_to_compare = options.filenames_to_include
289
311
 
290
312
  ##%% Find differences
291
313
 
@@ -298,9 +320,9 @@ def pairwise_compare_batch_results(options,output_index,pairwise_options):
298
320
  detections_a_only = {}
299
321
  detections_b_only = {}
300
322
  class_transitions = {}
301
-
302
- # fn = filenames_a[0]
303
- for fn in tqdm(filenames_a):
323
+
324
+ # fn = filenames_to_compare[0]
325
+ for fn in tqdm(filenames_to_compare):
304
326
 
305
327
  if fn not in filename_to_image_b:
306
328
 
@@ -330,14 +352,19 @@ def pairwise_compare_batch_results(options,output_index,pairwise_options):
330
352
 
331
353
  category_id = det['category']
332
354
 
333
- if category_id not in detection_categories:
355
+ if category_id not in detection_categories_a:
334
356
  print('Warning: unexpected category {} for model A on file {}'.format(category_id,fn))
335
357
  invalid_category_error = True
336
358
  break
337
359
 
338
360
  conf = det['conf']
339
361
 
340
- if conf >= pairwise_options.detection_thresholds_a[detection_categories[category_id]]:
362
+ if detection_categories_a[category_id] in pairwise_options.detection_thresholds_a:
363
+ conf_thresh = pairwise_options.detection_thresholds_a[detection_categories_a[category_id]]
364
+ else:
365
+ conf_thresh = pairwise_options.detection_thresholds_a['default']
366
+
367
+ if conf >= conf_thresh:
341
368
  categories_above_threshold_a.add(category_id)
342
369
 
343
370
  if invalid_category_error:
@@ -349,14 +376,19 @@ def pairwise_compare_batch_results(options,output_index,pairwise_options):
349
376
 
350
377
  category_id = det['category']
351
378
 
352
- if category_id not in detection_categories:
379
+ if category_id not in detection_categories_b:
353
380
  print('Warning: unexpected category {} for model B on file {}'.format(category_id,fn))
354
381
  invalid_category_error = True
355
382
  break
356
383
 
357
384
  conf = det['conf']
358
385
 
359
- if conf >= pairwise_options.detection_thresholds_b[detection_categories[category_id]]:
386
+ if detection_categories_b[category_id] in pairwise_options.detection_thresholds_b:
387
+ conf_thresh = pairwise_options.detection_thresholds_b[detection_categories_b[category_id]]
388
+ else:
389
+ conf_thresh = pairwise_options.detection_thresholds_a['default']
390
+
391
+ if conf >= conf_thresh:
360
392
  categories_above_threshold_b.add(category_id)
361
393
 
362
394
  if invalid_category_error:
@@ -368,7 +400,8 @@ def pairwise_compare_batch_results(options,output_index,pairwise_options):
368
400
  detection_b = (len(categories_above_threshold_b) > 0)
369
401
 
370
402
  if detection_a and detection_b:
371
- if categories_above_threshold_a == categories_above_threshold_b:
403
+ if (categories_above_threshold_a == categories_above_threshold_b) or \
404
+ options.class_agnostic_comparison:
372
405
  common_detections[fn] = im_pair
373
406
  else:
374
407
  class_transitions[fn] = im_pair
@@ -383,7 +416,7 @@ def pairwise_compare_batch_results(options,output_index,pairwise_options):
383
416
  # ...for each filename
384
417
 
385
418
  print('Of {} files:\n{} common detections\n{} common non-detections\n{} A only\n{} B only\n{} class transitions'.format(
386
- len(filenames_a),len(common_detections),
419
+ len(filenames_to_compare),len(common_detections),
387
420
  len(common_non_detections),len(detections_a_only),
388
421
  len(detections_b_only),len(class_transitions)))
389
422
 
@@ -453,14 +486,16 @@ def pairwise_compare_batch_results(options,output_index,pairwise_options):
453
486
  # Choose detection pairs we're going to render for this category
454
487
  image_pairs = categories_to_image_pairs[category]
455
488
  image_filenames = list(image_pairs.keys())
456
- if len(image_filenames) > options.max_images_per_category:
457
- print('Sampling {} of {} image pairs for category {}'.format(
458
- options.max_images_per_category,
459
- len(image_filenames),
460
- category))
461
- image_filenames = random.sample(image_filenames,
462
- options.max_images_per_category)
463
- assert len(image_filenames) <= options.max_images_per_category
489
+
490
+ if options.max_images_per_category is not None and options.max_images_per_category > 0:
491
+ if len(image_filenames) > options.max_images_per_category:
492
+ print('Sampling {} of {} image pairs for category {}'.format(
493
+ options.max_images_per_category,
494
+ len(image_filenames),
495
+ category))
496
+ image_filenames = random.sample(image_filenames,
497
+ options.max_images_per_category)
498
+ assert len(image_filenames) <= options.max_images_per_category
464
499
 
465
500
  input_image_absolute_paths = [os.path.join(options.image_folder,fn) for fn in image_filenames]
466
501
 
@@ -492,15 +527,34 @@ def pairwise_compare_batch_results(options,output_index,pairwise_options):
492
527
  max_conf_b = maxempty([det['conf'] for det in image_b['detections']])
493
528
 
494
529
  title = input_path_relative + ' (max conf {:.2f},{:.2f})'.format(max_conf_a,max_conf_b)
530
+
531
+ # Only used if sort_by_confidence is True
532
+ if category == 'common_detections':
533
+ sort_conf = max(max_conf_a,max_conf_b)
534
+ elif category == 'common_non_detections':
535
+ sort_conf = max(max_conf_a,max_conf_b)
536
+ elif category == 'detections_a_only':
537
+ sort_conf = max_conf_a
538
+ elif category == 'detections_b_only':
539
+ sort_conf = max_conf_b
540
+ elif category == 'class_transitions':
541
+ sort_conf = max(max_conf_a,max_conf_b)
542
+ else:
543
+ print('Warning: unknown sort category {}'.format(category))
544
+ sort_conf = max(max_conf_a,max_conf_b)
545
+
495
546
  info = {
496
547
  'filename': fn,
497
548
  'title': title,
498
549
  'textStyle': 'font-family:verdana,arial,calibri;font-size:' + \
499
550
  '80%;text-align:left;margin-top:20;margin-bottom:5',
500
- 'linkTarget': urllib.parse.quote(input_image_absolute_paths[i_fn])
551
+ 'linkTarget': urllib.parse.quote(input_image_absolute_paths[i_fn]),
552
+ 'sort_conf':sort_conf
501
553
  }
502
554
  image_info.append(info)
503
555
 
556
+ # ...for each image
557
+
504
558
  category_page_header_string = '<h1>{}</h1>'.format(categories_to_page_titles[category])
505
559
  category_page_header_string += '<p style="font-weight:bold;">\n'
506
560
  category_page_header_string += 'Model A: {}<br/>\n'.format(
@@ -521,11 +575,18 @@ def pairwise_compare_batch_results(options,output_index,pairwise_options):
521
575
  str(pairwise_options.rendering_confidence_threshold_b))
522
576
  category_page_header_string += '</p>\n'
523
577
 
578
+ # Default to sorting by filename
579
+ if options.sort_by_confidence:
580
+ image_info = sorted(image_info, key=lambda d: d['sort_conf'], reverse=True)
581
+ else:
582
+ image_info = sorted(image_info, key=lambda d: d['filename'])
583
+
524
584
  write_html_image_list(
525
585
  category_html_filename,
526
586
  images=image_info,
527
587
  options={
528
- 'headerHtml': category_page_header_string
588
+ 'headerHtml': category_page_header_string,
589
+ 'maxFiguresPerHtmlFile': options.max_images_per_page
529
590
  })
530
591
 
531
592
  # ...for each category
@@ -559,7 +620,7 @@ def pairwise_compare_batch_results(options,output_index,pairwise_options):
559
620
  html_output_string += '<br/>'
560
621
 
561
622
  html_output_string += ('Of {} total files:<br/><br/><div style="margin-left:15px;">{} common detections<br/>{} common non-detections<br/>{} A only<br/>{} B only<br/>{} class transitions</div><br/>'.format(
562
- len(filenames_a),len(common_detections),
623
+ len(filenames_to_compare),len(common_detections),
563
624
  len(common_non_detections),len(detections_a_only),
564
625
  len(detections_b_only),len(class_transitions)))
565
626
 
@@ -583,7 +644,7 @@ def pairwise_compare_batch_results(options,output_index,pairwise_options):
583
644
 
584
645
  return pairwise_results
585
646
 
586
- # ...def compare_batch_results()
647
+ # ...def pairwise_compare_batch_results()
587
648
 
588
649
 
589
650
  def compare_batch_results(options):
@@ -663,12 +724,9 @@ def n_way_comparison(filenames,options,detection_thresholds=None,rendering_thres
663
724
  pairwise_options.rendering_confidence_threshold_a = rendering_thresholds[i]
664
725
  pairwise_options.rendering_confidence_threshold_b = rendering_thresholds[j]
665
726
 
666
- pairwise_options.detection_thresholds_a = {'animal':detection_thresholds[i],
667
- 'person':detection_thresholds[i],
668
- 'vehicle':detection_thresholds[i]}
669
- pairwise_options.detection_thresholds_b = {'animal':detection_thresholds[j],
670
- 'person':detection_thresholds[j],
671
- 'vehicle':detection_thresholds[j]}
727
+ pairwise_options.detection_thresholds_a = {'default':detection_thresholds[i]}
728
+ pairwise_options.detection_thresholds_b = {'default':detection_thresholds[j]}
729
+
672
730
  options.pairwise_options.append(pairwise_options)
673
731
 
674
732
  return compare_batch_results(options)
@@ -679,32 +737,25 @@ def n_way_comparison(filenames,options,detection_thresholds=None,rendering_thres
679
737
  #%% Interactive driver
680
738
 
681
739
  if False:
682
-
683
- #%% Running KGA test
684
-
685
- # CUDA_VISIBLE_DEVICES=0 python run_detector_batch.py ~/models/camera_traps/megadetector/md_v5.0.0/md_v5a.0.0.pt ~/data/KGA/ ~/data/KGA-5a.json --recursive --output_relative_filenames --quiet
686
- # CUDA_VISIBLE_DEVICES=1 python run_detector_batch.py ~/models/camera_traps/megadetector/md_v5.0.0/md_v5b.0.0.pt ~/data/KGA/ ~/data/KGA-5b.json --recursive --output_relative_filenames --quiet
687
-
688
- # python run_detector_batch.py ~/models/camera_traps/megadetector/md_v4.1.0/md_v4.1.0.pb ~/data/KGA ~/data/KGA-4.json --recursive --output_relative_filenames --quiet
689
-
690
- # CUDA_VISIBLE_DEVICES=0 python run_detector_batch.py ~/models/camera_traps/megadetector/md_v5.0.0/md_v5a.0.0.pt ~/data/KGA/ ~/data/KGA-5a-pillow-9.2.0.json --recursive --output_relative_filenames --quiet
691
-
692
-
740
+
693
741
  #%% Test two-way comparison
694
742
 
695
743
  options = BatchComparisonOptions()
696
744
 
697
- options.parallelize_rendering_with_threads = False
745
+ options.parallelize_rendering_with_threads = True
746
+
747
+ options.job_name = 'BCT'
748
+ options.output_folder = r'g:\temp\comparisons'
749
+ options.image_folder = r'g:\camera_traps\camera_trap_images'
750
+ options.max_images_per_category = 100
751
+ options.sort_by_confidence = True
698
752
 
699
- options.job_name = 'KGA-test'
700
- options.output_folder = os.path.expanduser('~/tmp/md-comparison-test')
701
- options.image_folder = os.path.expanduser('~/data/KGA')
702
-
703
753
  options.pairwise_options = []
704
754
 
705
- filenames = [
706
- os.path.expanduser('~/data/KGA-5a.json'),
707
- os.path.expanduser('~/data/KGA-5b.json')
755
+ results_base = os.path.expanduser('~/postprocessing/bellevue-camera-traps')
756
+ filenames = [
757
+ 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'),
758
+ 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')
708
759
  ]
709
760
 
710
761
  detection_thresholds = [0.15,0.15]
@@ -835,4 +886,3 @@ def main():
835
886
  if __name__ == '__main__':
836
887
 
837
888
  main()
838
-