图片转ASCII字符图案的原理(可调整亮度对比度 宽高度)

2023-06-22 17:25:42 浏览数 (2)

来, 先看效果哈哈哈哈!

演示地址: http://ascii-picture.imlht.com/

代码语言:txt复制
            "` """        . "`"""""""""""""""""""w$@w"""""""""""""""""""                                      
                   """""""       `""""""""""""$$$$$$$$$00$$0"""""""""""""""""""                                
                            """"""""""""""""$$$$$$$$$$$$$$$$$$$$0""""""""""""""""""""                          
                                    """""""$$$$$$$$$$$$$$$$$$$$$$$$""""""0(""""""""""""""""                    
                                          $$$$$$$$$$$$$$$$$$$$$$$$$$00&0("""""""""""""""""""""""               
                         `               $$$$$$$$$$$$$$$$$$$$$$$$$$$$$&hLLLL(~~"""""""""""""""""""             
""".                             """""" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@0000000L""""""""""""""`           
""""""""""""""""                     ""0$$$$$$$$$0("(0$$$$$$$$$$$$$$$$$$$$$$$$$$$@&&h0000v"""""""""".          
"""""""""""""""""""""""""".          ""$$$$$$$$0"""""v00$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$000(""""""""            
"""""""""""""""""""""""""""""""""""""""$$$$$$0""""""""""(00h$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$w0"""               
"""""""""""""""""""""""""""""""""""""""$$$$00$$0""($$$$$$$"""$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$h"            
""""""""""""""""""""""""""""""""""""""0$$$$$$$$0"v$$$$0"(&0"""$$$$$$$$$$$$$&""$$$$$$$0""h$$$&h0w&$$$$"         
"""""""""""""""(vv~~"""""""""""""""""00&$$"0""0&0"$$$$$$$""""""$$$$$$$$$$$$$""$"$$$$$v0""$0$$$$$h$$$$$""""w"""0
"""""""""""""""0"0(0000v"0000"""""""0w0w$$""$$$""""""v""~"""`"""$$$$$$$$$$$(""""$$$$$$$""00"0$$h$&"~$$v""""""$@
   """""""""""("0"000"0"0000v"""""""0$$w$0 ""$"""` ""      """""$$$$$$$$$$$""`"""$$$$$$$0""0$$$($$(&$h"""""""$h
       """"""""~"""""0(0v0"00"""""""L$$h0     `   ""0"    .""""""0$$$v$$$$$""  "0""$$$$0"""w$$$$$w"~""`  """ @"
      """"""""""""~"""0v0000"""""""""$h&$    0 """"  $$""""""""""w$$0"$$$$$""""""0"" " """"v$$$$$$ .       " w"
      """""""""""0""""""""0""""""""""0$$@"""$$$$$$$$$""0&@&&"""""&0""$$$$$$$$$$$$$$$$$0"""L0"&$@""         " 0L
      ""  """"""""(0"""L~"""""""""""""0w$""L0"""$$" `"~0$$$&"""""$$$$$$$$$&$$$$$$$$$$$$$$$$$$"@""             "
      """""""""""""0""""""""""""""""""""""""""($$$$$$$00$hL~"""""$$$$$$$$$w@$$$$$$$$$$&$$$$$$0$&"              
""""""""""""""""""""""""""""""""""""""""""""""$$0@$$$$0w&0~vw&hwh@$$$$$$$""h$$ $$ $$$$"$$$$$~$"@"              
 """"""""""""""""""""""""""""""""""""""""""""" v"""""~"0h0$$$$@~v($$$$$$$""w$$$$   $$$0"$$$$&"h$"              
"""""""""""""""""""""""""""""""""""""""""""""""0"(@$$$wh&$$$$$@v""~$$$$$""""$$$$$jj$$$ "$$$$. 0 "      ;       
""""""""""""""""""""""""""""""""""""""""""` """~$$$0"""(&$$$$$$$"""$$$"     0$$$$$$$$$$ 0$$$$" ""     ;$$;"""""
"""""""""""""""""""""""""""""""""""""""""""" """""""L0$$$$$$$$$$$0""$               "L00w$$$$$.$L" """"$$,"""""
""""""""""""""""""""""""""""""""""""""""""""` "0w$$$$$$$$$$$$$$$$@"""                           "" """"$$$"$"`"
""""""""""""""""""""""""""""""""""""""""""""""  ""$$$Lh$$$$$$$$$$$&""                             "("0@@"""""""
"""""""""""""""""""""""""""""""""""""""""""""""   ." ""&$$$$$$$$$$$$$                              $$0"$$""""""
""""""""""""""""""""""""""""""""""""""""""""""""  .   "&$$$$$$$$$$$$$                              $$$$$$$""("$
"""""""""""""""""""""""""""""""""""""""""""""""""     "L$$$$$$$$$$$$w                               $$$$$$"@$$$
"""""""""""""""""""""""""""""""""""""""""""""          "$$$$$$$$$$$$"                                &$$$""@$$$
""""""""""""""""""""""""""""""""""""""""0""             $$$$$$$$$$$$                                 "$$~"""$$$
"""""""""""""""""""""""""""""""""""""""""              ."$$$$$$$$$$h                                  &$,$##$$0
"""""""""""""""""""""""""""""""""""""""                 ""$$$$$$$$@"                                   0~$-,$$$
"""""""""""""""""""""""""""""""""""""            ".     ..$$$$$$000"                         "     "   "$h""$0$
"""""""""""""""""""""""""""""""""""              ""        $$$$0h0$                         "" ""  "    $$$ """
""""""""""""""""""""""""""""""""""               ""        "$$$$$$$          "              """"  ""    $$$$0""
"""""""""""""""""""""""""""""""""""       "      ""         $$$$$$$         "              """  """     "$$$""0
"""""""""""""""""""""""""""&"""""""              ""         "$$$$$$       ""              ."".  .".      $$$00h

平时看代码会看到很多标点符号的字符拼起来的图案, 特别有趣, 像kong(一个高性能API网关), 除了源代码里面有图案, 命令行也藏了彩蛋:

代码语言:txt复制
Kong, the biggest ape in town

    /  ____
    <> ( oo )
    <>_| ^^ |_
    <>   @    
   /~~ . . _ |
  /~~~~    | |
 /~~~~~~/ _| |
 |[][][]/ / [m]
 |[][][[m]
 |[][][]|
 |[][][]|
 |[][][]|
 |[][][]|
 |[][][]|
 |[][][]|
 |[][][]|
 |[][][]|
 |[|--|]|
 |[|  |]|
 ========
==========
|[[    ]]|
==========

上面这个图案, 只是停留在外形轮廓上, 而我今天要玩的会深入一点: 基于图片的灰度值来生成图案. 此时的图片不单单有轮廓, 还有光影效果, 也就是素描中提及的黑白灰.

原理实际上挺简单的, 在白色背景下, 字符 $ 会有比较大面积的黑, 而字符 相对就淡了很多, 毫无疑问, 空格就是纯白了. 所以, 只要把一些字符按照 , , 排序, 并把这些字符映射为 0-255 的灰度值, 就可以根据图片生成更生动的字符画了.

至于这些字符按照灰度排序, 已经有人帮我们做好了, 具体可以查看这个Demo, 是用 Python 写的:

代码语言:Python复制
ascii_char = list("$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/|()1{}[]?-_ ~<>i!lI;:,"^`'. ")

看到这里, 是时候拿起 Python 干起来了! 可以照着链接在自己电脑跑一下, 制作一些白色背景的表情包, 但如果是照片的话会发现很糊, 根本看不清, 于是我拿出神器 Photoshop 调整了 亮度对比度, 尽量调高点, 生成的图案会清晰一些.

每次都去 Photoshop 调整真是繁琐, 每次失败了, 得重新用命令行生成, 然后看生成的图案怎么样, 一直重复这个步骤...而且宽度和高度都需要手工指定...所以萌生了这个想法: 把这些重复繁琐的操作, 交给界面去处理好了! 所以后面的代码都是用 JavaScript 实现的.

OK, 我们先扯回来, 说下灰度的映射算法, 也是很容易理解的, 上面的字符一共有 69 个, 0-255 一共有 256 个字符, 计算出比率 ratio 然后直接把字符取出来即可:

代码语言:JavaScript复制
/**
 * 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种算法, 代码实现如下:

代码语言:JavaScript复制
/**
 * 颜色值转换为灰度
 *
 * @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字符, 接下来要解决一个问题, 调整图像的亮度和对比度, 同样也是有公式的, 参考链接:

代码语言:JavaScript复制
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 值, 加上亮度对比度, 宽度高度的变换, 就大功告成了:

代码语言:JavaScript复制
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/

0 人点赞