PythonでCannyエッジを検出してみる

画像から枠線を取り出す手法としてエッジ検出について、代表的な手法としてCannyエッジ検出が有ります。Cannyエッジ検出の方法は以下の資料で確認できます。
http://www.cse.iitd.ernet.in/~pkalra/col783/canny.pdf http://www.massey.ac.nz/~mjjohnso/notes/59731/presentations/img_proc.PDF
OpenCVを使うのであれば具体的な処理の内容を知らなくても大丈夫かもしれませんが、知っておいて損はないと思いますのでCannyエッジ検出の手法をおさらいしてみたいと思います。

Cannyエッジ検出

Cannyエッジ検出の流れは大きく以下のようになっています。
- ガウシアンフィルタで画像を平滑化
- ソーベルフィルタで勾配の大きさと方向を求める
- 勾配方向と大きさを元に細線化する
- 閾値化でエッジを検出する

ガウシアンフィルタで画像を平滑化

ガウシアンフィルタはローパスフィルタと言われるもので、高周波のノイズを除去するのに使われます。例えば人の顔の写真でシミなどエッジと言えないものを誤検出しないよう滑らかにする効果があります。
ガウシアンフィルタでは3 × 3や5 × 5の画像の畳み込みを行うのですが、以下のように畳み込みテンプレートの中心を平均、分散をσ2とした確率密度関数に従い畳み込み用の重みが決まります。
f:id:steavevaivai:20180715004452p:plain
これだとわかりづらいですが、だいたい3 × 3の以下のような畳み込みテンプレートが使われます。 f:id:steavevaivai:20180715004516p:plain

畳み込みというのが普段聞きなれないかもしれないですが、一度やってみたら簡単に分かるかと思います。
例えば画像の輝度が以下のようになっている場合
f:id:steavevaivai:20180715004527p:plain
ガウシアンフィルターをかけた後、現在6の値の箇所は以下の計算で求められます。
1/16 * 1 + 2/16 * 2 + 1/16 * 3 + 2/16 * 5 + 4/16 * 6 + 2/16 * 7 + 1/16 * 9 + 2/16 * 10 + 1/16 * 11
畳み込みのテンプレートと重なった物を重みとして掛けたものを足し合わせたものになります。

ソーベルフィルタで勾配の大きさと方向を求める

ソーベルフィルタでは以下のx軸とy軸の勾配の大きさを求める畳み込みを行います。
- x軸の勾配の大きさを求める畳み込みテンプレート
f:id:steavevaivai:20180715004538p:plain
- y軸の勾配の大きさを求める畳み込みテンプレート
f:id:steavevaivai:20180715004553p:plain

それから最終的な勾配の大きさと方向を求めます。勾配の大きさは以下の式で求めます。 f:id:steavevaivai:20180715004608p:plain
次に勾配の方向は以下の式で求められます。
f:id:steavevaivai:20180715004619p:plain
背景の画像の輝度が高くて、物体の輝度が低い場合、勾配の方向は以下のように枠の部分から直角に背景をさすようになります。
f:id:steavevaivai:20180715004634p:plain

勾配方向と大きさを元に細線化する

ソーベルフィルタをつかって求めた勾配の大きさと方向を元に線を細線化します。ソーベルフィルターを掛けた後の勾配の大きさでは枠のあたりが大きな値になるのですが、細線化することで線が細くなります。
f:id:steavevaivai:20180715004648p:plain
f:id:steavevaivai:20180715004708p:plain
細線化では勾配方向の極大値以外は勾配の大きさが0になるようにします。ソーベルフィルタではは0 ~ 360度の範囲で勾配方向が求まりますが極大値かどうか求めるための比較対象となるピクセルは隣接した8ピクセルから選ぶので粗くなります。例えば、勾配の方向が22.5 ~ 67.5度なら右上と左下のピクセルの勾配の大きさと比べて、大きいのであればそのままで、小さいのであれば勾配の大きさを0にします。
以下のように勾配方向が左だったら左右のピクセルと比較して勾配が大きいものを残して、左上だったら左上、右下のピクセルと比較して勾配が大きければ残すようにします。
f:id:steavevaivai:20180715004724p:plain

閾値化でエッジを検出する

最後に閾値化でエッジを検出します。Cannyエッジでは大きい閾値と小さい閾値の2つを殺して閾値化します。大きい閾値より勾配の大きいものはエッジとして確定します。ただそれだけだとエッジが途切れ途切れになるので、小さい閾値以上で大きい閾値未満のものはエッジの候補とし、大きい方の閾値より大きいピクセルと隣接するものもエッジとします。Cannyエッジを求める際のこの閾値化の方法をヒステリシス閾値化と言います。

PythonOpenCVでエッジを検出してみる

やり方はわかったので自前で実装することもできますが、PythonではOpenCVという画像処理を行うのに便利なライブラリがすでに作られていますので、これを使いたいと思います。
まずライブラリのインストールを行います。

pip install opencv-python

それから、Cannyエッジは以下のように簡単に求められます。

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('images/kasago002.png',0)
edges = cv2.Canny(img, 150, 200)
cv2.imwrite("images/kasago002_edge.png", edges)

cv2.Canny(img, 150, 200)でCannyエッジを求めており、引数で渡している150, 200は2つの閾値になります。このときの元画像は以下のようになりエッジが求められています。
f:id:steavevaivai:20180715005014p:plain
f:id:steavevaivai:20180715004748p:plain

ヒステリシス閾値化の小さい方の閾値を調整すると検出するエッジが増えたり、減ったりするはずなので試しに以下の修正をした場合、小さい方の閾値をより小さくしたので検出するエッジが増えたのが確認できます。

edges = cv2.Canny(img, 50, 200)

f:id:steavevaivai:20180715004804p:plain