【Python】OpenCVで画像のエッジを検出する

今回はOpenCVでエッジを検出していきたいとおもいます。

エッジというと「縁、端」といった意味ですが、画像解析においては物体の輪郭検出を指します。エッジを検出することで、画像中の物体をより鮮明に浮かび上がらせることができます。

検出するやり方にもいくつかあるので、順に紹介していきます。

エッジ検出の原理

画像でのエッジ検出では、明るさが大きく変化する部分を抽出します。

そのために微分フィルタを用います。輪郭部分の接線を求めることで、その傾きから変化の度合いを求めます。

微分は本来連続値に用いますが、画像は連続値ではありません。なので、隣り合う画素間の差をとることで、近似的な微分として扱います。

一次微分フィルタ

一次微分フィルタでは、以下のようなカーネルを用います。

\begin{eqnarray*} K_x= \left[ \begin{array}{ccc} 0 & 0 & 0 \\ -1 & 0 & 1 \\ 0 & 0 & 0 \\ \end{array} \right], K_y= \left[ \begin{array}{ccc} 0 & -1 & 0 \\ 0 & 0 & 0 \\ 0 & 1 & 0 \\ \end{array} \right] \end{eqnarray*}

カーネルの中央が注目画素です。Kxでは横方向の微分値が得られ、エッジは縦方向のものが出てきます。

Kyでは縦方向の微分値が得られ、エッジは横方向のものがでてきます。

Prewittフィルタ

上記カーネルをそのまま使えばいいというわけではありません。ノイズもエッジとして検出されてしまうためです。

実際にエッジ検出する際は、ノイズを極力抑えながらやる必要があります。それを実現するのがPrewittフィルタです。

一次微分フィルタに平滑化処理を加えます。

\begin{eqnarray*} K_x= \left[ \begin{array}{ccc} -1 & 0 & 1 \\ -1 & 0 & 1 \\ -1 & 0 & 1 \\ \end{array} \right], K_y= \left[ \begin{array}{ccc} -1 & -1 & -1 \\ 0 & 0 & 0 \\ 1 & 1 & 1 \\ \end{array} \right] \end{eqnarray*}

これがPrewittフィルタで用いるカーネルです。

今回このフィルタを用いたエッジ検出は行いませんが、これをもとにしたSobelフィルタで検出してみます。

ちなみに、OpenCVではPrewittフィルタの関数はないので、実行する場合は自作する必要があります。

使用する画像

今回はこちらの花の画像を使用します。

オリジナルの画像
import cv2

path = "input/flowers.png"
img = cv2.imread(path)

#リサイズ
height, width = img.shape[:2]
img = cv2.resize(img,(round(width/4), round(height/4)))

前処理

オリジナルの画像をそのまま使うのではなく、いくつか前処理を施します。

まずは白黒画像に変換します。その後、場合によってはぼかし処理も行います。

#白黒変換
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# ガウシアンぼかし
img_blur = cv2.GaussianBlur(img_gray, (3,3), 0) 

エッジ検出では明るさの変化を検出しますが、オリジナルの画像ではノイズも検出してしまいます。ぼかし加工を加えて滑らかな画像に変換しておくことで、ノイズを抑えたエッジ検出が可能になります。

画像のぼかしについてはこちら↓↓

https://www.learning-nao.com/?p=1849

エッジ検出

Sobel

Sobelフィルタは、OpenCVでのエッジ検出で最も基本的な手法の1つです。Sobelフィルタでは、Prewittフィルタの中央に重みづけをします。つまり、注目画素と隣接する画素の重みが大きくなります。

\begin{eqnarray*} K_x= \left[ \begin{array}{ccc} -1& 0 & 1 \\ -2& 0 & 2 \\ -1& 0 & 1 \\ \end{array} \right], K_y= \left[ \begin{array}{ccc} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \\ \end{array} \right] \end{eqnarray*}

Pythonで実装

OpenCVではSobel()を用います。

cv2.Sobel(src, ddepth, dx, dy, ksize)

srcは入力画像です。ddepthは出力画像のビット深度を指定します。

dxはx方向、dyはy方向微分の次数を指定します。たとえばdx=1, dy=0の場合はx方向の微分を行い、画像には縦方向のエッジが示されます。両パラメータを1にすると、縦横両方のエッジが検出されます。

ksizeはカーネルサイズです。カーネルサイズは奇数の自然数を指定します。

以下、Pythonでのプログラムです。縦方向のみのエッジ、横方向のみのエッジ、縦横のエッジを検出した3種類の画像を出力してみます。

Sobelフィルタにかける画像は、白黒処理しただけの画像とします。

sobelx = cv2.Sobel(src=img_gray, ddepth=cv2.CV_32F, dx=1, dy=0, ksize=1)
cv2.imshow('Sobel X', sobelx)
sobely = cv2.Sobel(src=img_gray, ddepth=cv2.CV_32F, dx=0, dy=1, ksize=1)
cv2.imshow('Sobel Y', sobely)
sobelxy = cv2.Sobel(src=img_gray, ddepth=cv2.CV_32F, dx=1, dy=1, ksize=1)
cv2.imshow('Sobel X Y using Sobel() function', sobelxy)
Sobelフィルタの実行結果X, Y, XY

中はほぼ塗りつぶされてしまいましたが、花の輪郭はきっちりキャッチできています。

Canny

原理

Cannyフィルタは各輪郭を1本の線として表現することができます。処理には以下のような手順を踏みます。

  1. ガウシアンぼかしによる平滑化
  2. 微分による勾配の大きさと方向を計算する
  3. 輪郭の細線化
  4. Hysteresis Thresholding処理
ガウシアンぼかしによる平滑化

最初にガウシアンぼかしによって画像を平滑化します。これによって画像のノイズを取り除きます。

一般に、Cannyフィルタではガウシアンぼかしを用います。

微分画像勾配の大きさと方向を計算する

次に、勾配Gと方向θを算出します。

\begin{equation*} G = \sqrt{G_x^2 + G_y^2} \end{equation*}
\begin{equation*} \Theta = tan^{-1}\left(\frac{G_x}{G_y}\right) \end{equation*}
輪郭の細線化

non-maximum suppressionという技法を用いて、輪郭の線を細くしていきます。

手順としては、まず注目画素の画素値と、その勾配方向に隣り合う2つの画素の画素値を比較します。注目画素の画素値が3つの画素で最大でない場合、画素値を0に置き換えます。

Hysteresis Thresholding処理

最後に、輪郭が本当に輪郭なのかを閾値で判断します。細線化処理した画像の画素値と予め定めた閾値を用いて判断します。

注目画素が最大閾値より高い場合は信頼性の高い輪郭と判断し、出力画像に輪郭を残します。

注目画素が最小閾値より低い場合は、信頼性の低い輪郭として出力画像から消去します。

注目画素が最小閾値と最大閾値の間にある場合は、隣の画素を参照します。隣が信頼性の高い画素の場合、信頼性の高い画素として出力画像に残します。一方、隣が信頼性の低い画素の場合は信頼性の低い画素として出力画像から消去します。

Pythonで実装

OpenCVでは、cv2.Canny()を用います。

Canny(src, lower_threshold, upper_threshold2)

引数は元の画像、最小閾値、最大閾値です。

edges = cv2.Canny(image=img_blur, threshold1=100, threshold2=200)
cv2.imshow('Canny Edge Detection', edges)
Cannyフィルタの実行結果

きれいに輪郭を検出できました。

Laplacian

Laplacian(ラプラシアン)フィルタでは二次微分を利用して輪郭を検出します。

ラプラシアンフィルタには4近傍と8近傍のカーネルが存在します。

4近傍では、注目画素の上下左右の画素から二次微分を取ります。

\begin{eqnarray*} K_4= \left[ \begin{array}{ccc} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \\ \end{array} \right] \end{eqnarray*}
4近傍

8近傍では、上下左右に加え斜め方向の画素の二次微分もとります。

\begin{eqnarray*} K_8= \left[ \begin{array}{ccc} 1 & 1 &  1 \\ 1 &  -8 &  1 \\ 1 &  1 &  1 \\ \end{array} \right] \end{eqnarray*}
8近傍

Pythonで実装

OpenCVでは、cv2.Laplacian()を用います。

cv2.Laplacian(src, ddepth, ksize)

ddepthは出力画像のビット深度を指定します。 ksizeはカーネルサイズで、奇数の自然数を指定します。

LapEdges = cv2.Laplacian(img_blur, cv2.CV_32F, ksize=1)
cv2.imshow('Laplacian Edge Detection', LapEdges)
ラプラシアンフィルタの実行結果

OpenCVの学習におすすめ書籍

PythonでOpenCV始めてみようという方におすすめなのが以下書籍です。実装例も豊富なので、1からコードを書かずとも学習を進めることができます。

オライリーの1冊は読み物というより辞書としての利用におすすめです。お値段結構しますが、細かい情報までしっかりと詰め込まれています。

まとめ

OpenCVでエッジを検出(輪郭検出)する方法を紹介しました。エッジの検出にもいろいろ方法があります。

カーネルのサイズ等を変えると結果も変わるので、最適にエッジ検出ができるパラメータを探してみてください。

OpenCVの基礎を身につけるためのロードマップは以下を参考にしてください。

ではでは👋