algorithm - 将简单的单色绘图图像转换为二维文本数组
问题描述
我正在尝试开发一种将简单的单线图像(即迷宫)转换为文本二维数组的算法。
例如下面的图片,它会被转换为下面的文本数组。
[|------------ |]
[| | |]
[| |]
[| |------| ---- |]
[| | | |]
[| | --- |]
[|--- | | |]
[| |--- | |]
[| | | |]
[| --------------- |]
[| |]
[| -------------------|]
最后,像这样,0=障碍物,1=自由通行
[0000000000000111111110]
[0111110111111111111110]
[0111111111111111111110]
[0110000000011111100000]
[0111111111011111011110]
[0111111111011111000110]
[0000111111011111111010]
[0111111111000011111010]
[0111110111111011111110]
[0111100000000000000010]
[0111111111111111111110]
[0110000000000000000000]
我正在考虑使用图像到线条艺术文本之类的算法,即 https://www.text-image.com/convert/pic2ascii.cgi
您如何看待这种方法?
解决方案
有趣的问题是图像到 ASCII 艺术转换的基本矢量形式......我设法用这个算法做到了这一点:
预处理图像
您给了我们 JPG,它具有有损压缩,这意味着您的图像包含的不仅仅是 2 种颜色。所以有些阴影和伪影会搞砸。所以首先我们必须通过阈值化和重新着色来消除这些。所以我们可以有 2D BW 图像(没有灰度)
矢量化
您的迷宫是轴对齐的,因此它只包含水平和垂直 (
h
,v
) 线。因此,只需扫描每行图像,找到第一个起始墙像素,然后找到其结束像素并存储在某处......重复直到整行被处理并为所有行执行此操作。再次对图像行执行相同的操作。由于您的图像具有厚壁,因此忽略比厚度阈值分类的线并删除(几乎)相同的相邻(重复)线。从 h,v 线获取可能的网格坐标列表
只需从线的起点和终点列出所有 x 和 y(分别)坐标。然后对它们进行排序并删除太近的坐标(重复)。
现在最小值和最大值为您提供迷宫的 AABB,所有的 GCD
coordinate-lowest coordinate
将为您提供网格大小。将 h,v 线与网格对齐
只需将所有起点/终点四舍五入到最近的网格位置...
为迷宫创建文本缓冲区
AABB 以及网格大小将为您提供单元格中的 maz 分辨率,因此只需创建每个单元格都有
NxN
字符的 2D 文本缓冲区。我正在使用6x3
看起来足够漂亮的单元格(正方形并且内部有足够的空间)。将 h,v 行转换为文本
只需循环遍历所有行并渲染
-
或代替像素...如果目标位置不包含'',|
我也在使用。+
将二维文本数组转换为想要的文本输出
只需将这些行复制到单个文本中......或者如果你足够聪明,你可以在同一内存位置拥有 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)位图中返回文本。
推荐阅读
- android - 如何在文档范围内使用 setInterval
- python - 如何将 CP-SAT 公式(在 python 中)中的目标指定为所有决策变量值的最大值的最小化?
- css - Blazor css 如何显示无法重新连接到服务器。重新加载页面以恢复功能标题
- json - 无法将多个 Jsonb 行合并为一行
- c - 理解以下程序的问题
- python - 使用连接通过连接表获取值
- google-calendar-api - 在不添加 Google Meet 视频会议的情况下在 Google 日历中创建活动
- javascript - JavaScript / TypeScript 上是否有“节点”类型?ts(2345)
- javascript - 我添加了图像但没有出现
- javascript - Javascript 中的生成器是生成迭代器还是可迭代对象?