Widzenie maszynowe cz. 2
Rozpoznawanie i zmiana koloru
Głównym zadaniem jakie muszą realizować systemy widzenia maszynowego jest rozpoznawanie na obrazie pewnych rodzajów obiektów. W poprzednim wpisie pokazałem Ci jak wykorzystać Pythona do prostych operacji na obrazie. Tym razem spróbujemy zrealizować prostą klasyfikację pikseli. Będziemy chcieli na obrazie wykryć piksele odpowiadające skórze człowieka. Zrobimy to w bardzo prosty sposób – wykorzystując barwę obrazu.
Notanik z pełnym kodem, który będziemy realizować możesz ściągnąć korzystając z tego linku.
Import bibliotek i wczytanie bibliotek
Napiszemy prosty program w Pythonie. Ja przygotowując ten wpis używałem środowiska Jupyter Notebook, ale oczywiście można użyć dowolnego innego narzędzia. Pamiętaj, że w takiej sytuacji wyświetlenie rysunku wymaga wywołania dodatkowej funkcji plt.show() (pisałem o tym w poprzednim wpisie).
Zaczynamy od importu niezbędnych bibliotek:
import numpy as np
import matplotlib.pyplot as plt
import skimage.color
import skimage.morphology
import PIL
Niektóre z nich już znamy. Nowościami są dwa moduły z biblioteki scikit-image: skimage.color i skimage.morphology. Pierwszy z nich będzie nam potrzebny do realizacji wszystkich operacji związanych z barwą obrazu, a drugi do prostego wypełnienia małych luk w naszym algorytmie detekcji skóry.
Jeżeli korzystasz ze środowiska Jupyter Notebook to dobry pomysłem może być ustawienie domyślnego rozmiaru obrazu rysowanego przez pyplot:
plt.rcParams['figure.figsize'] = [9, 6]
Kolejnym krokiem jest wczytanie i wyświetlanie obrazu, na którym będziemy pracować:
im = np.array(PIL.Image.open('people.jpg'))
plt.imshow(im)
Analiza barwy
Kolorowy obraz cyfrowy jest reprezentowany przez trójwymiarową tablicę (w naszym przykładzie tablica ta jest obiektem klasy Ndarray z biblioteki NumPy). Wpisy w takich tablicach są adresowane za pomocą trzech liczb. Pierwsza z nich oznacza numer wiersza obrazu, druga – numer kolumny, a trzecia – numer kanału barwy.
Do opisu barwy potrzebne są trzy liczby. Standardowo liczby te oznaczają jasność trzech barw podstawowych tworzących dany piksel: czerwonej, zielonej i niebieskiej. Wartości tych składowych możemy łatwo wyświetlić posługując się składnią indeksowania list w Pythonie:
plt.figure(figsize=[16,12])
plt.subplot(1,3,1)
plt.imshow(im[:,:,0], cmap='gray')
plt.title('Red')
plt.subplot(1,3,2)
plt.imshow(im[:,:,1], cmap='gray')
plt.title('Green')
plt.subplot(1,3,3)
plt.imshow(im[:,:,2], cmap='gray')
plt.title('Blue')
W pierwszym wierszu tworzymy nowe okno na obrazki o zdefiniowanym rozmiarze. Ponieważ będziemy chcieli zaraz wyświetlić obok siebie trzy obrazki, to rozmiar całego okna (parametr figsize) definiujemy jako trochę większy niż wartość domyślna.
Funkcja plt.subplot(1,3,1) tworzy w oknie siatkę o jednym wierszu i trzech kolumnach (pierwsze dwa argumenty), a następnie definiuje, że obrazy mają się rysować w pierwszym polu tej siatki. Działanie funkcji imshow i title już znamy.
Zwróć uwagę na składnię im[:,:,0]. W naszym przykładzie im jest trójwymiarową tablicą. Jeżeli chcemy wyciągnąć dane o konkretnym pikselu to musimy podać w nawiasach kwadratowych trzy liczby definiujące jego indeks. Np. zapis im[100,200,0] oznacza, że chcemy uzyskać wartość kanału czerwonego (wartość 0 podana jako trzeci indeks) dla piksela o indeksie wiersza 100 i indeksie kolumny 200.
Zastąpienie konkretnych wartości w nawiasach kwadratowych dwukropkiem oznacza, że chcemy wybrać wszystkie możliwe wartości. Tak więc zapis im[:,:,0] wybierze wartości kanału czerwonego dla wszystkich wierszy i wszystkich kolumn obrazka. W efekcie otrzymamy dwuwymiarową tablicę, którą możemy narysować za pomocą funkcji imshow.
Efekt działania kodu będzie następujący:
Zwróć uwagę np. na teczkę trzymaną przez drugą osobę od lewej. Ma ona kolor pomarańczowy, a więc jasność piksela dla kanału czerwonego jest bardzo duża, a dla zielonego i niebieskiego są zdecydowanie mniejsze. Podobnie w przypadku teczki trzymanej przez trzecią osobę od lewej największą jasność ma kanał niebieski.
Na razie jednak ciężko znaleźć jakąś regułę, która pozwoliłaby nam zidentyfikować za pomocą koloru piksele należące do skóry znajdujących się na obrazku osób.
Model HSV
Definiowanie barwy poprzez podanie jasności trzech barw podstawowych nie jest jedyną możliwością. Innym sposobem jest zastosowanie tzw. modelu HSV. W modelu tym barwę opisuje się za pomocą trzech liczb:
- odcienia (ang. hue),
- nasycenia (ang. saturation),
- jasności (ang. value).
Obrazek poniżej pokazuje barwy różniące się między sobą każdym z tych parametrów:
Transformację między modelem RGB (red, green, blue) a HSV możemy przeprowadzić za pomocą modułu skimage.color:
#Transformacja do przestrzeni HSV
im_hsv = skimage.color.rgb2hsv(im)
#Wyświetlamy każdą składową HSV osobno
plt.figure(figsize=[16,12])
plt.subplot(1,3,1)
plt.imshow(im_hsv[:,:,0], cmap='jet')
plt.colorbar(orientation='horizontal') #Dodajemy jeszcze skalę kolorów
plt.title('Hue')
plt.subplot(1,3,2)
plt.imshow(im_hsv[:,:,1], cmap='jet')
plt.colorbar(orientation='horizontal')
plt.title('Saturation')
plt.subplot(1,3,3)
plt.imshow(im_hsv[:,:,2], cmap='jet')
plt.colorbar(orientation='horizontal')
plt.title('Value')
Kluczową rolę w powyższym kodzie odgrywa funkcja rgb2hsv, która zamienia składowe RGB na składowe HSV.
Po wykonaniu tego kodu powinny wyświetlić się poniższe obrazki. Tym razem wyświetliłem je za pomocą skali kolorów jet i dodałem pasek kolorów na dole (funkcja colorbar).
Tym razem wykrycie skóry wydaje się dość proste. Np. widać, że piksele skóry mają bardzo niską wartość parametru hue. Spróbujmy to wykorzystać.
Progowanie
Wybierzmy z całego obrazu tylko te piksele, których wartości hue, saturation i value znajdują się w określonych przedziałach. Możemy to zrobić następującym kodem:
# Na podstawie wykresów określamy granice progowania
hue_low = 0
hue_high = 0.2
sat_low = 0.1
sat_high = 0.7
val_low = 0.3
val_high = 0.9
# Progowanie
cond_hue = np.logical_and(im_hsv[:,:,0]<hue_high, im_hsv[:,:,1]>hue_low)
cond_sat = np.logical_and(im_hsv[:,:,1]<sat_high, im_hsv[:,:,1]>sat_low)
cond_val = np.logical_and(im_hsv[:,:,2]<val_high, im_hsv[:,:,2]>val_low)
# Łączenie warunków dla wszystkich trzech składowych
mask = np.logical_and(np.logical_and(cond_hue, cond_sat), cond_val)
# Wyświetlamy wyniki progowania
plt.figure(figsize=[16,12])
plt.subplot(1,3,1)
plt.imshow(cond_hue)
plt.title('Hue')
plt.subplot(1,3,2)
plt.imshow(cond_sat)
plt.title('Saturation')
plt.subplot(1,3,3)
plt.imshow(cond_val)
plt.title('Value')
plt.figure()
plt.imshow(mask)
plt.title('Mask')
Samą ideę progowania obrazu opisałem w poprzednim wpisie. Nowością jest tutaj funkcja logical_and, która wybiera nam te piksele, które spełniają jednocześnie dwa warunki logiczne. Po wykonaniu powyższego kodu zobaczymy następujące obrazy:
Ostatni obraz binarny pokazuje, które piksele zostały zaklasyfikowane jako ludzka skóra. Jak widać efekt nie jest idealny, ale jak na tak prostą metodę – całkiem przyzwoity.
Wypełnienie dziur
Moglibyśmy na tym poprzestać, ale chciałbym pokazać Ci jeszcze jak można w prosty sposób usunąć niewielkie luki na wyniku naszej detekcji. Posłużymy się do tego tzw. morfologicznym domknięciem. Operacje morfologiczne to bardzo proste w implementacji operacje na obrazach binarnych. Nie będziemy tutaj wnikać w ich szczegóły, ale pokażę Ci jak użyć zaimplementowanego w module skimage.morphology algorytmu morfologicznego zamknięcia:
# Przeprowadzamy morfologiczne zamknięcie
mask_closed = skimage.morphology.binary_closing(mask, np.ones((5,5)))
# Wyświetlamy wynik
plt.figure(figsize=[16,12])
plt.subplot(1,2,1)
plt.imshow(mask)
plt.title('Oryginalna maska')
plt.subplot(1,2,2)
plt.imshow(mask_closed)
plt.title('Domknięta maska')
Uzyskamy następujący efekt:
Zmiana barwy
Na koniec zobaczymy jak możemy wykonać operację na wybranych pikselach obrazu. Np. możemy zmienić odcień skóry osób na obrazku na bardziej zielony:
# Znajdujemy indeksy odpowiednich pikseli
ind = np.argwhere(mask_closed==1)
# Tworzymy kopię obrazu w przestrzeni HSV
im_hsv_cpy = im_hsv.copy()
# Modyfikujemy odcień wykrytych pikseli
im_hsv_cpy[ind[:,0], ind[:,1], 0] = im_hsv_cpy[ind[:,0], ind[:,1], 0] + 0.2
# Ponownie wracamy do przestrzeni RGB
im_rgb = skimage.color.hsv2rgb(im_hsv_cpy)
# Wyświetlamy obraz
plt.imshow(im_rgb)
Najpierw za pomocą funkcji argwhere tworzymy tablicę indeksów pikseli maski, które zaklasyfikowaliśmy jako skórę. Następnie tworzymy kopię obrazu i zwiększamy wartość składowej hue wybranych pikseli o 0.2. Zwróć uwagę na sposób, w jaki możemy użyć indeksowania, żeby przeprowadzić operację dodawania na całym zbiorze pikseli. Dzięki tej funkcjonalności Pythona i biblioteki NumPy nie musimy używać pętli, żeby wykonać pewną operację na części tablicy.
Efekt działania kodu będzie następujący:
Podsumowanie
Najważniejsze rzeczy, których się dowiedzieliśmy to:
- sposób indeksowania obrazów w Pythonie,
- definiowanie koloru za pomocą modelu HSV.
Sama metoda detekcji fragmentów obrazu tylko za pomocą barwy sprawdza się dobrze tylko w nielicznych przypadkach gdy szukamy obiektu mocno kontrastującego z resztą otoczenia. Niemniej jednak nasze proste podejście to przykład tzw. segmentacji obrazu, której celem jest podzielenie obrazu na klasy. Nowoczesne metody segmentacji wykorzystują zaawansowane modele matematyczne takie jak np. sztuczne sieci neuronowe, ale to temat na dalszą część tego cyklu.