diff --git a/EncodingSampleTest/Program.cs b/EncodingSampleTest/Program.cs index 4282e07..2b05849 100644 --- a/EncodingSampleTest/Program.cs +++ b/EncodingSampleTest/Program.cs @@ -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> vmafResults = new(); -var tasks = GpuTasks.Concat(CpuTasks).OfType>().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>> psnrResults = new(); +Dictionary> ssimResults = new(); +var taskList = GpuTasks.Concat(CpuTasks).OfType>().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 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 ComputeVMAFScore(string referenceFile, string distortedFile) { return VMAF; } +/// +/// Computes the PSNR score between a reference and distorted video file. +/// +/// The path to the reference video file. +/// The path to the distorted video file. +/// A tuple containing the average PSNR and average MSE values. +async Task> 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 mseVals = new(); + List 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 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,