首页 > 解决方案 > 将简单的单色绘图图像转换为二维文本数组

问题描述

我正在尝试开发一种将简单的单线图像(即迷宫)转换为文本二维数组的算法。

例如下面的图片,它会被转换为下面的文本数组。

在此处输入图像描述

[|------------         |]
[|     |               |]
[|                     |]
[|  |------|      ---- |]
[|         |      |    |]
[|         |     ---   |] 
[|---      |        |  |]
[|         |---     |  |] 
[|     |      |        |]
[|    ---------------  |]
[|                     |]
[|  -------------------|]
        

最后,像这样,0=障碍物,1=自由通行

[0000000000000111111110]
[0111110111111111111110]
[0111111111111111111110]
[0110000000011111100000]
[0111111111011111011110]
[0111111111011111000110] 
[0000111111011111111010]
[0111111111000011111010] 
[0111110111111011111110]
[0111100000000000000010]
[0111111111111111111110]
[0110000000000000000000]

我正在考虑使用图像到线条艺术文本之类的算法,即 https://www.text-image.com/convert/pic2ascii.cgi

您如何看待这种方法?

标签: algorithmtextdata-structuresmaze

解决方案


有趣的问题是图像到 ASCII 艺术转换的基本矢量形式......我设法用这个算法做到了这一点:

  1. 预处理图像

    您给了我们 JPG,它具有有损压缩,这意味着您的图像包含的不仅仅是 2 种颜色。所以有些阴影和伪影会搞砸。所以首先我们必须通过阈值化和重新着色来消除这些。所以我们可以有 2D BW 图像(没有灰度)

  2. 矢量化

    您的迷宫是轴对齐的,因此它只包含水平和垂直 ( h, v) 线。因此,只需扫描每行图像,找到第一个起始墙像素,然后找到其结束像素并存储在某处......重复直到整行被处理并为所有行执行此操作。再次对图像行执行相同的操作。由于您的图像具有厚壁,因此忽略比厚度阈值分类的线并删除(几乎)相同的相邻(重复)线。

  3. 从 h,v 线获取可能的网格坐标列表

    只需从线的起点和终点列出所有 x 和 y(分别)坐标。然后对它们进行排序并删除太近的坐标(重复)。

    现在最小值和最大值为您提供迷宫的 AABB,所有的 GCDcoordinate-lowest coordinate将为您提供网格大小。

  4. 将 h,v 线与网格对齐

    只需将所有起点/终点四舍五入到最近的网格位置...

  5. 为迷宫创建文本缓冲区

    AABB 以及网格大小将为您提供单元格中的 maz 分辨率,因此只需创建每个单元格都有NxN字符的 2D 文本缓冲区。我正在使用6x3看起来足够漂亮的单元格(正方形并且内部有足够的空间)。

  6. 将 h,v 行转换为文本

    只需循环遍历所有行并渲染-或代替像素...如果目标位置不包含'',|我也在使用。+

  7. 将二维文本数组转换为想要的文本输出

    只需将这些行复制到单个文本中......或者如果你足够聪明,你可以在同一内存位置拥有 1D 和 2D,并在行eol之间进行编码。

这是我从上面链接中的示例制作的 C++/VCL 中的简单示例:

//---------------------------------------------------------------------------
#include <vcl.h>
#include <jpeg.hpp>
#pragma hdrstop

#include "win_main.h"
#include "List.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
Graphics::TBitmap *bmp=new Graphics::TBitmap;
int txt_xs=0,txt_ys=0,txt_xf=0;
//---------------------------------------------------------------------------
template <class T> void sort_asc_bubble(T *a,int n)
    {
    int i,e; T a0,a1;
    for (e=1;e;n--)                                     // loop until no swap occurs
     for (e=0,a0=a[0],a1=a[1],i=1;i<n;a0=a1,i++,a1=a[i])// proces unsorted part of array
      if (a0>a1)                                        // condition if swap needed
      { a[i-1]=a1; a[i]=a0; a1=a0; e=1; }               // swap and allow to process array again
    }
//---------------------------------------------------------------------------
AnsiString bmp2lintxt(Graphics::TBitmap *bmp)
    {
    bool debug=false;
    const int cx=6;             // cell size
    const int cy=3;
    const int thr_bw=400;       // BW threshold
    const int thr_thickness=10; // wall thikness threshold
    char a;
    AnsiString txt="",eol="\r\n";
    int x,y,x0,y0,x1,y1,xs,ys,gx,gy,nx,ny,i,i0,i1,j;
    union { BYTE db[4]; DWORD dd; } c; DWORD **pyx;
    List<int> h,v;  // horizontal and vertical lines (x,y,size)
    List<int> tx,ty;// temp lists for grid GCD computation
    // [init stuff]
    bmp->HandleType=bmDIB;
    bmp->PixelFormat=pf32bit;
    xs=bmp->Width ;
    ys=bmp->Height;
    if (xs<=0) return txt;
    if (ys<=0) return txt;
    pyx=new DWORD*[ys];
    for (y=0;y<ys;y++) pyx[y]=(DWORD*)bmp->ScanLine[y];
    i=xs; if (i<ys) i=ys;
    // threshold bmp to B&W
    x0=xs; x1=0; y0=xs; y1=0;
    for (y=0;y<ys;y++)
     for (x=0;x<xs;x++)
        {
        c.dd=pyx[y][x];
        i =c.db[0];
        i+=c.db[1];
        i+=c.db[2];
        if (i>=thr_bw) c.dd=0x00FFFFFF;
        else           c.dd=0x00000000;
        pyx[y][x]=c.dd;
        }
    if (debug) bmp->SaveToFile("out0_bw.bmp");
    // [vectorize]
    // get horizontal lines
    i0=0; i1=0; h.num=0;
    for (y0=0;y0<ys;y0++)
        {
        for (x0=0;x0<xs;)
            {
            for (     ;x0<xs;x0++) if (!pyx[y0][x0]) break;
            for (x1=x0;x1<xs;x1++) if ( pyx[y0][x1]){ x1--; break; }
            i=x1-x0;
            if (i>thr_thickness)
                {
                h.add(x0);
                h.add(y0);
                h.add(i);
                }
            x0=x1+1;
            }
        // remove duplicate lines
        for (i=i0;i<i1;i+=3)
         for (j=i1;j<h.num;j+=3)
          if ((abs(h[i+0]-h[j+0])<thr_thickness)&&(abs(h[i+2]-h[j+2])<thr_thickness))
            {
            h.del(i);
            h.del(i);
            h.del(i);
            i1-=3; i-=3; break;
            }
        i0=i1; i1=h.num;
        }
    // get vertical lines
    i0=0; i1=0; v.num=0;
    for (x0=0;x0<xs;x0++)
        {
        for (y0=0;y0<ys;)
            {
            for (     ;y0<ys;y0++) if (!pyx[y0][x0]) break;
            for (y1=y0;y1<ys;y1++) if ( pyx[y1][x0]){ y1--; break; }
            i=y1-y0;
            if (i>thr_thickness)
                {
                v.add(x0);
                v.add(y0);
                v.add(i);
                }
            y0=y1+1;
            }
        // remove duplicate lines
        for (i=i0;i<i1;i+=3)
         for (j=i1;j<v.num;j+=3)
          if ((abs(v[i+1]-v[j+1])<thr_thickness)&&(abs(v[i+2]-v[j+2])<thr_thickness))
            {
            v.del(i);
            v.del(i);
            v.del(i);
            i1-=3; i-=3; break;
            }
        i0=i1; i1=v.num;
        }
    // [compute grid]
    x0=xs; y0=ys; x1=0; y1=0;   // AABB
    gx=10; gy=10;               // grid cell size
    nx=0; ny=0;                 // grid cells
    tx.num=0; ty.num=0;         // clear possible x,y coordinates
    for (i=0;i<h.num;i+=3)
        {
        x =h[i+0];
        y =h[i+1];
        if (x0>x) x0=x; if (x1<x) x1=x; for (j=0;j<tx.num;j++) if (tx[j]==x){ j=-1; break; } if (j>=0) tx.add(x);
        if (y0>y) y0=y; if (y1<y) y1=y; for (j=0;j<ty.num;j++) if (ty[j]==y){ j=-1; break; } if (j>=0) ty.add(y);
        x+=h[i+2];
        if (x0>x) x0=x; if (x1<x) x1=x; for (j=0;j<tx.num;j++) if (tx[j]==x){ j=-1; break; } if (j>=0) tx.add(x);
        }
    for (i=0;i<v.num;i+=3)
        {
        x =v[i+0];
        y =v[i+1];
        if (x0>x) x0=x; if (x1<x) x1=x; for (j=0;j<tx.num;j++) if (tx[j]==x){ j=-1; break; } if (j>=0) tx.add(x);
        if (y0>y) y0=y; if (y1<y) y1=y; for (j=0;j<ty.num;j++) if (ty[j]==y){ j=-1; break; } if (j>=0) ty.add(y);
        y+=v[i+2];
        if (y0>y) y0=y; if (y1<y) y1=y; for (j=0;j<ty.num;j++) if (ty[j]==y){ j=-1; break; } if (j>=0) ty.add(y);
        }
    // order tx,ty
    sort_asc_bubble(tx.dat,tx.num);
    sort_asc_bubble(ty.dat,ty.num);
    // remove too close coordinates
    for (i=1;i<tx.num;i++) if (tx[i]-tx[i-1]<=thr_thickness){ tx.del(i); i--; }
    for (i=1;i<ty.num;i++) if (ty[i]-ty[i-1]<=thr_thickness){ ty.del(i); i--; }
    // estimate gx,gy
    for (gx=x1-x0,i=1;i<tx.num;i++){ x=tx[i]-tx[i-1]; if (gx>x) gx=x; } nx=(x1-x0+1)/gx; gx=(x1-x0+1)/nx; x1=x0+nx*gx;
    for (gy=y1-y0,i=1;i<ty.num;i++){ y=ty[i]-ty[i-1]; if (gy>y) gy=y; } ny=(y1-y0+1)/gy; gy=(y1-y0+1)/ny; y1=y0+ny*gy;
    // align x,y to grid: multiplicate nx,ny by cx,cy to form boxes and enlarge by 1 for final border lines
    nx=(cx*nx)+1;
    ny=(cy*ny)+1;
    // align h,v lines to grid
    for (i=0;i<h.num;i+=3)
        {
        x=h[i+0]-x0; x=((x+(gx>>1))/gx)*gx; h[i+0]=x+x0;
        y=h[i+1]-y0; y=((y+(gy>>1))/gy)*gy; h[i+1]=y+y0;
        j=h[i+2];    j=((j+(gx>>1))/gx)*gx; h[i+2]=j;
        }
    for (i=0;i<v.num;i+=3)
        {
        x=v[i+0]-x0; x=((x+(gx>>1))/gx)*gx; v[i+0]=x+x0;
        y=v[i+1]-y0; y=((y+(gy>>1))/gy)*gy; v[i+1]=y+y0;
        j=v[i+2];    j=((j+(gy>>1))/gy)*gy; v[i+2]=j;
        }
    // [h,v lines -> ASCII Art]
    char *text=new char[nx*ny];
    char **tyx=new char*[ny];
    for (y=0;y<ny;y++)
     for (tyx[y]=text+(nx*y),x=0;x<nx;x++)
      tyx[y][x]=' ';
    // h lines
    for (i=0;i<h.num;i+=3)
        {
        x=(h[i+0]-x0)/gx;
        y=(h[i+1]-y0)/gy;
        j=(h[i+2]   )/gx; j+=x;
        x*=cx; y*=cy; j*=cx;
        for (;x<=j;x++) tyx[y][x]='-';
        }
    // v lines
    for (i=0;i<v.num;i+=3)
        {
        x=(v[i+0]-x0)/gx;
        y=(v[i+1]-y0)/gy;
        j=(v[i+2]   )/gy; j+=y;
        x*=cx; y*=cy; j*=cy;
        for (;y<=j;y++)
         if (tyx[y][x]=='-') tyx[y][x]='+';
          else               tyx[y][x]='|';
        }
    // convert char[ny][nx] to AnsiString
    for (txt="",y=0;y<ny;y++,txt+=eol)
     for (x=0;x<nx;x++) txt+=tyx[y][x];
    txt_xs=nx;  // just remember the text size for window resize
    txt_ys=ny;
    delete[] text;
    delete[] tyx;
    // [debug draw]
    // grid
    bmp->Canvas->Pen->Color=TColor(0x000000FF);
    for (i=1,x=x0;i;x+=gx)
        {
        if (x>=x1){ x=x1; i=0; }
        bmp->Canvas->MoveTo(x,y0);
        bmp->Canvas->LineTo(x,y1);
        }
    for (i=1,y=y0;i;y+=gy)
        {
        if (y>=y1){ y=y1; i=0; }
        bmp->Canvas->MoveTo(x0,y);
        bmp->Canvas->LineTo(x1,y);
        }
    if (debug) bmp->SaveToFile("out1_grid.bmp");
    // h,v lines
    bmp->Canvas->Pen->Color=TColor(0x00FF0000);
    bmp->Canvas->Pen->Width=2;
    for (i=0;i<h.num;)
        {
        x=h[i]; i++;
        y=h[i]; i++;
        j=h[i]; i++;
        bmp->Canvas->MoveTo(x,y);
        bmp->Canvas->LineTo(x+j,y);
        }
    for (i=0;i<v.num;)
        {
        x=v[i]; i++;
        y=v[i]; i++;
        j=v[i]; i++;
        bmp->Canvas->MoveTo(x,y);
        bmp->Canvas->LineTo(x,y+j);
        }
    bmp->Canvas->Pen->Width=1;
    if (debug) bmp->SaveToFile("out2_maze.bmp");

    delete[] pyx;
    return txt;
    }
//---------------------------------------------------------------------------
void update()
    {
    int x0,x1,y0,y1,i,l;
    x0=bmp->Width;
    y0=bmp->Height;
    // Font size
    Form1->mm_txt->Font->Size=Form1->cb_font->ItemIndex+4;
    txt_xf=abs(Form1->mm_txt->Font->Size);
    // mode
    Form1->mm_txt->Text=bmp2lintxt(bmp);
    // output
    Form1->mm_txt->Lines->SaveToFile("pic.txt");
    x1=txt_xs*txt_xf;
    y1=txt_ys*abs(Form1->mm_txt->Font->Height);
    if (y0<y1) y0=y1;
    x0+=x1+16+Form1->flb_pic->Width;
    y0+=Form1->pan_top->Height;
    if (x0<340) x0=340;
    if (y0<128) y0=128;
    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)
    {
    if (name=="") return;
    AnsiString ext=ExtractFileExt(name).LowerCase();
    if (ext==".bmp")
        {
        bmp->LoadFromFile(name);
        }
    if (ext==".jpg")
        {
        TJPEGImage *jpg=new TJPEGImage;
        jpg->LoadFromFile(name);
        bmp->Assign(jpg);
        delete jpg;
        }
    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)
    {
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
    {
    delete bmp;
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormPaint(TObject *Sender)
    {
    draw();
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::flb_picChange(TObject *Sender)
    {
    load(flb_pic->FileName);
    update();
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormActivate(TObject *Sender)
    {
    flb_pic->SetFocus();
    flb_pic->Update();
    if (flb_pic->ItemIndex==-1)
     if (flb_pic->Items->Count>0)
        {
        flb_pic->ItemIndex=0;
        flb_picChange(this);
        }
    }
//---------------------------------------------------------------------------

只需忽略 VCL 内容并将生成的文本转换为您可以使用的任何内容。我也使用我的动态列表模板,所以:


List<double> xxx;double xxx[];
xxx.add(5);添加5到列表末尾 相同
xxx[7]访问数组元素(安全)
xxx.dat[7]访问数组元素(不安全但快速直接访问)
xxx.num是数组的实际使用大小
xxx.reset()清除数组并为项目设置xxx.num=0
xxx.allocate(100)预分配空间100

因此,请使用您获得的任何列表或重新编码或使用std::vector...

我从您的图像中编辑了文本:

输入

这是使用它作为输入的结果:

+-----------+------------------     |
|           |                       |
|           |                       |
|     |     |     |     +-----------+
|     |           |     |           |
|     |           |     |           |
|     +-----------+     +-----+     |
|                 |           |     |
|                 |           |     |
+------     |     +-----+     |     |
|           |           |           |
|           |           |           |
|     ------+-----------+------     |
|                                   |
|                                   |
|     ------------------------------+

这里保存的调试位图(从左到右:BW,Grid,Maze):

体重 网格 迷宫

代码中唯一重要的东西是函数:

AnsiString bmp2lintxt(Graphics::TBitmap *bmp);

它从 VCL(基于 GDI)位图中返回文本。


推荐阅读