Konwersja obrazu do ASCII art

Prolog

Ten temat pojawia się tutaj na więc od czasu do czasu, ale jest usuwany zazwyczaj z powodu Źle napisanego pytania. Widziałem wiele takich pytań, a potem cisza od OP (zwykle niska rep), gdy wymagane są dodatkowe informacje. Od czasu do czasu, jeśli Dane wejściowe są dla mnie wystarczająco dobre, decyduję się odpowiedzieć odpowiedzią i zwykle dostaje kilka up-głosów dziennie podczas aktywnej, ale potem po kilku tygodniach pytanie zostaje usunięte / usunięte i wszystko zaczyna się od początek. Dlatego postanowiłem napisać to Q & A , Aby móc odwoływać się do takich pytań bezpośrednio bez przepisywania odpowiedzi w kółko ...

Kolejnym powodem jest również ten meta thread skierowany do mnie, więc jeśli masz dodatkowy wkład zapraszam do komentowania.

Pytanie

Jak przekonwertować obraz bitmapowy na ASCII art za pomocą C++?

Niektóre ograniczenia:

  • obrazy w skali szarości
  • używanie jednoprzestrzeni czcionki
  • [25]} utrzymanie go w prostocie (nie używanie zbyt zaawansowanych rzeczy dla początkujących programistów) {26]}
[2]}Tutaj jest powiązana strona Wiki ASCII art (Dzięki @RogerRowland)
Author: Community, 2015-10-07

1 answers

Istnieje więcej metod konwersji obrazów do ASCII art, które są głównie oparte na użyciu czcionek jednoprzestrzennych dla uproszczenia trzymam się tylko podstaw:

Na podstawie intensywności pikseli / powierzchni (cieniowanie)

To podejście obsługuje każdy piksel obszaru pikseli jako pojedynczą kropkę. Chodzi o to, aby obliczyć średnią intensywność skali szarości tej kropki, a następnie zastąpić ją znakiem o zbliżonej intensywności do obliczonej. Do tego potrzebujemy trochę listy użytecznych znaki z precomputed intensity let call it character map. Aby szybciej wybrać, która postać jest najlepsza dla której intensywności istnieją dwa sposoby:]}

  1. Liniowo rozproszona Mapa znaków intensywności

    Więc używamy tylko znaków, które mają różnicę intensywności z tym samym krokiem. Innymi słowy, gdy sortowane są rosnąco, to:

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

    Również gdy nasz znak map jest posortowany, możemy obliczyć znak bezpośrednio z intensywności (nie wymaga wyszukiwania)

    character=map[intensity_of(dot)/constant];
    
  2. Dowolne rozproszone natężenie Mapa znaków

    Mamy więc szereg użytecznych znaków i ich natężenia. Musimy znaleźć intensywność najbliższą intensity_of(dot) więc ponownie, jeśli posortowaliśmy map[] możemy użyć wyszukiwania binarnego, w przeciwnym razie potrzebujemy O(n) przeszukać pętlę min distance lub słownik O(1). Czasami dla uproszczenia znak map[] może być traktowany jako rozkład liniowy powodujący niewielkie zniekształcenia gamma zwykle niewidoczne w wynik, chyba że wiesz, czego szukać.

Konwersja na podstawie intensywności jest świetna również dla obrazów w skali szarości (nie tylko czerni i bieli). Jeśli wybierzesz kropkę jako pojedynczy piksel, wynik będzie duży (1 piksel -> pojedynczy znak), więc dla większych obrazów wybrany zostanie obszar (mnożenie rozmiaru czcionki), aby zachować proporcje i nie powiększać zbytnio.

Jak to zrobić:

  1. więc równomiernie Podziel obraz na (w skali szarości)piksele lub (prostokątne) obszary dot ' s
  2. Oblicz intensywność każdego piksela / obszaru
  3. zastąp go znakiem z mapy postaci z najbliższą intensywnością

Jako znak map Można używać dowolnych znaków, ale wynik jest lepszy, jeśli znak ma piksele rozłożone równomiernie wzdłuż obszaru znaków. Na początek możesz użyć:

  • char map[10]=" .,:;ox%#@";

Sortowane malejąco i udają, że są rozdzielane liniowo.

Więc jeśli intensywność piksela / obszaru wynosi i = <0-255> następnie znakiem zastępczym będzie

  • map[(255-i)*10/256];

Jeśli i==0 to piksel/obszar jest czarny, jeśli i==127 to piksel/obszar jest szary, a jeśli i==255 to piksel/obszar jest biały. Możesz eksperymentować z różnymi postaciami wewnątrz map[]...

Oto mój przykład w C++ i 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;
Jeśli nie używasz środowiska Borland/Embarcadero, musisz zastąpić/zignorować VCL]}
  • mm_log jest notatką, gdzie tekst jest outputted
  • bmp jest bitmapą wejściową
  • AnsiString jest ciągiem typu VCL indeksowanym w postaci 1, a nie od 0 jako char* !!!

Oto wynik: lekko NSFW intensywność przykładowy obraz

Po lewej stronie znajduje się wyjście ASCII art (rozmiar czcionki 5PX), a po prawej obraz wejściowy powiększony kilka razy. Jak widać wyjście jest większy piksel - > znak. jeśli używasz większych obszarów zamiast pikseli, zoom jest mniejszy, ale oczywiście wyjście jest mniej wizualne miło. to podejście jest bardzo łatwe i szybkie w kodowaniu / przetwarzaniu.

Gdy dodasz bardziej zaawansowane rzeczy jak:

  • automatyczne obliczenia map
  • automatyczny wybór rozmiaru piksela / obszaru
  • korekta proporcji obrazu

Następnie możesz przetwarzać bardziej złożone obrazy z lepszymi wynikami:

Tutaj wynik w stosunku 1: 1 (Powiększ, aby zobaczyć znaki):

intensywność zaawansowany przykład

Oczywiście do pobierania próbek obszaru tracisz drobne szczegóły. Jest to obraz o tej samej wielkości, co pierwszy przykład próbkowany z obszarami:

Lekko NSFW intensywność zaawansowany przykładowy obraz

Jak widać, jest to bardziej odpowiednie dla większych obrazów

Dopasowanie znaków (hybryda cieniowania i solidnej sztuki ASCII)

To podejście stara się zastąpić obszar (nie więcej pojedynczych pikseli kropek) znakiem o podobnej intensywności i kształcie. Prowadzi to do lepszych rezultatów nawet przy użyciu większych czcionek w porównaniu z poprzednim podejściem z drugiej strony podejście to jest oczywiście nieco wolniejsze. Jest na to więcej sposobów, ale główną ideą jest obliczenie różnicy (odległości) między obszarem obrazu (dot) a renderowanym znakiem. Możesz zacząć od naiwnej sumy różnicy abs między pikselami, ale to doprowadzi do niezbyt dobrych wyników, ponieważ nawet przesunięcie o 1 piksel spowoduje, że odległość będzie duża, zamiast tego możesz użyć korelacji lub różnych wskaźników. Ogólny algorytm jest prawie taki sam jak poprzednie podejście:

  1. więc równomiernie Podziel obraz na (w skali szarości) prostokątne obszary kropka ' s
    • najlepiej z tym samym współczynnikiem proporcji, co renderowane znaki czcionek (zachowa proporcje, nie zapominaj, że znaki Zwykle nakładają się na siebie w osi x)
  2. Oblicz intensywność każdego obszaru (dot)
  3. zastąp go znakiem od znaku map z najbliższą intensywnością / kształtem

Jak obliczyć odległość między znakiem a kropką?To najtrudniejsza część tego podejścia. Podczas eksperymentów rozwijam ten kompromis między szybkością, jakością i prostotą: {]}

  1. Podziel obszar znaków na strefy

    strefy

    • Oblicz osobną intensywność dla lewej, prawej, górnej, dolnej i środkowej strefy każdego znaku z alfabetu konwersji(map)
    • znormalizować wszystkie natężenia, aby były niezależne od rozmiar obszaru i=(i*256)/(xs*ys)
  2. Przetwarzanie obrazu źródłowego w obszarach prostokąta

    • (z tym samym współczynnikiem proporcji co czcionka docelowa)
    • dla każdego obszaru Oblicz intensywność w taki sam sposób jak w punkcie 1
    • Znajdź najbliższe dopasowanie z alfabetu konwersji
    • wyjście dopasowane znak

To jest wynik dla font size = 7PX

przykład mocowania znaków

Jak widać wyjście jest wizualnie przyjemny nawet przy większym rozmiarze czcionki (poprzedni przykład podejścia był przy rozmiarze czcionki 5px). Wyjście ma mniej więcej taki sam rozmiar jak obraz wejściowy (bez zoomu). Lepsze wyniki uzyskuje się, ponieważ znaki są bliższe oryginalnemu obrazowi nie tylko przez intensywność, ale także przez ogólny kształt, dlatego można używać większych czcionek i nadal zachowując szczegóły (do punktu zgrubienia).

Tutaj Pełny kod dla aplikacji do konwersji opartej na 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();
    }
//---------------------------------------------------------------------------

To proste formularz app (Form1) Z pojedynczym TMemo mm_txt w nim. Ładuje obraz "pic.bmp", a następnie zgodnie z rozdzielczością wybierz sposób konwersji na tekst, który zostanie zapisany do "pic.txt" i wysłany do notatki w celu wizualizacji. Dla tych, którzy nie mają VCL zignoruj VCL i zastąp AnsiString dowolnym typem ciągu, a także {[33] } dowolną klasą bitmapy lub obrazu, którą masz do dyspozycji z możliwością dostępu do pikseli.

Bardzo ważne uwaga jest to, że używa się ustawień mm_txt->Font więc upewnij się, że zestaw:

  • Font->Pitch=fpFixed
  • Font->Charset=OEM_CHARSET
  • Font->Name="System"

Aby to działało poprawnie, w przeciwnym razie czcionka nie będzie traktowana jako jednoprzestrzenna. Kółko myszy po prostu zmienia rozmiar czcionki w górę / w dół, aby zobaczyć wyniki dla różnych rozmiarów czcionek

[uwagi]

  • zobacz Wizualizacja portretów słownych
  • użyj języka z dostępem do bitmap/plików i możliwościami wyjścia tekstu
  • zdecydowanie polecam zacząć od pierwszego podejście, ponieważ jest bardzo proste i proste, a dopiero potem przejście do drugiego (co można zrobić jako modyfikację pierwszego, aby większość kodu pozostała taka, jaka jest i tak)
  • dobrym pomysłem jest obliczenie z odwróconą intensywnością (czarne piksele to maksymalna wartość), ponieważ standardowy podgląd tekstu jest na białym tle, co prowadzi do znacznie lepszych wyników.
  • możesz eksperymentować z rozmiarem, liczeniem i układem stref podziału lub użyć siatki podobnej do 3x3 zamiast tego.

[Edit1] porównanie

Wreszcie jest porównanie między dwoma podejściami na tym samym wejściu:

porównanie

Zaznaczone zieloną kropką obrazy są wykonywane za pomocą podejścia #2 i czerwone z #1 Wszystko na 6 rozmiar czcionki w pikselach. Jak widać na obrazie żarówki, podejście wrażliwe na kształt jest znacznie lepsze (nawet jeśli #1 odbywa się na 2x zoomed źródło obraz).

[Edit2] cool app

Czytając dzisiejsze nowe pytania, wpadłem na pomysł fajnej aplikacji, która chwyta wybrany region pulpitu i stale przekazuje go do konwertera ASCIIart i wyświetla wynik. Po Godzinie Kodowania jest zrobione i jestem tak zadowolony z wyniku, że po prostu muszę go dodać tutaj.

OK aplikacja składa się z zaledwie 2 okien. Pierwsze okno główne to w zasadzie moje stare okno konwertera bez wyboru obrazu i podglądu (wszystkie powyższe rzeczy są w nim). Ma tylko podgląd ASCII i ustawienia konwersji. Drugie okno to pusta forma z przezroczystym wnętrzem dla zaznaczenia obszaru chwytania (brak jakiejkolwiek funkcjonalności).

Teraz na timerze po prostu chwytam wybrany obszar za pomocą formularza wyboru, przekazuję go do konwersji i podgląd ASCIIart .

Więc zamykasz obszar, który chcesz przekonwertować przez okno wyboru i wyświetlasz wynik w oknie głównym. Może to być gra, przeglądarka,... Informatyka wygląda tak:

Przykład ASCIIart grabber

Więc teraz mogę oglądać nawet filmy w ASCIIart dla Zabawy. Niektóre są naprawdę ładne :).

ręce

[Edit3]

Jeśli chcesz spróbować zaimplementować to w GLSL spójrz na to:

 134
Author: Spektre,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-07-12 15:43:55