Tenho um aplicativo de console que gerencia imagens. Agora preciso de algo como uma visualização das imagens no aplicativo de console. Existe uma maneira de exibi-los no console?
Aqui está uma comparação das respostas atuais baseadas em caracteres:
Entrada:
Resultado:
Respostas:
Continuei brincando com o código de @DieterMeemken. Reduzi pela metade a resolução vertical e adicionei pontilhamento via ░▒▓. À esquerda está o resultado de Dieter Meemken, à direita meu. Na parte inferior está a imagem original redimensionada para coincidir com a saída. Embora a função de conversão do Malwyn seja impressionante, ela não usa todas as cores cinza, o que é uma pena.
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(); }
uso:
Bitmap bmpSrc = new Bitmap(@"HuwnC.gif", true); ConsoleWriteImage(bmpSrc);
EDITAR
Distância de cor é um tópico complexo ( aqui , aqui e links nessas páginas ...). Tentei calcular a distância em YUV e os resultados foram piores do que em RGB. Eles poderiam ser melhores com Lab e DeltaE, mas eu não tentei isso. A distância em RGB parece ser boa o suficiente. Na verdade, os resultados são muito semelhantes para as distâncias de euclidiana e manhattan no espaço de cores RGB, então eu suspeito que há poucas cores para escolher.
O resto é apenas comparação de força bruta de cor contra todas as combinações de cores e padrões (= símbolos). Eu declarei que a proporção de preenchimento para ░▒▓█ é 1/4, 2/4, 3/4 e 4/4. Nesse caso, o terceiro símbolo é de fato redundante para o primeiro. Mas se as proporções não fossem tão uniformes (depende da fonte), os resultados poderiam mudar, então eu deixei lá para melhorias futuras. A cor média do símbolo é calculada como a média ponderada de foregroudColor e backgroundColor de acordo com a proporção de preenchimento. Assume cores lineares, o que também é uma grande simplificação. Portanto, ainda há espaço para melhorias.
fonte
Embora mostrar uma imagem em um console não seja o uso pretendido do console, você certamente pode hackear as coisas, já que a janela do console é apenas uma janela, como qualquer outra janela.
Na verdade, uma vez comecei a desenvolver uma biblioteca de controles de texto para aplicativos de console com suporte gráfico. Nunca terminei isso, embora tenha uma demonstração de prova de conceito funcionando:
E se você obtiver o tamanho da fonte do console, poderá posicionar a imagem com muita precisão.
É assim que você pode fazer:
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); } } }
Veja como você pode obter o tamanho da fonte do console atual:
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); }
E as chamadas WinApi adicionais necessárias, constantes e tipos:
[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;
E o resultado:
[
fonte
Button
,TextBox
ainda estão faltando. Meu sonho é fazer um suporte XAML razoavelmente completo com vinculação de dados e com a filosofia "incorporar qualquer coisa em qualquer coisa" semelhante ao WPF. Mas estou muito longe disso ... bem, neste momento :)Se você usar ASCII 219 (█) duas vezes, terá algo como um pixel (██). Agora você está restrito pela quantidade de pixels e pelo número de cores em seu aplicativo de console.
se você manter as configurações padrão que você tem sobre 39x39 pixel, se você quiser mais você pode redimensionar a sua consola com
Console.WindowHeight = resSize.Height + 1;
eConsole.WindowWidth = resultSize.Width * 2;
você tem que manter a proporção da imagem o mais longe possível, então você não terá 39x39 na maioria dos casos
Malwyn postou um método totalmente subestimado para converter
System.Drawing.Color
paraSystem.ConsoleColor
então minha abordagem seria
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(); } }
então você pode
ConsoleWriteImage(new Bitmap(@"C:\image.gif"));
amostra de entrada:
saída de amostra:
fonte
foi divertido. Obrigado fubo , tentei a sua solução e consegui aumentar a resolução da pré-visualização em 4 (2x2).
Eu descobri que você pode definir a cor de fundo para cada caractere individual. Então, em vez de usar dois caracteres ASCII 219 (█), usei ASCII 223 (▀) duas vezes com cores diferentes de primeiro e segundo plano. Isso divide o grande Pixel (██) em 4 subpixels como este (▀▄).
Neste exemplo, coloquei as duas imagens uma ao lado da outra, para que você possa ver a diferença facilmente:
Aqui está o código:
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(); } } }
Para executar o exemplo, o bitmap "image.bmp" deve estar no mesmo diretório do executável. Aumentei o tamanho do console, o tamanho da visualização ainda é 39 e pode ser alterado em
int sMax = 39;
.A solução da taffer também é muito legal. Vocês dois têm meu voto positivo ...
fonte
Eu estava lendo sobre espaços de cores e o espaço LAB parece ser uma boa opção para você (veja estas perguntas: Encontrar uma “distância” precisa entre as cores e o algoritmo para verificar a similaridade das cores )
Citando a página CIELAB da Wikipedia , as vantagens deste espaço de cores são:
Para medir a distância entre as cores, você pode usar a distância Delta E.
Com isso você pode aproximar melhor de
Color
aConsoleColor
:Em primeiro lugar, você pode definir uma
CieLab
classe para representar as cores neste espaço: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 }; } }
Existem dois métodos estáticos, um para medir a distância usando Delta E (
DeltaE
) e outro para combinar duas cores especificando quanto de cada cor (Combine
).E para transformar de
RGB
em,LAB
você pode usar o seguinte método (a partir daqui ):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)) }; }
A ideia é usar caracteres de sombra como @AntoninLejsek do ('█', '▓', '▒', '░'), isso permite que você obtenha mais de 16 cores combinando as cores do console (usando o
Combine
método).Aqui, podemos fazer algumas melhorias ao pré-calcular as cores a serem usadas:
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 }); } } }
Outra melhoria pode ser o acesso direto aos dados da imagem usando em
LockBits
vez de usarGetPixel
.ATUALIZAÇÃO : Se a imagem tiver partes com a mesma cor você pode acelerar consideravelmente o processo de desenho de pedaços de caracteres com as mesmas cores, ao invés de caracteres individuais:
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; }
Finalmente, chame
DrawImage
assim:static void Main(string[] args) { ComputeColors(); Bitmap image = new Bitmap("image.jpg", true); DrawImage(image); }
Imagens de resultado:
As soluções a seguir não são baseadas em caracteres, mas fornecem imagens detalhadas completas
Você pode desenhar sobre qualquer janela usando seu manipulador para criar um
Graphics
objeto. Para obter o manipulador de um aplicativo de console, você pode importá-loGetConsoleWindow
:[DllImport("kernel32.dll", EntryPoint = "GetConsoleWindow", SetLastError = true)] private static extern IntPtr GetConsoleHandle();
Em seguida, crie um gráfico com o manipulador (usando
Graphics.FromHwnd
) e desenhe a imagem usando os métodos emGraphics
objeto, por exemplo: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); }
Parece bom, mas se o console for redimensionado ou rolado, a imagem desaparece porque a janela é atualizada (talvez seja possível implementar algum tipo de mecanismo para redesenhar a imagem no seu caso).
Outra solução é embutir uma janela (
Form
) no aplicativo de console. Para fazer isso, você deve importarSetParent
(eMoveWindow
realocar a janela dentro do 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);
Então você só precisa criar uma propriedade
Form
e definirBackgroundImage
a imagem desejada (faça em aThread
ouTask
para evitar bloquear o 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); }
Claro que você pode configurar
FormBorderStyle = FormBorderStyle.None
para ocultar as bordas das janelas (imagem à direita)Neste caso, você pode redimensionar o console e a imagem / janela ainda estará lá.
Um benefício dessa abordagem é que você pode localizar a janela onde quiser e alterar a imagem a qualquer momento, apenas alterando a
BackgroundImage
propriedade.fonte
Não há um caminho direto. Mas você pode tentar usar um conversor de imagem para ascii-art como este
fonte
Sim, você pode fazer isso, se esticar um pouco a questão abrindo um
Form
de dentro do aplicativo Console.Veja como você pode fazer com que seu aplicativo de console abra um formulário e exiba uma imagem:
System.Drawing
eSystem.Windows.Forms
using System.Windows.Forms; using System.Drawing;
Veja este post sobre como fazer isso !
Agora tudo o que você precisa para adicionar algo como isto:
Form form1 = new Form(); form1.BackgroundImage = bmp; form1.ShowDialog();
Claro, você também pode usar um
PictureBox
..E você pode usar
form1.Show();
para manter o console ativo enquanto a visualização é exibida.Postagem original: é claro que você não pode exibir uma imagem corretamente dentro de uma janela 25x80; mesmo se você usar uma janela maior e bloquear os gráficos, não será uma prévia, mas uma bagunça!
Atualização: Parece que você pode, afinal, desenhar uma imagem com GDI no Formulário do Console; veja a resposta do taffer!
fonte