一文搞懂基於透視變換的車道線擬合

laugh12321發表於2023-12-16

文章程式碼👉 laugh12321/RoadLaneFitting 歡迎star ✨

將前檢視轉為鳥瞰圖

將前檢視轉為鳥瞰圖的方法有兩種:

  • 有標定的情況下,可以直接使用標定引數進行轉換。
  • 沒有標定的情況下,可以選擇四個點計算透視變換矩陣來進行轉換。

在沒有標定的情況下,透視變換需要使用一個3x3的變換矩陣,確保直線在變換後仍然保持直線的性質。為了得到這個變換矩陣,需要在輸入影像上選擇4個點,並提供它們在輸出影像上的對應點。這4個點中,至少有3個點不能共線。透過使用cv2.getPerspectiveTransform函式,可以計算出這個變換矩陣,隨後可以透過cv2.warpPerspective將其應用於影像。

簡而言之,透視變換需要選取4個非共線的點,並透過這些點之間的對映關係來計算變換矩陣,最終應用於影像。

Your Image

以上圖為例,選擇1,2,3,4四個點,用以進行透視變換。經查閱,高速公路上的白色虛線標準長度為長度6米,間隔9米。高速公路單條車道寬度是3.75米。這裡假定,直線14,直線23長4米,直線12,直線34長30米。則輸入影像與輸出影像點的座標如上圖所示。

# GET MATRIX
src = np.float32([
    (243.3086, 2006.09253), (987.90594, 1271.23894),
    (1410.03022, 1272.49526), (2073.4596, 2003.7979)
])
dst = np.float32([
    (90, 500), (90, 200), (130, 200), (130, 500)
])
Matrix = cv2.getPerspectiveTransform(src, dst)

warped_image = cv2.warpPerspective(image, Matrix, (300, 500))
Your Image

車道線定位

假設已經獲得了車道線的分割影像,並將其轉換為鳥瞰圖。

Your Image

現在有了車道線分割圖的鳥瞰圖,那麼如何確定當前有幾條車道線以及車道線所處的位置呢?

可以對鳥瞰圖進行垂直方向的累加投影。理論上,有幾個峰值就有幾條車道線,而峰值點的位置即為車道線的位置座標。

Your Image

從上圖可以看出,一共有四條車道線,且車道線的大致位置也是已知的。之後可以透過滑動視窗法,以峰值點為起點對車道線的點進行搜尋。

滑動視窗法的工作原理如下:

  1. 設定視窗大小
    • 確定視窗的寬度和高度,通常是矩形區域。
    • 視窗的高度可以根據影像的大小和問題的特定要求進行調整。
  2. 滑動視窗
    • 從影像底部開始,以固定步長(通常是一個視窗的高度)向上滑動視窗。
    • 對於每個視窗,統計視窗內的非零畫素的個數
  3. 更新視窗
    • 若視窗內的非零畫素數量超過閾值,更新視窗中心位置為當前視窗內非零畫素的平均橫座標。
  4. 擬合曲線
    • 針對每個滑動視窗內的非零畫素,使用 np.polyfit 對這些點進行二階多項式擬合,得到曲線的係數。
def finding_line(warped_mask, x_points, sliding_window_num=9, margin=15, min_pixels_threshold=50):
    # 獲取影像的高度和寬度
    height, width = warped_mask.shape

    # 獲取影像中所有非零畫素的座標
    nonzero_y, nonzero_x = np.nonzero(warped_mask)

    # 計算滑動視窗的高度
    sliding_window_height = height // sliding_window_num

    # 用於儲存每個滑動視窗內的畫素索引
    line_pixel_indexes = [[] for _ in range(len(x_points))]

    # 遍歷滑動視窗
    for i in range(sliding_window_num):
        for idx, x_point in enumerate(x_points):
            # 確定視窗在y軸上的邊界
            top, bottom = height - (i + 1) * sliding_window_height, height - i * sliding_window_height

            # 確定視窗在x軸上的邊界
            left, right = x_point - margin, x_point + margin

            # 獲取視窗內的非零畫素索引
            window_pixel_indexes = ((nonzero_y >= top) & (nonzero_y < bottom) &
                                    (nonzero_x >= left) & (nonzero_x < right)).nonzero()[0]

            # 儲存當前視窗內的畫素索引
            line_pixel_indexes[idx].append(window_pixel_indexes)

            # 如果畫素數量足夠,更新視窗中心位置
            if len(window_pixel_indexes) > min_pixels_threshold:
                x_point = int(np.mean(nonzero_x[window_pixel_indexes]))

    # 用於儲存擬合的曲線係數
    lines = []

    # 處理每個滑動視窗的畫素索引
    for line_pixel_index in line_pixel_indexes:
        # 合併畫素索引
        line_pixel_index = np.concatenate(line_pixel_index)

        # 提取座標
        line_x, line_y = nonzero_x[line_pixel_index], nonzero_y[line_pixel_index]

        # 使用多項式擬合曲線,並將結果新增到lines中
        lines.append(np.polyfit(line_y, line_x, 2))

    return lines
Your Image

上述為擬合後的車道線在鳥瞰圖上的效果。

複雜情況

Your Image

然而,上述結果是在理想條件下(車道線分割結果準確無誤、車道線曲率不大)得到的結果。當情況複雜時,直接以峰值點作為車道線的個數以及大致位置的方式可能行不通。

Your Image Your Image

從上圖可以發現,實際共有5條車道線,但得到了10個峰值點,且擬合出的10條曲線有3條是重疊的(紅色、棕色分別重疊2、1次)。

根據這些資訊,可以採取兩種解決辦法:

  • 在擬合前進行過濾
  • 在擬合後進行過濾

在擬合前進行過濾

透過直方圖不難看出,車道線的間距在20~30畫素,且每條車道線的峰值畫素個數不小於50。可以根據這些關係對資料進行過濾。

Your Image

在擬合後進行過濾

由於使用二階多項式對車道線擬合, 而二階多項式係數在二次多項式方程中具有幾何意義,這個方程一般表示為:

\[f(x) = ax^2 + bx + c \]

其中,\(a\), \(b\), 和 \(c\) 是係數,決定了二次多項式的形狀。係數的組合產生了不同形狀和位置的二次曲線,反映了二次多項式方程在平面上的幾何特徵。

上文我們已經知道了車道線的間距在20~30畫素,可以透過比較相鄰兩二次多項式在 \(0 < f(x) < \text{{height}}\) 的情況下,以x的最大值作差,作為兩車道線的間距。若間距小於20,則代表是一條車道線,保留其中係數 b 最接近於0的(曲率最小的)作為車道線。

Your Image

將擬合後的車道線投影到原圖上

在完成車道線的擬合後,可以將擬合出的車道線投影回原始影像中。這個過程涉及逆透視變換,將鳥瞰圖上的車道線投影回原始影像上。

Your Image

相關文章