Display a Image in a console application

后端 未结 7 1927
爱一瞬间的悲伤
爱一瞬间的悲伤 2020-11-27 10:06

I have a console application that manages images. Now i need something like a preview of the Images within the console application. Is there a way to display them in the con

相关标签:
7条回答
  • 2020-11-27 10:14

    that was fun. Thanks fubo, i tried your solution and was able to increase the resolution of the preview by 4 (2x2).

    I found, that you can set the background color for each individual char. So, instead of using two ASCII 219 ( █ ) chars, i used ASCII 223 ( ▀ ) two times with different foreground and background Colors. That divides the big Pixel ( ██ ) in 4 subpixels like this ( ▀▄ ).

    In this example i put both images next to each other, so you can see the difference easily:

    Here is the code:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Drawing;
    
    namespace ConsoleWithImage
    {
      class Program
      {
    
        public static void ConsoleWriteImage(Bitmap bmpSrc)
        {
            int sMax = 39;
            decimal percent = Math.Min(decimal.Divide(sMax, bmpSrc.Width), decimal.Divide(sMax, bmpSrc.Height));
            Size resSize = new Size((int)(bmpSrc.Width * percent), (int)(bmpSrc.Height * percent));
            Func<System.Drawing.Color, int> ToConsoleColor = c =>
            {
                int index = (c.R > 128 | c.G > 128 | c.B > 128) ? 8 : 0;
                index |= (c.R > 64) ? 4 : 0;
                index |= (c.G > 64) ? 2 : 0;
                index |= (c.B > 64) ? 1 : 0;
                return index;
            };
            Bitmap bmpMin = new Bitmap(bmpSrc, resSize.Width, resSize.Height);
            Bitmap bmpMax = new Bitmap(bmpSrc, resSize.Width * 2, resSize.Height * 2);
            for (int i = 0; i < resSize.Height; i++)
            {
                for (int j = 0; j < resSize.Width; j++)
                {
                    Console.ForegroundColor = (ConsoleColor)ToConsoleColor(bmpMin.GetPixel(j, i));
                    Console.Write("██");
                }
    
                Console.BackgroundColor = ConsoleColor.Black;
                Console.Write("    ");
    
                for (int j = 0; j < resSize.Width; j++)
                {
                    Console.ForegroundColor = (ConsoleColor)ToConsoleColor(bmpMax.GetPixel(j * 2, i * 2));
                    Console.BackgroundColor = (ConsoleColor)ToConsoleColor(bmpMax.GetPixel(j * 2, i * 2 + 1));
                    Console.Write("▀");
    
                    Console.ForegroundColor = (ConsoleColor)ToConsoleColor(bmpMax.GetPixel(j * 2 + 1, i * 2));
                    Console.BackgroundColor = (ConsoleColor)ToConsoleColor(bmpMax.GetPixel(j * 2 + 1, i * 2 + 1));
                    Console.Write("▀");
                }
                System.Console.WriteLine();
            }
        }
    
        static void Main(string[] args)
        {
            System.Console.WindowWidth = 170;
            System.Console.WindowHeight = 40;
    
            Bitmap bmpSrc = new Bitmap(@"image.bmp", true);
    
            ConsoleWriteImage(bmpSrc);
    
            System.Console.ReadLine();
        }
      }
    }
    

    To run the example, the bitmap "image.bmp" has to be in the same directory as the executable. I increased the size of the console, the size of the preview is still 39 and can be changed at int sMax = 39;.

    The solution from taffer is also very cool. You two have my upvote...

    0 讨论(0)
  • 2020-11-27 10:17

    There's no direct way. But you may try to use an image-to-ascii-art converter like this one

    0 讨论(0)
  • 2020-11-27 10:19

    If you use ASCII 219 ( █ ) twice, you have something like a pixel ( ██ ). Now you are restricted by the amount of pixels and the number of colors in your console application.

    • if you keep default settings you have about 39x39 pixel, if you want more you can resize your console with Console.WindowHeight = resSize.Height + 1;and Console.WindowWidth = resultSize.Width * 2;

    • you have to keep the image's aspect-ratio as far as possible, so you won't have 39x39 in the most cases

    • Malwyn posted a totally underrated method to convert System.Drawing.Color to System.ConsoleColor

    so my approach would be

    using System.Drawing;
    
    public static int ToConsoleColor(System.Drawing.Color c)
    {
        int index = (c.R > 128 | c.G > 128 | c.B > 128) ? 8 : 0;
        index |= (c.R > 64) ? 4 : 0;
        index |= (c.G > 64) ? 2 : 0;
        index |= (c.B > 64) ? 1 : 0;
        return index;
    }
    
    public static void ConsoleWriteImage(Bitmap src)
    {
        int min = 39;
        decimal pct = Math.Min(decimal.Divide(min, src.Width), decimal.Divide(min, src.Height));
        Size res = new Size((int)(src.Width * pct), (int)(src.Height * pct));
        Bitmap bmpMin = new Bitmap(src, res);
        for (int i = 0; i < res.Height; i++)
        {
            for (int j = 0; j < res.Width; j++)
            {
                Console.ForegroundColor = (ConsoleColor)ToConsoleColor(bmpMin.GetPixel(j, i));
                Console.Write("██");
            }
            System.Console.WriteLine();
        }
    }
    

    so you can

    ConsoleWriteImage(new Bitmap(@"C:\image.gif"));
    

    sample input:

    sample output:

    0 讨论(0)
  • 2020-11-27 10:20

    I further played with code from @DieterMeemken. I halved vertical resolution and added dithering via ░▒▓. On the left is Dieter Meemken result, on the right my. On the bottom is original picture resized to rougly match the output. While Malwyns conversion function is impressive, it does not use all gray colors, what is pity.

    static int[] cColors = { 0x000000, 0x000080, 0x008000, 0x008080, 0x800000, 0x800080, 0x808000, 0xC0C0C0, 0x808080, 0x0000FF, 0x00FF00, 0x00FFFF, 0xFF0000, 0xFF00FF, 0xFFFF00, 0xFFFFFF };
    
    public static void ConsoleWritePixel(Color cValue)
    {
        Color[] cTable = cColors.Select(x => Color.FromArgb(x)).ToArray();
        char[] rList = new char[] { (char)9617, (char)9618, (char)9619, (char)9608 }; // 1/4, 2/4, 3/4, 4/4
        int[] bestHit = new int[] { 0, 0, 4, int.MaxValue }; //ForeColor, BackColor, Symbol, Score
    
        for (int rChar = rList.Length; rChar > 0; rChar--)
        {
            for (int cFore = 0; cFore < cTable.Length; cFore++)
            {
                for (int cBack = 0; cBack < cTable.Length; cBack++)
                {
                    int R = (cTable[cFore].R * rChar + cTable[cBack].R * (rList.Length - rChar)) / rList.Length;
                    int G = (cTable[cFore].G * rChar + cTable[cBack].G * (rList.Length - rChar)) / rList.Length;
                    int B = (cTable[cFore].B * rChar + cTable[cBack].B * (rList.Length - rChar)) / rList.Length;
                    int iScore = (cValue.R - R) * (cValue.R - R) + (cValue.G - G) * (cValue.G - G) + (cValue.B - B) * (cValue.B - B);
                    if (!(rChar > 1 && rChar < 4 && iScore > 50000)) // rule out too weird combinations
                    {
                        if (iScore < bestHit[3])
                        {
                            bestHit[3] = iScore; //Score
                            bestHit[0] = cFore;  //ForeColor
                            bestHit[1] = cBack;  //BackColor
                            bestHit[2] = rChar;  //Symbol
                        }
                    }
                }
            }
        }
        Console.ForegroundColor = (ConsoleColor)bestHit[0];
        Console.BackgroundColor = (ConsoleColor)bestHit[1];
        Console.Write(rList[bestHit[2] - 1]);
    }
    
    
    public static void ConsoleWriteImage(Bitmap source)
    {
        int sMax = 39;
        decimal percent = Math.Min(decimal.Divide(sMax, source.Width), decimal.Divide(sMax, source.Height));
        Size dSize = new Size((int)(source.Width * percent), (int)(source.Height * percent));   
        Bitmap bmpMax = new Bitmap(source, dSize.Width * 2, dSize.Height);
        for (int i = 0; i < dSize.Height; i++)
        {
            for (int j = 0; j < dSize.Width; j++)
            {
                ConsoleWritePixel(bmpMax.GetPixel(j * 2, i));
                ConsoleWritePixel(bmpMax.GetPixel(j * 2 + 1, i));
            }
            System.Console.WriteLine();
        }
        Console.ResetColor();
    }
    

    usage:

    Bitmap bmpSrc = new Bitmap(@"HuwnC.gif", true);    
    ConsoleWriteImage(bmpSrc);
    

    EDIT

    Color distance is complex topic (here, here and links on those pages...). I tried to calculate distance in YUV and results were rather worse than in RGB. They could be better with Lab and DeltaE, but I did not try that. Distance in RGB seems to be good enough. In fact results are very simmilar for both euclidean and manhattan distance in RGB color space, so I suspect there are just too few colors to choose from.

    The rest is just brute force compare of color against all combinations of colors and patterns (=symbols). I stated fill ratio for ░▒▓█ to be 1/4, 2/4, 3/4 and 4/4. In that case the third symbol is in fact redundant to the first. But if ratios were not such uniform (depends on font), results could change, so I left it there for future improvements. Average color of symbol is calculated as weighed average of foregroudColor and backgroundColor according to fill ratio. It assumes linear colors, what is also big simplification. So there is still room for improvement.

    0 讨论(0)
  • 2020-11-27 10:28

    I was reading about color spaces and LAB space appears to be a good option for you (see this questions: Finding an accurate “distance” between colors and Algorithm to check similarity of colors)

    Quoting Wikipedia CIELAB page, the advantages of this color space are:

    Unlike the RGB and CMYK color models, Lab color is designed to approximate human vision. It aspires to perceptual uniformity, and its L component closely matches human perception of lightness. Thus, it can be used to make accurate color balance corrections by modifying output curves in the a and b components.

    To measure the distance between colors you can use Delta E distance.

    With this you can approximate better from Color to ConsoleColor:

    Firstly, you can define an CieLab class to represent colors in this space:

    public class CieLab
    {
        public double L { get; set; }
        public double A { get; set; }
        public double B { get; set; }
    
        public static double DeltaE(CieLab l1, CieLab l2)
        {
            return Math.Pow(l1.L - l2.L, 2) + Math.Pow(l1.A - l2.A, 2) + Math.Pow(l1.B - l2.B, 2);
        }
    
        public static CieLab Combine(CieLab l1, CieLab l2, double amount)
        {
            var l = l1.L * amount + l2.L * (1 - amount);
            var a = l1.A * amount + l2.A * (1 - amount);
            var b = l1.B * amount + l2.B * (1 - amount);
    
            return new CieLab { L = l, A = a, B = b };
        }
    }
    

    There are two static methods, one to measure the distance using Delta E (DeltaE) and other to combine two colors specifying how much of each color (Combine).

    And for transform from RGB to LAB you can use the following method (from here):

    public static CieLab RGBtoLab(int red, int green, int blue)
    {
        var rLinear = red / 255.0;
        var gLinear = green / 255.0;
        var bLinear = blue / 255.0;
    
        double r = rLinear > 0.04045 ? Math.Pow((rLinear + 0.055) / (1 + 0.055), 2.2) : (rLinear / 12.92);
        double g = gLinear > 0.04045 ? Math.Pow((gLinear + 0.055) / (1 + 0.055), 2.2) : (gLinear / 12.92);
        double b = bLinear > 0.04045 ? Math.Pow((bLinear + 0.055) / (1 + 0.055), 2.2) : (bLinear / 12.92);
    
        var x = r * 0.4124 + g * 0.3576 + b * 0.1805;
        var y = r * 0.2126 + g * 0.7152 + b * 0.0722;
        var z = r * 0.0193 + g * 0.1192 + b * 0.9505;
    
        Func<double, double> Fxyz = t => ((t > 0.008856) ? Math.Pow(t, (1.0 / 3.0)) : (7.787 * t + 16.0 / 116.0));
    
        return new CieLab
        {
            L = 116.0 * Fxyz(y / 1.0) - 16,
            A = 500.0 * (Fxyz(x / 0.9505) - Fxyz(y / 1.0)),
            B = 200.0 * (Fxyz(y / 1.0) - Fxyz(z / 1.0890))
        };
    }
    

    The idea is use shade characters like @AntoninLejsek do ('█', '▓', '▒', '░'), this allows you to get more than 16 colors combining the console colors (using Combine method).

    Here, we can do some improvements by pre-computing the colors to use:

    class ConsolePixel
    {
        public char Char { get; set; }
    
        public ConsoleColor Forecolor { get; set; }
        public ConsoleColor Backcolor { get; set; }
        public CieLab Lab { get; set; }
    }
    
    static List<ConsolePixel> pixels;
    private static void ComputeColors()
    {
        pixels = new List<ConsolePixel>();
    
        char[] chars = { '█', '▓', '▒', '░' };
    
        int[] rs = { 0, 0, 0, 0, 128, 128, 128, 192, 128, 0, 0, 0, 255, 255, 255, 255 };
        int[] gs = { 0, 0, 128, 128, 0, 0, 128, 192, 128, 0, 255, 255, 0, 0, 255, 255 };
        int[] bs = { 0, 128, 0, 128, 0, 128, 0, 192, 128, 255, 0, 255, 0, 255, 0, 255 };
    
        for (int i = 0; i < 16; i++)
            for (int j = i + 1; j < 16; j++)
            {
                var l1 = RGBtoLab(rs[i], gs[i], bs[i]);
                var l2 = RGBtoLab(rs[j], gs[j], bs[j]);
    
                for (int k = 0; k < 4; k++)
                {
                    var l = CieLab.Combine(l1, l2, (4 - k) / 4.0);
    
                    pixels.Add(new ConsolePixel
                    {
                        Char = chars[k],
                        Forecolor = (ConsoleColor)i,
                        Backcolor = (ConsoleColor)j,
                        Lab = l
                    });
                }
            }
    }
    

    Another improvement could be access directly to the image data using LockBits instead of using GetPixel.

    UPDATE: If the image have parts with the same color you can speed up considerably the process drawing chunk of characters having the same colors, instead of individuals chars:

    public static void DrawImage(Bitmap source)
    {
        int width = Console.WindowWidth - 1;
        int height = (int)(width * source.Height / 2.0 / source.Width);
    
        using (var bmp = new Bitmap(source, width, height))
        {
            var unit = GraphicsUnit.Pixel;
            using (var src = bmp.Clone(bmp.GetBounds(ref unit), PixelFormat.Format24bppRgb))
            {
                var bits = src.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, src.PixelFormat);
                byte[] data = new byte[bits.Stride * bits.Height];
    
                Marshal.Copy(bits.Scan0, data, 0, data.Length);
    
                for (int j = 0; j < height; j++)
                {
                    StringBuilder builder = new StringBuilder();
                    var fore = ConsoleColor.White;
                    var back = ConsoleColor.Black;
    
                    for (int i = 0; i < width; i++)
                    {
                        int idx = j * bits.Stride + i * 3;
                        var pixel = DrawPixel(data[idx + 2], data[idx + 1], data[idx + 0]);
    
    
                        if (pixel.Forecolor != fore || pixel.Backcolor != back)
                        {
                            Console.ForegroundColor = fore;
                            Console.BackgroundColor = back;
                            Console.Write(builder);
    
                            builder.Clear();
                        }
    
                        fore = pixel.Forecolor;
                        back = pixel.Backcolor;
                        builder.Append(pixel.Char);
                    }
    
                    Console.ForegroundColor = fore;
                    Console.BackgroundColor = back;
                    Console.WriteLine(builder);
                }
    
                Console.ResetColor();
            }
        }
    }
    
    private static ConsolePixel DrawPixel(int r, int g, int b)
    {
        var l = RGBtoLab(r, g, b);
    
        double diff = double.MaxValue;
        var pixel = pixels[0];
    
        foreach (var item in pixels)
        {
            var delta = CieLab.DeltaE(l, item.Lab);
            if (delta < diff)
            {
                diff = delta;
                pixel = item;
            }
        }
    
        return pixel;
    }
    

    Finally, call DrawImage like so:

    static void Main(string[] args)
    {
        ComputeColors();
    
        Bitmap image = new Bitmap("image.jpg", true);
        DrawImage(image);
    
    }
    

    Result Images:



    The following solutions aren't based on chars but provides full detailed images


    You can draw over any window using its handler to create a Graphics object. To get the handler of a console application you can do it importing GetConsoleWindow:

    [DllImport("kernel32.dll", EntryPoint = "GetConsoleWindow", SetLastError = true)]
    private static extern IntPtr GetConsoleHandle();
    

    Then, create a graphics with the handler (using Graphics.FromHwnd) and draw the image using the methods in Graphics object, for example:

    static void Main(string[] args)
    {            
        var handler = GetConsoleHandle();
    
        using (var graphics = Graphics.FromHwnd(handler))
        using (var image = Image.FromFile("img101.png"))
            graphics.DrawImage(image, 50, 50, 250, 200);
    }
    

    This looks fine but if the console is resized or scrolled, the image disappears because the windows is refreshed (maybe implementing some kind of mechanism to redraw the image is possible in your case).


    Another solution is embedding a window (Form) into the console application. To do this you have to import SetParent (and MoveWindow to relocate the window inside the console):

    [DllImport("user32.dll")]
    public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
    
    [DllImport("user32.dll", SetLastError = true)]
    public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
    

    Then you just need to create a Form and set BackgroundImage property to the desired image (do it on a Thread or Task to avoid blocking the console):

    static void Main(string[] args)
    {
        Task.Factory.StartNew(ShowImage);
    
        Console.ReadLine();
    }
    
    static void ShowImage()
    {
        var form = new Form
        {                
            BackgroundImage = Image.FromFile("img101.png"),
            BackgroundImageLayout = ImageLayout.Stretch
        };
    
        var parent = GetConsoleHandle();
        var child = form.Handle;
    
        SetParent(child, parent);
        MoveWindow(child, 50, 50, 250, 200, true);
    
        Application.Run(form);
    }
    

    Of course you can set FormBorderStyle = FormBorderStyle.None to hide windows borders (right image)

    In this case you can resize the console and the image/window still be there.

    One benefit with this approach is that you can locate the window where you want and change the image at any time by just changing BackgroundImage property.

    0 讨论(0)
  • 2020-11-27 10:38

    Though showing an image in a console is not the intended usage of the console, you can surely hack the things, as the console window is just a window, like any other windows.

    Actually, once I have started to develop a text controls library for console applications with graphics support. I have never finished that, though I have a working proof-of-concept demo:

    And if you obtain the console font size, you can place the image very precisely.

    This is how you can do it:

    static void Main(string[] args)
    {
        Console.WriteLine("Graphics in console window!");
    
        Point location = new Point(10, 10);
        Size imageSize = new Size(20, 10); // desired image size in characters
    
        // draw some placeholders
        Console.SetCursorPosition(location.X - 1, location.Y);
        Console.Write(">");
        Console.SetCursorPosition(location.X + imageSize.Width, location.Y);
        Console.Write("<");
        Console.SetCursorPosition(location.X - 1, location.Y + imageSize.Height - 1);
        Console.Write(">");
        Console.SetCursorPosition(location.X + imageSize.Width, location.Y + imageSize.Height - 1);
        Console.WriteLine("<");
    
        string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonPictures), @"Sample Pictures\tulips.jpg");
        using (Graphics g = Graphics.FromHwnd(GetConsoleWindow()))
        {
            using (Image image = Image.FromFile(path))
            {
                Size fontSize = GetConsoleFontSize();
    
                // translating the character positions to pixels
                Rectangle imageRect = new Rectangle(
                    location.X * fontSize.Width,
                    location.Y * fontSize.Height,
                    imageSize.Width * fontSize.Width,
                    imageSize.Height * fontSize.Height);
                g.DrawImage(image, imageRect);
            }
        }
    }
    

    Here is how you can obtain the current console font size:

    private static Size GetConsoleFontSize()
    {
        // getting the console out buffer handle
        IntPtr outHandle = CreateFile("CONOUT$", GENERIC_READ | GENERIC_WRITE, 
            FILE_SHARE_READ | FILE_SHARE_WRITE,
            IntPtr.Zero,
            OPEN_EXISTING,
            0,
            IntPtr.Zero);
        int errorCode = Marshal.GetLastWin32Error();
        if (outHandle.ToInt32() == INVALID_HANDLE_VALUE)
        {
            throw new IOException("Unable to open CONOUT$", errorCode);
        }
    
        ConsoleFontInfo cfi = new ConsoleFontInfo();
        if (!GetCurrentConsoleFont(outHandle, false, cfi))
        {
            throw new InvalidOperationException("Unable to get font information.");
        }
    
        return new Size(cfi.dwFontSize.X, cfi.dwFontSize.Y);            
    }
    

    And the required additional WinApi calls, constants and types:

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern IntPtr GetConsoleWindow();
    
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern IntPtr CreateFile(
        string lpFileName,
        int dwDesiredAccess,
        int dwShareMode,
        IntPtr lpSecurityAttributes,
        int dwCreationDisposition,
        int dwFlagsAndAttributes,
        IntPtr hTemplateFile);
    
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool GetCurrentConsoleFont(
        IntPtr hConsoleOutput,
        bool bMaximumWindow,
        [Out][MarshalAs(UnmanagedType.LPStruct)]ConsoleFontInfo lpConsoleCurrentFont);
    
    [StructLayout(LayoutKind.Sequential)]
    internal class ConsoleFontInfo
    {
        internal int nFont;
        internal Coord dwFontSize;
    }
    
    [StructLayout(LayoutKind.Explicit)]
    internal struct Coord
    {
        [FieldOffset(0)]
        internal short X;
        [FieldOffset(2)]
        internal short Y;
    }
    
    private const int GENERIC_READ = unchecked((int)0x80000000);
    private const int GENERIC_WRITE = 0x40000000;
    private const int FILE_SHARE_READ = 1;
    private const int FILE_SHARE_WRITE = 2;
    private const int INVALID_HANDLE_VALUE = -1;
    private const int OPEN_EXISTING = 3;
    

    And the result:

    [Graphics in Console

    0 讨论(0)
提交回复
热议问题