图像到ASCII艺术转换

时间:2015-10-07 08:16:48

标签: c++ image-processing bitmap 2d ascii-art

序言

此主题会不时在 SO 上弹出,但通常会因为写得不好而被删除。我看到很多这样的问题,然后在请求附加信息时从 OP (通常的低代表)中保持沉默。如果输入对我来说足够好,我决定回答它并且通常在活动时每天获得一些向上投票,但几周之后问题被删除/删除并且所有从一开始就开始。所以我决定写下 Q& A ,这样我就可以直接引用这些问题,而不会一遍又一遍地重写答案......

另一个原因也是这个META thread针对我,所以如果您有其他意见,请随时发表评论。

问题

如何使用 C ++ 将位图图像转换为 ASCII art

一些限制:

  • 灰度图像
  • 使用单倍间距字体
  • 保持简单(不要为初级程序员使用太高级的东西)

这是一个相关的Wiki页面ASCII art(感谢@RogerRowland)

1 个答案:

答案 0 :(得分:141)

还有更多的图像到ASCII艺术转换的方法,主要是基于使用单倍间距字体来简化我只坚持基础:

基于像素/区域强度(阴影)

此方法将像素区域的每个像素处理为单个点。我们的想法是计算这个点的平均灰度强度,然后将其替换为具有足够接近计算强度的字符。为此我们需要一些可用字符列表,每个字符都有预先计算的强度,我们称之为字符map。要更快地选择哪个角色最适合哪种强度有两种方式:

  1. 线性分布的强度字符图

    因此我们只使用具有相同步长的强度差异的字符。换句话说,当按升序排序时:

    intensity_of(map[i])=intensity_of(map[i-1])+constant;
    

    当我们的角色map被排序时,我们可以直接从强度计算角色(不需要搜索)

    character=map[intensity_of(dot)/constant];
    
  2. 任意分布式强度字符映射

    所以我们有一系列可用的字符及其强度。我们需要找到最接近intensity_of(dot)的强度。如果我们对map[]进行排序,我们可以使用二分搜索,否则我们需要O(n)搜索最小距离循环或O(1)字典。有时为了简单起见,字符map[]可以处理为线性分布,导致结果中通常看不到的轻微伽玛失真,除非您知道要查找的内容。

  3. 基于强度的转换对于灰度图像(不仅仅是黑白)也很有用。如果您选择点作为单个像素,则结果会变大(1个像素 - >单个字符),因此对于较大的图像,选择一个区域(字体大小的倍数)来保留纵横比并且不会放大太多。

    怎么做:

    1. 将图像均匀分割为(灰度)像素或(矩形)区域' s
    2. 计算每个像素/区域的强度
    3. 使用最接近强度的字符映射中的字符替换
    4. 作为字符map,您可以使用任何字符,但如果字符沿着字符区域均匀分布像素,则结果会更好。对于初学者,您可以使用:

      • char map[10]=" .,:;ox%#@";

      按降序排序并假装线性分布。

      因此,如果像素/区域的强度为i = <0-255>,则替换字符将为

      • map[(255-i)*10/256];

      如果i==0则像素/区域为黑色,如果i==127则像素/区域为灰色,如果i==255则像素/区域为白色。您可以在map[] ...

      中尝试不同的字符

      这是我在C ++和VCL中的古老例子:

      AnsiString m=" .,:;ox%#@";
      Graphics::TBitmap *bmp=new Graphics::TBitmap;
      bmp->LoadFromFile("pic.bmp");
      bmp->HandleType=bmDIB;
      bmp->PixelFormat=pf24bit;
      
      int x,y,i,c,l;
      BYTE *p;
      AnsiString s,endl;
      endl=char(13); endl+=char(10);
      l=m.Length();
      s="";
      for (y=0;y<bmp->Height;y++)
          {
          p=(BYTE*)bmp->ScanLine[y];
          for (x=0;x<bmp->Width;x++)
              {
              i =p[x+x+x+0];
              i+=p[x+x+x+1];
              i+=p[x+x+x+2];
              i=(i*l)/768;
              s+=m[l-i];
              }
          s+=endl;
          }
      mm_log->Lines->Text=s;
      mm_log->Lines->SaveToFile("pic.txt");
      delete bmp;
      
      除非你使用Borland / Embarcadero环境

      ,否则你需要替换/忽略VCL内容
      • mm_log是输出文本的备忘录
      • bmp是输入位图
      • AnsiString是VCL类型字符串索引形式1而不是0 char* !!!

      这是结果:Slightly NSFW intensity example image

      左边是ASCII艺术输出(字体大小5px),右边输入图像缩放几次。正如您所看到的,输出是更大的像素 - &gt;字符。如果你使用更大的区域而不是像素,那么缩放比较小,但当然输出在视觉上不那么令人愉悦。 这种方法非常简单快速地进行编码/处理。

      当您添加更多高级内容时:

      • 自动地图计算
      • 自动像素/区域大小选择
      • 宽高比校正

      然后您可以处理更复杂的图像并获得更好的结果:

      这里产生1:1的比例(缩放以查看字符):

      intensity advanced example

      当然,对于区域采样,您会丢失细节。这是与区域采样的第一个示例大小相同的图像:

      Slightly NSFW intensity advanced example image

      如您所见,这更适合更大的图像

      字符拟合(着色和纯ASCII艺术之间的混合)

      这种方法试图用具有相似强度和形状的字符替换区域(不再是单个像素点)。这导致更好的结果,即使使用比以前的方法更大的字体,另一方面,这种方法当然有点慢。有更多的方法可以做到这一点,但主要的想法是计算图像区域(dot)和渲染字符之间的差异(距离)。您可以从像素之间的初始abs差异开始,但这将导致不太好的结果,因为即使1像素移位也会使距离变大,相反,您可以使用相关或不同的指标。整体算法与以前的方法几乎相同:

      1. 将图像均匀地划分为(灰度)矩形区域&#39; s
        • 理想情况下具有与渲染字体字符相同的宽高比(它将保留纵横比,不要忘记字符通常在x轴上重叠一点)
      2. 计算每个区域的强度(dot
      3. 将字符map中的字符替换为最接近的强度/形状
      4. 如何计算字符和点之间的距离?这是这种方法中最难的部分。在尝试实验时,我在速度,质量和简单性之间制定了这种妥协:

        1. 将字符区域划分为区域

          zones

          • 从转换字母表中计算每个字符的左,右,上,下和中心区域的单独强度(map
          • 将所有强度标准化,使它们与区域大小i=(i*256)/(xs*ys)
          • 无关
        2. 处理矩形区域中的源图像

          • (与目标字体具有相同的宽高比)
          • 对于每个区域以与子弹1相同的方式计算强度
          • 从转换字母表中找到与强度最接近的匹配
          • 输出拟合字符
        3. 这是字体大小= 7px

          的结果

          char fitting example

          正如您所看到的那样,即使使用更大的字体大小,输出也在视觉上令人愉悦(之前的方法示例是5px字体大小)。输出大小与输入图像大小相同(无缩放)。获得了更好的效果,因为角色不仅通过强度而且通过整体形状更接近原始图像,因此您可以使用更大的字体并且仍然保留细节(直到粗略点)。

          以下是基于VCL的转换应用的完整代码:

          //---------------------------------------------------------------------------
          #include <vcl.h>
          #pragma hdrstop
          
          #include "win_main.h"
          //---------------------------------------------------------------------------
          #pragma package(smart_init)
          #pragma resource "*.dfm"
          TForm1 *Form1;
          Graphics::TBitmap *bmp=new Graphics::TBitmap;
          //---------------------------------------------------------------------------
          class intensity
              {
          public:
              char c;                 // character
              int il,ir,iu,id,ic;     // intensity of part: left,right,up,down,center
              intensity() { c=0; reset(); }
              void reset() { il=0; ir=0; iu=0; id=0; ic=0; }
              void compute(DWORD **p,int xs,int ys,int xx,int yy) // p source image, (xs,ys) area size, (xx,yy) area position
                  {
                  int x0=xs>>2,y0=ys>>2;
                  int x1=xs-x0,y1=ys-y0;
                  int x,y,i;
                  reset();
                  for (y=0;y<ys;y++)
                   for (x=0;x<xs;x++)
                      {
                      i=(p[yy+y][xx+x]&255);
                      if (x<=x0) il+=i;
                      if (x>=x1) ir+=i;
                      if (y<=x0) iu+=i;
                      if (y>=x1) id+=i;
                      if ((x>=x0)&&(x<=x1)
                        &&(y>=y0)&&(y<=y1)) ic+=i;
                      }
                  // normalize
                  i=xs*ys;
                  il=(il<<8)/i;
                  ir=(ir<<8)/i;
                  iu=(iu<<8)/i;
                  id=(id<<8)/i;
                  ic=(ic<<8)/i;
                  }
              };
          //---------------------------------------------------------------------------
          AnsiString bmp2txt_big(Graphics::TBitmap *bmp,TFont *font) // charcter sized areas
              {
              int i,i0,d,d0;
              int xs,ys,xf,yf,x,xx,y,yy;
              DWORD **p=NULL,**q=NULL;    // bitmap direct pixel access
              Graphics::TBitmap *tmp;     // temp bitmap for single character
              AnsiString txt="";          // output ASCII art text
              AnsiString eol="\r\n";      // end of line sequence
              intensity map[97];          // character map
              intensity gfx;
          
              // input image size
              xs=bmp->Width;
              ys=bmp->Height;
              // output font size
              xf=font->Size;   if (xf<0) xf=-xf;
              yf=font->Height; if (yf<0) yf=-yf;
              for (;;) // loop to simplify the dynamic allocation error handling
                  {
                  // allocate and init buffers
                  tmp=new Graphics::TBitmap; if (tmp==NULL) break;
                      // allow 32bit pixel access as DWORD/int pointer
                      tmp->HandleType=bmDIB;    bmp->HandleType=bmDIB;
                      tmp->PixelFormat=pf32bit; bmp->PixelFormat=pf32bit;
                      // copy target font properties to tmp
                      tmp->Canvas->Font->Assign(font);
                      tmp->SetSize(xf,yf);
                      tmp->Canvas->Font ->Color=clBlack;
                      tmp->Canvas->Pen  ->Color=clWhite;
                      tmp->Canvas->Brush->Color=clWhite;
                      xf=tmp->Width;
                      yf=tmp->Height;
                  // direct pixel access to bitmaps
                  p  =new DWORD*[ys];        if (p  ==NULL) break; for (y=0;y<ys;y++) p[y]=(DWORD*)bmp->ScanLine[y];
                  q  =new DWORD*[yf];        if (q  ==NULL) break; for (y=0;y<yf;y++) q[y]=(DWORD*)tmp->ScanLine[y];
                  // create character map
                  for (x=0,d=32;d<128;d++,x++)
                      {
                      map[x].c=char(DWORD(d));
                      // clear tmp
                      tmp->Canvas->FillRect(TRect(0,0,xf,yf));
                      // render tested character to tmp
                      tmp->Canvas->TextOutA(0,0,map[x].c);
                      // compute intensity
                      map[x].compute(q,xf,yf,0,0);
                      } map[x].c=0;
                  // loop through image by zoomed character size step
                  xf-=xf/3; // characters are usually overlaping by 1/3
                  xs-=xs%xf;
                  ys-=ys%yf;
                  for (y=0;y<ys;y+=yf,txt+=eol)
                   for (x=0;x<xs;x+=xf)
                      {
                      // compute intensity
                      gfx.compute(p,xf,yf,x,y);
                      // find closest match in map[]
                      i0=0; d0=-1;
                      for (i=0;map[i].c;i++)
                          {
                          d=abs(map[i].il-gfx.il)
                           +abs(map[i].ir-gfx.ir)
                           +abs(map[i].iu-gfx.iu)
                           +abs(map[i].id-gfx.id)
                           +abs(map[i].ic-gfx.ic);
                          if ((d0<0)||(d0>d)) { d0=d; i0=i; }
                          }
                      // add fitted character to output
                      txt+=map[i0].c;
                      }
                  break;
                  }
              // free buffers
              if (tmp) delete tmp;
              if (p  ) delete[] p;
              return txt;
              }
          //---------------------------------------------------------------------------
          AnsiString bmp2txt_small(Graphics::TBitmap *bmp)    // pixel sized areas
              {
              AnsiString m=" `'.,:;i+o*%&$#@"; // constant character map
              int x,y,i,c,l;
              BYTE *p;
              AnsiString txt="",eol="\r\n";
              l=m.Length();
              bmp->HandleType=bmDIB;
              bmp->PixelFormat=pf32bit;
              for (y=0;y<bmp->Height;y++)
                  {
                  p=(BYTE*)bmp->ScanLine[y];
                  for (x=0;x<bmp->Width;x++)
                      {
                      i =p[(x<<2)+0];
                      i+=p[(x<<2)+1];
                      i+=p[(x<<2)+2];
                      i=(i*l)/768;
                      txt+=m[l-i];
                      }
                  txt+=eol;
                  }
              return txt;
              }
          //---------------------------------------------------------------------------
          void update()
              {
              int x0,x1,y0,y1,i,l;
              x0=bmp->Width;
              y0=bmp->Height;
              if ((x0<64)||(y0<64)) Form1->mm_txt->Text=bmp2txt_small(bmp);
               else                 Form1->mm_txt->Text=bmp2txt_big  (bmp,Form1->mm_txt->Font);
              Form1->mm_txt->Lines->SaveToFile("pic.txt");
              for (x1=0,i=1,l=Form1->mm_txt->Text.Length();i<=l;i++) if (Form1->mm_txt->Text[i]==13) { x1=i-1; break; }
              for (y1=0,i=1,l=Form1->mm_txt->Text.Length();i<=l;i++) if (Form1->mm_txt->Text[i]==13) y1++;
              x1*=abs(Form1->mm_txt->Font->Size);
              y1*=abs(Form1->mm_txt->Font->Height);
              if (y0<y1) y0=y1; x0+=x1+48;
              Form1->ClientWidth=x0;
              Form1->ClientHeight=y0;
              Form1->Caption=AnsiString().sprintf("Picture -> Text ( Font %ix%i )",abs(Form1->mm_txt->Font->Size),abs(Form1->mm_txt->Font->Height));
              }
          //---------------------------------------------------------------------------
          void draw()
              {
              Form1->ptb_gfx->Canvas->Draw(0,0,bmp);
              }
          //---------------------------------------------------------------------------
          void load(AnsiString name)
              {
              bmp->LoadFromFile(name);
              bmp->HandleType=bmDIB;
              bmp->PixelFormat=pf32bit;
              Form1->ptb_gfx->Width=bmp->Width;
              Form1->ClientHeight=bmp->Height;
              Form1->ClientWidth=(bmp->Width<<1)+32;
              }
          //---------------------------------------------------------------------------
          __fastcall TForm1::TForm1(TComponent* Owner):TForm(Owner)
              {
              load("pic.bmp");
              update();
              }
          //---------------------------------------------------------------------------
          void __fastcall TForm1::FormDestroy(TObject *Sender)
              {
              delete bmp;
              }
          //---------------------------------------------------------------------------
          void __fastcall TForm1::FormPaint(TObject *Sender)
              {
              draw();
              }
          //---------------------------------------------------------------------------
          void __fastcall TForm1::FormMouseWheel(TObject *Sender, TShiftState Shift,int WheelDelta, TPoint &MousePos, bool &Handled)
              {
              int s=abs(mm_txt->Font->Size);
              if (WheelDelta<0) s--;
              if (WheelDelta>0) s++;
              mm_txt->Font->Size=s;
              update();
              }
          //---------------------------------------------------------------------------
          

          这是一个简单的表单应用(Form1),其中包含单个TMemo mm_txt。它会加载图像"pic.bmp",然后根据分辨率选择使用哪种方法转换为保存到"pic.txt"并发送到备忘录以进行可视化的文本。对于那些没有VCL的人来说,忽略VCL的东西并用你拥有的任何字符串类型替换AnsiString,以及Graphics::TBitmap任何具有像素访问功能的位图或图像类。{/ p>

          非常重要请注意,这会使用mm_txt->Font的设置,因此请务必设置:

          • Font->Pitch=fpFixed
          • Font->Charset=OEM_CHARSET
          • Font->Name="System"

          使这项工作正常,否则字体将不会被处理为单倍间距。鼠标滚轮只是向上/向下更改字体大小以查看不同字体大小的结果

          <强> [注释]

          • 请参阅Word Portraits visualization
          • 使用具有位图/文件访问和文本输出功能的语言
          • 强烈建议从第一种方法开始,因为它很容易前进和简单,然后才移到第二种方法(这可以作为第一种方法的修改,所以大部分代码保持不变)
          • 使用反转强度进行计算是个好主意(黑色像素是最大值)因为标准文本预览是在白色背景上,因此可以产生更好的效果。
          • 您可以尝试细分区域的大小,数量和布局,或者使用3x3之类的网格。

          [Edit1]比较

          最后,这是对同一输入的两种方法的比较:

          comparison

          绿点标记的图像使用方法#2 完成,红色的#1 全部采用6像素字体大小。正如你在灯泡图像上看到的那样,形状敏感的方法要好得多(即使<2>缩放的源图像上的#1 )。

          [Edit2]很酷的应用

          在阅读今天的新问题时,我得到了一个很酷的应用程序的想法,它抓住桌面的选定区域并不断将其提供给 ASCIIart 转换器并查看结果。经过一个小时的编码完成后,我对结果非常满意,我必须在这里添加它。

          确定应用程序仅包含2个窗口。第一个主窗口基本上是我的旧转换器窗口,没有图像选择和预览(上面的所有内容都在其中)。它只有ASCII预览和转换设置。第二个窗口是空的,内部是透明的,用于抓取区域选择(无任何功能)。

          现在在计时器上我只需按选择表格抓取所选区域,将其传递给转换并预览 ASCIIart

          因此,您可以通过选择窗口包含要转换的区域,并在主窗口中查看结果。它可以是游戏,观众......它看起来像这样:

          ASCIIart grabber example

          所以现在我可以在 ASCIIart 中观看视频,以获得乐趣。有些非常好:)。

          hands

          <强> [EDIT3]

          如果您想尝试在 GLSL 中实现此功能,请看一下: