前言
本文是对视觉模块MaixCam实现二维云台人脸跟踪_哔哩哔哩_bilibili大佬的项目实践整理与拓展,侵权即删。
单路舵机基本控制
#导入必要模块
from maix import pwm, time , pinmap
#定义全局变量,设初值
SERVO_FREQ = 50 #主频
SERVO_MIN_DUTY = 2.5 #最小角度占空比
SERVO_MAX_DUTY = 12.5 #最大角度占空比
#选择pwm通道
pwm_id = 7
#引脚功能映射
pinmap.set_pin_function("A19", "PWM7")
#定义角度设置函数
def angle_to_duty(angle):
return (SERVO_MAX_DUTY - SERVO_MIN_DUTY) / 180 * angle + SERVO_MIN_DUTY #固定公式无需记忆
#创建PWM对象
out = pwm.PWM(pwm_id, freq = SERVO_FREQ, duty = angle_to_duty(0), enable = True)
for i in range(180):
out.duty(angle_to_duty(i))
time.sleep_ms(10)
上述代码实现了舵机从0°到180°的运动
舵机类的定义
class Servo:
#设置属性
SERVO_FREQ = 50 #主频
SERVO_MIN_DUTY = 2.5 #最小角度占空比
SERVO_MAX_DUTY = 12.5 #最大角度占空比
SERVO_MAX_ANGLE = 180 #最大旋转角
#初始化函数
def __init__(self, pwm_id:int, angle:int) -> None:
angle = Servo.SERVO_MAX_ANGLE if angle > Servo.SERVO_MAX_ANGLE else angle
angle = 0 if angle < 0 else angle
if pwm_id == 7:
pinmap.set_pin_function("A19", "PWM7")
self.pwm = pwm.PWM(pwm_id, freq = Servo.SERVO_FREQ, duty = self._angle_to_duty_(angle), enable = True)
elif pwm_id == 6:
pinmap.set_pin_function("A18", "PWM6")
self.pwm = pwm.PWM(pwm_id, freq = Servo.SERVO_FREQ, duty = self._angle_to_duty_(angle), enable = True)
elif pwm_id == 5:
pinmap.set_pin_function("A17", "PWM5")
self.pwm = pwm.PWM(pwm_id, freq = Servo.SERVO_FREQ, duty = self._angle_to_duty_(angle), enable = True)
elif pwm_id == 4:
pinmap.set_pin_function("A16", "PWM4")
self.pwm = pwm.PWM(pwm_id, freq = Servo.SERVO_FREQ, duty = self._angle_to_duty_(angle), enable = True)
def __del__(self) -> None :
self.pwm.disable()
def _angle_to_duty_(self,angle:int) -> float :
return (Servo.SERVO_MAX_DUTY - Servo.SERVO_MIN_DUTY) / 180 * angle + Servo.SERVO_MIN_DUTY
def angle(self, angle:int) -> None:
angle = Servo.SERVO_MAX_ANGLE if angle > Servo.SERVO_MAX_ANGLE else angle
angle = 0 if angle < 0 else angle
self.pwm.duty(self._angle_to_duty_(angle))
关于Python中“类”的简介
下面以这一段 Servo 舵机控制类 为例子,把 Python 中“类的定义规则、各参数/变量的作用域与访问规则” 逐条拆开讲清。只要记住 3 句话就能不迷路:
类里定义的变量分 类变量 和 实例变量。
函数参数和返回值可以写“类型注解”,但运行时不强制检查。
带
self.
的是实例自己的;不带的是类或局部临时的。
一、类的“壳子”怎么写
class Servo:
...
class
关键字 + 类名(首字母大写,PEP8 规范)。冒号后缩进 4 空格,内部放 类变量、方法。
二、类变量(Class Variables)
SERVO_FREQ = 50
SERVO_MIN_DUTY = 2.5
SERVO_MAX_DUTY = 12.5
SERVO_MAX_ANGLE = 180
写在类体里、任何方法外。
所有实例共享同一份;通过
类名.变量
或实例.变量
都能读Servo.SERVO_MAX_ANGLE # 推荐 my_servo.SERVO_MAX_ANGLE
如果某个实例想“私自”改值,会变成该实例自己的同名属性,不会动到类变量。
三、实例变量(Instance Variables)
实例变量在 __init__
里用 self.名字 = ...
绑定:
self.pwm = pwm.PWM(...)
每个对象各有一份,生命周期随对象。
访问必须通过实例:
my_servo.pwm
四、构造函数 __init__
def __init__(self, pwm_id: int, angle: int) -> None:
位置 | 含义 |
---|---|
self |
固定第 1 参数,指向当前正在创建的对象本身。 |
pwm_id: int |
形参 + 类型注解(告诉人/IDE 该传 int)。 |
angle: int |
同上。 |
-> None |
返回值注解:构造函数固定返回 None 。 |
五、形参、局部变量、类变量的区分示例
angle = Servo.SERVO_MAX_ANGLE if angle > Servo.SERVO_MAX_ANGLE else angle
左边
angle
是 局部变量(形参名被重新绑定)。Servo.SERVO_MAX_ANGLE
是 类变量。没有
self.
前缀,所以不会存成实例属性。
六、私有“工具函数”的命名惯例
def _angle_to_duty_(self, angle: int) -> float:
单下划线开头
_name
表示“内部使用”,Python 不会强制隐藏,仅提示程序员。带
self
→ 实例方法,能访问实例变量self.pwm
。angle: int -> float
再次使用类型注解。
七、析构函数 __del__
def __del__(self) -> None:
self.pwm.disable()
对象被垃圾回收前自动调用;常用于释放硬件资源。
同样带
self
,但不建议依赖它做关键清理,CPython 不保证时机。
八、实例方法 angle
def angle(self, angle: int) -> None:
调用方式:
servo.angle(90)
内部通过
self.pwm.duty(...)
修改实例自己的 PWM。
九、变量/属性的完整访问路径总结
写法 | 指向 |
---|---|
Servo.SERVO_FREQ |
类变量(所有实例共享) |
self.pwm |
实例变量(当前对象私有) |
angle (无前缀) |
局部变量(函数内临时) |
十、快速记忆表
概念 | 定义位置 | 访问方式 | 生命周期 |
---|---|---|---|
类变量 | 类体,方法外 | 类.变量 / 实例.变量 | 随类 |
实例变量 | __init__ 里用 self. |
实例.变量 | 随实例 |
形参/局部变量 | 函数参数或内部 | 直接变量名 | 函数调用期间 |
照以上规则,你就能看懂并写出任何类似的 Python 类。
项目实战——二位云台色块追踪
from maix import camera, display, image, app
import servo
### 初始化 ###
# 舵机初始角度
INIT_POS_X = 90
INIT_POS_Y = 100
# 滤波系数(越小越平滑,响应越慢)
FILTER_FACTOR = 0.15
# PID 系数(已调好,可微调)
KP = 0.018
KD = 0.20
# 摄像头与显示
cam = camera.Camera(320, 240) # 分辨率可改,但需与后续一致
dis = display.Display()
# 舵机(PWM6→水平,PWM7→垂直)
servo_x = servo.Servo(6, INIT_POS_X)
servo_y = servo.Servo(7, INIT_POS_Y)
# 目标角度初值
target_x_pos = INIT_POS_X
target_y_pos = INIT_POS_Y
last_err_x_pos = 0
last_err_y_pos = 0
# 图像中心
IMAGE_WIDTH = 320
IMAGE_HEIGHT = 240
# 红色色块的 LAB 阈值(需根据实际环境调整)
# 格式:(L_min, L_max, A_min, A_max, B_min, B_max)
color_threshold = [(0, 80, 30, 70, 10, 60)]
while not app.need_exit():
img = cam.read()
# 查找色块:merge=True 合并相邻块,pixels_threshold 过滤小面积
blobs = img.find_blobs(color_threshold, merge=True, pixels_threshold=300)
if not blobs: # 没检测到
dis.show(img)
continue
# 取最大色块作为目标
blob = max(blobs, key=lambda b: b.pixels())
# 画框和中心十字
img.draw_rect(blob.x(), blob.y(), blob.w(), blob.h(), color=image.COLOR_GREEN)
img.draw_cross(blob.cx(), blob.cy(), color=image.COLOR_RED, size=5)
# ---------- 横向 PID ----------
err_x_pos = IMAGE_WIDTH / 2 - blob.cx()
err_x_pos = FILTER_FACTOR * err_x_pos + (1 - FILTER_FACTOR) * last_err_x_pos
delta_x_pos = KD * (err_x_pos - last_err_x_pos) + KP * err_x_pos
last_err_x_pos = err_x_pos
target_x_pos += delta_x_pos
# ---------- 纵向 PID ----------
err_y_pos = IMAGE_HEIGHT / 2 - blob.cy()
err_y_pos = FILTER_FACTOR * err_y_pos + (1 - FILTER_FACTOR) * last_err_y_pos
delta_y_pos = KD * (err_y_pos - last_err_y_pos) + KP * err_y_pos
last_err_y_pos = err_y_pos
target_y_pos += delta_y_pos
# 舵机角度限幅(0°~180°)
target_x_pos = max(0, min(180, target_x_pos))
target_y_pos = max(0, min(180, target_y_pos))
# 驱动舵机
servo_x.angle(int(target_x_pos))
servo_y.angle(int(target_y_pos))
dis.show(img)
PID部分解释
零基础也能听懂的 PID 小车比喻
(把“色块追踪”想成“让小汽车自动开到路中间”)
────────────────────
先认识三个字母
P —— Proportional 比例
I —— Integral 积分
D —— Derivative 微分
(先不用背英文,记住它们各自干的事就行)
────────────────────
2. 把问题换成生活例子
• 你坐在一辆玩具小汽车里,车要停在一条长路的正中间。
• 你每隔 1 秒钟往窗外看一眼,测一下“车身离中线的距离”(这个距离就是误差 err)。
• 每一次看完,你就给方向盘一个“修正量”(delta),让车往中线靠。
PID 就是决定“修正量”的三兄弟。
────────────────────
3. 三兄弟分别做什么?
① 大哥 P(比例):
“离得越远,打得越猛!”
公式:P 部分 = KP × err
• KP 是“比例系数”,像方向盘灵敏度。
• 如果 KP 太小,车慢吞吞;KP 太大,车猛冲过头。
② 二哥 D(微分):
“快撞线了,赶紧松手!”
公式:D 部分 = KD × (err − last_err)
• 只看“误差变化的速度”。
• 当车快速接近中线时,D 会反向拉一把,避免冲过头。
• 相当于“阻尼”,让车不晃。
③ 小弟 I(积分):
“怎么老差一点?慢慢加把劲!”
• 把历史上的误差都加起来,再乘一个系数 KI。
• 对小误差做长期“补偿”。
• 本例为了简单,把 I 关掉(KI=0),所以代码里只有 P 和 D。
────────────────────
4. 代码逐句翻译
以横向为例:
err_x_pos = IMAGE_WIDTH/2 - blob.cx()
→ 看一眼:色块中心离画面中心有多少像素。
err_x_pos = FILTER_FACTOR*err_x_pos + (1-FILTER_FACTOR)*last_err_x_pos
→ 先做个“小滤波”,让测量值别太跳(和 PID 无关,只是让数据平滑)。
delta_x_pos = KD*(err_x_pos - last_err_x_pos) + KP*err_x_pos
→ 把 P 和 D 两个修正量合在一起:
• KPerr_x_pos → 大哥 P:离得多就转得多。
• KD(err-last) → 二哥 D:如果误差变化很快,就减速。
last_err_x_pos = err_x_pos
→ 把这次误差存起来,下次算 D 时用。
target_x_pos += delta_x_pos
→ 方向盘最终转角 = 上次转角 + 本次修正量。
纵向同理,只是换了一个方向。
────────────────────
5. 调参口诀(小白速成)
先把 KD 设为 0,只调 KP:
车慢 → 增大 KP
车抖动 → 减小 KP
再加 KD:
车冲到中线停不下来 → 增大 KD
车变得迟钝 → 减小 KD
如果静止时总有固定误差,再加一点 KI(本例不需要)。
一句话总结
P 管“现在有多偏”,D 管“偏得有多快”,I 管“长期小偏差”,三兄弟一起用力,就能把色块牢牢地锁在画面正中央!