PiRO — Projekt 2 (OCR)


Wstęp

Celem projektu jest stworzenie programu analizującego zdjęcie przedstawiające ręcznie spisaną listę studentów i (docelowo) jak najlepsze rozpoznanie numeru indeksu każdej pozycji.

Każda pozycja na liście obecności składa się z 3 lub 4 elementów: ([lp.], imię, nazwisko, numer indeksu). Liczba porządkowa może ale nie musi występować w danej liście obecności. Na liście obecności może występować pierwsza linijka z tytułem (na przykład napis: “Lista obecności”).

Przykładowe zdjęcia list można ściągnąć: tutaj.

Zadania

Program który należy napisać powinien:

  • wczytać kolorowe zdjęcie kartki (przykładowe pliki podane w linku powyżej)
  • zapisać dwa pliki z wynikami:

    1. tablica z ponumerowanymi linijkami tekstu oraz zaznaczonymi wyrazami, oraz
    2. lista rozpoznanych numerów indeksów.

Tak więc zasadniczo zadanie ma 2 podzadania które powinien zrobić:

  1. Wykrywać linijki i wyrazy w obrazie wejściowym.
  2. Rozpoznawać numery indeksów.

Pierwsze podzadanie wydaje się naturalnym etapem pośrednim do zadania drugiego.

Zaliczenie

Projekt wykonywany jest w grupach max 4 osobowych. Mniej liczne grupy będą lekko premiowane aby “złagodzić” dysproporcje.

Do zaliczenia projektu nie jest potrzebne idealne rozwiązanie — dopuszcza się błędy. W zasadzie trzeba się liczyć, że prawdopodobnie żadne rozwiązanie nie będzie poprawnie wykrywało wszystkich wyrazów na wszystkich zdjęciach, nie wspominając już o poprawnym rozpoznaniu cyfr z numerów indeksów. Porównywanie wyników będzie działało elastycznie — będzie mierzyło podobieństwo do poprawnych wartości.

Rozwiązanie zadania nie może korzystać z gotowych narzędzi/bibliotek OCR.

Formalne wymagania które muszą spełniać przysyłane projekty opisane są na końcu. Nie przestrzeganie krytycznych wymagań przez co niemożliwe będzie automatyczne sprawdzenie projektu na komputerze w laboratorium pod systemem Ubuntu (uruchomienie + porównanie wyników) spowoduje nie zaliczenie projektu. Tym razem nie będę ręcznie poprawiał kłopotliwych plików!

Opis

Proces przetwarzania zdjęcia można dość podzielić na następujące po sobie etapy przetwarzania/analizy obrazu.
Przykładowa lista etapów na której proponuję się wzorować (aczkolwiek można zrobić to inaczej):

  1. wykrycie kartki,
  2. skorygowanie perspektywy (przycięcie obrazu do samej kartki) aby kartka była prostokątem i linie tekstu były w miarę możliwości poziome,
  3. usunięcie pionowych/poziomych linii z tła gdy kartka papieru jest w kratkę,
  4. znajdowanie wierszy tekstu,
  5. znajdowanie wyrazów w wierszach,
  6. wykrycie pojedynczych cyfr,
  7. rozpoznanie cyfr.

Implementując etapy należy zwrócić uwagę na pewne aspekty wybranej metody i zastanowić się co robić w różnych możliwych sytuacjach. Implementacja musi być odporna na różne dane wejściowe, gdzie np. na niektórych obrazach będzie coś widocznego a na innych nie, raz to samo będzie wyglądało w jeden sposób, a raz w inny. Kod nie powinien zakładać, że zawsze coś uda się wykryć przy wybranych parametrach metody i kolejne etapy powinny mieć “plan awaryjny” mówiący co można alternatywnie zrobić w danej sytuacji (najgorszym z możliwych jest oczywiście “wywalenie się” programu).

Poniżej wskazówki jak można wykonać poszczególne etapy i na co warto zwrócić uwagę. Lista pytań i sugerowanych metod jest oczywiście otwarta i niekoniecznie trzeba/warto z nich korzystać — może inny pomysł okaże się znacznie lepszy?

Wykrycie kartki

Celem jest wykrycie 4 narożników aby później można było wyciąć interesujący nas fragment obrazka.
Warto zastanowić się:

  • czy zawsze są widoczne narożnik?
  • czy jasność kartki jest taka sama czy może widzimy gradient?
  • czy brzegi kartki są widoczne?
  • jakie cechy ma tło?

Typowe operacje wykonywane w tym etapie to: filtrowanie/odszumianie, progowanie obrazu (potencjalnie adaptacyjne), operacje morfologiczne, wykrycie i uproszczenie konturów, odfiltrowanie konturów i wybranie tego który dotyczy kartki, wyznaczenie współrzędnych narożników.

Skorygowanie perspektywy

Gdy mamy wyznaczone narożniki kartki wycięci odpowiedniego fragmentu jest już dość proste. Można użyć np. funkcji “`cv2.warpPerspective“`.

Warto zastanowić się:

  • co robić gdy wykryto mniej niż 4 narożniki kartki?
  • co robić gdy wykryto więcej niż 4 narożniki?
  • jaki rozmiar powinna mieć docelowa kartka (wynikowy obraz)?

Typowe operacje: obliczenie transformacji perspektywicznej i przekształcenie obrazu wg. niej.

Usunięcie kratki

Etap ten ma na celu ułatwienie działania w kolejnych etapach (np. rozdzielenie linijek tekstu/wyrazów od siebie, itd.)
Warto zastanowić się:

  • czy kratka jest zawsze widoczna? czy można to automatycznie wykryć?
  • jakie cechy ma kratka a jakie tekst? (kolor, grubość, itd.)
  • czym różni się kratka od napisów?
  • jak uniknąć zbyt agresywnego usuwania punktów?

Typowe operacje: morfologiczne (może warto rozważyć różne kernele), arytmetyczne (np. odejmowanie itd.)

Znajdowanie wierszy

Wynik tego etapu zawęża nam pole pracy dzięki czemu łatwiej (i szybciej) będzie nam szło wykrycie wyrazów/znaków.
W tym etapie jak najbardziej warto skorzystać z wiedzy, że linie tekstu powinny być już (prawie) poziome.
Warto zastanowić się:

  • czy wiersze są w równych czy różnych odstępach?
  • czym charakteryzują się przerwy między wierszami?
  • co robić w sytuacji “zamaszystego” charakteru pisma?

Typowe operacje: sumowanie wartości pikseli, wykrywanie maksimów/minimów, filtrowanie/wygładzanie, klastrowanie.

Znajdowanie wyrazów

Ten etap można wykonywać dla każdego wiersza niezależnie. Przetwarzanie może być podobne do wykrywania wierszy.
Warto zastanowić się:

  • jak wykorzystać wiedzę, że mamy ściśle określone jak powinien być zbudowany pojedynczy wpis?
  • jakie mamy długości wyrazów?
  • jakie mamy długości przerw?

Typowe operacje: sumowanie wartości pikseli, wykrywanie maksimów/minimów, filtrowanie/wygładzanie, klastrowanie.

Wykrycie pojedynczych cyfr

Tutaj podział na pojedyncze znaki chcemy wykonać już dokładniej aby móc odpowiedni fragment poddać klasyfikacji. Z reguły chcemy tutaj zaznaczyć każdą cyfrę prostokątem w którym się mieści tak aby każdy wyznaczony fragment w kolejnym etapie móc klasyfikować osobno.
Warto zastanowić się:

  • czy poprzednie etapy nie spowodowały ubytków w znakach?
  • a może zbyt mocno je pogrubiły?
  • czym charakteryzuje się pojedynczy znak/cyfra?

Typowe operacje: morfologiczne, wykrywanie konturów.

Rozpoznanie cyfr

W tym etapie najlepiej użyć jakiegoś klasyfikatora (niekoniecznie musi to być sieć neuronowa).
Najczęściej każdą cyfrę wykrytą w poprzednim etapie klasyfikuje się niezależnie ale można rozważyć np. wariant w którym ten i poprzedni etap są ściśle ze sobą połączone.
Do nauczenia klasyfikatora można wykorzystać dostępne zbiory danych np. MNIST lub jakiegoś jego następcę.
Warto zastanowić się:

  • jaki zbiór danych będzie odpowiedni?
  • jaki użyć klasyfikator?
  • w jakim formacie (np. wielkość) muszą być przygotowane dane z poprzedniego etapu?

Typowe operacje: uczenie maszynowe, pre/post-processing.

Wymagania formalne

Analogiczne jak w projekcie pierwszym

Proszę przygotować dwa (lub ewentualnie trzy) pliki o nazwach zgodnych ze wzorcem:

  • piro2_<indeks1>[_<indeks2>[_<indeks3>[_<indeks4>]]].<rozszerzenie>,
    (np. piro2_12345_54321.<rozszerzenie>)
  • gdzie zamiast <indeksN> należy wstawić numer indeksu odpowiedniego autora, a <rozszerzenie> oznacza odpowiednie rozszerzenie podane poniżej.

Uwaga! Pliki nazwane inaczej nie będą poprawnie przetworzone przez skrypt!

Wymagane pliki:

  1. rozszerzenie src.tar.bz2 — archiwum ze źródłami projektu wraz z jednym plikiem Dockerfile
  2. rozszerzenie pdf — plik pdf ze sprawozdaniem
  3. [opcjonalny] rozszerzenie img.tar.bz2 — skompresowany obraz dockera zbudowany i przetestowany przez Państwa.
    Obraz powinien być nazwany tak jak plik ale bez rozszerzenia (np. piro2_12345_54321)
    Jeśli nie będzie tego pliku to skrypt sam stworzy obraz używając polecenia podobnego do:
    docker build -t piro2_12345_54321 - < piro2_12345_54321.src.tar.bz2
    Powyższe polecenie musi zakończyć się sukcesem na jednej z maszyn w laboratorium 1.6.21, inaczej skrypt założy, że dostarczony projekt jest niepoprawny (w szczególności proszę zwrócić uwagę, że polecenie zakłada, że plik Dockerfile nie jest w żadnym podkatalogu stworzonego archiwum)

Państwa programy będą uruchamiane poleceniem podobnym do:
docker run --network none --mount type=bind,source=moje/dane,target=/tmp/obrazki,readonly piro2_12345_54321 /tmp/obrazki 123 /tmp/wyniki

Wymagania dotyczące obrazu dockera:

  • stworzony obraz musi się dać uruchamiać wg. schematu jak podano powyżej
  • należy dodać etykiety studentK (dla K=1,2,…) zawierających imię oraz nazwisko pierwszego, drugiego, itd. autora, w pliku Dockerfile należy dodać np.:
    LABEL student1="Adam Kowalski" student2="Monika Nowak"

Dostarczenie plików:

  1. pliki należy wgrać na chmurę PP
  2. następnie mi je udostępnić abym mógł je odczytać (proszę nie generować linków! – patrz uwaga niżej)
  3. wysłać mi maila z tytułem zawierającym indeksy autorów, np.: `[PiRO] proj2_12345_54321

Instrukcja obsługi chmury PP: https://instrukcje.put.poznan.pl/chmura-politechniki-poznanskiej/
UWAGA 1 — proszę przeczytać sekcję Udostępnianie, a w szczególności zwrócić uwagę na wzór adresu który należy podać:
student dla pracowników – imie.nazwisko@put.poznan.pl@chmura.put.poznan.pl
UWAGA 2 — w przypadku udostępniania folderu a nie pojedynczych plików proszę aby folder nazywał się analogicznie jak pliki (np. piro2_12345_54321)

  • Termin: koniec czerwca (30.06.2021)

Przysłany program będzie uruchamiany z 3 argumentami:

  1. ścieżka do katalogu ze zdjęciami które należy przetworzyć (input_path),
  2. liczą N oznaczającą liczbę obrazków do wczytania,
  3. ścieżką do katalogu w którym należy zapisać pliki z wynikami (output_path).

W katalogu input_path będą znajdowały się pliki o nazwach k.png, gdzie k to liczba z zakresu 0...N-1 (np. 0.png, 13.png).

W katalogu output_path dla każdego obrazu wejściowego należy zapisać dwa pliki z wynikami (format opisany poniżej):

  • k-wyrazy.png z zaznaczonymi wyrazami, oraz
  • k-indeksy.txt z wykrytymi numerami indeksów.

Zaznaczenie wyrazów

W wynikowym pliku k-wyrazy.png należy zaznaczyć pojedyncze wyrazy w zdjęciu oraz ponumerować linijki tekstu.
Obraz ten powinien być jednokanałowy (w odcieniach szarości) o wymiarach identycznych jak wczytany obrazek z zaznaczonymi wyrazami: obrazek ma zawierać wartości 0 dla punktów nie należących do żadnego wyrazu, 1 dla wyrazów z pierwszego wiersza, 2 – dla drugiego wiersza itd.
Pojedyncze zaznaczenie powinno zawierać się wewnątrz wyrazu i ciągnąć się od jego lewej strony do prawej. Jeden wyraz to jeden zbiór sąsiadujących ze sobą pikseli.

Przez “zaznaczenie” rozumie się zgrubne wskazanie punktów (niekoniecznie wszystkich!) należących do danego wyrazu. Nie chodzi tutaj o dokładne zaznaczenie wszystkich pikseli należących do wyrazu ale o zaznaczenie punktów wzdłuż niego, najlepiej od samego początku do samego końca (ale znowu – nie chodzi o wskazanie z dokładnością do 1 piksela).

Poglądowy rysunek jak mogą wyglądać zaznaczone wyrazy (w trzech pierwszych linijkach) naniesione na oryginalny obraz można zobaczyć tutaj.

UWAGA: jeśli wykrycie wyrazów odbywa się już po pewnych przekształceniach oryginalnego obrazu (np. obrót, skalowanie, przycinanie, itp.) to po zaznaczeniu wyrazu w takiej nowej przestrzeni trzeba będzie wrócić do oryginalnej (odwrócić wszystkie przekształcenia). Inaczej: sprawdzanie będzie polegało na automatycznym porównaniu wygenerowanego obrazka z oryginalnym obrazem na którym ręcznie zaznaczone będą wyrazy.

Wykryte numery indeksów

W pliku k-indeksy.txt należy zapisać odczytane numery indeksów (bez żadnych dodatkowych znaków), każdy numer w osobnym wierszu (w idealnym przypadku liczba wierszy w pliku powinna być równa liczbie wierszy widocznych na obrazie).
Odpowiedzi będą sprawdzane elastycznie, tzn. z uwzględnieniem możliwych pomyłek (np. brakujących/nadmiarowych cyfr, lub źle rozpoznanych fragmentów).