Sviluppo Software
Lo sviluppo del software è la parte più consistente dell'intero progetto e si distingue in:
- Acquisizione ed elaborazione video tramite computer con distrubuzione Linux
- Comunicazione seriale tra PC ed Arduino
- Azionamento dei servomotori tramite Arduino
Acquisizione ed elaborazione video tramite computer con distro Linux
Per la parte di acquisizione ed elaborazione video si è scelto di ricorrere all'uso delle librerie Open Source OpenCV 3.0 beta ed ai suoi “contrib modules”, ovvero ai moduli aggiuntivi delle librerie forniti da terze parti.
Come linguaggio di programmazione si è scelto di utilizzare il C/C++.
Si è scelto di ricorrere alle librerie OpenCV poiché queste offrono al programmatore un'interfaccia molto intuitiva (e gratuita) che facilita l'approccio ad un argomento delicato, come può essere l'elaborazione video real-time.
In particolare, la classe "tracker" definita nelle librerie offre uno strumento che permette in pochi passaggi di risolvere quello che probabilmente è il problema principale in un programma di tracciamento video, ovvero il riconoscimento di una serie di punti notevoli all'interno di un'immagine al fine di distinguere un particolare oggetto.
Per la preparazione dell'ambiente di lavoro si consiglia di visitare i seguenti link:
L'acquisizione video viene svolta sfruttando la classe VideoCapture di OpenCv che mette a disposizione una semplice interfaccia per poter catturare sia le immagini provenienti da un dispositivo di acquisizione esterno (ad esempio una webcam) che le immagini provenienti da un video salvato all'interno del proprio computer.
Vediamo più nel dettaglio il codice:
VideoCapture cap(atoi(argv[2])); if (!cap.isOpened()) { error = ERROR_CAM; printf("WebCam not found\n"); printf("Program terminated with ERROR %d!\n", error); return error; }
Come si può vedere, in poche righe le librerie OpenCV ci permettono di definire un oggetto VideoCapture, di assegnare a questo oggetto il dispositivo di acquisizione video (in questo caso l'identificativo del dispositivo - generalmente il valore 0 - viene passato come argomento al programma con argv[2]) e di verificare la corretta apertura dello stream video tramite il metodo VideoCapture::isOpened.
Il passo successivo consiste nell'elaborazione video. Per portare a termine questo obiettivo l'oggetto VideoCapture viene passato alla funzione SM_Handler responsabile del funzionamento della macchina a stati su cui si basa l'intero programma:
error = SM_Handler(cap, argv[1]);
Il secondo argomento passato alla funzione SM_Handler tramite argv[1] riguarda la comunicazione seriale e verrà trattato in seguito.
La macchina a stati è composta da 4 stati: INIT, CAMERA_MODE, MANUAL e TRACKING.
Il diagramma di transizione degli stati è il seguente:
Nella seguente tabella è riassunto il comportamento del programma in base allo stato e sono indicati gli insiemi delle azioni disponibili per ogni stato:
STATO | COMPORTAMENTO | AZIONI DISPONIBILI |
---|---|---|
INIT | Visualizzazione logo. | ESC per chiudere il programma |
SPAZIO per passare a CAMERA_MODE | ||
CAMERA_MODE | Acquisizione video. | ESC per chiudere il programma |
F per rovesciare orizzontalmente l'immagine | ||
P per scattare un'istantanea | ||
S per attivare/disattivare i servomotori | ||
R per invertire il movimento orizzontale dei servomotori | ||
V per cambiare la velocità di rotazione dei servomotori dopo averla impostata con l'apposita trackbar | ||
M per passare allo stato MANUAL | ||
T per passare allo stato TRACKING | ||
MANUAL | Acquisizione video e movimento manuale dei servomotori. | ESC per passare allo stato CAMERA_MODE |
F per rovesciare orizzontalmente l'immagine | ||
P per scattare un'istantanea | ||
R per invertire il movimento orizzontale dei servomotori | ||
V per cambiare la velocità di rotazione dei servomotori dopo averla impostata con l'apposita trackbar | ||
WASD per muovere i servomotori | ||
C per portare i servomotori alla posizione di default | ||
TRACKING | Acquisizione video e tracciamento di un particolare all'interno dell'immagine. Movimento automatico dei servomotori. | ESC per passare allo stato CAMERA_MODE |
V per cambiare la velocità di rotazione dei servomotori dopo averla impostata con l'apposita trackbar | ||
MOUSE_SX per disegnare un'area di riferimento o per selezionare l'oggetto da tracciare | ||
Concentriamoci ora sullo stato TRACKING e vediamo in particolare come viene realizzato il tracciamento di un determinato dettaglio all'interno di un'immagine.
I Contrib Modules di OpenCV mettono a disposizione la classe Tracker. Questa dispone dei seguenti metodi:
- Tracker::create(const String& trackerType);
Permette di creare un nuovo elemento Tracker utilizzando l'algoritmo scelto (“MIL” oppure “BOOSTING”). Il metodo create viene chiamato dal programma Video Follower ogni volta che si passa dallo stato CAMERA_MODE allo stato TRACKING:
else if (((c == 't') || (c == 'T')) && (servoRun == true)) { if(NULL == (tracker = Tracker::create("MIL"))) { error = ERROR_TRACKER; break; } state = TRACKING; }
- Tracker::init(const Mat& image, const Rect2d& boundingBox);
Inizializza l'oggetto Tracker con il dettaglio da seguire.
Il metodo viene chiamato quando l'utente ha completato la selezione del dettaglio all'interno dell'immagine.
- Tracker::update(const Mat& image, CV_OUT Rect2d& boundingBox);
Tracker con il nuovo frame del video e restituisce le coordinate di un rettangolo contenente l'oggetto da seguire:
if(true == objSelected) { if(false == trackerInitialized) { if(!tracker->init(image, objBox)) { error = ERROR_TRACKER; break; } trackerInitialized = true; } else { if( tracker->update(image, objBox)) { /* Move servos */ } } /* Other code */ }
- Tracker::clear();
Elimina l'oggetto Tracker. Viene chiamata quando si passa dllo stato TRACKING allo stato CAMERA_MODE.
Comunicazione seriale tra PC ed Arduino
La parte di comunicazione seriale tra PC ed Arduino viene portata a termine sfruttando la libreria libSerial installabile in qualsiasi distribuzione Linux.
Dopo aver dichiarato un oggetto di tipo SerialStream sono state scritte le seguenti funzioni:
- char connectServo (char serialName[]);
Apre una comunicazione seriale con il dispositivo indicato dalla stringa serialName (serialName viene passato come parametro argv[1] all'apertura del programma). - void servoSet (char param);
Invia un parametro di configurazione attraverso la porta seriale. I parametri inviabili sono i seguenti:- UP: movimento verso l'alto dei servomotori
- DOWN: movimento verso il basso dei servomotori
- LEFT: movimento a sinistra dei servomotori
- RIGHT: movimento verso destra dei servomotori
- TO_DEFAULT: i servomotori vengono riportati alla posizione predefinita
- SPEED_1: lo spostamento dei servomotori avviene di 1° alla volta
- SPEED_2: lo spostamento dei servomotori avviene di 2° alla volta
- SPEED_3: lo spostamento dei servomotori avviene di 3° alla volta
- SPEED_4: lo spostamento dei servomotori avviene di 4° alla volta
- SPEED_5: lo spostamento dei servomotori avviene di 5° alla volta
if( tracker->update(image, objBox)) { /* Find objBox center */ objCenter.x = objBox.x + objBox.width/2; objCenter.y = objBox.y + objBox.height/2; /* Move servos */ if (objCenter.x < referenceBox.x) { if (false == reversed) servoSet(LEFT); else servoSet(RIGHT); if (objCenter.x > referenceBox.x + referenceBox.width) { if (false == reversed) servoSet(RIGHT); else servoSet(LEFT); } if (objCenter.y < referenceBox.y) servoSet(UP); if (objCenter.y > referenceBox.y + referenceBox.height) servoSet(DOWN); } }
- char servoGetParam (char code);
Richiede lo stato di un parametro di Arduino. I parametri osservabili sono:- X_AXIS: Richiede ad Arduino la posizione corrente del servomotore responsabile del movimento sul piano orizzontale
- Y_AXIS: Richiede ad Arduino la posizione corrente del servomotore responsabile del movimento sul piano verticale
- RIGHT_LIMIT: Richiede ad Arduino qual è la rotazione massima dei servomotori verso destra
- LEFT_LIMIT: Richiede ad Arduino qual è la rotazione massima dei servomotori verso sinistra
- UPPER_LIMIT: Richiede ad Arduino qual è la rotazione massima dei servomotori verso l'alto
- LOWER_LIMIT: Richiede ad Arduino qual è la rotazione massima dei servomotori verso il basso
- void disconnectServo ();
Chiude la comunicazione seriale con i servomotori.
Azionamento dei servomotori tramite Arduino
Ora che il programma è in grado di inviare e ricevere informazioni ad Arduino tramite porta seriale, è necessario fare in modo che lo stesso Arduino sia capace di fare altrettanto.
La scelta dell'utilizzo di Arduino è legata al fatto che l'inetrfacciamento di questo oggetto con un computer, tramite interfaccia seriale, è relativamente semplice ed è disponibile in rete una grandissima quantità di documentazione che facilità ancora di più questa fase del progetto.
Arduino semplifica inoltre la fase di movimento dei servomotori, le librerie ufficiali, infatti, mettono a disposizione
delle funzioni che permettono un'alto livello di astrazione al programmatore.
Come prima cosa, bisogna dichiarare i due servomotori e l'apertura della comunicazione seriale nella funzione setup () dello sketch Arduino nel modo seguente:
void setup () { /* Serial port initialization */ Serial.begin(BAUDRATE); inputString.reserve(200); /* Servos initialization */ Xservo.attach(X_SERVO_PIN); Yservo.attach(Y_SERVO_PIN); }
Ad occuparsi della ricezione dei dati è la funzione SerialEvent (), questa viene chiamata quando viene rilevata una comunicazione verso Arduino. La funzione salva i simboli ricevuti nella stringa inputString.
void serialEvent() { while (Serial.available()) { char inChar = (char)Serial.read(); inputString += inChar; if (inChar == '\n') { stringComplete = true; } } }
Quando viene riconosciuto il carattere di terminazione '\n', dalla stringa viene estratto il parametro che era stato precedentemente inviato dal PC e si traduce il tutto in un'azione dei servomotori od in una comuinicazione da Arduino verso il PC nel seguente modo:
switch(move) { case UP: if(Yposition <= (UPPER_LIMIT - INCREMENT)) { Yposition += INCREMENT; Yservo.write(Yposition); delay(DELAY); } break; case DOWN: if(Yposition >= (LOWER_LIMIT + INCREMENT)) { Yposition -= INCREMENT; Yservo.write(Yposition); delay(DELAY); } break; case RIGHT: if(Xposition >= (RIGHT_LIMIT + INCREMENT)) { Xposition -= INCREMENT; Xservo.write(Xposition); delay(DELAY); } break; case LEFT: if(Xposition <= (LEFT_LIMIT - INCREMENT)) { Xposition += INCREMENT; Xservo.write(Xposition); delay(DELAY); } break; case TO_DEFAULT: digitalWrite(GREEN_LED, LOW); digitalWrite(YELLOW_LED, LOW); digitalWrite(RED_LED, HIGH); while(Xposition > DEFAULT_X) { Xposition--; Xservo.write(Xposition); delay(DELAY); } while(Xposition < DEFAULT_X) { Xposition++; Xservo.write(Xposition); delay(DELAY); } while(Yposition > DEFAULT_Y) { Yposition--; Yservo.write(Yposition); delay(DELAY); } while(Yposition < DEFAULT_Y) { Yposition++; Yservo.write(Yposition); delay(DELAY); } digitalWrite(RED_LED, LOW); digitalWrite(GREEN_LED, HIGH); break; case SPEED_1: INCREMENT = 1; break; case SPEED_2: INCREMENT = 2; break; case SPEED_3: INCREMENT = 3; break; case SPEED_4: INCREMENT = 4; break; case SPEED_5: INCREMENT = 5; break; case GET_XPOS: Serial.write(Xposition - DEFAULT_X); break; case GET_YPOS: Serial.write(Yposition - DEFAULT_Y); break; case GET_LEFT_LIMIT: Serial.write(LEFT_LIMIT - DEFAULT_X); break; case GET_RIGHT_LIMIT: Serial.write(RIGHT_LIMIT - DEFAULT_X); break; case GET_UPPER_LIMIT: Serial.write(UPPER_LIMIT - DEFAULT_Y); break; case GET_LOWER_LIMIT: Serial.write(LOWER_LIMIT - DEFAULT_Y); break; default: break; }