OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. |
| 2 # |
| 3 # Use of this source code is governed by a BSD-style license |
| 4 # that can be found in the LICENSE file in the root of the source |
| 5 # tree. An additional intellectual property rights grant can be found |
| 6 # in the file PATENTS. All contributing project authors may |
| 7 # be found in the AUTHORS file in the root of the source tree. |
| 8 |
| 9 """Plots statistics from WebRTC integration test logs. |
| 10 |
| 11 Usage: $ python plot_webrtc_test_logs.py filename.txt |
| 12 """ |
| 13 |
| 14 import numpy |
| 15 import sys |
| 16 import re |
| 17 |
| 18 import matplotlib.pyplot as plt |
| 19 |
| 20 # Log events. |
| 21 EVENT_START = 'RUN ] CodecSettings/PlotVideoProcessorIntegrationTest.' |
| 22 EVENT_END = 'OK ] CodecSettings/PlotVideoProcessorIntegrationTest.' |
| 23 |
| 24 # Metrics to plot, tuple: (name to parse in file, label to use when plotting). |
| 25 BITRATE = ('Target Bitrate', 'bitrate (kbps)') |
| 26 WIDTH = ('Width', 'width') |
| 27 HEIGHT = ('Height', 'height') |
| 28 FILENAME = ('Filename', 'clip') |
| 29 CODEC_TYPE = ('Codec type', 'Codec') |
| 30 ENCODER_IMPLEMENTATION_NAME = ('Encoder implementation name', 'enc name') |
| 31 DECODER_IMPLEMENTATION_NAME = ('Decoder implementation name', 'dec name') |
| 32 NUM_FRAMES = ('Total # of frames', 'num frames') |
| 33 CORES = ('#CPU cores used', 'cores') |
| 34 DENOISING = ('Denoising', 'denoising') |
| 35 RESILIENCE = ('Resilience', 'resilience') |
| 36 ERROR_CONCEALMENT = ('Error concealment', 'error concealment') |
| 37 PSNR = ('PSNR avg', 'PSNR (dB)') |
| 38 SSIM = ('SSIM avg', 'SSIM') |
| 39 ENC_BITRATE = ('Encoding bitrate', 'encoded bitrate (kbps)') |
| 40 FRAMERATE = ('Frame rate', 'fps') |
| 41 NUM_DROPPED_FRAMES = ('Number of dropped frames', 'num dropped frames') |
| 42 NUM_FRAMES_TO_TARGET = ('Number of frames to approach target rate', |
| 43 'frames to reach target rate') |
| 44 ENCODE_TIME = ('Encoding time', 'encode time (us)') |
| 45 ENCODE_TIME_AVG = ('Encoding time', 'encode time (us) avg') |
| 46 DECODE_TIME = ('Decoding time', 'decode time (us)') |
| 47 DECODE_TIME_AVG = ('Decoding time', 'decode time (us) avg') |
| 48 FRAME_SIZE = ('Frame sizes', 'frame size (bytes)') |
| 49 FRAME_SIZE_AVG = ('Frame sizes', 'frame size (bytes) avg') |
| 50 AVG_KEY_FRAME_SIZE = ('Average key frame size', 'avg key frame size (bytes)') |
| 51 AVG_NON_KEY_FRAME_SIZE = ('Average non-key frame size', |
| 52 'avg non-key frame size (bytes)') |
| 53 |
| 54 # Settings. |
| 55 SETTINGS = [ |
| 56 WIDTH, |
| 57 HEIGHT, |
| 58 FILENAME, |
| 59 CODEC_TYPE, |
| 60 NUM_FRAMES, |
| 61 ENCODER_IMPLEMENTATION_NAME, |
| 62 DECODER_IMPLEMENTATION_NAME, |
| 63 ENCODE_TIME, |
| 64 DECODE_TIME, |
| 65 FRAME_SIZE, |
| 66 ] |
| 67 |
| 68 # Settings, options for x-axis. |
| 69 X_SETTINGS = [ |
| 70 CORES, |
| 71 FRAMERATE, |
| 72 DENOISING, |
| 73 RESILIENCE, |
| 74 ERROR_CONCEALMENT, |
| 75 BITRATE, # TODO(asapersson): Needs to be last. |
| 76 ] |
| 77 |
| 78 # Results. |
| 79 RESULTS = [ |
| 80 PSNR, |
| 81 SSIM, |
| 82 ENC_BITRATE, |
| 83 NUM_DROPPED_FRAMES, |
| 84 NUM_FRAMES_TO_TARGET, |
| 85 ENCODE_TIME_AVG, |
| 86 DECODE_TIME_AVG, |
| 87 AVG_KEY_FRAME_SIZE, |
| 88 AVG_NON_KEY_FRAME_SIZE, |
| 89 ] |
| 90 |
| 91 METRICS_TO_PARSE = SETTINGS + X_SETTINGS + RESULTS |
| 92 |
| 93 Y_METRICS = [res[1] for res in RESULTS] |
| 94 |
| 95 # Parameters for plotting. |
| 96 FIG_SIZE_SCALE_FACTOR_X = 2 |
| 97 FIG_SIZE_SCALE_FACTOR_Y = 2.8 |
| 98 GRID_COLOR = [0.45, 0.45, 0.45] |
| 99 |
| 100 |
| 101 def ParseSetting(filename, setting): |
| 102 """Parses setting from file. |
| 103 |
| 104 Args: |
| 105 filename: The name of the file. |
| 106 setting: Name of setting to parse (e.g. width). |
| 107 |
| 108 Returns: |
| 109 A list holding parsed settings, e.g. ['width: 128.0', 'width: 160.0'] """ |
| 110 |
| 111 settings = [] |
| 112 |
| 113 f = open(filename) |
| 114 while True: |
| 115 line = f.readline() |
| 116 if not line: |
| 117 break |
| 118 if re.search(r'%s' % EVENT_START, line): |
| 119 # Parse event. |
| 120 parsed = {} |
| 121 while True: |
| 122 line = f.readline() |
| 123 if not line: |
| 124 break |
| 125 if re.search(r'%s' % EVENT_END, line): |
| 126 # Add parsed setting to list. |
| 127 if setting in parsed: |
| 128 s = setting + ': ' + str(parsed[setting]) |
| 129 if s not in settings: |
| 130 settings.append(s) |
| 131 break |
| 132 |
| 133 TryFindMetric(parsed, line, f) |
| 134 |
| 135 f.close() |
| 136 return settings |
| 137 |
| 138 |
| 139 def ParseMetrics(filename, setting1, setting2): |
| 140 """Parses metrics from file. |
| 141 |
| 142 Args: |
| 143 filename: The name of the file. |
| 144 setting1: First setting for sorting metrics (e.g. width). |
| 145 setting2: Second setting for sorting metrics (e.g. cores). |
| 146 |
| 147 Returns: |
| 148 A dictionary holding parsed metrics. |
| 149 |
| 150 For example: |
| 151 metrics[key1][key2][measurement] |
| 152 |
| 153 metrics = { |
| 154 "width: 352": { |
| 155 "cores: 1.0": { |
| 156 "encode time (us)": [0.718005, 0.806925, 0.909726, 0.931835, 0.953642], |
| 157 "PSNR (dB)": [25.546029, 29.465518, 34.723535, 36.428493, 38.686551], |
| 158 "bitrate (kbps)": [50, 100, 300, 500, 1000] |
| 159 }, |
| 160 "cores: 2.0": { |
| 161 "encode time (us)": [0.718005, 0.806925, 0.909726, 0.931835, 0.953642], |
| 162 "PSNR (dB)": [25.546029, 29.465518, 34.723535, 36.428493, 38.686551], |
| 163 "bitrate (kbps)": [50, 100, 300, 500, 1000] |
| 164 }, |
| 165 }, |
| 166 "width: 176": { |
| 167 "cores: 1.0": { |
| 168 "encode time (us)": [0.857897, 0.91608, 0.959173, 0.971116, 0.980961], |
| 169 "PSNR (dB)": [30.243646, 33.375592, 37.574387, 39.42184, 41.437897], |
| 170 "bitrate (kbps)": [50, 100, 300, 500, 1000] |
| 171 }, |
| 172 } |
| 173 } """ |
| 174 |
| 175 metrics = {} |
| 176 |
| 177 # Parse events. |
| 178 f = open(filename) |
| 179 while True: |
| 180 line = f.readline() |
| 181 if not line: |
| 182 break |
| 183 if re.search(r'%s' % EVENT_START, line): |
| 184 # Parse event. |
| 185 parsed = {} |
| 186 while True: |
| 187 line = f.readline() |
| 188 if not line: |
| 189 break |
| 190 if re.search(r'%s' % EVENT_END, line): |
| 191 # Add parsed values to metrics. |
| 192 key1 = setting1 + ': ' + str(parsed[setting1]) |
| 193 key2 = setting2 + ': ' + str(parsed[setting2]) |
| 194 if key1 not in metrics: |
| 195 metrics[key1] = {} |
| 196 if key2 not in metrics[key1]: |
| 197 metrics[key1][key2] = {} |
| 198 |
| 199 for label in parsed: |
| 200 if label not in metrics[key1][key2]: |
| 201 metrics[key1][key2][label] = [] |
| 202 metrics[key1][key2][label].append(parsed[label]) |
| 203 |
| 204 break |
| 205 |
| 206 TryFindMetric(parsed, line, f) |
| 207 |
| 208 f.close() |
| 209 return metrics |
| 210 |
| 211 |
| 212 def TryFindMetric(parsed, line, f): |
| 213 for metric in METRICS_TO_PARSE: |
| 214 name = metric[0] |
| 215 label = metric[1] |
| 216 if re.search(r'%s' % name, line): |
| 217 found, value = GetMetric(name, line) |
| 218 if not found: |
| 219 # TODO(asapersson): Change format. |
| 220 # Try find min, max, average stats. |
| 221 found, minimum = GetMetric("Min", f.readline()) |
| 222 if not found: |
| 223 return |
| 224 found, maximum = GetMetric("Max", f.readline()) |
| 225 if not found: |
| 226 return |
| 227 found, average = GetMetric("Average", f.readline()) |
| 228 if not found: |
| 229 return |
| 230 |
| 231 parsed[label + ' min'] = minimum |
| 232 parsed[label + ' max'] = maximum |
| 233 parsed[label + ' avg'] = average |
| 234 |
| 235 parsed[label] = value |
| 236 return |
| 237 |
| 238 |
| 239 def GetMetric(name, string): |
| 240 # Float (e.g. bitrate = 98.8253). |
| 241 pattern = r'%s\s*[:=]\s*([+-]?\d+\.*\d*)' % name |
| 242 m = re.search(r'%s' % pattern, string) |
| 243 if m is not None: |
| 244 return StringToFloat(m.group(1)) |
| 245 |
| 246 # Alphanumeric characters (e.g. codec type : VP8). |
| 247 pattern = r'%s\s*[:=]\s*(\w+)' % name |
| 248 m = re.search(r'%s' % pattern, string) |
| 249 if m is not None: |
| 250 return True, m.group(1) |
| 251 |
| 252 return False, -1 |
| 253 |
| 254 |
| 255 def StringToFloat(value): |
| 256 try: |
| 257 value = float(value) |
| 258 except ValueError: |
| 259 print "Not a float, skipped %s" % value |
| 260 return False, -1 |
| 261 |
| 262 return True, value |
| 263 |
| 264 |
| 265 def Plot(y_metric, x_metric, metrics): |
| 266 """Plots y_metric vs x_metric per key in metrics. |
| 267 |
| 268 For example: |
| 269 y_metric = 'PSNR (dB)' |
| 270 x_metric = 'bitrate (kbps)' |
| 271 metrics = { |
| 272 "cores: 1.0": { |
| 273 "PSNR (dB)": [25.546029, 29.465518, 34.723535, 36.428493, 38.686551], |
| 274 "bitrate (kbps)": [50, 100, 300, 500, 1000] |
| 275 }, |
| 276 "cores: 2.0": { |
| 277 "PSNR (dB)": [25.546029, 29.465518, 34.723535, 36.428493, 38.686551], |
| 278 "bitrate (kbps)": [50, 100, 300, 500, 1000] |
| 279 }, |
| 280 } |
| 281 """ |
| 282 for key in metrics: |
| 283 data = metrics[key] |
| 284 if y_metric not in data: |
| 285 print "Failed to find metric: %s" % y_metric |
| 286 continue |
| 287 |
| 288 y = numpy.array(data[y_metric]) |
| 289 x = numpy.array(data[x_metric]) |
| 290 if len(y) != len(x): |
| 291 print "Length mismatch for %s, %s" % (y, x) |
| 292 continue |
| 293 |
| 294 label = y_metric + ' - ' + str(key) |
| 295 |
| 296 plt.plot(x, y, label=label, linewidth=1.5, marker='o', markersize=5, |
| 297 markeredgewidth=0.0) |
| 298 |
| 299 |
| 300 def PlotFigure(settings, y_metrics, x_metric, metrics, title): |
| 301 """Plots metrics in y_metrics list. One figure is plotted and each entry |
| 302 in the list is plotted in a subplot (and sorted per settings). |
| 303 |
| 304 For example: |
| 305 settings = ['width: 128.0', 'width: 160.0']. Sort subplot per setting. |
| 306 y_metrics = ['PSNR (dB)', 'PSNR (dB)']. Metric to plot per subplot. |
| 307 x_metric = 'bitrate (kbps)' |
| 308 |
| 309 """ |
| 310 |
| 311 plt.figure() |
| 312 plt.suptitle(title, fontsize='small', fontweight='bold') |
| 313 rows = len(settings) |
| 314 cols = 1 |
| 315 pos = 1 |
| 316 while pos <= rows: |
| 317 plt.rc('grid', color=GRID_COLOR) |
| 318 ax = plt.subplot(rows, cols, pos) |
| 319 plt.grid() |
| 320 plt.setp(ax.get_xticklabels(), visible=(pos == rows), fontsize='small') |
| 321 plt.setp(ax.get_yticklabels(), fontsize='small') |
| 322 setting = settings[pos - 1] |
| 323 Plot(y_metrics[pos - 1], x_metric, metrics[setting]) |
| 324 plt.title(setting, fontsize='x-small') |
| 325 plt.legend(fontsize='xx-small') |
| 326 pos += 1 |
| 327 |
| 328 plt.xlabel(x_metric, fontsize='small') |
| 329 plt.subplots_adjust(left=0.04, right=0.98, bottom=0.04, top=0.96, hspace=0.1) |
| 330 |
| 331 |
| 332 def GetTitle(filename): |
| 333 title = '' |
| 334 codec_types = ParseSetting(filename, CODEC_TYPE[1]) |
| 335 for i in range(0, len(codec_types)): |
| 336 title += codec_types[i] + ', ' |
| 337 |
| 338 framerate = ParseSetting(filename, FRAMERATE[1]) |
| 339 for i in range(0, len(framerate)): |
| 340 title += framerate[i].split('.')[0] + ', ' |
| 341 |
| 342 enc_names = ParseSetting(filename, ENCODER_IMPLEMENTATION_NAME[1]) |
| 343 for i in range(0, len(enc_names)): |
| 344 title += enc_names[i] + ', ' |
| 345 |
| 346 dec_names = ParseSetting(filename, DECODER_IMPLEMENTATION_NAME[1]) |
| 347 for i in range(0, len(dec_names)): |
| 348 title += dec_names[i] + ', ' |
| 349 |
| 350 filenames = ParseSetting(filename, FILENAME[1]) |
| 351 title += filenames[0].split('_')[0] |
| 352 |
| 353 num_frames = ParseSetting(filename, NUM_FRAMES[1]) |
| 354 for i in range(0, len(num_frames)): |
| 355 title += ' (' + num_frames[i].split('.')[0] + ')' |
| 356 |
| 357 return title |
| 358 |
| 359 |
| 360 def ToString(input_list): |
| 361 return ToStringWithoutMetric(input_list, ('', '')) |
| 362 |
| 363 |
| 364 def ToStringWithoutMetric(input_list, metric): |
| 365 i = 1 |
| 366 output_str = "" |
| 367 for m in input_list: |
| 368 if m != metric: |
| 369 output_str = output_str + ("%s. %s\n" % (i, m[1])) |
| 370 i += 1 |
| 371 return output_str |
| 372 |
| 373 |
| 374 def GetIdx(text_list): |
| 375 return int(raw_input(text_list)) - 1 |
| 376 |
| 377 |
| 378 def main(): |
| 379 filename = sys.argv[1] |
| 380 |
| 381 # Setup. |
| 382 idx_metric = GetIdx("Choose metric:\n0. All\n%s" % ToString(RESULTS)) |
| 383 if idx_metric == -1: |
| 384 # Plot all metrics. One subplot for each metric. |
| 385 # Per subplot: metric vs bitrate (per resolution). |
| 386 cores = ParseSetting(filename, CORES[1]) |
| 387 setting1 = CORES[1] |
| 388 setting2 = WIDTH[1] |
| 389 sub_keys = [cores[0]] * len(Y_METRICS) |
| 390 y_metrics = Y_METRICS |
| 391 x_metric = BITRATE[1] |
| 392 else: |
| 393 resolutions = ParseSetting(filename, WIDTH[1]) |
| 394 idx = GetIdx("Select metric for x-axis:\n%s" % ToString(X_SETTINGS)) |
| 395 if X_SETTINGS[idx] == BITRATE: |
| 396 idx = GetIdx("Plot per:\n%s" % ToStringWithoutMetric(X_SETTINGS, BITRATE)) |
| 397 idx_setting = METRICS_TO_PARSE.index(X_SETTINGS[idx]) |
| 398 # Plot one metric. One subplot for each resolution. |
| 399 # Per subplot: metric vs bitrate (per setting). |
| 400 setting1 = WIDTH[1] |
| 401 setting2 = METRICS_TO_PARSE[idx_setting][1] |
| 402 sub_keys = resolutions |
| 403 y_metrics = [RESULTS[idx_metric][1]] * len(sub_keys) |
| 404 x_metric = BITRATE[1] |
| 405 else: |
| 406 # Plot one metric. One subplot for each resolution. |
| 407 # Per subplot: metric vs setting (per bitrate). |
| 408 setting1 = WIDTH[1] |
| 409 setting2 = BITRATE[1] |
| 410 sub_keys = resolutions |
| 411 y_metrics = [RESULTS[idx_metric][1]] * len(sub_keys) |
| 412 x_metric = X_SETTINGS[idx][1] |
| 413 |
| 414 metrics = ParseMetrics(filename, setting1, setting2) |
| 415 |
| 416 # Stretch fig size. |
| 417 figsize = plt.rcParams["figure.figsize"] |
| 418 figsize[0] *= FIG_SIZE_SCALE_FACTOR_X |
| 419 figsize[1] *= FIG_SIZE_SCALE_FACTOR_Y |
| 420 plt.rcParams["figure.figsize"] = figsize |
| 421 |
| 422 PlotFigure(sub_keys, y_metrics, x_metric, metrics, GetTitle(filename)) |
| 423 |
| 424 plt.show() |
| 425 |
| 426 |
| 427 if __name__ == '__main__': |
| 428 main() |
OLD | NEW |