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

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

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

Powered by Google App Engine
This is Rietveld 408576698