From 31c59a444445125265044789d0754db8f39f71be Mon Sep 17 00:00:00 2001 From: gdavila Date: Tue, 24 May 2022 00:06:38 -0300 Subject: [PATCH] ffmpeg 5 and cambi support --- FFmpeg.py | 16 +++++++++++++--- README.md | 16 ++++++++++++---- Vmaf.py | 10 +++------- config.py | 2 +- easyVmaf.py | 36 +++++++++++++++++++++++++----------- 5 files changed, 54 insertions(+), 26 deletions(-) diff --git a/FFmpeg.py b/FFmpeg.py index 489a5b9..e1a5a21 100644 --- a/FFmpeg.py +++ b/FFmpeg.py @@ -114,6 +114,7 @@ def __init__(self, main, ref, loglevel="info"): self.vmafFilter = [] self.invertedSrc = False self.vmafpath = None + self.vmaf_cambi_heatmap_path = None def _commit(self): """build the final cmd to run""" @@ -159,7 +160,7 @@ def getPsnr(self, stats_file=False): psnr = [s for s in stdout if "average" in s][0].split(":")[1] return float(psnr) - def getVmaf(self, log_path=None, model='HD', subsample=1, output_fmt='json', threads=0, print_progress=False, end_sync=False, features = None): + def getVmaf(self, log_path=None, model='HD', subsample=1, output_fmt='json', threads=0, print_progress=False, end_sync=False, features = None, cambi_heatmap = False): main = self.main.lastOutputID ref = self.ref.lastOutputID if output_fmt == 'xml': @@ -173,6 +174,11 @@ def getVmaf(self, log_path=None, model='HD', subsample=1, output_fmt='json', thr log_path = os.path.splitext(self.main.videoSrc)[ 0] + '_vmaf.json' self.vmafpath = log_path + + self.vmaf_cambi_heatmap_path = os.path.splitext(self.main.videoSrc)[0] + '_cambi_heatmap' + + + if model == 'HD': model_hd = f'version={HD_MODEL_VERSION}\\\\:name={HD_MODEL_NAME}|version={HD_NEG_MODEL_VERSION}\\\\:name={HD_NEG_MODEL_NAME}|version={HD_PHONE_MODEL_VERSION}\\\\:name={HD_PHONE_MODEL_NAME}\\\\:enable_transform=true' model = model_hd @@ -186,11 +192,15 @@ def getVmaf(self, log_path=None, model='HD', subsample=1, output_fmt='json', thr else: shortest = 0 - if features == None: + if not features: self.vmafFilter = [f'[{main}][{ref}]libvmaf=log_fmt={log_fmt}:model={model}:n_subsample={subsample}:log_path={log_path}:n_threads={threads}:shortest={shortest}'] - else: + + elif features and not cambi_heatmap: self.vmafFilter = [f'[{main}][{ref}]libvmaf=log_fmt={log_fmt}:model={model}:n_subsample={subsample}:log_path={log_path}:n_threads={threads}:shortest={shortest}:feature={features}'] + elif features and cambi_heatmap: + self.vmafFilter = [f'[{main}][{ref}]libvmaf=log_fmt={log_fmt}:model={model}:n_subsample={subsample}:log_path={log_path}:n_threads={threads}:shortest={shortest}:feature={features}\\\\:heatmaps_path={self.vmaf_cambi_heatmap_path}'] + self._commit() if self.loglevel == "verbose": diff --git a/README.md b/README.md index 8a578d4..e4ee00d 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,21 @@ Details about **How it Works** can be found [here](https://ottverse.com/vmaf-eas ## Updates -* Cambi features support +Since `easyVmaf` `2.0` only FFmpeg versions >= `5.0` will be supported. For using `easyVmaf` with FFmpeg < `5.0`, please consider rollingback to `easyVmaf` `1.3`. -* Added support for ffmpeg 5.0 and their built-in vmaf models +New feaures and updates: -* Progress indicator added `-progress`. It shows the progress while doing vmaf computations. +* [Cambi feature](https://github.com/Netflix/vmaf/blob/master/resource/doc/cambi.md#options) - Netflix banding detector, supported. -* Added the option to explicilty set the number of threads to run `-threads (int)` +* Command line ussage updated according to [libvmaf docs](https://ffmpeg.org/ffmpeg-filters.html#libvmaf) + +* [Cambi heatmap](https://github.com/Netflix/vmaf/issues/936) support added. The outputs may be visualized with [ffplay](https://github.com/Netflix/vmaf/issues/1016#issuecomment-1099591977) + +* Built-in VMAF models are only supported since they are included in FFmpeg >= `v5.0`. + +* Docker image - better handling of dependencies and built instruccions + +* 'HD Neg' and 'HD phone' models are computed by default ## Requirements diff --git a/Vmaf.py b/Vmaf.py index 99ed5f3..7cd0c98 100644 --- a/Vmaf.py +++ b/Vmaf.py @@ -140,6 +140,7 @@ def __init__(self, mainSrc, refSrc, output_fmt, model="HD", phone=False, logleve self.end_sync = end_sync self.cambi_heatmap = cambi_heatmap + def _initResolutions(self): """ initialization of resolutions for each vmaf model @@ -400,12 +401,7 @@ def getVmaf(self, autoSync=False): """Apply Offset filters, if offset =0 nothing happens """ self.setOffset() - if self.cambi_heatmap: - heatmap_path = os.path.splitext(self.main.videoSrc)[0] + '_cambi_heatmap' - self.features = f'name=psnr|name=cambi\\\\:full_ref=true\\\\:enc_width={self.main.streamInfo["width"]}\\\\:enc_height={self.main.streamInfo["height"]}\\\\:src_width={self.ref.streamInfo["width"]}\\\\:src_height={self.ref.streamInfo["height"]}\\\\:heatmaps_path={heatmap_path}' - - else: self.features = f'name=psnr|name=cambi\\\\:full_ref=true\\\\:enc_width={self.main.streamInfo["width"]}\\\\:enc_height={self.main.streamInfo["height"]}\\\\:src_width={self.ref.streamInfo["width"]}\\\\:src_height={self.ref.streamInfo["height"]}' - + self.features = f'name=psnr|name=cambi\\\\:full_ref=true\\\\:enc_width={self.main.streamInfo["width"]}\\\\:enc_height={self.main.streamInfo["height"]}\\\\:src_width={self.ref.streamInfo["width"]}\\\\:src_height={self.ref.streamInfo["height"]}' print("\n\n=======================================", flush=True) @@ -425,7 +421,7 @@ def getVmaf(self, autoSync=False): vmafProcess = self.ffmpegQos.getVmaf(model=self.model, subsample=self.subsample, - output_fmt=self.output_fmt, threads=self.threads, print_progress=self.print_progress, end_sync=self.end_sync, features=self.features) + output_fmt=self.output_fmt, threads=self.threads, print_progress=self.print_progress, end_sync=self.end_sync, features=self.features, cambi_heatmap = self.cambi_heatmap) return vmafProcess diff --git a/config.py b/config.py index a19f3bd..70a262d 100644 --- a/config.py +++ b/config.py @@ -22,5 +22,5 @@ SOFTWARE. """ -ffmpeg = '/Users/gabriel/Downloads/ffmpeg' +ffmpeg = '/usr/local/bin/ffmpeg' ffprobe = '/usr/local/bin/ffprobe' diff --git a/easyVmaf.py b/easyVmaf.py index 8eff264..e624317 100644 --- a/easyVmaf.py +++ b/easyVmaf.py @@ -29,8 +29,7 @@ import glob import xml.etree.ElementTree as ET -from FFmpeg import HD_MODEL_NAME -from FFmpeg import _4K_MODEL_NAME +from FFmpeg import HD_MODEL_NAME, HD_NEG_MODEL_NAME, HD_PHONE_MODEL_NAME ,_4K_MODEL_NAME, HD_PHONE_MODEL_VERSION from statistics import mean, harmonic_mean @@ -150,9 +149,6 @@ def error(self, message): main_pattern, flush=True) sys.exit(1) - if model == 'HD': vmaf_metric_name = HD_MODEL_NAME - elif model == '4K': vmaf_metric_name = _4K_MODEL_NAME - for main in mainFiles: myVmaf = vmaf(main, reference, loglevel=loglevel, subsample=n_subsample, model=model, output_fmt=output_fmt, threads=threads, print_progress=print_progress, end_sync=end_sync, manual_fps=fps, cambi_heatmap = cambi_heatmap) @@ -171,26 +167,44 @@ def error(self, message): vmafProcess = myVmaf.getVmaf() vmafpath = myVmaf.ffmpegQos.vmafpath vmafScore = [] + vmafNegScore = [] + vmafPhoneScore = [] if output_fmt == 'json': with open(vmafpath) as jsonFile: jsonData = json.load(jsonFile) for frame in jsonData['frames']: - vmafScore.append(frame["metrics"][vmaf_metric_name]) + if model == 'HD': + vmafScore.append(frame["metrics"][HD_MODEL_NAME]) + vmafNegScore.append(frame["metrics"][HD_NEG_MODEL_NAME]) + vmafPhoneScore.append(frame["metrics"][HD_PHONE_MODEL_NAME]) + if model == '4K': + vmafScore.append(frame["metrics"][_4K_MODEL_NAME]) elif output_fmt == 'xml': tree = ET.parse(vmafpath) root = tree.getroot() for frame in root.findall('frames/frame'): - value = frame.get(vmaf_metric_name) - vmafScore.append(float(value)) + if model == 'HD': + vmafScore.append(frame["metrics"][HD_MODEL_NAME]) + vmafNegScore.append(frame["metrics"][HD_NEG_MODEL_NAME]) + vmafPhoneScore.append(frame["metrics"][HD_PHONE_MODEL_NAME]) + if model == '4K': + vmafScore.append(frame["metrics"][_4K_MODEL_NAME]) print("\n \n \n \n \n ") print("=======================================", flush=True) print("VMAF computed", flush=True) print("=======================================", flush=True) print("offset: ", offset, " | psnr: ", psnr) - print("VMAF score (arithmetic mean): ", mean(vmafScore)) - print("VMAF score (harmonic mean): ", harmonic_mean(vmafScore)) - print("VMAF output File Path: ", myVmaf.ffmpegQos.vmafpath) + if model == 'HD': + print("VMAF HD: ", mean(vmafScore)) + print("VMAF Neg: ", mean(vmafNegScore)) + print("VMAF Phone: ", mean(vmafPhoneScore)) + if model == '4K': + print("VMAF 4K: ", mean(vmafScore)) + print("VMAF output file path: ", myVmaf.ffmpegQos.vmafpath) + if cambi_heatmap: + print("CAMBI Heatmap output path: ", myVmaf.ffmpegQos.vmaf_cambi_heatmap_path) + print("\n \n \n \n \n ")