ITや趣味など気軽に投稿しています。

【Python】OpenCVで画像から物体の輪郭を検出する

今回は物体の輪郭検出をやってみよう。

画像解析といえば物体検出ですよね♪

とはいっても、今回は機械学習モデルなどは使わず、アルゴリズムベースで実施する物体検出だがな。

はじめに

まず輪郭とは、画像内の物体の境界上の全ての点を結んだものである。画像処理における輪郭は、同じ色味をもった境界ピクセルのことを指す。

ということは、色が急激に変化する点を線で結んだものが物体の境界となるってことですか?

その通り。以下の画像の例がわかりやすいだろう。

なるほど、黒背景と白い鉛筆とカップが境界というわけですね。

うむ。アルゴリズムベースでは、このように色変化の大きい部分を輪郭とみなす。

輪郭検出の手順

OpenCVでは親切にも輪郭を検出する関数が用意されている。

便利ですね。早く教えてください。

但し、単純にインプット画像に対しその関数を適用するだけでは、期待する輪郭検出はできない可能性が高い。

ええ、なぜですか?

例えば画像が明るかったり暗かったり、背景と物体の色が似ていたり、、と、毎回条件が同じわけではない。解析時には、それらの条件をなるべく合わせてやることで、同じような解析結果を期待できる。

ということは、輪郭検出の前に「条件を合わせる」作業が必要なんですね。

その通り。それらの処理は前処理と呼ばれる。前処理を含めた輪郭検出の大まかな流れは以下の通りだ。

1. 入力画像のグレースケール化

 インプットとなる画像を読み込んだら、その画像をグレースケールに変換します。この処理は、次のステップで行う閾値処理のための準備です。

2. 閾値処理

続いて閾値処理を行い、物体の輪郭を明らかにします。このステップではバイナリ閾値処理やCannyのエッジ検出手法等が用いられます。

今回はバイナリ閾値処理を適用します。

閾値処理をすることで、検出したい物体が強調されて表示されるようになります。この処理によって、検出したい物体は白のピクセルに、背景は黒のピクセルに変換されます(二値化)。

閾値処理については以下の記事にまとめています。

Cannyでのエッジ検出についてはこちら

3. 輪郭を検出する

cv2.findContours()を用いて、閾値処理をした画像から輪郭を検出します。

4. 元の画像に輪郭を描画する

cv2.drawContours()を用いてインプットの画像に輪郭を重ねて表示します。

Pythonで実装する

まずはコード全体と結果を見てみよう。

import cv2
import matplotlib.pyplot as plt
import japanize_matplotlib

#入力画像
image = cv2.imread('input/cup.jpg')

#画像のサイズ縮小
height = image.shape[0]
width = image.shape[1]
image = cv2.resize(image,(round(width/4), round(height/4)))

#グレースケール化
image_gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)

#閾値処理
ret,thresh = cv2.threshold(image_gray,95,255,cv2.THRESH_BINARY)

#輪郭検出 (cv2.ChAIN_APPROX_SIMPLE)
contours1, hierarchy1 = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

#輪郭の描画
img_contours = cv2.drawContours(cv2.cvtColor(image_gray,cv2.COLOR_BGR2RGB), contours1, -1, (0, 255, 0), 2, cv2.LINE_AA)

# 画像とエッジ画像を表示
plt.figure(figsize=(10, 6))                                                     
plt.subplot(1, 2, 1)                                                     
plt.imshow(image)                                         
plt.title('Original')                                             
plt.axis("off")                                                        
plt.subplot(1, 2, 2)                                                      
plt.imshow(img_contours)                                       
plt.title('輪郭検出')                                             
plt.axis("off")                                                         

plt.show()
輪郭検出結果

前処理まで

前処理までの部分は簡単に説明する。

import cv2

#入力画像
image = cv2.imread('input/cup.jpg')

#画像のサイズ縮小
height = image.shape[0]
width = image.shape[1]
image = cv2.resize(image,(round(width/4), round(height/4)))

image_copy1 = image.copy()
#グレースケール化
image_copy1 = cv2.cvtColor(image_copy1,cv2.COLOR_BGR2GRAY)

#閾値処理
ret,thresh = cv2.threshold(image_copy1,95,255,cv2.THRESH_BINARY)

cv2.cvtColor()でグレースケールの画像にし、cv2.threshold()で閾値処理を実行している。

今回は、値が95より大きいピクセルを255(白)に設定し、それ以外のピクセルは0(黒)に設定する。

これをすることで、境界がはっきりするんですね。

その通り。ただし、画像によって必要な前処理は変わってくるので、そこだけは注意してほしい。

輪郭検出(cv2.findContours())

ここから本題に入る。輪郭検出はcv2.findContours()を使う。

cv2.findContours(image, mode, method[, contours[, hierarchy[, offset]]])

引数

OpenCV共通の引数はこちら

引数説明
mode輪郭の検索モード。RetrievalModesで定義されるパラメータを指定する。
method輪郭の近似方法。ContourApproximationModesで定義されるパラメータを指定する。
contours検出された輪郭を格納するList。
hierarchy輪郭の階層情報を格納するNumpy配列。
offsetすべての輪郭点を指定の値だけシフトする。
(x, y)形式で指定する。

コード解説

今回、modeにはcv2.RETR_TREEを、methodにはcv2.CHAIN_APPROX_SIMPLEを指定している。

#輪郭検出 (cv2.ChAIN_APPROX_SIMPLE)
contours1, hierarchy1 = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

戻り値として、輪郭情報はcontours1に格納される。

輪郭描画(cv2.drawContours())

最後は検出した輪郭を描画する。輪郭描画にはcv2.drawContours()を用いる。

cv2.drawContours(image, contours, contourIdx, color[, thickness[, lineType[, hierarchy[, maxLevel[, offset]]]]])

引数

OpenCV共通の引数はこちら

引数説明
contours描画する輪郭を格納するList。
contourIdx描画する輪郭のインデックス。
hierarchy輪郭の階層情報を格納するNumpy配列。
maxlevel描画する最大階層。

コード解説

輪郭情報は引数contoursとして配列で渡し、描画対象はcontourIdxで指定する。contourIdxは-1など負の値を指定すると、すべての輪郭を描画する。

#輪郭の描画
cv2.drawContours(image, contours1, -1, (0, 255, 0), 2, cv2.LINE_AA)

まとめ

今回は、OpenCVで物体の輪郭を検出し、描画する方法を紹介した。

輪郭の検出にはcv2.findContours()、輪郭の描画にはcv2.drawContours()を使うんでしたね。

精度を高めるために、前処理も忘れないように。では今回はここまでにしよう。

ありがとうございました~~♪