Skip to Content

Detector de color por vecino más cercano con TCS230 y Arduino

Detector de color por vecino más cercano con TCS230 y Arduino

En este tutorial aprenderás a usar el sensor de color TCS230 con un Arduino para construir un detector de color entrenable basado en un clasificador de vecino más cercano.

El sensor de color TCS230 no te dice directamente qué color detecta, sino que devuelve lecturas de los canales rojo, verde, azul (y claro). Para la mayoría de aplicaciones prácticas, tienes que convertir esas lecturas en un color detectado, por ejemplo «morado». Detectar un color de forma fiable usando umbrales en las lecturas de los canales puede ser complicado. Por eso aplicaremos un poco de aprendizaje automático y usaremos un clasificador de vecino más cercano entrenable.

Piezas necesarias

Necesitarás un sensor de color TCS230 y un microcontrolador. Yo usé un Arduino Uno para este proyecto, pero cualquier otro Arduino o ESP32 también funcionará. Además, usaremos un OLED para mostrar el color detectado y otra información en una pequeña pantalla.

Sensor de distancia TFmini Plus

Arduino

Arduino Uno

USB Data Sync cable Arduino

Cable USB para Arduino UNO

Dupont wire set

Juego de cables Dupont

Half_breadboard56a

Protoboard

OLED display

Pantalla 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.

Conceptos básicos del sensor de color TCS230

El TCS230 es un convertidor de luz a frecuencia para color. Está compuesto por una matriz de 8 x 8 fotodiodos que detectan luz. Hay 16 fotodiodos con filtro rojo sensibles a luz roja, 16 con filtro verde para luz verde, 16 con filtro azul y 16 sin filtro para luz «blanca».

Si miras de cerca la imagen del chip TCS230 abajo, puedes ver la matriz 8 x 8 de fotodiodos con sus filtros de colores (y claro).

TCS230 color light-to-frequency converter
Convertidor de luz a frecuencia para color TCS230

Función del sensor de color TCS230

Además de la matriz de fotodiodos, el TCS230 contiene un convertidor de corriente a frecuencia, que convierte la corriente que pasa por un conjunto de fotodiodos en una onda cuadrada. Un conjunto está compuesto por 16 fotodiodos para un color específico (rojo, verde, azul) o claro, y la frecuencia de la onda cuadrada es proporcional a la corriente que pasa por ese conjunto de fotodiodos.

Functional Block Diagram of TCS230
Diagrama funcional del TCS230 (source)

Para medir la intensidad de uno de los tres colores (o claro) seleccionas el conjunto correspondiente de fotodiodos (rojo, verde, azul, blanco) configurando dos entradas (S2, S3) y luego lees la frecuencia en la salida. El rango de frecuencia (escala) puede controlarse con otras dos entradas (S0, S1).

Pinout del sensor de color TCS230

La siguiente imagen muestra el pinout del chip TCS230. Puedes ver los pines S0 y S1 para seleccionar el rango de frecuencia de salida, y los pines S2, S3 para seleccionar el conjunto de fotodiodos (color).

Pinout of TCS230
Pinout del TCS230 (source)

La salida OUT puede habilitarse o deshabilitarse mediante el pin OE. VDD y GND son para la alimentación con un voltaje de 2.7 V a 5.5 V. La tabla a continuación resume el pinout:

PinNombreDescripción
1,2S0, S1Escala de frecuencia de salida
3OEHabilitar salida de frecuencia
4GNDMasa
5VDDAlimentación de voltaje
6OUTFrecuencia de salida 
7,8S2, S3Selección del tipo de fotodiodo

Selección de fotodiodo/color

Como se mencionó antes, antes de medir la intensidad de un canal de color específico, primero debes seleccionarlo mediante los pines S2 y S3. La tabla a continuación muestra qué configuración de S2 y S3 corresponde a qué canal de color:

Canal de colorS2S3
RojoLOWLOW
AzulLOWHIGH
ClaroHIGHLOW
VerdeHIGHHIGH

Escala de frecuencia

El rango de frecuencia de la salida puede variar entre 100%, 20%, 2% y apagado configurando los siguientes valores para los pines de control S0 y S1:

Escala de salidaS0S1f minf max
ApagadoLOWLOW
2%LOWHIGH10kHz12kHz
20%HIGHLOW100kHz120kHz
100%HIGHHIGH500kHz600kHz

Módulo sensor de color TCS230

Normalmente no se usa el TCS230 directamente, sino un módulo sensor de color TCS230 que incluye algunos componentes adicionales y es más fácil de conectar. La imagen abajo muestra el frente y la parte trasera de un módulo típico:

Front and back of TCS230 Color Sensor Module
Frontal y trasera del módulo sensor de color TCS230

En el frente del módulo hay cuatro LEDs blancos para iluminación y una carcasa protectora negra que contiene el chip TCS230. Si miras de cerca puedes ver el chip en el centro:

CS230 chip in Sensor Module
Chip TCS230 en el módulo sensor

Esquema del módulo sensor de color TCS230

El módulo TCS230 tiene esencialmente el mismo pinout que el chip TCS230, salvo dos cambios. No tiene el pin OE pero sí un pin adicional LED.

El esquema del módulo revela que OE está conectado a VSS, lo que significa que la salida está siempre habilitada. El pin LED está conectado a la compuerta de un MOSFET (Q1) que conmuta los cuatro LEDs de iluminación (D1…D4). Como el pin LED tiene una resistencia pullup interna, los LEDs están activos por defecto y puedes apagarlos conectando el pin LED a tierra.

Schematics of TCS230 Sensor Module
Esquema del módulo sensor TCS230


Ten en cuenta que los pines S0 y S1 también tienen pullups internos y la escala de salida por defecto para la frecuencia es del 100%. Por otro lado, los pines S2 y S3 no tienen pullups y debes configurarlos para seleccionar un canal de color.

En la siguiente sección conectamos el módulo TCS230 a un Arduino y escribimos código para probar el sensor.

Conectando el TCS230 al Arduino

El siguiente diagrama muestra cómo conectar el módulo TCS230 a un Arduino. Uso 5V como alimentación, pero 3.3V también funcionaría. Los pines de control S0, S1, S2 y S3 del módulo TCS230 están conectados a los pines 8, 9, 10, 11 del Arduino.

Connecting TCS230 to Arduino
Conexión del TCS230 al Arduino

La tabla a continuación muestra nuevamente las conexiones que debes hacer

TCS230 Arduino
VCC5V
GNDGND
S08
S19
S210
S311
OUT12

Ten en cuenta que el TCS230 tiene dos pines VCC y dos pines GND. Puedes usar cualquiera. El pin LED no está conectado, pero podrías conectarlo a una salida digital para encender o apagar los LEDs de iluminación.

Código para leer colores con TCS230

En el siguiente código usamos un sensor de color TCS230 para leer los valores de los diferentes canales de color (rojo, verde, azul y blanco) y mostrar los resultados en el monitor serial.

#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);
}

Vamos a desglosar el código en sus componentes para entenderlo mejor.

Inclusiones

El código usa la librería aprintf library, que simplifica el formateo de texto para imprimir. Es básicamente una versión de la función printf(). Para más detalles lee el tutorial How To Print To Serial Monitor On Arduino.

Para instalarla, ve a github repo, haz clic en el botón verde «Code» y selecciona «Download ZIP»:

Descargar la librería aprintf como archivo .ZIP

Luego ve a «Sketch» -> «Include Library» -> «Add .ZIP Library» para instalarla.

Installing a .ZIP Arduino library
Instalando una librería Arduino desde un archivo .ZIP

Para usar aprintf library necesitamos la siguiente inclusión:

#include "aprintf.h"

Constantes

A continuación definimos los pines usados para conectar el sensor TCS230 al Arduino.

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

Aquí, s0 y s1 son para configurar la escala de frecuencia, mientras que s2 y s3 son para seleccionar el canal de color. El pin out es donde el sensor emite la señal de frecuencia correspondiente a la intensidad del color detectado.

Inicialización de pines

La función initPins() configura los modos de los pines definidos. Los pines del sensor s0, s1, s2 y s3 se configuran como OUTPUT, mientras que el pin out se configura como INPUT.

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

Escala de frecuencia

La función setScaling() se usa para configurar la escala de frecuencia de la señal de salida del sensor. Ajustando los estados de s0 y s1 podemos modificar la resolución de la salida del sensor. Más sobre esto después.

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

Lectura de pulsos

La función readPulse() se encarga de leer la duración en microsegundos del pulso emitido por el sensor. Recibe los estados de s2 y s3 como parámetros para seleccionar qué canal de color leer. La función espera 20 milisegundos para estabilizar el sensor entre lecturas.

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

Ten en cuenta que esta función no devuelve la frecuencia de la onda cuadrada, sino la duración de los pulsos (muchos tutoriales sobre el TCS230 se equivocan en esto). Cuanto más intenso es el color, mayor es la frecuencia pero más corto el pulso. Por lo tanto, hay una relación inversa y cuanto más intenso es el color, menor es el valor devuelto.

Si quieres convertir el valor a frecuencia, tendrías que contar el número de pulsos n en un segundo y tomar el inverso:

f = 1 / n

pero trabajaremos directamente con las duraciones de los pulsos.

Función setup

En la función setup() inicializamos la comunicación serial a 9600 baudios y llamamos a la función initPins() para configurar los pines. También configuramos la escala al 20% llamando a setScaling(HIGH, LOW), lo que nos da una buena resolución.

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

Si comparas los factores de escala 2%, 20% y 100%, verás que las lecturas de los canales de color son mayores para el 2% y menores para el 100%. A continuación algunos ejemplos de lecturas para diferentes escalas cuando el sensor detectó un objeto verde y uno blanco con la misma iluminación y distancia:

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

El factor de escala 20% es un buen compromiso entre resolución y robustez. Además, las lecturas para 20% caben fácilmente en un entero, mientras que para 2% puedes tener desbordamientos cuando el sensor está en completa oscuridad. Por eso elegí el factor 20%.

Función loop

En la función loop() leemos las duraciones de pulso para los colores rojo, azul, verde y blanco llamando a readPulse() con los parámetros adecuados. Los resultados se almacenan en las variables red, green, blue y 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);

Como se mencionó antes, los valores de color son inversos a la intensidad de los colores detectados. Imprimimos los valores de color en el monitor serial usando la función aprintf(), que formatea la salida para mayor legibilidad. Finalmente, añadimos un retardo de 1000 milisegundos (1 segundo) antes de repetir el ciclo.

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

Ejemplo de salida

Si subes el código a tu Arduino y abres el Monitor Serial, deberías ver lecturas de los canales de color similares a las siguientes:

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 pones un objeto rojo frente al sensor, el valor rojo debería disminuir; de forma similar, si el sensor ve un objeto azul o verde, el valor del canal correspondiente debería bajar.

Sin embargo, hay un problema: los valores dependen mucho de la iluminación ambiental, lo que dificulta detectar colores de forma fiable. En la siguiente sección normalizaremos las lecturas.

Normalización de brillo para TCS230

El rango total de las lecturas de color depende de la escala elegida. Con 20% puedes obtener valores entre 0 y aproximadamente 15000. Puedes probarlo fácilmente cubriendo el sensor con el dedo (oscuridad total) o iluminándolo con una luz fuerte. Aquí un ejemplo de las lecturas que obtuve:

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

Esta fuerte dependencia de la iluminación ambiental hace casi imposible definir umbrales robustos para detectar un color. Por ejemplo, un código como el siguiente no funcionará muy bien:

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

Por eso necesitamos normalizar las lecturas de color. Esto se logra fácilmente dividiendo las lecturas de color por el valor del canal blanco (brillo). Aquí está la función correspondiente:

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);
}

Multiplica la lectura de color (rojo, verde, azul) por 100 y luego la divide por el valor blanco más 1. Añadimos el +1 para evitar una posible división por cero, y multiplicamos por 100 para mantener los valores de color en un rango similar (misma resolución).

Si comparas las lecturas de color con y sin normalización, verás que las normalizadas son más estables. Por ejemplo, coloqué el TCS230 a varias distancias (1cm … 4cm) de un objeto azul.

Reading blue value with TCS230
Lectura del valor azul con TCS230

Sin normalización, obtuve valores azules entre 43 y 169, una diferencia o variación de 126 unidades. Con normalización, medí valores entre 188 y 237, una diferencia de solo 49 unidades. Así que las lecturas de color son más estables (menos sensibles a la luz ambiental) con normalización.

En la función loop puedes añadir fácilmente la función de normalización así:

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);
}

Sin embargo, la normalización no es perfecta y la luz ambiental aún afecta las lecturas del sensor. Además, si quieres usar umbrales para detectar colores distintos de rojo, verde o azul, el código se vuelve complejo.

Por ejemplo, el morado es una combinación de valores rojo, verde y azul. Para detectarlo tendrías que implementar algo como lo siguiente:

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

Este código es complejo, los umbrales son difíciles de elegir y no funcionará muy bien. En cambio, entrenaremos un clasificador simple para aprender qué lecturas de canales de color corresponden a qué colores. Será mucho más robusto, fácil de implementar y también más divertido ; )

Clasificando lecturas de color del TCS230

Vamos a usar un nearest-neighbor-classifier para clasificar las lecturas de color del TCS230. Para entender mejor cómo funciona este clasificador, recopilé algunos datos. Coloqué el TCS230 frente a cuadrados rojo, verde, morado y amarillo e imprimí las lecturas de los canales de color. Aquí está la cuadrícula de colores que usé:


Colored squared used to train classifier
Cuadrados de colores usados para entrenar el clasificador

Gráfica de lecturas de canales de color

Repetí este proceso varias veces y registré las lecturas de rojo y verde. (Dejo fuera las lecturas azules por ahora, pero las usaremos después). Así se veían las muestras de datos:

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
...

Luego, tracé cada muestra en un gráfico de dispersión 2D. Cada punto en el gráfico representa una muestra (una lectura de los valores rojo y verde), y el color de cada punto indica qué cuadrado de color estaba frente al sensor.

2D Scatter plot of Red and Green channel readings
Gráfico de dispersión 2D de lecturas de canales rojo y verde

Como puedes ver, las muestras para objetos rojo, morado, amarillo y verde se agrupan muy bien, con la excepción de un posible valor atípico para un objeto morado en la esquina superior derecha.

Clasificando el color de un objeto

Con estos datos ahora es fácil determinar el color de un objeto. Simplemente tomamos una lectura de los canales rojo y verde y encontramos la muestra más cercana a esa lectura. En el gráfico abajo, la cruz marca el punto de una nueva lectura y puedes ver que la muestra más cercana es morada. Por lo tanto, clasificaríamos nuestro objeto como de color morado.

Classifying the color of objects
Clasificando el color de objetos

Este método para clasificar objetos basado en la muestra más cercana se llama Nearest-Neighbor Classifier o búsqueda de vecino más cercano. Hay muchas variaciones de este algoritmo.

Lo más importante es que este algoritmo también funciona con datos de mayor dimensión. Esto significa que podemos usar las tres lecturas de canales de color para determinar un color. El siguiente gráfico muestra algunas muestras para cinco colores (rojo, morado, amarillo, verde, azul) en un espacio 3D. Cada muestra tiene tres dimensiones, las lecturas de los canales rojo, verde y azul, y es un punto en este gráfico de dispersión 3D:

3D Scatter plot of Red, Green and Blue channel readings
Gráfico de dispersión 3D de lecturas de canales rojo, verde y azul

Como antes, el color del punto indica a qué color pertenece la muestra. Determinar el color de un nuevo objeto en este espacio 3D de mayor dimensión funciona igual que antes. Simplemente encuentra la muestra más cercana a tus lecturas de canales. Solo que es más difícil de visualizar, por eso empecé con un espacio 2D.

En la siguiente sección implementaremos un clasificador de vecino más cercano simple que funcione en este espacio 3D.

Código para clasificador de vecino más cercano con TCS230

El código en esta sección es una extensión del código anterior. Añade algunos datos de muestra y el clasificador de vecino más cercano. Imprime las lecturas de los canales de color (rojo, verde, azul) y el color detectado. La salida se verá así:

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

Ten en cuenta que puede detectar colores como «morado» o «amarillo», que son combinaciones de valores rojo, verde y azul. Además, si no puede detectar un color con confianza, imprime «???».

Echa un vistazo rápido al código primero y luego discutimos cómo funciona.

#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 para muestras, colores y rechazo

Al inicio del código añadimos tres constantes. La constante reject determina cuándo el clasificador decide que no puede detectar un color. Más sobre esto después.

Luego tenemos la lista de colors que vamos a detectar. El primer «color» es «???», que se devuelve cuando el clasificador no pudo detectar un color. Puedes cambiar y ampliar esta lista para detectar tantos colores como quieras.

Finalmente, tenemos una lista de muestras n_samples, donde cada muestra contiene lecturas para los canales rojo, verde y azul más un índice de color. El índice de color es simplemente la posición en la lista de 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 },
  ...
};

Por ejemplo, la última muestra { 193, 243, 450, 5 } significa que tenemos las lecturas para los tres canales de color red==193, green==243, blue==450 y que esta muestra corresponde al color colors[5] == "yellow".

Te mostraré más adelante cómo recopilar estas muestras.

Función de distancia

Un clasificador de vecino más cercano funciona encontrando la muestra más cercana a una lectura dada de colores de canales. Por eso necesitamos una función para calcular la distancia entre una lectura de colores y una muestra. Una función de distancia común para este propósito es la Euclidean distance.

La función eucDist calcula la Euclidean distance entre las lecturas de canales de color r, g, b y una muestra s. Cuanto más cercanas estén las lecturas a la muestra, menor será la Euclidean distance.

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 función cuadrado sqr es una función auxiliar que calcula el cuadrado de su argumento a.

Función de clasificación

La función classify recorre todas las samples, encuentra la muestra con la distancia euclidiana más pequeña mindist a las lecturas de canales de color r, g, b y devuelve el color detectado color como cadena.

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 distancia más pequeña mindist no es menor que el umbral reject, se devuelve el primer color en el arreglo colors. Esta es la cadena «???«, que indica que las lecturas de color no estaban lo suficientemente cerca de una muestra para detectar el color con fiabilidad.

Puedes hacer el umbral de rechazo muy grande (1e8) y el clasificador siempre responderá con un color, pero generalmente es mejor dejar que el clasificador te diga si no puede clasificar con confianza el color de un objeto.

Ejemplo de salida

Si compilas y subes el código, deberías ver las lecturas de los canales de color y el color detectado impresos así:

{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}  =>  ???

En la siguiente sección te mostraré cómo adaptar el código para elegir los colores que quieres detectar.

Recopilando datos de entrenamiento para el detector de color

El código anterior demuestra cómo implementar un detector de color usando un clasificador de vecino más cercano. Funciona para mi sensor, con mis condiciones de iluminación ambiental y puede detectar los colores «rojo», «verde», «azul», «morado», «amarillo». Su precisión y el número de colores dependen directamente de las muestras de datos que recopilé.

Normalmente, necesitas recopilar tus propias muestras para construir un detector de color que funcione en tu entorno y para los colores que te interesan. Por suerte, esto es fácil. Puedes usar el código existente. Solo comenta la línea donde se llama al clasificador y cambia la impresión como se muestra a continuación:

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);
}

Ahora, comienza a recopilar muestras eligiendo tu primer color (index=1) y coloca un objeto de ese color frente al sensor. Varía la distancia y la luz ambiental tanto como puedas. Después de unos segundos tendrás suficientes muestras impresas para tu primer color, por ejemplo «verde»

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

Descarta las muestras «malas», por ejemplo cuando el objeto fue retirado del detector, y guarda las muestras «buenas» en un editor de texto. Luego incrementa la variable index a 2 y repite el proceso para tu siguiente color, por ejemplo «morado»

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

Sigue haciendo esto hasta tener datos de muestra para todos los colores que quieres detectar. Luego copia todos los datos juntos en un gran arreglo:

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
  ...
};

Finalmente ajusta la constante n_samples para que coincida con el número de muestras en la lista y habrás terminado la recopilación de datos y el entrenamiento de tu clasificador.

Cambia el código para volver a llamar al clasificador e imprimir su respuesta para probar tu clasificador recién entrenado:

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

Si encuentras casos donde el detector falla al detectar un color correctamente, añade esos casos como muestras al conjunto de datos. Después de un tiempo el conjunto de datos puede volverse bastante grande. Puedes reducirlo eliminando duplicados o casi duplicados, por ejemplo.

{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},

Puedes hacer esto manualmente o escribir código para eliminar duplicados de forma programada.

En la siguiente y última sección añadiremos un OLED para mostrar el color detectado en una pequeña pantalla, en lugar de imprimirlo en el Monitor Serial.

Añadiendo un OLED para mostrar los colores detectados

Añadir el OLED es sencillo. Simplemente conecta SDA y SCL del OLED a A4 y A5 del Arduino (SDA->A4, SCL->A5). Como el OLED puede funcionar a 5V, podemos compartir las líneas de alimentación. Conecta VCC a 5V y GND a GND. La imagen abajo muestra el cableado completo:

Conectando TCS230 y OLED al Arduino

Código para mostrar colores detectados

Abajo está el código que muestra el color detectado por el TCS230 y nuestro clasificador de vecino más cercano en el 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);
}

Los principales cambios son las include de la Adafruit_SSD1306 Library necesarias para comunicarse con el OLED, la creación del objeto OLED, una función initOLED para inicializar el OLED, y una función display para imprimir el color detectado en el 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 aún no has instalado la Adafruit_SSD1306 Library, tendrás que hacerlo. Solo instálala vía el Library Manager como de costumbre:

Adafruit_SSD1306 library installed in Library Manager
Librería Adafruit_SSD1306 instalada en Library Manager

Ten en cuenta que la dirección I2C para la pantalla OLED está configurada a 0x3C en oled.begin(). La mayoría de estos pequeños OLED usan esta dirección (or 0x27) pero la tuya podría ser diferente. Si no ves nada en el OLED, probablemente tenga una dirección I2C distinta y tendrás que cambiarla.

Si no sabes la dirección I2C, echa un vistazo al tutorial How to Interface the SSD1306 I2C OLED Graphic Display With Arduino para encontrarla. También el tutorial Use SSD1306 I2C OLED Display With Arduino te enseñará más sobre cómo usar un OLED.

Función loop

El único cambio en la función loop es que reemplazamos la impresión al Monitor Serial llamando a la función display con el color detectado:

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);
}

¡Y eso es todo! Ahora deberías tener un detector de color que puedes ajustar (entrenar) para detectar cualquier color que quieras bajo condiciones de iluminación variables. El breve video abajo muestra mi detector de color en acción:

Demostración del detector de color vecino más cercano con TCS230

Puedes ver que detecta bien los tres colores morado, amarillo y azul, e imprime «???» cuando me muevo entre colores o cuando el detector está demasiado lejos.

Conclusiones

En este tutorial aprendiste a usar el sensor de color TCS230 con un Arduino para construir un detector de color entrenable usando un clasificador de vecino más cercano. Este detector funciona bastante bien, pero hay muchas mejoras posibles.

Si quieres detectar muchos colores y las lecturas del sensor son más ruidosas, un k-nearest neighbor classifier hará la detección más robusta.

En lugar de normalizar las lecturas de los canales de color por el canal blanco, podrías recopilar muestras de 4 dimensiones (r, g, b, w) y crear un clasificador de vecino más cercano de 4 dimensiones. Tal clasificador será más robusto frente a cambios en la luz ambiental, pero también necesitará más muestras de entrenamiento.

Sin embargo, puedes comprimir la lista de muestras y así reducir los requisitos de memoria y aumentar la velocidad de clasificación, almacenando solo los centros de los grupos de color. Alternativamente, podrías eliminar muestras del centro y mantener solo las cercanas al borde de la clase.

Finalmente, «entrenamos» el clasificador manualmente recopilando muestras y codificándolas. Pero también podrías añadir un botón de «entrenamiento» al circuito y código que almacene una muestra en la EEPROM al pulsarlo. Esto te permitiría entrenar dinámicamente tu detector de color.

Si tienes alguna pregunta, no dudes en dejarla en la sección de comentarios.

¡Feliz bricolaje ; )