用霍夫變換&SCNN碼一個車道追蹤器

機器之心發表於2019-02-10

車道標誌線向人類駕駛員指示了車道的位置,並作為導向參考來引導車輛的方向,但是無人駕駛汽車是如何「認路」的呢?這要依賴於它們識別和追蹤車道的能力。這項能力對於開發無人駕駛車輛的演算法來說至關重要。本教程將講解如何用計算機視覺技術構建車道追蹤器。

本教程使用霍夫變換和 SCNN 兩種方法來完成這項任務。

用霍夫變換&SCNN碼一個車道追蹤器

方法 1:霍夫變換

大多數車道都設計得相對簡單,這不僅是為了鼓勵有序,還為了讓人類駕駛員更容易以相同的速度駕駛車輛。因此,我們的方法可能會通過邊緣檢測特徵提取技術先檢測出攝像機饋送回來的直線。我們將用 OpenCV(一個開源的用來實現計算機視覺演算法的庫)。下圖是我們的方法流程的概述。

用霍夫變換&SCNN碼一個車道追蹤器

1. 配置你的環境

如果你還沒有安裝 OpenCV,開啟終端並執行:

pip install opencv-python

執行下列命令列,把教程所用的庫克隆下來:

git clone https://github.com/chuanenlin/lane-detector.git

接著用文字編輯器開啟 detector.py。這個 Python 檔案中有這一節所有的程式碼。

2. 處理視訊

將我們的樣本視訊以 10 毫秒為間隔變成一組連續的幀(影象)用於車道檢測。可以隨時按下「q」鍵退出程式。

import cv2 as cv

# The video feed is read in as a VideoCapture object
cap = cv.VideoCapture("input.mp4")
while (cap.isOpened()):
    # ret = a boolean return value from getting the frame, frame = the current frame being projected in the video
    ret, frame = cap.read()
    # Frames are read by intervals of 10 milliseconds. The programs breaks out of the while loop when the user presses the 'q' key
    if cv.waitKey(10) & 0xFF == ord('q'):
        break

# The following frees up resources and closes all windows
cap.release()
cv.destroyAllWindows()

3. 用 Canny 檢測器

Canny 檢測器是一種可以快速進行實時邊緣檢測的多階段優化演算法。該演算法的基本目標是檢測亮度的急劇變化(大梯度),比如從白色到黑色的變化,在給出一組閾值的情況下將它們定義為邊。Canny 演算法主要有 4 個階段:

A. 降噪

和所有邊緣檢測演算法一樣,噪聲是導致錯誤檢測的關鍵問題。用 5*5 的高斯濾波器卷積(平滑)影象,從而降低檢測器對噪聲的敏感度。這是通過在整個影象上執行數字正態分佈的核(本例中是 5*5 的核)來實現的,將每個畫素的值設定為相鄰畫素值的加權平均值。

用霍夫變換&SCNN碼一個車道追蹤器

5*5 的高斯核。星號表示卷積運算。

B.強度梯度

然後在平滑的影象上用 Sobel、Roberts 或 Prewitt 核(Sobel 用在 OpenCV 中)沿 x 軸和 y 軸檢測邊緣是水平的、垂直的還是呈對角線的。

用霍夫變換&SCNN碼一個車道追蹤器

用於計算水平方向和垂直方向的一階導數的 Sobel 核。

C.非極大值抑制

非極大值抑制用於「細化」和有效地銳化邊緣。檢查每個畫素的值在先前計算的梯度方向上是否為區域性最大值。

用霍夫變換&SCNN碼一個車道追蹤器

三個點上的非極大值抑制。

A 在垂直方向的邊上。由於梯度方向垂直於邊的方向,比較 A 的畫素值和 B、C 的畫素值來確定 A 是否是區域性最大值。如果 A 是區域性最大值,則在下一個點上測試非極大值抑制,否則將 A 的畫素值設定為 0 並抑制 A。

D. 滯後閾值

在非極大值抑制後,確認強畫素在最終的邊緣對映中。但還要對弱畫素進行進一步分析來確定它是邊緣還是噪聲。利用預先定義的 minVal 和 maxVal 閾值,我們認為強度梯度高於 maxVal 的是邊緣,低於 minVal 的不是邊緣並將其刪除。強度梯度在 minVal 和 maxVal 之間的畫素只有在和梯度高於 maxVal 的畫素相連時才是邊緣。

用霍夫變換&SCNN碼一個車道追蹤器

滯後閾值在兩條線上的例子。

A 邊高於 maxVal,所以是邊。B 邊在 maxVal 和 minVal 之間但沒有和任何高於 maxVal 的邊相連,所以刪除。C 邊在 maxVal 和 minVal 之間,且與 A 邊(高於 maxVal)相連,所以是邊。

根據上述流程,首先要灰度化我們的幀,因為我們只需用亮度通道來檢測邊緣,還要用 5*5 的高斯模糊來減少噪聲從而避免判斷出錯誤的邊。

# import cv2 as cv

def do_canny(frame):
    # Converts frame to grayscale because we only need the luminance channel for detecting edges - less computationally expensive
    gray = cv.cvtColor(frame, cv.COLOR_RGB2GRAY)
    # Applies a 5x5 gaussian blur with deviation of 0 to frame - not mandatory since Canny will do this for us
    blur = cv.GaussianBlur(gray, (5, 5), 0)
    # Applies Canny edge detector with minVal of 50 and maxVal of 150
    canny = cv.Canny(blur, 50, 150)
    return canny

# cap = cv.VideoCapture("input.mp4")
# while (cap.isOpened()):
#     ret, frame = cap.read()

    canny = do_canny(frame)

#     if cv.waitKey(10) & 0xFF == ord('q'):
#         break

# cap.release()
# cv.destroyAllWindows()

4. 分割車道區域

我們要手動製作一個三角形掩碼來分割幀中的車道區域,刪除幀中的不相關區域以便提高後期的效率。

用霍夫變換&SCNN碼一個車道追蹤器

三角形掩碼通過三個座標定義,用圖中的綠色圓圈表示。

# import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt

# def do_canny(frame):
#     gray = cv.cvtColor(frame, cv.COLOR_RGB2GRAY)
#     blur = cv.GaussianBlur(gray, (5, 5), 0)
#     canny = cv.Canny(blur, 50, 150)
#     return canny

def do_segment(frame):
    # Since an image is a multi-directional array containing the relative intensities of each pixel in the image, we can use frame.shape to return a tuple: [number of rows, number of columns, number of channels] of the dimensions of the frame
    # frame.shape[0] give us the number of rows of pixels the frame has. Since height begins from 0 at the top, the y-coordinate of the bottom of the frame is its height
    height = frame.shape[0]
    # Creates a triangular polygon for the mask defined by three (x, y) coordinates
    polygons = np.array([
                            [(0, height), (800, height), (380, 290)]
                        ])
    # Creates an image filled with zero intensities with the same dimensions as the frame
    mask = np.zeros_like(frame)
    # Allows the mask to be filled with values of 1 and the other areas to be filled with values of 0
    cv.fillPoly(mask, polygons, 255)
    # A bitwise and operation between the mask and frame keeps only the triangular area of the frame
    segment = cv.bitwise_and(frame, mask)
    return segment

# cap = cv.VideoCapture("input.mp4")
# while (cap.isOpened()):
#     ret, frame = cap.read()
    # canny = do_canny(frame)

    # First, visualize the frame to figure out the three coordinates defining the triangular mask
    plt.imshow(frame)
    plt.show()
    segment = do_segment(canny)

#     if cv.waitKey(10) & 0xFF == ord('q'):
#         break

# cap.release()
# cv.destroyAllWindows()

5. 霍夫變換

在笛卡爾座標系中,我們可以通過繪製 y 對 x 的影象來表示 y=mx+b。但在霍夫空間中,我們也可以通過繪製 b 對 m 的影象將這條線表示為一個點。例如,直線方程 y=2x+1 在霍夫空間中可能是用 (2, 1) 表示的。

用霍夫變換&SCNN碼一個車道追蹤器

現在,我們要在笛卡爾座標系中繪製一個點而不是一條線。可能會有許多條線經過這個點,每條線的引數 m 和 b 的值都不同。例如,經過 (2,12) 的線可能有 y=2x+8、y=3x+6、y=4x+4、y=5x+2 以及 y=6x 等。這些線在霍夫空間中表示為 (2, 8)、(3, 6)、(4, 4)、(5, 2) 和 (6, 0)。注意,這在霍夫空間中可能會產生一條 b 對 m 的線。

用霍夫變換&SCNN碼一個車道追蹤器

每當我們在笛卡爾座標系中看到一系列點,並且知道這些點可以用線連線起來時,我們可以先按上述方法繪製出笛卡爾座標系中的每一個點在霍夫空間中的線,然後在霍夫空間中找到交點,就可以找到那條線的方程。霍夫空間中的交點表示通過這一系列點的直線的 m 值和 b 值。

用霍夫變換&SCNN碼一個車道追蹤器

由於我們傳入 Canny 檢測器的幀可能會被簡單地看成表示我們影象空間中邊的一系列白點,因此我們可以用相同的技術來識別哪些點應該被連成同一條線,以及這些點連線之後的線的方程是什麼,以便我們可以在幀中繪製出這條線。

為了便於解釋,我們用笛卡爾座標來對應霍夫空間。但這種方法存在一個數學上的缺陷:當這條線垂直時,梯度是無窮大的,無法在霍夫空間中表示出來。為了解決這個問題,我們用極座標代替。過程還是大致相同的,只是我們不在霍夫空間中繪製 b 對 m 的圖,我們要繪製的是 r 對 θ 的圖。

用霍夫變換&SCNN碼一個車道追蹤器

例如,對極座標系中的點 (8, 6)、(4, 9) 和 (12, 3),我們在霍夫空間中繪製出的相應影象如下:

用霍夫變換&SCNN碼一個車道追蹤器

我們可以看到,霍夫空間中的線相交於 θ=0.925,r=9.6 處。極座標系中的線是由 y = xcosθ + ysinθ 定義的,因此我們可以將所有穿過這一點的一條線定義為 9.6 = xcos0.925 + ysin0.925。

一般而言,在霍夫空間中相交的曲線越多,意味著用交點表示的線對應的點越多。在實現中,我們在霍夫空間中定義了交點的最小閾值,以便檢測線。因此,霍夫變換基本上跟蹤了幀中的每個點的霍夫空間交點。如果交點數量超過了定義的閾值,我們就確定一條對應引數 θ 和 r 的線。

我們用霍夫變換來識別兩條直線——車道的左右邊界。

# import cv2 as cv
# import numpy as np
# # import matplotlib.pyplot as plt

# def do_canny(frame):
#     gray = cv.cvtColor(frame, cv.COLOR_RGB2GRAY)
#     blur = cv.GaussianBlur(gray, (5, 5), 0)
#     canny = cv.Canny(blur, 50, 150)
#     return canny

# def do_segment(frame):
#     height = frame.shape[0]
#     polygons = np.array([
#                             [(0, height), (800, height), (380, 290)]
#                         ])
#     mask = np.zeros_like(frame)
#     cv.fillPoly(mask, polygons, 255)
#     segment = cv.bitwise_and(frame, mask)
#     return segment

# cap = cv.VideoCapture("input.mp4")
# while (cap.isOpened()):
#     ret, frame = cap.read()
#     canny = do_canny(frame)
#     # plt.imshow(frame)
#     # plt.show()
#     segment = do_segment(canny)

    # cv.HoughLinesP(frame, distance resolution of accumulator in pixels (larger = less precision), angle resolution of accumulator in radians (larger = less precision), threshold of minimum number of intersections, empty placeholder array, minimum length of line in pixels, maximum distance in pixels between disconnected lines)
    hough = cv.HoughLinesP(segment, 2, np.pi / 180, 100, np.array([]), minLineLength = 100, maxLineGap = 50)

#     if cv.waitKey(10) & 0xFF == ord('q'):
#         break

# cap.release()
# cv.destroyAllWindows()

6. 視覺化

將車道視覺化為兩個淺綠色的、線性擬合的多項式,將它們疊加在我們的輸入幀上。

# import cv2 as cv
# import numpy as np
# # import matplotlib.pyplot as plt

# def do_canny(frame):
#     gray = cv.cvtColor(frame, cv.COLOR_RGB2GRAY)
#     blur = cv.GaussianBlur(gray, (5, 5), 0)
#     canny = cv.Canny(blur, 50, 150)
#     return canny

# def do_segment(frame):
#     height = frame.shape[0]
#     polygons = np.array([
#                             [(0, height), (800, height), (380, 290)]
#                         ])
#     mask = np.zeros_like(frame)
#     cv.fillPoly(mask, polygons, 255)
#     segment = cv.bitwise_and(frame, mask)
#     return segment

def calculate_lines(frame, lines):
    # Empty arrays to store the coordinates of the left and right lines
    left = []
    right = []
    # Loops through every detected line
    for line in lines:
        # Reshapes line from 2D array to 1D array
        x1, y1, x2, y2 = line.reshape(4)
        # Fits a linear polynomial to the x and y coordinates and returns a vector of coefficients which describe the slope and y-intercept
        parameters = np.polyfit((x1, x2), (y1, y2), 1)
        slope = parameters[0]
        y_intercept = parameters[1]
        # If slope is negative, the line is to the left of the lane, and otherwise, the line is to the right of the lane
        if slope < 0:
            left.append((slope, y_intercept))
        else:
            right.append((slope, y_intercept))
    # Averages out all the values for left and right into a single slope and y-intercept value for each line
    left_avg = np.average(left, axis = 0)
    right_avg = np.average(right, axis = 0)
    # Calculates the x1, y1, x2, y2 coordinates for the left and right lines
    left_line = calculate_coordinates(frame, left_avg)
    right_line = calculate_coordinates(frame, right_avg)
    return np.array([left_line, right_line])

def calculate_coordinates(frame, parameters):
    slope, intercept = parameters
    # Sets initial y-coordinate as height from top down (bottom of the frame)
    y1 = frame.shape[0]
    # Sets final y-coordinate as 150 above the bottom of the frame
    y2 = int(y1 - 150)
    # Sets initial x-coordinate as (y1 - b) / m since y1 = mx1 + b
    x1 = int((y1 - intercept) / slope)
    # Sets final x-coordinate as (y2 - b) / m since y2 = mx2 + b
    x2 = int((y2 - intercept) / slope)
    return np.array([x1, y1, x2, y2])

def visualize_lines(frame, lines):
    # Creates an image filled with zero intensities with the same dimensions as the frame
    lines_visualize = np.zeros_like(frame)
    # Checks if any lines are detected
    if lines is not None:
        for x1, y1, x2, y2 in lines:
            # Draws lines between two coordinates with green color and 5 thickness
            cv.line(lines_visualize, (x1, y1), (x2, y2), (0, 255, 0), 5)
    return lines_visualize

# cap = cv.VideoCapture("input.mp4")
# while (cap.isOpened()):
#     ret, frame = cap.read()
#     canny = do_canny(frame)
#     # plt.imshow(frame)
#     # plt.show()
#     segment = do_segment(canny)
#     hough = cv.HoughLinesP(segment, 2, np.pi / 180, 100, np.array([]), minLineLength = 100, maxLineGap = 50)

    # Averages multiple detected lines from hough into one line for left border of lane and one line for right border of lane
    lines = calculate_lines(frame, hough)
    # Visualizes the lines
    lines_visualize = visualize_lines(frame, lines)
    # Overlays lines on frame by taking their weighted sums and adding an arbitrary scalar value of 1 as the gamma argument
    output = cv.addWeighted(frame, 0.9, lines_visualize, 1, 1)
    # Opens a new window and displays the output frame
    cv.imshow("output", output)

#     if cv.waitKey(10) & 0xFF == ord('q'):
#         break

# cap.release()
# cv.destroyAllWindows()

現在,開啟終端並執行 python detector.py 來測試你的車道檢測器。以防你遺失了任何程式碼,下面是帶有註釋的完整的解決方案:

import cv2 as cv
import numpy as np
# import matplotlib.pyplot as plt

def do_canny(frame):
    # Converts frame to grayscale because we only need the luminance channel for detecting edges - less computationally expensive
    gray = cv.cvtColor(frame, cv.COLOR_RGB2GRAY)
    # Applies a 5x5 gaussian blur with deviation of 0 to frame - not mandatory since Canny will do this for us
    blur = cv.GaussianBlur(gray, (5, 5), 0)
    # Applies Canny edge detector with minVal of 50 and maxVal of 150
    canny = cv.Canny(blur, 50, 150)
    return canny

def do_segment(frame):
    # Since an image is a multi-directional array containing the relative intensities of each pixel in the image, we can use frame.shape to return a tuple: [number of rows, number of columns, number of channels] of the dimensions of the frame
    # frame.shape[0] give us the number of rows of pixels the frame has. Since height begins from 0 at the top, the y-coordinate of the bottom of the frame is its height
    height = frame.shape[0]
    # Creates a triangular polygon for the mask defined by three (x, y) coordinates
    polygons = np.array([
                            [(0, height), (800, height), (380, 290)]
                        ])
    # Creates an image filled with zero intensities with the same dimensions as the frame
    mask = np.zeros_like(frame)
    # Allows the mask to be filled with values of 1 and the other areas to be filled with values of 0
    cv.fillPoly(mask, polygons, 255)
    # A bitwise and operation between the mask and frame keeps only the triangular area of the frame
    segment = cv.bitwise_and(frame, mask)
    return segment

def calculate_lines(frame, lines):
    # Empty arrays to store the coordinates of the left and right lines
    left = []
    right = []
    # Loops through every detected line
    for line in lines:
        # Reshapes line from 2D array to 1D array
        x1, y1, x2, y2 = line.reshape(4)
        # Fits a linear polynomial to the x and y coordinates and returns a vector of coefficients which describe the slope and y-intercept
        parameters = np.polyfit((x1, x2), (y1, y2), 1)
        slope = parameters[0]
        y_intercept = parameters[1]
        # If slope is negative, the line is to the left of the lane, and otherwise, the line is to the right of the lane
        if slope < 0:
            left.append((slope, y_intercept))
        else:
            right.append((slope, y_intercept))
    # Averages out all the values for left and right into a single slope and y-intercept value for each line
    left_avg = np.average(left, axis = 0)
    right_avg = np.average(right, axis = 0)
    # Calculates the x1, y1, x2, y2 coordinates for the left and right lines
    left_line = calculate_coordinates(frame, left_avg)
    right_line = calculate_coordinates(frame, right_avg)
    return np.array([left_line, right_line])

def calculate_coordinates(frame, parameters):
    slope, intercept = parameters
    # Sets initial y-coordinate as height from top down (bottom of the frame)
    y1 = frame.shape[0]
    # Sets final y-coordinate as 150 above the bottom of the frame
    y2 = int(y1 - 150)
    # Sets initial x-coordinate as (y1 - b) / m since y1 = mx1 + b
    x1 = int((y1 - intercept) / slope)
    # Sets final x-coordinate as (y2 - b) / m since y2 = mx2 + b
    x2 = int((y2 - intercept) / slope)
    return np.array([x1, y1, x2, y2])

def visualize_lines(frame, lines):
    # Creates an image filled with zero intensities with the same dimensions as the frame
    lines_visualize = np.zeros_like(frame)
    # Checks if any lines are detected
    if lines is not None:
        for x1, y1, x2, y2 in lines:
            # Draws lines between two coordinates with green color and 5 thickness
            cv.line(lines_visualize, (x1, y1), (x2, y2), (0, 255, 0), 5)
    return lines_visualize

# The video feed is read in as a VideoCapture object
cap = cv.VideoCapture("input.mp4")
while (cap.isOpened()):
    # ret = a boolean return value from getting the frame, frame = the current frame being projected in the video
    ret, frame = cap.read()
    canny = do_canny(frame)
    cv.imshow("canny", canny)
    # plt.imshow(frame)
    # plt.show()
    segment = do_segment(canny)
    hough = cv.HoughLinesP(segment, 2, np.pi / 180, 100, np.array([]), minLineLength = 100, maxLineGap = 50)
    # Averages multiple detected lines from hough into one line for left border of lane and one line for right border of lane
    lines = calculate_lines(frame, hough)
    # Visualizes the lines
    lines_visualize = visualize_lines(frame, lines)
    cv.imshow("hough", lines_visualize)
    # Overlays lines on frame by taking their weighted sums and adding an arbitrary scalar value of 1 as the gamma argument
    output = cv.addWeighted(frame, 0.9, lines_visualize, 1, 1)
    # Opens a new window and displays the output frame
    cv.imshow("output", output)
    # Frames are read by intervals of 10 milliseconds. The programs breaks out of the while loop when the user presses the 'q' key
    if cv.waitKey(10) & 0xFF == ord('q'):
        break
# The following frees up resources and closes all windows
cap.release()
cv.destroyAllWindows()

 方法 2:空間 CNN

方法 1 中這種手動的傳統方法對於清晰且筆直的道路來說效果還不錯。但很明顯當遇到彎道或急轉彎時這種方法會失效。此外,我們注意到車道上有由直線組成的標記,比如繪製的箭頭標誌,可能會不時地擾亂車道檢測器,這從演示視訊中可以看得出來。解決這個問題的一種方法可能是將三角掩碼進一步細化為兩個獨立的、更精確的掩碼。但這些相當隨意的掩碼引數還是無法適應變化多端的道路環境。另一個缺點是車道檢測器可能會忽略只有點狀標記或者根本沒有清晰標記的車道,因為缺乏滿足霍夫變換閾值的連續直線。最後,天氣和照明條件也會影響線路可見度。

1. 架構

儘管卷積神經網路(CNN)可以有效地識別較低層次影象的簡單特徵(例如邊和顏色梯度)以及更高等級的複雜特徵和實體(例如目標識別),但它們很難表示這些特徵和實體的「姿勢」——也就是說,CNN 可以從原始畫素中提取語義,但無法捕獲影象中畫素的空間關係(例如旋轉關係和平移關係)。但車道檢測是具有強形狀先驗但弱外觀相干性的任務,因此這些空間關係對車道檢測任務來說至關重要。

例如,只通過提取語特徵義是很難判斷交通標誌的,因為它們缺少清晰且連貫的外觀線索,而且經常被遮擋。

用霍夫變換&SCNN碼一個車道追蹤器

用霍夫變換&SCNN碼一個車道追蹤器

左上角圖片中右邊的車和左下角圖片中右邊的摩托車遮擋了右邊的車道標記,對 CNN 的結果產生了負面影響。

 由於我們知道在交通道路上一般會出現類似於物體被垂直放在道路兩邊這樣的空間關係,因此我們瞭解到加強空間資訊的重要性。檢測車道的情況也是類似的。

為了解決這個問題,空間 CNN(SCNN)給出了一個架構,這個架構可以「將經典的深度逐層卷積在特徵對映下推廣到逐片(slice-by-slice)卷積」。這是什麼意思?在經典的層到層的 CNN 中,每一個卷積層都從前面的一層接收輸入,應用卷積和非線性啟用後,將輸出傳遞給後面的層。SCNN 將各個特徵對映行和列視為「層」,進一步應用這一步驟,按順序進行相同的過程(這裡的按順序指的是隻有當這一片從前面的一片中接收到資訊才會將資訊傳遞給後面一片),這個過程允許畫素資訊在同一層的不同神經元之間傳遞,可以有效增強空間資訊。

用霍夫變換&SCNN碼一個車道追蹤器

SCNN 是相對較新的、2018 年早些時候才釋出的方法,但已經超越了像 ReNet(RNN)、MRFNet(MRF+CNN)這樣更深的 ResNet 架構,以 96.53% 的準確率贏得了 TuSimple 基準車道檢測挑戰賽的冠軍。

用霍夫變換&SCNN碼一個車道追蹤器

此外,除了 SCNN,作者還發布了 CULane 資料集,這是一個包括交通車道註釋的大型資料集。CULane 資料集還包括許多極具挑戰性的場景,包括遮擋情況和光照條件不同的情況。

用霍夫變換&SCNN碼一個車道追蹤器

2. 模型

車道檢測需要精確的畫素識別和車道曲線預測。SCNN 的作者並未直接訓練車道並進行聚類,而是先將藍色、綠色、紅色和黃色的車道分別標記為四類。模型針對每一條曲線輸出了概率對映(probmaps),和語義分割任務相似,然後通過小型網路傳遞了 probmaps,並預測最終的 cubic spines。該模型基於 DeepLab-LargeFOV 模型的變體。

用霍夫變換&SCNN碼一個車道追蹤器

每條車道用超過 0.5 的存在值標記,以 20 行為間隔搜尋相應的 probmap 以找到響應最高的位置。為了確定是否檢測到了車道標記,計算真實資料(正確的標籤)和預測值間的 IoU,將高於設定閾值的 IoU 評估為真正(TP)樣本,用來計算精度和召回率。

3. 測試和訓練

你可以按照這個庫(https://github.com/XingangPan/SCNN)中的程式碼來重現 SCNN 論文中的結果,也可以用 CULane 資料集來測試你自己的模型。


原文連結:https://towardsdatascience.com/tutorial-build-a-lane-detector-679fd8953132

相關文章