サイトアイコン CV & Technologies

粒をカウントする (粒子解析)

画像から種子の数をカウントします。解析の種類としては、一般的には粒子解析と言われます。光の当たり方にムラのある条件でも可能な2値化手法など応用的な内容も含んでいますが、まず初めに実施してみてほしい簡単なチュートリアルです。Pythonで細胞をカウントする方法を解説したページはこちらになります。

水菜の種子があったので、それをサンプルの画像として種子の数を数えます。このチュートリアルは、他にも顕微鏡で細胞を観察したときに核の数をカウントすること等にも応用が可能です。もし、自身でサンプルの写真を撮影する場合には背景が均一でカウント対象の物体とのコントラストの差ができるだけ大きいようにするのが良いでしょう。今回は四隅が暗く背景が均一でない画像を用意しました。あまり良い撮影条件とは言えず、ちょっとした工夫が必要になりそうです。

画像を表示する

最終的な目的は種子数のカウントですが、まずは画像を表示することから始めましょう。これは画像へのパスは各々で画像の置いてある場所に合わせてください。

#include "opencv/cv.h"
#include "opencv/highgui.h"
using namespace cv;
int main(){
    Mat img = imread("seed.jpg", IMREAD_UNCHANGED);
    imshow("IMAGE",img);
    waitKey(10000);
    return 0;
}

まずは一般的な2値化を試す

画像を2値化してみましょう。下に表示されたウィンドウのスクリーンショットを載せています。種子の部分が黒く、背景が白くなっていますが、四隅の部分では背景が暗かったため、種子が判別できません。

#include "opencv/cv.h"
#include "opencv/highgui.h"
using namespace cv;
int main(){
    Mat img = imread("seed.jpg", IMREAD_UNCHANGED);
    Mat gray_img;
    cvtColor(img, gray_img, CV_BGR2GRAY);
    Mat bin_img;
    threshold(gray_img, bin_img, 0, 255, THRESH_BINARY|THRESH_OTSU);
    imshow("IMAGE",bin_img);
    waitKey(10000);
    return 0;
}

適応的な2値化を試す

次に、周囲の明るさに応じて閾値を自動的に設定できる2値化手法を試してみましょう。adaptiveThreshold関数を使用します。下の例で99となっているのは、周囲の明るさを参考にするとき、どれくらいの広さまで勘定するかという広さを与えています。この場合は99×99の矩形領域になります。領域周囲が暗くなっていましたが、今回は周辺部分の種子も判別できそうです。

#include "opencv/cv.h"
#include "opencv/highgui.h"
using namespace cv;
int main(){
    Mat img = imread("seed.jpg", IMREAD_UNCHANGED);
    Mat gray_img;
    cvtColor(img, gray_img, CV_BGR2GRAY);
    Mat bin_img;
    //ここを変更
    adaptiveThreshold(gray_img, bin_img, 255, CV_ADAPTIVE_THRESH_GAUSSIAN_C, CV_THRESH_BINARY, 99, 8); 
    imshow("IMAGE",bin_img);
    waitKey(10000);
    return 0;
}

OpenCVは2値画像では白をモノとして認識する

種子は黒色でした。そのため、2値化しても黒くなっています。OpenCVに限らずほとんどの画像処理ライブラリは2値画像においては白をモノとして認識します。このような理由から、後々の処理の際に便利になるので、色を反転し、種子を白にしてしまいます。色の反転は簡単です。チルダ「~」をMat型の前につけると反転した画像になります。下の例のようにします。

Mat img = imread("sample.png", IMREAD_UNCHANGED);
img = ~img; //反転したimgをimgに入れる

収縮処理

種子の画像を2値化した画像を拡大したのが下の1つ目の画像です。2つ目の画像はさらに色反転を行ったものです。このまま輪郭を検出して種子を認識をすると、くっついた種子が2個分に正しくカウントされません。よって、くっついた種子を切り離す処理、すなわち、白い部分を縮ませる処理を行います。これはerode関数により実装されています。今回の動画では4回、この収縮処理を行うことで、くっついた種子をバラバラにしています(3つ目)。ただし、収縮処理をした後の画像から面積などを求めるのは不適当であるということには注意してください。文字通り、面積は小さくなっています。

#include "opencv/cv.h"
#include "opencv/highgui.h"
using namespace cv;
int main(){
    Mat img = imread("seed.jpg", IMREAD_UNCHANGED);
    Mat gray_img;
    cvtColor(img, gray_img, CV_BGR2GRAY);
    Mat bin_img;
    adaptiveThreshold(gray_img, bin_img, 255, CV_ADAPTIVE_THRESH_GAUSSIAN_C, CV_THRESH_BINARY, 99, 8);
    //変更ここから
    bin_img = ~bin_img; //色の反転
    Mat element = Mat::ones(5,5,CV_8UC1); //5×5の行列で要素はすべて1 erode処理に必要な行列
    //以下4行は同じことを4回繰り返しています
    erode(bin_img, bin_img, element, Point(-1,-1), 1); //収縮処理
    erode(bin_img, bin_img, element, Point(-1,-1), 1); //収縮処理
    erode(bin_img, bin_img, element, Point(-1,-1), 1); //収縮処理
    erode(bin_img, bin_img, element, Point(-1,-1), 1); //収縮処理
    //変更ここまで
    imshow("IMAGE",bin_img);
    waitKey(10000);
    return 0;
}

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というものもあり、これは水平・垂直・斜めの線分を圧縮し、それらの端点のみを残します。すなわち、直線で表せる分に関してデータを圧縮します。

重心計算

種子と思われる輪郭を推定したところで、次にその位置を知りたいのですべての種子の重心を求めます。今回は輪郭を構成するすべての点の重心を計算することにしましょう。重心は各点のx座標、y座標それぞれについての平均をとれば求まります。輪郭を構成する点の数はvectorの大きさ(要素数)であるのでsize()で求めることができます。これによって求めた、輪郭を構成する点の数をn個とすると、重心x座標はすべての点のx座標を足し合わせ、最後にnで割れば平均となるので計算できます。重心y座標も同様に求めます。なお、OpenCVでは近似ではなく、数学的に正しい重心を計算することも可能です。厳密な重心の求め方はこちら

下の例では、求めた座標を中心とする半径3の円を元の動画に描画しています。それに伴い、表示する画像も2値画像ではなく元の画像に円を描画したものに変更しています。表示される動画のスクリーンショットは下図のようになります。数個の種子がカウントから漏れていることがわかりますが、ほぼすべての種子を数えることができています。全自動でさらに精度を上げるにはより時間をかけたコーディングが必要になります。最後に標準ライブラリのcoutを使って種子の数を出力しています。下のように出力されるはずです。596個の種子を認識しカウントできたことになります。
596 seeds

#include "opencv/cv.h"
#include "opencv/highgui.h"
#include <iostream> //追加
#include <vector> //追加
using namespace cv;
using namespace std; //追加
int main(){
    Mat img = imread("seed.jpg", IMREAD_UNCHANGED);
    Mat gray_img;
    cvtColor(img, gray_img, CV_BGR2GRAY);
    Mat bin_img;
    adaptiveThreshold(gray_img, bin_img, 255, CV_ADAPTIVE_THRESH_GAUSSIAN_C, CV_THRESH_BINARY, 99, 8);
    bin_img = ~bin_img; //色の反転
    Mat element = Mat::ones(5,5,CV_8UC1); //5×5の行列で要素はすべて1 erode処理に必要な行列

    erode(bin_img, bin_img, element, Point(-1,-1), 1); //収縮処理
    erode(bin_img, bin_img, element, Point(-1,-1), 1); //収縮処理
    erode(bin_img, bin_img, element, Point(-1,-1), 1); //収縮処理
    erode(bin_img, bin_img, element, Point(-1,-1), 1); //収縮処理

    //次の1行は上の4行と同じ意味を持ちます
    //erode(bin_img, bin_img, element, Point(-1,-1), 4); //最後の4が4回分ということを表す

    //追加ここから
    vector<vector<Point> > contours;
    findContours(bin_img, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
    for(int i=0; i<contours.size(); i++){
        int count=contours.at(i).size();
        double x=0; double y=0;
        for(int j=0;j<count;j++){
            x+=contours.at(i).at(j).x;
            y+=contours.at(i).at(j).y;
        }
        x/=count;
        y/=count;
        circle(img, Point(x,y),5, Scalar(0,0,255),2,4);
    }
    cout<<contours.size()<<" seeds"<<endl;
    //追加ここまで 
    imshow("IMAGE",img); //変更 
    waitKey(10000); return 0; 
}

モバイルバージョンを終了