Skip to Content

Détecteur de couleur par plus proche voisin avec TCS230 et Arduino

Détecteur de couleur par plus proche voisin avec TCS230 et Arduino

Dans ce tutoriel, vous apprendrez à utiliser le capteur de couleur TCS230 avec un Arduino pour construire un détecteur de couleur entraînable basé sur un classificateur des plus proches voisins.

Le capteur de couleur TCS230 ne vous indique pas directement la couleur qu’il voit, mais renvoie des mesures des canaux rouge, vert, bleu (et clair). Pour la plupart des applications pratiques, il faut d’une manière ou d’une autre convertir ces mesures en une couleur détectée, par exemple « violet ». Il peut être assez difficile de détecter une couleur de manière fiable en utilisant des seuils sur les mesures des canaux. Nous allons donc appliquer un peu d’apprentissage automatique et utiliser un classificateur des plus proches voisins entraînable.

Pièces requises

Vous aurez besoin d’un capteur de couleur TCS230 et d’un microcontrôleur. J’ai utilisé un Arduino Uno pour ce projet, mais tout autre Arduino ou ESP32 fonctionnera également. Nous utiliserons aussi un écran OLED pour afficher la couleur détectée et d’autres informations sur un petit écran.

Capteur de distance TFmini Plus

Arduino

Arduino Uno

USB Data Sync cable Arduino

Câble USB pour Arduino UNO

Dupont wire set

Jeu de fils Dupont

Half_breadboard56a

Plaque d’essai (breadboard)

OLED display

Écran OLED

Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.

Principes de base du capteur de couleur TCS230

Le TCS230 est un convertisseur lumière-couleur en fréquence. Il est composé d’une matrice 8 x 8 de photodiodes qui détectent la lumière. Il y a 16 photodiodes avec un filtre rouge sensibles à la lumière rouge, 16 avec un filtre vert pour la lumière verte, 16 avec un filtre bleu et 16 sans filtre pour la lumière « blanche ».

Si vous regardez de près la photo de la puce TCS230 ci-dessous, vous pouvez en fait voir la matrice 8 x 8 de photodiodes avec leurs filtres colorés (et clairs).

TCS230 color light-to-frequency converter
Convertisseur lumière-couleur en fréquence TCS230

Fonction du capteur de couleur TCS230

Outre la matrice de photodiodes, le TCS230 contient un convertisseur courant-fréquence, qui transforme le courant traversant un ensemble de photodiodes en une onde carrée. Un ensemble est composé de 16 photodiodes pour une couleur spécifique (rouge, vert, bleu) ou clair, et la fréquence de l’onde carrée est proportionnelle au courant traversant cet ensemble de photodiodes.

Functional Block Diagram of TCS230
Schéma fonctionnel du TCS230 (source)

Pour mesurer l’intensité d’une des trois couleurs (ou clair), vous sélectionnez l’ensemble de photodiodes correspondant (rouge, vert, bleu, blanc) en réglant deux entrées (S2, S3), puis vous lisez la fréquence en sortie. La plage de fréquence (échelle) peut être contrôlée par deux autres entrées (S0, S1).

Brochage du capteur de couleur TCS230

L’image suivante montre le brochage de la puce TCS230. Vous pouvez voir les broches S0 et S1 pour sélectionner la plage de fréquence de sortie, ainsi que les broches S2, S3 pour sélectionner l’ensemble de photodiodes (couleur).

Pinout of TCS230
Brochage du TCS230 (source)

La sortie OUT peut être activée ou désactivée via la broche OE. VDD et GND sont pour l’alimentation avec une tension de 2,7 V à 5,5 V. Le tableau ci-dessous résume le brochage :

BrocheNomDescription
1,2S0, S1Échelle de fréquence de sortie
3OEActivation de la sortie fréquence
4GNDMasse
5VDDAlimentation
6OUTFréquence de sortie 
7,8S2, S3Sélection du type de photodiode

Sélection photodiode/couleur

Comme mentionné plus haut, avant de pouvoir mesurer l’intensité d’un canal couleur spécifique, vous devez d’abord le sélectionner via les broches S2 et S3. Le tableau ci-dessous montre quelle configuration de S2 et S3 correspond à quel canal couleur :

Canal couleurS2S3
RougeLOWLOW
BleuLOWHIGH
ClairHIGHLOW
VertHIGHHIGH

Échelle de fréquence

La plage de fréquence de sortie peut varier entre 100 %, 20 %, 2 % et arrêt en réglant les valeurs suivantes pour les broches de contrôle S0 et S1 :

Échelle de sortieS0S1min fmax f
Mise hors tensionLOWLOW
2 %LOWHIGH10 kHz12 kHz
20 %HIGHLOW100 kHz120 kHz
100 %HIGHHIGH500 kHz600 kHz

Module capteur de couleur TCS230

En général, vous n’utilisez pas le TCS230 directement, mais un module capteur de couleur TCS230 qui intègre quelques composants supplémentaires et est plus facile à connecter. La photo ci-dessous montre le devant et le dos d’un module typique :

Front and back of TCS230 Color Sensor Module
Devant et dos du module capteur de couleur TCS230

Sur le devant du module, vous trouvez quatre LED blanches pour l’éclairage et un boîtier noir protecteur contenant la puce TCS230. En regardant de près, vous pouvez voir la puce TCS230 au centre :

CS230 chip in Sensor Module
Puce TCS230 dans le module capteur

Schéma du module capteur de couleur TCS230

Le module TCS230 a essentiellement le même brochage que la puce TCS230 elle-même, à deux exceptions près. Il n’y a pas de broche OE mais il y a une broche supplémentaire LED.

Le schéma du module révèle que OE est connecté à VSS, ce qui signifie que la sortie est toujours activée. La broche LED est connectée à la grille d’un MOSFET (Q1) qui commande les quatre LED d’éclairage (D1…D4). Comme la broche LED a une résistance de tirage interne, les LED sont actives par défaut et vous pouvez les éteindre en reliant la broche LED à la masse.

Schematics of TCS230 Sensor Module
Schéma du module capteur TCS230


Notez que les broches S0 et S1 ont aussi des résistances de tirage internes et que l’échelle de fréquence par défaut est donc à 100 %. En revanche, les broches S2 et S3 n’ont pas de résistances de tirage et vous devez les régler pour sélectionner un canal couleur.

Dans la section suivante, nous connectons le module TCS230 à un Arduino et écrivons un code pour tester le capteur.

Connexion du TCS230 à l’Arduino

Le schéma suivant montre comment connecter le module TCS230 à un Arduino. J’utilise 5 V comme alimentation, mais 3,3 V fonctionnerait aussi. Les broches de contrôle S0, S1, S2 et S3 du module TCS230 sont connectées aux broches 8, 9, 10, 11 de l’Arduino.

Connecting TCS230 to Arduino
Connexion du TCS230 à l’Arduino

Le tableau ci-dessous rappelle les connexions à effectuer

TCS230 Arduino
VCC5V
GNDGND
S08
S19
S210
S311
OUT12

Notez qu’il y a deux broches VCC et deux broches GND sur le TCS230. Vous pouvez utiliser l’une ou l’autre. La broche LED n’est pas connectée, mais vous pourriez la relier à une sortie numérique pour commander les LED d’éclairage.

Code pour lire les couleurs avec le TCS230

Dans le code suivant, nous utilisons un capteur de couleur TCS230 pour lire les valeurs des différents canaux couleur (rouge, vert, bleu et blanc) et afficher les résultats sur le moniteur série.

#include "aprintf.h"

const int s0 = 8;
const int s1 = 9;
const int s2 = 10;
const int s3 = 11;
const int out = 12;

void initPins() {
  pinMode(s0, OUTPUT);
  pinMode(s1, OUTPUT);
  pinMode(s2, OUTPUT);
  pinMode(s3, OUTPUT);
  pinMode(out, INPUT);
}

void setScaling(int s0state, int s1state) {
  digitalWrite(s0, s0state);
  digitalWrite(s1, s1state);
}

uint16_t readPulse(int s2state, int s3state) {
  delay(20);
  digitalWrite(s2, s2state);
  digitalWrite(s3, s3state);
  return pulseIn(out, LOW);
}

void setup() {
  Serial.begin(9600);
  initPins();
  setScaling(HIGH, LOW);  // 20 %
}

void loop() {
  uint16_t red = readPulse(LOW, LOW);
  uint16_t blue = readPulse(LOW, HIGH);
  uint16_t green = readPulse(HIGH, HIGH);
  uint16_t white = readPulse(HIGH, LOW);

  aprintf("R:%3d G:%3d  B:%3d  W:%3d\n",
          red, green, blue, white);

  delay(1000);
}

Décomposons le code en ses composants pour mieux comprendre.

Includes

Le code utilise la bibliothèque aprintf library, qui simplifie la mise en forme du texte à afficher. C’est essentiellement une version de la fonction printf(). Pour plus de détails, lisez le tutoriel How To Print To Serial Monitor On Arduino.

Pour l’installer, allez dans github repo, cliquez sur le bouton vert « Code » puis sélectionnez « Download ZIP » :

Télécharger la bibliothèque aprintf au format .ZIP

Ensuite, allez dans « Sketch » -> « Include Library » -> « Add .ZIP Library » pour l’installer.

Installing a .ZIP Arduino library
Installation d’une bibliothèque Arduino au format .ZIP

Pour utiliser aprintf library, nous avons besoin de l’inclusion suivante :

#include "aprintf.h"

Constantes

Ensuite, nous définissons les broches utilisées pour connecter le capteur TCS230 à l’Arduino.

const int s0 = 8;
const int s1 = 9;
const int s2 = 10;
const int s3 = 11;
const int out = 12;

Ici, s0 et s1 servent à régler l’échelle de fréquence, tandis que s2 et s3 servent à sélectionner le canal couleur. La broche out est celle où le capteur sort le signal de fréquence correspondant à l’intensité de la couleur détectée.

Initialisation des broches

La fonction initPins() configure les modes des broches pour chacune des broches définies. Les broches du capteur s0, s1, s2 et s3 sont configurées en OUTPUT, tandis que la broche out est configurée en INPUT.

void initPins() {
  pinMode(s0, OUTPUT);
  pinMode(s1, OUTPUT);
  pinMode(s2, OUTPUT);
  pinMode(s3, OUTPUT);
  pinMode(out, INPUT);
}

Échelle de fréquence

La fonction setScaling() sert à configurer l’échelle de fréquence du signal de sortie du capteur. En réglant les états de s0 et s1, on peut ajuster la résolution de la sortie du capteur. Nous y reviendrons plus tard.

void setScaling(int s0state, int s1state) {
  digitalWrite(s0, s0state);
  digitalWrite(s1, s1state);
}

Lecture des impulsions

La fonction readPulse() est responsable de la lecture de la durée en microsecondes de l’impulsion émise par le capteur. Elle prend en paramètres les états de s2 et s3 pour sélectionner le canal couleur à lire. La fonction attend 20 millisecondes pour stabiliser le capteur entre les lectures.

uint16_t readPulse(int s2state, int s3state) {
  delay(20);
  digitalWrite(s2, s2state);
  digitalWrite(s3, s3state);
  return pulseIn(out, LOW);
}

Notez que cette fonction ne retourne pas la fréquence de l’onde carrée mais la durée des impulsions (beaucoup de tutoriels sur le TCS230 se trompent à ce sujet). Plus la couleur est intense, plus la fréquence est élevée mais plus l’impulsion est courte. Il y a donc une relation inverse : plus la couleur est intense, plus la valeur retournée est faible.

Si vous voulez convertir la valeur en fréquence, il faudrait compter le nombre d’impulsions n en une seconde et prendre l’inverse :

f = 1 / n

mais nous travaillerons directement avec les durées d’impulsions.

Fonction Setup

Dans la fonction setup(), nous initialisons la communication série à 9600 bauds et appelons la fonction initPins() pour configurer les broches. Nous réglons aussi l’échelle à 20 % en appelant setScaling(HIGH, LOW), ce qui nous donne une bonne résolution.

void setup() {
  Serial.begin(9600);
  initPins();
  setScaling(HIGH, LOW);  // 20 %
}

Si vous comparez les facteurs d’échelle 2 %, 20 % et 100 %, vous verrez que les lectures des canaux couleur sont les plus grandes pour 2 % et les plus petites pour 100 %. Voici quelques exemples de lectures pour les différentes échelles lorsque le capteur a vu un objet vert et un objet blanc avec la même illumination et distance :

100%
R: 23 G: 16  B: 25  W:  8  // green
R:  7 G:  6  B:  6  W:  3  // white

20%
R:116 G: 82  B:124  W: 35  // green
R: 32 G: 31  B: 27  W: 10  // white

2%
R:1149 G:807  B:1220  W:339  // green
R:308  G:305  B:260   W: 96  // white

Le facteur d’échelle 20 % est un bon compromis entre résolution et robustesse. De plus, les lectures couleur pour 20 % tiennent facilement dans un entier, tandis qu’avec 2 % vous pouvez rencontrer des dépassements lorsque le capteur est dans l’obscurité totale. J’ai donc choisi le facteur 20 %.

Fonction Loop

Dans la fonction loop(), nous lisons les durées d’impulsions pour les couleurs rouge, bleu, vert et blanc en appelant readPulse() avec les paramètres appropriés. Les résultats sont stockés dans les variables red, green, blue et white.

  uint16_t red = readPulse(LOW, LOW);
  uint16_t blue = readPulse(LOW, HIGH);
  uint16_t green = readPulse(HIGH, HIGH);
  uint16_t white = readPulse(HIGH, LOW);

Comme mentionné, les valeurs couleur sont inverses à l’intensité des couleurs détectées. Nous affichons les valeurs couleur sur le moniteur série en utilisant la fonction aprintf(), qui formate la sortie pour une meilleure lisibilité. Enfin, nous ajoutons un délai de 1000 millisecondes (1 seconde) avant de répéter la boucle.

  aprintf("R:%3d G:%3d  B:%3d  W:%3d\n",
          red, green, blue, white);
  delay(1000);

Exemple de sortie

Si vous téléversez le code sur votre Arduino et ouvrez le moniteur série, vous devriez voir des lectures des canaux couleur similaires à celles-ci :

R: 56 G:123  B:112  W: 32
R: 47 G:117  B: 93  W: 28
R: 76 G: 71  B: 86  W: 26
R: 77 G: 74  B: 72  W: 26
R:104 G: 76  B: 47  W: 24
R:105 G: 75  B: 48  W: 24
R:165 G:101  B: 85  W: 28
R: 69 G: 71  B: 68  W: 24
R: 70 G: 71  B: 68  W: 24

Si vous placez un objet rouge devant le capteur, la valeur rouge devrait diminuer, de même si le capteur voit un objet bleu ou vert, la valeur du canal correspondant devrait diminuer.

Il y a cependant un problème : les valeurs dépendent fortement de la luminosité ambiante, ce qui rend difficile la détection fiable des couleurs. Dans la section suivante, nous normaliserons donc les lectures.

Normalisation de la luminosité pour le TCS230

La plage globale des lectures couleur dépend de l’échelle choisie. Avec 20 %, vous pouvez obtenir des valeurs entre 0 et environ 15000. Vous pouvez facilement tester cela en couvrant le capteur avec votre doigt (obscurité totale) ou en y dirigeant une lumière forte. Voici un exemple des lectures que j’ai obtenues :

R:  4 G:  3  B:  4  W:  4
R:  4 G:  4  B:  4  W:  4
R:2243 G:13215  B:10289  W:2058
R:2330 G:13986  B:10896  W:2119

Cette forte dépendance à la luminosité ambiante rend presque impossible la définition de seuils robustes pour détecter une couleur. Par exemple, un code comme celui-ci ne fonctionnera pas très bien :

if (red > 10 && red < 100) {
  Serial.println("red detected");
}

Nous devons donc normaliser les lectures couleur. Cela peut être facilement fait en divisant les lectures couleur par la valeur du canal blanc (luminosité). Voici la fonction correspondante :

void normalize(uint16_t &red, uint16_t &green, uint16_t &blue, uint16_t white) {
  red = 100 * red / (white + 1);
  green = 100 * green / (white + 1);
  blue = 100 * blue / (white + 1);
}

Elle multiplie la lecture couleur (rouge, vert, bleu) par 100 puis la divise par la valeur blanche plus 1. Nous ajoutons ce plus un pour éviter une division par zéro potentielle, et multiplions par 100 pour garder les valeurs couleur dans une plage similaire (même résolution).

Si vous comparez les lectures couleur avec et sans normalisation, vous verrez que celles avec normalisation sont plus stables. Par exemple, j’ai placé le TCS230 à différentes distances (1 cm … 4 cm) d’un objet bleu.

Reading blue value with TCS230
Lecture de la valeur bleue avec le TCS230

Sans normalisation, j’ai obtenu des valeurs bleues entre 43 et 169, soit une variation de 126 unités. Avec normalisation, j’ai mesuré des valeurs entre 188 et 237, soit une différence de seulement 49 unités. Les lectures couleur sont donc plus stables (moins sensibles à la lumière ambiante) avec la normalisation.

Dans la fonction loop, vous pouvez facilement ajouter la fonction de normalisation comme suit :

void loop() {
  uint16_t red = readPulse(LOW, LOW);
  uint16_t blue = readPulse(LOW, HIGH);
  uint16_t green = readPulse(HIGH, HIGH);
  uint16_t white = readPulse(HIGH, LOW);

  normalize(red, green, blue, white);

  aprintf("R:%3d G:%3d  B:%3d  W:%3d\n",
          red, green, blue, white);

  delay(1000);
}

Cependant, la normalisation n’est pas parfaite et la lumière ambiante affecte toujours les lectures du capteur. De plus, si vous voulez utiliser des seuils pour détecter des couleurs autres que rouge, vert ou bleu, le code devient complexe.

Par exemple, le violet est une combinaison des valeurs rouge, vert et bleu. Pour le détecter, vous devriez implémenter quelque chose comme ceci :

if(red > 250 && red < 300 && green > 330 && green  < 400 && blue > 190 && blue < 230) {
  Serial.println("purple detected");
}

Ce code est complexe, les seuils sont difficiles à choisir et cela ne fonctionnera pas très bien. À la place, nous entraînons un classificateur simple pour apprendre quelles lectures des canaux couleur correspondent à quelles couleurs. Ce sera beaucoup plus robuste, plus facile à implémenter et aussi plus amusant ; )

Classification des lectures couleur du TCS230

Nous allons utiliser un nearest-neighbor-classifier pour classifier les lectures couleur du TCS230. Pour mieux comprendre comment ce classificateur fonctionne, j’ai collecté des données. J’ai placé le TCS230 devant un carré rouge, vert, violet et jaune et imprimé les lectures des canaux couleur. Voici la grille de couleurs que j’ai utilisée :


Colored squared used to train classifier
Carrés colorés utilisés pour entraîner le classificateur

Représentation graphique des lectures des canaux couleur

J’ai répété ce processus plusieurs fois et enregistré les lectures rouge et vert. (Je laisse de côté les lectures bleues pour l’instant, mais nous les utiliserons plus tard). Voici à quoi ressemblaient les échantillons de données :

RED, GREEN, COLOR
175,471,red
160,400,red
...
296,221,green
303,226,green
...
193,233,yellow
207,257,yellow
...
269,358,purple
282,374,purple
...

Ensuite, j’ai tracé chaque échantillon dans un nuage de points 2D. Chaque point dans le graphique ci-dessous représente un échantillon (une lecture des valeurs rouge et vert), et la couleur de chaque point indique quel carré coloré était placé devant le capteur.

2D Scatter plot of Red and Green channel readings
Nuage de points 2D des lectures des canaux Rouge et Vert

Comme vous pouvez le voir, les échantillons pour les objets rouge, violet, jaune et vert se regroupent très bien, à l’exception d’un possible point aberrant pour un objet violet dans le coin supérieur droit.

Classification de la couleur d’un objet

Avec ces données, il est maintenant facile de déterminer la couleur d’un objet. On prend simplement une lecture des canaux rouge et vert et on trouve l’échantillon le plus proche de cette lecture. Dans le graphique ci-dessous, la croix marque le point d’une nouvelle lecture et vous pouvez voir que l’échantillon le plus proche est violet. Nous classerions donc notre objet comme étant de couleur violette.

Classifying the color of objects
Classification de la couleur des objets

Cette méthode pour classifier les objets basée sur l’échantillon le plus proche s’appelle une Nearest-Neighbor Classifier ou recherche du plus proche voisin. Il existe de nombreuses variantes de cet algorithme.

Surtout, cet algorithme fonctionne aussi avec des données de plus haute dimension. Ce qui signifie que nous pouvons utiliser les trois lectures des canaux couleur pour déterminer une couleur. Le graphique suivant montre quelques échantillons pour cinq couleurs (rouge, violet, jaune, vert, bleu) dans un espace 3D. Chaque échantillon a trois dimensions, les lectures des canaux rouge, vert et bleu, et est un point dans ce nuage 3D :

3D Scatter plot of Red, Green and Blue channel readings
Nuage de points 3D des lectures des canaux Rouge, Vert et Bleu

Comme avant, la couleur du point indique à quelle couleur appartient l’échantillon. Déterminer la couleur d’un nouvel objet dans cet espace 3D de plus haute dimension fonctionne de la même manière qu’avant. Il suffit de trouver l’échantillon le plus proche de vos lectures de canaux. C’est juste plus difficile à visualiser, c’est pourquoi j’ai commencé avec un espace 2D.

Dans la section suivante, nous allons implémenter un classificateur des plus proches voisins simple qui fonctionne dans cet espace 3D.

Code pour le classificateur des plus proches voisins avec TCS230

Le code de cette section est une extension du code précédent. Il ajoute des données d’échantillons et le classificateur des plus proches voisins. Il affiche les lectures des canaux couleur (rouge, vert, bleu) et la couleur détectée. La sortie ressemblera à ceci :

{292, 376, 221}  =>  purple
{393, 293, 193}  =>  blue
{274, 261, 306}  =>  green
{206, 253, 486}  =>  yellow
{165, 428, 375}  =>  red
{166, 246, 466}  =>  ???

Notez qu’il peut détecter des couleurs comme « violet » ou « jaune », qui sont des combinaisons des valeurs rouge, vert et bleu. De plus, s’il ne peut pas détecter une couleur avec confiance, il affiche « ??? ».

Jetez un coup d’œil rapide au code puis nous expliquerons son fonctionnement.

#include "aprintf.h"

const int s0 = 8;
const int s1 = 9;
const int s2 = 10;
const int s3 = 11;
const int out = 12;

const uint16_t reject = 1000; 
const char *colors[] = { "???", "red", "green", "blue", "purple", "yellow" };
const int n_samples = 33;
const uint16_t samples[n_samples][4] = {
  // red
  { 158, 422, 358, 1 },
  { 165, 354, 311, 1 },
  { 145, 347, 307, 1 },
  { 160, 376, 321, 1 },
  { 177, 343, 309, 1 },
  { 156, 403, 343, 1 },
  // green
  { 296, 225, 303, 2 },
  { 279, 232, 311, 2 },
  { 300, 223, 313, 2 },
  { 271, 231, 295, 2 },
  { 293, 230, 309, 2 },
  { 313, 231, 320, 2 },
  // blue
  { 413, 300, 186, 3 },
  { 337, 286, 191, 3 },
  { 376, 293, 200, 3 },
  { 354, 290, 206, 3 },
  { 395, 287, 187, 3 },
  { 351, 285, 197, 3 },
  { 427, 304, 190, 3 },
  // purple
  { 271, 361, 215, 4 },
  { 274, 346, 217, 4 },
  { 268, 355, 217, 4 },
  { 268, 333, 214, 4 },
  { 297, 387, 225, 4 },
  { 284, 365, 218, 4 },
  { 270, 356, 221, 4 },
  // yellow
  { 193, 243, 450, 5 },
  { 194, 252, 464, 5 },
  { 200, 244, 433, 5 },
  { 205, 252, 442, 5 },
  { 210, 245, 357, 5 },
  { 236, 284, 436, 5 },
  { 206, 253, 473, 5 },
};


float sqr(float a) {
  return a*a;
}

float eucDist(uint16_t r, uint16_t g, uint16_t b, uint16_t *s) {
  return sqrt(sqr(r - s[0]) + sqr(g - s[1]) + sqr(b - s[2]));
}

char *classify(uint16_t r, uint16_t g, uint16_t b) {
  char *color = colors[0];
  float mindist = reject;

  for (int i = 0; i < n_samples ; i++) {
    float dist = eucDist(r, g, b, samples[i]);
    if (dist < mindist) {
      mindist = dist;
      color = colors[samples[i][3]];
    }
  }

  return color;
}

void initPins() {
  pinMode(s0, OUTPUT);
  pinMode(s1, OUTPUT);
  pinMode(s2, OUTPUT);
  pinMode(s3, OUTPUT);
  pinMode(out, INPUT);
}

void setScaling(int s0state, int s1state) {
  digitalWrite(s0, s0state);
  digitalWrite(s1, s1state);
}

uint16_t readPulse(int s2state, int s3state) {
  delay(20);
  digitalWrite(s2, s2state);
  digitalWrite(s3, s3state);
  return pulseIn(out, LOW);
}

void normalize(uint16_t &red, uint16_t &green, uint16_t &blue, uint16_t white) {
  red = 100 * red / (white + 1);
  green = 100 * green / (white + 1);
  blue = 100 * blue / (white + 1);
}

void setup() {
  Serial.begin(9600);
  initPins();
  setScaling(HIGH, LOW);  // 20 %
}

void loop() {
  uint16_t red = readPulse(LOW, LOW);
  uint16_t blue = readPulse(LOW, HIGH);
  uint16_t green = readPulse(HIGH, HIGH);
  uint16_t white = readPulse(HIGH, LOW);

  normalize(red, green, blue, white);
  char *color = classify(red, green, blue);

  aprintf("{%3d, %3d, %3d}  =>  %s\n",
          red, green, blue, color);

  delay(1000);
}

Constantes pour les échantillons, les couleurs et le rejet

Au début du code, nous ajoutons trois constantes. La constante reject détermine quand le classificateur décide qu’il ne peut pas détecter une couleur. Nous y reviendrons plus tard.

Ensuite, nous avons la liste des colors que nous allons détecter. La première « couleur » est « ??? », qui est retournée lorsque le classificateur n’a pas pu détecter une couleur. Vous pouvez modifier et étendre cette liste pour détecter autant de couleurs que vous voulez.

Enfin, nous avons une liste d’échantillons n_samples, où chaque échantillon contient les lectures des canaux rouge, vert et bleu plus un index de couleur. L’index de couleur est simplement la position dans la liste colors.

const uint16_t reject = 1000; 
const char *colors[] = { "???", "red", "green", "blue", "purple", "yellow" };
const int n_samples = 33;
const uint16_t samples[n_samples][4] = {
  // red
  { 158, 422, 358, 1 },
  ...
  // green
  { 296, 225, 303, 2 },
  ...
  // blue
  { 413, 300, 186, 3 },
  ...
  // purple
  { 271, 361, 215, 4 },
  ...
  // yellow
  { 193, 243, 450, 5 },
  ...
};

Par exemple, le dernier échantillon { 193, 243, 450, 5 } signifie que nous avons les lectures pour les trois canaux couleur red==193, green==243, blue==450 et que cet échantillon correspond à la couleur colors[5] == "yellow".

Je vous montrerai plus tard comment collecter ces échantillons.

Fonction de distance

Un classificateur des plus proches voisins fonctionne en trouvant l’échantillon le plus proche d’une lecture donnée des canaux couleur. Nous avons donc besoin d’une fonction pour calculer la distance entre une lecture des canaux couleur et un échantillon. Une fonction de distance courante pour cela est la Euclidean distance.

La fonction eucDist calcule la Euclidean distance entre les lectures des canaux couleur r, g, b et un échantillon s. Plus les lectures sont proches de l’échantillon, plus la Euclidean distance sera petite.

float sqr(float a) {
  return a * a;
}

float eucDist(uint16_t r, uint16_t g, uint16_t b, uint16_t *s) {
  return sqrt(sqr(r - s[0]) + sqr(g - s[1]) + sqr(b - s[2]));
}

La fonction carré sqr est une fonction auxiliaire qui calcule le carré de son argument a.

Fonction de classification

La fonction classify parcourt tous les samples, trouve l’échantillon avec la plus petite distance euclidienne mindist aux lectures des canaux couleur r, g, b et retourne la couleur détectée color sous forme de chaîne.

char *classify(uint16_t r, uint16_t g, uint16_t b) {
  char *color = colors[0];
  float mindist = reject;

  for (int i = 0; i < n_samples; i++) {
    float dist = eucDist(r, g, b, samples[i]);
    if (dist < mindist) {
      mindist = dist;
      color = colors[samples[i][3]];
    }
  }

  return color;
}

Si la plus petite distance mindist n’est pas inférieure au seuil reject, la première couleur dans le tableau colors est retournée. C’est la chaîne « ??? », qui indique que les lectures des canaux couleur n’étaient pas assez proches d’un des échantillons pour détecter la couleur de manière fiable.

Vous pouvez rendre le seuil de rejet très grand (1e8) et le classificateur répondra toujours avec une couleur, mais en général il est préférable de laisser le classificateur vous dire s’il ne peut pas classifier la couleur d’un objet avec confiance.

Exemple de sortie

Si vous compilez et téléversez le code, vous devriez voir les lectures des canaux couleur et la couleur détectée affichées ainsi :

{263, 363, 221}  =>  ???
{292, 376, 221}  =>  purple
{284, 371, 215}  =>  purple
{392, 275, 200}  =>  ???
{392, 296, 200}  =>  blue
{393, 293, 193}  =>  blue
{303, 269, 236}  =>  ???
{280, 266, 316}  =>  green
{274, 261, 306}  =>  green
{274, 261, 306}  =>  green
{222, 302, 320}  =>  ???
{163, 420, 360}  =>  red
{163, 420, 356}  =>  red
{160, 416, 333}  =>  red
{160, 416, 353}  =>  red
{268, 237, 314}  =>  ???
{287, 237, 292}  =>  ???

Dans la section suivante, je vous montrerai comment adapter le code pour choisir les couleurs que vous voulez détecter.

Collecte des données d’entraînement pour le détecteur de couleur

Le code ci-dessus montre comment implémenter un détecteur de couleur utilisant un classificateur des plus proches voisins. Il fonctionne pour mon capteur, avec mes conditions d’éclairage ambiant, et peut détecter les couleurs « rouge », « vert », « bleu », « violet », « jaune ». Sa précision et le nombre de couleurs dépendent directement des échantillons de données que j’ai collectés.

En général, vous devez collecter vos propres échantillons pour construire un détecteur de couleur qui fonctionne dans votre environnement et pour les couleurs qui vous intéressent. Heureusement, c’est facile. Vous pouvez utiliser le code existant. Il suffit de commenter la ligne où le classificateur est appelé et de modifier l’affichage comme montré ci-dessous :

void loop() {
  uint16_t red = readPulse(LOW, LOW);
  uint16_t blue = readPulse(LOW, HIGH);
  uint16_t green = readPulse(HIGH, HIGH);
  uint16_t white = readPulse(HIGH, LOW);

  normalize(red, green, blue, white);
  //char *color = classify(red, green, blue);

  int index = 1;  // first color
  aprintf("{%3d, %3d, %3d, %d}",
          red, green, blue, index);

  delay(1000);
}

Commencez maintenant à collecter des échantillons en choisissant votre première couleur (index=1) et placez un objet de cette couleur devant le capteur. Variez la distance et la lumière ambiante autant que possible. Après quelques secondes, vous aurez assez d’échantillons imprimés pour votre première couleur, par exemple « vert »

// green
{296,225,303,1},
{279,232,311,1},
...
{293,230,309,1},
{313,231,320,1},

Éliminez les « mauvais » échantillons, par exemple quand l’objet a été retiré du détecteur, et sauvegardez les « bons » échantillons dans un éditeur de texte. Ensuite, incrémentez la variable index à 2 et répétez le processus pour votre couleur suivante, par exemple « violet »

// purple
{271,361,215,2},
{274,346,217,2},
...
{284,365,218,2},
{270,356,221,2},

Continuez ainsi jusqu’à avoir des données d’échantillons pour toutes les couleurs que vous voulez détecter. Puis copiez toutes les données d’échantillons dans un grand tableau :

const uint16_t samples[n_samples][4] = {
  // green
  {296,225,303,1},
  {279,232,311,1},
  ...
  {293,230,309,1},
  {313,231,320,1},

  // purple
  {271,361,215,2},
  {274,346,217,2},
  ...
  {284,365,218,2},
  {270,356,221,2},

  // other colors
  ...
};

Enfin, ajustez la constante n_samples pour qu’elle corresponde au nombre d’échantillons dans la liste et vous aurez terminé la collecte des données et l’entraînement de votre classificateur.

Remettez le code à appeler le classificateur et afficher sa réponse pour tester votre classificateur nouvellement entraîné :

void loop() { 
  ...
  char *color = classify(red, green, blue);
  aprintf("{%3d, %3d, %3d}  =>  %s\n",
          red, green, blue, color);
  ...
}

Si vous trouvez des cas où le détecteur ne détecte pas correctement une couleur, ajoutez ces cas comme échantillons dans le jeu de données. Au bout d’un moment, le jeu de données peut devenir assez volumineux. Vous pouvez le réduire en supprimant les doublons ou quasi-doublons, par exemple :

{271,361,215,3},
{274,346,217,3},  <= duplicate
{268,355,217,3},
{268,333,214,3},
{274,346,217,3},  <= duplicate
{297,387,225,3},
{273,346,216,3},  <= near duplicate
{270,356,221,3},

Vous pouvez faire cela manuellement ou écrire un code pour supprimer les doublons automatiquement.

Dans la dernière section, nous allons ajouter un écran OLED pour afficher la couleur détectée sur un petit écran, au lieu d’imprimer sur le moniteur série.

Ajout d’un OLED pour afficher les couleurs détectées

Ajouter l’OLED est simple. Il suffit de connecter SDA et SCL de l’OLED à A4 et A5 de l’Arduino (SDA->A4, SCL->A5). Comme l’OLED peut fonctionner en 5 V, nous pouvons partager les lignes d’alimentation. Connectez VCC à 5 V et GND à GND. La photo ci-dessous montre le câblage complet :

Connexion du TCS230 et de l’OLED à l’Arduino

Code pour afficher les couleurs détectées

Voici le code qui affiche la couleur détectée par le TCS230 et notre classificateur des plus proches voisins sur l’OLED :

#include "Adafruit_SSD1306.h"

const int s0 = 8;
const int s1 = 9;
const int s2 = 10;
const int s3 = 11;
const int out = 12;

const uint16_t reject = 1000;
const char *colors[] = { "???", "red", "green", "blue", "purple", "yellow" };
const int n_samples = 33;
const uint16_t samples[n_samples][4] = {
  // red
  { 158, 422, 358, 1 },
  { 165, 354, 311, 1 },
  { 145, 347, 307, 1 },
  { 160, 376, 321, 1 },
  { 177, 343, 309, 1 },
  { 156, 403, 343, 1 },
  // green
  { 296, 225, 303, 2 },
  { 279, 232, 311, 2 },
  { 300, 223, 313, 2 },
  { 271, 231, 295, 2 },
  { 293, 230, 309, 2 },
  { 313, 231, 320, 2 },
  // blue
  { 413, 300, 186, 3 },
  { 337, 286, 191, 3 },
  { 376, 293, 200, 3 },
  { 354, 290, 206, 3 },
  { 395, 287, 187, 3 },
  { 351, 285, 197, 3 },
  { 427, 304, 190, 3 },
  // purple
  { 271, 361, 215, 4 },
  { 274, 346, 217, 4 },
  { 268, 355, 217, 4 },
  { 268, 333, 214, 4 },
  { 297, 387, 225, 4 },
  { 284, 365, 218, 4 },
  { 270, 356, 221, 4 },
  // yellow
  { 193, 243, 450, 5 },
  { 194, 252, 464, 5 },
  { 200, 244, 433, 5 },
  { 205, 252, 442, 5 },
  { 210, 245, 357, 5 },
  { 236, 284, 436, 5 },
  { 206, 253, 473, 5 },
};

Adafruit_SSD1306 oled(128, 64, &Wire, -1);

void initOLED() {
  oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  oled.setTextColor(WHITE);
}

void display(const char *color) {
  oled.clearDisplay();

  oled.setTextSize(3);
  oled.setCursor(15,10);
  oled.print(color);
  oled.display();
}

float sqr(float a) {
  return a * a;
}

float eucDist(uint16_t r, uint16_t g, uint16_t b, uint16_t *s) {
  return sqrt(sqr(r - s[0]) + sqr(g - s[1]) + sqr(b - s[2]));
}

char *classify(uint16_t r, uint16_t g, uint16_t b) {
  char *color = colors[0];
  float mindist = reject;

  for (int i = 0; i < n_samples; i++) {
    float dist = eucDist(r, g, b, samples[i]);
    if (dist < mindist) {
      mindist = dist;
      color = colors[samples[i][3]];
    }
  }

  return color;
}

void initPins() {
  pinMode(s0, OUTPUT);
  pinMode(s1, OUTPUT);
  pinMode(s2, OUTPUT);
  pinMode(s3, OUTPUT);
  pinMode(out, INPUT);
}

void setScaling(int s0state, int s1state) {
  digitalWrite(s0, s0state);
  digitalWrite(s1, s1state);
}

uint16_t readPulse(int s2state, int s3state) {
  delay(20);
  digitalWrite(s2, s2state);
  digitalWrite(s3, s3state);
  return pulseIn(out, LOW);
}

void normalize(uint16_t &red, uint16_t &green, uint16_t &blue, uint16_t white) {
  red = 100 * red / (white + 1);
  green = 100 * green / (white + 1);
  blue = 100 * blue / (white + 1);
}

void setup() {
  initPins();
  initOLED();
  setScaling(HIGH, LOW);  // 20 %
}

void loop() {
  uint16_t red = readPulse(LOW, LOW);
  uint16_t blue = readPulse(LOW, HIGH);
  uint16_t green = readPulse(HIGH, HIGH);
  uint16_t white = readPulse(HIGH, LOW);

  normalize(red, green, blue, white);
  char *color = classify(red, green, blue);
  display(color);
 
  delay(100);
}

Les principaux changements sont l’include de la bibliothèque Adafruit_SSD1306 Library nécessaire pour communiquer avec l’OLED, la création de l’objet OLED, une fonction initOLED pour initialiser l’OLED, et une fonction display pour afficher la couleur détectée sur l’OLED :

#include "Adafruit_SSD1306.h"

...

Adafruit_SSD1306 oled(128, 64, &Wire, -1);

void initOLED() {
  oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  oled.setTextColor(WHITE);
}

void display(const char *color) {
  oled.clearDisplay();

  oled.setTextSize(3);
  oled.setCursor(15,10);
  oled.print(color);
  oled.display();
}

...


Si vous n’avez pas encore installé la bibliothèque Adafruit_SSD1306 Library, vous devrez le faire. Installez-la via le gestionnaire de bibliothèques comme d’habitude :

Adafruit_SSD1306 library installed in Library Manager
Bibliothèque Adafruit_SSD1306 installée via le gestionnaire de bibliothèques

Notez que l’adresse I2C pour l’écran OLED est réglée sur 0x3C dans oled.begin(). La plupart de ces petits OLED utilisent cette adresse (or 0x27) mais la vôtre peut être différente. Si vous ne voyez rien sur l’OLED, c’est probablement qu’il a une adresse I2C différente et vous devrez la modifier.

Si vous ne connaissez pas l’adresse I2C, consultez le tutoriel How to Interface the SSD1306 I2C OLED Graphic Display With Arduino pour la trouver. Le tutoriel Use SSD1306 I2C OLED Display With Arduino vous expliquera aussi comment utiliser un OLED.

Fonction Loop

Le seul changement dans la fonction loop est que nous remplaçons l’affichage sur le moniteur série par un appel à la fonction display avec la couleur détectée :

void loop() {
  uint16_t red = readPulse(LOW, LOW);
  uint16_t blue = readPulse(LOW, HIGH);
  uint16_t green = readPulse(HIGH, HIGH);
  uint16_t white = readPulse(HIGH, LOW);

  normalize(red, green, blue, white);
  char *color = classify(red, green, blue);
  display(color);
 
  delay(100);
}

Et voilà ! Vous devriez maintenant avoir un détecteur de couleur que vous pouvez ajuster (entraîner) pour détecter n’importe quelle couleur sous différentes conditions d’éclairage. La courte vidéo ci-dessous montre mon détecteur de couleur en action :

Démo du détecteur de couleur des plus proches voisins avec TCS230

Vous pouvez voir qu’il détecte bien les trois couleurs violet, jaune et bleu et affiche « ??? » quand je passe d’une couleur à l’autre ou quand le détecteur est trop loin.

Conclusions

Dans ce tutoriel, vous avez appris à utiliser le capteur de couleur TCS230 avec un Arduino pour construire un détecteur de couleur entraînable utilisant un classificateur des plus proches voisins. Ce détecteur fonctionne assez bien, mais il y a beaucoup d’améliorations possibles.

Si vous voulez détecter beaucoup de couleurs et que les lectures du capteur sont plus bruitées, un k-nearest neighbor classifier rendra la détection plus robuste.

Au lieu de normaliser les lectures des canaux couleur par le canal blanc, vous pourriez collecter des échantillons en 4 dimensions (r, g, b, w) et créer un classificateur des plus proches voisins en 4 dimensions. Un tel classificateur sera plus robuste aux changements de lumière ambiante mais nécessitera aussi plus d’échantillons d’entraînement.

Cependant, vous pouvez compresser la liste d’échantillons, réduisant ainsi les besoins en mémoire et augmentant la vitesse de classification, en ne stockant que les centres des clusters de couleurs. Alternativement, vous pouvez supprimer les échantillons du centre et ne garder que ceux proches de la frontière des classes.

Enfin, nous avons « entraîné » le classificateur manuellement en collectant des échantillons et en les codant en dur. Mais vous pourriez aussi ajouter un bouton « entraînement » au circuit et un code qui stocke un échantillon dans l’EEPROM lorsqu’il est pressé. Cela vous permettrait d’entraîner dynamiquement votre détecteur de couleur.

Si vous avez des questions, n’hésitez pas à les poser dans la section commentaires.

Bon bricolage ; )