图像到ASCII艺术转换
c++
image-processing
11
0

序幕

该主题有时会在SO上弹出,但通常由于写作问题不佳而被删除。我看到了许多这样的问题,然后在请求其他信息时从OP (通常是低声望)中保持沉默。如果输入的内容对我来说足够好,我决定回答一个问题,在活动期间,它通常每天会得到几次投票,但是几周后,该问题就被删除/删除,所有问题均从头开始。 。因此,我决定写此问答,以便我可以直接参考此类问题,而不必一遍又一遍地重写答案……

另一个原因也是该META线程也针对我,因此,如果您有其他意见,请随时发表评论。

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

一些约束:

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

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

参考资料:
Stack Overflow
收藏
评论
共 1 个回答
高赞 时间 活跃

图像到ASCII艺术转换的方法更多,为简单起见,大多数方法都是基于使用等宽字体 ,我只遵循基本知识:

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

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

  1. 线性分布强度特征图

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

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

    同样,当我们的字符map排序后,我们可以直接从强度计算字符(无需搜索)

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

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

基于强度的转换对于灰度图像(不仅仅是黑白图像)也非常有用。如果将点选择为单个像素,结果将变大(1像素->单个字符),因此对于较大的图像,将选择一个区域(字体大小的倍数)以保留长宽比,并且不要太大。

怎么做:

  1. 所以均匀地划分图像(灰度)像素或(矩形)区域
  2. 计算每个像素/区域的强度
  3. 用强度最接近的字符映射中的字符替换它

作为字符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*

结果如下: NSFW强度示例图像

左侧是ASCII美工输出(字体大小为5px),右侧是输入图像放大了几次。如您所见,输出是较大的像素->字符。如果使用较大的区域而不是像素,则缩放较小,但是输出的视觉效果当然较差。 这种方法非常容易且快速地进行编码/处理。

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

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

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

这导致1:1比例(放大以查看字符):

强度高级示例

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

NSFW强度稍高的示例图像

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

字符拟合(在底纹和纯ASCII艺术之间混合)

这种方法尝试用强度和形状相似的字符替换区域(不再有单个像素点)。另一方面,与以前的方法相比,即使使用更大的字体,这也会导致更好的结果,但是这种方法当然要慢一些。有更多方法可以执行此操作,但是主要思想是计算图像区域( dot )和渲染字符之间的差异(距离)。您可以从像素之间的绝对绝对差之和开始,但这将导致效果不佳,因为即使移动1个像素也会使距离变大,而可以使用相关性或不同的指标。总体算法与以前的方法几乎相同:

  1. 所以均匀地分割图像(灰度)的矩形区域
    • 理想情况下,其长宽比应与呈现的字体字符相同(它将保留长宽比,请不要忘记字符通常在x轴上重叠一点)
  2. 计算每个区域的强度( dot
  3. 用强度/形状最接近的字符map的字符替换它

如何计算字符和点之间的距离?那是这种方法中最难的部分。在进行实验时,我在速度,质量和简单性之间取得了这种折衷:

  1. 将角色区域划分为区域

    区域

    • 从转换字母( map )计算每个字符的左,右,上,下和中心区域的单独强度
    • 归一化所有强度,因此它们独立于面积大小i=(i*256)/(xs*ys)
  2. 在矩形区域中处理源图像

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

这是字体大小= 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替换为您拥有的具有像素访问功能的任何位图或图像类。

非常重要的一点是,此操作使用mm_txt->Font的设置,因此请确保设置:

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

使其正常工作,否则字体将不被视为等宽字体。鼠标滚轮只是向上/向下更改字体大小,以查看不同字体大小的结果

[笔记]

  • 请参阅Word Portraits可视化
  • 使用具有位图/文件访问和文本输出功能的语言
  • 强烈建议从第一种方法开始,因为这非常容易向前和简单,然后才移至第二种方法(可以对第一种方法进行修改,因此大多数代码仍然保持原样)
  • 用反向强度(黑色像素为最大值)进行计算是一个好主意,因为标准文本预览位于白色背景上,因此效果更好。
  • 您可以尝试细分区域的大小,数量和布局,也可以改用3x3网格。

[Edit1]比较

最后,这是在相同输入上的两种方法之间的比较:

比较

绿点标记的图像使用方法#2完成,而红点标记的图像则使用6像素字体大小的方法#1 。如您在灯泡图像上看到的,形状敏感方法要好得多(即使#1是在2倍缩放的源图像上完成的)。

[Edit2]很酷的应用

在阅读当今的新问题时,我想到了一个很酷的应用程序,该应用程序可以捕获选定的桌面区域,并将其连续馈送到ASCIIart转换器并查看结果。经过一个小时的编码,它完成了,我对结果非常满意,我只需要在这里添加它即可。

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

现在在计时器上,我只是通过选择表单来抓取所选区域,将其传递给转换并预览ASCIIart

因此,您可以在选择窗口中封闭要转换的区域,并在主窗口中查看结果。它可以是一个游戏,查看器,...看起来像这样:

ASCIIart采集器示例

因此,现在我什至可以观看ASCIIart中的视频来娱乐。有些真的很好:)。

手

[Edit3]

如果您想尝试在GLSL中实现此功能,请查看以下内容:

收藏
评论
新手导航
  • 社区规范
  • 提出问题
  • 进行投票
  • 个人资料
  • 优化问题
  • 回答问题