图像到 ASCII 艺术转换

Posted

技术标签:

【中文标题】图像到 ASCII 艺术转换【英文标题】:Image to ASCII art conversion 【发布时间】:2016-01-04 09:10:33 【问题描述】:

序幕

此主题不时出现在 *** 上,但通常因为写得不好而被删除。我看到了很多这样的问题,然后当要求提供更多信息时,OP(通常是低代表)保持沉默。有时,如果输入对我来说足够好,我决定回答一个答案,并且它通常在活跃时每天都会获得一些赞成票,但几周后问题被删除/删除,一切都从开始。所以我决定写这个Q&A,这样我就可以直接参考这些问题,而不必一遍又一遍地重写答案……

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

问题

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

一些限制:

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

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

这里类似maze to ASCII Art conversion问答。

【问题讨论】:

使用this wiki page 作为参考,您能澄清一下您指的是哪种类型的ASCII 艺术吗?在我看来,这听起来像是“图像到文本的转换”,这是从灰度像素到相应的文本字符的“简单”查找,所以我想知道你的意思是否不同。听起来你无论如何都要自己回答...... 相关:***.com/q/26347985/2564301 @RogerRowland 既简单(仅基于灰度强度),又考虑到字符形状更高级(但仍然足够简单) 虽然你的作品很棒,但我当然会欣赏一些更具SFW的样本。 @TimCastelijns 如果您阅读了序言,那么您会发现这不是第一次要求此类答案(大多数选民从一开始就熟悉之前的几个相关问题,所以其余的只是相应地投票),因为这是 Q&A 而不仅仅是 Q 我没有在 Q 部分浪费太多时间(我承认这是我的错) 对问题添加了一些限制,如果您有更好的可以随意编辑。 【参考方案1】:

图像到 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[] 可以被处理为线性分布,导致轻微的 gamma 失真,除非您知道要查找什么,否则通常在结果中看不到。

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

怎么做:

    将图像均匀划分为(灰度)像素或(矩形)区域s 计算每个像素/区域的强度 用最接近强度的字符映射中的字符替换它

作为字符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 是从 1 开始索引的 VCL 类型字符串,而不是像 char* 那样从 0 开始索引!!!

这是结果:Slightly NSFW intensity example image

左边是 ASCII 艺术输出(字体大小 5 像素),右边是输入图像放大几倍。如您所见,输出是较大的像素 -> 字符。如果您使用更大的区域而不是像素,则缩放会更小,但当然输出的视觉效果会不那么令人愉悦。 这种方法编码/处理非常简单快捷。

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

自动地图计算 自动像素/区域大小选择 纵横比修正

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

这是 1:1 比例的结果(放大以查看字符):

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

Slightly NSFW intensity advanced example image

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

字符拟合(阴影和纯 ASCII 艺术之间的混合)

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

    因此将图像均匀地划分为(灰度)矩形区域

    理想情况下与渲染字体字符具有相同的纵横比(它将保留纵横比。不要忘记字符通常在 x 轴上重叠一点)

    计算每个区域的强度 (dot)

    用最接近强度/形状的字符map中的字符替换它

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

    将字符区域划分为区域

    计算转换字母表中每个字符的左、右、上、下和中心区域的单独强度 (map)。 标准化所有强度,因此它们与区域大小无关,i=(i*256)/(xs*ys)

    在矩形区域处理源图像

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

这是字体大小 = 7 像素的结果

如您所见,即使使用更大的字体大小(前面的方法示例使用 5 像素字体大小),输出在视觉上也令人愉悦。输出与输入图像的大小大致相同(无缩放)。获得更好的结果是因为字符更接近原始图像,不仅在强度上,而且在整体形状上,因此您可以使用更大的字体并仍然保留细节(当然,最多)。

以下是基于 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) // Character  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;        // Temporary 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 initialise buffers
        tmp = new Graphics::TBitmap;
        if (tmp==NULL)
            break;

        // Allow 32 bit 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 the image by zoomed character size step
        xf -= xf/3; // Characters are usually overlapping 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 the 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-&gt;Font的设置,所以请确保你设置了:

Font-&gt;Pitch = fpFixed Font-&gt;Charset = OEM_CHARSET Font-&gt;Name = "System"

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

[备注]

见Word Portraits visualization 使用具有位图/文件访问和文本输出功能的语言 我强烈建议从第一种方法开始,因为它非常简单明了,然后才转到第二种方法(可以通过修改第一种方法来完成,因此大部分代码都保持原样) 最好使用反转强度(黑色像素是最大值)进行计算,因为标准文本预览是在白色背景上,因此会产生更好的结果。 您可以尝试细分区域的大小、数量和布局,或者改用 3x3 等网格。

比较

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

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

酷应用

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

好的,该应用程序仅包含两个窗口。第一个主窗口基本上是我的旧转换窗口,没有图像选择和预览(上面的所有东西都在里面)。它只有 ASCII 预览和转换设置。第二个窗口是一个内部透明的空窗体,用于抓取区域选择(没有任何功能)。

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

因此,您将要转换的区域包围在选择窗口中,并在主窗口中查看结果。它可以是游戏、查看器等。它看起来像这样:

所以现在我甚至可以观看 ASCIIart 中的视频来获得乐趣。有些真的很好:)。

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

Convert floating-point numbers to decimal digits in GLSL?

【讨论】:

您在这里做得非常出色!谢谢!我喜欢 ASCII 审查! 改进建议:计算方向导数,而不仅仅是强度。 @Yakk 需要详细说明吗? @tarik 不仅匹配强度,还匹配导数:或者,带通增强边缘。基本上强度不是人们看到的唯一东西:他们看到渐变和边缘。 @Yakk 区域细分间接地做了这样的事情。将字符处理为3x3 区域并比较 DCT 可能会更好,但我认为这会大大降低性能。

以上是关于图像到 ASCII 艺术转换的主要内容,如果未能解决你的问题,请参考以下文章

打印完整的 ascii 艺术

使用换行符和反斜杠渲染ASCII艺术字符串

ASCII 艺术文档的流行 JavaDoc 实践是啥? [关闭]

Python:从文本文件打印 ascii 艺术,反斜杠加倍

text ASCII艺术

sh ASCII艺术块