OpenCvSharpをつかう その24 (OpenCV 3.0のラベリング)

ラベリング3度目です。好きですね。露骨に作者の需要が現れています。

OpenCV3.0では、cv::connectedComponents という待望のラベリング(連結成分の抽出)関数が追加されました。

この関数を3.0.0.20150819版以降のラッパーでサポートしましたので、それを紹介します。

好みかもしれませんが、素のcv::connectedComponentsは、あまり使いやすい設計とは思えません。そこで、昔から添付しているOpenCvSharp.Blobに使い勝手を近づけたラッパーも作成しました。それも紹介します。

OpenCvSharpをつかう 記事一覧

使い方

元画像サンプル

f:id:Schima:20150819211605p:plain

準備

過去の記事同様ですが、二値化等の手法により、ラベル付けしたい領域が白、その他が黒である画像を用意します。

Mat src = new Mat("shapes.png", ImreadModes.Color);
Mat gray = src.CvtColor(ColorConversionCodes.BGR2GRAY);
Mat binary = gray.Threshold(0, 255, ThresholdTypes.Otsu | ThresholdTypes.Binary);

f:id:Schima:20150819211923p:plain

ラベリング実行

準備した二値画像から、Cv2.ConnectedComponentsにより実行します。

Mat label = new Mat();
int nLabels = Cv2.ConnectedComponents(binary, label, PixelConnectivity.Connectivity8, MatType.CV_32SC1);

labelは入力画像と同じサイズで、ラベル付けされた座標はその数値、されなかった座標は0が入っています。戻り値はラベルの個数-1です。

たとえば以下のようにして、ラベルごとに色分けした画像を作成できます。

Scalar[] colors = Enumerable.Range(0, nLabels+1).Select(_ => Scalar.RandomColor()).ToArray();
colors[0] = Scalar.Black;

int rows = binary.Rows;
int cols = binary.Cols;
var dst = new Mat(rows, cols, MatType.CV_8UC3);
var labelIndexer = label.GetGenericIndexer<int>();
var dstIndexer = dst.GetGenericIndexer<Vec3b>();

for (int y = 0; y < rows; y++)
{
    for (int x = 0; x < cols; x++)
    {
        int labelValue = labelIndexer[y, x];
        dstIndexer[y, x] = colors[labelValue].ToVec3b();
    }
}

f:id:Schima:20150819213048p:plain

ConnectedComponentsWithStats

ラベルごとのさらに詳しい情報を得られるメソッドも用意されています。Cv2.ConnectedComponentsWithStatsです。重心・面積・外接矩形の座標が得られます。

以下は、Matの型を明示してみました。仕様は、この記事の最初に挙げた記事を参照してください。

var label = new MatOfInt();
var stats = new MatOfInt(); // 行:ラベル数 列:5 
var centroids = new MatOfDouble();  // 行:ラベル数 列:2 (x,y)
var nLabels = Cv2.ConnectedComponentsWithStats(binary, label, stats, centroids, PixelConnectivity.Connectivity8, MatType.CV_32SC1);

例えば、ラベルごとの矩形はこのように取れます。

var statsIndexer = stats.GetGenericIndexer<int>();
for (int i = 0; i < nLabels; i++)
{
    var rect = new Rect
    {
        X = statsIndexer[i, 0],
        Y = statsIndexer[i, 1],
        Width = statsIndexer[i, 2],
        Height = statsIndexer[i, 3]
    };
}

ラッパー Cv2.ConnectedComponentsEx の使い方

以上のように、情報は全てMatから引っ張る仕様で、インデックスは仕様を見ないと分からず(一応enumがありますが)、直感的ではありません。そこで、OpenCvSharp.Blobに近づけたCv2.ConnectedComponentsEx を作成しました。

ラベリング実行

Mat src = new Mat("data/shapes.png", ImreadModes.Color);
Mat gray = src.CvtColor(ColorConversionCodes.BGR2GRAY);
Mat binary = gray.Threshold(0, 255, ThresholdTypes.Otsu | ThresholdTypes.Binary);

ConnectedComponents cc = Cv2.ConnectedComponentsEx(binary);

ConnectedComponents に、WithStatsで取れる情報込みでラベル情報が入っています。

各ラベルへのアクセス

ConnectedComponents.Labels から取得できます。int[,]型で、入力画像と同じサイズです。

for (int y = 0; y < height; y++)
{
    for (int x = 0; x < width; x++)
    {
        int labelValue = cc.Labels[y, x];
    }
}

各連結成分へのアクセス

適当な画像に、連結成分の外接矩形を描いていくサンプルです。Blobsプロパティから連結成分オブジェクトにアクセスでき、WithStatsで取れる重心・矩形・面積の情報が得られます。

cv::connectedComponentsは、背景領域をラベル値0の1つのblobとして扱う仕様なので、大抵は最初の要素を読み飛ばすと意図通りになりそうです。

foreach (var blob in cc.Blobs.Skip(1))
{
    img.Rectangle(blob.Rect, Scalar.Red);
}

希望の連結成分の絞り込みは、Linqを使うなどしましょう。以下は最大面積のblobを求める例です。(これも、普通は背景が最大なので、最初をSkipしないといつも背景が返ります。)

ConnectedComponents.Blob maxBlob1 = cc.Blobs.Skip(1).OrderByDescending(b => b.Area).First();

ConnectedComponents.Blob maxBlob2 = cc.GetLargestBlob(); // OpenCvSharp.Blob相当の便利メソッド

デバッグ用の描画

ラベル値から地道に描くのは面倒なので、ヘルパメソッドを用意しています。

var dst = new Mat();
cc.RenderBlobs(dst);

指定したラベル部分のみを切り抜く

FilterByBlob で可能です。複数あるなら FilterByBlobs で。これもラベルのMatから地道にするのはやや面倒なので用意しました。

var maxBlob = cc.GetLargestBlob();
var filtered = new Mat();
cc.FilterByBlob(src, filtered, maxBlob);

f:id:Schima:20150819215052p:plain

FilterByLabel FilterByLabels により、Blobオブジェクトではなくラベルの数値でフィルタをかけることもできます。

おわりに

たぶん楽ですので、Cv2.ConnectedComponentsExをぜひお使いください。

もう少しcv::connectedComponentsががんばってくれてたら...と思いますが、用意してくれただけまず感謝します。