图片转 ASCII art

0. 背景

  春节期间,小伙伴分享了一个视频并实名吐槽我……

  作为一名敬 (kǔ) 业 (bī) 的码农,这个有意思的玩意儿那是必须得了解一下的,经过奇奇怪怪的搜索找到原来这玩意儿叫 ASCII art,网上已经有很多关于 ASCII art 的文章,然后经历了各种查资料 + 瞎折腾,于是就有了这篇笔记,本文仅记录一下图片的玩法,视频暂不涉及 (~ ̄▽ ̄)~。

  我们现在常见的一些 ASCII art 实例:

1. 简介

  ASCII艺术是一种图形设计技术,它使用计算机进行演示,由 1963 年 ASCII 标准定义的95个可打印(总共 128 个)字符和具有专有扩展字符的 ASCII 兼容字符集(超过 128个 的标准 7 位 ASCII 字符)拼接而成。该术语也通常用于指代基于文本的视觉艺术。ASCII艺术可以使用任何文本编辑器创建,并且通常与自由格式语言一起使用。大多数 ASCII 艺术示例都需要固定宽度的字体(非传统打字机上的非比例字体)。

  最古老的 ASCII 艺术案例是当时为贝尔实验室工作的计算机艺术先驱肯尼斯·诺尔顿于1966年左右创造的。

  在很大程度上,发明 ASCII 艺术是因为早期的打印机通常缺乏图形能力,因此使用字符代替图形标记。此外,为了标记来自不同用户的不同打印作业之间的划分,批量打印机通常使用 ASCII 艺术来打印大海报,使得划分更容易,从而可以更容易地由计算机操作员分离结果。早期的电子邮件中无法嵌入图片也使用了 ASCII 艺术。


  上面几段直接摘抄自维基百科 -_-|| 简而言之,ASCII art 就是用 ASCII 字符绘制一幅图画,这里就不赘述了。下面主要记录一下图像转成 ASCII 文本的原理和 JavaScript 的实现方式。

2. 原理

  位图是由像素点组成的,每个像素点用 RGB 值表示,一幅图像的颜色数据是由一个 RGB 像素点矩阵构成,也即对应 R、G、B 三个颜色分量矩阵。ASCII art 是由字符粗略表示颜色的二维形式,三个颜色值矩阵无法直接表示一个二维数组,所以要对图像进行灰度化。

2.1 灰度值和灰度化

  在计算机领域中,灰度(Gray scale)数字图像是每个像素只有一个采样颜色的图像。这类图像通常显示为从最暗黑色到最亮的白色的灰度,值一般为 0-255。

  在图像处理中,图像的灰度化的就是把由 RGB 三通道的数据的彩色图像变为单通道的数据的灰度图像,在 RGB 模型中如果 R=G=B 时,则彩色表示一种灰度颜色,灰度值就是 R、G、B 的值,即三者相等时只需要存储该灰度值,而对于 R、G、B 不同的像素值也可以进行灰度化。

  根据重要性及其它指标,将三个分量以不同的权值进行加权平均。由于人眼对绿色的敏感最高,对蓝色敏感最低,因此,按下式对 RGB 三分量进行加权平均能得到较合理的灰度图像。

f(i,j)=0.30R(i,j)+0.59G(i,j)+0.11B(i,j)

2.2 “灰度”字符

  我们可以用字符表示不同的灰度,比如 0 用 @ 表示,255 用 (空格)表示等等,分别代表颜色的深浅;当然没必要用 256 个字符表示 256 个灰度等级也不容易找齐这么多字符,所以设置灰度范围对应一个字符,比如 0~24 这 25 个灰度都用 @ 表示,以此类推。

2.3 像素精度

  假设一张图片的像素是 800*600,则像素点有 480000 个,对于一个 ASCII art 来说全部转为字符其实没有必要(当然要全部转掉也是可以的),所以可以设置像素精度,比如每 10*10 个像素点用一个字符表示。

  “近看爱因斯坦远看梦露”这张图挺有意思,人眼看 ASCII art 效果其实和这张图类似。

3. 实现

  这次用了 nodejs 来尝试一下,获取图片像素信息用了get-pixels 这个库(get-pixels 支持 PNG、JPEG、GIF 格式)。因为存在动画 GIF,所以这里实现暂时只支持 PNG 和 JPEG 格式的图片,也还没有具体研究图片的数据结构 0.0,步骤其实很简单:

  1. 读取图片文件,获取图片宽高和像素点数据

  2. 构造 灰度/字符 哈希表,设置灰度精度

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function getCharMap() {
    const chars = [].slice.call(arguments);
    const grayScaleRange = 256;
    let grayScaleBlockCount = ~~(grayScaleRange / chars.length);
    if (grayScaleRange % chars.length !== 0) {
    grayScaleBlockCount += 1;
    }
    const map = {};
    for (let i = 0; i < grayScaleRange; i++) {
    map[i] = chars[~~(i / grayScaleBlockCount)];
    }
    return map;
    }
    const CHAR_MAP = getCharMap('@', '#', '$', '=', '*', '!', ';', ':', '~', '-', ',', '.', ' ');
  3. 设置 ASCII art 矩阵精度

  4. 转换图片像素数据

    1. 根据矩阵精度设置矩阵块
    2. 计算每个矩阵块的平均灰度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function getGrayScale(r, g, b) {
    return ~~(0.3 * r + 0.59 * g + 0.11 * b);
    }
    function getMatrixAverageGrayScale(x, y, width, height, pixels) {
    let grayScaleAmount = 0;
    for (let h = 0; h < height; h++) {
    for (let w = 0; w < width; w++) {
    let [r, g, b] = [0, 1, 2].map(index => pixels.get(x + w, y + h, index));
    grayScaleAmount += getGrayScale(r, g, b);
    }
    }
    return ~~(grayScaleAmount / (width * height));
    }
  5. 输出字符矩阵文本

  6. * 在 Web 显示设置等宽字体或字间距样式来防止“图片变形”

  7. 效果截图

具体实现代码在这里 ヾ(・ε・`*)

4. 参考资料