Add BDRate calculation with Python implementation.

A new flag in config file controls which implementation
want to be used: python or macro in excel file.

Change-Id: Iea1114003f0c876cce275d49dd0d1d22871b1a54
diff --git a/tools/convexhull_framework/src/CalcBDRate.py b/tools/convexhull_framework/src/CalcBDRate.py
new file mode 100644
index 0000000..a7557cb
--- /dev/null
+++ b/tools/convexhull_framework/src/CalcBDRate.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+## Copyright (c) 2019, Alliance for Open Media. All rights reserved
+##
+## This source code is subject to the terms of the BSD 2 Clause License and
+## the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License
+## was not distributed with this source code in the LICENSE file, you can
+## obtain it at www.aomedia.org/license/software. If the Alliance for Open
+## Media Patent License 1.0 was not distributed with this source code in the
+## PATENTS file, you can obtain it at www.aomedia.org/license/patent.
+##
+__author__ = "maggie.sun@intel.com, ryan.lei@intel.com"
+
+import numpy
+import math
+import scipy.interpolate
+import logging
+from Config import LoggerName
+
+subloggername = "CalcBDRate"
+loggername = LoggerName + '.' + '%s' % subloggername
+logger = logging.getLogger(loggername)
+
+# BJONTEGAARD    Bjontegaard metric
+# Calculation is adapted from Google implementation
+# PCHIP method - Piecewise Cubic Hermite Interpolating Polynomial interpolation
+def BD_RATE(br1, qtyMtrc1, br2, qtyMtrc2):
+    brqtypairs1 = [(br1[i], qtyMtrc1[i]) for i in range(min(len(qtyMtrc1), len(br1)))]
+    brqtypairs2 = [(br2[i], qtyMtrc2[i]) for i in range(min(len(qtyMtrc2), len(br2)))]
+
+    if not brqtypairs1 or not brqtypairs2:
+        logger.info("one of input lists is empty!")
+        return 0.0
+
+    brqtypairs1.sort(key=lambda tup: tup[1])
+    brqtypairs2.sort(key=lambda tup: tup[1])
+
+    logbr1 = [math.log(x[0]) for x in brqtypairs1]
+    qmetrics1 = [100.0 if x[1] == float('inf') else x[1] for x in brqtypairs1]
+    logbr2 = [math.log(x[0]) for x in brqtypairs2]
+    qmetrics2 = [100.0 if x[1] == float('inf') else x[1] for x in brqtypairs2]
+
+    min_int = max(min(qmetrics1), min(qmetrics2))
+    max_int = min(max(qmetrics1), max(qmetrics2))
+    if min_int >= max_int:
+        logger.info("no overlap from input 2 lists of quality metrics!")
+        return 0.0
+
+    lin = numpy.linspace(min_int, max_int, num=100, retstep=True)
+    interval = lin[1]
+    samples = lin[0]
+
+    v1 = scipy.interpolate.pchip_interpolate(qmetrics1, logbr1, samples)
+    v2 = scipy.interpolate.pchip_interpolate(qmetrics2, logbr2, samples)
+
+    # Calculate the integral using the trapezoid method on the samples.
+    int1 = numpy.trapz(v1, dx=interval)
+    int2 = numpy.trapz(v2, dx=interval)
+
+    # find avg diff
+    avg_exp_diff = (int2 - int1) / (max_int - min_int)
+    avg_diff = (math.exp(avg_exp_diff) - 1) * 100
+
+    return avg_diff
diff --git a/tools/convexhull_framework/src/Config.py b/tools/convexhull_framework/src/Config.py
index 5a6ebb0..027e596 100644
--- a/tools/convexhull_framework/src/Config.py
+++ b/tools/convexhull_framework/src/Config.py
@@ -115,6 +115,7 @@
                       'HDRTools']
 VMAF = os.path.join(BinPath, 'vmafossexec.exe')
 HDRTool = os.path.join(BinPath, 'HDRMetrics.exe')
+CalcBDRateInExcel = False
 
 ######################## config for exporting data to excel  #################
 #https://xlsxwriter.readthedocs.io/working_with_colors.html#colors
diff --git a/tools/convexhull_framework/src/ConvexHullTest.py b/tools/convexhull_framework/src/ConvexHullTest.py
index 327249b..27f65bc 100644
--- a/tools/convexhull_framework/src/ConvexHullTest.py
+++ b/tools/convexhull_framework/src/ConvexHullTest.py
@@ -356,7 +356,9 @@
 # main
 ######################################
 if __name__ == "__main__":
+    #sys.argv = ["", "-f", "encode", "-c", "hevc", "-m", "ffmpeg", "-p", "medium"]
     #sys.argv = ["","-f","convexhull","-c","hevc","-m","ffmpeg","-p","medium"]
+    #sys.argv = ["", "-f", "summary", "-c", "hevc", "-m", "ffmpeg", "-p", "medium"]
     ParseArguments(sys.argv)
 
     # preparation for executing functions
diff --git a/tools/convexhull_framework/src/PostAnalysis_Summary.py b/tools/convexhull_framework/src/PostAnalysis_Summary.py
index 1d711e9..5478a16 100644
--- a/tools/convexhull_framework/src/PostAnalysis_Summary.py
+++ b/tools/convexhull_framework/src/PostAnalysis_Summary.py
@@ -14,9 +14,10 @@
 import xlsxwriter
 import xlrd
 from Config import QPs, DnScaleRatio, QualityList, VbaBinFile, CvxH_WtRows,\
-    CvxH_WtLastCol, LoggerName
+    CvxH_WtLastCol, LoggerName, CalcBDRateInExcel, CvxH_WtCols
 from Utils import GetShortContentName, CalcRowsClassAndContentDict,\
     SweepScalingAlgosInOneResultFile
+from CalcBDRate import BD_RATE
 import logging
 
 subloggername = "PostAnalysisSummary"
@@ -30,8 +31,9 @@
 ################################################################################
 ### Helper Functions ###########################################################
 def GetSummaryFileName(encMethod, codecName, preset, path):
-    name = 'ConvexHullSummary_ScaleAlgosNum_%d_%s_%s_%s.xlsm'\
-           % (len(DnScaleRatio), encMethod, codecName, preset)
+    filetype = 'xlsm' if CalcBDRateInExcel else 'xlsx'
+    name = 'ConvexHullSummary_ScaleAlgosNum_%d_%s_%s_%s.%s'\
+           % (len(DnScaleRatio), encMethod, codecName, preset, filetype)
     return os.path.join(path, name)
 
 def GetConvexHullRDFileName(encMethod, codecName, preset, path):
@@ -77,7 +79,7 @@
                 logger.warning("not find convex hull result file for content:%s"
                                % content)
 
-def CalBDRate_OneSheet(sht,  cols, contentsdict, rows_class, cols_bdmtrs, cellformat):
+def CalBDRateWithExcel_OneSheet(sht, cols, contentsdict, rows_class, cols_bdmtrs, cellformat):
     row_refst = 0
     bdstep = 3
     for cols_bd, residx in zip(cols_bdmtrs, range(1, len(DnScaleRatio))):
@@ -97,16 +99,16 @@
                     refq_e = xlrd.cellnameabs(row_class + row_cont + row_refst
                                               + bdstep, cols[0] + 1 + y)
 
-                    testbr_b = xlrd.cellnameabs(row_class + row_cont,
+                    testbr_b = xlrd.cellnameabs(row_class + row_cont + row_refst,
                                                 cols[residx])
-                    testbr_e = xlrd.cellnameabs(row_class + row_cont + bdstep,
-                                                cols[residx])
-                    testq_b = xlrd.cellnameabs(row_class + row_cont,
+                    testbr_e = xlrd.cellnameabs(row_class + row_cont + row_refst
+                                             + bdstep, cols[residx])
+                    testq_b = xlrd.cellnameabs(row_class + row_cont + row_refst,
                                                cols[residx] + 1 + y)
-                    testq_e = xlrd.cellnameabs(row_class + row_cont + bdstep,
-                                               cols[residx] + 1 + y)
+                    testq_e = xlrd.cellnameabs(row_class + row_cont + row_refst
+                                              + bdstep, cols[residx] + 1 + y)
 
-                    #formula = '=-bdrate(%s:%s,%s:%s,%s:%s,%s:%s)' % (
+                    #formula = '=bdrate(%s:%s,%s:%s,%s:%s,%s:%s)' % (
                     #refbr_b, refbr_e, refq_b, refq_e, testbr_b, testbr_e,
                     # testq_b, testq_e)
                     formula = '=bdRateExtend(%s:%s,%s:%s,%s:%s,%s:%s)'\
@@ -115,6 +117,36 @@
                     sht.write_formula(row_class + row_cont, cols_bd + y, formula,
                                       cellformat)
 
+
+def CalBDRateWithPython_OneSheet(sht, contentsdict, rows_class, cols_bdmtrs, infile_path, cellformat):
+    row_refst = 0
+    bdstep = 3
+    assert row_refst + bdstep < len(CvxH_WtRows)
+    resultfiles = os.listdir(infile_path)
+    shtname = sht.get_name()
+    rdrows = CvxH_WtRows
+    rdcols = CvxH_WtCols
+    for cols_bd, residx in zip(cols_bdmtrs, range(1, len(DnScaleRatio))):
+        sht.write(0, cols_bd, 'BD-Rate %.2f vs. %.2f' % (DnScaleRatio[residx],
+                                                         DnScaleRatio[0]))
+        sht.write_row(1, cols_bd, QualityList)
+        for (cls, contents), row_class in zip(contentsdict.items(), rows_class):
+            rows_content = [i * len(QPs) for i in range(len(contents))]
+            for row_cont, content in zip(rows_content, contents):
+                key = GetShortContentName(content)
+                for resfile in resultfiles:
+                    if key in resfile:
+                        rdwb = xlrd.open_workbook(os.path.join(infile_path, resfile))
+                        rdsht = rdwb.sheet_by_name(shtname)
+                        break
+                for y in range(len(QualityList)):
+                    refbrs = rdsht.col_values(rdcols[0], rdrows[row_refst], rdrows[row_refst + bdstep] + 1)
+                    refqtys = rdsht.col_values(rdcols[0] + 1 + y, rdrows[row_refst], rdrows[row_refst + bdstep] + 1)
+                    testbrs = rdsht.col_values(rdcols[residx], rdrows[row_refst], rdrows[row_refst + bdstep] + 1)
+                    testqtys = rdsht.col_values(rdcols[residx] + 1 + y, rdrows[row_refst], rdrows[row_refst + bdstep] + 1)
+                    bdrate = BD_RATE(refbrs, refqtys, testbrs, testqtys) / 100.0
+                    sht.write(row_class + row_cont, cols_bd + y, bdrate, cellformat)
+
 def GenerateFormula_SumRows(shtname, rows, col):
     cells = ''
     for row in rows:
@@ -334,8 +366,8 @@
     contentsdict, rows_class = CalcRowsClassAndContentDict(rowstart,
                                                            content_path,
                                                            clips, len(QPs))
-
-    wb.add_vba_project(VbaBinFile)
+    if CalcBDRateInExcel:
+        wb.add_vba_project(VbaBinFile)
     cellformat = wb.add_format()
     cellformat.set_num_format('0.00%')
     #cols_bdmtrs is the column number to write the bdrate data
@@ -348,8 +380,12 @@
         CopyResultDataToSummaryFile_Onesheet(sht, sum_wtcols, contentsdict,
                                              rows_class, infile_path)
         # calculate bd rate in each scaling sheet
-        CalBDRate_OneSheet(sht, sum_wtcols, contentsdict, rows_class,
-                           cols_bdmtrs, cellformat)
+        if CalcBDRateInExcel:
+            CalBDRateWithExcel_OneSheet(sht, sum_wtcols, contentsdict, rows_class,
+                                        cols_bdmtrs, cellformat)
+        else:
+            CalBDRateWithPython_OneSheet(sht, contentsdict, rows_class, cols_bdmtrs,
+                                         infile_path, cellformat)
 
     # calculate average bitrate and quality metrics for each category and
     # write to "average" sheet