OCR - zrobię to sam…
Jak część z was zapewne wie, bawię się w czytanie różnego rodzaju obrazków, popularnie nazywanych captcha - takie niegroźne hobby (nigdzie nie publikowane, bez robienia szkód serwisom, które mają “słabe” captcha). W przypadku niektórych używałem gocr do ostatecznego odczytania tekstu (przez lenistwo) - niestety ostatnio gocr mnie zawodził, więc…
…postanowiłem napisać swój system rozpoznawania znaków. System bardzo prosty, ale jednocześnie bardzo skuteczny - opierający się na rozpoznawaniu wybranych elementów charakterystycznych dla znaków. Nie pozwala on co prawda na czytanie skanów, ale bez problemu rozpozna teksty drukowane na obrazkach.
W pierwszym etapie filtruję obrazki (usuwając wszelkie śmieci, które mogą zniekształcić obrazek), pozostałość (kiedy zostają już tylko litery) robię cz/b, rozbijam na pojedyncze litery (zapomniałem dodać, że sprawdzam, czy litera nie jest przekręcona), a te litery parsuję moim “OCR” (doszedłem do wniosku, że gdybym miał z miesiąc wolnego, mógłbym napisać “poważny” OCR, ale z pracy 3 dniowej też jestem zadowolony).
W pojedynczych literkach skupiam się na kilku charakterysytycznych cechach (ATM mówimy jedynie o literach drukowanych, małe literki pojawią się w wersji 2.0) - liniach prostych, skosach i zaokrągleniach - ścięciach. Okazuje się, że wcale nie jest ich tak dużo, oraz że litery różnią się znacząco od siebie. Po pierwszym filtrze zostaje nam ~2-6 liter, które trzeba dokładniej sprawdzić, po drugim zostaje max 3 (zazwyczaj 1), dla których porównujemy zapisane różnice (np. między D a O różnice są w prawym górnym i lewym dolnym rogu, różnica między F i E jest na dolnej kresce i dolnym prawym rogu - ale te różnice powinny zostać wychwycone już w pierwszych 2 etapcach i jedna z nich powinna zostać wykluczona).
System doskonale sprawdza się w przypadku liter drukowanych i “prostych” znaków - choć zapewne, jeśli sprecyzować dokładność sprawdzania (np. dla rogów mam sprawdzanie, czy jest prostokąt w pełni wypełniony, czy wypełniony “po skosie prawym (slash)”, “skosie lewym (backslash)” i która część skosu jest zapełniona.
Docelowo planuję jeszcze dodać (w przypadku, jeśli zostaną jakieś niepewności (G vs 6 - sprawdzenie, czy jest połączenie między kreseczką w środku) sposób porównywania właśnie typowych “niepewnych”.
System jeszcze nie dokończony, a już pojawił się nowy model sprawdzania litery - opływanie. Dla pojedynczej litery sprawdzamy jak wygląda jej kształt zamieniony na proste, w przypadku których koniec jednej jest początkiem drugiej (trudno to opisać, łatwo zobaczyć na rysunku, ale takowego akurat nie posiadam). Proste krótkie są później zamieniane na dłuższe (3 proste, których X zmienł się o 4px, a Y o 30 można śmiało zamienić na jedną długą z X początkowym pierwszego punktu, i końcowym ostatniego) - i te proste byłyby później porównywane. System ten, jest dużo bardziej skomplikowany, ale osiągnięty efekt może być dużo lepszy.
…a w trakcie pisania powyższego paragrafu, doszedłem do wniosku, że potrzebny mi będzie jeszcze system usuwania kapitalików przed parsowaniem liter…


2008-06-17 15:30
Nieźle, myślałem że technika OCR to coś bardzo skomplikowanego. W życiu nie pomyślałbym, że jakieś proste OCR dla zabawy można napisać w 3 dni.
2008-06-18 10:01
Wszystko opiera się na prostych zasadach, które każdy może wymyśleć - nie gwarantują one co prawda rozpoznawania tekstu w sytuacjach ekstremalnych (złe skany) - ale do moich potrzeb są wystarczające.
Co więcej - ATM mam rozpoznawanie wyłącznie pojedynczych liter (ze względu na captcha, jest to rozwiązanie, do którego dążyłem) - mając do dyspozycji “kartkę liter”, można wiązać pewnego rodzaju dopasowania (albo, jak to jest w “poważnych programach OCR” - dawać użytkownikowi możliwość poprawy błędnie rozpoznanego znaku i odpowiednio reagować) - jeśli np rozpoznawanie “środkowej poziomej kreski” jest błędne, i użytkownik zwróci na to uwagę, w następnych porównaniach przesuwamy ją w odpowiednim kierunku.
2008-11-09 15:37
Od niedawna staram się wykonać własny system OCR - rowniez oparty na PHP. Wszystko byłoby OK gdyby nie fakt że nie mam zielonego pojęcia jak rozdzielić na pojedyncze litery całe frazy w momencie, gdy owe litery są obrócone o pewien (różny dla każdej z nich) kąt i znajdują się bardzo blisko siebie.
Kiedy litery są proste wystarczy znaleźć puste kolumny aby je rozdzielić.
Wpadłem na pomysł aby korzystając z funkcji liniowej sprawdzać ukośnie pixele w poszukiwaniu pustych na całej długości “wykresu” tej funkcji w obrębie obrazka ale nie do końca wiem jak całość napisać aby działała jak należy…
Mógłby Pan podzielić się swoimi rozwiązaniami w tej kwesti? (nie koniecznie od strony kodowej)
pozdrawiam
2008-11-09 21:31
Zależy, czy litery na siebie nachodzą
- jeśli nie nachodzą, to sprawa jest bardzo prosta - potrzeba litery “wycinać” od lewej: wyszukujemy pierwszy piksel i przechodzimy po obrazku znajdując wszystkie połączone z nim piksele (używając lekkiego marginesu błędu [zależnie od wielkości obrazka]) - powstaje nam litera, wycinamy ją z obrazu głównego i powtarzamy wszystko do momentu aż zostanie pusty obrazek - litery mamy rozdzielone, trzeba je obrócić i przeskanować. Metoda ta posiada błąd znaków składających się z kilku elementów “: ? ! i ó ż ” (można to naprawić łącząc znalezione małe elementy z dużymi (małe to max 20% dużych)
- jeśli litery na siebie nachodzą, to nie ma niestety prostego rozwiązania (zwłaszcza dla algorytmu “nieuczącego” się litery [wielkości, kształtu, błędów, położenia wierszy]) - w zasadzie można próbować różnych kombinacji, ale często mogą to być błędne próby (wykrycie “potencjalnego” obrotu litery i jej wielkości - wycięcie wraz z “obcą” częścią i wyszukiwanie w takim kontekście).
Pierwszej metody używałem i sprawdza się dobrze, druga jest ryzykowna, jak będę miał za dużo wolnego czasu (w przyszłym tysiącleciu), albo pojawi się potrzeba - przetestuję
P.S. w przypadku obróconych litery wyciętych pierwszą metodą nie trzeba wyszukiwać kąta obrotu na podstawie wzoru (choć to jest bardziej eleganckie) - metoda “brute” jest prostsza - obracamy o kąty -60 do +60 i patrzymy kiedy szerokość jest najmniejsza [nie dotyczy wszystkich czcionek, ale w większości przypadków wystarczy].