【电赛学习笔记】MaxiCAM 项目实践——二维云台追踪指定目标
前言
本文是对视觉模块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 angleangle = 0 if angle < 0 else angleif 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 angleangle = 0 if angle < 0 else angleself.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_posdelta_x_pos = KD * (err_x_pos - last_err_x_pos) + KP * err_x_poslast_err_x_pos = err_x_postarget_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_posdelta_y_pos = KD * (err_y_pos - last_err_y_pos) + KP * err_y_poslast_err_y_pos = err_y_postarget_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 管“长期小偏差”,三兄弟一起用力,就能把色块牢牢地锁在画面正中央!