图像到ASCII艺术转换


102

序幕

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

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

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

一些约束:

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

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

这里类似于ASCII Art转换问答的迷宫


使用此Wiki页面作为参考,您能否弄清您所指的是哪种ASCII艺术?对我来说,这听起来像是“图像到文本的转换”,它是从灰度像素到相应的文本字符的“简单”查找,因此我想知道您的意思是否有所不同。听起来您还是要自己回答.....
罗杰·罗兰


@RogerRowland既简单(仅基于灰度强度),也考虑字符的形状(但仍然足够简单)进行更高级的处理
Spektre 2015年

1
尽管您的工作很棒,但我一定会选择一些SFW多一些的样本。
kmote 2015年

@TimCastelijns如果您读了序言,那么您会发现这不是第一次请求这种类型的答案(而且大多数选民从一开始就很熟悉以前的几个相关问题,所以其余的都相应地投票了),因为这是问答环节,而不仅仅是问:我并没有在Q部分上浪费太多时间(我承认这是我的错,这对您是否有更好的问题可以随时进行编辑,这个问题没有多大限制。
Spektre

Answers:


152

图像到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[]可以将字符处理为线性分布,从而导致轻微的伽玛失真,通常在结果中看不到,除非您知道要查找的内容。

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

怎么做:

  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是从1索引而不是从0索引的VCL类型字符串char*

结果是:略有NSFW强度示例图像

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

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

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

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

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

强度高级示例

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

NSFW强度稍高的示例图像

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

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

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

  1. 因此,均匀的图像划分到(灰阶)的矩形区域

    理想情况下,其长宽比应与呈现的字体字符相同(它将保留长宽比。请不要忘记字符通常在x轴上重叠一点)

  2. 计算每个区域的强度(dot

  3. map强度/形状最接近的字符替换它

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

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

    区域

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

    • (具有与目标字体相同的长宽比)
    • 对于每个区域,以与项目符号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->Font,因此请确保设置:

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

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

[笔记]

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

比较方式

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

比较方式

绿点标记的图像使用方法#2完成,红色标记的图像使用#1完成,所有图像尺寸均为六像素。如您在灯泡图像上看到的,形状敏感方法要好得多(即使#1是在2倍缩放的源图像上完成的)。

酷应用

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

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

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

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

ASCIIart采集器示例

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

手

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


30
您在这里做了出色的工作!谢谢!而且我喜欢ASCII审查!
安德·比古里

1
有改进的建议:计算方向性导数,而不只是强度。
Yakk-Adam Nevraumont 2015年

1
@Y牛在乎详细吗?
tariksbl

2
@tarik不仅在强度上匹配,而且在导数上匹配:或者,带通增强了边缘。基本上,强度不是人们唯一看到的东西:他们看到梯度和边缘。
Yakk-Adam Nevraumont

1
@Y牛分区细分间接地做这种事情。将字符作为3x3区域来处理并比较DCT可能会更好,但是我认为这会大大降低性能。
Spektre
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.