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:
@ -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.
|
34
webrtc/tools/barcode_tools/README
Normal file
34
webrtc/tools/barcode_tools/README
Normal 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.
|
@ -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__':
|
@ -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]"
|
@ -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():
|
@ -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
|
||||
|
||||
|
Reference in New Issue
Block a user