目標(biāo):實際公路的車道線檢測
素材中車道保持不變此叠,車道線清晰明確,易于檢測随珠,是車道檢測的基礎(chǔ)版本灭袁,網(wǎng)上也有很多針對復(fù)雜場景的高級實現(xiàn),感興趣的朋友可以自行了解窗看。
如果我們手動把這部分ROI區(qū)域摳出來茸歧,就會排除掉大部分干擾。接下來檢測直線肯定是用霍夫變換显沈,但ROI區(qū)域內(nèi)的邊緣直線信息還是很多软瞎,考慮到只有左右兩條車道線,一條斜率為正拉讯,一條為負(fù)涤浇,可將所有的線分為兩組,每組再通過均值或最小二乘法擬合的方式確定唯一一條線就可以完成檢測魔慷≤酱總體步驟如下:
- 灰度化
- 高斯模糊
- Canny邊緣檢測
- 不規(guī)則ROI區(qū)域截取
- 霍夫直線檢測
- 車道計算
- 對于視頻來說,只要一幅圖能檢查出來盖彭,合成下就可以了纹烹,問題不大页滚。
圖像預(yù)處理
灰度化和濾波操作是大部分圖像處理的必要步驟∑毯牵灰度化不必多說裹驰,因為不是基于色彩信息識別的任務(wù),所以沒有必要用彩色圖片挂,可以大大減少計算量幻林。而濾波會削弱圖像噪點,排除干擾信息音念。另外沪饺,根據(jù)前面學(xué)習(xí)的知識,邊緣提取是基于圖像梯度的闷愤,梯度對噪聲很敏感整葡,所以平滑濾波操作必不可少。
這次的代碼我們分模塊來寫讥脐,規(guī)范一點遭居。其中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獲取
按照前面描述的方案旬渠,只需保留邊緣圖中的紅線部分區(qū)域用于后續(xù)的霍夫直線檢測俱萍,其余都是無用的信息:
如何實現(xiàn)呢?我們可以創(chuàng)建一個梯形的mask掩膜告丢,然后與邊緣檢測結(jié)果圖混合運算枪蘑,掩膜中白色的部分保留,黑色的部分舍棄岖免。梯形的四個坐標(biāo)需要手動標(biāo)記:
def process_an_image(img):
# 1. 灰度化腥寇、濾波和Canny
# 2. 標(biāo)記四個坐標(biāo)點用于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):
# 創(chuàng)建掩膜
mask = np.zeros_like(img)
cv.fillPoly(mask, corner_points, 255)
masked_img = cv.bitwise_and(img, mask)
return masked_img
這樣,結(jié)果圖”roi_edges”應(yīng)該是:
霍夫直線提取
為了方便后續(xù)計算直線的斜率觅捆,我們使用統(tǒng)計概率霍夫直線變換(因為它能直接得到直線的起點和終點坐標(biāo))〈恚霍夫變換的參數(shù)比較多镇饮,可以放在代碼開頭岁歉,便于修改:
# 霍夫變換參數(shù)
rho = 1
theta = np.pi / 180
threshold = 15
min_line_len = 40
max_line_gap = 20
def process_an_image(img):
# 1. 灰度化、濾波和Canny
# 2. 標(biāo)記四個坐標(biāo)點用于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):
# 統(tǒng)計概率霍夫直線變換
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) # 畫出直線檢測結(jié)果
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()是用來畫直線檢測的結(jié)果赢赊,后面我們會接著處理直線,所以這里注釋掉了级历,可以取消注釋看下效果:
對本例的這張測試圖來說释移,如果打印出直線的條數(shù)print(len(lines)),應(yīng)該是有15條寥殖。
車道計算
這部分應(yīng)該算是本實驗的核心內(nèi)容了:前面通過霍夫變換得到了多條直線的起點和終點玩讳,我們的目的是通過某種算法只得到左右兩條車道線涩蜘。
第一步:根據(jù)斜率正負(fù)劃分某條線是左車道還是右車道。
再次強調(diào)熏纯,斜率計算是在圖像坐標(biāo)系下同诫,所以斜率正負(fù)/左右跟平面坐標(biāo)有區(qū)別。
第二步樟澜、迭代計算各直線斜率與斜率均值的差误窖,排除掉差值過大的異常數(shù)據(jù)。
注意這里迭代的含義秩贰,意思是第一次計算完斜率均值并排除掉異常值后霹俺,再在剩余的斜率中取均值,繼續(xù)排除……這樣迭代下去毒费。
第三步丙唧、最小二乘法擬合左右車道線。
經(jīng)過第二步的篩選蝗罗,就只剩下可能的左右車道線了艇棕,這樣只需從多條直線中擬合出一條就行。擬合方法有很多種串塑,最常用的便是最小二乘法沼琉,它通過最小化誤差的平方和來尋找數(shù)據(jù)的最佳匹配函數(shù)。
具體來說桩匪,假設(shè)目前可能的左車道線有6條打瘪,也就是12個坐標(biāo)點,包括12個x和12個y傻昙,我們的目的是擬合出這樣一條直線:
使得誤差平方和最泄肷А:
Python中可以直接使用np.polyfit()進行最小二乘法擬合。
def process_an_image(img):
# 1. 灰度化妆档、濾波和Canny
# 2. 標(biāo)記四個坐標(biāo)點用于ROI截取
# 3. 霍夫直線提取
# 4. 車道擬合計算
draw_lanes(drawing, lines)
# 5. 最終將結(jié)果合在原圖上
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. 清理異常數(shù)據(jù)
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])
# 注意這里點的順序,從左下角開始按照順序構(gòu)造梯形
vtxs = np.array([[left_results[1], left_results[0], right_results[0], right_results[1]]])
# d. 填充車道區(qū)域
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):
# 迭代計算斜率均值贾惦,排除掉與差值差異較大的數(shù)據(jù)
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第三個參數(shù)為擬合多項式的階數(shù)胸梆,所以1代表線性
fit = np.polyfit(y, x, 1)
fit_fn = np.poly1d(fit) # 獲取擬合的結(jié)果
xmin = int(fit_fn(ymin))
xmax = int(fit_fn(ymax))
return [(xmin, ymin), (xmax, ymax)]
這段代碼比較多,請每個步驟單獨來看须板。最后得到的是左右兩條車道線的起點和終點坐標(biāo)碰镜,可以選擇畫出車道線,這里我直接填充了整個區(qū)域:
搞定了一張圖习瑰,視頻也就沒什么問題了绪颖,關(guān)鍵就是視頻幀的提取和合成,為此甜奄,我們要用到Python的視頻編輯包moviepy:
pip install moviepy
另外還需要ffmpeg柠横,首次運行moviepy時會自動下載窃款,也可手動下載。
只需在開頭導(dǎo)入moviepy滓鸠,然后將主函數(shù)改掉就可以了雁乡,其余代碼不需要更改:
# 開頭導(dǎo)入moviepy
from moviepy.editor import VideoFileClip
# 主函數(shù)更改為:
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)
本文實現(xiàn)了車道檢測的基礎(chǔ)版本,如果你感興趣的話糜俗,可以自行搜索了解更多踱稍。
實現(xiàn)功能完整代碼
import cv2 as cv
import numpy as np
# 高斯濾波核大小
blur_ksize = 5
# Canny邊緣檢測高低閾值
canny_lth = 50
canny_hth = 150
# 霍夫變換參數(shù)
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. 標(biāo)記四個坐標(biāo)點用于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. 最終將結(jié)果合在原圖上
result = cv.addWeighted(img, 0.9, drawing, 0.2, 0)
return result
def roi_mask(img, corner_points):
# 創(chuàng)建掩膜
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):
# 統(tǒng)計概率霍夫直線變換
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)
# 畫出直線檢測結(jié)果
# 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. 清理異常數(shù)據(jù)
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.填充車道區(qū)域
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):
# 迭代計算斜率均值珠月,排除掉與差值差異較大的數(shù)據(jù)
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第三個參數(shù)為擬合多項式的階數(shù),所以1代表線性
fit = np.polyfit(y, x, 1)
fit_fn = np.poly1d(fit) # 獲取擬合的結(jié)果
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)