Wykrywanie kolorów w OpenCV. Stwórz samemu „płaszcz niewidkę”!

Wykrywanie kolorów

– Tatooooo! Kupimy dla mnie i Oti taki płaszcz jak ma Harry Potter? – zapytała Jagódka  z proszącym wzrokiem.

– A po co Wam takie coś?

– Bo mogłybyśmy się skradać i nikt by nas nie widział.

– I co, myślicie, że nie zauważylibyśmy z mamą jak znikają słodycze?

– Prosimyyy….

– Nie kupimy takiego płaszcza, bo w sklepach ich nie sprzedają. Przykro mi moje Księżniczki.

Zobaczyłem smutek w oczach dziewczynek.

– Hmm… ale można coś podobnego zrobić samemu – pomyślałem i wziąłem się od razu do roboty.

Już od bardzo dawna motyw znikania występował w bajkach i wiele osób o wręcz o tym marzy. Chociażby czapka niewidka pojawiała się już w japońskiej mitologii.

W tym poście udowodnię Ci, że korzystając z OpenCV możesz stworzyć własne narzędzie do znikania przedmiotów z obrazu kamerki internetowej. Obiecuję, że nie musisz się znać na magii! Wystarczy proste wykrywanie kolorów i podmiana na obraz w tle. A jeśli nie wiesz, czym jest OpenCV lub jak łatwo przygotować real-time detekcję twarzy zapraszam do TEGO postu.

Tutaj możesz zobaczyć efekt tego projektu:

Zacznijmy od detekcji koloru!

Wykrywanie kolorów na obrazku

Na początku wybierzmy odpowiedni kolor. Ja wykorzystam ulubiony czerwony szlafrok mojej żony. Zatem będę chciał w strumieniu wideo wyłapywać czerwone kolory.

Nie jestem żadnym ekspertem od Computer Vision. Po krótkiej konsultacji dowiedziałem się, że nie powinno wykorzystywać się obrazu zapisanego jako RGB (Red Gren Blue), jeśli naszym celem jest wykrywanie kolorów.

Dlaczego? Ponieważ nie zadziała skutecznie, bo wartości RGB są bardzo wrażliwe na oświetlenie. Zatem jak gdzieś są zagniecenia, czy inaczej pada światło i pojawią się cienie, to prosto nie wybierzemy odpowiednio zakresu dla naszego wybranego koloru. Ale na ratunek przychodzi HSV (ang. hue, saturation, value), na który możemy nasz obraz przekonwertować.

Co to HSV?

Jest to alternatywna przestrzeń kolorów dla RGB. Zaprojektowana została w latach 70 ubiegłego wieku przez badaczy grafiki komputerowej. Powstała w celu dokładniejszego odwzorowania sposobu, w jaki ludzki wzrok postrzega kolory. W modelu HSV kolory każdego odcienia są ułożone w formie promieniowego wycinka wokół centralnej osi neutralnych kolorów. Układają się od czarnego u dołu do białego u góry. Mniej więcej wygląda to tak:

Tak jak widać powyżej przestrzeń opisana jest za pomocą trzech wartości:

  • Barwa (hue): ten kanał to informacja o kolorze. Odcień można wyobrazić sobie pod kątem, w którym 0 stopni odpowiada kolorowi czerwonemu, 120 stopni odpowiada kolorowi zielonemu, a 240 stopni odpowiada kolorowi niebieskiemu.
  • Nasycenie (saturation): kanał ten koduje intensywność koloru, czyli im większa wartość, tym bardziej wyrazisty kolor.
  • Wartość (value): odpowiada za jasność koloru, czyli im mniejsza wartość, tym ciemniejszy odcień.

Wykrywanie kolorów w praktyce

Weźmy dla przykładu czerwonego kapturka, który ma płaszcz w takim samym kolorze, jaki będę wykrywał później w kamerce internetowej.

Załadujmy pakiety i wczytajmy obrazek:

import cv2
import time
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread('tmp.png')
plt.imshow(img)
plt.show()
czerwony kapturek open cv

A to niespodzianka. Czerwony kapturek wcale nie jest czerwony. Wygląda jakby kolor czerwony zamienił się w niebieski. W rzeczywistości OpenCV domyślnie odczytuje obrazy w formacie BGR.

To teraz to poprawmy:

img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)
plt.show()
opencv read file

Zamieńmy obraz na hsv.

hsv_img = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)
plt.imshow(hsv_img)
plt.show()
czerwony kapturek hsv

Dla mnie powstały obrazek nie wygląda na bardziej czytelny. Ale najważniejsze, że dla maszyny już tak :). I teraz wyszukajmy zakresu czerwonego. Możemy skopiować piksel i poszukać jego zakresu, albo … napisać pętelkę, by sprawdzić, jak wyglądają kolory w poszczególnych zakresach :).

Wykorzystamy do tego dwie funkcje z OpenCV:

  1. inRange – służy do stworzenia macierzy jak nasz obrazek, gdzie oznaczono jedynkami piksele, które znalazły się w podanym zakresie.
  2. bitwise_and – w tym przypadku wykorzystamy do wycięcia obszaru (maski z kroku 1) z obrazka (przykład tutaj).

Zobacz, jak prosto można to zrobić. Ustawiłem zakres co 10, aby wyciąć kolory:

for i in range(18):
    print(f'Zakres od {10*i} do {10*i+10}')
    color_from = (10*i,0,0)
    color_to = (10*i+10,255,255)
    mask = cv2.inRange(hsv_img, color_from, color_to)
    result = cv2.bitwise_and(img, img, mask=mask)
    
    f = plt.figure(figsize=(15,5))
    ax = f.add_subplot(121)
    ax2 = f.add_subplot(122)
    ax.imshow(mask, cmap="gray")
    ax2.imshow(result)
    plt.show()

Poniżej kilka początkowych przykładów. Możesz zobaczyć, jak HUE z zakresu 0-10 ładnie wycina kolor czerwony, 11-20 kolor pomarańczowy i brązowy.

opencv hue

Zakres 30-40 wybiera kolory jasnozielone, a 40-50 ciemnozielone.

hue opencv 30-50

I nagle…niespodzianka!! Dla zakresu 170-180 również wycięte są super kolory czerwone jako dopełnienie tych pikseli, których brakowało na początkowych obrazkach! Spójrz:

zakres 170-180 dla HSV

Jak widać, w tym przypadku warto połączyć dwa zakresy danych. Dodatkowo „wzrokowo” (czyli patrząc na wynik) dostroiłem jeszcze kolejne parametry (S oraz V). Następnie obie maski można połączyć i oto wynik:

light_red_part1 = (0,120,80)
dark_red_part1 = (10, 255, 255)

mask1 = cv2.inRange(hsv_img, light_red_part1, dark_red_part1)

light_red_part2 = (170,120,80)
dark_red_part2 = (180, 255, 230)

mask2 = cv2.inRange(hsv_img, light_red_part2, dark_red_part2)

mask = mask1+mask2
result = cv2.bitwise_and(img, img, mask=mask)

f = plt.figure(figsize=(15,5))
ax = f.add_subplot(121)
ax2 = f.add_subplot(122)

ax.imshow(mask, cmap="gray")
ax2.imshow(result);

opencv laczenie masek
morphologyEx na ratunek!

Idealnie jeszcze nie jest. Na szczęście można wykorzystać wbudowane funkcje morph! W dokumentacji znajdziesz szczegóły i wszystkie zastosowania! W naszym przypadku wykorzystamy:

  • MORPH_OPEN – jeden ze sposobów usuwania szumów,
  • MORPH_DILATE- troszkę rozszerza nasz obiekt, aby wyłapać szerszy kontekst,
  • MORPH_CLOSE- przydatne do zamykania małych otworów wewnątrz obiektów (małych czarnych punktów na obiekcie).

I spójrzcie na wynik:

mask_morph = cv2.morphologyEx(mask, cv2.MORPH_OPEN, np.ones((3,3),np.uint8))
mask_morph = cv2.morphologyEx(mask_morph, cv2.MORPH_DILATE, np.ones((3,3),np.uint8))
mask_morph = cv2.morphologyEx(mask_morph, cv2.MORPH_CLOSE, np.ones((5,5),np.uint8))

result_morph = cv2.bitwise_and(img, img, mask=mask_morph)

f = plt.figure(figsize=(15,5))
ax = f.add_subplot(121)
ax2 = f.add_subplot(122)
ax.imshow(mask_morph, cmap="gray")
ax2.imshow(result_morph);
czyszczenie

I jeszcze jedno. Jeśli masz przedmioty dwubarwne to możesz również połączyć ich kolory i odpowiednio wyciąć cały przedmiot :).

Obudujmy całość jeszcze w prostą funkcję i zaraz z niej skorzystamy 🙂

def create_mask_red_color(img):
    light_red_part1 = (0,120,80)
    dark_red_part1 = (10, 255, 255)
    
    light_red_part2 = (170,120,80)
    dark_red_part2 = (180, 255, 230)
    
    mask1 = cv2.inRange(hsv_img, light_red_part1, dark_red_part1)
    mask2 = cv2.inRange(hsv_img, light_red_part2, dark_red_part2)
    
    mask_open = cv2.morphologyEx(mask1+mask2, cv2.MORPH_OPEN, np.ones((3,3),np.uint8))
    mask_dilate = cv2.morphologyEx(mask_open, cv2.MORPH_DILATE, np.ones((3,3),np.uint8))
    mask_close = cv2.morphologyEx(mask_dilate, cv2.MORPH_CLOSE, np.ones((5,5),np.uint8))

    return mask_close

Koncepcja algorytmu

Sporo już umiemy, to zbierzmy wszystko w całość. Do naszego projektu wykorzystamy OpenCV. Z wcześniejszego projektu o wykrywaniu twarzy (LINK) wiemy jak odpalić kamerkę i wykryć twarze.

Wystarczy teraz połączyć wcześniejszą wiedzę i:

  1. Odpalić OpenCV i dać chwilkę kamerce na dostrojenie (np. 3 sekundy).
  2. Zapisać obraz wejściowy – będzie nam służył jako maska, którą będziemy nakładać na wycięty obraz.
  3. Przechwycić obraz i wyszukać kolor (jak w przypadku czerwonego kapturka).
  4. Podrasować morphologyEx.
  5. Następnie wyciąć z obrazu z kamerki kolor czerwony i nałożyć tło z kroku 2.
  6. Wyświetlić wynik i cieszyć się magią 🙂

Uwaga! Aby uzyskać fajny efekt, to w tle nie powinno być żadnych ruchomych przedmiotów oraz przedmiotów o wybranym przez Was kolorze (w moim przypadku czerwony).

Ostateczny kod w Python

Stwórzmy jeszcze tworzenie maski jako osobną funkcję (łatwiej tylko ją modyfikować szukając optymalnych parametrów na wycięcie koloru):

def create_mask_red_color(img):
    light_red_part1 = (0,120,50)
    dark_red_part1 = (10, 255, 255)
    
    light_red_part2 = (170,120,50)
    dark_red_part2 = (180, 255, 255)
    
    mask1 = cv2.inRange(hsv_img, light_red_part1, dark_red_part1)
    mask2 = cv2.inRange(hsv_img, light_red_part2, dark_red_part2)
    
    mask_open = cv2.morphologyEx(mask1+mask2, cv2.MORPH_OPEN, np.ones((5,5),np.uint8))
    mask_dilate = cv2.morphologyEx(mask_open, cv2.MORPH_DILATE, np.ones((5,5),np.uint8))
    mask_close = cv2.morphologyEx(mask_dilate, cv2.MORPH_CLOSE, np.ones((5,5),np.uint8))

    return mask_close

A tutaj wszystko o czym pisałem zebrane w całość:

# tworzymy objekt VideoCapture
cap = cv2.VideoCapture(0)

# dajemy czas kamerce na złapanie ostrości
time.sleep(3)

#zapisujemy w pamięci pierwsze zdjęcie po tych 3 sekundach
background=0
ret,background = cap.read()

while(True):
    # przechwytujemy obraz real-time z kamery
    ret, img = cap.read()

    # konwersja BGR na HSV
    hsv_img = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)
    
    # tworzymy maskę wykorzystujemy naszą funkcję
    mask = create_mask_red_color(hsv_img)
    
    # tworzymy drugą maskę - wszystko poza ubraniem 
    mask_inverted = cv2.bitwise_not(mask)
    
    # zostawiamy z przechwyconego obrazka wszystko poza płaszczem
    result_inverted = cv2.bitwise_and(img,img,mask=mask_inverted)  
    
    # z statycznego screena tła wycinamy piksele dla naszego płaszcza 
    result_mask = cv2.bitwise_and(background, background, mask = mask)
    
    # łączymy oba zdjęcia
    final_output = cv2.addWeighted(result_inverted,1,result_mask,1,0)
    
    #wyświetlamy wynik
    cv2.imshow("magic",final_output)

    if cv2.waitKey(30) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

Efekt końcowy tego kodu widziałeś już na początku artykułu.

Mam nadzieję, że się podobał i że masz wiele pomysłów jak możesz tę wiedzę wykorzystać do swoich celów 🙂

Pozdrawiam serdecznie,

podpis Mirek
.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *