OpenCV-Python Tutorials (6) ~Image Processing in OpenCV~

OpenCV-Python Tutorialsの記事,Image Processing in OpenCV の章の Contours in OpenCV を見ていく. 今回は,Contours : Getting Started と Contour Features の内容のメモ.

公式:Image Processing in OpenCV — OpenCV-Python Tutorials 1 documentation

この中で試したコードはGitHubに置いておくことに.

github.com

輪郭(Contours) : はじめに

目標
  • 輪郭とは何かを理解する
  • 輪郭の発見,輪郭の描画などを学ぶ
  • 次のような関数を見ていく:cv2.findContours()cv2.drawContours()
輪郭とは

輪郭は単純に,(境界にある)連続する全ての点を繋げた曲線で色や明度を持っているものとして説明できる.輪郭は形を分析したり,ものを検出・認識するのに有効な道具である.

  • より良い精度のため,2値化画像を用いる.よって輪郭を見つける前に,2値化やcannyエッジ検出を適用する.
  • findContours関数はソース画像に変更を加える.よってもし輪郭を見つけた後もソース画像が欲しい場合は,他の変数に格納しておく.
  • OpenCVでは,輪郭抽出は黒い背景から白い物体を見つけるようなものだ.だから,発見される物体は白,背景は黒でなければならないことを覚えておこう.

2値化画像からどのように輪郭を見つけるかを見ていこう.

import numpy as np
import cv2

im = cv2.imread()
imgray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(imgray, 127, 255, 0)
img, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

cv2.findContours()の中に3つの引数があり,1つ目はソース画像,2つ目は輪郭検索のモード,3つ目は輪郭近似の手法だ.そして,画像,輪郭,ヒエラルキーを出力する.contoursは画像中の全ての輪郭のPythonのリストだ.それぞれの独立した輪郭は境界点の(x,y)の組み合わせを表すNumpyのarrayだ.

メモ

2番目,3番目の引数とヒエラルキーについての詳細については後で述べる.それまでは,サンプルコードの中で与えられた変数は全ての画像に対してうまく働く.

どうやって輪郭を描く?

輪郭を描くためには,cv2.drawContours関数が使われる.これは境界点を持つ全ての形状に対しても使うことができる.これの最初の引数はソース画像,2番目の引数はPythonのリストとして渡される輪郭,3番目は輪郭のインデックス(独立した輪郭を描くときに有効.全ての輪郭を描くには,-1を渡す),残りは色と太さなどだ.

画像上に全ての輪郭を描くには:

img = cv2.drawContours(img, contours, -1, (0,255,0), 3)

独立した輪郭を描くには,4番目の輪郭を指定すると:

img = cv2.drawContours(img, contours, 3, (0,255,0), 3)

だがほとんどの場合,次の方法が有効だ:

cnt = contours[4]
img = cv2.drawContours(img, [cnt], 0, (0,255,0), 3)
メモ

後ろの二つの方法は同じだが,先に進めば最後のやり方がより有効だとわかるだろう.

輪郭の近似の方法

これはcv2.findContours関数の3番目の引数だ.実際に何を示しているのか?

上で,輪郭はある形の境界で同じ強度のものだと言った.それらはある形状の境界を(x, y)の組で保持している.しかし,全ての輪郭を保持しているのか?それはこの輪郭の近似の方法に示されている.

もしcv2.CHAIN_APPROX_NONEを渡すと,全ての境界点が保持される.しかし,実際に全ての点が必要だろうか?例えば,直線の輪郭をみつけたとしよう.その線を表現するのにその線上の全ての点が必要か?いや,端の2点だけが必要だ.これがcv2.CHAIN_APPROX_SIMPLEがすることだ.これは全ての余分な点を除き輪郭を圧縮する.それによってメモリが節約できる.

下の画像は長方形を用いてのこの技術のデモンストレーションだ.輪郭の配列にある点について円を描画しているだけだ.最初の画像はcv2.CHAIN_APPROX_NONEを用いて取得した点(512点),2つ目の画像はcv2.CHAIN_APPROX_SIMPLEを用いて取得した点(4点)を表している.どれだけのメモリが節約できたかがわかる.

f:id:asdm:20160225182839p:plainf:id:asdm:20160225182844p:plain

輪郭の特徴

目標
  • 領域,境界線,重心,境界ボックスなどのような輪郭の異なる特徴を見つける
  • 輪郭に関係する多くの関数を見ていく
1. モーメント

画像のモーメントは対象物のかたまりの重心,対象物の面積などのような画像の特徴を計算するのに役立つ.wikipediaImage Momentsのペーズをチェックしてみよう.

cv2.moments()は全てのモーメント値を計算した辞書型配列をあたえてくれる.下を見てみよう:

import cv2
import numpy as np

img = cv2.imread('star.jpg',0)
ret,thresh = cv2.threshold(img, 127, 255, 0)
image, contours, hierarchy = cv2.findContours(thresh, 1, 2)

cnt = contours[0]
M = cv2.moments(cnt)

print(M)

このモーメントから,面積や重臣など有用な情報を求めることができる.重力は,の関係で与えられる.これは次のようにできる:

cx = int(M['m10']/M['m00'])
cy = int(M['m01']/M['m00'])
2. 輪郭領域

輪郭領域はcv2.contourArea(),またはモーメントM['m00']から求められる.

area = cv2.contourArea(cnt)
3. 輪郭の境界線

これはアーク長(弧長)とも呼ばれる.cv2.arcLength関数を用いて求められる.2番目の引数は(Trueが渡されると)閉曲線か,または単なる曲線なのかを明示する.

perimeter = cv2.arcLength(cnt,True)
4. 輪郭の近似

正確な表現より少ない頂点の数によって輪郭の形を他の形に近似する.これはDouglas-Peucker algorithmの実行だ.アルゴリズムとデモンストレーションはwikipediaのページをチェックしよう.

これを理解するに次のことを考えてみよう.画像の中にある四角を見つけようとするが,画像中の幾つかの問題により完璧な四角は得られず"悪い形"(下の最初の図に示すように)である.ここで,形の近似を行うためにこの関数を使う.ここでは輪郭から近似された輪郭への距離を最大にするが,2番目の引数はepsilonと呼ばれその正確さを示す.epsilonの賢い選択は正しい出力を得るために必要とされる.

epsilon = 0.1*cv2.arcLength(cnt,True)
approx = cv2.approxPolyDP(cnt,epsilon,True)

下の2番目の画像での緑の線はepsilon = アーク長の10%の曲線の近似を示す.3番目の画像は同様のepsilon = アーク長の1%の曲線の近似を示す.3番目の引数は曲線が閉じているかどうかを指定する.

f:id:asdm:20160229191931j:plain
5. 凸包(とつほう)

凸包は輪郭の近似と類似しているように見えるが,異なるものだ(どちらもが同じ結果になる場合もあるが).cv2.convexHull()関数は凸面の曲線の欠落をチェックし修正する.一般的に,凸曲線は膨れ上がっている,もしくは少なくとも平坦である.そしてもし内側に突き出しているなら,convexity defectsと呼ばれる.例えば,下の手の画像を見てみよう.赤線が手の凸包を表している.矢印がconvexity defectsを表していて,輪郭から凸包への極初期に最大となる偏差である.

f:id:asdm:20160511181014j:plain

この構文について少し言うことがある:

hull = cv2.convexHull(points[, hull[, clockwise[, returnPoints]]
  • point は渡す輪郭だ
  • hull は出力で,普段は避けている
  • clockwise : 方向のフラグ.Trueなら,出力される凸包は時計回りになる.そうでないなら,反時計回りになる.
  • returnPoints : デフォルトではTrueで,欠落した点の座標を返す.Falseならば,欠落した点に対応した輪郭の点のインデックスを返す.

上の画像にあるような凸包を取得するためには,下記で十分だ:

hull = cv2.convexHull(cnt)

しかし,凸面の欠落を見つけたいなら,returnPoints = Falseを渡す必要がある.それを理解するには,上の矩形の画像を見てみよう.最初にcntとして輪郭を見つけた.そして,returnPoints = True で凸包を見つけ,次のような値が得られた:[[[234 202]], [[ 51 202]], [[ 51 79]], [[234 79]]]これは矩形の四隅の点だ.同じことをreturnPoints = Falseですると,次のような結果を得る:[[129],[ 67],[ 0],[142]].ここには輪郭と対応した点がある.例えば,最初の値を見てみると:cnt[129] = [[234, 202]]これは最初の結果と同じだ(そして他のも同様).

のちに凸面の欠落について議論する時に,またこれらを見ることになる.

6. 凸面のチェック

曲線が凸であるかのチェックを行う関数cv2.isContourConvex()がある.これは,単にTrueかFalseを返す.

k = cv2.isContourConvex(cnt)
7. 外接長方形

外接長方形には二種類ある.

7.a. Straight Bounding Rectangle

これは直立した矩形で,対象物の回転を考慮しない.したがって,外接長方形の面積は最小にならない.関数cv2.boundingRect()によって得られる.

(x,y)を左上の座標,(w,h)を矩形の幅と高さとする.

x,y,w,h = cv2.boundingRect(cnt)
img = cv2.rectangle(img,(x,y),(x+w,y+h),(0,255,0),2)
7.b. Rotated Rectangle

この外接長方形は最小の面積になるように描かれるため,回転を考える.ここで使われる関数はcv2.minAreaRect()だ.これは,(左上の座標(x,y), (幅, 高さ), 回転角)を含むBox2D構造のものを返す.しかし,この矩形を描くには,矩形の四隅が必要だ.これは関数cv2.boxPoints()によってわかる.

rect = cv2.minAreaRect(cnt)
box = cv2.boxPoints(rect)
box = np.int0(box)
im = cv2.drawContours(im,[box],0,(0,0,255),2)

両方の結果を下に示す.緑の矩形は通常の外接長方形,赤の矩形は回転外接長方形である.

f:id:asdm:20160512122136p:plain

8. Minimum Enclosing Circle

次に,対象物の外接円をcv2.minEnclosingCircle()によって求める.これは最小の面積で対象物を完全に覆う円である.

(x,y),radius = cv2.minEnclosingCircle(cnt)
center = (int(x),int(y))
radius = int(radius)
img = cv2.circle(img,center,radius,(0,255,0),2)

f:id:asdm:20160512145420p:plain

9. Fitting an Ellipse

次のものは,対象物に楕円を合わせるものだ.これは回転外接長方形に内接する楕円を返す.

ellipse = cv2.fitEllipse(cnt)
im = cv2.ellipse(im,ellipse,(0,255,0),2)
10. Fitting a Line

同様に点の集合に線を合わせることができる.下の画像は白い点の集合を含んでいる.これに合わせて直線を引くことができる.

f:id:asdm:20160512150315j:plain