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]}
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:]}
-
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];
-
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śmymap[]
możemy użyć wyszukiwania binarnego, w przeciwnym razie potrzebujemyO(n)
przeszukać pętlę min distance lub słownikO(1)
. Czasami dla uproszczenia znakmap[]
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ć:
- więc równomiernie Podziel obraz na (w skali szarości)piksele lub (prostokątne) obszary dot ' s
- Oblicz intensywność każdego piksela / obszaru
- 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 jakochar*
!!!
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):
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:
- 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)
- Oblicz intensywność każdego obszaru (
dot
) - 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ą: {]}
Podziel obszar znaków na strefy Przetwarzanie obrazu źródłowego w obszarach prostokąta To jest wynik dla font size = 7PX 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: To proste formularz app ( Bardzo ważne uwaga jest to, że używa się ustawień 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] [Edit1] porównanie Wreszcie jest porównanie między dwoma podejściami na tym samym wejściu: Zaznaczone zieloną kropką obrazy są wykonywane za pomocą podejścia #2 i czerwone z #1 Wszystko na [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: Więc teraz mogę oglądać nawet filmy w ASCIIart dla Zabawy. Niektóre są naprawdę ładne :). [Edit3]
map
)i=(i*256)/(xs*ys)
//---------------------------------------------------------------------------
#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
) 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.mm_txt->Font
więc upewnij się, że zestaw:
Font->Pitch=fpFixed
Font->Charset=OEM_CHARSET
Font->Name="System"
3x3
zamiast tego.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).
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