opencvのfindContoursの使い方のメモ。返り値のcontoursの理解を深める。

findContoursを使いたい動機は、数字が書かれている領域を取り出したい。書かれている領域の四角の4座標を自動で入手したい。

以下の画像の白い四角の輪郭抽出をfindContorsで実現します。

findContoursの使い方

contours, hierarchy = cv2.findContours(img, A, B)

入力

imgはグレースケールになっている必要があります。そのため、この処理の前にグレースケール化します。

Aに指定できる引数説明
cv2.RETR_LIST全ての輪郭・輪郭の親子なし
cv2.RETR_EXTERNAL最も外側の輪郭のみ
cv2.RETR_CCOMP全ての輪郭・輪郭の親子
cv2.RETR_TREE全ての輪郭・輪郭の親子孫ひ孫・・
Bに指定できる引数説明
cv2.CHAIN_APPROX_NONE輪郭の全点
cv2.CHAIN_APPROX_SIMPLE輪郭の代表点

返り値

contours・・・輪郭 後で例で見てみる

hierarchy・・・階層情報

サンプルコード

cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLEを使います。

import cv2
import numpy as np

img = cv2.imread(r"C:\Users\sample_edge.JPG")
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contours,hierarchy = cv2.findContours(img,cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

contoursの中身は、こんな形。

輪郭のグループごとに1つのnumpy ndarrayになっている。理想としては、1グループしか残らないほうがよかったです。しかし、実際は12グループも輪郭ができてしまいました。試しに1グループの中身をのぞいてみます。

インデックス1のグループを見てみる。

cnt = contours[1]
print(cnt)
[[[942 413]]
  [[941 414]]
  [[939 414]]
  [[939 415]]
  [[940 414]]
  [[941 415]]
  [[943 415]]
  [[943 413]]]

いったいどういう形をしているんでしょう。試しに描写してみます。

輪郭の描写

cv2.drawContours(画像,輪郭情報,輪郭情報のスライス番号, RGB, ラインの太さ)

dummy = np.zeros(shape = img.shape)

cnt = contours[1]
dummy2 = cv2.drawContours(dummy.copy(),[cnt],0, (255,255,255), 3)
cv2.imshow('image',dummy2)
cv2.waitKey()

え?真ん中より右下のところに、白い点があるけどこれだけ?

ほかの輪郭を描写してみましたが、インデックス11番号以外はすべてこんな感じでした。インデックス11は以下の形をしていました。

dummy = np.zeros(shape = img.shape)

cnt = contours[11]
dummy2 = cv2.drawContours(dummy.copy(),[cnt],0, (255,255,255), 3)
cv2.imshow('image',dummy2)
cv2.waitKey()

そうそうこんな感じです。でも、右側のラインが変になっています。インデックス11の輪郭の中身を見てみます。

cnt = contours[11]
print(cnt)
[[[400 208]]
  [[400 223]]
  [[401 224]]
  [[401 400]]
  [[402 400]]
  [[403 401]]
  [[401 403]]
  [[400 402]]
  [[400 407]]
  [[401 407]]
  [[402 408]]
  [[406 408]]
  [[407 409]]
  [[927 409]]
  [[927 407]]
  [[929 405]]
  [[930 406]]
  [[930 407]]
  [[931 406]]
  [[932 406]]
  [[933 407]]
  [[932 406]]
  [[934 404]]
  [[934 403]]
  [[935 402]]
  [[934 403]]
  [[933 402]]
  [[933 401]]
  [[935 399]]
  [[937 399]]
  [[937 224]]
  [[938 223]]
  [[939 223]]
  [[940 224]]
  [[940 399]]
  [[941 399]]
  [[941 224]]
  [[942 223]]
  [[943 224]]
  [[943 399]]
  [[943 220]]
  [[942 219]]
  [[942 208]]
  [[942 213]]
  [[941 214]]
  [[940 213]]
  [[940 208]]
  [[929 208]]
  [[928 209]]
  [[927 208]]]

4点どころじゃないです。たまに、len(cnt)==4を使って長方形を取り出している解説がありますが、無理そうです。

輪郭の近似

輪郭を近似して、四角にします。輪郭の近似は、以下の3コードで実現できるようです。0.08という数字は近似の強さです。

cnt = contours[11]
epsilon = 0.08*cv2.arcLength(cnt,True) #数字は試行錯誤が必要
approx = cv2.approxPolyDP(cnt,epsilon,True)

approxの中身を見てみます。

print(approx)
[[[400 208]]
 [[402 408]]
 [[943 399]]
 [[942 208]]]

無事に4点に近似できました。….実は、epsilon = 0.08* ~ の数字を試行錯誤してちょうどいい値を見つけたからこそ、4つのみをとりだせました。ここを0.1にすると2点しか取り出せなかったし、0.01にすると6点も残ってしまいます。近似の力を調整しないといけません。

どういった輪郭になっているか画像を確認してみます。念のため、今までのコードを全部入れておきます。

img = cv2.imread(r"C:\Users\sample_edge.JPG")
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contours,hierarchy = cv2.findContours(img,cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
dummy = np.zeros(shape = img.shape)
cnt = contours[11]
epsilon = 0.08*cv2.arcLength(cnt,True) #数字は試行錯誤が必要
approx = cv2.approxPolyDP(cnt,epsilon,True)
dummy2 = cv2.drawContours(dummy.copy(),[approx],0, (255,255,255), 3)
cv2.imshow('image',dummy2)
cv2.waitKey()

この方法だと、1グループずつ輪郭をチェックして求めている四角の輪郭を手動で見つけなければなりません。そんなのやってられません。そこで以下の方法を取ります。

Canny edge detection

canny edge detectionは、画像中の端っこを強調した画像(座標ではない)を作ることができます。これと、findcontourを組み合わせることでより精度の高く輪郭座標を抽出できます。

canny edge detectionの使い方は以下の通りです。

edged = cv2.Canny(img, 数字1, 数字2)

入力

imgはグレースケール画像
数字1は端っこのつながりを切る力
数字2は端っこを無視する力

数字1と数字2は試行錯誤するしかありません。数字1を小さくすると、端っこがつながっていき、数字2を小さくすると端っこがどんどん検出されていきます。

img = cv2.imread(r"C:\Users\sample_edge.JPG")
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edged = cv2.Canny(img, 50, 100) #数字は試行錯誤が必要
cv2.imshow('image',edged)
cv2.waitKey()

完璧な画像を作り出してしまいました。….実はこれも、Cannyの数字1と数字2を試行錯誤でよい値を見つけたからです。端っこの座標を知るためにこの画像をfindcontoursします。

contours,hierarchy = cv2.findContours(edged ,cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

contoursの中身を見てみます。今回も、1つだけのグループではなく2つ取れてしまいました。

中身をみてみます。

print(contours[0])
print(contours[1])

0と1でまったく同じでした。

[[[407 209]]
 [[405 211]]
 [[405 398]]
 [[407 400]]
 [[928 400]]
 [[930 398]]
 [[930 211]]
 [[928 209]]]

これは、線の内側と外側を輪郭だと思われます。線が細いために完全一致したものと推測します。もし線が太ければ、少し差がでたと思います。

また、4点にするために輪郭の近似が必要です。

cnt = contours[0]
epsilon = 0.08*cv2.arcLength(cnt,True) #数字は試行錯誤が必要
approx = cv2.approxPolyDP(cnt,epsilon,True)
print(approx)
[[[405 211]]
 [[928 209]]
 [[930 398]]
 [[407 400]]]

図に書いてみます。

dummy = np.zeros(shape = img.shape)
dummy2 = cv2.drawContours(dummy.copy(),[approx],0, (255,255,255), 3)
cv2.imshow('image',dummy2)
cv2.waitKey()

これなら、手動で輪郭を選択する手間が省けるので少しはいいですね!

Canny + findContoursの完成コード

import cv2
import numpy as np

img = cv2.imread(r"C:\Users\sample_edge.JPG")
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edged = cv2.Canny(img, 50, 100)

contours,hierarchy = cv2.findContours(edged ,cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cnt = contours[0]
epsilon = 0.08*cv2.arcLength(cnt,True) #数字は試行錯誤が必要
approx = cv2.approxPolyDP(cnt,epsilon,True)

dummy = np.zeros(shape = img.shape)
dummy2 = cv2.drawContours(dummy.copy(),[approx],0, (255,255,255), 3)
cv2.imshow('image',dummy2)
cv2.waitKey()

Categories:

category