Wednesday, 7 April, 2021 - 12:00pm

Zašto koristiti OpenCV?

Ako ste zainteresovani da učite o prepoznavanju lica u slikama ili detekciji objekata u videu, kao i o klasifikaciji slika primenom  metoda dubokog učenja (Deep Learning), biće vam od velike koristi da poznajete OpenCV biblioteku za rad sa slikom. 
OpenCV (Open Source Computer Vision Library) je najpopularnija softverska biblioteka za procesiranje  slike i videa. Kreirana je 2000. godine od strane Intel-a i danas predstavlja standardni alat za istraživanje i razvoj u oblasti kompjuterskog vida (Computer Vision). Osnovna verzija je u programskom jeziku C++, ali postoje interfejsi za  programske jezike Python i Java. Izvorni kod je dostupan na GitHub pod BSD 3-Clause licencom i najveći deo je besplatan čak i za komercijalnu upotrebu. 
Da bismo vam pomogli da brzo uđete u svet Computer Vision-a, kreirali smo ovaj kratak vodič za učenje osnova OpenCV biblioteke kroz programski jezik Python. Dobra vest je da vam nije neophodno napredno znanje matematike i obrade signala, već je dovoljno znati osnove Python-a. Na kraju ovog članka, bićete u stanju da napišete program koji vrši osnovne operacije nad slikom, kao što su: promena rezolucije (resize), crtanje i pisanje po slici, isecanje dela slike (crop), filtriranje (konvolucija), itd.

Instalacija - prosta kao 'pip'

Najbrži način da instalirate OpenCV je korišćenjem Python ugrađenog menadžera paketa – pip. Pre instalacije OpenCV, prvo treba ažurirati pip verziju kroz command prompt:

pip install --upgrade pip

Nakon toga se OpenCV instalira jednostavnom komandom:

pip install opencv-python

Napominjemo da prethodna komanda instalira paket sa glavnim OpenCV modulima, pri čemu postoji i proširena verzija OpenCV koja sadrži dodatne module (tzv. extra/contrib modules). Detalje možete pogledati na https://pypi.org/project/opencv-python/. Ova  verzija je proširena modulima za detekciju objekata, analizu optičkog toka, GPU optimizacijama, itd. Ako želite da instalirate ovu proširenu verziju, treba prvo deinstalirati postojeće verzije komandom pip uninstall opencv-python, a zatim instalirati nove kao: 

pip install opencv-contrib-python

Učitajte i prikažite sliku

Da bismo koristili OpenCV funkcije u Pythonu treba prvo importovati odgovarajući paket. Iako je naziv ovog paketa cv2, on zapravo predstavlja aktuelnu verziju OpenCV 4. Nakon toga su nam dostupne sve funkcije iz OpenCV biblioteke i sada možemo učitati sliku u radnu memoriju, prikazati njene parametre, kao i samu sliku na ekranu.

import cv2
img = cv2.imread(“syrmia_office.jpg”) # ucitavanje slike u memoriju
(rows, cols, channels) = img.shape # dimenzije slike kao matrice
print("Dimenzije slike u boji (visina, sirina, dubina):", rows, cols, channels)
print("Tip promenljive piksela:", img.dtype)
cv2.imshow("Slika", img) # prikaz slike na ekran
cv2.waitKey(0)

Rezultat je:

Dimenzije slike u boji (visina, sirina, dubina): 720 1280 3
Tip promenljive piksela: uint8

Primetite da je rezultat poziva funkcije cv2.imread(“syrmia_office.jpg”) promenljiva img koja predstavlja NumPy višedimenzionalni niz u kome je smeštena slika (kasnije ćemo detaljnije o NumPy). Svi NumPy nizovi imaju atribute shape i dtype, uz pomoć kojih možemo dobaviti dimenzije slike i tip promenljive za svaki piksel. U našem primeru se pozivom img.shape dobijaju dimenzije slike predstavljene kao n-torka (rows, cols, channels) koje predstavljaju broj vrsta, broj kolona i broj kanala boje slike. Dimenzije naše slike tj. NumPy niza su 720x1280x3. Broj kanala boje najčešće iznosi 3 za slučaj podrazumevanog RGB sistem boja.

Treba razumeti da se operacije nad slikom zapravo svode na rad sa matricama. Digitalna slika je matrica elemenata koji se nazivaju pikseli, pri čemu treba imati na umu da čak i manje slike imaju stotine hiljada piksela. Zato se za predstavljanje slike u Pythonu najčešće koristi pomoćna biblioteka NumPy (čita se ‘nampi’) koja je optimizovana za rad sa velikim matricama (>100x brža od obilaska piksel-po-piksel).  Oni koji su se susreli sa Matlab-om,  primetiće da je sintaksa za rad sa NumPy matricama veoma slična Matlab-u. Više o NumPy možete pronaći ovde Numpy for Matlab Users ili Numpy Biblioteka.

Za prikaz slike na ekranu smo koristili funkciju cv2.imshow("Naslov slike", image), dok u narednoj linijii sa cv2.waitKey(0) čekamo na unos sa tastature pre zatvaranja prozora. Vrednost 0 označava da se čeka zauvek, a drugačija brojka bi označila period čekanja u ms). Važno je pozvati ovu funkciju jer će u suprotnom slika biti prikazana i odmah zatvoren prozor, tako da je možda i ne primetimo.

Ovde skrećemo pažnju da cv2.imshow() podrazumeva da su intenziteti piksela celobrojne neoznačene vrednosti u opsegu [0, 255], što odgovara uint8 tipu (cv2.CV_8U). Ako je iz nekog razloga tip piksela konvertovan u realne brojeve npr. cv2.CV_64F, onda se za prikaz piksela podrazumeva normalizovani opseg [0.0, 1.0] što može predstavljati problem jer će sve vrednosti veće od 1.0 biti prikazane belom bojom. U tom slučaju se pre prikazivanja preporučuje konverzija slike u uint8 pozivom np.uint8(img).

Pristup pikselima - BGR ili RGB?

Pre svega treba razumeti da se boja za prikaz jednog piksela dobija kao kombinacija tri osnovne komponente boje: crvene, zelene i plave. Na ovaj način se slika u boji može predstaviti kao 3D matrica sastavljena od tri podmatrice koje predstavljaju po jedan kanal boje (Red, Green, Blue). Odmah skrećemo pažnju da je u OpenCV redosled boja BGR, što je suprotno uobičajenom RGB redosledu! Za svaki piksel, vrednost komponenti R, G i B su predstavljene celim brojem u opsegu od 0-255 (uint8). Na ovaj način se potpuno crna boja u OpenCV predstavlja kao (0, 0, 0), dok se npr. osnovna zelena boja predstavlja kao (0, 255, 0). Ukupno se može dobiti ~16.7 miliona različitih nijansi boje tj. 255*255*255 = 16 777 216.

Pristup pikselu u OpenCV se može obaviti na osnovu indeksa reda, kolone i kanala boje u matrici. Tako se npr. sa img[100, 50, 0] pristupa pikselu u 100-tom redu i 50-oj koloni matrice slike, pri čemu se dobavlja samo B komponenta boje (redosled je BGR!). Moguće je odjednom dobaviti i sve tri komponente boje jednog piksela, izostavljanjem poslednjeg indeksa npr. img[100, 50]. Sve ovo je slično Matlab-u, uz napomenu da u Python-u indeksiranje započinje od img[0, 0].

# Pristup pikselu na poziciji [100, 50]
# Redosled kanala boje je BGR u OpenCV!
# Sporija varijanta, ali intuitivna
(b, g, r) =  img[100, 50]
print("Komponente boje piksela (blue, green, red):", b, g, r);
# Moze i posebno ocitavanje svakog kanala boje
# b = img[100, 50, 0] # B komponenta boje piksela
# g = img[100, 50, 1] # G komponenta boje piksela
# r = img[100, 50, 2] # R komponenta boje piksela

Rezultat je:

Komponente boje piksela (blue, green, red): 149 147 146

Treba uočiti da poziv img[100, 50] vraća boju piksela koja je predstavljena kao n-torka (b, g, r), gde svaki element predstavlja vrednost jedne od 3 osnovne komponente boje tog piksela: plava (B), zelena (G) i crvena (R). Podsećamo da redosled boja u OpenCV odstupa od uobičajenog RGB redosleda (iz istorijskih razloga).

# Brza varijanta ~2x
b = img.item(100, 50, 0) # B komponenta boje piksela
g = img.item(100, 50, 1) # G komponenta boje piksela
r = img.item(100, 50, 2) # R komponenta boje piksela
print("Komponente boje piksela (blue, green, red) - varijanta 2:", b, g, r);

Ako želimo da promenimo boju nekog piksela u slici, to se može obaviti na sledeći način:

# Postavljanje boje piksel na poziciji [100, 5]
img[100, 50] = (255, 0, 0) # Boji piksel u plavo
# Drugi nacin (brze)
img.itemset((100, 50, 0),255)
img.itemset((100, 50, 1) ,0)
img.itemset((100, 50, 2) ,0)

Izdvojite delove slike (ROI)

Prilikom rada sa slikom, često vam može zatrebati da izdvojite samo određeni deo slike tzv. region od interesa (engl. Region Of Interest - ROI). Recimo da npr. radite na prepoznavanju lica u videu. Prvo biste pokrenuli algoritam za detekciju lica koji bi pronašo koordinate lica u svim frejmovima, a zatim bi nad detektovanim regionima lica mogli da obavite snimanje ili dalje pocesiranje (npr. blur).

OpenCV i NumPy vam nude veoma moćan operator dvotačka (:) kojim možemo zahvatiti opseg redova i kolona koje ograničavaju neki region, slično kao u Matlab-u. Na primer, ako želimo da izdvojimo region tj. podmatricu koja je ograničena redovima od 190 do 280 i kolonama od 80 do 440, onda ćemo to obaviti na sledeći način:

roi = img[190:280, 80:440]
cv2.imshow("Region slike", roi)

Napominjemo da se operator dvotačka može koristiti i bez zadavanja početnog i krajnjeg indeksa, pri čemu se u tom slučaju podrazumevaju svi elementi odgovarajućeg indeksa. Npr img[ : , 450:550] označava region koji obuhvata sve redove slike u kolonama od 450 do 550. Npr. ako želimo da izdvojimo svaki kanal boje kao jednu 2D matricu to ćemo uraditi tako što selektiramo sve vrste i sve kolone, ali samo jedan kanal boje po dubini:

B = img[:, :, 0] # B kanal boje kao podmatrica dimenzija (720, 1280)
G = img[:, :, 1] # G kanal boje kao podmatrica dimenzija (720, 1280)
R = img[:, :, 2] # R kanal boje kao podmatrica dimenzija (720, 1280)

Zatim ovo možemo iskoristiti da obavimo konverziju u jednokanalnu (sivu) sliku, koja je pogodnija za dalje procesiranje.

# Y = 0.299*R + 0.587*G + 0.114*B
img_gray = np.uint8( 0.299*R + 0.587*G + 0.114*B )
cv2.imshow("Siva slika (grayscale, monohromatska, 8-bitna)", img_gray)

Napominjemo da je prethodni postupak konverzije kolor slike u sivu sliku, moguće jednostavnije obaviti u OpenCV pozivom funkcije cv2.cvtColor():

img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

For-petlja i OpenCV 
(izbegavajte obilazak piksel-po-piksel)

Koristeći ranije opisani način za pristup pikselima, moguće je npr. izračunati srednju vrednost piksela u B kanalu boje. Da biste ovo uradili verovatno će vam prvo pasti na pamet da kroz dve for petlje obiđete sve elemente matrice, slično kao u nastavku:

# Varijanta 1 - sporija
sum_b = 0
for i in range(0 , rows):
    for j in range(0 , cols):
        b = img.item(i, j, 0) # B komponenta boje
        sum_b += b;
avg1_b = sum_b / (rows * cols) 
print("Prosecna vrednost piksela u B kanalu (for-loop):", avg1_b)

Rezultat je:

Prosecna vrednost piksela u B kanalu (for-loop): 112.253115234375

Odmah skrećemo pažnju da prethodni način za pristup svakom pikselu treba izbegavati kad god je to moguće, jer je relativno spor u odnosu na korišćenje NumPy optimizovanih funkcija. Uvek imajte na umu da je NumPy optimizovan za brza izračunavanja nad velikim maricama i trudite se da maksimalno koristite sve 'gotove' funkcije koje vam NumPy nudi (slično kao Matlab).

# Varijanta 2 - brze bar 100x
import numpy as np
img_b = img[:, :, 0] # Izdvaja B kanal slike kao 2D matricu, redosled je (B, G, R)!
avg2_b = np.sum(img_b) / (rows * cols)
print("Prosecna vrednost piksela u B kanalu (np.sum):", avg2_b)

Rezultat je:

Prosecna vrednost piksela u B kanalu (np.sum): 112.253115234375

Da bismo bismo pokazali koliko je varijanta 1. koda sporija od varijante 2, izmerićemo vreme izvršenja koda korišćenjem:

e1 = cv2.getTickCount()
# izvorni kod cije vreme izvrsenja merimo
e2 = cv2.getTickCount()
print("Vreme izvrsenja:", (e2 - e1)/cv2.getTickFrequency())

Kao rezultat na našem računaru izmerili smo da je 2. varijanta sa NumPy brza preko 200 puta:

Prosecna vrednost piksela u B kanalu (for-loop): 112.253115234375
Vreme izvrsenja: 0.0754265
Prosecna vrednost piksela u B kanalu (np.sum): 112.253115234375
Vreme izvrsenja: 0.0003671

Još detalja u vezi sa brzinom izvršenja OpenCV koda možete pogledati na:

https://www.pythonlikeyoumeanit.com/Module3_IntroducingNumpy/VectorizedOperations.html
http://datahacker.rs/opencv-pixel-intensity-change-and-watermark/

Kako smanjiti sliku uz očuvanje proporcija?

Promena veličine slika vam može zatrebati iz više razloga. Prvo, možda ćete želeti da umanjite veliku sliku da bi se kompletno prikazala na ekranu. Takođe, jasno je da je procesiranje manjih slika znatno brže u odnosu na velike slike. Osim toga, ukoliko primenjujete Deep Learning metode nad slikom, trebaće vam da ulaznu sliku prilagodite fiksnim dimenzijama koje neuronska mreža očekuje.  

OpenCV nam nudi funkciju cv2.resize(img, (width, height)) čija je jednostavna primena prikazana u narednom primeru:

img_resized = cv2.resize(img, (400, 400))
cv2.imshow("Nakon promene dimenzija – bez proporcija", img_resized);

Treba uočiti da su dimenzije slike sada postavljene na 400x400 piksela, pri čemu nismo vodili računa da očuvamo originalne proporcije tj. odnos širine i visine (engl. aspect ratio). Zato je slika izobličena tj. izdužena nakon ove operacije.

Da bismo očuvali proporcije na umanjenoj slici, treba da izračunamo aspect_ratio originalne slike i iskoristimo ga za umanjenu sliku. Pretpostavimo da želimo da promenimo dimenzije slike tako da ona bude fiksne širine 400px, a da pritom zadržimo odnos širine I I visine kao u originalnoj slici. Dakle treba postaviti cols_resized=400 i treba da važi očuvanje proporcija: cols/rows = cols_resized /rows_resized. Odatle dobijamo rows_resized = cols_resized * rows / cols.

To možemo uraditi na sledeći način:

(rows, cols, depth) = img.shape
cols_resized = 400; # fiksiramo širinu nove slike
rows_resized = int(cols_resized * rows / cols)
img_ resized = cv2.resize(img, (cols_resized, rows_resized))
cv2.imshow("Nakon promene dimenzija", img_resized);

Crtanje (i pisanje) po slici

Iscrtavanje po slici će vam najčešće trebati za prikaz pravougaonika oko objekata koji su rezultata algoritama za detekciju objekata (engl. bounding boxes). Osim pravougaonika, moguće je iscrtavati i kružnice, linije, ispisivati tekst, itd. Treba imati na umu da OpenCV funkcije za crtanje menjaju originalnu sliku nad kojom su pozvane. Zato je dobra praksa da se prvo napravi kopija slike i zatim crtanje vrši po toj kopiji.
Prvo ćemo nacrtati crveni pravougaonik u gornjem delu slike pozivom funkcije cv2.rectangle(…):

# Crtanje i pisanje po slici
out_img = img.copy()
top_left_x = 10 # x koordinata - odgovara koloni matrice
top_left_y = 10 # y koordinata - odgovara redu matrice
bottom_right_x = img.shape[1] - 10 # citava sirina
bottom_right_y = top_left_y + 30
rect_color = (0, 0, 255) # BGR
cv2.rectangle(out_img, (top_left_x, top_left_y), (bottom_right_x, bottom_right_y), rect_color, 1)

Treba uočiti da su prilikom direktnog poziva OpenCV funkcija koordinate tačaka obično izražene kao par (x, y) gde x predstavlja kolonu matrice, a y red matrice. Ovo je suprotno od NumPy gde se koriste matrične koordinate (red, kolona).

Zatim možemo ispisati tekst “Copyright by SYRMIA” unutar pravougaonika pozivom cv2.puttext():

font_face = cv2.FONT_HERSHEY_SIMPLEX
font_scale = 0.6
font_color = (0, 0, 255) # BGR
font_thick = 1 # integer
bottom_left_x = top_left_x + 10; # x - odgovara koloni matrice
bottom_left_y = bottom_right_y - 10; # y - odgovara redu matrice
cv2.putText(out_img, “Copyright by SYRMIA”, (bottom_left_x, bottom_left_y), font_face, font_scale, font_color, font_thick)
cv2.imshow("Rezultat", out_img)

Konvolucija bez Neuronskih mreža - čemu to služi?

Iako termin “konvolucija” može delovati komplikovano, to je zapravo veoma jednostavna linearna operacija. Konvolucija se svodi na množenje piksela određenim koeficijentima i sumiranje rezultata. Ukoliko ste koristili Photoshop ili neki od Instagram filtera, vi ste zapravo primenjivali konvoluciju a da toga niste ni bili svesni. Svaki efekat izoštravanja slike (sharpening), zamućenja (blur) ili uklanjanja šuma u slici (denoising), zapravo koristi konvoluciju.

U postupku konvolucije, za svaki piksel ulazne slike se izračunava njegov novi intenzitet koji se dobija množenjem okolnih piksela sa unapred određenim koeficijentima i sumiranjem rezultata. Koeficijenti kojima se množe okolni pikseli se predstavljaju kao matrica koja se naziva jezgro ili maska konvolucije (engl. kernel, mask, filter). Elementi jezgra mogu biti proizvoljni realni brojevi. Prikaz izračunavanja konvolucije za jedan piksel slike (8) je dat u nastavku:

Postupak konvolucije nad ulaznom slikom se sastoji iz 3 koraka koji se ponavljaju nad svakim pikselom slike:

  1. Postavljanje matrice jezgra (kernel) “iznad” tekućeg piksela slike.
  2. Množenje koeficijenata jezgra sa pikselima slike koji su “ispod” jezgra i sumiranje rezultata
  3. Upis prethodno izračunate sume u rezultujuću sliku.

U OpenCV postoji opšta funkcija cv2.filter2D(...) koja obavlja celokupan prethodni postupak i vraća rezultujuću sliku. U nastavku je dat primer definisanja nekoliko konvolucionih jezgara i poziv funkcije, Npr:

kernel_blur = np.array(
        [[0.11, 0.11, 0.11],
         [0.11, 0.11, 0.11],
         [0.11, 0.11, 0.11]])
img_result = cv2.filter2D(img, -1, kernel_blur)
cv2.imshow("Blur", img_result)

U čemu je onda složenost konvolucije? Glavno inženjersko znanje prilikom primene konvolucije se sastoji u određivanju koeficijenata jezgra kako bi se postigao odgovarajući efekat u rezultujućoj slici. Tako npr. ako želimo da izvršimo “glačanje” tj. “zamućenje” slike (blur), jezgro treba definisati tako da računa srednju vrednost okolnih piksela. Za jezgro dimenzija 3x3 (9 koeficijenata) to praktično znači da svi koeficijenti treba da iznose 1/9 = 0.11, kao u prethodnom primeru. Dok, ako npr. želimo da obavimo ‘izoštravanje’ slike, onda jezgro treba definisati na sledeći način:

kernel_laplace_sharp = np.array(
        [[0, -1, 0],
        [-1, 5, -1],
        [0, -1, 0]])
img_result = cv2.filter2D(img, -1, kernel_laplace_sharp)
cv2.imshow("Sharpening", img_result)

Primere različitih konvolucionih jezgara možete pogledati ovde link.

Napominjemo da OpenCV nudi i “jednostavniji” način za primenu standardnih konvolucija, koji ne zahteva da poznajemo konvoluciona jezgra. Naime, postoje gotove funkcije unutar kojih su već definisana odgovarajuća jezgra i koje vraćaju rezultujuću sliku, kao što su cv2.blur() ili cv2.Sobel(). Međutim, treba biti veoma oprezan sa parametrima i tipovima argumenata ovih funkcija, koji su specifični za svaku od njih.

Konvolucija je dobila na popularnosti nakon velikog uspeha dubokih neuronskih mreža. Zbog toga je jedna vrsta Neuronskih mreža nazvana Konvolucione neuronske mreže (engl. Convolutional Neural Networks – CNN). Suština ovih neuronskih mreža je da su u stanju da “nauče” optimalne koeficijente konvolucionog jezgra na osnovu primera rezultata koji se očekuje. Dakle, koeficijenti konvolucionog jezgra nisu unapred zadati od strane inženjera, već se kroz postupak treniranja neuronskih mreža izračunava njihova optimalna vrednost. Ovo je omogućilo značajno unapređenje rezultata u svim oblastima Computer Vision-a (klasifikacija, detekcija, segmentacija, itd.), u odnosu na prethodne metode koje su koristile “ručno” definisana jezgra od strane inženjera. Detaljnije objašnjenje Konvolucionih neuronskih mreža ostavljamo za neki od narednih članaka.

Gde pronaći još detalja o OpenCV?

Autori: Stevica Cvetković, Jelena Tošić, Petar Milivojević