Hey world!

I am singlebyte :D


  • 首页

  • 归档

图片转 ASCII art

发表于 2019-02-26

0. 背景

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

微信截图

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

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

curl 下载进度条

知乎 浏览器控制台打印

bilibili 浏览器控制台打印

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. 效果截图

    lenna

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

4. 参考资料

  • Wiki: ASCII art
  • 关于Ascii-art的一些总结
  • 图片转ASCII字符画算法
  • Jscii
  • 百度百科 - 灰度值
  • 图像处理的灰度化和二值化

Webpack 4 —— 神秘的 SplitChunks 插件

发表于 2018-11-02

原文链接 Webpack 4 — Mysterious SplitChunks Plugin

Webpack-4 正式版夸赞了它更快的构建速度(大约 98%)和更小的 chunk 体积。

然而 Webpack 的作者在开发者社区丢了一颗炸弹,他发布了一则关于负责跨多入口代码拆分的插件的公告。文档中 CommonsChunkPlugin 部分写道:

https://webpack.js.org/plugins/commons-chunk-plugin/

以下是我的一次简单的尝试,通过举一个常见的例子来理解并帮助你使用 SplitChunksPlugin chunks 的配置选项。


作为一个早期的爱好者我曾试图理解代码拆分背后的魔法。文档说 “splitChunks.chunks” 选项有三个可选值:“initial”、“async” 和 “all”。我有点困惑,也更加激发了我的好奇心!

我深入挖掘 Github history 和 WebpackOptions Schema 的文档找到了这些内容:

​ “三个可选值:initial、async 和 all”,配置优化时只能选择 初始块、按需块或所有块 —— Github History

​ “选择用于确定共享模块的块(默认 “async”、“initial” 或 “all” 需要将这些块添加至 HTML)” —— WebpackOptions Schema


举个例子,有两个导入相同 node_modules 的入口文件 a.js 和 b.js,将其中一些模块动态导入来检查代码拆分的行为。

我们用 webpack-bundle-analyzer 这个插件来帮助我们理解模块是如何被拆分的。

a.js :

只有 lodash 动态导入

1
2
3
4
5
6
7
8
import "react";
import("lodash"); // dynamic import
import "jquery";

console.log('I am a');
var a = 10;

export default a;

b.js :

React 和 lodash 动态导入

1
2
3
4
5
6
7
8
import("react"); // dynamic import
import("lodash"); // dynamic import
import "jquery";

console.log('I am b');
var b = 10;

export default b;

我选这个配置的主要原因是来理解当存在公共库的时候 Webpack 的配置是如何表现的,

  1. React - 一个入口动态导入另一个非动态导入
  2. lodash - 两个入口都动态导入
  3. jquery - 两个入口都非动态导入

我们保持这些文件不变,改变 Webpack 配置中 chunks 的值。

1. chunks: “async” —— 优化动态模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var entry = {
a: './public/js/a.js',
b: './public/js/b.js'
};

var output = {
filename: '[name].bundle.js'
};

var optimization = {
splitChunks: {
cacheGroups: {
node_vendors: {
test: /[\\/]node_modules[\\/]/,
chuns: 'async',
priority: 1
}
}
}
};

module.exports.config = {
entry: entry,
output: output,
optimization: optimization
};

webpack.config.js

BundleAnalyzer 截图

​ chunks: 'async' 告诉 webpack,

​ “嘿,webpack!我只关心动态导入的模块的优化,别管非动态模块。”

现在我们一步步看看发生了什么:

  • Webpack 把 react 从 b.js 里拎出来放到一个新文件里,a.js 里的不动。优化只发生在动态模块上,import("react") 这句声明会导致拆分文件,而 import "react" 不会。
  • Webpack 把 lodash 从 a.js 里拎出来放到一个新文件里,这个新文件也被 b.js 引用。
  • 尽管 a.js 和 b.js 都引用了 jquery 但是并不会对它进行优化。为啥?(提示:‘Async’)

2. chunks: “initial” —— 优化同步模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var entry = {
a: './public/js/a.js',
b: './public/js/b.js'
};

var output = {
filename: '[name].bundle.js'
};

var optimization = {
splitChunks: {
cacheGroups: {
node_vendors: {
test: /[\\/]node_modules[\\/]/,
chuns: 'initial',
priority: 1
}
}
}
};

module.exports.config = {
entry: entry,
output: output,
optimization: optimization
};

webpack.config.js

BundleAnalyzer 截图

​ chunks: 'initial' 告诉 webpack,

​ “嘿,webpack!我不关心动态导入的模块,你可以把它们每个都拆成单独的文件。然而我希望把所有非动态导入的模块打在一个包里,尽管我准备好跟其他文件共享并且分块这些非动态导入的模块,如果这些文件也需要非动态导入的模块的话。”

我们再一步步看看发生了什么:

  • a.js 里的 react 将被移到 node_vendors~a.bundle.js 而 b.js 里的 react 将被移到它单独的包 0.bundle.js 里。
  • a.js 和 b.js 里的 lodash 将被移到 1.bundle.js 里。为啥?这个 lodash 是个动态导入的模块。
  • jquery 作为一个非动态导入的公共模块将在 node_vendors~a~b.bundle.js 中被 a.js 和 b.js 共享。

3. chunks: “all” —— 优化同步和异步模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var entry = {
a: './public/js/a.js',
b: './public/js/b.js'
};

var output = {
filename: '[name].bundle.js'
};

var optimization = {
splitChunks: {
cacheGroups: {
node_vendors: {
test: /[\\/]node_modules[\\/]/,
chuns: 'all',
priority: 1
}
}
}
};

module.exports.config = {
entry: entry,
output: output,
optimization: optimization
};

webpack.config.js

BundleAnalyzer 截图

​ chunks: 'all' 告诉 webpack,

​ “嘿,webpack!我不管这个模块是动态还是非动态导入的,把它们全部优化掉。但是要确保……呐,你够聪明去干这活儿!”

我们继续一步步看看发生了什么:

  • react 在 a.js 中非动态导入,在 b.js 中动态导入。所以它转到一个单独的文件 0.bundle.js 中被两个文件引用。
  • lodash 在两个文件中都是动态导入的,所以显然得到一个单独的文件 1.bundle.js。
  • jquery 在两个文件中都是非动态导入的,所有它转到公共的共享模块 node_vendors~a~b.bundle.js,被两个文件引用。

最后


# 译注

​ 看到最后发现还是没讲为什么这么干,但是大致可以猜测是个什么样的规则,可能还是得去官方源码中找答案…… TODO!

HEXO 测试

发表于 2017-10-04

这是一篇测试文章。
现在只有三句话。
这是最后一句。

老蛋

老蛋

你的世界因你而美

3 日志
1 标签
© 2019 老蛋
由 Hexo 强力驱动
|
主题 — NexT.Gemini