图像到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[]
可以将字符处理为线性分布,从而导致轻微的伽玛失真,通常在结果中看不到,除非您知道要查找的内容。
基于强度的转换对于灰度图像(不仅仅是黑白图像)也非常有用。如果将点选为单个像素,结果将变大(一个像素->单个字符),因此,对于较大的图像,将选择一个区域(字体大小的倍数)以保留长宽比,并且不要太大。
怎么做:
- 均匀地将图像划分为(灰阶)像素或(矩形)区域点小号
- 计算每个像素/区域的强度
- 用强度最近的字符图中的字符替换它
作为字符,map
您可以使用任何字符,但是如果字符的像素沿字符区域均匀分布,则效果会更好。对于初学者,您可以使用:
char map[10]=" .,:;ox%#@";
降序排序并假装为线性分布。
因此,如果像素/区域的强度为,i = <0-255>
则替换字符为
如果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
)与渲染字符之间的差异(距离)。您可以从像素之间的绝对差的天真和开始,但这将导致效果不佳,因为即使移动一个像素也将使距离变大。相反,您可以使用相关性或其他指标。总体算法与以前的方法几乎相同:
因此,均匀的图像划分到(灰阶)的矩形区域点的
理想情况下,其长宽比应与呈现的字体字符相同(它将保留长宽比。请不要忘记字符通常在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->Font
,因此请确保设置:
Font->Pitch = fpFixed
Font->Charset = OEM_CHARSET
Font->Name = "System"
为了使其正常工作,否则字体将不被视为等宽字体。鼠标滚轮只是向上/向下更改字体大小,以查看不同字体大小的结果。
[笔记]
- 请参阅Word Portraits可视化
- 使用具有位图/文件访问和文本输出功能的语言
- 我强烈建议从第一种方法开始,因为它非常简单明了,然后才移至第二种方法(可以通过对第一种方法进行修改,因此大多数代码仍然保持原样)
- 最好使用反转强度(黑色像素为最大值)进行计算,因为标准文本预览位于白色背景上,因此效果更好。
- 您可以试验细分区域的大小,数量和布局,或者改用类似的网格
3x3
。
比较方式
最后,这是在相同输入上的两种方法之间的比较:
绿点标记的图像使用方法#2完成,红色标记的图像使用#1完成,所有图像尺寸均为六像素。如您在灯泡图像上看到的,形状敏感方法要好得多(即使#1是在2倍缩放的源图像上完成的)。
酷应用
在阅读当今的新问题时,我想到了一个很酷的应用程序,该应用程序可以捕获桌面的选定区域,并将其连续馈送到ASCIIart转换器并查看结果。经过一个小时的编码,它完成了,我对结果感到非常满意,我只需要在这里添加它即可。
OK,该应用程序仅包含两个窗口。第一个主窗口基本上是我的旧转换器窗口,没有图像选择和预览(上面的所有内容都在其中)。它只有ASCII预览和转换设置。第二个窗口是一个空的窗体,内部透明,可用于选择抓取区域(无任何功能)。
现在在计时器上,我只是通过选择表单来抓取所选区域,将其传递给转换,并预览ASCIIart。
因此,您可以在选择窗口中封闭要转换的区域,并在主窗口中查看结果。它可以是游戏,查看器等。它看起来像这样:
因此,现在我什至可以观看ASCIIart中的视频来娱乐。有些真的很好:)。
如果您想尝试在GLSL中实现此功能,请查看以下内容: