選自towardsdatascience
作者:Chuan-en Lin
機器之心編譯
參與:李詩萌、張倩
車道標(biāo)志線向人類駕駛員指示了車道的位置,并作為導(dǎo)向參考來引導(dǎo)車輛的方向,但是無人駕駛汽車是如何「認(rèn)路」的呢?這要依賴于它們識別和追蹤車道的能力。這項能力對于開發(fā)無人駕駛車輛的算法來說至關(guān)重要。本教程將講解如何用計算機視覺技術(shù)構(gòu)建車道追蹤器。
本教程使用霍夫變換和 SCNN 兩種方法來完成這項任務(wù)。
方法 1:霍夫變換
大多數(shù)車道都設(shè)計得相對簡單,這不僅是為了鼓勵有序,還為了讓人類駕駛員更容易以相同的速度駕駛車輛。因此,我們的方法可能會通過邊緣檢測和特征提取技術(shù)先檢測出攝像機饋送回來的直線。我們將用 OpenCV(一個開源的用來實現(xiàn)計算機視覺算法的庫)。下圖是我們的方法流程的概述。
在開始之前,這里有一個結(jié)果的演示視頻:
1. 配置你的環(huán)境
如果你還沒有安裝 OpenCV,打開終端并運行:
pip install opencv-python
運行下列命令行,把教程所用的庫克隆下來:
git clone https://github.com/chuanenlin/lane-detector.git
接著用文本編輯器打開 detector.py。這個 Python 文件中有這一節(jié)所有的代碼。
2. 處理視頻
將我們的樣本視頻以 10 毫秒為間隔變成一組連續(xù)的幀(圖像)用于車道檢測。可以隨時按下「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 檢測器是一種可以快速進(jìn)行實時邊緣檢測的多階段優(yōu)化算法。該算法的基本目標(biāo)是檢測亮度的急劇變化(大梯度),比如從白色到黑色的變化,在給出一組閾值的情況下將它們定義為邊。Canny 算法主要有 4 個階段:
A. 降噪
和所有邊緣檢測算法一樣,噪聲是導(dǎo)致錯誤檢測的關(guān)鍵問題。用 5*5 的高斯濾波器卷積(平滑)圖像,從而降低檢測器對噪聲的敏感度。這是通過在整個圖像上運行數(shù)字正態(tài)分布的核(本例中是 5*5 的核)來實現(xiàn)的,將每個像素的值設(shè)置為相鄰像素值的加權(quán)平均值。
5*5 的高斯核。星號表示卷積運算。
B.強度梯度
然后在平滑的圖像上用 Sobel、Roberts 或 Prewitt 核(Sobel 用在 OpenCV 中)沿 x 軸和 y 軸檢測邊緣是水平的、垂直的還是呈對角線的。
用于計算水平方向和垂直方向的一階導(dǎo)數(shù)的 Sobel 核。
C.非極大值抑制
非極大值抑制用于「細(xì)化」和有效地銳化邊緣。檢查每個像素的值在先前計算的梯度方向上是否為局部最大值。
三個點上的非極大值抑制。
A 在垂直方向的邊上。由于梯度方向垂直于邊的方向,比較 A 的像素值和 B、C 的像素值來確定 A 是否是局部最大值。如果 A 是局部最大值,則在下一個點上測試非極大值抑制,否則將 A 的像素值設(shè)置為 0 并抑制 A。
D. 滯后閾值
在非極大值抑制后,確認(rèn)強像素在最終的邊緣映射中。但還要對弱像素進(jìn)行進(jìn)一步分析來確定它是邊緣還是噪聲。利用預(yù)先定義的 minVal 和 maxVal 閾值,我們認(rèn)為強度梯度高于 maxVal 的是邊緣,低于 minVal 的不是邊緣并將其刪除。強度梯度在 minVal 和 maxVal 之間的像素只有在和梯度高于 maxVal 的像素相連時才是邊緣。
滯后閾值在兩條線上的例子。
A 邊高于 maxVal,所以是邊。B 邊在 maxVal 和 minVal 之間但沒有和任何高于 maxVal 的邊相連,所以刪除。C 邊在 maxVal 和 minVal 之間,且與 A 邊(高于 maxVal)相連,所以是邊。
根據(jù)上述流程,首先要灰度化我們的幀,因為我們只需用亮度通道來檢測邊緣,還要用 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. 分割車道區(qū)域
我們要手動制作一個三角形掩碼來分割幀中的車道區(qū)域,刪除幀中的不相關(guān)區(qū)域以便提高后期的效率。
三角形掩碼通過三個坐標(biāo)定義,用圖中的綠色圓圈表示。
# 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. 霍夫變換
在笛卡爾坐標(biāo)系中,我們可以通過繪制 y 對 x 的圖像來表示 y=mx+b。但在霍夫空間中,我們也可以通過繪制 b 對 m 的圖像將這條線表示為一個點。例如,直線方程 y=2x+1 在霍夫空間中可能是用 (2, 1) 表示的。
現(xiàn)在,我們要在笛卡爾坐標(biāo)系中繪制一個點而不是一條線??赡軙性S多條線經(jīng)過這個點,每條線的參數(shù) m 和 b 的值都不同。例如,經(jīng)過 (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)。注意,這在霍夫空間中可能會產(chǎn)生一條 b 對 m 的線。
每當(dāng)我們在笛卡爾坐標(biāo)系中看到一系列點,并且知道這些點可以用線連接起來時,我們可以先按上述方法繪制出笛卡爾坐標(biāo)系中的每一個點在霍夫空間中的線,然后在霍夫空間中找到交點,就可以找到那條線的方程?;舴蚩臻g中的交點表示通過這一系列點的直線的 m 值和 b 值。
由于我們傳入 Canny 檢測器的幀可能會被簡單地看成表示我們圖像空間中邊的一系列白點,因此我們可以用相同的技術(shù)來識別哪些點應(yīng)該被連成同一條線,以及這些點連接之后的線的方程是什么,以便我們可以在幀中繪制出這條線。
為了便于解釋,我們用笛卡爾坐標(biāo)來對應(yīng)霍夫空間。但這種方法存在一個數(shù)學(xué)上的缺陷:當(dāng)這條線垂直時,梯度是無窮大的,無法在霍夫空間中表示出來。為了解決這個問題,我們用極坐標(biāo)代替。過程還是大致相同的,只是我們不在霍夫空間中繪制 b 對 m 的圖,我們要繪制的是 r 對 θ 的圖。
例如,對極坐標(biāo)系中的點 (8, 6)、(4, 9) 和 (12, 3),我們在霍夫空間中繪制出的相應(yīng)圖像如下:
我們可以看到,霍夫空間中的線相交于 θ=0.925,r=9.6 處。極坐標(biāo)系中的線是由 y = xcosθ + ysinθ 定義的,因此我們可以將所有穿過這一點的一條線定義為 9.6 = xcos0.925 + ysin0.925。
一般而言,在霍夫空間中相交的曲線越多,意味著用交點表示的線對應(yīng)的點越多。在實現(xiàn)中,我們在霍夫空間中定義了交點的最小閾值,以便檢測線。因此,霍夫變換基本上跟蹤了幀中的每個點的霍夫空間交點。如果交點數(shù)量超過了定義的閾值,我們就確定一條對應(yīng)參數(shù) θ 和 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()
現(xiàn)在,打開終端并運行 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 中這種手動的傳統(tǒng)方法對于清晰且筆直的道路來說效果還不錯。但很明顯當(dāng)遇到彎道或急轉(zhuǎn)彎時這種方法會失效。此外,我們注意到車道上有由直線組成的標(biāo)記,比如繪制的箭頭標(biāo)志,可能會不時地擾亂車道檢測器,這從演示視頻中可以看得出來。解決這個問題的一種方法可能是將三角掩碼進(jìn)一步細(xì)化為兩個獨立的、更精確的掩碼。但這些相當(dāng)隨意的掩碼參數(shù)還是無法適應(yīng)變化多端的道路環(huán)境。另一個缺點是車道檢測器可能會忽略只有點狀標(biāo)記或者根本沒有清晰標(biāo)記的車道,因為缺乏滿足霍夫變換閾值的連續(xù)直線。最后,天氣和照明條件也會影響線路可見度。
1. 架構(gòu)
盡管卷積神經(jīng)網(wǎng)絡(luò)(CNN)可以有效地識別較低層次圖像的簡單特征(例如邊和顏色梯度)以及更高等級的復(fù)雜特征和實體(例如目標(biāo)識別),但它們很難表示這些特征和實體的「姿勢」——也就是說,CNN 可以從原始像素中提取語義,但無法捕獲圖像中像素的空間關(guān)系(例如旋轉(zhuǎn)關(guān)系和平移關(guān)系)。但車道檢測是具有強形狀先驗但弱外觀相干性的任務(wù),因此這些空間關(guān)系對車道檢測任務(wù)來說至關(guān)重要。
例如,只通過提取語特征義是很難判斷交通標(biāo)志的,因為它們?nèi)鄙偾逦疫B貫的外觀線索,而且經(jīng)常被遮擋。
左上角圖片中右邊的車和左下角圖片中右邊的摩托車遮擋了右邊的車道標(biāo)記,對 CNN 的結(jié)果產(chǎn)生了負(fù)面影響。
由于我們知道在交通道路上一般會出現(xiàn)類似于物體被垂直放在道路兩邊這樣的空間關(guān)系,因此我們了解到加強空間信息的重要性。檢測車道的情況也是類似的。
為了解決這個問題,空間 CNN(SCNN)給出了一個架構(gòu),這個架構(gòu)可以「將經(jīng)典的深度逐層卷積在特征映射下推廣到逐片(slice-by-slice)卷積」。這是什么意思?在經(jīng)典的層到層的 CNN 中,每一個卷積層都從前面的一層接收輸入,應(yīng)用卷積和非線性激活后,將輸出傳遞給后面的層。SCNN 將各個特征映射行和列視為「層」,進(jìn)一步應(yīng)用這一步驟,按順序進(jìn)行相同的過程(這里的按順序指的是只有當(dāng)這一片從前面的一片中接收到信息才會將信息傳遞給后面一片),這個過程允許像素信息在同一層的不同神經(jīng)元之間傳遞,可以有效增強空間信息。
SCNN 是相對較新的、2018 年早些時候才發(fā)布的方法,但已經(jīng)超越了像 ReNet(RNN)、MRFNet(MRF+CNN)這樣更深的 ResNet 架構(gòu),以 96.53% 的準(zhǔn)確率贏得了 TuSimple 基準(zhǔn)車道檢測挑戰(zhàn)賽的冠軍。
此外,除了 SCNN,作者還發(fā)布了 CULane 數(shù)據(jù)集,這是一個包括交通車道注釋的大型數(shù)據(jù)集。CULane 數(shù)據(jù)集還包括許多極具挑戰(zhàn)性的場景,包括遮擋情況和光照條件不同的情況。
2. 模型
車道檢測需要精確的像素識別和車道曲線預(yù)測。SCNN 的作者并未直接訓(xùn)練車道并進(jìn)行聚類,而是先將藍(lán)色、綠色、紅色和黃色的車道分別標(biāo)記為四類。模型針對每一條曲線輸出了概率映射(probmaps),和語義分割任務(wù)相似,然后通過小型網(wǎng)絡(luò)傳遞了 probmaps,并預(yù)測最終的 cubic spines。該模型基于 DeepLab-LargeFOV 模型的變體。
每條車道用超過 0.5 的存在值標(biāo)記,以 20 行為間隔搜索相應(yīng)的 probmap 以找到響應(yīng)最高的位置。為了確定是否檢測到了車道標(biāo)記,計算真實數(shù)據(jù)(正確的標(biāo)簽)和預(yù)測值間的 IoU,將高于設(shè)定閾值的 IoU 評估為真正(TP)樣本,用來計算精度和召回率。
3. 測試和訓(xùn)練
你可以按照這個庫(https://github.com/XingangPan/SCNN)中的代碼來重現(xiàn) SCNN 論文中的結(jié)果,也可以用 CULane 數(shù)據(jù)集來測試你自己的模型。
原文鏈接:https://towardsdatascience.com/tutorial-build-a-lane-detector-679fd8953132