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,846 @@
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
+ import getpass
29
+
30
+ from uuid import uuid1
31
+
32
+ from megadetector.detection import run_detector_batch
33
+ from megadetector.visualization import visualize_detector_output
34
+ from megadetector.utils.ct_utils import args_to_object
35
+ from megadetector.utils.path_utils import insert_before_extension, clean_path
36
+ from megadetector.detection.video_utils import video_to_frames
37
+ from megadetector.detection.video_utils import frames_to_video
38
+ from megadetector.detection.video_utils import frame_results_to_video_results
39
+ from megadetector.detection.video_utils import video_folder_to_frames
40
+ from megadetector.detection.video_utils import default_fourcc
41
+
42
+
43
+ #%% Classes
44
+
45
+ class ProcessVideoOptions:
46
+ """
47
+ Options controlling the behavior of process_video()
48
+ """
49
+
50
+ def __init__(self):
51
+
52
+ #: Can be a model filename (.pt or .pb) or a model name (e.g. "MDV5A")
53
+ self.model_file = 'MDV5A'
54
+
55
+ #: Video (of folder of videos) to process
56
+ self.input_video_file = ''
57
+
58
+ #: .json file to which we should write results
59
+ self.output_json_file = None
60
+
61
+ #: File to which we should write a video with boxes, only relevant if
62
+ #: render_output_video is True
63
+ self.output_video_file = None
64
+
65
+ #: Folder to use for extracted frames; will use a folder in system temp space
66
+ #: if this is None
67
+ self.frame_folder = None
68
+
69
+ # Folder to use for rendered frames (if rendering output video); will use a folder
70
+ #: in system temp space if this is None
71
+ self.frame_rendering_folder = None
72
+
73
+ #: Should we render a video with detection boxes?
74
+ #:
75
+ #: Only supported when processing a single video, not a folder.
76
+ self.render_output_video = False
77
+
78
+ #: If we are rendering boxes to a new video, should we keep the temporary
79
+ #: rendered frames?
80
+ self.keep_rendered_frames = False
81
+
82
+ #: Should we keep the extracted frames?
83
+ self.keep_extracted_frames = False
84
+
85
+ #: Should we delete the entire folder the extracted frames are written to?
86
+ #:
87
+ #: By default, we delete the frame files but leave the (probably-empty) folder in place,
88
+ #: for no reason other than being paranoid about deleting folders.
89
+ self.force_extracted_frame_folder_deletion = False
90
+
91
+ #: Should we delete the entire folder the rendered frames are written to?
92
+ #:
93
+ #: By default, we delete the frame files but leave the (probably-empty) folder in place,
94
+ #: for no reason other than being paranoid about deleting folders.
95
+ self.force_rendered_frame_folder_deletion = False
96
+
97
+ #: If we've already run MegaDetector on this video or folder of videos, i.e. if we
98
+ #: find a corresponding MD results file, should we re-use it? Defaults to reprocessing.
99
+ self.reuse_results_if_available = False
100
+
101
+ #: If we've already split this video or folder of videos into frames, should we
102
+ #: we re-use those extracted frames? Defaults to reprocessing.
103
+ self.reuse_frames_if_available = False
104
+
105
+ #: If [input_video_file] is a folder, should we search for videos recursively?
106
+ self.recursive = False
107
+
108
+ #: Enable additional debug console output
109
+ self.verbose = False
110
+
111
+ #: fourcc code to use for writing videos; only relevant if render_output_video is True
112
+ self.fourcc = None
113
+
114
+ #: Confidence threshold to use for writing videos with boxes, only relevant if
115
+ #: if render_output_video is True. Defaults to choosing a reasonable threshold
116
+ #: based on the model version.
117
+ self.rendering_confidence_threshold = None
118
+
119
+ #: Detections below this threshold will not be included in the output file.
120
+ self.json_confidence_threshold = 0.005
121
+
122
+ #: Sample every Nth frame; set to None (default) or 1 to sample every frame. Typically
123
+ #: we sample down to around 3 fps, so for typical 30 fps videos, frame_sample=10 is a
124
+ #: typical value.
125
+ self.frame_sample = None
126
+
127
+ #: Number of workers to use for parallelization; set to <= 1 to disable parallelization
128
+ self.n_cores = 1
129
+
130
+ #: For debugging only, stop processing after a certain number of frames.
131
+ self.debug_max_frames = -1
132
+
133
+ #: File containing non-standard categories, typically only used if you're running a non-MD
134
+ #: detector.
135
+ self.class_mapping_filename = None
136
+
137
+ #: JPEG quality for frame output, from 0-100. Defaults to the opencv default (typically 95)
138
+ self.quality = 90
139
+
140
+ #: Resize frames so they're at most this wide
141
+ self.max_width = 1600
142
+
143
+ # ...class ProcessVideoOptions
144
+
145
+
146
+ #%% Functions
147
+
148
+ def _select_temporary_output_folders(options):
149
+ """
150
+ Choose folders in system temp space for writing temporary frames. Does not create folders,
151
+ just defines them.
152
+ """
153
+
154
+ tempdir = os.path.join(tempfile.gettempdir(), 'process_camera_trap_video')
155
+
156
+ # If we create a folder like "process_camera_trap_video" in the system temp dir, it may
157
+ # be the case that no one else can write to it, even to create user-specific subfolders.
158
+ # If we create a uuid-named folder in the system temp dir, we make a mess.
159
+ #
160
+ # Compromise with "process_camera_trap_video-[user]".
161
+ user_tempdir = tempdir + '-' + getpass.getuser()
162
+
163
+ # I don't know whether it's possible for a username to contain characters that are
164
+ # not valid filename characters, but just to be sure...
165
+ user_tempdir = clean_path(user_tempdir)
166
+
167
+ frame_output_folder = os.path.join(
168
+ user_tempdir, os.path.basename(options.input_video_file) + '_frames_' + str(uuid1()))
169
+
170
+ rendering_output_folder = os.path.join(
171
+ tempdir, os.path.basename(options.input_video_file) + '_detections_' + str(uuid1()))
172
+
173
+ temporary_folder_info = \
174
+ {
175
+ 'temp_folder_base':user_tempdir,
176
+ 'frame_output_folder':frame_output_folder,
177
+ 'rendering_output_folder':rendering_output_folder
178
+ }
179
+
180
+ return temporary_folder_info
181
+
182
+ # ...def _create_frame_output_folders(...)
183
+
184
+
185
+ def _clean_up_rendered_frames(options,rendering_output_folder,detected_frame_files):
186
+ """
187
+ If necessary, delete rendered frames and/or the entire rendering output folder.
188
+ """
189
+
190
+ caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
191
+
192
+ # (Optionally) delete the temporary directory we used for rendered detection images
193
+ if not options.keep_rendered_frames:
194
+
195
+ try:
196
+
197
+ # If (a) we're supposed to delete the temporary rendering folder no
198
+ # matter where it is and (b) we created it in temp space, delete the
199
+ # whole tree
200
+ if options.force_rendered_frame_folder_deletion and \
201
+ (not caller_provided_rendering_output_folder):
202
+
203
+ if options.verbose:
204
+ print('Recursively deleting rendered frame folder {}'.format(
205
+ rendering_output_folder))
206
+
207
+ shutil.rmtree(rendering_output_folder)
208
+
209
+ # ...otherwise just delete the frames, but leave the folder in place
210
+ else:
211
+
212
+ if options.force_rendered_frame_folder_deletion:
213
+ assert caller_provided_rendering_output_folder
214
+ print('Warning: force_rendered_frame_folder_deletion supplied with a ' + \
215
+ 'user-provided folder, only removing frames')
216
+
217
+ for rendered_frame_fn in detected_frame_files:
218
+ os.remove(rendered_frame_fn)
219
+
220
+ except Exception as e:
221
+ print('Warning: error deleting rendered frames from folder {}:\n{}'.format(
222
+ rendering_output_folder,str(e)))
223
+ pass
224
+
225
+ elif options.force_rendered_frame_folder_deletion:
226
+
227
+ print('Warning: keep_rendered_frames and force_rendered_frame_folder_deletion both ' + \
228
+ 'specified, not deleting')
229
+
230
+ # ...def _clean_up_rendered_frames(...)
231
+
232
+
233
+ def _clean_up_extracted_frames(options,frame_output_folder,frame_filenames):
234
+ """
235
+ If necessary, delete extracted frames and/or the entire temporary frame folder.
236
+ """
237
+
238
+ caller_provided_frame_output_folder = (options.frame_folder is not None)
239
+
240
+ if not options.keep_extracted_frames:
241
+
242
+ try:
243
+
244
+ # If (a) we're supposed to delete the temporary frame folder no
245
+ # matter where it is and (b) we created it in temp space, delete the
246
+ # whole tree.
247
+ if options.force_extracted_frame_folder_deletion and \
248
+ (not caller_provided_frame_output_folder):
249
+
250
+ if options.verbose:
251
+ print('Recursively deleting frame output folder {}'.format(frame_output_folder))
252
+
253
+ shutil.rmtree(frame_output_folder)
254
+
255
+ # ...otherwise just delete the frames, but leave the folder in place
256
+ else:
257
+
258
+ if options.force_extracted_frame_folder_deletion:
259
+ assert caller_provided_frame_output_folder
260
+ print('Warning: force_extracted_frame_folder_deletion supplied with a ' + \
261
+ 'user-provided folder, only removing frames')
262
+
263
+ for extracted_frame_fn in frame_filenames:
264
+ os.remove(extracted_frame_fn)
265
+
266
+ except Exception as e:
267
+ print('Warning: error removing extracted frames from folder {}:\n{}'.format(
268
+ frame_output_folder,str(e)))
269
+ pass
270
+
271
+ elif options.force_extracted_frame_folder_deletion:
272
+
273
+ print('Warning: keep_extracted_frames and force_extracted_frame_folder_deletion both ' + \
274
+ 'specified, not deleting')
275
+
276
+ # ...def _clean_up_extracted_frames
277
+
278
+
279
+ def process_video(options):
280
+ """
281
+ Process a single video through MD, optionally writing a new video with boxes
282
+
283
+ Args:
284
+ options (ProcessVideoOptions): all the parameters used to control this process,
285
+ including filenames; see ProcessVideoOptions for details
286
+
287
+ Returns:
288
+ dict: frame-level MegaDetector results, identical to what's in the output .json file
289
+ """
290
+
291
+ if options.output_json_file is None:
292
+ options.output_json_file = options.input_video_file + '.json'
293
+
294
+ if options.render_output_video and (options.output_video_file is None):
295
+ options.output_video_file = options.input_video_file + '.detections.mp4'
296
+
297
+ # Track whether frame and rendering folders were created by this script
298
+ caller_provided_frame_output_folder = (options.frame_folder is not None)
299
+ caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
300
+
301
+ # This does not create any folders, just defines temporary folder names in
302
+ # case we need them.
303
+ temporary_folder_info = _select_temporary_output_folders(options)
304
+
305
+ if (caller_provided_frame_output_folder):
306
+ frame_output_folder = options.frame_folder
307
+ else:
308
+ frame_output_folder = temporary_folder_info['frame_output_folder']
309
+
310
+ os.makedirs(frame_output_folder, exist_ok=True)
311
+
312
+ frame_filenames, Fs = video_to_frames(
313
+ options.input_video_file, frame_output_folder,
314
+ every_n_frames=options.frame_sample, overwrite=(not options.reuse_frames_if_available),
315
+ quality=options.quality, max_width=options.max_width, verbose=options.verbose)
316
+
317
+ image_file_names = frame_filenames
318
+ if options.debug_max_frames > 0:
319
+ image_file_names = image_file_names[0:options.debug_max_frames]
320
+
321
+ if options.reuse_results_if_available and \
322
+ os.path.isfile(options.output_json_file):
323
+ print('Loading results from {}'.format(options.output_json_file))
324
+ with open(options.output_json_file,'r') as f:
325
+ results = json.load(f)
326
+ else:
327
+ results = run_detector_batch.load_and_run_detector_batch(
328
+ options.model_file, image_file_names,
329
+ confidence_threshold=options.json_confidence_threshold,
330
+ n_cores=options.n_cores,
331
+ class_mapping_filename=options.class_mapping_filename,
332
+ quiet=True)
333
+
334
+ run_detector_batch.write_results_to_file(
335
+ results, options.output_json_file,
336
+ relative_path_base=frame_output_folder,
337
+ detector_file=options.model_file,
338
+ custom_metadata={'video_frame_rate':Fs})
339
+
340
+
341
+ ## (Optionally) render output video
342
+
343
+ if options.render_output_video:
344
+
345
+ # Render detections to images
346
+ if (caller_provided_rendering_output_folder):
347
+ rendering_output_dir = options.frame_rendering_folder
348
+ else:
349
+ rendering_output_dir = temporary_folder_info['rendering_output_folder']
350
+
351
+ os.makedirs(rendering_output_dir,exist_ok=True)
352
+
353
+ detected_frame_files = visualize_detector_output.visualize_detector_output(
354
+ detector_output_path=options.output_json_file,
355
+ out_dir=rendering_output_dir,
356
+ images_dir=frame_output_folder,
357
+ confidence_threshold=options.rendering_confidence_threshold)
358
+
359
+ # Combine into a video
360
+ if options.frame_sample is None:
361
+ rendering_fs = Fs
362
+ else:
363
+ rendering_fs = Fs / options.frame_sample
364
+
365
+ print('Rendering {} frames to {} at {} fps (original video {} fps)'.format(
366
+ len(detected_frame_files), options.output_video_file,rendering_fs,Fs))
367
+ frames_to_video(detected_frame_files, rendering_fs, options.output_video_file,
368
+ codec_spec=options.fourcc)
369
+
370
+ # Possibly clean up rendered frames
371
+ _clean_up_rendered_frames(options,rendering_output_dir,detected_frame_files)
372
+
373
+ # ...if we're rendering video
374
+
375
+
376
+ ## (Optionally) delete the extracted frames
377
+ _clean_up_extracted_frames(options, frame_output_folder, frame_filenames)
378
+
379
+ # ...process_video()
380
+
381
+
382
+ def process_video_folder(options):
383
+ """
384
+ Process a folder of videos through MD
385
+
386
+ Args:
387
+ options (ProcessVideoOptions): all the parameters used to control this process,
388
+ including filenames; see ProcessVideoOptions for details
389
+ """
390
+
391
+ ## Validate options
392
+
393
+ assert os.path.isdir(options.input_video_file), \
394
+ '{} is not a folder'.format(options.input_video_file)
395
+
396
+ assert options.output_json_file is not None, \
397
+ 'When processing a folder, you must specify an output .json file'
398
+
399
+ assert options.output_json_file.endswith('.json')
400
+ video_json = options.output_json_file
401
+ frames_json = options.output_json_file.replace('.json','.frames.json')
402
+ os.makedirs(os.path.dirname(video_json),exist_ok=True)
403
+
404
+ # Track whether frame and rendering folders were created by this script
405
+ caller_provided_frame_output_folder = (options.frame_folder is not None)
406
+ caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
407
+
408
+ # This does not create any folders, just defines temporary folder names in
409
+ # case we need them.
410
+ temporary_folder_info = _select_temporary_output_folders(options)
411
+
412
+
413
+ ## Split every video into frames
414
+
415
+ if caller_provided_frame_output_folder:
416
+ frame_output_folder = options.frame_folder
417
+ else:
418
+ frame_output_folder = temporary_folder_info['frame_output_folder']
419
+
420
+ os.makedirs(frame_output_folder, exist_ok=True)
421
+
422
+ print('Extracting frames')
423
+ frame_filenames, Fs, video_filenames = \
424
+ video_folder_to_frames(input_folder=options.input_video_file,
425
+ output_folder_base=frame_output_folder,
426
+ recursive=options.recursive,
427
+ overwrite=(not options.reuse_frames_if_available),
428
+ n_threads=options.n_cores,
429
+ every_n_frames=options.frame_sample,
430
+ verbose=options.verbose,
431
+ quality=options.quality,
432
+ max_width=options.max_width)
433
+
434
+ image_file_names = list(itertools.chain.from_iterable(frame_filenames))
435
+
436
+ if len(image_file_names) == 0:
437
+ if len(video_filenames) == 0:
438
+ print('No videos found in folder {}'.format(options.input_video_file))
439
+ else:
440
+ print('No frames extracted from folder {}, this may be due to an '\
441
+ 'unsupported video codec'.format(options.input_video_file))
442
+ return
443
+
444
+ if options.debug_max_frames is not None and options.debug_max_frames > 0:
445
+ image_file_names = image_file_names[0:options.debug_max_frames]
446
+
447
+
448
+ ## Run MegaDetector on the extracted frames
449
+
450
+ if options.reuse_results_if_available and \
451
+ os.path.isfile(frames_json):
452
+ print('Bypassing inference, loading results from {}'.format(frames_json))
453
+ results = None
454
+ else:
455
+ print('Running MegaDetector')
456
+ results = run_detector_batch.load_and_run_detector_batch(
457
+ options.model_file, image_file_names,
458
+ confidence_threshold=options.json_confidence_threshold,
459
+ n_cores=options.n_cores,
460
+ class_mapping_filename=options.class_mapping_filename,
461
+ quiet=True)
462
+
463
+ run_detector_batch.write_results_to_file(
464
+ results, frames_json,
465
+ relative_path_base=frame_output_folder,
466
+ detector_file=options.model_file,
467
+ custom_metadata={'video_frame_rate':Fs})
468
+
469
+
470
+ ## Convert frame-level results to video-level results
471
+
472
+ print('Converting frame-level results to video-level results')
473
+ frame_results_to_video_results(frames_json,video_json)
474
+
475
+
476
+ ## (Optionally) render output videos
477
+
478
+ if options.render_output_video:
479
+
480
+ # Render detections to images
481
+ if (caller_provided_rendering_output_folder):
482
+ rendering_output_dir = options.frame_rendering_folder
483
+ else:
484
+ rendering_output_dir = temporary_folder_info['rendering_output_folder']
485
+
486
+ os.makedirs(rendering_output_dir,exist_ok=True)
487
+
488
+ detected_frame_files = visualize_detector_output.visualize_detector_output(
489
+ detector_output_path=frames_json,
490
+ out_dir=rendering_output_dir,
491
+ images_dir=frame_output_folder,
492
+ confidence_threshold=options.rendering_confidence_threshold,
493
+ preserve_path_structure=True,
494
+ output_image_width=-1)
495
+ detected_frame_files = [s.replace('\\','/') for s in detected_frame_files]
496
+
497
+ # Choose an output folder
498
+ output_folder_is_input_folder = False
499
+ if options.output_video_file is not None:
500
+ if os.path.isfile(options.output_video_file):
501
+ raise ValueError('Rendering videos for a folder, but an existing file was specified as output')
502
+ elif options.output_video_file == options.input_video_file:
503
+ output_folder_is_input_folder = True
504
+ output_video_folder = options.input_video_file
505
+ else:
506
+ os.makedirs(options.output_video_file,exist_ok=True)
507
+ output_video_folder = options.output_video_file
508
+ else:
509
+ output_folder_is_input_folder = True
510
+ output_video_folder = options.input_video_file
511
+
512
+ # For each video
513
+ #
514
+ # TODO: parallelize this loop
515
+ #
516
+ # i_video=0; input_video_file_abs = video_filenames[i_video]
517
+ for i_video,input_video_file_abs in enumerate(video_filenames):
518
+
519
+ video_fs = Fs[i_video]
520
+
521
+ if options.frame_sample is None:
522
+ rendering_fs = video_fs
523
+ else:
524
+ rendering_fs = video_fs / options.frame_sample
525
+
526
+ input_video_file_relative = os.path.relpath(input_video_file_abs,options.input_video_file)
527
+ video_frame_output_folder = os.path.join(rendering_output_dir,input_video_file_relative)
528
+
529
+ video_frame_output_folder = video_frame_output_folder.replace('\\','/')
530
+ assert os.path.isdir(video_frame_output_folder), \
531
+ 'Could not find frame folder for video {}'.format(input_video_file_relative)
532
+
533
+ # Find the corresponding rendered frame folder
534
+ video_frame_files = [fn for fn in detected_frame_files if \
535
+ fn.startswith(video_frame_output_folder)]
536
+ assert len(video_frame_files) > 0, 'Could not find rendered frames for video {}'.format(
537
+ input_video_file_relative)
538
+
539
+ # Select the output filename for the rendered video
540
+ if output_folder_is_input_folder:
541
+ video_output_file = insert_before_extension(input_video_file_abs,'annotated','_')
542
+ else:
543
+ video_output_file = os.path.join(output_video_folder,input_video_file_relative)
544
+
545
+ os.makedirs(os.path.dirname(video_output_file),exist_ok=True)
546
+
547
+ # Create the output video
548
+ print('Rendering detections for video {} to {} at {} fps (original video {} fps)'.format(
549
+ input_video_file_relative,video_output_file,rendering_fs,video_fs))
550
+ frames_to_video(video_frame_files, rendering_fs, video_output_file, codec_spec=options.fourcc)
551
+
552
+ # ...for each video
553
+
554
+ # Possibly clean up rendered frames
555
+ _clean_up_rendered_frames(options,rendering_output_dir,detected_frame_files)
556
+
557
+ # ...if we're rendering video
558
+
559
+
560
+ ## (Optionally) delete the extracted frames
561
+ _clean_up_extracted_frames(options, frame_output_folder, image_file_names)
562
+
563
+ # ...process_video_folder()
564
+
565
+
566
+ def options_to_command(options):
567
+ """
568
+ Convert a ProcessVideoOptions obejct to a corresponding command line.
569
+
570
+ Args:
571
+ options (ProcessVideoOptions): the options set to render as a command line
572
+
573
+ Returns:
574
+ str: the command line coresponding to [options]
575
+
576
+ :meta private:
577
+ """
578
+ cmd = 'python process_video.py'
579
+ cmd += ' "' + options.model_file + '"'
580
+ cmd += ' "' + options.input_video_file + '"'
581
+
582
+ if options.recursive:
583
+ cmd += ' --recursive'
584
+ if options.frame_folder is not None:
585
+ cmd += ' --frame_folder' + ' "' + options.frame_folder + '"'
586
+ if options.frame_rendering_folder is not None:
587
+ cmd += ' --frame_rendering_folder' + ' "' + options.frame_rendering_folder + '"'
588
+ if options.output_json_file is not None:
589
+ cmd += ' --output_json_file' + ' "' + options.output_json_file + '"'
590
+ if options.output_video_file is not None:
591
+ cmd += ' --output_video_file' + ' "' + options.output_video_file + '"'
592
+ if options.keep_extracted_frames:
593
+ cmd += ' --keep_extracted_frames'
594
+ if options.reuse_results_if_available:
595
+ cmd += ' --reuse_results_if_available'
596
+ if options.reuse_frames_if_available:
597
+ cmd += ' --reuse_frames_if_available'
598
+ if options.render_output_video:
599
+ cmd += ' --render_output_video'
600
+ if options.keep_rendered_frames:
601
+ cmd += ' --keep_rendered_frames'
602
+ if options.rendering_confidence_threshold is not None:
603
+ cmd += ' --rendering_confidence_threshold ' + str(options.rendering_confidence_threshold)
604
+ if options.json_confidence_threshold is not None:
605
+ cmd += ' --json_confidence_threshold ' + str(options.json_confidence_threshold)
606
+ if options.n_cores is not None:
607
+ cmd += ' --n_cores ' + str(options.n_cores)
608
+ if options.frame_sample is not None:
609
+ cmd += ' --frame_sample ' + str(options.frame_sample)
610
+ if options.debug_max_frames is not None:
611
+ cmd += ' --debug_max_frames ' + str(options.debug_max_frames)
612
+ if options.class_mapping_filename is not None:
613
+ cmd += ' --class_mapping_filename ' + str(options.class_mapping_filename)
614
+ if options.fourcc is not None:
615
+ cmd += ' --fourcc ' + options.fourcc
616
+ if options.quality is not None:
617
+ cmd += ' --quality ' + str(options.quality)
618
+ if options.max_width is not None:
619
+ cmd += ' --max_width ' + str(options.max_width)
620
+ if options.verbose:
621
+ cmd += ' --verbose'
622
+ if options.force_extracted_frame_folder_deletion:
623
+ cmd += ' --force_extracted_frame_folder_deletion'
624
+ if options.force_rendered_frame_folder_deletion:
625
+ cmd += ' --force_rendered_frame_folder_deletion'
626
+
627
+ return cmd
628
+
629
+
630
+ #%% Interactive driver
631
+
632
+ if False:
633
+
634
+ #%% Process a folder of videos
635
+
636
+ model_file = 'MDV5A'
637
+ input_dir = r'g:\temp\test-videos'
638
+ output_base = r'g:\temp\video_test'
639
+ frame_folder = os.path.join(output_base,'frames')
640
+ rendering_folder = os.path.join(output_base,'rendered-frames')
641
+ output_json_file = os.path.join(output_base,'video-test.json')
642
+ output_video_folder = os.path.join(output_base,'output_videos')
643
+
644
+ print('Processing folder {}'.format(input_dir))
645
+
646
+ options = ProcessVideoOptions()
647
+ options.model_file = model_file
648
+ options.input_video_file = input_dir
649
+ options.output_video_file = output_video_folder
650
+ options.output_json_file = output_json_file
651
+ options.recursive = True
652
+ options.reuse_frames_if_available = False
653
+ options.reuse_results_if_available = False
654
+ options.quality = 90
655
+ options.frame_sample = 10
656
+ options.max_width = 1280
657
+ options.n_cores = 5
658
+ options.verbose = True
659
+ options.render_output_video = True
660
+
661
+ options.frame_folder = None # frame_folder
662
+ options.frame_rendering_folder = None # rendering_folder
663
+
664
+ options.keep_extracted_frames = False
665
+ options.keep_rendered_frames = False
666
+ options.force_extracted_frame_folder_deletion = True
667
+ options.force_rendered_frame_folder_deletion = True
668
+
669
+ # options.confidence_threshold = 0.15
670
+ options.fourcc = 'mp4v'
671
+
672
+ cmd = options_to_command(options); print(cmd)
673
+
674
+ import clipboard; clipboard.copy(cmd)
675
+
676
+ if False:
677
+ process_video_folder(options)
678
+
679
+
680
+ #%% Process a single video
681
+
682
+ fn = r'g:\temp\test-videos\person_and_dog\DSCF0056.AVI'
683
+ model_file = 'MDV5A'
684
+ input_video_file = fn
685
+
686
+ output_base = r'g:\temp\video_test'
687
+ frame_folder = os.path.join(output_base,'frames')
688
+ rendering_folder = os.path.join(output_base,'rendered-frames')
689
+ output_json_file = os.path.join(output_base,'video-test.json')
690
+ output_video_file = os.path.join(output_base,'output_videos.mp4')
691
+
692
+ options = ProcessVideoOptions()
693
+ options.model_file = model_file
694
+ options.input_video_file = input_video_file
695
+ options.render_output_video = True
696
+ options.output_video_file = output_video_file
697
+
698
+ options.verbose = True
699
+
700
+ options.quality = 75
701
+ options.frame_sample = None # 10
702
+ options.max_width = 600
703
+
704
+ options.frame_folder = None # frame_folder
705
+ options.frame_rendering_folder = None # rendering_folder
706
+
707
+ options.keep_extracted_frames = False
708
+ options.keep_rendered_frames = False
709
+ options.force_extracted_frame_folder_deletion = True
710
+ options.force_rendered_frame_folder_deletion = True
711
+
712
+ # options.confidence_threshold = 0.15
713
+ options.fourcc = 'mp4v'
714
+
715
+ cmd = options_to_command(options); print(cmd)
716
+
717
+ import clipboard; clipboard.copy(cmd)
718
+
719
+ if False:
720
+ process_video(options)
721
+
722
+
723
+ #%% Command-line driver
724
+
725
+ def main():
726
+
727
+ default_options = ProcessVideoOptions()
728
+
729
+ parser = argparse.ArgumentParser(description=(
730
+ 'Run MegaDetector on each frame (or every Nth frame) in a video (or folder of videos), optionally '\
731
+ 'producing a new video with detections annotated'))
732
+
733
+ parser.add_argument('model_file', type=str,
734
+ help='MegaDetector model file (.pt or .pb) or model name (e.g. "MDV5A")')
735
+
736
+ parser.add_argument('input_video_file', type=str,
737
+ help='video file (or folder) to process')
738
+
739
+ parser.add_argument('--recursive', action='store_true',
740
+ help='recurse into [input_video_file]; only meaningful if a folder '\
741
+ 'is specified as input')
742
+
743
+ parser.add_argument('--frame_folder', type=str, default=None,
744
+ help='folder to use for intermediate frame storage, defaults to a folder '\
745
+ 'in the system temporary folder')
746
+
747
+ parser.add_argument('--frame_rendering_folder', type=str, default=None,
748
+ help='folder to use for rendered frame storage, defaults to a folder in '\
749
+ 'the system temporary folder')
750
+
751
+ parser.add_argument('--output_json_file', type=str,
752
+ default=None, help='.json output file, defaults to [video file].json')
753
+
754
+ parser.add_argument('--output_video_file', type=str,
755
+ default=None, help='video output file (or folder), defaults to '\
756
+ '[video file].mp4 for files, or [video file]_annotated for folders')
757
+
758
+ parser.add_argument('--keep_extracted_frames',
759
+ action='store_true', help='Disable the deletion of extracted frames')
760
+
761
+ parser.add_argument('--reuse_frames_if_available',
762
+ action='store_true', help="Don't extract frames that are already available in the frame extraction folder")
763
+
764
+ parser.add_argument('--reuse_results_if_available',
765
+ action='store_true', help='If the output .json files exists, and this flag is set,'\
766
+ 'we\'ll skip running MegaDetector')
767
+
768
+ parser.add_argument('--render_output_video', action='store_true',
769
+ help='enable video output rendering (not rendered by default)')
770
+
771
+ parser.add_argument('--fourcc', default=default_fourcc,
772
+ help='fourcc code to use for video encoding (default {}), only used if render_output_video is True'.format(
773
+ default_fourcc))
774
+
775
+ parser.add_argument('--keep_rendered_frames',
776
+ action='store_true', help='Disable the deletion of rendered (w/boxes) frames')
777
+
778
+ parser.add_argument('--force_extracted_frame_folder_deletion',
779
+ action='store_true', help='By default, when keep_extracted_frames is False, we '\
780
+ 'delete the frames, but leave the (probably-empty) folder in place. This option '\
781
+ 'forces deletion of the folder as well. Use at your own risk; does not check '\
782
+ 'whether other files were present in the folder.')
783
+
784
+ parser.add_argument('--force_rendered_frame_folder_deletion',
785
+ action='store_true', help='By default, when keep_rendered_frames is False, we '\
786
+ 'delete the frames, but leave the (probably-empty) folder in place. This option '\
787
+ 'forces deletion of the folder as well. Use at your own risk; does not check '\
788
+ 'whether other files were present in the folder.')
789
+
790
+ parser.add_argument('--rendering_confidence_threshold', type=float,
791
+ default=None, help="don't render boxes with confidence below this threshold (defaults to choosing based on the MD version)")
792
+
793
+ parser.add_argument('--json_confidence_threshold', type=float,
794
+ default=0.0, help="don't include boxes in the .json file with confidence "\
795
+ 'below this threshold (default {})'.format(
796
+ default_options.json_confidence_threshold))
797
+
798
+ parser.add_argument('--n_cores', type=int,
799
+ default=1, help='Number of cores to use for frame separation and detection. '\
800
+ 'If using a GPU, this option will be respected for frame separation but '\
801
+ 'ignored for detection. Only relevant to frame separation when processing '\
802
+ 'a folder.')
803
+
804
+ parser.add_argument('--frame_sample', type=int,
805
+ default=None, help='process every Nth frame (defaults to every frame)')
806
+
807
+ parser.add_argument('--quality', type=int,
808
+ default=default_options.quality,
809
+ help='JPEG quality for extracted frames (defaults to {})'.format(
810
+ default_options.quality))
811
+
812
+ parser.add_argument('--max_width', type=int,
813
+ default=default_options.max_width,
814
+ help='Resize frames larger than this before writing (defaults to {})'.format(
815
+ default_options.max_width))
816
+
817
+ parser.add_argument('--debug_max_frames', type=int,
818
+ default=-1, help='Trim to N frames for debugging (impacts model execution, '\
819
+ 'not frame rendering)')
820
+
821
+ parser.add_argument('--class_mapping_filename',
822
+ type=str,
823
+ default=None, help='Use a non-default class mapping, supplied in a .json file '\
824
+ 'with a dictionary mapping int-strings to strings. This will also disable '\
825
+ 'the addition of "1" to all category IDs, so your class mapping should start '\
826
+ 'at zero.')
827
+
828
+ parser.add_argument('--verbose', action='store_true',
829
+ help='Enable additional debug output')
830
+
831
+
832
+ if len(sys.argv[1:]) == 0:
833
+ parser.print_help()
834
+ parser.exit()
835
+
836
+ args = parser.parse_args()
837
+ options = ProcessVideoOptions()
838
+ args_to_object(args,options)
839
+
840
+ if os.path.isdir(options.input_video_file):
841
+ process_video_folder(options)
842
+ else:
843
+ process_video(options)
844
+
845
+ if __name__ == '__main__':
846
+ main()