megadetector 10.0.2__py3-none-any.whl → 10.0.4__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.
- megadetector/data_management/animl_to_md.py +158 -0
- megadetector/data_management/zamba_to_md.py +188 -0
- megadetector/detection/process_video.py +165 -946
- megadetector/detection/pytorch_detector.py +575 -276
- megadetector/detection/run_detector_batch.py +629 -202
- megadetector/detection/run_md_and_speciesnet.py +1319 -0
- megadetector/detection/video_utils.py +243 -107
- megadetector/postprocessing/classification_postprocessing.py +12 -1
- megadetector/postprocessing/combine_batch_outputs.py +2 -0
- megadetector/postprocessing/compare_batch_results.py +21 -2
- megadetector/postprocessing/merge_detections.py +16 -12
- megadetector/postprocessing/separate_detections_into_folders.py +1 -1
- megadetector/postprocessing/subset_json_detector_output.py +1 -3
- megadetector/postprocessing/validate_batch_results.py +25 -2
- megadetector/tests/__init__.py +0 -0
- megadetector/tests/test_nms_synthetic.py +335 -0
- megadetector/utils/ct_utils.py +69 -5
- megadetector/utils/extract_frames_from_video.py +303 -0
- megadetector/utils/md_tests.py +583 -524
- megadetector/utils/path_utils.py +4 -15
- megadetector/utils/wi_utils.py +20 -4
- megadetector/visualization/visualization_utils.py +1 -1
- megadetector/visualization/visualize_db.py +8 -22
- megadetector/visualization/visualize_detector_output.py +7 -5
- megadetector/visualization/visualize_video_output.py +607 -0
- {megadetector-10.0.2.dist-info → megadetector-10.0.4.dist-info}/METADATA +134 -135
- {megadetector-10.0.2.dist-info → megadetector-10.0.4.dist-info}/RECORD +30 -23
- {megadetector-10.0.2.dist-info → megadetector-10.0.4.dist-info}/licenses/LICENSE +0 -0
- {megadetector-10.0.2.dist-info → megadetector-10.0.4.dist-info}/top_level.txt +0 -0
- {megadetector-10.0.2.dist-info → megadetector-10.0.4.dist-info}/WHEEL +0 -0
|
@@ -16,30 +16,21 @@ repeat detection elimination).
|
|
|
16
16
|
|
|
17
17
|
import os
|
|
18
18
|
import sys
|
|
19
|
-
import tempfile
|
|
20
19
|
import argparse
|
|
21
|
-
import itertools
|
|
22
|
-
import json
|
|
23
|
-
import shutil
|
|
24
|
-
import getpass
|
|
25
|
-
|
|
26
|
-
from uuid import uuid1
|
|
27
20
|
|
|
28
21
|
from megadetector.detection import run_detector_batch
|
|
29
|
-
from megadetector.visualization import visualize_detector_output
|
|
30
22
|
from megadetector.utils.ct_utils import args_to_object
|
|
31
23
|
from megadetector.utils.ct_utils import dict_to_kvp_list, parse_kvp_list
|
|
32
|
-
from megadetector.
|
|
33
|
-
from megadetector.detection.video_utils import
|
|
34
|
-
from megadetector.detection.video_utils import run_callback_on_frames
|
|
24
|
+
from megadetector.detection.video_utils import _filename_to_frame_number
|
|
25
|
+
from megadetector.detection.video_utils import find_videos
|
|
35
26
|
from megadetector.detection.video_utils import run_callback_on_frames_for_folder
|
|
36
|
-
from megadetector.detection.video_utils import frames_to_video
|
|
37
|
-
from megadetector.detection.video_utils import frame_results_to_video_results
|
|
38
|
-
from megadetector.detection.video_utils import FrameToVideoOptions
|
|
39
|
-
from megadetector.detection.video_utils import _add_frame_numbers_to_results
|
|
40
|
-
from megadetector.detection.video_utils import video_folder_to_frames
|
|
41
|
-
from megadetector.detection.video_utils import default_fourcc
|
|
42
27
|
from megadetector.detection.run_detector import load_detector
|
|
28
|
+
from megadetector.postprocessing.validate_batch_results import \
|
|
29
|
+
ValidateBatchResultsOptions, validate_batch_results
|
|
30
|
+
|
|
31
|
+
# Notes to self re: upcoming work on checkpointing
|
|
32
|
+
from megadetector.utils.ct_utils import split_list_into_fixed_size_chunks # noqa
|
|
33
|
+
from megadetector.detection.run_detector_batch import write_checkpoint, load_checkpoint # noqa
|
|
43
34
|
|
|
44
35
|
|
|
45
36
|
#%% Classes
|
|
@@ -65,104 +56,23 @@ class ProcessVideoOptions:
|
|
|
65
56
|
#: .json file to which we should write results
|
|
66
57
|
self.output_json_file = None
|
|
67
58
|
|
|
68
|
-
#: File to which we should write a video with boxes, only relevant if
|
|
69
|
-
#: render_output_video is True
|
|
70
|
-
self.output_video_file = None
|
|
71
|
-
|
|
72
|
-
#: Folder to use for extracted frames; will use a folder in system temp space
|
|
73
|
-
#: if this is None
|
|
74
|
-
self.frame_folder = None
|
|
75
|
-
|
|
76
|
-
#: Folder to use for rendered frames (if rendering output video); will use a folder
|
|
77
|
-
#: in system temp space if this is None
|
|
78
|
-
self.frame_rendering_folder = None
|
|
79
|
-
|
|
80
|
-
#: Should we render a video with detection boxes?
|
|
81
|
-
#:
|
|
82
|
-
#: If processing a folder, this renders each input video to a separate
|
|
83
|
-
#: video with detection boxes.
|
|
84
|
-
self.render_output_video = False
|
|
85
|
-
|
|
86
|
-
#: If we are rendering boxes to a new video, should we keep the temporary
|
|
87
|
-
#: rendered frames?
|
|
88
|
-
self.keep_rendered_frames = False
|
|
89
|
-
|
|
90
|
-
#: Should we keep the extracted frames?
|
|
91
|
-
self.keep_extracted_frames = False
|
|
92
|
-
|
|
93
|
-
#: Should we delete the entire folder the extracted frames are written to?
|
|
94
|
-
#:
|
|
95
|
-
#: By default, we delete the frame files but leave the (probably-empty) folder in place,
|
|
96
|
-
#: for no reason other than being paranoid about deleting folders.
|
|
97
|
-
self.force_extracted_frame_folder_deletion = False
|
|
98
|
-
|
|
99
|
-
#: Should we delete the entire folder the rendered frames are written to?
|
|
100
|
-
#:
|
|
101
|
-
#: By default, we delete the frame files but leave the (probably-empty) folder in place,
|
|
102
|
-
#: for no reason other than being paranoid about deleting folders.
|
|
103
|
-
self.force_rendered_frame_folder_deletion = False
|
|
104
|
-
|
|
105
|
-
#: If we've already run MegaDetector on this video or folder of videos, i.e. if we
|
|
106
|
-
#: find a corresponding MD results file, should we re-use it? Defaults to reprocessing.
|
|
107
|
-
self.reuse_results_if_available = False
|
|
108
|
-
|
|
109
|
-
#: If we've already split this video or folder of videos into frames, should we
|
|
110
|
-
#: we re-use those extracted frames? Defaults to reprocessing.
|
|
111
|
-
self.reuse_frames_if_available = False
|
|
112
|
-
|
|
113
59
|
#: If [input_video_file] is a folder, should we search for videos recursively?
|
|
114
60
|
self.recursive = False
|
|
115
61
|
|
|
116
62
|
#: Enable additional debug console output
|
|
117
63
|
self.verbose = False
|
|
118
64
|
|
|
119
|
-
#: fourcc code to use for writing videos; only relevant if render_output_video is True
|
|
120
|
-
self.fourcc = None
|
|
121
|
-
|
|
122
|
-
#: force a specific frame rate for output videos; only relevant if render_output_video
|
|
123
|
-
#: is True
|
|
124
|
-
self.rendering_fs = None
|
|
125
|
-
|
|
126
|
-
#: Confidence threshold to use for writing videos with boxes, only relevant if
|
|
127
|
-
#: if render_output_video is True. Defaults to choosing a reasonable threshold
|
|
128
|
-
#: based on the model version.
|
|
129
|
-
self.rendering_confidence_threshold = None
|
|
130
|
-
|
|
131
65
|
#: Detections below this threshold will not be included in the output file.
|
|
132
66
|
self.json_confidence_threshold = 0.005
|
|
133
67
|
|
|
134
68
|
#: Sample every Nth frame; set to None (default) or 1 to sample every frame. Typically
|
|
135
69
|
#: we sample down to around 3 fps, so for typical 30 fps videos, frame_sample=10 is a
|
|
136
|
-
#: typical value. Mutually exclusive with [
|
|
70
|
+
#: typical value. Mutually exclusive with [time_sample].
|
|
137
71
|
self.frame_sample = None
|
|
138
72
|
|
|
139
|
-
#:
|
|
140
|
-
#: [frame_sample] and [time_sample].
|
|
141
|
-
self.frames_to_extract = None
|
|
142
|
-
|
|
143
|
-
# Sample frames every N seconds. Mutually exclusive with [frame_sample] and [frames_to_extract].
|
|
73
|
+
#: Sample frames every N seconds. Mutually exclusive with [frame_sample]
|
|
144
74
|
self.time_sample = None
|
|
145
75
|
|
|
146
|
-
#: Number of workers to use for parallelization; set to <= 1 to disable parallelization
|
|
147
|
-
self.n_cores = 1
|
|
148
|
-
|
|
149
|
-
#: For debugging only, stop processing after a certain number of frames.
|
|
150
|
-
self.debug_max_frames = -1
|
|
151
|
-
|
|
152
|
-
#: For debugging only, force on-disk frame extraction, even if it wouldn't otherwise be
|
|
153
|
-
#: necessary
|
|
154
|
-
self.force_on_disk_frame_extraction = False
|
|
155
|
-
|
|
156
|
-
#: File containing non-standard categories, typically only used if you're running a non-MD
|
|
157
|
-
#: detector.
|
|
158
|
-
self.class_mapping_filename = None
|
|
159
|
-
|
|
160
|
-
#: JPEG quality for frame output, from 0-100. Use None or -1 to let opencv decide.
|
|
161
|
-
self.quality = 90
|
|
162
|
-
|
|
163
|
-
#: Resize frames so they're at most this wide
|
|
164
|
-
self.max_width = None
|
|
165
|
-
|
|
166
76
|
#: Run the model at this image size (don't mess with this unless you know what you're
|
|
167
77
|
#: getting into)... if you just want to pass smaller frames to MD, use max_width
|
|
168
78
|
self.image_size = None
|
|
@@ -171,17 +81,23 @@ class ProcessVideoOptions:
|
|
|
171
81
|
self.augment = False
|
|
172
82
|
|
|
173
83
|
#: By default, a video with no frames (or no frames retrievable with the current parameters)
|
|
174
|
-
#: is
|
|
175
|
-
#: frame from each video, but a video only has 50 frames.
|
|
84
|
+
#: is treated as a failure; this causes it to be treated as a video with no detections.
|
|
176
85
|
self.allow_empty_videos = False
|
|
177
86
|
|
|
178
|
-
#: When processing a folder of videos, should we include just a single representative
|
|
179
|
-
#: frame result for each video (default), or every frame that was processed?
|
|
180
|
-
self.include_all_processed_frames = False
|
|
181
|
-
|
|
182
87
|
#: Detector-specific options
|
|
183
88
|
self.detector_options = None
|
|
184
89
|
|
|
90
|
+
#: Write a checkpoint file (to resume processing later) every N videos;
|
|
91
|
+
#: set to -1 (default) to disable checkpointing
|
|
92
|
+
self.checkpoint_frequency = -1
|
|
93
|
+
|
|
94
|
+
#: Path to checkpoint file; None (default) for auto-generation based on output filename
|
|
95
|
+
self.checkpoint_path = None
|
|
96
|
+
|
|
97
|
+
#: Resume from a checkpoint file, or "auto" to use the most recent checkpoint in the
|
|
98
|
+
#: output directory
|
|
99
|
+
self.resume_from_checkpoint = None
|
|
100
|
+
|
|
185
101
|
# ...class ProcessVideoOptions
|
|
186
102
|
|
|
187
103
|
|
|
@@ -197,369 +113,16 @@ def _validate_video_options(options):
|
|
|
197
113
|
n_sampling_options_configured += 1
|
|
198
114
|
if options.time_sample is not None:
|
|
199
115
|
n_sampling_options_configured += 1
|
|
200
|
-
if options.frames_to_extract is not None:
|
|
201
|
-
n_sampling_options_configured += 1
|
|
202
116
|
|
|
203
117
|
if n_sampling_options_configured > 1:
|
|
204
|
-
raise ValueError('frame_sample
|
|
118
|
+
raise ValueError('frame_sample and time_sample are mutually exclusive')
|
|
205
119
|
|
|
206
120
|
return True
|
|
207
121
|
|
|
208
122
|
|
|
209
|
-
def
|
|
123
|
+
def process_videos(options):
|
|
210
124
|
"""
|
|
211
|
-
|
|
212
|
-
just defines them.
|
|
213
|
-
"""
|
|
214
|
-
|
|
215
|
-
tempdir = os.path.join(tempfile.gettempdir(), 'process_camera_trap_video')
|
|
216
|
-
|
|
217
|
-
# If we create a folder like "process_camera_trap_video" in the system temp dir, it may
|
|
218
|
-
# be the case that no one else can write to it, even to create user-specific subfolders.
|
|
219
|
-
# If we create a uuid-named folder in the system temp dir, we make a mess.
|
|
220
|
-
#
|
|
221
|
-
# Compromise with "process_camera_trap_video-[user]".
|
|
222
|
-
user_tempdir = tempdir + '-' + getpass.getuser()
|
|
223
|
-
|
|
224
|
-
# I don't know whether it's possible for a username to contain characters that are
|
|
225
|
-
# not valid filename characters, but just to be sure...
|
|
226
|
-
user_tempdir = clean_path(user_tempdir)
|
|
227
|
-
|
|
228
|
-
frame_output_folder = os.path.join(
|
|
229
|
-
user_tempdir, os.path.basename(options.input_video_file) + '_frames_' + str(uuid1()))
|
|
230
|
-
|
|
231
|
-
rendering_output_folder = os.path.join(
|
|
232
|
-
tempdir, os.path.basename(options.input_video_file) + '_detections_' + str(uuid1()))
|
|
233
|
-
|
|
234
|
-
temporary_folder_info = \
|
|
235
|
-
{
|
|
236
|
-
'temp_folder_base':user_tempdir,
|
|
237
|
-
'frame_output_folder':frame_output_folder,
|
|
238
|
-
'rendering_output_folder':rendering_output_folder
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return temporary_folder_info
|
|
242
|
-
|
|
243
|
-
# ...def _create_frame_output_folders(...)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
def _clean_up_rendered_frames(options,rendering_output_folder,detected_frame_files):
|
|
247
|
-
"""
|
|
248
|
-
If necessary, delete rendered frames and/or the entire rendering output folder.
|
|
249
|
-
"""
|
|
250
|
-
|
|
251
|
-
if rendering_output_folder is None:
|
|
252
|
-
return
|
|
253
|
-
|
|
254
|
-
caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
|
|
255
|
-
|
|
256
|
-
# (Optionally) delete the temporary directory we used for rendered detection images
|
|
257
|
-
if not options.keep_rendered_frames:
|
|
258
|
-
|
|
259
|
-
try:
|
|
260
|
-
|
|
261
|
-
# If (a) we're supposed to delete the temporary rendering folder no
|
|
262
|
-
# matter where it is and (b) we created it in temp space, delete the
|
|
263
|
-
# whole tree
|
|
264
|
-
if options.force_rendered_frame_folder_deletion and \
|
|
265
|
-
(not caller_provided_rendering_output_folder):
|
|
266
|
-
|
|
267
|
-
if options.verbose:
|
|
268
|
-
print('Recursively deleting rendered frame folder {}'.format(
|
|
269
|
-
rendering_output_folder))
|
|
270
|
-
|
|
271
|
-
shutil.rmtree(rendering_output_folder)
|
|
272
|
-
|
|
273
|
-
# ...otherwise just delete the frames, but leave the folder in place
|
|
274
|
-
else:
|
|
275
|
-
|
|
276
|
-
if options.force_rendered_frame_folder_deletion:
|
|
277
|
-
assert caller_provided_rendering_output_folder
|
|
278
|
-
print('Warning: force_rendered_frame_folder_deletion supplied with a ' + \
|
|
279
|
-
'user-provided folder, only removing frames')
|
|
280
|
-
|
|
281
|
-
for rendered_frame_fn in detected_frame_files:
|
|
282
|
-
os.remove(rendered_frame_fn)
|
|
283
|
-
|
|
284
|
-
except Exception as e:
|
|
285
|
-
print('Warning: error deleting rendered frames from folder {}:\n{}'.format(
|
|
286
|
-
rendering_output_folder,str(e)))
|
|
287
|
-
pass
|
|
288
|
-
|
|
289
|
-
elif options.force_rendered_frame_folder_deletion:
|
|
290
|
-
|
|
291
|
-
print('Warning: keep_rendered_frames and force_rendered_frame_folder_deletion both ' + \
|
|
292
|
-
'specified, not deleting')
|
|
293
|
-
|
|
294
|
-
# ...def _clean_up_rendered_frames(...)
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
def _clean_up_extracted_frames(options,frame_output_folder,frame_filenames):
|
|
298
|
-
"""
|
|
299
|
-
If necessary, delete extracted frames and/or the entire temporary frame folder.
|
|
300
|
-
"""
|
|
301
|
-
|
|
302
|
-
if frame_output_folder is None:
|
|
303
|
-
return
|
|
304
|
-
|
|
305
|
-
caller_provided_frame_output_folder = (options.frame_folder is not None)
|
|
306
|
-
|
|
307
|
-
if not options.keep_extracted_frames:
|
|
308
|
-
|
|
309
|
-
try:
|
|
310
|
-
|
|
311
|
-
# If (a) we're supposed to delete the temporary frame folder no
|
|
312
|
-
# matter where it is and (b) we created it in temp space, delete the
|
|
313
|
-
# whole tree.
|
|
314
|
-
if options.force_extracted_frame_folder_deletion and \
|
|
315
|
-
(not caller_provided_frame_output_folder):
|
|
316
|
-
|
|
317
|
-
if options.verbose:
|
|
318
|
-
print('Recursively deleting frame output folder {}'.format(frame_output_folder))
|
|
319
|
-
|
|
320
|
-
shutil.rmtree(frame_output_folder)
|
|
321
|
-
|
|
322
|
-
# ...otherwise just delete the frames, but leave the folder in place
|
|
323
|
-
else:
|
|
324
|
-
|
|
325
|
-
if frame_filenames is None:
|
|
326
|
-
return
|
|
327
|
-
|
|
328
|
-
if options.force_extracted_frame_folder_deletion:
|
|
329
|
-
assert caller_provided_frame_output_folder
|
|
330
|
-
print('Warning: force_extracted_frame_folder_deletion supplied with a ' + \
|
|
331
|
-
'user-provided folder, only removing frames')
|
|
332
|
-
|
|
333
|
-
for extracted_frame_fn in frame_filenames:
|
|
334
|
-
os.remove(extracted_frame_fn)
|
|
335
|
-
|
|
336
|
-
except Exception as e:
|
|
337
|
-
print('Warning: error removing extracted frames from folder {}:\n{}'.format(
|
|
338
|
-
frame_output_folder,str(e)))
|
|
339
|
-
pass
|
|
340
|
-
|
|
341
|
-
elif options.force_extracted_frame_folder_deletion:
|
|
342
|
-
|
|
343
|
-
print('Warning: keep_extracted_frames and force_extracted_frame_folder_deletion both ' + \
|
|
344
|
-
'specified, not deleting')
|
|
345
|
-
|
|
346
|
-
# ...def _clean_up_extracted_frames
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
def process_video(options):
|
|
350
|
-
"""
|
|
351
|
-
Process a single video through MD, optionally writing a new video with boxes.
|
|
352
|
-
Can also be used just to split a video into frames, without running a model.
|
|
353
|
-
|
|
354
|
-
Args:
|
|
355
|
-
options (ProcessVideoOptions): all the parameters used to control this process,
|
|
356
|
-
including filenames; see ProcessVideoOptions for details
|
|
357
|
-
|
|
358
|
-
Returns:
|
|
359
|
-
dict: frame-level MegaDetector results, identical to what's in the output .json file
|
|
360
|
-
"""
|
|
361
|
-
|
|
362
|
-
# Check for incompatible options
|
|
363
|
-
_validate_video_options(options)
|
|
364
|
-
|
|
365
|
-
if options.output_json_file is None:
|
|
366
|
-
options.output_json_file = options.input_video_file + '.json'
|
|
367
|
-
|
|
368
|
-
if options.render_output_video and (options.output_video_file is None):
|
|
369
|
-
options.output_video_file = options.input_video_file + '.detections.mp4'
|
|
370
|
-
|
|
371
|
-
if options.time_sample is not None:
|
|
372
|
-
raise ValueError('Time-based sampling is not supported when processing a single video; ' + \
|
|
373
|
-
'consider processing a folder, or using frame_sample')
|
|
374
|
-
|
|
375
|
-
if options.model_file == 'no_detection' and not options.keep_extracted_frames:
|
|
376
|
-
print('Warning: you asked for no detection, but did not specify keep_extracted_frames, this is a no-op')
|
|
377
|
-
return
|
|
378
|
-
|
|
379
|
-
# Track whether frame and rendering folders were created by this script
|
|
380
|
-
caller_provided_frame_output_folder = (options.frame_folder is not None)
|
|
381
|
-
caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
|
|
382
|
-
|
|
383
|
-
frame_output_folder = None
|
|
384
|
-
frame_filenames = None
|
|
385
|
-
|
|
386
|
-
# If we should re-use existing results, and the output file exists, don't bother running MD
|
|
387
|
-
if (options.reuse_results_if_available and os.path.isfile(options.output_json_file)):
|
|
388
|
-
|
|
389
|
-
print('Loading results from {}'.format(options.output_json_file))
|
|
390
|
-
with open(options.output_json_file,'r') as f:
|
|
391
|
-
results = json.load(f)
|
|
392
|
-
|
|
393
|
-
# Run MD in memory if we don't need to generate frames
|
|
394
|
-
#
|
|
395
|
-
# Currently if we're generating an output video, we need to generate frames on disk first.
|
|
396
|
-
elif (not options.keep_extracted_frames and \
|
|
397
|
-
not options.render_output_video and \
|
|
398
|
-
not options.force_on_disk_frame_extraction):
|
|
399
|
-
|
|
400
|
-
# Run MegaDetector in memory
|
|
401
|
-
|
|
402
|
-
if options.verbose:
|
|
403
|
-
print('Running MegaDetector in memory for {}'.format(options.input_video_file))
|
|
404
|
-
|
|
405
|
-
if options.frame_folder is not None:
|
|
406
|
-
print('Warning: frame_folder specified, but keep_extracted_frames is ' + \
|
|
407
|
-
'not; no raw frames will be written')
|
|
408
|
-
|
|
409
|
-
detector = load_detector(options.model_file,detector_options=options.detector_options)
|
|
410
|
-
|
|
411
|
-
def frame_callback(image_np,image_id):
|
|
412
|
-
return detector.generate_detections_one_image(image_np,
|
|
413
|
-
image_id,
|
|
414
|
-
detection_threshold=options.json_confidence_threshold,
|
|
415
|
-
augment=options.augment)
|
|
416
|
-
|
|
417
|
-
frame_results = run_callback_on_frames(options.input_video_file,
|
|
418
|
-
frame_callback,
|
|
419
|
-
every_n_frames=options.frame_sample,
|
|
420
|
-
verbose=options.verbose,
|
|
421
|
-
frames_to_process=options.frames_to_extract)
|
|
422
|
-
|
|
423
|
-
frame_results['results'] = _add_frame_numbers_to_results(frame_results['results'])
|
|
424
|
-
|
|
425
|
-
run_detector_batch.write_results_to_file(
|
|
426
|
-
frame_results['results'],
|
|
427
|
-
options.output_json_file,
|
|
428
|
-
relative_path_base=None,
|
|
429
|
-
detector_file=options.model_file,
|
|
430
|
-
custom_metadata={'video_frame_rate':frame_results['frame_rate']})
|
|
431
|
-
|
|
432
|
-
# Extract frames and optionally run MegaDetector on those frames
|
|
433
|
-
else:
|
|
434
|
-
|
|
435
|
-
if options.verbose:
|
|
436
|
-
print('Extracting frames for {}'.format(options.input_video_file))
|
|
437
|
-
|
|
438
|
-
# This does not create any folders, just defines temporary folder names in
|
|
439
|
-
# case we need them.
|
|
440
|
-
temporary_folder_info = _select_temporary_output_folders(options)
|
|
441
|
-
|
|
442
|
-
if (caller_provided_frame_output_folder):
|
|
443
|
-
frame_output_folder = options.frame_folder
|
|
444
|
-
else:
|
|
445
|
-
frame_output_folder = temporary_folder_info['frame_output_folder']
|
|
446
|
-
|
|
447
|
-
os.makedirs(frame_output_folder, exist_ok=True)
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
## Extract frames
|
|
451
|
-
|
|
452
|
-
frame_filenames, fs = video_to_frames(
|
|
453
|
-
options.input_video_file,
|
|
454
|
-
frame_output_folder,
|
|
455
|
-
every_n_frames=options.frame_sample,
|
|
456
|
-
overwrite=(not options.reuse_frames_if_available),
|
|
457
|
-
quality=options.quality,
|
|
458
|
-
max_width=options.max_width,
|
|
459
|
-
verbose=options.verbose,
|
|
460
|
-
frames_to_extract=options.frames_to_extract,
|
|
461
|
-
allow_empty_videos=options.allow_empty_videos)
|
|
462
|
-
|
|
463
|
-
image_file_names = frame_filenames
|
|
464
|
-
if options.debug_max_frames > 0:
|
|
465
|
-
image_file_names = image_file_names[0:options.debug_max_frames]
|
|
466
|
-
|
|
467
|
-
## Run MegaDetector on those frames
|
|
468
|
-
|
|
469
|
-
if options.model_file != 'no_detection':
|
|
470
|
-
|
|
471
|
-
if options.verbose:
|
|
472
|
-
print('Running MD for {}'.format(options.input_video_file))
|
|
473
|
-
|
|
474
|
-
results = run_detector_batch.load_and_run_detector_batch(
|
|
475
|
-
options.model_file,
|
|
476
|
-
image_file_names,
|
|
477
|
-
confidence_threshold=options.json_confidence_threshold,
|
|
478
|
-
n_cores=options.n_cores,
|
|
479
|
-
class_mapping_filename=options.class_mapping_filename,
|
|
480
|
-
quiet=True,
|
|
481
|
-
augment=options.augment,
|
|
482
|
-
image_size=options.image_size,
|
|
483
|
-
detector_options=options.detector_options)
|
|
484
|
-
|
|
485
|
-
results = _add_frame_numbers_to_results(results)
|
|
486
|
-
|
|
487
|
-
run_detector_batch.write_results_to_file(
|
|
488
|
-
results,
|
|
489
|
-
options.output_json_file,
|
|
490
|
-
relative_path_base=frame_output_folder,
|
|
491
|
-
detector_file=options.model_file,
|
|
492
|
-
custom_metadata={'video_frame_rate':fs})
|
|
493
|
-
|
|
494
|
-
# ...if we are/aren't keeping raw frames on disk
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
## (Optionally) render output video
|
|
498
|
-
|
|
499
|
-
if options.render_output_video:
|
|
500
|
-
|
|
501
|
-
## Render detections to images
|
|
502
|
-
|
|
503
|
-
if (caller_provided_rendering_output_folder):
|
|
504
|
-
rendering_output_dir = options.frame_rendering_folder
|
|
505
|
-
else:
|
|
506
|
-
rendering_output_dir = temporary_folder_info['rendering_output_folder']
|
|
507
|
-
|
|
508
|
-
os.makedirs(rendering_output_dir,exist_ok=True)
|
|
509
|
-
|
|
510
|
-
detected_frame_files = visualize_detector_output.visualize_detector_output(
|
|
511
|
-
detector_output_path=options.output_json_file,
|
|
512
|
-
out_dir=rendering_output_dir,
|
|
513
|
-
images_dir=frame_output_folder,
|
|
514
|
-
confidence_threshold=options.rendering_confidence_threshold)
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
## Choose the frame rate at which we should render the output video
|
|
518
|
-
|
|
519
|
-
if options.rendering_fs is not None:
|
|
520
|
-
rendering_fs = options.rendering_fs
|
|
521
|
-
elif options.frame_sample is None and options.time_sample is None:
|
|
522
|
-
rendering_fs = fs
|
|
523
|
-
elif options.frame_sample is not None:
|
|
524
|
-
assert options.time_sample is None
|
|
525
|
-
# If the original video was 30fps and we sampled every 10th frame,
|
|
526
|
-
# render at 3fps
|
|
527
|
-
rendering_fs = fs / options.frame_sample
|
|
528
|
-
elif options.time_sample is not None:
|
|
529
|
-
rendering_fs = options.time_sample
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
## Render the output video
|
|
533
|
-
|
|
534
|
-
print('Rendering {} frames to {} at {} fps (original video {} fps)'.format(
|
|
535
|
-
len(detected_frame_files), options.output_video_file,rendering_fs,fs))
|
|
536
|
-
frames_to_video(detected_frame_files,
|
|
537
|
-
rendering_fs,
|
|
538
|
-
options.output_video_file,
|
|
539
|
-
codec_spec=options.fourcc)
|
|
540
|
-
|
|
541
|
-
# Possibly clean up rendered frames
|
|
542
|
-
_clean_up_rendered_frames(options,rendering_output_dir,detected_frame_files)
|
|
543
|
-
|
|
544
|
-
# ...if we're rendering video
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
## (Optionally) delete the extracted frames
|
|
548
|
-
|
|
549
|
-
_clean_up_extracted_frames(options, frame_output_folder, frame_filenames)
|
|
550
|
-
|
|
551
|
-
# ...process_video()
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
def process_video_folder(options):
|
|
555
|
-
"""
|
|
556
|
-
Process a folder of videos through MD. Can also be used just to split a folder of
|
|
557
|
-
videos into frames, without running a model.
|
|
558
|
-
|
|
559
|
-
When this function is used to run MD, two .json files will get written, one with
|
|
560
|
-
an entry for each *frame* (identical to what's created by process_video()), and
|
|
561
|
-
one with an entry for each *video* (which is more suitable for, e.g., reading into
|
|
562
|
-
Timelapse).
|
|
125
|
+
Process a video or folder of videos through MD.
|
|
563
126
|
|
|
564
127
|
Args:
|
|
565
128
|
options (ProcessVideoOptions): all the parameters used to control this process,
|
|
@@ -571,281 +134,137 @@ def process_video_folder(options):
|
|
|
571
134
|
# Check for incompatible options
|
|
572
135
|
_validate_video_options(options)
|
|
573
136
|
|
|
574
|
-
assert
|
|
575
|
-
'
|
|
576
|
-
|
|
577
|
-
if options.model_file == 'no_detection' and not options.keep_extracted_frames:
|
|
578
|
-
print('Warning: you asked for no detection, but did not specify keep_extracted_frames, this is a no-op')
|
|
579
|
-
return
|
|
580
|
-
|
|
581
|
-
if options.model_file != 'no_detection':
|
|
582
|
-
assert options.output_json_file is not None, \
|
|
583
|
-
'When processing a folder, you must specify an output .json file'
|
|
584
|
-
assert options.output_json_file.endswith('.json')
|
|
585
|
-
video_json = options.output_json_file
|
|
586
|
-
frames_json = options.output_json_file.replace('.json','.frames.json')
|
|
587
|
-
os.makedirs(os.path.dirname(video_json),exist_ok=True)
|
|
588
|
-
|
|
589
|
-
# Track whether frame and rendering folders were created by this script
|
|
590
|
-
caller_provided_frame_output_folder = (options.frame_folder is not None)
|
|
591
|
-
caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
|
|
592
|
-
|
|
593
|
-
# This does not create any folders, just defines temporary folder names in
|
|
594
|
-
# case we need them.
|
|
595
|
-
temporary_folder_info = _select_temporary_output_folders(options)
|
|
596
|
-
|
|
597
|
-
frame_output_folder = None
|
|
598
|
-
image_file_names = None
|
|
599
|
-
video_filename_to_fs = {}
|
|
137
|
+
assert options.output_json_file.endswith('.json'), \
|
|
138
|
+
'Illegal output file {}'.format(options.output_json_file)
|
|
600
139
|
|
|
601
140
|
if options.time_sample is not None:
|
|
602
141
|
every_n_frames_param = -1 * options.time_sample
|
|
603
142
|
else:
|
|
604
143
|
every_n_frames_param = options.frame_sample
|
|
605
144
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
# Currently if we're generating an output video, we need to generate frames on disk first.
|
|
609
|
-
if (not options.keep_extracted_frames and \
|
|
610
|
-
not options.render_output_video and \
|
|
611
|
-
not options.force_on_disk_frame_extraction):
|
|
145
|
+
if options.verbose:
|
|
146
|
+
print('Processing videos from input source {}'.format(options.input_video_file))
|
|
612
147
|
|
|
613
|
-
|
|
614
|
-
print('Running MegaDetector in memory for folder {}'.format(options.input_video_file))
|
|
148
|
+
detector = load_detector(options.model_file,detector_options=options.detector_options)
|
|
615
149
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
150
|
+
def frame_callback(image_np,image_id):
|
|
151
|
+
return detector.generate_detections_one_image(image_np,
|
|
152
|
+
image_id,
|
|
153
|
+
detection_threshold=options.json_confidence_threshold,
|
|
154
|
+
augment=options.augment,
|
|
155
|
+
image_size=options.image_size,
|
|
156
|
+
verbose=options.verbose)
|
|
619
157
|
|
|
620
|
-
|
|
158
|
+
"""
|
|
159
|
+
[md_results] will be dict with keys 'video_filenames' (list of str), 'frame_rates' (list of floats),
|
|
160
|
+
'results' (list of list of dicts). 'video_filenames' will contain *relative* filenames.
|
|
161
|
+
'results' is a list (one element per video) of lists (one element per frame) of whatever the
|
|
162
|
+
callback returns, typically (but not necessarily) dicts in the MD results format.
|
|
621
163
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
augment=options.augment)
|
|
164
|
+
For failed videos, the frame rate will be represented by -1, and "results"
|
|
165
|
+
will be a dict with at least the key "failure".
|
|
166
|
+
"""
|
|
167
|
+
if os.path.isfile(options.input_video_file):
|
|
627
168
|
|
|
628
|
-
|
|
169
|
+
video_folder = os.path.dirname(options.input_video_file)
|
|
170
|
+
video_bn = os.path.basename(options.input_video_file)
|
|
171
|
+
md_results = run_callback_on_frames_for_folder(input_video_folder=video_folder,
|
|
629
172
|
frame_callback=frame_callback,
|
|
630
173
|
every_n_frames=every_n_frames_param,
|
|
631
|
-
verbose=options.verbose
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
for i_video,video_filename in enumerate(md_results['video_filenames']):
|
|
636
|
-
video_filename = video_filename.replace('\\','/')
|
|
637
|
-
assert video_filename not in video_filename_to_fs
|
|
638
|
-
video_filename_to_fs[video_filename] = md_results['frame_rates'][i_video]
|
|
639
|
-
|
|
640
|
-
all_frame_results = []
|
|
641
|
-
|
|
642
|
-
# r = video_results[0]
|
|
643
|
-
for frame_results in video_results:
|
|
644
|
-
_add_frame_numbers_to_results(frame_results)
|
|
645
|
-
all_frame_results.extend(frame_results)
|
|
646
|
-
|
|
647
|
-
run_detector_batch.write_results_to_file(
|
|
648
|
-
all_frame_results,
|
|
649
|
-
frames_json,
|
|
650
|
-
relative_path_base=None,
|
|
651
|
-
detector_file=options.model_file)
|
|
174
|
+
verbose=options.verbose,
|
|
175
|
+
files_to_process_relative=[video_bn],
|
|
176
|
+
allow_empty_videos=options.allow_empty_videos)
|
|
652
177
|
|
|
653
178
|
else:
|
|
654
179
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
if options.verbose:
|
|
658
|
-
print('Extracting frames for folder {}'.format(options.input_video_file))
|
|
659
|
-
|
|
660
|
-
if caller_provided_frame_output_folder:
|
|
661
|
-
frame_output_folder = options.frame_folder
|
|
662
|
-
else:
|
|
663
|
-
frame_output_folder = temporary_folder_info['frame_output_folder']
|
|
664
|
-
|
|
665
|
-
os.makedirs(frame_output_folder, exist_ok=True)
|
|
666
|
-
|
|
667
|
-
frame_filenames, fs, video_filenames = \
|
|
668
|
-
video_folder_to_frames(input_folder=options.input_video_file,
|
|
669
|
-
output_folder_base=frame_output_folder,
|
|
670
|
-
recursive=options.recursive,
|
|
671
|
-
overwrite=(not options.reuse_frames_if_available),
|
|
672
|
-
n_threads=options.n_cores,
|
|
673
|
-
every_n_frames=every_n_frames_param,
|
|
674
|
-
verbose=options.verbose,
|
|
675
|
-
quality=options.quality,
|
|
676
|
-
max_width=options.max_width,
|
|
677
|
-
frames_to_extract=options.frames_to_extract,
|
|
678
|
-
allow_empty_videos=options.allow_empty_videos)
|
|
679
|
-
|
|
680
|
-
for i_video,video_filename_abs in enumerate(video_filenames):
|
|
681
|
-
video_filename_relative = os.path.relpath(video_filename_abs,options.input_video_file)
|
|
682
|
-
video_filename_relative = video_filename_relative.replace('\\','/')
|
|
683
|
-
assert video_filename_relative not in video_filename_to_fs
|
|
684
|
-
video_filename_to_fs[video_filename_relative] = fs[i_video]
|
|
685
|
-
|
|
686
|
-
print('Extracted frames for {} videos'.format(len(set(video_filenames))))
|
|
687
|
-
image_file_names = list(itertools.chain.from_iterable(frame_filenames))
|
|
688
|
-
|
|
689
|
-
if len(image_file_names) == 0:
|
|
690
|
-
if len(video_filenames) == 0:
|
|
691
|
-
print('No videos found in folder {}'.format(options.input_video_file))
|
|
692
|
-
else:
|
|
693
|
-
print('No frames extracted from folder {}, this may be due to an '\
|
|
694
|
-
'unsupported video codec'.format(options.input_video_file))
|
|
695
|
-
return
|
|
696
|
-
|
|
697
|
-
if options.debug_max_frames is not None and options.debug_max_frames > 0:
|
|
698
|
-
image_file_names = image_file_names[0:options.debug_max_frames]
|
|
699
|
-
|
|
700
|
-
if options.model_file == 'no_detection':
|
|
701
|
-
assert options.keep_extracted_frames, \
|
|
702
|
-
'Internal error: keep_extracted_frames not set, but no model specified'
|
|
703
|
-
return
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
## Run MegaDetector on the extracted frames
|
|
707
|
-
|
|
708
|
-
if options.reuse_results_if_available and \
|
|
709
|
-
os.path.isfile(frames_json):
|
|
710
|
-
|
|
711
|
-
print('Bypassing inference, loading results from {}'.format(frames_json))
|
|
712
|
-
with open(frames_json,'r') as f:
|
|
713
|
-
results = json.load(f)
|
|
714
|
-
|
|
715
|
-
else:
|
|
180
|
+
assert os.path.isdir(options.input_video_file), \
|
|
181
|
+
'{} is neither a file nor a folder'.format(options.input_video_file)
|
|
716
182
|
|
|
717
|
-
|
|
183
|
+
video_folder = options.input_video_file
|
|
718
184
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
quiet=True,
|
|
726
|
-
augment=options.augment,
|
|
727
|
-
image_size=options.image_size,
|
|
728
|
-
detector_options=options.detector_options)
|
|
185
|
+
md_results = run_callback_on_frames_for_folder(input_video_folder=options.input_video_file,
|
|
186
|
+
frame_callback=frame_callback,
|
|
187
|
+
every_n_frames=every_n_frames_param,
|
|
188
|
+
verbose=options.verbose,
|
|
189
|
+
recursive=options.recursive,
|
|
190
|
+
allow_empty_videos=options.allow_empty_videos)
|
|
729
191
|
|
|
730
|
-
|
|
192
|
+
# ...whether we're processing a file or a folder
|
|
731
193
|
|
|
732
|
-
|
|
733
|
-
results,
|
|
734
|
-
frames_json,
|
|
735
|
-
relative_path_base=frame_output_folder,
|
|
736
|
-
detector_file=options.model_file)
|
|
194
|
+
print('Finished running MD on videos')
|
|
737
195
|
|
|
738
|
-
|
|
196
|
+
video_results = md_results['results']
|
|
197
|
+
video_filenames = md_results['video_filenames']
|
|
198
|
+
video_frame_rates = md_results['frame_rates']
|
|
739
199
|
|
|
740
|
-
|
|
200
|
+
assert len(video_results) == len(video_filenames)
|
|
201
|
+
assert len(video_results) == len(video_frame_rates)
|
|
741
202
|
|
|
742
|
-
|
|
203
|
+
video_list_md_format = []
|
|
743
204
|
|
|
744
|
-
|
|
745
|
-
|
|
205
|
+
# i_video = 0; results_this_video = video_results[i_video]
|
|
206
|
+
for i_video,results_this_video in enumerate(video_results):
|
|
746
207
|
|
|
747
|
-
|
|
748
|
-
frame_results_to_video_results(frames_json,
|
|
749
|
-
video_json,
|
|
750
|
-
options=frame_to_video_options,
|
|
751
|
-
video_filename_to_frame_rate=video_filename_to_fs)
|
|
208
|
+
video_fn = video_filenames[i_video]
|
|
752
209
|
|
|
210
|
+
im = {}
|
|
211
|
+
im['file'] = video_fn
|
|
212
|
+
im['frame_rate'] = video_frame_rates[i_video]
|
|
213
|
+
im['frames_processed'] = []
|
|
753
214
|
|
|
754
|
-
|
|
215
|
+
if isinstance(results_this_video,dict):
|
|
755
216
|
|
|
756
|
-
|
|
217
|
+
assert 'failure' in results_this_video
|
|
218
|
+
im['failure'] = results_this_video['failure']
|
|
219
|
+
im['detections'] = None
|
|
757
220
|
|
|
758
|
-
# Render detections to images
|
|
759
|
-
if (caller_provided_rendering_output_folder):
|
|
760
|
-
rendering_output_dir = options.frame_rendering_folder
|
|
761
|
-
else:
|
|
762
|
-
rendering_output_dir = temporary_folder_info['rendering_output_folder']
|
|
763
|
-
|
|
764
|
-
os.makedirs(rendering_output_dir,exist_ok=True)
|
|
765
|
-
|
|
766
|
-
detected_frame_files = visualize_detector_output.visualize_detector_output(
|
|
767
|
-
detector_output_path=frames_json,
|
|
768
|
-
out_dir=rendering_output_dir,
|
|
769
|
-
images_dir=frame_output_folder,
|
|
770
|
-
confidence_threshold=options.rendering_confidence_threshold,
|
|
771
|
-
preserve_path_structure=True,
|
|
772
|
-
output_image_width=-1)
|
|
773
|
-
detected_frame_files = [s.replace('\\','/') for s in detected_frame_files]
|
|
774
|
-
|
|
775
|
-
# Choose an output folder
|
|
776
|
-
output_folder_is_input_folder = False
|
|
777
|
-
if options.output_video_file is not None:
|
|
778
|
-
if os.path.isfile(options.output_video_file):
|
|
779
|
-
raise ValueError('Rendering videos for a folder, but an existing file was specified as output')
|
|
780
|
-
elif options.output_video_file == options.input_video_file:
|
|
781
|
-
output_folder_is_input_folder = True
|
|
782
|
-
output_video_folder = options.input_video_file
|
|
783
|
-
else:
|
|
784
|
-
os.makedirs(options.output_video_file,exist_ok=True)
|
|
785
|
-
output_video_folder = options.output_video_file
|
|
786
221
|
else:
|
|
787
|
-
output_folder_is_input_folder = True
|
|
788
|
-
output_video_folder = options.input_video_file
|
|
789
222
|
|
|
790
|
-
|
|
791
|
-
#
|
|
792
|
-
# TODO: parallelize this loop
|
|
793
|
-
#
|
|
794
|
-
# i_video=0; input_video_file_abs = video_filenames[i_video]
|
|
795
|
-
for i_video,input_video_file_abs in enumerate(video_filenames):
|
|
223
|
+
im['detections'] = []
|
|
796
224
|
|
|
797
|
-
|
|
225
|
+
# results_one_frame = results_this_video[0]
|
|
226
|
+
for results_one_frame in results_this_video:
|
|
798
227
|
|
|
799
|
-
|
|
800
|
-
rendering_fs = options.rendering_fs
|
|
801
|
-
elif options.frame_sample is None:
|
|
802
|
-
rendering_fs = video_fs
|
|
803
|
-
else:
|
|
804
|
-
# If the original video was 30fps and we sampled every 10th frame,
|
|
805
|
-
# render at 3fps
|
|
806
|
-
rendering_fs = video_fs / options.frame_sample
|
|
228
|
+
assert results_one_frame['file'].startswith(video_fn)
|
|
807
229
|
|
|
808
|
-
|
|
809
|
-
video_frame_output_folder = os.path.join(rendering_output_dir,input_video_file_relative)
|
|
230
|
+
frame_number = _filename_to_frame_number(results_one_frame['file'])
|
|
810
231
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
'Could not find frame folder for video {}'.format(input_video_file_relative)
|
|
232
|
+
assert frame_number not in im['frames_processed'], \
|
|
233
|
+
'Received the same frame twice for video {}'.format(im['file'])
|
|
814
234
|
|
|
815
|
-
|
|
816
|
-
video_frame_files = [fn for fn in detected_frame_files if \
|
|
817
|
-
fn.startswith(video_frame_output_folder)]
|
|
818
|
-
assert len(video_frame_files) > 0, 'Could not find rendered frames for video {}'.format(
|
|
819
|
-
input_video_file_relative)
|
|
235
|
+
im['frames_processed'].append(frame_number)
|
|
820
236
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
video_output_file = insert_before_extension(input_video_file_abs,'annotated','_')
|
|
824
|
-
else:
|
|
825
|
-
video_output_file = os.path.join(output_video_folder,input_video_file_relative)
|
|
237
|
+
for det in results_one_frame['detections']:
|
|
238
|
+
det['frame_number'] = frame_number
|
|
826
239
|
|
|
827
|
-
|
|
240
|
+
# This is a no-op if there were no above-threshold detections
|
|
241
|
+
# in this frame
|
|
242
|
+
im['detections'].extend(results_one_frame['detections'])
|
|
828
243
|
|
|
829
|
-
#
|
|
830
|
-
print('Rendering detections for video {} to {} at {} fps (original video {} fps)'.format(
|
|
831
|
-
input_video_file_relative,video_output_file,rendering_fs,video_fs))
|
|
832
|
-
frames_to_video(video_frame_files,
|
|
833
|
-
rendering_fs,
|
|
834
|
-
video_output_file,
|
|
835
|
-
codec_spec=options.fourcc)
|
|
244
|
+
# ...for each frame
|
|
836
245
|
|
|
837
|
-
# ...
|
|
246
|
+
# ...was this a failed video?
|
|
838
247
|
|
|
839
|
-
|
|
840
|
-
_clean_up_rendered_frames(options,rendering_output_dir,detected_frame_files)
|
|
248
|
+
im['frames_processed'] = sorted(im['frames_processed'])
|
|
841
249
|
|
|
842
|
-
|
|
250
|
+
video_list_md_format.append(im)
|
|
843
251
|
|
|
252
|
+
# ...for each video
|
|
844
253
|
|
|
845
|
-
|
|
846
|
-
|
|
254
|
+
run_detector_batch.write_results_to_file(
|
|
255
|
+
video_list_md_format,
|
|
256
|
+
options.output_json_file,
|
|
257
|
+
relative_path_base=None,
|
|
258
|
+
detector_file=options.model_file)
|
|
847
259
|
|
|
848
|
-
|
|
260
|
+
validation_options = ValidateBatchResultsOptions()
|
|
261
|
+
validation_options.raise_errors = True
|
|
262
|
+
validation_options.check_image_existence = True
|
|
263
|
+
validation_options.return_data = False
|
|
264
|
+
validation_options.relative_path_base = video_folder
|
|
265
|
+
validate_batch_results(options.output_json_file,options=validation_options)
|
|
266
|
+
|
|
267
|
+
# ...process_videos()
|
|
849
268
|
|
|
850
269
|
|
|
851
270
|
def options_to_command(options):
|
|
@@ -860,62 +279,21 @@ def options_to_command(options):
|
|
|
860
279
|
|
|
861
280
|
:meta private:
|
|
862
281
|
"""
|
|
282
|
+
|
|
863
283
|
cmd = 'python process_video.py'
|
|
864
284
|
cmd += ' "' + options.model_file + '"'
|
|
865
285
|
cmd += ' "' + options.input_video_file + '"'
|
|
866
286
|
|
|
867
287
|
if options.recursive:
|
|
868
288
|
cmd += ' --recursive'
|
|
869
|
-
if options.frame_folder is not None:
|
|
870
|
-
cmd += ' --frame_folder' + ' "' + options.frame_folder + '"'
|
|
871
|
-
if options.frame_rendering_folder is not None:
|
|
872
|
-
cmd += ' --frame_rendering_folder' + ' "' + options.frame_rendering_folder + '"'
|
|
873
289
|
if options.output_json_file is not None:
|
|
874
290
|
cmd += ' --output_json_file' + ' "' + options.output_json_file + '"'
|
|
875
|
-
if options.output_video_file is not None:
|
|
876
|
-
cmd += ' --output_video_file' + ' "' + options.output_video_file + '"'
|
|
877
|
-
if options.keep_extracted_frames:
|
|
878
|
-
cmd += ' --keep_extracted_frames'
|
|
879
|
-
if options.reuse_results_if_available:
|
|
880
|
-
cmd += ' --reuse_results_if_available'
|
|
881
|
-
if options.reuse_frames_if_available:
|
|
882
|
-
cmd += ' --reuse_frames_if_available'
|
|
883
|
-
if options.render_output_video:
|
|
884
|
-
cmd += ' --render_output_video'
|
|
885
|
-
if options.keep_rendered_frames:
|
|
886
|
-
cmd += ' --keep_rendered_frames'
|
|
887
|
-
if options.rendering_confidence_threshold is not None:
|
|
888
|
-
cmd += ' --rendering_confidence_threshold ' + str(options.rendering_confidence_threshold)
|
|
889
291
|
if options.json_confidence_threshold is not None:
|
|
890
292
|
cmd += ' --json_confidence_threshold ' + str(options.json_confidence_threshold)
|
|
891
|
-
if options.n_cores is not None:
|
|
892
|
-
cmd += ' --n_cores ' + str(options.n_cores)
|
|
893
293
|
if options.frame_sample is not None:
|
|
894
294
|
cmd += ' --frame_sample ' + str(options.frame_sample)
|
|
895
|
-
if options.frames_to_extract is not None:
|
|
896
|
-
cmd += ' --frames_to_extract '
|
|
897
|
-
if isinstance(options.frames_to_extract,int):
|
|
898
|
-
frames_to_extract = [options.frames_to_extract]
|
|
899
|
-
else:
|
|
900
|
-
frames_to_extract = options.frames_to_extract
|
|
901
|
-
for frame_number in frames_to_extract:
|
|
902
|
-
cmd += ' {}'.format(frame_number)
|
|
903
|
-
if options.debug_max_frames is not None:
|
|
904
|
-
cmd += ' --debug_max_frames ' + str(options.debug_max_frames)
|
|
905
|
-
if options.class_mapping_filename is not None:
|
|
906
|
-
cmd += ' --class_mapping_filename ' + str(options.class_mapping_filename)
|
|
907
|
-
if options.fourcc is not None:
|
|
908
|
-
cmd += ' --fourcc ' + options.fourcc
|
|
909
|
-
if options.quality is not None:
|
|
910
|
-
cmd += ' --quality ' + str(options.quality)
|
|
911
|
-
if options.max_width is not None:
|
|
912
|
-
cmd += ' --max_width ' + str(options.max_width)
|
|
913
295
|
if options.verbose:
|
|
914
296
|
cmd += ' --verbose'
|
|
915
|
-
if options.force_extracted_frame_folder_deletion:
|
|
916
|
-
cmd += ' --force_extracted_frame_folder_deletion'
|
|
917
|
-
if options.force_rendered_frame_folder_deletion:
|
|
918
|
-
cmd += ' --force_rendered_frame_folder_deletion'
|
|
919
297
|
if options.detector_options is not None and len(options.detector_options) > 0:
|
|
920
298
|
cmd += '--detector_options {}'.format(dict_to_kvp_list(options.detector_options))
|
|
921
299
|
|
|
@@ -930,145 +308,58 @@ if False:
|
|
|
930
308
|
|
|
931
309
|
#%% Process a folder of videos
|
|
932
310
|
|
|
311
|
+
import os
|
|
312
|
+
from megadetector.detection.process_video import \
|
|
313
|
+
process_videos, ProcessVideoOptions
|
|
314
|
+
|
|
933
315
|
model_file = 'MDV5A'
|
|
934
|
-
|
|
935
|
-
# input_dir = r'G:\temp\md-test-package\md-test-images\video-samples'
|
|
936
|
-
input_dir = os.path.expanduser('~/AppData/Local/Temp/md-tests/md-test-images/video-samples')
|
|
316
|
+
input_dir = r"G:\temp\md-test-images\video-samples"
|
|
937
317
|
assert os.path.isdir(input_dir)
|
|
938
318
|
|
|
939
|
-
|
|
940
|
-
os.makedirs(output_base,exist_ok=True)
|
|
941
|
-
|
|
942
|
-
frame_folder = os.path.join(output_base,'frames')
|
|
943
|
-
rendering_folder = os.path.join(output_base,'rendered-frames')
|
|
944
|
-
output_json_file = os.path.join(output_base,'video-test.json')
|
|
945
|
-
output_video_folder = os.path.join(output_base,'output_videos')
|
|
946
|
-
|
|
319
|
+
output_json_file = os.path.join(input_dir,'mdv5a-video.json')
|
|
947
320
|
|
|
948
321
|
print('Processing folder {}'.format(input_dir))
|
|
949
322
|
|
|
950
323
|
options = ProcessVideoOptions()
|
|
324
|
+
options.json_confidence_threshold = 0.05
|
|
951
325
|
options.model_file = model_file
|
|
952
326
|
options.input_video_file = input_dir
|
|
953
|
-
options.output_video_file = output_video_folder
|
|
954
327
|
options.output_json_file = output_json_file
|
|
955
328
|
options.recursive = True
|
|
956
|
-
options.
|
|
957
|
-
options.
|
|
958
|
-
options.quality = None # 90
|
|
959
|
-
options.frame_sample = 10
|
|
960
|
-
options.max_width = None # 1280
|
|
961
|
-
options.n_cores = 4
|
|
329
|
+
# options.frame_sample = 10
|
|
330
|
+
options.time_sample = 2
|
|
962
331
|
options.verbose = True
|
|
963
|
-
options.render_output_video = False
|
|
964
|
-
options.frame_folder = frame_folder
|
|
965
|
-
options.frame_rendering_folder = rendering_folder
|
|
966
|
-
options.keep_extracted_frames = False
|
|
967
|
-
options.keep_rendered_frames = False
|
|
968
|
-
options.force_extracted_frame_folder_deletion = False
|
|
969
|
-
options.force_rendered_frame_folder_deletion = False
|
|
970
|
-
options.fourcc = 'mp4v'
|
|
971
|
-
options.force_on_disk_frame_extraction = False
|
|
972
|
-
# options.rendering_confidence_threshold = 0.15
|
|
973
|
-
|
|
974
|
-
cmd = options_to_command(options); print(cmd)
|
|
975
332
|
|
|
976
|
-
|
|
977
|
-
process_video_folder(options)
|
|
333
|
+
process_videos(options)
|
|
978
334
|
|
|
979
335
|
|
|
980
336
|
#%% Process a single video
|
|
981
337
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
output_base = r'g:\temp\video_test'
|
|
988
|
-
frame_folder = os.path.join(output_base,'frames')
|
|
989
|
-
rendering_folder = os.path.join(output_base,'rendered-frames')
|
|
990
|
-
output_json_file = os.path.join(output_base,'video-test.json')
|
|
991
|
-
output_video_file = os.path.join(output_base,'output_video.mp4')
|
|
992
|
-
|
|
993
|
-
options = ProcessVideoOptions()
|
|
994
|
-
options.model_file = model_file
|
|
995
|
-
options.input_video_file = input_video_file
|
|
996
|
-
options.render_output_video = True
|
|
997
|
-
options.output_video_file = output_video_file
|
|
998
|
-
options.output_json_file = output_json_file
|
|
999
|
-
options.verbose = True
|
|
1000
|
-
options.quality = 75
|
|
1001
|
-
options.frame_sample = 10
|
|
1002
|
-
options.max_width = 1600
|
|
1003
|
-
options.frame_folder = frame_folder
|
|
1004
|
-
options.frame_rendering_folder = rendering_folder
|
|
1005
|
-
options.keep_extracted_frames = False
|
|
1006
|
-
options.keep_rendered_frames = False
|
|
1007
|
-
options.force_extracted_frame_folder_deletion = True
|
|
1008
|
-
options.force_rendered_frame_folder_deletion = True
|
|
1009
|
-
options.fourcc = 'mp4v'
|
|
1010
|
-
# options.rendering_confidence_threshold = 0.15
|
|
1011
|
-
|
|
1012
|
-
cmd = options_to_command(options); print(cmd)
|
|
1013
|
-
|
|
1014
|
-
# import clipboard; clipboard.copy(cmd)
|
|
1015
|
-
process_video(options)
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
#%% Extract specific frames from a single video, no detection
|
|
338
|
+
import os
|
|
339
|
+
from megadetector.detection.process_video import \
|
|
340
|
+
process_videos, ProcessVideoOptions
|
|
341
|
+
from megadetector.detection.video_utils import find_videos
|
|
1019
342
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
output_base = r'g:\temp\video_test'
|
|
1026
|
-
frame_folder = os.path.join(output_base,'frames')
|
|
1027
|
-
output_video_file = os.path.join(output_base,'output_videos.mp4')
|
|
1028
|
-
|
|
1029
|
-
options = ProcessVideoOptions()
|
|
1030
|
-
options.model_file = model_file
|
|
1031
|
-
options.input_video_file = input_video_file
|
|
1032
|
-
options.verbose = True
|
|
1033
|
-
options.quality = 90
|
|
1034
|
-
options.frame_sample = None
|
|
1035
|
-
options.frames_to_extract = [0,100]
|
|
1036
|
-
options.max_width = None
|
|
1037
|
-
options.frame_folder = frame_folder
|
|
1038
|
-
options.keep_extracted_frames = True
|
|
1039
|
-
|
|
1040
|
-
cmd = options_to_command(options); print(cmd)
|
|
1041
|
-
|
|
1042
|
-
# import clipboard; clipboard.copy(cmd)
|
|
1043
|
-
process_video(options)
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
#%% Extract specific frames from a folder, no detection
|
|
343
|
+
model_file = 'MDV5A'
|
|
344
|
+
input_dir = r"G:\temp\md-test-images\video-samples"
|
|
345
|
+
assert os.path.isdir(input_dir)
|
|
346
|
+
video_fn_abs = find_videos(input_dir)[0]
|
|
1047
347
|
|
|
1048
|
-
|
|
1049
|
-
assert os.path.isdir(fn)
|
|
1050
|
-
model_file = 'no_detection'
|
|
1051
|
-
input_video_file = fn
|
|
348
|
+
output_json_file = os.path.join(input_dir,'mdv5a-single-video.json')
|
|
1052
349
|
|
|
1053
|
-
|
|
1054
|
-
frame_folder = os.path.join(output_base,'frames')
|
|
1055
|
-
output_video_file = os.path.join(output_base,'output_videos.mp4')
|
|
350
|
+
print('Processing video {}'.format(video_fn_abs))
|
|
1056
351
|
|
|
1057
352
|
options = ProcessVideoOptions()
|
|
353
|
+
options.json_confidence_threshold = 0.05
|
|
1058
354
|
options.model_file = model_file
|
|
1059
|
-
options.input_video_file =
|
|
355
|
+
options.input_video_file = video_fn_abs
|
|
356
|
+
options.output_json_file = output_json_file
|
|
357
|
+
options.recursive = True
|
|
358
|
+
# options.frame_sample = 10
|
|
359
|
+
options.time_sample = 2
|
|
1060
360
|
options.verbose = True
|
|
1061
|
-
options.quality = 90
|
|
1062
|
-
options.frame_sample = None
|
|
1063
|
-
options.frames_to_extract = [0,100]
|
|
1064
|
-
options.max_width = None
|
|
1065
|
-
options.frame_folder = frame_folder
|
|
1066
|
-
options.keep_extracted_frames = True
|
|
1067
|
-
|
|
1068
|
-
cmd = options_to_command(options); print(cmd)
|
|
1069
361
|
|
|
1070
|
-
|
|
1071
|
-
process_video(options)
|
|
362
|
+
process_videos(options)
|
|
1072
363
|
|
|
1073
364
|
|
|
1074
365
|
#%% Command-line driver
|
|
@@ -1092,111 +383,23 @@ def main(): # noqa
|
|
|
1092
383
|
help='recurse into [input_video_file]; only meaningful if a folder '\
|
|
1093
384
|
'is specified as input')
|
|
1094
385
|
|
|
1095
|
-
parser.add_argument('--frame_folder', type=str, default=None,
|
|
1096
|
-
help='folder to use for intermediate frame storage, defaults to a folder '\
|
|
1097
|
-
'in the system temporary folder')
|
|
1098
|
-
|
|
1099
|
-
parser.add_argument('--frame_rendering_folder', type=str, default=None,
|
|
1100
|
-
help='folder to use for rendered frame storage, defaults to a folder in '\
|
|
1101
|
-
'the system temporary folder')
|
|
1102
|
-
|
|
1103
386
|
parser.add_argument('--output_json_file', type=str,
|
|
1104
387
|
default=None, help='.json output file, defaults to [video file].json')
|
|
1105
388
|
|
|
1106
|
-
parser.add_argument('--output_video_file', type=str,
|
|
1107
|
-
default=None, help='video output file (or folder), defaults to '\
|
|
1108
|
-
'[video file].mp4 for files, or [video file]_annotated for folders')
|
|
1109
|
-
|
|
1110
|
-
parser.add_argument('--keep_extracted_frames',
|
|
1111
|
-
action='store_true', help='Disable the deletion of extracted frames')
|
|
1112
|
-
|
|
1113
|
-
parser.add_argument('--reuse_frames_if_available',
|
|
1114
|
-
action='store_true',
|
|
1115
|
-
help="Don't extract frames that are already available in the frame extraction folder")
|
|
1116
|
-
|
|
1117
|
-
parser.add_argument('--reuse_results_if_available',
|
|
1118
|
-
action='store_true',
|
|
1119
|
-
help='If the output .json files exists, and this flag is set,'\
|
|
1120
|
-
'we\'ll skip running MegaDetector')
|
|
1121
|
-
|
|
1122
|
-
parser.add_argument('--render_output_video', action='store_true',
|
|
1123
|
-
help='enable video output rendering (not rendered by default)')
|
|
1124
|
-
|
|
1125
|
-
parser.add_argument('--fourcc', default=default_fourcc,
|
|
1126
|
-
help=f'fourcc code to use for video encoding (default {default_fourcc}), ' + \
|
|
1127
|
-
'only used if render_output_video is True')
|
|
1128
|
-
|
|
1129
|
-
parser.add_argument('--keep_rendered_frames',
|
|
1130
|
-
action='store_true', help='Disable the deletion of rendered (w/boxes) frames')
|
|
1131
|
-
|
|
1132
|
-
parser.add_argument('--force_extracted_frame_folder_deletion',
|
|
1133
|
-
action='store_true', help='By default, when keep_extracted_frames is False, we '\
|
|
1134
|
-
'delete the frames, but leave the (probably-empty) folder in place. This option '\
|
|
1135
|
-
'forces deletion of the folder as well. Use at your own risk; does not check '\
|
|
1136
|
-
'whether other files were present in the folder.')
|
|
1137
|
-
|
|
1138
|
-
parser.add_argument('--force_rendered_frame_folder_deletion',
|
|
1139
|
-
action='store_true', help='By default, when keep_rendered_frames is False, we '\
|
|
1140
|
-
'delete the frames, but leave the (probably-empty) folder in place. This option '\
|
|
1141
|
-
'forces deletion of the folder as well. Use at your own risk; does not check '\
|
|
1142
|
-
'whether other files were present in the folder.')
|
|
1143
|
-
|
|
1144
|
-
parser.add_argument('--rendering_confidence_threshold', type=float,
|
|
1145
|
-
default=None,
|
|
1146
|
-
help="don't render boxes with confidence below this threshold " + \
|
|
1147
|
-
"(defaults to choosing based on the MD version)")
|
|
1148
|
-
|
|
1149
|
-
parser.add_argument('--rendering_fs', type=float,
|
|
1150
|
-
default=None,
|
|
1151
|
-
help='force a specific frame rate for output videos (only relevant when using '\
|
|
1152
|
-
'--render_output_video) (defaults to the original frame rate)')
|
|
1153
|
-
|
|
1154
389
|
parser.add_argument('--json_confidence_threshold', type=float,
|
|
1155
390
|
default=default_options.json_confidence_threshold,
|
|
1156
391
|
help="don't include boxes in the .json file with confidence "\
|
|
1157
392
|
'below this threshold (default {})'.format(
|
|
1158
393
|
default_options.json_confidence_threshold))
|
|
1159
394
|
|
|
1160
|
-
parser.add_argument('--n_cores', type=int,
|
|
1161
|
-
default=default_options.n_cores,
|
|
1162
|
-
help='Number of cores to use for frame separation and detection. '\
|
|
1163
|
-
'If using a GPU, this option will be respected for frame separation but '\
|
|
1164
|
-
'ignored for detection. Only relevant to frame separation when processing '\
|
|
1165
|
-
'a folder. Default {}.'.format(default_options.n_cores))
|
|
1166
|
-
|
|
1167
395
|
parser.add_argument('--frame_sample', type=int,
|
|
1168
396
|
default=None, help='process every Nth frame (defaults to every frame), mutually exclusive '\
|
|
1169
|
-
'with --
|
|
1170
|
-
|
|
1171
|
-
parser.add_argument('--frames_to_extract', nargs='+', type=int,
|
|
1172
|
-
default=None, help='extract specific frames (one or more ints), mutually exclusive '\
|
|
1173
|
-
'with --frame_sample and --time_sample.')
|
|
397
|
+
'with --time_sample.')
|
|
1174
398
|
|
|
1175
399
|
parser.add_argument('--time_sample', type=float,
|
|
1176
400
|
default=None, help='process frames every N seconds; this is converted to a '\
|
|
1177
401
|
'frame sampling rate, so it may not be exactly the requested interval in seconds. '\
|
|
1178
|
-
'mutually exclusive with --frame_sample
|
|
1179
|
-
|
|
1180
|
-
parser.add_argument('--quality', type=int,
|
|
1181
|
-
default=default_options.quality,
|
|
1182
|
-
help=f'JPEG quality for extracted frames (defaults to {default_options.quality}), ' + \
|
|
1183
|
-
'use -1 to force no quality setting')
|
|
1184
|
-
|
|
1185
|
-
parser.add_argument('--max_width', type=int,
|
|
1186
|
-
default=default_options.max_width,
|
|
1187
|
-
help='Resize frames larger than this before writing (defaults to {})'.format(
|
|
1188
|
-
default_options.max_width))
|
|
1189
|
-
|
|
1190
|
-
parser.add_argument('--debug_max_frames', type=int,
|
|
1191
|
-
default=-1, help='Trim to N frames for debugging (impacts model execution, '\
|
|
1192
|
-
'not frame rendering)')
|
|
1193
|
-
|
|
1194
|
-
parser.add_argument('--class_mapping_filename',
|
|
1195
|
-
type=str,
|
|
1196
|
-
default=None, help='Use a non-default class mapping, supplied in a .json file '\
|
|
1197
|
-
'with a dictionary mapping int-strings to strings. This will also disable '\
|
|
1198
|
-
'the addition of "1" to all category IDs, so your class mapping should start '\
|
|
1199
|
-
'at zero.')
|
|
402
|
+
'mutually exclusive with --frame_sample')
|
|
1200
403
|
|
|
1201
404
|
parser.add_argument('--verbose', action='store_true',
|
|
1202
405
|
help='Enable additional debug output')
|
|
@@ -1211,12 +414,6 @@ def main(): # noqa
|
|
|
1211
414
|
action='store_true',
|
|
1212
415
|
help='Enable image augmentation')
|
|
1213
416
|
|
|
1214
|
-
parser.add_argument('--include_all_processed_frames',
|
|
1215
|
-
action='store_true',
|
|
1216
|
-
help='When processing a folder of videos, this flag indicates that the output '\
|
|
1217
|
-
'should include results for every frame that was processed, rather than just '\
|
|
1218
|
-
'one representative frame for each detection category per video.')
|
|
1219
|
-
|
|
1220
417
|
parser.add_argument('--allow_empty_videos',
|
|
1221
418
|
action='store_true',
|
|
1222
419
|
help='By default, videos with no retrievable frames cause an error, this makes it a warning')
|
|
@@ -1228,6 +425,28 @@ def main(): # noqa
|
|
|
1228
425
|
default='',
|
|
1229
426
|
help='Detector-specific options, as a space-separated list of key-value pairs')
|
|
1230
427
|
|
|
428
|
+
parser.add_argument(
|
|
429
|
+
'--checkpoint_frequency',
|
|
430
|
+
type=int,
|
|
431
|
+
default=default_options.checkpoint_frequency,
|
|
432
|
+
help='Write a checkpoint file (to resume processing later) every N videos; ' + \
|
|
433
|
+
'set to -1 to disable checkpointing (default {})'.format(
|
|
434
|
+
default_options.checkpoint_frequency))
|
|
435
|
+
|
|
436
|
+
parser.add_argument(
|
|
437
|
+
'--checkpoint_path',
|
|
438
|
+
type=str,
|
|
439
|
+
default=None,
|
|
440
|
+
help='Path to checkpoint file; defaults to a file in the same directory ' + \
|
|
441
|
+
'as the output file')
|
|
442
|
+
|
|
443
|
+
parser.add_argument(
|
|
444
|
+
'--resume_from_checkpoint',
|
|
445
|
+
type=str,
|
|
446
|
+
default=None,
|
|
447
|
+
help='Resume from a specific checkpoint file, or "auto" to resume from the ' + \
|
|
448
|
+
'most recent checkpoint in the output directory')
|
|
449
|
+
|
|
1231
450
|
if len(sys.argv[1:]) == 0:
|
|
1232
451
|
parser.print_help()
|
|
1233
452
|
parser.exit()
|
|
@@ -1239,13 +458,13 @@ def main(): # noqa
|
|
|
1239
458
|
options.detector_options = parse_kvp_list(args.detector_options)
|
|
1240
459
|
|
|
1241
460
|
if os.path.isdir(options.input_video_file):
|
|
1242
|
-
|
|
461
|
+
process_videos(options)
|
|
1243
462
|
else:
|
|
1244
463
|
assert os.path.isfile(options.input_video_file), \
|
|
1245
464
|
'{} is not a valid file or folder name'.format(options.input_video_file)
|
|
1246
465
|
assert not options.recursive, \
|
|
1247
466
|
'--recursive is only meaningful when processing a folder'
|
|
1248
|
-
|
|
467
|
+
process_videos(options)
|
|
1249
468
|
|
|
1250
469
|
if __name__ == '__main__':
|
|
1251
470
|
main()
|