OLD | NEW |
---|---|
(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 if not options.ffmpeg: | |
87 parser.error('You most provide location for the ffmpeg executable.') | |
88 if not os.path.isfile(options.ffmpeg): | |
89 parser.error('Cannot find the ffmpeg executable.') | |
90 | |
91 # compare_videos.py dependencies. | |
92 if not os.path.isfile(options.compare_videos_script): | |
93 parser.warning('Cannot find compare_videos.py script, no metrics will be ' | |
94 'generated!') | |
95 if not os.path.isfile(options.frame_analyzer): | |
96 parser.warning('Cannot find frame_analyzer, no metrics will be generated!') | |
97 if not os.path.isfile(options.zxing_path): | |
98 parser.warning('Cannot find Zebra Xing, no metrics will be generated!') | |
99 | |
100 return options | |
101 | |
102 | |
103 def CreateRecordingDirs(options): | |
104 """Create root + sub directories for reference and test recordings. | |
105 | |
106 Args: | |
107 options(object): Contains all the provided command line options. | |
108 Return: | |
109 record_paths(dict): key: value pair with reference and test file | |
110 absolute paths. | |
111 """ | |
112 | |
113 # Create root directories for the video recordings. | |
114 if not os.path.isdir(options.ref_rec_dir): | |
115 os.makedirs(options.ref_rec_dir) | |
116 if not os.path.isdir(options.test_rec_dir): | |
117 os.makedirs(options.test_rec_dir) | |
118 | |
119 # Create and time-stamp directories for all the output files. | |
120 ref_rec_dir = os.path.join(options.ref_rec_dir, options.app_name + '_' + \ | |
121 CURRENT_TIME) | |
122 test_rec_dir = os.path.join(options.test_rec_dir, options.app_name + '_' + \ | |
123 CURRENT_TIME) | |
124 | |
125 os.makedirs(ref_rec_dir) | |
126 os.makedirs(test_rec_dir) | |
127 | |
128 record_paths = { | |
129 'ref_rec_location' : os.path.abspath(ref_rec_dir), | |
130 'test_rec_location' : os.path.abspath(test_rec_dir) | |
131 } | |
132 | |
133 return record_paths | |
134 | |
135 | |
136 def RestartMagewellDevices(options): | |
kjellander_webrtc
2017/03/08 07:31:21
Since you're only using two of the options here, I
janssonWebRTC
2017/03/09 15:51:00
Done.
| |
137 """Reset the USB ports where magewell capture devices are connected to. | |
138 | |
139 Tries to find the provided options.ref_video_device and | |
140 options.Sest_Rdeo_device devices which use video4linux and then do a soft | |
kjellander_webrtc
2017/03/08 07:31:21
options.Sest_Rdeo_device does not exist.
janssonWebRTC
2017/03/09 15:51:01
Done.
| |
141 reset by using USB unbind and bind. This is due to Magewell capture devices | |
142 have proven to be unstable after the first recording attempt. | |
143 | |
144 Args: | |
145 options(object): Contains all the provided command line options. | |
146 """ | |
147 | |
148 # Get the dev/videoN device name from the command line arguments. | |
149 ref_magewell = options.ref_video_device.split('/')[2] | |
150 test_magewell = options.test_video_device.split('/')[2] | |
151 | |
152 # Find the device location including USB and USB Bus ID's. | |
153 device_string = '/sys/bus/usb/devices/usb*/**/**/video4linux/' | |
154 ref_magewell_device = glob.glob('%s%s' % (device_string, ref_magewell)) | |
155 test_magewell_device = glob.glob('%s%s' % (device_string, test_magewell)) | |
156 | |
157 magewell_usb_ports = [] | |
158 | |
159 # Figure out the USB bus and port ID for each device. | |
160 ref_magewell_path = str(ref_magewell_device).split('/') | |
161 for directory in ref_magewell_path: | |
162 | |
163 # Find the folder with pattern "N-N", e.g. "4-3" or \ | |
164 # "[USB bus ID]-[USB port]" | |
165 if re.match(r'^\d-\d$', directory): | |
166 magewell_usb_ports.append(directory) | |
167 | |
168 test_magewell_path = str(test_magewell_device).split('/') | |
169 for directory in test_magewell_path: | |
170 | |
171 # Find the folder with pattern "N-N", e.g. "4-3" or \ | |
172 # "[USB bus ID]-[USB port]" | |
173 if re.match(r'^\d-\d$', directory): | |
174 magewell_usb_ports.append(directory) | |
175 | |
176 print '\nResetting USB ports where magewell devices are connected...' | |
177 | |
178 # Use the USB bus and port ID (e.g. 4-3) to unbind and bind the USB devices | |
179 # (i.e. soft eject and insert). | |
180 try: | |
181 for usb_port in magewell_usb_ports: | |
182 echo_cmd = ['echo', usb_port] | |
183 unbind_cmd = ['sudo', 'tee', '/sys/bus/usb/drivers/usb/unbind'] | |
184 bind_cmd = ['sudo', 'tee', '/sys/bus/usb/drivers/usb/bind'] | |
185 | |
186 # TODO(jansson) Figure out a way to call on echo once for bind & unbind | |
187 # if possible. | |
188 echo_unbind = subprocess.Popen(echo_cmd, stdout=subprocess.PIPE) | |
189 unbind = subprocess.Popen(unbind_cmd, stdin=echo_unbind.stdout) | |
190 echo_unbind.stdout.close() | |
191 unbind.communicate() | |
192 unbind.wait() | |
193 | |
194 echo_bind = subprocess.Popen(echo_cmd, stdout=subprocess.PIPE) | |
195 bind = subprocess.Popen(bind_cmd, stdin=echo_bind.stdout) | |
196 echo_bind.stdout.close() | |
197 bind.communicate() | |
198 bind.wait() | |
199 except OSError as e: | |
200 print 'Error while resetting magewell devices: ' + e | |
201 raise | |
202 | |
203 print 'Reset done!\n' | |
204 | |
205 | |
206 def StartRecording(options, record_paths): | |
207 """Starts recording from the two specified video devices. | |
208 | |
209 Args: | |
210 options(object): Contains all the provided command line options. | |
211 record_paths(dict): key: value pair with reference and test file | |
212 absolute paths. | |
213 """ | |
214 ref_file_name = '%s_%s_ref.%s' % (options.app_name, CURRENT_TIME, | |
215 options.video_container) | |
216 ref_file_location = os.path.join(record_paths['ref_rec_location'], | |
217 ref_file_name) | |
218 | |
219 test_file_name = '%s_%s_test.%s' % (options.app_name, CURRENT_TIME, | |
220 options.video_container) | |
221 test_file_location = os.path.join(record_paths['test_rec_location'], | |
222 test_file_name) | |
223 | |
224 # Reference video recorder command line. | |
225 ref_cmd = [ | |
226 options.ffmpeg, | |
227 '-v', 'error', | |
228 '-s', options.frame_width + 'x' + options.frame_height, | |
229 '-framerate', options.framerate, | |
230 '-f', options.recording_api, | |
231 '-i', options.ref_video_device, | |
232 '-pix_fmt', options.pixel_format, | |
233 '-s', options.frame_width + 'x' + options.frame_height, | |
234 '-t', options.ref_duration, | |
235 '-framerate', options.framerate, | |
236 ref_file_location | |
237 ] | |
238 | |
239 # Test video recorder command line. | |
240 test_cmd = [ | |
241 options.ffmpeg, | |
242 '-v', 'error', | |
243 '-s', options.frame_width + 'x' + options.frame_height, | |
244 '-framerate', options.framerate, | |
245 '-f', options.recording_api, | |
246 '-i', options.test_video_device, | |
247 '-pix_fmt', options.pixel_format, | |
248 '-s', options.frame_width + 'x' + options.frame_height, | |
249 '-t', options.test_duration, | |
250 '-framerate', options.framerate, | |
251 test_file_location | |
252 ] | |
253 print 'Trying to record from reference recorder...' | |
254 ref_recorder = subprocess.Popen(ref_cmd, stderr=sys.stderr) | |
255 | |
256 # Start the 2nd recording a little later to ensure the 1st one has started. | |
257 # TODO(jansson) Check that the ref_recorder output file exists rather than | |
258 # using sleep. | |
259 time.sleep(options.time_between_recordings) | |
260 print 'Trying to record from test recorder...' | |
261 test_recorder = subprocess.Popen(test_cmd, stderr=sys.stderr) | |
262 test_recorder.wait() | |
263 ref_recorder.wait() | |
264 | |
265 # ffmpeg does not abort when it fails, need to check return code. | |
266 assert ref_recorder.returncode == 0, '%s%s' % ('Ref recording failed, ' | |
kjellander_webrtc
2017/03/08 07:31:21
It looks strange to me to use the string formatter
janssonWebRTC
2017/03/09 15:51:00
Done.
| |
267 'check ffmpeg output and device: ', options.ref_video_device) | |
268 assert test_recorder.returncode == 0, '%s%s' % ('Test recording failed, ' | |
269 'check ffmpeg output and device: ', options.test_video_device) | |
270 | |
271 print 'Ref file recorded to: ' + os.path.abspath(ref_file_location) | |
272 print 'Test file recorded to: ' + os.path.abspath(test_file_location) | |
273 print 'Recording done!\n' | |
274 return FlipAndCropRecordings(options, test_file_name, | |
275 record_paths['test_rec_location'], ref_file_name, | |
276 record_paths['ref_rec_location']) | |
277 | |
278 | |
279 def FlipAndCropRecordings(options, test_file_name, test_file_location, | |
280 ref_file_name, ref_file_location): | |
281 """Performs a horizontal flip of the reference video to match the test video. | |
282 | |
283 This is done to the match orientation and then crops the ref and test videos | |
284 using the options.test_crop_parameters and options.ref_crop_parameters. | |
285 | |
286 Args: | |
287 options(object): Contains all the provided command line options. | |
288 test_file_name(string): Name of the test video file recording. | |
289 test_file_location(string): Path to the test video file recording. | |
290 ref_file_name(string): Name of the reference video file recording. | |
291 ref_file_location(string): Path to the reference video file recording. | |
292 Return: | |
293 recording_files_and_time(dict): key: value pair with the path to cropped | |
294 test and reference video files. | |
295 """ | |
296 print 'Trying to crop videos...' | |
297 | |
298 # Ref file cropping. | |
299 cropped_ref_file_name = 'cropped_' + ref_file_name | |
300 cropped_ref_file = os.path.abspath( | |
301 os.path.join(ref_file_location, cropped_ref_file_name)) | |
302 | |
303 ref_video_crop_cmd = [ | |
304 options.ffmpeg, | |
305 '-v', 'error', | |
306 '-s', options.frame_width + 'x' + options.frame_height, | |
307 '-i', os.path.join(ref_file_location, ref_file_name), | |
308 '-vf', options.ref_crop_parameters, | |
309 '-c:a', 'copy', | |
310 cropped_ref_file | |
311 ] | |
312 | |
313 # Test file cropping. | |
314 cropped_test_file_name = 'cropped_' + test_file_name | |
315 cropped_test_file = os.path.abspath( | |
316 os.path.join(test_file_location, cropped_test_file_name)) | |
317 | |
318 test_video_crop_cmd = [ | |
319 options.ffmpeg, | |
320 '-v', 'error', | |
321 '-s', options.frame_width + 'x' + options.frame_height, | |
322 '-i', os.path.join(test_file_location, test_file_name), | |
323 '-vf', options.test_crop_parameters, | |
324 '-c:a', 'copy', | |
325 cropped_test_file | |
326 ] | |
327 | |
328 ref_crop = subprocess.Popen(ref_video_crop_cmd) | |
329 ref_crop.wait() | |
330 print 'Ref file cropped to: ' + cropped_ref_file | |
331 | |
332 try: | |
333 test_crop = subprocess.Popen(test_video_crop_cmd) | |
334 test_crop.wait() | |
335 print 'Test file cropped to: ' + cropped_test_file | |
336 print 'Cropping done!\n' | |
337 | |
338 # Need to return these so they can be used by other parts. | |
339 cropped_recordings = { | |
340 'cropped_test_file' : cropped_test_file, | |
341 'cropped_ref_file' : cropped_ref_file | |
342 } | |
343 | |
344 return cropped_recordings | |
345 except subprocess.CalledProcessError as e: | |
346 print 'Something went wrong during cropping: ' + e | |
347 raise | |
348 | |
349 | |
350 def CompareVideos(options, recording_result): | |
351 """Runs the compare_video.py script from src/webrtc/tools using the file path. | |
352 | |
353 Uses the path from recording_result and writes the output to a file named | |
354 <options.app_name + '_' + CURRENT_TIME + '_result.txt> in the reference video | |
355 recording folder taken from recording_result. | |
356 | |
357 Args: | |
358 options(object): Contains all the provided command line options. | |
359 recording_files_and_time(dict): key: value pair with the path to cropped | |
360 test and reference video files | |
361 """ | |
362 print 'Starting comparison...' | |
363 print 'Grab a coffee, this might take a few minutes...' | |
364 cropped_ref_file = recording_result['cropped_ref_file'] | |
365 cropped_test_file = recording_result['cropped_test_file'] | |
366 compare_videos_script = os.path.abspath(options.compare_videos_script) | |
367 result_file_name = os.path.join('%s', '%s_%s%s') % ( | |
kjellander_webrtc
2017/03/08 07:31:21
Inline os.path.dirname(recording_result['cropped_r
janssonWebRTC
2017/03/09 15:51:01
Done.
| |
368 os.path.dirname(recording_result['cropped_ref_file']), options.app_name, | |
369 CURRENT_TIME, '_result.txt') | |
370 | |
371 # Find the crop dimensions (950 and 420) in the ref crop parameter string: | |
372 # 'hflip, crop=950:420:130:56' | |
373 for param in options.ref_crop_parameters.split('crop'): | |
kjellander_webrtc
2017/03/08 07:31:21
You're making hard assumptions on the format of th
janssonWebRTC
2017/03/09 15:51:01
Good catch, I've added that as check in _ParseArgs
| |
374 if param[0] == '=': | |
375 crop_width = param.split(':')[0].split('=')[1] | |
376 crop_height = param.split(':')[1] | |
377 | |
378 compare_cmd = [ | |
379 sys.executable, | |
380 compare_videos_script, | |
381 '--ref_video', cropped_ref_file, | |
382 '--test_video', cropped_test_file, | |
383 '--frame_analyzer', os.path.abspath(options.frame_analyzer), | |
384 '--zxing_path', options.zxing_path, | |
385 '--ffmpeg_path', options.ffmpeg, | |
386 '--stats_file_ref', os.path.join(os.path.dirname(cropped_ref_file), | |
387 cropped_ref_file + '_stats.txt'), | |
388 '--stats_file_test', os.path.join(os.path.dirname(cropped_test_file), | |
389 cropped_test_file + '_stats.txt'), | |
390 '--yuv_frame_height', crop_height, | |
391 '--yuv_frame_width', crop_width | |
392 ] | |
393 | |
394 try: | |
395 with open(result_file_name, 'w') as f: | |
396 compare_video_recordings = subprocess.Popen(compare_cmd, stdout=f) | |
397 compare_video_recordings.wait() | |
398 print 'Result recorded to: ' + os.path.abspath(result_file_name) | |
399 print 'Comparison done!' | |
400 except subprocess.CalledProcessError as e: | |
401 print 'Something went wrong when trying to compare videos: ' + e | |
402 raise | |
403 | |
404 | |
405 def main(): | |
406 """The main function. | |
407 | |
408 A simple invocation is: | |
409 ./run_video_analysis.py \ | |
410 --app_name AppRTCMobile \ | |
411 --ffmpeg ./ffmpeg --ref_video_device=/dev/video0 \ | |
412 --test_video_device=/dev/video1 \ | |
413 --zxing_path ./zxing \ | |
414 --test_crop_parameters 'crop=950:420:130:56' \ | |
415 --ref_crop_parameters 'hflip, crop=950:420:130:56' \ | |
416 --ref_rec_dir /tmp/ref \ | |
417 --test_rec_dir /tmp/test | |
418 | |
419 This will produce the following files if successful: | |
420 # Original video recordings. | |
421 /tmp/ref/AppRTCMobile_<recording date and time>_ref.yuv | |
422 /tmp/test/AppRTCMobile_<recording date and time>_test.yuv | |
423 | |
424 # Cropped video recordings according to the crop parameters. | |
425 /tmp/ref/cropped_AppRTCMobile_<recording date and time>_ref.yuv | |
426 /tmp/test/cropped_AppRTCMobile_<recording date and time>_ref.yuv | |
427 | |
428 # Comparison metrics from cropped test and ref videos. | |
429 /tmp/test/AppRTCMobile_<recording date and time>_result.text | |
430 | |
431 """ | |
432 options = _ParseArgs() | |
433 RestartMagewellDevices(options) | |
434 record_paths = CreateRecordingDirs(options) | |
435 recording_result = StartRecording(options, record_paths) | |
436 | |
437 # Do not require compare_video.py script to run, no metrics will be generated. | |
438 if (os.path.exists(options.frame_analyzer) or | |
kjellander_webrtc
2017/03/08 07:31:21
Skip os.path.exists here (you already check that i
janssonWebRTC
2017/03/09 15:51:01
Yeah I agree this is confusing, I will leave just
| |
439 os.path.exists(options.compare_videos_script) or | |
kjellander_webrtc
2017/03/08 07:31:21
+2 space indent on this line and the one below.
janssonWebRTC
2017/03/09 15:51:00
Done.
| |
440 os.path.exists(options.zxing_path)): | |
441 CompareVideos(options, recording_result) | |
442 else: | |
443 print ('Skipping compare videos step due to frame_analyzer, zxing or ' | |
444 'compare_videos flags were not passed.') | |
kjellander_webrtc
2017/03/08 07:31:21
indent with ( above.
janssonWebRTC
2017/03/09 15:51:00
Done.
| |
445 | |
446 | |
447 if __name__ == '__main__': | |
448 sys.exit(main()) | |
OLD | NEW |