findContours関数で輪郭を検出する
輪郭の内側の面積を計算し、画像内で最も大きいものをダンゴムシとして識別することにします。OpenCVでは輪郭は点の集合として保持されます。輪郭を構成する点の重心を計算することで近似的なダンゴムシの重心座標を求めます。findContours関数は2値画像を入力に使い、すべての輪郭、すなわち、すべての白と黒の境界線を検出します。それゆえ、検出される輪郭は複数になります。さらに、いくつの輪郭が検出されるかはプログラムを実行してみるまでわからないため可変長配列である必要があります。したがって、標準ライブラリのvectorを使うことになります。さらにややこしいことに各輪郭を構成する点もいくつかわからないので、これにもvectorを使うことになります。したがって、vectorの入れ子構造になります。初めはわかりにくいと思いますが、こういうものとして覚えてもらっても実質上の問題はありません。輪郭を保持する入れ物は下のように宣言します。これをfindContour関数に渡すと、輪郭を検出してこの中に点の集合として輪郭の情報を入れて戻してくれます。
vector<vector<Point> > contours;
findContours(bin_img, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
findContours関数のオプション
findContour関数の第三引数は輪郭の検出方法を指定します。上の例のCV_RETR_EXTERNALは一番外側の白の輪郭のみを取得することを意味します。他にも多くの種類がありますが、もう一つよく使うのはCV_RETR_LISTだと思います。白の輪郭、黒の輪郭、内側、外側関係なく、すべての輪郭が取得されます。第四引数には輪郭点の格納方法を指定します。CV_CHAIN_APPROX_NONEはすべての点を格納するため任意の隣接する2点は互いに8近傍内に存在します。CV_CHAIN_APPROX_SIMPLEというものもあり、これは水平・垂直・斜めの線分を圧縮し、それらの端点のみを残します。すなわち、直線で表せる分に関してデータを圧縮します。
面積計算
findContour関数はすべての輪郭を取得するためどれがダンゴムシの輪郭であるかは推定する必要があります。ここでは、最大の輪郭内面積を持つ輪郭をダンゴムシの輪郭と推定することにしましょう。輪郭内の面積はcontourArea()関数で計算することができます。下の例では、最大の面積を持つ輪郭を特定し、max_area_contourという整数型の変数に保持させています。なお、輪郭はvectorを使った構造に保持されているため、任意番目の輪郭にat()を使ってアクセスできます。
vector<vector<Point> > contours;
findContours(bin_img, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
double max_area=0;
int max_area_contour=-1;
for(int j=0;j<contours.size();j++){
double area=contourArea(contours.at(j));
if(max_area<area){
max_area=area;
max_area_contour=j;
}
}
重心計算
ダンゴムシと思われる輪郭を推定したところで、次にその位置を知りたいので重心を求めます。簡単な方法としては楕円フィッティングを行って楕円の中心を求める方法などが考えられますが、楕円フィッティングは楕円形状に輪郭が検出されていない場合に重心とはかけ離れた座標を計算してしまうことがあります。そこで、今回は輪郭を構成するすべての点の重心を計算することにしましょう。重心は各点のx座標、y座標それぞれについての平均をとれば求まります。輪郭を構成する点の数はvectorの大きさであるのでsize()で求めることができます。これによって求めた、輪郭を構成する点の数をn個とすると、重心x座標はすべての点のx座標を足し合わせ、最後にnで割れば平均となるので計算できます。重心y座標も同様に求めます。なお、OpenCVでは近似ではなく、数学的に正しい重心を計算することも可能です。厳密な重心の求め方はこちら。
下の例では、求めた座標を中心とする半径50の円を元の動画に描画しています。それに伴い、表示する画像も2値画像ではなく元の画像に円を描画したものに変更しています。表示される動画のスクリーンショットは下図のようになります。
#include "opencv/cv.h"
#include "opencv/highgui.h"
#include <vector> //追加
using namespace cv;
using namespace std; //追加
int main(){
Mat img;
Mat gray_img;
Mat bin_img;
Mat element = Mat::ones(3,3,CV_8UC1);
VideoCapture cap("pillbug.mp4");
int max_frame=cap.get(CV_CAP_PROP_FRAME_COUNT);
for(int i=0; i<max_frame;i++){ cap>>img;
cvtColor(img, gray_img, CV_BGR2GRAY);
threshold(gray_img,bin_img,70,255,THRESH_BINARY);
bin_img=~bin_img;
dilate(bin_img, bin_img, element, Point(-1,-1), 3);
//追加ここから
vector<vector<Point> > contours;
findContours(bin_img, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
double max_area=0;
int max_area_contour=-1;
for(int j=0;j<contours.size();j++){
double area=contourArea(contours.at(j));
if(max_area<area){
max_area=area;
max_area_contour=j;
}
}
int count=contours.at(max_area_contour).size();
double x=0;
double y=0;
for(int k=0;k<count;k++){
x+=contours.at(max_area_contour).at(k).x;
y+=contours.at(max_area_contour).at(k).y;
}
x/=count;
y/=count;
circle(img, Point(x,y),50, Scalar(0,0,255),3,4);
//追加ここまで
imshow("Video",img); //変更
waitKey(1);
}
return 0;
}