【电赛学习笔记】MaxiCAM 项目实践——二维云台追踪指定目标

发布于:2025-07-27 ⋅ 阅读:(15) ⋅ 点赞:(0)

前言

本文是对视觉模块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 句话就能不迷路:

  1. 类里定义的变量分 类变量实例变量

  2. 函数参数和返回值可以写“类型注解”,但运行时不强制检查。

  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 小车比喻
(把“色块追踪”想成“让小汽车自动开到路中间”)

────────────────────

  1. 先认识三个字母
    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. 调参口诀(小白速成)

  1. 先把 KD 设为 0,只调 KP:

    • 车慢 → 增大 KP

    • 车抖动 → 减小 KP

  2. 再加 KD:

    • 车冲到中线停不下来 → 增大 KD

    • 车变得迟钝 → 减小 KD

  3. 如果静止时总有固定误差,再加一点 KI(本例不需要)。

一句话总结
P 管“现在有多偏”,D 管“偏得有多快”,I 管“长期小偏差”,三兄弟一起用力,就能把色块牢牢地锁在画面正中央!


网站公告

今日签到

点亮在社区的每一天
去签到