Traqueur de ligne

Objectif : améliorer les performances d’un capteur grâce à l’intelligence artificielle

Ressources nécessaires :

  • Python avec les bibliothèques numpy, scipy, matplotlib et scikit learn
  • Banc de mesure avec un potentiomètre linéaire 100 mm
  • Robot Alphabot équipé du traqueur de ligne et contrôlé par un Arduino.
  • Logiciels : IDE Python, Terminal série (ex CoolTerm), IDE Python/Jupyter (ex VSCodium)

 

Vous répondrez aux questions sur un Notebook Jupyter (à télécharger et à compléter) :

TP_TRsensor.ipynb (faire clic-droit + Enregistrer la cible du lien sous…)

Les lignes de code (Python) à modifier sont suivies de : ### à compléter ...

La présentation doit être claire, concise et soignée.

 


Analyse du capteur

Le traqueur de ligne étudié est installé sur un robot Alphabot.

 

Lire le principe de fonctionnement pour comprendre comment le traqueur de ligne est constitué.

Dans cette étude, seuls les 3 détecteurs centraux seront pris en compte.

 

Réalisation des mesures

Les mesures sont réalisées grâce à un banc de mesure :

  • Une ligne noire sur fond blanc est placée sous le traqueur et peut être déplacée latéralement.
    le robot reste fixe !
  • Un potentiomètre linéaire double piste mesure la position de référence (position « réelle ») de la ligne sous le capteur.
  • Un programme Arduino (déjà téléversé).

Après une phase de calibrage (contraste de la ligne puis centrage du potentiomètre) le programme affiche (dans un terminal série) les différentes valeurs mesurées (sur le traqueur et sur le potentiomètre).

tp_trsensor_ia.ino

Téléchargement : tp_trsensor_ia.ino

#include "TRSensors2.h"

#define NUM_SENSORS 5

TRSensors trs = TRSensors();
unsigned int sensorValues[NUM_SENSORS];
unsigned int sen_val[3];
unsigned int position = 0;
unsigned int pos_pot1 = 0;
unsigned int pos_pot2 = 0;

unsigned int N = 10; // nombre de valeurs moyennées
unsigned int dt = 10;

void setup() {
  Serial.begin(115200);

  /******************************************************************************/
  Serial.println("Appuyer sur ENTER pour demarrer le calibrage du TRsensor");
  Serial.println("... puis faire faire à la ligne de lents aller-retours sous le capteur (durant 10s environ)");
  attente();
  
  Serial.println("TRSensor : calibrage en cours ...");
  for (int i = 0; i < 400; i++) { // la procedure dure environ 10 secondes
    trs.calibrate();       // calibrage ... 
  }
  Serial.println("Calibrage termine");
  for (int i = 0; i < NUM_SENSORS; i++) {
    Serial.print(trs.calibratedMin[i]);
    Serial.print('\t');
  }
  Serial.println();
  for (int i = 0; i < NUM_SENSORS; i++) {
    Serial.print(trs.calibratedMax[i]);
    Serial.print('\t');
  }
  Serial.println();


  /******************************************************************************/
  Serial.println("Appuyer sur ENTER pour demarrer le centrage du potentiometre (valeur 511-512)");
  Serial.println(" ... et à nouveau sur ENTER lorsque le centrage est termine");
  attente();

  while (!Serial.available()) {
    // Initialisation ****************
    pos_pot1 = 0;
    pos_pot2 = 0;

    // Acquisition ****************
    for (int i = 0 ; i < N ; i++) {
      pos_pot1 += analogRead(A0);
      pos_pot2 += analogRead(A1);
      delay(dt);
    }

    // Affichage ****************
    Serial.println((pos_pot1+pos_pot2)/N/2);
    delay(200);
  }
  while (Serial.available()) {
    Serial.read();
  }


  /******************************************************************************/
  Serial.println("Appuyer sur ENTER pour demarrer l'aquisition des donnees");
  Serial.println(" ... et à nouveau sur ENTER lorsque l'acquisition est terminee");
  attente();

  while (!Serial.available()) {
    // Initialisation ****************
    pos_pot1 = 0;
    pos_pot2 = 0;
    position = 0;
    for (unsigned char i = 0; i < 3; i++) {
      sen_val[i] = 0;
    }

    // Acquisition ****************
    for (unsigned char i = 0 ; i < N ; i++) {
      pos_pot1 += analogRead(A0);
      pos_pot2 += analogRead(A1);
      trs.AnalogRead(sensorValues);
      for (unsigned char j = 1; j < 4; j++) {
          sen_val[j-1] += sensorValues[j];
        }
      position += trs.readLine(sensorValues);
      delay(dt);
    }

    // Affichage ****************
    Serial.print(position/N);
    Serial.print(';');
    for (unsigned char i = 0; i < 3; i++) {
        Serial.print(sen_val[i]/N);
        Serial.print(';');
      }
    Serial.println((pos_pot1+pos_pot2)/N/2);
    delay(10);
  }


  while (Serial.available()) {
    Serial.read();
  }
  Serial.println("Acquisition terminee");
}


void loop() {
  delay(100);
}

void attente() {
  while (!Serial.available()) {delay(10);}
  delay(1000);
  while (Serial.available()) {
    Serial.read();
  }
}

 

Connecter l’Arduino à l’ordinateur

Lancer et configurer le logiciel de terminal série

Exemple avec CoolTerm

Cliquer sur Options pour :

  • Sélectionner le port série (celui auquel est relié l’Arduino)
  • Régler la vitesse de transmission à 115200 bauds

Cliquer sur Connect pour démarrer la communication Ordinateur↔Arduino

Si la connexion est réussie, des informations apparaissent en bas de la fenêtre :

Et les premiers messages dans la fenêtre principale :

  • Installer le capteur du robot au dessus du banc de mesure
  • Réaliser le mouvement de calibrage (pendant 10″ environ),
  • Placer le curseur du banc de mesure de sorte que la valeur mesurée soit la plus proche possible de 512,
  • Placer le robot (pas le curseur !) au centre de la ligne,
  • Placer le curseur (pas le robot !) à l’une de ses extrémités,
  • Démarrer l’acquisition en déplaçant lentement le curseur (pas le robot !) d’une extrémité à l’autre, en allant bien jusqu’au bout (pendant 10″ environ),
    Ne réaliser qu’un seul déplacement, d’un coté à l’autre, sans retour en arrière !
    L’échantillon doit contenir au moins 100 mesures !
  • Arrêter l’acquisition Entrée,
  • Copier/coller les mesures dans un fichier nommé mesures_position_ligne.csv.

 

Vérification des mesures

La première partie du programme charge le fichier et affiche les valeurs mesurée au niveau du capteur, dans l’ordre suivant :

  • positions mesurées (avec la fonction readLine, issue de la bibliothèque fournie avec le robot),
  • valeurs « brutes » gauche, centre et droite (représentant les intensités lumineuses réfléchies par le sol, par un nombre sur 10 bits),
  • positions « réelles » obtenues avec le potentiomètre (moyennes de 2 valeurs sur 10 bits).

 

Lancer VSCodium et ouvrir le notebook Jupyter TP_TRsensor.ipynb

Exécuter les 2 premières « cellules » de code (bouton à gauche des cellules de code)

Vérifier que les valeurs sont bien chargées et qu’il n’y a pas d’anomalies.

 

 

Pré-traitement des mesures

Étant donné que seuls les 3 détecteurs centraux du traqueur sont pris en compte, il faut limiter l’étude à leur zone de visibilité.

La courbe présente les mesures dans l’ordre de leurs indices.

Modifier les valeur du tuple intervalle de sorte de supprimer le début et la fin des mesures, lorsque toutes des valeurs « brutes » sont nulles.

 


Analyse des performances de la méthode readLine

Conversion des positions en mm

Premièrement, il faut convertir les données brutes dans une unité de la grandeur mesurée : le mm.

Les valeurs « brutes » sont extraites dans les variables Prb (position réelle brute) et Pmb (position mesurée brute).

Compléter les lignes de conversion en mm (si besoin, relire les caractéristiques de la méthode readLine, ainsi que celles du convertisseur Analogique-Numérique de l’Arduino), permettant d’obtenir Pr (position réelle en mm) et Pm (position mesurée en mm).

 

Modélisation par régression linéaire

Pour obtenir la meilleure loi possible permettant d’obtenir une valeur de distance « mesurée » la plus proche possible de la distance « réelle », nous utiliserons la méthode mathématique de régression linéaire.

Lorsque l’on veut obtenir la position en mm à partir de la valeur obtenue par readLine, on utilise une loi linéaire, telle que celle établie à la question précédente :

\(Pm=a\times Pmb+b\)

Afin d’analyser la performance (en termes de précision) du capteur avec la méthode readLine, nous traçons au dessus des points de mesure :

  • la droite de régression (en noir)
  • la droite représentant le comportement du capteur « parfait » (en pointillés rouges)
Déterminer l’erreur maximale commise lors de la mesure par readLine (voir cours sur la fonction acquérir). Faire apparaître cette erreur sur la courbe.

 

Amélioration de la loi linéaire

S’il est difficile de corriger les écarts à la droite de régression, il existe néanmoins une possibilité d’amélioration : essayer de faire coïncider la droite de régression avec la droite du capteur « parfait » en « jouant » sur les coefficient qui permettent la conversion en mm de la gradeur retournée par readLine.

Modifier les coefficients permettant la conversion en mm de la gradeur retournée par readLine.jusqu’à obtenir une superposition satisfaisante des deux droites.
Conseil : ajuster d’abord le coefficient directeur, puis l’ordonnée à l’origine.

 

Relever l’erreur maximale commise après cette correction de la loi.

 

 

La méthode readLine ne donne pas entièrement satisfaction car elle est peu précise. On va tenter de la remplacer par un réseau de neurones…

 

 

 


Utilisation de l’intelligence artificielle

On se propose de remplacer la méthode readLine par une fonction basée sur un réseau de neurones : un perceptron multicouche (MLP).

Ce type de perceptron permet de faire de la régression logistique.

 

Description du réseau de neurones

Nous choisissons d’utiliser un perceptron ne possédant qu’une seule couche cachée, de N neurones (avec N >= 3) :

Pour implémenter ce réseau de neurones en Python, nous utiliserons la bibliothèque scikit-learn, qui fournit un modèle de perceptron multicouche pour faire de la régression appelé MLPregressor.

 

Pré-traitement des données

Les valeurs « brutes » sont extraites dans les variables Prb (position réelle brute) et B (vecteur de 3 valeurs codées sur 10 bits).

La position « réelle » doit être convertie en mm.

Recopier la ligne Pr = ... avec la « formule » établie à la question 2.

 

Les 3 valeurs brutes obtenues par le capteur doivent être normalisées, c’est à dire converties en valeurs réelles comprises entre 0 et 1.

Compléter la ligne B = ... permettant de normaliser Bb (vecteur de 3 valeurs brutes sur 10 bits).

 

Implémentation et entraînement du perceptron

On utilise la classe MLPRegressor, avec des paramètres par défaut pour commencer :

  • Nombre de neurones dans la couche cachée (hidden_layer_sizes) : 4
  • Fonction d’activation (activation) : 'identity' (défaut), 'relu''tanh' ou 'logistic'
  • Tolérance du calcul (tol) : 0,001

 

Par essais successifs, et en n’en modifiant qu’un seul à la fois, déterminer les paramètres qui permettent d’obtenir la meilleure précision, sans trop rallonger le temps de calcul !
On considérera qu’une erreur inférieure à 2 mm est acceptable.

 

 

Implantation du MLP dans l’Arduino

À présent que l’apprentissage du MLP est terminé, ses poids synaptiques et ses biais ont été établis. La position de la ligne peut être rapidement calculée à partir des 3 valeurs des capteurs du traqueur, par propagation avant, grâce à la fonction prédictive \(f\) :

\(p=f(B)=W_1\cdot h(W_0\cdot B+B_0)+B_1\)

avec :

  • \(p\) : position en mm
  • \(B\) : vecteur d’entrée (valeurs normalisées des 3 capteurs centraux du traqueur)
  • \(h\) : fonction d’activation
  • \(W_0\) : poids de la couche d’entrée (matrice 3xN)
  • \(B_0\) : biais de la couche d’entrée (matrice 1xN)
  • \(W_1\) : poids de la couche cachée (matrice Nx1)
  • \(B_1\) : biais de la couche cachée (matrice 1×1)

 

La dernière partie du notebook Jupyter permet d’extraire les poids et les biais du MLP après l’apprentissage.

Le programme Arduino ci-dessous implémente un MLP et lance une acquisition en continue depuis le traqueur. Il affiche en parallèle les positions de la ligne (en mm) estimées avec readLine, et avec le MLP.

MLP_trsensor.ino

Téléchargement : MLP_trsensor.ino

#include "TRSensors2.h"
#include <BasicLinearAlgebra.h>

using namespace BLA;

const unsigned int N = ...; // Nombre de neurones dans la couche cachée

/* Poids synaptiques */
BLA::Matrix<3, N, float> poids0 = { // Coller les poids0 ci-dessous
};

BLA::Matrix<N, 1, float> poids1 = { // Coller les poids1 ci-dessous
};

/* Biais */
BLA::Matrix<1, N, float> bias0 = { // Coller les biais0 ci-dessous
};

BLA::Matrix<1, 1, float> bias1 = { // Coller le biais1 ci-dessous
};


#define NUM_SENSORS 5
TRSensors trs = TRSensors();
unsigned int sen_val[3];
unsigned int position = 0;
unsigned int sensorValues[NUM_SENSORS];

unsigned int Nm = 10; // nombre de valeurs moyennées
unsigned int dt = 10;

/***********************************************************************************/
void setup() {
  Serial.begin(115200);

  /******************************************************************************/
  Serial.println("Appuyer sur ENTER pour demarrer le calibrage du TRsensor");
  Serial.println("... puis faire faire à la ligne de lents aller-retours sous le capteur (durant 10s environ)");
  attente();
  
  Serial.println("TRSensor : calibrage en cours ...");
  for (int i = 0; i < 400; i++) { // la procedure dure environ 10 secondes
    trs.calibrate();       // calibrage ... 
  }
  Serial.println("Calibrage termine");
  for (int i = 0; i < NUM_SENSORS; i++) {
    Serial.print(trs.calibratedMin[i]);
    Serial.print('\t');
  }
  Serial.println();
  for (int i = 0; i < NUM_SENSORS; i++) {
    Serial.print(trs.calibratedMax[i]);
    Serial.print('\t');
  }
  Serial.println();
}

/***********************************************************************************/
void loop() {
  // Initialisation ****************
    position = 0;
    for (unsigned char i = 0; i < 3; i++) {
      sen_val[i] = 0;
    }

    // Acquisition ****************
    for (unsigned char i = 0 ; i < Nm ; i++) {
      trs.AnalogRead(sensorValues);
      for (unsigned char j = 1; j < 4; j++) {
          sen_val[j-1] += sensorValues[j];
        }
      position += trs.readLine(sensorValues);
      delay(dt);
    }

    // Affichage ****************
    BLA::Matrix<3, 1, float> x;
    Serial.print(float(position)/Nm*80/4000 - 40);
    Serial.print('\t');
    for (unsigned char i = 0; i < 3; i++) {
      x(i) = float(sen_val[i])/Nm/1024;
      }
    Serial.println(forward(x));
    delay(10);

}

/* Fonction de propagation AVANT */
float forward(BLA::Matrix<3, 1, float> x) {
  BLA::Matrix<N, 1, float> C;
  BLA::Matrix<1, 1, float> y;
  C = ~poids0 * x + ~bias0;
  for (int j=0; j<N; j++) {
    C(j) = 1/(1+exp(-C(j)));
  }
  y = ~poids1 * C + bias1;
  return y(0);
}

/* Fonction d'attente */
void attente() {
  while (!Serial.available()) {delay(10);}
  delay(1000);
  while (Serial.available()) { Serial.read();}
}

Lancer l’IDE Arduino et ouvrir le programme MLP_trsensor.ino

Copier/Coller les valeurs des paramètres du MLP dans le code Arduino.

Téléverser le programme dans l’Arduino

Tester le bon fonctionnement du MLP par comparaison avec l’ancienne méthode readLine.

 

 

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *