Istnieje więcej podejść do konwersji obrazu do grafiki ASCII, które są w większości oparte na użyciu czcionek o pojedynczej szerokości . Dla uproszczenia trzymam się tylko podstaw:
Na podstawie intensywności pikseli / obszaru (cieniowanie)
To podejście traktuje 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 intensywności zbliżonej do obliczonej. W tym celu potrzebujemy jakiejś listy użytecznych znaków, z których każda ma wstępnie obliczoną intensywność. Nazwijmy to postacią map
. Aby szybciej wybrać, która postać jest najlepsza dla danej intensywności, istnieją dwa sposoby:
Mapa znaków o rozkładzie liniowym intensywności
Więc używamy tylko znaków, które mają różnicę intensywności w tym samym kroku. Innymi słowy, po posortowaniu rosnąco:
intensity_of(map[i])=intensity_of(map[i-1])+constant;
Również kiedy nasza postać map
jest posortowana, możemy obliczyć znak bezpośrednio z intensywności (bez potrzeby wyszukiwania)
character = map[intensity_of(dot)/constant];
Mapa znaków o arbitralnym rozkładzie intensywności
Mamy więc szereg użytecznych postaci i ich intensywności. Musimy znaleźć intensywność najbliższą wartości, intensity_of(dot)
więc ponownie, jeśli posortowaliśmy map[]
, możemy użyć wyszukiwania binarnego, w przeciwnym razie potrzebujemy O(n)
pętli wyszukiwania minimalnej odległości lub O(1)
słownika. Czasami dla uproszczenia postać map[]
może być traktowana jako rozłożona liniowo, powodując niewielkie zniekształcenie gamma, zwykle niewidoczne w wyniku, chyba że wiesz, czego szukać.
Konwersja oparta na intensywności jest świetna również w przypadku obrazów w skali szarości (nie tylko czarno-białych). Jeśli wybierzesz kropkę jako pojedynczy piksel, wynik stanie się duży (jeden piksel -> pojedynczy znak), więc w przypadku większych obrazów zamiast tego wybierany jest obszar (wielokrotność rozmiaru czcionki), aby zachować proporcje i nie powiększać zbytnio.
Jak to zrobić:
- Równomiernego podziału obrazu w skali szarości (pikseli), lub (prostokątne) powierzchnie dot s
- Oblicz intensywność każdego piksela / obszaru
- Zastąp go znakiem z mapy znaków o najbliższej intensywności
Jako postać map
możesz użyć dowolnych znaków, ale wynik jest lepszy, jeśli postać ma równomiernie rozmieszczone piksele wzdłuż obszaru znaku. Na początek możesz użyć:
char map[10]=" .,:;ox%#@";
posortowane malejąco i udawaj rozkład liniowy.
Więc jeśli intensywność piksela / obszaru jest równa, i = <0-255>
to znak zastępczy będzie
Jeśli i==0
wtedy piksel / obszar jest czarny, jeśli i==127
wtedy piksel / obszar jest szary, a jeśli i==255
wtedy piksel / obszar jest biały. Możesz eksperymentować z różnymi postaciami w środku map[]
...
Oto mój starożytny 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;
Musisz wymienić / zignorować rzeczy VCL, chyba że używasz środowiska Borland / Embarcadero .
mm_log
to notatka, w której wyprowadzany jest tekst
bmp
jest wejściową mapą bitową
AnsiString
jest ciągiem typu VCL indeksowanym od 1, a nie od 0 jako char*
!!!
Oto wynik: Przykładowy obraz o lekkiej intensywności NSFW
Po lewej stronie znajduje się obraz wyjściowy ASCII (rozmiar czcionki 5 pikseli), a po prawej obraz wejściowy powiększony kilka razy. Jak widać, wynik ma większy piksel -> znak. Jeśli używasz większych obszarów zamiast pikseli, powiększenie jest mniejsze, ale oczywiście wynik jest mniej przyjemny wizualnie. Takie podejście jest bardzo łatwe i szybkie w kodowaniu / przetwarzaniu.
Gdy dodasz bardziej zaawansowane rzeczy, takie jak:
- automatyczne obliczenia map
- automatyczny wybór rozmiaru piksela / obszaru
- korekty współczynnika kształtu
Następnie możesz przetwarzać bardziej złożone obrazy z lepszymi wynikami:
Oto wynik w stosunku 1: 1 (powiększ, aby zobaczyć znaki):
Oczywiście przy próbkowaniu obszaru tracisz drobne szczegóły. To jest obraz o takim samym rozmiarze jak pierwszy przykład z próbkowanymi obszarami:
Zaawansowany przykładowy obraz o nieznacznej intensywności NSFW
Jak widać, jest to bardziej odpowiednie dla większych obrazów.
Dopasowywanie znaków (hybryda między cieniowaniem a jednolitą grafiką ASCII)
To podejście próbuje zastąpić obszar (nie więcej pojedynczych punktów pikselowych) charakterem o podobnej intensywności i kształcie. Prowadzi to do lepszych wyników, nawet przy użyciu większych czcionek w porównaniu z poprzednim podejściem. Z drugiej strony to podejście 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 bezwzględnej różnicy między pikselami, ale to doprowadzi do niezbyt dobrych wyników, ponieważ nawet przesunięcie o jeden piksel spowoduje, że odległość będzie duża. Zamiast tego możesz użyć korelacji lub innych metryk. Ogólny algorytm jest prawie taki sam jak w poprzednim podejściu:
Tak równomiernie podzielić obraz (skala szarości) obszary prostokątne dot „s
najlepiej z tym samym współczynnikiem proporcji co renderowane znaki czcionki (zachowa współczynnik proporcji. Nie zapominaj, że znaki zwykle nakładają się trochę na osi x)
Oblicz intensywność każdego obszaru ( dot
)
Zastąp go postacią z postaci map
o najbliższej intensywności / kształcie
Jak możemy obliczyć odległość między znakiem a kropką? To jest najtrudniejsza część tego podejścia. Eksperymentując, wypracowuję ten kompromis między szybkością, jakością i prostotą:
Podziel obszar postaci na strefy
- Oblicz osobną intensywność dla lewej, prawej, górnej, dolnej i środkowej strefy każdego znaku z alfabetu konwersji (
map
).
- Normalizuj wszystkie intensywności, aby były niezależne od wielkości obszaru
i=(i*256)/(xs*ys)
.
Przetwarzaj obraz źródłowy w obszarach prostokątnych
- (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 spośród intensywności w alfabecie konwersji
- Wpisz dopasowany znak
To jest wynik dla rozmiaru czcionki = 7 pikseli
Jak widać, wynik jest przyjemny wizualnie, nawet przy użyciu większego rozmiaru czcionki (w poprzednim przykładzie podejście miało rozmiar czcionki 5 pikseli). Plik wyjściowy ma mniej więcej taki sam rozmiar jak obraz wejściowy (bez powiększenia). Lepsze wyniki osiąga się, ponieważ postacie są bliżej oryginalnego obrazu, nie tylko ze względu na intensywność, ale także ogólny kształt, dzięki czemu można używać większych czcionek i nadal zachować szczegóły (do pewnego momentu).
Oto pełny kod 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) // 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();
}
//---------------------------------------------------------------------------
Jest to prosty formularz zgłoszeniowy ( Form1
) z pojedynczym TMemo mm_txt
w nim. Wczytuje obraz, "pic.bmp"
a następnie zgodnie z rozdzielczością wybiera metodę konwersji na tekst, który jest zapisywany "pic.txt"
i wysyłany do notatki w celu wizualizacji.
Dla tych bez VCL zignoruj rzeczy VCL i zastąp AnsiString
dowolnym typem łańcucha, a także Graphics::TBitmap
dowolną klasą bitmapową lub graficzną, którą masz do dyspozycji z możliwością dostępu do pikseli.
Bardzo ważną informacją jest to, że używa to ustawień mm_txt->Font
, więc upewnij się, że ustawiłeś:
Font->Pitch = fpFixed
Font->Charset = OEM_CHARSET
Font->Name = "System"
aby to działało poprawnie, w przeciwnym razie czcionka nie będzie obsługiwana jako mono-spaced. Kółko myszy po prostu zmienia rozmiar czcionki w górę / w dół, aby zobaczyć wyniki dla różnych rozmiarów czcionek.
[Uwagi]
- Zobacz wizualizację Portrety w programie Word
- Użyj języka z dostępem do bitmap / plików i możliwościami wyjścia tekstu
- Zdecydowanie polecam zacząć od pierwszego podejścia, ponieważ jest ono bardzo łatwe, proste i proste, a dopiero potem przejść do drugiego (co można zrobić jako modyfikację pierwszego, więc większość kodu pozostaje taka, jaka jest)
- Dobrym pomysłem jest obliczanie z odwróconą intensywnością (maksymalna wartość to czarne piksele), ponieważ standardowy podgląd tekstu znajduje się na białym tle, co prowadzi do znacznie lepszych wyników.
- możesz poeksperymentować z rozmiarem, liczbą i układem stref podziału lub
3x3
zamiast tego użyć jakiejś siatki .
Porównanie
Na koniec jest porównanie między dwoma podejściami na tym samym wejściu:
Obrazy oznaczone zieloną kropką są wykonane z podejściem nr 2, a czerwone z numerem 1 , wszystkie z sześciopikselowym rozmiarem czcionki. Jak widać na obrazie żarówki, podejście wrażliwe na kształt jest znacznie lepsze (nawet jeśli # 1 jest zrobiony na obrazie źródłowym powiększonym 2x).
Fajna aplikacja
Czytając dzisiejsze nowe pytania, wpadłem na pomysł fajnej aplikacji, która przechwytuje wybrany obszar pulpitu i stale przesyła go do konwertera ASCIIart i przegląda wynik. Po godzinie kodowania gotowe i jestem tak zadowolony z wyniku, że po prostu muszę go tutaj dodać.
OK, aplikacja składa się tylko z dwóch 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 pusty formularz z przezroczystym wnętrzem do wyboru obszaru chwytania (brak jakiejkolwiek funkcjonalności).
Teraz na liczniku czasu po prostu chwytam wybrany obszar za pomocą formularza wyboru, przekazuję go do konwersji i przeglądam ASCIIart .
Więc otaczasz obszar, który chcesz przekonwertować, oknem wyboru i wyświetlasz wynik w oknie głównym. Może to być gra, przeglądarka itp. Wygląda to tak:
Więc teraz mogę dla przyjemności oglądać nawet filmy w ASCIIart . Niektóre są naprawdę fajne :).
Jeśli chcesz spróbować zaimplementować to w GLSL , spójrz na to: