OpenCV

【C++】OpenCVで特徴点検出をマスター:ORB・SIFT・SURFの実装手順と実践テクニック

C++とOpenCVで画像処理の特徴点検出を行うなら、ORBが高速かつ特許フリー、SIFTはスケールと回転に不変で高精度です。

数行のコードでKeyPointとDescriptorを取得でき、マッチングや物体追跡へ即応用できるため、リアルタイムなコンピュータビジョン開発にも心強い選択肢です。

特徴点検出とは

画像処理やコンピュータビジョンの分野で、特徴点検出は非常に重要な役割を果たします。

特徴点とは、画像の中で他の部分と比べて特徴的な情報を持つ点のことを指します。

これらの点は、画像の中で物体の形状や構造を表現するのに役立ち、物体認識や画像マッチング、3D再構築など多くの応用に利用されます。

特徴点検出の目的は、画像の中から安定して検出できる特徴的な点を見つけ出し、それらを基に画像間の対応関係を構築することです。

例えば、異なる視点や照明条件で撮影された画像同士を比較するとき、特徴点を使うことで画像の対応点を見つけやすくなります。

特徴点は、単に画像の中の「目立つ点」ではなく、スケールや回転、照明変化に対しても頑健であることが求められます。

これにより、異なる条件下でも同じ特徴点を検出でき、信頼性の高いマッチングが可能になります。

特徴点の役割

特徴点は、画像の中で情報量が多く、かつ他の部分と区別しやすい点を指します。

これらの点は、以下のような役割を持っています。

  • 画像の識別

特徴点は画像の中で固有のパターンを持つため、物体やシーンの識別に役立ちます。

例えば、建物の角や文字の端などが特徴点として検出されやすいです。

  • 画像間の対応付け

複数の画像間で同じ特徴点を見つけることで、画像の位置合わせやマッチングが可能になります。

これにより、パノラマ画像の作成や物体追跡が実現します。

  • 3D再構築の基礎

複数の視点から得られた画像の特徴点を対応付けることで、物体の3D形状を推定できます。

これはストラクチャ・フロム・モーション(SfM)などの技術に利用されます。

  • ロバストな認識

特徴点はスケールや回転に不変な性質を持つことが多く、これにより異なる撮影条件でも安定して検出でき、認識精度の向上に寄与します。

特徴点は、画像の中で「コーナー」や「エッジ」、「ブロブ(斑点)」などの局所的な特徴を持つ部分に多く存在します。

これらの特徴点を検出し、特徴量(ディスクリプタ)として表現することで、画像の比較や認識が可能になります。

コーナー検出と特徴量

特徴点検出の中でも特に重要なのが「コーナー検出」です。

コーナーとは、画像の中でエッジが交差する点や、局所的に強い変化がある点を指します。

コーナーは、周囲のパターンが変化しやすいため、特徴点として非常に有効です。

コーナー検出の基本原理

コーナー検出は、画像の局所領域の輝度変化を解析して行います。

代表的な手法としては、Harrisコーナー検出器やShi-Tomasi法があります。

  • Harrisコーナー検出器

画像の微分を用いて、ある小さな領域を少し動かしたときの輝度の変化量を計算します。

変化量が大きい場合、その領域はコーナーであると判断します。

具体的には、以下の行列を用います。

\[M = \begin{bmatrix}\sum I_x^2 & \sum I_x I_y \\\sum I_x I_y & \sum I_y^2\end{bmatrix}\]

ここで、\(I_x\)と\(I_y\)は画像のx方向とy方向の勾配です。

行列\(M\)の固有値を解析し、両方の固有値が大きい場合にコーナーと判定します。

  • Shi-Tomasi法(Good Features to Track)

Harris法の改良版で、行列\(M\)の最小固有値を用いてコーナーの強さを評価します。

これにより、より安定した特徴点検出が可能です。

特徴量(ディスクリプタ)

特徴点を検出しただけでは、画像間の対応付けはできません。

特徴点の周囲の情報を数値化した「特徴量(ディスクリプタ)」を計算する必要があります。

特徴量は、特徴点の周囲のパターンを表現し、異なる画像間で同じ特徴点を見つけるための手がかりになります。

代表的な特徴量には以下のようなものがあります。

  • SIFT(Scale-Invariant Feature Transform)

特徴点の周囲の勾配方向のヒストグラムを用いて128次元のベクトルを作成します。

スケールや回転に不変で、非常に高い識別能力を持ちます。

  • SURF(Speeded-Up Robust Features)

SIFTを高速化した手法で、Hessian行列を用いた特徴点検出と、サブサンプリングによる高速な特徴量計算を行います。

  • ORB(Oriented FAST and Rotated BRIEF)

FASTコーナー検出器で特徴点を検出し、BRIEF記述子を回転不変に拡張した特徴量を計算します。

特許の問題がなく高速であるため、リアルタイム処理に適しています。

特徴量は、画像のスケールや回転、照明変化に対して頑健であることが求められます。

これにより、異なる条件下でも同じ特徴点を正確にマッチングできます。

特徴点検出は、画像の中で情報量が多く、かつ安定して検出できる点を見つける技術です。

特にコーナー検出は特徴点検出の基本であり、HarrisやShi-Tomasi法が代表的です。

検出した特徴点に対して、SIFTやSURF、ORBなどの特徴量を計算することで、画像間のマッチングや物体認識が可能になります。

これらの技術を理解し使いこなすことが、OpenCVを用いた画像処理の第一歩となります。

OpenCVにおける主要アルゴリズム一覧

ORB概要

ORB(Oriented FAST and Rotated BRIEF)は、特許の制約があるSIFTやSURFの代替として開発された高速かつ効率的な特徴点検出・記述アルゴリズムです。

ORBは、FASTコーナー検出器とBRIEF記述子を組み合わせ、回転不変性を持たせることでリアルタイム処理に適した性能を実現しています。

FASTコーナー検出

FAST(Features from Accelerated Segment Test)は、コーナー検出のための高速なアルゴリズムです。

画像のあるピクセルを中心に、その周囲の16個のピクセルの輝度値を比較し、中心ピクセルがコーナーかどうかを判定します。

具体的には、中心ピクセルの輝度値を\(I_p\)、周囲の16ピクセルの輝度値を順に\(I_1, I_2, …, I_{16}\)としたとき、連続した一定数(通常は12個以上)のピクセルが\(I_p + t\)より明るいか、または\(I_p – t\)より暗い場合、その中心ピクセルはコーナーと判定されます。

ここで、\(t\)は閾値です。

この方法は計算が非常に軽量で、リアルタイム処理に適していますが、回転に対して不変ではありません。

そのため、ORBでは後述の回転不変性の付与が重要になります。

BRIEF記述子

BRIEF(Binary Robust Independent Elementary Features)は、特徴点の周囲のパッチからバイナリの特徴量を生成する記述子です。

BRIEFは、パッチ内の2点の輝度値を比較し、どちらが明るいかを1ビットで表現します。

これを多数回繰り返すことで、バイナリの特徴ベクトルを作成します。

BRIEFの特徴は以下の通りです。

  • 計算が非常に高速である
  • バイナリ特徴量なので、マッチングも高速に行える(ハミング距離を利用)
  • しかし、回転に対して不変ではない

ORBでは、このBRIEF記述子に回転不変性を付与する工夫がなされています。

回転不変性の付与

ORBは、FASTで検出した特徴点に対して、周囲の勾配方向を計算し、特徴点の向きを決定します。

この向きを基準にBRIEF記述子のサンプリングパターンを回転させることで、回転に対して不変な特徴量を生成します。

具体的には、特徴点の周囲のパッチの重心を計算し、重心から特徴点へのベクトルの角度を特徴点の向きとして定義します。

この角度を用いてBRIEFのサンプリング点を回転させることで、回転に強い記述子が得られます。

この工夫により、ORBは高速でありながら、回転に対しても頑健な特徴点検出・記述が可能となっています。

SIFT概要

SIFT(Scale-Invariant Feature Transform)は、スケールや回転に不変な特徴点検出と記述を実現するアルゴリズムで、物体認識や画像マッチングで広く使われています。

SIFTは、特徴点の検出から記述子の生成まで複数のステップで構成されており、高い識別性能を持ちます。

スケール空間極値

SIFTは、画像のスケール空間を構築し、異なるスケールで特徴点を検出します。

スケール空間は、元画像に異なる大きさのガウシアンフィルタを適用して作成されます。

スケール空間の各レベルで、隣接するスケールの画像との差分(Difference of Gaussian, DoG)を計算し、DoG画像の極値(局所最大値または最小値)を特徴点候補として抽出します。

これにより、スケール変化に対して不変な特徴点が得られます。

キーポイントローカライゼーション

極値として検出された特徴点候補は、さらに精密な位置調整が行われます。

Taylor展開を用いてサブピクセル精度での位置を推定し、低コントラストの点やエッジに沿った点は除去されます。

この処理により、安定した特徴点のみが残り、ノイズや誤検出を減らします。

勾配ヒストグラムによる方向付け

各特徴点に対して、その周囲の勾配方向を計算し、勾配方向のヒストグラムを作成します。

最もピークの高い方向を特徴点の主方向として割り当てます。

この方向付けにより、特徴点は回転に対して不変になります。

複数のピークがある場合は、それぞれの方向に対して特徴点を複製し、回転不変性を強化します。

128次元ディスクリプタ

特徴点の周囲のパッチを16×16の領域に分割し、各領域で8方向の勾配ヒストグラムを計算します。

これらを連結して128次元の特徴ベクトルを作成します。

このディスクリプタは、スケールや回転、照明変化に対して頑健であり、高い識別性能を持ちます。

マッチングには通常、Euclidean距離が用いられます。

SURF概要

SURF(Speeded-Up Robust Features)は、SIFTの高性能を維持しつつ、計算速度を大幅に向上させた特徴点検出・記述アルゴリズムです。

SURFはHessian行列を用いた特徴点検出と、積分画像を活用した高速な計算が特徴です。

Hessian行列と積分画像

SURFは、Hessian行列の行列式を用いて特徴点を検出します。

Hessian行列は画像の2階微分を表し、以下のように定義されます。

\[H(x, y, \sigma) =\begin{bmatrix}L_{xx}(x, y, \sigma) & L_{xy}(x, y, \sigma) \\L_{xy}(x, y, \sigma) & L_{yy}(x, y, \sigma)\end{bmatrix}\]

ここで、\(L_{xx}\), \(L_{xy}\), \(L_{yy}\)はガウシアンで平滑化された2階微分画像です。

SURFでは、この行列式の値が大きい点を特徴点として選びます。

積分画像を用いることで、任意のサイズの矩形領域の画素和を高速に計算でき、Hessian行列の要素を効率的に求められます。

これにより、SIFTよりも高速な特徴点検出が可能です。

サブサンプリングによる高速化

SURFは、画像のスケール空間を構築する際に、画像を直接縮小するのではなく、フィルタのサイズを変えることでスケール変化を表現します。

これにより、画像のリサンプリングコストを削減し、高速化を実現しています。

また、特徴量の計算にも積分画像を活用し、勾配の計算を効率化しています。

これらの工夫により、SURFはSIFTに匹敵する精度を保ちつつ、リアルタイム処理に近い速度を実現しています。

アルゴリズム選択のポイント

精度 vs 速度

特徴点検出アルゴリズムを選ぶ際には、精度と速度のバランスを考慮することが重要です。

高精度なアルゴリズムは、より多くの計算リソースと時間を必要とする傾向があります。

一方で、リアルタイム処理や組み込みシステムでは高速性が求められ、多少の精度低下を許容する場合もあります。

アルゴリズム精度処理速度特徴
SIFT高い遅いスケール・回転不変、128次元ディスクリプタ
SURF高い速いSIFTに近い精度、積分画像で高速化
ORB中程度非常に速い特許フリー、回転不変、バイナリ記述子

SIFTは非常に高い識別性能を持ち、複雑なシーンや大規模なマッチングに適していますが、計算コストが高いためリアルタイム処理には不向きです。

SURFはSIFTに近い精度を保ちつつ高速化されていますが、特許の問題があります。

ORBは高速で軽量なため、リアルタイム処理やリソース制限のある環境に適していますが、精度はSIFTやSURFに劣る場合があります。

特許とライセンス

特徴点検出アルゴリズムには特許が関わるものがあり、商用利用や配布に制限がかかる場合があります。

これを理解し、適切なアルゴリズムを選択することが重要です。

  • SIFT

特許が存在し、商用利用にはライセンスが必要です。

OpenCVの一部バージョンでは非freeモジュールとして提供されていましたが、特許の期限切れにより最近は利用しやすくなっています。

ただし、プロジェクトの法的要件を確認してください。

  • SURF

SIFT同様に特許があり、商用利用にはライセンスが必要です。

OpenCVの非freeモジュールに含まれており、利用には注意が必要です。

  • ORB

特許フリーであり、オープンソースプロジェクトや商用アプリケーションで自由に利用できます。

特に特許問題を避けたい場合に推奨されます。

特許の問題は、プロジェクトの規模や用途によって影響が異なります。

研究目的や個人利用では問題にならないことも多いですが、商用製品に組み込む場合は必ず確認してください。

組込み・リアルタイム適性

組込みシステムやリアルタイムアプリケーションでは、計算リソースや処理時間に厳しい制約があります。

これらの環境に適した特徴点検出アルゴリズムを選ぶことが成功の鍵です。

  • ORBの適性

ORBは高速で軽量なため、CPUリソースが限られた組込み機器やモバイルデバイスでの利用に適しています。

バイナリ記述子を用いるため、マッチングも高速に行えます。

リアルタイム映像処理やドローン、ロボットの視覚システムで広く使われています。

  • SIFT・SURFの制約

SIFTやSURFは計算負荷が高く、リアルタイム処理には向きません。

GPUや専用ハードウェアを用いて高速化する方法もありますが、組込み環境ではコストや消費電力の面で課題があります。

  • ハードウェアアクセラレーション

OpenCVはOpenCLやCUDAを利用したGPUアクセラレーションをサポートしており、これを活用することでSIFTやSURFの処理速度を改善できます。

ただし、対応ハードウェアが必要であり、組込み環境での利用は限定的です。

  • メモリ使用量

組込み環境ではメモリ容量も制約となるため、特徴量のサイズや検出点数を調整することが求められます。

ORBのバイナリ記述子はメモリ効率が良く、適しています。

これらのポイントを踏まえ、用途や環境に応じて最適なアルゴリズムを選択してください。

ORB実装手順

検出器と記述子の生成

OpenCVでORBを使うには、まずcv::ORBクラスのインスタンスを生成します。

ORBは検出器と記述子を兼ね備えているため、同じオブジェクトで特徴点の検出と特徴量の計算が可能です。

以下のコードは、ORBのインスタンスを生成する例です。

パラメータとして最大検出数やスケールピラミッドのレベル数などを指定できます。

#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
#include <iostream>
int main() {
    // ORB検出器の生成(最大検出数500)
    cv::Ptr<cv::ORB> orb = cv::ORB::create(500);
    // ここから画像処理に進みます
    return 0;
}

この例では、最大500個の特徴点を検出する設定です。

パラメータは用途に応じて調整してください。

キーポイント検出

ORBのdetectメソッドを使って、画像から特徴点(キーポイント)を検出します。

キーポイントはstd::vector<cv::KeyPoint>型で取得されます。

#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
#include <iostream>
int main() {
    cv::Mat img = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
    if (img.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    cv::Ptr<cv::ORB> orb = cv::ORB::create(500);
    std::vector<cv::KeyPoint> keypoints;
    orb->detect(img, keypoints);
    std::cout << "検出されたキーポイント数: " << keypoints.size() << std::endl;
    return 0;
}

このコードでは、グレースケール画像からORBで特徴点を検出し、検出数を表示しています。

ディスクリプタ計算

検出したキーポイントに対して、computeメソッドを使い特徴量(ディスクリプタ)を計算します。

ディスクリプタはcv::Mat型で得られ、バイナリ形式の特徴量が格納されます。

#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
#include <iostream>
int main() {
    cv::Mat img = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
    if (img.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    cv::Ptr<cv::ORB> orb = cv::ORB::create(500);
    std::vector<cv::KeyPoint> keypoints;
    orb->detect(img, keypoints);
    cv::Mat descriptors;
    orb->compute(img, keypoints, descriptors);
    std::cout << "ディスクリプタのサイズ: " << descriptors.size() << std::endl;
    return 0;
}

descriptorsは、各キーポイントに対応するバイナリ特徴量が行ごとに格納された行列です。

なお、detectAndComputeメソッドを使うと、検出と計算を一度に行えます。

画像への描画

検出したキーポイントを画像上に描画するには、cv::drawKeypoints関数を使います。

これにより、特徴点の位置や向きが視覚的に確認できます。

#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
#include <iostream>
int main() {
    cv::Mat img = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
    if (img.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    cv::Ptr<cv::ORB> orb = cv::ORB::create(500);
    std::vector<cv::KeyPoint> keypoints;
    orb->detect(img, keypoints);
    cv::Mat img_keypoints;
    cv::drawKeypoints(img, keypoints, img_keypoints, cv::Scalar::all(-1), cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
    cv::imshow("ORB特徴点", img_keypoints);
    cv::waitKey(0);
    return 0;
}

DRAW_RICH_KEYPOINTSフラグを指定すると、特徴点の大きさや向きも描画されます。

マッチングとの連携

ORBの特徴量はバイナリ形式なので、マッチングにはハミング距離を用いるのが一般的です。

OpenCVのBFMatcherクラスを使って、2つの画像間の特徴点マッチングを行います。

BFMatcher利用

BFMatcherはBrute-Force(総当たり)マッチングを行うクラスです。

ORBのバイナリ特徴量にはNORM_HAMMINGを指定します。

#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
#include <iostream>
int main() {
    cv::Mat img1 = cv::imread("image1.jpg", cv::IMREAD_GRAYSCALE);
    cv::Mat img2 = cv::imread("image2.jpg", cv::IMREAD_GRAYSCALE);
    if (img1.empty() || img2.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    cv::Ptr<cv::ORB> orb = cv::ORB::create(500);
    std::vector<cv::KeyPoint> keypoints1, keypoints2;
    cv::Mat descriptors1, descriptors2;
    orb->detectAndCompute(img1, cv::Mat(), keypoints1, descriptors1);
    orb->detectAndCompute(img2, cv::Mat(), keypoints2, descriptors2);
    cv::BFMatcher matcher(cv::NORM_HAMMING);
    std::vector<cv::DMatch> matches;
    matcher.match(descriptors1, descriptors2, matches);
    std::cout << "マッチ数: " << matches.size() << std::endl;
    cv::Mat img_matches;
    cv::drawMatches(img1, keypoints1, img2, keypoints2, matches, img_matches);
    cv::imshow("ORBマッチング", img_matches);
    cv::waitKey(0);
    return 0;
}

このコードでは、2枚の画像からORB特徴点を検出・記述し、BFMatcherでマッチングを行い、結果を表示しています。

Hamming距離

ORBの特徴量はバイナリベクトルなので、マッチングの距離計算にはハミング距離を使います。

ハミング距離は2つのバイナリ列の異なるビット数を表し、計算が高速です。

BFMatcherのコンストラクタでcv::NORM_HAMMINGを指定することで、自動的にハミング距離が使われます。

RANSACで外れ値除去

マッチング結果には誤った対応(外れ値)が含まれることが多いため、幾何学的な整合性を検証して外れ値を除去します。

代表的な方法がRANSAC(Random Sample Consensus)です。

以下は、RANSACを用いてホモグラフィ行列を推定し、良好なマッチのみを抽出する例です。

#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
#include <iostream>
int main() {
    cv::Mat img1 = cv::imread("image1.jpg", cv::IMREAD_GRAYSCALE);
    cv::Mat img2 = cv::imread("image2.jpg", cv::IMREAD_GRAYSCALE);
    if (img1.empty() || img2.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    cv::Ptr<cv::ORB> orb = cv::ORB::create(500);
    std::vector<cv::KeyPoint> keypoints1, keypoints2;
    cv::Mat descriptors1, descriptors2;
    orb->detectAndCompute(img1, cv::Mat(), keypoints1, descriptors1);
    orb->detectAndCompute(img2, cv::Mat(), keypoints2, descriptors2);
    cv::BFMatcher matcher(cv::NORM_HAMMING);
    std::vector<cv::DMatch> matches;
    matcher.match(descriptors1, descriptors2, matches);
    // マッチの座標を抽出
    std::vector<cv::Point2f> points1, points2;
    for (const auto& match : matches) {
        points1.push_back(keypoints1[match.queryIdx].pt);
        points2.push_back(keypoints2[match.trainIdx].pt);
    }
    // RANSACでホモグラフィ推定
    std::vector<unsigned char> inliersMask(points1.size());
    cv::Mat homography = cv::findHomography(points1, points2, cv::RANSAC, 3.0, inliersMask);
    // インライアのみ抽出
    std::vector<cv::DMatch> inlierMatches;
    for (size_t i = 0; i < matches.size(); i++) {
        if (inliersMask[i]) {
            inlierMatches.push_back(matches[i]);
        }
    }
    std::cout << "インライア数: " << inlierMatches.size() << std::endl;
    cv::Mat img_inliers;
    cv::drawMatches(img1, keypoints1, img2, keypoints2, inlierMatches, img_inliers);
    cv::imshow("RANSACによる外れ値除去後のマッチング", img_inliers);
    cv::waitKey(0);
    return 0;
}

このコードでは、cv::findHomography関数のRANSACオプションを使い、幾何学的に整合するマッチのみを抽出しています。

これにより、誤ったマッチングを効果的に除去できます。

SIFT実装手順

cv::SIFTのインスタンス化

OpenCVでSIFTを利用するには、cv::SIFTクラスのインスタンスを生成します。

OpenCV 4.x以降では、cv::SIFT::create()メソッドを使って簡単にインスタンス化できます。

特許の問題が解消されたため、標準モジュールで利用可能です。

以下はSIFTインスタンスを生成する例です。

パラメータとして検出する特徴点の最大数やコントラスト閾値などを指定できます。

#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
#include <iostream>
int main() {
    // SIFT検出器の生成(最大検出数500)
    cv::Ptr<cv::SIFT> sift = cv::SIFT::create(500);
    return 0;
}

この例では最大500個の特徴点を検出する設定です。

パラメータは用途に応じて調整してください。

キーポイント抽出

SIFTのdetectメソッドを使って、画像から特徴点(キーポイント)を検出します。

キーポイントはstd::vector<cv::KeyPoint>型で取得されます。

#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
#include <iostream>
int main() {
    cv::Mat img = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
    if (img.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    cv::Ptr<cv::SIFT> sift = cv::SIFT::create(500);
    std::vector<cv::KeyPoint> keypoints;
    sift->detect(img, keypoints);
    std::cout << "検出されたキーポイント数: " << keypoints.size() << std::endl;
    return 0;
}

このコードでは、グレースケール画像からSIFTで特徴点を検出し、検出数を表示しています。

ディスクリプタ生成

検出したキーポイントに対して、computeメソッドを使い特徴量(ディスクリプタ)を計算します。

SIFTのディスクリプタは128次元の浮動小数点ベクトルで、スケールや回転に不変な特徴量です。

#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
#include <iostream>
int main() {
    cv::Mat img = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
    if (img.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    cv::Ptr<cv::SIFT> sift = cv::SIFT::create(500);
    std::vector<cv::KeyPoint> keypoints;
    sift->detect(img, keypoints);
    cv::Mat descriptors;
    sift->compute(img, keypoints, descriptors);
    std::cout << "ディスクリプタのサイズ: " << descriptors.size() << std::endl;
    return 0;
}

descriptorsは、各キーポイントに対応する128次元の特徴量が行ごとに格納された行列です。

detectAndComputeメソッドを使うと、検出と計算を一度に行えます。

FLANNによる高速マッチング

SIFTのディスクリプタは高次元の浮動小数点ベクトルであるため、マッチングには高速な近似最近傍探索アルゴリズムが必要です。

OpenCVではFLANN(Fast Library for Approximate Nearest Neighbors)を利用して高速マッチングが可能です。

以下は、2枚の画像のSIFT特徴量をFLANNでマッチングする例です。

#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
#include <iostream>
int main() {
    cv::Mat img1 = cv::imread("image1.jpg", cv::IMREAD_GRAYSCALE);
    cv::Mat img2 = cv::imread("image2.jpg", cv::IMREAD_GRAYSCALE);
    if (img1.empty() || img2.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    cv::Ptr<cv::SIFT> sift = cv::SIFT::create(500);
    std::vector<cv::KeyPoint> keypoints1, keypoints2;
    cv::Mat descriptors1, descriptors2;
    sift->detectAndCompute(img1, cv::Mat(), keypoints1, descriptors1);
    sift->detectAndCompute(img2, cv::Mat(), keypoints2, descriptors2);
    // FLANNベースのマッチャーの生成
    cv::FlannBasedMatcher matcher;
    std::vector<std::vector<cv::DMatch>> knnMatches;
    matcher.knnMatch(descriptors1, descriptors2, knnMatches, 2);
    std::cout << "マッチ候補数: " << knnMatches.size() << std::endl;
    return 0;
}

KD-Tree利用

FLANNはKD-Treeや他のデータ構造を用いて高速に近似最近傍探索を行います。

SIFTのような浮動小数点ベクトルに対しては、KD-Treeがよく使われます。

OpenCVのFlannBasedMatcherは内部で自動的に適切なインデックスを選択します。

KD-Treeは空間を分割し、探索範囲を絞ることで高速化を実現します。

これにより、膨大な特徴量の中から近い特徴量を効率的に見つけられます。

Lowe比率テスト

マッチング結果には誤った対応が含まれることが多いため、Lowe比率テストを用いて信頼性の高いマッチのみを抽出します。

これは、最も近いマッチと2番目に近いマッチの距離比を比較し、比率が一定以下の場合のみマッチを採用する方法です。

以下はLowe比率テストを適用した例です。

#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
#include <iostream>
int main() {
    cv::Mat img1 = cv::imread("image1.jpg", cv::IMREAD_GRAYSCALE);
    cv::Mat img2 = cv::imread("image2.jpg", cv::IMREAD_GRAYSCALE);
    if (img1.empty() || img2.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    cv::Ptr<cv::SIFT> sift = cv::SIFT::create(500);
    std::vector<cv::KeyPoint> keypoints1, keypoints2;
    cv::Mat descriptors1, descriptors2;
    sift->detectAndCompute(img1, cv::Mat(), keypoints1, descriptors1);
    sift->detectAndCompute(img2, cv::Mat(), keypoints2, descriptors2);
    cv::FlannBasedMatcher matcher;
    std::vector<std::vector<cv::DMatch>> knnMatches;
    matcher.knnMatch(descriptors1, descriptors2, knnMatches, 2);
    // Lowe比率テスト
    const float ratioThresh = 0.75f;
    std::vector<cv::DMatch> goodMatches;
    for (size_t i = 0; i < knnMatches.size(); i++) {
        if (knnMatches[i].size() >= 2) {
            const cv::DMatch& bestMatch = knnMatches[i][0];
            const cv::DMatch& betterMatch = knnMatches[i][1];
            if (bestMatch.distance < ratioThresh * betterMatch.distance) {
                goodMatches.push_back(bestMatch);
            }
        }
    }
    std::cout << "良好なマッチ数: " << goodMatches.size() << std::endl;
    cv::Mat imgMatches;
    cv::drawMatches(img1, keypoints1, img2, keypoints2, goodMatches, imgMatches);
    cv::imshow("Lowe比率テスト後のマッチング", imgMatches);
    cv::waitKey(0);
    return 0;
}

このコードでは、2つの近傍マッチの距離比が0.75未満の場合のみマッチを採用し、誤マッチを減らしています。

Lowe比率テストはSIFTマッチングの精度向上に非常に効果的です。

SURF実装手順

cv::xfeatures2d::SURF_create

OpenCVでSURFを利用するには、cv::xfeatures2dモジュールのSURF_create関数を使ってSURF検出器のインスタンスを生成します。

SURFは非freeモジュールに含まれているため、OpenCVのcontribモジュールをビルドしている必要があります。

以下はSURF検出器を生成する例です。

パラメータとして、Hessian行列の閾値hessianThresholdや検出する特徴点の最大数などを指定できます。

#include <opencv2/opencv.hpp>
#include <opencv2/xfeatures2d.hpp>
#include <iostream>
int main() {
    // Hessian閾値を設定(大きいほど検出点は少なくなる)
    double hessianThreshold = 400;
    // SURF検出器の生成
    cv::Ptr<cv::xfeatures2d::SURF> surf = cv::xfeatures2d::SURF::create(hessianThreshold);
    return 0;
}

hessianThresholdは特徴点の検出感度を調整するパラメータで、値が大きいほど検出される特徴点は少なくなります。

用途に応じて調整してください。

キーポイント抽出プロセス

SURFのdetectメソッドを使って、画像から特徴点(キーポイント)を検出します。

SURFはHessian行列の行列式を用いて特徴点を抽出し、スケール空間での検出も行います。

#include <opencv2/opencv.hpp>
#include <opencv2/xfeatures2d.hpp>
#include <iostream>
int main() {
    cv::Mat img = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
    if (img.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    double hessianThreshold = 400;
    cv::Ptr<cv::xfeatures2d::SURF> surf = cv::xfeatures2d::SURF::create(hessianThreshold);
    std::vector<cv::KeyPoint> keypoints;
    surf->detect(img, keypoints);
    std::cout << "検出されたキーポイント数: " << keypoints.size() << std::endl;
    return 0;
}

このコードでは、グレースケール画像からSURFで特徴点を検出し、検出数を表示しています。

ディスクリプタ計算

検出したキーポイントに対して、computeメソッドを使い特徴量(ディスクリプタ)を計算します。

SURFのディスクリプタは64次元または128次元の浮動小数点ベクトルで、スケールや回転に不変な特徴量です。

#include <opencv2/opencv.hpp>
#include <opencv2/xfeatures2d.hpp>
#include <iostream>
int main() {
    cv::Mat img = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
    if (img.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    double hessianThreshold = 400;
    cv::Ptr<cv::xfeatures2d::SURF> surf = cv::xfeatures2d::SURF::create(hessianThreshold);
    std::vector<cv::KeyPoint> keypoints;
    surf->detect(img, keypoints);
    cv::Mat descriptors;
    surf->compute(img, keypoints, descriptors);
    std::cout << "ディスクリプタのサイズ: " << descriptors.size() << std::endl;
    return 0;
}

descriptorsは、各キーポイントに対応する特徴量が行ごとに格納された行列です。

SURF::createの第2引数でextendedフラグをtrueにすると128次元、false(デフォルト)で64次元のディスクリプタが生成されます。

GPUアクセラレーション

SURFは計算コストが高いため、GPUを活用して高速化することが可能です。

OpenCVのcudaモジュールにはSURFのGPU版実装が含まれており、CUDA対応GPUを搭載した環境で利用できます。

以下はCUDA版SURFの基本的な使い方の例です。

#include <opencv2/opencv.hpp>
#include <opencv2/cudafeatures2d.hpp>
#include <iostream>
int main() {
    cv::Mat img = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
    if (img.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    // GPUメモリに画像をアップロード
    cv::cuda::GpuMat gpuImg;
    gpuImg.upload(img);
    // CUDA版SURF検出器の生成
    int hessianThreshold = 400;
    cv::Ptr<cv::cuda::SURF_CUDA> surfCuda = cv::cuda::SURF_CUDA::create(hessianThreshold);
    // キーポイント検出とディスクリプタ計算
    cv::cuda::GpuMat keypointsGPU, descriptorsGPU;
    surfCuda->detectWithDescriptors(gpuImg, cv::cuda::GpuMat(), keypointsGPU, descriptorsGPU);
    // キーポイントをCPUにダウンロード
    std::vector<cv::KeyPoint> keypoints;
    surfCuda->downloadKeypoints(keypointsGPU, keypoints);
    std::cout << "GPUで検出されたキーポイント数: " << keypoints.size() << std::endl;
    return 0;
}

このコードでは、画像をGPUメモリに転送し、CUDA版SURFで特徴点検出と特徴量計算を同時に行っています。

GPUの並列処理能力を活かし、CPU版より大幅に高速な処理が可能です。

ただし、CUDA環境のセットアップや対応GPUが必要であり、組込み環境では利用が難しい場合があります。

GPUアクセラレーションを活用する際は、環境の整備とパフォーマンス評価を行ってください。

高速化のヒント

マルチスレッドとTBB

OpenCVはマルチスレッド処理を活用して、特徴点検出や記述子計算の高速化を図れます。

特にIntelのTBB(Threading Building Blocks)を利用すると、複数のCPUコアを効率的に使い、処理時間を短縮できます。

OpenCVは内部でTBBをサポートしており、ビルド時に有効化されていれば自動的にマルチスレッド化されます。

例えば、cv::ORBcv::SIFTなどの特徴点検出器は、画像の異なる領域を並列処理することで高速化しています。

ユーザー側で明示的にTBBを使う場合は、parallel_for_関数を利用して独自の並列処理を実装可能です。

以下は簡単な例です。

#include <opencv2/opencv.hpp>
#include <tbb/parallel_for.h>
#include <iostream>
int main() {
    cv::Mat img = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
    if (img.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    cv::Mat result = img.clone();
    // 画像の各行を並列処理で反転する例
    tbb::parallel_for(0, img.rows, [&](int y) {
        for (int x = 0; x < img.cols; x++) {
            result.at<uchar>(y, x) = 255 - img.at<uchar>(y, x);
        }
    });
    cv::imshow("反転画像", result);
    cv::waitKey(0);
    return 0;
}

特徴点検出の高速化では、OpenCVの内部処理に任せることが多いですが、独自処理を組み合わせる際にTBBを活用すると効果的です。

OpenCL / CUDA

GPUを活用した高速化も有効な手段です。

OpenCVはOpenCLを利用したTransparent API(T-API)を提供しており、対応する関数は自動的にGPUで処理されます。

これにより、コードの大幅な変更なしにGPUアクセラレーションが可能です。

例えば、cv::UMat型を使うと、OpenCL対応の関数で自動的にGPU処理が行われます。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::UMat img;
    cv::imread("image.jpg", cv::IMREAD_GRAYSCALE).copyTo(img);
    cv::UMat edges;
    cv::Canny(img, edges, 100, 200);
    cv::Mat edges_cpu = edges.getMat(cv::ACCESS_READ);
    cv::imshow("エッジ検出", edges_cpu);
    cv::waitKey(0);
    return 0;
}

特徴点検出器の中には、OpenCL対応のものもありますが、すべてのアルゴリズムが対応しているわけではありません。

OpenCVのドキュメントで対応状況を確認してください。

また、CUDA対応GPUがある場合は、OpenCVのcudaモジュールを利用してさらに高速化できます。

CUDA版のORBやSURFなどが提供されており、大量の画像処理をリアルタイムで行う際に有効です。

CUDAを使う場合は、GPUメモリへのデータ転送や同期処理に注意が必要です。

以下はCUDA版ORBの簡単な例です。

#include <opencv2/opencv.hpp>
#include <opencv2/cudafeatures2d.hpp>
#include <iostream>
int main() {
    cv::Mat img = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
    if (img.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    cv::cuda::GpuMat gpuImg;
    gpuImg.upload(img);
    cv::Ptr<cv::cuda::ORB> orb = cv::cuda::ORB::create();
    cv::cuda::GpuMat keypointsGPU, descriptorsGPU;
    orb->detectAndComputeAsync(gpuImg, cv::cuda::GpuMat(), keypointsGPU, descriptorsGPU);
    std::vector<cv::KeyPoint> keypoints;
    orb->downloadKeypoints(keypointsGPU, keypoints);
    std::cout << "GPUで検出されたキーポイント数: " << keypoints.size() << std::endl;
    return 0;
}

GPUアクセラレーションは大幅な高速化が期待できますが、環境構築や対応ハードウェアの準備が必要です。

解像度とピラミッド調整

画像の解像度を調整することも高速化に効果的です。

高解像度画像は処理負荷が高いため、必要に応じてリサイズしてから特徴点検出を行うと処理時間を短縮できます。

cv::Mat img = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
cv::Mat resizedImg;
cv::resize(img, resizedImg, cv::Size(), 0.5, 0.5); // 半分の解像度に縮小

また、多くの特徴点検出アルゴリズムはスケールピラミッドを利用しており、画像を複数のスケールで解析します。

ピラミッドのレベル数やスケール係数を調整することで、検出精度と速度のバランスを制御できます。

例えば、ORBのcreate関数でnlevels(ピラミッドレベル数)を指定できます。

レベル数を減らすと処理は速くなりますが、スケール変化に対する頑健性が低下します。

cv::Ptr<cv::ORB> orb = cv::ORB::create(500, 1.2f, 4); // ピラミッドレベル数を4に設定

ピラミッドのスケール係数scaleFactorも調整可能で、デフォルトは1.2ですが、これを大きくするとスケール間隔が広がり、処理が軽くなります。

これらのパラメータを適切に設定し、処理速度と検出性能のバランスを最適化してください。

精度向上のヒント

前処理の重要性

特徴点検出の精度を高めるためには、入力画像の前処理が非常に重要です。

ノイズや照明のムラがあると、誤検出や特徴点の不安定化を招くため、適切な前処理を行うことで検出の信頼性を向上させられます。

ノイズ除去

画像に含まれるノイズは、特徴点検出の妨げになります。

特に高感度撮影や圧縮画像ではノイズが多くなるため、ノイズ除去を行うことが効果的です。

代表的なノイズ除去手法には以下があります。

  • ガウシアンブラー

画像を平滑化し、ランダムなノイズを低減します。

カーネルサイズや標準偏差を調整して適度な平滑化を行います。

cv::Mat img, imgDenoised;
cv::GaussianBlur(img, imgDenoised, cv::Size(5, 5), 1.5);
  • メディアンフィルタ

衝撃ノイズ(塩胡椒ノイズ)に強く、エッジを比較的保持しながらノイズを除去します。

cv::medianBlur(img, imgDenoised, 3);

ノイズ除去は過度に行うとエッジやコーナーの情報も失われるため、適切なパラメータ設定が重要です。

コントラスト強調

特徴点検出は画像の輝度変化を基に行われるため、コントラストが低い画像では検出性能が落ちます。

コントラスト強調を行うことで、特徴点の検出数や安定性を向上させられます。

代表的な手法は以下の通りです。

  • ヒストグラム均等化

画像全体の輝度分布を均一化し、コントラストを改善します。

cv::Mat imgEqualized;
cv::equalizeHist(img, imgEqualized);
  • CLAHE(適応的ヒストグラム均等化)

画像を小領域に分割して局所的にヒストグラム均等化を行い、過剰なコントラスト強調を防ぎます。

cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE();
clahe->setClipLimit(2.0);
cv::Mat imgCLAHE;
clahe->apply(img, imgCLAHE);

コントラスト強調は、特に暗い部分や影の多い画像で効果的です。

マルチスケール解析

特徴点はスケール変化に対して不変であることが望ましいため、多くのアルゴリズムはマルチスケール解析を行います。

これは、画像を異なる解像度(スケール)で解析し、各スケールで特徴点を検出する手法です。

スケール空間を構築することで、物体の大きさが変わっても同じ特徴点を検出可能になります。

SIFTやSURFはこの考え方を基にしています。

OpenCVのORBやSIFTでは、ピラミッド画像を自動的に生成し、各レベルで特徴点検出を行います。

ピラミッドのレベル数やスケール係数を調整することで、検出の精度と速度のバランスを制御できます。

マルチスケール解析を活用することで、異なる距離やズームレベルの画像間でも安定したマッチングが可能になります。

適応的Threshold調整

特徴点検出器には、コーナーやエッジの検出に用いる閾値(Threshold)が設定されています。

この閾値は固定値にすると、画像の明るさやコントラストの違いによって検出数が大きく変動することがあります。

適応的に閾値を調整することで、画像ごとに最適な特徴点数を確保し、検出の安定性を高められます。

例えば、ORBではFASTコーナー検出の閾値を動的に変える方法があります。

OpenCVのcv::FAST関数にはnonmaxSuppressionthresholdパラメータがあり、これらを調整して検出数を制御可能です。

また、画像のヒストグラムや輝度分布を解析し、閾値を自動設定するアルゴリズムを組み込むことも有効です。

適応的閾値調整の例として、検出数が少なければ閾値を下げ、多すぎれば上げるといったフィードバック制御を実装することが考えられます。

これにより、環境や撮影条件の変化に強い特徴点検出が実現できます。

応用シナリオ別レシピ

画像ステッチング

画像ステッチングは、複数の画像をつなぎ合わせて広い視野のパノラマ画像を作成する技術です。

特徴点検出とマッチングを活用し、画像間の対応点を求めて幾何変換を推定します。

Homography推定

画像ステッチングの中心的な処理は、画像間の射影変換(ホモグラフィ)を推定することです。

ホモグラフィは、平面上の点の対応関係を表す3×3の行列で、以下のように表されます。

\[\begin{bmatrix}x’ \\y’ \\w’\end{bmatrix}= H\begin{bmatrix}x \\y \\1\end{bmatrix}\]

ここで、\( (x, y) \)は元画像の座標、\( (x’, y’) \)は変換後の座標、\( H \)はホモグラフィ行列です。

OpenCVでは、特徴点のマッチング結果からcv::findHomography関数を使い、RANSACアルゴリズムで外れ値を除去しながらホモグラフィを推定します。

#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
#include <iostream>
int main() {
    cv::Mat img1 = cv::imread("image1.jpg", cv::IMREAD_GRAYSCALE);
    cv::Mat img2 = cv::imread("image2.jpg", cv::IMREAD_GRAYSCALE);
    if (img1.empty() || img2.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    // ORBで特徴点検出と記述子計算
    cv::Ptr<cv::ORB> orb = cv::ORB::create();
    std::vector<cv::KeyPoint> keypoints1, keypoints2;
    cv::Mat descriptors1, descriptors2;
    orb->detectAndCompute(img1, cv::Mat(), keypoints1, descriptors1);
    orb->detectAndCompute(img2, cv::Mat(), keypoints2, descriptors2);
    // BFMatcherでマッチング
    cv::BFMatcher matcher(cv::NORM_HAMMING);
    std::vector<cv::DMatch> matches;
    matcher.match(descriptors1, descriptors2, matches);
    // マッチ座標抽出
    std::vector<cv::Point2f> points1, points2;
    for (const auto& m : matches) {
        points1.push_back(keypoints1[m.queryIdx].pt);
        points2.push_back(keypoints2[m.trainIdx].pt);
    }
    // RANSACでホモグラフィ推定
    std::vector<unsigned char> inliersMask;
    cv::Mat H = cv::findHomography(points1, points2, cv::RANSAC, 3.0, inliersMask);
    std::cout << "ホモグラフィ行列:\n" << H << std::endl;
    return 0;
}

推定したホモグラフィを使い、cv::warpPerspective関数で画像を変換し、パノラマ合成を行います。

動体追跡

動体追跡は、動画や連続画像の中で特定の物体や領域を追いかける技術です。

特徴点を用いることで、物体の位置や動きを高精度に追跡できます。

キーポイント再検出

動体追跡では、フレーム間で特徴点の位置が変化するため、定期的にキーポイントを再検出し、追跡の精度を維持します。

OpenCVのcv::calcOpticalFlowPyrLK関数を使うと、前フレームの特徴点を次フレームで追跡できますが、特徴点が消失したり新たに出現したりするため、一定間隔でdetectを使って新しい特徴点を検出します。

以下は簡単な追跡の流れです。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::VideoCapture cap(0);
    if (!cap.isOpened()) {
        std::cerr << "カメラが開けません。" << std::endl;
        return -1;
    }
    cv::Mat prevGray, gray;
    std::vector<cv::Point2f> pointsPrev, pointsNext;
    // 最初のフレームで特徴点検出
    cap >> prevGray;
    cv::cvtColor(prevGray, prevGray, cv::COLOR_BGR2GRAY);
    cv::goodFeaturesToTrack(prevGray, pointsPrev, 500, 0.01, 10);
    while (true) {
        cv::Mat frame;
        cap >> frame;
        if (frame.empty()) break;
        cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);
        // オプティカルフローで特徴点追跡
        std::vector<uchar> status;
        std::vector<float> err;
        cv::calcOpticalFlowPyrLK(prevGray, gray, pointsPrev, pointsNext, status, err);
        // 有効な追跡点のみ描画
        for (size_t i = 0; i < pointsNext.size(); i++) {
            if (status[i]) {
                cv::circle(frame, pointsNext[i], 3, cv::Scalar(0, 255, 0), -1);
            }
        }
        cv::imshow("動体追跡", frame);
        if (cv::waitKey(30) >= 0) break;
        // 次フレーム用に更新
        prevGray = gray.clone();
        pointsPrev.clear();
        for (size_t i = 0; i < pointsNext.size(); i++) {
            if (status[i]) pointsPrev.push_back(pointsNext[i]);
        }
        // 特徴点が少なくなったら再検出
        if (pointsPrev.size() < 100) {
            cv::goodFeaturesToTrack(prevGray, pointsPrev, 500, 0.01, 10);
        }
    }
    return 0;
}

このように、追跡中に特徴点が減少した場合は再検出を行い、追跡の継続性を保ちます。

3D再構築

3D再構築は、複数の画像から物体やシーンの三次元形状を推定する技術です。

特徴点の対応関係を利用してカメラの位置や3D点群を推定します。

ストラクチャ from Motion

ストラクチャ from Motion(SfM)は、複数の視点から撮影した画像の特徴点マッチングを基に、カメラの動き(モーション)と3D構造(ストラクチャ)を同時に推定する手法です。

SfMの基本的な流れは以下の通りです。

  1. 複数画像の特徴点検出とマッチング
  2. カメラ間の相対姿勢推定(エッセンシャル行列やファンダメンタル行列の計算)
  3. 三角測量による3D点の復元
  4. バンドル調整による最適化

OpenCVでは、特徴点検出やマッチング、エッセンシャル行列推定、三角測量などの機能が提供されています。

以下は、2枚の画像からエッセンシャル行列を推定し、カメラの相対姿勢を求める例です。

#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
#include <iostream>
int main() {
    cv::Mat img1 = cv::imread("image1.jpg", cv::IMREAD_GRAYSCALE);
    cv::Mat img2 = cv::imread("image2.jpg", cv::IMREAD_GRAYSCALE);
    if (img1.empty() || img2.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    cv::Ptr<cv::ORB> orb = cv::ORB::create();
    std::vector<cv::KeyPoint> keypoints1, keypoints2;
    cv::Mat descriptors1, descriptors2;
    orb->detectAndCompute(img1, cv::Mat(), keypoints1, descriptors1);
    orb->detectAndCompute(img2, cv::Mat(), keypoints2, descriptors2);
    cv::BFMatcher matcher(cv::NORM_HAMMING);
    std::vector<cv::DMatch> matches;
    matcher.match(descriptors1, descriptors2, matches);
    std::vector<cv::Point2f> points1, points2;
    for (const auto& m : matches) {
        points1.push_back(keypoints1[m.queryIdx].pt);
        points2.push_back(keypoints2[m.trainIdx].pt);
    }
    // カメラ内部パラメータ(例)
    cv::Mat K = (cv::Mat_<double>(3,3) << 700, 0, 320,
                                          0, 700, 240,
                                          0, 0, 1);
    // エッセンシャル行列推定
    cv::Mat mask;
    cv::Mat E = cv::findEssentialMat(points1, points2, K, cv::RANSAC, 0.999, 1.0, mask);
    // 相対姿勢復元
    cv::Mat R, t;
    int inliers = cv::recoverPose(E, points1, points2, K, R, t, mask);
    std::cout << "復元された回転行列:\n" << R << std::endl;
    std::cout << "復元された並進ベクトル:\n" << t << std::endl;
    std::cout << "インライア数: " << inliers << std::endl;
    return 0;
}

このように、特徴点検出とマッチングを基にカメラの動きを推定し、三角測量で3D点群を復元することで、3D再構築が可能になります。

より高度なSfMシステムでは、複数画像の連続処理やバンドル調整を組み合わせて精度を高めます。

トラブルシューティング

キーポイントが少ない場合

特徴点検出でキーポイントが極端に少ない場合、画像の質やパラメータ設定が原因であることが多いです。

以下の点を確認・対処してください。

  • 画像の質を確認する

ぼやけていたり、ノイズが多い画像は特徴点が検出されにくいです。

シャープネスを上げたり、ノイズ除去を適切に行いましょう。

  • 画像のコントラストを改善する

コントラストが低いと特徴点が見つかりにくくなります。

ヒストグラム均等化やCLAHEでコントラストを強調してください。

  • 検出器のパラメータを調整する

例えば、ORBのnfeatures(最大検出数)やFASTの閾値を下げると検出数が増えます。

SIFTやSURFのcontrastThresholdhessianThresholdも同様です。

  • 画像解像度を上げる

解像度が低いと特徴点が少なくなるため、可能であれば高解像度画像を使うか、リサイズを控えましょう。

  • 前処理の見直し

過度な平滑化やノイズ除去は特徴点を消してしまうことがあります。

適切なバランスを探してください。

マッチング精度が低い場合

マッチングの精度が悪いと、誤った対応点が多くなり、後続処理に悪影響を及ぼします。

以下の対策を検討してください。

  • 特徴量の質を向上させる

前処理でノイズ除去やコントラスト強調を行い、特徴点の質を高めます。

  • マッチング手法の見直し

BFMatcherの距離計算方法が適切か確認します。

ORBならNORM_HAMMING、SIFT/SURFならNORM_L2を使います。

  • Lowe比率テストを導入する

2近傍マッチの距離比を比較し、信頼性の低いマッチを除外します。

これにより誤マッチが大幅に減ります。

  • RANSACなどの幾何学的検証を行う

ホモグラフィやエッセンシャル行列推定時にRANSACを使い、外れ値を除去します。

  • 特徴点数を増やす

検出数が少ないとマッチングの信頼性が下がるため、検出器のパラメータを調整して特徴点数を増やします。

  • 画像の視点や照明条件を揃える

大きな視点差や照明変化はマッチングを難しくします。

可能な限り条件を近づけるか、ロバストな特徴量を選択してください。

描画が崩れる場合

特徴点やマッチング結果の描画が正しく表示されない場合、以下の点を確認してください。

  • 画像の色空間を確認する

cv::drawKeypointscv::drawMatchesはカラー画像に描画することが多いです。

グレースケール画像に描画すると色が正しく表示されないことがあります。

必要に応じてcv::cvtColorでカラー変換してください。

  • 描画先の画像サイズを確認する

描画先の画像が元画像と異なるサイズやチャンネル数の場合、表示が崩れることがあります。

元画像と同じサイズ・チャンネル数の画像を用意しましょう。

  • 描画フラグの設定を見直す

cv::DrawMatchesFlagsの指定が適切か確認します。

例えば、DRAW_RICH_KEYPOINTSはキーポイントの大きさや向きを描画しますが、場合によっては見づらくなることがあります。

  • OpenCVのバージョンや環境を確認する

古いバージョンや環境依存の問題で描画が崩れることがあります。

最新版のOpenCVを使い、環境を整備してください。

  • ウィンドウ表示の問題

cv::imshowで表示が崩れる場合、ウィンドウサイズやスケーリング設定を調整すると改善することがあります。

これらのポイントを確認し、適切に対処することで描画の問題を解決できます。

ORBとSIFTを両方使う意味

ORBとSIFTはそれぞれ特徴点検出と記述子生成のアルゴリズムですが、両方を組み合わせて使うケースがあります。

主な理由は、両者の特性を活かして精度と速度のバランスを取るためです。

  • ORBの特徴

ORBは高速で軽量、特許フリーでありリアルタイム処理に適しています。

ただし、SIFTに比べると特徴量の識別性能はやや劣ります。

  • SIFTの特徴

SIFTは高精度でスケール・回転不変性に優れ、複雑なシーンや大規模なマッチングに強いですが、計算コストが高いです。

両方を使う例としては、まずORBで高速に特徴点を検出・マッチングし、候補点を絞った後にSIFTで詳細なマッチングを行う方法があります。

これにより、処理時間を抑えつつ高精度な結果を得られます。

また、異なるアルゴリズムの特徴点を組み合わせることで、より多様な特徴を捉え、マッチングのロバスト性を向上させることも可能です。

ディスクリプタ次元と処理速度

特徴量の次元数は処理速度に大きく影響します。

一般に、次元数が多いほど計算負荷が増え、マッチングも遅くなります。

  • SIFTの128次元

SIFTのディスクリプタは128次元の浮動小数点ベクトルで、高い識別性能を持ちますが、計算とマッチングに時間がかかります。

  • SURFの64または128次元

SURFは64次元(標準)または128次元(拡張)を選択可能で、SIFTより高速ですが次元数が多いほど処理は重くなります。

  • ORBの32バイト(256ビット)バイナリ

ORBのディスクリプタはバイナリ形式で32バイトの固定長です。

ハミング距離で高速にマッチングでき、処理速度が非常に速いです。

処理速度を優先する場合は、次元数の少ないバイナリ記述子(ORBなど)を選択し、精度を重視する場合は高次元の浮動小数点記述子(SIFTやSURF)を使うのが一般的です。

ライブラリ互換性

OpenCVのバージョンやモジュール構成によって、特徴点検出アルゴリズムの利用可否やAPIが異なることがあります。

  • SIFTとSURFの非freeモジュール問題

以前のOpenCV(3.xなど)では、SIFTやSURFは特許の関係でopencv_contribxfeatures2dモジュールに分離されていました。

利用には別途ビルドやライセンス確認が必要でした。

  • OpenCV 4.x以降の変化

特許の期限切れにより、SIFTは標準モジュールに統合され、cv::SIFT::create()で利用可能になりました。

SURFは依然としてxfeatures2dにありますが、利用は容易になっています。

  • CUDAやOpenCL対応

GPUアクセラレーションを使う場合、CUDA版やOpenCL対応版のAPIが異なります。

環境に応じて適切なモジュールを選択してください。

  • 言語バインディングの違い

PythonやJavaなど他言語バインディングでは、利用可能な機能やAPIが異なる場合があります。

C++での実装と差異があるため、ドキュメントを確認してください。

  • ビルド環境の整備

特徴点検出アルゴリズムを使う際は、OpenCVのビルドオプションや依存モジュールの有無を確認し、環境を整えることが重要です。

これらの点を踏まえ、使用するOpenCVのバージョンや環境に合わせて適切なアルゴリズムとAPIを選択してください。

まとめ

本記事では、OpenCVを用いたC++での特徴点検出アルゴリズム、特にORB・SIFT・SURFの概要と実装手順を詳しく解説しました。

各アルゴリズムの特性や選択ポイント、精度向上や高速化のテクニック、応用シナリオ別の活用法も紹介しています。

これにより、用途や環境に応じた最適な特徴点検出の実装が理解でき、実践的な画像処理開発に役立てられます。

関連記事

Back to top button