megadetector 5.0.11__py3-none-any.whl → 5.0.13__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 (203) hide show
  1. megadetector/api/__init__.py +0 -0
  2. megadetector/api/batch_processing/__init__.py +0 -0
  3. megadetector/api/batch_processing/api_core/__init__.py +0 -0
  4. megadetector/api/batch_processing/api_core/batch_service/__init__.py +0 -0
  5. megadetector/api/batch_processing/api_core/batch_service/score.py +439 -0
  6. megadetector/api/batch_processing/api_core/server.py +294 -0
  7. megadetector/api/batch_processing/api_core/server_api_config.py +97 -0
  8. megadetector/api/batch_processing/api_core/server_app_config.py +55 -0
  9. megadetector/api/batch_processing/api_core/server_batch_job_manager.py +220 -0
  10. megadetector/api/batch_processing/api_core/server_job_status_table.py +149 -0
  11. megadetector/api/batch_processing/api_core/server_orchestration.py +360 -0
  12. megadetector/api/batch_processing/api_core/server_utils.py +88 -0
  13. megadetector/api/batch_processing/api_core_support/__init__.py +0 -0
  14. megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +46 -0
  15. megadetector/api/batch_processing/api_support/__init__.py +0 -0
  16. megadetector/api/batch_processing/api_support/summarize_daily_activity.py +152 -0
  17. megadetector/api/batch_processing/data_preparation/__init__.py +0 -0
  18. megadetector/api/batch_processing/integration/digiKam/setup.py +6 -0
  19. megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +465 -0
  20. megadetector/api/batch_processing/integration/eMammal/test_scripts/config_template.py +5 -0
  21. megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +125 -0
  22. megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +55 -0
  23. megadetector/api/synchronous/__init__.py +0 -0
  24. megadetector/api/synchronous/api_core/animal_detection_api/__init__.py +0 -0
  25. megadetector/api/synchronous/api_core/animal_detection_api/api_backend.py +152 -0
  26. megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +263 -0
  27. megadetector/api/synchronous/api_core/animal_detection_api/config.py +35 -0
  28. megadetector/api/synchronous/api_core/tests/__init__.py +0 -0
  29. megadetector/api/synchronous/api_core/tests/load_test.py +110 -0
  30. megadetector/classification/__init__.py +0 -0
  31. megadetector/classification/aggregate_classifier_probs.py +108 -0
  32. megadetector/classification/analyze_failed_images.py +227 -0
  33. megadetector/classification/cache_batchapi_outputs.py +198 -0
  34. megadetector/classification/create_classification_dataset.py +627 -0
  35. megadetector/classification/crop_detections.py +516 -0
  36. megadetector/classification/csv_to_json.py +226 -0
  37. megadetector/classification/detect_and_crop.py +855 -0
  38. megadetector/classification/efficientnet/__init__.py +9 -0
  39. megadetector/classification/efficientnet/model.py +415 -0
  40. megadetector/classification/efficientnet/utils.py +607 -0
  41. megadetector/classification/evaluate_model.py +520 -0
  42. megadetector/classification/identify_mislabeled_candidates.py +152 -0
  43. megadetector/classification/json_to_azcopy_list.py +63 -0
  44. megadetector/classification/json_validator.py +699 -0
  45. megadetector/classification/map_classification_categories.py +276 -0
  46. megadetector/classification/merge_classification_detection_output.py +506 -0
  47. megadetector/classification/prepare_classification_script.py +194 -0
  48. megadetector/classification/prepare_classification_script_mc.py +228 -0
  49. megadetector/classification/run_classifier.py +287 -0
  50. megadetector/classification/save_mislabeled.py +110 -0
  51. megadetector/classification/train_classifier.py +827 -0
  52. megadetector/classification/train_classifier_tf.py +725 -0
  53. megadetector/classification/train_utils.py +323 -0
  54. megadetector/data_management/__init__.py +0 -0
  55. megadetector/data_management/annotations/__init__.py +0 -0
  56. megadetector/data_management/annotations/annotation_constants.py +34 -0
  57. megadetector/data_management/camtrap_dp_to_coco.py +237 -0
  58. megadetector/data_management/cct_json_utils.py +404 -0
  59. megadetector/data_management/cct_to_md.py +176 -0
  60. megadetector/data_management/cct_to_wi.py +289 -0
  61. megadetector/data_management/coco_to_labelme.py +283 -0
  62. megadetector/data_management/coco_to_yolo.py +662 -0
  63. megadetector/data_management/databases/__init__.py +0 -0
  64. megadetector/data_management/databases/add_width_and_height_to_db.py +33 -0
  65. megadetector/data_management/databases/combine_coco_camera_traps_files.py +206 -0
  66. megadetector/data_management/databases/integrity_check_json_db.py +493 -0
  67. megadetector/data_management/databases/subset_json_db.py +115 -0
  68. megadetector/data_management/generate_crops_from_cct.py +149 -0
  69. megadetector/data_management/get_image_sizes.py +189 -0
  70. megadetector/data_management/importers/add_nacti_sizes.py +52 -0
  71. megadetector/data_management/importers/add_timestamps_to_icct.py +79 -0
  72. megadetector/data_management/importers/animl_results_to_md_results.py +158 -0
  73. megadetector/data_management/importers/auckland_doc_test_to_json.py +373 -0
  74. megadetector/data_management/importers/auckland_doc_to_json.py +201 -0
  75. megadetector/data_management/importers/awc_to_json.py +191 -0
  76. megadetector/data_management/importers/bellevue_to_json.py +273 -0
  77. megadetector/data_management/importers/cacophony-thermal-importer.py +793 -0
  78. megadetector/data_management/importers/carrizo_shrubfree_2018.py +269 -0
  79. megadetector/data_management/importers/carrizo_trail_cam_2017.py +289 -0
  80. megadetector/data_management/importers/cct_field_adjustments.py +58 -0
  81. megadetector/data_management/importers/channel_islands_to_cct.py +913 -0
  82. megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +180 -0
  83. megadetector/data_management/importers/eMammal/eMammal_helpers.py +249 -0
  84. megadetector/data_management/importers/eMammal/make_eMammal_json.py +223 -0
  85. megadetector/data_management/importers/ena24_to_json.py +276 -0
  86. megadetector/data_management/importers/filenames_to_json.py +386 -0
  87. megadetector/data_management/importers/helena_to_cct.py +283 -0
  88. megadetector/data_management/importers/idaho-camera-traps.py +1407 -0
  89. megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +294 -0
  90. megadetector/data_management/importers/jb_csv_to_json.py +150 -0
  91. megadetector/data_management/importers/mcgill_to_json.py +250 -0
  92. megadetector/data_management/importers/missouri_to_json.py +490 -0
  93. megadetector/data_management/importers/nacti_fieldname_adjustments.py +79 -0
  94. megadetector/data_management/importers/noaa_seals_2019.py +181 -0
  95. megadetector/data_management/importers/pc_to_json.py +365 -0
  96. megadetector/data_management/importers/plot_wni_giraffes.py +123 -0
  97. megadetector/data_management/importers/prepare-noaa-fish-data-for-lila.py +359 -0
  98. megadetector/data_management/importers/prepare_zsl_imerit.py +131 -0
  99. megadetector/data_management/importers/rspb_to_json.py +356 -0
  100. megadetector/data_management/importers/save_the_elephants_survey_A.py +320 -0
  101. megadetector/data_management/importers/save_the_elephants_survey_B.py +329 -0
  102. megadetector/data_management/importers/snapshot_safari_importer.py +758 -0
  103. megadetector/data_management/importers/snapshot_safari_importer_reprise.py +665 -0
  104. megadetector/data_management/importers/snapshot_serengeti_lila.py +1067 -0
  105. megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +150 -0
  106. megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +153 -0
  107. megadetector/data_management/importers/sulross_get_exif.py +65 -0
  108. megadetector/data_management/importers/timelapse_csv_set_to_json.py +490 -0
  109. megadetector/data_management/importers/ubc_to_json.py +399 -0
  110. megadetector/data_management/importers/umn_to_json.py +507 -0
  111. megadetector/data_management/importers/wellington_to_json.py +263 -0
  112. megadetector/data_management/importers/wi_to_json.py +442 -0
  113. megadetector/data_management/importers/zamba_results_to_md_results.py +181 -0
  114. megadetector/data_management/labelme_to_coco.py +547 -0
  115. megadetector/data_management/labelme_to_yolo.py +272 -0
  116. megadetector/data_management/lila/__init__.py +0 -0
  117. megadetector/data_management/lila/add_locations_to_island_camera_traps.py +97 -0
  118. megadetector/data_management/lila/add_locations_to_nacti.py +147 -0
  119. megadetector/data_management/lila/create_lila_blank_set.py +558 -0
  120. megadetector/data_management/lila/create_lila_test_set.py +152 -0
  121. megadetector/data_management/lila/create_links_to_md_results_files.py +106 -0
  122. megadetector/data_management/lila/download_lila_subset.py +178 -0
  123. megadetector/data_management/lila/generate_lila_per_image_labels.py +516 -0
  124. megadetector/data_management/lila/get_lila_annotation_counts.py +170 -0
  125. megadetector/data_management/lila/get_lila_image_counts.py +112 -0
  126. megadetector/data_management/lila/lila_common.py +300 -0
  127. megadetector/data_management/lila/test_lila_metadata_urls.py +132 -0
  128. megadetector/data_management/ocr_tools.py +870 -0
  129. megadetector/data_management/read_exif.py +809 -0
  130. megadetector/data_management/remap_coco_categories.py +84 -0
  131. megadetector/data_management/remove_exif.py +66 -0
  132. megadetector/data_management/rename_images.py +187 -0
  133. megadetector/data_management/resize_coco_dataset.py +189 -0
  134. megadetector/data_management/wi_download_csv_to_coco.py +247 -0
  135. megadetector/data_management/yolo_output_to_md_output.py +446 -0
  136. megadetector/data_management/yolo_to_coco.py +676 -0
  137. megadetector/detection/__init__.py +0 -0
  138. megadetector/detection/detector_training/__init__.py +0 -0
  139. megadetector/detection/detector_training/model_main_tf2.py +114 -0
  140. megadetector/detection/process_video.py +846 -0
  141. megadetector/detection/pytorch_detector.py +355 -0
  142. megadetector/detection/run_detector.py +779 -0
  143. megadetector/detection/run_detector_batch.py +1219 -0
  144. megadetector/detection/run_inference_with_yolov5_val.py +1087 -0
  145. megadetector/detection/run_tiled_inference.py +934 -0
  146. megadetector/detection/tf_detector.py +192 -0
  147. megadetector/detection/video_utils.py +698 -0
  148. megadetector/postprocessing/__init__.py +0 -0
  149. megadetector/postprocessing/add_max_conf.py +64 -0
  150. megadetector/postprocessing/categorize_detections_by_size.py +165 -0
  151. megadetector/postprocessing/classification_postprocessing.py +716 -0
  152. megadetector/postprocessing/combine_api_outputs.py +249 -0
  153. megadetector/postprocessing/compare_batch_results.py +966 -0
  154. megadetector/postprocessing/convert_output_format.py +396 -0
  155. megadetector/postprocessing/load_api_results.py +195 -0
  156. megadetector/postprocessing/md_to_coco.py +310 -0
  157. megadetector/postprocessing/md_to_labelme.py +330 -0
  158. megadetector/postprocessing/merge_detections.py +412 -0
  159. megadetector/postprocessing/postprocess_batch_results.py +1908 -0
  160. megadetector/postprocessing/remap_detection_categories.py +170 -0
  161. megadetector/postprocessing/render_detection_confusion_matrix.py +660 -0
  162. megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +211 -0
  163. megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +83 -0
  164. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +1635 -0
  165. megadetector/postprocessing/separate_detections_into_folders.py +730 -0
  166. megadetector/postprocessing/subset_json_detector_output.py +700 -0
  167. megadetector/postprocessing/top_folders_to_bottom.py +223 -0
  168. megadetector/taxonomy_mapping/__init__.py +0 -0
  169. megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +491 -0
  170. megadetector/taxonomy_mapping/map_new_lila_datasets.py +150 -0
  171. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +142 -0
  172. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +588 -0
  173. megadetector/taxonomy_mapping/retrieve_sample_image.py +71 -0
  174. megadetector/taxonomy_mapping/simple_image_download.py +219 -0
  175. megadetector/taxonomy_mapping/species_lookup.py +834 -0
  176. megadetector/taxonomy_mapping/taxonomy_csv_checker.py +159 -0
  177. megadetector/taxonomy_mapping/taxonomy_graph.py +346 -0
  178. megadetector/taxonomy_mapping/validate_lila_category_mappings.py +83 -0
  179. megadetector/utils/__init__.py +0 -0
  180. megadetector/utils/azure_utils.py +178 -0
  181. megadetector/utils/ct_utils.py +613 -0
  182. megadetector/utils/directory_listing.py +246 -0
  183. megadetector/utils/md_tests.py +1164 -0
  184. megadetector/utils/path_utils.py +1045 -0
  185. megadetector/utils/process_utils.py +160 -0
  186. megadetector/utils/sas_blob_utils.py +509 -0
  187. megadetector/utils/split_locations_into_train_val.py +228 -0
  188. megadetector/utils/string_utils.py +92 -0
  189. megadetector/utils/url_utils.py +323 -0
  190. megadetector/utils/write_html_image_list.py +225 -0
  191. megadetector/visualization/__init__.py +0 -0
  192. megadetector/visualization/plot_utils.py +293 -0
  193. megadetector/visualization/render_images_with_thumbnails.py +275 -0
  194. megadetector/visualization/visualization_utils.py +1536 -0
  195. megadetector/visualization/visualize_db.py +552 -0
  196. megadetector/visualization/visualize_detector_output.py +405 -0
  197. {megadetector-5.0.11.dist-info → megadetector-5.0.13.dist-info}/LICENSE +0 -0
  198. {megadetector-5.0.11.dist-info → megadetector-5.0.13.dist-info}/METADATA +2 -2
  199. megadetector-5.0.13.dist-info/RECORD +201 -0
  200. megadetector-5.0.13.dist-info/top_level.txt +1 -0
  201. megadetector-5.0.11.dist-info/RECORD +0 -5
  202. megadetector-5.0.11.dist-info/top_level.txt +0 -1
  203. {megadetector-5.0.11.dist-info → megadetector-5.0.13.dist-info}/WHEEL +0 -0
@@ -0,0 +1,779 @@
1
+ """
2
+
3
+ run_detector.py
4
+
5
+ Module to run an animal detection model on images. The main function in this script also renders
6
+ the predicted bounding boxes on images and saves the resulting images (with bounding boxes).
7
+
8
+ **This script is not a good way to process lots of images**. It does not produce a useful
9
+ output format, and it does not facilitate checkpointing the results so if it crashes you
10
+ would have to start from scratch. **If you want to run a detector on lots of images, you should
11
+ check out run_detector_batch.py**.
12
+
13
+ That said, this script (run_detector.py) is a good way to test our detector on a handful of images
14
+ and get super-satisfying, graphical results.
15
+
16
+ If you would like to *not* use the GPU on the machine, set the environment
17
+ variable CUDA_VISIBLE_DEVICES to "-1".
18
+
19
+ This script will only consider detections with > 0.005 confidence at all times.
20
+ The threshold you provide is only for rendering the results. If you need to
21
+ see lower-confidence detections, you can change DEFAULT_OUTPUT_CONFIDENCE_THRESHOLD.
22
+
23
+ """
24
+
25
+ #%% Constants, imports, environment
26
+
27
+ import argparse
28
+ import os
29
+ import statistics
30
+ import sys
31
+ import time
32
+ import warnings
33
+ import tempfile
34
+
35
+ import humanfriendly
36
+ from tqdm import tqdm
37
+
38
+ from megadetector.utils import path_utils as path_utils
39
+ from megadetector.visualization import visualization_utils as vis_utils
40
+ from megadetector.utils.url_utils import download_url
41
+
42
+ # ignoring all "PIL cannot read EXIF metainfo for the images" warnings
43
+ warnings.filterwarnings('ignore', '(Possibly )?corrupt EXIF data', UserWarning)
44
+
45
+ # Metadata Warning, tag 256 had too many entries: 42, expected 1
46
+ warnings.filterwarnings('ignore', 'Metadata warning', UserWarning)
47
+
48
+ # Numpy FutureWarnings from tensorflow import
49
+ warnings.filterwarnings('ignore', category=FutureWarning)
50
+
51
+ # Useful hack to force CPU inference
52
+ # os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
53
+
54
+
55
+ # An enumeration of failure reasons
56
+ FAILURE_INFER = 'Failure inference'
57
+ FAILURE_IMAGE_OPEN = 'Failure image access'
58
+
59
+ # Number of decimal places to round to for confidence and bbox coordinates
60
+ CONF_DIGITS = 3
61
+ COORD_DIGITS = 4
62
+
63
+ # Label mapping for MegaDetector
64
+ DEFAULT_DETECTOR_LABEL_MAP = {
65
+ '1': 'animal',
66
+ '2': 'person',
67
+ '3': 'vehicle' # available in megadetector v4+
68
+ }
69
+
70
+ # Should we allow classes that don't look anything like the MegaDetector classes?
71
+ #
72
+ # By default, we error if we see unfamiliar classes.
73
+ #
74
+ # TODO: the use of a global variable to manage this was fine when this was really
75
+ # experimental, but this is really sloppy now that we actually use this code for
76
+ # models other than MegaDetector.
77
+ USE_MODEL_NATIVE_CLASSES = False
78
+
79
+ # Each version of the detector is associated with some "typical" values
80
+ # that are included in output files, so that downstream applications can
81
+ # use them as defaults.
82
+ DETECTOR_METADATA = {
83
+ 'v2.0.0':
84
+ {'megadetector_version':'v2.0.0',
85
+ 'typical_detection_threshold':0.8,
86
+ 'conservative_detection_threshold':0.3},
87
+ 'v3.0.0':
88
+ {'megadetector_version':'v3.0.0',
89
+ 'typical_detection_threshold':0.8,
90
+ 'conservative_detection_threshold':0.3},
91
+ 'v4.1.0':
92
+ {'megadetector_version':'v4.1.0',
93
+ 'typical_detection_threshold':0.8,
94
+ 'conservative_detection_threshold':0.3},
95
+ 'v5a.0.0':
96
+ {'megadetector_version':'v5a.0.0',
97
+ 'typical_detection_threshold':0.2,
98
+ 'conservative_detection_threshold':0.05},
99
+ 'v5b.0.0':
100
+ {'megadetector_version':'v5b.0.0',
101
+ 'typical_detection_threshold':0.2,
102
+ 'conservative_detection_threshold':0.05}
103
+ }
104
+
105
+ DEFAULT_RENDERING_CONFIDENCE_THRESHOLD = DETECTOR_METADATA['v5b.0.0']['typical_detection_threshold']
106
+ DEFAULT_OUTPUT_CONFIDENCE_THRESHOLD = 0.005
107
+
108
+ DEFAULT_BOX_THICKNESS = 4
109
+ DEFAULT_BOX_EXPANSION = 0
110
+ DEFAULT_LABEL_FONT_SIZE = 16
111
+ DETECTION_FILENAME_INSERT = '_detections'
112
+
113
+ # The model filenames "MDV5A", "MDV5B", and "MDV4" are special; they will trigger an
114
+ # automatic model download to the system temp folder, or they will use the paths specified in the
115
+ # $MDV4, $MDV5A, or $MDV5B environment variables if they exist.
116
+ downloadable_models = {
117
+ 'MDV4':'https://github.com/agentmorris/MegaDetector/releases/download/v4.1/md_v4.1.0.pb',
118
+ 'MDV5A':'https://github.com/agentmorris/MegaDetector/releases/download/v5.0/md_v5a.0.0.pt',
119
+ 'MDV5B':'https://github.com/agentmorris/MegaDetector/releases/download/v5.0/md_v5b.0.0.pt'
120
+ }
121
+
122
+ model_string_to_model_version = {
123
+ 'v2':'v2.0.0',
124
+ 'v3':'v3.0.0',
125
+ 'v4.1':'v4.1.0',
126
+ 'v5a.0.0':'v5a.0.0',
127
+ 'v5b.0.0':'v5b.0.0',
128
+ 'mdv5a':'v5a.0.0',
129
+ 'mdv5b':'v5b.0.0',
130
+ 'mdv4':'v4.1.0',
131
+ 'mdv3':'v3.0.0'
132
+ }
133
+
134
+ # Approximate inference speeds (in images per second) for MDv5 based on
135
+ # benchmarks, only used for reporting very coarse expectations about inference time.
136
+ device_token_to_mdv5_inference_speed = {
137
+ '4090':17.6,
138
+ '3090':11.4,
139
+ '3080':9.5,
140
+ '3050':4.2,
141
+ 'P2000':2.1,
142
+ # These are written this way because they're MDv4 benchmarks, and MDv5
143
+ # is around 3.5x faster than MDv4.
144
+ 'V100':2.79*3.5,
145
+ '2080':2.3*3.5,
146
+ '2060':1.6*3.5
147
+ }
148
+
149
+
150
+ #%% Utility functions
151
+
152
+ def convert_to_tf_coords(array):
153
+ """
154
+ Converts a bounding box from [x1, y1, width, height] to [y1, x1, y2, x2]. This
155
+ is mostly not helpful, this function only exists to maintain backwards compatibility
156
+ in the synchronous API, which possibly zero people in the world are using.
157
+
158
+ Args:
159
+ array (list): a bounding box in [x,y,w,h] format
160
+
161
+ Returns:
162
+ list: a bounding box in [y1,x1,y2,x2] format
163
+ """
164
+
165
+ x1 = array[0]
166
+ y1 = array[1]
167
+ width = array[2]
168
+ height = array[3]
169
+ x2 = x1 + width
170
+ y2 = y1 + height
171
+
172
+ return [y1, x1, y2, x2]
173
+
174
+
175
+ def get_detector_metadata_from_version_string(detector_version):
176
+ """
177
+ Given a MegaDetector version string (e.g. "v4.1.0"), returns the metadata for
178
+ the model. Used for writing standard defaults to batch output files.
179
+
180
+ Args:
181
+ detector_version (str): a detection version string, e.g. "v4.1.0", which you
182
+ can extract from a filename using get_detector_version_from_filename()
183
+
184
+ Returns:
185
+ dict: metadata for this model, suitable for writing to a MD output file
186
+ """
187
+
188
+ if detector_version not in DETECTOR_METADATA:
189
+ print('Warning: no metadata for unknown detector version {}'.format(detector_version))
190
+ default_detector_metadata = {
191
+ 'megadetector_version':'unknown',
192
+ 'typical_detection_threshold':0.5,
193
+ 'conservative_detection_threshold':0.25
194
+ }
195
+ return default_detector_metadata
196
+ else:
197
+ return DETECTOR_METADATA[detector_version]
198
+
199
+
200
+ def get_detector_version_from_filename(detector_filename):
201
+ r"""
202
+ Gets the version number component of the detector from the model filename.
203
+
204
+ [detector_filename] will almost always end with one of the following:
205
+
206
+ * megadetector_v2.pb
207
+ * megadetector_v3.pb
208
+ * megadetector_v4.1 (not produed by run_detector_batch.py, only found in output files from the deprecated Azure Batch API)
209
+ * md_v4.1.0.pb
210
+ * md_v5a.0.0.pt
211
+ * md_v5b.0.0.pt
212
+
213
+ This function identifies the version number as "v2.0.0", "v3.0.0", "v4.1.0",
214
+ "v4.1.0", "v5a.0.0", and "v5b.0.0", respectively.
215
+
216
+ Args:
217
+ detector_filename (str): model filename, e.g. c:/x/z/md_v5a.0.0.pt
218
+
219
+ Returns:
220
+ str: a detector version string, e.g. "v5a.0.0", or "multiple" if I'm confused
221
+ """
222
+
223
+ fn = os.path.basename(detector_filename).lower()
224
+ matches = []
225
+ for s in model_string_to_model_version.keys():
226
+ if s in fn:
227
+ matches.append(s)
228
+ if len(matches) == 0:
229
+ print('Warning: could not determine MegaDetector version for model file {}'.format(detector_filename))
230
+ return 'unknown'
231
+ elif len(matches) > 1:
232
+ print('Warning: multiple MegaDetector versions for model file {}'.format(detector_filename))
233
+ return 'multiple'
234
+ else:
235
+ return model_string_to_model_version[matches[0]]
236
+
237
+
238
+ def estimate_md_images_per_second(model_file, device_name=None):
239
+ r"""
240
+ Estimates how fast MegaDetector will run, based on benchmarks. Defaults to querying
241
+ the current device. Returns None if no data is available for the current card/model.
242
+ Estimates only available for a small handful of GPUs. Uses an absurdly simple lookup
243
+ approach, e.g. if the string "4090" appears in the device name, congratulations,
244
+ you have an RTX 4090.
245
+
246
+ Args:
247
+ model_file (str): model filename, e.g. c:/x/z/md_v5a.0.0.pt
248
+ device_name (str, optional): device name, e.g. blah-blah-4090-blah-blah
249
+
250
+ Returns:
251
+ float: the approximate number of images this model version can process on this
252
+ device per second
253
+ """
254
+
255
+ if device_name is None:
256
+ try:
257
+ import torch
258
+ device_name = torch.cuda.get_device_name()
259
+ except Exception as e:
260
+ print('Error querying device name: {}'.format(e))
261
+ return None
262
+
263
+ model_file = model_file.lower().strip()
264
+ if model_file in model_string_to_model_version.values():
265
+ model_version = model_file
266
+ else:
267
+ model_version = get_detector_version_from_filename(model_file)
268
+ if model_version not in model_string_to_model_version.values():
269
+ print('Error determining model version for model file {}'.format(model_file))
270
+ return None
271
+
272
+ mdv5_inference_speed = None
273
+ for device_token in device_token_to_mdv5_inference_speed.keys():
274
+ if device_token in device_name:
275
+ mdv5_inference_speed = device_token_to_mdv5_inference_speed[device_token]
276
+ break
277
+
278
+ if mdv5_inference_speed is None:
279
+ print('No speed estimate available for {}'.format(device_name))
280
+
281
+ if 'v5' in model_version:
282
+ return mdv5_inference_speed
283
+ elif 'v2' in model_version or 'v3' in model_version or 'v4' in model_version:
284
+ return mdv5_inference_speed / 3.5
285
+ else:
286
+ print('Could not estimate inference speed for model file {}'.format(model_file))
287
+ return None
288
+
289
+
290
+ def get_typical_confidence_threshold_from_results(results):
291
+ """
292
+ Given the .json data loaded from a MD results file, returns a typical confidence
293
+ threshold based on the detector version.
294
+
295
+ Args:
296
+ results (dict): a dict of MD results, as it would be loaded from a MD results .json file
297
+
298
+ Returns:
299
+ float: a sensible default threshold for this model
300
+ """
301
+
302
+ if 'detector_metadata' in results['info'] and \
303
+ 'typical_detection_threshold' in results['info']['detector_metadata']:
304
+ default_threshold = results['info']['detector_metadata']['typical_detection_threshold']
305
+ elif ('detector' not in results['info']) or (results['info']['detector'] is None):
306
+ print('Warning: detector version not available in results file, using MDv5 defaults')
307
+ detector_metadata = get_detector_metadata_from_version_string('v5a.0.0')
308
+ default_threshold = detector_metadata['typical_detection_threshold']
309
+ else:
310
+ print('Warning: detector metadata not available in results file, inferring from MD version')
311
+ detector_filename = results['info']['detector']
312
+ detector_version = get_detector_version_from_filename(detector_filename)
313
+ detector_metadata = get_detector_metadata_from_version_string(detector_version)
314
+ default_threshold = detector_metadata['typical_detection_threshold']
315
+
316
+ return default_threshold
317
+
318
+
319
+ def is_gpu_available(model_file):
320
+ r"""
321
+ Determines whether a GPU is available, importing PyTorch or TF depending on the extension
322
+ of model_file. Does not actually load model_file, just uses that to determine how to check
323
+ for GPU availability (PT vs. TF).
324
+
325
+ Args:
326
+ model_file (str): model filename, e.g. c:/x/z/md_v5a.0.0.pt
327
+
328
+ Returns:
329
+ bool: whether a GPU is available
330
+ """
331
+
332
+ if model_file.endswith('.pb'):
333
+ import tensorflow.compat.v1 as tf
334
+ gpu_available = tf.test.is_gpu_available()
335
+ print('TensorFlow version:', tf.__version__)
336
+ print('tf.test.is_gpu_available:', gpu_available)
337
+ return gpu_available
338
+ elif model_file.endswith('.pt'):
339
+ import torch
340
+ gpu_available = torch.cuda.is_available()
341
+ print('PyTorch reports {} available CUDA devices'.format(torch.cuda.device_count()))
342
+ if not gpu_available:
343
+ try:
344
+ # mps backend only available in torch >= 1.12.0
345
+ if torch.backends.mps.is_built and torch.backends.mps.is_available():
346
+ gpu_available = True
347
+ print('PyTorch reports Metal Performance Shaders are available')
348
+ except AttributeError:
349
+ pass
350
+ return gpu_available
351
+ else:
352
+ raise ValueError('Unrecognized model file extension for model {}'.format(model_file))
353
+
354
+
355
+ def load_detector(model_file, force_cpu=False):
356
+ r"""
357
+ Loads a TF or PT detector, depending on the extension of model_file.
358
+
359
+ Args:
360
+ model_file (str): model filename, e.g. c:/x/z/md_v5a.0.0.pt
361
+
362
+ Returns:
363
+ object: loaded detector object
364
+ """
365
+
366
+ # Possibly automatically download the model
367
+ model_file = try_download_known_detector(model_file)
368
+
369
+ start_time = time.time()
370
+ if model_file.endswith('.pb'):
371
+ from megadetector.detection.tf_detector import TFDetector
372
+ if force_cpu:
373
+ raise ValueError('force_cpu is not currently supported for TF detectors, ' + \
374
+ 'use CUDA_VISIBLE_DEVICES=-1 instead')
375
+ detector = TFDetector(model_file)
376
+ elif model_file.endswith('.pt'):
377
+ from megadetector.detection.pytorch_detector import PTDetector
378
+ detector = PTDetector(model_file, force_cpu, USE_MODEL_NATIVE_CLASSES)
379
+ else:
380
+ raise ValueError('Unrecognized model format: {}'.format(model_file))
381
+ elapsed = time.time() - start_time
382
+ print('Loaded model in {}'.format(humanfriendly.format_timespan(elapsed)))
383
+
384
+ return detector
385
+
386
+
387
+ #%% Main function
388
+
389
+ def load_and_run_detector(model_file,
390
+ image_file_names,
391
+ output_dir,
392
+ render_confidence_threshold=DEFAULT_RENDERING_CONFIDENCE_THRESHOLD,
393
+ crop_images=False,
394
+ box_thickness=DEFAULT_BOX_THICKNESS,
395
+ box_expansion=DEFAULT_BOX_EXPANSION,
396
+ image_size=None,
397
+ label_font_size=DEFAULT_LABEL_FONT_SIZE
398
+ ):
399
+ r"""
400
+ Loads and runs a detector on target images, and visualizes the results.
401
+
402
+ Args:
403
+ model_file (str): model filename, e.g. c:/x/z/md_v5a.0.0.pt, or a known model
404
+ string, e.g. "MDV5A"
405
+ image_file_names (list): list of absolute paths to process
406
+ output_dir (str): folder to write visualized images to
407
+ render_confidence_threshold (float, optional): only render boxes for detections
408
+ above this threshold
409
+ crop_images (bool, optional): whether to crop detected objects to individual images
410
+ (default is to render images with boxes, rather than cropping)
411
+ box_thickness (float, optional): thickness in pixels for box rendering
412
+ box_expansion (float, optional): box expansion in pixels
413
+ image_size (tuple, optional): image size to use for inference, only mess with this
414
+ if (a) you're using a model other than MegaDetector or (b) you know what you're
415
+ doing
416
+ label_font_size (float, optional): font size to use for displaying class names
417
+ and confidence values in the rendered images
418
+ """
419
+
420
+ if len(image_file_names) == 0:
421
+ print('Warning: no files available')
422
+ return
423
+
424
+ # Possibly automatically download the model
425
+ model_file = try_download_known_detector(model_file)
426
+
427
+ print('GPU available: {}'.format(is_gpu_available(model_file)))
428
+
429
+ detector = load_detector(model_file)
430
+
431
+ detection_results = []
432
+ time_load = []
433
+ time_infer = []
434
+
435
+ # Dictionary mapping output file names to a collision-avoidance count.
436
+ #
437
+ # Since we'll be writing a bunch of files to the same folder, we rename
438
+ # as necessary to avoid collisions.
439
+ output_filename_collision_counts = {}
440
+
441
+ def input_file_to_detection_file(fn, crop_index=-1):
442
+ """
443
+ Creates unique file names for output files.
444
+
445
+ This function does 3 things:
446
+ 1) If the --crop flag is used, then each input image may produce several output
447
+ crops. For example, if foo.jpg has 3 detections, then this function should
448
+ get called 3 times, with crop_index taking on 0, 1, then 2. Each time, this
449
+ function appends crop_index to the filename, resulting in
450
+ foo_crop00_detections.jpg
451
+ foo_crop01_detections.jpg
452
+ foo_crop02_detections.jpg
453
+
454
+ 2) If the --recursive flag is used, then the same file (base)name may appear
455
+ multiple times. However, we output into a single flat folder. To avoid
456
+ filename collisions, we prepend an integer prefix to duplicate filenames:
457
+ foo_crop00_detections.jpg
458
+ 0000_foo_crop00_detections.jpg
459
+ 0001_foo_crop00_detections.jpg
460
+
461
+ 3) Prepends the output directory:
462
+ out_dir/foo_crop00_detections.jpg
463
+
464
+ Args:
465
+ fn: str, filename
466
+ crop_index: int, crop number
467
+
468
+ Returns: output file path
469
+ """
470
+
471
+ fn = os.path.basename(fn).lower()
472
+ name, ext = os.path.splitext(fn)
473
+ if crop_index >= 0:
474
+ name += '_crop{:0>2d}'.format(crop_index)
475
+ fn = '{}{}{}'.format(name, DETECTION_FILENAME_INSERT, '.jpg')
476
+ if fn in output_filename_collision_counts:
477
+ n_collisions = output_filename_collision_counts[fn]
478
+ fn = '{:0>4d}'.format(n_collisions) + '_' + fn
479
+ output_filename_collision_counts[fn] += 1
480
+ else:
481
+ output_filename_collision_counts[fn] = 0
482
+ fn = os.path.join(output_dir, fn)
483
+ return fn
484
+
485
+ # ...def input_file_to_detection_file()
486
+
487
+ for im_file in tqdm(image_file_names):
488
+
489
+ try:
490
+ start_time = time.time()
491
+
492
+ image = vis_utils.load_image(im_file)
493
+
494
+ elapsed = time.time() - start_time
495
+ time_load.append(elapsed)
496
+
497
+ except Exception as e:
498
+ print('Image {} cannot be loaded. Exception: {}'.format(im_file, e))
499
+ result = {
500
+ 'file': im_file,
501
+ 'failure': FAILURE_IMAGE_OPEN
502
+ }
503
+ detection_results.append(result)
504
+ continue
505
+
506
+ try:
507
+ start_time = time.time()
508
+
509
+ result = detector.generate_detections_one_image(image, im_file,
510
+ detection_threshold=DEFAULT_OUTPUT_CONFIDENCE_THRESHOLD,
511
+ image_size=image_size)
512
+ detection_results.append(result)
513
+
514
+ elapsed = time.time() - start_time
515
+ time_infer.append(elapsed)
516
+
517
+ except Exception as e:
518
+ print('An error occurred while running the detector on image {}. Exception: {}'.format(im_file, e))
519
+ continue
520
+
521
+ try:
522
+ if crop_images:
523
+
524
+ images_cropped = vis_utils.crop_image(result['detections'], image,
525
+ confidence_threshold=render_confidence_threshold,
526
+ expansion=box_expansion)
527
+
528
+ for i_crop, cropped_image in enumerate(images_cropped):
529
+ output_full_path = input_file_to_detection_file(im_file, i_crop)
530
+ cropped_image.save(output_full_path)
531
+
532
+ else:
533
+
534
+ # Image is modified in place
535
+ vis_utils.render_detection_bounding_boxes(result['detections'], image,
536
+ label_map=DEFAULT_DETECTOR_LABEL_MAP,
537
+ confidence_threshold=render_confidence_threshold,
538
+ thickness=box_thickness, expansion=box_expansion,
539
+ label_font_size=label_font_size)
540
+ output_full_path = input_file_to_detection_file(im_file)
541
+ image.save(output_full_path)
542
+
543
+ except Exception as e:
544
+ print('Visualizing results on the image {} failed. Exception: {}'.format(im_file, e))
545
+ continue
546
+
547
+ # ...for each image
548
+
549
+ ave_time_load = statistics.mean(time_load)
550
+ ave_time_infer = statistics.mean(time_infer)
551
+ if len(time_load) > 1 and len(time_infer) > 1:
552
+ std_dev_time_load = humanfriendly.format_timespan(statistics.stdev(time_load))
553
+ std_dev_time_infer = humanfriendly.format_timespan(statistics.stdev(time_infer))
554
+ else:
555
+ std_dev_time_load = 'not available'
556
+ std_dev_time_infer = 'not available'
557
+ print('On average, for each image,')
558
+ print('- loading took {}, std dev is {}'.format(humanfriendly.format_timespan(ave_time_load),
559
+ std_dev_time_load))
560
+ print('- inference took {}, std dev is {}'.format(humanfriendly.format_timespan(ave_time_infer),
561
+ std_dev_time_infer))
562
+
563
+ # ...def load_and_run_detector()
564
+
565
+
566
+ def download_model(model_name,force_download=False):
567
+ """
568
+ Downloads one of the known models to local temp space if it hasn't already been downloaded.
569
+
570
+ Args:
571
+ model_name (str): a known model string, e.g. "MDV5A"
572
+ force_download (bool, optional): whether download the model even if the local target
573
+ file already exists
574
+ """
575
+
576
+ model_tempdir = os.path.join(tempfile.gettempdir(), 'megadetector_models')
577
+ os.makedirs(model_tempdir,exist_ok=True)
578
+
579
+ # This is a lazy fix to an issue... if multiple users run this script, the
580
+ # "megadetector_models" folder is owned by the first person who creates it, and others
581
+ # can't write to it. I could create uniquely-named folders, but I philosophically prefer
582
+ # to put all the individual UUID-named folders within a larger folder, so as to be a
583
+ # good tempdir citizen. So, the lazy fix is to make this world-writable.
584
+ try:
585
+ os.chmod(model_tempdir,0o777)
586
+ except Exception:
587
+ pass
588
+ if model_name not in downloadable_models:
589
+ print('Unrecognized downloadable model {}'.format(model_name))
590
+ return None
591
+ url = downloadable_models[model_name]
592
+ destination_filename = os.path.join(model_tempdir,url.split('/')[-1])
593
+ local_file = download_url(url, destination_filename=destination_filename, progress_updater=None,
594
+ force_download=force_download, verbose=True)
595
+ return local_file
596
+
597
+
598
+ def try_download_known_detector(detector_file):
599
+ """
600
+ Checks whether detector_file is really the name of a known model, in which case we will
601
+ either read the actual filename from the corresponding environment variable or download
602
+ (if necessary) to local temp space. Otherwise just returns the input string.
603
+
604
+ Args:
605
+ detector_file (str): a known model string (e.g. "MDV5A"), or any other string (in which
606
+ case this function is a no-op)
607
+
608
+ Returns:
609
+ str: the local filename to which the model was downloaded, or the same string that
610
+ was passed in, if it's not recognized as a well-known model name
611
+ """
612
+
613
+ if detector_file in downloadable_models:
614
+ if detector_file in os.environ:
615
+ fn = os.environ[detector_file]
616
+ print('Reading MD location from environment variable {}: {}'.format(
617
+ detector_file,fn))
618
+ detector_file = fn
619
+ else:
620
+ print('Downloading model {}'.format(detector_file))
621
+ detector_file = download_model(detector_file)
622
+ return detector_file
623
+
624
+
625
+
626
+ #%% Command-line driver
627
+
628
+ def main():
629
+
630
+ parser = argparse.ArgumentParser(
631
+ description='Module to run an animal detection model on images')
632
+
633
+ parser.add_argument(
634
+ 'detector_file',
635
+ help='Path detector model file (.pb or .pt). Can also be MDV4, MDV5A, or MDV5B to request automatic download.')
636
+
637
+ # Must specify either an image file or a directory
638
+ group = parser.add_mutually_exclusive_group(required=True)
639
+ group.add_argument(
640
+ '--image_file',
641
+ type=str,
642
+ default=None,
643
+ help='Single file to process, mutually exclusive with --image_dir')
644
+ group.add_argument(
645
+ '--image_dir',
646
+ type=str,
647
+ default=None,
648
+ help='Directory to search for images, with optional recursion by adding --recursive')
649
+
650
+ parser.add_argument(
651
+ '--recursive',
652
+ action='store_true',
653
+ help='Recurse into directories, only meaningful if using --image_dir')
654
+
655
+ parser.add_argument(
656
+ '--output_dir',
657
+ type=str,
658
+ default=None,
659
+ help='Directory for output images (defaults to same as input)')
660
+
661
+ parser.add_argument(
662
+ '--image_size',
663
+ type=int,
664
+ default=None,
665
+ help=('Force image resizing to a (square) integer size (not recommended to change this)'))
666
+
667
+ parser.add_argument(
668
+ '--threshold',
669
+ type=float,
670
+ default=DEFAULT_RENDERING_CONFIDENCE_THRESHOLD,
671
+ help=('Confidence threshold between 0 and 1.0; only render' +
672
+ ' boxes above this confidence (defaults to {})'.format(
673
+ DEFAULT_RENDERING_CONFIDENCE_THRESHOLD)))
674
+
675
+ parser.add_argument(
676
+ '--crop',
677
+ default=False,
678
+ action='store_true',
679
+ help=('If set, produces separate output images for each crop, '
680
+ 'rather than adding bounding boxes to the original image'))
681
+
682
+ parser.add_argument(
683
+ '--box_thickness',
684
+ type=int,
685
+ default=DEFAULT_BOX_THICKNESS,
686
+ help=('Line width (in pixels) for box rendering (defaults to {})'.format(
687
+ DEFAULT_BOX_THICKNESS)))
688
+
689
+ parser.add_argument(
690
+ '--box_expansion',
691
+ type=int,
692
+ default=DEFAULT_BOX_EXPANSION,
693
+ help=('Number of pixels to expand boxes by (defaults to {})'.format(
694
+ DEFAULT_BOX_EXPANSION)))
695
+
696
+ parser.add_argument(
697
+ '--label_font_size',
698
+ type=int,
699
+ default=DEFAULT_LABEL_FONT_SIZE,
700
+ help=('Label font size (defaults to {})'.format(
701
+ DEFAULT_LABEL_FONT_SIZE)))
702
+
703
+ parser.add_argument(
704
+ '--process_likely_output_images',
705
+ action='store_true',
706
+ help=('By default, we skip images that end in {}, because they probably came from this script. '\
707
+ .format(DETECTION_FILENAME_INSERT) + \
708
+ 'This option disables that behavior.'))
709
+
710
+ if len(sys.argv[1:]) == 0:
711
+ parser.print_help()
712
+ parser.exit()
713
+
714
+ args = parser.parse_args()
715
+
716
+ # If the specified detector file is really the name of a known model, find
717
+ # (and possibly download) that model
718
+ args.detector_file = try_download_known_detector(args.detector_file)
719
+
720
+ assert os.path.exists(args.detector_file), 'detector file {} does not exist'.format(
721
+ args.detector_file)
722
+ assert 0.0 < args.threshold <= 1.0, 'Confidence threshold needs to be between 0 and 1'
723
+
724
+ if args.image_file:
725
+ image_file_names = [args.image_file]
726
+ else:
727
+ image_file_names = path_utils.find_images(args.image_dir, args.recursive)
728
+
729
+ # Optionally skip images that were probably generated by this script
730
+ if not args.process_likely_output_images:
731
+ image_file_names_valid = []
732
+ for fn in image_file_names:
733
+ if os.path.splitext(fn)[0].endswith(DETECTION_FILENAME_INSERT):
734
+ print('Skipping likely output image {}'.format(fn))
735
+ else:
736
+ image_file_names_valid.append(fn)
737
+ image_file_names = image_file_names_valid
738
+
739
+ print('Running detector on {} images...'.format(len(image_file_names)))
740
+
741
+ if args.output_dir:
742
+ os.makedirs(args.output_dir, exist_ok=True)
743
+ else:
744
+ if args.image_dir:
745
+ args.output_dir = args.image_dir
746
+ else:
747
+ # but for a single image, args.image_dir is also None
748
+ args.output_dir = os.path.dirname(args.image_file)
749
+
750
+ load_and_run_detector(model_file=args.detector_file,
751
+ image_file_names=image_file_names,
752
+ output_dir=args.output_dir,
753
+ render_confidence_threshold=args.threshold,
754
+ box_thickness=args.box_thickness,
755
+ box_expansion=args.box_expansion,
756
+ crop_images=args.crop,
757
+ image_size=args.image_size,
758
+ label_font_size=args.label_font_size)
759
+
760
+ if __name__ == '__main__':
761
+ main()
762
+
763
+
764
+ #%% Interactive driver
765
+
766
+ if False:
767
+
768
+ #%%
769
+ model_file = r'c:\temp\models\md_v4.1.0.pb'
770
+ image_file_names = path_utils.find_images(r'c:\temp\demo_images\ssverymini')
771
+ output_dir = r'c:\temp\demo_images\ssverymini'
772
+ render_confidence_threshold = 0.8
773
+ crop_images = True
774
+
775
+ load_and_run_detector(model_file=model_file,
776
+ image_file_names=image_file_names,
777
+ output_dir=output_dir,
778
+ render_confidence_threshold=render_confidence_threshold,
779
+ crop_images=crop_images)