Cv2.ConnectedComponentsWithStats の使い方【OpenCV/C#(OpenCvSharp)】〜ラベリング処理で連結領域を解析する〜

OpenCV for C#
📌 準備: OpenCV の環境構築がまだの方はこちら → C++ 環境構築ガイド / C#(OpenCvSharp)セットアップ

Cv2.ConnectedComponentsWithStats の使い方【OpenCV/C#(OpenCvSharp)】〜ラベリング処理で連結領域を解析する〜

Cv2.ConnectedComponentsWithStats は、2値画像の連結領域(blob)にラベルを付け、各領域の面積・外接矩形・重心をまとめて返す関数です。FindContours との使い分けでよく問われますが、面積でフィルタリングしたい・全領域を一括で数値化したいシーンではこちらが素直に書けます。

最小の呼び出し形は次の通りです。

int nLabels = Cv2.ConnectedComponentsWithStats(
    binary, labelMap, stats, centroids,
    connectivity: PixelConnectivity.Connectivity8, ltype: MatType.CV_32S);

動作環境

  • OpenCvSharp4(OpenCV 4.x 系)
  • NuGet: OpenCvSharp4 + OpenCvSharp4.runtime.win(Windows 実行時)
  • .NET 8 コンソールアプリ想定

環境構築がまだの方は環境構築ガイドを先にどうぞ。

基本の使い方

黒背景に白い矩形を3つ描いたテスト画像を使い、各領域の統計情報を取得して結果を画像に描画します。

using OpenCvSharp;

// ---- テスト画像を生成(実運用では Cv2.ImRead に差し替え) ----
using var src = new Mat(300, 400, MatType.CV_8UC1, Scalar.Black);
Cv2.Rectangle(src, new Rect(20,  20,  80, 60),  Scalar.White, -1);
Cv2.Rectangle(src, new Rect(150, 50,  120, 40), Scalar.White, -1);
Cv2.Rectangle(src, new Rect(60,  180, 50,  90), Scalar.White, -1);

// ---- ラベリング ----
using var labelMap   = new Mat(); // ラベル画像(CV_32S)
using var stats      = new Mat(); // 統計情報(nLabels × 5)
using var centroids  = new Mat(); // 重心(nLabels × 2)

int nLabels = Cv2.ConnectedComponentsWithStats(
    src, labelMap, stats, centroids,
    connectivity: PixelConnectivity.Connectivity8,
    ltype: MatType.CV_32S);

// ---- 結果を表示(ラベル 0 は背景なので 1 から走査) ----
using var disp = new Mat(src.Size(), MatType.CV_8UC3, Scalar.Black);

for (int i = 1; i < nLabels; i++)
{
    // stats は ConnectedComponentsTypes の列インデックスで参照
    int x      = stats.At<int>(i, (int)ConnectedComponentsTypes.Left);
    int y      = stats.At<int>(i, (int)ConnectedComponentsTypes.Top);
    int w      = stats.At<int>(i, (int)ConnectedComponentsTypes.Width);
    int h      = stats.At<int>(i, (int)ConnectedComponentsTypes.Height);
    int area   = stats.At<int>(i, (int)ConnectedComponentsTypes.Area);

    double cx  = centroids.At<double>(i, 0);
    double cy  = centroids.At<double>(i, 1);

    // ランダムな色で外接矩形と重心を描画
    var color = new Scalar(60 * i % 256, 120 * i % 256, 200 * i % 256);
    Cv2.Rectangle(disp, new Rect(x, y, w, h), color, 2);
    Cv2.Circle(disp, new Point((int)cx, (int)cy), 4, color, -1);

    Console.WriteLine($"Label {i}: area={area}, rect=({x},{y},{w},{h}), center=({cx:F1},{cy:F1})");
}

Cv2.ImWrite("result.png", disp);
Console.WriteLine("result.png に保存しました。");

実行すると次のような出力が得られます。

Label 1: area=4800, rect=(20,20,80,60), center=(59.5,49.5)
Label 2: area=4800, rect=(150,50,120,40), center=(209.5,69.5)
Label 3: area=4500, rect=(60,180,50,90), center=(84.5,224.5)
result.png に保存しました。

コードの要点

行 解説
Cv2.ConnectedComponentsWithStats(...) 連結成分解析を実行。戻り値はラベル数(背景込み)
labelMap 各画素にラベル番号が書き込まれた CV_32S の Mat
stats 行 = ラベル、列 = Left/Top/Width/Height/Area の5列
centroids 行 = ラベル、列 = X座標・Y座標(double)
i = 1 から走査 ラベル 0 は必ず背景。1 以上が前景領域
ConnectedComponentsTypes 列挙体 列インデックスをマジックナンバーで書かずに済む

引数と戻り値

引数名 型 説明
image Mat 入力2値画像(CV_8U)
labels Mat 出力ラベル画像(CV_32S 推奨)
stats Mat 出力統計情報行列(nLabels × 5)
centroids Mat 出力重心行列(nLabels × 2、double)
connectivity PixelConnectivity 連結性。PixelConnectivity.Connectivity4 または Connectivity8(既定 Connectivity8)
ltype MatType ラベル画像の型。CV_32S(既定)または CV_16U
戻り値 int 検出ラベル数(背景込み)。前景は 戻り値 - 1 個

ConnectedComponentsTypes 列インデックス

定数名 値 意味
Left 0 外接矩形の左端 X
Top 1 外接矩形の上端 Y
Width 2 外接矩形の幅
Height 3 外接矩形の高さ
Area 4 面積(画素数)

実践例

面積でノイズ除去して大きな領域だけを取り出す

実務では「面積が小さい領域は外来ノイズ」として除去する処理が頻出です。

using OpenCvSharp;

// グレースケール画像を読み込んで2値化
using var gray   = Cv2.ImRead("input.png", ImreadModes.Grayscale);
using var binary = new Mat();
Cv2.Threshold(gray, binary, 0, 255, ThresholdTypes.Otsu | ThresholdTypes.Binary);

using var labelMap  = new Mat();
using var stats     = new Mat();
using var centroids = new Mat();

int nLabels = Cv2.ConnectedComponentsWithStats(
    binary, labelMap, stats, centroids);

// 面積閾値以上の領域だけ残したマスクを生成
int areaThreshold = 500;
using var filtered = new Mat(binary.Size(), MatType.CV_8UC1, Scalar.Black);

for (int i = 1; i < nLabels; i++)
{
    int area = stats.At<int>(i, (int)ConnectedComponentsTypes.Area);
    if (area < areaThreshold) continue;

    // labelMap の該当ラベル画素を抽出して filtered に合成
    using var mask = labelMap.Equals(i); // 対象ラベルのマスク
    Cv2.BitwiseOr(filtered, mask, filtered);
}

Cv2.ImWrite("filtered.png", filtered);
Console.WriteLine($"前景領域: {nLabels - 1} → 閾値通過: 確認は filtered.png で");

⚠️ labelMap.Equals(i) は Mat の要素比較を返し、対象ラベルの画素が 255 になるマスクを生成します。OpenCvSharp 固有の書き方です。

外接矩形を使って各領域を個別にクロップする

using OpenCvSharp;

using var src    = Cv2.ImRead("input.png", ImreadModes.Grayscale);
using var binary = new Mat();
Cv2.Threshold(src, binary, 128, 255, ThresholdTypes.Binary);

using var labelMap  = new Mat();
using var stats     = new Mat();
using var centroids = new Mat();

int nLabels = Cv2.ConnectedComponentsWithStats(binary, labelMap, stats, centroids);

for (int i = 1; i < nLabels; i++)
{
    int x = stats.At<int>(i, (int)ConnectedComponentsTypes.Left);
    int y = stats.At<int>(i, (int)ConnectedComponentsTypes.Top);
    int w = stats.At<int>(i, (int)ConnectedComponentsTypes.Width);
    int h = stats.At<int>(i, (int)ConnectedComponentsTypes.Height);

    // ROI でクロップして個別保存
    using var roi = new Mat(src, new Rect(x, y, w, h));
    Cv2.ImWrite($"crop_{i}.png", roi);
}

各領域の外接矩形を直接使えるのが ConnectedComponentsWithStats の強みです。FindContours + BoundingRect の組み合わせと比べてコードが短くまとまります(Cv2.BoundingRect の使い方も参考にしてください)。

つまずきポイント

⚠️ stats / centroids の Mat を使い回しても型が変わる

stats は CV_32S(int)、centroids は CV_64F(double)で出力されます。At<T> の型を間違えると 実行時例外(アクセス違反) になります。

// NG: stats を double で読む
double x = stats.At<double>(i, 0); // クラッシュの原因

// OK
int x = stats.At<int>(i, (int)ConnectedComponentsTypes.Left);
double cx = centroids.At<double>(i, 0);

⚠️ ラベル 0 は背景・処理対象は 1 から

ConnectedComponentsWithStats は背景もラベル 0 として必ず含みます。for (int i = 0; ...) から始めると背景の統計値を誤って処理する原因になります。常に i = 1 から走査してください。

⚠️ labelMap の型指定ミス

ltype を省略すると CV_32S になります。領域数が少ない場合に CV_16U を指定してメモリを節約することも可能ですが、At<T> で読む型も ushort に合わせる必要があります。型を混在させると値が化けるため、CV_32S 固定で統一するのが無難です。

✅ using による Mat の確実な解放

labelMap / stats / centroids はネイティブヒープを持つ Mat です。using を付け忘れると Dispose されずメモリリークします。本記事のサンプルのように全ての Mat に using を宣言してください。

関連する関数

  • Cv2.FindContours — 輪郭ベースの領域抽出。ポリゴン近似が必要な場合はこちら(Cv2.ApproxPolyDP の使い方も参照)
  • Cv2.BoundingRect / Cv2.MinAreaRect — 輪郭から外接矩形を求める(使い方はこちら)
  • Cv2.ConnectedComponents — Stats・重心が不要な場合の軽量版。ラベル画像だけを返す
  • Cv2.ContourArea / Cv2.ArcLength — 輪郭の面積・周長を個別に算出(使い方はこちら)

まとめ

Cv2.ConnectedComponentsWithStats は、2値画像の連結領域を1回の呼び出しでラベリング・面積・外接矩形・重心まで一括取得できる関数です。ノイズ除去や領域クロップなど面積ベースのフィルタリングに特に有効です。stats は CV_32S(int)、centroids は CV_64F(double)という型の違いと、ラベル 0 が背景である点を押さえれば実装上のトラブルはほぼ防げます。

🛠 画像処理のプロが開発するSDK/API

本ブログを運営するスワローインキュベートは、OpenCV ベースのなりすまし判定SDK/APIを開発しています。SDK は C#/.NET 8 に正式対応しており、OpenCvSharp を使うプロジェクトからそのまま組み込めます。

→ 顔認証のなりすまし対策 設計チェックリスト(当ブログ)
→ なりすまし判定SDK/APIの詳細を見る
→ API仕様書・サンプルコード

タイトルとURLをコピーしました