First version of PythonCharts.

The reason why it is so simple is that I wanted to get something into the project that people can use to compare different test runs easily. More functionality will come later.

tools/python_charts/src/gviz_api.py is a copy of the Google visualization Python API available from http://google-visualization-python.googlecode.com/svn/trunk/

Review URL: http://webrtc-codereview.appspot.com/257003

git-svn-id: http://webrtc.googlecode.com/svn/trunk@893 4adac7df-926f-26a2-2b94-8c16560cd09d
This commit is contained in:
kjellander@webrtc.org
2011-11-07 15:25:47 +00:00
parent b353d21560
commit 689cb300b5
10 changed files with 1649 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
#!/usr/bin/env python
# Copyright (c) 2011 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.

View File

@@ -0,0 +1,134 @@
#!/usr/bin/env python
# Copyright (c) 2011 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.
__author__ = 'kjellander@webrtc.org (Henrik Kjellander)'
class DataHelper(object):
"""
Helper class for managing table data.
This class does not verify the consistency of the data tables sent into it.
"""
def __init__(self, data_list, table_description, names_list, messages):
""" Initializes the DataHelper with data.
Args:
data_list: List of one or more data lists in the format that the
Google Visualization Python API expects (list of dictionaries, one
per row of data). See the gviz_api.DataTable documentation for more
info.
table_description: dictionary describing the data types of all
columns in the data lists, as defined in the gviz_api.DataTable
documentation.
names_list: List of strings of what we're going to name the data
columns after. Usually different runs of data collection.
messages: List of strings we might append error messages to.
"""
self.data_list = data_list
self.table_description = table_description
self.names_list = names_list
self.messages = messages
self.number_of_datasets = len(data_list)
self.number_of_frames = len(data_list[0])
def CreateData(self, field_name, start_frame=0, end_frame=0):
""" Creates a data structure for a specified data field.
Creates a data structure (data type description dictionary and a list
of data dictionaries) to be used with the Google Visualization Python
API. The frame_number column is always present and one column per data
set is added and its field name is suffixed by _N where N is the number
of the data set (0, 1, 2...)
Args:
field_name: String name of the field, must be present in the data
structure this DataHelper was created with.
start_frame: Frame number to start at (zero indexed). Default: 0.
end_frame: Frame number to be the last frame. If zero all frames
will be included. Default: 0.
Returns:
A tuple containing:
- a dictionary describing the columns in the data result_data_table below.
This description uses the name for each data set specified by
names_list.
Example with two data sets named 'Foreman' and 'Crew':
{
'frame_number': ('number', 'Frame number'),
'ssim_0': ('number', 'Foreman'),
'ssim_1': ('number', 'Crew'),
}
- a list containing dictionaries (one per row) with the frame_number
column and one column of the specified field_name column per data
set.
Example with two data sets named 'Foreman' and 'Crew':
[
{'frame_number': 0, 'ssim_0': 0.98, 'ssim_1': 0.77 },
{'frame_number': 1, 'ssim_0': 0.81, 'ssim_1': 0.53 },
]
"""
# Build dictionary that describes the data types
result_table_description = {'frame_number': ('string', 'Frame number')}
for dataset_index in range(self.number_of_datasets):
column_name = '%s_%s' % (field_name, dataset_index)
column_type = self.table_description[field_name][0]
column_description = self.names_list[dataset_index]
result_table_description[column_name] = (column_type, column_description)
# Build data table of all the data
result_data_table = []
# We're going to have one dictionary per row.
# Create that and copy frame_number values from the first data set
for source_row in self.data_list[0]:
row_dict = { 'frame_number': source_row['frame_number'] }
result_data_table.append(row_dict)
# Pick target field data points from the all data tables
if end_frame == 0: # Default to all frames
end_frame = self.number_of_frames
for dataset_index in range(self.number_of_datasets):
for row_number in range(start_frame, end_frame):
column_name = '%s_%s' % (field_name, dataset_index)
# Stop if any of the data sets are missing the frame
try:
result_data_table[row_number][column_name] = \
self.data_list[dataset_index][row_number][field_name]
except IndexError:
self.messages.append("Couldn't find frame data for row %d "
"for %s" % (row_number, self.names_list[dataset_index]))
break
return (result_table_description, result_data_table)
def GetOrdering(self, table_description):
""" Creates a list of column names, ordered alphabetically except for the
frame_number column which always will be the first column.
Args:
table_description: A dictionary of column definitions as defined by the
gviz_api.DataTable documentation.
Returns:
A list of column names, where frame_number is the first and the
remaining columns are sorted alphabetically.
"""
# The JSON data representation generated from gviz_api.DataTable.ToJSon()
# must have frame_number as its first column in order for the chart to
# use it as it's X-axis value series.
# gviz_api.DataTable orders the columns by name by default, which will
# be incorrect if we have column names that are sorted before frame_number
# in our data table.
columns_ordering = ['frame_number']
# add all other columns:
for column in sorted(table_description.keys()):
if column != 'frame_number':
columns_ordering.append(column)
return columns_ordering

View File

@@ -0,0 +1,92 @@
#!/usr/bin/env python
# Copyright (c) 2011 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.
__author__ = 'kjellander@webrtc.org (Henrik Kjellander)'
import unittest
import webrtc.data_helper
class Test(unittest.TestCase):
def setUp(self):
# Simulate frame data from two different test runs, with 2 frames each.
self.frame_data_0 = [{'frame_number': 0, 'ssim': 0.5, 'psnr': 30.5},
{'frame_number': 1, 'ssim': 0.55, 'psnr': 30.55}]
self.frame_data_1 = [{'frame_number': 0, 'ssim': 0.6, 'psnr': 30.6},
{'frame_number': 0, 'ssim': 0.66, 'psnr': 30.66}]
self.all_data = [ self.frame_data_0, self.frame_data_1 ]
# Test with frame_number column in a non-first position sice we need to
# support reordering that to be able to use the gviz_api as we want.
self.type_description = {
'ssim': ('number', 'SSIM'),
'frame_number': ('number', 'Frame number'),
'psnr': ('number', 'PSRN'),
}
self.names = ["Test 0", "Test 1"]
def testCreateData(self):
messages = []
helper = webrtc.data_helper.DataHelper(self.all_data, self.type_description,
self.names, messages)
description, data_table = helper.CreateData('ssim')
self.assertEqual(3, len(description))
self.assertTrue('frame_number' in description)
self.assertTrue('ssim_0' in description)
self.assertTrue('number' in description['ssim_0'][0])
self.assertTrue('Test 0' in description['ssim_0'][1])
self.assertTrue('ssim_1' in description)
self.assertTrue('number' in description['ssim_1'][0])
self.assertTrue('Test 1' in description['ssim_1'][1])
self.assertEqual(0, len(messages))
self.assertEquals(2, len(data_table))
row = data_table[0]
self.assertEquals(0, row['frame_number'])
self.assertEquals(0.5, row['ssim_0'])
self.assertEquals(0.6, row['ssim_1'])
row = data_table[1]
self.assertEquals(1, row['frame_number'])
self.assertEquals(0.55, row['ssim_0'])
self.assertEquals(0.66, row['ssim_1'])
description, data_table = helper.CreateData('psnr')
self.assertEqual(3, len(description))
self.assertTrue('frame_number' in description)
self.assertTrue('psnr_0' in description)
self.assertTrue('psnr_1' in description)
self.assertEqual(0, len(messages))
self.assertEquals(2, len(data_table))
row = data_table[0]
self.assertEquals(0, row['frame_number'])
self.assertEquals(30.5, row['psnr_0'])
self.assertEquals(30.6, row['psnr_1'])
row = data_table[1]
self.assertEquals(1, row['frame_number'])
self.assertEquals(30.55, row['psnr_0'])
self.assertEquals(30.66, row['psnr_1'])
def testGetOrdering(self):
""" Tests that the ordering help method returns a list with frame_number
first and the rest sorted alphabetically """
messages = []
helper = webrtc.data_helper.DataHelper(self.all_data, self.type_description,
self.names, messages)
description, data_table = helper.CreateData('ssim')
columns = helper.GetOrdering(description)
self.assertEqual(3, len(columns))
self.assertEqual(0, len(messages))
self.assertEqual('frame_number', columns[0])
self.assertEqual('ssim_0', columns[1])
self.assertEqual('ssim_1', columns[2])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,139 @@
#!/usr/bin/env python
# Copyright (c) 2011 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.
__author__ = 'kjellander@webrtc.org (Henrik Kjellander)'
import os
import gviz_api
import webrtc.data_helper
def main():
"""
This Python script displays a web page with test created with the
video_quality_measurement program, which is a tool in WebRTC.
The script requires on two external files and one Python library:
- A HTML template file with layout and references to the json variables
defined in this script
- A data file in Python format, containing the following:
- test_configuration - a dictionary of test configuration names and values.
- frame_data_types - a dictionary that maps the different metrics to their
data types
- frame_data - a list of dictionaries where each dictionary maps a metric to
it's value.
- The gviz_api.py of the Google Visualization Python API, available at
http://code.google.com/p/google-visualization-python/
The HTML file is shipped with the script, while the data file must be
generated by running video_quality_measurement with the --python flag
specified.
"""
print 'Content-type: text/html\n' # the newline is required!
page_template_filename = '../templates/chart_page_template.html'
# The data files must be located in the project tree for app engine being
# able to access them.
data_filenames = [ '../data/vp8_sw.py', '../data/vp8_hw.py' ]
# Will contain info/error messages to be displayed on the resulting page.
messages = []
# Load the page HTML template.
try:
f = open(page_template_filename)
page_template = f.read()
f.close()
except IOError as e:
ShowErrorPage('Cannot open page template file: %s<br>Details: %s' %
(page_template_filename, e))
return
# Read data from external Python script files. First check that they exist.
for filename in data_filenames:
if not os.path.exists(filename):
messages.append('Cannot open data file: %s' % filename)
data_filenames.remove(filename)
# Read data from all existing input files.
data_list = []
test_configurations_list = []
names = []
for filename in data_filenames:
read_vars = {} # empty dictionary to load the data into.
execfile(filename, read_vars, read_vars)
test_configuration = read_vars['test_configuration']
table_description = read_vars['frame_data_types']
table_data = read_vars['frame_data']
# Verify the data in the file loaded properly.
if not table_description or not table_data:
messages.append('Invalid input file: %s. Missing description list or '
'data dictionary variables.', filename)
continue
# Frame numbers appear as number type in the data, but Chart API requires
# values of the X-axis to be of string type.
# Change the frame_number column data type:
table_description['frame_number'] = ('string', 'Frame number')
# Convert all the values to string types:
for row in table_data:
row['frame_number'] = str(row['frame_number'])
# Store the unique data from this file in the high level lists.
test_configurations_list.append(test_configuration)
data_list.append(table_data)
# Use the filenames for name; strip away directory path and extension.
names.append(filename[filename.rfind('/')+1:filename.rfind('.')])
# Create data helper and build data tables for each graph.
helper = webrtc.data_helper.DataHelper(data_list, table_description,
names, messages)
# Loading it into gviz_api.DataTable objects and create JSON strings.
description, data = helper.CreateData('ssim')
ssim = gviz_api.DataTable(description, data)
json_ssim_data = ssim.ToJSon(helper.GetOrdering(description))
description, data = helper.CreateData('psnr')
psnr = gviz_api.DataTable(description, data)
json_psnr_data = psnr.ToJSon(helper.GetOrdering(description))
description, data = helper.CreateData('packets_dropped')
packet_loss = gviz_api.DataTable(description, data)
json_packet_loss_data = packet_loss.ToJSon(helper.GetOrdering(description))
description, data = helper.CreateData('bit_rate')
# Add a column of data points for the desired bit rate to be plotted.
# (uses test configuration from the last data set, assuming it is the same
# for all of them)
desired_bit_rate = -1
for row in test_configuration:
if row['name'] == 'bit_rate_in_kbps':
desired_bit_rate = int(row['value'])
if desired_bit_rate == -1:
ShowErrorPage('Cannot find bit rate in the test configuration.')
return
# Add new column data type description.
description['desired_bit_rate'] = ('number', 'Desired bit rate (kbps)')
for row in data:
row['desired_bit_rate'] = desired_bit_rate
bit_rate = gviz_api.DataTable(description, data)
json_bit_rate_data = bit_rate.ToJSon(helper.GetOrdering(description))
# Format the messages list with newlines.
messages = '\n'.join(messages)
# Put the variables as JSon strings into the template.
print page_template % vars()
def ShowErrorPage(error_message):
print '<html><body>%s</body></html>' % error_message
if __name__ == '__main__':
main()