Stavo leggendo degli spazi colore e lo spazio LAB sembra essere una buona opzione per te (vedi queste domande: Trovare una "distanza" accurata tra i colori e l' algoritmo per verificare la somiglianza dei colori )
Citando la pagina CIELAB di Wikipedia , i vantaggi di questo spazio colore sono:
A differenza dei modelli di colore RGB e CMYK, Lab color è progettato per approssimare la visione umana. Aspira all'uniformità percettiva e la sua componente L corrisponde strettamente alla percezione umana della leggerezza. Pertanto, può essere utilizzato per effettuare correzioni accurate del bilanciamento del colore modificando le curve di output nei componenti a e b.
Per misurare la distanza tra i colori è possibile utilizzare la distanza Delta E.
Con questo puoi approssimare meglio da Color
a ConsoleColor
:
Innanzitutto, puoi definire una CieLab
classe per rappresentare i colori in questo spazio:
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 };
}
}
Esistono due metodi statici, uno per misurare la distanza utilizzando Delta E ( DeltaE
) e l'altro per combinare due colori specificando la quantità di ciascun colore ( Combine
).
E per trasformare da RGB
a LAB
puoi usare il seguente metodo (da qui ):
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))
};
}
L'idea è usare caratteri sfumati come @AntoninLejsek do ('█', '▓', '▒', '░'), questo ti permette di ottenere più di 16 colori combinando i colori della console (usando il Combine
metodo).
Qui, possiamo apportare alcuni miglioramenti pre-calcolando i colori da utilizzare:
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
});
}
}
}
Un altro miglioramento potrebbe essere l'accesso diretto ai dati dell'immagine utilizzando LockBits
invece di utilizzare GetPixel
.
AGGIORNAMENTO : Se l'immagine ha parti con lo stesso colore puoi velocizzare notevolmente il processo disegnando pezzi di caratteri con gli stessi colori, invece di singoli caratteri:
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;
}
Infine, chiama in questo DrawImage
modo:
static void Main(string[] args)
{
ComputeColors();
Bitmap image = new Bitmap("image.jpg", true);
DrawImage(image);
}
Immagini dei risultati:
Le seguenti soluzioni non si basano sui caratteri ma forniscono immagini dettagliate complete
Puoi disegnare su qualsiasi finestra usando il suo gestore per creare un Graphics
oggetto. Per ottenere il gestore di un'applicazione console puoi farlo importando GetConsoleWindow
:
[DllImport("kernel32.dll", EntryPoint = "GetConsoleWindow", SetLastError = true)]
private static extern IntPtr GetConsoleHandle();
Quindi, crea una grafica con il gestore (usando Graphics.FromHwnd
) e disegna l'immagine usando i metodi in Graphics
oggetto, ad esempio:
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);
}
Questo sembra a posto, ma se la console viene ridimensionata o fatta scorrere, l'immagine scompare perché le finestre vengono aggiornate (forse nel tuo caso è possibile implementare una sorta di meccanismo per ridisegnare l'immagine).
Un'altra soluzione è incorporare una finestra ( Form
) nell'applicazione console. Per fare questo devi importare SetParent
(e MoveWindow
riposizionare la finestra all'interno della 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);
Quindi devi solo creare Form
e impostare la BackgroundImage
proprietà sull'immagine desiderata (fallo su a Thread
o Task
per evitare di bloccare la 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);
}
Ovviamente puoi impostare FormBorderStyle = FormBorderStyle.None
per nascondere i bordi delle finestre (immagine a destra)
In questo caso puoi ridimensionare la console e l'immagine / finestra sarà ancora lì.
Un vantaggio di questo approccio è che puoi posizionare la finestra dove vuoi e cambiare l'immagine in qualsiasi momento semplicemente cambiando la BackgroundImage
proprietà.