画像からASCIIアートへの変換


102

プロローグ

この主題は時々ここでStack Overflowに表示されますが、通常は書き方が悪い質問であるため削除されます。私はそのような多くの質問を見て、追加の情報が要求されたときにOP(通常は低担当者)から沈黙しました。時々、入力が私にとって十分である場合、私は回答で応答することを決定し、通常はアクティブなときに1日あたり数回の賛成票を獲得しますが、数週間後に質問が削除/削除され、すべてが始まり。そこで、このQ&Aを書いて、答えを何度も書き換えることなく直接質問を参照できるようにすることにしました…

もう1つの理由は、このメタスレッドが私をターゲットにしているためです。追加の入力を受け取った場合は、コメントしてください。

質問

C ++を使用してビットマップイメージをASCIIアートに変換するにはどうすればよいですか?

いくつかの制約:

  • グレースケール画像
  • 等幅フォントの使用
  • シンプルに保つ(初心者レベルのプログラマーには高度なものを使用しない)

これは関連するWikipediaページのASCIIアートです(@RogerRowlandに感謝します)。

ここでは、迷路とASCIIアートの変換に関する Q&Aが類似しています


このWikiページを参照として使用して、どのタイプのASCIIアートを参照しているのかを明確にできますか?グレースケールピクセルから対応するテキスト文字への「単純な」ルックアップである「画像からテキストへの変換」のように聞こえるので、何か別のことを意味しているのだろうかと思います。とにかく、自分で答えようとしているように聞こえますが.....
Roger Rowland


@RogerRowlandはシンプル(グレースケール強度のみに基づく)と、文字の形状も考慮に入れてより高度な(両方とも十分単純)の
両方

1
あなたの仕事は素晴らしいですが、もう少しSFWのサンプルを選んでいただければ幸いです。
kmote 2015年

@TimCastelijnsプロローグを読むと、そのようなタイプの回答が要求されたのはこれが初めてではないことがわかります(そして、ほとんどの有権者は、関連する以前のいくつかの質問に精通しており、残りはそれに応じて投票しただけです)、これは単なるQ&AではないためQ Qの部分で時間を無駄にしませんでした(これは私の認める側の誤りです)、より良いものを自由に編集できる場合、質問にいくつかの制限が追加されました。
Spektre、2015年

回答:


152

イメージからASCIIアートへの変換には、主にモノスペースフォントの使用に基づくより多くのアプローチがあります。簡単にするために、基本のみに固執します。

ピクセル/エリア強度ベース(シェーディング)

このアプローチは、ピクセル領域の各ピクセルを単一のドットとして扱います。アイデアは、このドットの平均グレースケール強度を計算し、計算された強度に十分近い強度を持つ文字に置き換えます。そのためには、それぞれが事前に計算された強度を持つ、使用可能な文字のリストが必要です。それをキャラクターと呼びましょうmap。どの文字がどの強度に最も適しているかをより迅速に選択するには、2つの方法があります。

  1. 線形分布強度文字マップ

    したがって、同じステップで強度の違いがある文字のみを使用します。つまり、昇順で並べ替えると、次のようになります。

     intensity_of(map[i])=intensity_of(map[i-1])+constant;

    また、文字mapを並べ替えると、強度から直接文字を計算できます(検索は必要ありません)。

     character = map[intensity_of(dot)/constant];
  2. 任意の分布強度文字マップ

    したがって、使用可能な文字とその強度の配列があります。intensity_of(dot)Soに最も近い強度を見つける必要があります。をソートした場合はmap[]、バイナリ検索を使用できます。そうでない場合は、O(n)検索最小距離ループまたはO(1)辞書が必要です。単純化のために、キャラクターmap[]を線形分布として処理することで、わずかなガンマ歪みが発生することがあります。これは、何を探すべきか分からない限り、通常は結果には現れません。

強度ベースの変換は、グレースケール画像(白黒だけでなく)にも最適です。ドットを単一ピクセルとして選択した場合、結果は大きくなります(1ピクセル->単一文字)。したがって、大きな画像の場合は、代わりに領域(フォントサイズの倍数)が選択され、アスペクト比が保持され、大きくなりすぎません。

どうやってするの:

  1. 均等(グレースケール)ピクセルまたは(長方形)の領域に画像を分割ドット
  2. 各ピクセル/エリアの強度を計算します
  3. 文字マップの文字に最も近い輝度で置き換えます

文字としては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 入力ビットマップです
  • AnsiString0からではなく、1からインデックスが付けられたVCLタイプの文字列char*です!!!

これは結果です:少しNSFW強度の例の画像

左側はASCIIアート出力(フォントサイズ5ピクセル)で、右側の入力画像は数回ズームされています。ご覧のとおり、出力はより大きなピクセル->文字です。ピクセルの代わりに大きな領域を使用する場合、ズームは小さくなりますが、当然、出力は視覚的にあまり快適ではありません。このアプローチは、コード化/処理が非常に簡単で高速です。

次のようなより高度なものを追加すると:

  • 自動マップ計算
  • 自動ピクセル/エリアサイズ選択
  • アスペクト比の修正

次に、より複雑な画像を処理して、より良い結果を得ることができます。

以下は、1:1の比率の結果です(ズームして文字を表示)。

強度の高度な例

もちろん、エリアサンプリングの場合、細部が失われます。これは、エリアでサンプリングされた最初の例と同じサイズの画像です。

少しNSFW強度の高度な例の画像

ご覧のように、これは大きな画像に適しています。

文字フィッティング(シェーディングとソリッドASCIIアートのハイブリッド)

このアプローチは、エリア(単一ピクセルドットではない)を、同様の強度と形状を持つ文字で置き換えようとします。これにより、以前のアプローチと比較して大きなフォントを使用しても、より良い結果が得られます。一方、このアプローチはもちろん少し遅いです。これを行う方法は他にもありますが、主な考え方は、画像領域(dot)とレンダリングされた文字の差(距離)を計算することです。ピクセル間の絶対差の単純な合計から始めることができますが、1ピクセルのシフトでも距離が大きくなるため、あまり良い結果にはなりません。代わりに、相関または異なるメトリックを使用できます。全体的なアルゴリズムは、前のアプローチとほとんど同じです。

  1. そう均等(グレースケール)の矩形領域に画像を分割ドットのを

    レンダリングされたフォント文字と同じアスペクト比が理想的です(アスペクト比が保持されます。通常、文字がx軸で少し重なっていることを忘れないでください)

  2. 各領域の強度を計算します(dot

  3. map最も近い強度/形状を持つ文字からの文字に置き換えます

文字とドットの間の距離をどのように計算できますか?これがこのアプローチの最も難しい部分です。実験しながら、スピード、品質、シンプルさの間の妥協点を開発しました。

  1. 文字領域をゾーンに分割する

    ゾーン

    • 変換アルファベット(map)から、各文字の左、右、上、下、および中央ゾーンの個別の強度を計算します。
    • すべての強度を正規化して、領域サイズに依存しないようにしますi=(i*256)/(xs*ys)
  2. 長方形の領域でソース画像を処理する

    • (ターゲットフォントと同じアスペクト比)
    • 各エリアについて、箇条書き#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グラバーの例

だから、今でも楽しみのためにASCIIartでビデオを見ることができる。いくつかは本当にいいです:)。

手

これを実装したい場合 GLSLご覧


30
あなたはここで素晴らしい仕事をしました!ありがとう!そして、私はASCII検閲が大好きです!
Ander Biguri、2015年

1
改善のための提案:強度だけでなく方向性導関数を計算します。
Yakk-Adam Nevraumont

1
@Yakkは詳しく説明しますか?
tariksbl 2015年

2
@tarikは、強度だけでなく導関数にも一致します。または、バンドパスエンハンスエッジに一致します。基本的に、強さだけが人に見えるものではありません。彼らは、グラデーションとエッジを見ます。
Yakk-Adam Nevraumont 2015年

1
@Yakkゾーンサブディビジョンは、このようなことを間接的に実行します。3x3ゾーンとして文字を処理してDCTを比較するのがさらに良いかもしれませんが、パフォーマンスは大幅に低下すると思います。
Spektre、2015年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.