TSM - Proiect IoT: Mașinuța prietenoasă

Ovidiu Mățan - Fondator @ Today Software Magazine


În numărul din decembrie, am demonstrat cum putem să ne facem o mașinuță cu telecomandă. Vom continua dezvoltarea acestui proiect. De data aceasta, îi vom adăuga un pic de autonomie prin inserarea unui Raspberry Pi și a unei camere video. Folosind OpenCV pentru procesarea imaginilor și machine learning pentru recunoașterea persoanelor, vom face mașinuța să ne urmărească. 

Pregătirea

Așa cum menționam în introducere, vom avea nevoie de un Raspberry PI și o cameră video. Camera video poate fi pe USB sau pe interfața CSI. Noi am ales să folosim camera Raspberry Pi V1.3 ce folosește interfața CSI.

Arhitectura sistemului

Până acum, mașinuța era controlată de un Arduino Uno, care printr-un driver controla rotația motoarelor, iar printr-un senzor infraroșu primea comenzi de la telecomandă. Acestui sistem îi vom adăuga placa Raspberry PI, care împreună cu camera vor putea să recunoască persoanele și în funcție de distanța la care se găsesc, să se apropie sau să depărteze de acestea. Comunicarea cu Arduino se va realiza prin interfața serială, iar în practică înseamnă conectarea celor două plăci cu un cablu USB.

Programarea sistemului

Odată ce am realizat conexiunile hardware:

Putem trece la partea de programare.

Transmiterea comenzilor

Vom folosi limbajul Python datorită suportului bun pe care îl are în proiectele IoT.

Conectarea se realizează prin deschiderea unui port serial. Pentru a identifica exact numele conexiunii, în Arduino Studio, pe Raspberry Pi, mergeți la Tools->Ports pentru a vedea porturile disponibile:

ser = serial.Serial('/dev/ttyACM0', 9600, timeout=1)

Trimiterea mesajelor se realizează prin folosirea comenzii:

ser.write(msg.encode())

Pe Arduino, citirea mesajelor se face astfel:

while (Serial.available() > 0) {

 String str = Serial.readString();  

…

}

Citirea imaginilor de la cameră

Primul pas înainte de a trece la partea de cod, este să inițializați camera video folosind următoarea comandă pe Raspberry PI:

sudo modprobe bcm2835-v4l2

Vom folosi OpenCV pentru citirea imaginilor și manipularea acestora:

cap = cv.VideoCapture(0)
if not cap.isOpened:
 print('--(!)Error opening video capture') exit(0)  
while True:
 ret, frame = cap.retrieve()  
...

Parametrul 0 de la VideoCapture() este specific OpenCv și se folosește 0 dacă avem o singură cameră.

Afișarea imaginii capturate este utilă pentru o verificare inițială a detectării feței dar la care vom renunța în momentul în care prototipul este gata de acțiune deoarece nu vom afișa imaginile, recunoașterea fețelor reflectându-se în mișcarea mașinuței.

cv.imshow('Capture', frame)

Recunoașterea feței

Următorul cod folosește clasificatorii Haar iar fișierele folosite sunt parte din distribuția standard OpenCV. Pentru mai multe detalii despre OpenCV și Java, vă recomand un articol recent publicat: Recunoașterea persoanelor și a mașinilor folosind OpenCV și Java

frame = cv.flip(frame, 0)
frame_gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
frame_gray = cv.equalizeHist(frame_gray)
#-- Detect faces
faces = face_cascade.detectMultiScale(frame_gray)
for (x,y,w,h) in faces:
  center = (x + w/2, y + h/2)
  frame = cv.ellipse(frame_gray, center, 
  (w/2, h/2), 0, 0, 360, (255, 0, 255), 4)
  print ((x,y,w,h))

Primul pas este să întoarcem imaginea. În cazul nostru, senzorul de captură video este mai ușor de orientat invers și a trebuit să folosim metoda flip(). În continuare, vom transforma imaginea în format alb negru și vom detecta posibilele pătrate ce conțin posibile fețe detectate. Puteți folosi diferite definiții, noi am ales haarcascade_frontalface_alt2.xml.

Odată detectate coordonatele (x,y,w,h), putem să definim o strategie de mișcare a mașinuței. Dacă suntem suficient de aproape de aceasta, o putem face să înainteze către noi (w>100)

START_COMMAND='--:'
END_COMMAND='--
….
  if w > 100:
    distances=[str(x),str(w)]
    msg = ','.join(distances)
    msg = START_COMMAND+'POS:'+msg+END_COMMAND
    print("sending serial message"+msg)
    ser.write(msg.encode())
    ser.write(0)
    ser.flush()
    time.sleep(1)   

Pe Arduino vom citi comenzile și vom aplica deplasarea necesară. Pentru aceasta, vom folosi librăria Motorscontrol pe care am discutat-o într-un articol anterior ()

 String str = Serial.readString();
int commandIndex = str.indexOf(POS_command);
if (commandIndex > -1) {
   Serial.println("received POS command");
   //'--:POS:'+msg+'--'
   int separatorIndex = str.indexOf(',', 
   POS_command.length() - 1);
   int endCommandIndex = str.indexOf('-', 
   separatorIndex);
   int x = str.substring(POS_command.length(), 
   separatorIndex).toInt();
   int w = str.substring(separatorIndex + 1, 
   endCommandIndex).toInt();
   Serial.println("got info");
   Serial.print(x);
   Serial.print("_");
    Serial.println(w);

   //First, check for stop
   if (!(x == 0 and w == 0)) {
     int direction = MAX_X / 2 - x;
     Serial.print("direction=");
     Serial.println(direction);
     int positiveVal = (direction < 0) ? 
     -direction : direction;
     if (positiveVal > CENTER_RANGE) {
       if (direction > 0) {
         Serial.println("go Right");
         motorA.right();
         motorB.right();
       } else {
         Serial.println("go Left");
         motorA.left();
         motorB.left();
       }
...   

Codul de mai sus este destul de simplu din perspectiva demonstrației, iar pentru a vă face o imagine de ansamblu corectă și funcțională, vă sugerez să aruncați o privire și peste sursele complete din GitHub

Probleme și soluții

OpenCV este o librărie realizată pentru afișarea și procesarea constantă a fluxului video. În cazul nostru, recunoașterea feței va adăuga o întârziere în citirea următoarei imagini. Deoarece imaginile sunt citite de la cameră și adăugate într-un buffer, această întârziere poate ajunge până la 10 sec. în mod real, iar sistemul nu mai poate fi folosit.

Soluția aleasă este una relativ simplă, mai ales că avem nevoie de un pic de timp pentru deplasarea mașinuței. După citirea unei imagini, vom închide și redeschide fluxul video:

cap = cv.VideoCapture(0)
if not cap.isOpened:
  print('--(!)Error opening video capture')
  exit(0)
while True:
  if not cap.isOpened():
    cap.open(0)
  cap.grab()
  width = cap.get(cv.CAP_PROP_FRAME_WIDTH)  # float
  height = cap.get(cv.CAP_PROP_FRAME_HEIGHT)  # float

  print("image size="+str(width)+"::"+str(height))
  ret, frame = cap.retrieve()
  cap.release()

Concluzie

Folosirea unui hardware relativ simplu combinată cu OpenCV, procesarea de imagini și machine learning, poate adăuga feature-uri interesante micilor noastre proiecte. În următoarea ediție TSM, vom discuta cum putem recunoaște cu acest sistem o anumită persoană.