在控制台应用程序中显示图像

时间:2015-11-05 07:08:10

标签: c# image console

我有一个管理图像的控制台应用程序。现在我需要在控制台应用程序中预览图像。有没有办法在控制台中显示它们?

以下是当前基于字符的答案的比较:

输入:

enter image description here

输出:

enter image description here

enter image description here

enter image description here

enter image description here

7 个答案:

答案 0 :(得分:82)

虽然在控制台中显示图像并不是控制台的预期用途,但您可以肯定地破解这些内容,因为控制台窗口只是一个窗口,就像任何其他窗口一样。

实际上,一旦我开始为具有图形支持的控制台应用程序开发文本控件库。我从未完成过,尽管我有一个工作概念验证演示:

Text controls with image

如果您获得控制台字体大小,则可以非常精确地放置图像。

这是你可以做到的:

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);
        }
    }
}

以下是获取当前控制台字体大小的方法:

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);            
}

所需的额外WinApi调用,常量和类型:

[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;

结果:

[Graphics in Console

答案 1 :(得分:54)

如果您使用两次ASCII 219(█),则会出现类似像素(██)的情况。 现在,您受到控制台应用程序中像素数量和颜色数量的限制。

  • 如果您保留默认设置,则大约有39x39像素,如果您想要更多,可以使用Console.WindowHeight = resSize.Height + 1;Console.WindowWidth = resultSize.Width * 2;

  • 调整控制台的大小
  • 你必须尽可能保持图像的宽高比,所以在大多数情况下你不会有39x39

  • Malwyn发布了一个完全被低估的方法,可将System.Drawing.Color转换为System.ConsoleColor

所以我的方法是

using System.Drawing;

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);
    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("██");
        }
        System.Console.WriteLine();
    }
}

所以你可以

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

示例输入:

enter image description here

示例输出:

enter image description here

答案 2 :(得分:51)

我进一步使用@DieterMeemken的代码。我将垂直分辨率减半,并通过░▒▓添加抖动。左边是Dieter Meemken的结果,右边是我的。在底部是原始图片调整大小以粗略匹配输出。 Output result 虽然Malwyns转换功能令人印象深刻,但它并没有使用所有灰色,可惜。

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();
}

用法:

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

修改

色彩距离是一个复杂的主题(herehere以及这些页面上的链接......)。我试图计算YUV的距离,结果比RGB差。 Lab和DeltaE可能会更好,但我没试过。 RGB的距离似乎足够好。事实上,对于RGB色彩空间中的欧氏距离和曼哈顿距离,结果非常相似,所以我怀疑只有太少的颜色可供选择。

其余的只是颜色与颜色和图案(=符号)的所有组合的强力比较。我说░▒▓█的填充率是1 / 4,2 / 4,3 / 4和4/4。在这种情况下,第三个符号实际上与第一个符号是多余的。但如果比率不是那么均匀(取决于字体),结果可能会改变,所以我把它留在那里以便将来改进。符号的平均颜色根据填充率计算为foregroudColor和backgroundColor的加权平均值。它假定线性颜色,也是大的简化。所以仍有改进的余地。

答案 3 :(得分:36)

这很有趣。 感谢fubo,我尝试了您的解决方案,并且能够将预览的分辨率提高4(2x2)。

我发现,您可以为每个字符设置背景颜色。因此,我使用ASCII 223(▀)两次使用不同的前景色和背景色,而不是使用两个ASCII 219(█)字符。这将大像素(██)划分为4个子像素,如下所示(▀▄)。

在这个示例中,我将两个图像放在一起,这样您就可以轻松看到差异:

enter image description here

以下是代码:

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();
    }
  }
}

要运行示例,位图&#34; image.bmp&#34;必须与可执行文件位于同一目录中。我增加了控制台的大小,预览的大小仍然是39,可以在int sMax = 39;更改。

来自taffer的解决方案也非常酷。 你们两个有我的upvote ......

答案 4 :(得分:22)

我正在阅读色彩空间 LAB 空间似乎是一个不错的选择(请参阅此问题:Finding an accurate “distance” between colorsAlgorithm to check similarity of colors

引用维基百科CIELAB页面,此色彩空间的优点是:

  

与RGB和CMYK颜色模型不同,Lab颜色设计用于近似人类视觉。它渴望感性均匀性,其L分量与人类对亮度的感知紧密相关。因此,它可以用于通过修改a和b组件中的输出曲线来进行准确的色彩平衡校正。

要测量颜色之间的距离,您可以使用Delta E距离。

使用此功能,您可以更好地从ColorConsoleColor

首先,您可以定义一个CieLab类来表示此空间中的颜色:

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 };
    }
}

有两种静态方法,一种是使用 Delta E DeltaE)测量距离,另一种是组合两种颜色,指定每种颜色的多少(Combine

要从RGB转换为LAB,您可以使用以下方法(来自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))
    };
}

这个想法是使用@AntoninLejsek做的阴影字符('█','▓','▒','░'),这样可以让你获得超过16种颜色的控制台颜色(使用{{1}方法)。

在这里,我们可以通过预先计算要使用的颜色来做一些改进:

Combine

另一项改进可能是使用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 }); } } } 直接访问图片数据,而不是使用LockBits

更新:如果图片中的部分颜色相同,则可以大大加快绘制具有相同颜色的字符块的过程,而不是单个字符:

GetPixel

最后,像这样致电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; }

DrawImage

结果图片:

Console1

Console2

以下解决方案不是基于字符,而是提供完整的详细图像

您可以使用处理程序在任何窗口上绘制以创建static void Main(string[] args) { ComputeColors(); Bitmap image = new Bitmap("image.jpg", true); DrawImage(image); } 对象。要获取控制台应用程序的处理程序,您可以导入Graphics

GetConsoleWindow

然后,使用处理程序创建图形(使用[DllImport("kernel32.dll", EntryPoint = "GetConsoleWindow", SetLastError = true)] private static extern IntPtr GetConsoleHandle(); )并使用Graphics.FromHwnd对象中的方法绘制图像,例如:

Graphics

Version 1

这看起来很好但是如果控制台被调整大小或滚动,图像会因为刷新窗口而消失(在你的情况下可能会实现某种机制来重绘图像)。

另一种解决方案是将窗口(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); } )嵌入到控制台应用程序中。为此,您必须导入Form(和SetParent以重新定位控制台内的窗口):

MoveWindow

然后,您只需要创建一个[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); 并将Form属性设置为所需的图像(在BackgroundImageThread上执行以避免阻止控制台):

Task

Version2

当然,您可以设置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); } 隐藏窗口边框(右图)

在这种情况下,您可以调整控制台的大小,图像/窗口仍然存在。

此方法的一个好处是,您可以通过更改FormBorderStyle = FormBorderStyle.None属性随时找到所需的窗口并更改图像。

答案 5 :(得分:4)

没有直接的方法。但您可以尝试使用像this one

这样的图像转换为艺术转换器

答案 6 :(得分:1)

是的,如果您通过在控制台应用程序中打开Form来稍微延长问题,则可以执行此操作。

以下是如何让控制台应用程序打开表单并显示图像:

  • 在您的项目中包含以下两个引用:System.DrawingSystem.Windows.Forms
  • 还包括两个名称空间:
using System.Windows.Forms;
using System.Drawing;

请参阅this post on how to do that

现在你需要它来添加这样的东西:

Form form1 = new Form();
form1.BackgroundImage = bmp;
form1.ShowDialog();

当然,您也可以使用PictureBox ..

您可以使用form1.Show();在预览显示时保持控制台处于活动状态..

原帖:当然,您无法在25x80窗口中正确显示内部图像;即使你使用一个更大的窗口并阻止图形,它也不是预览而是一团糟!

更新:看起来你可以在GDI之后将图像绘制到控制台表单上;见塔弗的回答!