1. ホーム
  2. c++

[解決済み] 画像からASCIIアートへの変換

2022-09-25 08:21:44

質問

プロローグ

この話題はStack Overflowで時々出てきますが、たいていは下手な質問のために削除されます。私は多くのそのような質問を見かけ、そして、そのような質問に対する OP (通常の低い担当者) が追加の情報を要求したときに沈黙するのを見ました。時々、入力が私にとって十分であれば、私は答えで答えることを決定し、それは通常、アクティブな間、一日にいくつかのアップボートを取得しますが、数週間後に質問が削除/削除され、すべてが最初から開始されます。そこで、私はこのように書くことにしました。 Q&A を書いて、何度も答えを書き直すことなく、そのような質問を直接参照できるようにしようと思いました.

もう一つの理由は、これまた メタスレッド をターゲットにしたものなので、もし追加の意見があれば、遠慮なくコメントしてください。

質問

ビットマップイメージを ASCII アート を使って C++ ?

いくつかの制約があります。

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

関連するWikipediaのページはこちらです。 アスキーアート (@RogerRowland に感謝) です。

こちらも同様 迷路からアスキーアートへの変換 Q&A

どのように解決するのですか?

画像からアスキーアートへの変換は、以下のような方法があります。 等幅フォント . 簡単のために、私は基本的なことだけに専念します。

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

この方法は、ピクセル領域の各ピクセルを1つのドットとして扱います。アイデアは、このドットの平均グレースケール強度を計算し、計算されたものに十分に近い強度を持つ文字でそれを置き換えることです。そのためには、あらかじめ計算された強度を持つ、使用可能な文字のリストが必要です。これを文字と呼ぶことにしましょう 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) 探索の最小距離ループか O(1) の辞書を使う。時には簡略化のために、文字 map[] は線形分布として扱われることがあり、わずかなガンマ歪みが生じますが、通常、何を探すかわからない限り、結果には現れません。

強度ベースの変換は、グレースケール画像(白黒だけでなく)にも最適です。ドットを1ピクセルとして選択すると、結果が大きくなる(1ピクセル ->1文字)ので、大きな画像の場合は、代わりに領域(フォントサイズの倍数)を選択して、縦横比を維持し、拡大しすぎないようにします。

どうやるか。

  1. 画像を(グレースケールの)ピクセルまたは(長方形の)領域に均等に分割する ドット s
  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;

を使用しない限り、VCLのものを置き換える/無視する必要があります。 ボーランド / エンバカデロ の環境で使用できます。

  • mm_log はテキストが出力されるメモ
  • bmp は入力ビットマップ
  • AnsiString のように0からではなく、1からインデックスされたVCLタイプの文字列です。 char* !!!

これがその結果です。 ややNSFWな強度例画像

左側はASCIIアートの出力(フォントサイズ5ピクセル)、右側は入力画像 拡大 を数回拡大したものです。見ての通り、出力はより大きなピクセル -> 文字になっています。もしピクセルの代わりに大きな領域を使えば、ズームは小さくなりますが、もちろん出力は視覚的に美しくありません。 この方法は、コード化/処理するのが非常に簡単で速いです。

もっと高度なものを追加すると、例えば

  • 地図計算の自動化
  • 自動的なピクセル/エリアサイズ選択
  • アスペクト比の補正

そうすれば、より複雑な画像を、より良い結果で処理することができます。

以下は、1:1の比率での結果です(文字を見るにはズームしてください)。

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

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

見ての通り、これはより大きな画像に向いています。

キャラクターフィッティング(シェーディングとベタ塗りアスキーのハイブリッド)

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

  1. そこで、画像を(グレースケールの)矩形領域に均等に分割する ドット 's

    と同じアスペクト比であることが理想です。 レンダリング フォント文字と同じアスペクト比であることが望ましい(アスペクト比が維持される。文字は通常、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 ) で、単一の 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"

を追加すると、正しく動作します。そうしないと、フォントは mono-spaced として処理されません。マウス ホイールは、異なるフォント サイズでの結果を確認するために、フォント サイズを上下に変更するだけです。

[ノート]

  • 参照 言葉のポートレートの可視化
  • ビットマップ/ファイルアクセスおよびテキスト出力機能を持つ言語を使用する
  • 私は、最初のアプローチが非常に簡単で単純であることから、最初のアプローチから始めることを強くお勧めします。そして、2番目のアプローチ(最初のアプローチの修正として行うことができるため、コードのほとんどはそのまま残ります)に進むだけでよいのです。
  • 標準のテキスト プレビューが白い背景であるため、反転した強度 (黒いピクセルが最大値) で計算するのは良いアイデアです。
  • のようなグリッドを使って、サブディビジョンゾーンのサイズ、数、およびレイアウトを実験することができます。 3x3 のようなグリッドを使用することもできます。

比較

最後に、同じ入力に対する2つのアプローチの比較です。

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

かっこいいアプリケーション

今日の新しい質問を読んでいるとき、私はデスクトップの選択された領域をつかんで、それを連続的に ASCIIart 変換器に連続的に送り、結果を表示するクールなアプリケーションのアイデアを得ました。1時間のコーディングの後、それは完成し、私はその結果にとても満足しているので、単にそれをここに追加しなければなりません。

アプリケーションは2つのウィンドウで構成されています。最初のマスター ウィンドウは、基本的に、イメージの選択とプレビューのない、私の古いコンバーター ウィンドウです (上記のすべてのものはその中にあります)。これは、ASCII プレビューと変換設定だけを持っています。2番目のウィンドウは、グラビングエリアを選択するための透明な内側を持つ空のフォームです(何の機能もありません)。

さて、タイマーで、私はただ選択フォームで選択された領域をつかみ、それを変換に渡し、プレビューするために アスキーアート .

つまり、変換したい領域を選択ウィンドウで囲み、その結果をマスターウィンドウで見るわけです。ゲームやビューアなどでも構いません。こんな感じです。

で動画も見られるようになりました。 アスキーアート での動画も見られるようになりました。中には本当に素敵なものもありますよ :).

で実装してみるなら GLSL

これをご覧ください。