Le suivi de balle en vision binoculaire est une tâche complexe qui peut être réalisée à l’aide de deux servomoteurs pan/tilt. Cependant, il est difficile de mettre en place une telle solution et de la faire fonctionner correctement. Heureusement, il est possible de réaliser cette tâche en utilisant une interface Processing et le module GSVideo. Cet article expliquera comment mettre en place une solution de suivi de balle en vision binoculaire à l’aide de deux servomoteurs pan/tilt et de l’interface Processing avec GSVideo, et comment le faire rapidement.
Suivi de balle en vision binoculaire par 2 tourelles pan/tilt (2 servomoteurs chacune) à partir d’une interface Processing – avec GSVideo – rapide !


1. Présentation
- Sur cette page, on étend à l’axe tilt (inclinaison) le suivi de balle par 2 webcams présenté ici : Suivi de balle en vision binoculaire par 2 tourelles pan (1 servomoteur chacune) à partir d’une interface Processing – avec GSVideo – rapide ! La réactivité du suivi est assurée ici grâce à l’utilisation de la librairie GSVideo pour la capture vidéo avec l’interface Processing, ce qui permet également de diminuer la sollicitation de la CPU. La librairie openCV est utilisée uniquement pour la reconnaissance de la balle colorée à partir du flux vidéo obtenu par GSVideo.
- Pour chaque webcam, les 2 servomoteurs pan et tilt connectés à la carte Arduino sont contrôlés par l’interface Processing qui réalise une reconnaissance d’objet coloré (une balle de ping pong colorée) à partir de la capture indépendante de chacun des flux vidéo et traitement d’image avec la librairie opencv. L’ensemble réalise un suivi d’objet en temps réel en vision binoculaire dans les 2 axes (inclinaison et panoramique) donnant un rendu quasi-« vivant » des 2 « yeux webcams ».
- L’interface Processing envoie sur le port série une chaine de caractères sous la forme servo1Pan(000) où 000 est la valeur de l’angle de positionnement du servomoteur. Chaque servomoteur est identifié par un numéro.
- Le programme Arduino analyse la chaine reçue et en extrait la valeur d’angle reçue
- Chaque servomoteur est positionné dans la position voulue
Ce programme utilise les fonctionnalités suivantes :
- Utilise 4 servomoteurs, 2 par webcam
Ressources utiles associées à ce programme :
- La librairie Servo – pour contrôler les servomoteurs.
- La librairie openCV pour Processing
- la librairie GSVideo pour Processing
- Les pages suivantes :
Pour info, j’ai utilisé ici :
- Intel Dual Core Quad à 2.8Ghz
- sous Ubuntu 10.04 LTS
- webcams : Hercules DualPix
- Processing 1.5
- Arduino 22
- librairie GSVideo 0.9
- librairie openCV
2. Schéma fonctionnel

- Le programme Arduino est capable de reconnaître les chaînes suivantes en provenance de Processing :
- servo1Pan(000) avec 000 = angle absolu en degrés pour un positionnement immédiat
- servo1Panto(000) avec 000 = angle absolu en degrés pour un positionnement progressif avec delai vitesse
- servo1PantoR(000) avec 000 = angle relatif en degrés pour un positionnement progressif avec delai vitesse
- vitesse(000) avec 000 = delai en millisecondes à utiliser entre 2 positions intermédiaires (10 = rapide, 50 = lent)
- Idem pour servo2.
- Idem pour les servo1Tilt et servo2PanTilt
3. Matériel Nécessaire
3.1 L’espace de développement Arduino
- … pour éditer, compiler le programme et programmer la carte Arduino.

3.2 Le matériel électronique suivant pour réaliser le montage associé
- Idéalement, un Shield kit EasyCard pour utilisation simplifiée jusqu’à 20 servos et/ou 6 capteurs analogiques – Easyrobotics

3.3 Le matériel mécanique suivant pour réaliser la mécanique associée
- 4 servomoteurs standards type Futaba S3003




- Pour étalonner un servomoteur dont on ne connaît pas les caractéristiques, voir le page : https://www.mon-club-elec.fr/pmwiki_mon_club_elec/pmwiki.php?n=MAIN.ArduinoExpertSerieDepuisPCPositionServomoteur

- deux webcams connectées au PC et montée chacune sur sa tourelle pan/tilt (fixée sur la fourche mobile de la briaque Easy du second servomoteur) :


4. Instructions de montage
- Enficher la carte d’extension EasyCard broche à broche sur la carte Arduino
- Connecter le servomoteur « pan » (rotation panoramique) de la webcam 1 sur la broche 8
- Connecter le servomoteur « tilt » (rotation inclinaison) de la webcam 1 sur la broche 9
- Connecter le servomoteur « pan » (rotation panoramique) de la webcam 2 sur la broche 10
- Connecter le servomoteur « tilt » (rotation inclinaison) de la webcam 2 sur la broche 11
- Les 2 webcams doivent être connectées au PC chacune sur un port USB.
- Dans le cas d’une carte Arduino :
- l’intensité maximale disponible sur une broche est de 40mA
- l’intensité maximale cumulée pour l’ensemble des broches est 200mA
- l’instensité maximale que peut fournir l’alimentation 5V de la carte est 500mA.
- Par conséquent, avec une carte Arduino :
- En ce qui concerne la ligne de commande du servomoteur par la carte Arduino :
- on pourra commander directement autant de servomoteur que l’on veut (broche de commande) avec une carte Arduino, le nombre maximal étant 200mA / I commande, soit 100 environ dans le cas du Futaba S3003, ce qui est largement supérieur au nombre de broches de la carte.
- Il n’y aura par ailleurs pas besoin d’amplification (type ULN 2803) sur la ligne de commande du servomoteur même si l’on utilise un grand nombre de servomoteurs.
- En ce qui concerne l’alimentation principale des servomoteurs par une carte Arduino
- on ne peut alimenter que 3 à 4 servomoteurs simultanément par l’alimentation 5V de la carte Arduino, le nombre maximal étant 500mA / I fonctionnement = 500 / 120 = 4 servomoteurs dans le cas du Futaba S3003.
- Une alimentation externe sera indispensable dès que l’on dépassera ce nombre pour ne pas risquer de « griller » la carte Arduino.
- En ce qui concerne la ligne de commande du servomoteur par la carte Arduino :
Pour plus de détails, voir : Principes d’utilisation des alimentations avec une carte Arduino et des servomoteurs
5. Le schéma théorique du montage
Le schéma théorique du montage (cliquer pour agrandir)
6. Le circuit du montage
Le schéma du montage à réaliser (cliquer pour agrandir)
7. Explication du programme
- L’interface Processing :
- assure de façon indépendante la capture du flux vidéo de chaque webcam,
- assure la reconnaissance de forme de la balle colorée sur chaque flux vidéo
- extrait la position de la forme dans l’image vidéo. On se base au besoin sur plusieurs images successives pour moyenner la position de l’objet et s’affranchir du « flou » lié au déplacement de la webcam.
- envoie sur le port série une chaine de caractères sous la forme servoPan1toR(000) où 000 est la valeur de l’angle de positionnement relatif du servomoteur. Chaque servomoteur est positionné.
- Le programme Arduino analyse la chaine reçue et en extrait la valeur d’angle reçue pour chaque servomoteur
- Le servomoteur de chaque webcam est positionné dans la position voulue
8. Mise en oeuvre du programme
8.1 Préparation du montage et programmation de la carte Arduino :
- Commencer par réaliser le montage indiqué sur plaque d’expérimentation
- Ensuite, programmer la carte Arduino avec ce programme (en bas de page) selon la procédure habituelle
8.2 Fonctionnement
- déplacer la balle colorée devant les webcams : les servomoteurs Pan et Tilt bougent automatiquement afin que la balle colorée reste au centre de l’image. Les 2 webcams bougent de façon concommittante afin de garder l’objet de façon centrée, réalisant la vision binoculaire. Il pourra être intéressant dans un second temps de corréler les angles des servomoteurs pour en déduire la distance de l’objet par exemple.
9. Le programme complet en langage Arduino
A copier/coller directement dans l’éditeur Arduino
// Trame de code générée par le générateur de code Arduino
// du site www.mon-club-elec.fr
// Auteur du Programme : X. HINAULT – Tous droits réservés
// Programme écrit le : 14/08/2011.
// ——- Licence du code de ce programme —–
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License,
// or any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// //////////////////// PRESENTATION DU PROGRAMME ////////////////////
// ——– Que fait ce programme ? ———
/* Quatre servomoteurs connectés à la carte Arduino sont contrôlés
à partir du PC (interface Processing) :
– 2 par tourelle pan/tilt
Controle par chaine de la forme : servoPan(000) avec 000 = angle en degrés
*/
// — Fonctionnalités utilisées —
// Utilise / fonctionne avec une interface Processing coté PC
// Utilise 4 servomoteurs
// ——– Circuit à réaliser ———
// ******* ATTENTION : il est possible de connecter directement 2 ou 3 servomoteurs sur la carte Arduino
// Connecter un servomoteur PAN sur la broche 8
// Connecter un servomoteur TILT sur la broche 9
// /////////////////////////////// 1. Entête déclarative ///////////////////////
// A ce niveau sont déclarées les librairies incluses, les constantes, les variables, les objets utiles…
// — Déclaration des constantes —
// — Inclusion des librairies —
#include <Servo.h> // librairie pour servomoteur
// — Déclaration des constantes utiles —
const int APPUI=LOW; // constante pour tester état BP
//— Constantes utilisées avec le servomoteur
const int ANGLE_MIN=0; // angle position MIN en degrés
const int POS_MIN=550; // largeur impulsion pour position ANGLE_MIN degrés du servomoteur
// POS_MIN=550 pour ANGLE_MIN=0 avec un futaba S3003
const int ANGLE_MAX=172; // angle position MAX en degrés
int POS_MAX=2400; // largeur impulsion pour position ANGLE_MAX degrés du servomoteur
// POS_MAX=2400 pour ANGLE_MAX=172 pour futaba S3003
// pour étalonner un servomoteur, voir la page :
//https://www.mon-club-elec.fr/pmwiki_mon_club_elec/pmwiki.php?n=MAIN.ArduinoExpertSerieDepuisPCPositionServomoteur
// — Déclaration des constantes des broches E/S numériques —
//— tourelle 1
const int broche_servoPan_1=8; // Constante pour la broche 8
const int broche_servoTilt_1=9; // Constante pour la broche 9
//—- tourelle 2
const int broche_servoPan_2=10; // Constante pour la broche 10
const int broche_servoTilt_2=11; // Constante pour la broche 11
// — Déclaration des constantes des broches analogiques —
// — Déclaration des variables globales —
boolean debug=false; // active ou non message de débug
int octetReception=0; // variable de stockage des valeurs reçues sur le port Série
long nombreReception=0; // variable de stockage du nombre reçu sur le port Série
long nombreReception0=0; // variable de stockage du dernier nombre reçu sur le port Série
String chaineReception=« »; // déclare un objet String vide pour reception chaine
int valeur=0; // variable utile
long param=0; // paramètre transmis
//— tourelle 1 —
float angleServoPan_1=90; // variable de position du servo Pan en degrés
float angleServoTilt_1=20; // variable de position du servo Tilt en degrés
float angleServoPan0_1=90; // variable de la dernière position du servo Pan en degrés
float angleServoTilt0_1=20; // variable de la dernière position du servo Tilt en degrés
//— tourelle 2 —
float angleServoPan_2=90; // variable de position du servo Pan en degrés
float angleServoTilt_2=20; // variable de position du servo Tilt en degrés
float angleServoPan0_2=90; // variable de la dernière position du servo Pan en degrés
float angleServoTilt0_2=20; // variable de la dernière position du servo Tilt en degrés
int vitesse=0; // variable utilisée pour délai entre 2 lecture port Série
// — Déclaration des objets utiles pour les fonctionnalités utilisées —
//— Création objet servomoteur
Servo servoPan_1; // crée un objet servo pour contrôler le servomoteur 1
Servo servoTilt_1; // crée un objet servo pour contrôler le servomoteur 2
Servo servoPan_2; // crée un objet servo pour contrôler le servomoteur 1
Servo servoTilt_2; // crée un objet servo pour contrôler le servomoteur 2
// ////////////////////////// 2. FONCTION SETUP = Code d’initialisation //////////////////////////
// La fonction setup() est exécutée en premier et 1 seule fois, au démarrage du programme
void setup() { // debut de la fonction setup()
// — ici instructions à exécuter 1 seule fois au démarrage du programme —
// ——- Initialisation fonctionnalités utilisées ——-
Serial.begin(115200); // initialise connexion série à 115200 bauds
// IMPORTANT : régler le terminal côté PC avec la même valeur de transmission
//— Initialisation Servomoteur
servoPan_1.attach(broche_servoPan_1); // attache l’objet servo à la broche de commande du servomoteur Pan
servoTilt_1.attach(broche_servoTilt_1); // attache l’objet servo à la broche de commande du servomoteur Tilt
servoPan_2.attach(broche_servoPan_2); // attache l’objet servo à la broche de commande du servomoteur Pan
servoTilt_2.attach(broche_servoTilt_2); // attache l’objet servo à la broche de commande du servomoteur Tilt
// ——- Broches en sorties numériques ——-
pinMode (broche_servoPan_1,OUTPUT); // Broche broche_servoPan configurée en sortie
pinMode (broche_servoTilt_1,OUTPUT); // Broche broche_servoPan configurée en sortie
pinMode (broche_servoPan_2,OUTPUT); // Broche broche_servoPan configurée en sortie
pinMode (broche_servoTilt_2,OUTPUT); // Broche broche_servoPan configurée en sortie
// ——- Broches en entrées numériques ——-
// ——- Activation si besoin du rappel au + (pullup) des broches en entrées numériques ——-
// ——- Initialisation des variables utilisées ——-
// ——- Codes d’initialisation utile ——-
servoPan_1.writeMicroseconds(angle(angleServoPan_1)); // crée impulsion à partir valeur angle – plus précis que write()
servoTilt_1.writeMicroseconds(angle(angleServoTilt_1)); // crée impulsion à partir valeur angle – plus précis que write()
servoPan_2.writeMicroseconds(angle(angleServoPan_2)); // crée impulsion à partir valeur angle – plus précis que write()
servoTilt_2.writeMicroseconds(angle(angleServoTilt_2)); // crée impulsion à partir valeur angle – plus précis que write()
Serial.println(« Arduino OK »); // debug
delay(200);
} // fin de la fonction setup()
// ********************************************************************************
////////////////////////////////// 3. FONCTION LOOP = Boucle sans fin = coeur du programme //////////////////
// la fonction loop() s’exécute sans fin en boucle aussi longtemps que l’Arduino est sous tension
void loop(){ // debut de la fonction loop()
//—- code type réception chaine sur le port série —
while (Serial.available()>0) { // tant qu’un octet en réception
octetReception=Serial.read(); // Lit le 1er octet reçu et le met dans la variable
if (octetReception==10) { // si Octet reçu est le saut de ligne
if (debug) Serial.print (« Chaine recue= »),Serial.print(chaineReception); // affiche la chaine recue
analyseChaine(chaineReception); // appelle la fonction d’analyse de la chaine en réception
chaineReception=« »; //RAZ le String de réception
break; // sort de la boucle while
}
else { // si le caractère reçu n’est pas un saut de ligne
chaineReception=chaineReception+char(octetReception); // ajoute le caratère au String
}
} // fin tant que octet réception
//—– une fois que le saut de ligne est reçu, on sort du While et on se positionne ici
//chaineReception= » »;
//————- fin analyse chaine —————
delay(vitesse);
} // fin de la fonction loop() – le programme recommence au début de la fonction loop sans fin
// ********************************************************************************
// ////////////////////////// FONCTIONS DE GESTION DES INTERRUPTIONS ////////////////////
// ////////////////////////// AUTRES FONCTIONS DU PROGRAMME ////////////////////
//————- fonction calibrage impulsion servomoteur à partir valeur angle en degrés
//——- mieux avec float —–
float angle(float valeur_angle) {
float impuls=0;
impuls=map(valeur_angle,ANGLE_MIN,ANGLE_MAX,POS_MIN, POS_MAX);
return impuls;
} // fin fonction impulsion servomoteur
//————- fonction d’analyse de la chaine reçue sur le port série —-
//———— analyseChaine —————
void analyseChaine(String chaineRecue) { // fonction d’analyse de la chaine recue
// —- analyse de la chaine recue sur le port Série —-
chaineReception=chaineReception.trim(); // enlève les espaces
//xxxxxxxxxxxxxxxxxxx instructions sans paramètres xxxxxxxxxxxx
//xxxxxxxxxxxxxxxxxxxx instructions avec paramètres xxxxxxxxxxxxxxx
// info : la valeur numérique extraite par testInstruction() est stockée dans variable globale param
//================= instructions paramètres généraux =============
//————– test instruction vitesse(xxx) ———–
if (testInstruction(« vitesse(« )==true) { // si instruction reçue valide
vitesse=param; // change valeur vitesse (= durée delay en ms)
Serial.print(« vitesse = « ), Serial.println(vitesse);
} // fin test vitesse(xxx)
//================ instructions servo Pan 1 =========
//————– test instruction servoPan1(xxx) ———–
if (testInstruction(« servoPan1(« )==true) { // si instruction reçue valide
servoPan_1.writeMicroseconds(angle(param)); // crée impulsion à partir valeur angle – plus précis que write()
angleServoPan0_1=param; // mémorise angle actuel
} // fin test servoPan1(xxx)
//————– test instruction servoPan1to(xxx) ———– // positionnement progressif
if (testInstruction(« servoPan1to(« )==true) { // si instruction reçue valide
// void servoTo( Servo toServo, float fromAngle, float toAngle, int toVitesse, int toPas)
servoTo( servoPan_1, angleServoPan0_1, param, vitesse, 1); //— positionnement progressif par pas fixe de 1 degré —
angleServoPan0_1=param; // met à jour l’angle courant servo avec valeur extraite par testInstruction()
if (debug) Serial.print(« angleServoPan0_1 = « ), Serial.println(angleServoPan0_1);
} // fin test servoPan1to(xxx)
//————– test instruction servoPan1toR(xxx) ———– // positionnement progressif
if (testInstruction(« servoPan1toR(« )==true) { // si instruction reçue valide
// void servoToR( Servo toServo, float fromAngle, float toAngle, int toVitesse, int toPas)
servoToR( servoPan_1, angleServoPan0_1, param, vitesse, 1); //— positionnement progressif par pas fixe de 1 degré —
angleServoPan0_1=param+angleServoPan0_1; // met à jour l’angle courant servo
if (debug) Serial.print(« angleServoPan0_1 = « ), Serial.println(angleServoPan0_1);
} // fin test servoPan1toR(xxx)
//================ instructions servo Pan 2 =========
//————– test instruction servoPan2(xxx) ———–
if (testInstruction(« servoPan2(« )==true) { // si instruction reçue valide
servoPan_2.writeMicroseconds(angle(param)); // crée impulsion à partir valeur angle – plus précis que write()
angleServoPan0_2=param; // mémorise angle actuel
} // fin test servoPan2(xxx)
//————– test instruction servoPan2to(xxx) ———– // positionnement progressif
if (testInstruction(« servoPan2to(« )==true) { // si instruction reçue valide
// void servoTo( Servo toServo, float fromAngle, float toAngle, int toVitesse, int toPas)
servoTo( servoPan_2, angleServoPan0_2, param, vitesse, 1); //— positionnement progressif par pas fixe de 1 degré —
angleServoPan0_2=param; // met à jour l’angle courant servo avec valeur extraite par testInstruction()
if (debug) Serial.print(« angleServoPan0_2 = « ), Serial.println(angleServoPan0_2);
} // fin test servoPan2to(xxx)
//————– test instruction servoPan2toR(xxx) ———– // positionnement progressif
if (testInstruction(« servoPan2toR(« )==true) { // si instruction reçue valide
// void servoToR( Servo toServo, float fromAngle, float toAngle, int toVitesse, int toPas)
servoToR( servoPan_2, angleServoPan0_2, param, vitesse, 1); //— positionnement progressif par pas fixe de 1 degré —
angleServoPan0_2=param+angleServoPan0_2; // met à jour l’angle courant servo
if (debug) Serial.print(« angleServoPan0_2 = « ), Serial.println(angleServoPan0_2);
} // fin test servoPan2toR(xxx)
//================ instructions servo Tilt =========
//—- webcam 1 —
//————– test instruction servo1Tilt(xxx) ———–
if (testInstruction(« servo1Tilt(« )==true) { // si instruction reçue valide
servoTilt_1.writeMicroseconds(angle(param)); // crée impulsion à partir valeur angle – plus précis que write()
angleServoTilt0_1=param; // mémorise angle actuel
} // fin servoTilt1(xxx)
//————– test instruction servoTilt1to(xxx) ———– // positionnement progressif
if (testInstruction(« servoTilt1to(« )==true) { // si instruction reçue valide
// void servoTo( Servo toServo, float fromAngle, float toAngle, int toVitesse, int toPas)
servoTo( servoTilt_1, angleServoTilt0_1, param, vitesse, 1); //— positionnement progressif par pas fixe de 1 degré —
angleServoTilt0_1=param; // met à jour l’angle courant servo avec valeur extraite par testInstruction()
if (debug) Serial.print(« angleServoTilt0_1 = « ), Serial.println(angleServoTilt0_1);
} // fin servoTilt1to(xxx)
//————– test instruction servoTilt1toR(xxx) ———– // positionnement progressif
if (testInstruction(« servoTilt1toR(« )==true) { // si instruction reçue valide
// void servoToR( Servo toServo, float fromAngle, float toAngle, int toVitesse, int toPas)
servoToR( servoTilt_1, angleServoTilt0_1, param, vitesse, 1); //— positionnement progressif par pas fixe de 1 degré —
angleServoTilt0_1=param+angleServoTilt0_1; // met à jour l’angle courant servo
if (debug) Serial.print(« angleServoTilt0_1 = « ), Serial.println(angleServoTilt0_1);
} // fin test servoTilt1toR(xxx)
//—- webcam 2 —
//————– test instruction servo1Tilt(xxx) ———–
if (testInstruction(« servo2Tilt(« )==true) { // si instruction reçue valide
servoTilt_2.writeMicroseconds(angle(param)); // crée impulsion à partir valeur angle – plus précis que write()
angleServoTilt0_2=param; // mémorise angle actuel
} // fin servoTilt2(xxx)
//————– test instruction servoTilt2to(xxx) ———– // positionnement progressif
if (testInstruction(« servoTilt2to(« )==true) { // si instruction reçue valide
// void servoTo( Servo toServo, float fromAngle, float toAngle, int toVitesse, int toPas)
servoTo( servoTilt_2, angleServoTilt0_2, param, vitesse, 1); //— positionnement progressif par pas fixe de 1 degré —
angleServoTilt0_2=param; // met à jour l’angle courant servo avec valeur extraite par testInstruction()
if (debug) Serial.print(« angleServoTilt0_2 = « ), Serial.println(angleServoTilt0_2);
} // fin servoTilt2to(xxx)
//————– test instruction servoTilt2toR(xxx) ———– // positionnement progressif
if (testInstruction(« servoTilt2toR(« )==true) { // si instruction reçue valide
// void servoToR( Servo toServo, float fromAngle, float toAngle, int toVitesse, int toPas)
servoToR( servoTilt_2, angleServoTilt0_2, param, vitesse, 1); //— positionnement progressif par pas fixe de 1 degré —
angleServoTilt0_2=param+angleServoTilt0_2; // met à jour l’angle courant servo
if (debug) Serial.print(« angleServoTilt0_2 = « ), Serial.println(angleServoTilt0_2);
} // fin test servoTilt2toR(xxx)
} // —————- fin fonction analyse chaine ———————-
//————— testInstruction : test si instruction de la forme instruction(xxx) ————
boolean testInstruction(String chaineTest) { // reçoit chaine et renvoie true si instruction valide
long posRef=chaineTest.length();// position de référence pour analyse (xxx)
if (chaineReception.substring(0,posRef)==chaineTest) { // si reçoit l’instruction chaineTest(000)
// nb substring : dernier caractere exclu
if (debug) Serial.print(« Arduino va executer : « );
if (debug) Serial.print(chaineTest); // affiche
if (chaineReception.substring(posRef,posRef+1)==« -« ) { // si signe – présent on décale de 1 position la prise en compte du nombre xxx
param=(–1)*stringToLong(chaineReception.substring(posRef+1,posRef+4)); // extraction valeur 3 chiffres à partir caractères et * par -1
posRef=posRef+1; // modif valeur posRef
} // fin if
else { // si pas de signe –
param=stringToLong(chaineReception.substring(posRef,posRef+3)); // extraction valeur 3 chiffres à partir caractères
} // fin else
if (debug) Serial.print(param); // affiche
if (chaineReception.substring(posRef+3,posRef+4)==« ) ») { // si fermeture parenthèse = instruction valide
if (debug) Serial.println(« ) »); // affiche
if (debug) Serial.println(« Instruction valide ! »); // affiche
return(true); // renvoie true si instruction valide
} // fin si fermeture parenthèse
else {
if (debug) Serial.println(« Instruction invalide ! »); // affiche
return(false); // renvoie false si instruction invalide
} // fin else
} // fin si reçoit l’instruction chaineTest(000)
} // fin fonction testInstruction
//——————- fin test instruction ————
// ———- fonction de conversion d’un String numérique en long
long stringToLong(String chaineLong) { // fonction conversion valeur numérique String en int
long nombreLong=0; // variable locale
int valeurInt=0; // variable locale
for (int i=0; i<chaineLong.length(); i++) { // défile caractères de la chaine numérique
valeurInt=chaineLong.charAt(i); // extrait le caractère ASCII à la position voulue – index 0 est le 1er caractère
valeurInt=valeurInt–48; // obtient la valeur décimale à partir de la valeur ASCII
if (valeurInt>=0 && valeurInt<=9) { // si caractère est entre 0 et 9
nombreLong=(nombreLong*10)+valeurInt;
} // fin si caractère est entre 0 et 9
} // fin for défile caractères
return (nombreLong); // renvoie valeur numérique
} // ———- fin stringToLong ————
//— fonction de positionnement progressif du servomoteur par pas fixe —–
void servoTo( Servo toServo, float fromAngle, float toAngle, int toVitesse, int toPas) {
//— positionnement progressif par pas fixe de 1 degré —
int delta=toAngle–fromAngle; // variation d’angle
if (debug) Serial.print(« delta = « ), Serial.println(delta);
if (delta>=0) { // si variation positive
for (int i=0; i<delta; i++) { // defile n positions pour atteindre angle final dans sens positif
fromAngle=fromAngle+1; // ajoute cran
toServo.writeMicroseconds(angle(fromAngle)); // crée impulsion à partir valeur angle – plus précis que write()
//Serial.print(« angle courant servo = « ), Serial.println(fromAngle);
if (vitesse)delay(vitesse); // pause entre chaque positionnement
} // fin for
} // fin if
else { // si variation négative
for (int i=-delta; i>0; i—) { // defile n positions pour atteindre angle final dans sens négatif
fromAngle=fromAngle–1; // ajoute cran
toServo.writeMicroseconds(angle(fromAngle)); // crée impulsion à partir valeur angle – plus précis que write()
if (debug)Serial.print(« angle courant servo = « ), Serial.println(fromAngle);
if (vitesse)delay(vitesse); // pause entre chaque positionnement
} // fin for
} // fin else
}
//— fonction de positionnement progressif du servomoteur par pas fixe – angle relatif à la position courante —–
void servoToR( Servo toServo, float fromAngle, float toAngle, int toVitesse, int toPas) {
//— positionnement progressif par pas fixe de 1 degré —
int delta=toAngle; // variation d’angle correspond à l’angle transmis
if (debug) Serial.print(« delta = « ), Serial.println(delta);
if (delta>=0) { // si variation positive
for (int i=0; i<delta; i++) { // defile n positions pour atteindre angle final dans sens positif
fromAngle=fromAngle+1; // ajoute cran
toServo.writeMicroseconds(angle(fromAngle)); // crée impulsion à partir valeur angle – plus précis que write()
if (debug) Serial.print(« angle courant servo = « ), Serial.println(fromAngle);
if (vitesse) delay(vitesse); // pause entre chaque positionnement
} // fin for
} // fin if
else { // si variation négative
for (int i=-delta; i>0; i—) { // defile n positions pour atteindre angle final dans sens négatif
fromAngle=fromAngle–1; // ajoute cran
toServo.writeMicroseconds(angle(fromAngle)); // crée impulsion à partir valeur angle – plus précis que write()
//Serial.print(« angle courant servo = « ), Serial.println(fromAngle);
if (debug) delay(vitesse); // pause entre chaque positionnement
} // fin for
} // fin else
}
// ////////////////////////// Fin du programme ////////////////////
10. Le programme Processing
10.1 Description
- L’interface Processing :
- assure de façon indépendante la capture du flux vidéo de chaque webcam,
- assure la reconnaissance de forme de la balle colorée sur chaque flux vidéo
- extrait la position de la forme dans l’image vidéo. On se base au besoin sur plusieurs images successives pour moyenner la position de l’objet et s’affranchir du « flou » lié au déplacement de la webcam.
- envoie sur le port série une chaine de caractères sous la forme servoPan1toR(000) où 000 est la valeur de l’angle de positionnement relatif du servomoteur. Chaque servomoteur est positionné.
10.2 Ressources utiles
- la librairie GSVideo pour la capture vidéo
- la librairie openCV pour la reconnaissance visuelle
10.3 Le programme complet en langage Processing
A copier/coller directement dans l’éditeur Processing
// généré avec le générateur de code Processing
// du site www.mon-club-elec.fr
// par X. HINAULT – tous droits réservés
// Programme écrit le : 14/8/2011.
// ——- Licence du code de ce programme : GPL v3—–
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License,
// or any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/////////////// Description du programme ////////////
// Utilise la librairie GSVideo de capture et lecture vidéo
// Utilise la librairie OpenCV de capture vidéo et reconnaissance visuelle
// XXXXXXXXXXXXXXXXXXXXXX ENTETE DECLARATIVE XXXXXXXXXXXXXXXXXXXXXX
// inclusion des librairies utilisées
import processing.serial.*; // importe la librairie série processing
import codeanticode.gsvideo.*; // importe la librairie vidéo GSVideo qui implémente GStreamer pour Processing (compatible Linux)
// librairie comparable à la librairie native vidéo de Processing (qui implémente QuickTime..)- Voir Reference librairie Video Processing
// cette librairie doit être présente dans le répertoire modes/java/libraries du répertoire Processing (1-5)
// voir ici : http://gsvideo.sourceforge.net/
// et ici : http://codeanticode.wordpress.com/2011/05/16/gsvideo-09-release
import hypermedia.video.*; // importe la librairie OpenCV qui implémente la capture vidéo et la reconnaissance visuelle pour Processing
// cette librairie doit être présente dans le répertoire modes/java/libraries du répertoire Processing (1-5)
// voir ici : http://ubaa.net/shared/processing/opencv/
import java.awt.Rectangle;
// impporte l’objet rectangle du langage Java qui est utilisé par certaines fonction de la librairie openCV
// l’objet rectangle fournit les champs x,y du centre et hauteur/largeur (height/width) du rectangle
// voir ici : http://download.oracle.com/javase/1.4.2/docs/api/java/awt/Rectangle.html
// déclaration objets
// — port Série —
Serial myPort; // Création objet désignant le port série
GSCapture[] cam = new GSCapture [2]; // déclare 2 objet GSCapture représentant chacun une webcam
// L’objet GSCapture étend PImage – se comporte comme un conteneur des frames issues de la webcam
OpenCV opencv; // déclare un objet OpenCV principal
// déclaration variables globales
int xmin, xmax, ymin, ymax; // coordonnées de la zone à tester
int[] comptImg={0,0}; // variable de comptage des images pour moyenne centre (pour chaque webcam)
int nbImg=1; // nombre images à prendre en compte avant mouvement
//— plus on prend en compte d’image, plus c’est stable, mais c’est moins rapide
long[] moyX={0,0}; // pour calcul moyenne X
long[] moyY={0,0}; // pour calcul moyenne Y
//—— déclaration des variables de couleur utiles —-
int jaune=color(255,255,0);
int vert=color(0,255,0);
int rouge=color(255,0,0);
int bleu=color(0,0,255);
int noir=color(0,0,0);
int blanc=color(255,255,255);
int bleuclair=color(0,255,255);
int violet=color(255,0,255);
// variable pour la taille de la capture video
// plus c’est petit, plus c’est rapide ! avec une bonne efficacité quand même
int widthCapture=160; // largeur capture
int heightCapture=120; // hauteur capture
int fpsCapture=30; // framerate (image/secondes) pour la capture video
// XXXXXXXXXXXXXXXXXXXXXX Fonction SETUP XXXXXXXXXXXXXXXXXXXXXX
void setup(){ // fonction d’initialisation exécutée 1 fois au démarrage
// —- initialisation paramètres graphiques utilisés
colorMode(RGB, 255,255,255); // fixe format couleur R G B pour fill, stroke, etc…
fill(0,0,255); // couleur remplissage RGB – noFill() si pas de remplissage
stroke (0,0,0); // couleur pourtour RGB – noStroke() si pas de pourtour
rectMode(CORNER); // origine rectangle : CORNER = coin sup gauche | CENTER : centre
imageMode(CORNER); // origine image : CORNER = coin sup gauche | CENTER : centre
ellipseMode(CENTER); // origine cercles / ellipses : CENTER : centre (autres : RADIUS, CORNERS, CORNER
//strokeWeight(0); // largeur pourtour
frameRate(fpsCapture);// Images par seconde – The default rate is 60 frames per second
// — initialisation fenêtre de base —
size(widthCapture*cam.length, heightCapture); // ouvre une fenêtre xpixels x ypixels
background(0,0,0); // couleur fond fenetre
// — initialisation des objets et fonctionnalités utilisées —
//————- initialisation port série —-
println(Serial.list()); // affiche dans la console la liste des ports séries
// Vérifier que le numéro du port série utilisé est le meme que celui utilisé avec Serial.list()[index]
myPort = new Serial(this, Serial.list()[0], 115200); // Initialise une nouvelle instance du port Série
//myPort = new Serial(this, « /dev/ttyACM0 », 115200); // Initialise une nouvelle instance du port Série
myPort.bufferUntil(‘\n‘); // attendre arrivée d’un saut de ligne pour générer évènement série
//======== Initialisation Objets GSVideo (capture et/ou lecture video =========
//for (int i=0; i<2; i++){ // défile les webcam
// webcam cam[0] – préférer configuration ligne à ligne à une boucle pour le nom des webcams
// GSCapture(this, int requestWidth, int requestHeight, [int frameRate], [String sourceName], [String cameraName])
cam[0] = new GSCapture(this, widthCapture, heightCapture,fpsCapture,« v4l2src »,« /dev/video1 »); // Initialise objet GSCapture désignant webcam
// largeur et hauteur doivent être compatible avec la webcam – typiquement 160×120 ou 320×240 ou 640×480…
// Meilleurs résultats avec framerate webcam entre 20 et 30 et frameRate programme idem ou multiple plus grand (40 pour 20 par ex)
// la liste des webcam installées sous Ubuntu (Gnu/Linux) est donnée par la commande : ls /dev/video*
//—- ATTENTION – être sûr que les indices des caméras correspondent aux servomoteurs correspondant — ++
// cam1.play(); // démarre objet GSCapture = la webcam – version GSVideo avant 0.9
cam[0].start(); // démarre objet GSCapture = la webcam – version GSVideo après 0.9
//}
cam[1] = new GSCapture(this, widthCapture, heightCapture,fpsCapture,« v4l2src »,« /dev/video3 »); // Initialise objet GSCapture désignant webcam
cam[1].start(); // démarre objet GSCapture = la webcam – version GSVideo après 0.9
//======== Initialisation Objets OpenCV (vidéo et reconnaissance visuelle =========
opencv = new OpenCV(this); // initialise objet OpenCV à partir du parent This
opencv.allocate(widthCapture,heightCapture); // crée le buffer image de la taille voulue
// la capture de flux multiples n’est pas possible avec openCV.
// Ici, on utilise GSVideo pour capturer le flux video et OpenCV pour l’analyse d’image seulement
// Au final, on abouti à une amélioration significative de la rapidité d’exécution
} // fin fonction Setup
// XXXXXXXXXXXXXXXXXXXXXX Fonction Draw XXXXXXXXXXXXXXXXXXXX
void draw() { // fonction exécutée en boucle
// Code type capture GSVideo – utilisation possible aussi de captureEvent()
for (int i=0; i<2; i++){
if (cam[i].available() == true) { // si une nouvelle frame est disponible sur la webcam
cam[i].read(); // acquisition d’un frame
//image(cam1, 0, 0); // affiche image
set(widthCapture*i, 0, cam[i]); // affiche image – plus rapide
analyseImage(cam[i].get(), widthCapture*i, 0, i); // appelle fonction avec coordonnées pour afficher image resultat
//img1=cam1.get(); // récupère l’image GS video dans Pimage
//opencv.copy(img1); // charge l’image dans le buffer openCV
//opencv.copy(cam1.get()); // autre possibilité – charge directement l’image GSVideo dans le buffer openCV
} // fin if available
} // fin for
// while(true); // stoppe boucle draw
} // fin de la fonction draw()
// XXXXXXXXXXXXXXXXXXXXXX Autres Fonctions XXXXXXXXXXXXXXXXXXXXXX
//— évènement capture vidéo avec librairie GSVideo—
//void captureEvent(GSCapture cam) { // est appelée lorsqu’une capture (nouvelle frame) survient – cam quelconque
// cf doc librairie Video Processing – cf exemple Capture LivePocky
// bloque pour plusieurs webcams
// cette fonction est appelée à chaque fois qu’une nouvelle frame est disponible, quelque soit la caméra
// utiliser des conditions pour tester la caméra disponible
//if (cam1.available() == true) cam1.read(); // acquisition d’une nouvelle frame
// } // fin fonction évènement captureEvent()
//————- Fonction de gestion des évènements série —-
void serialEvent (Serial myPort) { // fonction appelée lors de la survenue d’un évènement série
// ******** Gestion de la valeur reçue sur le port série : **********
String inString = myPort.readStringUntil(‘\n‘); // chaine stockant la chaîne reçue sur le port Série
// saut de ligne en marque de fin
if (inString != null) { // si la chaine recue n’est pas vide
print(inString); // affiche chaine recue
} // fin condition chaine recue pas vide
} // ———— fin de la fonction de gestion des évènements Série
// —————– Fonction d’analyse d’image ————
PImage analyseImage(PImage imgIn, int xIn, int yIn, int indiceIn) { // la fonction renvoie image modifiée sans modifier l’image de départ
PImage imgOut; // image renvoyée par la fonction
imgOut=imgIn; // les opérations de transformation seront faites sur imgOut pour ne pas modifier imgIn
//—– 1°) application du « mixeur de canaux » avec sortie sur canal Rouge
//—- coeff à appliquer
float coefRouge=1; // 100% de rouge
float coefVert=1.5; // 80% du vert
float coefBleu=-2; // – 200% du bleu
//———- le traitement le plus efficace qui fonctionne est à tester dans Gimp au préalable
//——— ici réglage pour balle de couleur orangée —-
imgOut.loadPixels(); // charge les pixels de l’image dans le tableau pixels[]
for (int i = 0; i < imgOut.width*imgOut.height; i++) { // passe en revue les pixels de l’image – index 0 en premier
float r = (red(imgOut.pixels[i])) + (green(imgOut.pixels[i])*coefVert) + (blue(imgOut.pixels[i])*coefBleu); // la couleur rouge
//—- fonction mixeur de canaux
//—- le canal rouge est le canal de sortie et a pour coeff 1
//—- auquel on ajoute du vert avec coeff vert
//—- et du bleu avec coeff bleu
// les deux autres canaux restent inchangés
float g = green(imgOut.pixels[i]); // la couleur verte
float b = blue(imgOut.pixels[i]); // la couleur bleue
imgOut.pixels[i] = color(r, g, b); // modifie le pixel en fonction
}
//imgOut.updatePixels(); // met à jour les pixels de l’image
//—– 2°) transformation de l’image en monochrome en se basant sur le canal rouge
//imgOut.loadPixels(); // charge les pixels de la fenetre d’affichage
for (int i = 0; i < imgOut.width*imgOut.height; i++) { // passe en revue les pixels de l’image – index 0 en premier
float r = red(imgOut.pixels[i]);// la couleur rouge
float g = red(imgOut.pixels[i]); // la couleur verte
float b = red(imgOut.pixels[i]); // la couleur bleue
imgOut.pixels[i] = color(r, g, b); // modifie le pixel en fonction
}
imgOut.updatePixels(); // met à jour les pixels
//—— on applique filtre de seuillage —
imgOut.filter(THRESHOLD,0.75); // applique filtre seuil à la fenetre d’affichage
// à adapter en fonction de la luminosité ambiante – plus sombre, valeur 0.7, plus lumineux, valeur 0.8 ou plus
//— on récupère l’image transformée —
//imgOut=get(0,0,width,height); // récupère image à partir fenetre d’affichage
//— on rebascule dans OpenCV —
opencv.copy(imgOut); // charge l’image modifiée dans le buffer opencv
// trouve les formes à l’aide de la librairie openCV
// blobs(minArea, maxArea, maxBlobs, findHoles, [maxVertices]);
Blob[] blobs = opencv.blobs( 10, imgOut.width*imgOut.height/4, 5, false, OpenCV.MAX_VERTICES*4 );
// —- recharge image vidéo non traitée —
//opencv.read(); // lecture flux vidéo via OpenCV
//noTint();
/* if (camIn==1) {
cam1.read();
image(cam1, xIn, yIn);
}
if (camIn==2) {
cam2.read();
image(cam2, xIn, yIn);
}
*/
// xxxxxxxxxxxx Analyse et gestion des formes reconnues xxxxxxxxxxxxxx
for( int i=0; i<blobs.length; i++ ) { // passe en revue les blobs (= formes détectées)
//—- détection du « centre » de l’objet —-
int centreX= blobs[i].centroid.x; // centroid renvoie un objet Point qui fournit x et y
int centreY= blobs[i].centroid.y; // centroid renvoie un objet Point qui fournit x et y
/*
//———- dessine un cercle autour du centre détecté ———–
noFill();
stroke(vert);
strokeWeight(2);
ellipse (centreX,centreY, 10,10);
*/
Rectangle rectangleBlob=blobs[i].rectangle; // récupère le rectangle qui contient la forme détectée
//—- analyse du rectangle objet —-
int ratioWH=rectangleBlob.width/rectangleBlob.height; // calcule le ratio largeur/hauteur
int ratioHW=rectangleBlob.height/rectangleBlob.width; // calcule le ratio hauteur/largeur
float aireBlob=blobs[i].area; // récupère l’aire de la forme courante
float ratioAire=aireBlob/(rectangleBlob.width*rectangleBlob.height); // calcul du ratio Aire forme / Aire rectangle
// aire cercle = pi * r²
// aire carré = (2r)²=4r²
// aire cercle/aire carré = (pi * r²)/ (4*r²) = pi/4 = 3/4 env
// un cercle occupe 3/4 du carré le contenant
//if ((ratioWH>0.9)) { // si forme proche du carré
if ((ratioAire>0.5) && ((ratioWH>0.8) || (ratioHW>0.8))) { // si le rapport de l’aire de la forme / rectangle contenant est compatible avec un cercle
//fill(jaune); // couleur remplissage RGB
noFill(); // pas de remplissage
stroke (bleuclair); // couleur pourtour RGB
//tracé d’un rectangle autour du blob
rect( xIn+rectangleBlob.x, yIn+rectangleBlob.y, rectangleBlob.width, rectangleBlob.height );
fill(jaune); // couleur remplissage RGB
stroke (rouge); // couleur pourtour RGB
// tracé des formes détectées
beginShape(); // début tracé forme complexe
for( int j=0; j<blobs[i].points.length; j++ ) { // parcourt tous les points du pourtour du blob
vertex( xIn+blobs[i].points[j].x, yIn+blobs[i].points[j].y ); // tracé des points de la forme
}
endShape(CLOSE); // tracé forme complexe
//————— gestion centre pour servomoteur ———-
// moyenne des X et des Y
moyX[indiceIn]=moyX[indiceIn]+centreX;
moyY[indiceIn]=moyY[indiceIn]+centreY;
comptImg[indiceIn]=comptImg[indiceIn]+1;
if (comptImg[indiceIn]>=nbImg) { // si le nombre d’image à prendre en compte dépassé
//println(« X = « + centreX);
//println(« Total X[« +indiceIn+ »]= « + moyX[indiceIn]);
//println(« comptImg[« +indiceIn+ »]= »+comptImg[indiceIn]);
moyX[indiceIn]=moyX[indiceIn]/(comptImg[indiceIn]); // calcule moyenne centre X
// println (« Moyenne X[« +indiceIn+ »] = « + moyX[indiceIn]);
//println(« Y = « + centreY);
//println(« Total Y = « + moyY);
//println(« comptImg= »+comptImg);
moyY[indiceIn]=moyY[indiceIn]/(comptImg[indiceIn]); // calcule moyenne centre Y
//println (« Moyenne Y = « + moyY);
//— positionnement servomoteur —
if (moyX[indiceIn]<((widthCapture/2)–15)) { // si on est vers la droite – la valeur retranchée évite oscillation sur centre…
//— plus l’angle utilisé ici est grand, plus c’est rapide… mais si trop grand, trop par « à coup »… 2 ou 3 degrès = bon compromis)
myPort.write(« servoPan »+(indiceIn+1)+« toR(002)\n« );// on incrémente la position de l’angle du servomoteur
//println (« servoPan »+(indiceIn+1)+ »toR(002)\n »); // debug
}
if (moyX[indiceIn]>((widthCapture/2)+15)) { // si on est vers la droite – la valeur retranchée évite oscillation sur centre…
myPort.write(« servoPan »+(indiceIn+1)+« toR(-002)\n« );// on incrémente la position de l’angle du servomoteur
//println (« servoPan »+(indiceIn+1)+ »toR(-002)\n »); // debug
}
//— positionnement servomoteur tilt —
if (moyY[indiceIn]<((heightCapture/2)–15)) { // si on est vers le haut
myPort.write(« servoTilt »+(indiceIn+1)+« toR(-002)\n« );// on incrémente la position de l’angle du servomoteur
//println (« servoTilt »+(indiceIn+1)+ »toR(-002)\n »); // debug
}
if (moyY[indiceIn]>((heightCapture/2)+15)) { // si on est vers la bas
myPort.write(« servoTilt »+(indiceIn+1)+« toR(002)\n« );// on incrémente la position de l’angle du servomoteur
//println (« servoTilt »+(indiceIn+1)+ »toR(002)\n »); // debug
}
comptImg[indiceIn]=0; // RAZ comptage images
moyY[indiceIn]=0; // RAZ moyY
moyX[indiceIn]=0; // RAZ moyX
}
} // if ratio Aire
else { // trace avec couleurs différentes les rectangles non souhaités
/*
fill(bleu); // couleur remplissage RGB
stroke (jaune); // couleur pourtour RGB
//tracé d’un rectangle autour du blob
rect( rectangleBlob.x, rectangleBlob.y, rectangleBlob.width, rectangleBlob.height );
fill(jaune); // couleur remplissage RGB
stroke (rouge); // couleur pourtour RGB
// tracé des formes détectées
beginShape(); // début tracé forme complexe
for( int j=0; j<blobs[i].points.length; j++ ) { // parcourt tous les points du pourtour du blob
vertex( blobs[i].points[j].x, blobs[i].points[j].y ); // tracé des points de la forme
}
endShape(CLOSE); // tracé forme complexe
*/
}
} // —- fin for blobs ——–
return(imgOut); // renvoie image
} // fin fonction analyseImage()
//————- Fonction d’arret de Processing —-
public void stop(){ // fonction d’arrêt de Processing
for (int i=0; i<2; i++){
cam[i].delete(); // efface l’objet GScapture
} // fin for
super.stop(); // obligatoire
} // fin fonction stop()
//XXXXXXXXXXXXXXXXXX Fin du programme XXXXXXXXXXXXXXXXX
Articles Liés
- Suivi de balle par une tourelle pan/tilt (2 servomoteurs - panoramique + inclinaison) à partir d'une interface Processing
Le suivi de balle par une tourelle pan/tilt à partir d'une interface Processing est une…
- Contrôler la rotation de 2 servomoteurs (tourelle pan/tilt) à partir d'une interface Processing
L'utilisation de servomoteurs pour contrôler des mouvements est une technologie très répandue dans le domaine…
- Mesure analogique à distance (télémétrie) multivoies à partir d'une carte Arduino "serveur" via deux interfaces Processing Client/Serveur sur 2 PC connectés en réseau wifi.
La télémétrie est une technologie qui permet de mesurer des données à distance. Elle est…