OpenCvSharpをつかう その18(ラベリング・改)

この度、ラベリング機能を提供するOpenCvSharp.Blobを、P/Invokeによる実装をやめ、すべてC#(マネージドコード)で実装しなおしました。これは以下の理由からです。

  • ●特にCvBlobインスタンスの取り回しが悪い。リスト側のCvBlobsが解放されるとCvBlobも解放されてしまうので、永続化しづらい。
  • ●上記に関連しメモリリークや異常終了が多発。ネイティブリソース解放周りの改善が、cvblobの実装上と難敵"GC"の兼ね合いでかなり苦しい。

変更は、2.4.5 (19 Jan., 2014)版から取り入れています。NuGetも更新済みです。
https://github.com/shimat/opencvsharp/releases

今回の変更は、出力のblobは変わらないように作ったつもりです。ただし若干APIに変更があるので、以前のコードとの互換性は失われています。より良くなったはずなのでご理解いただければと思います。
以下では、変更点を絡めながら改めて使い方を紹介します。

OpenCvSharpをつかう 記事一覧

導入

OpenCvSharp.Blob.dll を参照に追加してください。
なお、OpenCvSharpExtern.dll はもう不要です!

以降で述べるコードは、一番上にこのusingが書いてある前提です。

using OpenCvSharp;
using OpenCvSharp.Blob;

入力画像の準備

ラベリングの対象画像は、ラベルを付けたい箇所が白、そうでない箇所が黒という画像である必要があります。グレースケール化・二値化などの処理により、元の画像をそのような画像へと変換します。

IplImage imgSrc = new IplImage("shapes.png", LoadMode.Color);
IplImage imgBinary = new IplImage(imgSrc.Size, BitDepth.U8, 1);
Cv.CvtColor(imgSrc, imgBinary, ColorConversion.BgrToGray);           // グレースケール化
Cv.Threshold(imgBinary, imgBinary, 100, 255, ThresholdType.Binary);  // 二値化

f:id:Schima:20140119102739p:plain

ラベリング実行

CvBlobsクラスのコンストラクタか、またはLabelメソッドを呼び出します。どちらでも同じです。

CvBlobs blobs = new CvBlobs(imgBinary);
CvBlobs blobs = new CvBlobs();
blobs.Label(imgBinary);

従来APIからの変更点

  • CvBlobsはIDisposableではなくなりました。最後にDisposeしたりusingを書いたりする必要はありません。
  • ラベルデータ保存用の画像 (その12ではimgLabelとしていました)は、ユーザが用意する必要はありません。CvBlobs内部で自動的に確保してくれるので、Labelメソッドの引数からは無くなりました。ラベルのデータが欲しい場合の方法は後述します。

ラベリング結果の描画

CvBlobs.RenderBlobsを使います。デバッグ用の位置づけです。描画先には、元画像と同じサイズで、8ビット3チャンネルの画像を指定します。

IplImage imgRender = new IplImage(imgSrc.Size, BitDepth.U8, 3);
blobs.RenderBlobs(imgSrc, imgRender);
CvWindow.ShowImages(imgRender);

f:id:Schima:20140119111940p:plain
なお、描画結果にノイズが乗ったように見える際には、あらかじめ imgRender.Zero(); を呼び出してみてください。

各blobの取得

CvBlobsは、System.Collections.Generic.Dictionary<int, CvBlob> を継承しています。要するに実体がただのDictionaryなので、その感覚で使うだけです。

すべてのblobを見るときは、foreachを使うことができます。

foreach (KeyValuePair<int, CvBlob> item in blobs)
{
    int labelValue = item.Key;
    CvBlob blob = item.Value;

    Console.WriteLine(blob.Rect); 
}

個別のblobは、ラベルの数値をキーとして取得できます。

CvBlob blob5 = blobs[5];

従来APIからの変更点

blobの情報

CvBlobクラスが1つのblobを表します。以下、プロパティ/メソッドの一例を挙げます。(Angleのみは、モーメント情報から毎回計算で求めるため、メソッドとしています。)

Rect
外接矩形
Area
面積 (外接矩形の面積ではなく、blobの白い部分の面積)
Centroid
重心
Angle()
角度[rad]
Label
ラベルの数値
Contour
輪郭

blobの絞り込み

FilterByArea

指定した範囲外の面積のblobを破壊的に削除します。

blobs.FilterByArea(500, 2000); // 499以下、2001以上は削除
blobs.FilterByArea(0, 1000); // 1001以上は削除
blobs.FilterByArea(500, int.MaxValue); // 499以下は削除

GreaterBlob / LargestBlob

最大の面積のblobを返します。(従来APIではキーのラベル値を返していましたが、blobそのものを返すようになりました。)

CvBlob maxBlob = blobs.LargestBlob();

LINQをつかう

.NET Framework3.5以降なら、LINQを使って目的の情報を絞り込むのも良いでしょう。いくつか例を挙げます。(従来APIはネイティブリソースの兼ね合いでちょっとおっかない感じでした。

外接矩形が縦長のblobだけ集める
var verticalBlobs = blobs.Where(pair =>
{
    CvRect rect = pair.Value.Rect;
    return rect.Height > rect.Width;
});
外接矩形のみを集めた配列を得る
CvRect[] rectArray = blobs.Select(pair => pair.Value.Rect).ToArray();
重心が一番左にあるblobを得る
CvBlob leftBlob = blobs.Values.OrderBy(blob => blob.Centroid.X).First();
最も長いblob周囲長の値を得る
double longestPerimeter = blobs.Values.Max(blob => blob.Contour.Perimeter());

輪郭情報を得る

CvContourChainCode

CvBlob.Contourプロパティにより、blobの輪郭情報が得られます。
返り値はCvContourChainCodeというもので、輪郭の始点と、次の輪郭の点の向き(0~7)のリストで構成されています。Renderメソッドにより描画したり、Perimeterメソッドで輪郭長を得ることができます。

CvContourChainCode cc = blobs[0].Contour;
// 描画
IplImage img = new IplImage(imgSrc.Size, BitDepth.U8, 3);
cc.Render(img); 
// 周囲長
double perimeter = cc.Perimeter();

f:id:Schima:20140119112853p:plain]

CvContourPolygon

CvContourChainCodeは人間の直感的にはわかりづらいデータなので、輪郭上の頂点の集合データに変換することができます。それがCvContourPolygonで、実体はSystem.Collections.Generic.List<CvPoint>です。

以下は、CvContourPolygonの中身を描画してみるコードです。要素がそれぞれ輪郭の頂点を表しています。

CvContourPolygon polygon = cc.ConvertToPolygon();

IplImage img = new IplImage(imgSrc.Size, BitDepth.U8, 3);
foreach (CvPoint p in polygon)
{
    img.Rectangle(p.X, p.Y, p.X+1, p.Y+1, CvColor.Red);
}

f:id:Schima:20140119112923p:plain

CvContourPolygon.ContourConvexHull

Convex Hullとは凸包と訳されますが、手のひらのような引っ込んだ箇所がある輪郭の外側を結んで、凸多角形に変換してくれるのがContourConvexHullメソッドです。

CvContourPolygon convexHull = polygon.ContourConvexHull();
convexHull.Render(img);

下の図の右が、凸包に変換後のものです。どのような処理がなされたかわかると思います。
f:id:Schima:20140119114654p:plain

ラベルデータの取得

入力画像のピクセルがそれぞれどのラベルとなったのか知りたいことがあります。これはCvBlobs.GetLabelメソッドで取得できます。
以下は、(x:50, y:120)の位置のラベル値を取得するものです。どのラベルにも属さないならば0、そうでなければ1以上の値が返ります。

int x = 50;
int y = 120;
int label = blobs.GetLabel(x, y);

ラベルデータ配列の実体は blobs.Labels プロパティから見ることができます。画像ではなく、int[,]となりました。もしこちらから見る際は、以下のように(y, x)の順のインデックス指定となることに注意してください。(row, column)の順と思えば自然ですね。

int x = 50;
int y = 120;
int label = blobs.Labels[y, x];