opencv 圖像處理應(yīng)用之車道檢測

目標(biāo):實際公路的車道線檢測

道路圖像

素材中車道保持不變此叠,車道線清晰明確,易于檢測随珠,是車道檢測的基礎(chǔ)版本灭袁,網(wǎng)上也有很多針對復(fù)雜場景的高級實現(xiàn),感興趣的朋友可以自行了解窗看。


車道線位置基本固定在虛線框內(nèi)

如果我們手動把這部分ROI區(qū)域摳出來茸歧,就會排除掉大部分干擾。接下來檢測直線肯定是用霍夫變換显沈,但ROI區(qū)域內(nèi)的邊緣直線信息還是很多软瞎,考慮到只有左右兩條車道線,一條斜率為正拉讯,一條為負(fù)涤浇,可將所有的線分為兩組,每組再通過均值或最小二乘法擬合的方式確定唯一一條線就可以完成檢測魔慷≤酱總體步驟如下:

  1. 灰度化
  2. 高斯模糊
  3. Canny邊緣檢測
  4. 不規(guī)則ROI區(qū)域截取
  5. 霍夫直線檢測
  6. 車道計算
  7. 對于視頻來說,只要一幅圖能檢查出來盖彭,合成下就可以了纹烹,問題不大页滚。

圖像預(yù)處理

灰度化和濾波操作是大部分圖像處理的必要步驟∑毯牵灰度化不必多說裹驰,因為不是基于色彩信息識別的任務(wù),所以沒有必要用彩色圖片挂,可以大大減少計算量幻林。而濾波會削弱圖像噪點,排除干擾信息音念。另外沪饺,根據(jù)前面學(xué)習(xí)的知識,邊緣提取是基于圖像梯度的闷愤,梯度對噪聲很敏感整葡,所以平滑濾波操作必不可少。


原圖 vs 灰度濾波圖

這次的代碼我們分模塊來寫讥脐,規(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)
邊緣檢測結(jié)果圖

ROI獲取

按照前面描述的方案旬渠,只需保留邊緣圖中的紅線部分區(qū)域用于后續(xù)的霍夫直線檢測俱萍,其余都是無用的信息:


過濾掉紅框以外的信息

如何實現(xiàn)呢?我們可以創(chuàng)建一個梯形的mask掩膜告丢,然后與邊緣檢測結(jié)果圖混合運算枪蘑,掩膜中白色的部分保留,黑色的部分舍棄岖免。梯形的四個坐標(biāo)需要手動標(biāo)記:


掩膜mask
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)該是:


只保留關(guān)鍵區(qū)域的邊緣檢測圖

霍夫直線提取

為了方便后續(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é)果赢赊,后面我們會接著處理直線,所以這里注釋掉了级历,可以取消注釋看下效果:


霍夫直線檢測結(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ū)域:

填充車道線結(jié)果

搞定了一張圖习瑰,視頻也就沒什么問題了绪颖,關(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)
左圖:原圖楔敌;右圖:車道檢測結(jié)果
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末啤挎,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子卵凑,更是在濱河造成了極大的恐慌庆聘,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件勺卢,死亡現(xiàn)場離奇詭異伙判,居然都是意外死亡,警方通過查閱死者的電腦和手機黑忱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進店門宴抚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人甫煞,你說我怎么就攤上這事菇曲。” “怎么了抚吠?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵常潮,是天一觀的道長。 經(jīng)常有香客問我楷力,道長喊式,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任弥雹,我火速辦了婚禮,結(jié)果婚禮上延届,老公的妹妹穿的比我還像新娘剪勿。我一直安慰自己,他們只是感情好方庭,可當(dāng)我...
    茶點故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布厕吉。 她就那樣靜靜地躺著酱固,像睡著了一般。 火紅的嫁衣襯著肌膚如雪头朱。 梳的紋絲不亂的頭發(fā)上运悲,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天,我揣著相機與錄音项钮,去河邊找鬼班眯。 笑死,一個胖子當(dāng)著我的面吹牛烁巫,可吹牛的內(nèi)容都是我干的署隘。 我是一名探鬼主播,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼亚隙,長吁一口氣:“原來是場噩夢啊……” “哼磁餐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起阿弃,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤诊霹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后渣淳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體脾还,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年水由,在試婚紗的時候發(fā)現(xiàn)自己被綠了荠呐。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,001評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡砂客,死狀恐怖泥张,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情鞠值,我是刑警寧澤媚创,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站彤恶,受9級特大地震影響钞钙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜声离,卻給世界環(huán)境...
    茶點故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一芒炼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧术徊,春花似錦本刽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽暗挑。三九已至,卻和暖如春斜友,著一層夾襖步出監(jiān)牢的瞬間炸裆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工鲜屏, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留烹看,地道東北人。 一個月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓墙歪,卻偏偏與公主長得像听系,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子虹菲,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,955評論 2 355

推薦閱讀更多精彩內(nèi)容