#環境
作業はJupyter Notedbook上で実行しています。
OpenCV
のバージョンは(4.5.3)です。
#cv2.findContours
とは
OpenCVの関数で、同じ値をもつ連続した点をつなげて輪郭を検出することができます。以下が公式のガイドです。
必須となる引数は以下の3つとなります。
引数 | 内容 |
---|---|
image |
輪郭を検出したい画像。精度のためにエッジ検出した二値画像の入力が推奨されている。 |
mode |
輪郭検出の階層構造を4つの中から指定できる。 |
method |
輪郭の近似手法を4つの中から指定できる。 |
戻り値は検出した輪郭の座標と、階層構造の情報を保存した配列です。
mode
とmethod
は以下の通りになります。
mode |
概要 | method |
概要 | |
---|---|---|---|---|
CV_RETR_LIST |
検出した全ての輪郭に階層構造を与えない | CV_CHAIN_APPROX_NONE |
全ての輪郭点を格納 | |
CV_RETR_EXTERNAL |
最も外側の輪郭のみ抽出し、それらに階層構造を与えない | CV_CHAIN_APPROX_SIMPLE |
水平・垂直・斜めの成分を圧縮して端点として格納 | |
CV_RETR_CCOMP |
検出した全ての輪郭に2階層構造を与える | CV_CHAIN_APPROX_TC89_L1 |
Teh-Chinチェーン近似アルゴリズム(未確認) | |
CV_RETR_TREE |
検出した全ての輪郭に完全な階層構造を与える | CV_CHAIN_APPROX_TC89_KCOS |
Teh-Chinチェーン近似アルゴリズム(未確認) |
今回はmethod
はわかりやすくかつデータ量の少ないCV_CHAIN_APPROX_SIMPLE
を使うことにし、mode
について違いを確認していきたいと思います。
#mode
の確認
##準備
以下のように必要なライブラリをインポートし、元の画像とcv2.Canny
によってエッジ処理を行った画像を表示してみます。
import numpy as np
import cv2
import matplotlib.pyplot as plt
%matplotlib inline
org_image = cv2.cvtColor(cv2.imread('image.jpg', 1), cv2.COLOR_BGR2GRAY)
org_image = cv2.resize(org_image, (256, 256))
edge = cv2.Canny(org_image, 50, 150)
#描画
fig, ax = plt.subplots(1, 2, figsize=(20, 20), sharex=True, sharey=True)
ax[0].imshow(org_image, cmap='gray')
ax[0].set_title('orginal')
ax[1].imshow(edge, cmap='gray')
ax[1].set_title('edged')
ax[0].set_xticks([])
ax[0].set_yticks([])
plt.show()
このエッジ処理後の画像を使って確認していきます。
##RETR_LIST
RETR_LIST
は特に階層構造を与えずに全ての輪郭を検出します。輪郭と階層構造をそれぞれcontours
とhierachy
に格納し、hierachy
の中身を確認します。
contours, hierachy = cv2.findContours(edge, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
hierachy
#--->
#array([[[ 1, -1, -1, -1],
# [ 2, 0, -1, -1],
# [ 3, 1, -1, -1],
# [ 4, 2, -1, -1],
# [ 5, 3, -1, -1],
# [ 6, 4, -1, -1],
# [ 7, 5, -1, -1],
# [ 8, 6, -1, -1],
# [ 9, 7, -1, -1],
# [-1, 8, -1, -1]]], dtype=int32)
このhierachy
の中に格納されているリストの意味は次の通りです。
1 | 2 | 3 | 4 |
---|---|---|---|
同階層内の次の輪郭の番号(次がなければ-1) | 同階層内の前の輪郭の番号(次がなければ-1) | 子の輪郭の番号(下の階層がなければ-1) | 親の輪郭の番号(上の階層がなければ-1) |
RETR_LIST
では階層構造なく検出されるので、3, 4番目はすべて-1になっています。
また、cv2.drawContours()
関数を使ってcontours
に保存した輪郭を描画することができます。
fig, ax = plt.subplots(2, 5, sharex=True, sharey=True, figsize=(15,15))
ax = ax.flatten()
for i in range(len(contours)):
img = cv2.drawContours(cv2.cvtColor(org_image, cv2.COLOR_GRAY2RGB), #描画する画像
contours, #輪郭を保存したリスト
i, #リストの何番目を描くか
(0,255,0), #色の指定
2) #線の太さの指定
ax[i].imshow(img)
ax[i].set_title('contour{}'.format(i))
ax[0].set_xticks([])
ax[0].set_yticks([])
plt.tight_layout()
plt.show()
線の内側と外側で2つずつ、輪郭であってほしい部分がしっかりと検出されていることがわかります。
##RETR_EXTERNAL
RETR_EXTERNAL
は一番外側の輪郭のみを階層構造なしに返します。
contours, hierachy = cv2.findContours(edge, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
hierachy
#--->
#array([[[ 1, -1, -1, -1],
# [-1, 0, -1, -1]]], dtype=int32)
RETR_LIST
と同じように3, 4番目の値はすべて-1になっていますが、格納されている数が少なくなっています。描画すると以下のようになっています。
上の直線はしっかりと外側と認識されており、また、下の四角の中は輪郭検出されていません。
##RETR_CCOMP
RETR_CCOMP
はすべての輪郭を、物体の外側の輪郭を第1階層、内側の輪郭を第2階層、というふうに2階層構造にして返します。
contours, hierachy = cv2.findContours(edge, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
hierachy
#--->
#array([[[ 2, -1, 1, -1],
# [-1, -1, -1, 0],
# [ 4, 0, 3, -1],
# [-1, -1, -1, 2],
# [ 6, 2, 5, -1],
# [-1, -1, -1, 4],
# [ 8, 4, 7, -1],
# [-1, -1, -1, 6],
# [-1, 6, 9, -1],
# [-1, -1, -1, 8]]], dtype=int32)
0番目の輪郭について、以下のように考えられます。
- 4番目の要素が-1なので親がいません、すなわち第1階層の輪郭です
- 3番目の要素が1なので、1番目の輪郭が子にいることがわかります
- 2番目の要素が-1なので、最初に出てきた第1階層の輪郭です
- 1番目の要素が2なので、次の第1階層の輪郭は2番目の輪郭です
同じように1番目の輪郭について、以下のように考えられます。
- 4番目の要素が0なので、これは0番目の輪郭の子の輪郭です
- 3番目の要素が-1なので、この輪郭に子はいません
- 2番目の要素が-1なので、0番目の子の輪郭の中で最初に出てきた第2階層の輪郭です
- 1番目の要素が-1なので、0番目の輪郭の子はもうありません
このようにして階層構造を考えることができます。例えば以下のように第1階層の輪郭、すなわち物体の外側の輪郭のみをみることができます。
#0番目の輪郭から見て、1番目の要素を追って描画する
next_c = 0
fig, ax = plt.subplots(1, 5, sharex=True, sharey=True, figsize=(20, 20))
for i in range(len(contours)):
img = cv2.drawContours(cv2.cvtColor(org_image, cv2.COLOR_GRAY2RGB), contours, next_c, (0,255,0), 2)
ax[i].imshow(img)
ax[i].set_title('contour{}'.format(i))
next_c = hierachy[0][next_c][0]
if next_c == -1: #第1階層が最後のとき脱出
break
ax[0].set_xticks([])
ax[0].set_yticks([])
plt.tight_layout()
plt.show()
輪郭は全部で10個検出されましたが、そのうち外側の5つのみを描画することができました。
##RETR_TREE
RETR_TREE
はすべての輪郭を入れ子構造を完全に保存した階層構造を持たせて返します。例の画像では下の四角たちが入れ子構造になります。
contours, hierachy = cv2.findContours(edge, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
hierachy
#--->
#array([[[ 8, -1, 1, -1],
# [-1, -1, 2, 0],
# [-1, -1, 3, 1],
# [-1, -1, 4, 2],
# [ 6, -1, 5, 3],
# [-1, -1, -1, 4],
# [-1, 4, 7, 3],
# [-1, -1, -1, 6],
# [-1, 0, 9, -1],
# [-1, -1, -1, 8]]], dtype=int32)
入れ子構造は以下の図のようになっています。
四角の中に四角があるような部分はすべて一番外側の四角の下にあるように階層構造が作られています。
うまく利用すれば、物体ごとにマスクした画像を複製することもできそう。
#使用例:背景の削除
'RETR_EXTERNAL'を使って背景の削除を試します。
エッジ処理はCanny
だけだと輪郭が複雑すぎてしまうのでdilate
を使って太くしておきます。
image = cv2.cvtColor(cv2.imread('image2.png', 1), cv2.COLOR_BGR2RGB)
gray_image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
edge_image = cv2.Canny(gray_image, 50, 70)
kernel = np.ones((3, 3), np.uint8)
edge_image = cv2.dilate(edge_image, kernel, iterations=3)
plt.imshow(edge_image, cmap='gray')
次に外側の輪郭で、囲って面積が生まれるようなものを取り出し、マスク画像を作ります。
contours, hierachy = cv2.findContours(edge_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
out = []
for c in contours:
if cv2.contourArea(c) > 0:
out.append(c)
mask = np.ones(edge_image.shape)
for i in range(len(out)):
cv2.fillConvexPoly(mask, out[i], (0))
plt.imshow(mask, cmap='gray')
最後にマスクと元画像を組み合わせると背景が黒塗りされた状態の画像(すなわち背景情報を消した画像)を作ることができます。
mask = np.ones(edge_image.shape)
for i in range(len(contours)):
cv2.fillConvexPoly(mask, contours[i], (0))
mask = cv2.dilate(mask, None, iterations=3)
mask = cv2.erode(mask, None, iterations=3)
mask_stack = np.dstack([mask]*3).astype('float32')
image = (image/255.).astype('float32')
sub_image = cv2.subtract(image, mask_stack)
plt.imshow(sub_image, cmap='gray')
Cannyの閾値やdilate, erodeのイテレーション回数、カーネルサイズを動かすことで切り取り方をすこしかえることができます。
(ここをフォルダ内の画像すべてにうまい具合に適用する、というのがすごい難しそう。多分うまくいかない画像がたくさん出てくるので、よく観察しなければならないかもしれないです。例えばマスク画像であまりに黒い面積が生まれるならその画像は見ない、とかしたほうがいいような気かしました。今後実験してみたいです。)