在沒有深度學(xué)習(xí)的情況下找到道路
點(diǎn)擊上方“小白學(xué)視覺”,選擇加"星標(biāo)"或“置頂”
重磅干貨,第一時(shí)間送達(dá)

今天我們將一起參與一個(gè)項(xiàng)目,使用python在圖像和視頻中找到車道。對(duì)于該項(xiàng)目,我們將采用手動(dòng)方法。盡管我們確實(shí)可以使用深度學(xué)習(xí)等技術(shù)獲得更好的結(jié)果,但學(xué)習(xí)概念、工作原理和基礎(chǔ)知識(shí)也很重要,這樣我們?cè)跇?gòu)建高級(jí)模型時(shí)就可以應(yīng)用我們已經(jīng)學(xué)到的知識(shí)。在使用深度學(xué)習(xí)時(shí),可能還需要我們介紹的一些步驟。
我們將采取的步驟如下:
計(jì)算相機(jī)校準(zhǔn)并解決失真。
應(yīng)用透視變換來(lái)校正二值圖像(“鳥瞰圖”)。
使用顏色變換、漸變等來(lái)創(chuàng)建閾值二值圖像。
檢測(cè)車道像素并擬合以找到車道邊界。
確定車道的曲率和車輛相對(duì)于中心的位置。
將檢測(cè)到的車道邊界變形回原始圖像。
輸出車道邊界的視覺顯示以及車道曲率和車輛位置的數(shù)值估計(jì)。
所有代碼和解釋都可以在我們的Github 中找到 。
今天的廉價(jià)針孔相機(jī)給圖像帶來(lái)了很多失真,兩種主要的畸變是徑向畸變和切向畸變。
由于徑向畸變,直線會(huì)顯得彎曲,當(dāng)我們遠(yuǎn)離圖像的中心時(shí),它的影響更大。例如,如下圖所示,棋盤的兩個(gè)邊緣用紅線標(biāo)記,但是我們可以看到邊框不是一條直線,并且與紅線不匹配。所有預(yù)期的直線都凸出來(lái)了。
相機(jī)失真示例
為了解決這個(gè)問(wèn)題,我們將使用OpenCV python 庫(kù),并使用目標(biāo)相機(jī)拍攝的示例圖像來(lái)制作棋盤。為什么是棋盤?在棋盤圖像中,我們可以很容易地測(cè)量失真,因?yàn)槲覀冎牢矬w的外觀,我們可以計(jì)算從源點(diǎn)到目標(biāo)點(diǎn)的距離,并使用它們來(lái)計(jì)算失真系數(shù),然后使用這些系數(shù)來(lái)修復(fù)圖像。
下圖顯示了來(lái)自輸出圖像和未失真結(jié)果圖像的示例:
修復(fù)相機(jī)失真
(所有這些效果都發(fā)生在lib/camera.py文件中),但它是如何工作的?該過(guò)程包括 3 個(gè)步驟:
對(duì)圖像進(jìn)行采樣:
在這一步中,我們識(shí)別定義棋盤格的角點(diǎn),以防我們找不到棋盤,或者棋盤不完整,我們將丟棄樣本圖像。
# first we convert the image to grayscalegray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)# Find the chessboard cornersret, corners = cv2.findChessboardCorners(gray, (9, 6), None)
在這一步中,我們從校準(zhǔn)模式的多個(gè)視圖中找到相機(jī)的內(nèi)在和外在參數(shù),然后我們可以使用它們來(lái)生成結(jié)果圖像。
img_size = (self._valid_images[0].shape[1], self._valid_images[0].shape[0])ret, self._mtx, self._dist, t, t2 = cv2.calibrateCamera(self._obj_points, self._img_points, img_size, None, None)
在這最后一步中,我們實(shí)際上通過(guò)根據(jù)校準(zhǔn)步驟中檢測(cè)到的參數(shù)補(bǔ)償鏡頭失真來(lái)生成最終圖像。
cv2.undistort(img, self._mtx, self._dist, None, self._mtx)該過(guò)程的下一步是更改圖像的視角,從安裝在汽車前部的常規(guī)攝像頭視圖變?yōu)楦┮晥D,也稱為“鳥瞰圖”。這是它的樣子:
未扭曲的圖像
這種轉(zhuǎn)換非常簡(jiǎn)單,我們?cè)谄聊簧先∷膫€(gè)我們知道的點(diǎn),然后將它們轉(zhuǎn)換為屏幕的期望位置。讓我們使用上圖的示例更詳細(xì)地回顧一下,在圖片中,我們看到一個(gè)繪制在頂部的綠色形狀,這個(gè)矩形使用四個(gè)源點(diǎn)作為角,它與相機(jī)的常規(guī)直線道路重疊。矩形圍繞圖像的中心切割,因?yàn)橥敢暿墙志巴ǔ=Y(jié)束的地方,以讓位于天空。現(xiàn)在我們把這些點(diǎn)移到屏幕上我們想要的位置,這就是將綠色區(qū)域轉(zhuǎn)換成一個(gè)矩形,從 0 到圖片的高度,下面是我們將在代碼中使用的源點(diǎn)和目標(biāo)點(diǎn):
height, width, color = img.shapesrc = np.float32([[],[],[],[]])dst = np.float32([[],[],[],[]])
src, dst = self._calc_warp_points(img)if self._M is None:self._M = cv2.getPerspectiveTransform(src, dst)self._M_inv = cv2.getPerspectiveTransform(dst, src)return cv2.warpPerspective(img, self._M, (width, height), flags=cv2.INTER_LINEAR)
現(xiàn)在我們已經(jīng)有了圖像,我們需要開始丟棄所有不相關(guān)的信息,只保留線條。為此,我們將應(yīng)用一系列更改,接下來(lái)將詳細(xì)介紹:
將彩色圖像轉(zhuǎn)換為灰度
return cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)通過(guò)使用高斯模糊對(duì)圖像進(jìn)行平滑,并將原始圖像加權(quán)到平滑后的圖像中,執(zhí)行一些次要但重要的增強(qiáng)
dst = cv2.GaussianBlur(img, (0, 0), 3)out = cv2.addWeighted(img, 1.5, dst, -0.5, 0)
計(jì)算 X 軸上顏色變化函數(shù)的導(dǎo)數(shù),并應(yīng)用閾值來(lái)過(guò)濾高強(qiáng)度顏色變化,當(dāng)我們使用灰度時(shí),這將是邊界。
sobel = cv2.Sobel(img, cv2.CV_64F, True, False)abs_sobel = np.absolute(sobel)scaled_sobel = np.uint8(255 * abs_sobel / np.max(abs_sobel))return (scaled_sobel >= 20) & (scaled_sobel <= 220)
現(xiàn)在我們計(jì)算新閾值的方向?qū)?shù)
# Calculate the x and y gradientssobel_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=sobel_kernel)sobel_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=sobel_kernel)# Take the absolute value of the x and y gradientsgradient_direction = np.arctan2(np.absolute(sobel_y), np.absolute(sobel_x))gradient_direction = np.absolute(gradient_direction)return (gradient_direction >= np.pi/6) & (gradient_direction <= np.pi*5/6)
接下來(lái),我們將它們組合成一個(gè)漸變
# combine the gradient and direction thresholds.gradient_condition = ((sx_condition == 1) & (dir_condition == 1))
此過(guò)濾器適用于原始圖像,我們嘗試僅獲取那些黃色/白色的像素(如道路線)
r_channel = img[:, :, 0]g_channel = img[:, :, 1]return (r_channel > thresh) & (g_channel > thresh)
對(duì)于此任務(wù),有必要更改顏色空間,特別是,我們將使用 HSL 顏色空間,因?yàn)樗鼘?duì)我們使用的圖像具有有趣的特征。
def _hls_condition(self, img, channel, thresh=(220, 255)):channels = {"h": 0,"l": 1,"s": 2}hls = cv2.cvtColor(img, cv2.COLOR_RGB2HLS)hls = hls[:, :, channels[channel]]return (hls > thresh[0]) & (hls <= thresh[1])
最后我們將所有這些組合成一個(gè)最終圖像:
grey = self._to_greyscale(img)grey = self._enhance(grey)# apply gradient threshold on the horizontal gradientsx_condition = self._sobel_gradient_condition(grey, 'x', 20, 220)# apply gradient direction threshold so that only edges closer to vertical are detected.dir_condition = self._directional_condition(grey, thresh=(np.pi/6, np.pi*5/6))# combine the gradient and direction thresholds.gradient_condition = ((sx_condition == 1) & (dir_condition == 1))# and color thresholdcolor_condition = self._color_condition(img, thresh=200)# now let's take the HSL thresholdl_hls_condition = self._hls_condition(img, channel='l', thresh=(120, 255))s_hls_condition = self._hls_condition(img, channel='s', thresh=(100, 255))combined_condition = (l_hls_condition | color_condition) & (s_hls_condition | gradient_condition)result = np.zeros_like(color_condition)result[combined_condition] = 1
我們的新圖像現(xiàn)在如下圖所示:

你已經(jīng)看到那里形成的線條了嗎?
到目前為止,我們已經(jīng)能夠創(chuàng)建一個(gè)由只包含車道特征的鳥瞰圖組成的圖像(至少在大多數(shù)情況下,我們?nèi)匀挥幸恍┰胍簦S辛诉@張新圖像,我們現(xiàn)在可以開始進(jìn)行一些計(jì)算,將圖像轉(zhuǎn)換為我們可以使用的實(shí)際值,例如車道位置和曲率。
讓我們首先識(shí)別圖像上的像素并構(gòu)建一個(gè)表示車道函數(shù)的多項(xiàng)式。我們打算怎么做?事實(shí)證明,有一個(gè)非常好的方法,使用圖像下半部分的直方圖。下面是直方圖的示例:
直方圖
圖像上的峰值幫助我們識(shí)別車道的左側(cè)和右側(cè),以下是在代碼上構(gòu)建直方圖的方式:
# Take a histogram of the bottom half of the imagehistogram = np.sum(binary_warped[binary_warped.shape[0] // 2:, :], axis=0)# Find the peak of the left and right halves of the histogram# These will be the starting point for the left and right linesmidpoint = np.int(histogram.shape[0] // 2)left_x_base = np.argmax(histogram[:midpoint])right_x_base = np.argmax(histogram[midpoint:]) + midpoint
但我們可能會(huì)問(wèn),為什么只有下半部分?答案是我們只想關(guān)注緊挨著汽車的路段,因?yàn)檐嚨揽赡軙?huì)形成一條曲線,這會(huì)影響我們的直方圖。一旦我們找到離汽車更近的車道位置,我們就可以使用移動(dòng)窗口方法來(lái)找到其余車道,如下圖所示:
移動(dòng)窗口處理示例
這是它在代碼中的樣子:
# Choose the number of sliding windowsnum_windows = 9# Set the width of the windows +/- marginmargin = 50# Set minimum number of pixels found to recenter windowmin_pix = 100# Set height of windows - based on num_windows above and image shapewindow_height = np.int(binary_warped.shape[0] // num_windows)# Current positions to be updated later for each window in nwindowsleft_x_current = left_x_baseright_x_current = right_x_base# Create empty lists to receive left and right lane pixel indicesleft_lane_inds = []right_lane_inds = []# Step through the windows one by onefor window in range(num_windows):# Identify window boundaries in x and y (and right and left)win_y_low = binary_warped.shape[0] - (window + 1) * window_heightwin_y_high = binary_warped.shape[0] - window * window_heightwin_x_left_low = left_x_current - marginwin_x_left_high = left_x_current + marginwin_x_right_low = right_x_current - marginwin_x_right_high = right_x_current + marginif self._debug:# Draw the windows on the visualization imagecv2.rectangle(out_img, (win_x_left_low, win_y_low),(win_x_left_high, win_y_high), (0, 255, 0), 2)cv2.rectangle(out_img, (win_x_right_low, win_y_low),(win_x_right_high, win_y_high), (0, 255, 0), 2)# Identify the nonzero pixels in x and y within the window #good_left_inds = ((nonzero_y >= win_y_low) & (nonzero_y < win_y_high) &(nonzero_x >= win_x_left_low) & (nonzero_x < win_x_left_high)).nonzero()[0]good_right_inds = ((nonzero_y >= win_y_low) & (nonzero_y < win_y_high) &(nonzero_x >= win_x_right_low) & (nonzero_x < win_x_right_high)).nonzero()[0]# Append these indices to the listsleft_lane_inds.append(good_left_inds)right_lane_inds.append(good_right_inds)# If you found > min_pix pixels, recenter next window on their mean positionif len(good_left_inds) > min_pix:left_x_current = np.int(np.mean(nonzero_x[good_left_inds]))if len(good_right_inds) > min_pix:right_x_current = np.int(np.mean(nonzero_x[good_right_inds]))# Concatenate the arrays of indices (previously was a list of lists of pixels)try:left_lane_inds = np.concatenate(left_lane_inds)right_lane_inds = np.concatenate(right_lane_inds)except ValueError:# Avoids an error if the above is not implemented fullypass
這個(gè)過(guò)程非常緊急,所以在處理視頻時(shí),我們可以調(diào)整一些事情,因?yàn)槲覀儾⒉豢偸切枰獜牧汩_始,之前進(jìn)行的計(jì)算為我們提供了下一個(gè)車道的窗口,因此更容易找到. 所有這些都在存儲(chǔ)庫(kù)的最終代碼中實(shí)現(xiàn),請(qǐng)隨意查看。
一旦我們有了所有的窗口,我們現(xiàn)在可以使用所有確定的點(diǎn)構(gòu)建多項(xiàng)式,每條線(左和右)將獨(dú)立計(jì)算如下:
left_fit = np.polyfit(left_y, left_x, 2)right_fit = np.polyfit(right_y, right_x, 2)
數(shù)字 2 表示二階多項(xiàng)式。
現(xiàn)在我們知道了線條在圖像上的位置,并且我們知道汽車的位置(在相機(jī)的中心)我們可以做一些有趣的計(jì)算來(lái)確定車道的曲率和汽車相對(duì)于中心的位置車道的。
車道曲率是一個(gè)簡(jiǎn)單的多項(xiàng)式計(jì)算。
fit_cr = np.polyfit(self.all_y * self._ym_per_pix, self.all_x * self._xm_per_pix, 2)plot_y = np.linspace(0, 720 - 1, 720)y_eval = np.max(plot_y)curve = ((1 + (2 * fit_cr[0] * y_eval * self._ym_per_pix + fit_cr[1]) ** 2) ** 1.5) / np.absolute(2 * fit_cr[0])
但是有一個(gè)重要的考慮因素,對(duì)于這一步,我們不能在像素上工作,我們需要找到一種將像素轉(zhuǎn)換為米的方法,因此我們引入了 2 個(gè)變量:_ym_per_pix 和 _xm_per_pix,它們是預(yù)定義的值。
self._xm_per_pix = 3.7 / 1280self._ym_per_pix = 30 / 720
非常簡(jiǎn)單,計(jì)算車道中間的位置,并將其與圖像中心進(jìn)行比較,如下所示
lane_center = (self.left_lane.best_fit[-1] + self.right_lane.best_fit[-1]) / 2car_center = img.shape[1] / 2dx = (car_center - lane_center) * self._xm_per_pix
現(xiàn)在我們擁有了所需的所有信息,以及表示車道的多項(xiàng)式,最終結(jié)果應(yīng)如下所示:








交流群
歡迎加入公眾號(hào)讀者群一起和同行交流,目前有SLAM、三維視覺、傳感器、自動(dòng)駕駛、計(jì)算攝影、檢測(cè)、分割、識(shí)別、醫(yī)學(xué)影像、GAN、算法競(jìng)賽等微信群(以后會(huì)逐漸細(xì)分),請(qǐng)掃描下面微信號(hào)加群,備注:”昵稱+學(xué)校/公司+研究方向“,例如:”張三 + 上海交大 + 視覺SLAM“。請(qǐng)按照格式備注,否則不予通過(guò)。添加成功后會(huì)根據(jù)研究方向邀請(qǐng)進(jìn)入相關(guān)微信群。請(qǐng)勿在群內(nèi)發(fā)送廣告,否則會(huì)請(qǐng)出群,謝謝理解~

