Av1an/VMAF/vmaf.py

152 lines
5 KiB
Python
Raw Normal View History

2020-06-22 20:11:07 +00:00
#! /bin/env python
2020-08-19 07:06:42 +00:00
import json
import shlex
2020-06-27 19:21:00 +00:00
import subprocess
2020-07-06 15:53:25 +00:00
import sys
2020-06-27 19:21:00 +00:00
from pathlib import Path
from subprocess import PIPE, STDOUT
2020-08-19 07:06:42 +00:00
2020-07-06 15:53:25 +00:00
import numpy as np
2020-08-25 20:45:08 +00:00
from math import log10
2020-08-19 07:06:42 +00:00
from matplotlib import pyplot as plt
2020-08-29 09:56:48 +00:00
from Av1an.bar import process_pipe
2020-09-14 17:45:21 +00:00
from Chunks.chunk import Chunk
2020-08-29 09:56:48 +00:00
import matplotlib
2020-06-14 02:37:43 +00:00
2020-08-29 09:56:48 +00:00
matplotlib.use('Agg')
2020-06-14 02:37:43 +00:00
def read_vmaf_json(file, percentile):
"""Reads vmaf file with vmaf scores in it and return N percentile score from it.
2020-06-14 02:37:43 +00:00
:return: N percentile score
:rtype: float
"""
with open(file, 'r') as f:
file = json.load(f)
2020-07-30 09:32:40 +00:00
vmafs = [x['metrics']['vmaf'] for x in file['frames']]
perc = round(np.percentile(vmafs, percentile), 2)
return perc
2020-06-14 02:37:43 +00:00
2020-08-29 09:56:48 +00:00
def call_vmaf(chunk: Chunk, encoded: Path, n_threads, model, res,
2020-09-27 00:55:29 +00:00
fl_path: Path = None, vmaf_filter=None, vmaf_rate=0):
cmd = ''
2020-08-29 09:56:48 +00:00
# settings model path
2020-08-29 09:56:48 +00:00
mod = f":model_path={model}" if model else ''
2020-06-14 02:37:43 +00:00
# limiting amount of threads for calculation
2020-08-29 09:56:48 +00:00
n_threads = f':n_threads={n_threads}' if n_threads else ''
if fl_path is None:
fl_path = chunk.fake_input_path.with_name(encoded.stem).with_suffix('.json')
fl = fl_path.as_posix()
2020-09-27 00:55:29 +00:00
filter = vmaf_filter + ',' if vmaf_filter else ''
2020-06-22 20:11:07 +00:00
# For vmaf calculation both source and encoded segment scaled to 1080
2020-07-16 18:42:55 +00:00
# Also it's required to use -r before both files of vmaf calculation to avoid errors
2020-08-29 09:56:48 +00:00
cmd_in = ('ffmpeg', '-loglevel', 'info', '-y', '-thread_queue_size', '1024', '-hide_banner',
'-r', '60', '-i', encoded.as_posix(), '-r', '60', '-i', '-')
filter_complex = ('-filter_complex',)
2020-08-06 02:51:30 +00:00
2020-09-02 01:11:01 +00:00
# Change framerate of comparison to framerate of probe
select_frames = f"select=not(mod(n\\,{vmaf_rate}))," if vmaf_rate != 0 else ''
2020-08-29 09:56:48 +00:00
distorted = f'[0:v]{select_frames}scale={res}:flags=bicubic:\
force_original_aspect_ratio=decrease,setpts=PTS-STARTPTS[distorted];'
2020-09-27 00:55:29 +00:00
ref = fr'[1:v]{select_frames}{filter}scale={res}:flags=bicubic:'\
2020-08-29 09:56:48 +00:00
'force_original_aspect_ratio=decrease,setpts=PTS-STARTPTS[ref];'
2020-08-29 09:56:48 +00:00
vmaf_filter = f"[distorted][ref]libvmaf=log_fmt='json':eof_action=endall:\
log_path={shlex.quote(fl)}{mod}{n_threads}"
cmd_out = ('-f', 'null', '-')
cmd = (*cmd_in, *filter_complex, distorted + ref + vmaf_filter, *cmd_out)
2020-06-14 02:37:43 +00:00
ffmpeg_gen_pipe = subprocess.Popen(chunk.ffmpeg_gen_cmd, stdout=PIPE, stderr=STDOUT)
2020-08-29 09:56:48 +00:00
pipe = subprocess.Popen(cmd, stdin=ffmpeg_gen_pipe.stdout,
stdout=PIPE, stderr=STDOUT, universal_newlines=True)
2020-08-12 14:59:41 +00:00
process_pipe(pipe)
return fl_path
2020-06-14 02:37:43 +00:00
2020-08-06 02:51:30 +00:00
def plot_vmaf(source: Path, encoded: Path, args, model, vmaf_res):
2020-08-29 09:56:48 +00:00
"""
Making VMAF plot after encode is done
"""
2020-06-14 02:37:43 +00:00
print('Calculating Vmaf...\r', end='')
2020-08-29 09:56:48 +00:00
fl_path = encoded.with_name(f'{encoded.stem}_vmaflog').with_suffix(".json")
2020-08-06 02:51:30 +00:00
# call_vmaf takes a chunk, so make a chunk of the entire source
2020-08-29 09:56:48 +00:00
ffmpeg_gen_cmd = ['ffmpeg', '-y', '-hide_banner', '-loglevel', 'error', '-i',
source.as_posix(), *args.pix_format, '-f', 'yuv4mpegpipe', '-']
2020-08-06 05:46:49 +00:00
input_chunk = Chunk(args.temp, 0, ffmpeg_gen_cmd, '', 0, 0)
2020-08-06 02:51:30 +00:00
scores = call_vmaf(input_chunk, encoded, 0, model, vmaf_res, fl_path=fl_path)
2020-06-14 02:37:43 +00:00
if not scores.exists():
2020-08-06 02:51:30 +00:00
print(f'Vmaf calculation failed for chunks:\n {source.name} {encoded.stem}')
2020-06-22 20:11:07 +00:00
sys.exit()
2020-06-14 02:37:43 +00:00
2020-08-06 02:51:30 +00:00
file_path = encoded.with_name(f'{encoded.stem}_plot').with_suffix('.png')
plot_vmaf_score_file(scores, file_path)
def plot_vmaf_score_file(scores: Path, plot_path: Path):
2020-08-29 09:56:48 +00:00
"""
Read vmaf json and plot VMAF values for each frame
"""
perc_1 = read_vmaf_json(scores, 1)
perc_25 = read_vmaf_json(scores, 25)
perc_75 = read_vmaf_json(scores, 75)
mean = read_vmaf_json(scores, 50)
2020-06-20 19:18:16 +00:00
with open(scores) as f:
file = json.load(f)
vmafs = [x['metrics']['vmaf'] for x in file['frames']]
plot_size = len(vmafs)
2020-06-20 19:18:16 +00:00
2020-08-25 20:45:08 +00:00
figure_width = 3 + round((4 * log10(plot_size)))
plt.figure(figsize=(figure_width, 5))
plt.plot([1, plot_size], [perc_1, perc_1], '-', color='red')
plt.annotate(f'1%: {perc_1}', xy=(0, perc_1), color='red')
plt.plot([1, plot_size], [perc_25, perc_25], ':', color='orange')
plt.annotate(f'25%: {perc_25}', xy=(0, perc_25), color='orange')
plt.plot([1, plot_size], [perc_75, perc_75], ':', color='green')
plt.annotate(f'75%: {perc_75}', xy=(0, perc_75), color='green')
plt.plot([1, plot_size], [mean, mean], ':', color='black')
plt.annotate(f'Mean: {mean}', xy=(0, mean), color='black')
2020-06-14 02:37:43 +00:00
for i in range(0, 100):
plt.axhline(i, color='grey', linewidth=0.4)
if i % 5 == 0:
plt.axhline(i, color='black', linewidth=0.6)
plt.plot(range(plot_size), vmafs,
2020-08-19 06:41:58 +00:00
label=f'Frames: {plot_size}\nMean:{mean}\n'
2020-08-19 07:06:42 +00:00
f'1%: {perc_1} \n25%: {perc_25} \n75%: {perc_75}', linewidth=0.7)
2020-06-14 02:37:43 +00:00
plt.ylabel('VMAF')
plt.legend(loc="lower right", markerscale=0, handlelength=0, fancybox=True, )
plt.ylim(int(perc_1), 100)
plt.tight_layout()
plt.margins(0)
# Save
2020-08-25 17:12:26 +00:00
plt.savefig(plot_path, dpi=250)