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

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

Potentially problematic release.


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

Files changed (176) hide show
  1. megadetector/api/batch_processing/api_core/batch_service/score.py +4 -5
  2. megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +1 -1
  3. megadetector/api/batch_processing/api_support/summarize_daily_activity.py +1 -1
  4. megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +2 -2
  5. megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +1 -1
  6. megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +1 -1
  7. megadetector/api/synchronous/api_core/tests/load_test.py +2 -3
  8. megadetector/classification/aggregate_classifier_probs.py +3 -3
  9. megadetector/classification/analyze_failed_images.py +5 -5
  10. megadetector/classification/cache_batchapi_outputs.py +5 -5
  11. megadetector/classification/create_classification_dataset.py +11 -12
  12. megadetector/classification/crop_detections.py +10 -10
  13. megadetector/classification/csv_to_json.py +8 -8
  14. megadetector/classification/detect_and_crop.py +13 -15
  15. megadetector/classification/evaluate_model.py +7 -7
  16. megadetector/classification/identify_mislabeled_candidates.py +6 -6
  17. megadetector/classification/json_to_azcopy_list.py +1 -1
  18. megadetector/classification/json_validator.py +29 -32
  19. megadetector/classification/map_classification_categories.py +9 -9
  20. megadetector/classification/merge_classification_detection_output.py +12 -9
  21. megadetector/classification/prepare_classification_script.py +19 -19
  22. megadetector/classification/prepare_classification_script_mc.py +23 -23
  23. megadetector/classification/run_classifier.py +4 -4
  24. megadetector/classification/save_mislabeled.py +6 -6
  25. megadetector/classification/train_classifier.py +1 -1
  26. megadetector/classification/train_classifier_tf.py +9 -9
  27. megadetector/classification/train_utils.py +10 -10
  28. megadetector/data_management/annotations/annotation_constants.py +1 -1
  29. megadetector/data_management/camtrap_dp_to_coco.py +45 -45
  30. megadetector/data_management/cct_json_utils.py +101 -101
  31. megadetector/data_management/cct_to_md.py +49 -49
  32. megadetector/data_management/cct_to_wi.py +33 -33
  33. megadetector/data_management/coco_to_labelme.py +75 -75
  34. megadetector/data_management/coco_to_yolo.py +189 -189
  35. megadetector/data_management/databases/add_width_and_height_to_db.py +3 -2
  36. megadetector/data_management/databases/combine_coco_camera_traps_files.py +38 -38
  37. megadetector/data_management/databases/integrity_check_json_db.py +202 -188
  38. megadetector/data_management/databases/subset_json_db.py +33 -33
  39. megadetector/data_management/generate_crops_from_cct.py +38 -38
  40. megadetector/data_management/get_image_sizes.py +54 -49
  41. megadetector/data_management/labelme_to_coco.py +130 -124
  42. megadetector/data_management/labelme_to_yolo.py +78 -72
  43. megadetector/data_management/lila/create_lila_blank_set.py +81 -83
  44. megadetector/data_management/lila/create_lila_test_set.py +32 -31
  45. megadetector/data_management/lila/create_links_to_md_results_files.py +18 -18
  46. megadetector/data_management/lila/download_lila_subset.py +21 -24
  47. megadetector/data_management/lila/generate_lila_per_image_labels.py +91 -91
  48. megadetector/data_management/lila/get_lila_annotation_counts.py +30 -30
  49. megadetector/data_management/lila/get_lila_image_counts.py +22 -22
  50. megadetector/data_management/lila/lila_common.py +70 -70
  51. megadetector/data_management/lila/test_lila_metadata_urls.py +13 -14
  52. megadetector/data_management/mewc_to_md.py +339 -340
  53. megadetector/data_management/ocr_tools.py +258 -252
  54. megadetector/data_management/read_exif.py +231 -224
  55. megadetector/data_management/remap_coco_categories.py +26 -26
  56. megadetector/data_management/remove_exif.py +31 -20
  57. megadetector/data_management/rename_images.py +187 -187
  58. megadetector/data_management/resize_coco_dataset.py +41 -41
  59. megadetector/data_management/speciesnet_to_md.py +41 -41
  60. megadetector/data_management/wi_download_csv_to_coco.py +55 -55
  61. megadetector/data_management/yolo_output_to_md_output.py +117 -120
  62. megadetector/data_management/yolo_to_coco.py +195 -188
  63. megadetector/detection/change_detection.py +831 -0
  64. megadetector/detection/process_video.py +340 -337
  65. megadetector/detection/pytorch_detector.py +304 -262
  66. megadetector/detection/run_detector.py +177 -164
  67. megadetector/detection/run_detector_batch.py +364 -363
  68. megadetector/detection/run_inference_with_yolov5_val.py +328 -325
  69. megadetector/detection/run_tiled_inference.py +256 -249
  70. megadetector/detection/tf_detector.py +24 -24
  71. megadetector/detection/video_utils.py +290 -282
  72. megadetector/postprocessing/add_max_conf.py +15 -11
  73. megadetector/postprocessing/categorize_detections_by_size.py +44 -44
  74. megadetector/postprocessing/classification_postprocessing.py +415 -415
  75. megadetector/postprocessing/combine_batch_outputs.py +20 -21
  76. megadetector/postprocessing/compare_batch_results.py +528 -517
  77. megadetector/postprocessing/convert_output_format.py +97 -97
  78. megadetector/postprocessing/create_crop_folder.py +219 -146
  79. megadetector/postprocessing/detector_calibration.py +173 -168
  80. megadetector/postprocessing/generate_csv_report.py +508 -499
  81. megadetector/postprocessing/load_api_results.py +23 -20
  82. megadetector/postprocessing/md_to_coco.py +129 -98
  83. megadetector/postprocessing/md_to_labelme.py +89 -83
  84. megadetector/postprocessing/md_to_wi.py +40 -40
  85. megadetector/postprocessing/merge_detections.py +87 -114
  86. megadetector/postprocessing/postprocess_batch_results.py +313 -298
  87. megadetector/postprocessing/remap_detection_categories.py +36 -36
  88. megadetector/postprocessing/render_detection_confusion_matrix.py +205 -199
  89. megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +57 -57
  90. megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +27 -28
  91. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +702 -677
  92. megadetector/postprocessing/separate_detections_into_folders.py +226 -211
  93. megadetector/postprocessing/subset_json_detector_output.py +265 -262
  94. megadetector/postprocessing/top_folders_to_bottom.py +45 -45
  95. megadetector/postprocessing/validate_batch_results.py +70 -70
  96. megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +52 -52
  97. megadetector/taxonomy_mapping/map_new_lila_datasets.py +15 -15
  98. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +14 -14
  99. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +66 -66
  100. megadetector/taxonomy_mapping/retrieve_sample_image.py +16 -16
  101. megadetector/taxonomy_mapping/simple_image_download.py +8 -8
  102. megadetector/taxonomy_mapping/species_lookup.py +33 -33
  103. megadetector/taxonomy_mapping/taxonomy_csv_checker.py +14 -14
  104. megadetector/taxonomy_mapping/taxonomy_graph.py +10 -10
  105. megadetector/taxonomy_mapping/validate_lila_category_mappings.py +13 -13
  106. megadetector/utils/azure_utils.py +22 -22
  107. megadetector/utils/ct_utils.py +1018 -200
  108. megadetector/utils/directory_listing.py +21 -77
  109. megadetector/utils/gpu_test.py +22 -22
  110. megadetector/utils/md_tests.py +541 -518
  111. megadetector/utils/path_utils.py +1457 -398
  112. megadetector/utils/process_utils.py +41 -41
  113. megadetector/utils/sas_blob_utils.py +53 -49
  114. megadetector/utils/split_locations_into_train_val.py +61 -61
  115. megadetector/utils/string_utils.py +147 -26
  116. megadetector/utils/url_utils.py +463 -173
  117. megadetector/utils/wi_utils.py +2629 -2526
  118. megadetector/utils/write_html_image_list.py +137 -137
  119. megadetector/visualization/plot_utils.py +21 -21
  120. megadetector/visualization/render_images_with_thumbnails.py +37 -73
  121. megadetector/visualization/visualization_utils.py +401 -397
  122. megadetector/visualization/visualize_db.py +197 -190
  123. megadetector/visualization/visualize_detector_output.py +79 -73
  124. {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/METADATA +135 -132
  125. megadetector-5.0.29.dist-info/RECORD +163 -0
  126. {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/WHEEL +1 -1
  127. {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/licenses/LICENSE +0 -0
  128. {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/top_level.txt +0 -0
  129. megadetector/data_management/importers/add_nacti_sizes.py +0 -52
  130. megadetector/data_management/importers/add_timestamps_to_icct.py +0 -79
  131. megadetector/data_management/importers/animl_results_to_md_results.py +0 -158
  132. megadetector/data_management/importers/auckland_doc_test_to_json.py +0 -373
  133. megadetector/data_management/importers/auckland_doc_to_json.py +0 -201
  134. megadetector/data_management/importers/awc_to_json.py +0 -191
  135. megadetector/data_management/importers/bellevue_to_json.py +0 -272
  136. megadetector/data_management/importers/cacophony-thermal-importer.py +0 -793
  137. megadetector/data_management/importers/carrizo_shrubfree_2018.py +0 -269
  138. megadetector/data_management/importers/carrizo_trail_cam_2017.py +0 -289
  139. megadetector/data_management/importers/cct_field_adjustments.py +0 -58
  140. megadetector/data_management/importers/channel_islands_to_cct.py +0 -913
  141. megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +0 -180
  142. megadetector/data_management/importers/eMammal/eMammal_helpers.py +0 -249
  143. megadetector/data_management/importers/eMammal/make_eMammal_json.py +0 -223
  144. megadetector/data_management/importers/ena24_to_json.py +0 -276
  145. megadetector/data_management/importers/filenames_to_json.py +0 -386
  146. megadetector/data_management/importers/helena_to_cct.py +0 -283
  147. megadetector/data_management/importers/idaho-camera-traps.py +0 -1407
  148. megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +0 -294
  149. megadetector/data_management/importers/import_desert_lion_conservation_camera_traps.py +0 -387
  150. megadetector/data_management/importers/jb_csv_to_json.py +0 -150
  151. megadetector/data_management/importers/mcgill_to_json.py +0 -250
  152. megadetector/data_management/importers/missouri_to_json.py +0 -490
  153. megadetector/data_management/importers/nacti_fieldname_adjustments.py +0 -79
  154. megadetector/data_management/importers/noaa_seals_2019.py +0 -181
  155. megadetector/data_management/importers/osu-small-animals-to-json.py +0 -364
  156. megadetector/data_management/importers/pc_to_json.py +0 -365
  157. megadetector/data_management/importers/plot_wni_giraffes.py +0 -123
  158. megadetector/data_management/importers/prepare_zsl_imerit.py +0 -131
  159. megadetector/data_management/importers/raic_csv_to_md_results.py +0 -416
  160. megadetector/data_management/importers/rspb_to_json.py +0 -356
  161. megadetector/data_management/importers/save_the_elephants_survey_A.py +0 -320
  162. megadetector/data_management/importers/save_the_elephants_survey_B.py +0 -329
  163. megadetector/data_management/importers/snapshot_safari_importer.py +0 -758
  164. megadetector/data_management/importers/snapshot_serengeti_lila.py +0 -1067
  165. megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +0 -150
  166. megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +0 -153
  167. megadetector/data_management/importers/sulross_get_exif.py +0 -65
  168. megadetector/data_management/importers/timelapse_csv_set_to_json.py +0 -490
  169. megadetector/data_management/importers/ubc_to_json.py +0 -399
  170. megadetector/data_management/importers/umn_to_json.py +0 -507
  171. megadetector/data_management/importers/wellington_to_json.py +0 -263
  172. megadetector/data_management/importers/wi_to_json.py +0 -442
  173. megadetector/data_management/importers/zamba_results_to_md_results.py +0 -180
  174. megadetector/data_management/lila/add_locations_to_island_camera_traps.py +0 -101
  175. megadetector/data_management/lila/add_locations_to_nacti.py +0 -151
  176. megadetector-5.0.28.dist-info/RECORD +0 -209
@@ -12,7 +12,7 @@ Given a .json or .csv file containing MD results, do one or more of the followin
12
12
  truth)
13
13
 
14
14
  Ground truth, if available, must be in COCO Camera Traps format:
15
-
15
+
16
16
  https://github.com/agentmorris/MegaDetector/blob/main/megadetector/data_management/README.md#coco-camera-traps-format
17
17
 
18
18
  """
@@ -30,7 +30,6 @@ import time
30
30
  import uuid
31
31
  import warnings
32
32
  import random
33
- import json
34
33
 
35
34
  from enum import IntEnum
36
35
  from multiprocessing.pool import ThreadPool
@@ -76,40 +75,40 @@ class PostProcessingOptions:
76
75
  """
77
76
  Options used to parameterize process_batch_results().
78
77
  """
79
-
78
+
80
79
  def __init__(self):
81
-
80
+
82
81
  ### Required inputs
83
-
82
+
84
83
  #: MD results .json file to process
85
84
  self.md_results_file = ''
86
-
85
+
87
86
  #: Folder to which we should write HTML output
88
87
  self.output_dir = ''
89
-
88
+
90
89
  ### Options
91
-
90
+
92
91
  #: Folder where images live (filenames in [md_results_file] should be relative to this folder)
93
92
  #:
94
93
  #: Can be '' if [md_results_file] uses absolute paths.
95
94
  self.image_base_dir = ''
96
-
95
+
97
96
  ## These apply only when we're doing ground-truth comparisons
98
-
97
+
99
98
  #: Optional .json file containing ground truth information
100
99
  self.ground_truth_json_file = ''
101
-
102
- #: List of classes we'll treat as negative (defaults to "empty", typically includes
100
+
101
+ #: List of classes we'll treat as negative (defaults to "empty", typically includes
103
102
  #: classes like "blank", "misfire", etc.).
104
103
  #:
105
104
  #: Include the token "#NO_LABELS#" to indicate that an image with no annotations
106
105
  #: should be considered empty.
107
106
  self.negative_classes = DEFAULT_NEGATIVE_CLASSES
108
-
109
- #: List of classes we'll treat as neither positive nor negative (defaults to
107
+
108
+ #: List of classes we'll treat as neither positive nor negative (defaults to
110
109
  #: "unknown", typically includes classes like "unidentifiable").
111
110
  self.unlabeled_classes = DEFAULT_UNKNOWN_CLASSES
112
-
111
+
113
112
  #: List of output sets that we should count, but not render images for.
114
113
  #:
115
114
  #: Typically used to preview sets with lots of empties, where you don't want to
@@ -118,120 +117,122 @@ class PostProcessingOptions:
118
117
  #: detections, non_detections
119
118
  #: detections_animal, detections_person, detections_vehicle
120
119
  self.rendering_bypass_sets = []
121
-
120
+
122
121
  #: If this is None, choose a confidence threshold based on the detector version.
123
122
  #:
124
- #: This can either be a float or a dictionary mapping category names (not IDs) to
125
- #: thresholds. The category "default" can be used to specify thresholds for
126
- #: other categories. Currently the use of a dict here is not supported when
123
+ #: This can either be a float or a dictionary mapping category names (not IDs) to
124
+ #: thresholds. The category "default" can be used to specify thresholds for
125
+ #: other categories. Currently the use of a dict here is not supported when
127
126
  #: ground truth is supplied.
128
127
  self.confidence_threshold = None
129
-
128
+
130
129
  #: Confidence threshold to apply to classification (not detection) results
131
130
  #:
132
131
  #: Only a float is supported here (unlike the "confidence_threshold" parameter, which
133
- #: can be a dict).
132
+ #: can be a dict).
134
133
  self.classification_confidence_threshold = 0.5
135
-
134
+
136
135
  #: Used for summary statistics only
137
136
  self.target_recall = 0.9
138
-
137
+
139
138
  #: Number of images to sample, -1 for "all images"
140
139
  self.num_images_to_sample = 500
141
-
140
+
142
141
  #: Random seed for sampling, or None
143
142
  self.sample_seed = 0 # None
144
-
143
+
145
144
  #: Image width for images in the HTML output
146
145
  self.viz_target_width = 800
147
-
146
+
148
147
  #: Line width (in pixels) for rendering detections
149
148
  self.line_thickness = 4
150
-
149
+
151
150
  #: Box expansion (in pixels) for rendering detections
152
151
  self.box_expansion = 0
153
-
152
+
154
153
  #: Job name to include in big letters in the output HTML
155
154
  self.job_name_string = None
156
-
155
+
157
156
  #: Model version string to include in the output HTML
158
157
  self.model_version_string = None
159
-
158
+
160
159
  #: Sort order for the output, should be one of "filename", "confidence", or "random"
161
160
  self.html_sort_order = 'filename'
162
-
161
+
163
162
  #: If True, images in the output HTML will be links back to the original images
164
163
  self.link_images_to_originals = True
165
-
164
+
166
165
  #: Optionally separate detections into categories (animal/vehicle/human)
167
- #:
166
+ #:
168
167
  #: Currently only supported when ground truth is unavailable
169
168
  self.separate_detections_by_category = True
170
-
169
+
171
170
  #: Optionally replace one or more strings in filenames with other strings;
172
171
  #: useful for taking a set of results generated for one folder structure
173
172
  #: and applying them to a slightly different folder structure.
174
173
  self.api_output_filename_replacements = {}
175
-
174
+
176
175
  #: Optionally replace one or more strings in filenames with other strings;
177
176
  #: useful for taking a set of results generated for one folder structure
178
177
  #: and applying them to a slightly different folder structure.
179
178
  self.ground_truth_filename_replacements = {}
180
-
179
+
181
180
  #: Allow bypassing API output loading when operating on previously-loaded
182
181
  #: results. If present, this is a Pandas DataFrame. Almost never useful.
183
182
  self.api_detection_results = None
184
-
183
+
185
184
  #: Allow bypassing API output loading when operating on previously-loaded
186
185
  #: results. If present, this is a str --> obj dict. Almost never useful.
187
186
  self.api_other_fields = None
188
-
187
+
189
188
  #: Should we also split out a separate report about the detections that were
190
189
  #: just below our main confidence threshold?
191
190
  #:
192
191
  #: Currently only supported when ground truth is unavailable.
193
192
  self.include_almost_detections = False
194
-
193
+
195
194
  #: Only a float is supported here (unlike the "confidence_threshold" parameter, which
196
195
  #: can be a dict).
197
196
  self.almost_detection_confidence_threshold = None
198
-
199
- #: Enable/disable rendering parallelization
197
+
198
+ #: Enable/disable rendering parallelization
200
199
  self.parallelize_rendering = False
201
-
200
+
202
201
  #: Number of threads/processes to use for rendering parallelization
203
202
  self.parallelize_rendering_n_cores = 25
204
-
203
+
205
204
  #: Whether to use threads (True) or processes (False) for rendering parallelization
206
205
  self.parallelize_rendering_with_threads = True
207
-
206
+
208
207
  #: When classification results are present, should be sort alphabetically by class name (False)
209
208
  #: or in descending order by frequency (True)?
210
209
  self.sort_classification_results_by_count = False
211
-
210
+
212
211
  #: Should we split individual pages up into smaller pages if there are more than
213
212
  #: N images?
214
213
  self.max_figures_per_html_file = None
215
-
214
+
216
215
  #: Footer text for the index page
217
- # self.footer_text = '<br/><p style="font-size:80%;">Preview page created with the <a href="{}">MegaDetector Python package</a>.</p>'.\
216
+ # self.footer_text = \
217
+ # '<br/><p style="font-size:80%;">Preview page created with the ' + \
218
+ # <a href="{}">MegaDetector Python package</a>.</p>'.\
218
219
  # format('https://megadetector.readthedocs.io')
219
220
  self.footer_text = ''
220
221
 
221
222
  #: Character encoding to use when writing the index HTML html
222
223
  self.output_html_encoding = None
223
-
224
+
224
225
  #: Additional image fields to display in image headers. If this is a list,
225
226
  #: we'll include those fields; if this is a dict, we'll use that dict to choose
226
227
  #: alternative display names for each field.
227
228
  self.additional_image_fields_to_display = None
228
-
229
- #: If classification results are present, should we include a summary of
229
+
230
+ #: If classification results are present, should we include a summary of
230
231
  #: classification categories?
231
232
  self.include_classification_category_report = True
232
-
233
+
233
234
  # ...__init__()
234
-
235
+
235
236
  # ...PostProcessingOptions
236
237
 
237
238
 
@@ -239,15 +240,15 @@ class PostProcessingResults:
239
240
  """
240
241
  Return format from process_batch_results
241
242
  """
242
-
243
+
243
244
  def __init__(self):
244
-
245
+
245
246
  #: HTML file to which preview information was written
246
247
  self.output_html_file = ''
247
-
248
+
248
249
  #: Pandas Dataframe containing detection results
249
250
  self.api_detection_results = None
250
-
251
+
251
252
  #: str --> obj dictionary containing other information loaded from the results file
252
253
  self.api_other_fields = None
253
254
 
@@ -258,10 +259,10 @@ class DetectionStatus(IntEnum):
258
259
  """
259
260
  Flags used to mark images as positive or negative for P/R analysis
260
261
  (according to ground truth and/or detector output)
261
-
262
+
262
263
  :meta private:
263
264
  """
264
-
265
+
265
266
  DS_NEGATIVE = 0
266
267
  DS_POSITIVE = 1
267
268
 
@@ -294,7 +295,7 @@ def _mark_detection_status(indexed_db,
294
295
 
295
296
  returns (n_negative, n_positive, n_unknown, n_ambiguous)
296
297
  """
297
-
298
+
298
299
  negative_classes = set(negative_classes)
299
300
  unknown_classes = set(unknown_classes)
300
301
 
@@ -323,7 +324,7 @@ def _mark_detection_status(indexed_db,
323
324
 
324
325
  # If there are no image annotations...
325
326
  if len(categories) == 0:
326
-
327
+
327
328
  if '#NO_LABELS#' in negative_classes:
328
329
  n_negative += 1
329
330
  im['_detection_status'] = DetectionStatus.DS_NEGATIVE
@@ -379,10 +380,10 @@ def is_sas_url(s) -> bool:
379
380
  """
380
381
  Placeholder for a more robust way to verify that a link is a SAS URL.
381
382
  99.999% of the time this will suffice for what we're using it for right now.
382
-
383
+
383
384
  :meta private:
384
385
  """
385
-
386
+
386
387
  return (s.startswith(('http://', 'https://')) and ('core.windows.net' in s)
387
388
  and ('?' in s))
388
389
 
@@ -391,10 +392,10 @@ def relative_sas_url(folder_url, relative_path):
391
392
  """
392
393
  Given a container-level or folder-level SAS URL, create a SAS URL to the
393
394
  specified relative path.
394
-
395
+
395
396
  :meta private:
396
397
  """
397
-
398
+
398
399
  relative_path = relative_path.replace('%','%25')
399
400
  relative_path = relative_path.replace('#','%23')
400
401
  relative_path = relative_path.replace(' ','%20')
@@ -422,8 +423,8 @@ def _render_bounding_boxes(
422
423
  options=None):
423
424
  """
424
425
  Renders detection bounding boxes on a single image.
425
-
426
- This is an internal function; if you want tools for rendering boxes on images, see
426
+
427
+ This is an internal function; if you want tools for rendering boxes on images, see
427
428
  visualization.visualization_utils.
428
429
 
429
430
  The source image is:
@@ -432,18 +433,18 @@ def _render_bounding_boxes(
432
433
 
433
434
  The target image is, for example:
434
435
 
435
- [options.output_dir] /
436
- ['detections' or 'non_detections'] /
436
+ [options.output_dir] /
437
+ ['detections' or 'non_detections'] /
437
438
  [filename with slashes turned into tildes]
438
439
 
439
440
  "res" is a result type, e.g. "detections", "non-detections"; this determines the
440
441
  output folder for the rendered image.
441
-
442
+
442
443
  Only very preliminary support is provided for ground truth box rendering.
443
-
444
+
444
445
  Returns the html info struct for this image in the format that's used for
445
446
  write_html_image_list.
446
-
447
+
447
448
  :meta private:
448
449
  """
449
450
 
@@ -451,7 +452,7 @@ def _render_bounding_boxes(
451
452
  options = PostProcessingOptions()
452
453
 
453
454
  image_full_path = None
454
-
455
+
455
456
  if res in options.rendering_bypass_sets:
456
457
 
457
458
  sample_name = res + '_' + path_utils.flatten_path(image_relative_path)
@@ -467,26 +468,26 @@ def _render_bounding_boxes(
467
468
  # to just try/except on the image open.
468
469
  try:
469
470
  image = vis_utils.open_image(image_full_path)
470
- except:
471
- print('Warning: could not open image file {}'.format(image_full_path))
471
+ except Exception as e:
472
+ print('Warning: could not open image file {}: {}'.format(image_full_path,str(e)))
472
473
  image = None
473
474
  # return ''
474
-
475
+
475
476
  # Render images to a flat folder
476
477
  sample_name = res + '_' + path_utils.flatten_path(image_relative_path)
477
478
  fullpath = os.path.join(options.output_dir, res, sample_name)
478
479
 
479
480
  if image is not None:
480
-
481
+
481
482
  original_size = image.size
482
-
483
+
483
484
  # Resize the image if necessary
484
485
  if options.viz_target_width is not None:
485
486
  image = vis_utils.resize_image(image, options.viz_target_width)
486
-
487
+
487
488
  # Render ground truth boxes if necessary
488
489
  if ground_truth_boxes is not None and len(ground_truth_boxes) > 0:
489
-
490
+
490
491
  # Create class labels like "gt_1" or "gt_27"
491
492
  gt_classes = [0] * len(ground_truth_boxes)
492
493
  label_map = {0:'ground truth'}
@@ -495,7 +496,7 @@ def _render_bounding_boxes(
495
496
  vis_utils.render_db_bounding_boxes(ground_truth_boxes, gt_classes, image,
496
497
  original_size=original_size,label_map=label_map,
497
498
  thickness=4,expansion=4)
498
-
499
+
499
500
  # Prepare per-category confidence thresholds
500
501
  if isinstance(options.confidence_threshold,float):
501
502
  rendering_confidence_threshold = options.confidence_threshold
@@ -507,7 +508,7 @@ def _render_bounding_boxes(
507
508
  for category_id in category_ids:
508
509
  rendering_confidence_threshold[category_id] = \
509
510
  _get_threshold_for_category_id(category_id, options, detection_categories)
510
-
511
+
511
512
  # Render detection boxes
512
513
  vis_utils.render_detection_bounding_boxes(
513
514
  detections, image,
@@ -517,7 +518,7 @@ def _render_bounding_boxes(
517
518
  classification_confidence_threshold=options.classification_confidence_threshold,
518
519
  thickness=options.line_thickness,
519
520
  expansion=options.box_expansion)
520
-
521
+
521
522
  try:
522
523
  image.save(fullpath)
523
524
  except OSError as e:
@@ -539,18 +540,18 @@ def _render_bounding_boxes(
539
540
  'textStyle':\
540
541
  'font-family:verdana,arial,calibri;font-size:80%;text-align:left;margin-top:20;margin-bottom:5'
541
542
  }
542
-
543
+
543
544
  # Optionally add links back to the original images
544
545
  if options.link_images_to_originals and (image_full_path is not None):
545
-
546
+
546
547
  # Handling special characters in links has been pushed down into
547
548
  # write_html_image_list
548
549
  #
549
550
  # link_target = image_full_path.replace('\\','/')
550
551
  # link_target = urllib.parse.quote(link_target)
551
552
  link_target = image_full_path
552
- info['linkTarget'] = link_target
553
-
553
+ info['linkTarget'] = link_target
554
+
554
555
  return info
555
556
 
556
557
  # ..._render_bounding_boxes
@@ -561,12 +562,12 @@ def _prepare_html_subpages(images_html, output_dir, options=None):
561
562
  Write out a series of html image lists, e.g. the "detections" or "non-detections"
562
563
  pages.
563
564
 
564
- image_html is a dictionary mapping an html page name (e.g. "detections_animal") to
565
+ image_html is a dictionary mapping an html page name (e.g. "detections_animal") to
565
566
  a list of image structs friendly to write_html_image_list.
566
-
567
+
567
568
  Returns a dictionary mapping category names to image counts.
568
569
  """
569
-
570
+
570
571
  if options is None:
571
572
  options = PostProcessingOptions()
572
573
 
@@ -582,19 +583,20 @@ def _prepare_html_subpages(images_html, output_dir, options=None):
582
583
  sorted_array = sorted(array, key=lambda x: x['filename'])
583
584
  images_html_sorted[res] = sorted_array
584
585
  images_html = images_html_sorted
585
-
586
+
586
587
  # Optionally sort by confidence before writing to html
587
588
  elif options.html_sort_order == 'confidence':
588
589
  images_html_sorted = {}
589
590
  for res, array in images_html.items():
590
-
591
+
591
592
  if not all(['max_conf' in d for d in array]):
592
- print("Warning: some elements in the {} page don't have confidence values, can't sort by confidence".format(res))
593
+ print(f"Warning: some elements in the {res} page don't have confidence " + \
594
+ "values, can't sort by confidence")
593
595
  else:
594
596
  sorted_array = sorted(array, key=lambda x: x['max_conf'], reverse=True)
595
597
  images_html_sorted[res] = sorted_array
596
598
  images_html = images_html_sorted
597
-
599
+
598
600
  else:
599
601
  assert options.html_sort_order == 'random',\
600
602
  'Unrecognized sort order {}'.format(options.html_sort_order)
@@ -603,15 +605,15 @@ def _prepare_html_subpages(images_html, output_dir, options=None):
603
605
  sorted_array = random.sample(array,len(array))
604
606
  images_html_sorted[res] = sorted_array
605
607
  images_html = images_html_sorted
606
-
608
+
607
609
  # Write the individual HTML files
608
610
  for res, array in images_html.items():
609
-
610
- html_image_list_options = {}
611
+
612
+ html_image_list_options = {}
611
613
  html_image_list_options['maxFiguresPerHtmlFile'] = options.max_figures_per_html_file
612
614
  html_image_list_options['headerHtml'] = '<h1>{}</h1>'.format(res.upper())
613
615
  html_image_list_options['pageTitle'] = '{}'.format(res.lower())
614
-
616
+
615
617
  # Don't write empty pages
616
618
  if len(array) == 0:
617
619
  continue
@@ -630,49 +632,49 @@ def _get_threshold_for_category_name(category_name,options):
630
632
  """
631
633
  Determines the confidence threshold we should use for a specific category name.
632
634
  """
633
-
635
+
634
636
  if isinstance(options.confidence_threshold,float):
635
637
  return options.confidence_threshold
636
638
  else:
637
639
  assert isinstance(options.confidence_threshold,dict), \
638
640
  'confidence_threshold must either be a float or a dict'
639
-
641
+
640
642
  if category_name in options.confidence_threshold:
641
-
643
+
642
644
  return options.confidence_threshold[category_name]
643
-
645
+
644
646
  else:
645
647
  assert 'default' in options.confidence_threshold, \
646
648
  'category {} not in confidence_threshold dict, and no default supplied'.format(
647
649
  category_name)
648
650
  return options.confidence_threshold['default']
649
651
 
650
-
652
+
651
653
  def _get_threshold_for_category_id(category_id,options,detection_categories):
652
654
  """
653
655
  Determines the confidence threshold we should use for a specific category ID.
654
-
656
+
655
657
  [detection_categories] is a dict mapping category IDs to names.
656
658
  """
657
-
658
- if isinstance(options.confidence_threshold,float):
659
+
660
+ if isinstance(options.confidence_threshold,float):
659
661
  return options.confidence_threshold
660
-
662
+
661
663
  assert category_id in detection_categories, \
662
664
  'Invalid category ID {}'.format(category_id)
663
-
665
+
664
666
  category_name = detection_categories[category_id]
665
-
667
+
666
668
  return _get_threshold_for_category_name(category_name,options)
667
-
668
-
669
+
670
+
669
671
  def _get_positive_categories(detections,options,detection_categories):
670
672
  """
671
673
  Gets a sorted list of unique categories (as string IDs) above the threshold for this image
672
-
674
+
673
675
  [detection_categories] is a dict mapping category IDs to names.
674
676
  """
675
-
677
+
676
678
  positive_categories = set()
677
679
  for d in detections:
678
680
  threshold = _get_threshold_for_category_id(d['category'], options, detection_categories)
@@ -685,8 +687,8 @@ def _has_positive_detection(detections,options,detection_categories):
685
687
  """
686
688
  Determines whether any positive detections are present in the detection list
687
689
  [detections].
688
- """
689
-
690
+ """
691
+
690
692
  found_positive_detection = False
691
693
  for d in detections:
692
694
  threshold = _get_threshold_for_category_id(d['category'], options, detection_categories)
@@ -694,51 +696,51 @@ def _has_positive_detection(detections,options,detection_categories):
694
696
  found_positive_detection = True
695
697
  break
696
698
  return found_positive_detection
697
-
699
+
698
700
 
699
701
  def _render_image_no_gt(file_info,
700
702
  detection_categories_to_results_name,
701
703
  detection_categories,
702
704
  classification_categories,
703
705
  options):
704
- """
706
+ r"""
705
707
  Renders an image (with no ground truth information)
706
-
707
- Returns a list of rendering structs, where the first item is a category (e.g. "detections_animal"),
708
+
709
+ Returns a list of rendering structs, where the first item is a category (e.g. "detections_animal"),
708
710
  and the second is a dict of information needed for rendering. E.g.:
709
-
710
- [['detections_animal',
711
+
712
+ [['detections_animal',
711
713
  {
712
- 'filename': 'detections_animal/detections_animal_blah~01060415.JPG',
713
- 'title': '<b>Result type</b>: detections_animal,
714
+ 'filename': 'detections_animal/detections_animal_blah~01060415.JPG',
715
+ 'title': '<b>Result type</b>: detections_animal,
714
716
  <b>Image</b>: blah\\01060415.JPG,
715
717
  <b>Max conf</b>: 0.897',
716
718
  'textStyle': 'font-family:verdana,arial,calibri;font-size:80%;text-align:left;margin-top:20;margin-bottom:5',
717
719
  'linkTarget': 'full_path_to_%5C01060415.JPG'
718
720
  }]]
719
-
721
+
720
722
  When no classification data is present, this list will always be length-1. When
721
723
  classification data is present, an image may appear in multiple categories.
722
-
724
+
723
725
  Populates the 'max_conf' field of the first element of the list.
724
-
726
+
725
727
  Returns None if there are any errors.
726
728
  """
727
-
729
+
728
730
  image_relative_path = file_info['file']
729
-
731
+
730
732
  # Useful debug snippet
731
733
  #
732
734
  # if 'filename' in image_relative_path:
733
735
  # import pdb; pdb.set_trace()
734
-
736
+
735
737
  max_conf = file_info['max_detection_conf']
736
738
  detections = file_info['detections']
737
739
 
738
740
  # Determine whether any positive detections are present (using a threshold that
739
741
  # may vary by category)
740
742
  found_positive_detection = _has_positive_detection(detections,options,detection_categories)
741
-
743
+
742
744
  detection_status = DetectionStatus.DS_UNASSIGNED
743
745
  if found_positive_detection:
744
746
  detection_status = DetectionStatus.DS_POSITIVE
@@ -772,17 +774,17 @@ def _render_image_no_gt(file_info,
772
774
 
773
775
  # Are there any bonus fields we need to include in each image header?
774
776
  if options.additional_image_fields_to_display is not None:
775
-
777
+
776
778
  for field_name in options.additional_image_fields_to_display:
777
-
779
+
778
780
  if field_name in file_info:
779
-
781
+
780
782
  field_value = file_info[field_name]
781
-
783
+
782
784
  if (field_value is None) or \
783
785
  (isinstance(field_value,float) and np.isnan(field_value)):
784
786
  continue
785
-
787
+
786
788
  # Optionally use a display name that's different from the field name
787
789
  if isinstance(options.additional_image_fields_to_display,dict):
788
790
  field_display_name = \
@@ -791,12 +793,12 @@ def _render_image_no_gt(file_info,
791
793
  field_display_name = field_name
792
794
  field_string = '<b>{}</b>: {}'.format(field_display_name,field_value)
793
795
  display_name += ', {}'.format(field_string)
794
-
796
+
795
797
  rendering_options = copy.copy(options)
796
798
  if detection_status == DetectionStatus.DS_ALMOST:
797
799
  rendering_options.confidence_threshold = \
798
800
  rendering_options.almost_detection_confidence_threshold
799
-
801
+
800
802
  rendered_image_html_info = _render_bounding_boxes(
801
803
  image_base_dir=options.image_base_dir,
802
804
  image_relative_path=image_relative_path,
@@ -815,9 +817,9 @@ def _render_image_no_gt(file_info,
815
817
  image_result = [[res, rendered_image_html_info]]
816
818
  classes_rendered_this_image = set()
817
819
  max_conf = 0
818
-
820
+
819
821
  for det in detections:
820
-
822
+
821
823
  if det['conf'] > max_conf:
822
824
  max_conf = det['conf']
823
825
 
@@ -827,7 +829,7 @@ def _render_image_no_gt(file_info,
827
829
  _get_threshold_for_category_id(det['category'], options, detection_categories)
828
830
  if det['conf'] < detection_threshold:
829
831
  continue
830
-
832
+
831
833
  if ('classifications' in det) and (len(det['classifications']) > 0) and \
832
834
  (res != 'non_detections'):
833
835
 
@@ -837,14 +839,14 @@ def _render_image_no_gt(file_info,
837
839
  top1_class_name = classification_categories[top1_class_id]
838
840
  top1_class_score = classifications[0][1]
839
841
 
840
- # If we either don't have a classification confidence threshold, or
842
+ # If we either don't have a classification confidence threshold, or
841
843
  # we've met our classification confidence threshold
842
844
  if (options.classification_confidence_threshold < 0) or \
843
845
  (top1_class_score >= options.classification_confidence_threshold):
844
- class_string = 'class_{}'.format(top1_class_name)
846
+ class_string = 'class_{}'.format(top1_class_name)
845
847
  else:
846
848
  class_string = 'class_unreliable'
847
-
849
+
848
850
  if class_string not in classes_rendered_this_image:
849
851
  image_result.append([class_string,
850
852
  rendered_image_html_info])
@@ -855,13 +857,13 @@ def _render_image_no_gt(file_info,
855
857
  # ...for each detection
856
858
 
857
859
  image_result[0][1]['max_conf'] = max_conf
858
-
860
+
859
861
  # ...if we got valid rendering info back from _render_bounding_boxes()
860
-
862
+
861
863
  return image_result
862
864
 
863
865
  # ...def _render_image_no_gt()
864
-
866
+
865
867
 
866
868
  def _render_image_with_gt(file_info,ground_truth_indexed_db,
867
869
  detection_categories,classification_categories,options):
@@ -869,7 +871,7 @@ def _render_image_with_gt(file_info,ground_truth_indexed_db,
869
871
  Render an image with ground truth information. See _render_image_no_gt for return
870
872
  data format.
871
873
  """
872
-
874
+
873
875
  image_relative_path = file_info['file']
874
876
  max_conf = file_info['max_detection_conf']
875
877
  detections = file_info['detections']
@@ -890,7 +892,7 @@ def _render_image_with_gt(file_info,ground_truth_indexed_db,
890
892
  ground_truth_box = [x for x in ann['bbox']]
891
893
  ground_truth_box.append(ann['category_id'])
892
894
  ground_truth_boxes.append(ground_truth_box)
893
-
895
+
894
896
  gt_status = image['_detection_status']
895
897
 
896
898
  gt_presence = bool(gt_status)
@@ -920,7 +922,7 @@ def _render_image_with_gt(file_info,ground_truth_indexed_db,
920
922
  else:
921
923
  res = 'tn'
922
924
 
923
- display_name = '<b>Result type</b>: {}, <b>Presence</b>: {}, <b>Class</b>: {}, <b>Max conf</b>: {:0.3f}%, <b>Image</b>: {}'.format(
925
+ display_name = '<b>Result type</b>: {}, <b>Presence</b>: {}, <b>Class</b>: {}, <b>Max conf</b>: {:0.3f}%, <b>Image</b>: {}'.format( # noqa
924
926
  res.upper(), str(gt_presence), gt_class_summary,
925
927
  max_conf * 100, image_relative_path)
926
928
 
@@ -945,7 +947,7 @@ def _render_image_with_gt(file_info,ground_truth_indexed_db,
945
947
 
946
948
  # ...def _render_image_with_gt()
947
949
 
948
-
950
+
949
951
  #%% Main function
950
952
 
951
953
  def process_batch_results(options):
@@ -960,15 +962,15 @@ def process_batch_results(options):
960
962
  truth)
961
963
 
962
964
  Ground truth, if available, must be in COCO Camera Traps format:
963
-
965
+
964
966
  https://github.com/agentmorris/MegaDetector/blob/main/megadetector/data_management/README.md#coco-camera-traps-format
965
967
 
966
968
  Args:
967
969
  options (PostProcessingOptions): everything we need to render a preview/analysis for
968
970
  this set of results; see the PostProcessingOptions class for details.
969
-
971
+
970
972
  Returns:
971
- PostProcessingResults: information about the results/preview, most importantly the
973
+ PostProcessingResults: information about the results/preview, most importantly the
972
974
  HTML filename of the output. See the PostProcessingResults class for details.
973
975
  """
974
976
  ppresults = PostProcessingResults()
@@ -990,7 +992,7 @@ def process_batch_results(options):
990
992
  if (options.ground_truth_json_file is not None) and (len(options.ground_truth_json_file) > 0):
991
993
  assert (options.confidence_threshold is None) or (isinstance(options.confidence_threshold,float)), \
992
994
  'Variable confidence thresholds are not supported when supplying ground truth'
993
-
995
+
994
996
  if (options.ground_truth_json_file is not None) and (len(options.ground_truth_json_file) > 0):
995
997
 
996
998
  if options.separate_detections_by_category:
@@ -1027,7 +1029,7 @@ def process_batch_results(options):
1027
1029
  options.md_results_file, force_forward_slashes=True,
1028
1030
  filename_replacements=options.api_output_filename_replacements)
1029
1031
  ppresults.api_detection_results = detections_df
1030
- ppresults.api_other_fields = other_fields
1032
+ ppresults.api_other_fields = other_fields
1031
1033
 
1032
1034
  else:
1033
1035
  print('Bypassing detection results loading...')
@@ -1036,13 +1038,13 @@ def process_batch_results(options):
1036
1038
  other_fields = options.api_other_fields
1037
1039
 
1038
1040
  # Determine confidence thresholds if necessary
1039
-
1041
+
1040
1042
  if options.confidence_threshold is None:
1041
1043
  options.confidence_threshold = \
1042
1044
  get_typical_confidence_threshold_from_results(other_fields)
1043
1045
  print('Choosing default confidence threshold of {} based on MD version'.format(
1044
- options.confidence_threshold))
1045
-
1046
+ options.confidence_threshold))
1047
+
1046
1048
  if options.almost_detection_confidence_threshold is None and options.include_almost_detections:
1047
1049
  assert isinstance(options.confidence_threshold,float), \
1048
1050
  'If you are using a dictionary of confidence thresholds and almost-detections are enabled, ' + \
@@ -1050,7 +1052,7 @@ def process_batch_results(options):
1050
1052
  options.almost_detection_confidence_threshold = options.confidence_threshold - 0.05
1051
1053
  if options.almost_detection_confidence_threshold < 0:
1052
1054
  options.almost_detection_confidence_threshold = 0
1053
-
1055
+
1054
1056
  # Remove rows with inference failures (typically due to corrupt images)
1055
1057
  n_failures = 0
1056
1058
  if 'failure' in detections_df.columns:
@@ -1059,11 +1061,11 @@ def process_batch_results(options):
1059
1061
  # Explicitly forcing a copy() operation here to suppress "trying to be set
1060
1062
  # on a copy" warnings (and associated risks) below.
1061
1063
  detections_df = detections_df[detections_df['failure'].isna()].copy()
1062
-
1064
+
1063
1065
  assert other_fields is not None
1064
1066
 
1065
1067
  detection_categories = other_fields['detection_categories']
1066
-
1068
+
1067
1069
  # Convert keys and values to lowercase
1068
1070
  classification_categories = other_fields.get('classification_categories', {})
1069
1071
  if classification_categories is not None:
@@ -1075,19 +1077,19 @@ def process_batch_results(options):
1075
1077
  # Count detections and almost-detections for reporting purposes
1076
1078
  n_positives = 0
1077
1079
  n_almosts = 0
1078
-
1080
+
1079
1081
  print('Assigning images to rendering categories')
1080
-
1082
+
1081
1083
  for i_row,row in tqdm(detections_df.iterrows(),total=len(detections_df)):
1082
-
1084
+
1083
1085
  detections = row['detections']
1084
1086
  max_conf = row['max_detection_conf']
1085
1087
  if _has_positive_detection(detections, options, detection_categories):
1086
1088
  n_positives += 1
1087
1089
  elif (options.almost_detection_confidence_threshold is not None) and \
1088
1090
  (max_conf >= options.almost_detection_confidence_threshold):
1089
- n_almosts += 1
1090
-
1091
+ n_almosts += 1
1092
+
1091
1093
  print(f'Finished loading and preprocessing {len(detections_df)} rows '
1092
1094
  f'from detector output, predicted {n_positives} positives.')
1093
1095
 
@@ -1106,18 +1108,18 @@ def process_batch_results(options):
1106
1108
  job_name_string = 'unknown'
1107
1109
  else:
1108
1110
  job_name_string = os.path.basename(options.md_results_file)
1109
-
1111
+
1110
1112
  if options.model_version_string is not None:
1111
1113
  model_version_string = options.model_version_string
1112
1114
  else:
1113
-
1115
+
1114
1116
  if 'info' not in other_fields or 'detector' not in other_fields['info']:
1115
1117
  print('No model metadata supplied, assuming MDv4')
1116
1118
  model_version_string = 'MDv4 (assumed)'
1117
- else:
1119
+ else:
1118
1120
  model_version_string = other_fields['info']['detector']
1119
-
1120
-
1121
+
1122
+
1121
1123
  ##%% If we have ground truth, remove images we can't match to ground truth
1122
1124
 
1123
1125
  if ground_truth_indexed_db is not None:
@@ -1147,7 +1149,7 @@ def process_batch_results(options):
1147
1149
 
1148
1150
  output_html_file = ''
1149
1151
 
1150
- style_header = """<head>
1152
+ style_header = """<head>
1151
1153
  <title>Detection results preview</title>
1152
1154
  <style type="text/css">
1153
1155
  a { text-decoration: none; }
@@ -1176,9 +1178,9 @@ def process_batch_results(options):
1176
1178
 
1177
1179
  n_positive = 0
1178
1180
  n_negative = 0
1179
-
1181
+
1180
1182
  for i_detection, fn in enumerate(detector_files):
1181
-
1183
+
1182
1184
  image_id = ground_truth_indexed_db.filename_to_id[fn]
1183
1185
  image = ground_truth_indexed_db.image_id_to_image[image_id]
1184
1186
  detection_status = image['_detection_status']
@@ -1194,7 +1196,7 @@ def process_batch_results(options):
1194
1196
 
1195
1197
  print('Of {} ground truth values, found {} positives and {} negatives'.format(
1196
1198
  len(detections_df),n_positive,n_negative))
1197
-
1199
+
1198
1200
  # Don't include ambiguous/unknown ground truth in precision/recall analysis
1199
1201
  b_valid_ground_truth = gt_detections >= 0.0
1200
1202
 
@@ -1221,16 +1223,16 @@ def process_batch_results(options):
1221
1223
 
1222
1224
  # Thresholds go up throughout precisions/recalls/thresholds; find the last
1223
1225
  # value where recall is at or above target. That's our precision @ target recall.
1224
-
1226
+
1225
1227
  i_above_target_recall = (np.where(recalls >= options.target_recall))
1226
-
1227
- # np.where returns a tuple of arrays, but in this syntax where we're
1228
+
1229
+ # np.where returns a tuple of arrays, but in this syntax where we're
1228
1230
  # comparing an array with a scalar, there will only be one element.
1229
1231
  assert len (i_above_target_recall) == 1
1230
-
1232
+
1231
1233
  # Convert back to a list
1232
1234
  i_above_target_recall = i_above_target_recall[0].tolist()
1233
-
1235
+
1234
1236
  if len(i_above_target_recall) == 0:
1235
1237
  precision_at_target_recall = 0.0
1236
1238
  else:
@@ -1387,7 +1389,7 @@ def process_batch_results(options):
1387
1389
  t = 'Precision-Recall curve: AP={:0.1%}, P@{:0.1%}={:0.1%}'.format(
1388
1390
  average_precision, options.target_recall, precision_at_target_recall)
1389
1391
  fig = plot_utils.plot_precision_recall_curve(precisions, recalls, t)
1390
-
1392
+
1391
1393
  pr_figure_relative_filename = 'prec_recall.png'
1392
1394
  pr_figure_filename = os.path.join(output_dir, pr_figure_relative_filename)
1393
1395
  fig.savefig(pr_figure_filename)
@@ -1402,7 +1404,7 @@ def process_batch_results(options):
1402
1404
  # Accumulate html image structs (in the format expected by write_html_image_lists)
1403
1405
  # for each category, e.g. 'tp', 'fp', ..., 'class_bird', ...
1404
1406
  images_html = collections.defaultdict(list)
1405
-
1407
+
1406
1408
  # Add default entries by accessing them for the first time
1407
1409
  [images_html[res] for res in ['tp', 'tpc', 'tpi', 'fp', 'tn', 'fn']]
1408
1410
  for res in images_html.keys():
@@ -1426,28 +1428,35 @@ def process_batch_results(options):
1426
1428
 
1427
1429
  start_time = time.time()
1428
1430
  if options.parallelize_rendering:
1429
- if options.parallelize_rendering_n_cores is None:
1430
- if options.parallelize_rendering_with_threads:
1431
- pool = ThreadPool()
1432
- else:
1433
- pool = Pool()
1434
- else:
1435
- if options.parallelize_rendering_with_threads:
1436
- pool = ThreadPool(options.parallelize_rendering_n_cores)
1437
- worker_string = 'threads'
1431
+ pool = None
1432
+ try:
1433
+ if options.parallelize_rendering_n_cores is None:
1434
+ if options.parallelize_rendering_with_threads:
1435
+ pool = ThreadPool()
1436
+ else:
1437
+ pool = Pool()
1438
1438
  else:
1439
- pool = Pool(options.parallelize_rendering_n_cores)
1440
- worker_string = 'processes'
1441
- print('Rendering images with {} {}'.format(options.parallelize_rendering_n_cores,
1442
- worker_string))
1443
-
1444
- rendering_results = list(tqdm(pool.imap(
1445
- partial(_render_image_with_gt,
1446
- ground_truth_indexed_db=ground_truth_indexed_db,
1447
- detection_categories=detection_categories,
1448
- classification_categories=classification_categories,
1449
- options=options),
1450
- files_to_render), total=len(files_to_render)))
1439
+ if options.parallelize_rendering_with_threads:
1440
+ pool = ThreadPool(options.parallelize_rendering_n_cores)
1441
+ worker_string = 'threads'
1442
+ else:
1443
+ pool = Pool(options.parallelize_rendering_n_cores)
1444
+ worker_string = 'processes'
1445
+ print('Rendering images with {} {}'.format(options.parallelize_rendering_n_cores,
1446
+ worker_string))
1447
+
1448
+ rendering_results = list(tqdm(pool.imap(
1449
+ partial(_render_image_with_gt,
1450
+ ground_truth_indexed_db=ground_truth_indexed_db,
1451
+ detection_categories=detection_categories,
1452
+ classification_categories=classification_categories,
1453
+ options=options),
1454
+ files_to_render), total=len(files_to_render)))
1455
+ finally:
1456
+ if pool is not None:
1457
+ pool.close()
1458
+ pool.join()
1459
+ print("Pool closed and joined for GT rendering")
1451
1460
  else:
1452
1461
  for file_info in tqdm(files_to_render):
1453
1462
  rendering_results.append(_render_image_with_gt(
@@ -1488,19 +1497,19 @@ def process_batch_results(options):
1488
1497
  confidence_threshold_string = '{:.2%}'.format(options.confidence_threshold)
1489
1498
  else:
1490
1499
  confidence_threshold_string = str(options.confidence_threshold)
1491
-
1492
- index_page = """<html>
1500
+
1501
+ index_page = """<html>
1493
1502
  {}
1494
1503
  <body>
1495
1504
  <h2>Evaluation</h2>
1496
1505
 
1497
1506
  <h3>Job metadata</h3>
1498
-
1507
+
1499
1508
  <div class="contentdiv">
1500
1509
  <p>Job name: {}<br/>
1501
1510
  <p>Model version: {}</p>
1502
1511
  </div>
1503
-
1512
+
1504
1513
  <h3>Sample images</h3>
1505
1514
  <div class="contentdiv">
1506
1515
  <p>A sample of {} images, annotated with detections above confidence {}.</p>
@@ -1576,12 +1585,12 @@ def process_batch_results(options):
1576
1585
  # Write custom footer if it was provided
1577
1586
  if (options.footer_text is not None) and (len(options.footer_text) > 0):
1578
1587
  index_page += '{}\n'.format(options.footer_text)
1579
-
1588
+
1580
1589
  # Close open html tags
1581
1590
  index_page += '\n</body></html>\n'
1582
-
1591
+
1583
1592
  output_html_file = os.path.join(output_dir, 'index.html')
1584
- with open(output_html_file, 'w',
1593
+ with open(output_html_file, 'w',
1585
1594
  encoding=options.output_html_encoding) as f:
1586
1595
  f.write(index_page)
1587
1596
 
@@ -1599,34 +1608,34 @@ def process_batch_results(options):
1599
1608
  # Accumulate html image structs (in the format expected by write_html_image_list)
1600
1609
  # for each category
1601
1610
  images_html = collections.defaultdict(list)
1602
-
1611
+
1603
1612
  # Add default entries by accessing them for the first time
1604
1613
 
1605
- # Maps sorted tuples of detection category IDs (string ints) - e.g. ("1"), ("1", "4", "7") - to
1614
+ # Maps sorted tuples of detection category IDs (string ints) - e.g. ("1"), ("1", "4", "7") - to
1606
1615
  # result set names, e.g. "detections_human", "detections_cat_truck".
1607
1616
  detection_categories_to_results_name = {}
1608
-
1617
+
1609
1618
  # Keep track of which categories are single-class (e.g. "animal") and which are
1610
1619
  # combinations (e.g. "animal_vehicle")
1611
1620
  detection_categories_to_category_count = {}
1612
-
1621
+
1613
1622
  # For the creation of a "non-detections" category
1614
1623
  images_html['non_detections']
1615
1624
  detection_categories_to_category_count['non_detections'] = 0
1616
-
1617
-
1625
+
1626
+
1618
1627
  if not options.separate_detections_by_category:
1619
1628
  # For the creation of a "detections" category
1620
1629
  images_html['detections']
1621
- detection_categories_to_category_count['detections'] = 0
1630
+ detection_categories_to_category_count['detections'] = 0
1622
1631
  else:
1623
1632
  # Add a set of results for each category and combination of categories, e.g.
1624
1633
  # "detections_animal_vehicle". When we're using this script for non-MegaDetector
1625
1634
  # results, this can generate lots of categories, e.g. detections_bear_bird_cat_dog_pig.
1626
1635
  # We'll keep that huge set of combinations in this map, but we'll only write
1627
- # out links for the ones that are non-empty.
1636
+ # out links for the ones that are non-empty.
1628
1637
  used_combinations = set()
1629
-
1638
+
1630
1639
  # row = images_to_visualize.iloc[0]
1631
1640
  for i_row, row in images_to_visualize.iterrows():
1632
1641
  detections_this_row = row['detections']
@@ -1639,7 +1648,7 @@ def process_batch_results(options):
1639
1648
  continue
1640
1649
  sorted_categories_this_row = tuple(sorted(above_threshold_category_ids_this_row))
1641
1650
  used_combinations.add(sorted_categories_this_row)
1642
-
1651
+
1643
1652
  for sorted_subset in used_combinations:
1644
1653
  assert len(sorted_subset) > 0
1645
1654
  results_name = 'detections'
@@ -1647,7 +1656,7 @@ def process_batch_results(options):
1647
1656
  results_name = results_name + '_' + detection_categories[category_id]
1648
1657
  images_html[results_name]
1649
1658
  detection_categories_to_results_name[sorted_subset] = results_name
1650
- detection_categories_to_category_count[results_name] = len(sorted_subset)
1659
+ detection_categories_to_category_count[results_name] = len(sorted_subset)
1651
1660
 
1652
1661
  if options.include_almost_detections:
1653
1662
  images_html['almost_detections']
@@ -1658,7 +1667,7 @@ def process_batch_results(options):
1658
1667
  os.makedirs(os.path.join(output_dir, res), exist_ok=True)
1659
1668
 
1660
1669
  image_count = len(images_to_visualize)
1661
-
1670
+
1662
1671
  # Each element will be a list of 2-tuples, with elements [collection name,html info struct]
1663
1672
  rendering_results = []
1664
1673
 
@@ -1671,38 +1680,44 @@ def process_batch_results(options):
1671
1680
  for _, row in images_to_visualize.iterrows():
1672
1681
 
1673
1682
  assert isinstance(row['detections'],list)
1674
-
1683
+
1675
1684
  # Filenames should already have been normalized to either '/' or '\'
1676
1685
  files_to_render.append(row.to_dict())
1677
1686
 
1678
1687
  start_time = time.time()
1679
1688
  if options.parallelize_rendering:
1680
-
1681
- if options.parallelize_rendering_n_cores is None:
1682
- if options.parallelize_rendering_with_threads:
1683
- pool = ThreadPool()
1684
- else:
1685
- pool = Pool()
1686
- else:
1687
- if options.parallelize_rendering_with_threads:
1688
- pool = ThreadPool(options.parallelize_rendering_n_cores)
1689
- worker_string = 'threads'
1689
+ pool = None
1690
+ try:
1691
+ if options.parallelize_rendering_n_cores is None:
1692
+ if options.parallelize_rendering_with_threads:
1693
+ pool = ThreadPool()
1694
+ else:
1695
+ pool = Pool()
1690
1696
  else:
1691
- pool = Pool(options.parallelize_rendering_n_cores)
1692
- worker_string = 'processes'
1693
- print('Rendering images with {} {}'.format(options.parallelize_rendering_n_cores,
1694
- worker_string))
1695
-
1696
- # _render_image_no_gt(file_info,detection_categories_to_results_name,
1697
- # detection_categories,classification_categories)
1698
-
1699
- rendering_results = list(tqdm(pool.imap(
1700
- partial(_render_image_no_gt,
1701
- detection_categories_to_results_name=detection_categories_to_results_name,
1702
- detection_categories=detection_categories,
1703
- classification_categories=classification_categories,
1704
- options=options),
1705
- files_to_render), total=len(files_to_render)))
1697
+ if options.parallelize_rendering_with_threads:
1698
+ pool = ThreadPool(options.parallelize_rendering_n_cores)
1699
+ worker_string = 'threads'
1700
+ else:
1701
+ pool = Pool(options.parallelize_rendering_n_cores)
1702
+ worker_string = 'processes'
1703
+ print('Rendering images with {} {}'.format(options.parallelize_rendering_n_cores,
1704
+ worker_string))
1705
+
1706
+ # _render_image_no_gt(file_info,detection_categories_to_results_name,
1707
+ # detection_categories,classification_categories)
1708
+
1709
+ rendering_results = list(tqdm(pool.imap(
1710
+ partial(_render_image_no_gt,
1711
+ detection_categories_to_results_name=detection_categories_to_results_name,
1712
+ detection_categories=detection_categories,
1713
+ classification_categories=classification_categories,
1714
+ options=options),
1715
+ files_to_render), total=len(files_to_render)))
1716
+ finally:
1717
+ if pool is not None:
1718
+ pool.close()
1719
+ pool.join()
1720
+ print("Pool closed and joined for non-GT rendering")
1706
1721
  else:
1707
1722
  for file_info in tqdm(files_to_render):
1708
1723
  rendering_result = _render_image_no_gt(file_info,
@@ -1711,12 +1726,12 @@ def process_batch_results(options):
1711
1726
  classification_categories,
1712
1727
  options=options)
1713
1728
  rendering_results.append(rendering_result)
1714
-
1715
- elapsed = time.time() - start_time
1716
-
1729
+
1730
+ elapsed = time.time() - start_time
1731
+
1717
1732
  # Do we have classification results in addition to detection results?
1718
1733
  has_classification_info = False
1719
-
1734
+
1720
1735
  # Map all the rendering results in the list rendering_results into the
1721
1736
  # dictionary images_html
1722
1737
  image_rendered_count = 0
@@ -1731,7 +1746,7 @@ def process_batch_results(options):
1731
1746
 
1732
1747
  # Prepare the individual html image files
1733
1748
  image_counts = _prepare_html_subpages(images_html, output_dir, options)
1734
-
1749
+
1735
1750
  if image_rendered_count == 0:
1736
1751
  seconds_per_image = 0.0
1737
1752
  else:
@@ -1744,7 +1759,7 @@ def process_batch_results(options):
1744
1759
  # Write index.html
1745
1760
 
1746
1761
  # We can't just sum these, because image_counts includes images in both their
1747
- # detection and classification classes
1762
+ # detection and classification classes
1748
1763
  total_images = 0
1749
1764
  for k in image_counts.keys():
1750
1765
  v = image_counts[k]
@@ -1754,7 +1769,7 @@ def process_batch_results(options):
1754
1769
 
1755
1770
  if total_images != image_count:
1756
1771
  print('Warning, missing images: image_count is {}, total_images is {}'.format(total_images,image_count))
1757
-
1772
+
1758
1773
  almost_detection_string = ''
1759
1774
  if options.include_almost_detections:
1760
1775
  almost_detection_string = ' (&ldquo;almost detection&rdquo; threshold at {:.1%})'.format(
@@ -1765,15 +1780,15 @@ def process_batch_results(options):
1765
1780
  confidence_threshold_string = '{:.2%}'.format(options.confidence_threshold)
1766
1781
  else:
1767
1782
  confidence_threshold_string = str(options.confidence_threshold)
1768
-
1783
+
1769
1784
  index_page = """<html>\n{}\n<body>\n
1770
1785
  <h2>Visualization of results for {}</h2>\n
1771
1786
  <p>A sample of {} images (of {} total)FAILURE_PLACEHOLDER, annotated with detections above confidence {}{}.</p>\n
1772
-
1787
+
1773
1788
  <div class="contentdiv">
1774
1789
  <p>Model version: {}</p>
1775
1790
  </div>
1776
-
1791
+
1777
1792
  <h3>Detection results</h3>\n
1778
1793
  <div class="contentdiv">\n""".format(
1779
1794
  style_header, job_name_string, image_count, len(detections_df), confidence_threshold_string,
@@ -1781,9 +1796,9 @@ def process_batch_results(options):
1781
1796
 
1782
1797
  failure_string = ''
1783
1798
  if n_failures is not None:
1784
- failure_string = ' ({} failures)'.format(n_failures)
1799
+ failure_string = ' ({} failures)'.format(n_failures)
1785
1800
  index_page = index_page.replace('FAILURE_PLACEHOLDER',failure_string)
1786
-
1801
+
1787
1802
  def result_set_name_to_friendly_name(result_set_name):
1788
1803
  friendly_name = ''
1789
1804
  friendly_name = result_set_name.replace('_','-')
@@ -1793,7 +1808,7 @@ def process_batch_results(options):
1793
1808
  return friendly_name
1794
1809
 
1795
1810
  sorted_result_set_names = sorted(list(images_html.keys()))
1796
-
1811
+
1797
1812
  result_set_name_to_count = {}
1798
1813
  for result_set_name in sorted_result_set_names:
1799
1814
  image_count = image_counts[result_set_name]
@@ -1801,7 +1816,7 @@ def process_batch_results(options):
1801
1816
  sorted_result_set_names = sorted(sorted_result_set_names,
1802
1817
  key=lambda x: result_set_name_to_count[x],
1803
1818
  reverse=True)
1804
-
1819
+
1805
1820
  for result_set_name in sorted_result_set_names:
1806
1821
 
1807
1822
  # Don't print classification classes here; we'll do that later with a slightly
@@ -1812,17 +1827,17 @@ def process_batch_results(options):
1812
1827
  filename = result_set_name + '.html'
1813
1828
  label = result_set_name_to_friendly_name(result_set_name)
1814
1829
  image_count = image_counts[result_set_name]
1815
-
1830
+
1816
1831
  # Don't include line items for empty multi-category pages
1817
1832
  if image_count == 0 and \
1818
1833
  detection_categories_to_category_count[result_set_name] > 1:
1819
1834
  continue
1820
-
1835
+
1821
1836
  if total_images == 0:
1822
1837
  image_fraction = -1
1823
1838
  else:
1824
1839
  image_fraction = image_count / total_images
1825
-
1840
+
1826
1841
  # Write the line item for this category, including a link only if the
1827
1842
  # category is non-empty
1828
1843
  if image_count == 0:
@@ -1831,17 +1846,17 @@ def process_batch_results(options):
1831
1846
  else:
1832
1847
  index_page += '<a href="{}">{}</a> ({}, {:.1%})<br/>\n'.format(
1833
1848
  filename,label,image_count,image_fraction)
1834
-
1849
+
1835
1850
  # ...for each result set
1836
-
1851
+
1837
1852
  index_page += '</div>\n'
1838
1853
 
1839
1854
  # If classification information is present and we're supposed to create
1840
1855
  # a summary of classifications, we'll put it here
1841
1856
  category_count_footer = None
1842
-
1857
+
1843
1858
  if has_classification_info:
1844
-
1859
+
1845
1860
  index_page += '<h3>Species classification results</h3>'
1846
1861
  index_page += '<p>The same image might appear under multiple classes ' + \
1847
1862
  'if multiple species were detected.</p>\n'
@@ -1855,12 +1870,12 @@ def process_batch_results(options):
1855
1870
  class_names.append('unreliable')
1856
1871
 
1857
1872
  if options.sort_classification_results_by_count:
1858
- class_name_to_count = {}
1873
+ class_name_to_count = {}
1859
1874
  for cname in class_names:
1860
1875
  ccount = len(images_html['class_{}'.format(cname)])
1861
1876
  class_name_to_count[cname] = ccount
1862
- class_names = sorted(class_names,key=lambda x: class_name_to_count[x],reverse=True)
1863
-
1877
+ class_names = sorted(class_names,key=lambda x: class_name_to_count[x],reverse=True)
1878
+
1864
1879
  for cname in class_names:
1865
1880
  ccount = len(images_html['class_{}'.format(cname)])
1866
1881
  if ccount > 0:
@@ -1873,18 +1888,18 @@ def process_batch_results(options):
1873
1888
  # TODO: it's only for silly historical reasons that we re-read
1874
1889
  # the input file in this case; we're not currently carrying the json
1875
1890
  # representation around, only the Pandas representation.
1876
-
1891
+
1877
1892
  print('Generating classification category report')
1878
-
1893
+
1879
1894
  d = load_md_or_speciesnet_file(options.md_results_file)
1880
-
1895
+
1881
1896
  classification_category_to_count = {}
1882
1897
 
1883
1898
  # im = d['images'][0]
1884
1899
  for im in d['images']:
1885
1900
  if 'detections' in im and im['detections'] is not None:
1886
1901
  for det in im['detections']:
1887
- if 'classifications' in det:
1902
+ if ('classifications' in det) and (len(det['classifications']) > 0):
1888
1903
  class_id = det['classifications'][0][0]
1889
1904
  if class_id not in classification_category_to_count:
1890
1905
  classification_category_to_count[class_id] = 0
@@ -1910,31 +1925,31 @@ def process_batch_results(options):
1910
1925
 
1911
1926
  for category_name in category_name_to_count.keys():
1912
1927
  count = category_name_to_count[category_name]
1913
- category_count_html = '{}: {}<br>\n'.format(category_name,count)
1928
+ category_count_html = '{}: {}<br>\n'.format(category_name,count)
1914
1929
  category_count_footer += category_count_html
1915
1930
 
1916
1931
  category_count_footer += '</div>\n'
1917
-
1932
+
1918
1933
  # ...if we're generating a classification category report
1919
-
1934
+
1920
1935
  # ...if classification info is present
1921
-
1936
+
1922
1937
  if category_count_footer is not None:
1923
1938
  index_page += category_count_footer + '\n'
1924
-
1939
+
1925
1940
  # Write custom footer if it was provided
1926
1941
  if (options.footer_text is not None) and (len(options.footer_text) > 0):
1927
1942
  index_page += options.footer_text + '\n'
1928
-
1943
+
1929
1944
  # Close open html tags
1930
1945
  index_page += '\n</body></html>\n'
1931
-
1946
+
1932
1947
  output_html_file = os.path.join(output_dir, 'index.html')
1933
- with open(output_html_file, 'w',
1948
+ with open(output_html_file, 'w',
1934
1949
  encoding=options.output_html_encoding) as f:
1935
1950
  f.write(index_page)
1936
1951
 
1937
- print('Finished writing html to {}'.format(output_html_file))
1952
+ print('Finished writing html to {}'.format(output_html_file))
1938
1953
 
1939
1954
  # ...if we do/don't have ground truth
1940
1955
 
@@ -1965,8 +1980,8 @@ if False:
1965
1980
 
1966
1981
  #%% Command-line driver
1967
1982
 
1968
- def main():
1969
-
1983
+ def main(): # noqa
1984
+
1970
1985
  options = PostProcessingOptions()
1971
1986
 
1972
1987
  parser = argparse.ArgumentParser()
@@ -2015,42 +2030,42 @@ def main():
2015
2030
  '--n_cores', type=int, default=1,
2016
2031
  help='Number of threads to use for rendering (default: 1)')
2017
2032
  parser.add_argument(
2018
- '--parallelize_rendering_with_processes',
2033
+ '--parallelize_rendering_with_processes',
2019
2034
  action='store_true',
2020
2035
  help='Should we use processes (instead of threads) for parallelization?')
2021
2036
  parser.add_argument(
2022
- '--no_separate_detections_by_category',
2037
+ '--no_separate_detections_by_category',
2023
2038
  action='store_true',
2024
- help='Collapse all categories into just "detections" and "non-detections"')
2039
+ help='Collapse all categories into just "detections" and "non-detections"')
2025
2040
  parser.add_argument(
2026
- '--open_output_file',
2041
+ '--open_output_file',
2027
2042
  action='store_true',
2028
- help='Open the HTML output file when finished')
2043
+ help='Open the HTML output file when finished')
2029
2044
  parser.add_argument(
2030
- '--max_figures_per_html_file',
2045
+ '--max_figures_per_html_file',
2031
2046
  type=int, default=None,
2032
2047
  help='Maximum number of images to put on a single HTML page')
2033
-
2048
+
2034
2049
  if len(sys.argv[1:]) == 0:
2035
2050
  parser.print_help()
2036
2051
  parser.exit()
2037
2052
 
2038
2053
  args = parser.parse_args()
2039
-
2054
+
2040
2055
  if args.n_cores != 1:
2041
2056
  assert (args.n_cores > 1), 'Illegal number of cores: {}'.format(args.n_cores)
2042
2057
  if args.parallelize_rendering_with_processes:
2043
2058
  args.parallelize_rendering_with_threads = False
2044
2059
  args.parallelize_rendering = True
2045
- args.parallelize_rendering_n_cores = args.n_cores
2060
+ args.parallelize_rendering_n_cores = args.n_cores
2046
2061
 
2047
- args_to_object(args, options)
2062
+ args_to_object(args, options)
2048
2063
 
2049
2064
  if args.no_separate_detections_by_category:
2050
2065
  options.separate_detections_by_category = False
2051
-
2066
+
2052
2067
  ppresults = process_batch_results(options)
2053
-
2068
+
2054
2069
  if options.open_output_file:
2055
2070
  path_utils.open_file(ppresults.output_html_file)
2056
2071