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

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

Issue 2965593002: Move webrtc/{tools => rtc_tools} (Closed)
Patch Set: Adding back root changes Created 3 years, 5 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 | « webrtc/tools/testing/utils.py ('k') | webrtc/tools/video_analysis_test.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 glob
11 import optparse
12 import os
13 import shutil
14 import subprocess
15 import sys
16 import time
17
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='int', default=1280,
45 help='Width of the recording. Default: %default')
46 parser.add_option('--frame_height', type='int', default=720,
47 help='Height of the recording. Default: %default')
48 parser.add_option('--framerate', type='int', default=60,
49 help='Recording framerate. Default: %default')
50 parser.add_option('--ref_duration', type='int', default=20,
51 help='Reference recording duration. Default: %default')
52 parser.add_option('--test_duration', type='int', default=10,
53 help='Test recording duration. Default: %default')
54 parser.add_option('--time_between_recordings', type='int', 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 FindUsbPortForV4lDevices(ref_video_device, test_video_device):
162 """Tries to find the usb port for ref_video_device and test_video_device.
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
167 Args:
168 ref_device(string): reference recording device path.
169 test_device(string): test recording device path
170
171 Returns:
172 usb_ports(list): USB ports(string) for the devices found.
173 """
174
175 # Find the device location including USB and USB Bus ID's. Use the usb1
176 # in the path since the driver folder is a symlink which contains all the
177 # usb device port mappings and it's the same in all usbN folders. Tested
178 # on Ubuntu 14.04.
179 v4l_device_path = '/sys/bus/usb/devices/usb1/1-1/driver/**/**/video4linux/'
180 v4l_ref_device = glob.glob('%s%s' % (v4l_device_path, ref_video_device))
181 v4l_test_device = glob.glob('%s%s' % (v4l_device_path, test_video_device))
182 usb_ports = []
183 paths = []
184
185 # Split on the driver folder first since we are only interested in the
186 # folders thereafter.
187 try:
188 ref_path = str(v4l_ref_device).split('driver')[1].split('/')
189 test_path = str(v4l_test_device).split('driver')[1].split('/')
190 except IndexError:
191 print 'Could not find one or both of the specified recording devices.'
192 else:
193 paths.append(ref_path)
194 paths.append(test_path)
195
196 for path in paths:
197 for usb_id in path:
198 # Look for : separator and then use the first element in the list.
199 # E.g 3-3.1:1.0 split on : and [0] becomes 3-3.1 which can be used
200 # for bind/unbind.
201 if ':' in usb_id:
202 usb_ports.append(usb_id.split(':')[0])
203
204 return usb_ports
205
206
207 def RestartMagewellDevices(ref_video_device_path, test_video_device_path):
208 """Reset the USB ports where Magewell capture devices are connected to.
209
210 Performs a soft reset by using USB unbind and bind.
211 This is due to Magewell capture devices have proven to be unstable after the
212 first recording attempt.
213
214 Args:
215 ref_video_device_path(string): reference recording device path.
216 test_video_device_path(string): test recording device path
217
218 Raises:
219 MagewellError: If no magewell devices are found.
220 """
221
222 # Get the dev/videoN device name from the command line arguments.
223 ref_magewell_path = ref_video_device_path.split('/')[2]
224 test_magewell_path = test_video_device_path.split('/')[2]
225 magewell_usb_ports = FindUsbPortForV4lDevices(ref_magewell_path,
226 test_magewell_path)
227
228 # Abort early if no devices are found.
229 if len(magewell_usb_ports) == 0:
230 raise MagewellError('No magewell devices found.')
231 else:
232 print '\nResetting USB ports where magewell devices are connected...'
233 # Use the USB bus and port ID (e.g. 4-3) to unbind and bind the USB devices
234 # (i.e. soft eject and insert).
235 for usb_port in magewell_usb_ports:
236 echo_cmd = ['echo', usb_port]
237 unbind_cmd = ['sudo', 'tee', '/sys/bus/usb/drivers/usb/unbind']
238 bind_cmd = ['sudo', 'tee', '/sys/bus/usb/drivers/usb/bind']
239
240 # TODO(jansson) Figure out a way to call on echo once for bind & unbind
241 # if possible.
242 echo_unbind = subprocess.Popen(echo_cmd, stdout=subprocess.PIPE)
243 unbind = subprocess.Popen(unbind_cmd, stdin=echo_unbind.stdout)
244 echo_unbind.stdout.close()
245 unbind.wait()
246
247 echo_bind = subprocess.Popen(echo_cmd, stdout=subprocess.PIPE)
248 bind = subprocess.Popen(bind_cmd, stdin=echo_bind.stdout)
249 echo_bind.stdout.close()
250 bind.wait()
251 if bind.returncode == 0:
252 print 'Reset done!\n'
253
254
255 def StartRecording(options, ref_file_location, test_file_location):
256 """Starts recording from the two specified video devices.
257
258 Args:
259 options(object): Contains all the provided command line options.
260 record_paths(dict): key: value pair with reference and test file
261 absolute paths.
262
263 Returns:
264 recording_files_and_time(dict): key: value pair with the path to cropped
265 test and reference video files.
266
267 Raises:
268 FfmpegError: If the ffmpeg command fails.
269 """
270 ref_file_name = '%s_%s_ref.%s' % (options.app_name, CURRENT_TIME,
271 options.video_container)
272 ref_file = os.path.join(ref_file_location, ref_file_name)
273
274 test_file_name = '%s_%s_test.%s' % (options.app_name, CURRENT_TIME,
275 options.video_container)
276 test_file = os.path.join(test_file_location, test_file_name)
277
278 # Reference video recorder command line.
279 ref_cmd = [
280 options.ffmpeg,
281 '-v', 'error',
282 '-s', '%dx%d' % (options.frame_width, options.frame_height),
283 '-r', '%d' % options.framerate,
284 '-f', '%s' % options.recording_api,
285 '-i', '%s' % options.ref_video_device,
286 '-pix_fmt', '%s' % options.pixel_format,
287 '-s', '%dx%d' % (options.frame_width, options.frame_height),
288 '-t', '%d' % options.ref_duration,
289 '-r', '%d' % options.framerate,
290 ref_file
291 ]
292
293 # Test video recorder command line.
294 test_cmd = [
295 options.ffmpeg,
296 '-v', 'error',
297 '-s', '%dx%d' % (options.frame_width, options.frame_height),
298 '-r', '%d' % options.framerate,
299 '-f', '%s' % options.recording_api,
300 '-i', '%s' % options.test_video_device,
301 '-pix_fmt', '%s' % options.pixel_format,
302 '-s', '%dx%d' % (options.frame_width, options.frame_height),
303 '-t', '%d' % options.test_duration,
304 '-r', '%d' % options.framerate,
305 test_file
306 ]
307 print 'Trying to record from reference recorder...'
308 ref_recorder = subprocess.Popen(ref_cmd)
309
310 # Start the 2nd recording a little later to ensure the 1st one has started.
311 # TODO(jansson) Check that the ref_recorder output file exists rather than
312 # using sleep.
313 time.sleep(options.time_between_recordings)
314 print 'Trying to record from test recorder...'
315 test_recorder = subprocess.Popen(test_cmd)
316 test_recorder.wait()
317 ref_recorder.wait()
318
319 # ffmpeg does not abort when it fails, need to check return code.
320 if ref_recorder.returncode != 0 or test_recorder.returncode != 0:
321 # Cleanup recording directories.
322 shutil.rmtree(ref_file_location)
323 shutil.rmtree(test_file_location)
324 raise FfmpegError('Recording failed, check ffmpeg output.')
325 else:
326 print 'Ref file recorded to: ' + os.path.abspath(ref_file)
327 print 'Test file recorded to: ' + os.path.abspath(test_file)
328 print 'Recording done!\n'
329 return FlipAndCropRecordings(options, test_file_name, test_file_location,
330 ref_file_name, ref_file_location)
331
332
333 def FlipAndCropRecordings(options, test_file_name, test_file_location,
334 ref_file_name, ref_file_location):
335 """Performs a horizontal flip of the reference video to match the test video.
336
337 This is done to the match orientation and then crops the ref and test videos
338 using the options.test_crop_parameters and options.ref_crop_parameters.
339
340 Args:
341 options(object): Contains all the provided command line options.
342 test_file_name(string): Name of the test video file recording.
343 test_file_location(string): Path to the test video file recording.
344 ref_file_name(string): Name of the reference video file recording.
345 ref_file_location(string): Path to the reference video file recording.
346
347 Returns:
348 recording_files_and_time(dict): key: value pair with the path to cropped
349 test and reference video files.
350
351 Raises:
352 FfmpegError: If the ffmpeg command fails.
353 """
354 print 'Trying to crop videos...'
355
356 # Ref file cropping.
357 cropped_ref_file_name = 'cropped_' + ref_file_name
358 cropped_ref_file = os.path.abspath(
359 os.path.join(ref_file_location, cropped_ref_file_name))
360
361 ref_video_crop_cmd = [
362 options.ffmpeg,
363 '-v', 'error',
364 '-s', '%dx%d' % (options.frame_width, options.frame_height),
365 '-i', '%s' % os.path.join(ref_file_location, ref_file_name),
366 '-vf', '%s' % options.ref_crop_parameters,
367 '-c:a', 'copy',
368 cropped_ref_file
369 ]
370
371 # Test file cropping.
372 cropped_test_file_name = 'cropped_' + test_file_name
373 cropped_test_file = os.path.abspath(
374 os.path.join(test_file_location, cropped_test_file_name))
375
376 test_video_crop_cmd = [
377 options.ffmpeg,
378 '-v', 'error',
379 '-s', '%dx%d' % (options.frame_width, options.frame_height),
380 '-i', '%s' % os.path.join(test_file_location, test_file_name),
381 '-vf', '%s' % options.test_crop_parameters,
382 '-c:a', 'copy',
383 cropped_test_file
384 ]
385
386 ref_crop = subprocess.Popen(ref_video_crop_cmd)
387 ref_crop.wait()
388 test_crop = subprocess.Popen(test_video_crop_cmd)
389 test_crop.wait()
390
391 # ffmpeg does not abort when it fails, need to check return code.
392 if ref_crop.returncode != 0 or test_crop.returncode != 0:
393 # Cleanup recording directories.
394 shutil.rmtree(ref_file_location)
395 shutil.rmtree(test_file_location)
396 raise FfmpegError('Cropping failed, check ffmpeg output.')
397 else:
398 print 'Ref file cropped to: ' + cropped_ref_file
399 print 'Test file cropped to: ' + cropped_test_file
400 print 'Cropping done!\n'
401
402 # Need to return these so they can be used by other parts.
403 cropped_recordings = {
404 'cropped_test_file' : cropped_test_file,
405 'cropped_ref_file' : cropped_ref_file
406 }
407 return cropped_recordings
408
409
410 def CompareVideos(options, cropped_ref_file, cropped_test_file):
411 """Runs the compare_video.py script from src/webrtc/tools using the file path.
412
413 Uses the path from recording_result and writes the output to a file named
414 <options.app_name + '_' + CURRENT_TIME + '_result.txt> in the reference video
415 recording folder taken from recording_result.
416
417 Args:
418 options(object): Contains all the provided command line options.
419 cropped_ref_file(string): Path to cropped reference video file.
420 cropped_test_file(string): Path to cropped test video file.
421
422 Raises:
423 CompareVideosError: If compare_videos.py fails.
424 """
425 print 'Starting comparison...'
426 print 'Grab a coffee, this might take a few minutes...'
427 compare_videos_script = os.path.abspath(options.compare_videos_script)
428 rec_path = os.path.abspath(os.path.join(
429 os.path.dirname(cropped_test_file)))
430 result_file_name = os.path.join(rec_path, '%s_%s_result.txt') % (
431 options.app_name, CURRENT_TIME)
432
433 # Find the crop dimensions (e.g. 950 and 420) in the ref crop parameter
434 # string: 'hflip, crop=950:420:130:56'
435 for param in options.ref_crop_parameters.split('crop'):
436 if param[0] == '=':
437 crop_width = int(param.split(':')[0].split('=')[1])
438 crop_height = int(param.split(':')[1])
439
440 compare_cmd = [
441 compare_videos_script,
442 '--ref_video=%s' % cropped_ref_file,
443 '--test_video=%s' % cropped_test_file,
444 '--frame_analyzer=%s' % os.path.abspath(options.frame_analyzer),
445 '--zxing_path=%s' % options.zxing_path,
446 '--ffmpeg_path=%s' % options.ffmpeg,
447 '--stats_file_ref=%s_stats.txt' %
448 os.path.join(os.path.dirname(cropped_ref_file), cropped_ref_file),
449 '--stats_file_test=%s_stats.txt' %
450 os.path.join(os.path.dirname(cropped_test_file), cropped_test_file),
451 '--yuv_frame_height=%d' % crop_height,
452 '--yuv_frame_width=%d' % crop_width
453 ]
454
455 with open(result_file_name, 'w') as f:
456 try:
457 compare_video_recordings = subprocess.check_output(compare_cmd)
458 f.write(compare_video_recordings)
459 except subprocess.CalledProcessError as error:
460 raise CompareVideosError('Failed to perform comparison: %s' % error)
461 else:
462 print 'Result recorded to: %s' % os.path.abspath(result_file_name)
463 print 'Comparison done!'
464 return compare_video_recordings
465
466
467 def main():
468 """The main function.
469
470 A simple invocation is:
471 ./run_video_analysis.py \
472 --app_name AppRTCMobile \
473 --ffmpeg ./ffmpeg --ref_video_device=/dev/video0 \
474 --test_video_device=/dev/video1 \
475 --zxing_path ./zxing \
476 --test_crop_parameters 'crop=950:420:130:56' \
477 --ref_crop_parameters 'hflip, crop=950:420:130:56' \
478 --ref_rec_dir /tmp/ref \
479 --test_rec_dir /tmp/test
480
481 This will produce the following files if successful:
482 # Original video recordings.
483 /tmp/ref/AppRTCMobile_<recording date and time>_ref.yuv
484 /tmp/test/AppRTCMobile_<recording date and time>_test.yuv
485
486 # Cropped video recordings according to the crop parameters.
487 /tmp/ref/cropped_AppRTCMobile_<recording date and time>_ref.yuv
488 /tmp/test/cropped_AppRTCMobile_<recording date and time>_ref.yuv
489
490 # Comparison metrics from cropped test and ref videos.
491 /tmp/test/AppRTCMobile_<recording date and time>_result.text
492
493 """
494 options = _ParseArgs()
495 RestartMagewellDevices(options.ref_video_device, options.test_video_device)
496 record_paths = CreateRecordingDirs(options)
497 recording_result = StartRecording(options, record_paths['ref_rec_location'],
498 record_paths['test_rec_location'])
499
500 # Do not require compare_video.py script to run, no metrics will be generated.
501 if options.compare_videos_script:
502 CompareVideos(options, recording_result['cropped_ref_file'],
503 recording_result['cropped_test_file'])
504 else:
505 print ('Skipping compare videos step due to compare_videos flag were not '
506 'passed.')
507
508
509 if __name__ == '__main__':
510 sys.exit(main())
OLDNEW
« no previous file with comments | « webrtc/tools/testing/utils.py ('k') | webrtc/tools/video_analysis_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698