Prolog
Ten temat pojawia się od czasu do czasu w Stack Overflow, ale zwykle jest usuwany, ponieważ jest źle napisanym pytaniem. Widziałem wiele takich pytań, a następnie milczenie ze strony PO (zwykle niski przedstawiciel), gdy wymagane są dodatkowe informacje. Od czasu do czasu, jeśli dane wejściowe są dla mnie wystarczająco dobre, decyduję się odpowiedzieć i zwykle dostaje kilka głosów pozytywnych dziennie, gdy jest aktywny, ale potem po kilku tygodniach pytanie jest usuwane / usuwane i wszystko zaczyna się od początek. Postanowiłem więc napisać te pytania i odpowiedzi, aby móc bezpośrednio odnosić się do takich pytań, bez ponownego przepisywania odpowiedzi…
Innym powodem jest również ten metatątek skierowany do mnie, więc jeśli masz dodatkowe informacje, nie krępuj się komentować.
Pytanie
Jak przekonwertować obraz bitmapowy na grafikę ASCII przy użyciu C ++ ?
Niektóre ograniczenia:
- obrazy w skali szarości
- używając czcionek o mono-odstępach
- zachowanie prostoty (nie używanie zbyt zaawansowanych rzeczy dla początkujących programistów)
Oto powiązana strona Wikipedii Sztuka ASCII (dzięki @RogerRowland).
Tutaj podobny labirynt do konwersji ASCII Art .
Odpowiedzi:
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:
Również kiedy nasza postać
map
jest posortowana, możemy obliczyć znak bezpośrednio z intensywności (bez potrzeby wyszukiwania)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śmymap[]
, możemy użyć wyszukiwania binarnego, w przeciwnym razie potrzebujemyO(n)
pętli wyszukiwania minimalnej odległości lubO(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ć:
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ędziemap[(255-i)*10/256];
Jeśli
i==0
wtedy piksel / obszar jest czarny, jeślii==127
wtedy piksel / obszar jest szary, a jeślii==255
wtedy piksel / obszar jest biały. Możesz eksperymentować z różnymi postaciami w środkumap[]
...Oto mój starożytny przykład w C ++ i VCL:
Musisz wymienić / zignorować rzeczy VCL, chyba że używasz środowiska Borland / Embarcadero .
mm_log
to notatka, w której wyprowadzany jest tekstbmp
jest wejściową mapą bitowąAnsiString
jest ciągiem typu VCL indeksowanym od 1, a nie od 0 jakochar*
!!!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:
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łcieJak 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
map
).i=(i*256)/(xs*ys)
.Przetwarzaj obraz źródłowy w obszarach prostokątnych
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:
Jest to prosty formularz zgłoszeniowy (
Form1
) z pojedynczymTMemo 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żeGraphics::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]
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:
źródło
3x3
stref i porównać DCT, ale myślę, że to znacznie zmniejszyłoby wydajność.