記事ディレクトリ
1. テンプレートマッチング
1.1 原則
テンプレート画像は元の画像上で原点から移動し、テンプレートとテンプレートで覆われた元の画像の差を計算します。いくつかの計算方法があり、各計算の結果を出力行列に入れます。元の画像のサイズが A*B で、テンプレートのサイズが a*b の場合、出力行列は (A-a+1)*(B-b+1) になります。
1.2 API
CV_EXPORTS_W void matchTemplate( InputArray image, InputArray templ,
OutputArray result, int method, InputArray mask = noArray() );
- パラメータは次のとおりです
パラメータ | 意味 |
---|---|
画像 | 入力画像、データ型 Mat |
templ(テンプレート) | テンプレート画像、データ型 Mat |
結果 | 出力行列、深さは CV_32FC1 です。元の画像のサイズが A*B で、テンプレートのサイズが a*b の場合、出力行列のサイズは(A-a+1)*(B-b+1)になります。 |
方法 | テンプレートマッチングの計算方法。詳細は以下をご覧ください |
マスク | マスクのイメージ。テンプレート画像と同じサイズであり、グレースケールである必要があります。マッチングの際、マッチング アルゴリズムはマスク内の 0 以外のピクセルに対して機能しますが、マスク内のグレー値が 0 のピクセル位置に対してはマッチング アルゴリズムは機能しません。 |
1.3 テンプレートマッチングの計算方法
enum TemplateMatchModes {
TM_SQDIFF = 0,
TM_SQDIFF_NORMED = 1,
TM_CCORR = 2,
TM_CCORR_NORMED = 3,
TM_CCOEFF = 4,
TM_CCOEFF_NORMED = 5
};
- メソッドのオプションの値は次のとおりです
メソッドのオプションの値 | 意味 |
---|---|
TM_SQDIFF | 二乗誤差を計算します。計算値が小さいほど、一致が高くなります。 |
TM_CCORR | 相関関係を計算します。計算された値が大きいほど、一致が高くなります。 |
TM_CCOEFF | 相関係数を計算し、計算された値が大きいほど一致していることを示します。 |
TM_SQDIFF_NORMED | 正規化二乗誤差を計算します。計算された値が 0 に近づくほど、一致が高くなります。 |
TM_CCORR_NORMED | 正規化された相関を計算します。計算された値が 1 に近いほど、一致が高くなります。 |
TM_CCOEFF_NORMED | 正規化された相関係数を計算します。計算された値が 1 に近いほど、一致が高くなります。 |
1.4 マスクの使用
特徴マッチングを実行する場合、テンプレートの背景がマッチング結果に干渉する可能性があるため、画像全体をテンプレートとして使用する必要がない場合があります。したがって、テンプレート マッチングの背景をブロックするマスクを追加する必要があります。
マスクを入手
- テンプレート画像をグレースケールに変換する
- バイナリマスキングの背景
1.5 効果
Mat xuenai = imread("xuenai.jpg");
imshow("xuenai",xuenai);
Mat templ= imread("xuenai_rect.jpg");
imshow("template",templ);
Mat match_result;
matchTemplate(xuenai,templ,match_result,TM_SQDIFF);
Point temLoc;
Point minLoc;
Point maxLoc;
double min,max;
minMaxLoc(match_result,&min,&max,&minLoc,&maxLoc);
temLoc=minLoc;
rectangle(xuenai,Rect(temLoc.x,temLoc.y,templ.cols,templ.rows),Scalar(0,0,255));
imshow("xuenai_match",xuenai);
waitKey();
1.5 テンプレートマッチングの欠点
スピンに対応できない
Mat xuenai = imread("xuenai.jpg");
rotate(xuenai,xuenai,ROTATE_90_CLOCKWISE);
imshow("xuenai",xuenai);
Mat templ= imread("xuenai_rect.jpg");
Mat match_result;
matchTemplate(xuenai,templ,match_result,TM_SQDIFF);
Point temLoc;
Point minLoc;
Point maxLoc;
double min,max;
minMaxLoc(match_result,&min,&max,&minLoc,&maxLoc);
temLoc=minLoc;
rectangle(xuenai,Rect(temLoc.x,temLoc.y,templ.cols,templ.rows),Scalar(0,0,255));
imshow("xuenai_match",xuenai);
waitKey();
ズームを扱えない
Mat xuenai = imread("xuenai.jpg");
resize(xuenai,xuenai,Size(500,500));
imshow("xuenai",xuenai);
Mat templ= imread("xuenai_rect.jpg");
Mat match_result;
matchTemplate(xuenai,templ,match_result,TM_SQDIFF);
Point temLoc;
Point minLoc;
Point maxLoc;
double min,max;
minMaxLoc(match_result,&min,&max,&minLoc,&maxLoc);
temLoc=minLoc;
rectangle(xuenai,Rect(temLoc.x,temLoc.y,templ.cols,templ.rows),Scalar(0,0,255));
imshow("xuenai_match",xuenai);
waitKey();
2.cornerHarris (グレースケール画像用)
2.1 コーナーポイントの説明
- 一次導関数 (つまり、グレースケールの勾配) の極大値に対応するピクセル。
- 2 つ以上のエッジの交差。
- 勾配値と勾配方向の両方の変化率が非常に高い画像内の点。
- コーナーポイントの一次微分値が最大となり、二次微分値はゼロとなり、オブジェクトのエッジが不連続に変化する方向を示します。
2.2 原則 (前提知識: 線形代数) (bolcksize=2)
固定ウィンドウを使用して画像上を任意の方向にスライドし、スライド前とスライド後の 2 つのケースを比較して、ウィンドウ内のピクセルのグレースケールの変化の程度を比較します。いずれかの方向にスライドすると、グレースケールの変化が大きくなります。このウィンドウにはコーナーポイントがあると考えられます。
グレースケール画像を考えてみましょう。ウィンドウをスワイプすると (x 方向と x 方向に変位します)、ピクセルのグレースケールの変化が計算されます。
で:
- w(x,y) は位置 (x,y) のウィンドウです。
- I(x,y) は (x,y) での強度です。
- I(x+u,y+v)は移動したウィンドウ(x+u,y+v)での強度、
コーナーポイントのあるウィンドウを見つけるには、ピクセルのグレースケールが大きく変化するウィンドウを検索します。したがって、次の方程式を最大化すると予想されます:
テイラー展開:
- Ix、Iy はsobel 演算子によって計算される一次導関数です。
行列化:
二次形式が得られます。
したがって、方程式が存在します。
値は各ウィンドウで計算されます。この値は、コーナーポイントがこのウィンドウに含まれるかどうかを決定します。
このうち、det(M) = 行列 M の行列式、trace(M) = 行列 M のトレース
- R が正の値の場合はコーナー点が検出され、R が負の値の場合はエッジが検出され、R が小さい場合は平坦領域が検出されます。
2.3 API
CV_EXPORTS_W void cornerHarris( InputArray src, OutputArray dst, int blockSize,
int ksize, double k,
int borderType = BORDER_DEFAULT );
- パラメータは次のとおりです
パラメータ | 意味 |
---|---|
ソース(ソース) | 入力画像(グレースケール画像)、深度要件: CV_8UC1 または CV_32FC1 |
dst(目的地) | 出力画像、データ型 Mat |
ボルトサイズ | 検出ウィンドウのサイズ。大きいほどコーナーポイントに対する感度が高くなります。通常は 2 です。 |
ksize(核サイズ) | sobel 演算子を使用して一次導関数を計算する場合のフィルター サイズは、通常 3 です。 |
k | 計算に使用される係数は、一般に 0.02 ~ 0.06 の間であると認識されています。 |
境界線の種類 | 枠線の塗りつぶし方法。デフォルトは黒枠です。 |
2.4 プロセス
- グレースケール画像に変換する
- コーナーハリス関数を使用して検出
- 正規化関数を使用して、ScaleAbs を正規化して絶対値に変換します。
- 出力イメージをループし、コーナーポイントをフィルターします。イテレータトラバーサルは遅すぎるので使用しないでください。
- 実際にテストした結果、行数を指定して ptr 関数を呼び出す次のトラバーサル方法が最も高速です。
Mat xuenai = imread("xuenai.jpg");
imshow("xuenai", xuenai);
//转灰度图
Mat xuenai_gray(xuenai.size(),xuenai.type());
cvtColor(xuenai,xuenai_gray,COLOR_BGR2GRAY);
Mat xuenai_harris;
cornerHarris(xuenai_gray,xuenai_harris,2,3,0.04);
normalize(xuenai_harris,xuenai_harris,0,255,NORM_MINMAX,-1);
convertScaleAbs(xuenai_harris,xuenai_harris);
namedWindow("xuenai_harris");
createTrackbar("threshold","xuenai_harris", nullptr,255);
while (1) {
int thres = getTrackbarPos("threshold", "xuenai_harris");
if(thres==0)thres=100;
Mat harris_result=xuenai.clone();
for(int i=0;i<xuenai_harris.rows;i++){
uchar * ptr =xuenai_harris.ptr(i);
for(int j=0;j<xuenai_harris.cols;j++){
int value=(int) *ptr;
if(value>thres){
circle(harris_result, Point(j,i), 3, Scalar(0, 0, 255));
}
ptr++;
}
}
imshow("xuenai_harris",harris_result);
if (waitKey(0) == 'q')break;
}
2.5 メリットとデメリット
テストコード
Mat xuenai = imread("xuenai.jpg");
imshow("xuenai", xuenai);
namedWindow("panel");
createTrackbar("threshold","panel", nullptr,255);
createTrackbar("angle","panel", nullptr,360);
createTrackbar("width","panel", nullptr,1000);
createTrackbar("height","panel", nullptr,1000);
while (1) {
int thres = getTrackbarPos("threshold", "panel");
if(thres==0)thres=100;
int width = getTrackbarPos("width", "panel");
if(width==0)width=xuenai.cols;
int height = getTrackbarPos("height", "panel");
if(height==0)height=xuenai.rows;
int angle = getTrackbarPos("angle","panel");
Mat xuenai_harris, xuenai_transform=xuenai.clone();
resize(xuenai_transform,xuenai_transform,Size(width,height));
Mat M= getRotationMatrix2D(Point2f(xuenai.cols/2,xuenai.rows/2),angle,1);
warpAffine(xuenai_transform,xuenai_transform,M,xuenai_transform.size());
Mat xuenai_gray(xuenai.size(),xuenai.type());
cvtColor(xuenai_transform,xuenai_gray,COLOR_BGR2GRAY);
cornerHarris(xuenai_gray,xuenai_harris,2,3,0.04);
normalize(xuenai_harris,xuenai_harris,0,255,NORM_MINMAX,-1);
convertScaleAbs(xuenai_harris,xuenai_harris);
Mat harris_result=xuenai_transform.clone();
for(int i=0;i<xuenai_harris.rows;i++){
uchar * ptr =xuenai_harris.ptr(i);
for(int j=0;j<xuenai_harris.cols;j++){
int value=(int) *ptr;
if(value>thres){
circle(harris_result, Point(j,i), 3, Scalar(0, 0, 255));
}
ptr++;
}
}
imshow("xuenai_harris",harris_result);
if (waitKey(0) == 'q')break;
}
画像は回転しますが、角は変わりません
画像のスケーリング、コーナーポイントの変更
3.シ・トマシ(グレースケール画像用)
3.1 原則
コーナーハリスのコーナーチェックの安定性はkと密接な関係があり、kは経験値であるため最適な値を設定することが困難でしたが、Shi-Tomasiはこの点を改善しました。
- コーナーポイントの計算
3.2 API
CV_EXPORTS_W void goodFeaturesToTrack( InputArray image, OutputArray corners,
int maxCorners, double qualityLevel, double minDistance,
InputArray mask = noArray(), int blockSize = 3,
bool useHarrisDetector = false, double k = 0.04 );
CV_EXPORTS_W void goodFeaturesToTrack( InputArray image, OutputArray corners,
int maxCorners, double qualityLevel, double minDistance,
InputArray mask, int blockSize,
int gradientSize, bool useHarrisDetector = false,
double k = 0.04 );
- パラメータは次のとおりです
パラメータ | 意味 |
---|---|
画像 | 入力画像(グレースケール画像)、深度要件: CV_8UC1 または CV_32FC1 |
コーナー | コーナー点の点セットを出力します。データ型は Vector<Point2f> です。 |
マックスコーナー | 出力コーナー点セットの上限を制御します。つまり、corners.size() を制御します。上限がないことを示すには 0 を入力します |
品質レベル | 許容可能なコーナー ポイントの最小品質レベルを示す品質係数 (1.0 未満の正の数、通常は 0.01 ~ 0.1 の間) 。この係数は、最小許容スコアとして入力イメージ内の最大コーナー スコアに乗算されます。たとえば、入力イメージ内の最大コーナー スコアの値が 1500 で品質係数が 0.01 の場合、すべてのコーナーにコーナー スコアが適用されます。 15 未満は無視されます。 |
最小距離 | コーナーポイント間の最小ユークリッド距離。この距離より小さいポイントは無視されます。 |
マスク | マスクのイメージ。入力イメージと同じサイズであり、グレースケールである必要があります。計算時、このアルゴリズムはマスク内のゼロ以外のピクセルに対しては機能しますが、マスク内のグレー値が 0 のピクセル位置に対しては機能しません。 |
ブロックサイズ | 検出ウィンドウのサイズ。大きいほど、コーナーポイントに対する感度が高くなります。 |
HarrisDetector を使用する | コーナー検出方法の指定に使用されます。true の場合は Harris コーナー検出が使用され、false の場合は Shi Tomasi アルゴリズムが使用されます。デフォルトは False です。 |
k | デフォルトは 0.04 で、useHarrisDetector パラメーターが true の場合にのみ機能します。 |
3.3 プロセス
- グレースケール画像に変換する
- シトマシ機能による検知
- コーナーポイントセットをトラバースするだけです
3.4 効果
- Shi-Tomasi には回転不変性とスケール変動性もあります
Mat xuenai = imread("xuenai.jpg");
imshow("xuenai", xuenai);
namedWindow("panel");
createTrackbar("threshold","panel", nullptr,255);
createTrackbar("angle","panel", nullptr,360);
createTrackbar("width","panel", nullptr,1000);
createTrackbar("height","panel", nullptr,1000);
while (1) {
int thres = getTrackbarPos("threshold", "panel");
if(thres==0)thres=100;
int width = getTrackbarPos("width", "panel");
if(width==0)width=xuenai.cols;
int height = getTrackbarPos("height", "panel");
if(height==0)height=xuenai.rows;
int angle = getTrackbarPos("angle","panel");
Mat xuenai_transform=xuenai.clone();
resize(xuenai_transform,xuenai_transform,Size(width,height));
Mat M= getRotationMatrix2D(Point2f(xuenai.cols/2,xuenai.rows/2),angle,1);
warpAffine(xuenai_transform,xuenai_transform,M,xuenai_transform.size());
Mat xuenai_gray(xuenai.size(),xuenai.type());
cvtColor(xuenai_transform,xuenai_gray,COLOR_BGR2GRAY);
vector<Point2f>xuenai_cornersSet;
goodFeaturesToTrack(xuenai_gray,xuenai_cornersSet,0,0.1,10);
for(auto corner:xuenai_cornersSet){
circle(xuenai_transform,corner,3,Scalar(0,0,255));
}
imshow("xuenai_corners",xuenai_transform);
if (waitKey(0) == 'q')break;
}
4.SIFTとSURF(グレースケール画像用)
4.1 概要
コーナーハリスもシトマシもスケール上のコーナーポイントの安定性を保証できないため、SIFT と SURF はこの機能に最適化されています。数学的原理は比較的複雑なので、ここでは詳しく説明しませんので、各自で関連する論文や文献を参照してください。
コーナーハリスやシトマシと比較すると、SIFTやSURFの利点は大きく、検出されたコーナー点は回転、拡大縮小、明るさの変化などに対して不変であり、ある程度の透視変換、アフィン変換、ノイズ. 安定性の高い非常に優れた局所特徴記述アルゴリズムです。
なお、SIFTやSURFは計算量が多く、リアルタイム計算が困難です。SIFT と SURF を比較すると、SIFT の方が精度が高く、SURF の方が効率的です。
4.2 API
コンストラクタ
CV_WRAP static Ptr<SIFT> SIFT::create(int nfeatures = 0, int nOctaveLayers = 3,
double contrastThreshold = 0.04, double edgeThreshold = 10,
double sigma = 1.6);
CV_WRAP static Ptr<SIFT> SIFT::create(int nfeatures, int nOctaveLayers,
double contrastThreshold, double edgeThreshold,
double sigma, int descriptorType);
CV_WRAP static Ptr<SURF> SURF::create(double hessianThreshold=100,
int nOctaves = 4, int nOctaveLayers = 3,
bool extended = false, bool upright = false);
- コンストラクターのパラメーター設計の複雑な数学的原理についてはここでは説明しませんので、使用する際はデフォルトの構成を作成するだけです。
キーポイントの検出
CV_WRAP virtual void Feature2D::detect( InputArray image,
CV_OUT std::vector<KeyPoint>& keypoints,
InputArray mask=noArray() );
CV_WRAP virtual void Feature2D::detect( InputArrayOfArrays images,
CV_OUT std::vector<std::vector<KeyPoint> >& keypoints,
InputArrayOfArrays masks=noArray() );
- パラメータは次のとおりです
パラメータ | 意味 |
---|---|
画像 | 输入图像 (灰度图),深度要求:CV_8UC1或CV_32FC1 |
keypoints | 含多个关键点的vector<KeyPoint>。使用detect时作为输出,使用compute时作为输入,使用detectAndCompute时可以作为输入也可以作为输出。 |
mask | 掩码图像。其大小与输入图像必须相同,且必须为灰度图。计算时,对于掩码中的非0像素算法起作用,掩码中的灰度值为0的像素位置,算法不起作用。 |
描述子计算
CV_WRAP virtual void Feature2D::compute( InputArray image,
CV_OUT CV_IN_OUT std::vector<KeyPoint>& keypoints,
OutputArray descriptors );
CV_WRAP virtual void Feature2D::compute( InputArrayOfArrays images,
CV_OUT CV_IN_OUT std::vector<std::vector<KeyPoint> >& keypoints,
OutputArrayOfArrays descriptors );
CV_WRAP virtual void Feature2D::detectAndCompute( InputArray image, InputArray mask,
CV_OUT std::vector<KeyPoint>& keypoints,
OutputArray descriptors,
bool useProvidedKeypoints=false );
- 参数如下
参数 | 含义 |
---|---|
image | 输入图片 (灰度图),深度要求:CV_8UC1或CV_32FC1 |
keypoints | 含多个关键点的vector<KeyPoint>。使用detect时作为输出,使用compute时作为输入,使用detectAndCompute时可以作为输入也可以作为输出。 |
descriptors | 描述子,数据类型Mat。在进行特征匹配的时候会用到。 |
useProvidedKeypoints | false时,keypoints作为输出,并根据keypoints算出descriptors。true时,keypoints作为输入,不再进行detect,即不修改keypoints,并根据keypoints算出descriptors。 |
drawKeypoints绘制关键点
CV_EXPORTS_W void drawKeypoints( InputArray image, const std::vector<KeyPoint>& keypoints, InputOutputArray outImage,
const Scalar& color=Scalar::all(-1), DrawMatchesFlags flags=DrawMatchesFlags::DEFAULT );
enum struct DrawMatchesFlags
{
DEFAULT = 0, //!< Output image matrix will be created (Mat::create),
//!< i.e. existing memory of output image may be reused.
//!< Two source image, matches and single keypoints will be drawn.
//!< For each keypoint only the center point will be drawn (without
//!< the circle around keypoint with keypoint size and orientation).
DRAW_OVER_OUTIMG = 1, //!< Output image matrix will not be created (Mat::create).
//!< Matches will be drawn on existing content of output image.
NOT_DRAW_SINGLE_POINTS = 2, //!< Single keypoints will not be drawn.
DRAW_RICH_KEYPOINTS = 4 //!< For each keypoint the circle around keypoint with keypoint size and
//!< orientation will be drawn.
};
- 参数如下
参数 | 含义 |
---|---|
image | 输入图像,数据类型Mat |
keypoints | 含多个关键点的vector<KeyPoint> |
outImage | 输出图像,数据类型Mat |
color | 绘制颜色信息,默认绘制的是随机彩色。 |
flags | 特征点的绘制模式,其实就是设置特征点的那些信息需要绘制,那些不需要绘制。详见下表 |
- flags可选值如下
flags可选值 | 含义 |
---|---|
DrawMatchesFlags::DEFAULT | 只绘制特征点的坐标点,显示在图像上就是一个个小圆点,每个小圆点的圆心坐标都是特征点的坐标。 |
DrawMatchesFlags::DRAW_OVER_OUTIMG | 函数不创建输出的图像,而是直接在输出图像变量空间绘制,要求本身输出图像变量就是一个初始化好了的,size与type都是已经初始化好的变量。 |
DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS | 单点的特征点不被绘制 |
DrawMatchesFlags::DRAW_RICH_KEYPOINTS | 绘制特征点的时候绘制的是一个个带有方向的圆,这种方法同时显示图像的坐标,size和方向,是最能显示特征的一种绘制方式。 |
4.3 流程
- 实例化SIFT或SURF对象
- 将输入图像转灰度图
- 根据需要,调用detect函数或compute函数或detectAndCompute函数,检测关键点和计算描述子
- 调用drawKeypoints函数绘制关键点
4.4 效果
Mat xuenai = imread("xuenai.jpg");
imshow("xuenai", xuenai);
namedWindow("panel");
createTrackbar("threshold","panel", nullptr,255);
createTrackbar("angle","panel", nullptr,360);
createTrackbar("width","panel", nullptr,1000);
createTrackbar("height","panel", nullptr,1000);
while (1) {
int thres = getTrackbarPos("threshold", "panel");
if(thres==0)thres=100;
int width = getTrackbarPos("width", "panel");
if(width==0)width=xuenai.cols;
int height = getTrackbarPos("height", "panel");
if(height==0)height=xuenai.rows;
int angle = getTrackbarPos("angle","panel");
Mat xuenai_transform=xuenai.clone();
resize(xuenai_transform,xuenai_transform,Size(width,height));
Mat M= getRotationMatrix2D(Point2f(xuenai_transform.cols/2,xuenai_transform.rows/2),angle,1);
warpAffine(xuenai_transform,xuenai_transform,M,xuenai_transform.size());
Mat xuenai_gray(xuenai.size(),xuenai.type());
cvtColor(xuenai_transform,xuenai_gray,COLOR_BGR2GRAY);
Ptr<SIFT> sift=SIFT::create();
Ptr<SURF> surf=SURF::create();
vector<KeyPoint>xuenai_SiftKp,xuenai_Surfp;
sift->detect(xuenai_gray,xuenai_SiftKp);
surf->detect(xuenai_gray,xuenai_Surfp);
Mat sift_result=xuenai_transform.clone(),surf_result=xuenai_transform.clone();
drawKeypoints(sift_result,xuenai_SiftKp,sift_result,Scalar::all(-1),DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
drawKeypoints(surf_result,xuenai_Surfp,surf_result,Scalar::all(-1),DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
imshow("sift_result",sift_result);
imshow("surf_result",surf_result);
if (waitKey(0) == 'q')break;
}
进行缩放和旋转
- 可以看到,无论是旋转还是缩放,关键点都保持得非常稳定。
5.FAST到OBR(对灰度图)
5.1 概述
前文已经阐述,SIFT和SURF已经做到了角点在旋转和缩放下的稳定性,但是它们还有一个致命的缺陷,就是它们难以做到实时运算,因此,FAST和OBR应运而生了。
FAST原理
从图片中选取一个坐标点P,获取该点的像素值,接下来判定该点是否为特征点.
选取一个以选取点P坐标为圆心的半径等于r的Bresenham圆(一个计算圆的轨迹的离散算法,得到整数级的圆的轨迹点),一般来说,这个圆上有16个点,如下所示
- p在图像中表示一个被识别为兴趣点的像素。令它的强度为 Ip;
- 选择一个合适的阈值t;
- 考虑被测像素周围的16个像素的圆圈。 如果这16个像素中存在一组ñ个连续的像素的像素值,比 Ip+t 大,或比 Ip−t小,则像素p是一个角点。ñ被设置为12。
- 使用一种快速测试(high-speed test)可快速排除了大量的非角点。这个方法只检测在1、9、5、13个四个位置的像素,(首先检测1、9位置的像素与阈值比是否太亮或太暗,如果是,则检查5、13)。如果p是一个角点,则至少有3个像素比 Ip+t大或比 Ip−t暗。如果这两者都不是这样的话,那么p就不能成为一个角点。然后可以通过检查圆中的所有像素,将全部分段测试标准应用于通过的对候选的角点。这种探测器本身表现出很高的性能,但有一些缺点:
- 它不能拒绝n <12的候选角点。当n<12时可能会有较多的候选角点出现
- 检测到的角点不是最优的,因为它的效率取决于问题的排序和角点的分布。
- 角点分析的结果被扔掉了。过度依赖于阈值
- 多个特征点容易挤到一起。
- 前三点是用机器学习方法解决的。最后一个是使用非极大值抑制来解决。具体不再展开。
FAST算法虽然很快,但是没有建立关键点的描述子,也就无法进行特征匹配
OBR简介
ORB 是 Oriented Fast and Rotated Brief 的简称,从这个简介就可以看出,OBR算法是基础FAST算法的改进。其中,Fast 和 Brief 分别是特征检测算法和向量创建算法。ORB 首先会从图像中查找特殊区域,称为关键点。关键点即图像中突出的小区域,比如角点,比如它们具有像素值急剧的从浅色变为深色的特征。然后 ORB 会为每个关键点计算相应的特征向量。ORB 算法创建的特征向量只包含 1 和 0,称为二元特征向量。1 和 0 的顺序会根据特定关键点和其周围的像素区域而变化。该向量表示关键点周围的强度模式,因此多个特征向量可以用来识别更大的区域,甚至图像中的特定对象。
关于Brief算法的具体原理本文不再赘述,请自行查阅相关论文和文献。
5.2 API
构造函数
CV_WRAP static Ptr<FastFeatureDetector> create( int threshold=10,
bool nonmaxSuppression=true,
FastFeatureDetector::DetectorType type=FastFeatureDetector::TYPE_9_16 );
CV_WRAP static Ptr<ORB> create(int nfeatures=500, float scaleFactor=1.2f, int nlevels=8, int edgeThreshold=31,
int firstLevel=0, int WTA_K=2, ORB::ScoreType scoreType=ORB::HARRIS_SCORE, int patchSize=31, int fastThreshold=20);
threshold:进行FAST检测时用到的阈值,阈值越大检测到的角点越少
5.3 流程
- 实例化FAST或OBR对象
- 将输入图像转灰度图
- 根据需要,调用detect函数或compute函数或detectAndCompute函数,检测关键点和计算描述子
- 调用drawKeypoints函数绘制关键点
5.4 效果
Mat xuenai = imread("xuenai.jpg");
imshow("xuenai", xuenai);
namedWindow("panel");
createTrackbar("threshold","panel", nullptr,255);
createTrackbar("angle","panel", nullptr,360);
createTrackbar("width","panel", nullptr,1000);
createTrackbar("height","panel", nullptr,1000);
while (1) {
int thres = getTrackbarPos("threshold", "panel");
if(thres==0)thres=100;
int width = getTrackbarPos("width", "panel");
if(width==0)width=xuenai.cols;
int height = getTrackbarPos("height", "panel");
if(height==0)height=xuenai.rows;
int angle = getTrackbarPos("angle","panel");
Mat xuenai_transform=xuenai.clone();
resize(xuenai_transform,xuenai_transform,Size(width,height));
Mat M= getRotationMatrix2D(Point2f(xuenai_transform.cols/2,xuenai_transform.rows/2),angle,1);
warpAffine(xuenai_transform,xuenai_transform,M,xuenai_transform.size());
Mat xuenai_gray(xuenai.size(),xuenai.type());
cvtColor(xuenai_transform,xuenai_gray,COLOR_BGR2GRAY);
Ptr<FastFeatureDetector>fast=FastFeatureDetector::create(thres);
Ptr<ORB>obr=ORB::create();
vector<KeyPoint>xuenai_FastKp,xuenai_ObrKp;
fast->detect(xuenai_gray,xuenai_FastKp);
obr->detect(xuenai_gray,xuenai_ObrKp);
Mat fast_result=xuenai_transform.clone(),obr_result=xuenai_transform.clone();
drawKeypoints(fast_result,xuenai_FastKp,fast_result,Scalar::all(-1),DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
drawKeypoints(obr_result,xuenai_ObrKp,obr_result,Scalar::all(-1),DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
imshow("fast_result",fast_result);
imshow("obr_result",obr_result);
if (waitKey(0) == 'q')break;
}
调整threshold
进行缩放和旋转
错误
fast->detectAndCompute(xuenai_gray,noArray(),fast_kp,fast_des);
前文已经提及,FAST算法不支持描述子的计算
error: (-213:The function/feature is not implemented) in function 'detectAndCompute'
6.Brute-Force与FLANN特征匹配
6.1 概述
Brute-Force
暴力匹配(Brute-force matcher)是最简单的二维特征点匹配方法。对于从两幅图像中提取的两个特征描述符集合,对第一个集合中的每个描述符Ri,从第二个集合中找出与其距离最小的描述符Sj作为匹配点。
暴力匹配显然会导致大量错误的匹配结果,还会出现一配多的情况。通过交叉匹配或设置比较阈值筛选匹配结果的方法可以改进暴力匹配的质量。
- 如果参考图像中的描述符Ri与检测图像中的描述符Sj的互为最佳匹配,则称(Ri , Sj)为一致配对。交叉匹配通过删除非一致配对来筛选匹配结果,可以避免出现一配多的错误。
- 比较阈值筛选是指对于参考图像的描述符Ri,从检测图像中找到距离最小的描述符Sj1和距离次小的描述符Sj2。设置比较阈值t∈[0.5 , 0.9],只有当最优匹配距离与次优匹配距离满足阈值条件d (Ri , Sj1) ⁄ d (Ri , Sj2) < t时,表明匹配描述符Sj1具有显著性,才接受匹配结果(Ri , Sj1)。
FLANN
- 相比于Brute-Force,FLANN的速度更快
- 由于使用的是邻近近似值,所以精度较差
6.2 API
构造函数
CV_WRAP static Ptr<BFMatcher> BFMatcher::create( int normType=NORM_L2, bool crossCheck=false ) ;
CV_WRAP static Ptr<FlannBasedMatcher> create();
enum NormTypes {
NORM_INF = 1,
NORM_L1 = 2,
NORM_L2 = 4,
NORM_L2SQR = 5,
NORM_HAMMING = 6,
NORM_HAMMING2 = 7,
NORM_TYPE_MASK = 7,
NORM_RELATIVE = 8,
NORM_MINMAX = 32
};
- 参数如下
参数 | 含义 |
---|---|
normType | 计算距离用到的方法,默认是欧氏距离。详见下表 |
crossCheck | 是否使用交叉验证,默认不使用。 |
- normType可选值如下
normType可选值 | 含义 |
---|---|
NORM_L1 | L1范数,曼哈顿距离 |
NORM_L2 | L2范数,欧氏距离 |
NORM_HAMMING | 汉明距离 |
NORM_HAMMING2 | 汉明距离2,对每2个比特相加处理。 |
- NORM_L1、NORM_L2适用于SIFT和SURF检测算法
- NORM_HAMMING、NORM_HAMMING2适用于OBR算法
描述子匹配
匹配方式一
CV_WRAP void DescriptorMatcher::match( InputArray queryDescriptors, InputArray trainDescriptors,
CV_OUT std::vector<DMatch>& matches, InputArray mask=noArray() ) const;
- 参数如下
参数 | 含义 |
---|---|
queryDescriptors | 描述子的查询点集,数据类型Mat,即参考图像的特征描述符的集合。 |
trainDescriptors | 描述子的训练点集,数据类型Mat,即检测图像的特征描述符的集合。 |
matches | 匹配结果,长度为成功匹配的数量。 |
mask | 掩码图像。其大小与输入图像必须相同,且必须为灰度图。计算时,对于掩码中的非0像素算法起作用,掩码中的灰度值为0的像素位置,算法不起作用。 |
- 特别注意和区分哪个是查询集,哪个是训练集
匹配方式二
CV_WRAP void DescriptorMatcher::knnMatch( InputArray queryDescriptors, InputArray trainDescriptors,
CV_OUT std::vector<std::vector<DMatch> >& matches, int k,
InputArray mask=noArray(), bool compactResult=false ) const;
- 参数如下
参数 | 含义 |
---|---|
queryDescriptors | 描述子的查询点集,数据类型Mat,即参考图像的特征描述符的集合。 |
trainDescriptors | 描述子的训练点集,数据类型Mat,即检测图像的特征描述符的集合。 |
matches | vector<std::vector<DMatch>>类型,对每个特征点返回k个最优的匹配结果 |
k | 返回匹配点的数量 |
mask | 掩码图像。其大小与输入图像必须相同,且必须为灰度图。计算时,对于掩码中的非0像素算法起作用,掩码中的灰度值为0的像素位置,算法不起作用。 |
- 特别注意和区分哪个是查询集,哪个是训练集
Brute-Force与FLANN对输入描述子的要求
- Brute-Force要求输入的描述子必须是CV_8U或者CV_32S
- FLANN要求输入的描述子必须是CV_32F
drawMatches绘制匹配结果
CV_EXPORTS_W void drawMatches( InputArray img1, const std::vector<KeyPoint>& keypoints1,
InputArray img2, const std::vector<KeyPoint>& keypoints2,
const std::vector<DMatch>& matches1to2, InputOutputArray outImg,
const Scalar& matchColor=Scalar::all(-1), const Scalar& singlePointColor=Scalar::all(-1),
const std::vector<char>& matchesMask=std::vector<char>(), DrawMatchesFlags flags=DrawMatchesFlags::DEFAULT );
CV_EXPORTS_W void drawMatches( InputArray img1, const std::vector<KeyPoint>& keypoints1,
InputArray img2, const std::vector<KeyPoint>& keypoints2,
const std::vector<DMatch>& matches1to2, InputOutputArray outImg,
const int matchesThickness, const Scalar& matchColor=Scalar::all(-1),
const Scalar& singlePointColor=Scalar::all(-1), const std::vector<char>& matchesMask=std::vector<char>(),
DrawMatchesFlags flags=DrawMatchesFlags::DEFAULT );
CV_EXPORTS_AS(drawMatchesKnn) void drawMatches( InputArray img1, const std::vector<KeyPoint>& keypoints1,
InputArray img2, const std::vector<KeyPoint>& keypoints2,
const std::vector<std::vector<DMatch> >& matches1to2, InputOutputArray outImg,
const Scalar& matchColor=Scalar::all(-1), const Scalar& singlePointColor=Scalar::all(-1),
const std::vector<std::vector<char> >& matchesMask=std::vector<std::vector<char> >(), DrawMatchesFlags flags=DrawMatchesFlags::DEFAULT );
- 参数如下
参数 | 含义 |
---|---|
img1(image1) | 源图像1,数据类型Mat |
keypoints1 | 源图像1的关键点 |
img2(image2) | 源图像2,数据类型Mat |
keypoints2 | 源图像2的关键点 |
matches1to2 | 源图像1的描述子匹配源图像2的描述子的匹配结果 |
outImg(out image) | 输出图像,数据类型Mat |
matchColor | 匹配的颜色(特征点和连线),默认Scalar::all(-1),颜色随机 |
singlePointColor | 单个点的颜色,即未配对的特征点,默认Scalar::all(-1),颜色随机 |
matchesMask | 掩码,决定哪些点将被画出,若为空,则画出所有匹配点 |
flags | 特征点的绘制模式,其实就是设置特征点的那些信息需要绘制,那些不需要绘制。 |
6.3 流程
- 实例化BFMatcher对象
- 根据需要,调用match函数或knnMatch函数,进行特征匹配
- 调用drawMatches函数呈现原图,并且绘制匹配点
6.4 效果
Mat xuenai = imread("xuenai.jpg");
Mat xuenai_rect = imread("xuenai_rect.jpg");
Mat xuenai_rect_gray;cvtColor(xuenai_rect,xuenai_rect_gray,COLOR_BGR2GRAY);
imshow("xuenai", xuenai);
namedWindow("panel");
createTrackbar("threshold","panel", nullptr,255);
createTrackbar("angle","panel", nullptr,360);
createTrackbar("width","panel", nullptr,1000);
createTrackbar("height","panel", nullptr,1000);
while (true) {
int thres = getTrackbarPos("threshold", "panel");
if(thres==0)thres=100;
int width = getTrackbarPos("width", "panel");
if(width==0)width=xuenai.cols;
int height = getTrackbarPos("height", "panel");
if(height==0)height=xuenai.rows;
int angle = getTrackbarPos("angle","panel");
Mat xuenai_transform=xuenai.clone();
//调整尺寸
resize(xuenai_transform,xuenai_transform,Size(width,height));
//进行旋转
Mat M= getRotationMatrix2D(Point2f((float )xuenai_transform.cols/2,(float )xuenai_transform.rows/2),angle,1);
warpAffine(xuenai_transform,xuenai_transform,M,xuenai_transform.size());
//灰度图
Mat xuenai_gray(xuenai.size(),xuenai.type());
cvtColor(xuenai_transform,xuenai_gray,COLOR_BGR2GRAY);
//准备工作
Ptr<ORB>obr=ORB::create();
vector<KeyPoint>xuenai_ObrKp;
Mat BFMmatch_result;Mat FLANNmatch_result;
vector<KeyPoint>xuenai_rect_ObrKp;
Mat xuenai_obr_descriptorsForBF;Mat xuenai_rect_obr_descriptorsForBF;Mat xuenai_obr_descriptorsForFLANN;Mat xuenai_rect_obr_descriptorsForFLANN;
vector<vector<DMatch>>xuenai_BFMmatch_results;vector<vector<DMatch>>xuenai_FLANNmatch_results;
obr->detectAndCompute(xuenai_gray,noArray(),xuenai_ObrKp,xuenai_obr_descriptorsForBF);
obr->detectAndCompute(xuenai_rect_gray,noArray(),xuenai_rect_ObrKp,xuenai_rect_obr_descriptorsForBF);
xuenai_obr_descriptorsForBF.convertTo(xuenai_obr_descriptorsForFLANN,CV_32F);//注意这里不能进行原地运算
xuenai_rect_obr_descriptorsForBF.convertTo(xuenai_rect_obr_descriptorsForFLANN,CV_32F);//注意这里不能进行原地运算
//进行匹配
Ptr<BFMatcher>bfm=BFMatcher::create(NORM_HAMMING);
Ptr<FlannBasedMatcher>flann=FlannBasedMatcher::create();
bfm->knnMatch(xuenai_rect_obr_descriptorsForBF,xuenai_obr_descriptorsForBF,xuenai_BFMmatch_results,2);
flann->knnMatch(xuenai_rect_obr_descriptorsForFLANN,xuenai_obr_descriptorsForFLANN,xuenai_FLANNmatch_results,2);
//比率检验
vector<DMatch>goodBFMresult,goodFLANNresult;
for(auto match_result:xuenai_BFMmatch_results){
if(match_result.size()>1 && match_result[0].distance<0.7*match_result[1].distance ){
goodBFMresult.push_back(match_result[0]);
}
}
for(auto match_result:xuenai_FLANNmatch_results){
if(match_result.size()>1 && match_result[0].distance<0.7*match_result[1].distance ){
goodFLANNresult.push_back(match_result[0]);
}
}
//绘制匹配结果
if(!goodBFMresult.empty()) {
drawMatches(xuenai_rect, xuenai_rect_ObrKp,
xuenai_transform, xuenai_ObrKp,
goodBFMresult, BFMmatch_result,
Scalar::all(-1), Scalar::all(-1), vector<char>(),
DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
imshow("BFMmatch_result",BFMmatch_result);
}
if(!goodFLANNresult.empty()) {
drawMatches(xuenai_rect, xuenai_rect_ObrKp,
xuenai_transform, xuenai_ObrKp,
goodFLANNresult, FLANNmatch_result,
Scalar::all(-1), Scalar::all(-1), vector<char>(),
DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
imshow("FLANNmatch_result",FLANNmatch_result);
}
if (waitKey(0) == 'q')break;
}
不进行比率筛选
进行比率筛选
7.单应性矩阵
7.1 概述
使用最小均方误差或者RANSAC方法,计算多个二维点对之间的最优单映射变换矩阵 H(3行x3列),配合perspectivetransform函数,可以实现对图片的矫正
7.2 API
findHomography
CV_EXPORTS_W Mat findHomography( InputArray srcPoints, InputArray dstPoints,
int method = 0, double ransacReprojThreshold = 3,
OutputArray mask=noArray(), const int maxIters = 2000,
const double confidence = 0.995);
/** @overload */
CV_EXPORTS Mat findHomography( InputArray srcPoints, InputArray dstPoints,
OutputArray mask, int method = 0, double ransacReprojThreshold = 3 );
CV_EXPORTS_W Mat findHomography(InputArray srcPoints, InputArray dstPoints, OutputArray mask,
const UsacParams ¶ms);
- 参数如下
参数 | 含义 |
---|---|
srcPoints | 源平面中点的坐标矩阵,可以是CV_32FC2类型,也可以是vector<Point2f>类型 |
dstPoints | 目标平面中点的坐标矩阵,可以是CV_32FC2类型,也可以是vector<Point2f>类型 |
method | 计算单应矩阵所使用的方法。不同的方法对应不同的参数,参考如下表格。 |
ransacReprojThreshold | 将点对视为内点的最大允许重投影错误阈值(仅用于RANSAC和RHO方法)。若srcPoints和dstPoints是以vector<Point2f>为单位的,则该参数通常设置在1到10的范围内,建议选择5 |
mask | 可选输出掩码矩阵。通常由鲁棒算法(RANSAC或LMEDS)设置。 请注意,输入掩码矩阵是不需要设置的。 |
maxIters | RANSAC算法的最大迭代次数,默认值为2000 |
confidence | 可信度值,取值范围为0到1 |
- method可选值如下
method可选值 | 含义 |
---|---|
0 | 利用所有点的常规方法 |
RANSAC | 基于RANSAC的鲁棒算法 |
LMEDS | 最小中值鲁棒算法 |
RHO | 基于PROSAC的鲁棒算法 |
perspectivetransform
perspectivetransform函数与warpPerspective函数的区别在于:
- 透視変換はベクトルを変換することです<Point2f>
- warpPerspective は Mat を変形することです
CV_EXPORTS_W void perspectiveTransform(InputArray src, OutputArray dst, InputArray m );
7.2 プロセス
- 特徴検出アルゴリズムを通じてキーポイントを検出し、記述子を計算します
- 特徴照合アルゴリズムによる照合結果の取得
- マッチング結果をトラバースし、DMatch.queryIdx メンバー変数と DMatch.trainIdx メンバー変数を使用してクエリ キー ポイント セットとトレーニング キー ポイント セットにそれぞれインデックスを付け、KeyPoint.pt メンバー変数を使用してそれらの座標を取得し、2 つのベクトルを取得します。 <ポイント2f>
- findHomography 関数を呼び出してホモグラフィー行列を取得します。
- 関数perspectivetransformを呼び出してクエリグラフの四隅に対応する座標点セットvector<Point2f>を変換し、トレーニンググラフで見つけたい画像の四隅に対応する座標点セットvector<Point2f>を取得します。
- ポリライン関数を使用して結果をトレーニング グラフにプロットします。
7.3 効果
Mat xuenai = imread("xuenai.jpg");
Mat xuenai_rect = imread("xuenai_rect.jpg");
Mat xuenai_rect_gray;cvtColor(xuenai_rect,xuenai_rect_gray,COLOR_BGR2GRAY);
imshow("xuenai", xuenai);
namedWindow("panel");
createTrackbar("threshold","panel", nullptr,255);
createTrackbar("angle","panel", nullptr,360);
createTrackbar("width","panel", nullptr,1000);
createTrackbar("height","panel", nullptr,1000);
while (true) {
int thres = getTrackbarPos("threshold", "panel");
if(thres==0)thres=100;
int width = getTrackbarPos("width", "panel");
if(width==0)width=xuenai.cols;
int height = getTrackbarPos("height", "panel");
if(height==0)height=xuenai.rows;
int angle = getTrackbarPos("angle","panel");
Mat xuenai_transform=xuenai.clone();
resize(xuenai_transform,xuenai_transform,Size(width,height));
Mat M= getRotationMatrix2D(Point2f((float )xuenai_transform.cols/2,(float )xuenai_transform.rows/2),angle,1);
warpAffine(xuenai_transform,xuenai_transform,M,xuenai_transform.size());
Mat xuenai_gray(xuenai.size(),xuenai.type());
cvtColor(xuenai_transform,xuenai_gray,COLOR_BGR2GRAY);
//准备工作
Ptr<ORB>obr=ORB::create();
vector<KeyPoint>xuenai_ObrKp;
Mat BFMmatch_result;
vector<KeyPoint>xuenai_rect_ObrKp;
Mat xuenai_obr_descriptors;Mat xuenai_rect_obr_descriptors;
vector<vector<DMatch>>xuenai_BFMmatch_results;
obr->detectAndCompute(xuenai_gray,noArray(),xuenai_ObrKp,xuenai_obr_descriptors);
obr->detectAndCompute(xuenai_rect_gray,noArray(),xuenai_rect_ObrKp,xuenai_rect_obr_descriptors);
//进行匹配
Ptr<BFMatcher>bfm=BFMatcher::create(NORM_HAMMING);
bfm->knnMatch(xuenai_rect_obr_descriptors,xuenai_obr_descriptors,xuenai_BFMmatch_results,2);
//比率检验
vector<DMatch>goodBFMresult;
for(auto match_result:xuenai_BFMmatch_results){
if(match_result.size()>1 && match_result[0].distance<0.8*match_result[1].distance ){
goodBFMresult.push_back(match_result[0]);
}
}
//寻找单应性矩阵并绘制
vector<Point2f>srcPoints,dstPoints;
for (auto good_one:goodBFMresult){
srcPoints.push_back(xuenai_rect_ObrKp[good_one.queryIdx].pt);
dstPoints.push_back(xuenai_ObrKp[good_one.trainIdx].pt);
}
if (srcPoints.size()>=4){
Mat homo = findHomography(srcPoints, dstPoints, RANSAC,5);
vector<Point2f> origin_pts = {
Point2f(0, 0), Point2f(0, xuenai_rect.rows - 1),
Point2f(xuenai_rect.cols - 1, xuenai_rect.rows - 1),
Point2f(xuenai_rect.cols - 1, 0),};
vector<Point2f> result_pts;
vector<Point> result_pts_int;
perspectiveTransform(origin_pts, result_pts, homo);
//Point2f转Point
for (auto point: result_pts) {
result_pts_int.push_back(Point(point));
}
polylines(xuenai_transform, result_pts_int, true, Scalar(0, 0, 255));
}
//绘制结果
if(!goodBFMresult.empty()) {
drawMatches(xuenai_rect, xuenai_rect_ObrKp,
xuenai_transform, xuenai_ObrKp,
goodBFMresult, BFMmatch_result,
Scalar::all(-1), Scalar::all(-1), vector<char>(),
DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
imshow("FLANNmatch_result",BFMmatch_result);
}
if (waitKey(0) == 'q')break;
}