Widzenie maszynowe cz. 3
Detekcja krawędzi
W poprzednich częściach pokazałem Ci jak wykorzystać Pythona do przetwarzania obrazów cyfrowych oraz w jaki sposób analizować kolor tych obrazów. Kolejnym krokiem w naszej nauce widzenia maszynowego będzie rozpoznawanie obiektów na podstawie ich cech geometrycznych. Możemy np. wykrywać linie proste, albo obecne w obrazie okręgi. Ważnym krokiem w algorytmach przeprowadzających tego typu detekcję jest wykrywanie krawędzi. W tej części cyklu nauczymy się jak ten krok przeprowadzić.
Notatnik z kodem do tego artykułu możesz ściągnąć korzystając z tego linku.
Detekcja krawędzi za pomocą filtrów splotowych
Krawędzie na obrazie cyfrowym są miejscami, w których jasność sąsiednich pikseli mocno się od siebie różni. Jeżeli chcemy określić czy jakiś piksel znajduje się w pobliżu krawędzi to możemy odjąć od siebie jasność jego lewego i prawego sąsiada. Jeżeli różnica ta będzie dużo większa od zera to stwierdzimy, że w tym miejscu znajduje się pionowa krawędź obrazu. Odejmując od siebie jasności sąsiadów z góry i z dołu możemy wykryć krawędź poziomą. Ilustruje to obrazek poniżej.
Operacja, która zrealizuje nam taki proces detekcji na całym obrazie jest filtracja splotowa, którą stosowaliśmy już w pierwszej części cyklu. Musimy tylko właściwie dobrać współczynniki maski filtru. Poniżej napiszemy odpowiedni kod w Pythonie.
Na początek importujemy wszystkie niezbędne biblioteki:
import numpy as np
import PIL
import matplotlib.pyplot as plt
import scipy.signal
import skimage.feature
#Definiujemy domyślny rozmiar obrazka
plt.rcParams['figure.figsize'] = [12, 8]
Większość biblioteki już znasz. Moduł scipy.signal służy do realizacji filtrów splotowych. Modułu skimage.feature użyjemy później do tzw. detekcji Cannego, która jest bardziej zaawansowaną metodą detekcji krawędzi.
Teraz możemy wczytać obrazek i przekonwertować go do skali szarości:
#Wczytanie obrazu
im = np.array(PIL.Image.open('edges.jpg'))
#Konwersja do skali szarości
im = np.mean(im/255,axis=2)
#Wyświetlenie
plt.imshow(im, cmap='gray')
Tym razem użyjemy następującego obrazu:
Kolejny krok to zdefiniowanie masek dwóch filtrów. Jednego do detekcji krawędzi pionowych i drugiego do detekcji krawędzi poziomych:
mask_vertical = np.array([[-1,0,1],
[-1,0,1],
[-1,0,1]])
mask_horizontal = np.array([[-1,-1,-1],
[ 0, 0, 0],
[ 1, 1, 1]])
Po zdefiniowaniu masek możemy przeprowadzić filtrację:
edge_vertical = scipy.signal.convolve2d(im, mask_vertical) edge_horizontal = scipy.signal.convolve2d(im, mask_horizontal)
i wyświetlić jej wynik:
plt.subplot(1,2,1)
plt.imshow(edge_horizontal, clim=[-1,1], cmap='jet')
plt.colorbar(orientation='horizontal')
plt.title('Horizontal edges')
plt.subplot(1,2,2)
plt.imshow(edge_vertical, clim=[-1,1], cmap='jet')
plt.colorbar(orientation='horizontal')
plt.title('Vertical edges')
Otrzymamy następujący rezultat:
Zwróć uwagę na skalę kolorów. Miejsca, w których nie ma krawędzi przyjmują na przefiltrowanych obrazach wartość bliską 0, a te gdzie krawędzi występują mogą być zarówno ujemne jak i dodatnie. Zależy to od tego z której strony krawędzi obraz jest ciemniejszy.
Na koniec trzeba połączyć ze sobą krawędzie poziome i pionowe. Można to zrobić np. obliczając pierwiastek kwadratowy z sumy kwadratów obu przefiltrowanych obrazów:
#Scalanie krawędzi
edge = np.sqrt(edge_vertical**2 + edge_horizontal**2)
#Wyświetlenie wyniku
plt.imshow(edge, clim=[0, 1])
plt.colorbar(orientation='horizontal')
plt.title('Edges')
Teraz nasz obraz przyjmuje tylko wartości dodatnie. Są one tym większe im silniejszy jest kontrast krawędzi w danym punkcie. Docelowo chcemy jednak mieć obraz binarny tzn. taki, którego piksele przyjmują tylko jedną z dwóch wartości: 0 – jeżeli w danym punkcie nie ma krawędzi i 1 – jeżeli jest. Możemy taki obraz uzyskać za pomocą progowania:
edge_det = edge > 0.2
plt.imshow(edge_det)
Detektor Canny'ego
Nasza detekcja działa, ale ma kilka wad. Po pierwsze wykrywa dużo „śmieci”, czyli dużo szczegółów, które wcale nie są krawędziami. Poza tym krawędzie są bardzo grube. Oba te problemy możemy rozwiązać stosując tzw. detektor Canny’ego.
Detektor Canny’ego to popularny algorytm detekcji krawędzi. Składa się z następujących kroków:
- Wstępne rozmycie obrazu. Realizuje się je za pomocą filtru splotowego o masce gaussowskiej. Rozmycie to usuwa szum i niepotrzebne szczegóły z obrazu.
- Obliczenie natężenia i kierunku krawędzi. Natężenie oblicza się podobnie jak w naszym przykładzie, a kierunki wyznacza porównując ze sobą wartości pikseli dla obrazów po filtracji maską do detekcji krawędzi pionowych i poziomych.
- Zmniejszanie grubości krawędzi poprzez usuwanie wyniku detekcji punktów znajdujących się w kierunku prostopadłym do krawędzi zaczynając od jej maksymalnych wartości.
- Śledzenie z podwójnym progowaniem. Ustala się dwa progi: wysoki i niski. Gdy napotkamy w obrazie krawędź o natężenie większym niż próg wysoki – zaczynamy ją śledzić i śledzimy tak długo aż natężenie spadnie poniżej progu niskiego.
- Wszystkie punkty, które śledziliśmy są uznawane za krawędź (wartość 1), a pozostałe za jej brak (wartość 0).
Algorytm może się wydawać skomplikowany, ale jego implementacja nie jest taka trudna. Poza tym nie musimy jej przygotowywać sami, bo istnieją jej gotowe wersje w różnych bibliotekach. My skorzystamy z modułu Python skimage.feature:
edge_cn = skimage.feature.canny(im) plt.imshow(edge_cn)
Detektor Canny’ego można stroić ustalając różne szerokości filtu gaussowskiego realizującego wstępne rozmycie i różne poziomy progów przy śledzeniu krawędzi. Poniższy kod pokazuje jak można je zmieniać korzystając w module skimage.feature:
#Zwiększmy ochylenie standardowe filtru do 3 px
edge_cn_1 = skimage.feature.canny(im, sigma=3.0)
plt.imshow(edge_cn_1)
plt.title('Większe rozmycie')
Algorytm nie reaguje teraz na drobne szczegóły, ale straciliśmy też niektóre krawędzie. Można to poprawić zmieniając wartość niskiego progu:
edge_cn_2 = skimage.feature.canny(im, sigma=3.0, low_threshold = 0.02)
plt.imshow(edge_cn_2)
plt.title('Niższy próg')
Teraz widać trochę lepiej słabe krawędzie. Dobranie optymalnych parametrów do konkretnego problemu wymaga trochę eksperymentów.
Podsumowanie
Mając do dyspozycji algorytmy detekcji krawędzi możemy przejść do kolejnego etapu jakim jest detekcja prostych obiektów geometrycznych takich jak linie proste czy okręgi. Zajmiemy się tym w następnej części cyklu.
Do zobaczenia!