图像到 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>,则替换字符将为
如果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->Font的设置,所以请确保你设置了:
Font->Pitch = fpFixed
Font->Charset = OEM_CHARSET
Font->Name = "System"
要使其正常工作,否则字体将不会被处理为等宽。鼠标滚轮只是上下改变字体大小以查看不同字体大小的结果。
[备注]
- 见Word Portraits visualization
- 使用具有位图/文件访问和文本输出功能的语言
- 我强烈建议从第一种方法开始,因为它非常简单明了,然后才转到第二种方法(可以通过修改第一种方法来完成,因此大部分代码都保持原样)
- 最好使用反转强度(黑色像素是最大值)进行计算,因为标准文本预览是在白色背景上,因此会产生更好的结果。
- 您可以尝试细分区域的大小、数量和布局,或者改用
3x3 等网格。
比较
最后是两种方法在相同输入上的比较:
绿点标记的图像使用 #2 方法完成,而红色点标记的图像使用 #1 方法完成,均采用 6 像素字体大小。正如您在灯泡图像上看到的,形状敏感的方法要好得多(即使 #1 是在 2 倍缩放的源图像上完成的)。
酷应用
在阅读今天的新问题时,我想到了一个很酷的应用程序,它可以抓取桌面的选定区域并不断地将其提供给 ASCIIart 转换器并查看结果。经过一个小时的编码,它完成了,我对结果非常满意,我必须在这里添加它。
好的,该应用程序仅包含两个窗口。第一个主窗口基本上是我的旧转换窗口,没有图像选择和预览(上面的所有东西都在里面)。它只有 ASCII 预览和转换设置。第二个窗口是一个内部透明的空窗体,用于抓取区域选择(没有任何功能)。
现在在计时器上,我只是通过选择表单抓取所选区域,将其传递给转换,然后预览 ASCIIart。
因此,您将要转换的区域包围在选择窗口中,并在主窗口中查看结果。它可以是游戏、查看器等。它看起来像这样:
所以现在我甚至可以观看 ASCIIart 中的视频来获得乐趣。有些真的很好:)。
如果您想尝试在 GLSL 中实现此功能,请查看以下内容: