web-dev-qa-db-ja.com

ASCIIアート変換への画像

プロローグ

このサブジェクトはここに[〜#〜] so [〜#〜]でポップアップしますが、通常は不十分な記述のため削除されます質問。追加の情報が要求されたとき、多くのそのような質問を見て、[〜#〜] op [〜#〜](通常は低い担当者)から沈黙しました。時々入力が私にとって十分である場合、私は回答で応答することを決定し、通常はアクティブな間に1日に数回のアップ投票を取得しますが、その後数週間後に質問が削除/削除され、すべてが最初から始まります。だから私はこれを書くことにしましたQ&A答えを何度も書き直さずにそのような質問を直接参照できるようにします…

別の理由もこれです METAスレッド 私をターゲットにしているので、追加の入力があればコメントしてください。

質問

C++を使用してビットマップ画像をASCIIアートに変換するには?

いくつかの制約:

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

関連するWikiページを次に示します ASCII art (@RogerRowlandに感謝)

98
Spektre

ASCIIアート変換への画像のアプローチは他にもあります。これは主にモノスペースフォントを使用することに基づいています)基本のみにこだわる:

ピクセル/面積強度ベース(シェーディング)

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

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

    したがって、同じステップで強度の差がある文字のみを使用します。言い換えると、昇順でソートされた場合:

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

    また、文字mapがソートされると、輝度から直接文字を計算できます(検索は不要です)

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

    したがって、使用可能な文字とその強度の配列があります。 intensity_of(dot)に最も近い強度を見つける必要があるため、map[]を並べ替えた場合はバイナリ検索を使用できます。そうでない場合は、O(n) search min distance loopまたはO(1)辞書が必要です。簡単にするために、文字map[]は線形に分布するものとして扱うことができ、何を探すかわからない限り、通常は結果にわずかなガンマ歪みが生じます。

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

どうやるか:

  1. 画像を(グレースケール)ピクセルまたは(長方形)エリアdotに均等に分割します
  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は入力ビットマップです
  • AnsiStringは、0からではなくchar* !!!としてではなく、VCLタイプの文字列インデックス付きフォーム1です

これが結果です: わずかNSFW強度の例の画像

左側はASCII art output(font size 5px)、右側の入力画像は数倍に拡大されています。出力がより大きなピクセル->文字であることがわかります。ピクセルの代わりにズームは小さくなりますが、もちろん出力は見た目が良くありませんこのアプローチは非常に簡単で、コード/処理が高速です。

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

  • 自動マップ計算
  • 自動ピクセル/領域サイズ選択
  • アスペクト比補正

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

ここでは、1:1の比率になります(キャラクターを見るためにズームします):

intensity advanced example

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

わずかNSFW強度の高度なサンプル画像

ご覧のとおり、これは大きな画像に適しています

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

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

  1. そのため、画像を(グレースケールの)長方形の領域に均等に分割しますdot 's
    • 理想的には、renderedフォント文字と同じ縦横比(縦横比を維持します。通常、文字がx軸で少し重なることを忘れないでください)
  2. 各エリアの強度を計算します(dot
  3. 文字mapから最も近い輝度/形状の文字に置き換えます

文字とドットの間の距離を計算する方法は?それがこのアプローチの最も難しい部分です。実験中に、速度、品質、シンプルさの妥協案を作成します。

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

    zones

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

    • (対象のフォントと同じアスペクト比で)
    • 各領域について、箇条書き1と同じ方法で強度を計算します
    • 変換アルファベットの強度から最も近い一致を見つける
    • 出力適合文字

これは、フォントサイズ= 7ピクセルの場合の結果です

char fitting example

ご覧のとおり、大きなフォントサイズを使用しても出力は視覚的に快適です(前のアプローチ例は5pxフォントサイズでした)。出力は入力画像とほぼ同じサイズです(ズームなし)。文字は強度だけでなく全体的な形状によっても元の画像に近いため、より良い結果が得られます。したがって、より大きなフォントを使用し、詳細を維持することができます(粗い点まで)。

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) // charcter 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;     // temp 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 init buffers
        tmp=new Graphics::TBitmap; if (tmp==NULL) break;
            // allow 32bit 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 image by zoomed character size step
        xf-=xf/3; // characters are usually overlaping 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 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の視覚化 を参照してください
  • ビットマップ/ファイルアクセスおよびテキスト出力機能を備えた言語を使用する
  • 最初のアプローチから始めることを強くお勧めします。これは非常に簡単で単純であり、2番目に移動するだけです(最初のアプローチを変更すると、ほとんどのコードがそのまま維持されます)
  • 標準のテキストプレビューは白い背景上にあるため、より良い結果が得られるため、逆の強度で計算することをお勧めします(黒いピクセルが最大値です)。
  • サブディビジョンゾーンのサイズ、数、レイアウトを試すか、代わりに3x3などのグリッドを使用できます。

[Edit1]比較

最後に、同じ入力での2つのアプローチの比較を示します。

comparison

緑色の点でマークされた画像はアプローチ#2で、赤色の点は#1すべて6ピクセルフォントサイズ。電球の画像を見るとわかるように、形状に敏感なアプローチははるかに優れています(#1が2倍に拡大されたソース画像で行われた場合でも)。

[Edit2]クールなアプリ

今日の新しい質問を読んでいるうちに、デスクトップの選択した領域を取得し、ASCIIartコンバーターに継続的にフィードして結果を表示するクールなアプリのアイデアを得ました。コーディングの1時間後には完了し、結果に非常に満足しているため、ここに追加するだけです。

OK、アプリは2つのウィンドウから構成されます。最初のマスターウィンドウは、基本的に、画像の選択とプレビューを行わない古いコンバーターウィンドウです(上記のものはすべて含まれています)。 ASCIIプレビューおよび変換設定。2番目のウィンドウは空のフォームで、グラブエリア選択用の透明な内部です(機能性はありません)。

タイマーで、選択フォームで選択した領域を取得し、変換に渡し、ASCIIartをプレビューします。

そのため、変換する領域を選択ウィンドウで囲み、結果をマスターウィンドウで表示します。ゲーム、ビューア、...のようになります。

ASCIIart grabber example

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

hands

[Edit3]

[〜#〜] glsl [〜#〜]でこれを実装したい場合は、これを見てください:

143
Spektre