Individální projekty MPOA

Mikroprocesory s architekturou ARM

Uživatelské nástroje

Nástroje pro tento web


2018:raspberry-video

Zpracování obrazu na Raspberry Pi

Vypracoval: Tomáš Bravenec

Zadání

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.


Úvod

Raspberry Pi 3 Model B

Pro tvorbu projektu byla zvolen mikropočítač Raspberry Pi 3 Model B, díky tomu, že disponuje více jádry a umožňuje tak urychlit zpracování obrazu paralelizací, a také podporou operačních systémů, na kterých se dá aplikace vyvinout i s podporou grafického rozhraní.

Technické parametry

  • Quad Core 1.2GHz Broadcom BCM2837 64bit CPU
  • 1GB (900 MHz) LPDDR2 RAM
  • 100 Base Ethernet
  • 40-pin extended GPIO
  • 4 USB 2 ports
  • 4 Pole stereo output and composite video port
  • Další porty typu HDMI, 3,5mm jack atd…

Implementace

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:

  • libgtkmm-3.0-dev
  • libglibmm-2.4-dev
  • libomp-dev

Kompilace samotné aplikace, poté co jsou zajištěny všechny závislosti probíhá dvěma příkazy v terminálu:

  1. cmake .
  2. make

Nebo jejich kombinací:

  • cmake . && make

Grafické rozhraní

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); }
};

Vícejádrové zpracování dat

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. Při inicializaci aplikace je zjištěn dostupný počet jader, a tento počet je nastaven jako výchozí pomocí kódu (Pomocí Glade je výchozí počet jader nastaven na 1, proto není potřeba else část podmínky):

int coreCount = std::thread::hardware_concurrency();
if(coreCount > 0)
{
    cores->set_range(1, coreCount);
    cores->set_value(coreCount);
}

Prahování

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:

Vstupní obraz Práh = 0.5

Detekce hran

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.

Filtr Sobel

Detekce hran pomocí filtru sobel je tvořena výpočtem dvou gradientů pomocí 2D konvoluce filtrů s obrazem v odstínech šedi, každý z nich vyjadřuje sílu hrany v horizontálním nebo vertikálním směru. Pro získání jednoho obrazu z těchto gradientů, se využívá odmocniny ze součtu druhých mocnin obou gradientů, a to pro každý pixel. Jelikož je ale odmocnina výpočetně náročná, lze tento výpočet nahradit přibližnou aproximací pomocí součtu absolutních hodnot gradientů. Takto zkombinované horizontální a vertikální hrany poté stačí jen prohnat prahováním pro dosžení výsledku.

Detektor hran - Canny

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.

Srovnání fitru Sobel a Cannyho detektoru

Při porovnání obou přístupů, si lze všimnout, silnějších čar, které nebyly utlumeny ani při použití vysokého prahu, na rozdíl od toho, při použití Cannyho detektoru a velmi nízkého prahu jsou v obraze lépe viditelné hrany, a výstup vypadá celkově lépe, ale za cenu náročnějšího zpracování.

Vstupní obraz Filtr Sobel, Práh = 1.0 Cannyho detektor, Práh = 0.1

K-Means Clustering

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 dokola v dalších iteracích dokud se body budou v daném trojrozměrném poli posouvat (barva všech bodů nebude stejná, jako v předchozí iteraci).

Kód provádějící jednu iteraci je níže. Z kódu je opět vidět nastavení OpenMP, následované opět for cykly procházející každý řádek a sloupec, tentokrát ovšem obraz není v odstínech šedi, je proto nutné krokovat vždy po třech bytech (RGB), dále je potřeba při indexaci místo počtu sloupců využít step, cž je počet Bytů v jednom řádku, který je často o jeden až 3 Byty větší než počet sloupců * 3, jedná se o zarovnání řádků na násobek 32 bitů. Euklidovská vzdálenost je počítána bez odmocniny, která nemá vliv na srovnání vzdálenosti, a sníží se tak výpočetní náročnost. Vzdálenost pixelu k prvnímu bodu je vždy nejkratší, proto není potřeba žádná další kontrola. Pro následující body je již nutné vzdálenost zkontrolovat a pokud bude pixel blíž jinému než prvnímu bodu, změní se jeho přidělení. Pro přepočet bodů se poté přičtou jednotlivé barevné složky do bodu a zvedne se počítadlo pixelů které k tomuto bodu patří. Posledním krokem je nastavení pixelů výstupního obrazu na barvy bodů pro případ, že by již nedošlo k další iteraci.

void KMeans::iterate()
{
    omp_set_dynamic(0);
    omp_set_num_threads(threadCount);
 
    unsigned char *imageData = image.data;
    unsigned char *clusteredData = clustered.data;
 
#pragma omp parallel for
    for (int i = 0; i < image.rows; i++)
    {
        for (int j = 0; j < image.cols * 3; j += 3)
        {
            int shortestDist = 0, shortestCluster = 0;
            for (int k = 0; k < clusterCount; k++)
            {
                if (k == 0)
                {
                    shortestDist = (pow(imageData[i * image.step + j + 0] - newCluster[k].centroidRed, 2) +
                                    pow(imageData[i * image.step + j + 1] - newCluster[k].centroidGreen, 2) +
                                    pow(imageData[i * image.step + j + 2] - newCluster[k].centroidBlue, 2));
                    shortestCluster = k;
                }
                if ((pow(imageData[i * image.step + j + 0] - newCluster[k].centroidRed, 2) +
                     pow(imageData[i * image.step + j + 1] - newCluster[k].centroidGreen, 2) +
                     pow(imageData[i * image.step + j + 2] - newCluster[k].centroidBlue, 2)) < shortestDist)
                {
                    shortestDist = (pow(imageData[i * image.step + j + 0] - newCluster[k].centroidRed, 2) +
                                    pow(imageData[i * image.step + j + 1] - newCluster[k].centroidGreen, 2) +
                                    pow(imageData[i * image.step + j + 2] - newCluster[k].centroidBlue, 2));
                    shortestCluster = k;
                }
            }
 
            newCluster[shortestCluster].addRedValue(imageData[i * image.step + j + 0]);
            newCluster[shortestCluster].addGreenValue(imageData[i * image.step + j + 1]);
            newCluster[shortestCluster].addBlueValue(imageData[i * image.step + j + 2]);
            newCluster[shortestCluster].increaseCount();
 
            clusteredData[i * image.step + j + 0] = newCluster[shortestCluster].centroidRed;
            clusteredData[i * image.step + j + 1] = newCluster[shortestCluster].centroidGreen;
            clusteredData[i * image.step + j + 2] = newCluster[shortestCluster].centroidBlue;
        }
    }
}

Výstupem metody K-Means Clustering může být například:

Vstupní obraz K = 2 K = 4 K = 8

Video ukázka

Ve video ukázce je předveden výstup zpracování jednoho obrazu všemi implementovanými metodami. K Raspberry Pi je připojení realizováno pomocí VNC, následné nahrávání obrazu je prováděno z windows, aby nedocházelo k zatížení procesoru při výpočtech enkódováním nahrávaného obrazu.

V dolní části pod aplikací je otevřen terminál se spuštěnou konzolovou aplikací htop pro sledování využití jader a paměti. V druhém terminálu je spuštěna smyčka odečítající současný takt a teplotu jádra procesoru. Příkaz kterým je tohoto odečítání dosaženo je zde:

while true; do vcgencmd measure_clock arm; vcgencmd measure_temp; sleep 1; done

Zdrojový kód

Kompletní zdrojový kód aplikace je dostupný na GitLabu:

https://gitlab.com/tbravenec/image_processing_on_rpi

Nebo poslední verze git repozitáře před odevzdáním:

image_processing_on_rpi-master.zip

Závěr

Aplikace má implementovány všechny metody zpracování obrazu zmíněné v zadání, prahování a detekce hran pomocí filtru sobel, mají identický výsledek jako implementace pomocí OpenCV, cannyho detektor hran implementovaný manuálně vyžaduje pro podobný výsledek k OpenCV nižší práh, to může být dáno odlišnou implementací funkce pro potlačení nemaximálních pixelů a nebo odlišně navrženým prahováním s hysterezí. K-Means clustering podává mírně odlišné výsledky, což je ale očekávatelné, když jsou počáteční podmínky generovány náhodně.

Neúmyslným vedlejším efektem psaní aplikace pro Raspbian, je že je aplikace kompatibilní nejen s Raspberry Pi, ale i jakýmkoliv jiným procesorem a operačním systémem, pro který existuje implementace GTK, OpenMP a OpenCV.

Reference

2018/raspberry-video.txt · Poslední úprava: 2019/01/14 13:45 autor: Tomáš Bravenec