- 作者:
- 分类:项目&制作->小制作
- 阅读:3809
- 点赞:98
- 版权:CC BY-SA 4.0
- 创建:2020-08-20
- 更新:2020-08-31
在 K210 上使用MaixPy 编写一个简单的云台程序, 实现人脸跟随
版权声明:本文为 neucrack 的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接(持续更新):https://neucrack.com/p/308
原文链接(持续更新):https://neucrack.com/p/308
效果视频
硬件
- 可以运行 MaixPy 的 k210 开发板(此处用了maixbit 开发板)
- 屏幕,是买 maixbit 套餐里面带的,屏幕排线容易折断,要小心使用
- 两轴舵机,扭力尽量大一点,会更稳,上面视频使用的 20KG 扭力的舵机, 参数:
- 频率: 50 (Hz)
- 周期 T: 1/50 = 0.02s = 20ms
- 占空比 duty: [0.5ms, 2.5ms] -> [0.025, 0.125] -> [2.5%, 12.5%]
- 硬件连接
- IO24 <—> pitch(舵机俯仰轴)
- IO25 <—> roll (舵机横滚轴,这里实际是控制的航向轴)
原理
- 使用人脸检测模型来检测人脸,获取到人脸的中心坐标。
- 将当前的坐标与中心点坐标进行对比,获得当前与目标的差值。
- 根据这个与目标的差值,k210 驱动舵机给舵机一定的驱动量,使其将摄像头转动,达到中心对准人头。
这里的难点在第三步,根据与目标位置的差值如何驱动舵机转动使摄像头中心对准人头:
- 如果没有任何控制理论的同学会想,用判断语句,一点一点移动就好了。但这样看起来舵机非常僵硬,一卡一卡的。
- 稍微有点控制理论基础的应该都知道,这种情况使用 经典 PID 控制即可,简单地说就是根据误差值,通过一个带有三个参数(P I D)的公式得到一个输出值,将这个值作为增量输出到舵机控制舵机转动,比如公式这样:
这里的 Kp Tt Td 就是 PID 参数,看起来很复杂,实际可以先从简单的 只用 P 控制(其它两个值为0)的情况做起就比较简单,即输出增量= 和目标的差值 * Kp
。I是积分即把每次的误差累积起来,对于小误差很有用,随着时间累积会放大误差作用,比如离目标差几个像素,但是输出量太小不能让舵机转动,随着时间累积,输出量会越来越大直到让舵机动起来;D是微分,可以防止过冲,这里是将本次的误差和上次的误差变化参与输出值的计算,来让转动更加平滑。
这里只是大概解释一下,具体可以自行搜索 经典PID控制 学习,结合看代码就很好懂。
实现
- 先学会如何在 k210(maixbit) 上使用 maixpy 开发,可以看官方文档,或者是我的另一篇快速入门教程
- 使用人脸检测模型, 可以在模型库下载到
- 使用 kflash_gui 烧录模型到
0x300000
处 - 运行下面的代码即可
源代码
代码在这里: https://github.com/sipeed/MaixPy_scripts/tree/master/application/gimbal
‘’‘
@author neucrack
@license MIT
’‘’
import time, sys
from machine import Timer,PWM
from fpioa_manager import board_info
from math import pi
from machine import UART
class Servo:
def __init__(self, pwm, dir=50, duty_min=2.5, duty_max=12.5):
self.value = dir
self.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, inc):
self.value += inc
if self.value > 100:
self.value = 100
elif self.value < 0:
self.value = 0
self.pwm.duty(self.value/100*self.duty_range+self.duty_min)
class PID:
_kp = _ki = _kd = _integrator = _imax = 0
_last_error = _last_t = 0
_RC = 1/(2 * pi * 20)
def __init__(self, p=0, i=0, d=0, imax=0):
self._kp = float(p)
self._ki = float(i)
self._kd = float(d)
self._imax = abs(imax)
self._last_derivative = None
def get_pid(self, error, scaler):
tnow = time.ticks_ms()
dt = tnow - self._last_t
output = 0
if self._last_t == 0 or dt > 1000:
dt = 0
self.reset_I()
self._last_t = tnow
delta_time = float(dt) / float(1000)
output += error * self._kp
if abs(self._kd) > 0 and dt > 0:
if self._last_derivative == None:
derivative = 0
self._last_derivative = 0
else:
derivative = (error - self._last_error) / delta_time
derivative = self._last_derivative + \
((delta_time / (self._RC + delta_time)) * \
(derivative - self._last_derivative))
self._last_error = error
self._last_derivative = derivative
output += self._kd * derivative
output *= scaler
if abs(self._ki) > 0 and dt > 0:
self._integrator += (error * self._ki) * scaler * delta_time
if self._integrator < -self._imax: self._integrator = -self._imax
elif self._integrator > self._imax: self._integrator = self._imax
output += self._integrator
return output
def reset_I(self):
self._integrator = 0
self._last_derivative = None
class Gimbal:
def __init__(self, pitch, pid_pitch, roll=None, pid_roll=None, 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=50, yaw_err=50, pitch_reverse=False, roll_reverse=False, yaw_reverse=False):
out = self._pid_pitch.get_pid(pitch_err, 1)
# print("err: {}, out: {}".format(pitch_err, out))
if pitch_reverse:
out = - out
self._pitch.drive(out)
if self._roll:
out = self._pid_roll.get_pid(roll_err, 1)
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)
if __name__ == "__main__":
'''
servo:
freq: 50 (Hz)
T: 1/50 = 0.02s = 20ms
duty: [0.5ms, 2.5ms] -> [0.025, 0.125] -> [2.5%, 12.5%]
pin:
IO24 <--> pitch
IO25 <--> roll
'''
init_pitch = 80 # init position, value: [0, 100], means minimum angle to maxmum angle of servo
init_roll = 50 # 50 means middle
sensor_hmirror = False
sensor_vflip = False
lcd_rotation = 2
lcd_mirror = True
pitch_pid = [0.23, 0, 0.015, 0] # P I D I_max
roll_pid = [0.23, 0, 0.015, 0] # P I D I_max
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 = False # reverse out value direction
roll_reverse = True # ..
import sensor,image,lcd
import KPU as kpu
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
self.task_fd = kpu.load(0x300000) # face model addr in flash
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_fd, 0.5, 0.3, 5, anchor)
lcd.init()
lcd.rotation(lcd_rotation)
lcd.mirror(lcd_mirror)
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
if hmirror:
sensor.set_hmirror(1)
if vflip:
sensor.set_vflip(1)
def get_target_err(self):
img = sensor.snapshot()
code = kpu.run_yolo2(self.task_fd, img)
if code:
max_area = 0
max_i = 0
for i, j in enumerate(code):
a = j.w()*j.h()
if a > max_area:
max_i = i
max_area = a
img = img.draw_rectangle(code[max_i].rect())
self.pitch = (code[max_i].y() + code[max_i].h() / 2)/240*self.out_range*2 - self.out_range
self.roll = (code[max_i].x() + code[max_i].w() / 2)/320*self.out_range*2 - self.out_range
# limit
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(160, 120)
lcd.display(img)
return (self.pitch, self.roll)
else:
img = img.draw_cross(160, 120)
lcd.display(img)
return (0, 0)
target = Target(target_err_range, target_ignore_limit, sensor_hmirror, sensor_vflip, lcd_rotation, lcd_mirror)
tim0 = Timer(Timer.TIMER0, Timer.CHANNEL0, mode=Timer.MODE_PWM)
tim1 = Timer(Timer.TIMER0, Timer.CHANNEL1, mode=Timer.MODE_PWM)
pitch_pwm = PWM(tim0, freq=50, duty=0, pin=24)
roll_pwm = PWM(tim1, freq=50, duty=0, pin=25)
pitch = Servo(pitch_pwm, dir=init_pitch)
roll = Servo(roll_pwm, dir=init_roll)
pid_pitch = PID(p=pitch_pid[0], i=pitch_pid[1], d=pitch_pid[2], imax=pitch_pid[3])
pid_roll = PID(p=roll_pid[0], i=roll_pid[1], d=roll_pid[2], imax=roll_pid[3])
gimbal = Gimbal(pitch, pid_pitch, roll, pid_roll)
target_pitch = init_pitch
target_roll = init_roll
t = time.ticks_ms()
_dir = 0
t0 = time.ticks_ms()
stdin = UART.repl_uart()
while 1:
# get target error
err_pitch, err_roll = target.get_target_err()
# interval limit to > 10ms
if time.ticks_ms() - t0 < 10:
continue
t0 = time.ticks_ms()
# run
gimbal.run(err_pitch, err_roll, pitch_reverse = pitch_reverse, roll_reverse=roll_reverse)