Tôi đang đọc về không gian màu và không gian LAB có vẻ là một lựa chọn tốt cho bạn (xem câu hỏi này: Tìm “khoảng cách” chính xác giữa các màu và Thuật toán để kiểm tra độ tương đồng của các màu )
Trích dẫn trang CIELAB của Wikipedia , ưu điểm của không gian màu này là:
Không giống như các mô hình màu RGB và CMYK, màu Lab được thiết kế để gần đúng với tầm nhìn của con người. Nó khao khát sự đồng nhất về mặt tri giác và thành phần L của nó gần giống với nhận thức của con người về sự nhẹ nhàng. Do đó, nó có thể được sử dụng để thực hiện các hiệu chỉnh cân bằng màu chính xác bằng cách sửa đổi các đường cong đầu ra trong các thành phần a và b.
Để đo khoảng cách giữa các màu, bạn có thể sử dụng khoảng cách Delta E.
Với điều này bạn có thể xấp xỉ tốt hơn từ Color
để ConsoleColor
:
Đầu tiên, bạn có thể xác định một CieLab
lớp để đại diện cho màu sắc trong không gian này:
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 };
}
}
Có hai phương pháp tĩnh, một phương pháp để đo khoảng cách bằng cách sử dụng Delta E ( DeltaE
) và phương pháp khác để kết hợp hai màu xác định số lượng của mỗi màu ( Combine
).
Và để chuyển đổi từ RGB
sang LAB
bạn có thể sử dụng phương pháp sau (từ đây ):
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))
};
}
Ý tưởng là sử dụng các ký tự bóng râm như @AntoninLejsek do ('█', '▓', '▒', '░'), điều này cho phép bạn có được hơn 16 màu kết hợp các màu của bảng điều khiển (sử dụng Combine
phương pháp).
Tại đây, chúng tôi có thể thực hiện một số cải tiến bằng cách tính toán trước các màu sẽ sử dụng:
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
});
}
}
}
Một cải tiến khác có thể là truy cập trực tiếp vào dữ liệu hình ảnh bằng cách sử dụng LockBits
thay vì sử dụng GetPixel
.
CẬP NHẬT : Nếu hình ảnh có các phần có cùng màu, bạn có thể tăng tốc đáng kể quá trình vẽ các đoạn ký tự có cùng màu, thay vì các ký tự riêng lẻ:
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;
}
Cuối cùng, hãy gọi DrawImage
như vậy:
static void Main(string[] args)
{
ComputeColors();
Bitmap image = new Bitmap("image.jpg", true);
DrawImage(image);
}
Hình ảnh kết quả:
Các giải pháp sau đây không dựa trên ký tự nhưng cung cấp hình ảnh chi tiết đầy đủ
Bạn có thể vẽ qua bất kỳ cửa sổ nào bằng cách sử dụng trình xử lý của nó để tạo một Graphics
đối tượng. Để có được trình xử lý của ứng dụng bảng điều khiển, bạn có thể nhập GetConsoleWindow
:
[DllImport("kernel32.dll", EntryPoint = "GetConsoleWindow", SetLastError = true)]
private static extern IntPtr GetConsoleHandle();
Sau đó, tạo một đồ họa với trình xử lý (using Graphics.FromHwnd
) và vẽ hình ảnh bằng các phương thức trong Graphics
đối tượng, ví dụ:
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);
}
Điều này trông ổn nhưng nếu bảng điều khiển được thay đổi kích thước hoặc cuộn, hình ảnh sẽ biến mất do các cửa sổ được làm mới (có thể thực hiện một số loại cơ chế để vẽ lại hình ảnh có thể thực hiện được trong trường hợp của bạn).
Một giải pháp khác là nhúng một window ( Form
) vào ứng dụng console. Để làm điều này, bạn phải nhập SetParent
(và MoveWindow
di chuyển cửa sổ bên trong bảng điều khiển):
[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);
Sau đó, bạn chỉ cần tạo một thuộc tính Form
và đặt thuộc BackgroundImage
tính cho hình ảnh mong muốn (thực hiện trên Thread
hoặc Task
để tránh chặn bảng điều khiển):
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);
}
Tất nhiên bạn có thể đặt FormBorderStyle = FormBorderStyle.None
để ẩn đường viền cửa sổ (hình bên phải)
Trong trường hợp này, bạn có thể thay đổi kích thước bảng điều khiển và hình ảnh / cửa sổ vẫn ở đó.
Một lợi ích với cách tiếp cận này là bạn có thể định vị cửa sổ ở nơi bạn muốn và thay đổi hình ảnh bất kỳ lúc nào chỉ bằng cách thay đổi thuộc BackgroundImage
tính.