前言
本文是對視覺模塊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 管“長期小偏差”,三兄弟一起用力,就能把色塊牢牢地鎖在畫面正中央!