Note
Hello, welcome to the SunFounder Raspberry Pi & Arduino & ESP32 Enthusiasts Community on Facebook! Dive deeper into Raspberry Pi, Arduino, and ESP32 with fellow enthusiasts.
Why Join?
Expert Support: Solve post-sale issues and technical challenges with help from our community and team.
Learn & Share: Exchange tips and tutorials to enhance your skills.
Exclusive Previews: Get early access to new product announcements and sneak peeks.
Special Discounts: Enjoy exclusive discounts on our newest products.
Festive Promotions and Giveaways: Take part in giveaways and holiday promotions.
👉 Ready to explore and create with us? Click [here] and join today!
4.16 Pan-Tilt Camera Control System
Introduction
In this project, you will create a camera control system that can pan (move horizontally) and tilt (move vertically) using a joystick. The system allows you to remotely control the direction of a camera mounted on servos, preview the camera feed in real-time, and capture photos with the press of the joystick button. This project is perfect for surveillance applications, photography projects, or learning about servo motor control and camera integration.
What You’ll Need
The following components are required for this project:
COMPONENT INTRODUCTION |
PURCHASE LINK |
|---|---|
- |
|
- |
|
Raspberry Pi |
- |
Circuit Diagram
Wiring Diagram
To use camera module conveniently, Assemble the Pan-tilt (For Camera) is recommended.
Note
Assembling the pan-tilt may obscure some pins, so it is recommended to assemble it only when using the camera, or place it on the outside after assembly.
Follow this wiring diagram to set up the circuit:
Running the Example
Access the Raspberry Pi Desktop:
Remote Desktop: Use VNC for a full desktop experience.
Raspberry Pi Connect: Use Raspberry Pi Connect to access your Pi securely from any browser.
Open a Terminal and go to the code folder:
cd ~/ai-lab-kit/python
Run the script to start the camera:
sudo python3 pan_tilt_camera.py
After running the script, the pan-tilt camera system starts and initializes the camera and servos.
If a display is available, a live camera preview is shown; otherwise, the program runs normally in headless mode.
Moving the joystick left or right rotates the camera horizontally (pan), while moving it up or down tilts the camera vertically (tilt).
When the joystick button is pressed, the camera captures a photo and saves it to the
Pictures/camera_pan_tiltdirectory using a simple sequential filename such asphoto_001.jpg.The system continues running and responding to user input until you stop the program by pressing Ctrl + C.
Code
Below is the Python script used in this project:
#!/usr/bin/env python3
import os, time
from picamera2 import Picamera2, Preview
from fusion_hat.adc import ADC
from fusion_hat.pin import Pin, Mode, Pull
from fusion_hat.servo import Servo
# Servo channels for pan (horizontal) and tilt (vertical)
PAN_CHANNEL, TILT_CHANNEL = 2, 3
# Joystick ADC pins (X/Y axis) and button pin
X_PIN, Y_PIN = "A1", "A0"
BTN_PIN = 17
# Angle limits to protect servos
PAN_MIN, PAN_MAX = -90, 90
TILT_MIN, TILT_MAX = -45, 45
# Deadzone ignores small joystick movement
DEADZONE = 15
MOVE_SPEED = 3
LOOP_DELAY = 0.05
# Photo save directory (works with sudo)
REAL_USER = os.getenv("SUDO_USER") or os.getlogin()
PHOTO_DIR = os.path.join(f"/home/{REAL_USER}", "Pictures", "camera_pan_tilt")
os.makedirs(PHOTO_DIR, exist_ok=True)
# Initialize servos
pan_servo = Servo(PAN_CHANNEL)
tilt_servo = Servo(TILT_CHANNEL)
# Initialize joystick and button (active-low)
x_adc = ADC(X_PIN)
y_adc = ADC(Y_PIN)
joystick_button = Pin(BTN_PIN, mode=Mode.IN, pull=Pull.UP) # pressed -> 0
# Initialize camera
camera = Picamera2()
camera.configure(camera.create_preview_configuration(main={"size": (1280, 720)}))
preview_started = False
photo_count = 1
current_pan = 0
current_tilt = 0
last_button_state = 1 # Used for edge detection
def clamp(v, vmin, vmax):
# Limit value to a safe range
return max(vmin, min(vmax, v))
def map_value(value, in_min, in_max, out_min, out_max):
# Map ADC value to a new range
return (value - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
def apply_deadzone(v, dz):
# Ignore small joystick movement
return 0 if (-dz < v < dz) else v
def read_joystick():
# Read joystick X/Y position
x = map_value(x_adc.read(), 0, 4095, -100, 100)
y = map_value(y_adc.read(), 0, 4095, -100, 100)
return x, y
def check_button_press():
# Detect button press (HIGH -> LOW)
global last_button_state
current_state = joystick_button.value()
if last_button_state == 1 and current_state == 0:
last_button_state = current_state
return True
last_button_state = current_state
return False
def take_photo():
# Capture and save one photo
global photo_count
filename = f"photo_{photo_count:03d}.jpg"
filepath = os.path.join(PHOTO_DIR, filename)
camera.capture_file(filepath)
print("Saved:", filepath)
photo_count += 1
def start_preview_if_available():
# Start camera preview only if a display is available
global preview_started
preview_started = False
if os.getenv("DISPLAY"):
try:
camera.start_preview(Preview.QT)
preview_started = True
except Exception:
preview_started = False
def cleanup():
# Safely stop camera and release resources
try:
camera.stop()
except Exception:
pass
if preview_started:
try:
camera.stop_preview()
except Exception:
pass
try:
camera.close()
except Exception:
pass
def main():
global current_pan, current_tilt
start_preview_if_available()
camera.start()
# Center camera at startup
pan_servo.angle(0)
tilt_servo.angle(0)
try:
while True:
# Read joystick and move camera
x, y = read_joystick()
x = apply_deadzone(x, DEADZONE)
y = apply_deadzone(y, DEADZONE)
new_pan = current_pan + (MOVE_SPEED if x > DEADZONE else -MOVE_SPEED if x < -DEADZONE else 0)
new_tilt = current_tilt + (MOVE_SPEED if y > DEADZONE else -MOVE_SPEED if y < -DEADZONE else 0)
new_pan = clamp(new_pan, PAN_MIN, PAN_MAX)
new_tilt = clamp(new_tilt, TILT_MIN, TILT_MAX)
if new_pan != current_pan:
current_pan = new_pan
pan_servo.angle(current_pan)
if new_tilt != current_tilt:
current_tilt = new_tilt
tilt_servo.angle(current_tilt)
# Take photo when button is pressed
if check_button_press():
take_photo()
time.sleep(LOOP_DELAY)
except KeyboardInterrupt:
pass
finally:
cleanup()
if __name__ == "__main__":
main()
Understanding the Code
Hardware Initialization
Two servo motors are initialized to control camera pan (horizontal) and tilt (vertical) movement
The joystick uses ADC channels to read X and Y analog values, and a GPIO pin to detect button presses
The camera module is initialized and configured for preview mode, supporting both display and headless operation
Joystick Reading and Processing
read_joystick()reads raw analog values from the joystick’s X and Y axesmap_value()converts ADC values (0–4095) into a usable range of −100 to 100apply_deadzone()filters out small joystick movements to prevent unwanted camera drift
Camera Movement Control
Joystick input is translated into incremental changes in pan and tilt angles
clamp()ensures the angles stay within safe limits to protect the servo motorsServos are updated only when the angle changes, providing smooth and stable motion
Button Press Detection
The joystick button is configured as an active-low input using a pull-up resistor
check_button_press()detects a button press using edge detection (HIGH → LOW)This ensures only one photo is taken per button press, even if the button is held
Photo Capture and Storage
take_photo()captures an image using the camera modulePhotos are saved with sequential filenames (for example,
photo_001.jpg)All images are stored in the user’s
Pictures/camera_pan_tiltdirectory
Camera Preview Handling
A live camera preview is started only when a graphical display is available
The script continues to function normally when running without a display
Main Loop and Cleanup
The main loop continuously reads joystick input and responds in real time
When the program exits using
Ctrl + C, the camera is safely stoppedAll hardware resources are properly released to ensure a clean shutdown
Troubleshooting
Servos Not Moving:
Cause: Incorrect servo connections or power issues
Solution:
Verify servos are connected to correct channels (2 and 3)
Ensure Fusion HAT is properly powered
Check servo wiring for loose connections
Camera Preview Not Showing:
Cause: Camera module not detected or incorrect configuration
Solution:
Ensure camera cable is securely connected to CSI port
Check if camera is enabled in Raspberry Pi configuration
Verify camera module compatibility
Joystick Not Responding:
Cause: Incorrect pin assignments or ADC issues
Solution:
Verify joystick connections to A0, A1, and GPIO 17
Test ADC readings with simple print statements
Check if Fusion HAT ADC is functioning
Photos Not Saving:
Cause: Permission issues or directory problems
Solution:
Check if Pictures directory exists in user’s home
Verify write permissions for the photo directory
Try running with sudo if permission issues persist
Erratic Servo Movement:
Cause: Power fluctuations or software timing issues
Solution:
Ensure stable power supply to Fusion HAT
Adjust
MOVE_SPEEDand delay valuesAdd capacitors to servo power lines if needed
Extendable Ideas
Video Recording: Add video recording functionality with start/stop control:
def start_recording(): timestamp = time.strftime("%Y%m%d_%H%M%S") video_path = os.path.join(VIDEO_DIR, f"video_{timestamp}.mp4") camera.start_recording(video_path) print(f"Recording started: {video_path}") def stop_recording(): camera.stop_recording() print("Recording stopped")
Preset Positions: Create preset camera positions for quick access:
PRESETS = { 'center': (0, 0), 'left': (-45, 0), 'right': (45, 0), 'up': (0, 30), 'down': (0, -30) } def goto_preset(preset_name): if preset_name in PRESETS: pan, tilt = PRESETS[preset_name] pan_servo.angle(pan) tilt_servo.angle(tilt)
Conclusion
This project demonstrates how to create a sophisticated pan-tilt camera control system using Raspberry Pi, servos, and a camera module. It combines hardware control, real-time video processing, and user input handling into a cohesive system. The project provides a foundation for more advanced applications like surveillance systems, photography robots, or interactive art installations.