Getting a Raspberry Pi with OpenCV to reliably track objects requires more than just copying a tutorial. The transition from the legacy picamera stack to the modern picamera2 and libcamera architecture on the Raspberry Pi 5 has broken thousands of legacy scripts. If you are building an embedded vision project in 2026, you need to target the Pi 5's specific hardware pipelines and handle I2C bus addressing correctly for servo control.
This guide walks through building a color-tracking pan-tilt camera. We will wire a PCA9685 servo driver, write a fully compilable Python script with robust error handling, and dissect the exact fatal errors that crash OpenCV pipelines on ARM boards.
Hardware Spec Sheet & Parts List
This build targets the Raspberry Pi 5 (8GB variant). While the 4GB model will run basic OpenCV Haar Cascades, the 8GB model prevents out-of-memory kills when allocating frame buffers for 1080p video streams alongside neural network inference.
| Component | Exact Model / Variant | Approx. Price | Why This Variant? |
|---|---|---|---|
| Compute Board | Raspberry Pi 5 (8GB) | $80 | PCIe Gen 2 and 2-3x CPU bump over Pi 4 handles OpenCV matrix math natively. |
| Camera Sensor | Pi Camera Module 3 (IMX708) | $25 | Native libcamera support; PDAF autofocus prevents blurry tracking frames. |
| Servo Driver | PCA9685 16-Channel I2C PWM | $6 | Offloads PWM timing from Pi GPIO hardware timers; prevents servo jitter. |
| Servos (x2) | Tower Pro SG90 (9g Micro) | $4 | Adequate torque for the camera module; operates safely on 5V rail. |
| Power Supply | 27W USB-C PD (5V/5A) | $12 | Pi 5 requires 5A to prevent USB current limiting under camera + servo load. |
| Mechanics | 2-Axis Pan-Tilt Bracket Kit | $8 | Standard SG90 horn compatibility. |
Pin Mapping & Wiring Steps
The Raspberry Pi 5 uses a 40-pin header. We are using the primary I2C bus (i2c-1) to communicate with the PCA9685. The camera connects via the dedicated MIPI CSI-2 ribbon port.
PCA9685 to Pi 5 I2C Pinout
| PCA9685 Pin | Pi 5 Physical Pin | Pi 5 GPIO / Function | Wire Color (Std) |
|---|---|---|---|
| VCC | Pin 1 | 3.3V Power | Orange |
| GND | Pin 6 | Ground | Black |
| SCL | Pin 5 | GPIO 3 (SCL1) | Yellow |
| SDA | Pin 3 | GPIO 2 (SDA1) | Green |
Wiring Sequence
- Enable I2C: Boot the Pi, open terminal, run
sudo raspi-config→ Interface Options → I2C → Enable. Reboot. - Verify I2C Address: Run
sudo i2cdetect -y 1. You should see40in the grid. If you see70, it's the All-Call address;40is your target. - Power the Servos: Connect the PCA9685 V+ terminal block to an external 5V power source (or the Pi's 5V pin if only using two micro servos, but watch for brownouts). Connect GND to GND.
- Seat the Camera: Lift the plastic collar on the Pi 5 CAM1 port. Insert the ribbon cable with the blue tape facing the USB/Ethernet ports. Push the collar down.
Environment Setup & Compilable Python Code
Do not use pip install picamera. The legacy stack is deprecated on Bookworm. Install the modern ARM-optimized OpenCV and camera bindings:
sudo apt update
sudo apt install python3-picamera2 python3-opencv python3-smbus2
The following Python script initializes the Pi Camera 3, captures frames, applies an HSV color mask to track a blue object, and calculates the pan/tilt error to drive the PCA9685 servos. Pin definitions and I2C addresses are explicitly declared.
import cv2
import numpy as np
import time
from picamera2 import Picamera2
from picamera2 import MappedArray
from smbus2 import SMBus
# --- HARDWARE DEFINITIONS ---
I2C_BUS = 1
PCA9685_ADDR = 0x40
PAN_CHANNEL = 0
TILT_CHANNEL = 1
# PCA9685 Registers
MODE1 = 0x00
PRESCALE = 0xFE
LED0_ON_L = 0x06
class PCA9685:
def __init__(self, bus_num=I2C_BUS, address=PCA9685_ADDR):
self.bus = SMBus(bus_num)
self.address = address
self.set_pwm_freq(50) # Standard 50Hz for SG90 servos
def set_pwm_freq(self, freq):
prescaleval = 25000000.0 / 4096.0 / freq - 1.0
prescale = int(prescaleval + 0.5)
self.bus.write_byte_data(self.address, MODE1, 0x10) # Sleep
self.bus.write_byte_data(self.address, PRESCALE, prescale)
self.bus.write_byte_data(self.address, MODE1, 0x80) # Wake
time.sleep(0.005)
def set_pwm(self, channel, on, off):
base = LED0_ON_L + 4 * channel
self.bus.write_byte_data(self.address, base, on & 0xFF)
self.bus.write_byte_data(self.address, base + 1, on >> 8)
self.bus.write_byte_data(self.address, base + 2, off & 0xFF)
self.bus.write_byte_data(self.address, base + 3, off >> 8)
def move_servo(self, channel, angle):
# Map 0-180 degrees to PCA9685 pulse width (approx 150 to 600)
pulse = int(150 + (angle / 180.0) * 450)
self.set_pwm(channel, 0, pulse)
def main():
# Initialize Hardware
try:
servos = PCA9685()
except OSError as e:
print(f"[FATAL] I2C Communication Failed: {e}. Check wiring and i2cdetect.")
return
# Initialize Camera
picam2 = Picamera2()
picam2.configure(picam2.create_video_configuration(main={"size": (640, 480)}))
picam2.start()
time.sleep(2) # Allow sensor to adjust gain
# Servo starting positions (90 degrees = center)
pan_angle = 90
tilt_angle = 90
servos.move_servo(PAN_CHANNEL, pan_angle)
servos.move_servo(TILT_CHANNEL, tilt_angle)
print("[INFO] Tracking started. Press CTRL+C to exit.")
try:
while True:
# Capture frame from libcamera pipeline
frame_rgb = picam2.capture_array()
# Convert to BGR for OpenCV processing
frame_bgr = cv2.cvtColor(frame_rgb, cv2.COLOR_RGB2BGR)
hsv = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2HSV)
# Define range for blue color tracking
lower_blue = np.array([100, 150, 50])
upper_blue = np.array([140, 255, 255])
mask = cv2.inRange(hsv, lower_blue, upper_blue)
# Find contours
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if contours:
largest = max(contours, key=cv2.contourArea)
if cv2.contourArea(largest) > 500: # Noise filter
M = cv2.moments(largest)
if M["m00"] != 0:
cx = int(M["m10"] / M["m00"])
cy = int(M["m01"] / M["m00"])
# Calculate error from frame center (320, 240)
err_x = cx - 320
err_y = cy - 240
# Proportional control (P-controller)
pan_angle = max(0, min(180, pan_angle - (err_x / 20)))
tilt_angle = max(0, min(180, tilt_angle + (err_y / 20)))
servos.move_servo(PAN_CHANNEL, int(pan_angle))
servos.move_servo(TILT_CHANNEL, int(tilt_angle))
time.sleep(0.03) # ~30 FPS loop rate limit
except KeyboardInterrupt:
print("\n[INFO] Stopping servos and camera...")
finally:
picam2.stop()
servos.set_pwm(PAN_CHANNEL, 0, 0) # Turn off PWM signal
servos.set_pwm(TILT_CHANNEL, 0, 0)
if __name__ == "__main__":
main()
Debugging: Fatal OpenCV & Camera Errors
When working with embedded vision, hardware pipelines fail silently until OpenCV attempts to process a null buffer. Here is how to decode the exact error strings thrown by the interpreter.
Error 1: The Null Frame Crash
Exact 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'
Ranked Causes:
- Camera Ribbon Orientation: The Pi 5 CAM1 port requires the blue stiffener to face the USB ports. If reversed, the I2C handshake inside the camera module fails, returning a null frame.
- Legacy Stack Conflict: You have
picamera(legacy) andpicamera2installed simultaneously, causing the V4L2 driver to lock up. - Insufficient Boot Delay: The IMX708 sensor takes ~1.5 seconds to initialize its ISP. Calling
capture_array()before the sensor is ready yields an empty array.
Error 2: I2C Bus Failure
Exact Error String:
OSError: [Errno 121] Remote I/O error
Ranked Causes:
- I2C Disabled: You skipped
raspi-config. The kernel modulei2c_devis not loaded. - Missing Pull-up Resistors: Cheap PCA9685 clone boards sometimes omit the 10kΩ pull-ups on SDA/SCL. The Pi 5 has internal pull-ups, but long wires will cause signal degradation. Add external 4.7kΩ pull-ups to 3.3V if using wires longer than 10cm.
- Address Collision: Another device on the bus is holding SDA low.
1. Run
libcamera-hello -t 5000 in the terminal. If this doesn't show a preview window (or output frames headless), your issue is hardware/OS level, not Python.2. Run
sudo i2cdetect -y 1. If the grid is empty, check your jumper wires with a multimeter for continuity.3. Check your power supply voltage under load. Use a multimeter on the Pi's 5V and GND GPIO pins. If it drops below 4.8V while the servos move, the Pi will throttle and drop camera frames.
Extending and Simplifying the Build
Depending on your end goal, you may need to scale this architecture up or down.
How to Simplify (Remove the PCA9685)
If you only need to log coordinates and don't need physical movement, strip out the smbus2 and PCA9685 class entirely. You can also drop the resolution to 320x240 to push the Pi Zero 2 W to 60 FPS. If you must use servos without the driver board, use the Pi's hardware PWM pins (GPIO 12 and GPIO 13) via the gpiozero library, though expect slight jitter under CPU load.
How to Extend (Add Neural Inference)
To upgrade from HSV color tracking to YOLOv8 object detection, swap the cv2.inRange block for an inference call. The Pi 5's CPU can run YOLOv8n (nano) at ~12 FPS natively. For higher framerates, connect an AI accelerator via the Pi 5's PCIe Gen 2 lane using an M.2 HAT and a Coral TPU or Hailo-8L module.
Frequently Asked Questions (FAQ)
Can I run a Raspberry Pi with OpenCV without a monitor (headless)?
Yes, but you must configure picamera2 to bypass the display renderer. In the code above, capture_array() pulls frames directly from the memory buffer, which works perfectly over SSH. However, if you use cv2.imshow(), the script will crash with a Qt platform plugin "xcb" error. For headless debugging, save frames to disk using cv2.imwrite() or stream them via a lightweight Flask/MJPEG server.
Why is my Raspberry Pi with OpenCV running at only 5 FPS?
Low framerates on the Pi 5 almost always stem from using the default 12-megapixel still capture mode instead of the video pipeline. Ensure you are calling picam2.create_video_configuration() as shown in the script. Additionally, if you are running heavy morphological operations (like cv2.dilate or cv2.erode) on a 1080p frame, the ARM CPU will bottleneck. Downscale the frame using cv2.resize() before processing, or restrict the ROI (Region of Interest).
Is Raspberry Pi 5 better than Pi 4 for OpenCV machine learning?
Significantly. The Pi 5 features a Cortex-A76 quad-core processor that benchmarks roughly 2.5x faster than the Pi 4's Cortex-A72 in single-threaded OpenCV matrix operations. More importantly, the Pi 5 supports the modern libcamera stack natively, which utilizes hardware DMA (Direct Memory Access) to pass frames to OpenCV without CPU copying overhead—a major bottleneck on the Pi 4. For pure ML inference, however, both boards benefit equally from external USB/PCIe AI accelerators.






