Refactor barcode decoder to use Zxing's C++ version

By using the C++ version of Zxing, we can avoid having Java and Ant
as a dependency when running Video quality analysis on the bots.
This makes it far more easy to setup automation on new machines.

I also moved the scripts into the webrtc/ folder so it will be synced by default when building in Chrome (eliminating the need of a separate solution).

This CL also removes the need of the FFMPEG_HOME variable and replaces
its use with a command line flag to make the tool run smoothly on
Windows.

BUG=none
TEST=locally running the script on Windows, Mac and Linux.

Review URL: https://webrtc-codereview.appspot.com/1099007

git-svn-id: http://webrtc.googlecode.com/svn/trunk@3640 4adac7df-926f-26a2-2b94-8c16560cd09d
This commit is contained in:
kjellander@webrtc.org
2013-03-08 10:58:21 +00:00
parent 755e19adfc
commit 38ebf98c2a
8 changed files with 126 additions and 140 deletions

View File

@ -1,17 +0,0 @@
This file explains how to set up the Zebra Crossing (Zxing) library in order to
use it in the barcode encoder and decoder tools. Zxing could be found at:
https://code.google.com/p/zxing/
After checkout, the relevant files from Zxing should be in third_party/zxing,
relative to this README.
In order to run barcode_encoder.py and barcode_decoder.py we need to have build
two jar files: zxing/core/core.jar and zxing/javase/javase.jar. In order to
build these we have to have Ant already installed. Building is as simple as
running the build_zxing.py script:
./build_zxing.py,
which should automatically call ant with the respective build files from the
Zxing checkout.
For more information on how to run barcode_encoder.py and barcode_decoder.py
check the documentation in the main functions inside these tools.

View File

@ -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/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.

View File

@ -13,11 +13,9 @@ import sys
import helper_functions
_DEFAULT_BARCODE_WIDTH = 352
def convert_yuv_to_png_files(yuv_file_name, yuv_frame_width, yuv_frame_height,
output_directory = '.'):
output_directory, ffmpeg_dir=None):
"""Converts a YUV video file into PNG frames.
The function uses ffmpeg to convert the YUV file. The output of ffmpeg is in
@ -29,25 +27,25 @@ def convert_yuv_to_png_files(yuv_file_name, yuv_frame_width, yuv_frame_height,
yuv_frame_height(int): The height of one YUV frame.
output_directory(string): The output directory where the PNG frames will be
stored.
ffmpeg_dir(string): The directory containing the ffmpeg executable. If
omitted, 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')
ffmpeg_executable = 'ffmpeg'
if sys.platform == 'win32':
if os.getenv('FFMPEG_HOME'):
ffmpeg_executable = os.path.join(os.getenv('FFMPEG_HOME'), 'bin',
'ffmpeg.exe')
else:
ffmpeg_executable = 'ffmpeg.exe'
ffmpeg_executable = 'ffmpeg.exe' if sys.platform == 'win32' else 'ffmpeg'
if ffmpeg_dir:
ffmpeg_executable = os.path.join(ffmpeg_dir, ffmpeg_executable)
command = [ffmpeg_executable, '-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.run_shell_command(
command, msg='Error during YUV to PNG conversion')
command, fail_msg='Error during YUV to PNG conversion')
except helper_functions.HelperError, err:
print >> sys.stderr, 'Error executing command: %s. Error: %s' % (command,
err)
@ -55,69 +53,55 @@ def convert_yuv_to_png_files(yuv_file_name, yuv_frame_width, yuv_frame_height,
return True
def decode_frames(barcode_width, barcode_height, input_directory='.',
path_to_zxing='zxing-read-only'):
def decode_frames(input_directory, zxing_dir=None):
"""Decodes the barcodes overlaid in each frame.
The function uses the example Java command-line tool from the Zxing
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 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:
barcode_width(int): Width of the barcode.
barcode_height(int): Height of the barcode.
input_directory(string): The input directory from where the PNG frames are
read.
path_to_zxing(string): The path to Zxing.
zxing_dir(string): The directory containing the zxing executable. If
omitted, the PATH will be searched for it.
Return:
(bool): True if the decoding went without errors.
"""
jars = helper_functions.form_jars_string(path_to_zxing)
command_line_decoder = 'com.google.zxing.client.j2se.CommandLineRunner'
zxing_executable = 'zxing.exe' if sys.platform == 'win32' else 'zxing'
if zxing_dir:
zxing_executable = os.path.join(zxing_dir, zxing_executable)
print 'Decoding barcodes from PNG files with %s...' % zxing_executable
return helper_functions.perform_action_on_all_files(
directory=input_directory, file_pattern='frame_',
file_extension='png', start_number=1, action=_decode_barcode_in_file,
barcode_width=barcode_width, barcode_height=barcode_height, jars=jars,
command_line_decoder=command_line_decoder)
command_line_decoder=zxing_executable)
def _decode_barcode_in_file(file_name, barcode_width, barcode_height, jars,
command_line_decoder):
def _decode_barcode_in_file(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.
barcode_width(int): Width of the barcode (in pixels).
barcode_height(int): Height of the barcode (in pixels)
jars(string): The Zxing core and javase string.
command_line_decoder(string): The ZXing command-line decoding tool.
Return:
(bool): True upon success, False otherwise.
"""
java_executable = 'java'
if sys.platform == 'win32':
if os.getenv('JAVA_HOME'):
java_executable = os.path.join(os.getenv('JAVA_HOME'), 'bin',
'java.exe')
else:
java_executable = 'java.exe'
command = [java_executable, '-Djava.awt.headless=true', '-cp', '%s' % jars,
'%s' % command_line_decoder, '--products_only',
'--dump_results', '--brief', '--crop=%d,%d,%d,%d' %
(0, 0, barcode_width, barcode_height),
'%s' % file_name]
command = [command_line_decoder, '--try-harder', '--dump-raw', file_name]
try:
out = helper_functions.run_shell_command(
command, msg='Error during decoding of %s' % file_name)
if not 'Success' in out:
print >> sys.stderr, 'Barcode in %s cannot be decoded\n' % file_name
return False
command, fail_msg='Error during decoding of %s' % file_name)
print 'Image %s : decoded barcode: %s' % (file_name, out)
text_file = open('%s.txt' % file_name[:-4], 'w')
text_file.write(out)
text_file.close()
except helper_functions.HelperError, err:
print >> sys.stderr, 'Barcode in %s cannot be decoded.' % file_name
print >> sys.stderr, err
return False
return True
@ -134,6 +118,7 @@ def _generate_stats_file(stats_file_name, input_directory='.'):
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, _count_frames_in(input_directory=input_directory) + 1):
frame_number = helper_functions.zero_pad(i)
barcode_file_name = file_prefix + frame_number + '.txt'
@ -236,30 +221,29 @@ def _parse_args():
usage = "usage: %prog [options]"
parser = optparse.OptionParser(usage=usage)
parser.add_option('--yuv_frame_width', type='int', default=352,
help=('Width of the YUV file\'s frames. '
'Default: %default'))
parser.add_option('--yuv_frame_height', type='int', default=288,
help=('Height of the YUV file\'s frames. '
'Default: %default'))
parser.add_option('--barcode_width', type='int',
default=_DEFAULT_BARCODE_WIDTH,
help=('Width of the barcodes. Default: %default'))
parser.add_option('--barcode_height', type='int', default=32,
help=('Height of the barcodes. Default: %default'))
parser.add_option('--zxing_dir', type='string',
help=('The path to the directory where the zxing executable'
'is located. If omitted, it will be assumed to be '
'present in the PATH.'))
parser.add_option('--ffmpeg_dir', type='string', default=None,
help=('The path to the directory where the ffmpeg '
'executable is located. If omitted, it will be '
'assumed to be present in the PATH.'))
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'))
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_output_dir', type='string', default='.',
help=('The output directory for the generated PNG files. '
'Default: %default'))
parser.add_option('--png_input_dir', type='string', default='.',
help=('The input directory for the generated PNG files. '
'Default: %default'))
parser.add_option('--path_to_zxing', type='string', default='zxing',
help=('The path to Zxing. Default: %default'))
options = parser.parse_args()[0]
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, _args = parser.parse_args()
return options
@ -269,41 +253,30 @@ def _main():
A simple invocation is:
./tools/barcode_tolls/barcode_decoder.py
--yuv_file=<path_and_name_of_overlaid_yuv_video>
--yuv_frame_width=352 --yuv_frame_height=288 --barcode_height=32
--yuv_frame_width=640 --yuv_frame_height=480
--stats_file=<path_and_name_to_stats_file>
NOTE: On Windows, if you don't have ffmpeg and Java in your PATH, you can
set the JAVA_HOME and FFMPEG_HOME environment variables to help the script
find the executables to use.
"""
options = _parse_args()
# 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.yuv_frame_width
script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
zxing_dir = os.path.join(script_dir, 'third_party', 'zxing')
# Convert the overlaid YUV video into a set of PNG frames.
if not convert_yuv_to_png_files(options.yuv_file, options.yuv_frame_width,
options.yuv_frame_height,
output_directory=options.png_output_dir):
output_directory=options.png_working_dir,
ffmpeg_dir=options.ffmpeg_dir):
print >> sys.stderr, 'An error occurred converting from YUV to PNG frames.'
return -1
# Decode the barcodes from the PNG frames.
if not decode_frames(options.barcode_width, options.barcode_height,
input_directory=options.png_input_dir,
path_to_zxing=zxing_dir):
if not decode_frames(input_directory=options.png_working_dir,
zxing_dir=options.zxing_dir):
print >> sys.stderr, ('An error occurred decoding barcodes from PNG frames.'
'Have you built the zxing library JAR files?')
' Have you built the zxing C++ executable?')
return -2
# Generate statistics file.
_generate_stats_file(options.stats_file,
input_directory=options.png_input_dir)
input_directory=options.png_working_dir)
print 'Completed barcode decoding.'
return 0
if __name__ == '__main__':

View File

@ -39,7 +39,7 @@ def generate_upca_barcodes(number_of_barcodes, barcode_width, barcode_height,
(bool): True if the conversion is successful.
"""
base_file_name = os.path.join(output_directory, "barcode_")
jars = helper_functions.form_jars_string(path_to_zxing)
jars = _form_jars_string(path_to_zxing)
command_line_encoder = 'com.google.zxing.client.j2se.CommandLineEncoder'
barcode_width = str(barcode_width)
barcode_height = str(barcode_height)
@ -57,8 +57,8 @@ def generate_upca_barcodes(number_of_barcodes, barcode_width, barcode_height,
"--output=%s" % (output_file_name), "%s" % (content)]
try:
helper_functions.run_shell_command(
command, msg=('Error during barcode %s generation' % content))
except helper_functions.HelperError, err:
command, fail_msg=('Error during barcode %s generation' % content))
except helper_functions.HelperError as err:
print >> sys.stderr, err
errors = True
return not errors
@ -107,10 +107,10 @@ def _convert_to_yuv_and_delete(output_directory, file_name, pattern):
'%s' % (yuv_file_name)]
try:
helper_functions.run_shell_command(
command, msg=('Error during PNG to YUV conversion of %s' %
command, fail_msg=('Error during PNG to YUV conversion of %s' %
file_name))
os.remove(file_name)
except helper_functions.HelperError, err:
except helper_functions.HelperError as err:
print >> sys.stderr, err
return False
return True
@ -153,8 +153,8 @@ def _add_to_file_and_delete(output_file, file_name):
input_file.close()
try:
os.remove(file_name)
except Exception:
sys.stderr.write('Error in deleting file %s' % file_name)
except OSError as e:
print >> sys.stderr, 'Error deleting file %s.\nError: %s' % (file_name, e)
return False
return True
@ -258,6 +258,21 @@ def calculate_frames_number_from_yuv(yuv_width, yuv_height, file_name):
return int(file_size/frame_size) # Should be int anyway
def _form_jars_string(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 _parse_args():
"""Registers the command-line options."""
usage = "usage: %prog [options]"

View File

@ -27,8 +27,9 @@ def run_ant_build_command(path_to_ant_build_file):
if process.returncode != 0:
print >> sys.stderr, 'Failed to execute: %s' % ' '.join(cmd)
return process.returncode
except Exception:
print >> sys.stderr, 'Failed to execute: %s' % ' '.join(cmd)
except subprocess.CalledProcessError as e:
print >> sys.stderr, 'Failed to execute: %s.\nCause: %s' % (' '.join(cmd),
e)
return -1
def _main():

View File

@ -34,12 +34,12 @@ def zero_pad(number, padding=_DEFAULT_PADDING):
return str(number).zfill(padding)
def run_shell_command(command, msg=None):
def run_shell_command(cmd_list, fail_msg=None):
"""Executes a command.
Args:
command(list): Command list to execute.
msg(string): Message describing the error in case the command fails.
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.
@ -47,36 +47,18 @@ def run_shell_command(command, msg=None):
Raise:
HelperError: If command fails.
"""
cmd_list = [str(x) for x in command]
cmd = ' '.join(cmd_list)
process = subprocess.Popen(cmd_list, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
output, error = process.communicate()
if process.returncode != 0:
if msg:
print >> sys.stderr, msg
if fail_msg:
print >> sys.stderr, fail_msg
raise HelperError('Failed to run %s: command returned %d and printed '
'%s and %s' % (cmd, process.returncode, output, error))
'%s and %s' % (' '.join(cmd_list), process.returncode,
output, error))
return output.strip()
def form_jars_string(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 perform_action_on_all_files(directory, file_pattern, file_extension,
start_number, action, **kwargs):
"""Function that performs a given action on all files matching a pattern.
@ -112,5 +94,3 @@ def perform_action_on_all_files(directory, file_pattern, file_extension,
else:
file_exists = False
return not errors