图片转ASCII字符图案的原理(可调整亮度对比度 宽高度)
来, 先看效果哈哈哈哈!
演示地址: http://ascii-picture.imlht.com/
平时看代码会看到很多标点符号的字符拼起来的图案, 特别有趣, 像kong(一个高性能API网关), 除了源代码里面有图案, 命令行也藏了彩蛋:
Kong, the biggest ape in town /\ ____ <> ( oo ) <>_| ^^ |_ <> @ \ /~~\ . . _ | /~~~~\ | | /~~~~~~\/ _| | |[][][]/ / [m] |[][][[m] |[][][]| |[][][]| |[][][]| |[][][]| |[][][]| |[][][]| |[][][]| |[][][]| |[|--|]| |[| |]| ======== ========== |[[ ]]| ==========
上面这个图案, 只是停留在外形轮廓上, 而我今天要玩的会深入一点: 基于图片的灰度值来生成图案. 此时的图片不单单有轮廓, 还有光影效果, 也就是素描中提及的黑白灰.
原理实际上挺简单的, 在白色背景下, 字符 $
会有比较大面积的黑, 而字符 +
相对就淡了很多, 毫无疑问, 空格就是纯白了. 所以, 只要把一些字符按照 白
, 灰
, 黑
排序, 并把这些字符映射为 0-255 的灰度值, 就可以根据图片生成更生动的字符画了.
至于这些字符按照灰度排序, 已经有人帮我们做好了, 具体可以查看这个Demo, 是用 Python
写的:
ascii_char = list("$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. ")
看到这里, 是时候拿起 Python
干起来了! 可以照着链接在自己电脑跑一下, 制作一些白色背景的表情包, 但如果是照片的话会发现很糊, 根本看不清, 于是我拿出神器 Photoshop
调整了 亮度
和 对比度
, 尽量调高点, 生成的图案会清晰一些.
每次都去 Photoshop
调整真是繁琐, 每次失败了, 得重新用命令行生成, 然后看生成的图案怎么样, 一直重复这个步骤...而且宽度和高度都需要手工指定...所以萌生了这个想法: 把这些重复繁琐的操作, 交给界面去处理好了! 所以后面的代码都是用 JavaScript
实现的.
OK, 我们先扯回来, 说下灰度的映射算法, 也是很容易理解的, 上面的字符一共有 69
个, 0-255
一共有 256
个字符, 计算出比率 ratio
然后直接把字符取出来即可:
/** * ASCII Charset * * @type {String} */ const charset = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "; /** * 69/256 * * @type {Number} */ const ratio = charset.length / 256; /** * 颜色值转换为 ASCII 字符 * * @param {Number} r R * @param {Number} g G * @param {Number} b B * @param {Number} a A * @param {Number} type 类型 * @return {String} ASCII 字符 */ export const rgba_to_char = (r, g, b, a, type) => { if (a === 0) return ' '; r = Math.round(a / 255 * r); g = Math.round(a / 255 * g); b = Math.round(a / 255 * b); return charset[ Math.round( ratio * rgb_to_gray(r, g, b, type) ) ] || ' '; };
根据灰度生成字符, 那灰度怎么来的? 扒了挺多资料, 总体来说有几个公式, 具体可以看这篇文章
Gray = R*0.299 + G*0.587 + B*0.114
上面的 Python
代码用的是这个公式, 参考知乎:
Gray = 0.2126 R' + 0.7152 G' + 0.0722 B'
还有另一种, 这个是我实验后发现的, 用这个方法生成的图案细节会多一些, 大家也可以试试看. 算法是比较复杂的, 基本原理是将 RGB 色彩转为 XYZ 色彩, 再从 XYZ 转到 Lab. Lab颜色空间中的L分量用于表示像素的亮度, 最小值是0(纯黑), 最大值是100(纯白), 而a表红绿, b表黄蓝. 我们需要的是灰度值算法, 所以只需L分量就可以了.
再加上平均值, 最大值, 只取绿色通道, 一共就有6种算法, 代码实现如下:
/** * 颜色值转换为灰度 * * @param {Number} r R * @param {Number} g G * @param {Number} b B * @param {Number} type 类型 * @return {Number} 灰度值 */ const rgb_to_gray = (r, g, b, type) => { switch (type) { case 1: return g; case 2: return Math.max(r, g, b); case 3: return Math.round((r + g + b) / 3); case 4: return Math.round(0.299 * r + 0.587 * g + 0.114 * b); case 5: return Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b); case 6: // https://github.com/antimatter15/rgb-lab/blob/master/color.js // https://github.com/markusn/color-diff/blob/master/lib/convert.js r /= 255; g /= 255; b /= 255; r = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92; g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92; b = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92; let x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047; let y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.00000; let z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883; x = (x > 0.008856) ? Math.pow(x, 1 / 3) : (7.787 * x) + 16 / 116; y = (y > 0.008856) ? Math.pow(y, 1 / 3) : (7.787 * y) + 16 / 116; z = (z > 0.008856) ? Math.pow(z, 1 / 3) : (7.787 * z) + 16 / 116; return Math.round(255 / 100 * ((116 * y) - 16)); } };
OK, 目前我们已经实现了彩色的像素值变成ASCII字符, 接下来要解决一个问题, 调整图像的亮度和对比度, 同样也是有公式的, 参考链接:
bitmap() { return this.data.map((x, i) => { if ((i+1) % 4 === 0) { // alpha return x; } // http://blog.csdn.net/hbaizj/article/details/17376857 const B = this.brightness / 100; const c = this.contrast / 100; const k = Math.tan( (45 + 44 * c) / 180 * 3.1416 ); return [x - 127.5 * (1 - B)] * k + 127.5 * (1 + B); }); }
最后, 我们只需把用户选择的图片, 转换为 RGB
值, 加上亮度对比度, 宽度高度的变换, 就大功告成了:
onchange() { const files = document.getElementById('file').files; if (!files || files.length === 0) return; const that = this; let fr = new FileReader(); fr.onload = function (event) { let img = new Image(); img.onload = function () { let c = document.createElement('canvas'); if (!that.width && !that.height) { that.width = img.width; that.height = img.height; } else if (!that.width) { that.width = Math.round(img.width * (that.height / img.height)); } else if (!that.height) { that.height = Math.round(img.height * (that.width / img.width)); } c.width = that.width; c.height = that.height; let ctx = c.getContext('2d'); ctx.drawImage(img, 0, 0, that.width, that.height); that.data = ctx.getImageData(0, 0, that.width, that.height).data; } img.src = event.target.result; } fr.readAsDataURL(files[0]); }
完整的源码, 我放到 GitHub 上了, 求Star求Star求Star! 代码是用 Vue2
写的(上面的代码都是再里面摘出来的), 结合了饿了么前端框架做界面, 目前先这样, 有时间再调整下界面吧.
演示地址: http://ascii-picture.imlht.com/
文章来源于本人博客,发布于 2017-12-28,原文链接:https://imlht.com/archives/93/
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话