树莓派4B基于Opencv(人)猫脸追踪云台

前言:本文实现了基于树莓派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 具备训练器和检测器,也可以训练自己的对象分类器,如汽车、飞机等
参考:

  1. http://www.elecfans.com/d/643378.html
  2. https://gitee.com/BA_Figure/faceai/blob/master/doc/detectionOpenCV.md?_from=gitee_search

2. 目标检测流程

利用Opencv的Python接口实现猫脸检测流程如下:
树莓派Opencv库安装参考(无需编译):https://www.jianshu.com/p/8fc2424d5d6c

  1. 读取图片
  2. 将图片转换为灰度格式,便于猫脸检测
  3. 利用训练好的Haar特征检测图片中的猫脸
  4. 绘制猫脸的矩形区域
  5. 显示猫脸检测后的图片

参考:
https://blog.csdn.net/bf02jgtrs00xktcx/article/details/84076390

3. 舵机控制

舵机的控制基于PWM波,手头上的舵机控制周期为20ms(50Hz),发送不同占空比的脉冲能够使舵机转动一定的角度。测试过后我手上的舵机转动0°-180°对应占空比为2.5%-13%(该数据需要自己测试下,见参考资料)

实现伺服的原理就是根据检测到的猫脸矩形框中心与图片像素中心的X,Y差值,计算得到对应的舵机占空比的变化

图片.png

参考:
https://shumeipai.nxez.com/2018/06/21/pan-tilt-multi-servo-control.html

系统概览

成品图.jpg

特别注意: 我这里摄像头是正放的,排线朝下。实际上排线朝上比较合适点,那样的话需要将读取的图像旋转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文档就觉得头大哈哈哈:(

其他参考资料

  1. https://www.freesion.com/article/564916506/

  2. https://gitee.com/BA_Figure/faceai/blob/master/doc/detectionOpenCV.md?_from=gitee_search

3.http://www.360doc.com/content/19/0516/09/63281354_836028944.shtml

  1. https://www.pythonf.cn/read/98144

  2. https://www.cnblogs.com/xiaowuyi/p/4319699.html

6.https://www.jianshu.com/p/8fc2424d5d6c

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355