图像到 ASCII 艺术转换
Posted
技术标签:
【中文标题】图像到 ASCII 艺术转换【英文标题】:Image to ASCII art conversion 【发布时间】:2016-01-04 09:10:33 【问题描述】:序幕
此主题不时出现在 *** 上,但通常因为写得不好而被删除。我看到了很多这样的问题,然后当要求提供更多信息时,OP(通常是低代表)保持沉默。有时,如果输入对我来说足够好,我决定回答一个答案,并且它通常在活跃时每天都会获得一些赞成票,但几周后问题被删除/删除,一切都从开始。所以我决定写这个Q&A,这样我就可以直接参考这些问题,而不必一遍又一遍地重写答案……
另一个原因也是这个meta thread针对我,所以如果您有其他意见,请随时发表评论。
问题
如何使用 C++ 将位图图像转换为 ASCII 艺术?
一些限制:
灰度图像 使用等距字体 保持简单(不要为初级程序员使用太高级的东西)这是一个相关的***页面ASCII art(感谢@RogerRowland)。
这里类似maze to ASCII Art conversion问答。
【问题讨论】:
使用this wiki page 作为参考,您能澄清一下您指的是哪种类型的ASCII 艺术吗?在我看来,这听起来像是“图像到文本的转换”,这是从灰度像素到相应的文本字符的“简单”查找,所以我想知道你的意思是否不同。听起来你无论如何都要自己回答...... 相关:***.com/q/26347985/2564301 @RogerRowland 既简单(仅基于灰度强度),又考虑到字符形状更高级(但仍然足够简单) 虽然你的作品很棒,但我当然会欣赏一些更具SFW的样本。 @TimCastelijns 如果您阅读了序言,那么您会发现这不是第一次要求此类答案(大多数选民从一开始就熟悉之前的几个相关问题,所以其余的只是相应地投票),因为这是 Q&A 而不仅仅是 Q 我没有在 Q 部分浪费太多时间(我承认这是我的错) 对问题添加了一些限制,如果您有更好的可以随意编辑。 【参考方案1】:图像到 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>
,则替换字符将为
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 开始索引的 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 中实现此功能,请查看以下内容:
Convert floating-point numbers to decimal digits in GLSL?【讨论】:
您在这里做得非常出色!谢谢!我喜欢 ASCII 审查! 改进建议:计算方向导数,而不仅仅是强度。 @Yakk 需要详细说明吗? @tarik 不仅匹配强度,还匹配导数:或者,带通增强边缘。基本上强度不是人们看到的唯一东西:他们看到渐变和边缘。 @Yakk 区域细分间接地做了这样的事情。将字符处理为3x3
区域并比较 DCT 可能会更好,但我认为这会大大降低性能。以上是关于图像到 ASCII 艺术转换的主要内容,如果未能解决你的问题,请参考以下文章