A few years ago I wanted to build a home made color sensor for a project. I never got it working exactly right and ended up dropping the idea until last December. I had an idea for a different project that would require a color sensor and after analyzing my last attempt, I thought I might have known what was wrong with my last prototype. So I started planning.
The basic idea to this color sensor is that you can represent almost any color if you know how much red, green, and blue is in the color. The way this sensor will work, is I will have three LEDs. A red, a green, and a blue LED. There will also be a light sensor. The entire thing will need to be surrounded by some kind of dark shield to prevent external light from getting in. The sensor will be pressed against a colored object and first, the red light will turn on. The sensor will take a reading. Then the green light, and then the blue light. This way I can get readings of how much red, green, and blue light is reflected back to the sensor. With this information I should be able to roughly determine the color of the object.
When I last tried to build this sensor I tried using a photoresistor but it didn’t seem to work reliably. I also tried using a light to frequency counter but that didn’t seem to work well either. After some research I found this digital luminosity sensor on Adafruit’s website. This sensor can give readings of all light, or only infrared light. With those two readings you can determine only visible light. That is what I wanted. I thought maybe the other sensors were also reading infrared light and skewing my color values the last time I tried this, so this sensor might give better results. I also decided to buy the super bright red, green, and blue LED’s from Adafruit as well since they have similar brightness ratings. The green and red are both rated at 8000 mcd typical brightness and the blue is rated at 6000 mcd.
Once I knew what LEDs I was going to use I could figure out what size resistors I would need. I found an online resistor calculator that made it simple to figure out what size resistors I would need to power the LEDs at close to their maximum brightness.
Once all the components arrived, I soldered them all to a small round prototyping board I bought at RadioShack.
I put some heat shrink tubing on each of the LEDs. The idea is that the tubing will prevent any of the LED light from reaching the sensor directly form the LED. It forces the light to bounce off of the colored object first.
Each LED has its corresponding resistor attached to its anode. The other side of each resistor will have a wire attached that will eventually plug into digital IO pins on the Arduino. The cathode side of the LEDs all get tied to each other and to the ground pin of the sensor module. The sensor module is just soldered into the board. It will have a wire attached to each pin.
Next I decided to figure out what I would use for an enclosure for this board. I thought back to the last time I tried this and I had the old board taped inside of a black ABS fitting. I think that maybe the tape was too loose and the sensor could have wiggled around inside of the housing. If that was the case, it would explain why it would fall out of calibration sometimes when I moved it. I knew that I had to avoid that problem this time. Now that I have my own 3D printer, I decided to design a custom enclosure for this board. The enclosure will fit the board nice and snug, and also have screw holes so I can mount the board inside securely. It should also prevent outside light from reaching the sensor, and have a hole in the back so the sensor wires can leave the enclosure and attach to the Arduino.
That’s what I came up with. There are standoffs printed on the inside to hold the board up about halfway inside of the enclosure. Two of the standoffs have screw holes so I can mount the board in place.
Next I attached the wires for the LEDs and for the sensor. I separated out the LED wires from the sensor wires so it would be easier to figure out which wire was which later one.
Next I hooked up the LED wires and the ground wire to the Arduino board so I could figure out which wire went to which LED. I also needed to test and ensure that the LEDs functioned properly. I ran some test code to cycle through flashing each LED.
All of the LEDs worked. I labeled each of the wires so they would be easy to reconnect later on. Next I had to hook up all of the sensor wires and get the sensor working. I used the sample code that came with the Adafruit Arduino library for this sensor. Once I had the wires going to the right Arduino pins, it worked fine.
The rest of this project was really just code, test, repeat. After much trial and error, I finally got this sensor working pretty well. I will paste my color sensor code below and then explain how it works afterwards.
#include <Wire.h>
#include "TSL2561.h"
#include "LPD8806.h"
#include "SPI.h"
/*
ColorSensor
This sketch determines the color of an object by flashing a red, green, and blue LED.
This sketch uses the Adafruit TSL2561 digital luminosity sensor as the light sensor
By Rick Osgood
: connect TSL2561 SCL to analog 5
: connect TSL2561 SDA to analog 4
: connect VCC to 3.3V DC (MUST NOT BE GREATER THAN 3.3V!!!)
: connect GROUND to common ground
*/
#define redLed 10
#define greenLed 11
#define blueLed 12
TSL2561 tsl(TSL2561_ADDR_FLOAT);
//for white balancing
int whiteRed = 0;
int whiteGreen = 0;
int whiteBlue = 0;
//for black balancing
int blackRed = 0;
int blackGreen = 0;
int blackBlue = 0;
void setup() {
Serial.begin(9600);
// initialize the digital pins as outputs.
pinMode(redLed, OUTPUT);
pinMode(greenLed, OUTPUT);
pinMode(blueLed, OUTPUT);
// turn off all the outputs by default
digitalWrite(redLed, LOW);
digitalWrite(greenLed, LOW);
digitalWrite(blueLed, LOW);
// Collection time. Longer the time, the more light it collects
tsl.setTiming(TSL2561_INTEGRATIONTIME_13MS);
// Set gain of sensor
tsl.setGain(TSL2561_GAIN_16X);
//setup the while and black balancing
whiteBlackBalance();
}
void loop() {
//Take a sensor reading of all three colors
int redRead = readColor(redLed);
int greenRead = readColor(greenLed);
int blueRead = readColor(blueLed);
// Convert color readings to 0-255 RGB values
int redRead2 = getRGBValue(blackRed, whiteRed, redRead);
int greenRead2 = getRGBValue(blackGreen, whiteGreen, greenRead);
int blueRead2 = getRGBValue(blackBlue, whiteBlue, blueRead);
//Output RGB Values
Serial.print("R: ");
Serial.println(redRead2);
Serial.print("G: ");
Serial.println(greenRead2);
Serial.print("B:");
Serial.println(blueRead2);
Serial.println();
}
//takes a sensed color value (readColor) and returns an int representing the scaled value from 0-255
int getRGBValue(int blackColor, int whiteColor, int readColor) {
//since the black balance might not be zero, we subtract the black value to shift the range so it starts at 0
int maxColor = whiteColor - blackColor;
long x = readColor - blackColor;
x = x * 255;
x = x / maxColor;
if (x < 0) {
x = 0;
}
else if (x > 255) {
x = 255;
}
return x;
}
void whiteBlackBalance() {
delay(1000);
whiteBalance();
delay(1000);
whiteBalance();
delay(1000);
whiteBalance();
delay(3000);
blackBalance();
delay(1000);
blackBalance();
delay(1000);
blackBalance();
}
void whiteBalance() {
whiteRed = readColor(redLed);
whiteGreen = readColor(greenLed);
whiteBlue = readColor(blueLed);
}
void blackBalance() {
blackRed = readColor(redLed);
blackGreen = readColor(greenLed);
blackBlue = readColor(blueLed);
}
// pass this function the pin number of the led of the color you want to measure
int readColor(int ledPin) {
digitalWrite(ledPin, HIGH);
uint16_t x = tsl.getLuminosity(TSL2561_VISIBLE);
delay(300);
digitalWrite(ledPin, LOW);
return x;
}
This sensor only works if it calibrates itself to known black and white values. The sensor first goes through a white and black balancing process. It first takes three readings of “white”. While this is happening, you have to press the sensor against something white and preferably non glossy. It takes three readings because I noticed that it seems to “settle” into a more accurate reading after a few readings of the same color. Then it pauses three seconds and takes three readings of a black object.
These values get stored in the blackRed, whiteRed, etc variables. When it takes a reading of another color after calibration, it uses those black and white balancing variables to figure out where in the spectrum between black and white the values reside. The getRGBValue function takes care of this:
int maxColor = whiteColor - blackColor; long x = readColor - blackColor; x = x * 255; x = x / maxColor;
It first shifts the scale of minimum color to maximum color. For example, lets say the blackRed reading is 50 and the whiteRed reading is 500. This means that when the sensor reads a black object, it will still read 50 units of red. No object should show less than that because black is the darkest it should get. White will always show about 500 units and no other object (other than true red) should show 500 units of red because white is the brightest it should ever get. We need to shift the scale from 50 – 500 so it starts at zero. So the function first subtracts 50 (blackColor) from the whiteColor variable and also from the readColor variable. (readColor is how much of that color was just sensed on an object).
Next, I wanted to scale the sensed value so it was between 0 and 255. This would make it easy for me to plug into a computer paint program to see if the RGB values were correct. So the software takes the perceived value (now shifted down) and multiplies it by 255 and then divides it by the maxColor variable (whiteColor scaled down). It really is difficult to explain this in words. It makes much more sense if you just plug in sample values and see how the math works out.
The main loop of the program just takes the red, green, and blue readings, converts them to RGB values and then prints them out via the serial port:
//Output RGB Values
Serial.print("R: ");
Serial.println(redRead2);
Serial.print("G: ");
Serial.println(greenRead2);
Serial.print("B:");
Serial.println(blueRead2);
Serial.println();
I can just open up the Arduino serial terminal to view the RGB values. It spits the values out about every 500 milliseconds. Once I calibrate the sensor to black and white, I can just hold it up to any object and it gives me the RGB values between 0 and 255. I then take those values and plug them into MS Paint’s “custom colors” tool to see what color those values actually are.
I printed out this color chart below so I could see how well the color sensor works. I put the RGB values on each square for documentation purposes but they were not actually printed on the sheet of paper.
And here are the results:
Red: R: 209 G: 22 B: 20
Red looked really good. It wasn’t as bright as I expected but the color matched pretty closely.
Green: R: 22 G: 98 B: 31
Green looked alright but it was a bit dark.
Blue: R: 22 G: 50 B: 114
Blue was also pretty close but a bit darker than the real image.
Yellow: R: 239 G: 156 B: 34
This is the worst so far. It comes out more like a light orange. For some reason the green is not as sensitive as it needs to be.
Cyan: R: 22 G: 135 B: 197
This one is kind of close, but there’s a bit too much blue and not enough green
Magenta: R: 201 G: 17 B: 57
This is not very accurate. It shows up as a dark red. There is not enough blue in it to look like magenta.
I would say this first test was a success. The sensor definitely works but the colors are not all quite right. The red sensor seems to be working fine, but the blue and especially the green are not sensitive enough. I would expect that when I sense a 100% green color, the green sensor would sense somewhere over the 200 value for green. But it doesn’t. I wonder if the White balance is just too bright. Perhaps if I balance the red green and blue separately rather than just using white I would get more accurate readings. Time to tweak my code and find out!



Great project! I was thinking about something similar to link (with processing or something) to make a real colorpicker

Good luck with the “finetunings”
May I ask why you do all the maths in the getRGBValue() function?
Maybe I overlooked something, but I would replace it with the following:
int getRGBValue(int blackColor, int whiteColor, int readColor) {
//scale readColor from a val between blackColor and whiteColor to a val
// between 0-255
int x = map(readColor, blackColor, whiteColor, 0, 255);
// Now make sure the scaled value stays between 0 and 255
x = constrain(x, 0, 255);
}
Wow you are totally right. I had used the map function a long time ago in another project and forgot all about it. Also, I had never even seen the constrain function. I need to bone up on my Arduino programming I think. The next time I work on the code I will be changing it to use these functions. Thanks for the tip.
Hi ! Nice one, I made some a few months ago based on some instructables. It does not work as good as yours, but I’ll see if I can’t use your experience to make mine better. FYI you could have a cheaper solution (however more imprecise I guess) by using a CdS photoresistor instead of the 12 USD component. That works not too bad in my case.
Thanks for sharing your creation!
The CdS cheap version : http://kalshagar.wikispaces.com/cameleon
I think I actually did see yours. A couple years ago I tried to make a color sensor using a CdS photoresistor but it never seemed to work reliably. At the time I wasn’t sure if it was the photoresistor or some other part of my sensor that was causing the issues. When I finally decided to revisit the project I chose to use a better sensor just to rule out the possibility of the photoresistor being the culprit. I have noticed with this digital luminosity sensor I get very consistent readings after about 1-2 seconds on the same object. Back when I was using the photocell the readings seemed to vary slightly even when I barely moved the sensor. I’m starting to think that the main cause of my problems the last time I worked on this was that my sensor board was not securely placed in the enclosure, so even small movements of the board messed up the calibration. Thanks for sharing.
The discrepancies between what it reads from a printed sheet and what values you expected will have a lot to do with the printer, inks, and paper you use. For example, the green patch only read as a 98. Since the green LED outputs at 520nm ( according to the specs) the sensor will only report back now much light is reflected back at that wavelength. Unless the green patch on the paper also peaked at 52onm, and had high reflectivity, the green value from the sensor will be lower than expected.
It’s also possible the sensor is not linear across the entire brightness range for a given wavelength. You can test this by ramping the LED from black to full brightness and output the sensor reading at each step.
Interesting, potentially handy for blind people to check what object their visual to audio/tactile system is looking at.
Re. LEDs, I’d have used lime green LEDs as they seem to be closer to the correct colour space coordinates.
Also try underdriving them and keeping the temperature stable to make the peak wavelength stable long term.
Many cheaper LEDs especially InGaN have a pronounced spectral shift at >20mA as I found out.
@Conundrum Thanks for the tips! I hadn’t realized that about LEDs before. I wonder if that would make things a bit more stable and make the sensor even more consistant…
I haven’t looked into your code, but I did a fast scan of the RSL2561 datasheet.
In particular, the spectral response is not the same for all colors (ignoring the IR sensor). Red (600nm) tends to give about 95%, green (500nm) gives 86%, blue (400nm) gives 65%. I didn’t see anything regarding linearity response (does 50% green light really give 43% full scale?).
The actual wavelengths being measured are determined by your LEDs. As already noted (can’t find that message – sorry) driving the LEDs hard raises their temperature and shifts their peak spectral output. That alone would explain why you require several seconds to get a stable result. Reduce LED drive, and try to get RGB LEDs that correspond to human visual response.
For each color LED and the sensor, you need to linearize the readings based on light (not LED current). Linearization can also normalize the results to 0-100%. Take readings off grayscale cards or different shades of gray printed onto paper (every 10% from 0 to 100%, 11 shades total) and see if the raw readings match what you expect. You might be surprised. Sensors of all types (optical, magnetic, thermal, distance, etc) are generally NOT linear.
Linearization can be performed using a polynomial.
Y=A3*X^3 + A2*X^2 + A1*X + A0 (3rd order polynomial)
X is the raw reading from the sensor (hexadecimal converted to decimal).
Y is the desired result (0-100%, for example)
A3, A2, A1, A0 are the equation coefficients from Excel
In Excel, plot the data in a scatter graph (X-Y graph). The X values are the raw readings from the sensor, the Y values are what they should be converted to. Add a trendline to the graph of the polynomial type, and have it display on the graph. Adjust the poly order until the trendline fits the data (for a 4th order poly, you will need at least 4 points, 3 points for 3rd order, etc). Copy the resulting polynomial into your code (you may need to increase the number of displayed digits in the trendline function.)
This linearization process can be used for ANY type of sensor. Consider a silicon photo transistor/diode or CdS photocell. Even a silicon solar cell could be used.
Your device did not work as desired because you need three sensors whose responsivities match the the spectral power distribution of the human eye. This device is called a “colorimeter”; this might guide your search.
Wow Brett, that’s a lot of good information. Thanks for all of the tips. At this point the sensor is working as well as I wanted it too. I’m not sure how much further I will take this project but if I run into more problems with it down the line I will definitely keep this in mind. And at the very least it’s all archived here on this site now if others run into trouble. Thanks again!