python 一份簡單的車輛環視全景系統實現


 

 

 

 

 

 

鏈接:https://pan.baidu.com/s/1rmQbbmrtV1DbfGg4przlKw
提取碼:0yr3
--來自百度網盤超級會員V4的分享

 

 

 

 

關於車輛的全景環視系統網上已經有很多的資料,然而幾乎沒有可供參考的代碼,這一點對入門的新人來說非常不友好。這個項目的目的就是介紹全景系統的原理,並給出一份可以實際運行的、基本要素齊全的 Python 實現供大家參考。環視全景系統涉及的知識並不復雜,只需要讀者了解相機的標定、透視變換,並懂得如何使用 OpenCV。項目代碼維護在 github 上。

這個程序最初是在一輛搭載了一台 AGX Xavier 的無人小車上開發的,運行效果如下:

小車上搭載了四個 USB 環視魚眼攝像頭,相機傳回的畫面分辨率為 640x480,圖像首先經過畸變校正,然后在射影變換下轉換為對地面的鳥瞰圖,最后拼接起來經過平滑處理后得到了上面的效果。全部過程在 CPU 中進行處理,整體運行流暢。

后來我把代碼重構以后移植到一輛乘用車上 (處理器是同型號的 AGX),得到了差不多的效果:

這個版本使用的是四個 960x640 的 csi 攝像頭,輸出的全景圖分辨率為 1200x1600,在不進行亮度均衡處理時全景圖處理線程運行速度大約為 17 fps,加入亮度均衡處理后驟降到只有 7 fps。我認為適當縮小分辨率的話 (比如采用 480x640 的輸出可以將像素個數降低到原來的 1/6) 應該也可以獲得流暢的視覺效果。

:畫面中黑色的部分是相機投影后出現的盲區,這是因為前面的相機為了避開車標的部位安裝在了車頭左側且角度傾斜,所以視野受到了限制。想象一個人歪着脖子還斜着眼走路的樣子 …

 

 

這個項目的實現比較粗糙,僅作為演示項目展示生成環視全景圖的基本要素,大家領會精神即可。我開發這個項目的目的是為了在自動泊車時實時顯示車輛的軌跡,同時也用來作為我指導的實習生的實習項目。由於之前沒有經驗和參照,大多數算法和流程都是琢磨着寫的,不見得高明,請大家多指教。代碼是 Python 寫成的,效率上肯定不如 C++,所以僅適合作學習和驗證想法使用。

下面就來一一介紹我的實現步驟。

硬件和軟件配置

我想首先強調的是,硬件配置是這個項目中最不需要費心的事情,在第一個小車項目中使用的硬件如下:

  1. 四個 USB 魚眼相機,支持的分辨率為 640x480|800x600|1920x1080 三種。我這里因為是需要在 Python 下面實時運行,為了效率考慮設置的分辨率是 640x480。
  2. 一台 AGX Xavier。實際上在普通筆記本上跑一樣溜得飛起。

第二個乘用車項目使用的硬件如下:

  1. 四個 csi 攝像頭,設置的分辨率是 960x640。
  2. 一台 AGX Xavier,型號同上面的小車項目一樣,不過外加了一個工控機接收 csi 攝像頭畫面。

我認為只要你只要有四個視野足夠覆蓋車周圍的攝像頭,再加一個普通筆記本電腦就足夠進行全部的離線開發了。

 

 

軟件配置如下:

  1. 操作系統 Ubuntu 16.04/18.04.
  2. Python>=3.
  3. OpenCV>=3.
  4. PyQt5.

其中 PyQt5 主要用來實現多線程,方便將來移植到 Qt 環境。

項目采用的若干約定

為了方便起見,在本項目中四個環視相機分別用 front、back、left、right 來指代,並假定其對應的設備號是整數,例如 0, 1, 2, 3。實際開發中請針對具體情況進行修改。

相機的內參矩陣記作 camera_matrix,這是一個 3x3 的矩陣。畸變系數記作 dist_coeffs,這是一個 1x4 的向量。相機的投影矩陣記作 project_matrix,這是一個 3x3 的射影矩陣。

准備工作:獲得原始圖像與相機內參

首先我們需要獲取每個相機的內參矩陣與畸變系數。我在項目中附上了一個腳本 run_calibrate_camera.py,你只需要運行這個腳本,通過命令行參數告訴它相機設備號,是否是魚眼相機,以及標定板的網格大小,然后手舉標定板在相機面前擺幾個姿勢即可。

以下是視頻中四個相機分別拍攝的原始畫面,順序依次為前、后、左、右,並命名為 front.png、back.png、left.png、right.png 保存在項目的 images/ 目錄下。

 

 

 

 

 

 

四個相機的內參文件分別為 front.yaml、back.yaml、left.yaml、right.yaml,這些圖像和內參文件都存放在項目的 yaml 子目錄下。

你可以看到圖中地面上鋪了一張標定布,這個布的尺寸是 6mx10m,每個黑白方格的尺寸為 40cmx40cm,每個圓形圖案所在的方格是 80cmx80cm。我們將利用這個標定物來手動選擇對應點獲得投影矩陣。

設置投影范圍和參數

接下來我們需要獲取每個相機到地面的投影矩陣,這個投影矩陣會把相機校正后的畫面轉換為對地面上某個矩形區域的鳥瞰圖。這四個相機的投影矩陣不是獨立的,它們必須保證投影后的區域能夠正好拼起來。

這一步是通過聯合標定實現的。即在車的四周地面上擺放標定物,拍攝圖像,手動選取對應點,然后獲取投影矩陣。

請看下圖:

image

首先在車身的四角擺放四個標定板,標定板的圖案大小並無特殊要求,只要尺寸一致,能在圖像中清晰看到即可。每個標定板應當恰好位於相鄰的兩個相機視野的重合區域中。

在上面拍攝的相機畫面中車的四周鋪了一張標定布,這個具體是標定板還是標定布不重要,只要能清楚的看到特征點就可以了。

然后我們需要設置幾個參數:(以下所有參數均以厘米為單位)

  • innerShiftWidth, innerShiftHeight:標定板內側邊緣與車輛左右兩側的距離,標定板內側邊緣與車輛前后方的距離。

 

    • shiftWidth, shiftHeight:這兩個參數決定了在鳥瞰圖中向標定板的外側看多遠。這兩個值越大,鳥瞰圖看的范圍就越大,相應地遠處的物體被投影后的形變也越嚴重,所以應酌情選擇。

  • totalWidth, totalHeight:這兩個參數代表鳥瞰圖的總寬高,在我們這個項目中標定布寬 6m 高 10m,於是鳥瞰圖中地面的范圍為 (600 + 2 * shiftWidth, 1000 + 2 * shiftHeight)。為方便計我們讓每個像素對應 1 厘米,於是鳥瞰圖的總寬高為

    1
    2
    totalWidth = 600 + 2 * shiftWidth
    totalHeight = 1000 + 2 * shiftHeight
    • 1
    • 2
  • 車輛所在矩形區域的四角 (圖中標注的紅色圓點),這四個角點的坐標分別為 (xl, yt), (xr, yt), (xl, yb), (xr, yb) (l 表示 left, r 表示 right,t 表示 top,b 表示 bottom)。這個矩形區域相機是看不到的,我們會用一張車輛的圖標來覆蓋此處。

注意這個車輛區域四邊的延長線將整個鳥瞰圖分為前左 (FL)、前中 (F)、前右 (FR)、左 (L)、右 ?、后左 (BL)、后中 (B)、后右 (BR) 八個部分,其中 FL (區域 I)、FR (區域 II)、BL (區域 III)、BR (區域 IV) 是相鄰相機視野的重合區域,也是我們重點需要進行融合處理的部分。F、R、L、R 四個區域屬於每個相機單獨的視野,不需要進行融合處理。

以上參數存放在 param_settings.py 中。

設置好參數以后,每個相機的投影區域也就確定了,比如前方相機對應的投影區域如下:

image

接下來我們需要通過手動選取標志點來獲取到地面的投影矩陣。

手動標定獲取投影矩陣

首先運行項目中 run_get_projection_maps.py 這個腳本,這個腳本需要你輸入如下的參數:

  • -camera: 指定是哪個相機。
  • -scale: 校正后畫面的橫向和縱向放縮比。
  • -shift: 校正后畫面中心的橫向和縱向平移距離。

為什么需要 scale 和 shift 這兩個參數呢?這是因為默認的 OpenCV 的校正方式是在魚眼相機校正后的圖像中裁剪出一個 OpenCV “認為” 合適的區域並將其返回,這必然會丟失一部分像素,特別地可能會把我們希望選擇的特征點給裁掉。幸運的是 cv2.fisheye.initUndistortRectifyMap 這個函數允許我們再傳入一個新的內參矩陣,對校正后但是裁剪前的畫面作一次放縮和平移。你可以嘗試調整並選擇合適的橫向、縱向壓縮比和圖像中心的位置使得地面上的標志點出現在畫面中舒服的位置上,以方便進行標定。

運行

1
python run_get_projection_maps.py -camera front -scale 0.7 0.8 -shift -150 -100
  • 1

后顯示前方相機校正后的畫面如下:

 

 

[圖片上傳中…(image-cf20e7-1594974592667-10)]

然后依次點擊事先確定好的四個標志點 (順序不能錯!),得到的效果如下:

[圖片上傳中…(image-a0db05-1594974592667-9)]

注意標志點的設置代碼在這里。

這四個點是可以自由設置的,但是你需要在程序中手動修改它們在鳥瞰圖中的像素坐標。當你在校正圖中點擊這四個點時,OpenCV 會根據它們在校正圖中的像素坐標和在鳥瞰圖中的像素坐標的對應關系計算一個射影矩陣。這里用到的原理就是四點對應確定一個射影變換。(四點對應可以給出八個方程,從而求解出射影矩陣的八個未知量。注意射影矩陣的最后一個分量總是固定為 1)

如果你不小心點歪了的話可以按 d 鍵刪除上一個錯誤的點。選擇好以后點回車,就會顯示投影后的效果圖:

image

覺得效果可以的話敲回車,就會將投影矩陣寫入 front.yaml 中,這個矩陣的名字為 project_matrix。失敗的話就點 q 退出再來一次。

再比如后面相機的標定如下圖所示:

[圖片上傳中…(image-c62af8-1594974592667-7)]

對應的投影圖為

image

對四個相機分別采用此操作,我們就得到了四個相機的鳥瞰圖,以及對應的四個投影矩陣。下一步我們的任務是把這四個鳥瞰圖拼起來。

鳥瞰圖的拼接與平滑

如果你前面操作一切正常的話,運行 run_get_weight_matrices.py 后應該會顯示如下的拼接圖:

image

我來逐步介紹它是怎么做到的:

未處理的原始拼接畫面

亮度平衡后的畫面

色彩平衡后的畫面

  1. 由於相鄰相機之間有重疊的區域,所以這部分的融合是關鍵。如果直接采取兩幅圖像加權平均 (權重各自為 1/2) 的方式融合的話你會得到類似下面的結果:

    image

    你可以看到由於校正和投影的誤差,相鄰相機在重合區域的投影結果並不能完全吻合,導致拼接的結果出現亂碼和重影。這里的關鍵在於權重系數應該是隨像素變化而變化的,並且是隨着像素連續變化。

  2. 以左上角區域為例,這個區域是 front, left 兩個相機視野的重疊區域。我們首先將投影圖中的重疊部分取出來:

    image

    灰度化並二值化:

    image

    注意這里面有噪點,可以用形態學操作去掉 (不必特別精細,大致去掉即可):

    image

    至此我們就得到了重疊區域的一個完整 mask。

  3. 將 front, left 圖像各自位於重疊區域外部的邊界檢測出來,這一步是通過先調用 cv2.findContours 求出最外圍的邊界,再用 cv2.approxPolyDP 獲得逼近的多邊形輪廓。

    我們把 front 相機減去重疊區域后的輪廓記作 polyA (左上圖中藍色邊界),left 相機減去重疊區域后的輪廓記作 polyB (右上圖中綠色邊界)。

  4. 對重疊區域中的每個像素,利用 cv2.pointPolygonTest 計算其到這兩個多邊形 polyA 和 polyB 的距離 dA,dB,則該像素對應的權值為 w=dB2/(dA2+dB2),即如果這個像素落在 front 畫面內,則它與 polyB 的距離就更遠,從而權值更大。

  5. 對不在重疊區域內的像素,若其屬於 front 相機的范圍則其權值為 1,否則權值為 0。於是我們得到了一個連續變化的,取值范圍在 0~1 之間的矩陣 G,其灰度圖如下:

    image

    用 G 作為權值可得融合后的圖像為 front * G + (1- G) * left。

  6. 注意由於重疊區域中的像素值是來自兩幅圖像的加權平均,所以出現在這個區域內的物體會不可避免出現虛影的現象,所以我們需要盡量壓縮重疊區域的范圍,盡可能只對拼接縫周圍的像素計算權值,拼接縫上方的像素盡量使用來自 front 的原像素,拼接縫下方的像素盡量使用來自 back 的原像素。這一步可以通過控制 dB 的值得到。

  7. 我們還漏掉了重要的一步:由於不同相機的曝光度不同,導致不同的區域會出現明暗的亮度差,影響美觀。我們需要調整每個區域的亮度,使得整個拼接圖像的亮度趨於一致。這一步做法不唯一,自由發揮的空間很大。我查閱了一下網上提到的方法,發現它們要么過於復雜,幾乎不可能是實時的;要么過於簡單,無法達到理想的效果。特別在上面第二個視頻的例子中,由於前方相機的視野被車標遮擋導致感光范圍不足,導致其與其它三個相機的畫面亮度差異很大,調整起來很費勁。

    一個基本的想法是這樣的:每個相機傳回的畫面有 BGR 三個通道,四個相機傳回的畫面總共有 12 個通道。我們要計算 12 個系數,將這 12 個系數分別乘到這 12 個通道上,然后再合並起來形成調整后的畫面。過亮的通道要調暗一些所以乘的系數小於 1,過暗的通道要調亮一些所以乘的系數大於 1。這些系數可以通過四個畫面在四個重合區域內的亮度比值得出,你可以自由設計計算系數的方式,只要滿足這個基本原理即可。

     

     

    我的實現見這里。感覺就像一段 shader 代碼。

    還有一種偷懶的辦法是事先計算一個 tone mapping 函數 (比如逐段線性的,或者 AES tone mapping function),然后強制把所有像素進行轉換,這個方法最省力,但是得到的畫面色調會與真實場景有較大差距。似乎有的市面產品就是采用的這種方法。

  8. 最后由於有些情況下攝像頭不同通道的強度不同,還需要進行一次色彩平衡,見下圖。

    在第二個視頻的例子中,畫面的顏色偏紅,加入色彩平衡后畫面恢復了正常。

具體實現的注意事項

權重矩陣

mask 矩陣

    1. 多線程與線程同步。本文的兩個例子中四個攝像頭都不是硬件觸發保證同步的,而且即便是硬件同步的,四個畫面的處理線程也未必同步,所以需要有一個線程同步機制。這個項目的實現采用的是比較原始的一種,其核心代碼如下:

class MultiBufferManager(object):

...

def sync(self, device_id):
    # only perform sync if enabled for specified device/stream
    self.mutex.lock()
    if device_id in self.sync_devices:
        # increment arrived count
        self.arrived += 1
        # we are the last to arrive: wake all waiting threads
        if self.do_sync and self.arrived == len(self.sync_devices):
            self.wc.wakeAll()
        # still waiting for other streams to arrive: wait
        else:
            self.wc.wait(self.mutex)
        # decrement arrived count
        self.arrived -= 1
    self.mutex.unlock()

  

  1. 這里使用了一個 MultiBufferManager 對象來管理所有的線程,每個攝像頭的線程在每次循環時會調用它的 sync 方法,並通過將計數器加 1 的方法來通知這個對象 “報告,我已做完上次的任務,請將我加入休眠池等待下次任務”。一旦計數器達到 4 就會觸發喚醒所有線程進入下一輪的任務循環。

  2. 建立查找表 (lookup table) 以加快運算速度。魚眼鏡頭的畫面需要經過校正、投影、翻轉以后才能用於拼接,這三步涉及頻繁的圖像內存分配和銷毀,非常費時間。在我的測試中抓取線程始終穩定在 30fps 多一點左右,但是每個畫面的處理線程只有 20 fps 左右。這一步最好是通過預計算一個查找表來加速。你還記得 cv2.fisheye.initUndistortRectifyMap 這個函數嗎?它返回的 mapx, mapy 就是兩個查找表。比如當你指定它返回的矩陣類型為 cv2.CV_16SC2 的時候,它返回的 mapx 就是一個逐像素的查找表,mapy 是一個用於插值平滑的一維數組 (可以扔掉不要)。同理對於 project_matrix 也不難獲得一個查找表,兩個合起來就可以得到一個直接從原始畫面到投影畫面的查找表 (當然損失了用於插值的信息)。 在這個項目中由於采用的是 Python 實現,而 Python 的 for 循環效率不高,所以沒有采用這種查找表的方式。

  3. 四個權重矩陣可以作為 RGBA 四個通道壓縮到一張圖片中,這樣存儲和讀取都很方便。四個重疊區域對應的 mask 矩陣也是如此:

實車運行

你可以在實車上運行 run_live_demo.py 來驗證最終的效果。

你需要注意修改相機設備號,以及 OpenCV 打開攝像頭的方式。usb 相機可以直接用 cv2.VideoCapture(i) (i 是 usb 設備號) 的方式打開,csi 相機則需要調用 gstreamer 打開,對應的代碼在這里和這里。

附錄:項目各腳本一覽

項目中目前的腳本根據執行順序排列如下:

  1. run_calibrate_camera.py:用於相機內參標定。
  2. param_settings.py:用於設置投影區域的各參數。
  3. run_get_projection_maps.py:用於手動標定獲取到地面的投影矩陣。
  4. run_get_weight_matrices.py:用於計算四個重疊區域對應的權重矩陣以及 mask 矩陣,並顯示拼接效果。
  5. run_live_demo.py:用於在實車上運行的最終版本。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM