- 作者:
- 分类:项目&制作->小制作
- 阅读:5433
- 点赞:99
- 版权: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, sysfrom machine import Timer,PWMfrom fpioa_manager import board_infofrom math import pifrom machine import UARTclass Servo:def __init__(self, pwm, dir=50, duty_min=2.5, duty_max=12.5):self.value = dirself.pwm = pwmself.duty_min = duty_minself.duty_max = duty_maxself.duty_range = duty_max -duty_minself.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 = 100elif percentage < 0:percentage = 0self.pwm.duty(percentage/100*self.duty_range+self.duty_min)def drive(self, inc):self.value += incif self.value > 100:self.value = 100elif self.value < 0:self.value = 0self.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 = Nonedef get_pid(self, error, scaler):tnow = time.ticks_ms()dt = tnow - self._last_toutput = 0if self._last_t == 0 or dt > 1000:dt = 0self.reset_I()self._last_t = tnowdelta_time = float(dt) / float(1000)output += error * self._kpif abs(self._kd) > 0 and dt > 0:if self._last_derivative == None:derivative = 0self._last_derivative = 0else:derivative = (error - self._last_error) / delta_timederivative = self._last_derivative + \((delta_time / (self._RC + delta_time)) * \(derivative - self._last_derivative))self._last_error = errorself._last_derivative = derivativeoutput += self._kd * derivativeoutput *= scalerif abs(self._ki) > 0 and dt > 0:self._integrator += (error * self._ki) * scaler * delta_timeif self._integrator < -self._imax: self._integrator = -self._imaxelif self._integrator > self._imax: self._integrator = self._imaxoutput += self._integratorreturn outputdef reset_I(self):self._integrator = 0self._last_derivative = Noneclass Gimbal:def __init__(self, pitch, pid_pitch, roll=None, pid_roll=None, yaw=None, pid_yaw=None):self._pitch = pitchself._roll = rollself._yaw = yawself._pid_pitch = pid_pitchself._pid_roll = pid_rollself._pid_yaw = pid_yawdef set_out(self, pitch, roll, yaw=None):passdef 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 = - outself._pitch.drive(out)if self._roll:out = self._pid_roll.get_pid(roll_err, 1)if roll_reverse:out = - outself._roll.drive(out)if self._yaw:out = self._pid_yaw.get_pid(yaw_err, 1)if yaw_reverse:out = - outself._yaw.drive(out)if __name__ == "__main__":'''servo:freq: 50 (Hz)T: 1/50 = 0.02s = 20msduty: [0.5ms, 2.5ms] -> [0.025, 0.125] -> [2.5%, 12.5%]pin:IO24 <--> pitchIO25 <--> roll'''init_pitch = 80 # init position, value: [0, 100], means minimum angle to maxmum angle of servoinit_roll = 50 # 50 means middlesensor_hmirror = Falsesensor_vflip = Falselcd_rotation = 2lcd_mirror = Truepitch_pid = [0.23, 0, 0.015, 0] # P I D I_maxroll_pid = [0.23, 0, 0.015, 0] # P I D I_maxtarget_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 0pitch_reverse = False # reverse out value directionroll_reverse = True # ..import sensor,image,lcdimport KPU as kpuclass Target():def __init__(self, out_range=10, ignore_limit=0.02, hmirror=False, vflip=False, lcd_rotation=2, lcd_mirror=True):self.pitch = 0self.roll = 0self.out_range = out_rangeself.ignore = ignore_limitself.task_fd = kpu.load(0x300000) # face model addr in flashanchor = (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 = 0max_i = 0for i, j in enumerate(code):a = j.w()*j.h()if a > max_area:max_i = imax_area = aimg = 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_rangeself.roll = (code[max_i].x() + code[max_i].w() / 2)/320*self.out_range*2 - self.out_range# limitif abs(self.pitch) < self.out_range*self.ignore:self.pitch = 0if abs(self.roll) < self.out_range*self.ignore:self.roll = 0img = 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_pitchtarget_roll = init_rollt = time.ticks_ms()_dir = 0t0 = time.ticks_ms()stdin = UART.repl_uart()while 1:# get target errorerr_pitch, err_roll = target.get_target_err()# interval limit to > 10msif time.ticks_ms() - t0 < 10:continuet0 = time.ticks_ms()# rungimbal.run(err_pitch, err_roll, pitch_reverse = pitch_reverse, roll_reverse=roll_reverse)

