この度、ラベリング機能を提供する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.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); // 二値化
ラベリング実行
CvBlobsクラスのコンストラクタか、またはLabelメソッドを呼び出します。どちらでも同じです。
CvBlobs blobs = new CvBlobs(imgBinary);
CvBlobs blobs = new CvBlobs();
blobs.Label(imgBinary);
ラベリング結果の描画
CvBlobs.RenderBlobsを使います。デバッグ用の位置づけです。描画先には、元画像と同じサイズで、8ビット3チャンネルの画像を指定します。
IplImage imgRender = new IplImage(imgSrc.Size, BitDepth.U8, 3); blobs.RenderBlobs(imgSrc, imgRender); CvWindow.ShowImages(imgRender);
なお、描画結果にノイズが乗ったように見える際には、あらかじめ 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からの変更点
- ラベルの数値は従来uintでしたが、intになりました。理由は、平たく言うとuintは.NETで一般的ではないため、おカタく言うと"CLS Compliant"ではないためです。http://msdn.microsoft.com/en-us/library/12a7a7h3.aspx
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();
]
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); }
CvContourPolygon.ContourConvexHull
Convex Hullとは凸包と訳されますが、手のひらのような引っ込んだ箇所がある輪郭の外側を結んで、凸多角形に変換してくれるのがContourConvexHullメソッドです。
CvContourPolygon convexHull = polygon.ContourConvexHull(); convexHull.Render(img);
下の図の右が、凸包に変換後のものです。どのような処理がなされたかわかると思います。
ラベルデータの取得
入力画像のピクセルがそれぞれどのラベルとなったのか知りたいことがあります。これは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];