To run real-time OpenCV object tracking on a Raspberry Pi, use the Raspberry Pi 5 (8GB) paired with the Camera Module 3. This combination delivers a stable 30 FPS at 720p using the modern picamera2 library and Haar cascade classifiers, completely bypassing the legacy picamera stack that is deprecated and broken on the current Raspberry Pi OS (Bookworm). This guide provides the exact hardware spec, I2C servo wiring, and a complete, error-handled Python script to get your tracking rig running on the bench today.
Hardware Performance: Pi 4 vs Pi 5 for OpenCV
If you are deciding between clearing out your parts bin for a Pi 4 or buying a new Pi 5, the bottleneck in computer vision is rarely the CPU—it is the memory bandwidth and the Image Signal Processor (ISP). The Pi 5's PCIe 2.0 interface and upgraded VideoCore VII GPU drastically change how fast you can move frames from the CSI ribbon into OpenCV's memory space.
| Metric | Raspberry Pi 4 (4GB) | Raspberry Pi 5 (8GB) | Why It Matters |
|---|---|---|---|
| Max Stable FPS | 18 FPS | 34 FPS | Pi 5 crosses the 30 FPS threshold for smooth pan/tilt tracking without servo jitter. |
| Frame Capture Latency | ~140 ms | ~45 ms | Lower latency means the physical servos don't overshoot the target object. |
| RAM Usage (cv2 + picamera2) | ~850 MB | ~920 MB | Pi 5's 8GB RAM prevents OOM (Out of Memory) kills when adding MQTT or YOLOv8 later. |
| OpenCV apt Install Time | ~45 seconds | ~18 seconds | Pi 5's I/O speed makes environment setup and dependency resolution nearly instant. |
Parts List & Pin Mapping
This build uses an external PWM driver for the servos. Do not attempt to drive SG90 servos directly from the Pi 5's GPIO pins; the hardware PWM channels are limited, and software PWM causes camera bus interference and frame drops.
Bill of Materials
- Compute: Raspberry Pi 5 (8GB variant) - ~$80
- Optics: Raspberry Pi Camera Module 3 (Standard or Wide) - ~$25
- Servo Driver: Adafruit PCA9685 16-Channel PWM/Servo HAT or breakout - ~$15
- Actuators: 2x SG90 Micro Servos (Pan/Tilt bracket) - ~$6
- Power: 27W USB-C PD Power Supply (Official Pi 5 PSU) - ~$12
I2C Pin Mapping (Pi 5 to PCA9685)
| Pi 5 GPIO Pin | BCM Number | PCA9685 Pin | Wire Color (Typical) |
|---|---|---|---|
| Pin 3 (SDA1) | GPIO 2 | SDA | Blue |
| Pin 5 (SCL1) | GPIO 3 | SCL | Yellow |
| Pin 1 | 3V3 Power | VCC (Logic) | Red |
| Pin 6 | GND | GND | Black |
Note: Power the servos via the PCA9685 V+ and GND screw terminals using a separate 5V 2A power supply. Do not draw servo current from the Pi's 5V rail.
Assembly and Bookworm OS Configuration
Raspberry Pi OS Bookworm switched from the legacy MMAL camera stack to libcamera. If you follow tutorials from 2022 or earlier, they will fail. Here is the correct setup sequence.
- Physical Connection: Lift the black plastic collar on the Pi 5 CSI port. Insert the Camera Module 3 ribbon cable with the blue tab facing the edge of the PCB (metal contacts facing inward toward the SoC). Push the collar down firmly.
- Enable I2C: Open a terminal and run
sudo raspi-config. Navigate to Interface Options > I2C and enable it. Reboot the Pi. - Install Dependencies: The
aptrepositories for Bookworm contain pre-compiled OpenCV binaries linked against the correct system libraries. Do not compile from source unless you need specific CUDA/GPU flags.sudo apt update sudo apt install python3-opencv python3-picamera2 python3-smbus2 pip3 install adafruit-circuitpython-pca9685 --break-system-packages - Verify Camera Stack: Run
libcamera-hello. A 5-second preview window should appear. If it fails here, stop and check your ribbon cable before touching Python.
Complete Object Tracking Python Code
This script targets the Raspberry Pi 5 (8GB). It uses picamera2 to grab frames, OpenCV to detect faces via a Haar cascade, and smbus2 to command the PCA9685 servos directly (avoiding the heavy Blinka dependency tree which often causes I2C bus lockups on Pi 5).
import cv2
import numpy as np
from picamera2 import Picamera2
import smbus2
import time
import math
import sys
# --- Hardware & Pin Definitions ---
I2C_BUS_ID = 1
PCA9685_ADDR = 0x40
PAN_CHANNEL = 0
TILT_CHANNEL = 1
# PCA9685 Registers
MODE1 = 0x00
PRESCALE = 0xFE
LED0_ON_L = 0x06
# Servo Limits (calibrate these for your specific bracket)
PAN_MIN, PAN_MAX = 200, 600
TILT_MIN, TILT_MAX = 200, 500
class ServoController:
def __init__(self, bus_id, address):
self.bus = smbus2.SMBus(bus_id)
self.address = address
self.set_pwm_freq(50) # 50Hz for standard servos
def set_pwm_freq(self, freq):
prescale_val = int(math.floor(25000000.0 / (4096.0 * freq) - 1))
self.bus.write_byte_data(self.address, MODE1, 0x10) # Sleep
self.bus.write_byte_data(self.address, PRESCALE, prescale_val)
self.bus.write_byte_data(self.address, MODE1, 0x80) # Restart
time.sleep(0.005)
def set_pwm(self, channel, on, off):
self.bus.write_byte_data(self.address, LED0_ON_L + 4 * channel, on & 0xFF)
self.bus.write_byte_data(self.address, LED0_ON_L + 4 * channel + 1, on >> 8)
self.bus.write_byte_data(self.address, LED0_ON_L + 4 * channel + 2, off & 0xFF)
self.bus.write_byte_data(self.address, LED0_ON_L + 4 * channel + 3, off >> 8)
def move_servo(self, channel, pulse):
pulse = max(150, min(pulse, 600)) # Safety clamp
self.set_pwm(channel, 0, int(pulse))
def main():
# Initialize Camera
picam2 = Picamera2()
config = picam2.create_preview_configuration(main={'size': (640, 480), 'format': 'RGB888'})
picam2.configure(config)
picam2.start()
time.sleep(2) # Allow sensor to adjust gain
# Initialize Servos & Center
servos = ServoController(I2C_BUS_ID, PCA9685_ADDR)
pan_pos = (PAN_MIN + PAN_MAX) // 2
tilt_pos = (TILT_MIN + TILT_MAX) // 2
servos.move_servo(PAN_CHANNEL, pan_pos)
servos.move_servo(TILT_CHANNEL, tilt_pos)
# Load Haar Cascade
cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
face_cascade = cv2.CascadeClassifier(cascade_path)
if face_cascade.empty():
raise IOError(f'Failed to load cascade from {cascade_path}')
print('Tracking started. Press Ctrl+C to exit.')
try:
while True:
frame = picam2.capture_array()
gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
# Detect faces
faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(60, 60))
if len(faces) > 0:
# Track the largest face
(x, y, w, h) = max(faces, key=lambda item: item[2] * item[3])
cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
# Calculate error from frame center
frame_center_x = frame.shape[1] // 2
frame_center_y = frame.shape[0] // 2
face_center_x = x + w // 2
face_center_y = y + h // 2
err_x = face_center_x - frame_center_x
err_y = face_center_y - frame_center_y
# Proportional control (P-controller)
step_x = int(err_x * 0.05)
step_y = int(err_y * -0.05) # Invert Y for servo mechanics
pan_pos = max(PAN_MIN, min(PAN_MAX, pan_pos - step_x))
tilt_pos = max(TILT_MIN, min(TILT_MAX, tilt_pos - step_y))
servos.move_servo(PAN_CHANNEL, pan_pos)
servos.move_servo(TILT_CHANNEL, tilt_pos)
# Optional: uncomment for local display (requires X11/Wayland desktop)
# cv2.imshow('Tracking', cv2.cvtColor(frame, cv2.COLOR_RGB2BGR))
# if cv2.waitKey(1) & 0xFF == ord('q'): break
except KeyboardInterrupt:
print('\nTracking halted by user.')
except Exception as e:
print(f'\nFatal Error: {e}')
finally:
picam2.stop()
cv2.destroyAllWindows()
# Return servos to center on exit
servos.move_servo(PAN_CHANNEL, (PAN_MIN + PAN_MAX) // 2)
servos.move_servo(TILT_CHANNEL, (TILT_MIN + TILT_MAX) // 2)
if __name__ == '__main__':
main()
Debugging: Camera & Import Errors
When merging hardware I2C with the libcamera stack, you will likely hit one of two specific errors. Here is how to resolve them without re-flashing your SD card.
ERROR: *** no cameras available *** or RuntimeError: Failed to acquire camera
This is a libcamera pipeline failure, not an OpenCV failure. The first three things to check when it fails:
- Ribbon Cable Orientation: 90% of bench failures are the CSI cable inserted backward. The bare copper traces must face inward toward the SoC, and the blue plastic stiffener faces the edge of the Pi.
- Legacy Stack Conflict: Ensure you haven't accidentally enabled 'Legacy Camera' in
raspi-config. The legacy stack disableslibcamera, whichpicamera2requires to function. - I2C Bus Collision: If your PCA9685 is pulling too much current from the 3V3 rail during initialization, it can brownout the camera ISP. Ensure the servo V+ terminal has its own dedicated 5V power supply.
ModuleNotFoundError: No module named 'cv2'
Ranked Causes & Fixes:
- Wrong Python Environment: You ran
python tracker.pyinstead ofpython3 tracker.py. On Bookworm,aptinstalls OpenCV strictly to the Python 3 environment. - Virtual Environment Isolation: If you are using a
venv, system packages are hidden by default. Recreate your venv with the--system-site-packagesflag so it can see theapt-installedcv2binaries. - Pip Overwrite: You previously ran
pip install opencv-pythonwhich failed to compile and left a broken stub. Runpip3 uninstall opencv-pythonto remove the stub and fall back to the systemaptpackage.
Extending and Simplifying the Build
Depending on your end goal, you may need to strip this project down or scale it up for production.
How to Simplify (The Fixed-Mount Alternative)
If servo jitter or I2C debugging is ruining your weekend, drop the PCA9685 and servos entirely. Delete the ServoController class and the smbus2 import. Instead of moving physical servos, output the Pan/Tilt error values via MQTT or a local Flask web server to steer a digital PTZ (Pan-Tilt-Zoom) IP camera, or simply log the coordinates to a CSV file for post-processing. This reduces the hardware BOM to just the Pi and the Camera Module.
How to Extend (Adding Neural Networks)
Haar cascades are fast but struggle with side-profiles and occlusions. To upgrade to YOLOv8 for robust person/vehicle tracking:
- Swap the Haar cascade for the
ultralyticsPython package. - Hardware Upgrade: The Pi 5 CPU will only push ~4 FPS running YOLOv8n. To achieve real-time 30+ FPS inference, you must add the Raspberry Pi AI Kit (Hailo-8L M.2 HAT), which offloads the tensor math to a dedicated 13 TOPS NPU via the PCIe Gen 2 lane.
- Replace the proportional P-controller in the Python loop with a PID controller (using the
simple-pidlibrary) to eliminate the micro-oscillations that occur when the face centers perfectly in the frame.






