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,698 @@
1
+ """
2
+
3
+ video_utils.py
4
+
5
+ Utilities for splitting, rendering, and assembling videos.
6
+
7
+ """
8
+
9
+ #%% Constants, imports, environment
10
+
11
+ import os
12
+ import cv2
13
+ import glob
14
+ import json
15
+
16
+ from collections import defaultdict
17
+ from multiprocessing.pool import ThreadPool
18
+ from multiprocessing.pool import Pool
19
+ from tqdm import tqdm
20
+ from functools import partial
21
+ from inspect import signature
22
+
23
+ from megadetector.utils import path_utils
24
+ from megadetector.visualization import visualization_utils as vis_utils
25
+
26
+ default_fourcc = 'h264'
27
+
28
+
29
+ #%% Path utilities
30
+
31
+ VIDEO_EXTENSIONS = ('.mp4','.avi','.mpeg','.mpg')
32
+
33
+ def is_video_file(s,video_extensions=VIDEO_EXTENSIONS):
34
+ """
35
+ Checks a file's extension against a set of known video file
36
+ extensions to determine whether it's a video file. Performs a
37
+ case-insensitive comparison.
38
+
39
+ Args:
40
+ s (str): filename to check for probable video-ness
41
+ video_extensions (list, optional): list of video file extensions
42
+
43
+ Returns:
44
+ bool: True if this looks like a video file, else False
45
+ """
46
+
47
+ ext = os.path.splitext(s)[1]
48
+ return ext.lower() in video_extensions
49
+
50
+
51
+ def find_video_strings(strings):
52
+ """
53
+ Given a list of strings that are potentially video file names, looks for
54
+ strings that actually look like video file names (based on extension).
55
+
56
+ Args:
57
+ strings (list): list of strings to check for video-ness
58
+
59
+ Returns:
60
+ list: a subset of [strings] that looks like they are video filenames
61
+ """
62
+
63
+ return [s for s in strings if is_video_file(s.lower())]
64
+
65
+
66
+ def find_videos(dirname,
67
+ recursive=False,
68
+ convert_slashes=True,
69
+ return_relative_paths=False):
70
+ """
71
+ Finds all files in a directory that look like video file names.
72
+
73
+ Args:
74
+ dirname (str): folder to search for video files
75
+ recursive (bool, optional): whether to search [dirname] recursively
76
+ convert_slashes (bool, optional): forces forward slashes in the returned files,
77
+ otherwise uses the native path separator
78
+ return_relative_paths (bool, optional): forces the returned filenames to be
79
+ relative to [dirname], otherwise returns absolute paths
80
+
81
+ Returns:
82
+ A list of filenames within [dirname] that appear to be videos
83
+ """
84
+
85
+ if recursive:
86
+ files = glob.glob(os.path.join(dirname, '**', '*.*'), recursive=True)
87
+ else:
88
+ files = glob.glob(os.path.join(dirname, '*.*'))
89
+
90
+ if return_relative_paths:
91
+ files = [os.path.relpath(fn,dirname) for fn in files]
92
+
93
+ if convert_slashes:
94
+ files = [fn.replace('\\', '/') for fn in files]
95
+
96
+ files = [fn for fn in files if os.path.isfile(fn)]
97
+
98
+ return find_video_strings(files)
99
+
100
+
101
+ #%% Function for rendering frames to video and vice-versa
102
+
103
+ # http://tsaith.github.io/combine-images-into-a-video-with-python-3-and-opencv-3.html
104
+
105
+ def frames_to_video(images, Fs, output_file_name, codec_spec=default_fourcc):
106
+ """
107
+ Given a list of image files and a sample rate, concatenates those images into
108
+ a video and writes to a new video file.
109
+
110
+ Args:
111
+ images (list): a list of frame file names to concatenate into a video
112
+ Fs (float): the frame rate in fps
113
+ output_file_name (str): the output video file, no checking is performed to make
114
+ sure the extension is compatible with the codec
115
+ codec_spec (str, optional): codec to use for encoding; h264 is a sensible default
116
+ and generally works on Windows, but when this fails (which is around 50% of the time
117
+ on Linux), mp4v is a good second choice
118
+ """
119
+
120
+ if codec_spec is None:
121
+ codec_spec = 'h264'
122
+
123
+ if len(images) == 0:
124
+ print('Warning: no frames to render')
125
+ return
126
+
127
+ os.makedirs(os.path.dirname(output_file_name),exist_ok=True)
128
+
129
+ # Determine the width and height from the first image
130
+ frame = cv2.imread(images[0])
131
+ cv2.imshow('video',frame)
132
+ height, width, channels = frame.shape
133
+
134
+ # Define the codec and create VideoWriter object
135
+ fourcc = cv2.VideoWriter_fourcc(*codec_spec)
136
+ out = cv2.VideoWriter(output_file_name, fourcc, Fs, (width, height))
137
+
138
+ for image in images:
139
+ frame = cv2.imread(image)
140
+ out.write(frame)
141
+
142
+ out.release()
143
+ cv2.destroyAllWindows()
144
+
145
+
146
+ def get_video_fs(input_video_file):
147
+ """
148
+ Retrieves the frame rate of [input_video_file].
149
+
150
+ Args:
151
+ input_video_file (str): video file for which we want the frame rate
152
+
153
+ Returns:
154
+ float: the frame rate of [input_video_file]
155
+ """
156
+
157
+ assert os.path.isfile(input_video_file), 'File {} not found'.format(input_video_file)
158
+ vidcap = cv2.VideoCapture(input_video_file)
159
+ Fs = vidcap.get(cv2.CAP_PROP_FPS)
160
+ vidcap.release()
161
+ return Fs
162
+
163
+
164
+ def _frame_number_to_filename(frame_number):
165
+ """
166
+ Ensures that frame images are given consistent filenames.
167
+ """
168
+
169
+ return 'frame{:06d}.jpg'.format(frame_number)
170
+
171
+
172
+ def video_to_frames(input_video_file, output_folder, overwrite=True,
173
+ every_n_frames=None, verbose=False, quality=None,
174
+ max_width=None):
175
+ """
176
+ Renders frames from [input_video_file] to a .jpg in [output_folder].
177
+
178
+ With help from:
179
+
180
+ https://stackoverflow.com/questions/33311153/python-extracting-and-saving-video-frames
181
+
182
+ Args:
183
+ input_video_file (str): video file to split into frames
184
+ output_folder (str): folder to put frame images in
185
+ overwrite (bool, optional): whether to overwrite existing frame images
186
+ every_n_frames (int, optional): sample every Nth frame starting from the first frame;
187
+ if this is None or 1, every frame is extracted
188
+ verbose (bool, optional): enable additional debug console output
189
+ quality (int, optional): JPEG quality for frame output, from 0-100. Defaults
190
+ to the opencv default (typically 95).
191
+ max_width (int, optional): resize frames to be no wider than [max_width]
192
+
193
+ Returns:
194
+ tuple: length-2 tuple containing (list of frame filenames,frame rate)
195
+ """
196
+
197
+ assert os.path.isfile(input_video_file), 'File {} not found'.format(input_video_file)
198
+
199
+ vidcap = cv2.VideoCapture(input_video_file)
200
+ n_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
201
+ Fs = vidcap.get(cv2.CAP_PROP_FPS)
202
+
203
+ # If we're not over-writing, check whether all frame images already exist
204
+ if overwrite == False:
205
+
206
+ missing_frame_number = None
207
+ missing_frame_filename = None
208
+ frame_filenames = []
209
+ found_existing_frame = False
210
+
211
+ for frame_number in range(0,n_frames):
212
+
213
+ if every_n_frames is not None:
214
+ if (frame_number % every_n_frames) != 0:
215
+ continue
216
+
217
+ frame_filename = _frame_number_to_filename(frame_number)
218
+ frame_filename = os.path.join(output_folder,frame_filename)
219
+ frame_filenames.append(frame_filename)
220
+ if os.path.isfile(frame_filename):
221
+ found_existing_frame = True
222
+ continue
223
+ else:
224
+ missing_frame_number = frame_number
225
+ missing_frame_filename = frame_filename
226
+ break
227
+
228
+ if verbose and missing_frame_number is not None:
229
+ print('Missing frame {} ({}) for video {}'.format(
230
+ missing_frame_number,
231
+ missing_frame_filename,
232
+ input_video_file))
233
+
234
+ # OpenCV seems to over-report the number of frames by 1 in some cases, or fails
235
+ # to read the last frame; either way, I'm allowing one missing frame.
236
+ allow_last_frame_missing = True
237
+
238
+ # This doesn't have to mean literally the last frame number, it just means that if
239
+ # we find this frame or later, we consider the video done
240
+ last_expected_frame_number = n_frames-1
241
+ if every_n_frames is not None:
242
+ last_expected_frame_number -= (every_n_frames*2)
243
+
244
+ # If no frames are missing, or only frames very close to the end of the video are "missing",
245
+ # skip this video
246
+ if (missing_frame_number is None) or \
247
+ (allow_last_frame_missing and (missing_frame_number >= last_expected_frame_number)):
248
+ if verbose:
249
+ print('Skipping video {}, all output frames exist'.format(input_video_file))
250
+ return frame_filenames,Fs
251
+ else:
252
+ # If we found some frames, but not all, print a message
253
+ if verbose and found_existing_frame:
254
+ print("Rendering video {}, couldn't find frame {} ({}) of {}".format(
255
+ input_video_file,
256
+ missing_frame_number,
257
+ missing_frame_filename,
258
+ last_expected_frame_number))
259
+
260
+ # ...if we need to check whether to skip this video entirely
261
+
262
+ if verbose:
263
+ print('Reading {} frames at {} Hz from {}'.format(n_frames,Fs,input_video_file))
264
+
265
+ frame_filenames = []
266
+
267
+ # YOLOv5 does some totally bananas monkey-patching of opencv,
268
+ # which causes problems if we try to supply a third parameter to
269
+ # imwrite (to specify JPEG quality). Detect this case, and ignore the quality
270
+ # parameter if it looks like imwrite has been messed with.
271
+ imwrite_patched = False
272
+ n_imwrite_parameters = None
273
+
274
+ try:
275
+ # calling signature() on the native cv2.imwrite function will
276
+ # fail, so an exception here is a good thing. In fact I don't think
277
+ # there's a case where this *succeeds* and the number of parameters
278
+ # is wrong.
279
+ sig = signature(cv2.imwrite)
280
+ n_imwrite_parameters = len(sig.parameters)
281
+ except Exception:
282
+ pass
283
+
284
+ if (n_imwrite_parameters is not None) and (n_imwrite_parameters < 3):
285
+ imwrite_patched = True
286
+ if verbose and (quality is not None):
287
+ print('Warning: quality value supplied, but YOLOv5 has mucked with cv2.imwrite, ignoring quality')
288
+
289
+ # for frame_number in tqdm(range(0,n_frames)):
290
+ for frame_number in range(0,n_frames):
291
+
292
+ success,image = vidcap.read()
293
+ if not success:
294
+ assert image is None
295
+ if verbose:
296
+ print('Read terminating at frame {} of {}'.format(frame_number,n_frames))
297
+ break
298
+
299
+ if every_n_frames is not None:
300
+ if frame_number % every_n_frames != 0:
301
+ continue
302
+
303
+ # Has resizing been requested?
304
+ if max_width is not None:
305
+
306
+ # image.shape is h/w/dims
307
+ input_shape = image.shape
308
+ assert input_shape[2] == 3
309
+ input_width = input_shape[1]
310
+
311
+ # Is resizing necessary?
312
+ if input_width > max_width:
313
+
314
+ scale = max_width / input_width
315
+ assert scale <= 1.0
316
+
317
+ # INTER_AREA is recommended for size reduction
318
+ image = cv2.resize(image, (0,0), fx=scale, fy=scale, interpolation=cv2.INTER_AREA)
319
+
320
+ # ...if we need to deal with resizing
321
+
322
+ frame_filename = _frame_number_to_filename(frame_number)
323
+ frame_filename = os.path.join(output_folder,frame_filename)
324
+ frame_filenames.append(frame_filename)
325
+
326
+ if overwrite == False and os.path.isfile(frame_filename):
327
+ # print('Skipping frame {}'.format(frame_filename))
328
+ pass
329
+ else:
330
+ try:
331
+ if frame_filename.isascii():
332
+
333
+ if quality is None or imwrite_patched:
334
+ cv2.imwrite(os.path.normpath(frame_filename),image)
335
+ else:
336
+ cv2.imwrite(os.path.normpath(frame_filename),image,
337
+ [int(cv2.IMWRITE_JPEG_QUALITY), quality])
338
+ else:
339
+ if quality is None:
340
+ is_success, im_buf_arr = cv2.imencode('.jpg', image)
341
+ else:
342
+ encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), quality]
343
+ is_success, im_buf_arr = cv2.imencode('.jpg', image, encode_param)
344
+ im_buf_arr.tofile(frame_filename)
345
+ assert os.path.isfile(frame_filename), \
346
+ 'Output frame {} unavailable'.format(frame_filename)
347
+ except KeyboardInterrupt:
348
+ vidcap.release()
349
+ raise
350
+ except Exception as e:
351
+ print('Error on frame {} of {}: {}'.format(frame_number,n_frames,str(e)))
352
+
353
+ if verbose:
354
+ print('\nExtracted {} of {} frames for {}'.format(
355
+ len(frame_filenames),n_frames,input_video_file))
356
+
357
+ vidcap.release()
358
+ return frame_filenames,Fs
359
+
360
+ # ...def video_to_frames(...)
361
+
362
+
363
+ def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,
364
+ every_n_frames,overwrite,verbose,quality,max_width):
365
+ """
366
+ Internal function to call video_to_frames in the context of video_folder_to_frames;
367
+ makes sure the right output folder exists, then calls video_to_frames.
368
+ """
369
+
370
+ input_fn_absolute = os.path.join(input_folder,relative_fn)
371
+ assert os.path.isfile(input_fn_absolute),\
372
+ 'Could not find file {}'.format(input_fn_absolute)
373
+
374
+ # Create the target output folder
375
+ output_folder_video = os.path.join(output_folder_base,relative_fn)
376
+ os.makedirs(output_folder_video,exist_ok=True)
377
+
378
+ # Render frames
379
+ # input_video_file = input_fn_absolute; output_folder = output_folder_video
380
+ frame_filenames,fs = video_to_frames(input_fn_absolute,output_folder_video,
381
+ overwrite=overwrite,every_n_frames=every_n_frames,
382
+ verbose=verbose,quality=quality,max_width=max_width)
383
+
384
+ return frame_filenames,fs
385
+
386
+
387
+ def video_folder_to_frames(input_folder, output_folder_base,
388
+ recursive=True, overwrite=True,
389
+ n_threads=1, every_n_frames=None,
390
+ verbose=False, parallelization_uses_threads=True,
391
+ quality=None, max_width=None):
392
+ """
393
+ For every video file in input_folder, creates a folder within output_folder_base, and
394
+ renders frame of that video to images in that folder.
395
+
396
+ Args:
397
+ input_folder (str): folder to process
398
+ output_folder_base (str): root folder for output images; subfolders will be
399
+ created for each input video
400
+ recursive (bool, optional): whether to recursively process videos in [input_folder]
401
+ overwrite (bool, optional): whether to overwrite existing frame images
402
+ n_threads (int, optional): number of concurrent workers to use; set to <= 1 to disable
403
+ parallelism
404
+ every_n_frames (int, optional): sample every Nth frame starting from the first frame;
405
+ if this is None or 1, every frame is extracted
406
+ verbose (bool, optional): enable additional debug console output
407
+ parallelization_uses_threads (bool, optional): whether to use threads (True) or
408
+ processes (False) for parallelization; ignored if n_threads <= 1
409
+ quality (int, optional): JPEG quality for frame output, from 0-100. Defaults
410
+ to the opencv default (typically 95).
411
+ max_width (int, optional): resize frames to be no wider than [max_width]
412
+
413
+ Returns:
414
+ tuple: a length-3 tuple containing:
415
+ - list of lists of frame filenames; the Nth list of frame filenames corresponds to
416
+ the Nth video
417
+ - list of video frame rates; the Nth value corresponds to the Nth video
418
+ - list of video filenames
419
+ """
420
+
421
+ # Recursively enumerate video files
422
+ input_files_full_paths = find_videos(input_folder,recursive=recursive)
423
+ print('Found {} videos in folder {}'.format(len(input_files_full_paths),input_folder))
424
+ if len(input_files_full_paths) == 0:
425
+ return [],[],[]
426
+
427
+ input_files_relative_paths = [os.path.relpath(s,input_folder) for s in input_files_full_paths]
428
+ input_files_relative_paths = [s.replace('\\','/') for s in input_files_relative_paths]
429
+
430
+ os.makedirs(output_folder_base,exist_ok=True)
431
+
432
+ frame_filenames_by_video = []
433
+ fs_by_video = []
434
+
435
+ if n_threads == 1:
436
+ # For each video
437
+ #
438
+ # input_fn_relative = input_files_relative_paths[0]
439
+ for input_fn_relative in tqdm(input_files_relative_paths):
440
+
441
+ frame_filenames,fs = \
442
+ _video_to_frames_for_folder(input_fn_relative,input_folder,output_folder_base,
443
+ every_n_frames,overwrite,verbose,quality,max_width)
444
+ frame_filenames_by_video.append(frame_filenames)
445
+ fs_by_video.append(fs)
446
+ else:
447
+ if parallelization_uses_threads:
448
+ print('Starting a worker pool with {} threads'.format(n_threads))
449
+ pool = ThreadPool(n_threads)
450
+ else:
451
+ print('Starting a worker pool with {} processes'.format(n_threads))
452
+ pool = Pool(n_threads)
453
+ process_video_with_options = partial(_video_to_frames_for_folder,
454
+ input_folder=input_folder,
455
+ output_folder_base=output_folder_base,
456
+ every_n_frames=every_n_frames,
457
+ overwrite=overwrite,
458
+ verbose=verbose,
459
+ quality=quality,
460
+ max_width=max_width)
461
+ results = list(tqdm(pool.imap(
462
+ partial(process_video_with_options),input_files_relative_paths),
463
+ total=len(input_files_relative_paths)))
464
+ frame_filenames_by_video = [x[0] for x in results]
465
+ fs_by_video = [x[1] for x in results]
466
+
467
+ return frame_filenames_by_video,fs_by_video,input_files_full_paths
468
+
469
+ # ...def video_folder_to_frames(...)
470
+
471
+
472
+ class FrameToVideoOptions:
473
+ """
474
+ Options controlling the conversion of frame-level results to video-level results via
475
+ frame_results_to_video_results()
476
+ """
477
+
478
+ def __init__(self):
479
+
480
+ #: One-indexed indicator of which frame-level confidence value to use to determine detection confidence
481
+ #: for the whole video, i.e. "1" means "use the confidence value from the highest-confidence frame"
482
+ self.nth_highest_confidence = 1
483
+
484
+ #: What to do if a file referred to in a .json results file appears not to be a
485
+ #: video; can be 'error' or 'skip_with_warning'
486
+ self.non_video_behavior = 'error'
487
+
488
+
489
+ def frame_results_to_video_results(input_file,output_file,options=None):
490
+ """
491
+ Given an MD results file produced at the *frame* level, corresponding to a directory
492
+ created with video_folder_to_frames, maps those frame-level results back to the
493
+ video level for use in Timelapse.
494
+
495
+ Preserves everything in the input .json file other than the images.
496
+
497
+ Args:
498
+ input_file (str): the frame-level MD results file to convert to video-level results
499
+ output_file (str): the .json file to which we should write video-level results
500
+ options (FrameToVideoOptions, optional): parameters for converting frame-level results
501
+ to video-level results, see FrameToVideoOptions for details
502
+ """
503
+
504
+ if options is None:
505
+ options = FrameToVideoOptions()
506
+
507
+ # Load results
508
+ with open(input_file,'r') as f:
509
+ input_data = json.load(f)
510
+
511
+ images = input_data['images']
512
+ detection_categories = input_data['detection_categories']
513
+
514
+ ## Break into videos
515
+
516
+ video_to_frame_info = defaultdict(list)
517
+
518
+ # im = images[0]
519
+ for im in tqdm(images):
520
+
521
+ fn = im['file']
522
+ video_name = os.path.dirname(fn)
523
+ if not is_video_file(video_name):
524
+ if options.non_video_behavior == 'error':
525
+ raise ValueError('{} is not a video file'.format(video_name))
526
+ elif options.non_video_behavior == 'skip_with_warning':
527
+ print('Warning: {} is not a video file'.format(video_name))
528
+ continue
529
+ else:
530
+ raise ValueError('Unrecognized non-video handling behavior: {}'.format(
531
+ options.non_video_behavior))
532
+ video_to_frame_info[video_name].append(im)
533
+
534
+ print('Found {} unique videos in {} frame-level results'.format(
535
+ len(video_to_frame_info),len(images)))
536
+
537
+ output_images = []
538
+
539
+ ## For each video...
540
+
541
+ # video_name = list(video_to_frame_info.keys())[0]
542
+ for video_name in tqdm(video_to_frame_info):
543
+
544
+ frames = video_to_frame_info[video_name]
545
+
546
+ all_detections_this_video = []
547
+
548
+ # frame = frames[0]
549
+ for frame in frames:
550
+ if ('detections' in frame) and (frame['detections'] is not None):
551
+ all_detections_this_video.extend(frame['detections'])
552
+
553
+ # At most one detection for each category for the whole video
554
+ canonical_detections = []
555
+
556
+ # category_id = list(detection_categories.keys())[0]
557
+ for category_id in detection_categories:
558
+
559
+ category_detections = [det for det in all_detections_this_video if \
560
+ det['category'] == category_id]
561
+
562
+ # Find the nth-highest-confidence video to choose a confidence value
563
+ if len(category_detections) >= options.nth_highest_confidence:
564
+
565
+ category_detections_by_confidence = sorted(category_detections,
566
+ key = lambda i: i['conf'],reverse=True)
567
+ canonical_detection = category_detections_by_confidence[options.nth_highest_confidence-1]
568
+ canonical_detections.append(canonical_detection)
569
+
570
+ # Prepare the output representation for this video
571
+ im_out = {}
572
+ im_out['file'] = video_name
573
+ im_out['detections'] = canonical_detections
574
+
575
+ # 'max_detection_conf' is no longer included in output files by default
576
+ if False:
577
+ im_out['max_detection_conf'] = 0
578
+ if len(canonical_detections) > 0:
579
+ confidences = [d['conf'] for d in canonical_detections]
580
+ im_out['max_detection_conf'] = max(confidences)
581
+
582
+ output_images.append(im_out)
583
+
584
+ # ...for each video
585
+
586
+ output_data = input_data
587
+ output_data['images'] = output_images
588
+ s = json.dumps(output_data,indent=1)
589
+
590
+ # Write the output file
591
+ with open(output_file,'w') as f:
592
+ f.write(s)
593
+
594
+ # ...def frame_results_to_video_results(...)
595
+
596
+
597
+ #%% Test driver
598
+
599
+ if False:
600
+
601
+ #%% Constants
602
+
603
+ Fs = 30.01
604
+ confidence_threshold = 0.75
605
+ input_folder = 'z:\\'
606
+ frame_folder_base = r'e:\video_test\frames'
607
+ detected_frame_folder_base = r'e:\video_test\detected_frames'
608
+ rendered_videos_folder_base = r'e:\video_test\rendered_videos'
609
+
610
+ results_file = r'results.json'
611
+ os.makedirs(detected_frame_folder_base,exist_ok=True)
612
+ os.makedirs(rendered_videos_folder_base,exist_ok=True)
613
+
614
+
615
+ #%% Split videos into frames
616
+
617
+ frame_filenames_by_video,fs_by_video,video_filenames = \
618
+ video_folder_to_frames(input_folder,frame_folder_base,recursive=True)
619
+
620
+
621
+ #%% List image files, break into folders
622
+
623
+ frame_files = path_utils.find_images(frame_folder_base,True)
624
+ frame_files = [s.replace('\\','/') for s in frame_files]
625
+ print('Enumerated {} total frames'.format(len(frame_files)))
626
+
627
+ Fs = 30.01
628
+ # Find unique folders
629
+ folders = set()
630
+ # fn = frame_files[0]
631
+ for fn in frame_files:
632
+ folders.add(os.path.dirname(fn))
633
+ folders = [s.replace('\\','/') for s in folders]
634
+ print('Found {} folders for {} files'.format(len(folders),len(frame_files)))
635
+
636
+
637
+ #%% Load detector output
638
+
639
+ with open(results_file,'r') as f:
640
+ detection_results = json.load(f)
641
+ detections = detection_results['images']
642
+ detector_label_map = detection_results['detection_categories']
643
+ for d in detections:
644
+ d['file'] = d['file'].replace('\\','/').replace('video_frames/','')
645
+
646
+
647
+ #%% Render detector frames
648
+
649
+ # folder = list(folders)[0]
650
+ for folder in folders:
651
+
652
+ frame_files_this_folder = [fn for fn in frame_files if folder in fn]
653
+ folder_relative = folder.replace((frame_folder_base + '/').replace('\\','/'),'')
654
+ detection_results_this_folder = [d for d in detections if folder_relative in d['file']]
655
+ print('Found {} detections in folder {}'.format(len(detection_results_this_folder),folder))
656
+ assert len(frame_files_this_folder) == len(detection_results_this_folder)
657
+
658
+ rendered_frame_output_folder = os.path.join(detected_frame_folder_base,folder_relative)
659
+ os.makedirs(rendered_frame_output_folder,exist_ok=True)
660
+
661
+ # d = detection_results_this_folder[0]
662
+ for d in tqdm(detection_results_this_folder):
663
+
664
+ input_file = os.path.join(frame_folder_base,d['file'])
665
+ output_file = os.path.join(detected_frame_folder_base,d['file'])
666
+ os.makedirs(os.path.dirname(output_file),exist_ok=True)
667
+ vis_utils.draw_bounding_boxes_on_file(input_file,output_file,d['detections'],
668
+ confidence_threshold)
669
+
670
+ # ...for each file in this folder
671
+
672
+ # ...for each folder
673
+
674
+
675
+ #%% Render output videos
676
+
677
+ # folder = list(folders)[0]
678
+ for folder in tqdm(folders):
679
+
680
+ folder_relative = folder.replace((frame_folder_base + '/').replace('\\','/'),'')
681
+ rendered_detector_output_folder = os.path.join(detected_frame_folder_base,folder_relative)
682
+ assert os.path.isdir(rendered_detector_output_folder)
683
+
684
+ frame_files_relative = os.listdir(rendered_detector_output_folder)
685
+ frame_files_absolute = [os.path.join(rendered_detector_output_folder,s) \
686
+ for s in frame_files_relative]
687
+
688
+ output_video_filename = os.path.join(rendered_videos_folder_base,folder_relative)
689
+ os.makedirs(os.path.dirname(output_video_filename),exist_ok=True)
690
+
691
+ original_video_filename = output_video_filename.replace(
692
+ rendered_videos_folder_base,input_folder)
693
+ assert os.path.isfile(original_video_filename)
694
+ Fs = get_video_fs(original_video_filename)
695
+
696
+ frames_to_video(frame_files_absolute, Fs, output_video_filename)
697
+
698
+ # ...for each video
File without changes