Neste tutorial, vais aprender a usar o Sensor de Cor TCS230 com um Arduino para construir um detector de cores treinável baseado num classificador de vizinho mais próximo.
O Sensor de Cor TCS230 não indica diretamente qual cor está a ver, mas retorna leituras dos canais vermelho, verde, azul (e claro). Para a maioria das aplicações práticas, é necessário converter essas leituras numa cor detectada, por exemplo, “roxo”. Pode ser complicado detetar uma cor de forma fiável usando limiares nas leituras dos canais. Por isso, vamos aplicar um pouco de machine learning e usar um classificador treinável de vizinho mais próximo.
Peças Necessárias
Vais precisar de um Sensor de Cor TCS230 e um microcontrolador. Usei um Arduino Uno para este projeto, mas qualquer outro Arduino ou ESP32 também funciona. Também vamos usar um OLED para mostrar a cor detectada e outras informações num pequeno ecrã.

Sensor de Distância TFmini Plus

Arduino Uno

Cabo USB para Arduino UNO

Conjunto de Fios Dupont

Breadboard

Ecrã 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.
Noções Básicas do Sensor de Cor TCS230
O TCS230 é um conversor de luz para frequência de cor. É composto por uma matriz 8 x 8 de fotodíodos que detetam luz. Existem 16 fotodíodos com filtro vermelho sensíveis à luz vermelha, 16 com filtro verde para luz verde, 16 com filtro azul e 16 sem filtro para luz “branca”.
Se olhares atentamente para a imagem do chip TCS230 abaixo, podes ver a matriz 8 x 8 de fotodíodos com os seus filtros coloridos (e claro).

Função do Sensor de Cor TCS230
Além da matriz de fotodíodos, o TCS230 contém um Conversor Corrente-frequência, que converte a corrente que passa por um conjunto de fotodíodos numa onda quadrada. Um conjunto é composto por 16 fotodíodos para uma cor específica (vermelho, verde, azul) ou claro, e a frequência da onda quadrada é proporcional à corrente através do conjunto de fotodíodos.

Para medir a intensidade de uma das três cores (ou claro), selecionas o conjunto correspondente de fotodíodos (vermelho, verde, azul, branco) definindo duas entradas (S2, S3) e depois lês a frequência na saída. A gama de frequência (escala) pode ser controlada por outras duas entradas (S0, S1).
Pinout do Sensor de Cor TCS230
A imagem seguinte mostra o pinout do chip TCS230. Podes ver os pinos S0 e S1 para selecionar a gama de frequência de saída, e os pinos S2, S3 para selecionar o conjunto de fotodíodos (cor).

A saída OUT pode ser ativada ou desativada via o pino OE. VDD e GND são para a alimentação com uma tensão de 2,7 V a 5,5 V. A tabela abaixo resume o pinout:
| Pino | Nome | Descrição |
| 1,2 | S0, S1 | Escala da frequência de saída |
| 3 | OE | Ativação da frequência de saída |
| 4 | GND | Terra |
| 5 | VDD | Alimentação de tensão |
| 6 | OUT | Frequência de saída |
| 7,8 | S2, S3 | Seleção do tipo de fotodíodo |
Seleção do fotodíodo/cor
Como mencionado acima, antes de medires a intensidade de um canal de cor específico, tens de o selecionar via os pinos S2 e S3. A tabela abaixo mostra qual configuração de S2 e S3 corresponde a qual canal de cor:
| Canal de cor | S2 | S3 |
| Vermelho | LOW | LOW |
| Azul | LOW | HIGH |
| Claro | HIGH | LOW |
| Verde | HIGH | HIGH |
Escala de frequência
A gama de frequência da saída pode variar entre 100%, 20%, 2% e desligado, definindo os seguintes valores para os pinos de controlo S0 e S1:
| Escala de saída | S0 | S1 | f min | f max |
| Desligar | LOW | LOW | – | – |
| 2% | LOW | HIGH | 10kHz | 12kHz |
| 20% | HIGH | LOW | 100kHz | 120kHz |
| 100% | HIGH | HIGH | 500kHz | 600kHz |
Módulo Sensor de Cor TCS230
Normalmente não usas o TCS230 diretamente, mas sim um Módulo Sensor de Cor TCS230 que tem alguns componentes adicionais e é mais fácil de ligar. A imagem abaixo mostra a frente e o verso de um típico Módulo Sensor de Cor TCS230:

Na frente do módulo encontras quatro LEDs brancos para iluminação e uma caixa preta protetora que contém o chip TCS230. Se olhares atentamente, podes ver o chip TCS230 no centro:

Esquema do Módulo Sensor de Cor TCS230
O Módulo TCS230 tem essencialmente o mesmo pinout que o chip TCS230, exceto por duas alterações. Não existe o pino OE, mas há um pino adicional LED.
O esquema do módulo revela que OE está ligado a VSS, o que significa que a saída está sempre ativada. O pino LED está ligado à porta de um MOSFET (Q1) que comuta os quatro LEDs de iluminação (D1…D4). Como o pino LED tem um pullup interno, os LEDs estão ativos por defeito e podes desligá-los ligando o pino LED ao terra.

Note que os pinos S0 e S1 também têm pullups internos e a escala de saída padrão para a frequência é de 100%. Por outro lado, os pinos S2 e S3 não têm pullups e tens de os definir para selecionar um canal de cor.
Na próxima secção vamos ligar o Módulo TCS230 a um Arduino e escrever algum código para testar o sensor.
Ligando o TCS230 ao Arduino
O diagrama seguinte mostra como ligar o Módulo TCS230 a um Arduino. Eu uso 5V como alimentação, mas 3.3V também funcionaria. Os pinos de controlo S0, S1, S2 e S3 do Módulo TCS230 estão ligados aos pinos 8, 9, 10, 11 do Arduino.

A tabela abaixo mostra novamente as ligações que tens de fazer
| TCS230 | Arduino |
| VCC | 5V |
| GND | GND |
| S0 | 8 |
| S1 | 9 |
| S2 | 10 |
| S3 | 11 |
| OUT | 12 |
Note que existem dois pinos VCC e dois pinos GND no TCS230. Podes usar qualquer um deles. O pino LED não está ligado, mas poderias ligá-lo a uma saída digital para ligar ou desligar os LEDs de iluminação.
Código para ler cores com o TCS230
No código seguinte, usamos um sensor de cor TCS230 para ler os valores dos diferentes canais de cor (vermelho, verde, azul e branco) e imprimir os resultados no 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 decompor o código nos seus componentes para melhor compreensão.
Includes
O código usa a aprintf library, que simplifica a formatação de texto para impressão. É essencialmente uma versão da função printf(). Para mais detalhes, lê o tutorial How To Print To Serial Monitor On Arduino.
Para instalar, vai a github repo, clica no botão verde “Code” e depois seleciona “Download ZIP”:

Depois, vai a “Sketch” -> “Include Library” -> “Add .ZIP Library” para instalar.

Para usar a aprintf library precisamos da seguinte inclusão:
#include "aprintf.h"
Constantes
De seguida definimos os pinos usados para ligar o sensor TCS230 ao Arduino.
const int s0 = 8; const int s1 = 9; const int s2 = 10; const int s3 = 11; const int out = 12;
Aqui, s0 e s1 são para definir a escala da frequência, enquanto s2 e s3 são para selecionar o canal de cor. O pino out é onde o sensor envia o sinal de frequência correspondente à intensidade da cor detetada.
Inicialização dos Pinos
A função initPins() define os modos dos pinos para cada um dos pinos definidos. Os pinos do sensor s0, s1, s2 e s3 são configurados como OUTPUT, enquanto o pino out é configurado como INPUT.
void initPins() {
pinMode(s0, OUTPUT);
pinMode(s1, OUTPUT);
pinMode(s2, OUTPUT);
pinMode(s3, OUTPUT);
pinMode(out, INPUT);
}
Escala de Frequência
A função setScaling() é usada para configurar a escala da frequência do sinal de saída do sensor. Definindo os estados de s0 e s1, podemos ajustar a resolução da saída do sensor. Falaremos mais sobre isso depois.
void setScaling(int s0state, int s1state) {
digitalWrite(s0, s0state);
digitalWrite(s1, s1state);
}
Leitura dos Pulsos
A função readPulse() é responsável por ler a duração em microssegundos do pulso enviado pelo sensor. Recebe os estados de s2 e s3 como parâmetros para selecionar qual canal de cor ler. A função espera 20 milissegundos para estabilizar o sensor entre leituras.
uint16_t readPulse(int s2state, int s3state) {
delay(20);
digitalWrite(s2, s2state);
digitalWrite(s3, s3state);
return pulseIn(out, LOW);
}
Note que esta função não retorna a frequência da onda quadrada, mas sim a duração dos pulsos (muitos tutoriais sobre o TCS230 erram aqui). Quanto mais intensa a cor, maior a frequência, mas mais curto o pulso. Portanto, há uma relação inversa e quanto mais intensa a cor, menor o valor retornado.
Se quiseres converter o valor para frequência, terias de contar o número de pulsos n num segundo e tirar o inverso:
f = 1 / n
mas vamos trabalhar diretamente com as durações dos pulsos.
Função Setup
Na função setup(), inicializamos a comunicação serial a 9600 baud e chamamos a função initPins() para configurar os pinos. Também definimos a escala para 20% chamando setScaling(HIGH, LOW), o que nos dá uma boa resolução.
void setup() {
Serial.begin(9600);
initPins();
setScaling(HIGH, LOW); // 20 %
}
Se comparares os fatores de escala 2%, 20% e 100%, vais ver que as leituras dos canais de cor são maiores para a escala de 2% e menores para a de 100%. Abaixo alguns exemplos de leituras para as diferentes escalas quando o sensor foi exposto a um objeto verde e um branco com a mesma iluminação e distância:
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
O fator de escala de 20% é um bom compromisso entre resolução e robustez. Além disso, as leituras para 20% cabem facilmente num inteiro, enquanto para 2% podes ter overflow quando o sensor está em completa escuridão. Por isso escolhi a escala de 20%.
Função Loop
Na função loop(), lemos as durações dos pulsos para as cores vermelho, azul, verde e branco chamando readPulse() com os parâmetros adequados. Os resultados são guardados nas variáveis red, green, blue e 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 mencionado antes, os valores das cores são inversos à intensidade das cores detetadas. Imprimimos os valores das cores no monitor serial usando a função aprintf(), que formata a saída para melhor leitura. Finalmente, adicionamos um atraso de 1000 milissegundos (1 segundo) antes de repetir o loop.
aprintf("R:%3d G:%3d B:%3d W:%3d\n",
red, green, blue, white);
delay(1000);
Exemplo de Saída
Se carregares o código no Arduino e abrires o Monitor Serial, deverás ver leituras dos canais de cor semelhantes às abaixo:
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
Se colocares um objeto vermelho à frente do sensor, o valor vermelho deve diminuir; de forma semelhante, se o sensor vir um objeto azul ou verde, o valor do canal correspondente deve diminuir.
Há um problema, no entanto: os valores dependem muito da luminosidade ambiente, o que dificulta a deteção fiável das cores. Na próxima secção, vamos normalizar as leituras.
Normalização da luminosidade para o TCS230
A gama total das leituras de cor depende da escala escolhida. Com 20% podes obter valores entre 0 e cerca de 15000. Podes testar facilmente cobrindo o sensor com o dedo (escuridão total) ou iluminando-o com uma luz forte. Aqui está um exemplo das leituras que obtive:
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 forte dependência da luminosidade ambiente torna quase impossível definir limiares robustos para detetar uma cor. Por exemplo, o código seguinte não funciona muito bem:
if (red > 10 && red < 100) {
Serial.println("red detected");
}
Por isso, precisamos normalizar as leituras de cor. Isto pode ser feito facilmente dividindo as leituras das cores pelo valor do canal branco (luminosidade). Aqui está a função correspondente:
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 a leitura da cor (vermelho, verde, azul) por 100 e depois divide pelo valor branco mais 1. Adicionamos o mais um para evitar um possível erro de divisão por zero, e multiplicamos por 100 para manter os valores das cores numa gama semelhante (mesma resolução).
Se comparares as leituras de cor com e sem normalização, vais ver que as normalizadas são mais estáveis. Por exemplo, coloquei o TCS230 a várias distâncias (1cm … 4cm) de um objeto azul.

Sem normalização, obtive valores azuis entre 43 e 169. Isso é uma diferença ou variação de 126 unidades. Com normalização, por outro lado, medi valores azuis entre 188 e 237, uma diferença de apenas 49 unidades. Portanto, as leituras de cor são mais estáveis (menos sensíveis à luz ambiente) com normalização.
Na função loop podes facilmente adicionar a função de normalização assim:
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);
}
No entanto, a normalização não é perfeita e a luz ambiente ainda afeta as leituras do sensor. Além disso, se quiseres usar limiares para detetar cores além de vermelho, verde ou azul, o código torna-se complexo.
Por exemplo, roxo é uma combinação dos valores vermelho, verde e azul. Para detetá-lo, terias de implementar algo como o seguinte:
if(red > 250 && red < 300 && green > 330 && green < 400 && blue > 190 && blue < 230) {
Serial.println("purple detected");
}
Este código é complexo, os limiares são difíceis de escolher e não funciona muito bem. Em vez disso, treinamos um classificador simples para aprender quais leituras dos canais de cor correspondem a quais cores. Será muito mais robusto, fácil de implementar e também mais divertido ; )
Classificando leituras de cor do TCS230
Vamos usar um nearest-neighbor-classifier para classificar as leituras de cor do TCS230. Para entender melhor como este classificador funciona, recolhi alguns dados. Coloquei o TCS230 em frente a quadrados vermelho, verde, roxo e amarelo e imprimi as leituras dos canais de cor. Aqui está a grelha de cores que usei:

Plotando leituras dos canais de cor
Repiti este processo várias vezes e registei as leituras dos canais vermelho e verde. (Deixei de fora as leituras azuis por agora, mas vamos usá-las depois). Assim eram as amostras de dados:
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 ...
De seguida, plotei cada amostra num gráfico de dispersão 2D. Cada ponto no gráfico abaixo representa uma amostra (uma leitura dos valores dos canais vermelho e verde), e a cor de cada ponto indica qual quadrado colorido estava em frente ao sensor.

Como podes ver, as amostras para objetos vermelho, roxo, amarelo e verde agrupam-se muito bem, com exceção de um possível outlier para um objeto roxo no canto superior direito.
Classificando a cor de um objeto
Com estes dados, é agora fácil determinar a cor de um objeto. Simplesmente tiramos uma leitura dos canais vermelho e verde e encontramos a amostra mais próxima da leitura. No gráfico abaixo, o cruzamento marca o ponto para uma nova leitura e podes ver que a amostra mais próxima é roxa. Portanto, classificaríamos o nosso objeto como tendo cor roxa.

Este método para classificar objetos baseado na amostra mais próxima chama-se Nearest-Neighbor Classifier ou Pesquisa do Vizinho Mais Próximo. Existem muitas variações deste algoritmo.
O mais importante é que este algoritmo funciona também com dados de maior dimensão. O que significa que podemos usar as leituras dos três canais de cor para determinar uma cor. O gráfico seguinte mostra algumas amostras para cinco cores (vermelho, roxo, amarelo, verde, azul) num espaço 3D. Cada amostra aqui tem três dimensões, as leituras dos canais vermelho, verde e azul, e é um ponto neste gráfico de dispersão 3D:

Como antes, a cor do ponto indica a que cor a amostra pertence. Determinar a cor de um novo objeto neste espaço 3D de maior dimensão funciona da mesma forma que antes. Basta encontrar a amostra mais próxima das tuas leituras dos canais. É só mais difícil de visualizar, por isso comecei com um espaço 2D.
Na próxima secção vamos implementar um classificador simples de Vizinho Mais Próximo que funciona neste espaço 3D.
Código para Classificador de Vizinho Mais Próximo com TCS230
O código nesta secção é uma extensão do código acima. Adiciona alguns dados de amostra e o classificador de Vizinho Mais Próximo. Imprime as leituras dos canais de cor (vermelho, verde, azul) e a cor detetada. A saída será assim:
{292, 376, 221} => purple
{393, 293, 193} => blue
{274, 261, 306} => green
{206, 253, 486} => yellow
{165, 428, 375} => red
{166, 246, 466} => ???
Note que pode detetar cores como “roxo” ou “amarelo”, que são combinações dos valores vermelho, verde e azul. Além disso, se não conseguir detetar uma cor com confiança, imprime “???”.
Dá uma vista rápida ao código primeiro e depois discutimos como 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 Amostras, Cores e Rejeição
No início do código adicionamos três constantes. A constante reject determina quando o classificador decide que não consegue detetar uma cor. Falaremos mais sobre isso depois.
De seguida temos a lista de colors que vamos detetar. A primeira “cor” é “???”, que é retornada quando o classificador não conseguiu detetar uma cor. Podes alterar e expandir esta lista para detetar quantas cores quiseres.
Finalmente, temos uma lista de amostras n_samples, onde cada amostra contém leituras dos canais vermelho, verde e azul mais um índice de cor. O índice de cor é simplesmente a posição na 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 exemplo, a última amostra { 193, 243, 450, 5 }, significa que temos as leituras para os três canais de cor red==193, green==243, blue==450 e que esta amostra é para a cor colors[5] == "yellow".
Mostro-te mais tarde como recolher estas amostras.
Função de Distância
Um classificador de vizinho mais próximo funciona encontrando a amostra mais próxima de uma dada leitura dos canais de cor. Por isso precisamos de uma função para calcular a distância entre uma leitura dos canais de cor e uma amostra. Uma função de distância comum para este propósito é a Euclidean distance.
A função eucDist calcula a Euclidean distance entre as leituras dos canais de cor r, g, b e uma amostra s. Quanto mais próximas as leituras da amostra, menor será a 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]));
}
A função quadrado sqr é uma função auxiliar que calcula o quadrado do seu argumento a.
Função de Classificação
A função classify percorre todas as samples, encontra a amostra com a menor distância Euclidiana mindist às leituras dos canais de cor r, g, b e retorna a cor detetada color como uma string.
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;
}
Se a menor distância mindist não for menor que o valor limite reject, retorna-se a primeira cor no array colors. Esta é a string “???“, que indica que as leituras dos canais de cor não estavam suficientemente próximas de uma das amostras para detetar a cor de forma fiável.
Podes definir o limite de rejeição muito alto (1e8) e o classificador responderá sempre com uma cor, mas geralmente é melhor deixar o classificador indicar quando não consegue classificar a cor de um objeto com confiança.
Exemplo de Saída
Se compilares e carregares o código, deverás ver as leituras dos canais de cor e a cor detetada impressas assim:
{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} => ???
Na próxima secção mostro-te como adaptar o código para escolher as cores que queres detetar.
Recolha de Dados de Treino para o Detector de Cor
O código acima demonstra como implementar um Detector de Cor usando um Classificador de Vizinho Mais Próximo. Funciona para o meu sensor, com as minhas condições de iluminação ambiente e pode detetar as cores “vermelho”, “verde”, “azul”, “roxo”, “amarelo”. A sua precisão e o número de cores está diretamente relacionado com as amostras de dados que recolhi.
Normalmente, precisas de recolher as tuas próprias amostras para construir um Detector de Cor que funcione no teu ambiente e para as cores que te interessam. Felizmente, isto é fácil. Podes usar o código existente. Basta comentar a linha onde o classificador é chamado e alterar a impressão como mostrado abaixo:
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);
}
Agora, começa a recolher amostras escolhendo a tua primeira cor (index=1) e coloca um objeto dessa cor em frente ao sensor. Varia a distância e a luz ambiente o máximo possível. Depois de alguns segundos terás amostras suficientes impressas para a tua primeira cor, por exemplo, “verde”
// green
{296,225,303,1},
{279,232,311,1},
...
{293,230,309,1},
{313,231,320,1},
Descarta as amostras “más”, por exemplo, quando o objeto foi removido do detector, e guarda as amostras “boas” num editor de texto. Depois incrementa a variável index para 2 e repete o processo para a tua próxima cor, por exemplo, “roxo”
// purple
{271,361,215,2},
{274,346,217,2},
...
{284,365,218,2},
{270,356,221,2},
Continua assim até teres dados de amostra para todas as cores que queres detetar. Depois junta os dados de amostra num grande array:
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 a constante n_samples para que corresponda ao número de amostras na lista e terminaste a recolha de dados e o treino do teu classificador.
Volta a alterar o código para chamar o classificador e imprimir a sua resposta para testar o teu classificador recém-treinado:
void loop() {
...
char *color = classify(red, green, blue);
aprintf("{%3d, %3d, %3d} => %s\n",
red, green, blue, color);
...
}
Se encontrares casos em que o detector falha em detetar uma cor corretamente, adiciona esses casos como amostras ao conjunto de dados. Depois de algum tempo, o conjunto de dados pode ficar bastante grande. Podes reduzi-lo removendo duplicados ou quase duplicados do conjunto de dados, por exemplo:
{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},
Podes fazer isto manualmente ou escrever algum código para remover os duplicados programaticamente.
Na próxima e última secção vamos adicionar um OLED para mostrar a cor detetada num pequeno ecrã, em vez de imprimir no Monitor Serial.
Adicionar um OLED para mostrar as cores detetadas
Adicionar o OLED é simples. Basta ligar o SDA e SCL do OLED ao A4 e A5 do Arduino (SDA->A4, SCL->A5). Como o OLED pode funcionar a 5V, podemos partilhar as linhas de alimentação. Liga o VCC a 5V e o GND ao GND. A imagem abaixo mostra a ligação completa:

Código para mostrar as cores detetadas
Abaixo está o código que mostra a cor detetada pelo TCS230 e pelo nosso classificador de vizinho mais próximo no 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);
}
As principais alterações são a include da Adafruit_SSD1306 Library necessária para comunicar com o OLED, a criação do objeto OLED, uma função initOLED para inicializar o OLED, e uma função display para imprimir a cor detetada no 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();
}
...
Se ainda não instalaste a Adafruit_SSD1306 Library, terás de o fazer. Basta instalar via Library Manager como de costume:

Note que o endereço I2C para o ecrã OLED está definido para 0x3C em oled.begin(). A maioria destes pequenos OLEDs usa este endereço (or 0x27), mas o teu pode ser diferente. Se não vês nada no OLED, provavelmente tem um endereço I2C diferente e tens de alterar o endereço.
Se não sabes o endereço I2C, dá uma vista de olhos no tutorial How to Interface the SSD1306 I2C OLED Graphic Display With Arduino para o encontrar. Também o tutorial Use SSD1306 I2C OLED Display With Arduino te dirá mais sobre como usar um OLED.
Função Loop
A única alteração na função loop é que substituímos a impressão no Monitor Serial por uma chamada à função display com a cor detetada:
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);
}
E é tudo! Agora deverás ter um detector de cores que podes ajustar (treinar) para detetar qualquer cor que queiras sob condições de iluminação variadas. O pequeno vídeo abaixo mostra o meu detector de cores em ação:

Podes ver que deteta bem as três cores roxo, amarelo e azul e imprime “???”, quando me movo entre cores ou quando o detector está muito longe.
Conclusões
Neste tutorial aprendeste a usar o Sensor de Cor TCS230 com um Arduino para construir um detector de cores treinável usando um classificador de vizinho mais próximo. Este detector de cores funciona bastante bem, mas há muitas melhorias possíveis.
Se quiseres detetar muitas cores e as leituras do sensor forem mais ruidosas, um k-nearest neighbor classifier tornará a deteção mais robusta.
Em vez de normalizar as leituras dos canais de cor pelo canal branco, poderias recolher amostras em 4 dimensões (r, g, b, w) e criar um classificador de vizinho mais próximo em 4 dimensões. Tal classificador será mais robusto a mudanças na luz ambiente, mas também precisará de mais amostras de treino.
No entanto, podes comprimir a lista de amostras, reduzindo assim os requisitos de memória e aumentando a velocidade de classificação, armazenando apenas os centros dos clusters de cor. Alternativamente, podes remover amostras do centro e manter apenas amostras próximas da fronteira da classe.
Finalmente, “treinámos” o classificador manualmente, recolhendo amostras e codificando-as. Mas também poderias adicionar um botão de “treino” ao circuito e código que armazena uma amostra na EEPROM quando pressionado. Isso permitiria treinar dinamicamente o teu detector de cores.
Se tiveres alguma dúvida, não hesites em deixar nos comentários.
Boas construções ; )

