Added SSIM and PSNR scoring, multithreaded cmdline option added for VMAF scoring command line

This commit is contained in:
Samuele Lorefice
2025-12-24 02:55:38 +01:00
parent d025031a2f
commit 2631d6f13b

View File

@@ -11,7 +11,7 @@ string outputDir = Path.Combine(Environment.CurrentDirectory, "output");
const string inputFile = "testVid.mp4";
Directory.CreateDirectory(outputDir);
Directory.CreateDirectory(tempPath);
const string tableHeader = "Width\tHeight\tPreset\tCRF\tSize Ratio\tEncode Time\tSpeed\tVMAF\n";
const string tableHeader = "Width\tHeight\tPreset\tCRF\tSize Ratio\tEncode Time\tSpeed\tVMAF\tSSIM\tPSNR_avg\tPSNR_MSE\n";
StringBuilder AV1GPUStats = new(tableHeader);
StringBuilder HEVCGPUStats = new(tableHeader);
StringBuilder H264GPUStats = new(tableHeader);
@@ -43,32 +43,32 @@ CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
CancellationToken cancellationToken = cancellationTokenSource.Token;
Console.WriteLine("Generating Tasks:");
for (int cq = 24; cq <= 34; cq += 3) {
for (int preset = 12; preset <= 18; preset+=3) {
GpuTasks.Add(Encode(Encoder.AV1_NVENC, 1280, 720, cq, preset, cancellationToken));
GpuTasks.Add(Encode(Encoder.AV1_NVENC, 1920, 1080, cq, preset, cancellationToken));
for (int preset = 12; preset <= 18; preset+=2) {
//GpuTasks.Add(Encode(Encoder.AV1_NVENC, 1280, 720, cq, preset, cancellationToken));
//GpuTasks.Add(Encode(Encoder.AV1_NVENC, 1920, 1080, cq, preset, cancellationToken));
GpuTasks.Add(Encode(Encoder.AV1_NVENC, W, H, cq, preset, cancellationToken));
GpuTasks.Add(Encode(Encoder.HEVC_NVENC, 1280, 720, cq, preset, cancellationToken));
GpuTasks.Add(Encode(Encoder.HEVC_NVENC, 1920, 1080, cq, preset, cancellationToken));
//GpuTasks.Add(Encode(Encoder.HEVC_NVENC, 1280, 720, cq, preset, cancellationToken));
//GpuTasks.Add(Encode(Encoder.HEVC_NVENC, 1920, 1080, cq, preset, cancellationToken));
GpuTasks.Add(Encode(Encoder.HEVC_NVENC, W, H, cq, preset, cancellationToken));
GpuTasks.Add(Encode(Encoder.H264_NVENC, 1280, 720, cq, preset, cancellationToken));
GpuTasks.Add(Encode(Encoder.H264_NVENC, 1920, 1080, cq, preset, cancellationToken));
GpuTasks.Add(Encode(Encoder.H264_NVENC, W, H, cq, preset, cancellationToken));
//GpuTasks.Add(Encode(Encoder.H264_NVENC, 1280, 720, cq, preset, cancellationToken));
//GpuTasks.Add(Encode(Encoder.H264_NVENC, 1920, 1080, cq, preset, cancellationToken));
//GpuTasks.Add(Encode(Encoder.H264_NVENC, W, H, cq, preset, cancellationToken));
}
for (int preset = 4; preset <= 7; preset++) {
CpuTasks.Add(Encode(Encoder.AV1_CPU, 1280, 720, cq, preset, cancellationToken));
CpuTasks.Add(Encode(Encoder.AV1_CPU, 1920, 1080, cq, preset, cancellationToken));
for (int preset = 5; preset <= 7; preset++) {
//CpuTasks.Add(Encode(Encoder.AV1_CPU, 1280, 720, cq, preset, cancellationToken));
//CpuTasks.Add(Encode(Encoder.AV1_CPU, 1920, 1080, cq, preset, cancellationToken));
CpuTasks.Add(Encode(Encoder.AV1_CPU, W, H, cq, preset, cancellationToken));
}
for (int preset = 3; preset <= 5; preset++) {
CpuTasks.Add(Encode(Encoder.HEVC_CPU, 1280, 720, cq, preset, cancellationToken));
CpuTasks.Add(Encode(Encoder.HEVC_CPU, 1920, 1080, cq, preset, cancellationToken));
//CpuTasks.Add(Encode(Encoder.HEVC_CPU, 1280, 720, cq, preset, cancellationToken));
//CpuTasks.Add(Encode(Encoder.HEVC_CPU, 1920, 1080, cq, preset, cancellationToken));
CpuTasks.Add(Encode(Encoder.HEVC_CPU, W, H, cq, preset, cancellationToken));
CpuTasks.Add(Encode(Encoder.H264_CPU, 1280, 720, cq, preset, cancellationToken));
CpuTasks.Add(Encode(Encoder.H264_CPU, 1920, 1080, cq, preset, cancellationToken));
CpuTasks.Add(Encode(Encoder.H264_CPU, W, H, cq, preset, cancellationToken));
//CpuTasks.Add(Encode(Encoder.H264_CPU, 1280, 720, cq, preset, cancellationToken));
//CpuTasks.Add(Encode(Encoder.H264_CPU, 1920, 1080, cq, preset, cancellationToken));
//CpuTasks.Add(Encode(Encoder.H264_CPU, W, H, cq, preset, cancellationToken));
}
}
@@ -84,26 +84,30 @@ while (CpuTasks.Count(x => x.IsCompleted) < CpuTasks.Count) {
Console.WriteLine(CpuTasks.Count(x => !x.IsCompleted) + " CPU tasks remaining...");
}
//Calculate VMAF score in parallel
//Calculate VMAF, PSNR and SSIM scores
Console.WriteLine("Computing VMAF Scores:");
Dictionary<EncodeStats,Task<float>> vmafResults = new();
var tasks = GpuTasks.Concat(CpuTasks).OfType<Task<EncodeStats>>().Select(x => x.Result).ToList();
while (tasks.Count > 0) {
var vmafTasks = tasks.Take(MAX_JOBS);
tasks = tasks.Skip(MAX_JOBS).ToList();
foreach (var task in vmafTasks)
vmafResults.Add(task, ComputeVMAFScore(inputFile, task.Filename));
Task.WhenAll(vmafResults.Values).Wait();
Console.WriteLine(tasks.Count + " VMAF tasks remaining...");
Dictionary<EncodeStats,Task<Tuple<float,float>>> psnrResults = new();
Dictionary<EncodeStats,Task<float>> ssimResults = new();
var taskList = GpuTasks.Concat(CpuTasks).OfType<Task<EncodeStats>>().Select(x => x.Result).ToList();
foreach (var task in taskList) {
var encodeStats = task;
vmafResults[encodeStats] = ComputeVMAFScore(inputFile, encodeStats.Filename);
psnrResults[encodeStats] = ComputePSNRScore(inputFile, encodeStats.Filename);
ssimResults[encodeStats] = ComputeSSIMScore(inputFile, encodeStats.Filename);
}
Console.WriteLine("Computing Results:");
foreach (var result in vmafResults) {
//"Width\tHeight\tPreset\tCRF\tSize Ratio\tEncode Time\tSpeed\n";
var es = result.Key;
foreach (var result in taskList) {
//"Width\tHeight\tPreset\tCRF\tSize Ratio\tEncode Time\tSpeed\tVMAF\tSSIM\tPSNR_avg\tPSNR_MSE\n"
var es = result;
string line = $"{es.Width}\t{es.Height}\t{es.preset}\t{es.CRF}\t{es.SizeRatio:F3}\t{es.EncodeTime:g}\t"+
$"{(es.EncodeTime.TotalSeconds / mediaInfo.Duration.TotalSeconds):F3}x\t{result.Value.Result:F2}\n";
$"{(mediaInfo.Duration.TotalSeconds / es.EncodeTime.TotalSeconds):F3}x"+
$"\t{vmafResults[result]:F2}\n" + //VMAF
$"\t{ssimResults[result]:F4}\t" + //SSIM
$"\t{psnrResults[result].Result.Item1:F2}\t" + //PSNR_avg
$"{psnrResults[result].Result.Item2:F2}"; //PSNR_MSE
Console.WriteLine(line);
switch (es.EncoderType) {
case Encoder.AV1_NVENC: AV1GPUStats.Append(line); break;
@@ -116,10 +120,10 @@ foreach (var result in vmafResults) {
}
File.WriteAllText(Path.Combine(outputDir, "AV1_NVENC_Stats.tsv"), AV1GPUStats.ToString());
File.WriteAllText(Path.Combine(outputDir, "HEVC_NVENC_Stats.tsv"), HEVCGPUStats.ToString());
File.WriteAllText(Path.Combine(outputDir, "H264_NVENC_Stats.tsv"), H264GPUStats.ToString());
//File.WriteAllText(Path.Combine(outputDir, "H264_NVENC_Stats.tsv"), H264GPUStats.ToString());
File.WriteAllText(Path.Combine(outputDir, "AV1_CPU_Stats.tsv"), AV1CPUStats.ToString());
File.WriteAllText(Path.Combine(outputDir, "HEVC_CPU_Stats.tsv"), HEVCCPUStats.ToString());
File.WriteAllText(Path.Combine(outputDir, "H264_CPU_Stats.tsv"), H264CPUStats.ToString());
//File.WriteAllText(Path.Combine(outputDir, "H264_CPU_Stats.tsv"), H264CPUStats.ToString());
cancellationTokenSource.Cancel();
return;
@@ -259,7 +263,7 @@ async Task<float> ComputeVMAFScore(string referenceFile, string distortedFile) {
float VMAF = -1;
ProcessStartInfo startInfo = new ProcessStartInfo {
FileName = Path.Combine(ffmpegPath, "ffmpeg"),
Arguments = $"-i \"{distortedFile}\" -i \"{referenceFile}\" -lavfi libvmaf -f null -",
Arguments = $"-i \"{distortedFile}\" -i \"{referenceFile}\" -lavfi libvmaf=n_threads=32 -f null -",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
@@ -281,6 +285,67 @@ async Task<float> ComputeVMAFScore(string referenceFile, string distortedFile) {
return VMAF;
}
/// <summary>
/// Computes the PSNR score between a reference and distorted video file.
/// </summary>
/// <param name="referenceFile">The path to the reference video file.</param>
/// <param name="distortedFile">The path to the distorted video file.</param>
/// <returns>A tuple containing the average PSNR and average MSE values.</returns>
async Task<Tuple<float,float>> ComputePSNRScore(string referenceFile, string distortedFile) {
float PSNR = -1;
float MSE = -1;
ProcessStartInfo startInfo = new ProcessStartInfo {
FileName = Path.Combine(ffmpegPath, "ffmpeg"),
Arguments = $"-i \"{distortedFile}\" -i \"{referenceFile}\" -lavfi psnr=stats_file={Path.GetFileNameWithoutExtension(distortedFile)}.psnr:stats_version=2 -f null -",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using Process ffmpeg = Process.Start(startInfo)!;
string output = ffmpeg.StandardOutput.ReadToEnd();
await ffmpeg.WaitForExitAsync();
// Parse PSNR score from stats file
var PsnrLog = File.ReadAllLines(Path.Combine(Path.GetDirectoryName(distortedFile)!, $"{Path.GetFileNameWithoutExtension(distortedFile)}.psnr"));
List<float> mseVals = new();
List<float> psnrVals = new();
foreach (var line in PsnrLog) {
if (!line.Contains("mse_avg:")) continue; // Skip header and irrelevant lines
string[] parts = line.Split(' ');
foreach (var part in parts) {
if (part.StartsWith("psnr_avg:")) psnrVals.Add(float.Parse(part.Split(':')[1]));
if (part.StartsWith("mse_avg:")) mseVals.Add(float.Parse(part.Split(':')[1]));
}
}
PSNR = psnrVals.Count > 0 ? psnrVals.Average() : -1;
MSE = mseVals.Count > 0 ? mseVals.Average() : -1;
return new (PSNR, MSE);
}
async Task<float> ComputeSSIMScore(string referenceFile, string distortedFile) {
float SSIM = -1;
ProcessStartInfo startInfo = new ProcessStartInfo {
FileName = Path.Combine(ffmpegPath, "ffmpeg"),
Arguments = $"-i \"{distortedFile}\" -i \"{referenceFile}\" -lavfi ssim -f null -",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using Process ffmpeg = Process.Start(startInfo)!;
string output = ffmpeg.StandardOutput.ReadToEnd();
await ffmpeg.WaitForExitAsync();
// Parse SSIM score from output
string marker = "All:";
int index = output.LastIndexOf(marker, StringComparison.Ordinal);
if (index == -1) return SSIM;
int start = index + marker.Length;
int end = output.IndexOf(" ", start, StringComparison.Ordinal);
string scoreStr = output.Substring(start, end - start).Trim();
if (float.TryParse(scoreStr, out float score)) SSIM = score;
return SSIM;
}
Speed ToX264Speed(int p) => p switch {
1 => Speed.VerySlow,
2 => Speed.Slower,