【问题标题】:How can I split an 8-bit color value into two 4-bit color values?如何将 8 位颜色值拆分为两个 4 位颜色值?
【发布时间】:2011-06-01 08:59:41
【问题描述】:

所以我编写了一个程序,它读取位图并使用 windows.h 打印到控制台。

Windows(在控制台中)允许我为每个字符空间设置两种颜色 - 一种前景色和一种背景色。

我仅限于这些颜色的 4 位调色板:

http://www.infotart.com/blog/wp-content/uploads/2008/06/windows_4bit_color_swatches.png

我的程序适用于 16 种颜色,但我无法计算出 256 种颜色。 (或弄清楚是否有可能)

我需要获取索引颜色的 RGB 值(来自 256 种 8 位颜色,例如 224、64、0)并将其显示为 16 种可用颜色中的两种,其中一种是抖动的。

前景字符将是 ASCII 抖动字符之一(我认为是 176、177、178)。

所以我认为每个背景都需要有 0、128、255 等的 R、G、B 值 前景可以是 0、32、64、96、128、160、192、224 或 255

所以如果我有数字 RGB = 192,0,0 我可以将背景设置为 RGB = 128,0,0 并且前景为 RGB = 255,0,0,ASCII 字符为 176(25% 抖动)

如果我有一个单独的抖动字符可分别用于红色绿色和蓝色,这似乎会非常简单,但遗憾的是我没有。

我知道控制台是一个糟糕的选择,但我必须在没有 windows gdi 帮助的情况下尝试这样做。

我完全无法弄清楚这方面的算法,甚至无法确定我的逻辑是否有意义。

有人能解释一下吗?感谢所有帮助,我碰壁了。

【问题讨论】:

标签: c windows algorithm programming-languages colors


【解决方案1】:

正如其他答案已经指出的那样,使用 ASCII 阴影字符从 16 种基本颜色生成更多颜色的技术称为抖动。抖动是以牺牲一些图像分辨率为代价的。另见传说中的8088 Corruption/8088 Domination程序。

我想为您提供一些关于如何通过算法查找颜色对和抖动阴影字符的代码。下面的方法既适用于 Windows/linux 控制台,也适用于 SSH 和适用于 Windows 的 Linux 子系统。

一般程序是:

  1. 将源图像缩小到控制台分辨率
  2. 将每个像素的颜色映射到最匹配的控制台颜色
  3. 用选定的颜色绘制块/阴影字符

作为测试图像,我使用 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 帖子都是不完整的。这向您展示了对抖动的期望。

【讨论】:

    【解决方案2】:

    4 位色板具有 RGB 值,当您将其中两个与抖动字符混合时,RGB 值将是每个单独 RGB 元素的加权平均值。权重取决于所使用的抖动模式,因此对于棋盘模式,每个 RGB 值将具有相同的权重,因此红色+绿色变为:

    [255,0,0] + [0,128, 0] = [(255+0)/2, (0+128)/2, (0+0)/2] = [127, 64, 0] (浅褐色)

    其他模式的权重将由前景像素与背景像素的比例决定。

    使用它来有效地找到最近的颜色可能是困难的部分!三个字符,16 种颜色和两个前景/背景选项,有大量的组合,虽然我想在色域中可能会有很大的差距。如果您只需要从 256 调色板转换为这些组合之一,而不是从完整的 RGB 转换,那么一个简单的解决方案是编写一个程序来可能详尽地搜索前景、背景和抖动的最佳组合。 256 种颜色的颜色,并生成一个查找表,然后可以在最终应用程序中使用该表来生成直接查找。

    当然,这种固定查找表方法仅适用于 256 调色板也是固定的(不一定是这种情况)。如果不是,那么您可能需要确定一种更有效的方法来找到最佳匹配颜色。我确信它可能比单纯的详尽搜索更聪明。

    【讨论】:

      【解决方案3】:

      我建议阅读 ImageMagick (Apache 2.0 license) quantization (Color Reduction) 文档作为起点。然后你可以看看color quantization,其中我相信最流行的两种方法是median cut方法,或者使用octrees

      您可能还更喜欢在非 RGB color space 中工作,例如 Lab color space,因为它具有一些不错的属性,欧几里得距离更符合感知差异(Src:Wikipedia)。

      dithering 有几种,有序图案抖动、随机抖动和纠错抖动,在这种情况下,我相信您希望纠错以减少明显的颜色错误。

      【讨论】:

      • 他在控制台模式下没有那种灵活性 - 16 种固定的预定义颜色,每个字符空间两种颜色和三种固定抖动模式。这就是问题所在。
      • @Clifford:是的,你是对的,我确实忽略了他的限制,即为 MS-Windows 下的控制台应用程序执行此操作。谢谢。
      【解决方案4】:

      虽然这可能不是关于从 RGB 转换为彩色 ASCII 表示的直接答案,但8088 Corruption 程序可能是一个很好的参考,可以帮助您了解从位图图像转换为 CGA 可以采取的方法屏幕。

      8088 Corruption 程序旨在运行full-motion video with sound on an original IBM PC (Google Video link)

      在解释视频编解码器的设计方式(演示可在archive.org 获得)时,创作者尝试了多种技术,其中之一是使用“ASCII 抖动字符”,但对最终结果并不满意“图片”的质量。

      于是他继续尝试一种方法,将多个像素映射为一个 ASCII 字符。例如,如果有两条垂直重叠的线,则 ASCII 字符 X 将被绘制在屏幕上。

      我实际上并没有看过源代码(我相信它是用 x86 汇编编写的),但从我读过的对所用技术的描述来看,它可能值得一试看看。

      【讨论】:

      • +1 我不确定这是否能提供答案,但这太棒了!我想您必须使用过旧的 IBM CGA PC 才能理解那是多么令人印象深刻!?
      【解决方案5】:

      嗯,一般来说,您必须“发明”一种从任何 RGB 到特定颜色字符子集的映射。

      由于难以计算确切的公式,我可能会坚持使用一个巨大的预先计算的查找表。表格必须是 3 维的(R、G、B 的一个维度)和每个维度的 [0..255]。表格的每个单元格应包含三条信息(打包为 2 个字节):代表字符、前景色、背景色。

      该表应按以下方式预先计算:对于要用作输出的每个字符,选择每种前景色和背景色,然后计算该字符与该颜色显示的结果 RGB 混合。然后,具有给定 RGB 混合坐标的单元格应使用该字符和颜色的信息进行更新。

      当然会有空单元格,因为对于 256^3 种颜色,我们最多只有 256*16*16 个彩色字符变体,因此我们必须使用某种最接近的填充单元格来更新空颜色。

      然后,对于任何输入像素,我们只需查找该表,检索字符和颜色,然后将它们放入输出中。

      也有可能以相反的方式工作 - 计算 256x16x16 的表格与生成的 RGB 混合,然后搜索它以找到最适合输入 RGB 的混合。

      【讨论】:

        猜你喜欢
        • 2016-10-09
        • 2022-11-25
        • 2011-08-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-02-13
        • 2012-09-30
        • 2014-12-01
        相关资源
        最近更新 更多