Toto je starší verze dokumentu!
Vypracoval: Tomáš Bravenec
Naprogramujte aplikaci pro segmentaci a zpracování obrazu určenou pro běh v operačním systému Raspbian na platformě Raspberry Pi 3 Model B. Mezi implementovanými metodami pro zpracování obrazu by mělo být prahování, detekce hran a K-Means clustering. Samotný výpočet by měl probíhat s využitím volitelného množství jader procesoru. Aplikace by též měla obsahovat grafické rozhraní pro volbu obrazu, nastavení pro zpracování a zobrazení vstupu a výstupu.
Aplikace byla vytvořena s využitím knihoven gtkmm, OpenCV 4 a OpenMP. Pro kompilaci je navíc potřeba mít nainstalovaný balíček Cmake. V případě použití knihoven OpenCV jiné verze než 4, bude potřeba změnit cesty u #include direktiv, jenž počítají s OpenCV 4. Pro snadnou kompilaci OpenCV, stačí následovat tento návod:Kompilace OpenCV
Pro úspěšnou kompilaci musí být v systému také nainstalovány balíčky:
Kompilace samotné aplikace, poté co jsou zajištěny všechny závislosti probíhá dvěma příkazy v terminálu:
Nebo jejich kombinací:
Pro tvorbu grafického rozhraní bylo využito GTK, přesněji knihovny gtkmm, která je objektovou verzí knihoven GTK určenou pro C++. Samotné rozhraní bylo vytvořeno pomocí programu Glade, který vygeneruje soubor se strukturu xml s popisem všech widgetů v okně.
Celé grafické rozhraní je vytvořeno jako třída, obsahující ukazatele na jednotlivé ovládací prvky, callback funkce reagující na stisky tlačítek a změnu nastavení, vstupní a výstupní obraz. Struktura třídy je v následujícím kódu:
class ApplicationGUI { private: Gtk::ComboBox *method; Gtk::ComboBox *implementation; Gtk::MenuItem *menuItemOpen; Gtk::MenuItem *menuItemSave; Gtk::MenuItem *menuItemClose; Gtk::MenuItem *menuItemStart; Gtk::MenuItem *menuItemClear; Gtk::MenuItem *menuItemAbout; Gtk::SpinButton *cores; Gtk::Image *inputImageBox; Gtk::Image *outputImageBox; Gtk::AboutDialog *aboutDialog; Glib::RefPtr<Gtk::Builder> builder; cv::Mat inputImage; cv::Mat outputImage; Glib::RefPtr<Gdk::Pixbuf> inputPixbuf; Glib::RefPtr<Gdk::Pixbuf> outputPixbuf; void on_method_changed(); void on_implementation_changed(); void on_gtkMenuItem_Open_activate(); void on_gtkMenuItem_Save_activate(); void on_gtkMenuItem_Start_activate(); void on_gtkMenuItem_Clear_activate(); void on_gtkMenuItem_About_activate(); Gtk::Main main; Gtk::Window *window; public: ApplicationGUI(); ~ApplicationGUI(); void run() { main.run(*window); } };
Zpracování obrazu pomocí procesoru je časově náročné, z důvodu obrovského množství dat, je výhodné využít všechna dostupná jádra procesoru pro rychlejší zpracování. Pro snadnou paralelizaci cyklů existuje knihovna OpenMP, která umožňuje zparalelizovat cykly pouhým přidáním direktivy preprocesoru před cyklus který chcme provést paralelně. Pokud je více cyklů vnořených, paralelizován bude pouze ten, který má před svým začátkem direktivu preprocesoru. Ve výchozím nastavení ovšem tento výpočet bude provádět na dynamicky voleném počtu vláken, proto pokud chceme pevně nastavit kolik vláken může aplikace pro průběh cyklu využít, musíme vypnout dynamickou volbu počtu jader a následně nastavit kolik vláken se může použít. Použití funkcí a direktivy OpenMP je v následující ukázce kódu, která má za úkol projít všechny pixely v obraze, zatímco všechny pixely v jednom řádku zpracovává jedno vlákno:
#include <omp.h> omp_set_dynamic(0); omp_set_num_threads(threads); #pragma omp parallel for for(int h = 0; h < image.rows; h++) { for(int w = 0; w < image.cols; w++) { /* Code */ } }
Za zmínku stojí také to, že v takto paralelizovaných cyklech není možné využívat proměnné definované jen pro jedno vlákno, jelikož by začalo docházek k souběhu (anglicky Race Condition) a aplikace by nepodávala správné výsledky. Knihovna OpenMP obsahuje mnohem více funkcí a direktiv, určených pro paralelizaci, pro danou aplikaci ovšem nebyly potřeba.
Funkce prahování patří mezi nejsnazší operace které lze s obrazem provést. Na její vstup je přiveden obraz v odstínech šedi a pokud jas pixelu přesáhne zvolený práh je nastaven na maximální hodnotu jasu a naopak, pokud je jas pixelu pod prahem, je nastaven na nulu. Z ukázky kódu je vidět mimo samotné prahování, i nastavení paralelního zpracování pomocí OpenMP.
cv::Mat threshold(cv::Mat image, unsigned char thresh, unsigned char max, int threads) { omp_set_dynamic(0); omp_set_num_threads(threads); unsigned char *data = image.data; #pragma omp parallel for for(int h = 0; h < image.rows; h++) { for(int w = 0; w < image.cols; w++) { if(data[h * image.cols + w] <= thresh) { data[h * image.cols + w] = 0; } else { data[h * image.cols + w] = max; } } } return image; }
Za zmínku stojí využití dvourozměrného indexování v jednorozměrném poli. Kdy se indexem řádku vynásobí počet sloupců, což posune index vždy na začátek daného řádku. Index sloupce se poté jen normálně přičte.
Výstupem metody prahování může být například:
/* ToDo */
Další z běžných metod pro zpracování obrazu je hranová detekce, pro kterou existuje více algoritmů, v aplikaci jsou implementovány dva, detekce pomocí filtru sobel a detektor hran canny.
Detekce hran pomocí filtru sobel je tvořena výpočtem
Cannyho detektor hran využívá z počátku stejný přístup jako detekce pomocí filtru sobel, avšak kromě kombinace gradientů, vypočítává i směr v jakém je hrana natočena. Následně projde celý obraz a podle toho v jakém směru je hrana, provede potlačení všech hodnot kromě nejvyšších vyskytující se kolmo ke směru hrany - zůží hranu až na tloušťku jednoho pixelu. Posledním krokem je prahování s hysterezí, to znamená, že detektor bere dvě prahové hodnoty, pokud má pixel jas pod dolní prahovou hodnotou, je jeho jas nastaven na nulu. Pokud má pixel jas vyšší než horní prah, nastaví se na maximální hodnotu. Následuje rozhodování o zbývajících pixelech tak, že pokud je okolo pixelu jiný pixel s hodnotou nad horním prahem, tak je pixel označen za hranu a nastaven na maximální hodnotu, pokud okolo pixelu není žádný který by překročil horní prah, je nastaven na nulu. Tento přístup odstraní většinu tzv. falešných hran.
K-Means clustering (česky shlukování do k středních hodnot, nebo pro obrazy barevná kvantizace), jedná se o algoritmus určený pro shlukování podobných dat. Na rozdíl od Prahování a detekce hran, které potřebují k získání výsledného stavu jen jeden průchod, K-Means clustering je iterační metoda, která skončí teprve poté co se od sebe výstup dvou po sobě jdoucích iterací neliší, nebo nedojde k zastavení po nastaveném množství iterací.
Pro snadné pochopení jak K-Means funguje v případě obrazů, si lze představit trojrozměrné pole se všemi stranami o délce 256 prvků (každá z os představuje jednu z barevných složek RGB). Pro začátek se náhodně zvolí K bodů (clusterů) z tohoto pole, načež se pro každý pixel vstupního obrazu vypočítá euklidovská vzdálenost ke každému z těchto bodů. Následně se zprůměrují hodnoty barevných složek které jsou nejblíže k jednomu z bodů a tento průměr se nastaví jako nový bod do další iterace. Toto se provede pro všech K bodů, a poté stále dokonal v dalších iteracích dokud se body budou v daném trojrozměrném poli posouvat.
Výstupem metody K-Means Clustering může být například:
/* ToDo */
Kompletní zdrojový kód aplikace je dostupný na GitLabu: