cv::drawKeypoints / drawMatches の使い方【OpenCV/C++】〜特徴点・マッチング結果を可視化する〜

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

cv::drawKeypoints / drawMatches の使い方【OpenCV/C++】〜特徴点・マッチング結果を可視化する〜

特徴点検出やマッチングのデバッグで最初に使う関数が cv::drawKeypoints と cv::drawMatches です。検出した KeyPoint の位置・スケール・向きを円で重ね描きしたり、2枚の画像を並べてマッチング対応線を引いたりできます。

最小の呼び出しイメージは次のとおりです。

cv::drawKeypoints(img, keypoints, out, cv::Scalar::all(-1),
                  cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);

cv::drawMatches(img1, kp1, img2, kp2, good_matches, out);

動作環境

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

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


基本の使い方

ORB で特徴点を検出し、drawKeypoints で描画する最小完全プログラムです。

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

int main()
{
    // グレースケールで読み込む(特徴点検出はグレースケール推奨)
    cv::Mat gray = cv::imread("input.jpg", cv::IMREAD_GRAYSCALE);
    if (gray.empty()) {
        std::cerr << "画像の読み込みに失敗しました" << std::endl;
        return 1;
    }

    // ORB 検出器を生成(最大500点)
    cv::Ptr<cv::ORB> orb = cv::ORB::create(500);

    std::vector<cv::KeyPoint> keypoints;
    orb->detect(gray, keypoints);

    std::cout << "検出された特徴点数: " << keypoints.size() << std::endl;

    // drawKeypoints: 特徴点をカラー画像に重ね描き
    cv::Mat out;
    cv::drawKeypoints(
        gray,                                    // 入力画像
        keypoints,                               // KeyPoint のリスト
        out,                                     // 出力画像
        cv::Scalar::all(-1),                     // -1 でランダム色
        cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS // スケール・向きも描画
    );

    cv::imwrite("keypoints_out.jpg", out);
    std::cout << "keypoints_out.jpg を保存しました" << std::endl;

    return 0;
}

実行結果:

検出された特徴点数: 487
keypoints_out.jpg を保存しました

keypoints_out.jpg を開くと、各特徴点の位置に円(大きさ=スケール、線=向き)がランダムな色で描かれています。


引数と戻り値

cv::drawKeypoints

引数 型 説明
image const cv::Mat& 入力画像(グレー・カラー問わず)
keypoints const std::vector<KeyPoint>& 描画する特徴点リスト
outImage cv::Mat& 出力画像(image と同一でも可)
color const cv::Scalar& 描画色。Scalar::all(-1) でランダム色
flags cv::DrawMatchesFlags 描画オプション(下表参照)

DrawMatchesFlags の主要値:

フラグ 意味
DEFAULT 中心点のみ描画
DRAW_RICH_KEYPOINTS スケール・向きも円で描画
NOT_DRAW_SINGLE_POINTS マッチしていない点を描画しない
DRAW_OVER_OUTIMG 出力画像を初期化せず上書き

cv::drawMatches

引数 型 説明
img1 const cv::Mat& 1枚目の入力画像
keypoints1 const std::vector<KeyPoint>& 1枚目の特徴点
img2 const cv::Mat& 2枚目の入力画像
keypoints2 const std::vector<KeyPoint>& 2枚目の特徴点
matches1to2 const std::vector<DMatch>& マッチング結果
outImg cv::Mat& 出力画像(2枚を横並びに合成)
matchColor cv::Scalar マッチング線の色(省略可)
singlePointColor cv::Scalar 未マッチ点の色(省略可)
matchesMask std::vector<char> 描画するマッチを絞るマスク(省略可)
flags cv::DrawMatchesFlags 描画オプション(省略可)

戻り値はなし(outImg に結果が書き込まれます)。


実践例

2枚の画像間でマッチング結果を可視化する

BFMatcher で ORB 記述子をマッチングし、距離でフィルタした結果を描画します。

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

int main()
{
    cv::Mat img1 = cv::imread("scene1.jpg", cv::IMREAD_GRAYSCALE);
    cv::Mat img2 = cv::imread("scene2.jpg", cv::IMREAD_GRAYSCALE);
    if (img1.empty() || img2.empty()) {
        std::cerr << "画像の読み込みに失敗しました" << std::endl;
        return 1;
    }

    cv::Ptr<cv::ORB> orb = cv::ORB::create(1000);

    std::vector<cv::KeyPoint> kp1, kp2;
    cv::Mat desc1, desc2;
    // detect + compute を同時に行う
    orb->detectAndCompute(img1, cv::noArray(), kp1, desc1);
    orb->detectAndCompute(img2, cv::noArray(), kp2, desc2);

    // BFMatcher: ORB は NORM_HAMMING を使う
    cv::BFMatcher matcher(cv::NORM_HAMMING, /*crossCheck=*/true);
    std::vector<cv::DMatch> matches;
    matcher.match(desc1, desc2, matches);

    // 距離でソートして上位 50 件だけ残す
    std::sort(matches.begin(), matches.end(),
              [](const cv::DMatch& a, const cv::DMatch& b){ return a.distance < b.distance; });
    if (matches.size() > 50)
        matches.resize(50);

    std::cout << "表示するマッチ数: " << matches.size() << std::endl;

    // drawMatches で 2 枚を横並びにしてマッチング線を描画
    cv::Mat out;
    cv::drawMatches(
        img1, kp1,
        img2, kp2,
        matches, out,
        cv::Scalar(0, 255, 0),                  // マッチング線: 緑
        cv::Scalar(255, 0, 0),                   // 未マッチ点: 青
        std::vector<char>(),
        cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS
    );

    cv::imwrite("matches_out.jpg", out);
    std::cout << "matches_out.jpg を保存しました" << std::endl;

    return 0;
}

実行結果:

表示するマッチ数: 50
matches_out.jpg を保存しました

matches_out.jpg には img1・img2 が横に並び、対応する特徴点が緑の線で結ばれた画像が出力されます。


knnMatch 結果を ratio test でフィルタして描画する

Lowe’s ratio test を使いたい場合は drawMatchesKnn ではなく、DMatch に変換してから drawMatches を使うのが実務的です。

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

int main()
{
    cv::Mat img1 = cv::imread("scene1.jpg", cv::IMREAD_GRAYSCALE);
    cv::Mat img2 = cv::imread("scene2.jpg", cv::IMREAD_GRAYSCALE);
    if (img1.empty() || img2.empty()) {
        std::cerr << "画像の読み込みに失敗しました" << std::endl;
        return 1;
    }

    cv::Ptr<cv::ORB> orb = cv::ORB::create(1000);
    std::vector<cv::KeyPoint> kp1, kp2;
    cv::Mat desc1, desc2;
    orb->detectAndCompute(img1, cv::noArray(), kp1, desc1);
    orb->detectAndCompute(img2, cv::noArray(), kp2, desc2);

    cv::BFMatcher matcher(cv::NORM_HAMMING);
    std::vector<std::vector<cv::DMatch>> knn_matches;
    // k=2 で最近傍を 2 点取得
    matcher.knnMatch(desc1, desc2, knn_matches, 2);

    // Lowe's ratio test: 0.75 は調整可
    const float ratio_thresh = 0.75f;
    std::vector<cv::DMatch> good;
    for (const auto& m : knn_matches) {
        if (m.size() == 2 && m[0].distance < ratio_thresh * m[1].distance)
            good.push_back(m[0]);
    }

    std::cout << "ratio test 通過数: " << good.size() << std::endl;

    cv::Mat out;
    cv::drawMatches(img1, kp1, img2, kp2, good, out);
    cv::imwrite("ratio_matches_out.jpg", out);
    std::cout << "ratio_matches_out.jpg を保存しました" << std::endl;

    return 0;
}

実行結果:

ratio test 通過数: 38
ratio_matches_out.jpg を保存しました

つまずきポイント

⚠️ ORB の記述子に NORM_L2 を使うとマッチング結果がおかしくなる

ORB・BRISK・BRIEF などバイナリ記述子は cv::NORM_HAMMING を使う必要があります。SIFT・SURF など浮動小数点記述子は cv::NORM_L2 を使います。誤ったノルムを指定しても例外は出ず、単に距離の意味が壊れるため、drawMatches の出力を見てマッチ線が明らかにおかしい場合はまずここを確認してください。

// ORB → NORM_HAMMING
cv::BFMatcher matcher(cv::NORM_HAMMING);

// SIFT → NORM_L2
cv::BFMatcher matcher(cv::NORM_L2);

⚠️ crossCheck=true と knnMatch は併用できない

BFMatcher の crossCheck=true を有効にすると knnMatch を呼び出したとき実行時エラーになります(OpenCV 4.x 時点)。ratio test を使いたい場合は crossCheck=false のまま knnMatch を呼び、自前でフィルタしてください。

⚠️ desc1 または desc2 が空のときに match() がクラッシュする

特徴点が 0 件の場合、記述子 Mat が空になり matcher.match() が内部アサーションで落ちます。match() を呼ぶ前に desc1.empty() || desc2.empty() のチェックを入れるのが確実です。

if (desc1.empty() || desc2.empty()) {
    std::cerr << "記述子が空です(特徴点が検出されていない可能性)" << std::endl;
    return 1;
}

関連する関数

  • cv::ORB::create / cv::SIFT::create — 特徴点検出器の生成。drawKeypoints と組み合わせて使う基本セット
  • cv::BFMatcher / cv::FlannBasedMatcher — drawMatches の入力となるマッチング結果を生成する
  • cv::drawMatchesKnn — knnMatch の結果(vector<vector<DMatch>>)をそのまま渡せるオーバーロード。ratio test 前の全候補を可視化したい場合に便利
  • cv::CascadeClassifier の使い方【OpenCV/C++】〜Haar Cascadeで顔検出〜 — 別の物体検出アプローチ。特徴点ベースとの使い分け参考に

まとめ

  • cv::drawKeypoints で特徴点の位置・スケール・向きを手軽に可視化できる。DRAW_RICH_KEYPOINTS フラグを付けると情報量が増えデバッグに役立つ。
  • cv::drawMatches は2枚の画像を横並びにしてマッチング対応線を描画する。フィルタ前後を比較することでパラメータ調整の指針になる。
  • BFMatcher を使う際はノルム指定(NORM_HAMMING vs NORM_L2)と crossCheck の制約を把握しておくと余計なバグを避けられる。

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

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

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

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