任務:
一共要完成兩項任務:
1. 在所提供的公路圖片上檢測出車道線並標記
2. 在所提供的公路視頻上檢測出車道線並標記
方案:
要檢測出當前車道,就是要檢測出左右兩條車道直線。由於無人車一直保持在當前車道,那么無人車上的相機拍攝視頻中,車道線的位置應該基本固定在某一個范圍內:
如果我們手動把這部分ROI區域摳出來,就會排除大部分干擾。接下來檢測直線肯定用霍夫變換,但ROI區域內的邊緣直線信息還是很多,考慮到只有左右兩條車道,一條斜率為正、一條斜率為負,可將所有的線分為兩組,每組再通過均值或最小二乘法擬合的方式確定唯一一條線就可以完成檢測。具體步驟如下:
1. 灰度化
2. 高斯模糊
3. Canny邊緣檢測
4. 不規則ROI區域截取
5. 霍夫直線檢測
6. 車道計算
對於視頻來說,只要能檢測出一幅圖,后面將圖像合成一下即可。
圖像預處理
灰度化和濾波操作是大部分圖像處理的必要步驟。灰度化是因為不需要色彩信息,可以減少計算量。而濾波會削弱圖像噪點,排除干擾信息。另外,邊緣提取是基於圖像梯度的,梯度對噪聲很敏感,所以平滑操作必不可少。
這里我們用分模塊來寫,方便調用:
import cv2 import numpy as np # 高斯濾波核大小 blur_ksize = 5 # Canny邊緣檢測高低閾值 canny_lth = 50 canny_hth = 150 def process_an_image(img): # 1. 灰度化、濾波和Canny gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) blur_gray = cv2.GaussianBlur(gray, (blur_ksize, blur_ksize), 1) edges = cv2.Canny(blur_gray, canny_lth, canny_hth) if __name__ == "__main__": img = cv2.imread('test_pictures/lane.jpg') result = process_an_image(img) cv2.imshow("lane", np.hstack((img, result))) cv2.waitKey(0)
邊緣檢測結果圖
ROI截取
按前面方案中提到的,只需保留邊緣圖中紅線部分區域用於后續的霍夫直線檢測,其余的都是無用的信息:
我們可以穿件一個梯形的掩膜,然后與邊緣檢測結果圖混合運算,掩膜中白色部分保留,黑色部分舍棄。梯形的四個坐標需要手動標記:
def process_an_image(img): # 1. 灰度化、濾波和Canny # 2. 標記四個坐標點用於ROI截取 rows, cols = edges.shape points = np.array([[(0, rows), (460, 325), (520, 325), (cols, rows)]]) # [[[0 540], [460 325], [520 325], [960 540]]] roi_edges = roi_mask(edges, points) def roi_mask(img, corner_points): # 創建掩膜 mask = np.zeros_like(img) cv2.fillPoly(mask, corner_points, 255) masked_img = cv2.bitwise_and(img, mask) return masked_img
結果圖 roi_edges如下:
只保留關鍵區域的邊緣檢測圖
霍夫直線提取
為了方便后續計算直線的斜率,我們使用統計概率霍夫直線變換(因為它能得到直線的起點和終點坐標):
# 霍夫變換參數 rho = 1 theta = np.pi / 180 threshold = 15 min_line_len = 40 max_line_gap = 20 def process_an_image(img): # 1. 灰度化、濾波和Canny # 2. 標記四個坐標點用於ROI截取 # 3. 霍夫直線提取 drawing, lines = hough_lines(roi_edges, rho, theta, threshold, min_line_len, max_line_gap) def hough_lines(img, rho, theta, threshold, min_line_len, max_line_gap): # 統計概率霍夫直線變換 lines = cv2.HoughLinesP(img, rho, theta, threshold, minLineLength=min_line_len, maxLineGap=max_line_gap) # 新建一副空白畫布 drawing = np.zeros((img.shape[0], img.shape[1], 3), dtype=np.uint8) # draw_lines(drawing, lines) # 畫出直線檢測結果 return drawing, lines def draw_lines(img, lines, color=[0, 0, 255], thickness=1): for line in lines: for x1, y1, x2, y2 in line: cv2.line(img, (x1, y1), (x2, y2), color, thickness)
draw_lines()用來畫直線檢測的結果,后面我們會接着處理直線,所以這里注釋掉了。可以取消注釋看看效果:
霍夫變換結果圖
對本例的這張測試圖來說,如果打印出直線的條數print(len(lines)),應該是16條
車道計算
前面通過霍夫變換得到了多條直線的地點和終點,我們的目的是通過某種算法只得到左右兩條車道線
第一步、 根據斜率正負划分某條線是左車道還是右車道。
其中左車道(斜率 < 0),右車道(斜率 > 0)。原因如下圖所示:
左車道與圖像坐標系成鈍角,斜率為負,右車道與圖像坐標系成銳角,斜率為正。
第二步、迭代計算各直線斜率與斜率均值的差,排除掉差值過大的異常數據
第一次計算完斜率均值並排除掉異常值后,再在剩余的斜率中取均值,繼續排除。一直迭代下去。
第三步、最小二乘法擬合左右車道線
經過第二步的篩選,就只剩下可能的左右車道線了。我們需要從多條直線中擬合出一條。使用最小二乘法,它通過最小化誤差的平方和來尋找數據的最佳匹配函數。
假設目前可能的左車道線有6條,也就是12個坐標點。
我們的目標是擬合出這樣一條直線:
使得誤差平方和最小:
Python中可以直接使用 np.polyfit() 進行最小二乘法擬合
def process_an_image(img): # 1. 灰度化、濾波和Canny # 2. 標記四個坐標點用於ROI截取 # 3. 霍夫直線提取 # 4. 車道擬合計算 draw_lanes(drawing, lines) # 5. 最終將結果合在原圖上 result = cv2.addWeighted(img, 0.9, drawing, 0.2, 0) return result def draw_lanes(img, lines, color=[255, 0, 0], thickness=8): # a. 划分左右車道 left_lines, right_lines = [], [] for line in lines: for x1, y1, x2, y2 in line: k = (y2 - y1) / (x2 - x1) if k < 0: left_lines.append(line) else: right_lines.append(line) if (len(left_lines) <= 0 or len(right_lines) <= 0): return # b. 清理異常數據 clean_lines(left_lines, 0.1) clean_lines(right_lines, 0.1) # c. 得到左右車道線點的集合,擬合直線 left_points = [(x1, y1) for line in left_lines for x1, y1, x2, y2 in line] left_points = left_points + [(x2, y2) for line in left_lines for x1, y1, x2, y2 in line] right_points = [(x1, y1) for line in right_lines for x1, y1, x2, y2 in line] right_points = right_points + [(x2, y2) for line in right_lines for x1, y1, x2, y2 in line] left_results = least_squares_fit(left_points, 325, img.shape[0]) right_results = least_squares_fit(right_points, 325, img.shape[0]) # 注意這里點的順序 vtxs = np.array([[left_results[1], left_results[0], right_results[0], right_results[1]]]) # d. 填充車道區域 cv2.fillPoly(img, vtxs, (0, 255, 0)) # 或者只畫車道線 # cv2.line(img, left_results[0], left_results[1], (0, 0, 255), thickness) # cv2.line(img, right_results[0], right_results[1], (0, 0, 255), thickness) def clean_lines(lines, threshold): # 迭代計算斜率均值,排除掉與差值差異較大的數據 slope = [(y2 - y1) / (x2 - x1) for line in lines for x1, y1, x2, y2 in line] while len(lines) > 0: mean = np.mean(slope) diff = [abs(s - mean) for s in slope] idx = np.argmax(diff) if diff[idx] > threshold: slope.pop(idx) lines.pop(idx) else: break def least_squares_fit(point_list, ymin, ymax): # 最小二乘法擬合 x = [p[0] for p in point_list] y = [p[1] for p in point_list] # polyfit第三個參數為擬合多項式的階數,所以1代表線性 fit = np.polyfit(y, x, 1) fit_fn = np.poly1d(fit) # 獲取擬合的結果 xmin = int(fit_fn(ymin)) xmax = int(fit_fn(ymax)) return [(xmin, ymin), (xmax, ymax)]
最后得到的是左右兩條車道線的起點和終點坐標。可以選擇畫出車道線,也可以填充整個區域:
畫出車道線的效果不是很好,還是選用填充比較直觀。
視頻處理
搞定了一張圖,視頻的話也就沒什么難度了。關鍵是視頻幀的提取和合成,為此我們需要用到Python的視頻編輯包 moviepy:
pip install moviepy
另外還需要 ffmpeg,首次運行moviepy時會自動下載。也可手動下載。建議手動下載,不知為什么,博主自動下載老是失敗。ffmpeg-win32-v3.2.4.exe
只需要在開頭導入 moviepy,然后主函數改掉就可以了,其余代碼不需要修改:
# 開頭導入moviepy from moviepy.editor import VideoFileClip # 主函數更改為: if __name__ == "__main__": output = 'test_videos/output.mp4' clip = VideoFileClip("test_videos/cv2_white_lane.mp4") out_clip = clip.fl_image(process_an_image) out_clip.write_videofile(output, audio=False)