cv::findContours の使い方【OpenCV/C++】〜輪郭を検出する〜

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

cv::findContours の使い方【OpenCV/C++】〜輪郭を検出する〜

cv::findContours は、2値画像から物体の輪郭(境界線の座標列)を抽出する関数です。検出した輪郭を使えば、物体の数を数える・面積でフィルタする・バウンディングボックスを取得するといった処理を簡潔に実装できます。最小限の呼び出しは次のとおりです。

cv::findContours(binary, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);

動作環境

  • OpenCV 4.x(4.5 以降推奨)
  • コンパイル例:
g++ -std=c++17 main.cpp `pkg-config --cflags --libs opencv4` -o main

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


基本の使い方

完全プログラム

#include <opencv2/opencv.hpp>
#include <iostream>

int main()
{
    // グレースケールで読み込み
    cv::Mat src = cv::imread("shapes.png", cv::IMREAD_GRAYSCALE);
    if (src.empty()) {
        std::cerr << "画像を読み込めませんでした\n";
        return 1;
    }

    // 2値化(findContours は 2値画像を入力とする)
    cv::Mat binary;
    cv::threshold(src, binary, 128, 255, cv::THRESH_BINARY);

    // 輪郭検出
    std::vector<std::vector<cv::Point>> contours;
    std::vector<cv::Vec4i> hierarchy;
    cv::findContours(binary, contours, hierarchy,
                     cv::RETR_EXTERNAL,       // 最外輪郭のみ取得
                     cv::CHAIN_APPROX_SIMPLE); // 冗長点を省略

    std::cout << "検出した輪郭数: " << contours.size() << "\n";

    // カラー画像に輪郭を描画して確認
    cv::Mat dst;
    cv::cvtColor(src, dst, cv::COLOR_GRAY2BGR);
    cv::drawContours(dst, contours, -1, cv::Scalar(0, 0, 255), 2);

    cv::imwrite("result.png", dst);
    std::cout << "result.png を保存しました\n";
    return 0;
}

実行結果

検出した輪郭数: 3
result.png を保存しました

result.png を開くと、2値化された白領域の境界が赤色でなぞられた画像が確認できます。

主要行の解説

解説
cv::threshold(...) findContours は 2値画像が前提。まずここで 0/255 に変換する
std::vector<std::vector<cv::Point>> contours 各輪郭 = cv::Point の配列。外側の vector が輪郭ごとの配列になる
std::vector<cv::Vec4i> hierarchy 輪郭の親子関係を格納。使わない場合も宣言して渡す必要がある
cv::RETR_EXTERNAL 最外輪郭のみを取得するモード。穴(内側輪郭)は無視される
cv::CHAIN_APPROX_SIMPLE 直線部の中間点を省略し、端点だけを保持。メモリを節約できる
cv::drawContours(..., -1, ...) インデックスを -1 にすると全輪郭を一括描画

引数と戻り値

void cv::findContours(
    InputOutputArray  image,      // [入出力] 8ビット2値画像(処理で内容が変わる場合あり)
    OutputArrayOfArrays contours, // [出力]  検出した輪郭(Point の vector の vector)
    OutputArray       hierarchy,  // [出力]  輪郭の親子関係(Vec4i の vector)
    int               mode,       // 輪郭取得モード(RetrievalModes)
    int               method,     // 輪郭近似方法(ContourApproximationModes)
    Point             offset = Point() // 輪郭座標に加算するオフセット(省略可)
);

mode(RetrievalModes)

定数 説明
cv::RETR_EXTERNAL 最外輪郭のみ。物体の外形だけ欲しいときに使う
cv::RETR_LIST 全輪郭をフラットなリストで返す。階層不要のときに効率的
cv::RETR_CCOMP 2階層(外輪郭 + 穴)で返す
cv::RETR_TREE 全階層を完全なツリー構造で返す

method(ContourApproximationModes)

定数 説明
cv::CHAIN_APPROX_NONE 輪郭上の全ピクセルを保持(点数が多くなる)
cv::CHAIN_APPROX_SIMPLE 直線部の端点のみ保持(実務では最もよく使う)
cv::CHAIN_APPROX_TC89_L1 Teh-Chin チェーン近似(より積極的に点を削減)

実践例

例1: 面積フィルタで小さなノイズ輪郭を除去する

ノイズが多い画像をそのまま2値化すると、微小な輪郭が大量に検出されます。cv::contourArea で面積閾値を設けて除去するのが実務での定番パターンです。

#include <opencv2/opencv.hpp>
#include <iostream>

int main()
{
    cv::Mat src = cv::imread("noisy_shapes.png", cv::IMREAD_GRAYSCALE);
    if (src.empty()) {
        std::cerr << "画像を読み込めませんでした\n";
        return 1;
    }

    cv::Mat binary;
    cv::threshold(src, binary, 128, 255, cv::THRESH_BINARY);

    std::vector<std::vector<cv::Point>> contours;
    std::vector<cv::Vec4i> hierarchy;
    cv::findContours(binary, contours, hierarchy,
                     cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);

    // 面積 200px² 以上の輪郭だけ残す
    const double minArea = 200.0;
    std::vector<std::vector<cv::Point>> filtered;
    for (const auto& c : contours) {
        if (cv::contourArea(c) >= minArea) {
            filtered.push_back(c);
        }
    }

    std::cout << "全輪郭数: " << contours.size()
              << " / フィルタ後: " << filtered.size() << "\n";

    cv::Mat dst;
    cv::cvtColor(src, dst, cv::COLOR_GRAY2BGR);
    cv::drawContours(dst, filtered, -1, cv::Scalar(0, 200, 0), 2);
    cv::imwrite("filtered.png", dst);
    return 0;
}

例2: バウンディングボックスで各物体を囲む

輪郭から cv::boundingRect で矩形を取り出し、cv::rectangle で描画します。物体の個数カウントや ROI 切り出しの前処理として頻繁に使います。

#include <opencv2/opencv.hpp>
#include <iostream>

int main()
{
    cv::Mat src = cv::imread("objects.png", cv::IMREAD_GRAYSCALE);
    if (src.empty()) {
        std::cerr << "画像を読み込めませんでした\n";
        return 1;
    }

    cv::Mat binary;
    cv::threshold(src, binary, 128, 255, cv::THRESH_BINARY);

    std::vector<std::vector<cv::Point>> contours;
    std::vector<cv::Vec4i> hierarchy;
    cv::findContours(binary, contours, hierarchy,
                     cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);

    cv::Mat dst;
    cv::cvtColor(src, dst, cv::COLOR_GRAY2BGR);

    for (size_t i = 0; i < contours.size(); ++i) {
        if (cv::contourArea(contours[i]) < 100.0) continue; // 微小輪郭をスキップ

        cv::Rect bbox = cv::boundingRect(contours[i]);
        cv::rectangle(dst, bbox, cv::Scalar(255, 128, 0), 2);

        // 番号をバウンディングボックスの左上に描画
        cv::putText(dst, std::to_string(i),
                    bbox.tl(), cv::FONT_HERSHEY_SIMPLEX, 0.6,
                    cv::Scalar(255, 128, 0), 2);
    }

    cv::imwrite("bbox.png", dst);
    std::cout << "bbox.png を保存しました\n";
    return 0;
}

実行すると、各物体を青いバウンディングボックスで囲み、インデックス番号を付けた bbox.png が保存されます。


つまずきポイント

⚠️ 入力画像は「8ビット2値画像」でなければならない

cv::findContours に渡す画像は 8ビット(CV_8U)でなければなりません。cv::Cannycv::threshold の出力は CV_8U なので問題ありませんが、CV_8UC3(カラー)や CV_32F を渡すと実行時に assertion エラーが発生します。

OpenCV Error: Assertion failed (image.type() == CV_8UC1 || image.type() == CV_8S C1)

カラー画像を渡す場合は事前に cv::cvtColor でグレースケール変換し、cv::thresholdcv::Canny で2値化してから渡してください。

⚠️ OpenCV 4.x から関数シグネチャが変わっている

OpenCV 3.x では戻り値として cv::Mat が返っていました(image, contours, hierarchy を含む戻り値)。OpenCV 4.x では void に変わっており、シグネチャは (image, contours, hierarchy, mode, method) の5引数です。古いサンプルコードをコピーすると error: too many arguments や型不一致でコンパイルエラーになります。

⚠️ hierarchy は省略できないが内容を使わないケースが多い

hierarchy は内部輪郭(穴)の親子関係を追うときにしか使いません。cv::RETR_EXTERNAL を使う場合は中身を参照する必要はほぼありませんが、引数として宣言して渡すことは必須です。渡し忘れるとコンパイルエラーになります。ダミーとして宣言するだけでかまいません。


関連する関数


まとめ

cv::findContours は2値画像から輪郭の座標列を取得する関数です。cv::RETR_EXTERNAL + cv::CHAIN_APPROX_SIMPLE の組み合わせが実務では最も頻繁に使われます。cv::contourArea による面積フィルタや cv::boundingRect によるROI抽出と組み合わせることで、物体検出・計数・切り出しの前処理として幅広い場面で活用できます。

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

本ブログを運営するスワローインキュベートは、OpenCV ベースのなりすまし判定SDK/API(C++製・OpenCV 4.10)を開発しています。顔認証システムへの組み込み実績多数。

→ なりすまし判定SDK/APIの詳細を見る
→ API仕様書・サンプルコード

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