Index: webrtc/tools/run_video_analysis.py |
diff --git a/webrtc/tools/run_video_analysis.py b/webrtc/tools/run_video_analysis.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..350bc556fb71cd4a461639eb045d14432133a85b |
--- /dev/null |
+++ b/webrtc/tools/run_video_analysis.py |
@@ -0,0 +1,456 @@ |
+#!/usr/bin/env python |
+# Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. |
+# |
+# Use of this source code is governed by a BSD-style license |
+# that can be found in the LICENSE file in the root of the source |
+# tree. An additional intellectual property rights grant can be found |
+# in the file PATENTS. All contributing project authors may |
+# be found in the AUTHORS file in the root of the source tree. |
+ |
+import optparse |
+import os |
+import subprocess |
+import sys |
+import time |
+import glob |
+import re |
+ |
+# Used to time-stamp output files and directories |
+CURRENT_TIME = time.strftime("%d_%m_%Y-%H:%M:%S") |
+ |
+def _ParseArgs(): |
+ """Registers the command-line options.""" |
+ usage = 'usage: %prog [options]' |
+ parser = optparse.OptionParser(usage=usage) |
+ |
+ parser.add_option('--frame_width', type='string', default='1280', |
+ help='Width of the recording. Default: %default') |
+ parser.add_option('--frame_height', type='string', default='720', |
+ help='Height of the recording. Default: %default') |
+ parser.add_option('--framerate', type='string', default='60', |
+ help='Recording framerate. Default: %default') |
+ parser.add_option('--ref_duration', type='string', default='20', |
+ help='Reference recording duration. Default: %default') |
+ parser.add_option('--test_duration', type='string', default='10', |
+ help='Test recording duration. Default: %default') |
+ parser.add_option('--time_between_recordings', type=float, default=5, |
+ help='Time between starting test recording after ref.' |
+ 'Default: %default') |
+ parser.add_option('--ref_video_device', type='string', default='/dev/video0', |
+ help='Reference recording device. Default: %default') |
+ parser.add_option('--test_video_device', type='string', default='/dev/video1', |
+ help='Test recording device. Default: %default') |
+ parser.add_option('--app_name', type='string', |
+ help='Name of the app under test.') |
+ parser.add_option('--recording_api', type='string', default='Video4Linux2', |
+ help='Recording API to use. Default: %default') |
+ parser.add_option('--pixel_format', type='string', default='yuv420p', |
+ help='Recording pixel format Default: %default') |
+ parser.add_option('--ffmpeg', type='string', |
+ help='Path to the ffmpeg executable for the reference ' |
+ 'device.') |
+ parser.add_option('--video_container', type='string', default='yuv', |
+ help='Video container for the recordings.' |
+ 'Default: %default') |
+ parser.add_option('--compare_videos_script', type='string', |
+ default='compare_videos.py', |
+ help='Path to script used to compare and generate metrics.' |
+ 'Default: %default') |
+ parser.add_option('--frame_analyzer', type='string', |
+ default='../../out/Default/frame_analyzer', |
+ help='Path to the frame analyzer executable.' |
+ 'Default: %default') |
+ parser.add_option('--zxing_path', type='string', |
+ help='Path to the zebra xing barcode analyzer.') |
+ parser.add_option('--ref_rec_dir', type='string', default='ref', |
+ help='Path to where reference recordings will be created.' |
+ 'Ideally keep the ref and test directories on separate' |
+ 'drives. Default: %default') |
+ parser.add_option('--test_rec_dir', type='string', default='test', |
+ help='Path to where test recordings will be created.' |
+ 'Ideally keep the ref and test directories on separate ' |
+ 'drives. Default: %default') |
+ parser.add_option('--test_crop_parameters', type='string', |
+ help='ffmpeg processing parameters for the test video.') |
+ parser.add_option('--ref_crop_parameters', type='string', |
+ help='ffmpeg processing parameters for the ref video.') |
+ |
+ options, _ = parser.parse_args() |
+ |
+ if not options.app_name: |
+ parser.error('You must provide an application name!') |
+ |
+ if not options.test_crop_parameters or not options.ref_crop_parameters: |
+ parser.error('You must provide ref and test crop parameters!') |
+ |
+ # Ensure the crop filter is included in the crop parameters used for ffmpeg. |
+ if 'crop' not in options.ref_crop_parameters: |
+ parser.error('You must provide a reference crop filter for ffmpeg.') |
+ if 'crop' not in options.test_crop_parameters: |
+ parser.error('You must provide a test crop filter for ffmpeg.') |
+ |
+ if not options.ffmpeg: |
+ parser.error('You most provide location for the ffmpeg executable.') |
+ if not os.path.isfile(options.ffmpeg): |
+ parser.error('Cannot find the ffmpeg executable.') |
+ |
+ # compare_videos.py dependencies. |
+ if not os.path.isfile(options.compare_videos_script): |
+ parser.warning('Cannot find compare_videos.py script, no metrics will be ' |
+ 'generated!') |
+ if not os.path.isfile(options.frame_analyzer): |
+ parser.warning('Cannot find frame_analyzer, no metrics will be generated!') |
+ if not os.path.isfile(options.zxing_path): |
+ parser.warning('Cannot find Zebra Xing, no metrics will be generated!') |
+ |
+ return options |
+ |
+ |
+def CreateRecordingDirs(options): |
+ """Create root + sub directories for reference and test recordings. |
+ |
+ Args: |
+ options(object): Contains all the provided command line options. |
+ Return: |
+ record_paths(dict): key: value pair with reference and test file |
+ absolute paths. |
+ """ |
+ |
+ # Create root directories for the video recordings. |
+ if not os.path.isdir(options.ref_rec_dir): |
+ os.makedirs(options.ref_rec_dir) |
+ if not os.path.isdir(options.test_rec_dir): |
+ os.makedirs(options.test_rec_dir) |
+ |
+ # Create and time-stamp directories for all the output files. |
+ ref_rec_dir = os.path.join(options.ref_rec_dir, options.app_name + '_' + \ |
+ CURRENT_TIME) |
+ test_rec_dir = os.path.join(options.test_rec_dir, options.app_name + '_' + \ |
+ CURRENT_TIME) |
+ |
+ os.makedirs(ref_rec_dir) |
+ os.makedirs(test_rec_dir) |
+ |
+ record_paths = { |
+ 'ref_rec_location' : os.path.abspath(ref_rec_dir), |
+ 'test_rec_location' : os.path.abspath(test_rec_dir) |
+ } |
+ |
+ return record_paths |
+ |
+ |
+def RestartMagewellDevices(ref_video_device, test_video_device): |
+ """Reset the USB ports where Magewell capture devices are connected to. |
+ |
+ Tries to find the provided ref_video_device and test_video_device devices |
+ which use video4linux and then do a soft reset by using USB unbind and bind. |
+ This is due to Magewell capture devices have proven to be unstable after the |
+ first recording attempt. |
+ |
+ Args: |
+ ref_video_device(string): reference recording device path. |
+ test_video_device(string): test recording device path |
+ """ |
+ |
+ # Get the dev/videoN device name from the command line arguments. |
+ ref_magewell = ref_video_device.split('/')[2] |
+ test_magewell = test_video_device.split('/')[2] |
+ |
+ # Find the device location including USB and USB Bus ID's. |
+ device_string = '/sys/bus/usb/devices/usb*/**/**/video4linux/' |
+ ref_magewell_device = glob.glob('%s%s' % (device_string, ref_magewell)) |
+ test_magewell_device = glob.glob('%s%s' % (device_string, test_magewell)) |
+ |
+ magewell_usb_ports = [] |
+ |
+ # Figure out the USB bus and port ID for each device. |
+ ref_magewell_path = str(ref_magewell_device).split('/') |
+ for directory in ref_magewell_path: |
+ |
+ # Find the folder with pattern "N-N", e.g. "4-3" or \ |
+ # "[USB bus ID]-[USB port]" |
+ if re.match(r'^\d-\d$', directory): |
+ magewell_usb_ports.append(directory) |
+ |
+ test_magewell_path = str(test_magewell_device).split('/') |
+ for directory in test_magewell_path: |
+ |
+ # Find the folder with pattern "N-N", e.g. "4-3" or \ |
+ # "[USB bus ID]-[USB port]" |
+ if re.match(r'^\d-\d$', directory): |
+ magewell_usb_ports.append(directory) |
+ |
+ print '\nResetting USB ports where magewell devices are connected...' |
+ |
+ # Use the USB bus and port ID (e.g. 4-3) to unbind and bind the USB devices |
+ # (i.e. soft eject and insert). |
+ try: |
+ for usb_port in magewell_usb_ports: |
+ echo_cmd = ['echo', usb_port] |
+ unbind_cmd = ['sudo', 'tee', '/sys/bus/usb/drivers/usb/unbind'] |
+ bind_cmd = ['sudo', 'tee', '/sys/bus/usb/drivers/usb/bind'] |
+ |
+ # TODO(jansson) Figure out a way to call on echo once for bind & unbind |
+ # if possible. |
+ echo_unbind = subprocess.Popen(echo_cmd, stdout=subprocess.PIPE) |
+ unbind = subprocess.Popen(unbind_cmd, stdin=echo_unbind.stdout) |
+ echo_unbind.stdout.close() |
+ unbind.communicate() |
+ unbind.wait() |
+ |
+ echo_bind = subprocess.Popen(echo_cmd, stdout=subprocess.PIPE) |
+ bind = subprocess.Popen(bind_cmd, stdin=echo_bind.stdout) |
+ echo_bind.stdout.close() |
+ bind.communicate() |
+ bind.wait() |
+ except OSError as e: |
+ print 'Error while resetting magewell devices: ' + e |
+ raise |
+ |
+ print 'Reset done!\n' |
+ |
+ |
+def StartRecording(options, record_paths): |
+ """Starts recording from the two specified video devices. |
+ |
+ Args: |
+ options(object): Contains all the provided command line options. |
+ record_paths(dict): key: value pair with reference and test file |
+ absolute paths. |
+ """ |
+ ref_file_name = '%s_%s_ref.%s' % (options.app_name, CURRENT_TIME, |
+ options.video_container) |
+ ref_file_location = os.path.join(record_paths['ref_rec_location'], |
+ ref_file_name) |
+ |
+ test_file_name = '%s_%s_test.%s' % (options.app_name, CURRENT_TIME, |
+ options.video_container) |
+ test_file_location = os.path.join(record_paths['test_rec_location'], |
+ test_file_name) |
+ |
+ # Reference video recorder command line. |
+ ref_cmd = [ |
+ options.ffmpeg, |
+ '-v', 'error', |
+ '-s', options.frame_width + 'x' + options.frame_height, |
+ '-framerate', options.framerate, |
+ '-f', options.recording_api, |
+ '-i', options.ref_video_device, |
+ '-pix_fmt', options.pixel_format, |
+ '-s', options.frame_width + 'x' + options.frame_height, |
+ '-t', options.ref_duration, |
+ '-framerate', options.framerate, |
+ ref_file_location |
+ ] |
+ |
+ # Test video recorder command line. |
+ test_cmd = [ |
+ options.ffmpeg, |
+ '-v', 'error', |
+ '-s', options.frame_width + 'x' + options.frame_height, |
+ '-framerate', options.framerate, |
+ '-f', options.recording_api, |
+ '-i', options.test_video_device, |
+ '-pix_fmt', options.pixel_format, |
+ '-s', options.frame_width + 'x' + options.frame_height, |
+ '-t', options.test_duration, |
+ '-framerate', options.framerate, |
+ test_file_location |
+ ] |
+ print 'Trying to record from reference recorder...' |
+ ref_recorder = subprocess.Popen(ref_cmd, stderr=sys.stderr) |
+ |
+ # Start the 2nd recording a little later to ensure the 1st one has started. |
+ # TODO(jansson) Check that the ref_recorder output file exists rather than |
+ # using sleep. |
+ time.sleep(options.time_between_recordings) |
+ print 'Trying to record from test recorder...' |
+ test_recorder = subprocess.Popen(test_cmd, stderr=sys.stderr) |
+ test_recorder.wait() |
+ ref_recorder.wait() |
+ |
+ # ffmpeg does not abort when it fails, need to check return code. |
+ assert ref_recorder.returncode == 0, ( |
+ 'Ref recording failed, check ffmpeg output and device: %s' |
+ % options.ref_video_device) |
+ assert test_recorder.returncode == 0, ( |
+ 'Test recording failed, check ffmpeg output and device: %s' |
+ % options.test_video_device) |
+ |
+ print 'Ref file recorded to: ' + os.path.abspath(ref_file_location) |
+ print 'Test file recorded to: ' + os.path.abspath(test_file_location) |
+ print 'Recording done!\n' |
+ return FlipAndCropRecordings(options, test_file_name, |
+ record_paths['test_rec_location'], ref_file_name, |
+ record_paths['ref_rec_location']) |
+ |
+ |
+def FlipAndCropRecordings(options, test_file_name, test_file_location, |
+ ref_file_name, ref_file_location): |
+ """Performs a horizontal flip of the reference video to match the test video. |
+ |
+ This is done to the match orientation and then crops the ref and test videos |
+ using the options.test_crop_parameters and options.ref_crop_parameters. |
+ |
+ Args: |
+ options(object): Contains all the provided command line options. |
+ test_file_name(string): Name of the test video file recording. |
+ test_file_location(string): Path to the test video file recording. |
+ ref_file_name(string): Name of the reference video file recording. |
+ ref_file_location(string): Path to the reference video file recording. |
+ Return: |
+ recording_files_and_time(dict): key: value pair with the path to cropped |
+ test and reference video files. |
+ """ |
+ print 'Trying to crop videos...' |
+ |
+ # Ref file cropping. |
+ cropped_ref_file_name = 'cropped_' + ref_file_name |
+ cropped_ref_file = os.path.abspath( |
+ os.path.join(ref_file_location, cropped_ref_file_name)) |
+ |
+ ref_video_crop_cmd = [ |
+ options.ffmpeg, |
+ '-v', 'error', |
+ '-s', options.frame_width + 'x' + options.frame_height, |
+ '-i', os.path.join(ref_file_location, ref_file_name), |
+ '-vf', options.ref_crop_parameters, |
+ '-c:a', 'copy', |
+ cropped_ref_file |
+ ] |
+ |
+ # Test file cropping. |
+ cropped_test_file_name = 'cropped_' + test_file_name |
+ cropped_test_file = os.path.abspath( |
+ os.path.join(test_file_location, cropped_test_file_name)) |
+ |
+ test_video_crop_cmd = [ |
+ options.ffmpeg, |
+ '-v', 'error', |
+ '-s', options.frame_width + 'x' + options.frame_height, |
+ '-i', os.path.join(test_file_location, test_file_name), |
+ '-vf', options.test_crop_parameters, |
+ '-c:a', 'copy', |
+ cropped_test_file |
+ ] |
+ |
+ ref_crop = subprocess.Popen(ref_video_crop_cmd) |
+ ref_crop.wait() |
+ print 'Ref file cropped to: ' + cropped_ref_file |
+ |
+ try: |
+ test_crop = subprocess.Popen(test_video_crop_cmd) |
+ test_crop.wait() |
+ print 'Test file cropped to: ' + cropped_test_file |
+ print 'Cropping done!\n' |
+ |
+ # Need to return these so they can be used by other parts. |
+ cropped_recordings = { |
+ 'cropped_test_file' : cropped_test_file, |
+ 'cropped_ref_file' : cropped_ref_file |
+ } |
+ |
+ return cropped_recordings |
+ except subprocess.CalledProcessError as e: |
+ print 'Something went wrong during cropping: ' + e |
+ raise |
+ |
+ |
+def CompareVideos(options, recording_result): |
+ """Runs the compare_video.py script from src/webrtc/tools using the file path. |
+ |
+ Uses the path from recording_result and writes the output to a file named |
+ <options.app_name + '_' + CURRENT_TIME + '_result.txt> in the reference video |
+ recording folder taken from recording_result. |
+ |
+ Args: |
+ options(object): Contains all the provided command line options. |
+ recording_files_and_time(dict): key: value pair with the path to cropped |
+ test and reference video files |
+ """ |
+ print 'Starting comparison...' |
+ print 'Grab a coffee, this might take a few minutes...' |
+ cropped_ref_file = recording_result['cropped_ref_file'] |
+ cropped_test_file = recording_result['cropped_test_file'] |
+ compare_videos_script = os.path.abspath(options.compare_videos_script) |
+ rec_path = os.path.abspath(os.path.join( |
+ os.path.dirname(recording_result['cropped_ref_file']))) |
+ result_file_name = os.path.join(rec_path, '%s_%s_result.txt') % ( |
+ options.app_name, CURRENT_TIME) |
+ |
+ # Find the crop dimensions (950 and 420) in the ref crop parameter string: |
+ # 'hflip, crop=950:420:130:56' |
+ for param in options.ref_crop_parameters.split('crop'): |
+ if param[0] == '=': |
+ crop_width = param.split(':')[0].split('=')[1] |
+ crop_height = param.split(':')[1] |
+ |
+ compare_cmd = [ |
+ sys.executable, |
+ compare_videos_script, |
+ '--ref_video', cropped_ref_file, |
+ '--test_video', cropped_test_file, |
+ '--frame_analyzer', os.path.abspath(options.frame_analyzer), |
+ '--zxing_path', options.zxing_path, |
+ '--ffmpeg_path', options.ffmpeg, |
+ '--stats_file_ref', os.path.join(os.path.dirname(cropped_ref_file), |
+ cropped_ref_file + '_stats.txt'), |
+ '--stats_file_test', os.path.join(os.path.dirname(cropped_test_file), |
+ cropped_test_file + '_stats.txt'), |
+ '--yuv_frame_height', crop_height, |
+ '--yuv_frame_width', crop_width |
+ ] |
+ |
+ try: |
+ with open(result_file_name, 'w') as f: |
+ compare_video_recordings = subprocess.Popen(compare_cmd, stdout=f) |
+ compare_video_recordings.wait() |
+ print 'Result recorded to: ' + os.path.abspath(result_file_name) |
+ print 'Comparison done!' |
+ except subprocess.CalledProcessError as e: |
+ print 'Something went wrong when trying to compare videos: ' + e |
+ raise |
+ |
+ |
+def main(): |
+ """The main function. |
+ |
+ A simple invocation is: |
+ ./run_video_analysis.py \ |
+ --app_name AppRTCMobile \ |
+ --ffmpeg ./ffmpeg --ref_video_device=/dev/video0 \ |
+ --test_video_device=/dev/video1 \ |
+ --zxing_path ./zxing \ |
+ --test_crop_parameters 'crop=950:420:130:56' \ |
+ --ref_crop_parameters 'hflip, crop=950:420:130:56' \ |
+ --ref_rec_dir /tmp/ref \ |
+ --test_rec_dir /tmp/test |
+ |
+ This will produce the following files if successful: |
+ # Original video recordings. |
+ /tmp/ref/AppRTCMobile_<recording date and time>_ref.yuv |
+ /tmp/test/AppRTCMobile_<recording date and time>_test.yuv |
+ |
+ # Cropped video recordings according to the crop parameters. |
+ /tmp/ref/cropped_AppRTCMobile_<recording date and time>_ref.yuv |
+ /tmp/test/cropped_AppRTCMobile_<recording date and time>_ref.yuv |
+ |
+ # Comparison metrics from cropped test and ref videos. |
+ /tmp/test/AppRTCMobile_<recording date and time>_result.text |
+ |
+ """ |
+ options = _ParseArgs() |
+ RestartMagewellDevices(options.ref_video_device, options.test_video_device) |
+ record_paths = CreateRecordingDirs(options) |
+ recording_result = StartRecording(options, record_paths) |
+ |
+ # Do not require compare_video.py script to run, no metrics will be generated. |
+ if options.compare_videos_script: |
+ CompareVideos(options, recording_result) |
+ else: |
+ print ('Skipping compare videos step due to compare_videos flag were not ' |
+ 'passed.') |
+ |
+ |
+if __name__ == '__main__': |
+ sys.exit(main()) |