megadetector 10.0.15__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.
Files changed (147) hide show
  1. megadetector/__init__.py +0 -0
  2. megadetector/api/__init__.py +0 -0
  3. megadetector/api/batch_processing/integration/digiKam/setup.py +6 -0
  4. megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +465 -0
  5. megadetector/api/batch_processing/integration/eMammal/test_scripts/config_template.py +5 -0
  6. megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +125 -0
  7. megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +55 -0
  8. megadetector/classification/__init__.py +0 -0
  9. megadetector/classification/aggregate_classifier_probs.py +108 -0
  10. megadetector/classification/analyze_failed_images.py +227 -0
  11. megadetector/classification/cache_batchapi_outputs.py +198 -0
  12. megadetector/classification/create_classification_dataset.py +626 -0
  13. megadetector/classification/crop_detections.py +516 -0
  14. megadetector/classification/csv_to_json.py +226 -0
  15. megadetector/classification/detect_and_crop.py +853 -0
  16. megadetector/classification/efficientnet/__init__.py +9 -0
  17. megadetector/classification/efficientnet/model.py +415 -0
  18. megadetector/classification/efficientnet/utils.py +608 -0
  19. megadetector/classification/evaluate_model.py +520 -0
  20. megadetector/classification/identify_mislabeled_candidates.py +152 -0
  21. megadetector/classification/json_to_azcopy_list.py +63 -0
  22. megadetector/classification/json_validator.py +696 -0
  23. megadetector/classification/map_classification_categories.py +276 -0
  24. megadetector/classification/merge_classification_detection_output.py +509 -0
  25. megadetector/classification/prepare_classification_script.py +194 -0
  26. megadetector/classification/prepare_classification_script_mc.py +228 -0
  27. megadetector/classification/run_classifier.py +287 -0
  28. megadetector/classification/save_mislabeled.py +110 -0
  29. megadetector/classification/train_classifier.py +827 -0
  30. megadetector/classification/train_classifier_tf.py +725 -0
  31. megadetector/classification/train_utils.py +323 -0
  32. megadetector/data_management/__init__.py +0 -0
  33. megadetector/data_management/animl_to_md.py +161 -0
  34. megadetector/data_management/annotations/__init__.py +0 -0
  35. megadetector/data_management/annotations/annotation_constants.py +33 -0
  36. megadetector/data_management/camtrap_dp_to_coco.py +270 -0
  37. megadetector/data_management/cct_json_utils.py +566 -0
  38. megadetector/data_management/cct_to_md.py +184 -0
  39. megadetector/data_management/cct_to_wi.py +293 -0
  40. megadetector/data_management/coco_to_labelme.py +284 -0
  41. megadetector/data_management/coco_to_yolo.py +701 -0
  42. megadetector/data_management/databases/__init__.py +0 -0
  43. megadetector/data_management/databases/add_width_and_height_to_db.py +107 -0
  44. megadetector/data_management/databases/combine_coco_camera_traps_files.py +210 -0
  45. megadetector/data_management/databases/integrity_check_json_db.py +563 -0
  46. megadetector/data_management/databases/subset_json_db.py +195 -0
  47. megadetector/data_management/generate_crops_from_cct.py +200 -0
  48. megadetector/data_management/get_image_sizes.py +164 -0
  49. megadetector/data_management/labelme_to_coco.py +559 -0
  50. megadetector/data_management/labelme_to_yolo.py +349 -0
  51. megadetector/data_management/lila/__init__.py +0 -0
  52. megadetector/data_management/lila/create_lila_blank_set.py +556 -0
  53. megadetector/data_management/lila/create_lila_test_set.py +192 -0
  54. megadetector/data_management/lila/create_links_to_md_results_files.py +106 -0
  55. megadetector/data_management/lila/download_lila_subset.py +182 -0
  56. megadetector/data_management/lila/generate_lila_per_image_labels.py +777 -0
  57. megadetector/data_management/lila/get_lila_annotation_counts.py +174 -0
  58. megadetector/data_management/lila/get_lila_image_counts.py +112 -0
  59. megadetector/data_management/lila/lila_common.py +319 -0
  60. megadetector/data_management/lila/test_lila_metadata_urls.py +164 -0
  61. megadetector/data_management/mewc_to_md.py +344 -0
  62. megadetector/data_management/ocr_tools.py +873 -0
  63. megadetector/data_management/read_exif.py +964 -0
  64. megadetector/data_management/remap_coco_categories.py +195 -0
  65. megadetector/data_management/remove_exif.py +156 -0
  66. megadetector/data_management/rename_images.py +194 -0
  67. megadetector/data_management/resize_coco_dataset.py +665 -0
  68. megadetector/data_management/speciesnet_to_md.py +41 -0
  69. megadetector/data_management/wi_download_csv_to_coco.py +247 -0
  70. megadetector/data_management/yolo_output_to_md_output.py +594 -0
  71. megadetector/data_management/yolo_to_coco.py +984 -0
  72. megadetector/data_management/zamba_to_md.py +188 -0
  73. megadetector/detection/__init__.py +0 -0
  74. megadetector/detection/change_detection.py +840 -0
  75. megadetector/detection/process_video.py +479 -0
  76. megadetector/detection/pytorch_detector.py +1451 -0
  77. megadetector/detection/run_detector.py +1267 -0
  78. megadetector/detection/run_detector_batch.py +2172 -0
  79. megadetector/detection/run_inference_with_yolov5_val.py +1314 -0
  80. megadetector/detection/run_md_and_speciesnet.py +1604 -0
  81. megadetector/detection/run_tiled_inference.py +1044 -0
  82. megadetector/detection/tf_detector.py +209 -0
  83. megadetector/detection/video_utils.py +1379 -0
  84. megadetector/postprocessing/__init__.py +0 -0
  85. megadetector/postprocessing/add_max_conf.py +72 -0
  86. megadetector/postprocessing/categorize_detections_by_size.py +166 -0
  87. megadetector/postprocessing/classification_postprocessing.py +1943 -0
  88. megadetector/postprocessing/combine_batch_outputs.py +249 -0
  89. megadetector/postprocessing/compare_batch_results.py +2110 -0
  90. megadetector/postprocessing/convert_output_format.py +403 -0
  91. megadetector/postprocessing/create_crop_folder.py +629 -0
  92. megadetector/postprocessing/detector_calibration.py +570 -0
  93. megadetector/postprocessing/generate_csv_report.py +522 -0
  94. megadetector/postprocessing/load_api_results.py +223 -0
  95. megadetector/postprocessing/md_to_coco.py +428 -0
  96. megadetector/postprocessing/md_to_labelme.py +351 -0
  97. megadetector/postprocessing/md_to_wi.py +41 -0
  98. megadetector/postprocessing/merge_detections.py +392 -0
  99. megadetector/postprocessing/postprocess_batch_results.py +2140 -0
  100. megadetector/postprocessing/remap_detection_categories.py +226 -0
  101. megadetector/postprocessing/render_detection_confusion_matrix.py +677 -0
  102. megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +206 -0
  103. megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +82 -0
  104. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +1665 -0
  105. megadetector/postprocessing/separate_detections_into_folders.py +795 -0
  106. megadetector/postprocessing/subset_json_detector_output.py +964 -0
  107. megadetector/postprocessing/top_folders_to_bottom.py +238 -0
  108. megadetector/postprocessing/validate_batch_results.py +332 -0
  109. megadetector/taxonomy_mapping/__init__.py +0 -0
  110. megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +491 -0
  111. megadetector/taxonomy_mapping/map_new_lila_datasets.py +211 -0
  112. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +165 -0
  113. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +543 -0
  114. megadetector/taxonomy_mapping/retrieve_sample_image.py +71 -0
  115. megadetector/taxonomy_mapping/simple_image_download.py +231 -0
  116. megadetector/taxonomy_mapping/species_lookup.py +1008 -0
  117. megadetector/taxonomy_mapping/taxonomy_csv_checker.py +159 -0
  118. megadetector/taxonomy_mapping/taxonomy_graph.py +346 -0
  119. megadetector/taxonomy_mapping/validate_lila_category_mappings.py +83 -0
  120. megadetector/tests/__init__.py +0 -0
  121. megadetector/tests/test_nms_synthetic.py +335 -0
  122. megadetector/utils/__init__.py +0 -0
  123. megadetector/utils/ct_utils.py +1857 -0
  124. megadetector/utils/directory_listing.py +199 -0
  125. megadetector/utils/extract_frames_from_video.py +307 -0
  126. megadetector/utils/gpu_test.py +125 -0
  127. megadetector/utils/md_tests.py +2072 -0
  128. megadetector/utils/path_utils.py +2872 -0
  129. megadetector/utils/process_utils.py +172 -0
  130. megadetector/utils/split_locations_into_train_val.py +237 -0
  131. megadetector/utils/string_utils.py +234 -0
  132. megadetector/utils/url_utils.py +825 -0
  133. megadetector/utils/wi_platform_utils.py +968 -0
  134. megadetector/utils/wi_taxonomy_utils.py +1766 -0
  135. megadetector/utils/write_html_image_list.py +239 -0
  136. megadetector/visualization/__init__.py +0 -0
  137. megadetector/visualization/plot_utils.py +309 -0
  138. megadetector/visualization/render_images_with_thumbnails.py +243 -0
  139. megadetector/visualization/visualization_utils.py +1973 -0
  140. megadetector/visualization/visualize_db.py +630 -0
  141. megadetector/visualization/visualize_detector_output.py +498 -0
  142. megadetector/visualization/visualize_video_output.py +705 -0
  143. megadetector-10.0.15.dist-info/METADATA +115 -0
  144. megadetector-10.0.15.dist-info/RECORD +147 -0
  145. megadetector-10.0.15.dist-info/WHEEL +5 -0
  146. megadetector-10.0.15.dist-info/licenses/LICENSE +19 -0
  147. megadetector-10.0.15.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1379 @@
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 re
13
+ import cv2
14
+ import glob
15
+ import json
16
+
17
+ from collections import defaultdict
18
+ from multiprocessing.pool import ThreadPool
19
+ from multiprocessing.pool import Pool
20
+ from tqdm import tqdm
21
+ from functools import partial
22
+ from inspect import signature
23
+
24
+ from megadetector.utils import path_utils
25
+ from megadetector.utils.path_utils import clean_path
26
+ from megadetector.utils.ct_utils import sort_list_of_dicts_by_key
27
+ from megadetector.visualization import visualization_utils as vis_utils
28
+
29
+ default_fourcc = 'h264'
30
+
31
+ video_progress_bar_description = 'Processing video'
32
+
33
+ #%% Path utilities
34
+
35
+ VIDEO_EXTENSIONS = ('.mp4','.avi','.mpeg','.mpg','.mov','.mkv','.flv')
36
+
37
+ def is_video_file(s,video_extensions=VIDEO_EXTENSIONS):
38
+ """
39
+ Checks a file's extension against a set of known video file
40
+ extensions to determine whether it's a video file. Performs a
41
+ case-insensitive comparison.
42
+
43
+ Args:
44
+ s (str): filename to check for probable video-ness
45
+ video_extensions (list, optional): list of video file extensions
46
+
47
+ Returns:
48
+ bool: True if this looks like a video file, else False
49
+ """
50
+
51
+ ext = os.path.splitext(s)[1]
52
+ return ext.lower() in video_extensions
53
+
54
+
55
+ def find_video_strings(strings):
56
+ """
57
+ Given a list of strings that are potentially video file names, looks for
58
+ strings that actually look like video file names (based on extension).
59
+
60
+ Args:
61
+ strings (list): list of strings to check for video-ness
62
+
63
+ Returns:
64
+ list: a subset of [strings] that looks like they are video filenames
65
+ """
66
+
67
+ return [s for s in strings if is_video_file(s.lower())]
68
+
69
+
70
+ def find_videos(dirname,
71
+ recursive=False,
72
+ convert_slashes=True,
73
+ return_relative_paths=False):
74
+ """
75
+ Finds all files in a directory that look like video file names.
76
+
77
+ Args:
78
+ dirname (str): folder to search for video files
79
+ recursive (bool, optional): whether to search [dirname] recursively
80
+ convert_slashes (bool, optional): forces forward slashes in the returned files,
81
+ otherwise uses the native path separator
82
+ return_relative_paths (bool, optional): forces the returned filenames to be
83
+ relative to [dirname], otherwise returns absolute paths
84
+
85
+ Returns:
86
+ A list of filenames within [dirname] that appear to be videos
87
+ """
88
+
89
+ if recursive:
90
+ files = glob.glob(os.path.join(dirname, '**', '*.*'), recursive=True)
91
+ else:
92
+ files = glob.glob(os.path.join(dirname, '*.*'))
93
+
94
+ files = [fn for fn in files if os.path.isfile(fn)]
95
+
96
+ if return_relative_paths:
97
+ files = [os.path.relpath(fn,dirname) for fn in files]
98
+
99
+ if convert_slashes:
100
+ files = [fn.replace('\\', '/') for fn in files]
101
+
102
+ return find_video_strings(files)
103
+
104
+
105
+ #%% Shared function for opening videos
106
+
107
+ DEFAULT_BACKEND = -1
108
+
109
+ # This is the order in which we'll try to open backends.
110
+ #
111
+ # In general, the defaults are as follows, though they vary depending
112
+ # on what's installed:
113
+ #
114
+ # Windows: CAP_DSHOW or CAP_MSMF
115
+ # Linux: CAP_FFMPEG
116
+ # macOS: CAP_AVFOUNDATION
117
+ #
118
+ # Technically if the default fails, we may try the same backend again, but this
119
+ # is rare, and it's not worth the complexity of figuring out what the system
120
+ # default is.
121
+ backend_id_to_name = {
122
+ DEFAULT_BACKEND:'default',
123
+ cv2.CAP_FFMPEG: 'CAP_FFMPEG',
124
+ cv2.CAP_DSHOW: 'CAP_DSHOW',
125
+ cv2.CAP_MSMF: 'CAP_MSMF',
126
+ cv2.CAP_AVFOUNDATION: 'CAP_AVFOUNDATION',
127
+ cv2.CAP_GSTREAMER: 'CAP_GSTREAMER'
128
+ }
129
+
130
+ def open_video(video_path,verbose=False):
131
+ """
132
+ Open the video at [video_path], trying multiple OpenCV backends if necessary.
133
+
134
+ Args:
135
+ video_path (str): the file to open
136
+ verbose (bool, optional): enable additional debug output
137
+
138
+ Returns:
139
+ (cv2.VideoCapture,image): a tuple containing (a) the open video capture device
140
+ (or None if no backends succeeded) and (b) the first frame of the video (or None)
141
+ """
142
+
143
+ if not os.path.isfile(video_path):
144
+ print('Video file {} not found'.format(video_path))
145
+ return None,None
146
+
147
+ backend_ids = backend_id_to_name.keys()
148
+
149
+ for backend_id in backend_ids:
150
+
151
+ backend_name = backend_id_to_name[backend_id]
152
+ if verbose:
153
+ print('Trying backend {}'.format(backend_name))
154
+
155
+ try:
156
+ if backend_id == DEFAULT_BACKEND:
157
+ vidcap = cv2.VideoCapture(video_path)
158
+ else:
159
+ vidcap = cv2.VideoCapture(video_path, backend_id)
160
+ except Exception as e:
161
+ if verbose:
162
+ print('Warning: error opening {} with backend {}: {}'.format(
163
+ video_path,backend_name,str(e)))
164
+ continue
165
+
166
+ if not vidcap.isOpened():
167
+ if verbose:
168
+ print('Warning: isOpened() is False for {} with backend {}'.format(
169
+ video_path,backend_name))
170
+ try:
171
+ vidcap.release()
172
+ except Exception:
173
+ pass
174
+ continue
175
+
176
+ success, image = vidcap.read()
177
+ if success and (image is not None):
178
+ if verbose:
179
+ print('Successfully opened {} with backend: {}'.format(
180
+ video_path,backend_name))
181
+ return vidcap,image
182
+
183
+ print('Warning: failed to open {} with backend {}'.format(
184
+ video_path,backend_name))
185
+ try:
186
+ vidcap.release()
187
+ except Exception:
188
+ pass
189
+
190
+ # ...for each backend
191
+
192
+ print('Error: failed to open {} with any backend'.format(video_path))
193
+ return None,None
194
+
195
+ # ...def open_video(...)
196
+
197
+
198
+ #%% Functions for rendering frames to video and vice-versa
199
+
200
+ # http://tsaith.github.io/combine-images-into-a-video-with-python-3-and-opencv-3.html
201
+
202
+ def frames_to_video(images, fs, output_file_name, codec_spec=default_fourcc):
203
+ """
204
+ Given a list of image files and a sample rate, concatenates those images into
205
+ a video and writes to a new video file.
206
+
207
+ Args:
208
+ images (list): a list of frame file names to concatenate into a video
209
+ fs (float): the frame rate in fps
210
+ output_file_name (str): the output video file, no checking is performed to make
211
+ sure the extension is compatible with the codec
212
+ codec_spec (str, optional): codec to use for encoding; h264 is a sensible default
213
+ and generally works on Windows, but when this fails (which is around 50% of the time
214
+ on Linux), mp4v is a good second choice
215
+ """
216
+
217
+ if codec_spec is None:
218
+ codec_spec = 'h264'
219
+
220
+ if len(images) == 0:
221
+ print('Warning: no frames to render')
222
+ return
223
+
224
+ output_dir = os.path.dirname(output_file_name)
225
+ if len(output_dir) > 0:
226
+ os.makedirs(output_dir, exist_ok=True)
227
+
228
+ # Determine the width and height from the first image
229
+ frame = cv2.imread(images[0])
230
+ # cv2.imshow('video',frame)
231
+ height, width, channels = frame.shape
232
+
233
+ # Define the codec and create VideoWriter object
234
+ fourcc = cv2.VideoWriter_fourcc(*codec_spec)
235
+ out = cv2.VideoWriter(output_file_name, fourcc, fs, (width, height))
236
+
237
+ for image in images:
238
+ frame = cv2.imread(image)
239
+ out.write(frame)
240
+
241
+ out.release()
242
+ cv2.destroyAllWindows()
243
+
244
+
245
+ def get_video_fs(input_video_file,verbose=False):
246
+ """
247
+ Retrieves the frame rate of [input_video_file].
248
+
249
+ Args:
250
+ input_video_file (str): video file for which we want the frame rate
251
+ verbose (bool, optional): enable additional debug output
252
+
253
+ Returns:
254
+ float: the frame rate of [input_video_file], or None if no frame
255
+ rate could be extracted
256
+ """
257
+
258
+ assert os.path.isfile(input_video_file), \
259
+ 'File {} not found'.format(input_video_file)
260
+ vidcap,_ = open_video(input_video_file,verbose=verbose)
261
+ if vidcap is None:
262
+ if verbose:
263
+ print('Failed to get frame rate for {}'.format(input_video_file))
264
+ return None
265
+ fs = vidcap.get(cv2.CAP_PROP_FPS)
266
+ try:
267
+ vidcap.release()
268
+ except Exception as e:
269
+ print('Warning: error closing video handle for {}: {}'.format(
270
+ input_video_file,str(e)))
271
+ return fs
272
+
273
+
274
+ def _frame_number_to_filename(frame_number):
275
+ """
276
+ Ensures that frame images are given consistent filenames.
277
+ """
278
+
279
+ return 'frame{:06d}.jpg'.format(frame_number)
280
+
281
+
282
+ def _filename_to_frame_number(filename):
283
+ """
284
+ Extract the frame number from a filename that was created using
285
+ _frame_number_to_filename.
286
+
287
+ Args:
288
+ filename (str): a filename created with _frame_number_to_filename.
289
+
290
+ Returns:
291
+ int: the frame number extracted from [filename]
292
+ """
293
+
294
+ filename = os.path.basename(filename)
295
+ match = re.search(r'frame(\d+)\.jpg', filename)
296
+ if match is None:
297
+ raise ValueError('{} does not appear to be a frame file'.format(filename))
298
+ frame_number = match.group(1)
299
+ try:
300
+ frame_number = int(frame_number)
301
+ except Exception:
302
+ raise ValueError('Filename {} does not contain a valid frame number'.format(filename))
303
+
304
+ return frame_number
305
+
306
+
307
+ def _add_frame_numbers_to_results(results):
308
+ """
309
+ Given the 'images' list from a set of MD results that was generated on video frames,
310
+ add a 'frame_number' field to each image, and return the list, sorted by frame number.
311
+ Also modifies "results" in place.
312
+
313
+ Args:
314
+ results (list): list of image dicts
315
+ """
316
+
317
+ # This indicate that this was a failure for a single video
318
+ if isinstance(results,dict):
319
+ assert 'failure' in results
320
+ return results
321
+
322
+ # Add video-specific fields to the results
323
+ for im in results:
324
+ fn = im['file']
325
+ frame_number = _filename_to_frame_number(fn)
326
+ im['frame_number'] = frame_number
327
+
328
+ results = sort_list_of_dicts_by_key(results,'frame_number')
329
+ return results
330
+
331
+
332
+ def run_callback_on_frames(input_video_file,
333
+ frame_callback,
334
+ every_n_frames=None,
335
+ verbose=False,
336
+ frames_to_process=None,
337
+ allow_empty_videos=False):
338
+ """
339
+ Calls the function frame_callback(np.array,image_id) on all (or selected) frames in
340
+ [input_video_file].
341
+
342
+ Args:
343
+ input_video_file (str): video file to process
344
+ frame_callback (function): callback to run on frames, should take an np.array and a string and
345
+ return a single value. callback should expect two arguments: (1) a numpy array with image
346
+ data, in the typical PIL image orientation/channel order, and (2) a string identifier
347
+ for the frame, typically something like "frame0006.jpg" (even though it's not a JPEG
348
+ image, this is just an identifier for the frame).
349
+ every_n_frames (int or float, optional): sample every Nth frame starting from the first frame;
350
+ if this is None or 1, every frame is processed. If this is a negative value, it's
351
+ interpreted as a sampling rate in seconds, which is rounded to the nearest frame sampling
352
+ rate. Mutually exclusive with frames_to_process.
353
+ verbose (bool, optional): enable additional debug console output
354
+ frames_to_process (list of int, optional): process this specific set of frames;
355
+ mutually exclusive with every_n_frames. If all values are beyond the length
356
+ of the video, no frames are extracted. Can also be a single int, specifying
357
+ a single frame number.
358
+ allow_empty_videos (bool, optional): Just print a warning if a video appears to have no
359
+ frames (by default, this raises an Exception).
360
+
361
+ Returns:
362
+ dict: dict with keys 'frame_filenames' (list), 'frame_rate' (float), 'results' (list).
363
+ 'frame_filenames' are synthetic filenames (e.g. frame000000.jpg). Elements in
364
+ 'results' are whatever is returned by the callback, typically dicts in the same format used in
365
+ the 'images' array in the MD results format. [frame_filenames] and [results] both have
366
+ one element per processed frame.
367
+ """
368
+
369
+ assert os.path.isfile(input_video_file), 'File {} not found'.format(input_video_file)
370
+
371
+ if isinstance(frames_to_process,int):
372
+ frames_to_process = [frames_to_process]
373
+
374
+ if (frames_to_process is not None) and (every_n_frames is not None):
375
+ raise ValueError('frames_to_process and every_n_frames are mutually exclusive')
376
+
377
+ vidcap = None
378
+
379
+ try:
380
+
381
+ vidcap,image = open_video(input_video_file,verbose=verbose)
382
+ n_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
383
+ frame_rate = vidcap.get(cv2.CAP_PROP_FPS)
384
+
385
+ if verbose:
386
+ print('Video {} contains {} frames at {} Hz'.format(input_video_file,n_frames,frame_rate))
387
+
388
+ frame_filenames = []
389
+ results = []
390
+
391
+ if (every_n_frames is not None):
392
+
393
+ if (every_n_frames < 0):
394
+ every_n_seconds = abs(every_n_frames)
395
+ every_n_frames = int(every_n_seconds * frame_rate)
396
+ if verbose:
397
+ print('Interpreting a time sampling rate of {} hz as a frame interval of {}'.format(
398
+ every_n_seconds,every_n_frames))
399
+ # 0 and 1 both mean "process every frame"
400
+ elif every_n_frames == 0:
401
+ every_n_frames = 1
402
+ elif every_n_frames > 0:
403
+ every_n_frames = int(every_n_frames)
404
+
405
+ # ...if every_n_frames was supplied
406
+
407
+ # frame_number = 0
408
+ for frame_number in range(0,n_frames):
409
+
410
+ # We've already read the first frame, when we opened the video
411
+ if frame_number != 0:
412
+ success,image = vidcap.read()
413
+ else:
414
+ success = True
415
+
416
+ if not success:
417
+ assert image is None
418
+ if verbose:
419
+ print('Read terminating at frame {} of {}'.format(frame_number,n_frames))
420
+ break
421
+
422
+ if every_n_frames is not None:
423
+ if (frame_number % every_n_frames) != 0:
424
+ continue
425
+
426
+ if frames_to_process is not None:
427
+ if frame_number > max(frames_to_process):
428
+ break
429
+ if frame_number not in frames_to_process:
430
+ continue
431
+
432
+ frame_filename_relative = _frame_number_to_filename(frame_number)
433
+ frame_filenames.append(frame_filename_relative)
434
+
435
+ # Convert from OpenCV conventions to PIL conventions
436
+ image_np = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
437
+
438
+ # Run the callback
439
+ frame_results = frame_callback(image_np,frame_filename_relative)
440
+
441
+ results.append(frame_results)
442
+
443
+ # ...for each frame
444
+
445
+ if len(frame_filenames) == 0:
446
+ if allow_empty_videos:
447
+ print('Warning: found no frames in file {}'.format(input_video_file))
448
+ else:
449
+ raise Exception('Error: found no frames in file {}'.format(input_video_file))
450
+
451
+ if verbose:
452
+ print('\nProcessed {} of {} frames for {}'.format(
453
+ len(frame_filenames),n_frames,input_video_file))
454
+
455
+ finally:
456
+
457
+ if vidcap is not None:
458
+ try:
459
+ vidcap.release()
460
+ except Exception:
461
+ pass
462
+
463
+ to_return = {}
464
+ to_return['frame_filenames'] = frame_filenames
465
+ to_return['frame_rate'] = frame_rate
466
+ to_return['results'] = results
467
+
468
+ return to_return
469
+
470
+ # ...def run_callback_on_frames(...)
471
+
472
+
473
+ def run_callback_on_frames_for_folder(input_video_folder,
474
+ frame_callback,
475
+ every_n_frames=None,
476
+ verbose=False,
477
+ recursive=True,
478
+ files_to_process_relative=None,
479
+ error_on_empty_video=False):
480
+ """
481
+ Calls the function frame_callback(np.array,image_id) on all (or selected) frames in
482
+ all videos in [input_video_folder].
483
+
484
+ Args:
485
+ input_video_folder (str): video folder to process
486
+ frame_callback (function): callback to run on frames, should take an np.array and a string and
487
+ return a single value. callback should expect two arguments: (1) a numpy array with image
488
+ data, in the typical PIL image orientation/channel order, and (2) a string identifier
489
+ for the frame, typically something like "frame0006.jpg" (even though it's not a JPEG
490
+ image, this is just an identifier for the frame).
491
+ every_n_frames (int or float, optional): sample every Nth frame starting from the first frame;
492
+ if this is None or 1, every frame is processed. If this is a negative value, it's
493
+ interpreted as a sampling rate in seconds, which is rounded to the nearest frame
494
+ sampling rate.
495
+ verbose (bool, optional): enable additional debug console output
496
+ recursive (bool, optional): recurse into [input_video_folder]
497
+ files_to_process_relative (list, optional): only process specific relative paths
498
+ error_on_empty_video (bool, optional): by default, videos with errors or no valid frames
499
+ are silently stored as failures; this turns them into exceptions
500
+
501
+ Returns:
502
+ dict: dict with keys 'video_filenames' (list of str), 'frame_rates' (list of floats),
503
+ 'results' (list of list of dicts). 'video_filenames' will contain *relative* filenames.
504
+ 'results' is a list (one element per video) of lists (one element per frame) of whatever the
505
+ callback returns, typically (but not necessarily) dicts in the MD results format.
506
+
507
+ For failed videos, the frame rate will be represented by -1, and "results"
508
+ will be a dict with at least the key "failure".
509
+ """
510
+
511
+ to_return = {'video_filenames':[],'frame_rates':[],'results':[]}
512
+
513
+ if files_to_process_relative is not None:
514
+ input_files_full_paths = \
515
+ [os.path.join(input_video_folder,fn) for fn in files_to_process_relative]
516
+ input_files_full_paths = [fn.replace('\\','/') for fn in input_files_full_paths]
517
+ else:
518
+ # Recursively enumerate video files
519
+ input_files_full_paths = find_videos(input_video_folder,
520
+ recursive=recursive,
521
+ convert_slashes=True,
522
+ return_relative_paths=False)
523
+
524
+ print('Processing {} videos from folder {}'.format(len(input_files_full_paths),input_video_folder))
525
+
526
+ if len(input_files_full_paths) == 0:
527
+ print('No videos to process')
528
+ return to_return
529
+
530
+ # Process each video
531
+
532
+ # video_fn_abs = input_files_full_paths[0]
533
+ for video_fn_abs in tqdm(input_files_full_paths,desc=video_progress_bar_description):
534
+
535
+ video_filename_relative = os.path.relpath(video_fn_abs,input_video_folder)
536
+ video_filename_relative = video_filename_relative.replace('\\','/')
537
+ to_return['video_filenames'].append(video_filename_relative)
538
+
539
+ try:
540
+
541
+ # video_results is a dict with fields:
542
+ #
543
+ # frame_rate
544
+ #
545
+ # results (list of objects returned by the callback, typically dicts in the MD
546
+ # per-image format)
547
+ #
548
+ # frame_filenames (list of frame IDs, i.e. synthetic filenames)
549
+ video_results = run_callback_on_frames(input_video_file=video_fn_abs,
550
+ frame_callback=frame_callback,
551
+ every_n_frames=every_n_frames,
552
+ verbose=verbose,
553
+ frames_to_process=None,
554
+ allow_empty_videos=False)
555
+
556
+ except Exception as e:
557
+
558
+ if (not error_on_empty_video):
559
+ print('Warning: error processing video {}: {}'.format(
560
+ video_fn_abs,str(e)
561
+ ))
562
+ to_return['frame_rates'].append(-1.0)
563
+ failure_result = {}
564
+ failure_result['failure'] = 'Failure processing video: {}'.format(str(e))
565
+ to_return['results'].append(failure_result)
566
+ continue
567
+ else:
568
+ raise
569
+
570
+ # ...try/except
571
+
572
+ to_return['frame_rates'].append(video_results['frame_rate'])
573
+ for r in video_results['results']:
574
+ assert r['file'].startswith('frame')
575
+ r['file'] = video_filename_relative + '/' + r['file']
576
+ to_return['results'].append(video_results['results'])
577
+
578
+ # ...for each video
579
+
580
+ n_videos = len(input_files_full_paths)
581
+ assert len(to_return['video_filenames']) == n_videos
582
+ assert len(to_return['frame_rates']) == n_videos
583
+ assert len(to_return['results']) == n_videos
584
+
585
+ return to_return
586
+
587
+ # ...def run_callback_on_frames_for_folder(...)
588
+
589
+
590
+ def video_to_frames(input_video_file,
591
+ output_folder,
592
+ overwrite=True,
593
+ every_n_frames=None,
594
+ verbose=False,
595
+ quality=None,
596
+ max_width=None,
597
+ frames_to_extract=None,
598
+ allow_empty_videos=True):
599
+ """
600
+ Renders frames from [input_video_file] to .jpg files in [output_folder].
601
+
602
+ With help from:
603
+
604
+ https://stackoverflow.com/questions/33311153/python-extracting-and-saving-video-frames
605
+
606
+ Args:
607
+ input_video_file (str): video file to split into frames
608
+ output_folder (str): folder to put frame images in
609
+ overwrite (bool, optional): whether to overwrite existing frame images
610
+ every_n_frames (int, optional): sample every Nth frame starting from the first frame;
611
+ if this is None or 1, every frame is extracted. If this is a negative value, it's
612
+ interpreted as a sampling rate in seconds, which is rounded to the nearest frame sampling
613
+ rate. Mutually exclusive with frames_to_extract.
614
+ verbose (bool, optional): enable additional debug console output
615
+ quality (int, optional): JPEG quality for frame output, from 0-100. Defaults
616
+ to the opencv default (typically 95).
617
+ max_width (int, optional): resize frames to be no wider than [max_width]
618
+ frames_to_extract (list of int, optional): extract this specific set of frames;
619
+ mutually exclusive with every_n_frames. If all values are beyond the length
620
+ of the video, no frames are extracted. Can also be a single int, specifying
621
+ a single frame number. In the special case where frames_to_extract
622
+ is [], this function still reads video frame rates and verifies that videos
623
+ are readable, but no frames are extracted.
624
+ allow_empty_videos (bool, optional): Just print a warning if a video appears to have
625
+ no frames (by default, this is an error).
626
+
627
+ Returns:
628
+ tuple: length-2 tuple containing (list of frame filenames,frame rate)
629
+ """
630
+
631
+ assert os.path.isfile(input_video_file), 'File {} not found'.format(input_video_file)
632
+
633
+ if quality is not None and quality < 0:
634
+ quality = None
635
+
636
+ if isinstance(frames_to_extract,int):
637
+ frames_to_extract = [frames_to_extract]
638
+
639
+ if (frames_to_extract is not None) and (every_n_frames is not None):
640
+ raise ValueError('frames_to_extract and every_n_frames are mutually exclusive')
641
+
642
+ bypass_extraction = ((frames_to_extract is not None) and (len(frames_to_extract) == 0))
643
+
644
+ if not bypass_extraction:
645
+ os.makedirs(output_folder,exist_ok=True)
646
+
647
+ vidcap = cv2.VideoCapture(input_video_file)
648
+ n_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
649
+ fs = vidcap.get(cv2.CAP_PROP_FPS)
650
+
651
+ if (every_n_frames is not None) and (every_n_frames < 0):
652
+ every_n_seconds = abs(every_n_frames)
653
+ every_n_frames = int(every_n_seconds * fs)
654
+ if verbose:
655
+ print('Interpreting a time sampling rate of {} hz as a frame interval of {}'.format(
656
+ every_n_seconds,every_n_frames))
657
+
658
+ # If we're not over-writing, check whether all frame images already exist
659
+ if (not overwrite) and (not bypass_extraction):
660
+
661
+ missing_frame_number = None
662
+ missing_frame_filename = None
663
+ frame_filenames = []
664
+ found_existing_frame = False
665
+
666
+ for frame_number in range(0,n_frames):
667
+
668
+ if every_n_frames is not None:
669
+ assert frames_to_extract is None, \
670
+ 'Internal error: frames_to_extract and every_n_frames are exclusive'
671
+ if (frame_number % every_n_frames) != 0:
672
+ continue
673
+
674
+ if frames_to_extract is not None:
675
+ assert every_n_frames is None, \
676
+ 'Internal error: frames_to_extract and every_n_frames are exclusive'
677
+ if frame_number not in frames_to_extract:
678
+ continue
679
+
680
+ frame_filename = _frame_number_to_filename(frame_number)
681
+ frame_filename = os.path.join(output_folder,frame_filename)
682
+ frame_filenames.append(frame_filename)
683
+ if os.path.isfile(frame_filename):
684
+ found_existing_frame = True
685
+ continue
686
+ else:
687
+ missing_frame_number = frame_number
688
+ missing_frame_filename = frame_filename
689
+ break
690
+
691
+ if verbose and missing_frame_number is not None:
692
+ print('Missing frame {} ({}) for video {}'.format(
693
+ missing_frame_number,
694
+ missing_frame_filename,
695
+ input_video_file))
696
+
697
+ # OpenCV seems to over-report the number of frames by 1 in some cases, or fails
698
+ # to read the last frame; either way, I'm allowing one missing frame.
699
+ allow_last_frame_missing = True
700
+
701
+ # This doesn't have to mean literally the last frame number, it just means that if
702
+ # we find this frame or later, we consider the video done
703
+ last_expected_frame_number = n_frames-1
704
+ if every_n_frames is not None:
705
+ last_expected_frame_number -= (every_n_frames*2)
706
+
707
+ # When specific frames are requested, if anything is missing, reprocess the video
708
+ if (frames_to_extract is not None) and (missing_frame_number is not None):
709
+ pass
710
+
711
+ # If no frames are missing, or only frames very close to the end of the video are "missing",
712
+ # skip this video
713
+ elif (missing_frame_number is None) or \
714
+ (allow_last_frame_missing and (missing_frame_number >= last_expected_frame_number)):
715
+
716
+ if verbose:
717
+ print('Skipping video {}, all output frames exist'.format(input_video_file))
718
+ return frame_filenames,fs
719
+
720
+ else:
721
+
722
+ # If we found some frames, but not all, print a message
723
+ if verbose and found_existing_frame:
724
+ print("Rendering video {}, couldn't find frame {} ({}) of {}".format(
725
+ input_video_file,
726
+ missing_frame_number,
727
+ missing_frame_filename,
728
+ last_expected_frame_number))
729
+
730
+ # ...if we need to check whether to skip this video entirely
731
+
732
+ if verbose:
733
+ print('Video {} contains {} frames at {} Hz'.format(input_video_file,n_frames,fs))
734
+
735
+ frame_filenames = []
736
+
737
+ # YOLOv5 does some totally bananas monkey-patching of opencv, which causes
738
+ # problems if we try to supply a third parameter to imwrite (to specify JPEG
739
+ # quality). Detect this case, and ignore the quality parameter if it looks
740
+ # like imwrite has been messed with.
741
+ #
742
+ # See:
743
+ #
744
+ # https://github.com/ultralytics/yolov5/issues/7285
745
+ imwrite_patched = False
746
+ n_imwrite_parameters = None
747
+
748
+ try:
749
+ # calling signature() on the native cv2.imwrite function will
750
+ # fail, so an exception here is a good thing. In fact I don't think
751
+ # there's a case where this *succeeds* and the number of parameters
752
+ # is wrong.
753
+ sig = signature(cv2.imwrite)
754
+ n_imwrite_parameters = len(sig.parameters)
755
+ except Exception:
756
+ pass
757
+
758
+ if (n_imwrite_parameters is not None) and (n_imwrite_parameters < 3):
759
+ imwrite_patched = True
760
+ if verbose and (quality is not None):
761
+ print('Warning: quality value supplied, but YOLOv5 has mucked with cv2.imwrite, ignoring quality')
762
+
763
+ # for frame_number in tqdm(range(0,n_frames)):
764
+ for frame_number in range(0,n_frames):
765
+
766
+ # Special handling for the case where we're just doing dummy reads
767
+ if bypass_extraction:
768
+ break
769
+
770
+ success,image = vidcap.read()
771
+ if not success:
772
+ assert image is None
773
+ if verbose:
774
+ print('Read terminating at frame {} of {}'.format(frame_number,n_frames))
775
+ break
776
+
777
+ if every_n_frames is not None:
778
+ if (frame_number % every_n_frames) != 0:
779
+ continue
780
+
781
+ if frames_to_extract is not None:
782
+ if frame_number > max(frames_to_extract):
783
+ break
784
+ if frame_number not in frames_to_extract:
785
+ continue
786
+
787
+ # Has resizing been requested?
788
+ if max_width is not None:
789
+
790
+ # image.shape is h/w/dims
791
+ input_shape = image.shape
792
+ assert input_shape[2] == 3
793
+ input_width = input_shape[1]
794
+
795
+ # Is resizing necessary?
796
+ if input_width > max_width:
797
+
798
+ scale = max_width / input_width
799
+ assert scale <= 1.0
800
+
801
+ # INTER_AREA is recommended for size reduction
802
+ image = cv2.resize(image, (0,0), fx=scale, fy=scale, interpolation=cv2.INTER_AREA)
803
+
804
+ # ...if we need to deal with resizing
805
+
806
+ frame_filename_relative = _frame_number_to_filename(frame_number)
807
+ frame_filename = os.path.join(output_folder,frame_filename_relative)
808
+ frame_filenames.append(frame_filename)
809
+
810
+ if (not overwrite) and (os.path.isfile(frame_filename)):
811
+ # print('Skipping frame {}'.format(frame_filename))
812
+ pass
813
+ else:
814
+ try:
815
+ if frame_filename.isascii():
816
+
817
+ if quality is None or imwrite_patched:
818
+ cv2.imwrite(os.path.normpath(frame_filename),image)
819
+ else:
820
+ cv2.imwrite(os.path.normpath(frame_filename),image,
821
+ [int(cv2.IMWRITE_JPEG_QUALITY), quality])
822
+ else:
823
+ if quality is None:
824
+ is_success, im_buf_arr = cv2.imencode('.jpg', image)
825
+ else:
826
+ encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), quality]
827
+ is_success, im_buf_arr = cv2.imencode('.jpg', image, encode_param)
828
+ im_buf_arr.tofile(frame_filename)
829
+ assert os.path.isfile(frame_filename), \
830
+ 'Output frame {} unavailable'.format(frame_filename)
831
+ except KeyboardInterrupt:
832
+ vidcap.release()
833
+ raise
834
+ except Exception as e:
835
+ print('Error on frame {} of {}: {}'.format(frame_number,n_frames,str(e)))
836
+
837
+ # ...for each frame
838
+
839
+ if len(frame_filenames) == 0:
840
+ if allow_empty_videos:
841
+ print('Warning: no frames extracted from file {}'.format(input_video_file))
842
+ else:
843
+ raise Exception('Error: no frames extracted from file {}'.format(input_video_file))
844
+
845
+ if verbose:
846
+ print('\nExtracted {} of {} frames for {}'.format(
847
+ len(frame_filenames),n_frames,input_video_file))
848
+
849
+ vidcap.release()
850
+ return frame_filenames,fs
851
+
852
+ # ...def video_to_frames(...)
853
+
854
+
855
+ def _video_to_frames_with_per_video_frames(args):
856
+ """
857
+ Wrapper function to handle extracting a different list of frames for
858
+ each video in a multiprocessing context.
859
+
860
+ Takes a tuple of (relative_fn, frames_for_this_video, other_args),
861
+ where (other_args) contains the arguments that are the same for each
862
+ iteration.
863
+ """
864
+
865
+ relative_fn, frames_for_this_video, other_args = args
866
+ (input_folder, output_folder_base, every_n_frames, overwrite, verbose,
867
+ quality, max_width, allow_empty_videos) = other_args
868
+
869
+ return _video_to_frames_for_folder(relative_fn, input_folder, output_folder_base,
870
+ every_n_frames, overwrite, verbose, quality, max_width,
871
+ frames_for_this_video, allow_empty_videos)
872
+
873
+
874
+ def _video_to_frames_for_folder(relative_fn,input_folder,output_folder_base,
875
+ every_n_frames,overwrite,verbose,quality,max_width,
876
+ frames_to_extract,allow_empty_videos):
877
+ """
878
+ Internal function to call video_to_frames for a single video in the context of
879
+ video_folder_to_frames; makes sure the right output folder exists, then calls
880
+ video_to_frames.
881
+ """
882
+
883
+ input_fn_absolute = os.path.join(input_folder,relative_fn)
884
+ assert os.path.isfile(input_fn_absolute),\
885
+ 'Could not find file {}'.format(input_fn_absolute)
886
+
887
+ # Create the target output folder
888
+ output_folder_video = os.path.join(output_folder_base,relative_fn)
889
+ try:
890
+ os.makedirs(output_folder_video,exist_ok=True)
891
+ except Exception:
892
+ output_folder_clean = clean_path(output_folder_video)
893
+ print('Warning: failed to create folder {}, trying {}'.format(
894
+ output_folder_video,output_folder_clean))
895
+ output_folder_video = output_folder_clean
896
+ os.makedirs(output_folder_video,exist_ok=True)
897
+
898
+ # Render frames
899
+ # input_video_file = input_fn_absolute; output_folder = output_folder_video
900
+ frame_filenames,fs = video_to_frames(input_fn_absolute,
901
+ output_folder_video,
902
+ overwrite=overwrite,
903
+ every_n_frames=every_n_frames,
904
+ verbose=verbose,
905
+ quality=quality,
906
+ max_width=max_width,
907
+ frames_to_extract=frames_to_extract,
908
+ allow_empty_videos=allow_empty_videos)
909
+
910
+ return frame_filenames,fs
911
+
912
+
913
+ def video_folder_to_frames(input_folder,
914
+ output_folder_base,
915
+ recursive=True,
916
+ overwrite=True,
917
+ n_threads=1,
918
+ every_n_frames=None,
919
+ verbose=False,
920
+ parallelization_uses_threads=True,
921
+ quality=None,
922
+ max_width=None,
923
+ frames_to_extract=None,
924
+ allow_empty_videos=False,
925
+ relative_paths_to_process=None):
926
+ """
927
+ For every video file in input_folder, creates a folder within output_folder_base, and
928
+ renders frame of that video to images in that folder.
929
+
930
+ Args:
931
+ input_folder (str): folder to process
932
+ output_folder_base (str): root folder for output images; subfolders will be
933
+ created for each input video
934
+ recursive (bool, optional): whether to recursively process videos in [input_folder]
935
+ overwrite (bool, optional): whether to overwrite existing frame images
936
+ n_threads (int, optional): number of concurrent workers to use; set to <= 1 to disable
937
+ parallelism
938
+ every_n_frames (int or float, optional): sample every Nth frame starting from the first
939
+ frame; if this is None or 1, every frame is extracted. If this is a negative value,
940
+ it's interpreted as a sampling rate in seconds, which is rounded to the nearest frame
941
+ sampling rate. Mutually exclusive with frames_to_extract.
942
+ verbose (bool, optional): enable additional debug console output
943
+ parallelization_uses_threads (bool, optional): whether to use threads (True) or
944
+ processes (False) for parallelization; ignored if n_threads <= 1
945
+ quality (int, optional): JPEG quality for frame output, from 0-100. Defaults
946
+ to the opencv default (typically 95).
947
+ max_width (int, optional): resize frames to be no wider than [max_width]
948
+ frames_to_extract (int, list of int, or dict, optional): extract this specific set of frames
949
+ from each video; mutually exclusive with every_n_frames. If all values are beyond the
950
+ length of a video, no frames are extracted. Can also be a single int, specifying a single
951
+ frame number. In the special case where frames_to_extract is [], this function still
952
+ reads video frame rates and verifies that videos are readable, but no frames are
953
+ extracted. Can be a dict mapping relative paths to lists of frame numbers to extract different
954
+ frames from each video.
955
+ allow_empty_videos (bool, optional): just print a warning if a video appears to have no
956
+ frames (by default, this is an error).
957
+ relative_paths_to_process (list, optional): only process the relative paths on this
958
+ list
959
+
960
+ Returns:
961
+ tuple: a length-3 tuple containing:
962
+ - list of lists of frame filenames; the Nth list of frame filenames corresponds to
963
+ the Nth video
964
+ - list of video frame rates; the Nth value corresponds to the Nth video
965
+ - list of video filenames
966
+ """
967
+
968
+ # Enumerate video files if necessary
969
+ if relative_paths_to_process is None:
970
+ if verbose:
971
+ print('Enumerating videos in {}'.format(input_folder))
972
+ input_files_full_paths = find_videos(input_folder,recursive=recursive)
973
+ if verbose:
974
+ print('Found {} videos in folder {}'.format(len(input_files_full_paths),input_folder))
975
+ if len(input_files_full_paths) == 0:
976
+ return [],[],[]
977
+
978
+ input_files_relative_paths = [os.path.relpath(s,input_folder) for s in input_files_full_paths]
979
+ else:
980
+ input_files_relative_paths = relative_paths_to_process
981
+ input_files_full_paths = [os.path.join(input_folder,fn) for fn in input_files_relative_paths]
982
+
983
+ input_files_relative_paths = [s.replace('\\','/') for s in input_files_relative_paths]
984
+
985
+ os.makedirs(output_folder_base,exist_ok=True)
986
+
987
+ frame_filenames_by_video = []
988
+ fs_by_video = []
989
+
990
+ if n_threads == 1:
991
+
992
+ # For each video
993
+ #
994
+ # input_fn_relative = input_files_relative_paths[0]
995
+ for input_fn_relative in tqdm(input_files_relative_paths,desc='Video to frames'):
996
+
997
+ # If frames_to_extract is a dict, get the specific frames for this video
998
+ if isinstance(frames_to_extract, dict):
999
+ frames_for_this_video = frames_to_extract.get(input_fn_relative, [])
1000
+ else:
1001
+ frames_for_this_video = frames_to_extract
1002
+
1003
+ frame_filenames,fs = \
1004
+ _video_to_frames_for_folder(input_fn_relative,
1005
+ input_folder,
1006
+ output_folder_base,
1007
+ every_n_frames,
1008
+ overwrite,
1009
+ verbose,
1010
+ quality,
1011
+ max_width,
1012
+ frames_for_this_video,
1013
+ allow_empty_videos)
1014
+ frame_filenames_by_video.append(frame_filenames)
1015
+ fs_by_video.append(fs)
1016
+
1017
+ else:
1018
+
1019
+ pool = None
1020
+ results = None
1021
+ try:
1022
+
1023
+ if parallelization_uses_threads:
1024
+ print('Starting a worker pool with {} threads'.format(n_threads))
1025
+ pool = ThreadPool(n_threads)
1026
+ else:
1027
+ print('Starting a worker pool with {} processes'.format(n_threads))
1028
+ pool = Pool(n_threads)
1029
+
1030
+ if isinstance(frames_to_extract, dict):
1031
+
1032
+ # For the dict case, we need to extract different frames from each video.
1033
+
1034
+ # These arguments are the same for every iteration
1035
+ other_args = (input_folder, output_folder_base, every_n_frames, overwrite,
1036
+ verbose, quality, max_width, allow_empty_videos)
1037
+
1038
+ # The filename and list of frames to extract vary with each iteration
1039
+ args_for_pool = [(relative_fn, frames_to_extract.get(relative_fn, []), other_args)
1040
+ for relative_fn in input_files_relative_paths]
1041
+
1042
+ results = list(tqdm(pool.imap(_video_to_frames_with_per_video_frames, args_for_pool),
1043
+ total=len(args_for_pool),desc='Video to frames'))
1044
+
1045
+ else:
1046
+
1047
+ process_video_with_options = partial(_video_to_frames_for_folder,
1048
+ input_folder=input_folder,
1049
+ output_folder_base=output_folder_base,
1050
+ every_n_frames=every_n_frames,
1051
+ overwrite=overwrite,
1052
+ verbose=verbose,
1053
+ quality=quality,
1054
+ max_width=max_width,
1055
+ frames_to_extract=frames_to_extract,
1056
+ allow_empty_videos=allow_empty_videos)
1057
+ results = list(tqdm(pool.imap(process_video_with_options, input_files_relative_paths),
1058
+ total=len(input_files_relative_paths),desc='Video to frames'))
1059
+
1060
+ # ...if we need to pass different frames for each video
1061
+
1062
+ finally:
1063
+
1064
+ if pool is not None:
1065
+ pool.close()
1066
+ pool.join()
1067
+ print('Pool closed and joined for video processing')
1068
+
1069
+ # ...try/finally
1070
+
1071
+ frame_filenames_by_video = [x[0] for x in results]
1072
+ fs_by_video = [x[1] for x in results]
1073
+
1074
+ # ...if we're working on a single thread vs. multiple workers
1075
+
1076
+ return frame_filenames_by_video,fs_by_video,input_files_full_paths
1077
+
1078
+ # ...def video_folder_to_frames(...)
1079
+
1080
+
1081
+ class FrameToVideoOptions:
1082
+ """
1083
+ Options controlling the conversion of frame-level results to video-level results via
1084
+ frame_results_to_video_results()
1085
+ """
1086
+
1087
+ def __init__(self):
1088
+
1089
+ #: One-indexed indicator of which frame-level confidence value to use to determine detection confidence
1090
+ #: for the whole video, i.e. "1" means "use the confidence value from the highest-confidence frame"
1091
+ self.nth_highest_confidence = 1
1092
+
1093
+ #: Should we include just a single representative frame result for each video (default), or
1094
+ #: every frame that was processed?
1095
+ self.include_all_processed_frames = False
1096
+
1097
+ #: What to do if a file referred to in a .json results file appears not to be a
1098
+ #: video; can be 'error' or 'skip_with_warning'
1099
+ self.non_video_behavior = 'error'
1100
+
1101
+ #: Are frame rates required?
1102
+ self.frame_rates_are_required = False
1103
+
1104
+ #: Enable additional debug output
1105
+ self.verbose = False
1106
+
1107
+
1108
+ def frame_results_to_video_results(input_file,
1109
+ output_file,
1110
+ options=None,
1111
+ video_filename_to_frame_rate=None):
1112
+ """
1113
+ Given an MD results file produced at the *frame* level, corresponding to a directory
1114
+ created with video_folder_to_frames, maps those frame-level results back to the
1115
+ video level for use in Timelapse.
1116
+
1117
+ Preserves everything in the input .json file other than the images.
1118
+
1119
+ Args:
1120
+ input_file (str): the frame-level MD results file to convert to video-level results
1121
+ output_file (str): the .json file to which we should write video-level results
1122
+ options (FrameToVideoOptions, optional): parameters for converting frame-level results
1123
+ to video-level results, see FrameToVideoOptions for details
1124
+ video_filename_to_frame_rate (dict, optional): maps (relative) video path names to frame
1125
+ rates, used only to populate the output file
1126
+ """
1127
+
1128
+ if options is None:
1129
+ options = FrameToVideoOptions()
1130
+
1131
+ if options.frame_rates_are_required:
1132
+ assert video_filename_to_frame_rate is not None, \
1133
+ 'You specified that frame rates are required, but you did not ' + \
1134
+ 'supply video_filename_to_frame_rate'
1135
+
1136
+ # Load results
1137
+ with open(input_file,'r') as f:
1138
+ input_data = json.load(f)
1139
+
1140
+ images = input_data['images']
1141
+ detection_categories = input_data['detection_categories']
1142
+
1143
+
1144
+ ## Break into videos
1145
+
1146
+ video_to_frame_info = defaultdict(list)
1147
+
1148
+ # im = images[0]
1149
+ for im in tqdm(images):
1150
+
1151
+ fn = im['file']
1152
+ video_name = os.path.dirname(fn)
1153
+
1154
+ if not is_video_file(video_name):
1155
+
1156
+ if options.non_video_behavior == 'error':
1157
+ raise ValueError('{} is not a video file'.format(video_name))
1158
+ elif options.non_video_behavior == 'skip_with_warning':
1159
+ print('Warning: {} is not a video file'.format(video_name))
1160
+ continue
1161
+ else:
1162
+ raise ValueError('Unrecognized non-video handling behavior: {}'.format(
1163
+ options.non_video_behavior))
1164
+
1165
+ # Attach video-specific fields to the output, specifically attach the frame
1166
+ # number to both the video and each detection.
1167
+ frame_number = _filename_to_frame_number(fn)
1168
+ im['frame_number'] = frame_number
1169
+ for detection in im['detections']:
1170
+ detection['frame_number'] = frame_number
1171
+
1172
+ video_to_frame_info[video_name].append(im)
1173
+
1174
+ # ...for each frame referred to in the results file
1175
+
1176
+ print('Found {} unique videos in {} frame-level results'.format(
1177
+ len(video_to_frame_info),len(images)))
1178
+
1179
+ output_images = []
1180
+
1181
+
1182
+ ## For each video...
1183
+
1184
+ # video_name = list(video_to_frame_info.keys())[0]
1185
+ for video_name in tqdm(video_to_frame_info):
1186
+
1187
+ # Prepare the output representation for this video
1188
+ im_out = {}
1189
+ im_out['file'] = video_name
1190
+
1191
+ if (video_filename_to_frame_rate is not None):
1192
+
1193
+ if video_name not in video_filename_to_frame_rate:
1194
+
1195
+ s = 'Could not determine frame rate for {}'.format(video_name)
1196
+ if options.frame_rates_are_required:
1197
+ raise ValueError(s)
1198
+ elif options.verbose:
1199
+ print('Warning: {}'.format(s))
1200
+
1201
+ if video_name in video_filename_to_frame_rate:
1202
+ im_out['frame_rate'] = video_filename_to_frame_rate[video_name]
1203
+
1204
+ # Find all detections for this video
1205
+ all_detections_this_video = []
1206
+
1207
+ frames = video_to_frame_info[video_name]
1208
+
1209
+ # frame = frames[0]
1210
+ for frame in frames:
1211
+ if ('detections' in frame) and (frame['detections'] is not None):
1212
+ all_detections_this_video.extend(frame['detections'])
1213
+
1214
+ # Should we keep detections for all frames?
1215
+ if (options.include_all_processed_frames):
1216
+
1217
+ im_out['detections'] = all_detections_this_video
1218
+
1219
+ # ...or should we keep just a canonical detection for each category?
1220
+ else:
1221
+
1222
+ canonical_detections = []
1223
+
1224
+ # category_id = list(detection_categories.keys())[0]
1225
+ for category_id in detection_categories:
1226
+
1227
+ category_detections = [det for det in all_detections_this_video if \
1228
+ det['category'] == category_id]
1229
+
1230
+ # Find the nth-highest-confidence video to choose a confidence value
1231
+ if len(category_detections) >= options.nth_highest_confidence:
1232
+
1233
+ category_detections_by_confidence = sorted(category_detections,
1234
+ key = lambda i: i['conf'],reverse=True)
1235
+ canonical_detection = category_detections_by_confidence[options.nth_highest_confidence-1]
1236
+ canonical_detections.append(canonical_detection)
1237
+
1238
+ im_out['detections'] = canonical_detections
1239
+
1240
+ # 'max_detection_conf' is no longer included in output files by default
1241
+ if False:
1242
+ im_out['max_detection_conf'] = 0
1243
+ if len(canonical_detections) > 0:
1244
+ confidences = [d['conf'] for d in canonical_detections]
1245
+ im_out['max_detection_conf'] = max(confidences)
1246
+
1247
+ # ...if we're keeping output for all frames / canonical frames
1248
+
1249
+ output_images.append(im_out)
1250
+
1251
+ # ...for each video
1252
+
1253
+ output_data = input_data
1254
+ output_data['images'] = output_images
1255
+ s = json.dumps(output_data,indent=1)
1256
+
1257
+ # Write the output file
1258
+ with open(output_file,'w') as f:
1259
+ f.write(s)
1260
+
1261
+ # ...def frame_results_to_video_results(...)
1262
+
1263
+
1264
+ #%% Test drivers
1265
+
1266
+ if False:
1267
+
1268
+ pass
1269
+
1270
+ #%% Constants
1271
+
1272
+ input_folder = r'G:\temp\usu-long\data'
1273
+ frame_folder_base = r'g:\temp\usu-long-single-frames'
1274
+ assert os.path.isdir(input_folder)
1275
+
1276
+
1277
+ #%% Split videos into frames
1278
+
1279
+ frame_filenames_by_video,fs_by_video,video_filenames = \
1280
+ video_folder_to_frames(input_folder,
1281
+ frame_folder_base,
1282
+ recursive=True,
1283
+ overwrite=True,
1284
+ n_threads=10,
1285
+ every_n_frames=None,
1286
+ verbose=True,
1287
+ parallelization_uses_threads=True,
1288
+ quality=None,
1289
+ max_width=None,
1290
+ frames_to_extract=150)
1291
+
1292
+
1293
+ #%% Constants for detection tests
1294
+
1295
+ detected_frame_folder_base = r'e:\video_test\detected_frames'
1296
+ rendered_videos_folder_base = r'e:\video_test\rendered_videos'
1297
+ os.makedirs(detected_frame_folder_base,exist_ok=True)
1298
+ os.makedirs(rendered_videos_folder_base,exist_ok=True)
1299
+ results_file = r'results.json'
1300
+ confidence_threshold = 0.75
1301
+
1302
+
1303
+ #%% Load detector output
1304
+
1305
+ with open(results_file,'r') as f:
1306
+ detection_results = json.load(f)
1307
+ detections = detection_results['images']
1308
+ detector_label_map = detection_results['detection_categories']
1309
+ for d in detections:
1310
+ d['file'] = d['file'].replace('\\','/').replace('video_frames/','')
1311
+
1312
+
1313
+ #%% List image files, break into folders
1314
+
1315
+ frame_files = path_utils.find_images(frame_folder_base,True)
1316
+ frame_files = [s.replace('\\','/') for s in frame_files]
1317
+ print('Enumerated {} total frames'.format(len(frame_files)))
1318
+
1319
+ # Find unique folders
1320
+ folders = set()
1321
+ # fn = frame_files[0]
1322
+ for fn in frame_files:
1323
+ folders.add(os.path.dirname(fn))
1324
+ folders = [s.replace('\\','/') for s in folders]
1325
+ print('Found {} folders for {} files'.format(len(folders),len(frame_files)))
1326
+
1327
+
1328
+ #%% Render detector frames
1329
+
1330
+ # folder = list(folders)[0]
1331
+ for folder in folders:
1332
+
1333
+ frame_files_this_folder = [fn for fn in frame_files if folder in fn]
1334
+ folder_relative = folder.replace((frame_folder_base + '/').replace('\\','/'),'')
1335
+ detection_results_this_folder = [d for d in detections if folder_relative in d['file']]
1336
+ print('Found {} detections in folder {}'.format(len(detection_results_this_folder),folder))
1337
+ assert len(frame_files_this_folder) == len(detection_results_this_folder)
1338
+
1339
+ rendered_frame_output_folder = os.path.join(detected_frame_folder_base,folder_relative)
1340
+ os.makedirs(rendered_frame_output_folder,exist_ok=True)
1341
+
1342
+ # d = detection_results_this_folder[0]
1343
+ for d in tqdm(detection_results_this_folder):
1344
+
1345
+ input_file = os.path.join(frame_folder_base,d['file'])
1346
+ output_file = os.path.join(detected_frame_folder_base,d['file'])
1347
+ os.makedirs(os.path.dirname(output_file),exist_ok=True)
1348
+ vis_utils.draw_bounding_boxes_on_file(input_file,output_file,d['detections'],
1349
+ confidence_threshold)
1350
+
1351
+ # ...for each file in this folder
1352
+
1353
+ # ...for each folder
1354
+
1355
+
1356
+ #%% Render output videos
1357
+
1358
+ # folder = list(folders)[0]
1359
+ for folder in tqdm(folders):
1360
+
1361
+ folder_relative = folder.replace((frame_folder_base + '/').replace('\\','/'),'')
1362
+ rendered_detector_output_folder = os.path.join(detected_frame_folder_base,folder_relative)
1363
+ assert os.path.isdir(rendered_detector_output_folder)
1364
+
1365
+ frame_files_relative = os.listdir(rendered_detector_output_folder)
1366
+ frame_files_absolute = [os.path.join(rendered_detector_output_folder,s) \
1367
+ for s in frame_files_relative]
1368
+
1369
+ output_video_filename = os.path.join(rendered_videos_folder_base,folder_relative)
1370
+ os.makedirs(os.path.dirname(output_video_filename),exist_ok=True)
1371
+
1372
+ original_video_filename = output_video_filename.replace(
1373
+ rendered_videos_folder_base,input_folder)
1374
+ assert os.path.isfile(original_video_filename)
1375
+ fs = get_video_fs(original_video_filename)
1376
+
1377
+ frames_to_video(frame_files_absolute, fs, output_video_filename)
1378
+
1379
+ # ...for each video