A colour detection and sorting robot using OpenCV and Arduino is a classic mechatronics project that demonstrates real-world automation. This system uses a camera to identify the colour of objects on a conveyor or sorter mechanism, then commands servo motors or solenoids to direct objects to the correct bin. Popular for STEM competitions in India and as an automation prototype for small manufacturing units. This tutorial covers HSV colour detection, Arduino servo communication, and complete system integration.
Table of Contents
- System Architecture
- Hardware Components
- HSV Colour Detection with OpenCV
- Colour Calibration Tool
- Arduino Servo Control Code
- Raspberry Pi to Arduino Serial Communication
- Complete System Code
- FAQ
System Architecture
The sorting robot has three main subsystems:
- Vision subsystem: Camera + Raspberry Pi running OpenCV detects object colour
- Control subsystem: Arduino receives colour command via serial and activates servo/solenoid
- Mechanical subsystem: Conveyor belt + diverter servo arm directs objects to bins
Communication: Pi sends single-byte colour code (‘R’=red, ‘G’=green, ‘B’=blue, ‘Y’=yellow, ‘N’=none) via USB serial. Arduino reads the byte and positions the diverter servo accordingly.
Hardware Components
Arducam IMX219 8MP Camera Module
8MP camera for accurate colour detection. High resolution ensures small coloured objects are captured clearly. CSI interface provides low-latency feed to Raspberry Pi for real-time sorting decisions.
Waveshare IMX219-77 Camera Module
IMX219 with 77-degree FOV, compatible with Raspberry Pi. Suitable for sorting robots with wider conveyor belts where all sorting positions need to be in frame.
HSV Colour Detection with OpenCV
HSV (Hue, Saturation, Value) colour space is far more robust than RGB for colour detection under varying lighting. Hue directly encodes colour (0-179 degrees: 0=red, 60=yellow, 120=green, 240=blue), making it easy to define colour ranges independent of brightness.
import cv2
import numpy as np
# HSV ranges for common colours (Hue 0-179, Sat 0-255, Val 0-255)
COLOUR_RANGES = {
'red': [(0, 100, 100), (10, 255, 255)], # Red wraps around 0/180
'red2': [(170, 100, 100), (179, 255, 255)], # Red upper range
'green': [(40, 70, 70), (85, 255, 255)],
'blue': [(100, 100, 100), (130, 255, 255)],
'yellow': [(20, 100, 100), (35, 255, 255)],
}
def detect_colour(frame, roi=None):
if roi:
x, y, w, h = roi
frame = frame[y:y+h, x:x+w]
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
# Apply slight blur to reduce noise
hsv = cv2.GaussianBlur(hsv, (7, 7), 0)
results = {}
# Check each colour
for colour, (lower, upper) in COLOUR_RANGES.items():
if colour == 'red2':
continue # Handle separately
mask = cv2.inRange(hsv, np.array(lower), np.array(upper))
if colour == 'red':
mask2 = cv2.inRange(hsv, np.array(COLOUR_RANGES['red2'][0]),
np.array(COLOUR_RANGES['red2'][1]))
mask = cv2.bitwise_or(mask, mask2)
pixel_count = cv2.countNonZero(mask)
results[colour] = pixel_count
# Return colour with most pixels above threshold
best = max(results, key=results.get)
if results[best] > 500: # Minimum pixel threshold
return best, results
return 'none', results
Colour Calibration Tool
import cv2
import numpy as np
def nothing(x): pass
cap = cv2.VideoCapture(0) # Or use Picamera2
cv2.namedWindow('Calibration')
# Trackbars for HSV range adjustment
for name in ['H_Low','H_High','S_Low','S_High','V_Low','V_High']:
cv2.createTrackbar(name, 'Calibration', 0, 255, nothing)
cv2.setTrackbarPos('H_High','Calibration',179)
cv2.setTrackbarPos('S_High','Calibration',255)
cv2.setTrackbarPos('V_High','Calibration',255)
while True:
ret, frame = cap.read()
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
lo = np.array([cv2.getTrackbarPos(n,'Calibration') for n in ['H_Low','S_Low','V_Low']])
hi = np.array([cv2.getTrackbarPos(n,'Calibration') for n in ['H_High','S_High','V_High']])
mask = cv2.inRange(hsv, lo, hi)
result = cv2.bitwise_and(frame, frame, mask=mask)
cv2.imshow('Calibration', result)
cv2.imshow('Original', frame)
print(f'H:[{lo[0]}-{hi[0]}] S:[{lo[1]}-{hi[1]}] V:[{lo[2]}-{hi[2]}]', end='r')
if cv2.waitKey(1) & 0xFF == ord('q'): break
cap.release()
Arduino Servo Control Code
#include <Servo.h>
Servo diverter;
const int SERVO_PIN = 9;
// Servo angles for each bin (tune mechanically)
const int ANGLE_RED = 30;
const int ANGLE_GREEN = 90;
const int ANGLE_BLUE = 150;
const int ANGLE_YELLOW = 60;
const int ANGLE_NEUTRAL = 90;
void setup() {
Serial.begin(9600);
diverter.attach(SERVO_PIN);
diverter.write(ANGLE_NEUTRAL);
delay(500);
}
void loop() {
if (Serial.available()) {
char cmd = Serial.read();
int angle = ANGLE_NEUTRAL;
switch(cmd) {
case 'R': angle = ANGLE_RED; break;
case 'G': angle = ANGLE_GREEN; break;
case 'B': angle = ANGLE_BLUE; break;
case 'Y': angle = ANGLE_YELLOW; break;
default: angle = ANGLE_NEUTRAL;
}
diverter.write(angle);
delay(400); // Wait for servo to reach position + object to pass
diverter.write(ANGLE_NEUTRAL);
Serial.println('OK'); // Acknowledge
}
}
Raspberry Pi to Arduino Serial Communication
import serial
import time
# Connect Arduino to Pi USB port (usually /dev/ttyUSB0 or /dev/ttyACM0)
arduino = serial.Serial('/dev/ttyUSB0', 9600, timeout=1)
time.sleep(2) # Wait for Arduino reset
def send_sort_command(colour):
cmd_map = {'red':'R', 'green':'G', 'blue':'B', 'yellow':'Y', 'none':'N'}
cmd = cmd_map.get(colour, 'N')
arduino.write(cmd.encode())
response = arduino.readline().decode().strip()
return response == 'OK'
Arducam OV5642 Auto-Focus Camera
5MP camera with auto-focus – useful for colour sorting robots where object height varies. Auto-focus ensures clear images of objects at different heights on the conveyor, maintaining colour detection accuracy.
Complete System Code
from picamera2 import Picamera2
import cv2, serial, time, numpy as np
picam2 = Picamera2()
picam2.configure(picam2.create_preview_configuration(
main={'size': (640, 480), 'format': 'BGR888'}))
picam2.start()
time.sleep(2)
arduino = serial.Serial('/dev/ttyUSB0', 9600, timeout=1)
time.sleep(2)
# ROI: centre of frame where object passes (tune for your conveyor)
ROI = (250, 180, 140, 120) # x, y, w, h
last_sort_time = 0
SORT_INTERVAL = 2.0 # seconds between sorting commands
while True:
frame = picam2.capture_array()
colour, counts = detect_colour(frame, ROI)
# Draw ROI
x,y,w,h = ROI
cv2.rectangle(frame, (x,y), (x+w,y+h), (255,255,0), 2)
cv2.putText(frame, f'Detected: {colour}', (10,30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0,255,0), 2)
now = time.time()
if colour != 'none' and (now - last_sort_time) > SORT_INTERVAL:
send_sort_command(colour)
last_sort_time = now
print(f'Sorted: {colour}')
cv2.imshow('Colour Sorter', frame)
if cv2.waitKey(1) & 0xFF == ord('q'): break
picam2.stop()
FAQ
How do I handle varying lighting conditions in Indian factories?
Use a fixed LED ring light around the camera to create consistent illumination. Avoid ambient fluorescent or mercury vapour lights that flicker at 50Hz (Indian mains frequency) – use LED-only lighting in the inspection area. Enclose the inspection zone to block ambient light variation.
Can this detect more than 4 colours?
Yes. Add more colour ranges to the COLOUR_RANGES dictionary. With consistent lighting, you can reliably distinguish 8-10 distinct colours. Beyond that, use a spectrophotometer for precise colour measurement.
What if two colours are detected simultaneously?
Return the colour with the highest pixel count above threshold. Ensure only one object is in the ROI at a time by controlling conveyor belt speed relative to sorting cycle time.
Can I replace Arduino with direct Raspberry Pi GPIO servo control?
Yes. Use pigpio library on Pi for hardware PWM servo control. Eliminates the Arduino, simplifies wiring. Use this approach for prototype builds; keep Arduino for production where electrical isolation is beneficial.
Add comment