Color Matching with the Light Sensor and CircuitPython: Adafruit Circuit Playground Express

Updated 24 September 2018

The Adafruit Circuit Playground Express comes equipped with an analog light sensor, but it can be used for much more than just sensing light or darkness! The light sensor has a similar spectral response to the human eye. Its connected to analog pin A8 and will return a value between 0 and 1023. A normal indoor light level reading is about 300, with higher numbers being brighter.

With a light sensor you can obviously read ambient light, but did you know you could also use it to sense color or to measure your heartbeat? For this tutorial, we will use the light sensor and the buttons to make a color matching ring of NeoPixels!

To use a light sensor to read color you just need a NeoPixel near the light sensor. Lucky for us there is one already there on the Circuit Playground Express! When we see something that’s blue, its because it is reflecting blue light back at us from the full spectrum. If we light up our pixel near the light sensor each primary color and take a reading from each, we can calculate how much of each color was reflected and duplicate it on the NeoPixels. It's not a perfect system, as our NeoPixel cannot create true primary colors, and our NeoPixel ring won’t be able to perfectly duplicate every color we try to match. We can expect to accurately replicate at least 12 colors! Just hold the object that you want to color match very close to the sensor! Press button B to take a reading, Press Button A to clear the NeoPixel ring!


Adafruit Circuit Playground Express Lights Sensor

The Code

Let's take a look at the completed code, then break it down into parts.

# Color Matching with the Light Sensor: Adafruit Circuit Playground Express
# Created by Stephen @ core-electronics.com.au

from adafruit_circuitplayground.express import cpx
import time
import simpleio 

cpx.pixels.fill((0, 0, 0))
cpx.pixels.show()

while True:
    # when button b is pressed, detect color and set ring to that color
    if cpx.button_b:
        
        # set all cpx.pixels to off
        cpx.pixels.fill((0, 0, 0))
        time.sleep(.1)
        
        # turn pixel 1 Red and read light level, turn off after
        cpx.pixels[1] = [255, 0, 0]
        redRaw = cpx.light
        time.sleep(.1)
        cpx.pixels.fill((0, 0, 0))
        
        # turn pixel 1 to green and read light leve, turn off after
        cpx.pixels[1] = [0, 255, 0]
        greenRaw = cpx.light
        time.sleep(.1)      
        cpx.pixels.fill((0, 0, 0))
        
        # turn pixel 1 to blue and read light leve, turn off after
        cpx.pixels[1] = [0, 0, 255]
        blueRaw = cpx.light
        time.sleep(.1)     
        cpx.pixels.fill((0, 0, 0))
        
        # determine highest light reading
        maximum = max(redRaw, greenRaw, blueRaw)
        minimum = min(redRaw, greenRaw, blueRaw)

        # map light from between minimum light and maximum reading to 0-255
        red = simpleio.map_range(redRaw, minimum, maximum, 0, 255)
        green = simpleio.map_range(greenRaw, minimum, maximum, 0, 255)   
        blue = simpleio.map_range(blueRaw, minimum, maximum, 0, 255)
            
        # this simplifies detected colors into a smaller range, 
        # removes washed out colors that just appear white
        if red < 30:
            red = 0
        if green < 30:
            green = 0
        if blue < 30:
            blue = 0
                
        # converts the float values of red green and blue into intergers
        r = int(red)
        g = int(green) 
        b = int(blue) 
        
        # Serial monitor print
        print("Maximum Light: %i" % (maximum))
        print("Minimum Light: %i" % (minimum))        
        print("Raw RGB: {0} {1} {2}". format(redRaw, greenRaw, blueRaw))

        # send data to cpx.pixels, display nothing on bad reading
        if maximum - minimum <= 30:
            cpx.pixels.fill((0, 0, 0))
            print("No Color Detected!")
        else:
            cpx.pixels.fill((r, g, b))   
            print("Output RGB: {0} {1} {2} ". format(r, g, b))
        print()

    # turn all cpx.pixels off when button A is pressed
    if cpx.button_a:
        cpx.pixels.fill((0, 0, 0))
    
 
    
 

Take the Readings

The first thing that we do in the program is wait for a button B press. When the button is pressed the color sensing process begins. We first set all the pixels off, so any previously displayed colors don’t interfere with the sensing of a new color. There is a small sleep time between each light action just to make sure the NeoPixel has sufficient time to react. We then turn the nearest pixel to the light sensor on red and take a sensor reading while its on. We repeat this for green and blue.

while True:
    # when button b is pressed, detect color and set ring to that color
    if cpx.button_b:
        
        # set all cpx.pixels to off
        cpx.pixels.fill((0, 0, 0))
        time.sleep(.1)
        
        # turn pixel 1 Red and read light level, turn off after
        cpx.pixels[1] = [255, 0, 0]
        redRaw = cpx.light
        time.sleep(.1)
        cpx.pixels.fill((0, 0, 0))
        
        # turn pixel 1 to green and read light leve, turn off after
        cpx.pixels[1] = [0, 255, 0]
        greenRaw = cpx.light
        time.sleep(.1)      
        cpx.pixels.fill((0, 0, 0))
        
        # turn pixel 1 to blue and read light leve, turn off after
        cpx.pixels[1] = [0, 0, 255]
        blueRaw = cpx.light
        time.sleep(.1)     
        cpx.pixels.fill((0, 0, 0)) 

Calculate the Colors

This part of the code begins calculating color by first determining what the highest and lowest value read was and storing them as minimum and maximum light level detected. This becomes our range for our raw light input. We then remap the range of detected light levels to a range of 0-255 so it can be used as an output value for our NeoPixels. Making the range this way simplifies the detected colors because the input color will be mapped to 0 and the highest will be mapped to 255. Since we are replicating the colors on NeoPixels that don’t duplicate colors in a way that looks the same to our eyes, this ends up creating a much more satisfying result. To simplify the color reading further we then remove any RGB value under 30. This allows us to display true Red Green and Blue. There is almost always a bit of another color detected, so this makes it display just red, green, or blue when its close to those colors.

       # determine highest light reading
        maximum = max(redRaw, greenRaw, blueRaw)
        minimum = min(redRaw, greenRaw, blueRaw)

        # map light from between minimum light and maximum reading to 0-255
        red = simpleio.map_range(redRaw, minimum, maximum, 0, 255)
        green = simpleio.map_range(greenRaw, minimum, maximum, 0, 255)   
        blue = simpleio.map_range(blueRaw, minimum, maximum, 0, 255)
            
        # this simplifies detected colors into a smaller range, 
        # removes washed out colors that just appear white
        if red < 30:
            red = 0
        if green < 30:
            green = 0
        if blue < 30:
            blue = 0 

Output

The readings taken from the light sensor on the Circuit Playground Express are an analog float value, this means that it’s a variable with lots of decimal places. The NeoPixels need an integer (no decimal places). We convert the float values of red, green, and blue to integers and store them as r, g, b.

We then print the readings to the serial monitor. This helps with troubleshooting or if you want to make some changes to the way colors are simplified. Finally, there is some logic to determine if the reading was good or not. If the difference between the minimum and maximum reading is too small, it most likely means that there is nothing above the color sensor. If we have a bad reading, then the lights are left off and “No Color Detected” is printed. If there was a good color sensing, then we set all the pixels to the detected color and print the RGB value.

        # converts the float values of red green and blue into intergers
        r = int(red)
        g = int(green) 
        b = int(blue) 
        
        # Serial monitor print
        print("Maximum Light: %i" % (maximum))
        print("Minimum Light: %i" % (minimum))        
        print("Raw RGB: {0} {1} {2}". format(redRaw, greenRaw, blueRaw))

        # send data to cpx.pixels, display nothing on bad reading
        if maximum - minimum <= 30:
            cpx.pixels.fill((0, 0, 0))
            print("No Color Detected!")
        else:
            cpx.pixels.fill((r, g, b))   
            print("Output RGB: {0} {1} {2} ". format(r, g, b))
        print()

    # turn all cpx.pixels off when button A is pressed
    if cpx.button_a:
        cpx.pixels.fill((0, 0, 0))
    
  

The readings taken from the light sensor on the Circuit Playground Express are an analog float value, this means that it’s a variable with lots of decimal places. The NeoPixels need an integer (no decimal places). We convert the float values of red, green, and blue to integers and store them as r, g, b.

We then print the readings to the serial monitor. This helps with troubleshooting or if you want to make some changes to the way colors are simplified. Finally, there is some logic to determine if the reading was good or not. If the difference between the minimum and maximum reading is too small, it most likely means that there is nothing above the color sensor. If we have a bad reading, then the lights are left off and “No Color Detected” is printed. If there was a good color sensing, then we set all the pixels to the detected color and print the RGB value.

While color sensing using a light sensor isn’t necessarily the best way to read color, it does allow us to sense color with the Circuit Playground Express with no additional hardware. If you really want to detect color accurately you can use a dedicated IR color sensor. I have found that these read colors very accurately, but I still must simplify the detected colors to replicate them on an LED. Using a light sensor gives just as useable results for this sort of application, you just might need to take a couple readings before the color is just right.

If you want to learn more about the Adafruit Circuit Playground Express and CircuitPython, head over to our Circuit Playground Tutorials for great info on using the various sensors and lights! If you are an educator or new to programming, this same project can be done using MakeCode and we even have a tutorial for that!

Have a question? Ask the Author of this guide today!

Please enter minimum 20 characters

Your comment will be posted (automatically) on our Support Forum which is publicly accessible. Don't enter private information, such as your phone number.

Expect a quick reply during business hours, many of us check-in over the weekend as well.

Comments


Loading...
Feedback

Please continue if you would like to leave feedback for any of these topics:

  • Website features/issues
  • Content errors/improvements
  • Missing products/categories
  • Product assignments to categories
  • Search results relevance

For all other inquiries (orders status, stock levels, etc), please contact our support team for quick assistance.

Note: click continue and a draft email will be opened to edit. If you don't have an email client on your device, then send a message via the chat icon on the bottom left of our website.

Makers love reviews as much as you do, please follow this link to review the products you have purchased.