opencv 圖像處理應用之車道檢測

目標:實際公路的車道線檢測

道路圖像
素材中車道保持不變,車道線清晰明確,易於檢測,是車道檢測的基礎版本,網上也有很多針對複雜場景的高級實現,感興趣的朋友可以自行了解。
車道線位置基本固定在虛線框內
如果我們手動把這部分ROI區域摳出來,就會排除掉大部分干擾。接下來檢測直線肯定是用霍夫變換,但ROI區域內的邊緣直線信息還是很多,考慮到只有左右兩條車道線,一條斜率爲正,一條爲負,可將所有的線分爲兩組,每組再通過均值或最小二乘法擬合的方式確定唯一一條線就可以完成檢測。總體步驟如下:
1. 灰度化
2. 高斯模糊
3. Canny邊緣檢測
4. 不規則ROI區域截取
5. 霍夫直線檢測
6. 車道計算
7. 對於視頻來說,只要一幅圖能檢查出來,合成下就可以了,問題不大。

圖像預處理

灰度化和濾波操作是大部分圖像處理的必要步驟。灰度化不必多說,因爲不是基於色彩信息識別的任務,所以沒有必要用彩色圖,可以大大減少計算量。而濾波會削弱圖像噪點,排除干擾信息。另外,根據前面學習的知識,邊緣提取是基於圖像梯度的,梯度對噪聲很敏感,所以平滑濾波操作必不可少。
原圖 vs 灰度濾波圖
這次的代碼我們分模塊來寫,規範一點。其中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掩膜,然後與邊緣檢測結果圖混合運算,掩膜中白色的部分保留,黑色的部分捨棄。梯形的四個座標需要手動標記:
掩膜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)

左圖:原圖;右圖:車道檢測結果

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章