megadetector 5.0.11__py3-none-any.whl → 5.0.12__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 (201) 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 +98 -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 +152 -0
  11. megadetector/api/batch_processing/api_core/server_orchestration.py +360 -0
  12. megadetector/api/batch_processing/api_core/server_utils.py +92 -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 +126 -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 +266 -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 +610 -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 +239 -0
  58. megadetector/data_management/cct_json_utils.py +395 -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 +272 -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 +477 -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 +796 -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 +874 -0
  129. megadetector/data_management/read_exif.py +681 -0
  130. megadetector/data_management/remap_coco_categories.py +84 -0
  131. megadetector/data_management/remove_exif.py +66 -0
  132. megadetector/data_management/resize_coco_dataset.py +189 -0
  133. megadetector/data_management/wi_download_csv_to_coco.py +246 -0
  134. megadetector/data_management/yolo_output_to_md_output.py +441 -0
  135. megadetector/data_management/yolo_to_coco.py +676 -0
  136. megadetector/detection/__init__.py +0 -0
  137. megadetector/detection/detector_training/__init__.py +0 -0
  138. megadetector/detection/detector_training/model_main_tf2.py +114 -0
  139. megadetector/detection/process_video.py +702 -0
  140. megadetector/detection/pytorch_detector.py +341 -0
  141. megadetector/detection/run_detector.py +779 -0
  142. megadetector/detection/run_detector_batch.py +1219 -0
  143. megadetector/detection/run_inference_with_yolov5_val.py +917 -0
  144. megadetector/detection/run_tiled_inference.py +934 -0
  145. megadetector/detection/tf_detector.py +189 -0
  146. megadetector/detection/video_utils.py +606 -0
  147. megadetector/postprocessing/__init__.py +0 -0
  148. megadetector/postprocessing/add_max_conf.py +64 -0
  149. megadetector/postprocessing/categorize_detections_by_size.py +163 -0
  150. megadetector/postprocessing/combine_api_outputs.py +249 -0
  151. megadetector/postprocessing/compare_batch_results.py +958 -0
  152. megadetector/postprocessing/convert_output_format.py +396 -0
  153. megadetector/postprocessing/load_api_results.py +195 -0
  154. megadetector/postprocessing/md_to_coco.py +310 -0
  155. megadetector/postprocessing/md_to_labelme.py +330 -0
  156. megadetector/postprocessing/merge_detections.py +401 -0
  157. megadetector/postprocessing/postprocess_batch_results.py +1902 -0
  158. megadetector/postprocessing/remap_detection_categories.py +170 -0
  159. megadetector/postprocessing/render_detection_confusion_matrix.py +660 -0
  160. megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +211 -0
  161. megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +83 -0
  162. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +1631 -0
  163. megadetector/postprocessing/separate_detections_into_folders.py +730 -0
  164. megadetector/postprocessing/subset_json_detector_output.py +696 -0
  165. megadetector/postprocessing/top_folders_to_bottom.py +223 -0
  166. megadetector/taxonomy_mapping/__init__.py +0 -0
  167. megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +491 -0
  168. megadetector/taxonomy_mapping/map_new_lila_datasets.py +150 -0
  169. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +142 -0
  170. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +590 -0
  171. megadetector/taxonomy_mapping/retrieve_sample_image.py +71 -0
  172. megadetector/taxonomy_mapping/simple_image_download.py +219 -0
  173. megadetector/taxonomy_mapping/species_lookup.py +834 -0
  174. megadetector/taxonomy_mapping/taxonomy_csv_checker.py +159 -0
  175. megadetector/taxonomy_mapping/taxonomy_graph.py +346 -0
  176. megadetector/taxonomy_mapping/validate_lila_category_mappings.py +83 -0
  177. megadetector/utils/__init__.py +0 -0
  178. megadetector/utils/azure_utils.py +178 -0
  179. megadetector/utils/ct_utils.py +612 -0
  180. megadetector/utils/directory_listing.py +246 -0
  181. megadetector/utils/md_tests.py +968 -0
  182. megadetector/utils/path_utils.py +1044 -0
  183. megadetector/utils/process_utils.py +157 -0
  184. megadetector/utils/sas_blob_utils.py +509 -0
  185. megadetector/utils/split_locations_into_train_val.py +228 -0
  186. megadetector/utils/string_utils.py +92 -0
  187. megadetector/utils/url_utils.py +323 -0
  188. megadetector/utils/write_html_image_list.py +225 -0
  189. megadetector/visualization/__init__.py +0 -0
  190. megadetector/visualization/plot_utils.py +293 -0
  191. megadetector/visualization/render_images_with_thumbnails.py +275 -0
  192. megadetector/visualization/visualization_utils.py +1536 -0
  193. megadetector/visualization/visualize_db.py +550 -0
  194. megadetector/visualization/visualize_detector_output.py +405 -0
  195. {megadetector-5.0.11.dist-info → megadetector-5.0.12.dist-info}/METADATA +1 -1
  196. megadetector-5.0.12.dist-info/RECORD +199 -0
  197. megadetector-5.0.12.dist-info/top_level.txt +1 -0
  198. megadetector-5.0.11.dist-info/RECORD +0 -5
  199. megadetector-5.0.11.dist-info/top_level.txt +0 -1
  200. {megadetector-5.0.11.dist-info → megadetector-5.0.12.dist-info}/LICENSE +0 -0
  201. {megadetector-5.0.11.dist-info → megadetector-5.0.12.dist-info}/WHEEL +0 -0
@@ -0,0 +1,702 @@
1
+ """
2
+
3
+ process_video.py
4
+
5
+ Splits a video (or folder of videos) into frames, runs the frames through run_detector_batch.py,
6
+ and optionally stitches together results into a new video with detection boxes.
7
+
8
+ Operates by separating the video into frames, typically sampling every Nth frame, and writing
9
+ those frames to disk, before running MD. This approach clearly has a downside: it requires
10
+ a bunch more disk space, compared to extracting frames and running MD on them without ever
11
+ writing them to disk. The upside, though, is that this approach allows you to run repeat
12
+ detection elimination after running MegaDetector, and it allows allows more efficient re-use
13
+ of frames if you end up running MD more than once, or running multiple versions of MD.
14
+
15
+ TODO: optionally skip writing frames to disk, and process frames in memory.
16
+
17
+ """
18
+
19
+ #%% Imports
20
+
21
+ import os
22
+ import sys
23
+ import tempfile
24
+ import argparse
25
+ import itertools
26
+ import json
27
+ import shutil
28
+
29
+ from uuid import uuid1
30
+
31
+ from megadetector.detection import run_detector_batch
32
+ from megadetector.visualization import visualize_detector_output
33
+ from megadetector.utils.ct_utils import args_to_object
34
+ from megadetector.utils.path_utils import insert_before_extension
35
+ from megadetector.detection.video_utils import video_to_frames
36
+ from megadetector.detection.video_utils import frames_to_video
37
+ from megadetector.detection.video_utils import frame_results_to_video_results
38
+ from megadetector.detection.video_utils import video_folder_to_frames
39
+ from megadetector.detection.video_utils import default_fourcc
40
+
41
+
42
+ #%% Classes
43
+
44
+ class ProcessVideoOptions:
45
+ """
46
+ Options controlling the behavior of process_video()
47
+ """
48
+
49
+ #: Can be a model filename (.pt or .pb) or a model name (e.g. "MDV5A")
50
+ model_file = 'MDV5A'
51
+
52
+ #: Video (of folder of videos) to process
53
+ input_video_file = ''
54
+
55
+ #: .json file to which we should write results
56
+ output_json_file = None
57
+
58
+ #: File to which we should write a video with boxes, only relevant if
59
+ #: render_output_video is True
60
+ output_video_file = None
61
+
62
+ #: Folder to use for extracted frames; will use a folder in system temp space
63
+ #: if this is None
64
+ frame_folder = None
65
+
66
+ # Folder to use for rendered frames (if rendering output video); will use a folder
67
+ #: in system temp space if this is None
68
+ frame_rendering_folder = None
69
+
70
+ #: Should we render a video with detection boxes?
71
+ #:
72
+ #: Only supported when processing a single video, not a folder.
73
+ render_output_video = False
74
+
75
+ #: If we are rendering boxes to a new video, should we keep the temporary
76
+ #: rendered frames?
77
+ keep_rendered_frames = False
78
+
79
+ #: Should we keep the extracted frames?
80
+ keep_extracted_frames = False
81
+
82
+ #: Should we delete the entire folder the extracted frames are written to?
83
+ #:
84
+ #: By default, we delete the frame files but leave the (probably-empty) folder in place,
85
+ #: for no reason other than being paranoid about deleting folders.
86
+ force_extracted_frame_folder_deletion = False
87
+
88
+ #: Should we delete the entire folder the rendered frames are written to?
89
+ #:
90
+ #: By default, we delete the frame files but leave the (probably-empty) folder in place,
91
+ #: for no reason other than being paranoid about deleting folders.
92
+ force_rendered_frame_folder_deletion = False
93
+
94
+ #: If we've already run MegaDetector on this video or folder of videos, i.e. if we
95
+ #: find a corresponding MD results file, should we re-use it? Defaults to reprocessing.
96
+ reuse_results_if_available = False
97
+
98
+ #: If we've already split this video or folder of videos into frames, should we
99
+ #: we re-use those extracted frames? Defaults to reprocessing.
100
+ reuse_frames_if_available = False
101
+
102
+ #: If [input_video_file] is a folder, should we search for videos recursively?
103
+ recursive = False
104
+
105
+ #: Enable additional debug console output
106
+ verbose = False
107
+
108
+ #: fourcc code to use for writing videos; only relevant if render_output_video is True
109
+ fourcc = None
110
+
111
+ #: Confidence threshold to use for writing videos with boxes, only relevant if
112
+ #: if render_output_video is True. Defaults to choosing a reasonable threshold
113
+ #: based on the model version.
114
+ rendering_confidence_threshold = None
115
+
116
+ #: Detections below this threshold will not be included in the output file.
117
+ json_confidence_threshold = 0.005
118
+
119
+ #: Sample every Nth frame; set to None (default) or 1 to sample every frame. Typically
120
+ #: we sample down to around 3 fps, so for typical 30 fps videos, frame_sample=10 is a
121
+ #: typical value.
122
+ frame_sample = None
123
+
124
+ #: Number of workers to use for parallelization; set to <= 1 to disable parallelization
125
+ n_cores = 1
126
+
127
+ #: For debugging only, stop processing after a certain number of frames.
128
+ debug_max_frames = -1
129
+
130
+ #: File containing non-standard categories, typically only used if you're running a non-MD
131
+ #: detector.
132
+ class_mapping_filename = None
133
+
134
+ # ...class ProcessVideoOptions
135
+
136
+
137
+ #%% Functions
138
+
139
+ def process_video(options):
140
+ """
141
+ Process a single video through MD, optionally writing a new video with boxes
142
+
143
+ Args:
144
+ options (ProcessVideoOptions): all the parameters used to control this process,
145
+ including filenames; see ProcessVideoOptions for details
146
+
147
+ Returns:
148
+ dict: frame-level MegaDetector results, identical to what's in the output .json file
149
+ """
150
+
151
+ if options.output_json_file is None:
152
+ options.output_json_file = options.input_video_file + '.json'
153
+
154
+ if options.render_output_video and (options.output_video_file is None):
155
+ options.output_video_file = options.input_video_file + '.detections.mp4'
156
+
157
+ tempdir = os.path.join(tempfile.gettempdir(), 'process_camera_trap_video')
158
+ os.makedirs(tempdir,exist_ok=True)
159
+
160
+ # TODO:
161
+ #
162
+ # This is a lazy fix to an issue... if multiple users run this script, the
163
+ # "process_camera_trap_video" folder is owned by the first person who creates it, and others
164
+ # can't write to it. I could create uniquely-named folders, but I philosophically prefer
165
+ # to put all the individual UUID-named folders within a larger folder, so as to be a
166
+ # good tempdir citizen. So, the lazy fix is to make this world-writable.
167
+ try:
168
+ os.chmod(tempdir,0o777)
169
+ except Exception:
170
+ pass
171
+
172
+ if options.frame_folder is not None:
173
+ frame_output_folder = options.frame_folder
174
+ else:
175
+ frame_output_folder = os.path.join(
176
+ tempdir, os.path.basename(options.input_video_file) + '_frames_' + str(uuid1()))
177
+
178
+ # TODO: keep track of whether we created this folder, delete if we're deleting the extracted
179
+ # frames and we created the folder, and the output files aren't in the same folder. For now,
180
+ # we're just deleting the extracted frames and leaving the empty folder around in this case.
181
+ os.makedirs(frame_output_folder, exist_ok=True)
182
+
183
+ frame_filenames, Fs = video_to_frames(
184
+ options.input_video_file, frame_output_folder,
185
+ every_n_frames=options.frame_sample, overwrite=(not options.reuse_frames_if_available))
186
+
187
+ image_file_names = frame_filenames
188
+ if options.debug_max_frames > 0:
189
+ image_file_names = image_file_names[0:options.debug_max_frames]
190
+
191
+ if options.reuse_results_if_available and \
192
+ os.path.isfile(options.output_json_file):
193
+ print('Loading results from {}'.format(options.output_json_file))
194
+ with open(options.output_json_file,'r') as f:
195
+ results = json.load(f)
196
+ else:
197
+ results = run_detector_batch.load_and_run_detector_batch(
198
+ options.model_file, image_file_names,
199
+ confidence_threshold=options.json_confidence_threshold,
200
+ n_cores=options.n_cores,
201
+ quiet=(not options.verbose),
202
+ class_mapping_filename=options.class_mapping_filename)
203
+
204
+ run_detector_batch.write_results_to_file(
205
+ results, options.output_json_file,
206
+ relative_path_base=frame_output_folder,
207
+ detector_file=options.model_file,
208
+ custom_metadata={'video_frame_rate':Fs})
209
+
210
+
211
+ ## (Optionally) render output video
212
+
213
+ if options.render_output_video:
214
+
215
+ # Render detections to images
216
+ if options.frame_rendering_folder is not None:
217
+ rendering_output_dir = options.frame_rendering_folder
218
+ else:
219
+ rendering_output_dir = os.path.join(
220
+ tempdir, os.path.basename(options.input_video_file) + '_detections')
221
+
222
+ # TODO: keep track of whether we created this folder, delete if we're deleting the rendered
223
+ # frames and we created the folder, and the output files aren't in the same folder. For now,
224
+ # we're just deleting the rendered frames and leaving the empty folder around in this case.
225
+ os.makedirs(rendering_output_dir,exist_ok=True)
226
+
227
+ detected_frame_files = visualize_detector_output.visualize_detector_output(
228
+ detector_output_path=options.output_json_file,
229
+ out_dir=rendering_output_dir,
230
+ images_dir=frame_output_folder,
231
+ confidence_threshold=options.rendering_confidence_threshold)
232
+
233
+ # Combine into a video
234
+ if options.frame_sample is None:
235
+ rendering_fs = Fs
236
+ else:
237
+ rendering_fs = Fs / options.frame_sample
238
+
239
+ print('Rendering video to {} at {} fps (original video {} fps)'.format(
240
+ options.output_video_file,rendering_fs,Fs))
241
+ frames_to_video(detected_frame_files, rendering_fs, options.output_video_file, codec_spec=options.fourcc)
242
+
243
+ # Delete the temporary directory we used for detection images
244
+ if not options.keep_rendered_frames:
245
+ try:
246
+ if options.force_rendered_frame_folder_deletion:
247
+ shutil.rmtree(rendering_output_dir)
248
+ else:
249
+ for rendered_frame_fn in detected_frame_files:
250
+ os.remove(rendered_frame_fn)
251
+ except Exception as e:
252
+ print('Warning: error deleting rendered frames from folder {}:\n{}'.format(
253
+ rendering_output_dir,str(e)))
254
+ pass
255
+
256
+ # ...if we're rendering video
257
+
258
+
259
+ ## (Optionally) delete the extracted frames
260
+
261
+ if not options.keep_extracted_frames:
262
+
263
+ try:
264
+ if options.force_extracted_frame_folder_deletion:
265
+ print('Recursively deleting frame output folder {}'.format(frame_output_folder))
266
+ shutil.rmtree(frame_output_folder)
267
+ else:
268
+ for extracted_frame_fn in frame_filenames:
269
+ os.remove(extracted_frame_fn)
270
+ except Exception as e:
271
+ print('Warning: error removing extracted frames from folder {}:\n{}'.format(
272
+ frame_output_folder,str(e)))
273
+ pass
274
+
275
+ return results
276
+
277
+ # ...process_video()
278
+
279
+
280
+ def process_video_folder(options):
281
+ """
282
+ Process a folder of videos through MD
283
+
284
+ Args:
285
+ options (ProcessVideoOptions): all the parameters used to control this process,
286
+ including filenames; see ProcessVideoOptions for details
287
+ """
288
+
289
+ ## Validate options
290
+
291
+ assert os.path.isdir(options.input_video_file), \
292
+ '{} is not a folder'.format(options.input_video_file)
293
+
294
+ assert options.output_json_file is not None, \
295
+ 'When processing a folder, you must specify an output .json file'
296
+
297
+ assert options.output_json_file.endswith('.json')
298
+ video_json = options.output_json_file
299
+ frames_json = options.output_json_file.replace('.json','.frames.json')
300
+ os.makedirs(os.path.dirname(video_json),exist_ok=True)
301
+
302
+
303
+ ## Split every video into frames
304
+
305
+ if options.frame_folder is not None:
306
+ frame_output_folder = options.frame_folder
307
+ else:
308
+ tempdir = os.path.join(tempfile.gettempdir(), 'process_camera_trap_video')
309
+ os.makedirs(tempdir,exist_ok=True)
310
+
311
+ # TODO: see above; this is a lazy fix to a permissions issue
312
+ try:
313
+ os.chmod(tempdir,0o777)
314
+ except Exception:
315
+ pass
316
+
317
+ frame_output_folder = os.path.join(
318
+ tempdir, os.path.basename(options.input_video_file) + '_frames_' + str(uuid1()))
319
+
320
+ os.makedirs(frame_output_folder, exist_ok=True)
321
+
322
+ print('Extracting frames')
323
+ frame_filenames, Fs, video_filenames = \
324
+ video_folder_to_frames(input_folder=options.input_video_file,
325
+ output_folder_base=frame_output_folder,
326
+ recursive=options.recursive,
327
+ overwrite=(not options.reuse_frames_if_available),
328
+ n_threads=options.n_cores,every_n_frames=options.frame_sample,
329
+ verbose=options.verbose)
330
+
331
+ image_file_names = list(itertools.chain.from_iterable(frame_filenames))
332
+
333
+ if len(image_file_names) == 0:
334
+ if len(video_filenames) == 0:
335
+ print('No videos found in folder {}'.format(options.input_video_file))
336
+ else:
337
+ print('No frames extracted from folder {}, this may be due to an '\
338
+ 'unsupported video codec'.format(options.input_video_file))
339
+ return
340
+
341
+ if options.debug_max_frames is not None and options.debug_max_frames > 0:
342
+ image_file_names = image_file_names[0:options.debug_max_frames]
343
+
344
+
345
+ ## Run MegaDetector on the extracted frames
346
+
347
+ if options.reuse_results_if_available and \
348
+ os.path.isfile(frames_json):
349
+ print('Loading results from {}'.format(frames_json))
350
+ results = None
351
+ else:
352
+ print('Running MegaDetector')
353
+ results = run_detector_batch.load_and_run_detector_batch(
354
+ options.model_file, image_file_names,
355
+ confidence_threshold=options.json_confidence_threshold,
356
+ n_cores=options.n_cores,
357
+ quiet=(not options.verbose),
358
+ class_mapping_filename=options.class_mapping_filename)
359
+
360
+ run_detector_batch.write_results_to_file(
361
+ results, frames_json,
362
+ relative_path_base=frame_output_folder,
363
+ detector_file=options.model_file,
364
+ custom_metadata={'video_frame_rate':Fs})
365
+
366
+
367
+ ## Convert frame-level results to video-level results
368
+
369
+ print('Converting frame-level results to video-level results')
370
+ frame_results_to_video_results(frames_json,video_json)
371
+
372
+
373
+ ## (Optionally) render output videos
374
+
375
+ if options.render_output_video:
376
+
377
+ # Render detections to images
378
+ if options.frame_rendering_folder is not None:
379
+ frame_rendering_output_dir = options.frame_rendering_folder
380
+ else:
381
+ frame_rendering_output_dir = os.path.join(
382
+ tempdir, os.path.basename(options.input_video_file) + '_detections')
383
+
384
+ # TODO: keep track of whether we created this folder, delete if we're deleting the rendered
385
+ # frames and we created the folder, and the output files aren't in the same folder. For now,
386
+ # we're just deleting the rendered frames and leaving the empty folder around in this case.
387
+ os.makedirs(frame_rendering_output_dir,exist_ok=True)
388
+
389
+ detected_frame_files = visualize_detector_output.visualize_detector_output(
390
+ detector_output_path=frames_json,
391
+ out_dir=frame_rendering_output_dir,
392
+ images_dir=frame_output_folder,
393
+ confidence_threshold=options.rendering_confidence_threshold,
394
+ preserve_path_structure=True,
395
+ output_image_width=-1)
396
+
397
+ # Choose an output folder
398
+ output_folder_is_input_folder = False
399
+ if options.output_video_file is not None:
400
+ if os.path.isfile(options.output_video_file):
401
+ raise ValueError('Rendering videos for a folder, but an existing file was specified as output')
402
+ elif options.output_video_file == options.input_video_file:
403
+ output_folder_is_input_folder = True
404
+ output_video_folder = options.input_video_file
405
+ else:
406
+ os.makedirs(options.output_video_file,exist_ok=True)
407
+ output_video_folder = options.output_video_file
408
+ else:
409
+ output_folder_is_input_folder = True
410
+ output_video_folder = options.input_video_file
411
+
412
+ # For each video
413
+ #
414
+ # TODO: parallelize this loop
415
+ #
416
+ # i_video=0; input_video_file_abs = video_filenames[i_video]
417
+ for i_video,input_video_file_abs in enumerate(video_filenames):
418
+
419
+ video_fs = Fs[i_video]
420
+
421
+ if options.frame_sample is None:
422
+ rendering_fs = video_fs
423
+ else:
424
+ rendering_fs = video_fs / options.frame_sample
425
+
426
+ input_video_file_relative = os.path.relpath(input_video_file_abs,options.input_video_file)
427
+ video_frame_output_folder = os.path.join(frame_rendering_output_dir,input_video_file_relative)
428
+ assert os.path.isdir(video_frame_output_folder), \
429
+ 'Could not find frame folder for video {}'.format(input_video_file_relative)
430
+
431
+ # Find the corresponding rendered frame folder
432
+ video_frame_files = [fn for fn in detected_frame_files if \
433
+ fn.startswith(video_frame_output_folder)]
434
+ assert len(video_frame_files) > 0, 'Could not find rendered frames for video {}'.format(
435
+ input_video_file_relative)
436
+
437
+ # Select the output filename for the rendered video
438
+ if output_folder_is_input_folder:
439
+ video_output_file = insert_before_extension(input_video_file_abs,'annotated','_')
440
+ else:
441
+ video_output_file = os.path.join(output_video_folder,input_video_file_relative)
442
+
443
+ os.makedirs(os.path.dirname(video_output_file),exist_ok=True)
444
+
445
+ # Create the output video
446
+ print('Rendering detections for video {} to {} at {} fps (original video {} fps)'.format(
447
+ input_video_file_relative,video_output_file,rendering_fs,video_fs))
448
+ frames_to_video(video_frame_files, rendering_fs, video_output_file, codec_spec=options.fourcc)
449
+
450
+ # ...for each video
451
+
452
+ # Possibly clean up rendered frames
453
+ if not options.keep_rendered_frames:
454
+ try:
455
+ if options.force_rendered_frame_folder_deletion:
456
+ shutil.rmtree(frame_rendering_output_dir)
457
+ else:
458
+ for rendered_frame_fn in detected_frame_files:
459
+ os.remove(rendered_frame_fn)
460
+ except Exception as e:
461
+ print('Warning: error deleting rendered frames from folder {}:\n{}'.format(
462
+ frame_rendering_output_dir,str(e)))
463
+ pass
464
+
465
+ # ...if we're rendering video
466
+
467
+
468
+ ## (Optionally) delete the extracted frames
469
+
470
+ if not options.keep_extracted_frames:
471
+ try:
472
+ print('Deleting frame cache')
473
+ if options.force_extracted_frame_folder_deletion:
474
+ print('Recursively deleting frame output folder {}'.format(frame_output_folder))
475
+ shutil.rmtree(frame_output_folder)
476
+ else:
477
+ for frame_fn in image_file_names:
478
+ os.remove(frame_fn)
479
+ except Exception as e:
480
+ print('Warning: error deleting frames from folder {}:\n{}'.format(
481
+ frame_output_folder,str(e)))
482
+ pass
483
+
484
+ # ...process_video_folder()
485
+
486
+
487
+ def options_to_command(options):
488
+
489
+ cmd = 'python process_video.py'
490
+ cmd += ' "' + options.model_file + '"'
491
+ cmd += ' "' + options.input_video_file + '"'
492
+
493
+ if options.recursive:
494
+ cmd += ' --recursive'
495
+ if options.frame_folder is not None:
496
+ cmd += ' --frame_folder' + ' "' + options.frame_folder + '"'
497
+ if options.frame_rendering_folder is not None:
498
+ cmd += ' --frame_rendering_folder' + ' "' + options.frame_rendering_folder + '"'
499
+ if options.output_json_file is not None:
500
+ cmd += ' --output_json_file' + ' "' + options.output_json_file + '"'
501
+ if options.output_video_file is not None:
502
+ cmd += ' --output_video_file' + ' "' + options.output_video_file + '"'
503
+ if options.keep_extracted_frames:
504
+ cmd += ' --keep_extracted_frames'
505
+ if options.reuse_results_if_available:
506
+ cmd += ' --reuse_results_if_available'
507
+ if options.reuse_frames_if_available:
508
+ cmd += ' --reuse_frames_if_available'
509
+ if options.render_output_video:
510
+ cmd += ' --render_output_video'
511
+ if options.keep_rendered_frames:
512
+ cmd += ' --keep_rendered_frames'
513
+ if options.rendering_confidence_threshold is not None:
514
+ cmd += ' --rendering_confidence_threshold ' + str(options.rendering_confidence_threshold)
515
+ if options.json_confidence_threshold is not None:
516
+ cmd += ' --json_confidence_threshold ' + str(options.json_confidence_threshold)
517
+ if options.n_cores is not None:
518
+ cmd += ' --n_cores ' + str(options.n_cores)
519
+ if options.frame_sample is not None:
520
+ cmd += ' --frame_sample ' + str(options.frame_sample)
521
+ if options.debug_max_frames is not None:
522
+ cmd += ' --debug_max_frames ' + str(options.debug_max_frames)
523
+ if options.class_mapping_filename is not None:
524
+ cmd += ' --class_mapping_filename ' + str(options.class_mapping_filename)
525
+ if options.fourcc is not None:
526
+ cmd += ' --fourcc ' + options.fourcc
527
+
528
+ return cmd
529
+
530
+
531
+ #%% Interactive driver
532
+
533
+ if False:
534
+
535
+ #%% Process a folder of videos
536
+
537
+ model_file = 'MDV5A'
538
+ input_dir = r'c:\git\MegaDetector\test_images\test_images'
539
+ frame_folder = r'g:\temp\video_test\frames'
540
+ rendering_folder = r'g:\temp\video_test\rendered-frames'
541
+ output_json_file = r'g:\temp\video_test\video-test.json'
542
+ output_video_folder = r'g:\temp\video_test\output_videos'
543
+
544
+ print('Processing folder {}'.format(input_dir))
545
+
546
+ options = ProcessVideoOptions()
547
+ options.model_file = model_file
548
+ options.input_video_file = input_dir
549
+ options.output_video_file = output_video_folder
550
+ options.frame_folder = frame_folder
551
+ options.output_json_file = output_json_file
552
+ options.frame_rendering_folder = rendering_folder
553
+ options.render_output_video = True
554
+ options.keep_extracted_frames = True
555
+ options.keep_rendered_frames = True
556
+ options.recursive = True
557
+ options.reuse_frames_if_available = True
558
+ options.reuse_results_if_available = True
559
+ # options.confidence_threshold = 0.15
560
+ # options.fourcc = 'mp4v'
561
+
562
+ cmd = options_to_command(options)
563
+ print(cmd)
564
+ # import clipboard; clipboard.copy(cmd)
565
+
566
+ if False:
567
+ process_video_folder(options)
568
+
569
+
570
+ #%% Process a single video
571
+
572
+ fn = os.path.expanduser('~/tmp/video-test/test-video.mp4')
573
+ model_file = 'MDV5A'
574
+ input_video_file = fn
575
+ frame_folder = os.path.expanduser('~/tmp/video-test/frames')
576
+ rendering_folder = os.path.expanduser('~/tmp/video-test/rendered-frames')
577
+
578
+ options = ProcessVideoOptions()
579
+ options.model_file = model_file
580
+ options.input_video_file = input_video_file
581
+ options.frame_folder = frame_folder
582
+ options.frame_rendering_folder = rendering_folder
583
+ options.render_output_video = True
584
+ options.output_video_file = os.path.expanduser('~/tmp/video-test/detections.mp4')
585
+
586
+ cmd = options_to_command(options)
587
+ print(cmd)
588
+ # import clipboard; clipboard.copy(cmd)
589
+
590
+ if False:
591
+ process_video(options)
592
+
593
+
594
+ #%% Command-line driver
595
+
596
+ def main():
597
+
598
+ default_options = ProcessVideoOptions()
599
+
600
+ parser = argparse.ArgumentParser(description=(
601
+ 'Run MegaDetector on each frame (or every Nth frame) in a video (or folder of videos), optionally '\
602
+ 'producing a new video with detections annotated'))
603
+
604
+ parser.add_argument('model_file', type=str,
605
+ help='MegaDetector model file (.pt or .pb) or model name (e.g. "MDV5A")')
606
+
607
+ parser.add_argument('input_video_file', type=str,
608
+ help='video file (or folder) to process')
609
+
610
+ parser.add_argument('--recursive', action='store_true',
611
+ help='recurse into [input_video_file]; only meaningful if a folder '\
612
+ 'is specified as input')
613
+
614
+ parser.add_argument('--frame_folder', type=str, default=None,
615
+ help='folder to use for intermediate frame storage, defaults to a folder '\
616
+ 'in the system temporary folder')
617
+
618
+ parser.add_argument('--frame_rendering_folder', type=str, default=None,
619
+ help='folder to use for rendered frame storage, defaults to a folder in '\
620
+ 'the system temporary folder')
621
+
622
+ parser.add_argument('--output_json_file', type=str,
623
+ default=None, help='.json output file, defaults to [video file].json')
624
+
625
+ parser.add_argument('--output_video_file', type=str,
626
+ default=None, help='video output file (or folder), defaults to '\
627
+ '[video file].mp4 for files, or [video file]_annotated for folders')
628
+
629
+ parser.add_argument('--keep_extracted_frames',
630
+ action='store_true', help='Disable the deletion of extracted frames')
631
+
632
+ parser.add_argument('--reuse_frames_if_available',
633
+ action='store_true', help="Don't extract frames that are already available in the frame extraction folder")
634
+
635
+ parser.add_argument('--reuse_results_if_available',
636
+ action='store_true', help='If the output .json files exists, and this flag is set,'\
637
+ 'we\'ll skip running MegaDetector')
638
+
639
+ parser.add_argument('--render_output_video', action='store_true',
640
+ help='enable video output rendering (not rendered by default)')
641
+
642
+ parser.add_argument('--fourcc', default=default_fourcc,
643
+ help='fourcc code to use for video encoding (default {}), only used if render_output_video is True'.format(default_fourcc))
644
+
645
+ parser.add_argument('--keep_rendered_frames',
646
+ action='store_true', help='Disable the deletion of rendered (w/boxes) frames')
647
+
648
+ parser.add_argument('--force_extracted_frame_folder_deletion',
649
+ action='store_true', help='By default, when keep_extracted_frames is False, we '\
650
+ 'delete the frames, but leave the (probably-empty) folder in place. This option '\
651
+ 'forces deletion of the folder as well. Use at your own risk; does not check '\
652
+ 'whether other files were present in the folder.')
653
+
654
+ parser.add_argument('--force_rendered_frame_folder_deletion',
655
+ action='store_true', help='By default, when keep_rendered_frames is False, we '\
656
+ 'delete the frames, but leave the (probably-empty) folder in place. This option '\
657
+ 'forces deletion of the folder as well. Use at your own risk; does not check '\
658
+ 'whether other files were present in the folder.')
659
+
660
+ parser.add_argument('--rendering_confidence_threshold', type=float,
661
+ default=None, help="don't render boxes with confidence below this threshold (defaults to choosing based on the MD version)")
662
+
663
+ parser.add_argument('--json_confidence_threshold', type=float,
664
+ default=0.0, help="don't include boxes in the .json file with confidence "\
665
+ 'below this threshold (default {})'.format(
666
+ default_options.json_confidence_threshold))
667
+
668
+ parser.add_argument('--n_cores', type=int,
669
+ default=1, help='number of cores to use for frame separation and detection. '\
670
+ 'If using a GPU, this option will be respected for frame separation but '\
671
+ 'ignored for detection. Only relevant to frame separation when processing '\
672
+ 'a folder.')
673
+
674
+ parser.add_argument('--frame_sample', type=int,
675
+ default=None, help='process every Nth frame (defaults to every frame)')
676
+
677
+ parser.add_argument('--debug_max_frames', type=int,
678
+ default=-1, help='trim to N frames for debugging (impacts model execution, '\
679
+ 'not frame rendering)')
680
+
681
+ parser.add_argument('--class_mapping_filename',
682
+ type=str,
683
+ default=None, help='Use a non-default class mapping, supplied in a .json file '\
684
+ 'with a dictionary mapping int-strings to strings. This will also disable '\
685
+ 'the addition of "1" to all category IDs, so your class mapping should start '\
686
+ 'at zero.')
687
+
688
+ if len(sys.argv[1:]) == 0:
689
+ parser.print_help()
690
+ parser.exit()
691
+
692
+ args = parser.parse_args()
693
+ options = ProcessVideoOptions()
694
+ args_to_object(args,options)
695
+
696
+ if os.path.isdir(options.input_video_file):
697
+ process_video_folder(options)
698
+ else:
699
+ process_video(options)
700
+
701
+ if __name__ == '__main__':
702
+ main()