イメージからASCIIアートへの変換には、主にモノスペースフォントの使用に基づくより多くのアプローチがあります。簡単にするために、基本のみに固執します。
ピクセル/エリア強度ベース(シェーディング)
このアプローチは、ピクセル領域の各ピクセルを単一のドットとして扱います。アイデアは、このドットの平均グレースケール強度を計算し、計算された強度に十分近い強度を持つ文字に置き換えます。そのためには、それぞれが事前に計算された強度を持つ、使用可能な文字のリストが必要です。それをキャラクターと呼びましょうmap
。どの文字がどの強度に最も適しているかをより迅速に選択するには、2つの方法があります。
線形分布強度文字マップ
したがって、同じステップで強度の違いがある文字のみを使用します。つまり、昇順で並べ替えると、次のようになります。
intensity_of(map[i])=intensity_of(map[i-1])+constant;
また、文字map
を並べ替えると、強度から直接文字を計算できます(検索は必要ありません)。
character = map[intensity_of(dot)/constant];
任意の分布強度文字マップ
したがって、使用可能な文字とその強度の配列があります。intensity_of(dot)
Soに最も近い強度を見つける必要があります。をソートした場合はmap[]
、バイナリ検索を使用できます。そうでない場合は、O(n)
検索最小距離ループまたはO(1)
辞書が必要です。単純化のために、キャラクターmap[]
を線形分布として処理することで、わずかなガンマ歪みが発生することがあります。これは、何を探すべきか分からない限り、通常は結果には現れません。
強度ベースの変換は、グレースケール画像(白黒だけでなく)にも最適です。ドットを単一ピクセルとして選択した場合、結果は大きくなります(1ピクセル->単一文字)。したがって、大きな画像の場合は、代わりに領域(フォントサイズの倍数)が選択され、アスペクト比が保持され、大きくなりすぎません。
どうやってするの:
- 均等(グレースケール)ピクセルまたは(長方形)の領域に画像を分割ドット秒
- 各ピクセル/エリアの強度を計算します
- 文字マップの文字に最も近い輝度で置き換えます
文字としては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
0からではなく、1からインデックスが付けられたVCLタイプの文字列char*
です!!!
これは結果です:少しNSFW強度の例の画像
左側はASCIIアート出力(フォントサイズ5ピクセル)で、右側の入力画像は数回ズームされています。ご覧のとおり、出力はより大きなピクセル->文字です。ピクセルの代わりに大きな領域を使用する場合、ズームは小さくなりますが、当然、出力は視覚的にあまり快適ではありません。このアプローチは、コード化/処理が非常に簡単で高速です。
次のようなより高度なものを追加すると:
- 自動マップ計算
- 自動ピクセル/エリアサイズ選択
- アスペクト比の修正
次に、より複雑な画像を処理して、より良い結果を得ることができます。
以下は、1:1の比率の結果です(ズームして文字を表示)。
もちろん、エリアサンプリングの場合、細部が失われます。これは、エリアでサンプリングされた最初の例と同じサイズの画像です。
少しNSFW強度の高度な例の画像
ご覧のように、これは大きな画像に適しています。
文字フィッティング(シェーディングとソリッドASCIIアートのハイブリッド)
このアプローチは、エリア(単一ピクセルドットではない)を、同様の強度と形状を持つ文字で置き換えようとします。これにより、以前のアプローチと比較して大きなフォントを使用しても、より良い結果が得られます。一方、このアプローチはもちろん少し遅いです。これを行う方法は他にもありますが、主な考え方は、画像領域(dot
)とレンダリングされた文字の差(距離)を計算することです。ピクセル間の絶対差の単純な合計から始めることができますが、1ピクセルのシフトでも距離が大きくなるため、あまり良い結果にはなりません。代わりに、相関または異なるメトリックを使用できます。全体的なアルゴリズムは、前のアプローチとほとんど同じです。
そう均等(グレースケール)の矩形領域に画像を分割ドットのを
レンダリングされたフォント文字と同じアスペクト比が理想的です(アスペクト比が保持されます。通常、文字が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
)が1つ含ま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の視覚化を見る
- ビットマップ/ファイルアクセスおよびテキスト出力機能を備えた言語を使用する
- 非常に簡単で単純なので、最初のアプローチから始めて、次に2番目のアプローチに進むことを強くお勧めします(最初のアプローチを変更として実行できるため、ほとんどのコードはそのままです)。
- 標準のテキストプレビューは白い背景で行われるため、強度を逆にして計算することをお勧めします(黒いピクセルが最大値です)。これにより、より良い結果が得られます。
- サブディビジョンゾーンのサイズ、数、レイアウトを試すか、
3x3
代わりにグリッドを使用できます。
比較
最後に、同じ入力に対する2つのアプローチの比較を次に示します。
緑のドットでマークされた画像は、アプローチ#2で、赤の画像は#1で、すべて6ピクセルのフォントサイズで作成されています。電球の画像でわかるように、形状に敏感なアプローチははるかに優れています(2倍に拡大されたソース画像で#1が行われた場合でも)。
クールなアプリケーション
今日の新しい質問を読んでいると、デスクトップの選択した領域を取得し、それを継続的にASCIIartコンバーターに供給して結果を表示する、すばらしいアプリケーションのアイデアを得ました。1時間のコーディングの後、コードは完成し、結果に満足しているので、ここに追加する必要があります。
アプリケーションは2つのウィンドウで構成されています。最初のマスターウィンドウは、基本的には画像の選択とプレビューのない古いコンバーターウィンドウです(上記のものはすべてそこにあります)。ASCIIプレビューと変換設定のみが含まれています。2番目のウィンドウは、領域を選択するために内部が透明な空のフォームです(機能はまったくありません)。
タイマーを使用して、選択した領域を選択フォームでつかみ、変換に渡して、ASCIIartをプレビューします。
そのため、変換したい領域を選択ウィンドウで囲み、結果をマスターウィンドウに表示します。ゲームやビューアなどが考えられます。次のようになります。
だから、今でも楽しみのためにASCIIartでビデオを見ることができる。いくつかは本当にいいです:)。
これを実装したい場合 GLSLご覧。