閾值到預(yù)測濾波的三輪迭代實(shí)戰(zhàn))
簡介本資源是一套面向智能車競賽與嵌入式視覺控制學(xué)習(xí)者的完整工程代碼基于逐飛科技英飛凌TC264主控芯片實(shí)現(xiàn)攝像頭三輪智能車的循跡、環(huán)島識別與自動泊車功能適用于高校電賽、恩智浦智能車等實(shí)踐場景適合具備C語言基礎(chǔ)與嵌入式開發(fā)經(jīng)驗(yàn)的中高級學(xué)習(xí)者。壓縮包共1106個(gè)文件含349個(gè)頭文件.h定義外設(shè)驅(qū)動與算法接口、200個(gè)C源碼.c實(shí)現(xiàn)圖像二值化大津法、八鄰域掃線、雙環(huán)PID轉(zhuǎn)向/速度控制及環(huán)島元素識別邏輯另有200個(gè)編譯中間文件.o、93個(gè)Makefile相關(guān)構(gòu)建腳本.mk及調(diào)試所需的.map、.elf、.hex等輸出文件整體大小為10.18MB。已有5837人學(xué)習(xí)下載資源結(jié)構(gòu)規(guī)范包含Ifx系列底層驅(qū)動如IfxCif.c、IfxQspi_SpiMaster.c、FFT查表文件、Shell交互模塊及字體庫等關(guān)鍵組件可直接編譯燒錄運(yùn)行是理解汽車電子MCU圖像處理與閉環(huán)控制集成方案的優(yōu)質(zhì)實(shí)操范例。1. 項(xiàng)目背景與核心目標(biāo)最近在整理一個(gè)老項(xiàng)目翻出來一套基于逐飛科技TC264主控板的智能車攝像頭循跡代碼。這套代碼是當(dāng)年參加智能車競賽時(shí)為了應(yīng)對復(fù)雜的賽道元素比如環(huán)島、十字、坡道而反復(fù)打磨出來的算是“三輪”迭代后的一個(gè)相對穩(wěn)定的版本。所謂“三輪”并不是指物理上的三個(gè)輪子而是指代碼經(jīng)歷了三次大的架構(gòu)重構(gòu)和算法優(yōu)化周期從最初的簡單二值化處理到后來的動態(tài)閾值、圖像分區(qū)管理再到最后融入了預(yù)測和抗擾邏輯整個(gè)過程踩坑無數(shù)。現(xiàn)在很多同學(xué)上手智能車拿到逐飛的開源庫和例程后往往卡在如何將基礎(chǔ)的圖像采集功能與實(shí)際的賽道識別和控制策略串聯(lián)起來。官方例程通常只給到“點(diǎn)亮攝像頭”、“看到圖像”這一步而真正的難點(diǎn)在于如何從一幅噪點(diǎn)滿滿、光照不均的灰度圖像中穩(wěn)定、快速地提取出賽道中心線并且讓小車能基于這條線做出合理的決策。這套“三輪完整代碼”就是來解決這個(gè)核心問題的。它不是一個(gè)簡單的函數(shù)庫調(diào)用演示而是一個(gè)包含了圖像采集、預(yù)處理、特征提取、中線計(jì)算、偏差輸出以及基礎(chǔ)控制框架的完整工程你可以直接把它作為你車模的“大腦”來使用或者拆解其中的算法模塊融入你自己的設(shè)計(jì)。這套代碼特別適合那些使用英飛凌AURIX TC264/TC364/TC367等系列單片機(jī)作為主控并搭配逐飛科技“總鉆風(fēng)”、“蜻蜓”等型號灰度攝像頭的同學(xué)。即使你用的攝像頭型號不同只要接口協(xié)議兼容比如DVP或SPI圖像處理的算法部分也極具參考價(jià)值。接下來我會把這套代碼的骨架和核心思想掰開揉碎了講清楚重點(diǎn)不是給你一堆看不懂的代碼而是告訴你每個(gè)部分為什么要這么寫實(shí)際跑起來會遇到什么妖魔鬼怪以及我們當(dāng)時(shí)是怎么降妖除魔的。2. TC264主控與逐飛庫的環(huán)境搭建要點(diǎn)在開始聊代碼之前得先把“戰(zhàn)場”布置好。英飛凌的TC264屬于AURIX?家族性能強(qiáng)悍外設(shè)豐富但開發(fā)環(huán)境和常見的STM32有些不同初次接觸容易在環(huán)境配置上栽跟頭。2.1 編譯器與IDE的選擇困境與解決TC264的主流編譯器是Tasking和HighTec。逐飛科技提供的開源庫通?;贖ighTec編譯器HT進(jìn)行開發(fā)。我的建議是直接使用逐飛配套提供的HighTec IDE版本或者他們封裝好的開發(fā)環(huán)境。不要自己從頭去下載最新的HighTec因?yàn)椴煌姹局g的庫文件和編譯配置可能有細(xì)微差異這些差異足以讓你在鏈接階段遇到一堆“未定義引用”的錯(cuò)誤。注意如果你從逐飛社區(qū)或資料包獲取了工程模板請務(wù)必保持整個(gè)工程路徑為英文且不要過深。有些編譯腳本對中文路徑的支持不好可能導(dǎo)致編譯失敗。安裝好環(huán)境后打開工程你首先應(yīng)該檢查這幾個(gè)關(guān)鍵配置設(shè)備型號確認(rèn)選中的是TC264DA或者你具體使用的型號。編譯器版本在工程屬性中確認(rèn)使用的是HighTec編譯器。頭文件路徑和庫文件路徑確保包含了逐飛庫的include文件夾和lib文件夾。通常模板工程已經(jīng)配好但如果移動了文件位置需要手動更新。2.2 逐飛庫的核心模塊解析逐飛的庫封裝了TC264的底層驅(qū)動讓我們能更專注于應(yīng)用層。對于攝像頭應(yīng)用你需要重點(diǎn)關(guān)注這幾個(gè)模塊lcd用于在屏幕上顯示圖像或調(diào)試信息。雖然比賽車可能不用屏幕但調(diào)試階段它是你的“眼睛”。mt9v03x或ov7725這是攝像頭驅(qū)動文件。根據(jù)你使用的攝像頭型號選擇。我們用的是mt9v03x.c/h對應(yīng)總鉆風(fēng)攝像頭。pin和gpt引腳定義和通用定時(shí)器。攝像頭的數(shù)據(jù)采集通常依賴于精確的時(shí)序會用到GPT定時(shí)器來產(chǎn)生行場中斷。ips114如果你使用IPS屏幕會用到這個(gè)驅(qū)動。在代碼中你通常會看到這樣的初始化鏈// 系統(tǒng)基礎(chǔ)初始化 sys_init(); // 系統(tǒng)時(shí)鐘、中斷向量表等 // 外設(shè)初始化 lcd_init(); // 顯示屏 mt9v03x_init(); // 攝像頭 // 功能模塊初始化 image_init(); // 圖像處理相關(guān)變量、數(shù)組初始化 motor_init(); // 電機(jī)驅(qū)動初始化 servo_init(); // 舵機(jī)初始化這個(gè)順序是有講究的。硬件外設(shè)如攝像頭的初始化必須在功能模塊之前因?yàn)楣δ苣K依賴于硬件采集到的數(shù)據(jù)。2.3 圖像采集的硬件連接與中斷配置這是第一個(gè)容易出問題的地方。總鉆風(fēng)攝像頭通過DVP接口與TC264連接需要用到一組GPIO數(shù)據(jù)線和至少兩個(gè)中斷引腳行同步VSYNC、場同步HREF/HSYNC。在mt9v03x.c的初始化函數(shù)里會配置這些引腳和對應(yīng)的中斷服務(wù)函數(shù)。你需要檢查headfile.h或pin.h中的宏定義確保它們和你實(shí)際的硬件接線一致。比如#define CAMERA_VSYNC_PIN P33 // VSYNC 接在P3.3 #define CAMERA_HREF_PIN P34 // HREF 接在P3.4 // 數(shù)據(jù)總線 P1口8位如果接錯(cuò)了圖像數(shù)據(jù)永遠(yuǎn)進(jìn)不來。更關(guān)鍵的是中斷服務(wù)函數(shù)。圖像采集是“事件驅(qū)動”的VSYNC中斷幀開始當(dāng)攝像頭開始輸出一幀新圖像時(shí)產(chǎn)生一個(gè)VSYNC脈沖。在中斷里我們通常會重置行計(jì)數(shù)器并可能設(shè)置一個(gè)幀采集開始的標(biāo)志。HREF中斷行有效當(dāng)攝像頭開始輸出一行有效像素?cái)?shù)據(jù)時(shí)產(chǎn)生HREF脈沖。在中斷里我們會啟動DMA直接存儲器訪問或者準(zhǔn)備讀取數(shù)據(jù)。逐飛的庫通常已經(jīng)寫好了這些中斷服務(wù)函數(shù)的框架。你的主要任務(wù)是理解其流程并確保中斷的優(yōu)先級設(shè)置合理。圖像采集中斷的優(yōu)先級應(yīng)該設(shè)為較高避免被其他任務(wù)如電機(jī)控制PID計(jì)算打斷導(dǎo)致圖像數(shù)據(jù)錯(cuò)亂。在HighTec環(huán)境中中斷優(yōu)先級通常在IfxCpu_Irq_installInterruptHandler函數(shù)調(diào)用時(shí)設(shè)置。3. 圖像處理算法的三次迭代演進(jìn)拿到原始的圖像數(shù)據(jù)數(shù)組比如uint8 image[188][120]188行120列只是第一步。如何從中找到賽道是算法的核心。我們的三輪迭代正是圍繞這個(gè)核心展開的。3.1 第一輪基礎(chǔ)二值化與掃線最初的版本非常簡單粗暴。全局固定閾值二值化設(shè)定一個(gè)固定的灰度值比如120遍歷整個(gè)圖像大于120的認(rèn)為是白色賽道設(shè)為1小于等于的認(rèn)為是黑色背景設(shè)為0。結(jié)果存到一個(gè)二值化數(shù)組bin_image中。從下往上的“掃線”從圖像最底部靠近車頭的一行開始從左到右掃描尋找從0到1和從1到0的跳變點(diǎn)分別記錄為左邊界left_edge和右邊界right_edge。計(jì)算中線center_line (left_edge right_edge) / 2。這個(gè)中線與圖像中心列的偏差就是舵機(jī)需要糾正的誤差。遇到的問題與反思光照敏感固定閾值在早上、中午、晚上或者室內(nèi)外光線變化時(shí)完全失效。要么把整個(gè)背景都當(dāng)成賽道要么丟失賽道??乖肽芰Σ顖D像上的一個(gè)噪點(diǎn)就可能被誤判為邊界導(dǎo)致中線跳變。彎道處理差在急彎處賽道在圖像中可能只出現(xiàn)在一側(cè)另一側(cè)邊界會丟失簡單的平均計(jì)算會導(dǎo)致中線跑到圖像外系統(tǒng)崩潰。這一輪的價(jià)值在于快速搭建了“采集-處理-輸出”的閉環(huán)讓我們的小車能“動起來看效果”但距離穩(wěn)定運(yùn)行還差得遠(yuǎn)。3.2 第二輪動態(tài)閾值與圖像分區(qū)為了解決光照問題我們引入了動態(tài)閾值也叫大津法或OTSU算法。它的核心思想是根據(jù)當(dāng)前幀圖像的灰度直方圖自動計(jì)算出一個(gè)最佳的分割閾值。這樣無論圖像整體是亮是暗都能較好地區(qū)分賽道和背景。在TC264上實(shí)現(xiàn)OTSU算法需要注意效率因?yàn)樗荗(N)復(fù)雜度需要對整幅圖像的像素進(jìn)行一次統(tǒng)計(jì)和計(jì)算。我們將其放在每幀開始處理時(shí)進(jìn)行計(jì)算出的閾值用于后續(xù)整個(gè)幀的二值化。同時(shí)我們引入了“圖像分區(qū)”的概念。不再從最底部開始掃線而是將圖像在垂直方向上分成4-6個(gè)區(qū)域Region。例如分成遠(yuǎn)、中、近三個(gè)區(qū)域。先在最遠(yuǎn)的區(qū)域圖像頂部尋找賽道如果找到就以這個(gè)區(qū)域的邊界作為“種子”引導(dǎo)在下一個(gè)區(qū)域更近的區(qū)域的搜索范圍。這就像人的視線先看到遠(yuǎn)處的路再順著它看近處。具體操作在頂部區(qū)域ROI內(nèi)使用動態(tài)閾值二值化并掃線得到初始的左右邊界。將這個(gè)邊界向下一個(gè)區(qū)域投影并在這個(gè)投影的左右一定范圍內(nèi)稱為“搜索窗”進(jìn)行下一行的邊界搜索。這樣可以有效避免在彎道時(shí)搜索到錯(cuò)誤的邊界比如旁邊的廣告牌。如果某個(gè)區(qū)域搜索失敗比如邊界連續(xù)幾行超出合理范圍則啟動“丟線處理”邏輯比如擴(kuò)大搜索窗或使用上一幀的邊界進(jìn)行預(yù)測。這一輪迭代后小車在光線變化和一般彎道下的穩(wěn)定性大大提升。代碼結(jié)構(gòu)也從線性流程變成了基于狀態(tài)正常、左丟線、右丟線、全丟線的決策流程。3.3 第三輪預(yù)測濾波與特殊元素處理第三輪的目標(biāo)是讓小車更“聰明”和“平滑”。邊界預(yù)測與濾波直接使用原始掃線得到的邊界是毛刺很多的。我們引入了簡單的預(yù)測算法比如使用上一幀的邊界位置和變化趨勢來預(yù)測當(dāng)前幀的邊界可能位置作為搜索的起始點(diǎn)。同時(shí)對連續(xù)多行掃描到的邊界點(diǎn)進(jìn)行中值濾波或一階滯后濾波平滑掉突變的噪點(diǎn)。// 偽代碼示例一階滯后濾波 filtered_left_edge[i] ALPHA * filtered_left_edge[i-1] (1-ALPHA) * raw_left_edge[i]; // ALPHA是一個(gè)介于0和1之間的濾波系數(shù)越接近1濾波效果越強(qiáng)響應(yīng)越慢。中線計(jì)算策略升級不再簡單地對左右邊界平均。在直道上平均是有效的。但在彎道特別是只有單邊邊界可見時(shí)我們采用“邊線偏移法”。即假設(shè)賽道的寬度是已知的例如在圖像中約為50個(gè)像素那么當(dāng)只找到左邊界時(shí)中線 左邊界 1/2賽道寬度。這樣計(jì)算出的中線更符合物理實(shí)際。特殊賽道元素識別這是區(qū)分高手的關(guān)鍵。我們開始嘗試識別環(huán)島、十字路口、坡道等。環(huán)島通過分析邊界線的連續(xù)變化趨勢例如左邊界持續(xù)向左擴(kuò)展右邊界持續(xù)向右收縮然后合并結(jié)合路徑的曲率變化來判斷進(jìn)入環(huán)島。一旦識別就切換到一個(gè)固定的“環(huán)島控制模式”控制小車沿環(huán)島內(nèi)側(cè)行駛。十字路口當(dāng)掃線發(fā)現(xiàn)左右邊界同時(shí)大幅向外擴(kuò)展且中間區(qū)域出現(xiàn)大面積的“無邊界”狀態(tài)時(shí)可能是十字。處理策略通常是“沖過去”即保持進(jìn)入十字前的舵機(jī)角度和速度忽略短時(shí)間內(nèi)的中線誤差。坡道坡道會改變攝像頭的俯仰角導(dǎo)致圖像中的賽道寬度發(fā)生變化??梢酝ㄟ^監(jiān)測平均賽道寬度的突變來檢測坡道并動態(tài)調(diào)整二值化閾值或搜索參數(shù)。這一輪的代碼加入了有限狀態(tài)機(jī)小車在不同的賽道元素下會進(jìn)入不同的處理狀態(tài)每個(gè)狀態(tài)有獨(dú)立的圖像處理參數(shù)和控制參數(shù)。代碼的復(fù)雜度最高但也是小車能夠應(yīng)對復(fù)雜賽場環(huán)境、穩(wěn)定完賽的保障。4. 核心代碼模塊拆解與詳解下面我結(jié)合代碼片段講解幾個(gè)最關(guān)鍵的函數(shù)模塊。請注意這是經(jīng)過簡化和說明的偽代碼風(fēng)格重點(diǎn)在于理解邏輯。4.1 圖像采集中斷服務(wù)函數(shù)這是數(shù)據(jù)流的源頭必須高效、準(zhǔn)確。// VSYNC 中斷服務(wù)函數(shù) void VSYNC_IRQHandler(void) { if(檢查到是VSYNC上升沿) { g_camera_frame_ready 0; // 清除幀就緒標(biāo)志 g_row_cnt 0; // 行計(jì)數(shù)器清零 // 可以在這里啟動一些幀開始的準(zhǔn)備工作 } } // HREF 中斷服務(wù)函數(shù) (或使用DMA完成中斷) void HREF_IRQHandler(void) { if(檢查到是HREF上升沿即行有效開始) { if(g_row_cnt IMAGE_HEIGHT) { // 防止數(shù)組越界 // 方式1使用DMA自動搬運(yùn)一行數(shù)據(jù)到 image[g_row_cnt] 數(shù)組 // 方式2在中斷中延時(shí)等待然后通過GPIO讀取數(shù)據(jù) // 逐飛庫通常使用DMA方式效率高不占用CPU。 // 啟動DMA傳輸目標(biāo)地址是 image[g_row_cnt][0] } g_row_cnt; // 行號增加 } } // DMA傳輸完成中斷 void DMA_IRQHandler(void) { // 一行數(shù)據(jù)已存入數(shù)組可以設(shè)置標(biāo)志或進(jìn)行簡單處理 // 如果使用雙緩沖區(qū)可以在這里切換緩沖區(qū) }注意在TC264上合理配置DMA通道和中斷優(yōu)先級至關(guān)重要。要確保DMA傳輸完成中斷的優(yōu)先級低于HREF中斷避免數(shù)據(jù)覆蓋。4.2 動態(tài)閾值計(jì)算函數(shù)這是圖像預(yù)處理的關(guān)鍵。uint8 calculate_otsu_threshold(uint8 *image, uint16 width, uint16 height) { uint32 histogram[256] {0}; // 灰度直方圖 uint32 total_pixels width * height; uint32 sum 0, sumB 0; uint32 wB 0, wF 0; float variance_max 0.0, variance; uint8 threshold 0; // 1. 統(tǒng)計(jì)灰度直方圖 for(int i0; itotal_pixels; i) { histogram[image[i]]; } // 2. 計(jì)算總灰度值 for(int i0; i256; i) { sum i * histogram[i]; } // 3. 遍歷所有可能的閾值T尋找使類間方差最大的T for(int t0; t256; t) { wB histogram[t]; // 背景像素?cái)?shù)累積 if(wB 0) continue; wF total_pixels - wB; // 前景像素?cái)?shù) if(wF 0) break; // 如果前景沒了后面也不用算了 sumB t * histogram[t]; // 背景灰度值累積 float mB (float)sumB / wB; // 背景平均灰度 float mF (float)(sum - sumB) / wF; // 前景平均灰度 // 計(jì)算類間方差 variance (float)wB * (float)wF * (mB - mF) * (mB - mF); if(variance variance_max) { variance_max variance; threshold t; } } return threshold; }這個(gè)函數(shù)計(jì)算量較大如果每幀都全圖計(jì)算對TC264的算力是個(gè)挑戰(zhàn)。一個(gè)優(yōu)化技巧是只在圖像中選取一個(gè)代表性的區(qū)域比如中間偏下的幾行來計(jì)算閾值因?yàn)檫@個(gè)區(qū)域的賽道和背景信息最豐富。計(jì)算出的閾值可以用于整幀或者結(jié)合歷史閾值進(jìn)行平滑。4.3 分區(qū)掃線與中線計(jì)算函數(shù)這是算法的執(zhí)行核心。void find_track_center(void) { uint8 dynamic_thresh calculate_otsu_threshold(roi_image); // 計(jì)算動態(tài)閾值 uint8 left_edge[REGION_NUM], right_edge[REGION_NUM]; uint8 lost_left_flag 0, lost_right_flag 0; // 初始化搜索起點(diǎn)可以從上一幀的中線推導(dǎo)或者使用圖像中心 uint8 search_start_left g_last_center - DEFAULT_WIDTH/2; uint8 search_start_right g_last_center DEFAULT_WIDTH/2; // 從最遠(yuǎn)區(qū)域i0向最近區(qū)域iREGION_NUM-1遍歷 for(int i0; iREGION_NUM; i) { uint8 row region_start_row[i]; // 該區(qū)域的起始行 // 左邊界搜索 left_edge[i] search_edge_left(row, search_start_left, dynamic_thresh); if(left_edge[i] EDGE_NOT_FOUND) { lost_left_flag; // 丟線處理使用預(yù)測值或上一行邊界 left_edge[i] predict_left_edge(i); } else { lost_left_flag 0; // 更新左邊界預(yù)測模型 update_left_predictor(left_edge[i]); // 濾波處理 left_edge[i] filter_left(left_edge[i], i); } // 右邊界搜索邏輯類似 right_edge[i] search_edge_right(row, search_start_right, dynamic_thresh); // ... 右邊界處理 ... // 計(jì)算本區(qū)域中線 if(!lost_left_flag !lost_right_flag) { g_center_line[i] (left_edge[i] right_edge[i]) / 2; } else if(lost_left_flag !lost_right_flag) { // 只找到右邊界 g_center_line[i] right_edge[i] - ASSUMED_TRACK_WIDTH/2; } else if(!lost_left_flag lost_right_flag) { // 只找到左邊界 g_center_line[i] left_edge[i] ASSUMED_TRACK_WIDTH/2; } else { // 兩邊都丟了進(jìn)入全丟線處理狀態(tài) g_center_line[i] g_last_center; // 保持上一幀值或使用默認(rèn)值 enter_full_lost_state(); } // 為下一個(gè)區(qū)域的搜索提供起點(diǎn)預(yù)測 search_start_left left_edge[i] - SEARCH_MARGIN; search_start_right right_edge[i] SEARCH_MARGIN; } // 最終我們可能選擇最近區(qū)域iREGION_NUM-1的中線作為控制誤差 // 或者對幾個(gè)區(qū)域的中線進(jìn)行加權(quán)平均讓控制更平滑。 g_final_center g_center_line[REGION_NUM-1]; g_control_error g_final_center - IMAGE_CENTER_COL; }search_edge_left和search_edge_right函數(shù)負(fù)責(zé)在指定的行和搜索范圍內(nèi)根據(jù)閾值尋找邊界跳變點(diǎn)。它們內(nèi)部會處理圖像噪點(diǎn)比如要求連續(xù)幾個(gè)像素滿足條件才認(rèn)為是真正的邊界。5. 控制策略與參數(shù)整定經(jīng)驗(yàn)得到中線偏差control_error后如何驅(qū)動舵機(jī)轉(zhuǎn)向是另一個(gè)大學(xué)問。最常用的是PD控制器。// 簡單的PD控制器 float kp 0.8, kd 2.5; // 參數(shù)需要調(diào)試 int16 error g_control_error; int16 last_error g_last_error; int16 error_diff error - last_error; // 微分項(xiàng)近似為誤差變化量 int16 steering_output (int16)(kp * error kd * error_diff); // 限制輸出范圍防止舵機(jī)打滿 if(steering_output MAX_STEERING) steering_output MAX_STEERING; if(steering_output -MAX_STEERING) steering_output -MAX_STEERING; servo_set_angle(CENTER_ANGLE steering_output); // 設(shè)置舵機(jī)角度 g_last_error error; // 更新上一次誤差參數(shù)整定的血淚經(jīng)驗(yàn)先P后D先把kd設(shè)為0只調(diào)kp。讓小車在直道上能基本沿著線走但會出現(xiàn)左右搖擺振蕩。這說明kp太大了需要減小。直到小車在直道上能平順行駛過彎時(shí)雖然轉(zhuǎn)向不足但能跟上。加入D抑制振蕩逐漸增加kd。你會發(fā)現(xiàn)小車的擺動迅速減小過彎更干脆。但kd太大會帶來兩個(gè)問題一是對圖像噪聲極其敏感一個(gè)像素的跳動可能導(dǎo)致舵機(jī)劇烈抖動二是在長直道進(jìn)入急彎的瞬間由于誤差變化率突然增大kd項(xiàng)會產(chǎn)生一個(gè)巨大的“提前轉(zhuǎn)向”可能導(dǎo)致小車甩尾。所以kd的取值需要非常謹(jǐn)慎通常比kp小一個(gè)數(shù)量級左右開始調(diào)試。速度前饋更高級的策略是加入速度前饋。在高速時(shí)同樣的誤差需要更大的舵機(jī)轉(zhuǎn)角來補(bǔ)償??梢越⒁粋€(gè)簡單的映射steering_output speed * feed_forward_gain。彎道內(nèi)切為了讓小車過彎更快可以故意讓計(jì)算出的中線偏向彎道內(nèi)側(cè)。這可以在圖像處理階段完成比如在識別到彎道通過左右邊界差判斷時(shí)將最終用于控制的final_center在彎道方向額外偏移幾個(gè)像素。調(diào)試工具至關(guān)重要無線串口將關(guān)鍵數(shù)據(jù)如誤差、邊界位置、控制輸出、圖像閾值實(shí)時(shí)發(fā)送到電腦上位機(jī)用曲線觀察。LCD屏幕在車上直接顯示二值化圖像、邊界線、中線直觀看到算法效果。按鍵設(shè)置通過車上的按鍵在運(yùn)行時(shí)動態(tài)微調(diào)kpkd等參數(shù)觀察小車行為的即時(shí)變化這是最高效的調(diào)試方法。6. 工程優(yōu)化與常見問題排查一套代碼要穩(wěn)定運(yùn)行除了算法工程層面的優(yōu)化和問題排查同樣重要。6.1 內(nèi)存與計(jì)算效率優(yōu)化TC264的RAM和算力對于188*120的圖像處理是足夠的但優(yōu)化無止境。使用uint8類型圖像數(shù)據(jù)范圍是0-255用uint8數(shù)組存儲比int節(jié)省大量內(nèi)存。減少全局變量頻繁訪問的變量如行計(jì)數(shù)器、誤差值可以放在SRAM的快速訪問區(qū)或者使用寄存器變量修飾但需謹(jǐn)慎編譯器優(yōu)化可能已經(jīng)做得很好。查表法對于一些復(fù)雜的計(jì)算如三角函數(shù)、濾波系數(shù)如果參數(shù)是固定的可以預(yù)先計(jì)算好存成數(shù)組用查表代替實(shí)時(shí)計(jì)算。編譯器優(yōu)化等級在HighTec工程設(shè)置中將優(yōu)化等級調(diào)到-O2或-O3可以顯著提升代碼執(zhí)行速度。但要注意高優(yōu)化等級可能會對某些依賴嚴(yán)格時(shí)序的代碼如精確延時(shí)產(chǎn)生影響需要進(jìn)行測試。6.2 圖像不穩(wěn)定與丟線的硬件根源很多時(shí)候算法沒問題問題是硬件帶來的。電源噪聲電機(jī)、舵機(jī)工作時(shí)會產(chǎn)生巨大的電源噪聲如果攝像頭供電不干凈圖像會出現(xiàn)橫條紋、閃爍直接導(dǎo)致二值化結(jié)果不穩(wěn)定。務(wù)必為攝像頭模塊使用獨(dú)立的LDO穩(wěn)壓供電并與電機(jī)驅(qū)動電源進(jìn)行隔離如使用磁珠或0歐電阻電容濾波。機(jī)械振動車體抖動會導(dǎo)致攝像頭拍攝的圖像模糊邊界難以識別。加固攝像頭支架使用減震材料如海綿、硅膠墊可以有效緩解。鏡頭焦距與安裝角度鏡頭焦距決定了視野大小。焦距太短視野廣遠(yuǎn)處賽道像素少識別精度差焦距太長視野窄近處賽道容易出畫面。安裝的俯仰角也至關(guān)重要需要調(diào)整到能讓賽道在圖像中占據(jù)合適的位置和比例。這是一個(gè)需要反復(fù)上電、靜態(tài)調(diào)試的過程。曝光時(shí)間逐飛的攝像頭驅(qū)動通常允許設(shè)置曝光時(shí)間。在光線暗的環(huán)境下增加曝光時(shí)間可以提升圖像亮度但也會增加運(yùn)動模糊。需要根據(jù)賽場光照條件進(jìn)行權(quán)衡和動態(tài)調(diào)整如果支持。6.3 程序跑飛與HardFault調(diào)試這是最令人頭疼的問題。程序運(yùn)行一段時(shí)間后死機(jī)或者進(jìn)入HardFault中斷。數(shù)組越界這是最常見的原因。檢查所有數(shù)組的訪問下標(biāo)特別是圖像數(shù)組image[row][col]確保row和col不會超過定義的大小。在掃線函數(shù)中搜索范圍的邊界檢查尤為重要。棧溢出如果函數(shù)內(nèi)定義了很大的局部數(shù)組比如一個(gè)圖像緩沖區(qū)或者遞歸調(diào)用層次太深可能導(dǎo)致棧溢出。可以將大數(shù)組定義為全局變量或靜態(tài)變量。中斷沖突多個(gè)中斷服務(wù)函數(shù)訪問了同一個(gè)全局變量而沒有進(jìn)行保護(hù)關(guān)中斷或使用原子操作可能導(dǎo)致數(shù)據(jù)錯(cuò)亂。對于像g_row_cntimage這類在中斷和主循環(huán)中共享的變量訪問時(shí)最好進(jìn)行臨界區(qū)保護(hù)。使用HighTec的調(diào)試器連接JTAG調(diào)試器當(dāng)發(fā)生HardFault時(shí)查看調(diào)用堆棧和寄存器值特別是PC和LR寄存器可以定位到出錯(cuò)的代碼附近。7. 從循跡到競速的進(jìn)階思路當(dāng)你的小車能穩(wěn)定循跡后就可以考慮如何跑得更快了。這涉及到更復(fù)雜的感知和控制策略。1. 元素預(yù)判與路徑規(guī)劃 單純的“跟隨當(dāng)前中線”是滯后的。更高級的做法是使用圖像中多個(gè)區(qū)域的中線擬合出一條近似的賽道曲線比如二次曲線。通過這條曲線可以預(yù)測前方一段距離的賽道走向從而提前打舵實(shí)現(xiàn)更平滑的過彎。這就是“預(yù)瞄控制”的思想。2. 速度規(guī)劃 不同的賽道段應(yīng)該用不同的速度。直道加速入彎前減速彎心后加速??梢愿鶕?jù)擬合出的賽道曲率來動態(tài)設(shè)定目標(biāo)速度。曲率大彎急目標(biāo)速度就低曲率小直道目標(biāo)速度就高。實(shí)現(xiàn)一個(gè)簡單的“速度-曲率”映射表。3. 圖像特征的進(jìn)一步利用邊線類型識別識別出賽道邊線是白色還是黑色可以幫助判斷賽道的類型正常賽道、斷路、障礙等。斑馬線檢測通過檢測圖像中特定區(qū)域的橫向黑白跳變頻率可以識別起跑線/終點(diǎn)線的斑馬線用于圈數(shù)計(jì)數(shù)。4. 傳感器融合 單攝像頭存在盲區(qū)特別是近處??梢越Y(jié)合編碼器測速、陀螺儀感知車身旋轉(zhuǎn)角速度進(jìn)行數(shù)據(jù)融合。例如在攝像頭短暫丟線時(shí)利用陀螺儀的積分來估算車身偏航角維持一段時(shí)間的直行或緩彎控制為攝像頭重新捕獲賽道爭取時(shí)間。這套“三輪完整代碼”提供了一個(gè)堅(jiān)實(shí)的起點(diǎn)。它可能不是最快的但一定是經(jīng)過充分測試、相對穩(wěn)健的。智能車開發(fā)的樂趣就在于從讓車“能動”到“能跑”再到“跑得快”每一個(gè)環(huán)節(jié)都充滿了挑戰(zhàn)和解決問題的成就感。希望這份詳細(xì)的拆解能幫你少走些我們當(dāng)年走過的彎路。最后記住硬件是基礎(chǔ)調(diào)試是王道多跑、多試、多觀察數(shù)據(jù)不會騙人。本文還有配套的精品資源點(diǎn)擊獲取