using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Drawing; using System.Diagnostics; namespace imageJoin { class Program { /// /// Command line for FFMPEG. /// Explanation: takes video input and renders it with 1 fps and downscaled to a single pixel to an array of png files /// const string CMDLINE = "-i \"{0}\" -lavfi fps=1,scale=1:{2}:flags=lanczos \"{1}\\%06d.png\""; /// /// Main entry of Application /// /// Command line arguments static void Main(string[] args) { Color[] Colors; string TempDir = GetTempDir("frameband"); #if DEBUG //in debug mode I use hardcoded information because lazy. string VideoFile = @"S:\Media\Video\Star Wars\Star Wars IV\Star Wars IV.mkv"; string OutputFile = @"C:\temp\band.png"; string FFmpegFile = @"D:\Programme\VIDEO\ffmpeg\ffmpeg.exe"; bool SingleColor = false; int Height = 200; #else string VideoFile = Ask("Source video file").Trim('"'); string OutputFile = Ask("Destination image file").Trim('"'); string FFmpegFile = @"ffmpeg.exe"; int Height = 0; while (!int.TryParse("Image Height", out Height) || Height < 1) ; bool SingleColor = Ask("Use single color [y/n]").ToLower() == "y"; #endif //Instead of all this ffmpeg path detection crap it would be better to bundle ffmpeg as resource and extract it at runtime. while (!File.Exists(FFmpegFile)) { Console.WriteLine("Can't find {0}", FFmpegFile); FFmpegFile = Ask("ffmpeg.exe file").Trim('"'); } DateTime Start = DateTime.Now; //Generate single frames into PNG //Note: check if input is a video with at least 1 frame at all. CreateImages(VideoFile, TempDir, FFmpegFile, SingleColor ? 1 : Height); DateTime VideoGenerated = DateTime.Now; if (SingleColor) { //Extract color information from the PNG images. //GetFiles is rather slow, especially for a large number of files, //but it is certainly easier to use than manually using the Windows API Colors = ExtractColorFromPixel(Directory.GetFiles(TempDir, "*.png")); //It is possible, that no colors were extracted, //for example if the input is an audio file. if (Colors.Length > 0) { RenderBand(Colors, OutputFile, Height); } else { //Probably invalid video file Console.WriteLine("No frames were extracted"); } } else { JoinImages(Directory.GetFiles(TempDir, "*.png"), OutputFile); } //Remove temporary PNG files. //Ideally this would be in a try/catch block at the end, //that loops until it succeeds. Directory.Delete(TempDir, true); //Stats for nerds TimeSpan tsTotal = DateTime.Now.Subtract(Start); TimeSpan tsVideo = VideoGenerated.Subtract(Start); TimeSpan tsImage = DateTime.Now.Subtract(VideoGenerated); Console.WriteLine(@"Done. Durations: Total: {0:00}:{1:00}:{2:00} Video: {3:00}:{4:00}:{5:00} Image: {6:00}:{7:00}:{8:00}", tsTotal.Hours, tsTotal.Minutes, tsTotal.Seconds, tsVideo.Hours, tsVideo.Minutes, tsVideo.Seconds, tsImage.Hours, tsImage.Minutes, tsImage.Seconds); #if DEBUG Console.WriteLine("#END"); //Flush key buffer while (Console.KeyAvailable) { Console.ReadKey(); } Console.ReadKey(); #endif } /// /// Renders colors into a frame band /// /// List of colors /// Destination image file name /// Height of band private static void RenderBand(Color[] Colors, string DestinationFile, int Height) { using (Bitmap Output = new Bitmap(Colors.Length, Height)) { //if height is 1, then use SetPixel if (Height == 1) { for (int i = 0; i < Colors.Length; i++) { Output.SetPixel(i, 0, Colors[i]); //This actually slows down the application quite a lot. Console.Write('-'); } } else { using (Graphics G = Graphics.FromImage(Output)) { //Note: Setting G.InterpolationMode to a "cheap" value has no effect, //as it is only used for scaling. //Creating a 1 pixel high image and then scale the height up does not increases the speed. for (int i = 0; i < Colors.Length; i++) { //Why is this an IDisposable? using (Pen P = new Pen(Colors[i])) { //Drawing a line is by far faster than setting individual pixels. G.DrawLine(P, new Point(i, 0), new Point(i, Height - 1)); //This actually slows down the application quite a lot. Console.Write('-'); } } } } //This detects the image format from the extension //See System.Drawing.Imaging.ImageFormat enumeration for supported types. Output.Save(DestinationFile); Console.WriteLine(); } } /// /// Create an array of frame images from a video file /// /// Video file /// Path to hold frame band images /// ffmpeg executable path /// Height of generated frame bands. private static void CreateImages(string Video, string ImagePath, string FFmpeg, int Height) { using (Process P = Process.Start(FFmpeg, string.Format(CMDLINE, Video, ImagePath, Height))) { //Instead of just waiting, spawn the Process hidden and show the progress of stderr. //I have some code for this already, but was too lazy to go and copy it. P.WaitForExit(); } } /// /// Extracts Color information from the top left pixel /// /// List of files /// List of pixel colors private static Color[] ExtractColorFromPixel(string[] Files) { List Colors = new List(); foreach (string s in Files) { Bitmap I = null; try { //Ironically Bitmap.FromFile comes from the Image class and will return an Image object, //but it is fully compatible with the Bitmap type so we just cast. I = (Bitmap)Bitmap.FromFile(s); } catch { //I never had this happen but better add an error resolver. //In this case, we just ignore that frame. Console.Write('X'); continue; } using (I) { //Extract image color Colors.Add(I.GetPixel(0, 0)); //This actually slows down the application quite a lot. Console.Write('.'); } } Console.WriteLine(); return Colors.ToArray(); } /// /// Joins an array of images horizontally /// /// List of images /// Destination File private static void JoinImages(string[] Files, string OutputFile) { int x = 0; int Height = 0; using (Image I = Image.FromFile(Files[0])) { Height = I.Height; } using (Bitmap B = new Bitmap(Files.Length, Height)) { using (Graphics G = Graphics.FromImage(B)) { foreach(string Name in Files) { using (Image I = Image.FromFile(Name)) { G.DrawImageUnscaled(I, new Point(x++, 0)); } Console.Write('-'); } } B.Save(OutputFile); Console.WriteLine(); } } /// /// Prints a line of text and asks for input /// /// Text to show. Will append colon and space /// Line input private static string Ask(string p) { Console.Write("{0}: ", p); return Console.ReadLine(); } /// /// Attempts to create a temporary folder /// /// Name of the folder to create /// Full path of folder created static string GetTempDir(string Dirname) { int i = 0; string TempRoot = Path.Combine(Path.GetTempPath(), Dirname + "_"); while (true) { string current = string.Format("{0}{1}", TempRoot, i++); try { Directory.CreateDirectory(current); return current; } catch { //you can increase i here instead if you want. } } } } }