目標:實際公路的車道線檢測
素材中車道保持不變,車道線清晰明確,易於檢測,是車道檢測的基礎版本,網上也有很多針對復雜場景的高級實現,感興趣的朋友可以自行了解。
如果我們手動把這部分ROI區域摳出來,就會排除掉大部分干擾。接下來檢測直線肯定是用霍夫變換,但ROI區域內的邊緣直線信息還是很多,考慮到只有左右兩條車道線,一條斜率為正,一條為負,可將所有的線分為兩組,每組再通過均值或最小二乘法擬合的方式確定唯一一條線就可以完成檢測。總體步驟如下:
1. 灰度化
2. 高斯模糊
3. Canny邊緣檢測
4. 不規則ROI區域截取
5. 霍夫直線檢測
6. 車道計算
7. 對於視頻來說,只要一幅圖能檢查出來,合成下就可以了,問題不大。
圖像預處理
灰度化和濾波操作是大部分圖像處理的必要步驟。灰度化不必多說,因為不是基於色彩信息識別的任務,所以沒有必要用彩色圖,可以大大減少計算量。而濾波會削弱圖像噪點,排除干擾信息。另外,根據前面學習的知識,邊緣提取是基於圖像梯度的,梯度對噪聲很敏感,所以平滑濾波操作必不可少。
這次的代碼我們分模塊來寫,規范一點。其中process_an_image()是主要的圖像處理流程:
import cv2 as cv
import numpy as np
# 高斯濾波核大小
blur_ksize = 5
# Canny邊緣檢測高低閾值
canny_lth = 50
canny_hth = 150
def process_an_image(img):
# 1. 灰度化、濾波和Canny
gray = cv.cvtColor(img, cv.COLOR_RGB2GRAY)
blur_gray = cv.GaussianBlur(gray, (blur_ksize, blur_ksize), 1)
edges = cv.Canny(blur_gray, canny_lth, canny_hth)
if __name__ == "__main__":
img = cv.imread('test_pictures/lane.jpg')
result = process_an_image(img)
cv.imshow("lane", np.hstack((img, result)))
cv.waitKey(0)
ROI獲取
按照前面描述的方案,只需保留邊緣圖中的紅線部分區域用於后續的霍夫直線檢測,其余都是無用的信息:
如何實現呢?我們可以創建一個梯形的mask掩膜,然后與邊緣檢測結果圖混合運算,掩膜中白色的部分保留,黑色的部分舍棄。梯形的四個坐標需要手動標記:
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)
cv.fillPoly(mask, corner_points, 255)
masked_img = cv.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 = cv.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:
cv.line(img, (x1, y1), (x2, y2), color, thickness)
draw_lines()是用來畫直線檢測的結果,后面我們會接着處理直線,所以這里注釋掉了,可以取消注釋看下效果:
對本例的這張測試圖來說,如果打印出直線的條數print(len(lines)),應該是有15條。
車道計算
這部分應該算是本實驗的核心內容了:前面通過霍夫變換得到了多條直線的起點和終點,我們的目的是通過某種算法只得到左右兩條車道線。
第一步:根據斜率正負划分某條線是左車道還是右車道。
再次強調,斜率計算是在圖像坐標系下,所以斜率正負/左右跟平面坐標有區別。
第二步、迭代計算各直線斜率與斜率均值的差,排除掉差值過大的異常數據。
注意這里迭代的含義,意思是第一次計算完斜率均值並排除掉異常值后,再在剩余的斜率中取均值,繼續排除……這樣迭代下去。
第三步、最小二乘法擬合左右車道線。
經過第二步的篩選,就只剩下可能的左右車道線了,這樣只需從多條直線中擬合出一條就行。擬合方法有很多種,最常用的便是最小二乘法,它通過最小化誤差的平方和來尋找數據的最佳匹配函數。
具體來說,假設目前可能的左車道線有6條,也就是12個坐標點,包括12個x和12個y,我們的目的是擬合出這樣一條直線:
使得誤差平方和最小:
Python中可以直接使用np.polyfit()進行最小二乘法擬合。
def process_an_image(img):
# 1. 灰度化、濾波和Canny
# 2. 標記四個坐標點用於ROI截取
# 3. 霍夫直線提取
# 4. 車道擬合計算
draw_lanes(drawing, lines)
# 5. 最終將結果合在原圖上
result = cv.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. 填充車道區域
cv.fillPoly(img, vtxs, (0, 255, 0))
# 或者只畫車道線
# cv.line(img, left_results[0], left_results[1], (0, 255, 0), thickness)
# cv.line(img, right_results[0], right_results[1], (0, 255, 0), 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時會自動下載,也可手動下載。
只需在開頭導入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)
本文實現了車道檢測的基礎版本,如果你感興趣的話,可以自行搜索了解更多。
實現功能完整代碼
import cv2 as cv
import numpy as np
# 高斯濾波核大小
blur_ksize = 5
# Canny邊緣檢測高低閾值
canny_lth = 50
canny_hth = 150
# 霍夫變換參數
rho = 1
theta = np.pi / 180
threshold = 15
min_line_len = 40
max_line_gap = 20
def process_an_image(img):
# 1. 灰度化、濾波和Canny
gray = cv.cvtColor(img, cv.COLOR_RGB2GRAY)
blur_gray = cv.GaussianBlur(gray, (blur_ksize, blur_ksize), 1)
edges = cv.Canny(blur_gray, canny_lth, canny_hth)
# 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)
# 3. 霍夫直線提取
drawing, lines = hough_lines(roi_edges, rho, theta,
threshold, min_line_len, max_line_gap)
# 4. 車道擬合計算
draw_lanes(drawing, lines)
# 5. 最終將結果合在原圖上
result = cv.addWeighted(img, 0.9, drawing, 0.2, 0)
return result
def roi_mask(img, corner_points):
# 創建掩膜
mask = np.zeros_like(img)
cv.fillPoly(mask, corner_points, 255)
masked_img = cv.bitwise_and(img, mask)
return masked_img
def hough_lines(img, rho, theta, threshold, min_line_len, max_line_gap):
# 統計概率霍夫直線變換
lines = cv.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)
# print(len(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:
cv.line(img, (x1, y1), (x2, y2), color, thickness)
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.填充車道區域
cv.fillPoly(img, vtxs, (0, 255, 0))
# 或者只畫車道線
# cv.line(img, left_results[0], left_results[1], (0, 255, 0), thickness)
# cv.line(img, right_results[0], right_results[1], (0, 255, 0), 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)]
if __name__ == "__main__":
img = cv.imread('test_pictures/lane2.jpg')
result = process_an_image(img)
cv.imshow("lane", np.hstack((img, result)))
cv.waitKey(0)