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 ] TestWithBitrate/PlotVideoProcessorIntegrationTest.' | |
22 EVENT_END = 'OK ] TestWithBitrate/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 = [] | |
brandtr
2017/02/09 13:32:42
Could this be replaced by list concatenation?
MET
åsapersson
2017/02/09 16:39:37
Done.
| |
92 for k in SETTINGS: | |
93 METRICS_TO_PARSE.append(k) | |
94 for x_setting in X_SETTINGS: | |
95 METRICS_TO_PARSE.append(x_setting) | |
96 for result in RESULTS: | |
97 METRICS_TO_PARSE.append(result) | |
98 | |
99 Y_METRICS = [] | |
brandtr
2017/02/09 13:32:42
And here a list comprehension?
Y_METRICS = [ res[
åsapersson
2017/02/09 16:39:37
Done.
| |
100 for res in RESULTS: | |
101 Y_METRICS.append(res[1]) | |
102 | |
103 | |
104 # Parameters for plotting. | |
105 FIG_SIZE_SCALE_FACTOR_Y = 2.8 | |
106 FIG_SIZE_SCALE_FACTOR_X = 2 | |
brandtr
2017/02/09 13:32:42
Place FIG_SIZE_SCALE_FACTOR_X before the correspon
åsapersson
2017/02/09 16:39:37
Done.
| |
107 GRID_COLOR = [0.45, 0.45, 0.45] | |
108 | |
109 | |
110 def ToString(input_list): | |
brandtr
2017/02/09 13:32:42
Move ToString and ToStringWithoutMetric closer to
åsapersson
2017/02/09 16:39:37
Done.
| |
111 return ToStringWithoutMetric(input_list, ('', '')) | |
112 | |
113 | |
114 def ToStringWithoutMetric(input_list, metric): | |
115 i = 1 | |
116 output_str = "" | |
117 for m in input_list: | |
118 if m != metric: | |
119 output_str = output_str + ("%s. %s\n" % (i, m[1])) | |
120 i += 1 | |
121 return output_str | |
122 | |
123 | |
124 def ParseSetting(filename, setting): | |
125 """Parses setting from file. | |
126 | |
127 Args: | |
128 filename: The name of the file. | |
129 setting: Name of setting to parse (e.g. width). | |
130 | |
131 Returns: | |
132 A list holding parsed settings, e.g. ['width: 128.0', 'width: 160.0'] """ | |
133 | |
134 settings = [] | |
135 | |
136 f = open(filename) | |
137 while True: | |
138 line = f.readline() | |
139 if not line: | |
140 break | |
141 if re.search(r'%s' % EVENT_START, line): | |
142 # Parse event. | |
143 parsed = {} | |
144 while True: | |
145 line = f.readline() | |
146 if not line: | |
147 break | |
148 if re.search(r'%s' % EVENT_END, line): | |
149 # Add parsed setting to list. | |
150 if setting in parsed: | |
151 s = setting + ': ' + str(parsed[setting]) | |
152 if s not in settings: | |
153 settings.append(s) | |
154 break | |
155 | |
156 TryFindMetric(parsed, line, f) | |
157 | |
158 f.close() | |
159 return settings | |
160 | |
161 | |
162 def ParseMetrics(filename, setting1, setting2): | |
163 """Parses metrics from file. | |
164 | |
165 Args: | |
166 filename: The name of the file. | |
167 setting1: First setting for sorting metrics (e.g. width). | |
168 setting2: Second setting for sorting metrics (e.g. cores). | |
169 | |
170 Returns: | |
171 A dictionary holding parsed metrics. | |
172 | |
173 For example: | |
174 metrics[key1][key2][measurement] | |
175 | |
176 metrics = { | |
177 "width: 352": { | |
178 "cores: 1.0": { | |
179 "encode time (us)": [0.718005, 0.806925, 0.909726, 0.931835, 0.953642], | |
180 "PSNR (dB)": [25.546029, 29.465518, 34.723535, 36.428493, 38.686551], | |
181 "bitrate (kbps)": [50, 100, 300, 500, 1000] | |
182 }, | |
183 "cores: 2.0": { | |
184 "encode time (us)": [0.718005, 0.806925, 0.909726, 0.931835, 0.953642], | |
185 "PSNR (dB)": [25.546029, 29.465518, 34.723535, 36.428493, 38.686551], | |
186 "bitrate (kbps)": [50, 100, 300, 500, 1000] | |
187 }, | |
188 }, | |
189 "width: 176": { | |
190 "cores: 1.0": { | |
191 "encode time (us)": [0.857897, 0.91608, 0.959173, 0.971116, 0.980961], | |
192 "PSNR (dB)": [30.243646, 33.375592, 37.574387, 39.42184, 41.437897], | |
193 "bitrate (kbps)": [50, 100, 300, 500, 1000] | |
194 }, | |
195 } | |
196 } """ | |
197 | |
198 metrics = {} | |
199 | |
200 # Parse events. | |
201 f = open(filename) | |
202 while True: | |
203 line = f.readline() | |
204 if not line: | |
205 break | |
206 if re.search(r'%s' % EVENT_START, line): | |
207 # Parse event. | |
208 parsed = {} | |
209 while True: | |
210 line = f.readline() | |
211 if not line: | |
212 break | |
213 if re.search(r'%s' % EVENT_END, line): | |
214 # Add parsed values to metrics. | |
215 key1 = setting1 + ': ' + str(parsed[setting1]) | |
216 key2 = setting2 + ': ' + str(parsed[setting2]) | |
217 if key1 not in metrics: | |
218 metrics[key1] = {} | |
219 if key2 not in metrics[key1]: | |
220 metrics[key1][key2] = {} | |
221 | |
222 for label in parsed: | |
223 if label not in metrics[key1][key2]: | |
224 metrics[key1][key2][label] = [] | |
225 metrics[key1][key2][label].append(parsed[label]) | |
226 | |
227 break | |
228 | |
229 TryFindMetric(parsed, line, f) | |
230 | |
231 f.close() | |
232 return metrics | |
233 | |
234 | |
235 def TryFindMetric(parsed, line, f): | |
236 for metric in METRICS_TO_PARSE: | |
237 name = metric[0] | |
238 label = metric[1] | |
239 if re.search(r'%s' % name, line): | |
240 found, value = GetMetric(name, line) | |
241 if not found: | |
242 # TODO(asapersson): Change format. | |
243 # Try find min, max, average stats. | |
244 found, minimum = GetMetric("Min", f.readline()) | |
245 if not found: | |
246 return | |
247 found, maximum = GetMetric("Max", f.readline()) | |
248 if not found: | |
249 return | |
250 found, average = GetMetric("Average", f.readline()) | |
251 if not found: | |
252 return | |
253 | |
254 parsed[label + ' min'] = minimum | |
255 parsed[label + ' max'] = maximum | |
256 parsed[label + ' avg'] = average | |
257 | |
258 parsed[label] = value | |
259 return | |
260 | |
261 | |
262 def GetMetric(name, string): | |
263 # Float. | |
264 pattern = r'%s\s*[:=]\s*([+-]?\d+\.*\d*)' % name | |
brandtr
2017/02/09 13:32:42
I don't fully understand this regex. Maybe a short
åsapersson
2017/02/09 16:39:37
Done.
| |
265 m = re.search(r'%s' % pattern, string) | |
266 if m is not None: | |
267 return StringToFloat(m.group(1)) | |
268 | |
269 # Alphanumeric characters. | |
270 pattern = r'%s\s*[:=]\s*(\w+)' % name | |
brandtr
2017/02/09 13:32:42
Same thing here.
åsapersson
2017/02/09 16:39:37
Done.
| |
271 m = re.search(r'%s' % pattern, string) | |
272 if m is not None: | |
273 return True, m.group(1) | |
274 | |
275 return False, -1 | |
276 | |
277 | |
278 def StringToFloat(value): | |
279 try: | |
280 value = float(value) | |
281 except ValueError: | |
282 print "Not a float, skipped %s" % value | |
283 return False, -1 | |
284 | |
285 return True, value | |
286 | |
287 | |
288 def Plot(y_metric, x_metric, metrics): | |
289 """Plots y_metric vs x_metric per key in metrics. | |
290 | |
291 For example: | |
292 y_metric = 'PSNR (dB)' | |
293 x_metric = 'bitrate (kbps)' | |
294 metrics = { | |
295 "cores: 1.0": { | |
296 "PSNR (dB)": [25.546029, 29.465518, 34.723535, 36.428493, 38.686551], | |
297 "bitrate (kbps)": [50, 100, 300, 500, 1000] | |
298 }, | |
299 "cores: 2.0": { | |
300 "PSNR (dB)": [25.546029, 29.465518, 34.723535, 36.428493, 38.686551], | |
301 "bitrate (kbps)": [50, 100, 300, 500, 1000] | |
302 }, | |
303 } | |
304 """ | |
305 for key in metrics: | |
306 data = metrics[key] | |
307 if y_metric not in data: | |
308 print "Failed to find metric: %s" % y_metric | |
309 continue | |
310 | |
311 y = numpy.array(data[y_metric]) | |
312 x = numpy.array(data[x_metric]) | |
313 if len(y) != len(x): | |
314 print "Length mismatch for %s, %s" % (y, x) | |
315 continue | |
316 | |
317 label = y_metric + ' - ' + str(key) | |
318 | |
319 plt.plot(x, y, label=label, linewidth=1.5, marker='o', markersize=5, | |
320 markeredgewidth=0.0) | |
321 | |
322 | |
323 def PlotFigure(settings, y_metrics, x_metric, metrics, title): | |
324 """Plots metrics in y_metrics list. One figure is plotted and each entry | |
325 in the list is plotted in a subplot (and sorted per settings). | |
326 | |
327 For example: | |
328 settings = ['width: 128.0', 'width: 160.0']. Sort subplot per setting. | |
329 y_metrics = ['PSNR (dB)', 'PSNR (dB)']. Metric to plot per subplot. | |
330 x_metric = 'bitrate (kbps)' | |
331 | |
332 """ | |
333 | |
334 plt.figure() | |
335 plt.suptitle(title, fontsize='small', fontweight='bold') | |
336 rows = len(settings) | |
337 cols = 1 | |
338 pos = 1 | |
339 while pos <= rows: | |
340 plt.rc('grid', color=GRID_COLOR) | |
341 ax = plt.subplot(rows, cols, pos) | |
342 plt.grid() | |
343 plt.setp(ax.get_xticklabels(), visible=(pos == rows), fontsize='small') | |
344 plt.setp(ax.get_yticklabels(), fontsize='small') | |
345 setting = settings[pos - 1] | |
346 Plot(y_metrics[pos - 1], x_metric, metrics[setting]) | |
347 plt.title(setting, fontsize='x-small') | |
348 plt.legend(fontsize='xx-small') | |
349 pos += 1 | |
350 | |
351 plt.xlabel(x_metric, fontsize='small') | |
352 plt.subplots_adjust(left=0.04, right=0.98, bottom=0.04, top=0.96, hspace=0.1) | |
353 | |
354 | |
355 def GetTitle(filename): | |
356 title = '' | |
357 codec_types = ParseSetting(filename, CODEC_TYPE[1]) | |
358 for i in range(0, len(codec_types)): | |
359 title += codec_types[i] + ', ' | |
360 | |
361 framerate = ParseSetting(filename, FRAMERATE[1]) | |
362 for i in range(0, len(framerate)): | |
363 title += framerate[i].split('.')[0] + ', ' | |
364 | |
365 enc_names = ParseSetting(filename, ENCODER_IMPLEMENTATION_NAME[1]) | |
366 for i in range(0, len(enc_names)): | |
367 title += enc_names[i] + ', ' | |
368 | |
369 dec_names = ParseSetting(filename, DECODER_IMPLEMENTATION_NAME[1]) | |
370 for i in range(0, len(dec_names)): | |
371 title += dec_names[i] + ', ' | |
372 | |
373 filenames = ParseSetting(filename, FILENAME[1]) | |
374 title += filenames[0].split('_')[0] | |
375 | |
376 num_frames = ParseSetting(filename, NUM_FRAMES[1]) | |
377 for i in range(0, len(num_frames)): | |
378 title += ' (' + num_frames[i].split('.')[0] + ')' | |
379 | |
380 return title | |
381 | |
382 | |
383 def GetIdx(text_list): | |
384 return int(raw_input(text_list)) - 1 | |
385 | |
386 | |
387 def main(): | |
388 filename = sys.argv[1] | |
389 | |
390 # Setup. | |
391 idx_metric = GetIdx("Choose metric:\n0. All\n%s" % ToString(RESULTS)) | |
392 if idx_metric == -1: | |
393 # Plot all metrics. One subplot for each metric. | |
394 # Per subplot: metric vs bitrate (per resolution). | |
395 cores = ParseSetting(filename, CORES[1]) | |
396 setting1 = CORES[1] | |
397 setting2 = WIDTH[1] | |
398 sub_keys = [cores[0]] * len(Y_METRICS) | |
399 y_metrics = Y_METRICS | |
400 x_metric = BITRATE[1] | |
401 else: | |
402 resolutions = ParseSetting(filename, WIDTH[1]) | |
403 idx = GetIdx("Select metric for x-axis:\n%s" % ToString(X_SETTINGS)) | |
404 if X_SETTINGS[idx] == BITRATE: | |
405 idx = GetIdx("Plot per:\n%s" % ToStringWithoutMetric(X_SETTINGS, BITRATE)) | |
406 idx_setting = METRICS_TO_PARSE.index(X_SETTINGS[idx]) | |
407 # Plot one metric. One subplot for each resolution. | |
408 # Per subplot: metric vs bitrate (per setting). | |
409 setting1 = WIDTH[1] | |
410 setting2 = METRICS_TO_PARSE[idx_setting][1] | |
411 sub_keys = resolutions | |
412 y_metrics = [RESULTS[idx_metric][1]] * len(sub_keys) | |
413 x_metric = BITRATE[1] | |
414 else: | |
415 # Plot one metric. One subplot for each resolution. | |
416 # Per subplot: metric vs setting (per bitrate). | |
417 setting1 = WIDTH[1] | |
418 setting2 = BITRATE[1] | |
419 sub_keys = resolutions | |
420 y_metrics = [RESULTS[idx_metric][1]] * len(sub_keys) | |
421 x_metric = X_SETTINGS[idx][1] | |
422 | |
423 metrics = ParseMetrics(filename, setting1, setting2) | |
424 | |
425 # Stretch fig size. | |
426 figsize = plt.rcParams["figure.figsize"] | |
427 figsize[0] *= FIG_SIZE_SCALE_FACTOR_X | |
428 figsize[1] *= FIG_SIZE_SCALE_FACTOR_Y | |
429 plt.rcParams["figure.figsize"] = figsize | |
430 | |
431 PlotFigure(sub_keys, y_metrics, x_metric, metrics, GetTitle(filename)) | |
432 | |
433 plt.show() | |
434 | |
435 | |
436 if __name__ == '__main__': | |
437 main() | |
OLD | NEW |