diff --git a/examples/BUILD.gn b/examples/BUILD.gn index c94e346821..76526eb3be 100644 --- a/examples/BUILD.gn +++ b/examples/BUILD.gn @@ -176,6 +176,8 @@ if (is_android) { "../build/android/adb_reverse_forwarder.py", "../examples/androidtests/video_quality_loopback_test.py", "../resources/reference_video_640x360_30fps.y4m", + "../rtc_tools/barcode_tools/barcode_decoder.py", + "../rtc_tools/barcode_tools/helper_functions.py", "../rtc_tools/compare_videos.py", "../rtc_tools/testing/prebuilt_apprtc.zip", "../rtc_tools/testing/golang/linux/go.tar.gz", diff --git a/rtc_tools/barcode_tools/DEPS b/rtc_tools/barcode_tools/DEPS new file mode 100644 index 0000000000..d0325a65aa --- /dev/null +++ b/rtc_tools/barcode_tools/DEPS @@ -0,0 +1,13 @@ +# This is trimmed down version of the main tools DEPS file which is to be used +# in Chromiums PyAuto WebRTC video quality measurement test. We will only +# need the Zxing dependencies as we only use the barcode tools in this test. + +deps = { + # Used by barcode_tools + "barcode_tools/third_party/zxing/core": + "http://zxing.googlecode.com/svn/trunk/core@2349", + + # Used by barcode_tools + "barcode_tools/third_party/zxing/javase": + "http://zxing.googlecode.com/svn/trunk/javase@2349", +} diff --git a/rtc_tools/barcode_tools/README b/rtc_tools/barcode_tools/README new file mode 100644 index 0000000000..a23e798064 --- /dev/null +++ b/rtc_tools/barcode_tools/README @@ -0,0 +1,34 @@ +This file explains how to get the dependencies needed for the barcode tools. + +barcode_encoder.py +================== +This script depends on: +* Zxing (Java version) +* Ant (must be installed manually) +* Java + +To automatically download Zxing for the encoder script, checkout this directory +as a separate gclient solution, like this: +gclient config http://webrtc.googlecode.com/svn/trunk/webrtc/rtc_tools/barcode_tools +gclient sync +Then the Zxing Java source code will be put in third_party/zxing. + +In order to run barcode_encoder.py you then need to build: +* zxing/core/core.jar +* zxing/javase/javase.jar +These are compiled using Ant by running build_zxing.py: +python build_zxing.py + +For more info about Zxing, see https://code.google.com/p/zxing/ + + +barcode_decoder.py +================== +This script depends on: +* Zxing (C++ version). You need to checkout from Subversion and build the libs + and zxing SCons targets. SVN URL: http://zxing.googlecode.com/svn/trunk/cpp +* FFMPEG fmpeg 0.11.1 + +These dependencies must be precompiled separately before running the script. +Make sure to add FFMPEG to the PATH environment variable and provide the path +to the zxing executable using the mandatory command line flag to the script. diff --git a/rtc_tools/barcode_tools/barcode_decoder.py b/rtc_tools/barcode_tools/barcode_decoder.py new file mode 100755 index 0000000000..2abd677b4b --- /dev/null +++ b/rtc_tools/barcode_tools/barcode_decoder.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python +# Copyright (c) 2012 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 sys + +if __name__ == '__main__': + # Make sure we always can import helper_functions. + sys.path.append(os.path.dirname(__file__)) + +import helper_functions + +# Chrome browsertests will throw away stderr; avoid that output gets lost. +sys.stderr = sys.stdout + + +def ConvertYuvToPngFiles(yuv_file_name, yuv_frame_width, yuv_frame_height, + output_directory, ffmpeg_path): + """Converts a YUV video file into PNG frames. + + The function uses ffmpeg to convert the YUV file. The output of ffmpeg is in + the form frame_xxxx.png, where xxxx is the frame number, starting from 0001. + + Args: + yuv_file_name(string): The name of the YUV file. + yuv_frame_width(int): The width of one YUV frame. + yuv_frame_height(int): The height of one YUV frame. + output_directory(string): The output directory where the PNG frames will be + stored. + ffmpeg_path(string): The path to the ffmpeg executable. If None, the PATH + will be searched for it. + + Return: + (bool): True if the conversion was OK. + """ + size_string = str(yuv_frame_width) + 'x' + str(yuv_frame_height) + output_files_pattern = os.path.join(output_directory, 'frame_%04d.png') + if not ffmpeg_path: + ffmpeg_path = 'ffmpeg.exe' if sys.platform == 'win32' else 'ffmpeg' + command = [ffmpeg_path, '-s', '%s' % size_string, '-i', '%s' + % yuv_file_name, '-f', 'image2', '-vcodec', 'png', + '%s' % output_files_pattern] + try: + print 'Converting YUV file to PNG images (may take a while)...' + print ' '.join(command) + helper_functions.RunShellCommand( + command, fail_msg='Error during YUV to PNG conversion') + except helper_functions.HelperError, err: + print 'Error executing command: %s. Error: %s' % (command, err) + return False + except OSError: + print 'Did not find %s. Have you installed it?' % ffmpeg_path + return False + return True + + +def DecodeFrames(input_directory, zxing_path): + """Decodes the barcodes overlaid in each frame. + + The function uses the Zxing command-line tool from the Zxing C++ distribution + to decode the barcode in every PNG frame from the input directory. The frames + should be named frame_xxxx.png, where xxxx is the frame number. The frame + numbers should be consecutive and should start from 0001. + The decoding results in a frame_xxxx.txt file for every successfully decoded + barcode. This file contains the decoded barcode as 12-digit string (UPC-A + format: 11 digits content + one check digit). + + Args: + input_directory(string): The input directory from where the PNG frames are + read. + zxing_path(string): The path to the zxing binary. If specified as None, + the PATH will be searched for it. + Return: + (bool): True if the decoding succeeded. + """ + if not zxing_path: + zxing_path = 'zxing.exe' if sys.platform == 'win32' else 'zxing' + print 'Decoding barcodes from PNG files with %s...' % zxing_path + return helper_functions.PerformActionOnAllFiles( + directory=input_directory, file_pattern='frame_', + file_extension='png', start_number=1, action=_DecodeBarcodeInFile, + command_line_decoder=zxing_path) + + +def _DecodeBarcodeInFile(file_name, command_line_decoder): + """Decodes the barcode in the upper left corner of a PNG file. + + Args: + file_name(string): File name of the PNG file. + command_line_decoder(string): The ZXing command-line decoding tool. + + Return: + (bool): True upon success, False otherwise. + """ + command = [command_line_decoder, '--try-harder', '--dump-raw', file_name] + try: + out = helper_functions.RunShellCommand( + command, fail_msg='Error during decoding of %s' % file_name) + text_file = open('%s.txt' % file_name[:-4], 'w') + text_file.write(out) + text_file.close() + except helper_functions.HelperError, err: + print 'Barcode in %s cannot be decoded.' % file_name + print err + return False + except OSError: + print 'Did not find %s. Have you installed it?' % command_line_decoder + return False + return True + + +def _GenerateStatsFile(stats_file_name, input_directory='.'): + """Generate statistics file. + + The function generates a statistics file. The contents of the file are in the + format , where frame name is the name of every frame + (effectively the frame number) and barcode is the decoded barcode. The frames + and the helper .txt files are removed after they have been used. + """ + file_prefix = os.path.join(input_directory, 'frame_') + stats_file = open(stats_file_name, 'w') + + print 'Generating stats file: %s' % stats_file_name + for i in range(1, _CountFramesIn(input_directory=input_directory) + 1): + frame_number = helper_functions.ZeroPad(i) + barcode_file_name = file_prefix + frame_number + '.txt' + png_frame = file_prefix + frame_number + '.png' + entry_frame_number = helper_functions.ZeroPad(i-1) + entry = 'frame_' + entry_frame_number + ' ' + + if os.path.isfile(barcode_file_name): + barcode = _ReadBarcodeFromTextFile(barcode_file_name) + os.remove(barcode_file_name) + + if _CheckBarcode(barcode): + entry += (helper_functions.ZeroPad(int(barcode[0:11])) + '\n') + else: + entry += 'Barcode error\n' # Barcode is wrongly detected. + else: # Barcode file doesn't exist. + entry += 'Barcode error\n' + + stats_file.write(entry) + os.remove(png_frame) + + stats_file.close() + + +def _ReadBarcodeFromTextFile(barcode_file_name): + """Reads the decoded barcode for a .txt file. + + Args: + barcode_file_name(string): The name of the .txt file. + Return: + (string): The decoded barcode. + """ + barcode_file = open(barcode_file_name, 'r') + barcode = barcode_file.read() + barcode_file.close() + return barcode + + +def _CheckBarcode(barcode): + """Check weather the UPC-A barcode was decoded correctly. + + This function calculates the check digit of the provided barcode and compares + it to the check digit that was decoded. + + Args: + barcode(string): The barcode (12-digit). + Return: + (bool): True if the barcode was decoded correctly. + """ + if len(barcode) != 12: + return False + + r1 = range(0, 11, 2) # Odd digits + r2 = range(1, 10, 2) # Even digits except last + dsum = 0 + # Sum all the even digits + for i in r1: + dsum += int(barcode[i]) + # Multiply the sum by 3 + dsum *= 3 + # Add all the even digits except the check digit (12th digit) + for i in r2: + dsum += int(barcode[i]) + # Get the modulo 10 + dsum = dsum % 10 + # If not 0 substract from 10 + if dsum != 0: + dsum = 10 - dsum + # Compare result and check digit + return dsum == int(barcode[11]) + + +def _CountFramesIn(input_directory='.'): + """Calculates the number of frames in the input directory. + + The function calculates the number of frames in the input directory. The + frames should be named frame_xxxx.png, where xxxx is the number of the frame. + The numbers should start from 1 and should be consecutive. + + Args: + input_directory(string): The input directory. + Return: + (int): The number of frames. + """ + file_prefix = os.path.join(input_directory, 'frame_') + file_exists = True + num = 1 + + while file_exists: + file_name = (file_prefix + helper_functions.ZeroPad(num) + '.png') + if os.path.isfile(file_name): + num += 1 + else: + file_exists = False + return num - 1 + + +def _ParseArgs(): + """Registers the command-line options.""" + usage = "usage: %prog [options]" + parser = optparse.OptionParser(usage=usage) + + parser.add_option('--zxing_path', type='string', + help=('The path to where the zxing executable is located. ' + 'If omitted, it will be assumed to be present in the ' + 'PATH with the name zxing[.exe].')) + parser.add_option('--ffmpeg_path', type='string', + help=('The path to where the ffmpeg executable is located. ' + 'If omitted, it will be assumed to be present in the ' + 'PATH with the name ffmpeg[.exe].')) + parser.add_option('--yuv_frame_width', type='int', default=640, + help='Width of the YUV file\'s frames. Default: %default') + parser.add_option('--yuv_frame_height', type='int', default=480, + help='Height of the YUV file\'s frames. Default: %default') + parser.add_option('--yuv_file', type='string', default='output.yuv', + help='The YUV file to be decoded. Default: %default') + parser.add_option('--stats_file', type='string', default='stats.txt', + help='The output stats file. Default: %default') + parser.add_option('--png_working_dir', type='string', default='.', + help=('The directory for temporary PNG images to be stored ' + 'in when decoding from YUV before they\'re barcode ' + 'decoded. If using Windows and a Cygwin-compiled ' + 'zxing.exe, you should keep the default value to ' + 'avoid problems. Default: %default')) + options, _ = parser.parse_args() + return options + + +def main(): + """The main function. + + A simple invocation is: + ./webrtc/rtc_tools/barcode_tools/barcode_decoder.py + --yuv_file= + --yuv_frame_width=640 --yuv_frame_height=480 + --stats_file= + """ + options = _ParseArgs() + + # Convert the overlaid YUV video into a set of PNG frames. + if not ConvertYuvToPngFiles(options.yuv_file, options.yuv_frame_width, + options.yuv_frame_height, + output_directory=options.png_working_dir, + ffmpeg_path=options.ffmpeg_path): + print 'An error occurred converting from YUV to PNG frames.' + return -1 + + # Decode the barcodes from the PNG frames. + if not DecodeFrames(input_directory=options.png_working_dir, + zxing_path=options.zxing_path): + print 'An error occurred decoding barcodes from PNG frames.' + return -2 + + # Generate statistics file. + _GenerateStatsFile(options.stats_file, + input_directory=options.png_working_dir) + print 'Completed barcode decoding.' + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/rtc_tools/barcode_tools/barcode_encoder.py b/rtc_tools/barcode_tools/barcode_encoder.py new file mode 100755 index 0000000000..9ab8b50754 --- /dev/null +++ b/rtc_tools/barcode_tools/barcode_encoder.py @@ -0,0 +1,372 @@ +#!/usr/bin/env python +# Copyright (c) 2012 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 sys + +import helper_functions + +_DEFAULT_BARCODE_WIDTH = 352 +_DEFAULT_BARCODES_FILE = 'barcodes.yuv' + + +def GenerateUpcaBarcodes(number_of_barcodes, barcode_width, barcode_height, + output_directory='.', + path_to_zxing='zxing-read-only'): + """Generates UPC-A barcodes. + + This function generates a number_of_barcodes UPC-A barcodes. The function + calls an example Java encoder from the Zxing library. The barcodes are + generated as PNG images. The width of the barcodes shouldn't be less than 102 + pixels because otherwise Zxing can't properly generate the barcodes. + + Args: + number_of_barcodes(int): The number of barcodes to generate. + barcode_width(int): Width of barcode in pixels. + barcode_height(int): Height of barcode in pixels. + output_directory(string): Output directory where to store generated + barcodes. + path_to_zxing(string): The path to Zxing. + + Return: + (bool): True if the conversion is successful. + """ + base_file_name = os.path.join(output_directory, "barcode_") + jars = _FormJarsString(path_to_zxing) + command_line_encoder = 'com.google.zxing.client.j2se.CommandLineEncoder' + barcode_width = str(barcode_width) + barcode_height = str(barcode_height) + + errors = False + for i in range(number_of_barcodes): + suffix = helper_functions.ZeroPad(i) + # Barcodes starting from 0 + content = helper_functions.ZeroPad(i, 11) + output_file_name = base_file_name + suffix + ".png" + + command = ["java", "-cp", jars, command_line_encoder, + "--barcode_format=UPC_A", "--height=%s" % barcode_height, + "--width=%s" % barcode_width, + "--output=%s" % (output_file_name), "%s" % (content)] + try: + helper_functions.RunShellCommand( + command, fail_msg=('Error during barcode %s generation' % content)) + except helper_functions.HelperError as err: + print >> sys.stderr, err + errors = True + return not errors + + +def ConvertPngToYuvBarcodes(input_directory='.', output_directory='.'): + """Converts PNG barcodes to YUV barcode images. + + This function reads all the PNG files from the input directory which are in + the format frame_xxxx.png, where xxxx is the number of the frame, starting + from 0000. The frames should be consecutive numbers. The output YUV file is + named frame_xxxx.yuv. The function uses ffmpeg to do the conversion. + + Args: + input_directory(string): The input direcotry to read the PNG barcodes from. + output_directory(string): The putput directory to write the YUV files to. + Return: + (bool): True if the conversion was without errors. + """ + return helper_functions.PerformActionOnAllFiles( + input_directory, 'barcode_', 'png', 0, _ConvertToYuvAndDelete, + output_directory=output_directory, pattern='barcode_') + + +def _ConvertToYuvAndDelete(output_directory, file_name, pattern): + """Converts a PNG file to a YUV file and deletes the PNG file. + + Args: + output_directory(string): The output directory for the YUV file. + file_name(string): The PNG file name. + pattern(string): The file pattern of the PNG/YUV file. The PNG/YUV files are + named patternxx..x.png/yuv, where xx..x are digits starting from 00..0. + Return: + (bool): True upon successful conversion, false otherwise. + """ + # Pattern should be in file name + if not pattern in file_name: + return False + pattern_position = file_name.rfind(pattern) + + # Strip the path to the PNG file and replace the png extension with yuv + yuv_file_name = file_name[pattern_position:-3] + 'yuv' + yuv_file_name = os.path.join(output_directory, yuv_file_name) + + command = ['ffmpeg', '-i', '%s' % (file_name), '-pix_fmt', 'yuv420p', + '%s' % (yuv_file_name)] + try: + helper_functions.RunShellCommand( + command, fail_msg=('Error during PNG to YUV conversion of %s' % + file_name)) + os.remove(file_name) + except helper_functions.HelperError as err: + print >> sys.stderr, err + return False + return True + + +def CombineYuvFramesIntoOneFile(output_file_name, input_directory='.'): + """Combines several YUV frames into one YUV video file. + + The function combines the YUV frames from input_directory into one YUV video + file. The frames should be named in the format frame_xxxx.yuv where xxxx + stands for the frame number. The numbers have to be consecutive and start from + 0000. The YUV frames are removed after they have been added to the video. + + Args: + output_file_name(string): The name of the file to produce. + input_directory(string): The directory from which the YUV frames are read. + Return: + (bool): True if the frame stitching went OK. + """ + output_file = open(output_file_name, "wb") + success = helper_functions.PerformActionOnAllFiles( + input_directory, 'barcode_', 'yuv', 0, _AddToFileAndDelete, + output_file=output_file) + output_file.close() + return success + +def _AddToFileAndDelete(output_file, file_name): + """Adds the contents of a file to a previously opened file. + + Args: + output_file(file): The ouput file, previously opened. + file_name(string): The file name of the file to add to the output file. + + Return: + (bool): True if successful, False otherwise. + """ + input_file = open(file_name, "rb") + input_file_contents = input_file.read() + output_file.write(input_file_contents) + input_file.close() + try: + os.remove(file_name) + except OSError as e: + print >> sys.stderr, 'Error deleting file %s.\nError: %s' % (file_name, e) + return False + return True + + +def _OverlayBarcodeAndBaseFrames(barcodes_file, base_file, output_file, + barcodes_component_sizes, + base_component_sizes): + """Overlays the next YUV frame from a file with a barcode. + + Args: + barcodes_file(FileObject): The YUV file containing the barcodes (opened). + base_file(FileObject): The base YUV file (opened). + output_file(FileObject): The output overlaid file (opened). + barcodes_component_sizes(list of tuples): The width and height of each Y, U + and V plane of the barcodes YUV file. + base_component_sizes(list of tuples): The width and height of each Y, U and + V plane of the base YUV file. + Return: + (bool): True if there are more planes (i.e. frames) in the base file, false + otherwise. + """ + # We will loop three times - once for the Y, U and V planes + for ((barcode_comp_width, barcode_comp_height), + (base_comp_width, base_comp_height)) in zip(barcodes_component_sizes, + base_component_sizes): + for base_row in range(base_comp_height): + barcode_plane_traversed = False + if (base_row < barcode_comp_height) and not barcode_plane_traversed: + barcode_plane = barcodes_file.read(barcode_comp_width) + if barcode_plane == "": + barcode_plane_traversed = True + else: + barcode_plane_traversed = True + base_plane = base_file.read(base_comp_width) + + if base_plane == "": + return False + + if not barcode_plane_traversed: + # Substitute part of the base component with the top component + output_file.write(barcode_plane) + base_plane = base_plane[barcode_comp_width:] + output_file.write(base_plane) + return True + + +def OverlayYuvFiles(barcode_width, barcode_height, base_width, base_height, + barcodes_file_name, base_file_name, output_file_name): + """Overlays two YUV files starting from the upper left corner of both. + + Args: + barcode_width(int): The width of the barcode (to be overlaid). + barcode_height(int): The height of the barcode (to be overlaid). + base_width(int): The width of a frame of the base file. + base_height(int): The height of a frame of the base file. + barcodes_file_name(string): The name of the YUV file containing the YUV + barcodes. + base_file_name(string): The name of the base YUV file. + output_file_name(string): The name of the output file where the overlaid + video will be written. + """ + # Component sizes = [Y_sizes, U_sizes, V_sizes] + barcodes_component_sizes = [(barcode_width, barcode_height), + (barcode_width/2, barcode_height/2), + (barcode_width/2, barcode_height/2)] + base_component_sizes = [(base_width, base_height), + (base_width/2, base_height/2), + (base_width/2, base_height/2)] + + barcodes_file = open(barcodes_file_name, 'rb') + base_file = open(base_file_name, 'rb') + output_file = open(output_file_name, 'wb') + + data_left = True + while data_left: + data_left = _OverlayBarcodeAndBaseFrames(barcodes_file, base_file, + output_file, + barcodes_component_sizes, + base_component_sizes) + + barcodes_file.close() + base_file.close() + output_file.close() + + +def CalculateFramesNumberFromYuv(yuv_width, yuv_height, file_name): + """Calculates the number of frames of a YUV video. + + Args: + yuv_width(int): Width of a frame of the yuv file. + yuv_height(int): Height of a frame of the YUV file. + file_name(string): The name of the YUV file. + Return: + (int): The number of frames in the YUV file. + """ + file_size = os.path.getsize(file_name) + + y_plane_size = yuv_width * yuv_height + u_plane_size = (yuv_width/2) * (yuv_height/2) # Equals to V plane size too + frame_size = y_plane_size + (2 * u_plane_size) + return int(file_size/frame_size) # Should be int anyway + + +def _FormJarsString(path_to_zxing): + """Forms the the Zxing core and javase jars argument. + + Args: + path_to_zxing(string): The path to the Zxing checkout folder. + Return: + (string): The newly formed jars argument. + """ + javase_jar = os.path.join(path_to_zxing, "javase", "javase.jar") + core_jar = os.path.join(path_to_zxing, "core", "core.jar") + delimiter = ':' + if os.name != 'posix': + delimiter = ';' + return javase_jar + delimiter + core_jar + +def _ParseArgs(): + """Registers the command-line options.""" + usage = "usage: %prog [options]" + parser = optparse.OptionParser(usage=usage) + + parser.add_option('--barcode_width', type='int', + default=_DEFAULT_BARCODE_WIDTH, + help=('Width of the barcodes to be overlaid on top of the' + ' base file. Default: %default')) + parser.add_option('--barcode_height', type='int', default=32, + help=('Height of the barcodes to be overlaid on top of the' + ' base file. Default: %default')) + parser.add_option('--base_frame_width', type='int', default=352, + help=('Width of the base YUV file\'s frames. ' + 'Default: %default')) + parser.add_option('--base_frame_height', type='int', default=288, + help=('Height of the top YUV file\'s frames. ' + 'Default: %default')) + parser.add_option('--barcodes_yuv', type='string', + default=_DEFAULT_BARCODES_FILE, + help=('The YUV file with the barcodes in YUV. ' + 'Default: %default')) + parser.add_option('--base_yuv', type='string', default='base.yuv', + help=('The base YUV file to be overlaid. ' + 'Default: %default')) + parser.add_option('--output_yuv', type='string', default='output.yuv', + help=('The output YUV file containing the base overlaid' + ' with the barcodes. Default: %default')) + parser.add_option('--png_barcodes_output_dir', type='string', default='.', + help=('Output directory where the PNG barcodes will be ' + 'generated. Default: %default')) + parser.add_option('--png_barcodes_input_dir', type='string', default='.', + help=('Input directory from where the PNG barcodes will be ' + 'read. Default: %default')) + parser.add_option('--yuv_barcodes_output_dir', type='string', default='.', + help=('Output directory where the YUV barcodes will be ' + 'generated. Default: %default')) + parser.add_option('--yuv_frames_input_dir', type='string', default='.', + help=('Input directory from where the YUV will be ' + 'read before combination. Default: %default')) + parser.add_option('--zxing_dir', type='string', default='zxing', + help=('Path to the Zxing barcodes library. ' + 'Default: %default')) + options = parser.parse_args()[0] + return options + + +def main(): + """The main function. + + A simple invocation will be: + ./webrtc/rtc_tools/barcode_tools/barcode_encoder.py --barcode_height=32 + --base_frame_width=352 --base_frame_height=288 + --base_yuv= + --output_yuv= + """ + options = _ParseArgs() + # The barcodes with will be different than the base frame width only if + # explicitly specified at the command line. + if options.barcode_width == _DEFAULT_BARCODE_WIDTH: + options.barcode_width = options.base_frame_width + # If the user provides a value for the barcodes YUV video file, we will keep + # it. Otherwise we create a temp file which is removed after it has been used. + keep_barcodes_yuv_file = False + if options.barcodes_yuv != _DEFAULT_BARCODES_FILE: + keep_barcodes_yuv_file = True + + # Calculate the number of barcodes - it is equal to the number of frames in + # the base file. + number_of_barcodes = CalculateFramesNumberFromYuv( + options.base_frame_width, options.base_frame_height, options.base_yuv) + + script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) + zxing_dir = os.path.join(script_dir, 'third_party', 'zxing') + # Generate barcodes - will generate them in PNG. + GenerateUpcaBarcodes(number_of_barcodes, options.barcode_width, + options.barcode_height, + output_directory=options.png_barcodes_output_dir, + path_to_zxing=zxing_dir) + # Convert the PNG barcodes to to YUV format. + ConvertPngToYuvBarcodes(options.png_barcodes_input_dir, + options.yuv_barcodes_output_dir) + # Combine the YUV barcodes into one YUV file. + CombineYuvFramesIntoOneFile(options.barcodes_yuv, + input_directory=options.yuv_frames_input_dir) + # Overlay the barcodes over the base file. + OverlayYuvFiles(options.barcode_width, options.barcode_height, + options.base_frame_width, options.base_frame_height, + options.barcodes_yuv, options.base_yuv, options.output_yuv) + + if not keep_barcodes_yuv_file: + # Remove the temporary barcodes YUV file + os.remove(options.barcodes_yuv) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/rtc_tools/barcode_tools/build_zxing.py b/rtc_tools/barcode_tools/build_zxing.py new file mode 100755 index 0000000000..92696a78c2 --- /dev/null +++ b/rtc_tools/barcode_tools/build_zxing.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# Copyright (c) 2012 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 os +import subprocess +import sys + + +def RunAntBuildCommand(path_to_ant_build_file): + """Tries to build the passed build file with ant.""" + ant_executable = 'ant' + if sys.platform == 'win32': + if os.getenv('ANT_HOME'): + ant_executable = os.path.join(os.getenv('ANT_HOME'), 'bin', 'ant.bat') + else: + ant_executable = 'ant.bat' + cmd = [ant_executable, '-buildfile', path_to_ant_build_file] + try: + process = subprocess.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr) + process.wait() + if process.returncode != 0: + print >> sys.stderr, 'Failed to execute: %s' % ' '.join(cmd) + return process.returncode + except subprocess.CalledProcessError as e: + print >> sys.stderr, 'Failed to execute: %s.\nCause: %s' % (' '.join(cmd), + e) + return -1 + +def main(): + core_build = os.path.join('third_party', 'zxing', 'core', 'build.xml') + RunAntBuildCommand(core_build) + + javase_build = os.path.join('third_party', 'zxing', 'javase', 'build.xml') + return RunAntBuildCommand(javase_build) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/rtc_tools/barcode_tools/helper_functions.py b/rtc_tools/barcode_tools/helper_functions.py new file mode 100644 index 0000000000..e27322f41f --- /dev/null +++ b/rtc_tools/barcode_tools/helper_functions.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# Copyright (c) 2012 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 multiprocessing +import os +import subprocess +import sys + +_DEFAULT_PADDING = 4 + + +class HelperError(Exception): + """Exception raised for errors in the helper.""" + pass + + +def ZeroPad(number, padding=_DEFAULT_PADDING): + """Converts an int into a zero padded string. + + Args: + number(int): The number to convert. + padding(int): The number of chars in the output. Note that if you pass for + example number=23456 and padding=4, the output will still be '23456', + i.e. it will not be cropped. If you pass number=2 and padding=4, the + return value will be '0002'. + Return: + (string): The zero padded number converted to string. + """ + return str(number).zfill(padding) + + +def RunShellCommand(cmd_list, fail_msg=None): + """Executes a command. + + Args: + cmd_list(list): Command list to execute. + fail_msg(string): Message describing the error in case the command fails. + + Return: + (string): The standard output from running the command. + + Raise: + HelperError: If command fails. + """ + process = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + output, error = process.communicate() + if process.returncode != 0: + if fail_msg: + print >> sys.stderr, fail_msg + raise HelperError('Failed to run %s: command returned %d and printed ' + '%s and %s' % (' '.join(cmd_list), process.returncode, + output, error)) + return output.strip() + + +def PerformActionOnAllFiles(directory, file_pattern, file_extension, + start_number, action, **kwargs): + """Function that performs a given action on all files matching a pattern. + + It is assumed that the files are named file_patternxxxx.file_extension, where + xxxx are digits starting from start_number. + + Args: + directory(string): The directory where the files live. + file_pattern(string): The name pattern of the files. + file_extension(string): The files' extension. + start_number(int): From where to start to count frames. + action(function): The action to be performed over the files. Must return + False if the action failed, True otherwise. It should take a file name + as the first argument and **kwargs as arguments. The function must be + possible to pickle, so it cannot be a bound function (for instance). + + Return: + (bool): Whether performing the action over all files was successful or not. + """ + file_prefix = os.path.join(directory, file_pattern) + file_number = start_number + + process_pool = multiprocessing.Pool(processes=multiprocessing.cpu_count()) + results = [] + while True: + zero_padded_file_number = ZeroPad(file_number) + file_name = file_prefix + zero_padded_file_number + '.' + file_extension + if not os.path.isfile(file_name): + break + future = process_pool.apply_async(action, args=(file_name,), kwds=kwargs) + results.append(future) + file_number += 1 + + successful = True + for result in results: + if not result.get(): + print "At least one action %s failed for files %sxxxx.%s." % ( + action, file_pattern, file_extension) + successful = False + + process_pool.close() + return successful diff --git a/rtc_tools/barcode_tools/yuv_cropper.py b/rtc_tools/barcode_tools/yuv_cropper.py new file mode 100755 index 0000000000..609f07dc9c --- /dev/null +++ b/rtc_tools/barcode_tools/yuv_cropper.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +# Copyright (c) 2012 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 sys + + +def _CropOneFrame(yuv_file, output_file, component_sizes): + """Crops one frame. + + This function crops one frame going through all the YUV planes and cropping + respective amount of rows. + + Args: + yuv_file(file): The opened (for binary reading) YUV file. + output_file(file): The opened (for binary writing) file. + component_sizes(list of 3 3-ples): The list contains the sizes for all the + planes (Y, U, V) of the YUV file plus the crop_height scaled for every + plane. The sizes equal width, height and crop_height for the Y plane, + and are equal to width/2, height/2 and crop_height/2 for the U and V + planes. + Return: + (bool): True if there are more frames to crop, False otherwise. + """ + for comp_width, comp_height, comp_crop_height in component_sizes: + for row in range(comp_height): + # Read the plane data for this row. + yuv_plane = yuv_file.read(comp_width) + + # If the plane is empty, we have reached the end of the file. + if yuv_plane == "": + return False + + # Only write the plane data for the rows bigger than crop_height. + if row >= comp_crop_height: + output_file.write(yuv_plane) + return True + + +def CropFrames(yuv_file_name, output_file_name, width, height, crop_height): + """Crops rows of pixels from the top of the YUV frames. + + This function goes through all the frames in a video and crops the crop_height + top pixel rows of every frame. + + Args: + yuv_file_name(string): The name of the YUV file to be cropped. + output_file_name(string): The name of the output file where the result will + be written. + width(int): The width of the original YUV file. + height(int): The height of the original YUV file. + crop_height(int): The height (the number of pixel rows) to be cropped from + the frames. + """ + # Component sizes = [Y_sizes, U_sizes, V_sizes]. + component_sizes = [(width, height, crop_height), + (width/2, height/2, crop_height/2), + (width/2, height/2, crop_height/2)] + + yuv_file = open(yuv_file_name, 'rb') + output_file = open(output_file_name, 'wb') + + data_left = True + while data_left: + data_left = _CropOneFrame(yuv_file, output_file, component_sizes) + + yuv_file.close() + output_file.close() + + +def _ParseArgs(): + """Registers the command-line options.""" + usage = "usage: %prog [options]" + parser = optparse.OptionParser(usage=usage) + + parser.add_option('--width', type='int', + default=352, + help=('Width of the YUV file\'s frames. ' + 'Default: %default')) + parser.add_option('--height', type='int', default=288, + help=('Height of the YUV file\'s frames. ' + 'Default: %default')) + parser.add_option('--crop_height', type='int', default=32, + help=('How much of the top of the YUV file to crop. ' + 'Has to be module of 2. Default: %default')) + parser.add_option('--yuv_file', type='string', + help=('The YUV file to be cropped.')) + parser.add_option('--output_file', type='string', default='output.yuv', + help=('The output YUV file containing the cropped YUV. ' + 'Default: %default')) + options = parser.parse_args()[0] + if not options.yuv_file: + parser.error('yuv_file argument missing. Please specify input YUV file!') + return options + + +def main(): + """A tool to crop rows of pixels from the top part of a YUV file. + + A simple invocation will be: + ./yuv_cropper.py --width=640 --height=480 --crop_height=32 + --yuv_file= + --output_yuv= + """ + options = _ParseArgs() + + if os.path.getsize(options.yuv_file) == 0: + sys.stderr.write('Error: The YUV file you have passed has size 0. The ' + 'produced output will also have size 0.\n') + return -1 + + CropFrames(options.yuv_file, options.output_file, options.width, + options.height, options.crop_height) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/rtc_tools/compare_videos.py b/rtc_tools/compare_videos.py index 4748e81f78..40a2aabb32 100755 --- a/rtc_tools/compare_videos.py +++ b/rtc_tools/compare_videos.py @@ -9,8 +9,10 @@ import optparse import os +import shutil import subprocess import sys +import tempfile SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -35,25 +37,40 @@ def _ParseArgs(): parser.add_option('--frame_analyzer', type='string', help='Path to the frame analyzer executable.') parser.add_option('--barcode_decoder', type='string', - help=('DEPRECATED')) + help=('Path to the barcode decoder script. By default, we ' + 'will assume we can find it in barcode_tools/' + 'relative to this directory.')) parser.add_option('--ffmpeg_path', type='string', - help=('DEPRECATED')) + help=('The path to where the ffmpeg executable is located. ' + 'If omitted, it will be assumed to be present in the ' + 'PATH with the name ffmpeg[.exe].')) parser.add_option('--zxing_path', type='string', - help=('DEPRECATED')) + help=('The path to where the zxing executable is located. ' + 'If omitted, it will be assumed to be present in the ' + 'PATH with the name zxing[.exe].')) parser.add_option('--stats_file_ref', type='string', default='stats_ref.txt', - help=('DEPRECATED')) + help=('Path to the temporary stats file to be created and ' + 'used for the reference video file. ' + 'Default: %default')) parser.add_option('--stats_file_test', type='string', - help=('DEPRECATED')) + default='stats_test.txt', + help=('Path to the temporary stats file to be created and ' + 'used for the test video file. Default: %default')) parser.add_option('--stats_file', type='string', help=('DEPRECATED')) parser.add_option('--yuv_frame_width', type='int', default=640, - help=('DEPRECATED')) + help='Width of the YUV file\'s frames. Default: %default') parser.add_option('--yuv_frame_height', type='int', default=480, - help=('DEPRECATED')) + help='Height of the YUV file\'s frames. Default: %default') parser.add_option('--chartjson_result_file', type='str', default=None, help='Where to store perf results in chartjson format.') options, _ = parser.parse_args() + if options.stats_file: + options.stats_file_test = options.stats_file + print ('WARNING: Using deprecated switch --stats_file. ' + 'The new flag is --stats_file_test.') + if not options.ref_video: parser.error('You must provide a path to the reference video!') if not os.path.exists(options.ref_video): @@ -78,23 +95,73 @@ def _DevNull(): """ return open(os.devnull, 'r') +def DecodeBarcodesInVideo(options, path_to_decoder, video, stat_file): + # Run barcode decoder on the test video to identify frame numbers. + png_working_directory = tempfile.mkdtemp() + cmd = [ + sys.executable, + path_to_decoder, + '--yuv_file=%s' % video, + '--yuv_frame_width=%d' % options.yuv_frame_width, + '--yuv_frame_height=%d' % options.yuv_frame_height, + '--stats_file=%s' % stat_file, + '--png_working_dir=%s' % png_working_directory, + ] + if options.zxing_path: + cmd.append('--zxing_path=%s' % options.zxing_path) + if options.ffmpeg_path: + cmd.append('--ffmpeg_path=%s' % options.ffmpeg_path) + + + barcode_decoder = subprocess.Popen(cmd, stdin=_DevNull(), + stdout=sys.stdout, stderr=sys.stderr) + barcode_decoder.wait() + + shutil.rmtree(png_working_directory) + if barcode_decoder.returncode != 0: + print 'Failed to run barcode decoder script.' + return 1 + return 0 + def main(): """The main function. A simple invocation is: - ./webrtc/rtc_tools/compare_videos.py + ./webrtc/rtc_tools/barcode_tools/compare_videos.py --ref_video= --test_video= --frame_analyzer= + + Notice that the prerequisites for barcode_decoder.py also applies to this + script. The means the following executables have to be available in the PATH: + * zxing + * ffmpeg """ options = _ParseArgs() + if options.barcode_decoder: + path_to_decoder = options.barcode_decoder + else: + path_to_decoder = os.path.join(SCRIPT_DIR, 'barcode_tools', + 'barcode_decoder.py') + + if DecodeBarcodesInVideo(options, path_to_decoder, + options.ref_video, options.stats_file_ref) != 0: + return 1 + if DecodeBarcodesInVideo(options, path_to_decoder, + options.test_video, options.stats_file_test) != 0: + return 1 + # Run frame analyzer to compare the videos and print output. cmd = [ options.frame_analyzer, '--label=%s' % options.label, '--reference_file=%s' % options.ref_video, '--test_file=%s' % options.test_video, + '--stats_file_ref=%s' % options.stats_file_ref, + '--stats_file_test=%s' % options.stats_file_test, + '--width=%d' % options.yuv_frame_width, + '--height=%d' % options.yuv_frame_height, ] if options.chartjson_result_file: cmd.append('--chartjson_result_file=%s' % options.chartjson_result_file) diff --git a/rtc_tools/frame_analyzer/frame_analyzer.cc b/rtc_tools/frame_analyzer/frame_analyzer.cc index 443a8681a7..f5a9c2b827 100644 --- a/rtc_tools/frame_analyzer/frame_analyzer.cc +++ b/rtc_tools/frame_analyzer/frame_analyzer.cc @@ -16,7 +16,6 @@ #include #include "rtc_tools/frame_analyzer/video_quality_analysis.h" -#include "rtc_tools/frame_analyzer/video_temporal_aligner.h" #include "rtc_tools/simple_command_line_parser.h" #include "rtc_tools/y4m_file_reader.h" #include "test/testsupport/perf_test.h" @@ -25,7 +24,12 @@ * A command line tool running PSNR and SSIM on a reference video and a test * video. The test video is a record of the reference video which can start at * an arbitrary point. It is possible that there will be repeated frames or - * skipped frames as well. The video files should be 1420 Y4M videos. + * skipped frames as well. In order to have a way to compare corresponding + * frames from the two videos, two stats files should be provided. One for the + * reference video and one for the test video. The stats file + * is a text file assumed to be in the format: + * frame_xxxx yyyy where xxxx is the frame number in and yyyy is the + * corresponding barcode. The video files should be 1420 YUV videos. * The tool prints the result to standard output in the Chromium perf format: * RESULT :