C# 将图片转换为ASCii字符

时间:2022-11-11 21:16:09

将图片转换为ASCii字符,就是根据图片的亮度(或者称灰度),用ASCii字符代替图片的像素点或图片的分块。效果如下图:
C# 将图片转换为ASCii字符
Lena灰度图
C# 将图片转换为ASCii字符
LenaASCii字符图


基本原理

灰度图

首先要引入一个概念:灰度图。
我们都知道,平时日常见到的彩色数字图片是由一个一个紧密排布的像素点组成,而每个像素点的颜色由红色Red、绿色Green和蓝色Blue三种颜色根据不同的比例组合而成。这种彩色图片通常也称为RGB图,其中R、G、B各自的取值范围在0-255,数值越大,说明该种颜色越深。例如:
RGB=[0,0,0]为黑色;RGB=[255,0,0]为红色;RGB=[255,255,255]为白色。
而灰度图则是人们日常看到的“黑白”图片,图片中每个像素点只有一个值,用来指示该像素的灰度,取值范围也是0-255.这种黑白图片就是人们常说的灰度图,例如Gray=255,该像素点为白色;Gray=0,该像素点为纯黑色;Gray值越接近255,该像素越接近白色;Gray值越接近0,该像素越接近黑色。

RGB图转灰度图

RGB图转换为灰度图的过程为根据RGB的值,使用某种算法将其转换为一个可以表征其灰度的0-255的值。以下是一种经典的算法:

f(Gray)=R0.299+G0.587+B0.114

ASCii字符的灰度

每一个ASCii字符如果看成一个小图片的话,是一个标准的Gray值只取0和255的灰度图,这种灰度值只存在0和255的图片又称为二值图。即,ASCii字符的小图片的每个像素点是一个非黑即白的灰度图,由于其体积较小,人眼在远观时可以认为这是一个很小的图片分块,而这个分块的灰度我们定义为该字符的平均灰度。例如,字符M中黑色像素点较为浓密,则该字符分块的灰度值更接近0,也就是说M字符看起来更“黑”,颜色更“深”;而字符-中黑色像素点较为稀疏,则该字符分块的灰度值更接近255,也就是说字符-看起来更“白”,颜色更“浅”。

转换原理

将一幅彩色图转换为ASCii字符需要经历两个步骤

  1. 将RGB图转换为Gray图
  2. 根据Gray图的灰度选择平均灰度相近的字符代替Gray图
Created with Raphaël 2.1.0 开始 读入RGB图 转换为Gray图 计算合理的分块大小 匹配每个分块近似的字符 结束

代码实现

RGB转Gray函数

这个函数较为简单,因为我们处理的过程中并不需要将灰度图显示出来,因此可以使用一个二维的整型数组来存储该图片的每个像素点的灰度。若需要将灰度图在C#中显示出来,则需要将原图的RGB值都设为其灰度,生成一个新的BitMap。代码示例如下:

private int[,] RGB2Gray(Bitmap bmp)
{
     int[,] Gray = new int[bmp.Width, bmp.Height];
     Color curColor;
     for (int i = 0; i < bmp.Width; i++)
     {
         for (int j = 0; j < bmp.Height; j++)
         {
             curColor = bmp.GetPixel(i,j);
             Gray[i, j] = (int)(curColor.R * 0.299 + curColor.G * 0.587 + curColor.B * 0.114);
         }
     }
     return Gray;
}

构建字符序列

在此处本程序并没有严格的计算每个字符的平均灰度,而是采用了较为直观的肉眼判断法,对灰度分为了15个档MNHQ&OC?7>!:-;.,灰度值由小增大。代码示例:

protected static readonly string charset = "MNHQ&OC?7>!:-;.";

替换字符

若将原图的每个像素都替换为字符,则会有以下两个问题:

  • 字符过多,转换时间长,最终形成的文档太过大而导致无法在屏幕中显示出直观的效果
  • 由于字符并不是一个正方形区域,最终会导致图片纵横比失真

针对以上两个问题,我们需要做以下两件事:

  • 对原图进行分块,使用一个字符代替一个分块,减少字符数量
  • 设置合理的分块纵横比,抵消图片纵横比失真

根据实验结果来看,将原图宽度分为100块结果较为理想;而由于字符本身的纵横比大约为2:1,因此将分块大小纵横比固定设置为1:2比较合理。
代码实现的基本原理如下:
1. 遍历每个像素的灰度值
2. 计算每个分块的平均灰度
3. 寻找对应等级的字符替代

代码示例如下:

//就算分块大小
int RowSize = pictureBox1.Width / 100 + 1;
int ColSize = RowSize / 2;

//转换字符
private void GenerateString(BackgroundWorker worker, DoWorkEventArgs e)
{
     Bitmap img = (Bitmap)pictureBox1.Image;

     int[,] GrayImg = RGB2Gray(img);
     //分块大小
     int RowSize = (int)numericUpDown1.Value;
     int ColSize = (int)numericUpDown2.Value;
     //遍历各分块
     for (int h = 0; h < img.Height/RowSize; h++)
     {
          int Hoffset = h * RowSize;
          for (int w = 0; w < img.Width/ColSize; w++)
          {
              int Woffset = w * ColSize;
              int AvgGray = 0;
              for (int x = 0; x < RowSize; x++)
              {
                  for (int y = 0; y < ColSize; y++)
                  {
                       AvgGray += GrayImg[Woffset + y, Hoffset + x];
                  }
              }
              AvgGray /= RowSize * ColSize;
              //计算灰度处在字符集的哪一个灰度等级,来选择合适的字符
              if (AvgGray / 17 < charset.Length)
              {
                   res += charset[AvgGray / 17];
              }
              else
              {
                   res += " ";
              }
              //报告完成进度
              int percentComplete = (int)((float)(h * img.Width / ColSize + w) / (float)((img.Height / RowSize) * (img.Width / ColSize)) * 100);
              if (percentComplete > highestPercentageReached)
              {
                  highestPercentageReached = percentComplete;
                  worker.ReportProgress(percentComplete);
              }
          }
          res += "\r\n";
      }

}

在具体实现的过程中,转换字符的时间较长,因此我加入了一个进度条来指示完成进度,代码中的比较奇怪的变量就是用于进度条更新的。关于进度条的使用可以参考我的这篇文章:C# ToolStripProgressBar 用法简析

总结

本文主要介绍了如何将图片转换为字符,一种很有意思图像处理技巧,此处贴出完整的工程代码:下载Pic2ASCii,快去给你心爱的人生成一个有趣的图片吧~