0. はじめに
どうも、お久しぶりです。原田@C++大好きです。
本当は、皆様にMonad実装してみた!とかやって見たかったのですが…
タイムアップでして、そのうち実装してみます。
今回は、もうScala関係なしにC++とOpenCVネタです。
あーこんなのあるのかー程度に見ていただければ幸いです。
1. watershedって何さ?
まずは英語、分水嶺(れい)、分水界、(川の)流域、分岐点、転機という意味です。
まぁ、何かというと画像の上から水を流しこむような形で画像の領域を判断するアルゴリズムです。
詳しくはここらあたりを見てもらえるとちょっとはわかりやすくなるかも。
ウォーターシェッド(領域分割) | 計測 | ヴィスコの画像処理技術 | ヴィスコ・テクノロジーズ株式会社
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);

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);

二値化してさっきの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);

膨張させた画像からマーカーを生成します。
この時背景もマーカーにいれておきます。
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);

画像を適切なチャンネルに変更し、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);

最後に、各々のマーカーカラーから分離された画像を取り出します。
// 分割した画像をそれぞれの画像に書き込む
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;


最後に
今回はしょっぱい矩形の分割でしたが、実際の写真とか垂れ流してみると物体検知とかができます。
OpenCVにはこれの他にも、顔認識やらいろいろなフィルターが詰まっています。
画像処理を本気でやってみたい人にはいいライブラリです。(Javaでも書けるしねってことはScalaでも…)
ちなみに、ScalaでOpenCVとかだと… ScalaでOpenCVを使って画像処理 « Rest Term
是非是非、触ってみてはいかがでしょうか?
戯言
最近、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