准备

硬件

开发板:m1w dock、esp32
banner-2.jpg
云台:链接
舵机:链接
语音控制、电源管理、舵机驱动电路设计:链接
微信截图_20220331153904.png
微信截图_20220331153750.png
微信截图_20220331153758.png
微信截图_20220331153715.png
实在是懒得动了,esp32最小系统电路就不自己画了,真的难受,直接用最小系统板,这样也好,可以重复使用。

软件及程序代码

烧录工具:kflash、flash_download_tool_3.9.2
参考麻省理工maixpy例程:github链接(开vpn或ssr)
人脸识别模型训练:链接
IDE:Mixly4MixNo
Arduino
esp32的blinker固件:github链接(开vpn或ssr)

Linux环境配置(也可选用windows跳过此步骤)

测试linux环境Ubuntu 18.04.6 LTS
1.下载Mixly2.0 for Linux版本,不同的版本名字可能不同,这里是Mixly2.0-rc0-Linux-x64.7z
2.直接复制到桌面,然后解压缩
3.安装python3,如果没有的话

sudo apt install python3 python3-pip python3-serial esptool
pip3 install pyserial
sudo apt install  python-is-python3 //报错就安装下面的
sudo apt install  python python-pip
pip install pyserial

4.修改串口权限

sudo chmod +666 /dev/tty* 这句话每插入一个新的硬件就要执行一次

4.进入桌面后尝试运行Mixly,应该可以正常打开
(下面为AI库)
安装python扩展库

sudo apt-get install build-essential gtk2-engines-pixbuf libasound-dev portaudio19-dev libportaudio2 libportaudiocpp0  python3-tk python3-pip libcanberra-gtk-module
pip3 install pyaudio  opencv-python  baidu-aip matplotlib pandas numpy  scikit-learn

4.挨个测试
Arduino AVR
使用 Arduino Nano328 old bootloader 可以编译,可以上载
Arduino ESP32
使用 Mixgo 可以编译 可以上载
Arduino ESP32-C3
使用 淘宝一开发板 可以编译 可以上载
Arduino ESP32-S2
使用 Mixgo CE 可以编译 可以上载
micropython:microbit 可以上载

circuitpython:mixgoce 可以初始化 可以上载
micropython:mixgoAI 可以初始化 可以上载
micropython:mixgo 可以初始化 可以上载

跟踪原理

我们的摄像头的帧大小是320 * 240,我们需要对检测的摄像头检测到不同位置的脸,采取不同的操作:看下图:
20200428151856879.png
如果人脸的中心x位置小于150,让水平舵机 变量X轴 = X轴 -2,如果人脸中心的x位置大于170,让水平舵机 变量Y轴 = Y轴 + 2.如果人脸中心的Y位置小于110,让水平舵机 变量Y轴 = Y轴 +2,如果人脸中心的Y位置大于130,让水平舵机 变量Y轴 = Y轴 - 2,再结合舵机角度的范围进行判断。

注:

1、由于人距离摄像的远近会改变人脸中心,x,y的变化,为了更好的演示效果,我将检测到的人脸大小規定在一定的範圍内。
2、可以加入PID算法,让舵机转动更加精准。

舵机测试代码

'''
实验名称:舵机控制(Servo Control)
版本:v1.0
日期:2022.3.24
作者:zhoujiahao 【zhoujiahao.top】
说明:通过编程控制舵机旋转到不同角度
'''

from machine import Timer,PWM
import time

#PWM通过定时器配置,接到IO17引脚(Pin IO17)
tim = Timer(Timer.TIMER0, Timer.CHANNEL0, mode=Timer.MODE_PWM)
S1 = PWM(tim, freq=50, duty=0, pin=17)

'''
说明:舵机控制函数
功能:180度舵机:angle:-90至90 表示相应的角度
     360连续旋转度舵机:angle:-90至90 旋转方向和速度值。
    【duty】占空比值:0-100
'''

def Servo(servo,angle):
    S1.duty((angle+90)/180*10+2.5)


while True:
    #-90度
    Servo(S1,-90)
    time.sleep(1)

    #-45度
    Servo(S1,-45)
    time.sleep(1)

    #0度
    Servo(S1,0)
    time.sleep(1)

    #45度
    Servo(S1,45)
    time.sleep(1)

    #90度
    Servo(S1,90)
    time.sleep(1)

人脸跟踪测试程序

在官方代码上改了pd代码,只是自己用的习惯,效果没有进行比较过。
然后加了点注释,看起来应该会更容易一点

from machine import Timer,PWM
import sensor, image, time, lcd
from fpioa_manager import  fm
from Maix import GPIO
import KPU as kpu

"""位置式PD的类"""
class PID:
    def __init__(self,minout,maxout,intergral_limit, kp, ki, kd):
        self.p= kp
        self.i = ki
        self.d = kd
        self.Minoutput = minout
        self.MaxOutput = maxout
        self.IntegralLimit = intergral_limit
        self.pout=0
        self.iout=0
        self.dout=0
        self.delta_u=0
        self.delta_out=0
        self.last_delta_out=0
        self._set=[0,0,0]
        self._get=[0,0,0]
        self._err=[0,0,0]


    def pid_calc(self, _get, _set) :
        #self._get[2] = _get
        #self._set[2] = _set
        self._err[2] = _set - _get

        self.pout =self.p * self._err[2]
        #self.iout = self.i * self._err[2]
        self.dout = self.d * (self._err[2] - self._err[1])
        #print("pout: {}, dout: {}".format(self.pout , self.dout ))
        #if self.iout>=self.IntegralLimit:
           # self.iout=self.IntegralLimit
        #if self.iout<-self.IntegralLimit:
           # self.iout=-self.IntergralLimit

        self.delta_out = self.pout + self.iout +self.dout
        #self.delta_out = self.last_delta_out + self.delta_u

        #if self.delta_out>self.MaxOutput:
         #  self.delta_out=self.MaxOutput
        #if self.delta_out<self.Minoutput:
         #  self.delta_out=self.Minoutput

       # self.last_delta_out = self.delta_out
        self._err[0] = self._err[1]
        self._err[1] = self._err[2]
       # self._get[0] = self._get[1]
        #self._get[1] = self._get[2]
        #self._set[0] = self._set[1]
        #self._set[1] = self._set[2]
        return  self.delta_out

"""关于舵机的类"""
class SERVO:
    def __init__(self, pwm, dir=50, duty_min=2.5, duty_max=12.5):
        self.value = dir        #【0~100】
        self.pwm = pwm          #pwm的类
        self.duty_min = duty_min
        self.duty_max = duty_max
        self.duty_range = duty_max -duty_min
        self.enable(True)
        self.pwm.duty(self.value/100*self.duty_range+self.duty_min)

    def enable(self, en):        #使能
        if en:
            self.pwm.enable()
        else:
            self.pwm.disable()

    def dir(self, percentage):     #手动控制角度调用
        if percentage > 100:
            percentage = 100
        elif percentage < 0:
            percentage = 0
        self.pwm.duty(percentage/100*self.duty_range+self.duty_min)

    def drive(self, add):          #连续控制角度调用
        print("value:",self.value,"add:",add)
        self.value = self.value+add
        if self.value > 100:
            self.value = 100
        elif self.value < 0:
            self.value = 0
        #print("value:",self.value,"add:",add)
        self.pwm.duty(self.value/100*self.duty_range+self.duty_min)

#人脸目标类
class TARGET():
    def __init__(self, out_range=10, ignore_limit=0.02, hmirror=False, vflip=False, lcd_rotation=2, lcd_mirror=True):

        self.pitch = 0
        self.roll = 0
        self.out_range = out_range
        self.ignore = ignore_limit

          #摄像头初始化参数配置
        sensor.reset()
        sensor.set_pixformat(sensor.RGB565)
        sensor.set_framesize(sensor.QVGA)
        if hmirror:
            sensor.set_hmirror(1)
        if vflip:
            sensor.set_vflip(1)

        #lcd初始化配置
        lcd.init(freq=15000000)
        lcd.clear(lcd.WHITE)
        #lcd.rotation(lcd_rotation)
        #lcd.mirror(lcd_mirror)

        #人脸检测模型加载,参数配置和初始化
        self.task = kpu.load(0x300000)
        anchor = (1.889, 2.5245, 2.9465, 3.94056, 3.99987, 5.3658, 5.155437, 6.92275, 6.718375, 9.01025)
        kpu.init_yolo2(self.task, 0.5, 0.3, 5, anchor)

    #获得误差
    def get_target_err(self):
        img = sensor.snapshot()
        objects = kpu.run_yolo2(self.task, img)
        if objects:
            max_area = 0
            max_i = 0
            for i, j in enumerate(objects):#查找最大面积最大的对象 i为索引值
                a = j.w()*j.h()
                if a > max_area:
                    max_i = i
                    max_area = a

            img = img.draw_rectangle(objects[max_i].rect())
            core_y=objects[max_i].y() + objects[max_i].h() / 2
            core_x=objects[max_i].x() + objects[max_i].w() / 2
           #将误差缩放到这个范围【-10,10】
            self.pitch = core_y/240*self.out_range*2 - self.out_range
            self.roll = core_x/320*self.out_range*2 - self.out_range

            # 在这个范围【-0.2,0.2】,则认为在中心
            if abs(self.pitch) < self.out_range*self.ignore:
                self.pitch = 0
            if abs(self.roll) < self.out_range*self.ignore:
                self.roll = 0
            img = img.draw_cross(int(core_x), int(core_y))
            lcd.display(img)
            return (self.pitch, self.roll)
        else:
            img = img.draw_cross(160, 120)
            lcd.display(img)
            return (0, 0)              #               没找到人 就返回在中央坐标
class GIMBAL:
    def __init__(self, pitch, pid_pitch, roll, pid_roll, yaw=None, pid_yaw=None):
        self._pitch = pitch
        self._roll = roll
        #self._yaw = yaw
        self._pid_pitch = pid_pitch
        self._pid_roll = pid_roll
        #self._pid_yaw = pid_yaw

    def set_out(self, pitch, roll, yaw=None):
        pass

    def run(self, pitch_err, roll_err, yaw_err=None, pitch_reverse=False, roll_reverse=False, yaw_reverse=False):
        out = self._pid_pitch.pid_calc(pitch_err, 0)
        #print("pitch_err: {}, out: {}".format(pitch_err, out))

        if pitch_reverse:
            out = - out
        self._pitch.drive(out)

        out = self._pid_roll.pid_calc(roll_err, 0)
       # print("roll_err: {}, out: {}".format(roll_err, out))
        if roll_reverse:
            out = - out
        self._roll.drive(out)

            #if self._yaw:
            #out = self._pid_yaw.get_pid(yaw_err, 1)
            #if yaw_reverse:
                #out = - out
            #self._yaw.drive(out)

#摄像头和lcd的反转还是不反转根据实际情况设定


#c参数设置
init_pitch = 60       # init position, value: [0, 100], means minimum angle to maxmum angle of servo
init_roll = 50        # 50 means middle
sensor_hmirror = False    #水平反转
sensor_vflip = True        #垂直反转
lcd_rotation = 2            #旋转180度
lcd_mirror = True             #水平反转
pitch_pid = [0,100,0,0.8, 0, 0.5]    # min max I_max P I D  #0,100,0,0.23, 0, 0.015
roll_pid  = [0,100,0,0.8, 0, 0.5]    # min max I_max P I D
target_err_range = 10            # target error output range, default [0, 10]
target_ignore_limit = 0.02       # when target error < target_err_range*target_ignore_limit , set target error to 0
pitch_reverse = True # reverse out value direction
roll_reverse = True   # ..

#pwm初始化
pitch_pin_1=24
roll_pin_2=25
tim0= Timer(Timer.TIMER0, Timer.CHANNEL0, mode=Timer.MODE_PWM)
pitch_pwm = PWM(tim0, freq=50, duty=0, pin=pitch_pin_1)
tim2 = Timer(Timer.TIMER0, Timer.CHANNEL1, mode=Timer.MODE_PWM)
roll_pwm = PWM(tim2, freq=50, duty=0, pin=roll_pin_2)

#舵机初始化
pitch = SERVO(pitch_pwm, dir=init_pitch)
roll = SERVO(roll_pwm, dir=init_roll)

#pid初始化
pid_pitch = PID(minout=pitch_pid[0],maxout=pitch_pid[1],intergral_limit=pitch_pid[2],kp=pitch_pid[3], ki=pitch_pid[4], kd=pitch_pid[5] )
pid_roll = PID(minout=roll_pid[0],maxout=roll_pid[1],intergral_limit=roll_pid[2],kp=roll_pid[3], ki=roll_pid[4], kd=roll_pid[5])

#云台初始化
gimbal = GIMBAL(pitch, pid_pitch, roll, pid_roll)


#人脸追踪初始化
target = TARGET(target_err_range, target_ignore_limit, sensor_hmirror, sensor_vflip, lcd_rotation, lcd_mirror)
#target_pitch = init_pitch
#target_roll = init_roll

while 1:
    # get target error
    err_pitch, err_roll = target.get_target_err()
    #print("误差值",err_pitch,err_roll) #y x
    # run
    gimbal.run(err_pitch, err_roll, pitch_reverse = pitch_reverse, roll_reverse=roll_reverse)

ESP32的blinker代码和pwm控速代码

#define BLINKER_WIFI 
#define BLINKER_MIOT_LIGHT//小爱支持
#include <Blinker.h>

/******开始定义变量*******/
#define Slider_1 "SliderKey"
#define LED 25 //继电器
char auth[] = "123456789";//wifi密码
char ssid[] = "周嘉浩的iphone";//wifi名称
char pswd[] = "99CB13E55X6X";//blinker密钥

// 新建组件对象(将设备与app界面的ui组件进行绑定)
BlinkerButton Button1("btn-1");//按钮
BlinkerSlider Slider1("ran_1");//滑块

//btn_1按钮子函数
void button1_callback(const String & state) {
    BLINKER_LOG("get button state: ", state);
    digitalWrite(LED, !digitalRead(LED));
    if(state == "on")
    {
      digitalWrite(LED,LOW);
      Button1.print("开机*********");
      }
    else if(state == "off")
    {
      digitalWrite(LED,HIGH);
      Button1.print("关机*********");
    }
    Blinker.vibrate();
}

//slider子函数
void slider1_callback(int32_t value)
{
    //analogWriteResolution(12);//设置分辨率0~20.
    analogWrite(30,value);//pwm控制语法(pin,value),value为占空比0~255.
    BLINKER_LOG("get slider value: ", value);//app显示信息
}

//小爱回调函数(小爱控制命令处理函数)
void miotPowerState(const String & state)
{
    BLINKER_LOG("need set power state: ", state);

    if (state == BLINKER_CMD_ON) {
        digitalWrite(LED_BUILTIN, HIGH);

        BlinkerMIOT.powerState("on");
        BlinkerMIOT.print();
    }
    else if (state == BLINKER_CMD_OFF) {
        digitalWrite(LED_BUILTIN, LOW);

        BlinkerMIOT.powerState("off");
        BlinkerMIOT.print();
    }
}
//主程序开始
void setup() {
    // 初始化串口
    Serial.begin(115200);//设置端口频率
    BLINKER_DEBUG.stream(Serial);//设置下载方式
 
    // 初始化IO
    pinMode(LED, OUTPUT);
    digitalWrite(LED, HIGH);
    pinMode(18, OUTPUT);
    pinMode(19, OUTPUT);

    // 初始化blinker
    Blinker.begin(auth, ssid, pswd);
    Button1.attach(button1_callback);
    Slider1.attach(slider1_callback);

     //小爱同学务必在回调函数中反馈该控制状态 
     BlinkerMIOT.attachPowerState(miotPowerState);//注册回调函数
}
 
void loop() {
    Blinker.run();
}