1#!/usr/bin/env python
2# Copyright (c) 2012 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
10import optparse
11import os
12import sys
13
14if __name__ == '__main__':
15  # Make sure we always can import helper_functions.
16  sys.path.append(os.path.dirname(__file__))
17
18import helper_functions
19
20# Chrome browsertests will throw away stderr; avoid that output gets lost.
21sys.stderr = sys.stdout
22
23
24def convert_yuv_to_png_files(yuv_file_name, yuv_frame_width, yuv_frame_height,
25                             output_directory, ffmpeg_path):
26  """Converts a YUV video file into PNG frames.
27
28  The function uses ffmpeg to convert the YUV file. The output of ffmpeg is in
29  the form frame_xxxx.png, where xxxx is the frame number, starting from 0001.
30
31  Args:
32    yuv_file_name(string): The name of the YUV file.
33    yuv_frame_width(int): The width of one YUV frame.
34    yuv_frame_height(int): The height of one YUV frame.
35    output_directory(string): The output directory where the PNG frames will be
36      stored.
37    ffmpeg_path(string): The path to the ffmpeg executable. If None, the PATH
38      will be searched for it.
39
40  Return:
41    (bool): True if the conversion was OK.
42  """
43  size_string = str(yuv_frame_width) + 'x' + str(yuv_frame_height)
44  output_files_pattern = os.path.join(output_directory, 'frame_%04d.png')
45  if not ffmpeg_path:
46    ffmpeg_path = 'ffmpeg.exe' if sys.platform == 'win32' else 'ffmpeg'
47  command = [ffmpeg_path, '-s', '%s' % size_string, '-i', '%s'
48             % yuv_file_name, '-f', 'image2', '-vcodec', 'png',
49             '%s' % output_files_pattern]
50  try:
51    print 'Converting YUV file to PNG images (may take a while)...'
52    print ' '.join(command)
53    helper_functions.run_shell_command(
54        command, fail_msg='Error during YUV to PNG conversion')
55  except helper_functions.HelperError, err:
56    print 'Error executing command: %s. Error: %s' % (command, err)
57    return False
58  except OSError:
59    print 'Did not find %s. Have you installed it?' % ffmpeg_path
60    return False
61  return True
62
63
64def decode_frames(input_directory, zxing_path):
65  """Decodes the barcodes overlaid in each frame.
66
67  The function uses the Zxing command-line tool from the Zxing C++ distribution
68  to decode the barcode in every PNG frame from the input directory. The frames
69  should be named frame_xxxx.png, where xxxx is the frame number. The frame
70  numbers should be consecutive and should start from 0001.
71  The decoding results in a frame_xxxx.txt file for every successfully decoded
72  barcode. This file contains the decoded barcode as 12-digit string (UPC-A
73  format: 11 digits content + one check digit).
74
75  Args:
76    input_directory(string): The input directory from where the PNG frames are
77      read.
78    zxing_path(string): The path to the zxing binary. If specified as None,
79      the PATH will be searched for it.
80  Return:
81    (bool): True if the decoding succeeded.
82  """
83  if not zxing_path:
84    zxing_path = 'zxing.exe' if sys.platform == 'win32' else 'zxing'
85  print 'Decoding barcodes from PNG files with %s...' % zxing_path
86  return helper_functions.perform_action_on_all_files(
87      directory=input_directory, file_pattern='frame_',
88      file_extension='png', start_number=1, action=_decode_barcode_in_file,
89      command_line_decoder=zxing_path)
90
91
92def _decode_barcode_in_file(file_name, command_line_decoder):
93  """Decodes the barcode in the upper left corner of a PNG file.
94
95  Args:
96    file_name(string): File name of the PNG file.
97    command_line_decoder(string): The ZXing command-line decoding tool.
98
99  Return:
100    (bool): True upon success, False otherwise.
101  """
102  command = [command_line_decoder, '--try-harder', '--dump-raw', file_name]
103  try:
104    out = helper_functions.run_shell_command(
105        command, fail_msg='Error during decoding of %s' % file_name)
106    text_file = open('%s.txt' % file_name[:-4], 'w')
107    text_file.write(out)
108    text_file.close()
109  except helper_functions.HelperError, err:
110    print 'Barcode in %s cannot be decoded.' % file_name
111    print err
112    return False
113  except OSError:
114    print 'Did not find %s. Have you installed it?' % command_line_decoder
115    return False
116  return True
117
118
119def _generate_stats_file(stats_file_name, input_directory='.'):
120  """Generate statistics file.
121
122  The function generates a statistics file. The contents of the file are in the
123  format <frame_name> <barcode>, where frame name is the name of every frame
124  (effectively the frame number) and barcode is the decoded barcode. The frames
125  and the helper .txt files are removed after they have been used.
126  """
127  file_prefix = os.path.join(input_directory, 'frame_')
128  stats_file = open(stats_file_name, 'w')
129
130  print 'Generating stats file: %s' % stats_file_name
131  for i in range(1, _count_frames_in(input_directory=input_directory) + 1):
132    frame_number = helper_functions.zero_pad(i)
133    barcode_file_name = file_prefix + frame_number + '.txt'
134    png_frame = file_prefix + frame_number + '.png'
135    entry_frame_number = helper_functions.zero_pad(i-1)
136    entry = 'frame_' + entry_frame_number + ' '
137
138    if os.path.isfile(barcode_file_name):
139      barcode = _read_barcode_from_text_file(barcode_file_name)
140      os.remove(barcode_file_name)
141
142      if _check_barcode(barcode):
143        entry += (helper_functions.zero_pad(int(barcode[0:11])) + '\n')
144      else:
145        entry += 'Barcode error\n'  # Barcode is wrongly detected.
146    else:  # Barcode file doesn't exist.
147      entry += 'Barcode error\n'
148
149    stats_file.write(entry)
150    os.remove(png_frame)
151
152  stats_file.close()
153
154
155def _read_barcode_from_text_file(barcode_file_name):
156  """Reads the decoded barcode for a .txt file.
157
158  Args:
159    barcode_file_name(string): The name of the .txt file.
160  Return:
161    (string): The decoded barcode.
162  """
163  barcode_file = open(barcode_file_name, 'r')
164  barcode = barcode_file.read()
165  barcode_file.close()
166  return barcode
167
168
169def _check_barcode(barcode):
170  """Check weather the UPC-A barcode was decoded correctly.
171
172  This function calculates the check digit of the provided barcode and compares
173  it to the check digit that was decoded.
174
175  Args:
176    barcode(string): The barcode (12-digit).
177  Return:
178    (bool): True if the barcode was decoded correctly.
179  """
180  if len(barcode) != 12:
181    return False
182
183  r1 = range(0, 11, 2)  # Odd digits
184  r2 = range(1, 10, 2)  # Even digits except last
185  dsum = 0
186  # Sum all the even digits
187  for i in r1:
188    dsum += int(barcode[i])
189  # Multiply the sum by 3
190  dsum *= 3
191  # Add all the even digits except the check digit (12th digit)
192  for i in r2:
193    dsum += int(barcode[i])
194  # Get the modulo 10
195  dsum = dsum % 10
196  # If not 0 substract from 10
197  if dsum != 0:
198    dsum = 10 - dsum
199  # Compare result and check digit
200  return dsum == int(barcode[11])
201
202
203def _count_frames_in(input_directory='.'):
204  """Calculates the number of frames in the input directory.
205
206  The function calculates the number of frames in the input directory. The
207  frames should be named frame_xxxx.png, where xxxx is the number of the frame.
208  The numbers should start from 1 and should be consecutive.
209
210  Args:
211    input_directory(string): The input directory.
212  Return:
213    (int): The number of frames.
214  """
215  file_prefix = os.path.join(input_directory, 'frame_')
216  file_exists = True
217  num = 1
218
219  while file_exists:
220    file_name = (file_prefix + helper_functions.zero_pad(num) + '.png')
221    if os.path.isfile(file_name):
222      num += 1
223    else:
224      file_exists = False
225  return num - 1
226
227
228def _parse_args():
229  """Registers the command-line options."""
230  usage = "usage: %prog [options]"
231  parser = optparse.OptionParser(usage=usage)
232
233  parser.add_option('--zxing_path', type='string',
234                    help=('The path to where the zxing executable is located. '
235                          'If omitted, it will be assumed to be present in the '
236                          'PATH with the name zxing[.exe].'))
237  parser.add_option('--ffmpeg_path', type='string',
238                    help=('The path to where the ffmpeg executable is located. '
239                          'If omitted, it will be assumed to be present in the '
240                          'PATH with the name ffmpeg[.exe].'))
241  parser.add_option('--yuv_frame_width', type='int', default=640,
242                    help='Width of the YUV file\'s frames. Default: %default')
243  parser.add_option('--yuv_frame_height', type='int', default=480,
244                    help='Height of the YUV file\'s frames. Default: %default')
245  parser.add_option('--yuv_file', type='string', default='output.yuv',
246                    help='The YUV file to be decoded. Default: %default')
247  parser.add_option('--stats_file', type='string', default='stats.txt',
248                    help='The output stats file. Default: %default')
249  parser.add_option('--png_working_dir', type='string', default='.',
250                    help=('The directory for temporary PNG images to be stored '
251                          'in when decoding from YUV before they\'re barcode '
252                          'decoded. If using Windows and a Cygwin-compiled '
253                          'zxing.exe, you should keep the default value to '
254                          'avoid problems. Default: %default'))
255  options, _ = parser.parse_args()
256  return options
257
258
259def _main():
260  """The main function.
261
262  A simple invocation is:
263  ./webrtc/tools/barcode_tools/barcode_decoder.py
264  --yuv_file=<path_and_name_of_overlaid_yuv_video>
265  --yuv_frame_width=640 --yuv_frame_height=480
266  --stats_file=<path_and_name_to_stats_file>
267  """
268  options = _parse_args()
269
270  # Convert the overlaid YUV video into a set of PNG frames.
271  if not convert_yuv_to_png_files(options.yuv_file, options.yuv_frame_width,
272                                  options.yuv_frame_height,
273                                  output_directory=options.png_working_dir,
274                                  ffmpeg_path=options.ffmpeg_path):
275    print 'An error occurred converting from YUV to PNG frames.'
276    return -1
277
278  # Decode the barcodes from the PNG frames.
279  if not decode_frames(input_directory=options.png_working_dir,
280                       zxing_path=options.zxing_path):
281    print 'An error occurred decoding barcodes from PNG frames.'
282    return -2
283
284  # Generate statistics file.
285  _generate_stats_file(options.stats_file,
286                       input_directory=options.png_working_dir)
287  print 'Completed barcode decoding.'
288  return 0
289
290if __name__ == '__main__':
291  sys.exit(_main())
292