在控制台应用中显示图像

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

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

输入:

enter image description here

产出:

enter image description here

enter image description here

enter image description here

enter image description here

74257 次浏览

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

Yes, you can do it, if you stretch the question a little by opening a Form from within the Console application.

Here is how you can have you console application open a form and display an image:

  • include these two references in your project: System.Drawing and System.Windows.Forms
  • include the two namespaces as well:

using System.Windows.Forms;
using System.Drawing;

See this post on how to do that!

Now all you need it to add something like this:

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

Of course you can also use a PictureBox..

And you can use form1.Show(); to keep the console alive while the preview shows..

Original post: Of course you can't properly display an image inside a 25x80 window; even if you use a larger window and block graphics it wouldn't be a preview but a mess!

Update: Looks like you can after all GDI-draw an image onto the Console Form; see taffer's answer!

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:

enter image description here

sample output:

enter image description here

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:

Text controls with image

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

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:

enter image description here

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...

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. Output result 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.

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:

Console1

Console2



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

Version 1

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

Version2

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.