图像到ASCII艺术转换

序幕

这个问题不时在这里popup来,但通常是因为写得不好而被删除。 我看到很多这样的问题,然后在请求附加信息时,从OP (通常的低代表)沉默。 有时候,如果input对我来说足够好,我决定回答一个答案,通常在活动时每天得到一些赞成票,但几个星期后问题就会被删除/删除,一切从头开始。 所以我决定写这个问答,所以我可以直接参考这些问题,而不必一遍又一遍地重写答案。

另一个原因是这个META线程针对我,所以如果你有额外的input随时发表评论。

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

一些限制:

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

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

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

基于像素/面积强度(阴影)

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

  1. 线性分布的强度特征图

    所以我们只使用与同一步骤有强度差异的字符。 换句话说,当按升序sorting时:

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

    同样,当我们的字符mapsorting,然后我们可以直接从强度计算字符(不需要search)

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

    所以我们有一些可用的字符和它们的强度。 我们需要find最接近intensity_of(dot)intensity_of(dot)所以,如果我们对map[]进行sorting,我们可以使用二分search,否则我们需要O(n)search最小距离循环或O(1)字典。 有时为了简单起见,字符map[]可以被处理为线性分布,从而导致通常在结果中看不到的轻微的伽玛失真,除非您知道要寻找什么。

基于强度的转换也适用于灰度图像(不仅仅是黑白)。 如果select点作为单个像素,则结果会变大(1像素 – >单个字符),因此对于较大的图像,将select一个区域(字体大小的乘积),以保留宽高比并且不会放大太多。

怎么做:

  1. 如此均匀地将图像分成(灰度)像素或(矩形)区域
  2. 计算每个像素/面积的强度
  3. 用最接近强度的字符图replace字符

作为字符map您可以使用任何字符,但如果字符的像素沿字符区域均匀分散,结果会变得更好。 对于初学者,您可以使用:

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

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

所以如果像素/区域的强度是i = <0-255>那么replace字符将会是

  • 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[li]; } s+=endl; } mm_log->Lines->Text=s; mm_log->Lines->SaveToFile("pic.txt"); delete bmp; 

除非你使用Borland / Embarcadero环境,否则你需要replace/忽略VCL的东西

  • mm_log是输出文本的备忘录
  • bmp是input位图
  • AnsiString是VCLtypes的string索引forms1不是从0作为char*

这是结果: 稍微NSFW强度示例图像

左边是ASCII艺术输出(字体大小5px),右边的input图像放大几倍。 正如你所看到的输出是更大的像素 – >字符。 如果您使用更大的区域而不是像素,那么缩放比较小,但是当然输出的视觉效果不太好。 这种方法对代码/处理非常简单快捷。

当你添加更高级的东西,如:

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

那么你可以处理更复杂的图像,更好的结果:

这里导致1:1比例(缩放查看字符):

强度先进的例子

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

稍微NSFW强度先进的示例图像

正如你所看到的,这更适合更大的图像

字符拟合(Shading和Solid ASCII Art之间的混合)

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

  1. 将图像均匀地分割成(灰阶)矩形区域
    • 理想情况下与渲染的字体字符具有相同的长宽比(它将保留纵横比,不要忘记字符通常在X轴上重叠一点)
  2. 计算每个区域的强度( dot
  3. 用最接近强度/形状的字符mapreplace字符

如何计算字符和点之间的距离? 这是这种方法中最困难的部分。 在实验过程中,我在速度,质量和简单性之间形成了这种妥协:

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

    区

    • 从您的转换字母表中计算每个字符的左,右,上,下和中间区域的单独亮度( map
    • 规范所有的强度,使他们是独立的面积大小i=(i*256)/(xs*ys)
  2. 在矩形区域处理源图像

    • (具有与目标字体相同的宽高比)
    • 对于每个区域计算强度的方式与第1项相同
    • 从转换字母表中的强度中find最接近的匹配
    • 输出合适的字符

这是字体大小= 7px的结果

字符拟合的例子

正如你所看到的,即使使用更大的字体,输出也是视觉上令人满意的(以前的方法是使用5px的字体大小)。 输出的大小与input图像(无缩放)大致相同。 更好的结果是由于字符更接近于原始图像不仅是强度,而且整体的形状,因此你可以使用更大的字体,仍然保留细节(直到一个粗糙的点)。

这里是基于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[li]; } 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(); } //--------------------------------------------------------------------------- 

这是简单的forms应用程序( Form1 )与其中单个TMemo mm_txt 。 它加载图像"pic.bmp" ,然后根据分辨率select使用哪种方法转换为文本保存到"pic.txt" ,并发送到备忘录可视化。 对于那些没有VCL的人来说,忽略VCL的东西,用任何你所拥有的stringtypes来取代AnsiString ,而且Graphics::TBitmap位图或图像类可以用像素访问function。

非常重要的是,这使用mm_txt->Font的设置,所以确保你设置:

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

为了使这个工作正常,否则字体将不会被处理为单间隔。 鼠标滚轮只是上下改变字体大小,以查看不同字体大小的结果

[笔记]

  • 请参阅Word肖像可视化
  • 使用位图/文件访问和文本输出function的语言
  • 强烈build议从第一种方法开始,因为它很容易向前和简单,只有然后移动到第二个(这可以作为修改的第一个,所以大部分的代码保持不变)
  • 由于标准文本预览在白色背景上,因此计算反转强度(黑色像素是最大值)是一个好主意,因此导致更好的结果。
  • 您可以尝试细分区域的大小,数量和布局,或者使用一些像3x3网格来代替。

[编辑1]比较

最后,这里是两种方法在相同input上的比较:

对照

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

[编辑2]很酷的应用程序

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

确定应用程序只包含2个窗口。 第一个主窗口基本上是我的旧转换器窗口没有图像select和预览(所有上面的东西都在其中)。 它只有ASCII预览和转换设置。 第二个窗口是空的forms,里面有透明的抓取区域select(没有任何function)。

现在在计时器上,我只需要通过select表单来select区域,将其传递给转换并预览ASCIIart

因此,您可以通过select窗口附上要转换的区域,并在主窗口中查看结果。 它可以是一个游戏,观众,…看起来像这样:

ASCIIart抓取器的例子

所以现在我甚至可以在ASCIIart上观看video。 有些非常好:)。

[EDIT3]

如果你想尝试在GLSL中实现这个function,请看这个:

  • 将浮点数转换为GLSL中的十进制数字?