O idee care mie mi s-a părut foarte tare și pe care am susținut-o până cu o zi înainte de RobotX a fost folosirea unor camere video (camere de telefoane mobile, spre exemplu ;) ) pe cele trei locuri de baliză care sunt disponibile unei echipe la Eurobot, pentru a determina permanent unde sunt obiectele poziționate pe masă. Am mai scris despre ideea asta aici.
Ideea mi s-a părut bună pentru că (1) masa este colorată optim din punct de vedere al contrastului pentru a face computer vision pe ce iese din camere, (2) telefoane mobile ar avea suficientă putere de procesare, camere suficient de bune și un factor de formă ideal pentru ce aveam noi nevoie.
Deși până la urmă soluția nu a putut fi folosită cu Pufoșenia, pentru că nu am avut vreme să o definitivăm, niște rezultate tot am obținut, și poate pot părea interesante.Ideea de bază e simplă: putem pune trei balize pe masă. Două în colțurile de pe latura scurtă apropiată de zona noastră de start, și una pe jumătatea laturii opuse. Stâlpii de baliză sunt la 35 de cm înălțime, și baliza trebuie să se încadreze în 8 x 8 x 16cm.
Ipoteza noastră a fost că, preluând imagini din cele 3 locuri, putem face cumva să reconstruim o privire de deasupra a mesei (sau o vedere completa 2D a suprafeței de joc).
Ca să ne înțelegem, am pornit de la niște imagini de genul:
În mod evident, aveam nevoie de definirea unei funcții de perspectivă, care să transforme imaginile astea în niște vederi de deasupra. În mod la fel de evident, transformarea nu avea cum să fie perfectă, pentru că pe măsură ce obiectele sunt mai îndepărtate, avem mai puțină informație despre ele.
Aici intervine OpenCV care ne ajută foarte mult. O dată, cu funcția WarpPerspective, care aplică o transformare unei imaginii pentru a o despacheta, și apoi cu funcția GetPerspectiveTransform care pregătește transformarea plecând de la poziția a patru puncte din imaginea inițială și poziția acelorlași patru puncte în imaginea transformată.
Este o bucată de cod destul de plictisitoare în care se pregătesc imaginile destinație, și se calculează dimensiunea lor (pentru că, evident, transformata de perspectivă va necesita considerabil mai mult spațiu de desfășurare). Partea interesantă din cod e următoarea:
# aloca matricea de transformare
mmat = cv.CreateMat(3, 3, CV_32FC1)
# puncte din imaginea originala
c1 = ((x_p1, y_p1), (x_p2, y_p2), (x_p3, y_p3), (x_p4, y_p4))
# puncte din imaginea destinatie
c2 = ((x_pd1, y_pd1), (x_pd2, y_pd2), (x_pd3, y_pd3), (x_pd4, y_pd4))
# creeaza matricea (functia) de transformare
cv.GetPerspectiveTransform(c1, c2, mmat)
cv.WarpPerspective(imagine_in, imagine_out, mmat)
Secretul este la cum se aleg punctele din c1 și din c2. Ce am făcut, profitând de modul în care este construită masa, am luat unul dintre pătratele roșii (la întâmplare) de pe masă de referință. Am luat coordonatele celor patru colțuri (folosind un program de editare grafică, gen GIMP) și am imaginat unde ar trebui să fie locul lor în imaginea finală. Am început cu imaginea a treia, cea cu vederea de pe latura opusă.
Apoi, pentru că obiectivul meu era suprapunerea celor trei imagini, am considerat același pătrat de referință văzut din celălalte două unghiuri, am păstrat vectorul c2 identic, și am avut grijă să construiesc c1 în așa fel încât să păstrez ordinea punctelor (adica colțul care era p1 pentru o imagine, să fie tot p1 și în celălalte două imagini, chiar dacă vederea este rotită).
Rezultatele, crop-uite la tabla de 6 x 6 pătrate (pentru că în realitate putem scoate și din zonele verzi), după transfomare, arată cam așa:
Se vede clar că departe se vede mai aiurea, pentru că informația este interpolată, practic dintr-un număr mic de pixeli de infromație din imagine, producem o imagine completă.
Ok, acum să vedem cum facem să rămânem numai cu pozițiile concrete ale pionilor, fără să fim deranjați de înălțimea lor. După mai multe încercări, a devenit clar că cel mai simplu în acest caz este nu să luam spațiul de culori HSV, ci să rămânem în BGR, mai concret să rămânem doar cu G(reen) – să scăpăm de roșu și de albastru.
După ce rămânem cu imagine single channel pe verde, putem binariza imaginea asta si sa ramanem doar cu lucrurile interesante. Codul relevant:
# img sunt pe rand, imaginile (transformate) de mai sus
tmp_img = cv.CreateImage(cv.GetSize(img), IPL_DEPTH_8U, 1)
# scoatem componenta verde (B[G]R)
cv.Split(img, None, tmp_img, None, None)
# se face binarizarea cu prag fix
cv.Threshold(img, img, 50, 255, CV_THRESH_BINARY)
Și obținem niște lucruri de genu:
Ok, acuma evident există și prostii între toate cele. Și deși pragul ăla poate să mai fie prelucrat, mai ales într-un scenariu real, nu se pot exclude aberațiile. Și aici intră în partea interesantă.
Inițial, am vrut să facem un AND între toate imaginile, și să primim doar un rezultat. Totuși, făcând niște calcule, am descoperit că nu avem suficientă acoperire încât să luam în considerare DOAR zona comună a celor 3 camere.
Așa că propunerea finală a fost să facem AND între imagini două câte două, și apoi să facem un sau între rezultate, ca să avem o singură imagine finală. Practic, ar trebui să rămână orice obiect care este văzut măcar de două camere. Din fericire, pentru imagini binare, OpenCV are doua functii, And și Or, care fac exact ce avem nevoie.
# and_img este un array format din imaginile de deasupra
cv.And(and_img[0], and_img[1], final_a)
cv.And(and_img[1], and_img[2], final_b)
cv.And(and_img[0], and_img[2], final_c)
cv.Or(final_a, final_b, final)
cv.Or(final_b, final_c, final)
cv.Or(final_c, final_a, final)
Imaginea final arată cam așa:
Care seamănă foarte foarte mult cu o oarecare realitate. Sunt sigur că, cu un pic de tweak-ing, procedura asta poate ajuta puternic la Eurobot de-acum încolo.