FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

watershedアルゴリズムを使って画像を領域毎に分割する

0. はじめに

どうも、お久しぶりです。原田@C++大好きです。

本当は、皆様にMonad実装してみた!とかやって見たかったのですが…

タイムアップでして、そのうち実装してみます。

今回は、もうScala関係なしにC++OpenCVネタです。

あーこんなのあるのかー程度に見ていただければ幸いです。

1. watershedって何さ?

watershedの意味 - 英和辞典 Weblio辞書

まずは英語、分水嶺(れい)、分水界、(川の)流域、分岐点、転機という意味です。

まぁ、何かというと画像の上から水を流しこむような形で画像の領域を判断するアルゴリズムです。

詳しくはここらあたりを見てもらえるとちょっとはわかりやすくなるかも。

ウォーターシェッド(領域分割) | 計測 | ヴィスコの画像処理技術 | ヴィスコ・テクノロジーズ株式会社

2. 実際のコード

// 面積計算アルゴリズム(画素数)
int calculateArea(cv::Mat * image) {
    cv::Mat grayMat;
    cv::cvtColor(image, grayMat, CV_BGR2GRAY);
    return cv::countNonZero(grayMat);
}

// 画像の分割アルゴリズム
void divide(cv::Mat base, cv::Mat & distA, cv::Mat & distB) {
    // Watershed分割
    // グレースケール
    cv::Mat bw;
    cv::cvtColor(base, bw, CV_BGR2GRAY);
    cv::threshold(bw, bw, 40, 255, CV_THRESH_BINARY | CV_THRESH_OTSU);
    cv::imshow("dist", bw);

    cv::Mat dist;
    // 輪郭から内側にいけばいくほど濃くする
    cv::distanceTransform(bw, dist, CV_DIST_L2, 3);
    cv::normalize(dist, dist, 0, 1., cv::NORM_MINMAX);
    cv::imshow("Distance Transform Image", dist);

    // 二値化してゴミ取り
    cv::threshold(dist, dist, .4, 1., CV_THRESH_BINARY);
    cv::Mat kernel1 = cv::Mat::ones(3, 3, CV_8UC1);
    cv::dilate(dist, dist, kernel1);
    imshow("Peaks", dist);

    cv::Mat dist_8u;
    dist.convertTo(dist_8u, CV_8U);

    // すべてのマーカーを取得
    cv::vector < cv::vector < cv::Point > > contours;
    cv::findContours(dist_8u, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);

    // watershedに流し込む用のマーカー画像作成
    cv::Mat markers = cv::Mat::zeros(dist.size(), CV_32SC1);
    // マーカーを描画
    for (size_t i = 0; i < contours.size(); i++) {
        cv::drawContours(markers, contours, static_cast < int > (i), cv::Scalar::all(static_cast < int > (i) + 1), -1);
    }
    // 背景用のマーカー作成
    circle(markers, cv::Point(5, 5), 3, CV_RGB(255, 255, 255), -1);
    imshow("Markers", markers * 10000);

    // ベース画像を8ビット変換
    cv::Mat stage_8bit_3channel;
    cv::cvtColor(base, stage_8bit_3channel, CV_BGR2RGB, 3);
    cv::watershed(stage_8bit_3channel, markers);
    imshow("watershed ", markers * 10000);

    cv::Mat dividA = cv::Mat::zeros(base.size(), CV_8UC4);
    cv::Mat dividB = cv::Mat::zeros(base.size(), CV_8UC4);

  // 分割した画像をそれぞれの画像に書き込む
    for (int i = 0; i < markers.rows; i++) {
        for (int j = 0; j < markers.cols; j++) {
            // 白固定してますが、色わけしたい場合は適当にいじってください。
            int r = 255;
            int g = 255;
            int b = 255;
            int index = markers.at < int > (i, j);
            if (index == 1) {
                dividA.at < cv::Vec4b > (i, j) = cv::Vec4b(r, g, b, 255);
            } else if (index == 2) {
                dividB.at < cv::Vec4b > (i, j) = cv::Vec4b(r, g, b, 255);
            } else {
                dividA.at < cv::Vec4b > (i, j) = cv::Vec4b(0, 0, 0, 0);
                dividB.at < cv::Vec4b > (i, j) = cv::Vec4b(0, 0, 0, 0);
            }
        }
    }
    imshow("dividA", dividA);
    imshow("dividB", dividB);

    distA = dividA;
    distB = dividB;

    // dividBに画像が存在したら
    if (calculateArea(distB) != 0) {
        return;
    }
  // 分水流の欠点であまりにも小さすぎると破棄されてしまうことがあるので、
  // ベース画像から他の画像を抜く処理
    for (int i = 0; i < base.rows; i++) {
        for (int j = 0; j < markers.cols; j++) {
            auto stagePixel = base.at < cv::Vec4b > (i, j);
            auto divAPixel = dividA.at < cv::Vec4b > (i, j);
            int r = 255;
            int g = 255;
            int b = 255;
            if (stagePixel[0] == 0 &&
                stagePixel[1] == 0 &&
                stagePixel[2] == 0 &&
                stagePixel[3] == 255) {
                dividB.at < cv::Vec4b > (i, j) = cv::Vec4b(0, 0, 0, 0);
                continue;
            }
            if (divAPixel[0] == r && divAPixel[1] == g && divAPixel[2] == b) {
                dividB.at < cv::Vec4b > (i, j) = cv::Vec4b(0, 0, 0, 0);
                continue;
            }
            dividB.at < cv::Vec4b > (i, j) = cv::Vec4b(r, g, b, 255);
        }
    }
    imshow("dividBv2", dividB);
    distB = dividB;
}

詳細な動作

まずは画像を二値化します。

    // グレースケール
    cv::Mat bw;
    cv::cvtColor(base, bw, CV_BGR2GRAY);
    cv::threshold(bw, bw, 40, 255, CV_THRESH_BINARY | CV_THRESH_OTSU);
    cv::imshow("dist", bw);

f:id:y_harada:20160405002916p:plain

distanceTransformで輪郭からの距離がながくなると濃くなるフィルターかけます。

    cv::Mat dist;
    // 輪郭から内側にいけばいくほど濃くする
    cv::distanceTransform(bw, dist, CV_DIST_L2, 3);
    cv::normalize(dist, dist, 0, 1., cv::NORM_MINMAX);
    cv::imshow("Distance Transform Image", dist);

f:id:y_harada:20160405002920p:plain

二値化してさっきのdistanceTransformから薄い部分を取り除き、膨張させます

    // 二値化してゴミ取り
    cv::threshold(dist, dist, .4, 1., CV_THRESH_BINARY);
    cv::Mat kernel1 = cv::Mat::ones(3, 3, CV_8UC1);
    cv::dilate(dist, dist, kernel1);
    imshow("Peaks", dist);

f:id:y_harada:20160405002922p:plain

膨張させた画像からマーカーを生成します。

この時背景もマーカーにいれておきます。

    cv::Mat dist_8u;
    dist.convertTo(dist_8u, CV_8U);

    // すべてのマーカーを取得
    cv::vector < cv::vector < cv::Point > > contours;
    cv::findContours(dist_8u, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);

    // watershedに流し込む用のマーカー画像作成
    cv::Mat markers = cv::Mat::zeros(dist.size(), CV_32SC1);
    // マーカーを描画
    for (size_t i = 0; i < contours.size(); i++) {
        cv::drawContours(markers, contours, static_cast < int > (i), cv::Scalar::all(static_cast < int > (i) + 1), -1);
    }
    // 背景用のマーカー作成
    circle(markers, cv::Point(5, 5), 3, CV_RGB(255, 255, 255), -1);
    imshow("Markers", markers * 10000);

f:id:y_harada:20160405002927p:plain

画像を適切なチャンネルに変更し、watershedにかけます。

    // ベース画像を8ビット変換
    cv::Mat stage_8bit_3channel;
    cv::cvtColor(base, stage_8bit_3channel, CV_BGR2RGB, 3);
    cv::watershed(stage_8bit_3channel, markers);
    imshow("watershed ", markers * 10000);

f:id:y_harada:20160405002933p:plain

最後に、各々のマーカーカラーから分離された画像を取り出します。

  // 分割した画像をそれぞれの画像に書き込む
    for (int i = 0; i < markers.rows; i++) {
        for (int j = 0; j < markers.cols; j++) {
            // 白固定してますが、色わけしたい場合は適当にいじってください。
            int r = 255;
            int g = 255;
            int b = 255;
            int index = markers.at < int > (i, j);
            if (index == 1) {
                dividA.at < cv::Vec4b > (i, j) = cv::Vec4b(r, g, b, 255);
            } else if (index == 2) {
                dividB.at < cv::Vec4b > (i, j) = cv::Vec4b(r, g, b, 255);
            } else {
                dividA.at < cv::Vec4b > (i, j) = cv::Vec4b(0, 0, 0, 0);
                dividB.at < cv::Vec4b > (i, j) = cv::Vec4b(0, 0, 0, 0);
            }
        }
    }
    imshow("dividA", dividA);
    imshow("dividB", dividB);

    distA = dividA;
    distB = dividB;

f:id:y_harada:20160405002938p:plainf:id:y_harada:20160405002935p:plain

最後に

今回はしょっぱい矩形の分割でしたが、実際の写真とか垂れ流してみると物体検知とかができます。

OpenCVにはこれの他にも、顔認識やらいろいろなフィルターが詰まっています。

画像処理を本気でやってみたい人にはいいライブラリです。(Javaでも書けるしねってことはScalaでも…)

ちなみに、ScalaOpenCVとかだと… ScalaでOpenCVを使って画像処理 « Rest Term

是非是非、触ってみてはいかがでしょうか?

戯言

最近、C++ラムダ式楽しいです。 C++ でのラムダ式

あと型推論が便利 自動 (C++

参考にさせていただいたサイト様

OpenCV 2.2 C++ リファレンス — opencv 2.2 documentation

OpenCV: Image Segmentation with Watershed Algorithm

OpenCV: Image Segmentation with Distance Transform and Watershed Algorithm