图片转 Unicode 四象限 ANSI:原理、实现与浏览器工具
图片转 Unicode 四象限 ANSI:原理、实现与浏览器工具
站内搜索
直接问 AI

图片转 Unicode 四象限 ANSI:原理、实现与浏览器工具

我之前已经有一个 truecolor half-block 版本:把图片压到终端宽度后,用上半块和下半块分别保存两行像素的颜色。这个方法稳定、兼容性好,但每个字符只能表达上下两个采样点。想让图片在命令行里更细,就要把一个字符格继续拆开,改用 Unicode 的四象限块字符。

这篇文章说明新版转换器的思路。使用入口在 实用功能 页面:把图片拖进去,生成预览,下载 .ans 文件,然后在支持 24-bit truecolor 的终端里运行 cat 文件名.ans

一、为什么不是简单地把四个像素直接塞进一个字符

ANSI 终端的一个字符格通常只能同时设置一种前景色和一种背景色。Unicode 四象限字符能决定这个字符格里哪些象限显示前景色,哪些象限显示背景色,但它不能让左上、右上、左下、右下四个象限分别拥有四种独立颜色。

所以“四象限 full color”实际面对一个约束:每个 2 x 2 像素块最多只能压缩成两组颜色。转换器要做的不是假装没有损失,而是在这个约束下找到误差最小的两色表示。

二、四象限字符怎样编码 2 x 2 像素

一个字符格被看作四个位置:左上、右上、左下、右下。每个位置要么属于前景色,要么属于背景色,因此一共有 16 种组合。常见字符包括空格、上半块、下半块、左半块、右半块、对角块和整块,例如 ▘ ▝ ▀ ▖ ▌ ▞ ▛ ▗ ▚ ▐ ▜ ▄ ▙ ▟ █

转换器先把原图按目标终端宽度缩放到一个采样网格。假设输出宽度是 180 个终端字符,那么采样网格的宽度就是 360 个像素;每两个横向采样点和两个纵向采样点合成一个终端字符。高度会按终端字体比例自动计算,默认把一个终端字符当作高宽比约 2:1。

三、怎样选择前景色、背景色和字符

对每一个 2 x 2 像素块,转换器枚举 16 种象限遮罩。某一种遮罩会把四个像素分成两组:前景组和背景组。前景组取平均 RGB 作为前景色,背景组取平均 RGB 作为背景色,然后计算四个原始像素与两组代表色之间的平方误差。

最终选择误差最小的一组:

  1. 枚举 16 种象限遮罩。
  2. 根据遮罩把四个采样像素分成前景组和背景组。
  3. 分别计算两组 RGB 平均值。
  4. 计算四个像素的重建误差。
  5. 输出误差最小的 Unicode 字符和 ANSI truecolor 前景/背景色。

这比固定使用半块字符更细,因为它同时保留了左右方向的边缘信息。代价是文件会更大,某些终端字体对四象限块的显示也可能不如半块稳定。

四、生成的 .ans 文件里是什么

.ans 文件本质上是普通 UTF-8 文本,只是里面包含 ANSI 转义序列。每个字符前会按需要写入类似 ESC[38;2;R;G;B;48;2;R;G;Bm 的控制码,分别设置前景色和背景色,然后写入一个四象限 Unicode 字符。每行结束时再用 ESC[0m 重置样式,避免背景色污染后面的命令行。

在 macOS Terminal、iTerm2、Windows Terminal、GNOME Terminal 等支持 truecolor 和 Unicode block elements 的终端中,可以直接运行:

cat image-quadrant-180x120-truecolor.ans

如果图片太宽,终端自动换行会破坏画面;这时要么把终端窗口拉宽,要么在转换时降低输出宽度。

五、为什么工具页不把图片上传到服务器

图片转换不需要服务器参与。浏览器已经能解码常见图片格式,并能通过 Canvas 读取缩放后的像素。把转换放在前端有三个好处:

  • 隐私更简单:图片不离开用户设备。
  • 存储压力更小:服务器不保存上传文件,也不需要定时清理原图。
  • 反馈更快:拖入图片后可以直接预览、调整宽度并重新生成。

页面仍然设置了 30 分钟失效逻辑:生成的 Blob 下载链接、预览 HTML 和当前图片引用会在本页中自动释放。用户已经下载到本机的 .ans 文件不受影响。

六、实现中的几个边界

第一,浏览器只能处理它自己能解码的格式。PNG、JPEG、WebP、GIF、AVIF 和 SVG 通常没有问题,但 HEIC、损坏文件或引用外部资源的 SVG 可能失败。

第二,四象限字符并不等于四倍无损分辨率。每个字符依然只有前景和背景两种颜色;复杂纹理、噪声和高频细节会被压缩成两组代表色。

第三,终端字体会影响观感。理想字体应该让块元素紧密贴合,没有明显缝隙;如果看到格线或字符宽度不齐,需要换等宽字体或调整终端行距。

七、为什么这版比 half-block 更适合细节图

Half-block 的优势是每个字符保存上下两行颜色,色彩表达稳定;四象限版本的优势是同一个字符里可以出现左右方向的结构,因此头发、轮廓线、小面积高光和斜边会更清楚。对于动漫头像、图标和边缘明显的插画,四象限通常能在相同终端宽度下保留更多形状信息。

如果目标是最大兼容性,half-block 仍然是很好的选择;如果目标是在支持 Unicode 和 truecolor 的现代终端里尽可能精细,四象限加最小误差两色聚类更合适。

八、ANSI 转换策略对照表

策略 每个字符表达的信息 适合图片 主要限制
Plain ASCII 只用字符密度近似明暗,不保留真实颜色。 高对比黑白图、终端兼容性要求极高的场景。 色彩和细小边缘损失最大。
Half-block truecolor 一个字符保存上下两个采样点,各自一组颜色。 照片、渐变、对兼容性要求较高的彩色图。 横向结构弱,斜线和小轮廓容易变粗。
Quadrant truecolor 一个字符覆盖 2 x 2 采样点,用两色最小误差近似。 头像、图标、插画、轮廓明显的图。 每格仍只有前景/背景两色,不是四象限四种独立颜色。
完整图片输出 保留原始像素,由图片查看器渲染。 需要精确保真、打印或后期处理的场景。 不再是纯终端文本,也不能直接嵌入命令行输出。

九、终端兼容性验证清单

生成 .ans 后,不建议只看浏览器预览就下结论。浏览器使用的是 Canvas 和页面字体,真实终端还会受字体、行高、色彩配置和自动换行影响。一个可复查的测试记录至少应包含终端名称、字体、字号、输出列宽、图片合成背景和实际运行命令。

检查项 通过标准 失败时优先调整
Truecolor 支持 渐变色和照片色块没有退化成 256 色近似。 更换终端或确认 COLORTERM=truecolor
Unicode block elements 四象限字符无缺字、无明显缝隙。 更换等宽字体,降低行距或改用 half-block。
输出宽度 一行 ANSI 内容不会被终端自动折行。 降低转换宽度,或把终端窗口拉宽后重新测试。
透明背景 透明区域与终端背景接近,不产生奇怪边框。 选择接近主题背景的合成色后重新导出。

终端字符不是正方形,图会被拉长

把 2×2 像素塞进一个字符格之后,还有一件必须处理的事:终端的字符格不是正方形。

绝大多数等宽字体的字符格宽高比在 1:2 附近——高度大约是宽度的两倍。四象限方案里,一个字符格横向放 2 个像素、纵向也放 2 个像素,于是每个像素在屏幕上占据的实际比例是「半格宽 × 一格高」,也就是高是宽的四倍

直接转换的结果就是图像被纵向拉长到原来的两倍,人脸会变成长脸。这个失真很明显,但因为终端里没有参照物,容易被误认为是”分辨率不够”。

修法是在采样之前先按字符格的宽高比预压缩:

CELL_RATIO = 2.0          # 字符格高/宽,多数终端在 1.8 ~ 2.2

# 目标字符列数决定横向采样数(每格 2 像素)
cols = target_cols
src_w, src_h = img.size
# 纵向要多压一倍,抵消字符格本身的高宽比
rows = int(cols * (src_h / src_w) / CELL_RATIO)
img = img.resize((cols * 2, rows * 2))

关键是 / CELL_RATIO 这一步。少了它图就是拉长的;而这个比例无法从程序里查到——它取决于用户的终端和字体设置,只能给一个可调的默认值。2.0 对大多数环境合适,建议把它做成参数而不是写死。

顺带一个验证方法:转一张正方形网格图。如果输出的格子在终端里看起来是正方形,比例就对了;这比拿照片试直观得多,因为照片没有已知的参考形状。

真彩色不是每个终端都支持

前面用的 24 位真彩色转义序列(\033[38;2;R;G;Bm)表达能力最好,但它需要终端支持。在只支持 256 色的环境里,这串序列要么被忽略、要么直接以乱码形式打印出来,整个输出就毁了。

能力检测的惯例是看两个环境变量:

import os
truecolor = os.environ.get('COLORTERM', '') in ('truecolor', '24bit')
term256   = '256color' in os.environ.get('TERM', '')

降级到 256 色时,需要把 RGB 映射到 xterm 的调色板。这个调色板的结构是:0–15 是系统色,16–231 是一个 6×6×6 的色立方,232–255 是 24 级灰阶。映射时先把每个通道量化到 6 档:

LEVELS = [0, 95, 135, 175, 215, 255]      # xterm 色立方的六档,不是等距的

def rgb_to_256(r, g, b):
    if abs(r - g) < 8 and abs(g - b) < 8 and 8 <= r <= 238:   # 接近灰色
        return 232 + (r - 8) * 24 // 231
    q = lambda v: min(range(6), key=lambda i: abs(LEVELS[i] - v))
    return 16 + 36 * q(r) + 6 * q(g) + q(b)

这段代码有两个容易写错的地方,都会静默产生错误的颜色。

第一,色立方的六档不是等距的。它们是 0、95、135、175、215、255——第一档到第二档跨了 95,之后每档只跨 40。用 v * 5 // 256 这类线性公式量化会得到系统性偏移,而且很容易在 v = 255 时算出索引 6,越界撞进灰阶段。可以用上面那种「取最近档位」的写法,它不依赖任何公式推导,也不会越界。

验证方法很直接:纯红应该映射到 196、纯绿 46、纯蓝 21、纯白 231、纯黑 16,这些是 xterm 调色板的固定值,对不上就是量化写错了。

第二,灰色要单独走灰阶段。色立方里能表示的灰色只有那六档,而 232–255 这段专门的灰阶有 24 档。不做这个分支的话,灰度图会出现明显的色阶断层——这恰恰是转黑白照片时最先被看出来的问题。灰阶的两端要留出边界(8 <= r <= 238),因为纯黑和纯白在色立方里已经有更准确的表示。

十、使用建议

  • 头像或竖图可以先用 160 到 200 列宽度试一次。
  • 如果终端会自动换行,就降低宽度,而不是继续提高精度。
  • 透明 PNG 建议选择接近终端背景的合成色。
  • 复杂照片会生成很大的 ANSI 文件,插画、图标和人物头像通常效果更好。

完整使用入口在 实用功能页面。转换器本身不依赖账号,也不需要后端保存文件;它只是把图片采样、两色近似和 ANSI 输出这三步放到了同一个浏览器界面里。

本文讲的整套遮罩搜索和真彩色编码,做成了可以直接用的网页工具:图片转 ANSI 字符画。拖入图片即可生成,转换全程在你自己的浏览器里完成,文件不上传服务器。

发表回复

向下探索