mirror of
https://github.com/n00mkrad/flowframes.git
synced 2025-12-22 03:09:28 +01:00
Better CFR-based encoding, fixed dupe frame when using loop
This commit is contained in:
@@ -92,17 +92,17 @@ namespace Flowframes
|
||||
DeleteSource(inputFile);
|
||||
}
|
||||
|
||||
public static async Task FramesToVideoVfr(string framesFile, string outPath, Interpolate.OutMode outMode, float fps, AvProcess.LogMode logMode = AvProcess.LogMode.OnlyLastLine, bool isChunk = false)
|
||||
public static async Task FramesToVideoConcat(string framesFile, string outPath, Interpolate.OutMode outMode, float fps, AvProcess.LogMode logMode = AvProcess.LogMode.OnlyLastLine, bool isChunk = false)
|
||||
{
|
||||
if (logMode != AvProcess.LogMode.Hidden)
|
||||
Logger.Log($"Encoding video...");
|
||||
string encArgs = Utils.GetEncArgs(Utils.GetCodec(outMode)) + " -pix_fmt yuv420p ";
|
||||
if (!isChunk) encArgs += $"-movflags +faststart";
|
||||
string vfrFilename = Path.GetFileName(framesFile);
|
||||
string vsync = (Interpolate.current.interpFactor == 2) ? "-vsync 1" : "-vsync 2";
|
||||
//string vsync = (Interpolate.current.interpFactor == 2) ? "-vsync 1" : "-vsync 2";
|
||||
string rate = fps.ToString().Replace(",", ".");
|
||||
string extraArgs = Config.Get("ffEncArgs");
|
||||
string args = $"{vsync} -f concat -i {vfrFilename} -r {rate} {encArgs} {extraArgs} -threads {Config.GetInt("ffEncThreads")} {outPath.Wrap()}";
|
||||
string args = $"-loglevel error -vsync 0 -f concat -r {rate} -i {vfrFilename} {encArgs} {extraArgs} -threads {Config.GetInt("ffEncThreads")} {outPath.Wrap()}";
|
||||
//string args = $"-vsync 0 -f concat -i {vfrFilename} {encArgs} {extraArgs} -threads {Config.GetInt("ffEncThreads")} {outPath.Wrap()}";
|
||||
await AvProcess.RunFfmpeg(args, framesFile.GetParentDir(), logMode);
|
||||
}
|
||||
@@ -183,7 +183,7 @@ namespace Flowframes
|
||||
public static async Task Encode(string inputFile, string vcodec, string acodec, int crf, int audioKbps = 0, bool delSrc = false)
|
||||
{
|
||||
string outPath = Path.ChangeExtension(inputFile, null) + "-convert.mp4";
|
||||
string args = $" -i {inputFile.Wrap()} -c:v {vcodec} -crf {crf} -pix_fmt yuv420p -c:a {acodec} -b:a {audioKbps}k {outPath.Wrap()}";
|
||||
string args = $" -i {inputFile.Wrap()} -c:v {vcodec} -crf {crf} -pix_fmt yuv420p -c:a {acodec} -b:a {audioKbps}k -vf {divisionFilter} {outPath.Wrap()}";
|
||||
if (string.IsNullOrWhiteSpace(acodec))
|
||||
args = args.Replace("-c:a", "-an");
|
||||
if (audioKbps < 0)
|
||||
|
||||
@@ -155,5 +155,15 @@ namespace Flowframes
|
||||
|
||||
return str.Remove(place, stringToReplace.Length).Insert(place, replaceWith);
|
||||
}
|
||||
|
||||
public static string[] SplitBy (this string str, string splitBy)
|
||||
{
|
||||
return str.Split(new string[] { splitBy }, StringSplitOptions.None);
|
||||
}
|
||||
|
||||
public static string RemoveComments (this string str)
|
||||
{
|
||||
return str.Split('#')[0].SplitBy("//")[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,9 @@ namespace Flowframes.Magick
|
||||
|
||||
bool hasReachedEnd = false;
|
||||
|
||||
string infoFile = Path.Combine(path.GetParentDir(), $"dupes.ini");
|
||||
string fileContent = "";
|
||||
|
||||
for (int i = 0; i < framePaths.Length; i++) // Loop through frames
|
||||
{
|
||||
if (hasReachedEnd)
|
||||
@@ -133,16 +136,17 @@ namespace Flowframes.Magick
|
||||
}
|
||||
else
|
||||
{
|
||||
fileContent += $"{Path.GetFileNameWithoutExtension(framePaths[i].Name)}:{currentDupeCount}\n";
|
||||
statsFramesKept++;
|
||||
currentOutFrame++;
|
||||
currentDupeCount = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
if (sw.ElapsedMilliseconds >= 1000 || (i+1) == framePaths.Length)
|
||||
if (sw.ElapsedMilliseconds >= 1000 || (i+1) == framePaths.Length) // Print every 1s (or when done)
|
||||
{
|
||||
sw.Restart();
|
||||
Logger.Log($"[FrameDedup] Difference from {Path.GetFileName(frame1)} to {Path.GetFileName(frame2)}: {diff.ToString("0.00")}% - {delStr}. Total: {framePaths.Length - statsFramesDeleted} kept / {statsFramesDeleted} deleted.", false, true);
|
||||
Logger.Log($"[FrameDedup] Difference from {Path.GetFileName(frame1)} to {Path.GetFileName(frame2)}: {diff.ToString("0.00")}% - {delStr}.", false, true);
|
||||
Program.mainForm.SetProgress((int)Math.Round(((float)i / framePaths.Length) * 100f));
|
||||
if (imageCache.Count > bufferSize || (imageCache.Count > 50 && OSUtils.GetFreeRamMb() < 2500))
|
||||
ClearCache();
|
||||
@@ -169,6 +173,8 @@ namespace Flowframes.Magick
|
||||
}
|
||||
}
|
||||
|
||||
File.WriteAllText(infoFile, fileContent);
|
||||
|
||||
foreach (string frame in framesToDelete)
|
||||
IOUtils.TryDeleteIfExists(frame);
|
||||
|
||||
@@ -176,10 +182,15 @@ namespace Flowframes.Magick
|
||||
|
||||
if (Interpolate.canceled) return;
|
||||
|
||||
int framesLeft = IOUtils.GetAmountOfFiles(path, false, $"*.png");
|
||||
int framesDeleted = framePaths.Length - framesLeft;
|
||||
float percentDeleted = ((float)framesDeleted / framePaths.Length) * 100f;
|
||||
string keptPercent = $"{(100f - percentDeleted).ToString("0.0")}%";
|
||||
|
||||
if (skipped)
|
||||
Logger.Log($"[FrameDedup] First {skipAfterNoDupesFrames} frames did not have any duplicates - Skipping the rest!", false, true);
|
||||
else
|
||||
Logger.Log($"[FrameDedup]{testStr} Done. Kept {statsFramesKept} frames, deleted {statsFramesDeleted} frames.", false, true);
|
||||
Logger.Log($"[FrameDedup]{testStr} Done. Kept {framesLeft} ({keptPercent}) frames, deleted {framesDeleted} frames.", false, true);
|
||||
|
||||
if (statsFramesKept <= 0)
|
||||
Interpolate.Cancel("No frames were left after de-duplication!\n\nTry decreasing the de-duplication threshold.");
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace Flowframes.Main
|
||||
bool h265 = Config.GetInt("mp4Enc") == 1;
|
||||
int crf = h265 ? Config.GetInt("h265Crf") : Config.GetInt("h264Crf");
|
||||
|
||||
await FFmpegCommands.FramesToVideoVfr(vfrFile, outPath, mode, fps);
|
||||
await FFmpegCommands.FramesToVideoConcat(vfrFile, outPath, mode, fps);
|
||||
await MergeAudio(i.current.inPath, outPath);
|
||||
|
||||
if (changeFps > 0)
|
||||
@@ -185,7 +185,7 @@ namespace Flowframes.Main
|
||||
string vfrFile = Path.Combine(i.current.tempFolder, $"vfr-chunk-{firstFrameNum}-{firstFrameNum + framesAmount}.ini");
|
||||
File.WriteAllLines(vfrFile, IOUtils.ReadLines(vfrFileOriginal).Skip(firstFrameNum * 2).Take(framesAmount * 2));
|
||||
|
||||
await FFmpegCommands.FramesToVideoVfr(vfrFile, outPath, mode, i.current.outFps, AvProcess.LogMode.Hidden, true);
|
||||
await FFmpegCommands.FramesToVideoConcat(vfrFile, outPath, mode, i.current.outFps, AvProcess.LogMode.Hidden, true);
|
||||
}
|
||||
|
||||
static async Task Loop(string outPath, int looptimes)
|
||||
|
||||
@@ -14,14 +14,25 @@ namespace Flowframes.Main
|
||||
{
|
||||
class FrameTiming
|
||||
{
|
||||
public enum Mode { CFR, VFR }
|
||||
public static int timebase = 10000;
|
||||
|
||||
public static async Task CreateTimecodeFiles(string framesPath, bool loopEnabled, int times, bool noTimestamps)
|
||||
public static async Task CreateTimecodeFiles(string framesPath, Mode mode, bool loopEnabled, int times, bool noTimestamps)
|
||||
{
|
||||
Logger.Log("Generating timecodes...");
|
||||
try
|
||||
{
|
||||
if (mode == Mode.VFR)
|
||||
await CreateTimecodeFile(framesPath, loopEnabled, times, false, noTimestamps);
|
||||
if (mode == Mode.CFR)
|
||||
await CreateEncFile(framesPath, loopEnabled, times, false);
|
||||
Logger.Log($"Generating timecodes... Done.", false, true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log($"Error generating timecodes: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task CreateTimecodeFile(string framesPath, bool loopEnabled, int interpFactor, bool notFirstRun, bool noTimestamps)
|
||||
{
|
||||
@@ -138,5 +149,124 @@ namespace Flowframes.Main
|
||||
File.Copy(frameFiles.First().FullName, loopFrameTargetPath);
|
||||
}
|
||||
}
|
||||
|
||||
static Dictionary<string, int> dupesDict = new Dictionary<string, int>();
|
||||
|
||||
static void LoadDupesFile (string path)
|
||||
{
|
||||
if (!File.Exists(path)) return;
|
||||
dupesDict.Clear();
|
||||
string[] dupesFileLines = IOUtils.ReadLines(path);
|
||||
foreach(string line in dupesFileLines)
|
||||
{
|
||||
string[] values = line.Split(':');
|
||||
dupesDict.Add(values[0], values[1].GetInt());
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task CreateEncFile (string framesPath, bool loopEnabled, int interpFactor, bool notFirstRun)
|
||||
{
|
||||
if (Interpolate.canceled) return;
|
||||
Logger.Log($"Generating timecodes for {interpFactor}x...", false, true);
|
||||
|
||||
bool loop = Config.GetBool("enableLoop");
|
||||
bool sceneDetection = true;
|
||||
string ext = InterpolateUtils.GetOutExt();
|
||||
|
||||
FileInfo[] frameFiles = new DirectoryInfo(framesPath).GetFiles($"*.png");
|
||||
string vfrFile = Path.Combine(framesPath.GetParentDir(), $"vfr-{interpFactor}x.ini");
|
||||
string fileContent = "";
|
||||
string dupesFile = Path.Combine(framesPath.GetParentDir(), $"dupes.ini");
|
||||
LoadDupesFile(dupesFile);
|
||||
|
||||
string scnFramesPath = Path.Combine(framesPath.GetParentDir(), Paths.scenesDir);
|
||||
string interpPath = Paths.interpDir;
|
||||
|
||||
List<string> sceneFrames = new List<string>();
|
||||
if (Directory.Exists(scnFramesPath))
|
||||
sceneFrames = Directory.GetFiles(scnFramesPath).Select(file => Path.GetFileNameWithoutExtension(file)).ToList();
|
||||
|
||||
int totalFileCount = 1;
|
||||
for (int i = 0; i < (frameFiles.Length - 1); i++)
|
||||
{
|
||||
if (Interpolate.canceled) return;
|
||||
|
||||
int interpFramesAmount = interpFactor;
|
||||
string inputFilenameNoExt = Path.GetFileNameWithoutExtension(frameFiles[i].Name);
|
||||
int dupesAmount = dupesDict.ContainsKey(inputFilenameNoExt) ? dupesDict[inputFilenameNoExt] : 0;
|
||||
//Logger.Log($"{Path.GetFileNameWithoutExtension(frameFiles[i].Name)} has {dupesAmount} dupes", true);
|
||||
|
||||
bool discardThisFrame = (sceneDetection && (i + 2) < frameFiles.Length && sceneFrames.Contains(Path.GetFileNameWithoutExtension(frameFiles[i + 1].Name))); // i+2 is in scene detection folder, means i+1 is ugly interp frame
|
||||
|
||||
// TODO: Check if this is needed
|
||||
// If loop is enabled, account for the extra frame added to the end for loop continuity
|
||||
if (loopEnabled && i == (frameFiles.Length - 2))
|
||||
interpFramesAmount = interpFramesAmount * 2;
|
||||
|
||||
//Logger.Log($"Writing out frames for in frame {i} which has {dupesAmount} dupes", true);
|
||||
// Generate frames file lines
|
||||
for (int frm = 0; frm < interpFramesAmount; frm++)
|
||||
{
|
||||
//Logger.Log($"Writing out frame {frm+1}/{interpFramesAmount}", true);
|
||||
|
||||
|
||||
if (discardThisFrame && totalFileCount > 1) // If frame is scene cut frame
|
||||
{
|
||||
int lastNum = totalFileCount;
|
||||
|
||||
//Logger.Log($"Writing frame {totalFileCount} [Discarding Next]", true);
|
||||
fileContent += $"file '{interpPath}/{totalFileCount.ToString().PadLeft(Padding.interpFrames, '0')}.{ext}'\n";
|
||||
totalFileCount++;
|
||||
|
||||
//Logger.Log("Discarding interp frames with out num " + totalFileCount);
|
||||
for (int dupeCount = 1; dupeCount < interpFramesAmount; dupeCount++)
|
||||
{
|
||||
//Logger.Log($"Writing frame {totalFileCount} which is actually repeated frame {lastNum}");
|
||||
fileContent += $"file '{interpPath}/{lastNum.ToString().PadLeft(Padding.interpFrames, '0')}.{ext}'\n";
|
||||
totalFileCount++;
|
||||
}
|
||||
|
||||
frm = interpFramesAmount;
|
||||
}
|
||||
else
|
||||
{
|
||||
for(int writtenDupes = -1; writtenDupes < dupesAmount; writtenDupes++) // Write duplicates
|
||||
{
|
||||
//Logger.Log($"Writing frame {totalFileCount}", true, false);
|
||||
fileContent += $"file '{interpPath}/{totalFileCount.ToString().PadLeft(Padding.interpFrames, '0')}.{ext}'\n";
|
||||
}
|
||||
totalFileCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if ((i + 1) % 100 == 0)
|
||||
await Task.Delay(1);
|
||||
}
|
||||
|
||||
// Use average frame duration for last frame - TODO: Use real duration??
|
||||
//string durationStrLast = ((totalDuration / (totalFileCount - 1)) / timebase).ToString("0.0000000", CultureInfo.InvariantCulture);
|
||||
fileContent += $"file '{interpPath}/{totalFileCount.ToString().PadLeft(Padding.interpFrames, '0')}.{ext}'\n";
|
||||
totalFileCount++;
|
||||
|
||||
string finalFileContent = fileContent.Trim();
|
||||
if(loop)
|
||||
finalFileContent = finalFileContent.Remove(finalFileContent.LastIndexOf("\n"));
|
||||
File.WriteAllText(vfrFile, finalFileContent);
|
||||
|
||||
if (notFirstRun) return; // Skip all steps that only need to be done once
|
||||
|
||||
if (loop)
|
||||
{
|
||||
int lastFileNumber = frameFiles.Last().Name.GetInt() + 1;
|
||||
string loopFrameTargetPath = Path.Combine(frameFiles.First().FullName.GetParentDir(), lastFileNumber.ToString().PadLeft(Padding.inputFrames, '0') + $".png");
|
||||
if (File.Exists(loopFrameTargetPath))
|
||||
{
|
||||
Logger.Log($"Won't copy loop frame - {Path.GetFileName(loopFrameTargetPath)} already exists.", true);
|
||||
return;
|
||||
}
|
||||
File.Copy(frameFiles.First().FullName, loopFrameTargetPath);
|
||||
Logger.Log($"Copied loop frame to {loopFrameTargetPath}.", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ namespace Flowframes
|
||||
if (canceled) return;
|
||||
|
||||
bool useTimestamps = Config.GetInt("timingMode") == 1; // TODO: Auto-Disable timestamps if input frames are sequential, not timestamped
|
||||
await FrameTiming.CreateTimecodeFiles(current.framesFolder, Config.GetBool("enableLoop"), current.interpFactor, !useTimestamps);
|
||||
await FrameTiming.CreateTimecodeFiles(current.framesFolder, FrameTiming.Mode.CFR, Config.GetBool("enableLoop"), current.interpFactor, !useTimestamps);
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace Flowframes.Main
|
||||
public static int currentFactor;
|
||||
public static async void GetProgressByFrameAmount(string outdir, int target)
|
||||
{
|
||||
Logger.Log($"Starting GetProgressByFrameAmount() loop for outdir '{outdir}', target is {target} frames", true);
|
||||
bool firstProgUpd = true;
|
||||
Program.mainForm.SetProgress(0);
|
||||
targetFrames = target;
|
||||
|
||||
Reference in New Issue
Block a user