周末整理储物间时,翻出几个闲置的SG90舵机,突然想起去年用树莓派做的那个会追着人脸转动的摄像头项目。当时为了调试这个系统,连续三个晚上熬到凌晨两点,现在回想起来那些坑真是让人又爱又恨。今天就把这个项目的完整实现过程整理出来,带大家从零开始构建一个真正能用的智能跟踪系统。
树莓派4B作为控制核心确实是个明智之选,它的40针GPIO接口和充足的算力完全能满足我们的需求。我测试过用3B+版本也能运行,但在处理多个人脸识别时帧率会明显下降。以下是项目所需的核心组件清单:
| 组件 | 规格要求 | 备注 |
|---|---|---|
| 树莓派 | 4B 2GB/4GB | 建议4GB内存版 |
| 摄像头 | 官方摄像头模块v2 | 支持720p@60fps |
| 舵机 | SG90 180°版本 | 需塑料齿轮款 |
| 云台支架 | 二自由度金属结构 | 承重≥200g |
| 电源 | 5V 3A输出 | 需独立供电 |
特别注意:舵机工作时会产生电流波动,建议使用独立电源供电而非树莓派USB口,否则可能导致系统重启。
第一次连接时我犯了个低级错误——把信号线接到了3.3V引脚上,结果舵机完全没反应。正确的接线方式应该是:
python复制# 接线示意图
"""
树莓派GPIO14(物理引脚8) → 舵机信号线(黄色)
树莓派5V(物理引脚2) → 舵机电源线(红色)
树莓派GND(物理引脚6) → 舵机地线(黑色)
"""
如果使用外部电源,务必记得将电源负极与树莓派GND相连,这个共地原则是很多初学者容易忽略的关键点。我曾因为没共地导致舵机随机乱转,调试了整整一个下午。
SG90舵机的控制信号其实很有讲究。标准的50Hz PWM信号(周期20ms)中,脉冲宽度与角度的对应关系如下:
但在实际测试中发现,不同舵机存在个体差异。我的解决方案是引入校正系数:
python复制from gpiozero import Servo
from time import sleep
my_correction = 0.45 # 需要实测调整
max_pw = (2.5 + my_correction)/1000
min_pw = (0.5 - my_correction)/1000
servo = Servo(14, min_pulse_width=min_pw, max_pulse_width=max_pw)
servo.mid() # 归中位置
舵机抖动问题困扰了我很久,后来发现根本原因是占空比没有及时清零。有效的解决方案是:
python复制import RPi.GPIO as GPIO
GPIO.setup(14, GPIO.OUT)
pwm = GPIO.PWM(14, 50)
pwm.start(0)
def set_angle(angle):
duty = angle / 18 + 2.5
pwm.ChangeDutyCycle(duty)
sleep(0.05)
pwm.ChangeDutyCycle(0) # 关键消抖步骤
sleep(0.01)
推荐使用Python3.7+OpenCV4.x的组合,安装时要注意:
bash复制# 最新版树莓派系统推荐这样安装
sudo apt install libopencv-dev python3-opencv
pip install opencv-contrib-python==4.5.3.56
验证安装时发现一个坑:如果直接import cv2报错,可能需要手动设置环境变量:
python复制import os
os.environ['OPENCV_VIDEOIO_PRIORITY_MSMF'] = '0'
import cv2
经过多次优化,这个版本在树莓派上能达到15fps的处理速度:
python复制def detect_faces():
cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
cap = cv2.VideoCapture(0)
cap.set(3, 320) # 降低分辨率提升帧率
cap.set(4, 240)
while True:
_, frame = cap.read()
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
faces = cascade.detectMultiScale(gray, 1.1, 4)
if len(faces) > 0:
x, y, w, h = faces[0]
cv2.rectangle(frame, (x,y), (x+w,y+h), (255,0,0), 2)
return (x + w//2, y + h//2) # 返回人脸中心坐标
cv2.imshow('Frame', frame)
if cv2.waitKey(1) == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
将人脸坐标转换为舵机角度时,需要考虑坐标系转换。我的经验公式是:
code复制水平角度 = 90° - (人脸X坐标 - 160) × 0.3
垂直角度 = 60° - (人脸Y坐标 - 120) × 0.4
这个系数需要根据摄像头视场角调整,建议先用固定值测试:
python复制def coordinate_to_angle(x, y):
# 坐标系转换参数
Kx = 0.3 # 水平灵敏度
Ky = 0.4 # 垂直灵敏度
pan_angle = 90 - (x - 160) * Kx
tilt_angle = 60 - (y - 120) * Ky
# 限制角度范围
pan_angle = max(0, min(180, pan_angle))
tilt_angle = max(30, min(90, tilt_angle))
return pan_angle, tilt_angle
在实际部署中发现三个典型问题:
最终的主控制循环结构如下:
python复制from threading import Thread
class Tracker:
def __init__(self):
self.pan_angle = 90
self.tilt_angle = 60
self.last_detected = time.time()
def tracking_thread(self):
while True:
face_pos = detect_faces()
if face_pos:
self.last_detected = time.time()
new_pan, new_tilt = coordinate_to_angle(*face_pos)
if abs(new_pan - self.pan_angle) > 5:
set_pan_angle(new_pan)
if abs(new_tilt - self.tilt_angle) > 5:
set_tilt_angle(new_tilt)
elif time.time() - self.last_detected > 3:
# 超时回归初始位置
set_pan_angle(90)
set_tilt_angle(60)
原装塑料齿轮的SG90在连续工作两周后出现了齿轮磨损,建议进行以下改进:
在基础版本上可以添加这些有趣的功能:
python复制# 简单的HTTP视频流示例
from flask import Flask, Response
app = Flask(__name__)
def generate_frames():
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
if not ret: break
_, buffer = cv2.imencode('.jpg', frame)
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + buffer.tobytes() + b'\r\n')
@app.route('/video_feed')
def video_feed():
return Response(generate_frames(),
mimetype='multipart/x-mixed-replace; boundary=frame')
调试这个项目最大的收获是:硬件项目永远会有意外情况。记得第一次组装好时,舵机会在半夜突然自己转动,后来发现是电源干扰问题。现在这个系统已经稳定运行半年多,成了我工作室的智能看护设备。