正如其他答案已经指出的那样,使用 ASCII 阴影字符从 16 种基本颜色生成更多颜色的技术称为抖动。抖动是以牺牲一些图像分辨率为代价的。另见传说中的8088 Corruption/8088 Domination程序。
我想为您提供一些关于如何通过算法查找颜色对和抖动阴影字符的代码。下面的方法既适用于 Windows/linux 控制台,也适用于 SSH 和适用于 Windows 的 Linux 子系统。
一般程序是:
- 将源图像缩小到控制台分辨率
- 将每个像素的颜色映射到最匹配的控制台颜色
- 用选定的颜色绘制块/阴影字符
作为测试图像,我使用 HSV 颜色图:
首先,这里是双倍垂直分辨率的 16 色。使用块字符(char)223 (▀),您可以通过使用文本/背景颜色来独立绘制每个字符的上半部分和下半部分,将垂直分辨率提高一倍。为了匹配颜色,我使用了目标和探针颜色 rgb 分量之间的距离矢量,并对所有 16 种不同颜色进行了蛮力测试。函数sq(x) 返回正方形x*x。
int get_console_color(const int color) {
const int r=(color>>16)&255, g=(color>>8)&255, b=color&255;
const int matches[16] = {
sq(r- 0)+sq(g- 0)+sq(b- 0), // color_black 0 0 0 0
sq(r- 0)+sq(g- 55)+sq(b-218), // color_dark_blue 1 0 55 218
sq(r- 19)+sq(g-161)+sq(b- 14), // color_dark_green 2 19 161 14
sq(r- 58)+sq(g-150)+sq(b-221), // color_light_blue 3 58 150 221
sq(r-197)+sq(g- 15)+sq(b- 31), // color_dark_red 4 197 15 31
sq(r-136)+sq(g- 23)+sq(b-152), // color_magenta 5 136 23 152
sq(r-193)+sq(g-156)+sq(b- 0), // color_orange 6 193 156 0
sq(r-204)+sq(g-204)+sq(b-204), // color_light_gray 7 204 204 204
sq(r-118)+sq(g-118)+sq(b-118), // color_gray 8 118 118 118
sq(r- 59)+sq(g-120)+sq(b-255), // color_blue 9 59 120 255
sq(r- 22)+sq(g-198)+sq(b- 12), // color_green 10 22 198 12
sq(r- 97)+sq(g-214)+sq(b-214), // color_cyan 11 97 214 214
sq(r-231)+sq(g- 72)+sq(b- 86), // color_red 12 231 72 86
sq(r-180)+sq(g- 0)+sq(b-158), // color_pink 13 180 0 158
sq(r-249)+sq(g-241)+sq(b-165), // color_yellow 14 249 241 165
sq(r-242)+sq(g-242)+sq(b-242) // color_white 15 242 242 242
};
int m=195075, k=0;
for(int i=0; i<16; i++) if(matches[i]<m) m = matches[k=i];
return k;
}
16 种颜色是相当有限的。所以解决方法是抖动,混合两种颜色以获得更好的颜色,代价是图像分辨率。我使用阴影字符(char)176/(char)177/(char)178(Windows)或\u2588/\u2584/\u2580(Linux);这些表示为 (░/▒/▓)。在我使用的 12x7 字体大小中,颜色混合比分别为 1:6、2:5 和 1:2。要查找字体设置的混合比例,请在控制台中打印三个阴影字符、截屏、放大并计算像素。
三种不同的明暗比将 16 种基色变成了多达 616 种颜色,这还不包括重复的颜色。为了匹配最接近的颜色,我首先将颜色与阴影字符比率混合,然后计算目标的距离向量以探测 rgb 颜色分量,并对所有探测颜色组合进行暴力破解。为了编码使用哪个阴影字符以及哪两种颜色是前景色和背景色,我使用位移将其全部转换为一个 int 返回值。
int get_console_color_dither(const int color) {
const int r=(color>>16)&255, g=(color>>8)&255, b=color&255;
const int red [16] = { 0, 0, 19, 58,197,136,193,204,118, 59, 22, 97,231,180,249,242};
const int green[16] = { 0, 55,161,150, 15, 23,156,204,118,120,198,214, 72, 0,241,242};
const int blue [16] = { 0,218, 14,221, 31,152, 0,204,118,255, 12,214, 86,158,165,242};
int m=195075, k=0;
for(int i=0; i<16; i++) {
for(int j=0; j<16; j++) {
const int mixred=(red[i]+6*red[j])/7, mixgreen=(green[i]+6*green[j])/7, mixblue=(blue[i]+6*blue[j])/7; // (char)176: pixel ratio 1:6
const int match = sq(r-mixred)+sq(g-mixgreen)+sq(b-mixblue);
if(match<m) {
m = match;
k = i<<4|j;
}
}
}
for(int i=0; i<16; i++) {
for(int j=0; j<16; j++) {
const int mixred=(2*red[i]+5*red[j])/7, mixgreen=(2*green[i]+5*green[j])/7, mixblue=(2*blue[i]+5*blue[j])/7; // (char)177: pixel ratio 2:5
const int match = sq(r-mixred)+sq(g-mixgreen)+sq(b-mixblue);
if(match<m) {
m = match;
k = 1<<8|i<<4|j;
}
}
}
for(int i=0; i<16; i++) {
for(int j=0; j<i; j++) {
const int mixred=(red[i]+red[j])/2, mixgreen=(green[i]+green[j])/2, mixblue=(blue[i]+blue[j])/2; // (char)178: pixel ratio 1:2
const int match = sq(r-mixred)+sq(g-mixgreen)+sq(b-mixblue);
if(match<m) {
m = match;
k = 2<<8|i<<4|j;
}
}
}
return k;
}
最后,通过位移位和位掩码提取阴影字符和两种颜色:
const int dither = get_console_color_dither(rgb_color);
const int textcolor=(dither>>4)&0xF, backgroundcolor=dither&0xF;
const int shade = dither>>8;
string character = ""
switch(shade) {
#if defined(_WIN32)
case 0: character += (char)176; break;
case 1: character += (char)177; break;
case 2: character += (char)178; break;
#elif defined(__linux__)
case 0: character += "\u2591"; break;
case 1: character += "\u2592"; break;
case 2: character += "\u2593"; break;
#endif // Windows/Linux
}
print(character, textcolor, backgroundcolor);
print(...) 函数由here 提供。生成的图像如下所示:
最后,没有 Lenna 测试图像,任何 asciiart 帖子都是不完整的。这向您展示了对抖动的期望。