前言:本文实现了基于树莓派4B的猫脸追踪装置,可以利用摄像头实时检测猫脸,并驱动舵机锁定追踪猫脸,使猫脸始终位于图像中心
硬件:树莓派4B,两个SG-90舵机组成的云台,500万像素CSI接口摄像头
算法: Opencv库,GPIO库,PID控制
视觉追踪思路:
1,利用Opencv库实现摄像头图像的采集与处理
2,利用Opencv中提供的级联分类器,载入训练好的Haar特征分类器,实现猫脸的识别
3,绘制猫脸的矩形区域,并将该区域中心与图片中心(像素点中心)做对比,作为控制误差来源
4, 使用PID算法根据误差计算舵机占空比,进而控制舵机转动角度,实现摄像头的移动
1. 目标检测原理
基于Haar特征的级联分类器
这是由Paul Viola 和 Michael Jones 2001 年在论文《Rapid Object Detection using a Boosted Cascade of Simple Features》中提出的一种高效目标检测方法。
这种机器学习方法基于大量正面、负面图像训练级联函数,然后用于检测其他图像中的对象。
这里,我们将用它进行猫脸识别。最初,该算法需要大量正类图像(猫脸图像)和负类图像(不带猫脸的图像)来训练分类器。然后我们需要从中提取特征。
OpenCV 具备训练器和检测器,也可以训练自己的对象分类器,如汽车、飞机等
参考:
- http://www.elecfans.com/d/643378.html
- https://gitee.com/BA_Figure/faceai/blob/master/doc/detectionOpenCV.md?_from=gitee_search
2. 目标检测流程
利用Opencv的Python接口实现猫脸检测流程如下:
树莓派Opencv库安装参考(无需编译):https://www.jianshu.com/p/8fc2424d5d6c
- 读取图片
- 将图片转换为灰度格式,便于猫脸检测
- 利用训练好的Haar特征检测图片中的猫脸
- 绘制猫脸的矩形区域
- 显示猫脸检测后的图片
参考:
https://blog.csdn.net/bf02jgtrs00xktcx/article/details/84076390
3. 舵机控制
舵机的控制基于PWM波,手头上的舵机控制周期为20ms(50Hz),发送不同占空比的脉冲能够使舵机转动一定的角度。测试过后我手上的舵机转动0°-180°对应占空比为2.5%-13%(该数据需要自己测试下,见参考资料)
实现伺服的原理就是根据检测到的猫脸矩形框中心与图片像素中心的X,Y差值,计算得到对应的舵机占空比的变化
参考:
https://shumeipai.nxez.com/2018/06/21/pan-tilt-multi-servo-control.html
系统概览
特别注意: 我这里摄像头是正放的,排线朝下。实际上排线朝上比较合适点,那样的话需要将读取的图像旋转180°后进行处理,具体请参考:http://www.waveshare.net/study/article-903-1.html
或Opencv函数:cv2.rotate(),具体请参考Opencv文档
我这里是先摄像头读取,然后一帧一帧处理,见后面代码
代码实现
1. 载入分类器
OpenCV中提供有级联分类器类CascadeClassifier
首先,创建一个分类器对象face_cascade,
将训练好的猫脸分类器“haarcascade_frontalcatface.xml”载入到face_cascade对象中
import cv2 as cv
cap = cv.VideoCapture(0)
#实际后面是一帧一帧图像处理,可能这就是延时的原因吧:(
cap.set(3, 320)
cap.set(4, 240)
#设置图片大小320x240,处理速度快, 图片中心也就是x=160,y=120
face_cascade = cv.CascadeClassifier(
'/home/pi/Code/haarcascades/haarcascade_frontalcatface.xml')
#该处分类器下载地址:https://github.com/opencv/opencv/tree/master/data/haarcascades
#下载后放在树莓派中,地址按自己修改的填
2. 图像预处理与分类器检测
逐帧读取图像处理成灰度图像,加快检测速度;灰度图像保存在frame_gray中
调用CascadeClassifier类的detectMultiScale方法,输入待检测灰度像等相关参数,实现输入图像的检测,返回被检测物体的矩形框向量组faces
detectMultiScale是多尺度多目标检测
受制于本人水平,图像预处理没啥骚操作:)
ret, frame = cap.read()
gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(gray)
参考:
1.https://www.cnblogs.com/lyx2018/p/7073025.html
2.http://www.85kf.com/news/4739.html
3. 绘制矩形区域,得到矩形区域位置信息
if len(faces) > 0:
#(x,y)代表了矩形框的左上角像素值,(w,h)就是矩形框的长度和高,
#由此可知矩形框中心像素位置,与图片中心做对比产生控制误差
for (x, y, w, h) in faces:
cv.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
result_x[0] = x
result_x[1] = w
result_y[0] = y
result_y[1] = h
4. GPIO初始化
pan_pin = 33
tilt_pin = 31
GPIO.setmode(GPIO.BOARD)
# 注意BOARD和BCM模式是不一样的,别搞错了,两个舵机信号线也别接反了。。
GPIO.setup(pan_pin, GPIO.OUT)
GPIO.setup(tilt_pin, GPIO.OUT)
GPIO.setwarnings(False)
pan = GPIO.PWM(pan_pin, 50) # 50HZ
tilt = GPIO.PWM(tilt_pin, 50)
#PWM频率是50HZ,对应控制周期20ms,与舵机匹配
#启动时舵机占空比7.75%即90°位置
pan.start(7.75)
tilt.start(7.75)
time.sleep(1)
5. PID控制
为了后面方便开多进程,和使代码简化,创建个PID计算函数,这里只使用了PD比例微分控制,使用列表作为参数,便于在函数中更新参数值
def set_pwm(list1, list2):
err = list1[0] + list1[1] / 2 - list2[2]
pwm = err * 0.0012 + 0.0001 * (err - list2[0])
list2[0] = err
list2[1] += pwm
if list2[1] > 13:
list2[1] = 12
if list2[1] < 2.5:
list2[1] = 3
6. 多线程计算及舵机控制
lastError_x = 0
lastError_y = 0
w_center = 160
h_center = 120
result_x = [160, 0]
result_y = [120, 0]
para_x = [lastError_x, X_Duty, w_center]
para_y = [lastError_y, Y_Duty, h_center]
#以上是参数列表,方便向函数中传入参数并修改
threads = []
t1 = threading.Thread(target=set_pwm, args=(result_x, para_x))
threads.append(t1)
t2 = threading.Thread(target=set_pwm, args=(result_y, para_y))
threads.append(t2)
for t in threads:
t.setDaemon(True)
t.start()
print('start')
t.join()
#这里不知道写的对不对:(
print('X_Duty-> ' + str(para_x[1]))
print('Y_Duty-> ' + str(para_y[1]))
tilt.ChangeDutyCycle(para_y[1])
pan.ChangeDutyCycle(15.5 - para_x[1])
#15.5是根据舵机个性化修改的,目的是与PWM数据匹配,因为俯仰舵机和旋转舵机在减小误差上控制占空比增减是相反的
#按理说PID函数中,X_Duty -= PWM,为了PID函数的通用性,我都用了Duty += PWM,在方法ChangeDutyCycle()中做了匹配修改
#sleep必不可少,不然反应不过来,舵机不动
time.sleep(0.3)
pan.ChangeDutyCycle(0)
tilt.ChangeDutyCycle(0)
#将占空比置为0,使得舵机定位后不再动
cv.imshow("capture", frame)
完整代码
import cv2 as cv
import RPi.GPIO as GPIO
import time
import signal
import threading
pan_pin = 33
tilt_pin = 31
lastError_x = 0
lastError_y = 0
# duty: 2.5-13
X_Duty = 7.75
Y_Duty = 7.75
w_center = 160
h_center = 120
result_x = [160, 0]
result_y = [120, 0]
para_x = [lastError_x, X_Duty, w_center]
para_y = [lastError_y, Y_Duty, h_center]
pan_pin = 33
tilt_pin = 31
GPIO.setmode(GPIO.BOARD)
GPIO.setup(pan_pin, GPIO.OUT)
GPIO.setup(tilt_pin, GPIO.OUT)
GPIO.setwarnings(False)
pan = GPIO.PWM(pan_pin, 50) # 50HZ
tilt = GPIO.PWM(tilt_pin, 50)
pan.start(7.75)
tilt.start(7.75)
time.sleep(1)
def set_pwm(list1, list2):
err = list1[0] + list1[1] / 2 - list2[2]
pwm = err * 0.0012 + 0.0001 * (err - list2[0])
list2[0] = err
list2[1] += pwm
if list2[1] > 13:
list2[1] = 12
if list2[1] < 2.5:
list2[1] = 3
cap = cv.VideoCapture(0)
cap.set(3, 320)
cap.set(4, 240)
face_cascade = cv.CascadeClassifier(
'/home/pi/Code/haarcascades/haarcascade_frontalcatface.xml')
while True:
ret, frame = cap.read()
gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(gray)
if len(faces) > 0:
for (x, y, w, h) in faces:
cv.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
result_x[0] = x
result_x[1] = w
result_y[0] = y
result_y[1] = h
threads = []
t1 = threading.Thread(target=set_pwm, args=(result_x, para_x))
threads.append(t1)
t2 = threading.Thread(target=set_pwm, args=(result_y, para_y))
threads.append(t2)
for t in threads:
t.setDaemon(True)
t.start()
print('start')
t.join()
print('X_Duty-> ' + str(para_x[1]))
print('Y_Duty-> ' + str(para_y[1]))
pan.ChangeDutyCycle(15.5 - para_x[1])
tilt.ChangeDutyCycle(para_y[1])
time.sleep(0.3)
pan.ChangeDutyCycle(0)
tilt.ChangeDutyCycle(0)
cv.imshow("capture", frame)
if cv.waitKey(1) > 0:
break
cap.release()
GPIO.cleanup()
cv.destroyAllWindows()
不足
- 在淘宝25.5买的舵机云台不靠谱,旋转舵机一卡一卡的,日
- 延迟将近1s吧,可能是因为计算代价高?可能是逐帧处理太慢?后续有机会,姿势水平提高了,会继续优化
- 欢迎大神指出问题, 本文仅做参考,作为课程作业的一个记录,也是第一次接触Opencv视觉伺服,挺好玩的。PS.光看Opencv文档就觉得头大哈哈哈:(
其他参考资料
3.http://www.360doc.com/content/19/0516/09/63281354_836028944.shtml