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,1164 @@
1
+ """
2
+
3
+ md_tests.py
4
+
5
+ A series of tests to validate basic repo functionality and verify either "correct"
6
+ inference behavior, or - when operating in environments other than the training
7
+ environment - acceptable deviation from the correct results.
8
+
9
+ This module should not depend on anything else in this repo outside of the
10
+ tests themselves, even if it means some duplicated code (e.g. for downloading files),
11
+ since much of what it tries to test is, e.g., imports.
12
+
13
+ """
14
+
15
+ #%% Imports and constants
16
+
17
+ ### Only standard imports belong here, not MD-specific imports ###
18
+
19
+ import os
20
+ import json
21
+ import glob
22
+ import tempfile
23
+ import urllib
24
+ import urllib.request
25
+ import zipfile
26
+ import subprocess
27
+ import argparse
28
+ import inspect
29
+
30
+
31
+ #%% Classes
32
+
33
+ class MDTestOptions:
34
+ """
35
+ Options controlling test behavior
36
+ """
37
+
38
+ def __init__(self):
39
+
40
+ ## Required ##
41
+
42
+ #: Force CPU execution
43
+ self.disable_gpu = False
44
+
45
+ #: If GPU execution is requested, but a GPU is not available, should we error?
46
+ self.cpu_execution_is_error = False
47
+
48
+ #: Skip tests related to video processing
49
+ self.skip_video_tests = False
50
+
51
+ #: Skip tests launched via Python functions (as opposed to CLIs)
52
+ self.skip_python_tests = False
53
+
54
+ #: Skip CLI tests
55
+ self.skip_cli_tests = False
56
+
57
+ #: Force a specific folder for temporary input/output
58
+ self.scratch_dir = None
59
+
60
+ #: Where does the test data live?
61
+ self.test_data_url = 'https://lila.science/public/md-test-package.zip'
62
+
63
+ #: Download test data even if it appears to have already been downloaded
64
+ self.force_data_download = False
65
+
66
+ #: Unzip test data even if it appears to have already been unzipped
67
+ self.force_data_unzip = False
68
+
69
+ #: By default, any unexpected behavior is an error; this forces most errors to
70
+ #: be treated as warnings.
71
+ self.warning_mode = False
72
+
73
+ #: How much deviation from the expected detection coordinates should we allow before
74
+ #: a disrepancy becomes an error?
75
+ self.max_coord_error = 0.001
76
+
77
+ #: How much deviation from the expected confidence values should we allow before
78
+ #: a disrepancy becomes an error?
79
+ self.max_conf_error = 0.005
80
+
81
+ #: Current working directory when running CLI tests
82
+ self.cli_working_dir = None
83
+
84
+ #: YOLOv5 installation, only relevant if we're testing run_inference_with_yolov5_val.
85
+ #:
86
+ #: If this is None, we'll skip that test.
87
+ self.yolo_working_dir = None
88
+
89
+ #: fourcc code to use for video tests that involve rendering video
90
+ self.video_fourcc = 'mp4v'
91
+
92
+ #: Default model to use for testing (filename, URL, or well-known model string)
93
+ self.default_model = 'MDV5A'
94
+
95
+ #: For comparison tests, use a model that produces slightly different output
96
+ self.alt_model = 'MDV5B'
97
+
98
+ #: PYTHONPATH to set for CLI tests; if None, inherits from the parent process. Only
99
+ #: impacts the called functions, not the parent process.
100
+ self.cli_test_pythonpath = None
101
+
102
+ # ...class MDTestOptions()
103
+
104
+
105
+ #%% Support functions
106
+
107
+ def get_expected_results_filename(gpu_is_available):
108
+ """
109
+ Expected results vary just a little across inference environments, particularly
110
+ between PT 1.x and 2.x, so when making sure things are working acceptably, we
111
+ compare to a reference file that matches the current environment.
112
+
113
+ This function gets the correct filename to compare to current results, depending
114
+ on whether a GPU is available.
115
+
116
+ Args:
117
+ gpu_is_available (bool): whether a GPU is available
118
+
119
+ Returns:
120
+ str: relative filename of the results file we should use (within the test
121
+ data zipfile)
122
+ """
123
+
124
+ if gpu_is_available:
125
+ hw_string = 'gpu'
126
+ else:
127
+ hw_string = 'cpu'
128
+ import torch
129
+ torch_version = str(torch.__version__)
130
+ if torch_version.startswith('1'):
131
+ assert torch_version == '1.10.1', 'Only tested against PT 1.10.1 and PT 2.x'
132
+ pt_string = 'pt1.10.1'
133
+ else:
134
+ assert torch_version.startswith('2'), 'Unknown torch version: {}'.format(torch_version)
135
+ pt_string = 'pt2.x'
136
+
137
+ # A hack for now to account for the fact that even with acceleration enabled and PT2
138
+ # installed, Apple silicon appears to provide the same results as CPU/PT1 inference
139
+ try:
140
+ import torch
141
+ m1_inference = torch.backends.mps.is_built and torch.backends.mps.is_available()
142
+ if m1_inference:
143
+ print('I appear to be running on M1/M2 hardware')
144
+ hw_string = 'cpu'
145
+ pt_string = 'pt1.10.1'
146
+ except Exception:
147
+ pass
148
+
149
+ return 'md-test-results-{}-{}.json'.format(hw_string,pt_string)
150
+
151
+
152
+ def download_test_data(options=None):
153
+ """
154
+ Downloads the test zipfile if necessary, unzips if necessary.
155
+
156
+ Args:
157
+ options (MDTestOptions, optional): see MDTestOptions for details
158
+
159
+ Returns:
160
+ MDTestOptions: the same object passed in as input, or the options that
161
+ were used if [options] was supplied as None
162
+ """
163
+
164
+ if options is None:
165
+ options = MDTestOptions()
166
+
167
+ if options.scratch_dir is None:
168
+ tempdir_base = tempfile.gettempdir()
169
+ scratch_dir = os.path.join(tempdir_base,'md-tests')
170
+ else:
171
+ scratch_dir = options.scratch_dir
172
+
173
+ os.makedirs(scratch_dir,exist_ok=True)
174
+
175
+ # See whether we've already downloaded the data zipfile
176
+ download_zipfile = True
177
+ if not options.force_data_download:
178
+ local_zipfile = os.path.join(scratch_dir,options.test_data_url.split('/')[-1])
179
+ if os.path.isfile(local_zipfile):
180
+ url_info = urllib.request.urlopen(options.test_data_url).info()
181
+ remote_size = int(url_info['Content-Length'])
182
+ target_file_size = os.path.getsize(local_zipfile)
183
+ if remote_size == target_file_size:
184
+ download_zipfile = False
185
+
186
+ if download_zipfile:
187
+ print('Downloading test data zipfile')
188
+ urllib.request.urlretrieve(options.test_data_url, local_zipfile)
189
+ print('Finished download to {}'.format(local_zipfile))
190
+ else:
191
+ print('Bypassing test data zipfile download for {}'.format(local_zipfile))
192
+
193
+
194
+ ## Unzip data
195
+
196
+ zipf = zipfile.ZipFile(local_zipfile)
197
+ zip_contents = zipf.filelist
198
+
199
+ # file_info = zip_contents[1]
200
+ for file_info in zip_contents:
201
+
202
+ expected_size = file_info.file_size
203
+ if expected_size == 0:
204
+ continue
205
+ fn_relative = file_info.filename
206
+ target_file = os.path.join(scratch_dir,fn_relative)
207
+ unzip_file = True
208
+ if (not options.force_data_unzip) and os.path.isfile(target_file):
209
+ existing_file_size = os.path.getsize(target_file)
210
+ if existing_file_size == expected_size:
211
+ unzip_file = False
212
+ if unzip_file:
213
+ os.makedirs(os.path.dirname(target_file),exist_ok=True)
214
+ with open(target_file,'wb') as f:
215
+ f.write(zipf.read(fn_relative))
216
+
217
+ # ...for each file in the zipfile
218
+
219
+ # Warn if file are present that aren't expected
220
+ test_files = glob.glob(os.path.join(scratch_dir,'**/*'), recursive=True)
221
+ test_files = [os.path.relpath(fn,scratch_dir).replace('\\','/') for fn in test_files]
222
+ test_files_set = set(test_files)
223
+ expected_images_set = set(zipf.namelist())
224
+ for fn in expected_images_set:
225
+ if fn.endswith('/'):
226
+ continue
227
+ assert fn in test_files_set, 'File {} is missing from the test image folder'.format(fn)
228
+
229
+ # Populate the test options with test data information
230
+ options.scratch_dir = scratch_dir
231
+ options.all_test_files = test_files
232
+ options.test_images = [fn for fn in test_files if os.path.splitext(fn.lower())[1] in ('.jpg','.jpeg','.png')]
233
+ options.test_videos = [fn for fn in test_files if os.path.splitext(fn.lower())[1] in ('.mp4','.avi')]
234
+ options.test_videos = [fn for fn in options.test_videos if 'rendered' not in fn]
235
+ options.test_videos = [fn for fn in options.test_videos if \
236
+ os.path.isfile(os.path.join(scratch_dir,fn))]
237
+
238
+ print('Finished unzipping and enumerating test data')
239
+
240
+ return options
241
+
242
+ # ...def download_test_data(...)
243
+
244
+
245
+ def is_gpu_available(verbose=True):
246
+ """
247
+ Checks whether a GPU (including M1/M2 MPS) is available.
248
+
249
+ Args:
250
+ verbose (bool, optional): enable additional debug console output
251
+
252
+ Returns:
253
+ bool: whether a GPU is available
254
+ """
255
+
256
+ # Import torch inside this function, so we have a chance to set CUDA_VISIBLE_DEVICES
257
+ # before checking GPU availability.
258
+ import torch
259
+ gpu_available = torch.cuda.is_available()
260
+
261
+ if gpu_available:
262
+ if verbose:
263
+ print('CUDA available: {}'.format(gpu_available))
264
+ device_ids = list(range(torch.cuda.device_count()))
265
+ if len(device_ids) > 1:
266
+ print('Found multiple devices: {}'.format(str(device_ids)))
267
+ else:
268
+ try:
269
+ gpu_available = torch.backends.mps.is_built and torch.backends.mps.is_available()
270
+ except AttributeError:
271
+ pass
272
+ if gpu_available:
273
+ print('Metal performance shaders available')
274
+
275
+ if not gpu_available:
276
+ print('No GPU available')
277
+
278
+ return gpu_available
279
+
280
+ # ...def is_gpu_available(...)
281
+
282
+
283
+ def output_files_are_identical(fn1,fn2,verbose=False):
284
+ """
285
+ Checks whether two MD-formatted output files are identical other than file sorting.
286
+
287
+ Args:
288
+ fn1 (str): the first filename to compare
289
+ fn2 (str): the second filename to compare
290
+
291
+ Returns:
292
+ bool: whether [fn1] and [fn2] are identical other than file sorting.
293
+ """
294
+
295
+ if verbose:
296
+ print('Comparing {} to {}'.format(fn1,fn2))
297
+
298
+ with open(fn1,'r') as f:
299
+ fn1_results = json.load(f)
300
+ fn1_results['images'] = \
301
+ sorted(fn1_results['images'], key=lambda d: d['file'])
302
+
303
+ with open(fn2,'r') as f:
304
+ fn2_results = json.load(f)
305
+ fn2_results['images'] = \
306
+ sorted(fn2_results['images'], key=lambda d: d['file'])
307
+
308
+ if len(fn1_results['images']) != len(fn1_results['images']):
309
+ if verbose:
310
+ print('{} images in {}, {} images in {}'.format(
311
+ len(fn1_results['images']),fn1,
312
+ len(fn2_results['images']),fn2))
313
+ return False
314
+
315
+ for i_image,fn1_image in enumerate(fn1_results['images']):
316
+
317
+ fn2_image = fn2_results['images'][i_image]
318
+
319
+ if fn1_image['file'] != fn2_image['file']:
320
+ if verbose:
321
+ print('Filename difference: {} vs {} '.format(fn1_image['file'],fn1_image['file']))
322
+ return False
323
+
324
+ if fn1_image != fn2_image:
325
+ if verbose:
326
+ print('Image-level difference in image {}'.format(fn1_image['file']))
327
+ return False
328
+
329
+ return True
330
+
331
+ # ...def output_files_are_identical(...)
332
+
333
+
334
+ def _args_to_object(args, obj):
335
+ """
336
+ Copies all fields from a Namespace (typically the output from parse_args) to an
337
+ object. Skips fields starting with _. Does not check existence in the target
338
+ object.
339
+
340
+ Args:
341
+ args (argparse.Namespace): the namespace to convert to an object
342
+ obj (object): object whose whose attributes will be updated
343
+
344
+ Returns:
345
+ object: the modified object (modified in place, but also returned)
346
+ """
347
+
348
+ for n, v in inspect.getmembers(args):
349
+ if not n.startswith('_'):
350
+ setattr(obj, n, v)
351
+
352
+ return obj
353
+
354
+
355
+ #%% CLI functions
356
+
357
+ # These are copied from process_utils.py to avoid imports outside of the test
358
+ # functions.
359
+
360
+ os.environ["PYTHONUNBUFFERED"] = "1"
361
+
362
+ def execute(cmd):
363
+ """
364
+ Runs [cmd] (a single string) in a shell, yielding each line of output to the caller.
365
+
366
+ Args:
367
+ cmd (str): command to run
368
+
369
+ Returns:
370
+ int: the command's return code, always zero, otherwise a CalledProcessError is raised
371
+ """
372
+
373
+ # https://stackoverflow.com/questions/4417546/constantly-print-subprocess-output-while-process-is-running
374
+ popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
375
+ shell=True, universal_newlines=True)
376
+ for stdout_line in iter(popen.stdout.readline, ""):
377
+ yield stdout_line
378
+ popen.stdout.close()
379
+ return_code = popen.wait()
380
+ if return_code:
381
+ raise subprocess.CalledProcessError(return_code, cmd)
382
+ return return_code
383
+
384
+
385
+ def execute_and_print(cmd,print_output=True,catch_exceptions=False):
386
+ """
387
+ Runs [cmd] (a single string) in a shell, capturing (and optionally printing) output.
388
+
389
+ Args:
390
+ cmd (str): command to run
391
+ print_output (bool, optional): whether to print output from [cmd]
392
+
393
+ Returns:
394
+ dict: a dictionary with fields "status" (the process return code) and "output"
395
+ (the content of stdout)
396
+ """
397
+
398
+ to_return = {'status':'unknown','output':''}
399
+ output=[]
400
+ try:
401
+ for s in execute(cmd):
402
+ output.append(s)
403
+ if print_output:
404
+ print(s,end='',flush=True)
405
+ to_return['status'] = 0
406
+ except subprocess.CalledProcessError as cpe:
407
+ if not catch_exceptions:
408
+ raise
409
+ print('execute_and_print caught error: {}'.format(cpe.output))
410
+ to_return['status'] = cpe.returncode
411
+ to_return['output'] = output
412
+
413
+ return to_return
414
+
415
+
416
+ #%% Python tests
417
+
418
+ def run_python_tests(options):
419
+ """
420
+ Runs Python-based (as opposed to CLI-based) package tests.
421
+
422
+ Args:
423
+ options (MDTestOptions): see MDTestOptions for details
424
+ """
425
+
426
+ print('\n*** Starting module tests ***\n')
427
+
428
+ ## Prepare data
429
+
430
+ download_test_data(options)
431
+
432
+
433
+ ## Run inference on an image
434
+
435
+ from megadetector.detection import run_detector
436
+ from megadetector.visualization import visualization_utils as vis_utils
437
+ image_fn = os.path.join(options.scratch_dir,options.test_images[0])
438
+ model = run_detector.load_detector(options.default_model)
439
+ pil_im = vis_utils.load_image(image_fn)
440
+ result = model.generate_detections_one_image(pil_im) # noqa
441
+
442
+
443
+ ## Run inference on a folder
444
+
445
+ from megadetector.detection.run_detector_batch import load_and_run_detector_batch,write_results_to_file
446
+ from megadetector.utils import path_utils
447
+
448
+ image_folder = os.path.join(options.scratch_dir,'md-test-images')
449
+ assert os.path.isdir(image_folder), 'Test image folder {} is not available'.format(image_folder)
450
+ inference_output_file = os.path.join(options.scratch_dir,'folder_inference_output.json')
451
+ image_file_names = path_utils.find_images(image_folder,recursive=True)
452
+ results = load_and_run_detector_batch(options.default_model, image_file_names, quiet=True)
453
+ _ = write_results_to_file(results,inference_output_file,
454
+ relative_path_base=image_folder,detector_file=options.default_model)
455
+
456
+ # Read results
457
+ with open(inference_output_file,'r') as f:
458
+ results_from_file = json.load(f) # noqa
459
+
460
+
461
+ ## Verify results
462
+
463
+ # Read expected results
464
+ expected_results_filename = get_expected_results_filename(is_gpu_available(verbose=False))
465
+
466
+ with open(os.path.join(options.scratch_dir,expected_results_filename),'r') as f:
467
+ expected_results = json.load(f)
468
+
469
+ filename_to_results = {im['file'].replace('\\','/'):im for im in results_from_file['images']}
470
+ filename_to_results_expected = {im['file'].replace('\\','/'):im for im in expected_results['images']}
471
+
472
+ assert len(filename_to_results) == len(filename_to_results_expected), \
473
+ 'Error: expected {} files in results, found {}'.format(
474
+ len(filename_to_results_expected),
475
+ len(filename_to_results))
476
+
477
+ max_coord_error = 0
478
+ max_conf_error = 0
479
+
480
+ # fn = next(iter(filename_to_results.keys()))
481
+ for fn in filename_to_results.keys():
482
+
483
+ actual_image_results = filename_to_results[fn]
484
+ expected_image_results = filename_to_results_expected[fn]
485
+
486
+ if 'failure' in actual_image_results:
487
+ assert 'failure' in expected_image_results and \
488
+ 'detections' not in actual_image_results and \
489
+ 'detections' not in expected_image_results
490
+ continue
491
+ assert 'failure' not in expected_image_results
492
+
493
+ actual_detections = actual_image_results['detections']
494
+ expected_detections = expected_image_results['detections']
495
+
496
+ s = 'expected {} detections for file {}, found {}'.format(
497
+ len(expected_detections),fn,len(actual_detections))
498
+ s += '\nExpected results file: {}\nActual results file: {}'.format(
499
+ expected_results_filename,inference_output_file)
500
+
501
+ if options.warning_mode:
502
+ if len(actual_detections) != len(expected_detections):
503
+ print('Warning: {}'.format(s))
504
+ continue
505
+ assert len(actual_detections) == len(expected_detections), \
506
+ 'Error: {}'.format(s)
507
+
508
+ # i_det = 0
509
+ for i_det in range(0,len(actual_detections)):
510
+ actual_det = actual_detections[i_det]
511
+ expected_det = expected_detections[i_det]
512
+ assert actual_det['category'] == expected_det['category']
513
+ conf_err = abs(actual_det['conf'] - expected_det['conf'])
514
+ coord_differences = []
515
+ for i_coord in range(0,4):
516
+ coord_differences.append(abs(actual_det['bbox'][i_coord]-expected_det['bbox'][i_coord]))
517
+ coord_err = max(coord_differences)
518
+
519
+ if conf_err > max_conf_error:
520
+ max_conf_error = conf_err
521
+ if coord_err > max_coord_error:
522
+ max_coord_error = coord_err
523
+
524
+ # ...for each detection
525
+
526
+ # ...for each image
527
+
528
+ if not options.warning_mode:
529
+
530
+ assert max_conf_error <= options.max_conf_error, \
531
+ 'Confidence error {} is greater than allowable ({})'.format(
532
+ max_conf_error,options.max_conf_error)
533
+
534
+ assert max_coord_error <= options.max_coord_error, \
535
+ 'Coord error {} is greater than allowable ({})'.format(
536
+ max_coord_error,options.max_coord_error)
537
+
538
+ print('Max conf error: {}'.format(max_conf_error))
539
+ print('Max coord error: {}'.format(max_coord_error))
540
+
541
+
542
+ ## Postprocess results
543
+
544
+ from megadetector.postprocessing.postprocess_batch_results import \
545
+ PostProcessingOptions,process_batch_results
546
+ postprocessing_options = PostProcessingOptions()
547
+
548
+ postprocessing_options.md_results_file = inference_output_file
549
+ postprocessing_options.output_dir = os.path.join(options.scratch_dir,'postprocessing_output')
550
+ postprocessing_options.image_base_dir = image_folder
551
+
552
+ postprocessing_results = process_batch_results(postprocessing_options)
553
+ assert os.path.isfile(postprocessing_results.output_html_file), \
554
+ 'Postprocessing output file {} not found'.format(postprocessing_results.output_html_file)
555
+
556
+
557
+ ## Partial RDE test
558
+
559
+ from megadetector.postprocessing.repeat_detection_elimination.repeat_detections_core import \
560
+ RepeatDetectionOptions, find_repeat_detections
561
+
562
+ rde_options = RepeatDetectionOptions()
563
+ rde_options.occurrenceThreshold = 2
564
+ rde_options.confidenceMin = 0.001
565
+ rde_options.outputBase = os.path.join(options.scratch_dir,'rde_working_dir')
566
+ rde_options.imageBase = image_folder
567
+ rde_output_file = inference_output_file.replace('.json','_filtered.json')
568
+ assert rde_output_file != inference_output_file
569
+ rde_results = find_repeat_detections(inference_output_file, rde_output_file, rde_options)
570
+ assert os.path.isfile(rde_results.filterFile),\
571
+ 'Could not find RDE output file {}'.format(rde_results.filterFile)
572
+
573
+
574
+ ## Run inference on a folder (with YOLOv5 val script)
575
+
576
+ if options.yolo_working_dir is None:
577
+
578
+ print('Skipping YOLO val inference tests, no YOLO folder supplied')
579
+
580
+ else:
581
+
582
+ from megadetector.detection.run_inference_with_yolov5_val import \
583
+ YoloInferenceOptions, run_inference_with_yolo_val
584
+
585
+ inference_output_file_yolo_val = os.path.join(options.scratch_dir,'folder_inference_output_yolo_val.json')
586
+
587
+ yolo_inference_options = YoloInferenceOptions()
588
+ yolo_inference_options.input_folder = os.path.join(options.scratch_dir,'md-test-images')
589
+ yolo_inference_options.output_file = inference_output_file_yolo_val
590
+ yolo_inference_options.yolo_working_folder = options.yolo_working_dir
591
+ yolo_inference_options.model_filename = options.default_model
592
+ yolo_inference_options.augment = False
593
+ yolo_inference_options.overwrite_handling = 'overwrite'
594
+
595
+ run_inference_with_yolo_val(yolo_inference_options)
596
+
597
+ # Run again, without symlinks this time
598
+
599
+ from megadetector.utils.path_utils import insert_before_extension
600
+ inference_output_file_yolo_val_no_links = insert_before_extension(inference_output_file_yolo_val,
601
+ 'no-links')
602
+ yolo_inference_options.output_file = inference_output_file_yolo_val_no_links
603
+ yolo_inference_options.use_symlinks = False
604
+ run_inference_with_yolo_val(yolo_inference_options)
605
+
606
+ # Run again, with chunked inference and symlinks
607
+
608
+ inference_output_file_yolo_val_checkpoints = insert_before_extension(inference_output_file_yolo_val,
609
+ 'checkpoints')
610
+ yolo_inference_options.output_file = inference_output_file_yolo_val_checkpoints
611
+ yolo_inference_options.use_symlinks = True
612
+ yolo_inference_options.checkpoint_frequency = 5
613
+ run_inference_with_yolo_val(yolo_inference_options)
614
+
615
+ # Run again, with chunked inference and no symlinks
616
+
617
+ inference_output_file_yolo_val_checkpoints_no_links = \
618
+ insert_before_extension(inference_output_file_yolo_val,'checkpoints-no-links')
619
+ yolo_inference_options.output_file = inference_output_file_yolo_val_checkpoints_no_links
620
+ yolo_inference_options.use_symlinks = False
621
+ yolo_inference_options.checkpoint_frequency = 5
622
+ run_inference_with_yolo_val(yolo_inference_options)
623
+
624
+ fn1 = inference_output_file_yolo_val
625
+
626
+ output_files_to_compare = [
627
+ inference_output_file_yolo_val_no_links,
628
+ inference_output_file_yolo_val_checkpoints,
629
+ inference_output_file_yolo_val_checkpoints_no_links
630
+ ]
631
+
632
+ for fn2 in output_files_to_compare:
633
+ assert output_files_are_identical(fn1, fn2, verbose=True)
634
+
635
+ # ...if we need to run the YOLO val inference tests
636
+
637
+
638
+ if not options.skip_video_tests:
639
+
640
+ ## Video test (single video)
641
+
642
+ from megadetector.detection.process_video import ProcessVideoOptions, process_video
643
+
644
+ video_options = ProcessVideoOptions()
645
+ video_options.model_file = options.default_model
646
+ video_options.input_video_file = os.path.join(options.scratch_dir,options.test_videos[0])
647
+ video_options.output_json_file = os.path.join(options.scratch_dir,'single_video_output.json')
648
+ video_options.output_video_file = os.path.join(options.scratch_dir,'video_scratch/rendered_video.mp4')
649
+ video_options.frame_folder = os.path.join(options.scratch_dir,'video_scratch/frame_folder')
650
+ video_options.frame_rendering_folder = os.path.join(options.scratch_dir,'video_scratch/rendered_frame_folder')
651
+ video_options.render_output_video = True
652
+ # video_options.keep_rendered_frames = False
653
+ # video_options.keep_rendered_frames = False
654
+ video_options.force_extracted_frame_folder_deletion = True
655
+ video_options.force_rendered_frame_folder_deletion = True
656
+ # video_options.reuse_results_if_available = False
657
+ # video_options.reuse_frames_if_available = False
658
+ video_options.recursive = True
659
+ video_options.verbose = False
660
+ video_options.fourcc = options.video_fourcc
661
+ # video_options.rendering_confidence_threshold = None
662
+ # video_options.json_confidence_threshold = 0.005
663
+ video_options.frame_sample = 5
664
+ video_options.n_cores = 5
665
+ # video_options.debug_max_frames = -1
666
+ # video_options.class_mapping_filename = None
667
+
668
+ _ = process_video(video_options)
669
+
670
+ assert os.path.isfile(video_options.output_video_file), \
671
+ 'Python video test failed to render output video file'
672
+ assert os.path.isfile(video_options.output_json_file), \
673
+ 'Python video test failed to render output .json file'
674
+
675
+
676
+ ## Video test (folder)
677
+
678
+ from megadetector.detection.process_video import ProcessVideoOptions, process_video_folder
679
+
680
+ video_options = ProcessVideoOptions()
681
+ video_options.model_file = options.default_model
682
+ video_options.input_video_file = os.path.join(options.scratch_dir,
683
+ os.path.dirname(options.test_videos[0]))
684
+ video_options.output_json_file = os.path.join(options.scratch_dir,'video_folder_output.json')
685
+ # video_options.output_video_file = None
686
+ video_options.frame_folder = os.path.join(options.scratch_dir,'video_scratch/frame_folder')
687
+ video_options.frame_rendering_folder = os.path.join(options.scratch_dir,'video_scratch/rendered_frame_folder')
688
+ video_options.render_output_video = False
689
+ # video_options.keep_rendered_frames = False
690
+ # video_options.keep_rendered_frames = False
691
+ video_options.force_extracted_frame_folder_deletion = True
692
+ video_options.force_rendered_frame_folder_deletion = True
693
+ # video_options.reuse_results_if_available = False
694
+ # video_options.reuse_frames_if_available = False
695
+ video_options.recursive = True
696
+ video_options.verbose = True
697
+ video_options.fourcc = options.video_fourcc
698
+ # video_options.rendering_confidence_threshold = None
699
+ # video_options.json_confidence_threshold = 0.005
700
+ video_options.frame_sample = 5
701
+ video_options.n_cores = 5
702
+ # video_options.debug_max_frames = -1
703
+ # video_options.class_mapping_filename = None
704
+
705
+ _ = process_video_folder(video_options)
706
+
707
+ assert os.path.isfile(video_options.output_json_file), \
708
+ 'Python video test failed to render output .json file'
709
+
710
+ # ...if we're not skipping video tests
711
+
712
+ print('\n*** Finished module tests ***\n')
713
+
714
+ # ...def run_python_tests(...)
715
+
716
+
717
+ #%% Command-line tests
718
+
719
+ def run_cli_tests(options):
720
+ """
721
+ Runs CLI (as opposed to Python-based) package tests.
722
+
723
+ Args:
724
+ options (MDTestOptions): see MDTestOptions for details
725
+ """
726
+
727
+ print('\n*** Starting CLI tests ***\n')
728
+
729
+
730
+ ## Environment management
731
+
732
+ if options.cli_test_pythonpath is not None:
733
+ os.environ['PYTHONPATH'] = options.cli_test_pythonpath
734
+
735
+
736
+ ## chdir if necessary
737
+
738
+ if options.cli_working_dir is not None:
739
+ os.chdir(options.cli_working_dir)
740
+
741
+
742
+ ## Prepare data
743
+
744
+ download_test_data(options)
745
+
746
+
747
+ ## Run inference on an image
748
+
749
+ image_fn = os.path.join(options.scratch_dir,options.test_images[0])
750
+ output_dir = os.path.join(options.scratch_dir,'single_image_test')
751
+ if options.cli_working_dir is None:
752
+ cmd = 'python -m megadetector.detection.run_detector'
753
+ else:
754
+ cmd = 'python megadetector/detection/run_detector.py'
755
+ cmd += ' "{}" --image_file "{}" --output_dir "{}"'.format(
756
+ options.default_model,image_fn,output_dir)
757
+ print('Running: {}'.format(cmd))
758
+ cmd_results = execute_and_print(cmd)
759
+
760
+ if options.cpu_execution_is_error:
761
+ gpu_available_via_cli = False
762
+ for s in cmd_results['output']:
763
+ if 'GPU available: True' in s:
764
+ gpu_available_via_cli = True
765
+ break
766
+ if not gpu_available_via_cli:
767
+ raise Exception('GPU execution is required, but not available')
768
+
769
+
770
+ ## Run inference on a folder
771
+
772
+ image_folder = os.path.join(options.scratch_dir,'md-test-images')
773
+ assert os.path.isdir(image_folder), 'Test image folder {} is not available'.format(image_folder)
774
+ inference_output_file = os.path.join(options.scratch_dir,'folder_inference_output.json')
775
+ if options.cli_working_dir is None:
776
+ cmd = 'python -m megadetector.detection.run_detector_batch'
777
+ else:
778
+ cmd = 'python megadetector/detection/run_detector_batch.py'
779
+ cmd += ' "{}" "{}" "{}" --recursive'.format(
780
+ options.default_model,image_folder,inference_output_file)
781
+ cmd += ' --output_relative_filenames --quiet --include_image_size'
782
+ cmd += ' --include_image_timestamp --include_exif_data'
783
+ print('Running: {}'.format(cmd))
784
+ cmd_results = execute_and_print(cmd)
785
+
786
+
787
+ ## Run again with checkpointing enabled, make sure the results are the same
788
+
789
+ cmd += ' --checkpoint_frequency 5'
790
+ from megadetector.utils.path_utils import insert_before_extension
791
+ inference_output_file_checkpoint = insert_before_extension(inference_output_file,'_checkpoint')
792
+ assert inference_output_file_checkpoint != inference_output_file
793
+ cmd = cmd.replace(inference_output_file,inference_output_file_checkpoint)
794
+ print('Running: {}'.format(cmd))
795
+ cmd_results = execute_and_print(cmd)
796
+
797
+ assert output_files_are_identical(fn1=inference_output_file,
798
+ fn2=inference_output_file_checkpoint,verbose=True)
799
+
800
+
801
+ ## Postprocessing
802
+
803
+ postprocessing_output_dir = os.path.join(options.scratch_dir,'postprocessing_output_cli')
804
+
805
+ if options.cli_working_dir is None:
806
+ cmd = 'python -m megadetector.postprocessing.postprocess_batch_results'
807
+ else:
808
+ cmd = 'python megadetector/postprocessing/postprocess_batch_results.py'
809
+ cmd += ' "{}" "{}"'.format(
810
+ inference_output_file,postprocessing_output_dir)
811
+ cmd += ' --image_base_dir "{}"'.format(image_folder)
812
+ print('Running: {}'.format(cmd))
813
+ cmd_results = execute_and_print(cmd)
814
+
815
+
816
+ ## RDE
817
+
818
+ rde_output_dir = os.path.join(options.scratch_dir,'rde_output_cli')
819
+
820
+ if options.cli_working_dir is None:
821
+ cmd = 'python -m megadetector.postprocessing.repeat_detection_elimination.find_repeat_detections'
822
+ else:
823
+ cmd = 'python megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py'
824
+ cmd += ' "{}"'.format(inference_output_file)
825
+ cmd += ' --imageBase "{}"'.format(image_folder)
826
+ cmd += ' --outputBase "{}"'.format(rde_output_dir)
827
+ cmd += ' --occurrenceThreshold 1' # Use an absurd number here to make sure we get some suspicious detections
828
+ print('Running: {}'.format(cmd))
829
+ cmd_results = execute_and_print(cmd)
830
+
831
+ # Find the latest filtering folder
832
+ filtering_output_dir = os.listdir(rde_output_dir)
833
+ filtering_output_dir = [fn for fn in filtering_output_dir if fn.startswith('filtering_')]
834
+ filtering_output_dir = [os.path.join(rde_output_dir,fn) for fn in filtering_output_dir]
835
+ filtering_output_dir = [fn for fn in filtering_output_dir if os.path.isdir(fn)]
836
+ filtering_output_dir = sorted(filtering_output_dir)[-1]
837
+
838
+ print('Using RDE filtering folder {}'.format(filtering_output_dir))
839
+
840
+ filtered_output_file = inference_output_file.replace('.json','_filtered.json')
841
+
842
+ if options.cli_working_dir is None:
843
+ cmd = 'python -m megadetector.postprocessing.repeat_detection_elimination.remove_repeat_detections'
844
+ else:
845
+ cmd = 'python megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py'
846
+ cmd += ' "{}" "{}" "{}"'.format(inference_output_file,filtered_output_file,filtering_output_dir)
847
+ print('Running: {}'.format(cmd))
848
+ cmd_results = execute_and_print(cmd)
849
+
850
+ assert os.path.isfile(filtered_output_file), \
851
+ 'Could not find RDE output file {}'.format(filtered_output_file)
852
+
853
+
854
+ ## Run inference on a folder (tiled)
855
+
856
+ image_folder = os.path.join(options.scratch_dir,'md-test-images')
857
+ tiling_folder = os.path.join(options.scratch_dir,'tiling-folder')
858
+ inference_output_file_tiled = os.path.join(options.scratch_dir,'folder_inference_output_tiled.json')
859
+ if options.cli_working_dir is None:
860
+ cmd = 'python -m megadetector.detection.run_tiled_inference'
861
+ else:
862
+ cmd = 'python megadetector/detection/run_tiled_inference.py'
863
+ cmd += ' "{}" "{}" "{}" "{}"'.format(
864
+ options.default_model,image_folder,tiling_folder,inference_output_file_tiled)
865
+ cmd += ' --overwrite_handling overwrite'
866
+ print('Running: {}'.format(cmd))
867
+ cmd_results = execute_and_print(cmd)
868
+
869
+ with open(inference_output_file_tiled,'r') as f:
870
+ results_from_file = json.load(f) # noqa
871
+
872
+
873
+ ## Run inference on a folder (augmented)
874
+
875
+ if options.yolo_working_dir is None:
876
+
877
+ print('Bypassing YOLOv5 val tests, no yolo folder supplied')
878
+
879
+ else:
880
+
881
+ image_folder = os.path.join(options.scratch_dir,'md-test-images')
882
+ yolo_results_folder = os.path.join(options.scratch_dir,'yolo-output-folder')
883
+ yolo_symlink_folder = os.path.join(options.scratch_dir,'yolo-symlink_folder')
884
+ inference_output_file_yolo_val = os.path.join(options.scratch_dir,'folder_inference_output_yolo_val.json')
885
+ if options.cli_working_dir is None:
886
+ cmd = 'python -m megadetector.detection.run_inference_with_yolov5_val'
887
+ else:
888
+ cmd = 'python megadetector/detection/run_inference_with_yolov5_val.py'
889
+ cmd += ' "{}" "{}" "{}"'.format(
890
+ options.default_model,image_folder,inference_output_file_yolo_val)
891
+ cmd += ' --yolo_working_folder "{}"'.format(options.yolo_working_dir)
892
+ cmd += ' --yolo_results_folder "{}"'.format(yolo_results_folder)
893
+ cmd += ' --symlink_folder "{}"'.format(yolo_symlink_folder)
894
+ cmd += ' --augment_enabled 1'
895
+ # cmd += ' --no_use_symlinks'
896
+ cmd += ' --overwrite_handling overwrite'
897
+ print('Running: {}'.format(cmd))
898
+ cmd_results = execute_and_print(cmd)
899
+
900
+ # Run again with checkpointing, make sure the output are identical
901
+ cmd += ' --checkpoint_frequency 5'
902
+ inference_output_file_yolo_val_checkpoint = \
903
+ os.path.join(options.scratch_dir,'folder_inference_output_yolo_val_checkpoint.json')
904
+ assert inference_output_file_yolo_val_checkpoint != inference_output_file_yolo_val
905
+ cmd = cmd.replace(inference_output_file_yolo_val,inference_output_file_yolo_val_checkpoint)
906
+ cmd_results = execute_and_print(cmd)
907
+
908
+ assert output_files_are_identical(fn1=inference_output_file_yolo_val,
909
+ fn2=inference_output_file_yolo_val_checkpoint)
910
+
911
+ if not options.skip_video_tests:
912
+
913
+ ## Video test
914
+
915
+ video_inference_output_file = os.path.join(options.scratch_dir,'video_inference_output.json')
916
+ output_video_file = os.path.join(options.scratch_dir,'video_scratch/cli_rendered_video.mp4')
917
+ frame_folder = os.path.join(options.scratch_dir,'video_scratch/frame_folder_cli')
918
+ frame_rendering_folder = os.path.join(options.scratch_dir,'video_scratch/rendered_frame_folder_cli')
919
+
920
+ video_fn = os.path.join(options.scratch_dir,options.test_videos[-1])
921
+ assert os.path.isfile(video_fn), 'Could not find video file {}'.format(video_fn)
922
+
923
+ output_dir = os.path.join(options.scratch_dir,'single_video_test_cli')
924
+ if options.cli_working_dir is None:
925
+ cmd = 'python -m megadetector.detection.process_video'
926
+ else:
927
+ cmd = 'python megadetector/detection/process_video.py'
928
+ cmd += ' "{}" "{}"'.format(options.default_model,video_fn)
929
+ cmd += ' --frame_folder "{}" --frame_rendering_folder "{}" --output_json_file "{}" --output_video_file "{}"'.format(
930
+ frame_folder,frame_rendering_folder,video_inference_output_file,output_video_file)
931
+ cmd += ' --render_output_video --fourcc {}'.format(options.video_fourcc)
932
+ cmd += ' --force_extracted_frame_folder_deletion --force_rendered_frame_folder_deletion --n_cores 5 --frame_sample 3'
933
+ cmd += ' --verbose'
934
+ print('Running: {}'.format(cmd))
935
+ cmd_results = execute_and_print(cmd)
936
+
937
+ # ...if we're not skipping video tests
938
+
939
+
940
+ ## Run inference on a folder (with MDV5B, so we can do a comparison)
941
+
942
+ image_folder = os.path.join(options.scratch_dir,'md-test-images')
943
+ inference_output_file_alt = os.path.join(options.scratch_dir,'folder_inference_output_alt.json')
944
+ if options.cli_working_dir is None:
945
+ cmd = 'python -m megadetector.detection.run_detector_batch'
946
+ else:
947
+ cmd = 'python megadetector/detection/run_detector_batch.py'
948
+ cmd += ' "{}" "{}" "{}" --recursive'.format(
949
+ options.alt_model,image_folder,inference_output_file_alt)
950
+ cmd += ' --output_relative_filenames --quiet --include_image_size'
951
+ cmd += ' --include_image_timestamp --include_exif_data'
952
+ print('Running: {}'.format(cmd))
953
+ cmd_results = execute_and_print(cmd)
954
+
955
+ with open(inference_output_file_alt,'r') as f:
956
+ results_from_file = json.load(f) # noqa
957
+
958
+
959
+ ## Compare the two files
960
+
961
+ comparison_output_folder = os.path.join(options.scratch_dir,'results_comparison')
962
+ image_folder = os.path.join(options.scratch_dir,'md-test-images')
963
+ results_files_string = '"{}" "{}"'.format(
964
+ inference_output_file,inference_output_file_alt)
965
+ if options.cli_working_dir is None:
966
+ cmd = 'python -m megadetector.postprocessing.compare_batch_results'
967
+ else:
968
+ cmd = 'python megadetector/postprocessing/compare_batch_results.py'
969
+ cmd += ' "{}" "{}" {}'.format(comparison_output_folder,image_folder,results_files_string)
970
+ print('Running: {}'.format(cmd))
971
+ cmd_results = execute_and_print(cmd)
972
+
973
+ assert cmd_results['status'] == 0, 'Error generating comparison HTML'
974
+ assert os.path.isfile(os.path.join(comparison_output_folder,'index.html')), \
975
+ 'Failed to generate comparison HTML'
976
+
977
+ print('\n*** Finished CLI tests ***\n')
978
+
979
+ # ...def run_cli_tests(...)
980
+
981
+
982
+ #%% Main test wrapper
983
+
984
+ def run_tests(options):
985
+ """
986
+ Runs Python-based and/or CLI-based package tests.
987
+
988
+ Args:
989
+ options (MDTestOptions): see MDTestOptions for details
990
+ """
991
+
992
+ # Prepare data folder
993
+ download_test_data(options)
994
+
995
+ if options.disable_gpu:
996
+ os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
997
+
998
+ # Verify GPU
999
+ gpu_available = is_gpu_available()
1000
+
1001
+ # If the GPU is required and isn't available, error
1002
+ if options.cpu_execution_is_error and (not gpu_available):
1003
+ raise ValueError('GPU not available, and cpu_execution_is_error is set')
1004
+
1005
+ # If the GPU should be disabled, verify that it is
1006
+ if options.disable_gpu:
1007
+ assert (not gpu_available), 'CPU execution specified, but the GPU appears to be available'
1008
+
1009
+ # Run python tests
1010
+ if not options.skip_python_tests:
1011
+ run_python_tests(options)
1012
+
1013
+ # Run CLI tests
1014
+ if not options.skip_cli_tests:
1015
+ run_cli_tests(options)
1016
+
1017
+
1018
+ #%% Interactive driver
1019
+
1020
+ if False:
1021
+
1022
+ pass
1023
+
1024
+ #%%
1025
+
1026
+ options = MDTestOptions()
1027
+
1028
+ options.disable_gpu = False
1029
+ options.cpu_execution_is_error = False
1030
+ options.skip_video_tests = False
1031
+ options.skip_python_tests = False
1032
+ options.skip_cli_tests = False
1033
+ options.scratch_dir = None
1034
+ options.test_data_url = 'https://lila.science/public/md-test-package.zip'
1035
+ options.force_data_download = False
1036
+ options.force_data_unzip = False
1037
+ options.warning_mode = True
1038
+ options.max_coord_error = 0.001
1039
+ options.max_conf_error = 0.005
1040
+ options.cli_working_dir = r'c:\git\MegaDetector'
1041
+ options.yolo_working_dir = r'c:\git\yolov5-md'
1042
+
1043
+
1044
+ #%%
1045
+
1046
+ run_tests(options)
1047
+
1048
+
1049
+ #%% Command-line driver
1050
+
1051
+ def main():
1052
+
1053
+ options = MDTestOptions()
1054
+
1055
+ parser = argparse.ArgumentParser(
1056
+ description='MegaDetector test suite')
1057
+
1058
+ parser.add_argument(
1059
+ '--disable_gpu',
1060
+ action='store_true',
1061
+ help='Disable GPU operation')
1062
+
1063
+ parser.add_argument(
1064
+ '--cpu_execution_is_error',
1065
+ action='store_true',
1066
+ help='Fail if the GPU appears not to be available')
1067
+
1068
+ parser.add_argument(
1069
+ '--scratch_dir',
1070
+ default=None,
1071
+ type=str,
1072
+ help='Directory for temporary storage (defaults to system temp dir)')
1073
+
1074
+ parser.add_argument(
1075
+ '--skip_video_tests',
1076
+ action='store_true',
1077
+ help='Skip tests related to video (which can be slow)')
1078
+
1079
+ parser.add_argument(
1080
+ '--skip_python_tests',
1081
+ action='store_true',
1082
+ help='Skip python tests')
1083
+
1084
+ parser.add_argument(
1085
+ '--skip_cli_tests',
1086
+ action='store_true',
1087
+ help='Skip CLI tests')
1088
+
1089
+ parser.add_argument(
1090
+ '--force_data_download',
1091
+ action='store_true',
1092
+ help='Force download of the test data file, even if it\'s already available')
1093
+
1094
+ parser.add_argument(
1095
+ '--force_data_unzip',
1096
+ action='store_true',
1097
+ help='Force extraction of all files in the test data file, even if they\'re already available')
1098
+
1099
+ parser.add_argument(
1100
+ '--warning_mode',
1101
+ action='store_true',
1102
+ help='Turns numeric/content errors into warnings')
1103
+
1104
+ parser.add_argument(
1105
+ '--max_conf_error',
1106
+ type=float,
1107
+ default=options.max_conf_error,
1108
+ help='Maximum tolerable confidence value deviation from expected (default {})'.format(
1109
+ options.max_conf_error))
1110
+
1111
+ parser.add_argument(
1112
+ '--max_coord_error',
1113
+ type=float,
1114
+ default=options.max_coord_error,
1115
+ help='Maximum tolerable coordinate value deviation from expected (default {})'.format(
1116
+ options.max_coord_error))
1117
+
1118
+ parser.add_argument(
1119
+ '--cli_working_dir',
1120
+ type=str,
1121
+ default=None,
1122
+ help='Working directory for CLI tests')
1123
+
1124
+ parser.add_argument(
1125
+ '--yolo_working_dir',
1126
+ type=str,
1127
+ default=None,
1128
+ help='Working directory for yolo inference tests')
1129
+
1130
+ parser.add_argument(
1131
+ '--cli_test_pythonpath',
1132
+ type=str,
1133
+ default=None,
1134
+ help='PYTHONPATH to set for CLI tests; if None, inherits from the parent process'
1135
+ )
1136
+
1137
+ # token used for linting
1138
+ #
1139
+ # no_arguments_required
1140
+
1141
+ args = parser.parse_args()
1142
+
1143
+ _args_to_object(args,options)
1144
+
1145
+ run_tests(options)
1146
+
1147
+ if __name__ == '__main__':
1148
+ main()
1149
+
1150
+
1151
+ #%% Sample invocations
1152
+
1153
+ """
1154
+ # Windows
1155
+ set PYTHONPATH=c:\git\MegaDetector;c:\git\yolov5-md
1156
+ python md_tests.py --cli_working_dir "c:\git\MegaDetector" --yolo_working_dir "c:\git\yolov5-md" --cli_test_pythonpath "c:\git\MegaDetector;c:\git\yolov5-md"
1157
+
1158
+ # Linux
1159
+ export PYTHONPATH=/mnt/c/git/MegaDetector:/mnt/c/git/yolov5-md
1160
+ python md_tests.py --cli_working_dir "/mnt/c/git/MegaDetector" --yolo_working_dir "/mnt/c/git/yolov5-md" --cli_test_pythonpath "/mnt/c/git/MegaDetector:/mnt/c/git/yolov5-md"
1161
+
1162
+ python -c "import md_tests; print(md_tests.get_expected_results_filename(True))"
1163
+ """
1164
+