To run OpenCV in Raspberry Pi 5 reliably today, you must abandon the legacy camera stack. The winning combination for real-time computer vision is the Raspberry Pi 5 (8GB) running Bookworm 64-bit, capturing frames via the picamera2 library (libcamera), and processing them with opencv-python-headless. This guide walks you through building a vision-guided hardware sorter, mapping the exact I2C pins, and debugging the specific pipeline errors that trap most makers on the new OS.
The Hardware Stack: Decision Path for OpenCV in Raspberry Pi
Before buying parts, you need to match your compute requirement to the right board and sensor. OpenCV contour detection is lightweight, but modern object detection (YOLO) requires neural accelerators. Use this decision matrix to lock in your exact BOM.
| Use Case | Board Variant | Camera Module | Verdict & Reasoning |
|---|---|---|---|
| Basic Color/Contour Sorting | Pi 4 Model B (4GB) | Camera Module V2 (IMX219) | Viable, but legacy. Pi 4 throttles under sustained CV loads without active cooling. |
| Real-Time Multi-Object Tracking | Pi 5 (8GB) | Camera Module 3 (IMX708) | DEFAULT PICK. PCIe bus and 8GB RAM handle 1080p @ 30fps OpenCV pipelines without frame drops. |
| Edge AI / YOLOv8 Inference | Pi 5 (8GB) + Hailo-8L | Camera Module 3 or HQ | Required if moving beyond HSV color masking to neural network classification. |
Hardware BOM and Pin Mapping
This project actuates a physical sorting gate using a PCA9685 16-channel PWM driver. The Pi 5's hardware PWM pins are often occupied by audio or fan headers, so offloading to an I2C PWM driver is the most robust jobsite practice.
Parts List
- Compute: Raspberry Pi 5 (8GB) with Active Cooler and 27W USB-C PSU
- Vision: Raspberry Pi Camera Module 3 (Standard or Wide angle)
- Actuator Driver: Adafruit PCA9685 16-Channel 12-bit PWM/Servo Driver (Product ID: 815)
- Servos: 2x SG90 Micro Servos (for diverter gates)
- Wiring: Female-to-Female jumper wires, Pi 5 mini HDMI cable (for initial setup)
Pin Mapping Table (Pi 5 to PCA9685)
| Pi 5 GPIO (Physical Pin) | Function | PCA9685 Pin | Notes |
|---|---|---|---|
| GPIO 2 (Pin 3) | I2C SDA | SDA | Requires 3.3V logic (Pi 5 native) |
| GPIO 3 (Pin 5) | I2C SCL | SCL | Ensure pull-ups are enabled on PCA9685 board |
| 3.3V (Pin 1) | VCC (Logic) | VCC | Do NOT connect to 5V (will fry Pi 5 GPIO) |
| GND (Pin 6) | Ground | GND | Common ground is mandatory for I2C stability |
| N/A (External 5V PSU) | Servo Power | V+ (Green terminal) | Never power servos directly from the Pi's 5V rail |
Bulletproof Installation on Bookworm OS
The transition to Debian Bookworm broke thousands of legacy OpenCV tutorials. The picamera library is deprecated. You must use picamera2 and install OpenCV without GUI dependencies to prevent Wayland/X11 conflicts.
- Flash the OS: Use Raspberry Pi Imager to flash Raspberry Pi OS (64-bit) Bookworm. Enable SSH and set your WiFi in the imager settings.
- Update System Packages:
sudo apt update && sudo apt upgrade -y - Install libcamera and Picamera2:
sudo apt install -y python3-picamera2 python3-libcamera - Create a Virtual Environment: Bookworm enforces PEP 668, blocking global pip installs.
python3 -m venv cv_env && source cv_env/bin/activate - Install OpenCV and I2C Libraries:
pip install opencv-python-headless numpy adafruit-circuitpython-pca9685 - Enable I2C: Run
sudo raspi-config, navigate to Interface Options > I2C, and enable it. Reboot.
The Code: Vision-Guided Servo Actuation
This script captures frames via picamera2, converts the numpy array to an OpenCV BGR image, isolates red objects using HSV color masking, and triggers the PCA9685 servo if the object's centroid crosses the center threshold. It includes robust error handling for I2C and camera pipeline failures.
import time
import cv2
import numpy as np
from picamera2 import Picamera2
import board
import busio
from adafruit_pca9685 import PCA9685
# --- PIN & HARDWARE DEFINITIONS ---
I2C_SDA = board.SDA
I2C_SCL = board.SCL
SERVO_CHANNEL = 0
SERVO_MIN_PULSE = 150
SERVO_MAX_PULSE = 600
CENTER_THRESHOLD_X = 320 # Half of 640 width
def init_servo_driver():
try:
i2c = busio.I2C(I2C_SCL, I2C_SDA)
pca = PCA9685(i2c, address=0x40)
pca.frequency = 50
return pca
except ValueError as e:
print(f'I2C Init Error: {e}. Check wiring and i2cdetect -y 1.')
raise
def move_servo(pca, channel, angle):
pulse_width = SERVO_MIN_PULSE + (angle / 180.0) * (SERVO_MAX_PULSE - SERVO_MIN_PULSE)
pca.channels[channel].duty_cycle = int(pulse_width * 0xFFFF / 20000)
def main():
print('Initializing Camera and Servos...')
pca = init_servo_driver()
move_servo(pca, SERVO_CHANNEL, 90) # Center position
picam2 = Picamera2()
config = picam2.create_preview_configuration(main={'size': (640, 480), 'format': 'RGB888'})
picam2.configure(config)
try:
picam2.start()
time.sleep(2) # Allow camera AGC to settle
print('Pipeline active. Sorting...')
except RuntimeError as e:
print(f'Camera Start Failed: {e}')
return
try:
while True:
# Capture frame as numpy array (RGB)
frame_rgb = picam2.capture_array()
# Convert RGB to BGR for OpenCV processing
frame_bgr = cv2.cvtColor(frame_rgb, cv2.COLOR_RGB2BGR)
# Convert to HSV for color masking
hsv = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2HSV)
# Define range for red color (requires two masks due to HSV wrap-around)
lower_red1 = np.array([0, 120, 70])
upper_red1 = np.array([10, 255, 255])
lower_red2 = np.array([170, 120, 70])
upper_red2 = np.array([180, 255, 255])
mask1 = cv2.inRange(hsv, lower_red1, upper_red1)
mask2 = cv2.inRange(hsv, lower_red2, upper_red2)
mask = cv2.bitwise_or(mask1, mask2)
# Find contours
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if contours:
largest_contour = max(contours, key=cv2.contourArea)
area = cv2.contourArea(largest_contour)
if area > 500: # Filter out noise
M = cv2.moments(largest_contour)
if M['m00'] != 0:
cx = int(M['m10'] / M['m00'])
# Actuate servo based on X position
if cx > CENTER_THRESHOLD_X + 50:
move_servo(pca, SERVO_CHANNEL, 45) # Push Right
elif cx < CENTER_THRESHOLD_X - 50:
move_servo(pca, SERVO_CHANNEL, 135) # Push Left
else:
move_servo(pca, SERVO_CHANNEL, 90) # Center
except KeyboardInterrupt:
print('Stopping...')
finally:
picam2.stop()
move_servo(pca, SERVO_CHANNEL, 90)
pca.deinit()
if __name__ == '__main__':
main()
Debugging: First 3 Checks and Exact Error Strings
When your pipeline fails, it usually fails at the intersection of libcamera and I2C. Before rewriting code, run through this diagnostic sequence.
- Verify the physical camera link: Run
libcamera-hello -t 5000in the terminal. If this fails, your ribbon cable is seated backward or the CAM1 connector latch is broken. - Verify I2C addressing: Run
i2cdetect -y 1. You must see40(or70for the PCA9685 all-call address). If the grid is empty, your SDA/SCL wires are swapped or the Pi's I2C interface is disabled. - Verify the Virtual Environment: Ensure your terminal prompt starts with
(cv_env). Running the script outside the venv will throwModuleNotFoundError: No module named 'cv2'due to Bookworm's PEP 668 restrictions.
Ranked Causes for Common Error Strings
Error String: cv2.error: OpenCV(4.8.1) /io/opencv/modules/imgproc/src/color.cpp:182: error: (-215:Assertion failed) !_src.empty() in function 'cvtColor'
- Cause 1 (Most Likely): You are trying to use
cv2.VideoCapture(0)instead ofpicamera2. On Bookworm,/dev/video0is not populated by default without specific V4L2 kernel overlays. - Cause 2: The
picam2.capture_array()returnedNonebecause the camera pipeline dropped. Fix: Add atime.sleep(2)afterpicam2.start()to let the AGC (Auto Gain Control) initialize.
Error String: OSError: [Errno 121] Remote I/O error (Thrown during PCA9685(i2c) initialization)
- Cause 1: The PCA9685 is not receiving 5V power on the green V+ terminal block. The logic chip might power up via VCC, but the I2C handshake fails under load.
- Cause 2: Missing pull-up resistors. The Adafruit board has 10k pull-ups onboard, but if you are using a generic clone, you may need to add 4.7k resistors between SDA/SCL and 3.3V.
Scaling the Build: Simplify or Extend
Once the baseline color sorter is running, you need to decide how to adapt it for your specific bench or production environment. Do not leave the system in a half-finished state; commit to one of these two paths.
Path A: Simplify for Data Logging (No Actuators)
If you only need to count parts or log defect rates, strip out the adafruit-circuitpython-pca9685 dependency entirely. Replace the servo actuation block with an MQTT publish command using paho-mqtt. Send the cx, cy, and area variables as a JSON payload to a local Mosquitto broker. This reduces hardware failure points and drops the CPU load by roughly 4%, allowing you to step up the camera resolution to 1080p without frame drops.
Path B: Extend to Neural Object Detection
HSV color masking fails under shifting ambient light. To make the system robust against shadows and varying part orientations, upgrade to YOLOv8.
You will need to install the ultralytics package and export a custom-trained model to ONNX format. Because the Pi 5 CPU will bottleneck at 2-3 FPS running YOLOn, you must add the Raspberry Pi AI Kit (Hailo-8L) to the PCIe M.2 HAT+ slot. This offloads the tensor math to the NPU, pushing your inference back up to a usable 15-20 FPS for real-time sorting.
For deeper integration details on the libcamera pipeline, refer to the official Raspberry Pi Camera Software documentation, and for OpenCV wheel specifics, check the opencv-python GitHub repository. Always verify your I2C wiring against the Adafruit PCA9685 CircuitPython guide before applying power to the servo rail.






