In diesem Tutorial lernst du, wie du den TCS230 Farbsensor mit einem Arduino verwendest, um einen trainierbaren Farberkenner basierend auf einem Nearest-Neighbor-Klassifikator zu bauen.
Der TCS230 Farbsensor gibt nicht direkt die erkannte Farbe aus, sondern liefert Sensormesswerte für die Rot-, Grün-, Blau- (und Klar-) Kanäle. Für die meisten praktischen Anwendungen musst du diese Messwerte irgendwie in eine erkannte Farbe umwandeln, z.B. „lila“. Es kann ziemlich knifflig sein, eine Farbe zuverlässig mit Schwellenwerten auf den Kanalwerten zu erkennen. Deshalb verwenden wir etwas maschinelles Lernen und setzen stattdessen einen trainierbaren Nearest-Neighbor-Klassifikator ein.
Benötigte Teile
Du benötigst einen TCS230 Farbsensor und einen Mikrocontroller. Ich habe für dieses Projekt einen Arduino Uno verwendet, aber jeder andere Arduino oder ESP32 funktioniert ebenfalls. Außerdem verwenden wir ein OLED-Display, um die erkannte Farbe und weitere Informationen anzuzeigen.

TFmini Plus Distanzsensor

Arduino Uno

USB-Kabel für Arduino UNO

Dupont-Kabelset

Breadboard

OLED-Display
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.
Grundlagen des TCS230 Farbsensors
Der TCS230 ist ein Farb-Licht-zu-Frequenz-Wandler. Er besteht aus einem 8 x 8 Array von Fotodioden, die Licht detektieren. Es gibt 16 Fotodioden mit einem Rotfilter, die auf rotes Licht empfindlich sind, 16 mit Grünfilter für grünes Licht, 16 mit Blaufilter und 16 ohne Filter für „weißes“ Licht.
Wenn du dir das Bild des TCS230 Chips unten genau ansiehst, kannst du tatsächlich das 8 x 8 Array der Fotodioden mit ihren farbigen (und klaren) Filtern erkennen.

Funktion des TCS230 Farbsensors
Neben dem Array von Fotodioden enthält der TCS230 einen Strom-zu-Frequenz-Wandler, der den Strom, der durch eine Gruppe von Fotodioden fließt, in ein Rechtecksignal umwandelt. Eine Gruppe besteht aus 16 Fotodioden für eine bestimmte Farbe (rot, grün, blau) oder klar, und die Frequenz des Rechtecksignals ist proportional zum Strom durch diese Gruppe von Fotodioden.

Um die Intensität einer der drei Farben (oder klar) zu messen, wählst du die entsprechende Gruppe von Fotodioden (rot, grün, blau, weiß) durch Setzen von zwei Eingängen (S2, S3) aus und liest dann die Frequenz am Ausgang ab. Der Frequenzbereich (Skalierung) kann über zwei weitere Eingänge (S0, S1) gesteuert werden.
Pinbelegung des TCS230 Farbsensors
Das folgende Bild zeigt die Pinbelegung des TCS230 Chips. Du kannst die Pins S0 und S1 sehen, um den Ausgangsfrequenzbereich auszuwählen, sowie die S2 und S3 Pins zur Auswahl der Fotodiodengruppe (Farbe).

Der Ausgang OUT kann über den OE Pin ein- oder ausgeschaltet werden.VDD und GND sind für die Stromversorgung mit einer Betriebsspannung von 2,7 V bis 5,5 V zuständig. Die folgende Tabelle fasst die Pinbelegung zusammen:
| Pin | Name | Beschreibung |
| 1,2 | S0, S1 | Ausgangsfrequenz-Skalierung |
| 3 | OE | Enable für Ausgangsfrequenz |
| 4 | GND | Masse |
| 5 | VDD | Versorgungsspannung |
| 6 | OUT | Ausgangsfrequenz |
| 7,8 | S2, S3 | Auswahl des Fotodiodentyps |
Fotodioden-/Farbauswahl
Wie oben erwähnt, musst du vor der Messung der Intensität eines bestimmten Farbkanals diesen zuerst über die Pins S2 und S3 auswählen. Die folgende Tabelle zeigt, welche Einstellung von S2 und S3 welchem Farbkanal entspricht:
| Farbkanal | S2 | S3 |
| Rot | LOW | LOW |
| Blau | LOW | HIGH |
| Klar | HIGH | LOW |
| Grün | HIGH | HIGH |
Frequenzskalierung
Der Frequenzbereich des Ausgangs kann durch Setzen der folgenden Werte für die Steuerpins S0 und S1 variiert werden:
| Ausgangsskalierung | S0 | S1 | min f | max f |
| Power down | LOW | LOW | – | – |
| 2% | LOW | HIGH | 10kHz | 12kHz |
| 20% | HIGH | LOW | 100kHz | 120kHz |
| 100% | HIGH | HIGH | 500kHz | 600kHz |
TCS230 Farbsensormodul
Normalerweise verwendet man den TCS230 nicht direkt, sondern ein TCS230 Farbsensormodul, das einige zusätzliche Bauteile enthält und einfacher anzuschließen ist. Das Bild unten zeigt Vorder- und Rückseite eines typischen TCS230 Farbsensormoduls:

Auf der Vorderseite des Moduls findest du vier weiße LEDs zur Beleuchtung und ein schwarzes Schutzgehäuse, das den TCS230 Chip enthält. Wenn du genau hinsiehst, erkennst du den TCS230 Chip in der Mitte:

Schaltplan des TCS230 Farbsensormoduls
Das TCS230 Modul hat im Wesentlichen die gleiche Pinbelegung wie der TCS230 Chip selbst, abgesehen von zwei Änderungen. Es gibt keinen OE Pin, dafür aber einen zusätzlichen LED Pin.
Der Schaltplan des Moduls zeigt, dass OE mit VSS verbunden ist, was bedeutet, dass der Ausgang immer aktiviert ist. Der LED Pin ist mit dem Gate eines MOSFETs (Q1) verbunden, der die vier Beleuchtungs-LEDs (D1…D4) schaltet. Da der LED Pin einen internen Pullup hat, sind die LEDs standardmäßig aktiv und du kannst sie ausschalten, indem du den LED Pin mit Masse verbindest.

Beachte, dass die Pins S0 und S1 ebenfalls interne Pullups haben und die Standard-Ausgangsskalierung für die Frequenz daher bei 100% liegt. Andererseits haben die Pins S2 und S3 keine Pullups und müssen gesetzt werden, um einen Farbkanal auszuwählen.
Im nächsten Abschnitt verbinden wir das TCS230 Modul mit einem Arduino und schreiben etwas Code, um den Sensor zu testen.
Anschluss des TCS230 an Arduino
Das folgende Diagramm zeigt, wie du das TCS230 Modul mit einem Arduino verbindest. Ich verwende 5V als Stromversorgung, aber 3,3V würde auch funktionieren. Die Steuerpins S0, S1, S2 und S3 des TCS230 Moduls sind mit den Pins 8, 9, 10, 11 des Arduino verbunden.

Die folgende Tabelle zeigt dir nochmal die Verbindungen, die du herstellen musst
| TCS230 | Arduino |
| VCC | 5V |
| GND | GND |
| S0 | 8 |
| S1 | 9 |
| S2 | 10 |
| S3 | 11 |
| OUT | 12 |
Beachte, dass es zwei VCC- und zwei GND-Pins am TCS230 gibt. Du kannst jeden davon verwenden. Der LED-Pin ist nicht verbunden, aber du könntest ihn an einen digitalen Ausgang anschließen, um die Beleuchtungs-LEDs ein- oder auszuschalten.
Code zum Auslesen von Farben mit TCS230
Im folgenden Code verwenden wir einen TCS230 Farbsensor, um die Werte der verschiedenen Farbkanäle (rot, grün, blau und weiß) auszulesen und die Ergebnisse im seriellen Monitor auszugeben.
#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);
}
Lass uns den Code in seine Bestandteile zerlegen, um ihn besser zu verstehen.
Includes
Der Code verwendet die aprintf library, die das Formatieren von Text zum Drucken vereinfacht. Es ist im Grunde eine Version der printf() Funktion. Für mehr Details lies das How To Print To Serial Monitor On Arduino Tutorial.
Um sie zu installieren, gehe zum github repo klicke auf den grünen „Code“-Button und wähle „Download ZIP“:

Danach gehe zu „Sketch“ -> „Include Library“ -> „Add .ZIP Library“, um sie zu installieren.

Um die aprintf library zu verwenden, brauchen wir folgendes Include:
#include "aprintf.h"
Konstanten
Als nächstes definieren wir die Pins, die zum Anschluss des TCS230 Sensors an den Arduino verwendet werden.
const int s0 = 8; const int s1 = 9; const int s2 = 10; const int s3 = 11; const int out = 12;
Hier sind s0 und s1 für die Einstellung der Frequenzskalierung, während s2 und s3 für die Auswahl des Farbkanals zuständig sind. Der out Pin ist der Ausgang, an dem der Sensor das Frequenzsignal entsprechend der erkannten Farbintensität ausgibt.
Pin-Initialisierung
Die initPins() Funktion setzt die Pin-Modi für jeden der definierten Pins. Die Sensorpins s0, s1, s2 und s3 werden als OUTPUT gesetzt, während der out Pin als INPUT konfiguriert wird.
void initPins() {
pinMode(s0, OUTPUT);
pinMode(s1, OUTPUT);
pinMode(s2, OUTPUT);
pinMode(s3, OUTPUT);
pinMode(out, INPUT);
}
Frequenzskalierung
Die setScaling() Funktion wird verwendet, um die Frequenzskalierung des Ausgangssignals vom Sensor zu konfigurieren. Durch Setzen der Zustände von s0 und s1 können wir die Auflösung des Sensorsignals anpassen. Mehr dazu später.
void setScaling(int s0state, int s1state) {
digitalWrite(s0, s0state);
digitalWrite(s1, s1state);
}
Pulsdauer auslesen
Die readPulse() Funktion liest die Dauer in Mikrosekunden des vom Sensor ausgegebenen Pulses aus. Sie nimmt die Zustände von s2 und s3 als Parameter, um den auszulesenden Farbkanal auszuwählen. Die Funktion wartet 20 Millisekunden, um den Sensor zwischen den Messungen zu stabilisieren.
uint16_t readPulse(int s2state, int s3state) {
delay(20);
digitalWrite(s2, s2state);
digitalWrite(s3, s3state);
return pulseIn(out, LOW);
}
Beachte, dass diese Funktion nicht die Frequenz des Rechtecksignals zurückgibt, sondern die Pulsdauer (viele Tutorials zum TCS230 machen hier einen Fehler). Je intensiver die Farbe, desto höher die Frequenz, aber desto kürzer der Puls. Es besteht also eine inverse Beziehung, und je intensiver die Farbe, desto kleiner ist der zurückgegebene Wert.
Wenn du den Wert in eine Frequenz umrechnen möchtest, müsstest du die Anzahl der Pulse n innerhalb einer Sekunde zählen und den Kehrwert nehmen:
f = 1 / n
aber wir arbeiten direkt mit den Pulsdauern.
Setup-Funktion
In der setup() Funktion initialisieren wir die serielle Kommunikation mit 9600 Baud und rufen die initPins() Funktion auf, um die Pins einzurichten. Außerdem setzen wir die Skalierung auf 20% durch Aufruf von setScaling(HIGH, LOW), was uns eine gute Auflösung gibt.
void setup() {
Serial.begin(9600);
initPins();
setScaling(HIGH, LOW); // 20 %
}
Wenn du die Skalierungsfaktoren 2%, 20% und 100% vergleichst, wirst du feststellen, dass die Farbkanalwerte bei 2% Skalierung am größten und bei 100% am kleinsten sind. Unten einige Beispielwerte für die verschiedenen Skalierungen, wenn der Sensor ein grünes und ein weißes Objekt bei gleicher Beleuchtung und Entfernung sah:
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
Der 20%-Skalierungsfaktor ist ein guter Kompromiss zwischen Auflösung und Robustheit. Außerdem passen die Farbwerte bei 20% Skalierung gut in einen Integer, während bei 2% Skalierung Überläufe auftreten können, wenn der Sensor komplett dunkel ist. Deshalb habe ich den 20%-Skalierungsfaktor gewählt.
Loop-Funktion
In der loop() Funktion lesen wir die Pulsdauern für Rot, Blau, Grün und Weiß aus, indem wir readPulse() mit den passenden Parametern aufrufen. Die Ergebnisse werden in den Variablen red, green, blue und white gespeichert.
uint16_t red = readPulse(LOW, LOW); uint16_t blue = readPulse(LOW, HIGH); uint16_t green = readPulse(HIGH, HIGH); uint16_t white = readPulse(HIGH, LOW);
Wie bereits erwähnt, sind die Farbwerte invers zur Intensität der erkannten Farben. Wir geben die Farbwerte mit der aprintf() Funktion aus, die die Ausgabe besser lesbar formatiert. Schließlich fügen wir eine Verzögerung von 1000 Millisekunden (1 Sekunde) ein, bevor die Schleife wiederholt wird.
aprintf("R:%3d G:%3d B:%3d W:%3d\n",
red, green, blue, white);
delay(1000);
Beispielausgabe
Wenn du den Code auf deinen Arduino lädst und den Seriellen Monitor öffnest, solltest du Farbkanalwerte ähnlich den folgenden sehen:
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
Wenn du ein rotes Objekt vor den Sensor hältst, sollte der Rotwert kleiner werden, ebenso wenn der Sensor ein blaues oder grünes Objekt sieht, sollte der Wert für den entsprechenden Kanal kleiner werden.
Es gibt jedoch ein Problem: Die Werte hängen stark von der Umgebungshelligkeit ab, was die zuverlässige Farberkennung erschwert. Im nächsten Abschnitt normalisieren wir daher die Messwerte.
Helligkeitsnormalisierung für TCS230
Der Gesamtbereich der Farbwerte hängt von der gewählten Skalierung ab. Bei 20% erhält man Werte zwischen 0 und etwa 15000. Du kannst das leicht testen, indem du den Sensor mit deinem Finger abdeckst (komplette Dunkelheit) oder ein helles Licht darauf scheinen lässt. Hier ein Beispiel der Messwerte, die ich erhalten habe:
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
Diese starke Abhängigkeit von der Umgebungshelligkeit macht es nahezu unmöglich, robuste Schwellenwerte zur Farberkennung zu definieren. Zum Beispiel funktioniert Code wie der folgende nicht sehr gut:
if (red > 10 && red < 100) {
Serial.println("red detected");
}
Wir müssen daher die Farbwerte normalisieren. Das erreicht man einfach, indem man die Farbwerte durch den Weißkanalwert (Helligkeit) teilt. Hier ist die entsprechende Funktion:
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);
}
Sie multipliziert den Farbwert (rot, grün, blau) mit 100 und teilt dann durch den Weißwert plus 1. Das Plus Eins verhindert eine mögliche Division durch Null, und die Multiplikation mit 100 hält die Farbwerte im ähnlichen Bereich (gleiche Auflösung).
Wenn du die Farbwerte mit und ohne Normalisierung vergleichst, wirst du feststellen, dass die normalisierten stabiler sind. Zum Beispiel habe ich den TCS230 in verschiedenen Abständen (1cm … 4cm) zu einem blauen Objekt platziert.

Ohne Normalisierung erhielt ich Blauwerte zwischen 43 und 169, also eine Differenz von 126 Einheiten. Mit Normalisierung maß ich Blauwerte zwischen 188 und 237, also nur eine Differenz von 49 Einheiten. Die Farbwerte sind also stabiler (weniger empfindlich gegenüber Umgebungslicht) mit Normalisierung.
In der Loop-Funktion kannst du die Normalisierungsfunktion einfach so hinzufügen:
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);
}
Die Normalisierung ist jedoch nicht perfekt und Umgebungslicht beeinflusst die Sensorwerte weiterhin. Außerdem wird der Code komplex, wenn du Schwellenwerte für Farben außer Rot, Grün oder Blau verwenden möchtest.
Zum Beispiel ist Lila eine Kombination aus Rot-, Grün- und Blauwerten. Um es zu erkennen, müsstest du so etwas wie Folgendes implementieren:
if(red > 250 && red < 300 && green > 330 && green < 400 && blue > 190 && blue < 230) {
Serial.println("purple detected");
}
Dieser Code ist komplex, die Schwellenwerte schwer zu wählen und funktioniert nicht sehr gut. Stattdessen trainieren wir einen einfachen Klassifikator, der lernt, welche Farbkanalwerte zu welchen Farben gehören. Das ist viel robuster, einfacher zu implementieren und macht auch mehr Spaß ; )
Klassifizierung der TCS230 Farbwerte
Wir verwenden einen nearest-neighbor-classifier, um die Farbwerte des TCS230 zu klassifizieren. Für ein besseres Verständnis, wie dieser Klassifikator funktioniert, habe ich einige Daten gesammelt. Ich habe den TCS230 vor rote, grüne, lila und gelbe Quadrate gestellt und die Farbkanalwerte ausgedruckt. Hier ist das Farbraster, das ich verwendet habe:

Darstellung der Farbkanalwerte
Ich habe diesen Vorgang mehrfach wiederholt und die Rot- und Grünwerte aufgezeichnet. (Die Blauwerte lasse ich vorerst weg, wir verwenden sie später). So sahen die Datenproben aus:
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 ...
Anschließend habe ich jede Probe in einem 2D-Streudiagramm geplottet. Jeder Punkt im Diagramm unten repräsentiert eine Probe (eine Messung von Rot- und Grünwert), und die Farbe des Punktes zeigt an, welches Farbfeld vor dem Sensor lag.

Wie du sehen kannst, gruppieren sich die Proben für rote, lila, gelbe und grüne Objekte sehr schön zusammen, mit Ausnahme eines möglichen Ausreißers für ein lila Objekt oben rechts.
Farbklassifizierung eines Objekts
Mit diesen Daten ist es nun einfach, die Farbe eines Objekts zu bestimmen. Wir nehmen einfach eine Messung der Rot- und Grünkanäle und finden die Probe, die der Messung am nächsten ist. Im Diagramm unten markiert das Kreuz den Punkt einer neuen Messung, und du siehst, dass die nächste Probe lila ist. Wir würden also das Objekt als lila klassifizieren.

Diese Methode, Objekte basierend auf der nächsten Probe zu klassifizieren, nennt man Nearest-Neighbor Classifier oder Nearest-Neighbor-Suche. Es gibt viele Varianten dieses Algorithmus.
Wichtig ist, dass dieser Algorithmus auch mit höherdimensionalen Daten funktioniert. Das heißt, wir können alle drei Farbkanalwerte verwenden, um eine Farbe zu bestimmen. Das folgende Diagramm zeigt einige Proben für fünf Farben (rot, lila, gelb, grün, blau) im 3D-Raum. Jede Probe hat hier drei Dimensionen, die Farbkanalwerte für rot, grün und blau, und ist ein Punkt in diesem 3D-Streudiagramm:

Wie zuvor zeigt die Farbe des Punktes, zu welcher Farbe die Probe gehört. Die Bestimmung der Farbe eines neuen Objekts in diesem höherdimensionalen 3D-Raum funktioniert genauso wie zuvor. Einfach die Probe finden, die den Kanalwerten am nächsten ist. Es ist nur schwerer zu visualisieren, weshalb ich mit einem 2D-Raum begonnen habe.
Im nächsten Abschnitt implementieren wir einen einfachen Nearest-Neighbor-Klassifikator, der in diesem 3D-Raum arbeitet.
Code für Nearest-Neighbor-Klassifikator mit TCS230
Der Code in diesem Abschnitt ist eine Erweiterung des obigen Codes. Er fügt einige Beispieldaten und den Nearest-Neighbor-Klassifikator hinzu. Er gibt die Messwerte der Farbkanäle (rot, grün, blau) und die erkannte Farbe aus. Die Ausgabe sieht so aus:
{292, 376, 221} => purple
{393, 293, 193} => blue
{274, 261, 306} => green
{206, 253, 486} => yellow
{165, 428, 375} => red
{166, 246, 466} => ???
Beachte, dass er Farben wie „lila“ oder „gelb“ erkennen kann, die Kombinationen aus Rot-, Grün- und Blauwerten sind. Außerdem gibt er „???“ aus, wenn er keine Farbe sicher erkennen kann.
Schau dir den Code zuerst kurz an, dann besprechen wir, wie er funktioniert.
#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);
}
Konstanten für Proben, Farben und Ablehnung
Am Anfang des Codes fügen wir drei Konstanten hinzu. Die reject Konstante bestimmt, wann der Klassifikator entscheidet, dass er keine Farbe erkennen kann. Mehr dazu später.
Dann haben wir die Liste der colors, die wir erkennen wollen. Die erste „Farbe“ ist „???“, die zurückgegeben wird, wenn der Klassifikator keine Farbe erkennen konnte. Du kannst diese Liste ändern und erweitern, um beliebig viele Farben zu erkennen.
Schließlich haben wir eine Liste von Proben n_samples, wobei jede Probe Messwerte für die Rot-, Grün- und Blaukanäle plus einen Farbindex enthält. Der Farbindex ist einfach die Position in der Liste der 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 },
...
};
Zum Beispiel bedeutet die letzte Probe { 193, 243, 450, 5 }, dass wir die Messwerte für die drei Farbkanäle red==193, green==243, blue==450 haben und dass diese Probe zur Farbe colors[5] == "yellow" gehört.
Ich zeige dir später, wie du diese Proben sammelst.
Distanzfunktion
Ein Nearest-Neighbor-Klassifikator arbeitet, indem er die Probe findet, die einer gegebenen Messung der Farbkanäle am nächsten ist. Wir brauchen also eine Funktion, die die Distanz zwischen einer Messung der Farbkanäle und einer Probe berechnet. Eine gängige Distanzfunktion dafür ist die Euclidean distance.
Die Funktion eucDist berechnet die Euclidean distance zwischen den Farbkanalwerten r, g, b und einer Probe s. Je näher die Farbkanalwerte an der Probe sind, desto kleiner ist die 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]));
}
Die Quadratfunktion sqr ist eine Hilfsfunktion, die das Quadrat ihres Arguments a berechnet.
Klassifizierungsfunktion
Die classify-Funktion durchläuft alle samples, findet die Probe mit der kleinsten euklidischen Distanz mindist zu den Farbkanalwerten r, g, b und gibt die erkannte color als String zurück.
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;
}
Wenn die kleinste Distanz mindist nicht kleiner als der reject Schwellenwert ist, wird die erste Farbe im colors Array zurückgegeben. Das ist der String „???„, der anzeigt, dass die Farbkanalwerte keiner Probe nahe genug waren, um die Farbe zuverlässig zu erkennen.
Du kannst den Ablehnungsschwellenwert sehr groß machen (1e8) und der Klassifikator gibt immer eine Farbe zurück, aber generell ist es besser, wenn der Klassifikator dir sagt, wenn er eine Farbe nicht sicher klassifizieren kann.
Beispielausgabe
Wenn du den Code kompilierst und hochlädst, solltest du Farbkanalwerte und die erkannte Farbe so ausgegeben sehen:
{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} => ???
Im nächsten Abschnitt zeige ich dir, wie du den Code anpasst, um die Farben auszuwählen, die du erkennen möchtest.
Sammeln von Trainingsdaten für den Farberkenner
Der obige Code zeigt, wie man einen Farberkenner mit einem Nearest-Neighbor-Klassifikator implementiert. Er funktioniert für meinen Sensor, unter meinen Umgebungslichtbedingungen und erkennt die Farben „rot“, „grün“, „blau“, „lila“, „gelb“. Die Genauigkeit und die Anzahl der Farben hängen direkt von den gesammelten Datenproben ab.
Typischerweise musst du eigene Proben sammeln, um einen Farberkenner zu bauen, der in deiner Umgebung und für die Farben, die dich interessieren, funktioniert. Glücklicherweise ist das einfach. Du kannst den bestehenden Code verwenden. Kommentiere einfach die Zeile aus, in der der Klassifikator aufgerufen wird, und ändere die Ausgabe wie unten gezeigt:
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);
}
Starte nun mit dem Sammeln von Proben, indem du deine erste Farbe (index=1) auswählst und ein Objekt mit dieser Farbe vor den Sensor hältst. Variiere Abstand und Umgebungslicht so viel wie möglich. Nach ein paar Sekunden hast du genug Proben für deine erste Farbe, z.B. „grün“.
// green
{296,225,303,1},
{279,232,311,1},
...
{293,230,309,1},
{313,231,320,1},
Wirf die „schlechten“ Proben weg, z.B. wenn das Objekt entfernt wurde, und speichere die „guten“ Proben in einem Texteditor. Erhöhe dann die index Variable auf 2 und wiederhole den Vorgang für deine nächste Farbe, z.B. „lila“.
// purple
{271,361,215,2},
{274,346,217,2},
...
{284,365,218,2},
{270,356,221,2},
Mach so weiter, bis du Proben für alle Farben hast, die du erkennen möchtest. Dann kopiere die Proben zusammen in ein großes 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
...
};
Zum Schluss passe die n_samples Konstante an, sodass sie der Anzahl der Proben in der Liste entspricht, und du bist mit dem Sammeln der Daten und dem Training deines Klassifikators fertig.
Ändere den Code zurück, sodass der Klassifikator aufgerufen wird und seine Ausgabe gedruckt wird, um deinen neu trainierten Klassifikator zu testen:
void loop() {
...
char *color = classify(red, green, blue);
aprintf("{%3d, %3d, %3d} => %s\n",
red, green, blue, color);
...
}
Wenn du Fälle findest, in denen der Detektor eine Farbe nicht korrekt erkennt, füge diese Fälle als Proben zum Datensatz hinzu. Nach einer Weile kann der Datensatz ziemlich groß werden. Du kannst ihn reduzieren, indem du Duplikate oder fast Duplikate entfernst, z.B.
{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},
Das kannst du von Hand machen oder Code schreiben, der die Duplikate programmatisch entfernt.
Im nächsten und letzten Abschnitt fügen wir ein OLED hinzu, um die erkannte Farbe auf einem kleinen Bildschirm anzuzeigen, anstatt sie im Seriellen Monitor auszugeben.
OLED hinzufügen, um erkannte Farben anzuzeigen
Das Hinzufügen des OLED ist einfach. Verbinde einfach SDA und SCL des OLED mit A4 und A5 des Arduino (SDA->A4, SCL->A5). Da das OLED mit 5V betrieben werden kann, können wir die Stromversorgungsleitungen teilen. Verbinde VCC mit 5V und GND mit GND. Das Bild unten zeigt die komplette Verkabelung:

Code zur Anzeige erkannter Farben
Unten ist der Code, der die vom TCS230 und unserem Nearest-Neighbor-Klassifikator erkannte Farbe auf dem OLED anzeigt:
#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);
}
Die Hauptänderungen sind die include der Adafruit_SSD1306 Library, die für die Kommunikation mit dem OLED benötigt wird, die Erstellung des OLED-Objekts, eine Funktion initOLED zur Initialisierung des OLED und eine display Funktion, um die erkannte Farbe auf dem OLED auszugeben:
#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();
}
...
Wenn du die Adafruit_SSD1306 Library noch nicht installiert hast, musst du das tun. Installiere sie einfach wie gewohnt über den Library Manager:

Beachte, dass die I2C-Adresse für das OLED-Display in 0x3C auf oled.begin() gesetzt ist. Die meisten dieser kleinen OLEDs verwenden diese Adresse (or 0x27), aber deine könnte anders sein. Wenn du nichts auf dem OLED siehst, hat es höchstwahrscheinlich eine andere I2C-Adresse und du musst die Adresse ändern.
Wenn du die I2C-Adresse nicht kennst, schau dir das How to Interface the SSD1306 I2C OLED Graphic Display With Arduino Tutorial an, um sie zu finden. Auch das Use SSD1306 I2C OLED Display With Arduino Tutorial erklärt mehr zur Verwendung eines OLED.
Loop-Funktion
Die einzige Änderung in der loop Funktion ist, dass wir die Ausgabe im Seriellen Monitor durch den Aufruf der display Funktion mit der erkannten Farbe ersetzen:
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);
}
Und das war’s! Jetzt solltest du einen Farberkenner haben, den du anpassen (trainieren) kannst, um jede gewünschte Farbe unter wechselnden Lichtbedingungen zu erkennen. Der kurze Videoclip unten zeigt meinen Farberkenner in Aktion:

Du siehst, dass er die drei Farben lila, gelb und blau schön erkennt und „???“ ausgibt, wenn ich zwischen den Farben wechsle oder der Detektor zu weit entfernt ist.
Fazit
In diesem Tutorial hast du gelernt, wie du den TCS230 Farbsensor mit einem Arduino verwendest, um einen trainierbaren Farberkenner mit einem Nearest-Neighbor-Klassifikator zu bauen. Dieser Farberkenner funktioniert ziemlich gut, aber es gibt viele mögliche Verbesserungen.
Wenn du viele Farben erkennen möchtest und die Sensorwerte verrauschter sind, macht ein k-nearest neighbor classifier die Erkennung robuster.
Anstatt die Farbkanalwerte durch den Weißkanal zu normalisieren, könntest du 4-dimensionale Proben (r, g, b, w) sammeln und einen 4-dimensionalen Nearest-Neighbor-Klassifikator erstellen. So ein Klassifikator ist robuster gegenüber Änderungen im Umgebungslicht, benötigt aber auch mehr Trainingsdaten.
Du kannst die Liste der Proben komprimieren und damit Speicherbedarf reduzieren und Klassifikationsgeschwindigkeit erhöhen, indem du nur die Zentren der Farbcluster speicherst. Alternativ kannst du Proben aus der Mitte entfernen und nur Proben nahe der Klassen-Grenze behalten.
Schließlich haben wir den Klassifikator manuell trainiert, indem wir Proben gesammelt und fest im Code hinterlegt haben. Du könntest aber auch einen „Trainings“-Knopf zum Schaltkreis und Code hinzufügen, der beim Drücken eine Probe im EEPROM speichert. So könntest du deinen Farberkenner dynamisch trainieren.
Wenn du Fragen hast, kannst du sie gerne im Kommentarbereich stellen.
Viel Spaß beim Tüfteln ; )

