Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1)

Side by Side Diff: webrtc/tools/run_video_analysis.py

Issue 2704113004: Add video recording wrapper (Closed)
Patch Set: Fixed according to comments Created 3 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/usr/bin/env python
2 # Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
3 #
4 # Use of this source code is governed by a BSD-style license
5 # that can be found in the LICENSE file in the root of the source
6 # tree. An additional intellectual property rights grant can be found
7 # in the file PATENTS. All contributing project authors may
8 # be found in the AUTHORS file in the root of the source tree.
9
10 import optparse
11 import os
12 import subprocess
13 import sys
14 import time
15 import glob
16 import re
17
18 # Used to time-stamp output files and directories
19 CURRENT_TIME = time.strftime("%d_%m_%Y-%H:%M:%S")
20
21 def _ParseArgs():
22 """Registers the command-line options."""
23 usage = 'usage: %prog [options]'
24 parser = optparse.OptionParser(usage=usage)
25
26 parser.add_option('--frame_width', type='string', default='1280',
27 help='Width of the recording. Default: %default')
28 parser.add_option('--frame_height', type='string', default='720',
29 help='Height of the recording. Default: %default')
30 parser.add_option('--framerate', type='string', default='60',
31 help='Recording framerate. Default: %default')
32 parser.add_option('--ref_duration', type='string', default='20',
33 help='Reference recording duration. Default: %default')
34 parser.add_option('--test_duration', type='string', default='10',
35 help='Test recording duration. Default: %default')
36 parser.add_option('--time_between_recordings', type=float, default=5,
37 help='Time between starting test recording after ref.'
38 'Default: %default')
39 parser.add_option('--ref_video_device', type='string', default='/dev/video0',
40 help='Reference recording device. Default: %default')
41 parser.add_option('--test_video_device', type='string', default='/dev/video1',
42 help='Test recording device. Default: %default')
43 parser.add_option('--app_name', type='string',
44 help='Name of the app under test.')
45 parser.add_option('--recording_api', type='string', default='Video4Linux2',
46 help='Recording API to use. Default: %default')
47 parser.add_option('--pixel_format', type='string', default='yuv420p',
48 help='Recording pixel format Default: %default')
49 parser.add_option('--ffmpeg', type='string',
50 help='Path to the ffmpeg executable for the reference '
51 'device.')
52 parser.add_option('--video_container', type='string', default='yuv',
53 help='Video container for the recordings.'
54 'Default: %default')
55 parser.add_option('--compare_videos_script', type='string',
56 default='compare_videos.py',
57 help='Path to script used to compare and generate metrics.'
58 'Default: %default')
59 parser.add_option('--frame_analyzer', type='string',
60 default='../../out/Default/frame_analyzer',
61 help='Path to the frame analyzer executable.'
62 'Default: %default')
63 parser.add_option('--zxing_path', type='string',
64 help='Path to the zebra xing barcode analyzer.')
65 parser.add_option('--ref_rec_dir', type='string', default='ref',
66 help='Path to where reference recordings will be created.'
67 'Ideally keep the ref and test directories on separate'
68 'drives. Default: %default')
69 parser.add_option('--test_rec_dir', type='string', default='test',
70 help='Path to where test recordings will be created.'
71 'Ideally keep the ref and test directories on separate '
72 'drives. Default: %default')
73 parser.add_option('--test_crop_parameters', type='string',
74 help='ffmpeg processing parameters for the test video.')
75 parser.add_option('--ref_crop_parameters', type='string',
76 help='ffmpeg processing parameters for the ref video.')
77
78 options, _ = parser.parse_args()
79
80 if not options.app_name:
81 parser.error('You must provide an application name!')
82
83 if not options.test_crop_parameters or not options.ref_crop_parameters:
84 parser.error('You must provide ref and test crop parameters!')
85
86 # Ensure the crop filter is included in the crop parameters used for ffmpeg.
87 if 'crop' not in options.ref_crop_parameters:
88 parser.error('You must provide a reference crop filter for ffmpeg.')
89 if 'crop' not in options.test_crop_parameters:
90 parser.error('You must provide a test crop filter for ffmpeg.')
91
92 if not options.ffmpeg:
93 parser.error('You most provide location for the ffmpeg executable.')
94 if not os.path.isfile(options.ffmpeg):
95 parser.error('Cannot find the ffmpeg executable.')
96
97 # compare_videos.py dependencies.
98 if not os.path.isfile(options.compare_videos_script):
99 parser.warning('Cannot find compare_videos.py script, no metrics will be '
100 'generated!')
101 if not os.path.isfile(options.frame_analyzer):
102 parser.warning('Cannot find frame_analyzer, no metrics will be generated!')
103 if not os.path.isfile(options.zxing_path):
104 parser.warning('Cannot find Zebra Xing, no metrics will be generated!')
105
106 return options
107
108
109 def CreateRecordingDirs(options):
110 """Create root + sub directories for reference and test recordings.
111
112 Args:
113 options(object): Contains all the provided command line options.
114 Return:
115 record_paths(dict): key: value pair with reference and test file
116 absolute paths.
117 """
118
119 # Create root directories for the video recordings.
120 if not os.path.isdir(options.ref_rec_dir):
121 os.makedirs(options.ref_rec_dir)
122 if not os.path.isdir(options.test_rec_dir):
123 os.makedirs(options.test_rec_dir)
124
125 # Create and time-stamp directories for all the output files.
126 ref_rec_dir = os.path.join(options.ref_rec_dir, options.app_name + '_' + \
127 CURRENT_TIME)
128 test_rec_dir = os.path.join(options.test_rec_dir, options.app_name + '_' + \
129 CURRENT_TIME)
130
131 os.makedirs(ref_rec_dir)
132 os.makedirs(test_rec_dir)
133
134 record_paths = {
135 'ref_rec_location' : os.path.abspath(ref_rec_dir),
136 'test_rec_location' : os.path.abspath(test_rec_dir)
137 }
138
139 return record_paths
140
141
142 def RestartMagewellDevices(ref_video_device, test_video_device):
143 """Reset the USB ports where Magewell capture devices are connected to.
144
145 Tries to find the provided ref_video_device and test_video_device devices
146 which use video4linux and then do a soft reset by using USB unbind and bind.
147 This is due to Magewell capture devices have proven to be unstable after the
148 first recording attempt.
149
150 Args:
151 ref_video_device(string): reference recording device path.
152 test_video_device(string): test recording device path
153 """
154
155 # Get the dev/videoN device name from the command line arguments.
156 ref_magewell = ref_video_device.split('/')[2]
157 test_magewell = test_video_device.split('/')[2]
158
159 # Find the device location including USB and USB Bus ID's.
160 device_string = '/sys/bus/usb/devices/usb*/**/**/video4linux/'
161 ref_magewell_device = glob.glob('%s%s' % (device_string, ref_magewell))
162 test_magewell_device = glob.glob('%s%s' % (device_string, test_magewell))
163
164 magewell_usb_ports = []
165
166 # Figure out the USB bus and port ID for each device.
167 ref_magewell_path = str(ref_magewell_device).split('/')
168 for directory in ref_magewell_path:
169
170 # Find the folder with pattern "N-N", e.g. "4-3" or \
171 # "[USB bus ID]-[USB port]"
172 if re.match(r'^\d-\d$', directory):
173 magewell_usb_ports.append(directory)
174
175 test_magewell_path = str(test_magewell_device).split('/')
176 for directory in test_magewell_path:
177
178 # Find the folder with pattern "N-N", e.g. "4-3" or \
179 # "[USB bus ID]-[USB port]"
180 if re.match(r'^\d-\d$', directory):
181 magewell_usb_ports.append(directory)
182
183 print '\nResetting USB ports where magewell devices are connected...'
184
185 # Use the USB bus and port ID (e.g. 4-3) to unbind and bind the USB devices
186 # (i.e. soft eject and insert).
187 try:
188 for usb_port in magewell_usb_ports:
189 echo_cmd = ['echo', usb_port]
190 unbind_cmd = ['sudo', 'tee', '/sys/bus/usb/drivers/usb/unbind']
191 bind_cmd = ['sudo', 'tee', '/sys/bus/usb/drivers/usb/bind']
192
193 # TODO(jansson) Figure out a way to call on echo once for bind & unbind
194 # if possible.
195 echo_unbind = subprocess.Popen(echo_cmd, stdout=subprocess.PIPE)
196 unbind = subprocess.Popen(unbind_cmd, stdin=echo_unbind.stdout)
197 echo_unbind.stdout.close()
198 unbind.communicate()
199 unbind.wait()
200
201 echo_bind = subprocess.Popen(echo_cmd, stdout=subprocess.PIPE)
202 bind = subprocess.Popen(bind_cmd, stdin=echo_bind.stdout)
203 echo_bind.stdout.close()
204 bind.communicate()
205 bind.wait()
206 except OSError as e:
207 print 'Error while resetting magewell devices: ' + e
208 raise
209
210 print 'Reset done!\n'
211
212
213 def StartRecording(options, record_paths):
214 """Starts recording from the two specified video devices.
215
216 Args:
217 options(object): Contains all the provided command line options.
218 record_paths(dict): key: value pair with reference and test file
219 absolute paths.
220 """
221 ref_file_name = '%s_%s_ref.%s' % (options.app_name, CURRENT_TIME,
222 options.video_container)
223 ref_file_location = os.path.join(record_paths['ref_rec_location'],
224 ref_file_name)
225
226 test_file_name = '%s_%s_test.%s' % (options.app_name, CURRENT_TIME,
227 options.video_container)
228 test_file_location = os.path.join(record_paths['test_rec_location'],
229 test_file_name)
230
231 # Reference video recorder command line.
232 ref_cmd = [
233 options.ffmpeg,
234 '-v', 'error',
235 '-s', options.frame_width + 'x' + options.frame_height,
236 '-framerate', options.framerate,
237 '-f', options.recording_api,
238 '-i', options.ref_video_device,
239 '-pix_fmt', options.pixel_format,
240 '-s', options.frame_width + 'x' + options.frame_height,
241 '-t', options.ref_duration,
242 '-framerate', options.framerate,
243 ref_file_location
244 ]
245
246 # Test video recorder command line.
247 test_cmd = [
248 options.ffmpeg,
249 '-v', 'error',
250 '-s', options.frame_width + 'x' + options.frame_height,
251 '-framerate', options.framerate,
252 '-f', options.recording_api,
253 '-i', options.test_video_device,
254 '-pix_fmt', options.pixel_format,
255 '-s', options.frame_width + 'x' + options.frame_height,
256 '-t', options.test_duration,
257 '-framerate', options.framerate,
258 test_file_location
259 ]
260 print 'Trying to record from reference recorder...'
261 ref_recorder = subprocess.Popen(ref_cmd, stderr=sys.stderr)
262
263 # Start the 2nd recording a little later to ensure the 1st one has started.
264 # TODO(jansson) Check that the ref_recorder output file exists rather than
265 # using sleep.
266 time.sleep(options.time_between_recordings)
267 print 'Trying to record from test recorder...'
268 test_recorder = subprocess.Popen(test_cmd, stderr=sys.stderr)
269 test_recorder.wait()
270 ref_recorder.wait()
271
272 # ffmpeg does not abort when it fails, need to check return code.
273 assert ref_recorder.returncode == 0, (
274 'Ref recording failed, check ffmpeg output and device: %s'
275 % options.ref_video_device)
276 assert test_recorder.returncode == 0, (
277 'Test recording failed, check ffmpeg output and device: %s'
278 % options.test_video_device)
279
280 print 'Ref file recorded to: ' + os.path.abspath(ref_file_location)
281 print 'Test file recorded to: ' + os.path.abspath(test_file_location)
282 print 'Recording done!\n'
283 return FlipAndCropRecordings(options, test_file_name,
284 record_paths['test_rec_location'], ref_file_name,
285 record_paths['ref_rec_location'])
286
287
288 def FlipAndCropRecordings(options, test_file_name, test_file_location,
289 ref_file_name, ref_file_location):
290 """Performs a horizontal flip of the reference video to match the test video.
291
292 This is done to the match orientation and then crops the ref and test videos
293 using the options.test_crop_parameters and options.ref_crop_parameters.
294
295 Args:
296 options(object): Contains all the provided command line options.
297 test_file_name(string): Name of the test video file recording.
298 test_file_location(string): Path to the test video file recording.
299 ref_file_name(string): Name of the reference video file recording.
300 ref_file_location(string): Path to the reference video file recording.
301 Return:
302 recording_files_and_time(dict): key: value pair with the path to cropped
303 test and reference video files.
304 """
305 print 'Trying to crop videos...'
306
307 # Ref file cropping.
308 cropped_ref_file_name = 'cropped_' + ref_file_name
309 cropped_ref_file = os.path.abspath(
310 os.path.join(ref_file_location, cropped_ref_file_name))
311
312 ref_video_crop_cmd = [
313 options.ffmpeg,
314 '-v', 'error',
315 '-s', options.frame_width + 'x' + options.frame_height,
316 '-i', os.path.join(ref_file_location, ref_file_name),
317 '-vf', options.ref_crop_parameters,
318 '-c:a', 'copy',
319 cropped_ref_file
320 ]
321
322 # Test file cropping.
323 cropped_test_file_name = 'cropped_' + test_file_name
324 cropped_test_file = os.path.abspath(
325 os.path.join(test_file_location, cropped_test_file_name))
326
327 test_video_crop_cmd = [
328 options.ffmpeg,
329 '-v', 'error',
330 '-s', options.frame_width + 'x' + options.frame_height,
331 '-i', os.path.join(test_file_location, test_file_name),
332 '-vf', options.test_crop_parameters,
333 '-c:a', 'copy',
334 cropped_test_file
335 ]
336
337 ref_crop = subprocess.Popen(ref_video_crop_cmd)
338 ref_crop.wait()
339 print 'Ref file cropped to: ' + cropped_ref_file
340
341 try:
342 test_crop = subprocess.Popen(test_video_crop_cmd)
343 test_crop.wait()
344 print 'Test file cropped to: ' + cropped_test_file
345 print 'Cropping done!\n'
346
347 # Need to return these so they can be used by other parts.
348 cropped_recordings = {
349 'cropped_test_file' : cropped_test_file,
350 'cropped_ref_file' : cropped_ref_file
351 }
352
353 return cropped_recordings
354 except subprocess.CalledProcessError as e:
355 print 'Something went wrong during cropping: ' + e
356 raise
357
358
359 def CompareVideos(options, recording_result):
360 """Runs the compare_video.py script from src/webrtc/tools using the file path.
361
362 Uses the path from recording_result and writes the output to a file named
363 <options.app_name + '_' + CURRENT_TIME + '_result.txt> in the reference video
364 recording folder taken from recording_result.
365
366 Args:
367 options(object): Contains all the provided command line options.
368 recording_files_and_time(dict): key: value pair with the path to cropped
369 test and reference video files
370 """
371 print 'Starting comparison...'
372 print 'Grab a coffee, this might take a few minutes...'
373 cropped_ref_file = recording_result['cropped_ref_file']
374 cropped_test_file = recording_result['cropped_test_file']
375 compare_videos_script = os.path.abspath(options.compare_videos_script)
376 rec_path = os.path.join(os.path.dirname(recording_result['cropped_ref_file']))
377 result_file_name = rec_path + '%s_%s%s' % (options.app_name, CURRENT_TIME,
kjellander_webrtc 2017/03/10 13:48:38 It would make more sense to inline '_result.txt' i
janssonWebRTC 2017/03/10 14:40:02 Done.
378 '_result.txt')
379
380 # Find the crop dimensions (950 and 420) in the ref crop parameter string:
381 # 'hflip, crop=950:420:130:56'
382 for param in options.ref_crop_parameters.split('crop'):
383 if param[0] == '=':
384 crop_width = param.split(':')[0].split('=')[1]
385 crop_height = param.split(':')[1]
386
387 compare_cmd = [
388 sys.executable,
389 compare_videos_script,
390 '--ref_video', cropped_ref_file,
391 '--test_video', cropped_test_file,
392 '--frame_analyzer', os.path.abspath(options.frame_analyzer),
393 '--zxing_path', options.zxing_path,
394 '--ffmpeg_path', options.ffmpeg,
395 '--stats_file_ref', os.path.join(os.path.dirname(cropped_ref_file),
396 cropped_ref_file + '_stats.txt'),
397 '--stats_file_test', os.path.join(os.path.dirname(cropped_test_file),
398 cropped_test_file + '_stats.txt'),
399 '--yuv_frame_height', crop_height,
400 '--yuv_frame_width', crop_width
401 ]
402
403 try:
404 with open(result_file_name, 'w') as f:
405 compare_video_recordings = subprocess.Popen(compare_cmd, stdout=f)
406 compare_video_recordings.wait()
407 print 'Result recorded to: ' + os.path.abspath(result_file_name)
408 print 'Comparison done!'
409 except subprocess.CalledProcessError as e:
410 print 'Something went wrong when trying to compare videos: ' + e
411 raise
412
413
414 def main():
415 """The main function.
416
417 A simple invocation is:
418 ./run_video_analysis.py \
419 --app_name AppRTCMobile \
420 --ffmpeg ./ffmpeg --ref_video_device=/dev/video0 \
421 --test_video_device=/dev/video1 \
422 --zxing_path ./zxing \
423 --test_crop_parameters 'crop=950:420:130:56' \
424 --ref_crop_parameters 'hflip, crop=950:420:130:56' \
425 --ref_rec_dir /tmp/ref \
426 --test_rec_dir /tmp/test
427
428 This will produce the following files if successful:
429 # Original video recordings.
430 /tmp/ref/AppRTCMobile_<recording date and time>_ref.yuv
431 /tmp/test/AppRTCMobile_<recording date and time>_test.yuv
432
433 # Cropped video recordings according to the crop parameters.
434 /tmp/ref/cropped_AppRTCMobile_<recording date and time>_ref.yuv
435 /tmp/test/cropped_AppRTCMobile_<recording date and time>_ref.yuv
436
437 # Comparison metrics from cropped test and ref videos.
438 /tmp/test/AppRTCMobile_<recording date and time>_result.text
439
440 """
441 options = _ParseArgs()
442 RestartMagewellDevices(options.ref_video_device, options.test_video_device)
443 record_paths = CreateRecordingDirs(options)
444 recording_result = StartRecording(options, record_paths)
445
446 # Do not require compare_video.py script to run, no metrics will be generated.
447 if options.compare_videos_script:
448 CompareVideos(options, recording_result)
449 else:
450 print ('Skipping compare videos step due to compare_videos flag were not '
451 'passed.')
452
453
454 if __name__ == '__main__':
455 sys.exit(main())
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698