blob: 4eb049dcec716afaa4b0923325aff6f70076fef2 [file] [log] [blame]
#!/usr/bin/python
# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Analyzes test results in USB drive written by factory_CameraPerformanceAls in
Module-level and AB Covers camera testing.
This file must be able to run standalone without Chrome OS system. For example,
it should run with ActivePython on MS-Windows. The external library dependency
is kept as minimum as possible.
"""
import argparse
from collections import defaultdict, namedtuple, OrderedDict
import csv
import glob
import numpy as np
import os
import re
# default filename of exported CSV file
_DEFAULT_CSV_FILE = 'result.csv'
# field names in CSV file
_FIELDS = ['Serial', 'Result', 'Camera_Status',
'ShiftTilt_Status', 'LenShading_Status',
'MTF_Status', 'ALS_Status',
'MedianMTF', 'LowestMTF', 'Shift',
'Shift_X', 'Shift_Y', 'Tilt', 'LenShading']
# serial numbers to ignore
_SN_BLACKLIST = ['dummy_sn']
# Data structure for test pass criteria. Min_value and max_value are floats or
# None. At least one of min_value and max_value must be None.
Criteria = namedtuple('Criteria',
['display_name', 'min_value', 'max_value'])
# ordered dictionary of format field-name => criteria
_PASS_CRITERIA = OrderedDict([
('MedianMTF', Criteria('Median MTF', 0.240, None)),
('LowestMTF', Criteria('Lowest MTF', 0.145, None)),
('Shift', Criteria('Shift', None, 0.045)),
('Tilt', Criteria('Tilt', None, 1.0)),
('LenShading', Criteria('Lens Shading', 0.60, None))])
# Format to parsing the text file. List of (field-name, regexp-match-pattern).
_TEXT_FILE_FORMAT = [
('Result', r"'result': *'([^']+)'"),
('Camera_Status', r"'cam_stat': *'([^']+)'"),
('ShiftTilt_Status', r"'cam_vc': *'([^']+)'"),
('LenShading_Status', r"'cam_ls': *'([^']+)'"),
('MTF_Status', r"'cam_mtf': *'([^']+)'"),
('ALS_Status', r"'als_stat': *'([^']+)'"),
('MedianMTF', r'^MTF value:\s*(\S+)$'),
('LowestMTF', r'^Lowest MTF value:\s*(\S+)$'),
('Shift', r'^Image shift percentage:\s*(\S+)$'),
('Shift_X', r'^Image shift X:\s*(\S+)$'),
('Shift_Y', r'^Image shift Y:\s*(\S+)$'),
('Tilt', r'^Image tilt:\s*(\S+)\s+degrees'),
('LenShading', r'^Len shading ratio:\s*(\S+)$')]
def _Percent(a, b):
"""
Returns:
Floating percentage of a / b.
"""
return 100.0 * a / b
def _PrintStatistics(values, criteria):
"""Calculates statistics on a list of values accroding to given criteria.
Args:
values: a list of numeric values.
criteria: Criteria object.
Returns:
Floating percentage of a / b.
"""
display_name = criteria.display_name
min_value = criteria.min_value
max_value = criteria.max_value
total_count = len(values)
assert min_value == None or max_value == None
if min_value:
failed_count = len(filter((lambda x: abs(x) < min_value), values))
failed_condition = '< %.3f' % min_value
elif max_value:
failed_count = len(filter((lambda x: abs(x) > max_value), values))
failed_condition = '> %.3f' % max_value
else:
failed_condition = None
print(display_name + ':')
print(' Average: %.3f' % np.average(values))
print(' Median: %.3f' % np.median(values))
print(' Std deviation: %.3f' % np.std(values))
print(' Range: (%.3f - %.3f)' % (np.min(values), np.max(values)))
if failed_condition:
print(' %s: %d/%d (%.1f%%)' % (failed_condition, failed_count,
total_count,
_Percent(failed_count, total_count)))
def AnalyzeData(data_list):
"""Anaylzes data and print the summary.
Args:
data_list: A list of dictionaries. Each contains the results of one DUT.
Returns:
A list of dictionaries, where each contains the results of one DUT.
"""
numeric_pattern = re.compile(r'^[+\-]?[0-9.]+$')
data_count = len(data_list)
values = defaultdict(list)
for row in data_list:
for field in _FIELDS:
if row[field] == 'N/A':
# Skip it directly. Statistics like avg won't take it into account.
continue
if numeric_pattern.match(row[field]):
v = float(row[field])
else:
v = row[field]
values[field].append(v)
# Passed/ Failed
passed_count = values['Result'].count('PASSED')
failed_count = values['Result'].count('FAILED')
assert passed_count + failed_count == data_count
print("Passed: %d/%d (%.1f%%)" % (passed_count,
data_count,
_Percent(passed_count, data_count)))
print("Failed: %d/%d (%.1f%%)" % (failed_count,
data_count,
_Percent(failed_count, data_count)))
for field, criteria in _PASS_CRITERIA.iteritems():
_PrintStatistics(values[field], criteria)
def CollectDataAndExportCSV(data_path, csv_filename):
"""Read all .txt files in the current folder, and export its data to returned
data structure and csv_filename.
Args:
data_path: source data path (None if using the current working directory)
csv_filename: output filename of CSV file (None if no CSV file is needed)
Returns:
A list of dictionaries, where each contains the results of one DUT.
"""
data_dict = defaultdict(dict)
def _read_attr(lines, attr, pattern, fallback='N/A'):
matches = [re.search(pattern, l).group(1)
for l in lines if re.search(pattern, l)]
if not matches:
data_dict[sn][attr] = fallback
else:
data_dict[sn][attr] = matches[-1]
if data_path:
glob_pattern = os.path.join(data_path, '*.txt')
else:
glob_pattern = '*.txt'
for txt_file in glob.glob(glob_pattern):
sn = re.match(r'^(.*)\.txt$', txt_file).group(1)
data_dict[sn]['Serial'] = sn
with open(txt_file, 'r') as txt:
lines = txt.read().splitlines()
for field, pattern in _TEXT_FILE_FORMAT:
_read_attr(lines, field, pattern)
# Export data_dict to CSV file
fields_dict = OrderedDict([(f, f) for f in _FIELDS])
if csv_filename:
with open(csv_filename, 'w') as csvfile:
writer = csv.DictWriter(csvfile, fields_dict)
writer.writeheader()
for sn in sorted(data_dict.keys()):
writer.writerow(data_dict[sn])
return [data for sn, data in data_dict.iteritems()]
def main():
"""Main routine."""
prog_desc = """Analyze raw data collected from camera test fixtures.
For module-level and AB-covers camera testing, the test results for each DUT are
stored in a single text file ([SerialNumber].txt) on USB drive. This program can
find all text files under one directory and analyze the results. The results are
also written to a CSV file, which can be imported in spreadsheet software. """
parser = argparse.ArgumentParser(
description=prog_desc)
parser.add_argument('--data-path', '-d', dest='data_path',
help='source data path '
'(default: current working directory)')
parser.add_argument('--csv-file', '-f', dest='csv_filename',
default=_DEFAULT_CSV_FILE,
help='output filename of the CSV file '
'(default: %s)' % _DEFAULT_CSV_FILE)
parser.add_argument('--no-csv', action='store_false', dest='export_csv',
help='disable output of CSV file')
args = parser.parse_args()
data_list = CollectDataAndExportCSV(
args.data_path, args.csv_filename if args.export_csv else None)
AnalyzeData(data_list)
if __name__ == '__main__':
main()