Я читал о цветовых пространствах, и пространство LAB кажется вам хорошим вариантом (см. Следующие вопросы: Определение точного «расстояния» между цветами и алгоритм проверки схожести цветов )
Цитируя страницу Wikipedia CIELAB , преимущества этого цветового пространства:
В отличие от цветовых моделей RGB и CMYK, цвет Lab приближен к человеческому зрению. Он стремится к единообразию восприятия, и его L-компонент полностью соответствует человеческому восприятию легкости. Таким образом, его можно использовать для точной корректировки цветового баланса путем изменения выходных кривых в компонентах a и b.
Чтобы измерить расстояние между цветами, вы можете использовать расстояние Delta E.
С этим вы можете лучше приблизиться от Color
до ConsoleColor
:
Во-первых, вы можете определить 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
вы можете использовать следующий метод ( отсюда ):
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 do ('█', '▓', '▒', '░'), это позволяет получить более 16 цветов, комбинируя цвета консоли (используя 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
так:
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();
Затем создайте графику с помощью обработчика (using Graphics.FromHwnd
) и нарисуйте изображение, используя методы в Graphics
объекте, например:
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);
}
Это выглядит нормально, но если размер консоли изменяется или прокручивается, изображение исчезает, потому что окна обновляются (возможно, в вашем случае возможна реализация какого-то механизма для перерисовки изображения).
Другое решение - встроить window ( 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
и установить BackgroundImage
свойство для желаемого изображения (сделайте это на Thread
или, Task
чтобы не блокировать консоль):
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
скрытие границ окон (правое изображение)
В этом случае вы можете изменить размер консоли, и изображение / окно все еще будут там.
Одним из преимуществ этого подхода является то, что вы можете разместить окно там, где хотите, и изменить изображение в любое время, просто изменив BackgroundImage
свойство.