
1. 項目背景與核心價值最近在整理過去的項目資料翻到了2021年參加全國大學生智能汽車競賽我們通常簡稱“國賽”時為F題“送藥小車”編寫的OpenMV巡線代碼。當時為了趕進度代碼寫得比較“飛線”注釋也是寥寥無幾。現在回過頭看雖然比賽已經過去幾年但這段代碼里涉及的圖像處理思路、PID控制邏輯以及嵌入式編程的坑對于剛接觸OpenMV做視覺巡線的新手來說依然有很強的參考價值。特別是看到網上很多同學在搜索“OpenMV巡線代碼注釋”、“國賽送藥小車”這類關鍵詞說明這個需求一直很旺盛但能找到的、帶詳細解說的完整代碼并不多。所以我決定把這段代碼重新拿出來逐行加上詳細的注釋并結合比賽中的實際調試經驗把背后的原理、為什么這么寫、以及調試時踩過的坑都講清楚。這不僅僅是一份代碼注釋更像是一份濃縮了實戰經驗的視覺巡線開發筆記。無論你是正在備戰智能車競賽、電賽還是單純想用OpenMV做個巡線小車相信都能從中獲得直接的幫助。代碼本身是基于OpenMV IDE使用MicroPython開發的核心任務是通過攝像頭識別賽道上的黑色引導線計算出小車偏離中心的誤差然后輸出控制量給底層電機實現自動循跡。2. 代碼整體結構與設計思路拆解2.1 核心需求與方案選型2021年國賽F題“送藥小車”是一個典型的室內場景任務小車需要在模擬醫院走廊的賽道上行駛完成送藥任務。賽道通常由白色底板和黑色引導線組成有時會有十字路口、環島等元素。我們的核心需求是穩定、快速地識別并跟蹤這條黑線。為什么選擇OpenMV而不是其他傳感器當時主要基于幾點考量信息豐富度相比單個紅外對管或灰度傳感器陣列攝像頭能獲取一行甚至一個區域的完整圖像信息不僅能判斷是否壓線還能提前預知彎道曲率實現更前瞻、更平滑的控制。靈活性通過修改圖像處理算法可以相對容易地適應不同光照、賽道材質的變化甚至后期擴展識別路口、數字等任務。開發效率OpenMV集成了MicroPython和豐富的機器視覺庫對于學生團隊來說從獲取圖像到輸出控制量的開發周期比從零搭建一個基于單片機的圖像處理系統要短得多。方案上我們采用了最經典也最可靠的“單行掃描重心法求偏差”策略。即不對整幅圖像進行處理而是只提取圖像中某一行通常是中間偏下的行的像素數據在這一行里尋找黑線所在的區域并計算該區域的中心位置。將這個中心位置與圖像的中心線位置進行比較其差值就是橫向偏差error。這個偏差值隨后被送入PID控制器計算出電機調速的PWM占空比。2.2 代碼文件與模塊規劃我們的主要代碼通常寫在一個main.py文件中這是OpenMV上電后自動執行的文件。代碼結構大致分為以下幾個部分初始化模塊導入必要的庫sensor,image,time,pyb等初始化攝像頭傳感器參數分辨率、像素格式、白平衡等。圖像處理函數核心函數負責獲取一幀圖像提取特定行進行二值化尋找色塊黑線并計算偏差。PID控制模塊包含PID參數和計算函數將圖像處理得到的偏差轉化為控制輸出。電機控制模塊根據PID輸出設置左右電機的PWM信號實現差速轉向。主循環不斷循環執行“圖像采集-處理-計算偏差-PID運算-電機控制”這個流程并可以加入一些調試信息輸出如通過串口發送偏差值、圖像等方便上位機調參。這種模塊化的思想雖然簡單但在緊張的比賽調試中非常有效可以快速定位問題是出在圖像識別不準還是PID參數不對或是電機驅動有問題。3. 核心代碼逐行注釋與原理詳解下面我將以代碼塊的形式展示關鍵代碼并附上詳細的注釋。請注意為了適應文章格式代碼可能不是百分百原樣但核心邏輯和注釋完全一致。3.1 傳感器與攝像頭初始化import sensor, image, time, pyb # 初始化攝像頭傳感器 sensor.reset() # 重置并初始化攝像頭傳感器硬件 sensor.set_pixformat(sensor.GRAYSCALE) # 設置像素格式為灰度圖。巡線通常不需要彩色信息灰度圖處理速度更快內存占用更小。 sensor.set_framesize(sensor.QQVGA) # 設置圖像分辨率為QQVGA (160x120)。分辨率越低處理速度越快但細節越少。QQVGA是巡線常用的平衡點。 sensor.skip_frames(time 2000) # 等待攝像頭設置生效。剛啟動時自動增益、白平衡等需要時間穩定跳過開始的2000幀讓畫面穩定。 sensor.set_auto_gain(False) # 關閉自動增益。自動增益會隨環境光變化調整整體亮度可能導致二值化閾值失效必須關閉 sensor.set_auto_whitebal(False) # 關閉自動白平衡。理由同上保持圖像處理條件穩定。 # 注意有時也需要關閉自動曝光sensor.set_auto_exposure但具體取決于環境光變化是否劇烈。 clock time.clock() # 創建一個時鐘對象用于計算幀率FPS評估算法效率。關鍵點解析與避坑sensor.GRAYSCALE巡線任務中顏色信息往往是干擾。黑色引導線在灰度圖下就是深色區域直接用灰度值閾值分割最簡單可靠。除非賽道是彩色線否則不要用RGB。sensor.QQVGA (160x120)這是一個經驗值。更高的分辨率如QVGA(320x240)會顯著增加處理時間可能無法達到高幀率如30fps以上的要求導致控制延遲大。160x120足夠在大多數場景下清晰分辨黑線。set_auto_gain(False)和set_auto_whitebal(False)這是巡線穩定的生命線如果開啟攝像頭會根據環境自動調整畫面亮度/色溫。早上和下午的光照不同可能導致同一閾值上午能識別黑線下午就識別不了了。固定這些參數意味著我們假設環境光相對穩定或者通過補光如在小車上加LED燈條來創造穩定光照條件。3.2 圖像處理與偏差計算函數這是整個代碼的核心我們將其封裝為一個函數方便調用和調試。def find_line_error(img): 在圖像中尋找黑線并計算橫向偏差。 參數: img: 由 sensor.snapshot() 獲取的圖像對象。 返回: error: 計算出的橫向偏差值單位像素。正數通常表示線在中心右側負數表示在左側。 found: 布爾值是否成功找到黑線。 # 1. 定義感興趣區域(ROI)和掃描行 # 我們不需要處理整個圖像只關心地面附近的區域。ROI可以排除天花板等干擾。 # roi格式: (x, y, width, height) roi (0, 80, 160, 40) # 從y80像素開始高度40像素的區域。這個值需要根據攝像頭安裝高度和角度調整。 scan_line_y roi[1] roi[3] // 2 # 取ROI中間的那一行進行掃描。這里是 y 80 20 100。 # 2. 提取掃描行的像素數據 # 獲取圖像第scan_line_y行的所有像素值灰度值0-255。 line img.get_pixel(scan_line_y, roi[0], roi[2]) # line現在是一個包含160個整數的列表因為寬度是160每個整數代表一個像素的灰度值。 # 3. 二值化與尋找黑線區域 threshold 50 # 二值化閾值。灰度值低于此值的被認為是黑線。需要根據實際賽道顏色和光照手動調整。 black_start None black_end None black_pixels [] # 記錄被認為是黑線的像素的x坐標 for x in range(roi[0], roi[0] roi[2]): # 遍歷ROI寬度內的每一個x坐標 # 注意img.get_pixel(y, x) 返回的是(x,y)坐標的灰度值。我們之前獲取的line是按x順序的。 # 這里我們直接使用line列表索引是 x - roi[0] index x - roi[0] pixel_value line[index] if index len(line) else 255 # 安全訪問 if pixel_value threshold: black_pixels.append(x) # 記錄黑點位置 if black_start is None: black_start x # 記錄黑線區域的起始x坐標 black_end x # 不斷更新最終記錄黑線區域的結束x坐標 # 注意這里沒有做區域合并等復雜處理假設黑線是連續的。 # 4. 計算偏差 error 0 found False img_center_x roi[0] roi[2] // 2 # ROI區域的水平中心x坐標本例中為80。 if black_pixels: # 如果找到了至少一個黑點 found True # 方法1使用黑線區域中心 if black_start is not None and black_end is not None: line_center_x (black_start black_end) // 2 error line_center_x - img_center_x # 偏差 線中心 - 圖像中心 # 方法2使用所有黑像素的平均位置重心法抗干擾更好 # line_center_x sum(black_pixels) // len(black_pixels) # error line_center_x - img_center_x # 在圖像上畫出掃描線和識別到的黑線區域用于調試會消耗時間正式比賽可注釋掉 img.draw_line(roi[0], scan_line_y, roi[0]roi[2], scan_line_y, color127) # 畫掃描線 if black_start and black_end: img.draw_line(black_start, scan_line_y-2, black_end, scan_line_y-2, color0) # 畫識別到的線段 # 如果沒找到黑線可以根據上一幀的誤差或設定一個默認值這里簡單返回0并標記未找到。 # 實際應用中可能需要加入“丟線處理”邏輯比如保持上次轉角或原地旋轉尋線。 return error, found關鍵點解析與避坑ROI感興趣區域這是提升處理速度和抗干擾的關鍵。攝像頭畫面可能包含遠處的墻壁、天花板燈等。我們只關心地面附近的一條帶狀區域。roi的y值和height需要根據攝像頭俯角物理調整。一個技巧是將小車放在賽道上在OpenMV IDE中查看實時畫面確定黑線穩定出現的y軸范圍。二值化閾值threshold這個值本例中為50不是固定的。它取決于賽道底色白和引導線黑的灰度對比度。需要在比賽現場的光照條件下進行校準。可以在代碼中加入按鍵調整閾值的功能或者通過串口發送指令來微調。重心法 vs 起止點中心法注釋中提到了兩種計算線中心的方法。起止點中心法計算簡單但如果黑線邊緣有毛刺或噪聲中心點容易跳動。重心法求所有黑點坐標的平均值對噪聲的魯棒性更好是更推薦的做法但計算量稍大。丟線處理if black_pixels:這個判斷至關重要。當小車沖出賽道、遇到強光干擾導致全白或者遇到十字路口橫向黑線時black_pixels可能為空。代碼必須妥善處理這種情況否則error會保持為0小車可能直沖出去。高級策略包括使用上一次有效的誤差、讓小車減速并原地旋轉直到重新找線、或者進入路口處理狀態機。3.3 PID控制器實現得到偏差error后我們需要一個控制器來將其轉化為平滑的控制指令。PID比例-積分-微分是首選。# PID 參數定義 Kp 0.5 # 比例系數。決定了對當前誤差的反應強度。太大易震蕩太小響應慢。 Ki 0.01 # 積分系數。用于消除靜態誤差例如小車長期偏向一側。但容易積分飽和引起超調。 Kd 0.2 # 微分系數。預測誤差變化趨勢抑制振蕩。對噪聲敏感需要濾波。 # PID 變量初始化 error_last 0 # 上一次的誤差 error_integral 0 # 誤差積分項 output 0 # PID輸出值 def pid_compute(error, dt): PID控制計算函數位置式PID。 參數: error: 當前誤差值。 dt: 距離上次計算的時間間隔秒用于積分和微分。 返回: output: 計算出的控制量。 global error_last, error_integral # 比例項 P p_term Kp * error # 積分項 I (加入抗積分飽和) error_integral error * dt # 限制積分項防止積分飽和導致系統失控 integral_limit 100 # 積分限幅根據實際系統調整 if error_integral integral_limit: error_integral integral_limit elif error_integral -integral_limit: error_integral -integral_limit i_term Ki * error_integral # 微分項 D (使用不完全微分或對誤差進行濾波會更好) d_term Kd * (error - error_last) / dt if dt 0 else 0 # 注意對誤差直接微分會放大噪聲。實踐中常對誤差進行低通濾波或使用“微分先行”等變種。 error_last error # 更新上次誤差 # 總和輸出 output p_term i_term d_term return output關鍵點解析與避坑參數整定Kp, Ki, Kd這三個參數需要現場調試。經典口訣是“先比例后積分再微分”。先將Ki和Kd設為0調Kp讓小車能基本循跡但略有振蕩然后加入較小的Ki來消除長期偏向一側的誤差最后加入Kd來抑制振蕩使運行更平穩。積分飽和這是PID實踐中的一個經典問題。當誤差長時間存在比如卡在角落積分項會不斷累積變得非常大一旦誤差反向需要很長時間才能“消化”掉這個巨大的積分項導致控制反應遲鈍甚至反向失控。代碼中加入的integral_limit積分限幅是必須的防護措施。微分噪聲微分項對誤差的變化率敏感而攝像頭識別帶來的誤差值本身可能有幾個像素的跳動噪聲這會被微分放大導致輸出抖動。解決方案包括對誤差error進行一階低通濾波后再微分或者使用“微分先行”PID只對測量值微分而不對設定值微分。時間間隔dtPID計算需要知道兩次計算的時間差。可以用time.ticks_ms()獲取毫秒時間戳來計算dt。dt不準確會影響積分和微分的效果。3.4 電機控制與主循環整合最后我們將PID輸出映射到左右電機的速度差實現轉向。# 假設電機控制接口函數具體取決于你的電機驅動板可能是PWM、DIR引腳控制 def set_motor_speed(left_speed, right_speed): 設置左右電機速度。 參數: left_speed: 左電機速度范圍例如 -100 到 100正數前進負數后退。 right_speed: 右電機速度。 # 這里需要根據你的硬件來寫例如使用 pyb.PWM 對象 # left_pwm.duty(left_speed) # 偽代碼 # right_pwm.duty(right_speed) # 偽代碼 pass # 主循環 while(True): clock.tick() # 開始追蹤這一幀的時間 img sensor.snapshot() # 拍攝一張圖片 # 1. 找線計算誤差 error, line_found find_line_error(img) # 2. 如果丟線執行丟線處理策略 if not line_found: # 策略示例減速并施加一個固定的旋轉速度尋線 set_motor_speed(20, -20) # 原地左轉尋線 # 或者保持上一次的output值 # 更復雜的策略可以記錄丟線前的誤差方向向那個方向旋轉 continue # 跳過本次PID計算 # 3. 計算時間間隔 dt (秒) dt clock.avg() # clock.avg() 返回平均幀間時間秒。也可以使用 clock.diff() # 4. PID計算 turn_output pid_compute(error, dt) # 5. 將轉向輸出轉換為電機速度 base_speed 50 # 基礎前進速度 # 差速轉向一個輪子加速一個輪子減速 left_motor_speed base_speed - turn_output right_motor_speed base_speed turn_output # 6. 限制電機速度在合理范圍內例如0-100 left_motor_speed max(0, min(100, left_motor_speed)) right_motor_speed max(0, min(100, right_motor_speed)) # 7. 執行電機控制 set_motor_speed(int(left_motor_speed), int(right_motor_speed)) # 8. 調試用打印幀率和誤差到串口 # print(clock.fps(), error)關鍵點解析與避坑差速轉向模型左速 基礎速度 - 轉向輸出右速 基礎速度 轉向輸出。這是最常用的模型。當error為正線在右邊turn_output為正則左輪減速右輪加速小車向左轉以追線。速度限幅必須對計算出的電機速度進行限幅防止超過電機或驅動板能承受的PWM范圍否則可能導致硬件損壞或控制異常。dt的獲取clock.avg()返回的是平均幀時間用于PID計算是可行的。更精確的做法是使用time.ticks_diff()計算本次循環與上次循環的實際時間差。丟線處理主循環中加入了簡單的丟線處理if not line_found。這里的示例是讓小車原地左轉。在實際比賽中你需要設計更魯棒的策略比如判斷丟線前最后幾幀誤差的趨勢朝誤差減小的方向旋轉或者結合陀螺儀如果小車有進行角度控制尋線。4. 調試技巧與常見問題排查代碼寫完了調參和調試才是真正的戰場。以下是一些血淚換來的經驗。4.1 圖像識別調試使用OpenMV IDE的“幀緩沖區”和“串行終端”把img.draw_line等繪圖函數打開在IDE里實時查看攝像頭看到了什么掃描線畫在哪識別出的黑線區域是否準確。通過print()語句將threshold、error、black_pixels的長度等關鍵變量輸出到串行終端。可以寫一個簡單的協議用鍵盤輸入數字來動態調整閾值threshold這是現場調參的神器。光照是最大的敵人補光補光補光在小車攝像頭周圍加裝LED燈條自己創造穩定的光照環境。這是提升巡線穩定性的最有效手段沒有之一。注意燈光角度避免在賽道上形成反光點。可以嘗試用磨砂紙柔化光線。如果賽場光線不可控可以考慮動態閾值算法比如大津法OTSU但計算量會增大。二值化閾值怎么定把小車放在賽道典型位置直道、彎道、明處、暗處在IDE中查看黑線和白底的灰度值。閾值取一個中間值例如(黑線最大灰度值 白底最小灰度值) / 2并留出足夠的余量。4.2 PID參數調試“看波形”調試法將error偏差和outputPID輸出通過串口發送到電腦用SerialPlot、CoolTerm等軟件繪制實時曲線。理想狀態小車在直道上error在0附近小幅波動進入彎道error平穩地偏向一側output也隨之平穩變化。問題診斷振蕩嚴重Kp太大或Kd太小。響應遲鈍過彎時壓線甚至沖出Kp太小。長期偏向彎道內側有靜態誤差需要增加Ki但一定要配合積分限幅。電機“抽搐”可能是Kd太大放大了誤差噪聲。嘗試減小Kd或對誤差進行濾波。“聽聲音”調試法在直道上電機應該發出平穩的“嗡——”聲。如果發出“嗡-嗡-嗡-”有節奏的頓挫聲說明Kp太大導致小車在直道上不斷左右微調此時應減小Kp。4.3 常見問題速查表問題現象可能原因排查與解決思路完全找不到線1. 閾值threshold設置不當。2. 攝像頭焦距不對畫面模糊。3. ROI區域設置錯誤黑線不在掃描范圍內。4. 環境光太強/太弱導致黑白對比度低。1. 用串口打印實時灰度值調整閾值。2. 手動調整攝像頭焦距確保圖像清晰。3. 在IDE中查看畫面調整ROI的y坐標和高度。4. 加強補光或調整攝像頭曝光參數。尋線抖動小車“畫龍”1. 比例系數Kp過大。2. 微分系數Kd太小或為0。3. 圖像識別出的error本身就在跳動噪聲大。1. 逐步減小Kp。2. 適當增加Kd。3. 檢查圖像處理嘗試對掃描行的灰度值進行中值濾波或采用多行掃描取平均誤差。過彎時反應慢沖出賽道1. 比例系數Kp過小。2. 前瞻距離太短掃描行太靠下。3. 基礎速度base_speed過高。1. 逐步增大Kp。2. 將掃描行scan_line_y上移減小y值讓小車“看得更遠”。3. 彎道前適當降低基礎速度。直道上跑偏不居中存在靜態誤差需要積分項。1. 引入較小的Ki。2.務必同時設置integral_limit積分限幅。3. 檢查小車機械結構是否對稱輪胎摩擦力是否一致。遇到十字路口誤判十字路口會出現橫向黑線單行掃描可能識別為一個很寬的黑塊中心計算錯誤。1. 加入路口識別邏輯當識別到的黑線寬度(black_end - black_start)超過某個閾值時判定為十字路口進入預設的路口處理程序如直行一段或根據標記轉彎。2. 使用多行掃描判斷黑線的連續性。幀率太低控制延遲大1. 圖像分辨率設置過高。2. 圖像處理算法太復雜如使用了彩色識別、復雜的濾波。3. 串口打印了太多調試信息。1. 使用QQVGA或更低分辨率。2. 優化代碼只做必要的計算。關閉IDE中的圖像傳輸功能以釋放帶寬。3. 正式運行時注釋掉或減少print語句。5. 性能優化與進階思路當基礎巡線穩定后可以考慮以下優化來提升速度和魯棒性以應對更復雜的賽道和更高的競賽要求。5.1 圖像處理優化多行掃描與前瞻控制只掃描一行雖然快但信息有限。可以掃描多行例如近處一行遠處一行近處的誤差用于快速反應保持穩定遠處的誤差用于提前預判彎道實現更平滑的轉向。兩者的誤差可以加權融合。error_near, found_near find_line_error(img, scan_line_y100) # 近處 error_far, found_far find_line_error(img, scan_line_y60) # 遠處 if found_near and found_far: error 0.7 * error_near 0.3 * error_far # 加權融合側重近處動態ROI讓ROI隨著識別到的黑線位置動態移動。例如上一幀黑線在x50那么下一幀的ROI可以設置在x30到x70的范圍內。這樣可以進一步排除畫面邊緣的干擾并減少需要處理的像素數量提高速度。圖像濾波如果賽道有污漬或光照不均原始灰度圖噪聲會很大。可以在二值化前對line數組進行簡單的軟件濾波比如一個窗口大小為3的中值濾波能有效去除孤立的噪聲點。5.2 控制算法優化變參數PID讓PID參數根據誤差大小或小車速度動態變化。例如當誤差很小時使用一組較小的Kp來保證直道平穩當誤差很大急彎時切換為一組較大的Kp來快速響應。也可以根據車速調整參數高速時參數應更柔和。分段PID對直道、彎道、十字路口等不同賽道元素使用完全不同的PID參數組和控制邏輯。這需要配合一個狀態機來識別當前所處的賽道階段。結合其他傳感器純視覺在高速或復雜環境下有其局限性。可以融合編碼器測速進行速度閉環控制融合陀螺儀IMU進行角度輔助控制。例如在長直道上可以用陀螺儀來維持航向角減少對視覺的依賴從而允許小車跑得更快。5.3 工程實踐要點電源管理OpenMV、單片機、舵機、電機驅動共用電池時電機啟動的瞬間會產生很大的電壓跌落可能導致OpenMV重啟。務必在電源處加入大電容如4700uF進行緩沖并使用穩壓模塊為OpenMV單獨供電。機械結構攝像頭安裝高度和俯角直接影響ROI的設置。安裝要牢固避免抖動導致圖像模糊。車輪的對稱性和傳動機構的順滑度直接影響控制的對稱性機械問題是很多“玄學”bug的根源。代碼版本管理調試過程中會頻繁修改參數。一定要做好版本記錄每次只改變一個變量并記錄下效果。可以用注釋、或者簡單的文本文件來記錄每次測試的參數和表現。回過頭來看這份代碼的注釋工作更像是對那段備賽時光的一次復盤。從最初的攝像頭畫面一片模糊到調通第一版PID小車能踉蹌走完全程再到最后為了提升0.1秒的成績而反復打磨參數和算法每一個環節都充滿了挑戰和解決問題的樂趣。視覺巡線作為嵌入式與機器視覺的經典結合點其核心思想——感知、處理、決策、控制——在更廣闊的機器人領域也是相通的。希望這份詳細的注釋和心得能幫你少走一些我們曾經走過的彎路更順暢地駛入智能車開發的精彩世界。如果在實現過程中遇到具體問題不妨從圖像是否清晰、誤差計算是否準確、PID輸出是否合理這三個維度像剝洋蔥一樣一層層去排查大概率能找到問題的根源。