【OpenCV】直線/白線を検出する方法【自動運転】

title_white_line_detection OpenCV

OpenCVを用いた直線を検出する方法を紹介します。

直線検出を行う画像は、走行中の車から見える景色を用います。この画像の特徴は、走行時と同じく道路をまっすぐ見た画像であることと、中央に点線が含まれるところがポイントです。
ただし、今回紹介する手法は白線に限らず、一般的な画像の直線検出にも応用できる内容になっています。

白線検出までの流れ

次のステップに従って白線を検出していきます。

  1. ガウシアンフィルタ(cv2.GaussianBlur)を用いた平滑化
  2. (option)指定した色でフィルタリング(cv2.inRange)
  3. エッジ検出(cv2.Canny)
  4. (option)領域選択
  5. ハフ変換を用いた直線検出(cv2.HoughLinesP)

optionとなっている2番と4番は車における白線検出用の処理なので、一般的な直線検出の場合は不要です。

また今回使用する道路の画像はこちらのフリー写真をお借りしました。

1. ガウシアンフィルタ(cv2.GaussianBlur)を用いた平滑化

初めに画像にガウシアンフィルタを適用し、平滑化することでノイズとなる成分を除去します。

img = cv2.imread('./road.jpg') # 道路の画像を読み込み
blur_img = cv2.GaussianBlur(img, (3,3), 0) # ガウシアンフィルタ
  • cv2.GaussianBlur
    • 第2引数: カーネル(フィルタ)サイズ。奇数である必要がある。
    • 第3引数: ガウシアンの標準偏差値。0指定で自動的に計算されるため、基本は0で良い。

フィルタをかけると僅かにぼやけた画像が生成され、ドットのようなノイズ成分が弱まります。
フィルタの強さはカーネルサイズで変更できますが、強くしすぎると必要な情報も落ちてしまうので用途に合わせた調整が必要です。

2. (option)指定した色でフィルタリング(cv2.inRange)

次に色情報を活用して画像をフィルタリングします。検出したい直線の色が特定の範囲に収まっている場合は、この手法によりノイズの除去ができます。
検出したい直線の色が定まっていない場合は、この手法は使えないのでスキップしてください。

# カラーコード(180,180,180) ~ 白(255,255,255)を抽出
bin_blur_img = cv2.inRange(blur_img, (180,180,180), (255,255,255)) # 抽出する色の範囲を選択する
  • cv2.inRange
    • 第2引数: 下限値を指定
    • 第3引数: 上限値を指定
    • 返り値: 入力と同じサイズかつCV_8Uタイプの2値化(0 or 255)されたndarray。

RGB(180,180,180) ~ (255,255,255)の範囲の色情報をでフィルタリングした結果は次のとおりです。
白に近い値(180以上)だけが抽出されているのがわかります。

3. エッジ検出(cv2.Canny)

エッジの検出にはcv2.Cannyを使います。Cannyエッジ検出は2つの閾値を用い、エッジの検出を行います。

# エッジ検出
edges = cv2.Canny(img, minVal=150, maxVal=250)
  • cv2.Canny
    • minVal: エッジと見なす最小値となる閾値。この値を下回る場合はエッジと見なさない。
    • maxVal: エッジと見なす最大値となる閾値。この値を上回る場合はエッジと見なす。最小値と最大値の間にくる値は、それが最大値を上回るエッジと接続されている場合はエッジと見做される。

検出されるエッジは、minVal, maxValの値によって大きく変わるため、cv2.Cannyを用いる場合パラメータの調整は重要です。
Cannyエッジ検出の詳細はこちらのリファレンスが参考になります。
リファレンス: Canny Edge Detection

4. (option)領域選択

次に検出したい画像領域を選択します。これは車で白線検出する際に用いられる処理のため、画像全体から直線を検出したい場合はスキップしてください。
車に取り付けたカメラから白線を検出する状況では、白線が映る領域はおおよそアタリがつきます。具体的には、画像の上半分は遠くの景色を写しているので白線検出においてその範囲のエッジ情報はノイズとなるため不要です。

領域選択には、多角形の領域内を塗り潰すcv2.fillPolyと、重複する領域をピクセル単位で取得するcv2.bitwise_andを使用します。

# 画像shapeに対する取得領域(4点)を、縦横の比率で指定
imshape = np.array(edges.T.shape).T # 画像のshape(W, H)
left_bottom = imshape * np.array([0,0.8]) # 左下の座標
left_top = imshape * np.array([0.3, 0.3]) # 左上の座標
right_top = imshape * np.array([0.7, 0.3]) # 右上の座標
right_bottom = imshape * np.array([1, 0.8]) # 右下の座標
region_coord = np.array([[left_bottom, left_top, right_top, right_bottom]], dtype=np.int32) # 先行車領域の座標4点(左下から時計回り)

# マスク画像の作成
mask = np.zeros_like(edges)
cv2.fillPoly(mask, region_coord, color=255)

作成したマスク画像と、3.で検出したエッジ画像の重複部分を取得します。

masked_edges = cv2.bitwise_and(edges, mask)

背景となる画像の上半分の情報が削除され、必要な白線部分の情報が取得できています。

5. ハフ変換を用いた直線検出(cv2.HoughLinesP)

最後に直線検出を行います。直線検出は、エッジ検出画像に対してcv2.HoughLinesPを適用することで実現できます。

# cv2.HoughLinesPによる直線検出
lines = cv2.HoughLinesP(masked_edges, rho, theta, threshold, np.array([]), minLineLength=min_line_len, maxLineGap=max_line_gap)
  • cv2.HoughLinesP
    • threshold: 直線を検出するための交点の最小数
    • minLineLength: 検出する直線の最小の長さ
    • maxLineGap: 2つの直線を1つの直線と見なす際に許容する最大ギャップ
    • rho: パラメータ$rho$のピクセル単位の解像度。基本は1。
    • theta: パラメータ$theta$のラジアン単位の解像度。基本はnp.pi/180。

cv2.HoughLinesPを用いる際は、min_line_lenおよびmax_line_gapが重要なパラメータです。
点線も一つの直線として検出したい場合はmax_line_gapの値を大きくすることで、短い直線も検出したい場合は、min_line_lenを小さくすることで、より幅広く直線を検出できます。
ただし、どちらも適切に設定しないと不要なエッジも直線として検出されてしまうので注意が必要です。

cv2.HoughLinesPによる直線検出の原理についてより詳しく知りたい方は下記のチュートリアルが分かりやすいので、ご参照ください。
チュートリアル: ハフ変換による直線検出

検出した直線を、画像に描画して、元画像と重ね合わせた結果は次のとおりです。

# 直線をimgに描画する関数
def draw_ext_lines(img, lines, color=[255, 0, 0], thickness=2):
    d = 300 # required extend length
    for line in lines:
        for x1,y1,x2,y2 in line:
            if (x2 != x1):
                slope = (y2-y1)/(x2-x1)
                sita = np.arctan(slope)
                if (slope > 0): # 傾きに応じて場合分け
                    if (x2 > x1):
                        x3 = int(x2 + d*np.cos(sita))
                        y3 = int(y2 + d*np.sin(sita))
                        cv2.line(img, (x3, y3), (x1, y1), color, thickness)
                    else:
                        x3 = int(x1 + d*np.cos(sita))
                        y3 = int(y1 + d*np.sin(sita))
                        cv2.line(img, (x3, y3), (x2, y2), color, thickness)
                elif (slope < 0):
                    if (x2 > x1):
                        x3 = int(x1 - d*np.cos(sita))
                        y3 = int(y1 - d*np.sin(sita))
                        cv2.line(img, (x3, y3), (x2, y2), color, thickness)
                    else:
                        x3 = int(x2 - d*np.cos(sita))
                        y3 = int(y2 - d*np.sin(sita))
                        cv2.line(img, (x3, y3), (x1, y1), color, thickness)

line_img = np.zeros((img.shape[0], img.shape[1], 3), dtype=np.uint8)

# 直線が検出された場合は画像に描画する
if (not isinstance(lines,type(None))):
    draw_ext_lines(line_img, lines) # 自作の直線描画関数

overlay_img = cv2.addWeighted(img, a=0.6, line_img, b=1, c=0) # 元画像に直線画像を重ね合わせ

以上で、中央にある点線も含めて全ての白線が検出できました。

参考リンク

白線検出について参考になるリンクをまとめておきます。

コメント

タイトルとURLをコピーしました