mirror of
https://github.com/n00mkrad/flowframes.git
synced 2025-12-24 20:29:26 +01:00
Case-sensitive rename operation (1/2)
This commit is contained in:
@@ -1,166 +0,0 @@
|
||||
using Flowframes.Data;
|
||||
using Flowframes.MiscUtils;
|
||||
using ImageMagick;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Flowframes.IO;
|
||||
|
||||
namespace Flowframes.Magick
|
||||
{
|
||||
class Blend
|
||||
{
|
||||
public static async Task BlendSceneChanges(string framesFilePath, bool setStatus = true)
|
||||
{
|
||||
Stopwatch sw = new Stopwatch();
|
||||
sw.Restart();
|
||||
int totalFrames = 0;
|
||||
|
||||
string keyword = "SCN:";
|
||||
string fileContent = File.ReadAllText(framesFilePath);
|
||||
|
||||
if (!fileContent.Contains(keyword))
|
||||
{
|
||||
Logger.Log("Skipping BlendSceneChanges as there are no scene changes in this frames file.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
string[] framesLines = fileContent.SplitIntoLines(); // Array with frame filenames
|
||||
|
||||
string oldStatus = Program.mainForm.GetStatus();
|
||||
|
||||
if (setStatus)
|
||||
Program.mainForm.SetStatus("Blending scene transitions...");
|
||||
|
||||
int amountOfBlendFrames = (int)Interpolate.current.interpFactor - 1;
|
||||
|
||||
string[] frames = FrameRename.framesAreRenamed ? new string[0] : IOUtils.GetFilesSorted(Interpolate.current.framesFolder);
|
||||
|
||||
List<Task> runningTasks = new List<Task>();
|
||||
int maxThreads = Environment.ProcessorCount * 2;
|
||||
|
||||
foreach (string line in framesLines)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (line.Contains(keyword))
|
||||
{
|
||||
string trimmedLine = line.Split(keyword).Last();
|
||||
string[] inputFrameNames = trimmedLine.Split('>');
|
||||
string frameFrom = FrameRename.framesAreRenamed ? inputFrameNames[0] : frames[inputFrameNames[0].GetInt()];
|
||||
string frameTo = FrameRename.framesAreRenamed ? inputFrameNames[1] : frames[inputFrameNames[1].GetInt()];
|
||||
|
||||
string img1 = Path.Combine(Interpolate.current.framesFolder, frameFrom);
|
||||
string img2 = Path.Combine(Interpolate.current.framesFolder, frameTo);
|
||||
|
||||
string firstOutputFrameName = line.Split('/').Last().Remove("'").Split('#').First();
|
||||
string ext = Path.GetExtension(firstOutputFrameName);
|
||||
int firstOutputFrameNum = firstOutputFrameName.GetInt();
|
||||
List<string> outputFilenames = new List<string>();
|
||||
|
||||
for (int blendFrameNum = 1; blendFrameNum <= amountOfBlendFrames; blendFrameNum++)
|
||||
{
|
||||
int outputNum = firstOutputFrameNum + blendFrameNum;
|
||||
string outputPath = Path.Combine(Interpolate.current.interpFolder, outputNum.ToString().PadLeft(Padding.interpFrames, '0'));
|
||||
outputPath = Path.ChangeExtension(outputPath, ext);
|
||||
outputFilenames.Add(outputPath);
|
||||
}
|
||||
|
||||
if (runningTasks.Count >= maxThreads)
|
||||
{
|
||||
do
|
||||
{
|
||||
await Task.Delay(10);
|
||||
RemoveCompletedTasks(runningTasks);
|
||||
} while (runningTasks.Count >= maxThreads);
|
||||
}
|
||||
|
||||
Logger.Log($"Starting task for transition {inputFrameNames[0]} > {inputFrameNames[1]} ({runningTasks.Count}/{maxThreads} running)", true);
|
||||
Task newTask = Task.Run(() => BlendImages(img1, img2, outputFilenames.ToArray()));
|
||||
runningTasks.Add(newTask);
|
||||
totalFrames += outputFilenames.Count;
|
||||
|
||||
await Task.Delay(1);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log("Failed to blend scene changes: " + e.Message, true);
|
||||
}
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
RemoveCompletedTasks(runningTasks);
|
||||
|
||||
if (runningTasks.Count < 1)
|
||||
break;
|
||||
|
||||
await Task.Delay(10);
|
||||
}
|
||||
|
||||
Logger.Log($"Created {totalFrames} blend frames in {FormatUtils.TimeSw(sw)} ({(totalFrames / (sw.ElapsedMilliseconds / 1000f)).ToString("0.00")} FPS)", true);
|
||||
|
||||
if (setStatus)
|
||||
Program.mainForm.SetStatus(oldStatus);
|
||||
}
|
||||
|
||||
static void RemoveCompletedTasks(List<Task> runningTasks)
|
||||
{
|
||||
foreach (Task task in new List<Task>(runningTasks))
|
||||
{
|
||||
if (task.IsCompleted)
|
||||
runningTasks.Remove(task);
|
||||
}
|
||||
}
|
||||
|
||||
public static void BlendImages(string img1Path, string img2Path, string imgOutPath)
|
||||
{
|
||||
MagickImage img1 = new MagickImage(img1Path);
|
||||
MagickImage img2 = new MagickImage(img2Path);
|
||||
img2.Alpha(AlphaOption.Opaque);
|
||||
img2.Evaluate(Channels.Alpha, EvaluateOperator.Set, new Percentage(50));
|
||||
img1.Composite(img2, Gravity.Center, CompositeOperator.Over);
|
||||
img1.Format = MagickFormat.Png24;
|
||||
img1.Quality = 10;
|
||||
img1.Write(imgOutPath);
|
||||
}
|
||||
|
||||
public static async Task BlendImages(string img1Path, string img2Path, string[] imgOutPaths)
|
||||
{
|
||||
try
|
||||
{
|
||||
MagickImage img1 = new MagickImage(img1Path);
|
||||
MagickImage img2 = new MagickImage(img2Path);
|
||||
|
||||
int alphaFraction = (100f / (imgOutPaths.Length + 1)).RoundToInt(); // Alpha percentage per image
|
||||
int currentAlpha = alphaFraction;
|
||||
|
||||
foreach (string imgOutPath in imgOutPaths)
|
||||
{
|
||||
string outPath = imgOutPath.Trim();
|
||||
|
||||
MagickImage img1Inst = new MagickImage(img1);
|
||||
MagickImage img2Inst = new MagickImage(img2);
|
||||
|
||||
img2Inst.Alpha(AlphaOption.Opaque);
|
||||
img2Inst.Evaluate(Channels.Alpha, EvaluateOperator.Set, new Percentage(currentAlpha));
|
||||
currentAlpha += alphaFraction;
|
||||
|
||||
img1Inst.Composite(img2Inst, Gravity.Center, CompositeOperator.Over);
|
||||
img1Inst.Format = MagickFormat.Png24;
|
||||
img1Inst.Quality = 10;
|
||||
img1Inst.Write(outPath);
|
||||
await Task.Delay(1);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log("BlendImages Error: " + e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
using Flowframes;
|
||||
using Flowframes.IO;
|
||||
using Flowframes.UI;
|
||||
using ImageMagick;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Flowframes.Magick
|
||||
{
|
||||
|
||||
class Converter
|
||||
{
|
||||
public static async Task Convert (string dir, MagickFormat format, int quality, string ext = "", bool print = true, bool setProgress = true)
|
||||
{
|
||||
var files = IOUtils.GetFilesSorted(dir);
|
||||
if(print) Logger.Log($"Converting {files.Length} files in {dir}");
|
||||
int counter = 0;
|
||||
foreach (string file in files)
|
||||
{
|
||||
if (print) Logger.Log("Converting " + Path.GetFileName(file) + " to " + format.ToString().StripNumbers().ToUpper(), false, true);
|
||||
MagickImage img = new MagickImage(file);
|
||||
img.Format = format;
|
||||
img.Quality = quality;
|
||||
string outpath = file;
|
||||
if (!string.IsNullOrWhiteSpace(ext)) outpath = Path.ChangeExtension(outpath, ext);
|
||||
img.Write(outpath);
|
||||
counter++;
|
||||
if(setProgress)
|
||||
Program.mainForm.SetProgress((int)Math.Round(((float)counter / files.Length) * 100f));
|
||||
await Task.Delay(1);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task MakeBinary (string inputDir, string outputDir, bool print = true, bool setProgress = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var files = IOUtils.GetFilesSorted(inputDir);
|
||||
if (print) Logger.Log($"Processing alpha channel...");
|
||||
Directory.CreateDirectory(outputDir);
|
||||
Stopwatch sw = new Stopwatch();
|
||||
sw.Restart();
|
||||
int counter = 0;
|
||||
foreach (string file in files)
|
||||
{
|
||||
MagickImage img = new MagickImage(file);
|
||||
img.Format = MagickFormat.Png24;
|
||||
img.Quality = 10;
|
||||
img.Threshold(new Percentage(75));
|
||||
|
||||
string outPath = Path.Combine(outputDir, Path.GetFileName(file));
|
||||
img.Write(outPath);
|
||||
counter++;
|
||||
if (sw.ElapsedMilliseconds > 250)
|
||||
{
|
||||
if (setProgress)
|
||||
Program.mainForm.SetProgress((int)Math.Round(((float)counter / files.Length) * 100f));
|
||||
await Task.Delay(1);
|
||||
sw.Restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log("MakeBinary Error: " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ExtractAlpha (string inputDir, string outputDir, bool print = true, bool setProgress = true, bool removeInputAlpha = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var files = IOUtils.GetFilesSorted(inputDir);
|
||||
if (print) Logger.Log($"Extracting alpha channel from images...");
|
||||
Directory.CreateDirectory(outputDir);
|
||||
Stopwatch sw = new Stopwatch();
|
||||
sw.Restart();
|
||||
int counter = 0;
|
||||
foreach (string file in files)
|
||||
{
|
||||
MagickImage alphaImg = new MagickImage(file);
|
||||
|
||||
if (removeInputAlpha)
|
||||
{
|
||||
MagickImage rgbImg = alphaImg;
|
||||
rgbImg.Format = MagickFormat.Png24;
|
||||
rgbImg.Quality = 10;
|
||||
MagickImage bg = new MagickImage(MagickColors.Black, rgbImg.Width, rgbImg.Height);
|
||||
bg.Composite(rgbImg, CompositeOperator.Over);
|
||||
rgbImg = bg;
|
||||
rgbImg.Write(file);
|
||||
}
|
||||
|
||||
alphaImg.Format = MagickFormat.Png24;
|
||||
alphaImg.Quality = 10;
|
||||
|
||||
alphaImg.FloodFill(MagickColors.None, 0, 0); // Fill the image with a transparent background
|
||||
alphaImg.InverseOpaque(MagickColors.None, MagickColors.White); // Change all the pixels that are not transparent to white.
|
||||
alphaImg.ColorAlpha(MagickColors.Black); // Change the transparent pixels to black.
|
||||
|
||||
string outPath = Path.Combine(outputDir, Path.GetFileName(file));
|
||||
alphaImg.Write(outPath);
|
||||
counter++;
|
||||
if (sw.ElapsedMilliseconds > 250)
|
||||
{
|
||||
if (setProgress)
|
||||
Program.mainForm.SetProgress((int)Math.Round(((float)counter / files.Length) * 100f));
|
||||
await Task.Delay(1);
|
||||
sw.Restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log("ExtractAlpha Error: " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task Preprocess (string dir, bool setProgress = true)
|
||||
{
|
||||
var files = IOUtils.GetFilesSorted(dir);
|
||||
Logger.Log($"Preprocessing {files} files in {dir}");
|
||||
int counter = 0;
|
||||
foreach (string file in files)
|
||||
{
|
||||
//Logger.Log("Converting " + Path.GetFileName(file) + " to " + format, false, true);
|
||||
MagickImage img = new MagickImage(file);
|
||||
//img.Format = MagickFormat.Bmp;
|
||||
//img.Write(file);
|
||||
//img = new MagickImage(file);
|
||||
img.Format = MagickFormat.Png24;
|
||||
img.Quality = 10;
|
||||
counter++;
|
||||
if (setProgress)
|
||||
Program.mainForm.SetProgress((int)Math.Round(((float)counter / files.Length) * 100f));
|
||||
await Task.Delay(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Flowframes.IO;
|
||||
using ImageMagick;
|
||||
using Flowframes.OS;
|
||||
using Flowframes.Data;
|
||||
using System.Drawing;
|
||||
using Paths = Flowframes.IO.Paths;
|
||||
|
||||
namespace Flowframes.Magick
|
||||
{
|
||||
class Dedupe
|
||||
{
|
||||
public enum Mode { None, Info, Enabled, Auto }
|
||||
public static Mode currentMode;
|
||||
public static float currentThreshold;
|
||||
|
||||
public static async Task Run(string path, bool testRun = false, bool setStatus = true)
|
||||
{
|
||||
if (path == null || !Directory.Exists(path) || Interpolate.canceled)
|
||||
return;
|
||||
|
||||
currentMode = Mode.Auto;
|
||||
|
||||
if(setStatus)
|
||||
Program.mainForm.SetStatus("Running frame de-duplication");
|
||||
|
||||
currentThreshold = Config.GetFloat(Config.Key.dedupThresh);
|
||||
Logger.Log("Running accurate frame de-duplication...");
|
||||
|
||||
if (currentMode == Mode.Enabled || currentMode == Mode.Auto)
|
||||
await RemoveDupeFrames(path, currentThreshold, "*", testRun, false, (currentMode == Mode.Auto));
|
||||
}
|
||||
|
||||
public static Dictionary<string, MagickImage> imageCache = new Dictionary<string, MagickImage>();
|
||||
static MagickImage GetImage(string path)
|
||||
{
|
||||
bool allowCaching = true;
|
||||
|
||||
if (!allowCaching)
|
||||
return new MagickImage(path);
|
||||
|
||||
if (!imageCache.ContainsKey(path))
|
||||
imageCache.Add(path, new MagickImage(path));
|
||||
|
||||
return imageCache[path];
|
||||
}
|
||||
|
||||
public static void ClearCache ()
|
||||
{
|
||||
imageCache.Clear();
|
||||
}
|
||||
|
||||
public static async Task RemoveDupeFrames(string path, float threshold, string ext, bool testRun = false, bool debugLog = false, bool skipIfNoDupes = false)
|
||||
{
|
||||
Stopwatch sw = new Stopwatch();
|
||||
sw.Restart();
|
||||
Logger.Log("Removing duplicate frames - Threshold: " + threshold.ToString("0.00"));
|
||||
|
||||
FileInfo[] framePaths = IOUtils.GetFileInfosSorted(path, false, "*." + ext);
|
||||
List<string> framesToDelete = new List<string>();
|
||||
|
||||
int bufferSize = await GetBufferSize();
|
||||
|
||||
int currentOutFrame = 1;
|
||||
int currentDupeCount = 0;
|
||||
|
||||
int statsFramesKept = 0;
|
||||
int statsFramesDeleted = 0;
|
||||
|
||||
int skipAfterNoDupesFrames = Config.GetInt(Config.Key.autoDedupFrames);
|
||||
bool hasEncounteredAnyDupes = false;
|
||||
bool skipped = false;
|
||||
|
||||
bool hasReachedEnd = false;
|
||||
|
||||
string fileContent = "";
|
||||
|
||||
for (int i = 0; i < framePaths.Length; i++) // Loop through frames
|
||||
{
|
||||
if (hasReachedEnd)
|
||||
break;
|
||||
|
||||
string frame1 = framePaths[i].FullName;
|
||||
|
||||
int compareWithIndex = i + 1;
|
||||
|
||||
while (true) // Loop dupes
|
||||
{
|
||||
//compareWithIndex++;
|
||||
if (compareWithIndex >= framePaths.Length)
|
||||
{
|
||||
hasReachedEnd = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (framesToDelete.Contains(framePaths[compareWithIndex].FullName) || !File.Exists(framePaths[compareWithIndex].FullName))
|
||||
{
|
||||
//Logger.Log($"Frame {compareWithIndex} was already deleted - skipping");
|
||||
compareWithIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
string frame2 = framePaths[compareWithIndex].FullName;
|
||||
float diff = GetDifference(frame1, frame2);
|
||||
|
||||
if (diff < threshold) // Is a duped frame.
|
||||
{
|
||||
if (!testRun)
|
||||
{
|
||||
framesToDelete.Add(frame2);
|
||||
if (debugLog) Logger.Log("Deduplication: Deleted " + Path.GetFileName(frame2));
|
||||
hasEncounteredAnyDupes = true;
|
||||
}
|
||||
statsFramesDeleted++;
|
||||
currentDupeCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
fileContent += $"{Path.GetFileNameWithoutExtension(framePaths[i].Name)}:{currentDupeCount}\n";
|
||||
statsFramesKept++;
|
||||
currentOutFrame++;
|
||||
currentDupeCount = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sw.ElapsedMilliseconds >= 500 || (i + 1) == framePaths.Length) // Print every 0.5s (or when done)
|
||||
{
|
||||
sw.Restart();
|
||||
Logger.Log($"Deduplication: Running de-duplication ({i}/{framePaths.Length}), deleted {statsFramesDeleted} ({(((float)statsFramesDeleted / framePaths.Length) * 100f).ToString("0")}%) duplicate frames so far...", false, true);
|
||||
Program.mainForm.SetProgress((int)Math.Round(((float)i / framePaths.Length) * 100f));
|
||||
if (imageCache.Count > bufferSize || (imageCache.Count > 50 && OSUtils.GetFreeRamMb() < 3500))
|
||||
ClearCache();
|
||||
}
|
||||
|
||||
// int oldIndex = -1; // TODO: Compare with 1st to fix loops?
|
||||
// if (i >= framePaths.Length) // If this is the last frame, compare with 1st to avoid OutOfRange error
|
||||
// {
|
||||
// oldIndex = i;
|
||||
// i = 0;
|
||||
// }
|
||||
|
||||
if (i % 3 == 0)
|
||||
await Task.Delay(1);
|
||||
|
||||
if (Interpolate.canceled) return;
|
||||
|
||||
if (!testRun && skipIfNoDupes && !hasEncounteredAnyDupes && skipAfterNoDupesFrames > 0 && i >= skipAfterNoDupesFrames)
|
||||
{
|
||||
skipped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string frame in framesToDelete)
|
||||
IOUtils.TryDeleteIfExists(frame);
|
||||
|
||||
string testStr = testRun ? " [TestRun]" : "";
|
||||
|
||||
if (Interpolate.canceled) return;
|
||||
|
||||
int framesLeft = IOUtils.GetAmountOfFiles(path, false, "*" + Interpolate.current.framesExt);
|
||||
int framesDeleted = framePaths.Length - framesLeft;
|
||||
float percentDeleted = ((float)framesDeleted / framePaths.Length) * 100f;
|
||||
string keptPercent = $"{(100f - percentDeleted).ToString("0.0")}%";
|
||||
|
||||
if (skipped)
|
||||
Logger.Log($"Deduplication: First {skipAfterNoDupesFrames} frames did not have any duplicates - Skipping the rest!", false, true);
|
||||
else
|
||||
Logger.Log($"[Deduplication]{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.");
|
||||
}
|
||||
|
||||
static float GetDifference (string img1Path, string img2Path)
|
||||
{
|
||||
MagickImage img2 = GetImage(img2Path);
|
||||
MagickImage img1 = GetImage(img1Path);
|
||||
|
||||
double err = img1.Compare(img2, ErrorMetric.Fuzz);
|
||||
float errPercent = (float)err * 100f;
|
||||
return errPercent;
|
||||
}
|
||||
|
||||
static async Task<int> GetBufferSize ()
|
||||
{
|
||||
Size res = await Interpolate.current.GetScaledRes();
|
||||
long pixels = res.Width * res.Height; // 4K = 8294400, 1440p = 3686400, 1080p = 2073600, 720p = 921600, 540p = 518400, 360p = 230400
|
||||
int bufferSize = 100;
|
||||
if (pixels < 518400) bufferSize = 1800;
|
||||
if (pixels >= 518400) bufferSize = 1400;
|
||||
if (pixels >= 921600) bufferSize = 800;
|
||||
if (pixels >= 2073600) bufferSize = 400;
|
||||
if (pixels >= 3686400) bufferSize = 200;
|
||||
if (pixels >= 8294400) bufferSize = 100;
|
||||
if (pixels == 0) bufferSize = 100;
|
||||
Logger.Log($"Using magick dedupe buffer size {bufferSize} for frame resolution {res.Width}x{res.Height}", true);
|
||||
return bufferSize;
|
||||
}
|
||||
|
||||
public static async Task CreateDupesFile (string framesPath, int lastFrameNum, string ext)
|
||||
{
|
||||
bool debug = Config.GetBool("dupeScanDebug", false);
|
||||
string infoFile = Path.Combine(framesPath.GetParentDir(), "dupes.ini");
|
||||
string fileContent = "";
|
||||
|
||||
FileInfo[] frameFiles = IOUtils.GetFileInfosSorted(framesPath, false, "*" + ext);
|
||||
|
||||
if (debug)
|
||||
Logger.Log($"Running CreateDupesFile for '{framesPath}' ({frameFiles.Length} files), lastFrameNum = {lastFrameNum}, ext = {ext}.", true, false, "dupes");
|
||||
|
||||
for(int i = 0; i < frameFiles.Length; i++)
|
||||
{
|
||||
bool isLastItem = (i + 1) == frameFiles.Length;
|
||||
|
||||
int frameNum1 = frameFiles[i].Name.GetInt();
|
||||
int frameNum2 = isLastItem ? lastFrameNum : frameFiles[i+1].Name.GetInt();
|
||||
|
||||
int diff = frameNum2 - frameNum1;
|
||||
int dupes = diff - 1;
|
||||
|
||||
if(debug)
|
||||
Logger.Log($"{(isLastItem ? "[isLastItem] " : "")}frameNum1 (frameFiles[{i}]) = {frameNum1}, frameNum2 (frameFiles[{i+1}]) = {frameNum2} => dupes = {dupes}", true, false, "dupes");
|
||||
|
||||
fileContent += $"{Path.GetFileNameWithoutExtension(frameFiles[i].Name)}:{dupes}\n";
|
||||
}
|
||||
|
||||
File.WriteAllText(infoFile, fileContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
using Flowframes.MiscUtils;
|
||||
using ImageMagick;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Flowframes.Magick
|
||||
{
|
||||
public enum BitmapDensity
|
||||
{
|
||||
/// <summary>
|
||||
/// Ignore the density of the image when creating the bitmap.
|
||||
/// </summary>
|
||||
Ignore,
|
||||
|
||||
/// <summary>
|
||||
/// Use the density of the image when creating the bitmap.
|
||||
/// </summary>
|
||||
Use,
|
||||
}
|
||||
|
||||
public static class MagickExtensions
|
||||
{
|
||||
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "False positive.")]
|
||||
public static Bitmap ToBitmap(this MagickImage magickImg, BitmapDensity density)
|
||||
{
|
||||
string mapping = "BGR";
|
||||
var format = PixelFormat.Format24bppRgb;
|
||||
|
||||
var image = magickImg;
|
||||
|
||||
try
|
||||
{
|
||||
if (image.ColorSpace != ColorSpace.sRGB)
|
||||
{
|
||||
image = (MagickImage)magickImg.Clone();
|
||||
image.ColorSpace = ColorSpace.sRGB;
|
||||
}
|
||||
|
||||
if (image.HasAlpha)
|
||||
{
|
||||
mapping = "BGRA";
|
||||
format = PixelFormat.Format32bppArgb;
|
||||
}
|
||||
|
||||
using (var pixels = image.GetPixelsUnsafe())
|
||||
{
|
||||
var bitmap = new Bitmap(image.Width, image.Height, format);
|
||||
var data = bitmap.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadWrite, format);
|
||||
var destination = data.Scan0;
|
||||
|
||||
for (int y = 0; y < image.Height; y++)
|
||||
{
|
||||
byte[] bytes = pixels.ToByteArray(0, y, image.Width, 1, mapping);
|
||||
Marshal.Copy(bytes, 0, destination, bytes.Length);
|
||||
|
||||
destination = new IntPtr(destination.ToInt64() + data.Stride);
|
||||
}
|
||||
|
||||
bitmap.UnlockBits(data);
|
||||
SetBitmapDensity(magickImg, bitmap, density);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!ReferenceEquals(image, magickImg))
|
||||
image.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public static Bitmap ToBitmap(this MagickImage imageMagick) => ToBitmap(imageMagick, BitmapDensity.Ignore);
|
||||
|
||||
public static Bitmap ToBitmap(this MagickImage imageMagick, ImageFormat imageFormat) => ToBitmap(imageMagick, imageFormat, BitmapDensity.Ignore);
|
||||
|
||||
public static Bitmap ToBitmap(this MagickImage imageMagick, ImageFormat imageFormat, BitmapDensity bitmapDensity)
|
||||
{
|
||||
imageMagick.Format = InternalMagickFormatInfo.GetFormat(imageFormat);
|
||||
|
||||
MemoryStream memStream = new MemoryStream();
|
||||
imageMagick.Write(memStream);
|
||||
memStream.Position = 0;
|
||||
|
||||
/* Do not dispose the memStream, the bitmap owns it. */
|
||||
var bitmap = new Bitmap(memStream);
|
||||
|
||||
SetBitmapDensity(imageMagick, bitmap, bitmapDensity);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public static void FromBitmap(this MagickImage imageMagick, Bitmap bitmap)
|
||||
{
|
||||
using (MemoryStream memStream = new MemoryStream())
|
||||
{
|
||||
if (IsSupportedImageFormat(bitmap.RawFormat))
|
||||
bitmap.Save(memStream, bitmap.RawFormat);
|
||||
else
|
||||
bitmap.Save(memStream, ImageFormat.Bmp);
|
||||
|
||||
memStream.Position = 0;
|
||||
imageMagick.Read(memStream);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSupportedImageFormat(ImageFormat format)
|
||||
{
|
||||
return
|
||||
format.Guid.Equals(ImageFormat.Bmp.Guid) ||
|
||||
format.Guid.Equals(ImageFormat.Gif.Guid) ||
|
||||
format.Guid.Equals(ImageFormat.Icon.Guid) ||
|
||||
format.Guid.Equals(ImageFormat.Jpeg.Guid) ||
|
||||
format.Guid.Equals(ImageFormat.Png.Guid) ||
|
||||
format.Guid.Equals(ImageFormat.Tiff.Guid);
|
||||
}
|
||||
|
||||
private static void SetBitmapDensity(MagickImage imageMagick, Bitmap bitmap, BitmapDensity bitmapDensity)
|
||||
{
|
||||
if (bitmapDensity == BitmapDensity.Use)
|
||||
{
|
||||
var dpi = GetDpi(imageMagick, bitmapDensity);
|
||||
bitmap.SetResolution((float)dpi.X, (float)dpi.Y);
|
||||
}
|
||||
}
|
||||
|
||||
private static Density GetDpi(MagickImage imageMagick, BitmapDensity bitmapDensity)
|
||||
{
|
||||
if (bitmapDensity == BitmapDensity.Ignore || (imageMagick.Density.Units == DensityUnit.Undefined && imageMagick.Density.X == 0 && imageMagick.Density.Y == 0))
|
||||
return new Density(96);
|
||||
|
||||
return imageMagick.Density.ChangeUnits(DensityUnit.PixelsPerInch);
|
||||
}
|
||||
}
|
||||
|
||||
public class InternalMagickFormatInfo
|
||||
{
|
||||
internal static MagickFormat GetFormat(ImageFormat format)
|
||||
{
|
||||
if (format == ImageFormat.Bmp || format == ImageFormat.MemoryBmp)
|
||||
return MagickFormat.Bmp;
|
||||
else if (format == ImageFormat.Gif)
|
||||
return MagickFormat.Gif;
|
||||
else if (format == ImageFormat.Icon)
|
||||
return MagickFormat.Icon;
|
||||
else if (format == ImageFormat.Jpeg)
|
||||
return MagickFormat.Jpeg;
|
||||
else if (format == ImageFormat.Png)
|
||||
return MagickFormat.Png;
|
||||
else if (format == ImageFormat.Tiff)
|
||||
return MagickFormat.Tiff;
|
||||
else
|
||||
throw new NotSupportedException("Unsupported image format: " + format.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
using Flowframes.IO;
|
||||
using ImageMagick;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Flowframes.Magick
|
||||
{
|
||||
class SceneDetect
|
||||
{
|
||||
public static async Task RunSceneDetection (string path)
|
||||
{
|
||||
string outFolder = path + "-analyzed";
|
||||
Directory.CreateDirectory(outFolder);
|
||||
string ext = "png";
|
||||
FileInfo[] frames = IOUtils.GetFileInfosSorted(path, false, "*." + ext);
|
||||
|
||||
for (int i = 1; i < frames.Length; i++)
|
||||
{
|
||||
FileInfo frame = frames[i];
|
||||
FileInfo lastFrame = frames[i - 1];
|
||||
Task.Run(() => ProcessFrame(frame, lastFrame, outFolder));
|
||||
}
|
||||
}
|
||||
|
||||
public static Dictionary<string, MagickImage> imageCache = new Dictionary<string, MagickImage>();
|
||||
static MagickImage GetImage(string path, bool allowCaching = true)
|
||||
{
|
||||
if (!allowCaching)
|
||||
return new MagickImage(path);
|
||||
|
||||
if (imageCache.Count >= 30)
|
||||
ClearCache();
|
||||
|
||||
if (!imageCache.ContainsKey(path))
|
||||
imageCache.Add(path, new MagickImage(path));
|
||||
|
||||
return imageCache[path];
|
||||
}
|
||||
|
||||
public static void ClearCache()
|
||||
{
|
||||
imageCache.Clear();
|
||||
}
|
||||
|
||||
static async Task ProcessFrame (FileInfo frame, FileInfo lastFrame, string outFolder)
|
||||
{
|
||||
MagickImage prevFrame = GetImage(lastFrame.FullName, false);
|
||||
MagickImage currFrame = GetImage(frame.FullName, false);
|
||||
|
||||
Size originalSize = new Size(currFrame.Width, currFrame.Height);
|
||||
int downscaleHeight = 144;
|
||||
prevFrame.Scale(currFrame.Width / (currFrame.Height / downscaleHeight), downscaleHeight);
|
||||
currFrame.Scale(currFrame.Width / (currFrame.Height / downscaleHeight), downscaleHeight);
|
||||
|
||||
double errNormalizedCrossCorrelation = currFrame.Compare(prevFrame, ErrorMetric.NormalizedCrossCorrelation);
|
||||
double errRootMeanSquared = currFrame.Compare(prevFrame, ErrorMetric.RootMeanSquared);
|
||||
|
||||
string str = $"\nMetrics of {frame.Name.Split('.')[0]} against {lastFrame.Name.Split('.')[0]}:\n";
|
||||
str += $"NormalizedCrossCorrelation: {errNormalizedCrossCorrelation.ToString("0.000")}\n";
|
||||
str += $"RootMeanSquared: {errRootMeanSquared.ToString("0.000")}\n";
|
||||
str += "\n\n";
|
||||
|
||||
bool nccTrigger = errNormalizedCrossCorrelation < 0.45f;
|
||||
bool rMeanSqrTrigger = errRootMeanSquared > 0.18f;
|
||||
bool rmsNccTrigger = errRootMeanSquared > 0.18f && errNormalizedCrossCorrelation < 0.6f;
|
||||
bool nccRmsTrigger = errNormalizedCrossCorrelation < 0.45f && errRootMeanSquared > 0.11f;
|
||||
|
||||
if (rmsNccTrigger) str += "\n\nRMS -> NCC DOUBLE SCENE CHANGE TRIGGER!";
|
||||
if (nccRmsTrigger) str += "\n\nNCC -> RMS DOUBLE SCENE CHANGE TRIGGER!";
|
||||
|
||||
currFrame.Scale(originalSize.Width / 2, originalSize.Height / 2);
|
||||
|
||||
new Drawables()
|
||||
.FontPointSize(12)
|
||||
.Font("Consolas", FontStyleType.Normal, FontWeight.Bold, FontStretch.Normal)
|
||||
.FillColor(MagickColors.Red)
|
||||
.TextAlignment(TextAlignment.Left)
|
||||
.Text(1, 10, str)
|
||||
.Draw(currFrame);
|
||||
|
||||
currFrame.Write(Path.Combine(outFolder, frame.Name));
|
||||
|
||||
prevFrame.Dispose();
|
||||
currFrame.Dispose();
|
||||
|
||||
await Task.Delay(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user