此主题会不时在 SO 上弹出,但通常会因为写得不好而被删除。我看到很多这样的问题,然后在请求附加信息时从 OP (通常的低代表)中保持沉默。如果输入对我来说足够好,我决定回答它并且通常在活动时每天获得一些向上投票,但几周之后问题被删除/删除并且所有从一开始就开始。所以我决定写下 Q& A ,这样我就可以直接引用这些问题,而不会一遍又一遍地重写答案......
另一个原因也是这个META thread针对我,所以如果您有其他意见,请随时发表评论。
如何使用 C ++ 将位图图像转换为 ASCII art ?
一些限制:
这是一个相关的Wiki页面ASCII art(感谢@RogerRowland)
答案 0 :(得分:141)
还有更多的图像到ASCII艺术转换的方法,主要是基于使用单倍间距字体来简化我只坚持基础:
基于像素/区域强度(阴影)
此方法将像素区域的每个像素处理为单个点。我们的想法是计算这个点的平均灰度强度,然后将其替换为具有足够接近计算强度的字符。为此我们需要一些可用字符列表,每个字符都有预先计算的强度,我们称之为字符map
。要更快地选择哪个角色最适合哪种强度有两种方式:
线性分布的强度字符图
因此我们只使用具有相同步长的强度差异的字符。换句话说,当按升序排序时:
intensity_of(map[i])=intensity_of(map[i-1])+constant;
当我们的角色map
被排序时,我们可以直接从强度计算角色(不需要搜索)
character=map[intensity_of(dot)/constant];
任意分布式强度字符映射
所以我们有一系列可用的字符及其强度。我们需要找到最接近intensity_of(dot)
的强度。如果我们对map[]
进行排序,我们可以使用二分搜索,否则我们需要O(n)
搜索最小距离循环或O(1)
字典。有时为了简单起见,字符map[]
可以处理为线性分布,导致结果中通常看不到的轻微伽玛失真,除非您知道要查找的内容。
基于强度的转换对于灰度图像(不仅仅是黑白)也很有用。如果您选择点作为单个像素,则结果会变大(1个像素 - >单个字符),因此对于较大的图像,选择一个区域(字体大小的倍数)来保留纵横比并且不会放大太多。
怎么做:
作为字符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的比例(缩放以查看字符):
当然,对于区域采样,您会丢失细节。这是与区域采样的第一个示例大小相同的图像:
Slightly NSFW intensity advanced example image
如您所见,这更适合更大的图像
字符拟合(着色和纯ASCII艺术之间的混合)
这种方法试图用具有相似强度和形状的字符替换区域(不再是单个像素点)。这导致更好的结果,即使使用比以前的方法更大的字体,另一方面,这种方法当然有点慢。有更多的方法可以做到这一点,但主要的想法是计算图像区域(dot
)和渲染字符之间的差异(距离)。您可以从像素之间的初始abs差异开始,但这将导致不太好的结果,因为即使1像素移位也会使距离变大,相反,您可以使用相关或不同的指标。整体算法与以前的方法几乎相同:
dot
)map
中的字符替换为最接近的强度/形状如何计算字符和点之间的距离?这是这种方法中最难的部分。在尝试实验时,我在速度,质量和简单性之间制定了这种妥协:
将字符区域划分为区域
map
)i=(i*256)/(xs*ys)
处理矩形区域中的源图像
这是字体大小= 7px
的结果正如您所看到的那样,即使使用更大的字体大小,输出也在视觉上令人愉悦(之前的方法示例是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"
使这项工作正常,否则字体将不会被处理为单倍间距。鼠标滚轮只是向上/向下更改字体大小以查看不同字体大小的结果
<强> [注释] 强>
3x3
之类的网格。[Edit1]比较
最后,这是对同一输入的两种方法的比较:
绿点标记的图像使用方法#2 完成,红色的#1 全部采用6
像素字体大小。正如你在灯泡图像上看到的那样,形状敏感的方法要好得多(即使<2>缩放的源图像上的#1 )。
[Edit2]很酷的应用
在阅读今天的新问题时,我得到了一个很酷的应用程序的想法,它抓住桌面的选定区域并不断将其提供给 ASCIIart 转换器并查看结果。经过一个小时的编码完成后,我对结果非常满意,我必须在这里添加它。
确定应用程序仅包含2个窗口。第一个主窗口基本上是我的旧转换器窗口,没有图像选择和预览(上面的所有内容都在其中)。它只有ASCII预览和转换设置。第二个窗口是空的,内部是透明的,用于抓取区域选择(无任何功能)。
现在在计时器上我只需按选择表格抓取所选区域,将其传递给转换并预览 ASCIIart 。
因此,您可以通过选择窗口包含要转换的区域,并在主窗口中查看结果。它可以是游戏,观众......它看起来像这样:
所以现在我可以在 ASCIIart 中观看视频,以获得乐趣。有些非常好:)。
<强> [EDIT3] 强>
如果您想尝试在 GLSL 中实现此功能,请看一下: