Proiectul presupune crearea pe un ecran LCD a unui stickman care sa imite mișcările unei persoane aflate in fata unui XBOX 360 Kinect.
Consider că proiectul meu reprezintă un POC pentru o idee care ar putea fi aplicată pe semafoarele din zonele turistice urmând:
Au fost deja implementate astfel de proiecte având efectele precizate anterior. Aveți un exemplu aici.
Am ales acest proiect din următoarele motive:
Pași de funcționare a proiectului:
Proiectul este alcătuit din următoarele componente:
Descriere a asamblării:
De menționat:
Pentru implementarea funcționalității am utilizat următoarele tehnologii (medii de dezvoltate, limbaje de programare și librării):
Deoarece Kinect-ul si plăcuța Arduino sunt conectate prin intermediul laptop-ului, vom avea pe lângă dancing_stickman.ino
, pentru Arduino, și dancing_stickman.pde
, un program scris în Processing, prin intermediul căruia Kinect-ul poate comunica cu plăcuța folosind USART.
Pentru Arduino nu trebuie să facem nimic diferit, în Processing însă va trebui să accesăm portul serial astfel:
import processing.serial.*; Serial port; void setup() { String portName = Serial.list()[SERIAL_PORT_INDEX]; port = new Serial(this, portName, BAUD_RATE); }
SERIAL_PORT_INDEX
trebuie încercate mai multe valori prin trial and error pentru a descoperi valoarea care corespunde port-ului serial. Majoritatea tutorialelor de pe internet folosesc 0, dar în cazul meu am folosit 2.
Dacă setup-ul este corect, apelul funcției port.write()
din Processing ar trebui să trimită octeți către Arduino pe care să îi recepționeze cu funcția Serial.read()
.
Dorim ca Kinect-ul să detecteze scheletul user-ului și gesturile acestuia. Pentru a utiliza aceste funcționalități ale Kinect-ului, vom avea nevoie și de matricea de adâncime (depth map), care calculează distanța de la Kinect la tot ceea ce vede utilizând două camere (RGB și IR) și un proiector de raze infraroșii. Aveți aici o explicație cu reprezentare vizuală.
Exemplu de harta de adâncime. Observați faptul ca brațul stâng apare negru, deși este foarte aproape de cameră, deci ne-am aștepta să apară alb. Acest fenomen este cauzat de felul în care Kinect-ul se folosește de razele infraroșii pentru a calcula distanța până la obiecte. Distanța la care Kinect-ul este cel mai eficient este de obicei între 1.5 și 4 metri.
Kinect-ul este inițializat astfel:
import SimpleOpenNI.*; SimpleOpenNI kinect; void setup() { kinect = new SimpleOpenNI(this); kinect.enableDepth(); // activează matricea de adâncime kinect.enableUser(); // activează recunoașterea utilizatorilor (recunoașterea corpului) kinect.enableHand(); // activează recunoașterea gesturilor kinect.startGesture(SimpleOpenNI.GESTURE_WAVE); // activează recunoașterea gestului WAVE /* se pregătește fereasta în care vom afișa matricea de adâncime */ size(KINECT_WIDTH, KINECT_HEIGHT); // KINECT_WIDTH = 640, KINECT_HEIGHT = 480 fill(255, 0, 0); } void draw() { /* actualizează fereastra cu noua matrice de adâncime */ kinect.update(); image(kinect.depthImage(), 0, 0); }
Activarea acestor funcționalități vor declanșa si apelul funcțiilor de callback onNewUser
, onLostUser
, onCompletedGesture
, care vor fi folosite pentru etapa de calibrare și lansare a recunoașterii corpului.
Pentru detectarea corpului utilizatorului, Kinect-ul recunoaște 15 puncte cu care formeaza scheletul: cap, gât, doi umeri, două coate, două mâini, trunchi, două șolduri, doi genunchi, două tălpi.
Exemplu de recunoaștere a scheletului peste matricea de adâncime.
Obținerea unor coordonate 2D la aceste puncte este trivială. Folosind biblioteca SimpleOpenNI
se utilizează funcția getJointPositionSkeleton
pentru fiecare punct pentru a obtine poziția lui în spațiul 3D, apoi se proiectează la 2D folosind convertRealWorldToProjective
.
PVector getProjectiveJoint(int userID, int jointID) { /* Obține coordonatele 3D a punctului de pe schelet */ PVector joint = new PVector(); float confidence = kinect.getJointPositionSkeleton(userID, jointID, joint); // confidence poate fi folosit pentru verificarea preciziei rezultatelor /* Convertește la spațiul 2D calculând proiecția */ PVector projectiveJoint = new PVector(); kinect.convertRealWorldToProjective(joint, projectiveJoint); return projectiveJoint; }
Partea mai complicată la acest proiect este transmiterea celor 15 puncte de la Kinect la Arduino, prin intermediul USART, din următoarele motive:
Acest fapt înseamnă că este nevoie să fie schițat un protocol de comunicație minimal care să fie folosit de cele două dispozitive. Partea bună este totuși că această comunicație se poate produce doar într-un singur sens, de la Kinect la Arduino.
Soluția la care am ajuns a fost utilizarea funcției map(value, fromLow, fromHigh, toLow, toHigh)
, existentă în ambele limbaje de programare folosite, pentru a codifica coordonatele (x, y) ale punctelor astfel:
// Processing - Trimitere de la Kinect void sendCoordinateAxis(float value, int lowerBound, int upperBound) { int intVal = floor(between(value, lowerBound, upperBound)); int asByte = (int)map(intVal, lowerBound, upperBound, 0, 255); port.write(asByte); }
Pentru recepție de pe Arduino:
// Arduino - Recepție pe plăcuță int readCoordinateX() { byte xAsByte = Serial.read(); int x = (int)map(xAsByte, 0, 255, 0, width); Serial.println(x); return x; } int readCoordinateY() { byte yAsByte = Serial.read(); int y = (int)map(yAsByte, 0, 255, 0, height); Serial.println(y); return y; }
Această tehnică va atrage mici pierderi de informație la maparea pe LCD, însă acesta sunt rareori observabile. Avantajul masiv pe care îl oferă este însă acela că minimizează numărul de octeți trimiși într-un mesaj(31):
Această soluție este cea mai bună, deoarece permite un refresh rate mai mare.
Mesajele plăcuței vor fi mereu de 31 de octeți dintre care primul va fi folosit pentru recunoașterea mesajului:
BYTE_RECOGNISED
- Kinect-ul a detectat jucătorul;BYTE_START_PLAYING
- Kinect-ul a detectat gestul de wave și semnalează începerea jocului;BYTE_LOST
- Kinect-ul nu mai detectează jucătorul;BYTE_START
- Kinect-ul trimite coordonatele curente ale scheletului utilizatorului;BYTE_NOT_RECOGNISED
- Valore dată coordonatelor punctelor de pe schelet care nu sunt vizibile pentru Kinect.
SKEL_HEAD
) la tălpi (SKEL_LEFT_FOOT
, SKEL_RIGHT_FOOT
);
Pentru desenarea corpului pe display-ul LCD se vor folosi din biblioteca LCDWIKI_GUI
funcțiile:
Fill_Circle(int16_t x, int16_t y, int16_t radius)
;Fill_Triangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1,int16_t x2, int16_t y2)
;Draw_Line(int16_t x1, int16_t y1, int16_t x2, int16_t y2)
.Acestea sunt utilizate în funcții wrapper pentru a desena corpul stickman-ului care va imita utilizatorul:
void drawHead() { lcd.Fill_Circle(skeletonPoints[SKEL_HEAD].x, skeletonPoints[SKEL_HEAD].y, 25); }
void drawBodyPart(int firstIndex, int secondIndex, int thirdIndex) { PVector p1 = skeletonPoints[firstIndex]; PVector p2 = skeletonPoints[secondIndex]; PVector p3 = skeletonPoints[thirdIndex]; lcd.Fill_Triangle(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); }
void drawLimb(int firstIndex, int secondIndex) { PVector p1 = skeletonPoints[firstIndex]; if (p1.x == BYTE_NOT_DETECTED && p1.y == BYTE_NOT_DETECTED) { return; } PVector p2 = skeletonPoints[secondIndex]; if (p2.x == BYTE_NOT_DETECTED && p2.y == BYTE_NOT_DETECTED) { return; } lcd.Draw_Line(p1.x, p1.y, p2.x, p2.y); /* Draw around the point for thicker lines */ for (int i = 1; i <= 2; i++) { lcd.Draw_Line(p1.x + i, p1.y, p2.x + i, p2.y); lcd.Draw_Line(p1.x - i, p1.y, p2.x - i, p2.y); lcd.Draw_Line(p1.x, p1.y + i, p2.x, p2.y + i); lcd.Draw_Line(p1.x, p1.y - i, p2.x, p2.y - i); } }
void drawSkeleton() { lcd.Fill_Screen(BLACK); drawHead(); /* Draw a circle for each skeleton point */ for (int i = 1; i < NUM_SKEL_POINTS; i++) { PVector p = skeletonPoints[i]; lcd.Fill_Circle(p.x, p.y, 4); } drawLimb(SKEL_HEAD, SKEL_NECK); drawLimb(SKEL_NECK, SKEL_LEFT_SHOULDER); drawLimb(SKEL_LEFT_SHOULDER, SKEL_LEFT_ELBOW); drawLimb(SKEL_LEFT_ELBOW, SKEL_LEFT_HAND); drawLimb(SKEL_NECK, SKEL_RIGHT_SHOULDER); drawLimb(SKEL_RIGHT_SHOULDER, SKEL_RIGHT_ELBOW); drawLimb(SKEL_RIGHT_ELBOW, SKEL_RIGHT_HAND); drawLimb(SKEL_LEFT_SHOULDER, SKEL_TORSO); drawLimb(SKEL_RIGHT_SHOULDER, SKEL_TORSO); drawLimb(SKEL_TORSO, SKEL_LEFT_HIP); drawLimb(SKEL_LEFT_HIP, SKEL_LEFT_KNEE); drawLimb(SKEL_LEFT_KNEE, SKEL_LEFT_FOOT); drawLimb(SKEL_TORSO, SKEL_RIGHT_HIP); drawLimb(SKEL_RIGHT_HIP, SKEL_RIGHT_KNEE); drawLimb(SKEL_RIGHT_KNEE, SKEL_RIGHT_FOOT); drawLimb(SKEL_LEFT_HIP, SKEL_RIGHT_HIP); drawBodyPart(SKEL_LEFT_SHOULDER, SKEL_RIGHT_SHOULDER, SKEL_TORSO); drawBodyPart(SKEL_LEFT_HIP, SKEL_RIGHT_HIP, SKEL_TORSO); }
Ma bucur că am descoperit cum funcționează tehnologiile folosite de Kinect (depth image, skeleton recognition) și am reușit să creez un proiect fun și relativ unic. Sper că acest proiect va motiva viitorii studenți să încerce și ei includerea unui Kinect în proiectul lor. Este mult mai ușor decât pare, iar eu am încercat în această documentație să menționez toate blocajele pe care le-am avut în timpul fazei de setup pentru a trece mult mai repede la partea creativă. Singurul regret pe care îl am este că display-ul meu nu a avut un refresh rate suficient de mare încât să pot reda imagini în timp real.
Setup Kinect:
Programare Kinect și comunicare cu Arduino:
Alte probleme întâmpinate de mine:
Resurse Software:
Resurse Hardware:
Resurse Kinect: