SLAM之特徵匹配(一)————RANSAC-------OpenCV中findFundamentalMat函數使用的模型

 

 

隨機抽樣一致性(RANSAC)算法,可以在一組包含“外點”的數據集中,採用不斷迭代的方法,尋找最優參數模型,不符合最優模型的點,被定義爲“外點”。在圖像配准以及拼接上得到廣泛的應用,本文將對RANSAC算法在OpenCV中角點誤匹配對的檢測中進行解析。

 

1.RANSAC原理  

  OpenCV中濾除誤匹配對採用RANSAC算法尋找一個最佳單應性矩陣H,矩陣大小爲3×3。RANSAC目的是找到最優的參數矩陣使得滿足該矩陣的數據點個數最多,通常令h33=1來歸一化矩陣。由於單應性矩陣有8個未知參數,至少需要8個線性方程求解,對應到點位置信息上,一組點對可以列出兩個方程,則至少包含4組匹配點對。

                                                                      其中(x,y)表示目標圖像角點位置,(x',y')爲場景圖像角點位置,s爲尺度參數

  RANSAC算法從匹配數據集中隨機抽出4個樣本並保證這4個樣本之間不共線,計算出單應性矩陣,然後利用這個模型測試所有數據,並計算滿足這個模型數據點的個數與投影誤差(即代價函數),若此模型爲最優模型,則對應的代價函數最小。

-----------------------------------------------------------------------------------------------------------------

RANSAC算法步驟: 

          1. 隨機從數據集中隨機抽出4個樣本數據 (此4個樣本之間不能共線),計算出變換矩陣H,記爲模型M;

          2. 計算數據集中所有數據與模型M的投影誤差,若誤差小於閾值,加入內點集 I ;

          3. 如果當前內點集 I 元素個數大於最優內點集 I_best , 則更新 I_best = I,同時更新迭代次數k ;

          4. 如果迭代次數大於k,則退出 ; 否則迭代次數加1,並重覆上述步驟;

  注:迭代次數k在不大於最大迭代次數的情況下,是在不斷更新而不是固定的;

                                       其中,p爲置信度,一般取0.995;w爲"內點"的比例 ; m爲計算模型所需要的最少樣本數=4;

-----------------------------------------------------------------------------------------------------------------

 

2.例程

OpenCV中此功能通過調用findHomography函數調用,下面是個例程:

 

[cpp] view plain copy
 
 
  1. #include <iostream>  
  2. #include "opencv2/opencv.hpp"  
  3. #include "opencv2/core/core.hpp"  
  4. #include "opencv2/features2d/features2d.hpp"  
  5. #include "opencv2/highgui/highgui.hpp"  
  6. using namespace cv;  
  7. using namespace std;  
  8. int main(int argc, char** argv)  
  9. {  
  10.     Mat obj=imread("F:\\Picture\\obj.jpg");   //載入目標圖像  
  11.     Mat scene=imread("F:\\Picture\\scene.jpg"); //載入場景圖像  
  12.     if (obj.empty() || scene.empty() )  
  13.     {  
  14.         cout<<"Can't open the picture!\n";  
  15.         return 0;  
  16.     }  
  17.     vector<KeyPoint> obj_keypoints,scene_keypoints;  
  18.     Mat obj_descriptors,scene_descriptors;  
  19.     ORB detector;     //採用ORB算法提取特徵點  
  20.     detector.detect(obj,obj_keypoints);  
  21.     detector.detect(scene,scene_keypoints);  
  22.     detector.compute(obj,obj_keypoints,obj_descriptors);  
  23.     detector.compute(scene,scene_keypoints,scene_descriptors);  
  24.     BFMatcher matcher(NORM_HAMMING,true); //漢明距離做爲相似度度量  
  25.     vector<DMatch> matches;  
  26.     matcher.match(obj_descriptors, scene_descriptors, matches);  
  27.     Mat match_img;  
  28.     drawMatches(obj,obj_keypoints,scene,scene_keypoints,matches,match_img);  
  29.     imshow("濾除誤匹配前",match_img);  
  30.   
  31.     //保存匹配對序號  
  32.     vector<int> queryIdxs( matches.size() ), trainIdxs( matches.size() );  
  33.     forsize_t i = 0; i < matches.size(); i++ )  
  34.     {  
  35.         queryIdxs[i] = matches[i].queryIdx;  
  36.         trainIdxs[i] = matches[i].trainIdx;  
  37.     }     
  38.   
  39.     Mat H12;   //變換矩陣  
  40.   
  41.     vector<Point2f> points1; KeyPoint::convert(obj_keypoints, points1, queryIdxs);  
  42.     vector<Point2f> points2; KeyPoint::convert(scene_keypoints, points2, trainIdxs);  
  43.     int ransacReprojThreshold = 5;  //拒絕閾值  
  44.   
  45.   
  46.     H12 = findHomography( Mat(points1), Mat(points2), CV_RANSAC, ransacReprojThreshold );  
  47.     vector<char> matchesMask( matches.size(), 0 );    
  48.     Mat points1t;  
  49.     perspectiveTransform(Mat(points1), points1t, H12);  
  50.     forsize_t i1 = 0; i1 < points1.size(); i1++ )  //保存‘內點’  
  51.     {  
  52.         if( norm(points2[i1] - points1t.at<Point2f>((int)i1,0)) <= ransacReprojThreshold ) //給內點做標記  
  53.         {  
  54.             matchesMask[i1] = 1;  
  55.         }     
  56.     }  
  57.     Mat match_img2;   //濾除‘外點’後  
  58.     drawMatches(obj,obj_keypoints,scene,scene_keypoints,matches,match_img2,Scalar(0,0,255),Scalar::all(-1),matchesMask);  
  59.   
  60.     //畫出目標位置  
  61.     std::vector<Point2f> obj_corners(4);  
  62.     obj_corners[0] = cvPoint(0,0); obj_corners[1] = cvPoint( obj.cols, 0 );  
  63.     obj_corners[2] = cvPoint( obj.cols, obj.rows ); obj_corners[3] = cvPoint( 0, obj.rows );  
  64.     std::vector<Point2f> scene_corners(4);  
  65.     perspectiveTransform( obj_corners, scene_corners, H12);  
  66.     line( match_img2, scene_corners[0] + Point2f(static_cast<float>(obj.cols), 0),   
  67.         scene_corners[1] + Point2f(static_cast<float>(obj.cols), 0),Scalar(0,0,255),2);  
  68.     line( match_img2, scene_corners[1] + Point2f(static_cast<float>(obj.cols), 0),   
  69.         scene_corners[2] + Point2f(static_cast<float>(obj.cols), 0),Scalar(0,0,255),2);  
  70.     line( match_img2, scene_corners[2] + Point2f(static_cast<float>(obj.cols), 0),   
  71.         scene_corners[3] + Point2f(static_cast<float>(obj.cols), 0),Scalar(0,0,255),2);  
  72.     line( match_img2, scene_corners[3] + Point2f(static_cast<float>(obj.cols), 0),  
  73.         scene_corners[0] + Point2f(static_cast<float>(obj.cols), 0),Scalar(0,0,255),2);  
  74.   
  75.     imshow("濾除誤匹配後",match_img2);  
  76.     waitKey(0);  
  77.       
  78.     return 0;  
  79. }  


3. RANSAC源碼解析

 

(1)findHomography內部調用cvFindHomography函數

srcPoints:目標圖像點集

dstPoints:場景圖像點集

method: 最小中值法、RANSAC方法、最小二乘法

ransacReprojTheshold:投影誤差閾值

mask:掩碼

 

[cpp] view plain copy
 
 
  1. cvFindHomography( const CvMat* objectPoints, const CvMat* imagePoints,  
  2.                   CvMat* __H, int method, double ransacReprojThreshold,  
  3.                   CvMat* mask )  
  4. {  
  5.     const double confidence = 0.995;  //置信度  
  6.     const int maxIters = 2000;    //迭代最大次數  
  7.     const double defaultRANSACReprojThreshold = 3; //默認拒絕閾值  
  8.     bool result = false;  
  9.     Ptr<CvMat> m, M, tempMask;  
  10.   
  11.     double H[9];  
  12.     CvMat matH = cvMat( 3, 3, CV_64FC1, H );  //變換矩陣  
  13.     int count;  
  14.   
  15.     CV_Assert( CV_IS_MAT(imagePoints) && CV_IS_MAT(objectPoints) );  
  16.   
  17.     count = MAX(imagePoints->cols, imagePoints->rows);  
  18.     CV_Assert( count >= 4 );           //至少有4個樣本  
  19.     if( ransacReprojThreshold <= 0 )  
  20.         ransacReprojThreshold = defaultRANSACReprojThreshold;  
  21.   
  22.     m = cvCreateMat( 1, count, CV_64FC2 );  //轉換爲齊次座標  
  23.     cvConvertPointsHomogeneous( imagePoints, m );  
  24.   
  25.     M = cvCreateMat( 1, count, CV_64FC2 );//轉換爲齊次座標  
  26.     cvConvertPointsHomogeneous( objectPoints, M );  
  27.   
  28.     if( mask )  
  29.     {  
  30.         CV_Assert( CV_IS_MASK_ARR(mask) && CV_IS_MAT_CONT(mask->type) &&  
  31.             (mask->rows == 1 || mask->cols == 1) &&  
  32.             mask->rows*mask->cols == count );  
  33.     }  
  34.     if( mask || count > 4 )  
  35.         tempMask = cvCreateMat( 1, count, CV_8U );  
  36.     if( !tempMask.empty() )  
  37.         cvSet( tempMask, cvScalarAll(1.) );  
  38.   
  39.     CvHomographyEstimator estimator(4);  
  40.     if( count == 4 )  
  41.         method = 0;  
  42.     if( method == CV_LMEDS )  //最小中值法  
  43.         result = estimator.runLMeDS( M, m, &matH, tempMask, confidence, maxIters );  
  44.     else if( method == CV_RANSAC )    //採用RANSAC算法  
  45.         result = estimator.runRANSAC( M, m, &matH, tempMask, ransacReprojThreshold, confidence, maxIters);//(2)解析  
  46.     else  
  47.         result = estimator.runKernel( M, m, &matH ) > 0; //最小二乘法  
  48.   
  49.     if( result && count > 4 )  
  50.     {  
  51.         icvCompressPoints( (CvPoint2D64f*)M->data.ptr, tempMask->data.ptr, 1, count );  //保存內點集  
  52.         count = icvCompressPoints( (CvPoint2D64f*)m->data.ptr, tempMask->data.ptr, 1, count );  
  53.         M->cols = m->cols = count;  
  54.         if( method == CV_RANSAC )  //  
  55.             estimator.runKernel( M, m, &matH );  //內點集上採用最小二乘法重新估算變換矩陣  
  56.         estimator.refine( M, m, &matH, 10 );//   
  57.     }  
  58.   
  59.     if( result )  
  60.         cvConvert( &matH, __H );  //保存變換矩陣  
  61.   
  62.     if( mask && tempMask )  
  63.     {  
  64.         if( CV_ARE_SIZES_EQ(mask, tempMask) )  
  65.            cvCopy( tempMask, mask );  
  66.         else  
  67.            cvTranspose( tempMask, mask );  
  68.     }  
  69.   
  70.     return (int)result;  
  71. }  

 

(2) runRANSAC
maxIters:最大迭代次數

m1,m2 :數據樣本

model:變換矩陣

reprojThreshold:投影誤差閾值

confidence:置信度  0.995

 

[cpp] view plain copy
 
 
  1. bool CvModelEstimator2::runRANSAC( const CvMat* m1, const CvMat* m2, CvMat* model,  
  2.                                     CvMat* mask0, double reprojThreshold,  
  3.                                     double confidence, int maxIters )  
  4. {  
  5.     bool result = false;  
  6.     cv::Ptr<CvMat> mask = cvCloneMat(mask0);  
  7.     cv::Ptr<CvMat> models, err, tmask;  
  8.     cv::Ptr<CvMat> ms1, ms2;  
  9.   
  10.     int iter, niters = maxIters;  
  11.     int count = m1->rows*m1->cols, maxGoodCount = 0;  
  12.     CV_Assert( CV_ARE_SIZES_EQ(m1, m2) && CV_ARE_SIZES_EQ(m1, mask) );  
  13.   
  14.     if( count < modelPoints )  
  15.         return false;  
  16.   
  17.     models = cvCreateMat( modelSize.height*maxBasicSolutions, modelSize.width, CV_64FC1 );  
  18.     err = cvCreateMat( 1, count, CV_32FC1 );//保存每組點對應的投影誤差  
  19.     tmask = cvCreateMat( 1, count, CV_8UC1 );  
  20.   
  21.     if( count > modelPoints )  //多於4個點  
  22.     {  
  23.         ms1 = cvCreateMat( 1, modelPoints, m1->type );  
  24.         ms2 = cvCreateMat( 1, modelPoints, m2->type );  
  25.     }  
  26.     else  //等於4個點  
  27.     {  
  28.         niters = 1; //迭代一次  
  29.         ms1 = cvCloneMat(m1);  //保存每次隨機找到的樣本點  
  30.         ms2 = cvCloneMat(m2);  
  31.     }  
  32.   
  33.     for( iter = 0; iter < niters; iter++ )  //不斷迭代  
  34.     {  
  35.         int i, goodCount, nmodels;  
  36.         if( count > modelPoints )  
  37.         {  
  38.            //在(3)解析getSubset  
  39.             bool found = getSubset( m1, m2, ms1, ms2, 300 ); //隨機選擇4組點,且三點之間不共線,(3)解析  
  40.             if( !found )  
  41.             {  
  42.                 if( iter == 0 )  
  43.                     return false;  
  44.                 break;  
  45.             }  
  46.         }  
  47.   
  48.         nmodels = runKernel( ms1, ms2, models );  //估算近似變換矩陣,返回1  
  49.         if( nmodels <= 0 )  
  50.             continue;  
  51.         for( i = 0; i < nmodels; i++ )//執行一次   
  52.         {  
  53.             CvMat model_i;  
  54.             cvGetRows( models, &model_i, i*modelSize.height, (i+1)*modelSize.height );//獲取3×3矩陣元素  
  55.             goodCount = findInliers( m1, m2, &model_i, err, tmask, reprojThreshold );  //找出內點,(4)解析  
  56.   
  57.             if( goodCount > MAX(maxGoodCount, modelPoints-1) )  //當前內點集元素個數大於最優內點集元素個數  
  58.             {  
  59.                 std::swap(tmask, mask);  
  60.                 cvCopy( &model_i, model );  //更新最優模型  
  61.                 maxGoodCount = goodCount;  
  62.                 //confidence 爲置信度默認0.995,modelPoints爲最少所需樣本數=4,niters迭代次數  
  63.                 niters = cvRANSACUpdateNumIters( confidence,  //更新迭代次數,(5)解析  
  64.                     (double)(count - goodCount)/count, modelPoints, niters );  
  65.             }  
  66.         }  
  67.     }  
  68.   
  69.     if( maxGoodCount > 0 )  
  70.     {  
  71.         if( mask != mask0 )  
  72.             cvCopy( mask, mask0 );  
  73.         result = true;  
  74.     }  
  75.   
  76.     return result;  
  77. }  

 

step one niters最初的值爲2000,這就是初始時的RANSAC算法的循環次數,getSubset()函數是從一組對應的序列中隨機的選出4組(因爲要想計算出一個3X3的矩陣,至少需要4組對應的座標),m1和m2是我們輸入序列,ms1和ms2是隨機選出的對應的4組匹配。

 隨機的選出4組匹配後,就應該根據這4個匹配計算相應的矩陣,所以函數runKernel()就是根據4組匹配計算矩陣,參數裏的models就是得到的矩陣。

stept wo 這個矩陣只是一個假設,爲了驗證這個假設,需要用其他的點去計算,看看其他的點是內點還是外點。

findInliers()函數就是用來計算內點的。利用前面得到的矩陣,把所有的序列帶入,計算得出哪些是內點,哪些是外點,函數的返回值爲goodCount,就是此次計算的內點的個數。

step three  函數中還有一個值爲maxGoodCount,每次循環的內點個數的最大值保存在這個值中,一個估計的矩陣如果有越多的內點,那麼這個矩陣就越有可能是正確的。

step four   所以計算內點個數以後,緊接着判斷一下goodCount和maxGoodCount的大小關係,如果goodCount>maxGoodCount,則把goodCount賦值給maxGoodCount。賦值之後的一行代碼非常關鍵,我們單獨拿出來說一下,代碼如下:

 

 

  1. niters = cvRANSACUpdateNumIters( confidence,  
  2.                    (double)(count - goodCount)/count, modelPoints, niters );  
  3.  

niters本來是迭代的次數,也就是循環的次數。但是通過這行代碼我們發現,每次循環後,都會對niters這個值進行更新,也就是每次循環後都會改變循環的總次數。

cvRANSACUpdateNumIters()函數利用confidence(置信度)count(總匹配個數)goodCount(當前內點個數)niters(當前的總迭代次數)這幾個參數,來動態的改變總迭代次數的大小。該函數的中心思想就是當內點佔的比例較多時,那麼很有可能已經找到了正確的估計,所以就適當的減少迭代次數來節省時間。這個迭代次數的減少是以指數形式減少的,所以節省的時間開銷也是非常的可觀。因此最初設計的2000的迭代次數,可能最終的迭代次數只有幾十。同樣的,如果你自己一開始把迭代次數設置成10000或者更大,進過幾次迭代後,niters又會變得非常小了。所以初始時的niters設置的再大,其實對最終的運行時間也沒什麼影響。

所以,們現在應該清楚爲什麼輸入數據增加,而算法運行時間不會增加了。openCV的RANSAC算法首先把迭代的次數設置爲2000,然後再迭代的過程中,動態的改變總迭代次數,無論輸入數據有多少,總的迭代次數不會增加,並且通過4個匹配計算出估計的矩陣這個時間是不變的,通過估計矩陣來計算內點,這方面的增加的時間開銷基本上可以忽略。所以導致的最終結果就是,無論輸入點有多少,運算時間基本不會有太大變化。

 

以上就是RANSAC算法的核心代碼,其中用到的一些函數,下面一一給出。

 

(3)getSubset

 

ms1,ms2:保存隨機找到的4個樣本

maxAttempts:最大迭代次數,300

 

[cpp] view plain copy
 
 
  1. bool CvModelEstimator2::getSubset( const CvMat* m1, const CvMat* m2,  
  2.                                    CvMat* ms1, CvMat* ms2, int maxAttempts )  
  3. {  
  4.     cv::AutoBuffer<int> _idx(modelPoints); //modelPoints 所需要最少的樣本點個數  
  5.     int* idx = _idx;  
  6.     int i = 0, j, k, idx_i, iters = 0;  
  7.     int type = CV_MAT_TYPE(m1->type), elemSize = CV_ELEM_SIZE(type);  
  8.     const int *m1ptr = m1->data.i, *m2ptr = m2->data.i;  
  9.     int *ms1ptr = ms1->data.i, *ms2ptr = ms2->data.i;  
  10.     int count = m1->cols*m1->rows;  
  11.   
  12.     assert( CV_IS_MAT_CONT(m1->type & m2->type) && (elemSize % sizeof(int) == 0) );  
  13.     elemSize /= sizeof(int); //每個數據佔用字節數  
  14.   
  15.     for(; iters < maxAttempts; iters++)  
  16.     {  
  17.         for( i = 0; i < modelPoints && iters < maxAttempts; )  
  18.         {  
  19.             idx[i] = idx_i = cvRandInt(&rng) % count;  // 隨機選取1組點  
  20.             for( j = 0; j < i; j++ )  // 檢測是否重複選擇  
  21.                 if( idx_i == idx[j] )  
  22.                     break;  
  23.             if( j < i )  
  24.                 continue;  //重新選擇  
  25.             for( k = 0; k < elemSize; k++ )  //拷貝點數據  
  26.             {  
  27.                 ms1ptr[i*elemSize + k] = m1ptr[idx_i*elemSize + k];  
  28.                 ms2ptr[i*elemSize + k] = m2ptr[idx_i*elemSize + k];  
  29.             }  
  30.             if( checkPartialSubsets && (!checkSubset( ms1, i+1 ) || !checkSubset( ms2, i+1 )))//檢測點之間是否共線  
  31.             {  
  32.                 iters++;  //若共線,重新選擇一組  
  33.                 continue;  
  34.             }  
  35.             i++;  
  36.         }  
  37.         if( !checkPartialSubsets && i == modelPoints &&  
  38.             (!checkSubset( ms1, i ) || !checkSubset( ms2, i )))  
  39.             continue;  
  40.         break;  
  41.     }  
  42.   
  43.     return i == modelPoints && iters < maxAttempts;  //返回找到的樣本點個數  
  44. }  

 

(4) findInliers & computerReprojError

 

[cpp] view plain copy
 
 
  1. int CvModelEstimator2::findInliers( const CvMat* m1, const CvMat* m2,  
  2.                                     const CvMat* model, CvMat* _err,  
  3.                                     CvMat* _mask, double threshold )  
  4. {  
  5.     int i, count = _err->rows*_err->cols, goodCount = 0;  
  6.     const float* err = _err->data.fl;  
  7.     uchar* mask = _mask->data.ptr;  
  8.   
  9.     computeReprojError( m1, m2, model, _err );  //計算每組點的投影誤差  
  10.     threshold *= threshold;  
  11.     for( i = 0; i < count; i++ )  
  12.         goodCount += mask[i] = err[i] <= threshold;//誤差在限定範圍內,加入‘內點集’  
  13.     return goodCount;  
  14. }  
  15. //--------------------------------------  
  16. void CvHomographyEstimator::computeReprojError( const CvMat* m1, const CvMat* m2,const CvMat* model, CvMat* _err )  
  17. {  
  18.     int i, count = m1->rows*m1->cols;  
  19.     const CvPoint2D64f* M = (const CvPoint2D64f*)m1->data.ptr;  
  20.     const CvPoint2D64f* m = (const CvPoint2D64f*)m2->data.ptr;  
  21.     const double* H = model->data.db;  
  22.     float* err = _err->data.fl;  
  23.   
  24.     for( i = 0; i < count; i++ )        //保存每組點的投影誤差,對應上述變換公式  
  25.     {  
  26.         double ww = 1./(H[6]*M[i].x + H[7]*M[i].y + 1.);      
  27.         double dx = (H[0]*M[i].x + H[1]*M[i].y + H[2])*ww - m[i].x;  
  28.         double dy = (H[3]*M[i].x + H[4]*M[i].y + H[5])*ww - m[i].y;  
  29.         err[i] = (float)(dx*dx + dy*dy);  
  30.     }  
  31. }  

(5)cvRANSACUpdateNumIters

 

對應上述k的計算公式

p:置信度

ep:外點比例

 

[cpp] view plain copy
 
 
  1. cvRANSACUpdateNumIters( double p, double ep,  
  2.                         int model_points, int max_iters )  
  3. {  
  4.     if( model_points <= 0 )  
  5.         CV_Error( CV_StsOutOfRange, "the number of model points should be positive" );  
  6.   
  7.     p = MAX(p, 0.);  
  8.     p = MIN(p, 1.);  
  9.     ep = MAX(ep, 0.);  
  10.     ep = MIN(ep, 1.);  
  11.   
  12.     // avoid inf's & nan's  
  13.     double num = MAX(1. - p, DBL_MIN);  //num=1-p,做分子  
  14.     double denom = 1. - pow(1. - ep,model_points);//做分母  
  15.     if( denom < DBL_MIN )  
  16.         return 0;  
  17.   
  18.     num = log(num);  
  19.     denom = log(denom);  
  20.   
  21.     return denom >= 0 || -num >= max_iters*(-denom) ?  
  22.         max_iters : cvRound(num/denom);  
  23. }  
  24.  

參考https://blog.csdn.net/laobai1015/article/details/51683076

cv::findFundamentalMat

在處理立體圖像對的時候經常會用到對極幾何的知識,計算基礎矩陣也是很常見的事。OpenCV實現了基本矩陣的算法。對於老版本的C代碼,計算基本矩陣的RANSAC方法中有一個迭代次數不收斂的bug,可能導致每次計算的採樣次數都達到最大限制的次數,其根源在於計算採樣可信度的公式有誤,新版本的代碼還沒有仔細看過,不敢確定是否已經修正了這個bug。但是這個bug並不會對最後的結果造成多大影響,只是會影響計算的效率——原本幾次採樣即可結束迭代,在這個bug的影響下可能要採樣數百次。

    新版本的計算基本矩陣的函數的使用也有一些問題,下面來看看cv::findFundamentalMat函數:

//! the algorithm for finding fundamental matrix
enum
{
    FM_7POINT = CV_FM_7POINT, //!< 7-point algorithm
    FM_8POINT = CV_FM_8POINT, //!< 8-point algorithm
    FM_LMEDS = CV_FM_LMEDS,  //!< least-median algorithm
    FM_RANSAC = CV_FM_RANSAC  //!< RANSAC algorithm
};

//! finds fundamental matrix from a set of corresponding 2D points
CV_EXPORTS Mat findFundamentalMat( const Mat& points1, const Mat& points2,
                                     CV_OUT vector<uchar>& mask, int method=FM_RANSAC,
                                     double param1=3., double param2=0.99 );

//! finds fundamental matrix from a set of corresponding 2D points
CV_EXPORTS_W Mat findFundamentalMat( const Mat& points1, const Mat& points2,
                                     int method=FM_RANSAC,
                                     double param1=3., double param2=0.99 );

 

    上面是OpenCV計算基本矩陣的C++接口,其內部實現仍然是調用的C代碼函數,僅僅是在上層進行了一次封裝。網上有些文檔中說const Mat& points1和points2這兩個參數可以直接由vector<Point2f>類型作爲傳入參數,其實這是錯誤的。直接用vector<Point2f>傳遞,在編譯時可能不會報錯,但是運行時會直接崩潰。因爲cv::Mat的構造函數並不會把Point2f轉化爲兩個浮點數存於Mat的兩個元素中,而是仍然以Point2f存於Mat的一個元素中,於是findFundamentalMat一讀取這個Mat就會出錯。

    因此我們最好老老實實地去構建Mat points1和Mat points2。該矩陣的維度可以是2xN,也可以是Nx2的,其中N是特徵點的數目。另一個要注意的地方就是該矩陣的類型不能是CV_64F,也就是說findFundamentalMat內部解析points1和points2時是按照float類型去解析的,設爲CV_64F將導致讀取數據失敗,程序崩潰。最好設爲CV_32F。下面是從匹配好的特徵點計算F的代碼示例:

 

// vector<KeyPoint> m_LeftKey;
// vector<KeyPoint> m_RightKey;

// vector<DMatch> m_Matches;

// 以上三個變量已經被計算出來,分別是提取的關鍵點及其匹配,下面直接計算F

 

// 分配空間

int ptCount = (int)m_Matches.size();
Mat p1(ptCount, 2, CV_32F);
Mat p2(ptCount, 2, CV_32F);

 

// 把Keypoint轉換爲Mat

Point2f pt;
for (int i=0; i<ptCount; i++)
{
     pt = m_LeftKey[m_Matches[i].queryIdx].pt;
     p1.at<float>(i, 0) = pt.x;
     p1.at<float>(i, 1) = pt.y;
  
     pt = m_RightKey[m_Matches[i].trainIdx].pt;
     p2.at<float>(i, 0) = pt.x;
     p2.at<float>(i, 1) = pt.y;
}

 

// 用RANSAC方法計算F

// Mat m_Fundamental;

// 上面這個變量是基本矩陣

// vector<uchar> m_RANSACStatus;

// 上面這個變量已經定義過,用於存儲RANSAC後每個點的狀態

m_Fundamental = findFundamentalMat(p1, p2, m_RANSACStatus, FM_RANSAC);

 

// 計算野點個數

int OutlinerCount = 0;
for (int i=0; i<ptCount; i++)
{
     if (m_RANSACStatus[i] == 0) // 狀態爲0表示野點
     {
          OutlinerCount++;
     }
}

 

// 計算內點

// vector<Point2f> m_LeftInlier;
// vector<Point2f> m_RightInlier;
// vector<DMatch> m_InlierMatches;

// 上面三個變量用於保存內點和匹配關係

int InlinerCount = ptCount - OutlinerCount;
m_InlierMatches.resize(InlinerCount);
m_LeftInlier.resize(InlinerCount);
m_RightInlier.resize(InlinerCount);
InlinerCount = 0;
for (int i=0; i<ptCount; i++)
{
     if (m_RANSACStatus[i] != 0)
     {
          m_LeftInlier[InlinerCount].x = p1.at<float>(i, 0);
          m_LeftInlier[InlinerCount].y = p1.at<float>(i, 1);
          m_RightInlier[InlinerCount].x = p2.at<float>(i, 0);
          m_RightInlier[InlinerCount].y = p2.at<float>(i, 1);
          m_InlierMatches[InlinerCount].queryIdx = InlinerCount;
          m_InlierMatches[InlinerCount].trainIdx = InlinerCount;
          InlinerCount++;
     }
}

 

// 把內點轉換爲drawMatches可以使用的格式

vector<KeyPoint> key1(InlinerCount);
vector<KeyPoint> key2(InlinerCount);
KeyPoint::convert(m_LeftInlier, key1);
KeyPoint::convert(m_RightInlier, key2);

 

// 顯示計算F過後的內點匹配

// Mat m_matLeftImage;
// Mat m_matRightImage;

// 以上兩個變量保存的是左右兩幅圖像

Mat OutImage;
drawMatches(m_matLeftImage, key1, m_matRightImage, key2, m_InlierMatches, OutImage);
cvNamedWindow( "Match features", 1);
cvShowImage("Match features", &(IplImage(OutImage)));
cvWaitKey( 0 );
cvDestroyWindow( "Match features" );

 

針孔相機模型和變形

 

 

這一節裏的函數都使用針孔攝像機模型,這就是說,一幅視圖是通過透視變換將三維空間中的點投影到圖像平面。投影公式如下:

s \cdot m' = A\cdot[R|t] \cdot M'或者

s\cdot  \begin{bmatrix} u \\ v \\ 1 \end{bmatrix} = \begin{bmatrix} fx & 0 & cx \\ 0 & fy & cy \\ 0 & 0 & 1 \end{bmatrix} \cdot \begin{bmatrix} r_{11} & r_{12} & r_{13} & t_{1} \\ r_{21} & r_{22} & r_{23} & t_{2} \\ r_{31} & r_{32} & r_{33} & t_{3} \end{bmatrix} \cdot \begin{bmatrix} X \\ Y \\ Z \\ 1 \end{bmatrix}

這裏(X, Y, Z)是一個點的世界座標,(u, v)是點投影在圖像平面的座標,以像素爲單位。A被稱作攝像機矩陣,或者內參數矩陣。(cx, cy)是基準點(通常在圖像的中心),fx, fy是以像素爲單位的焦距。所以如果因爲某些因素對來自於攝像機的一幅圖像升採樣或者降採樣,所有這些參數(fx, fy, cx和cy)都將被縮放(乘或者除)同樣的尺度。內參數矩陣不依賴場景的視圖,一旦計算出,可以被重複使用(只要焦距固定)。旋轉-平移矩陣[R|t]被稱作外參數矩陣,它用來描述相機相對於一個固定場景的運動,或者相反,物體圍繞相機的的剛性運動。也就是[R|t]將點(X, Y, Z)的座標變換到某個座標系,這個座標系相對於攝像機來說是固定不變的。上面的變換等價與下面的形式(z≠0):

\begin{bmatrix}x \\ y \\z \end{bmatrix} = R \cdot \begin{bmatrix}X \\ Y \\Z \end{bmatrix} + t

x' = x / z

y' = y / z

u=fx \cdot x' + cx

v=fy \cdot y' + cy

真正的鏡頭通常有一些形變,主要的變形爲徑向形變,也會有輕微的切向形變。所以上面的模型可以擴展爲:

\begin{bmatrix}x \\ y \\z \end{bmatrix} = R \cdot \begin{bmatrix}X \\ Y \\Z \end{bmatrix} + t

x' = x / z

y' = y / z

x'' = x' \cdot (1 + k_1 \cdot r^2 + k_2 \cdot r^4) + 2 \cdot p_1 \cdot x'\cdot y' + p_2 \cdot (r^2+2x'^2)

y'' = y' \cdot (1 + k_1 \cdot r^2 + k_2 \cdot r^4) + p_1 \cdot (r^2+2 \cdot y'^2) + 2 \cdot p_2 \cdot x'\cdot y'

這裏 r2 = x'2 + y'2

u = fx \cdot x'' + cx

v = fy \cdot y'' + cy

k1和k2是徑向形變係數,p1和p1是切向形變係數。OpenCV中沒有考慮高階係數。形變係數跟拍攝的場景無關,因此它們是內參數,而且與拍攝圖像的分辨率無關。

後面的函數使用上面提到的模型來做如下事情:

  • 給定內參數和外參數,投影三維點到圖像平面。
  • 給定內參數、幾個三維點座標和其對應的圖像座標,來計算外參數。
  • 根據已知的定標模式,從幾個角度(每個角度都有幾個對應好的3D-2D點對)的照片來計算相機的外參數和內參數。


照相機定標

ProjectPoints2

投影三維點到圖像平面

void cvProjectPoints2( const CvMat* object_points, const CvMat* rotation_vector,
                       const CvMat* translation_vector, const CvMat* intrinsic_matrix,
                       const CvMat* distortion_coeffs, CvMat* image_points,
                       CvMat* dpdrot=NULL, CvMat* dpdt=NULL, CvMat* dpdf=NULL,
                       CvMat* dpdc=NULL, CvMat* dpddist=NULL );

object_points

物體點的座標,爲3xN或者Nx3的矩陣,這兒N是視圖中的所有所有點的數目。

rotation_vector

旋轉向量,1x3或者3x1。

translation_vector

平移向量,1x3或者3x1。

intrinsic_matrix

攝像機內參數矩陣A:\begin{bmatrix}fx & 0 & cx\\ 0 & fy & cy\\ 0&0&1\end{bmatrix}

distortion_coeffs

形變參數向量,4x1或者1x4,爲[k1,k2,p1,p2]。如果是NULL,所有形變係數都設爲0。

image_points

輸出數組,存儲圖像點座標。大小爲2xN或者Nx2,這兒N是視圖中的所有點的數目。

dpdrot

可選參數,關於旋轉向量部分的圖像上點的導數,Nx3矩陣。

dpdt

可選參數,關於平移向量部分的圖像上點的導數,Nx3矩陣。

dpdf

可選參數,關於fx和fy的圖像上點的導數,Nx2矩陣。

dpdc

可選參數,關於cx和cy的圖像上點的導數,Nx2矩陣。

dpddist

可選參數,關於形變係數的圖像上點的導數,Nx4矩陣。

函數cvProjectPoints2通過給定的內參數和外參數計算三維點投影到二維圖像平面上的座標。另外,這個函數可以計算關於投影參數的圖像點偏導數的雅可比矩陣。雅可比矩陣可以用在cvCalibrateCamera2和cvFindExtrinsicCameraParams2函數的全局優化中。這個函數也可以用來計算內參數和外參數的反投影誤差。注意,將內參數和(或)外參數設置爲特定值,這個函數可以用來計算外變換(或內變換)。

 

FindHomography

 


FindHomography

計算兩個平面之間的透視變換

void cvFindHomography( const CvMat* src_points,
                       const CvMat* dst_points,
                       CvMat* homography );
src_points
原始平面的點座標,大小爲2xN,Nx2,3xN或者 Nx3矩陣(後兩個表示齊次座標),這兒N表示點的數目。

dst_points

目標平面的點座標大小爲2xN,Nx2,3xN或者 Nx3矩陣(後兩個表示齊次座標)。

homography

輸出的3x3的homography矩陣。

函數cvFindHomography計算源平面和目標平面之間的透視變換H=\begin{bmatrix}h_{ij}\end{bmatrix}_{i,j}.

s_i \begin{bmatrix}x'_i \\ y'_i \\ 1\end{bmatrix}  \approx  H  \begin{bmatrix}x_i \\ y_i \\ 1\end{bmatrix}

使得反投影錯誤最小:

\sum_i((x'_i-\frac{h_{11}x_i + h_{12}y_i + h_{13}}{h_{31}x_i + h_{32}y_i + h_{33}})^2+          (y'_i-\frac{h_{21}x_i + h_{22}y_i + h_{23}}{h_{31}x_i + h_{32}y_i + h_{33}})^2)

這個函數可以用來計算初始的內參數和外參數矩陣。由於Homography矩陣的尺度可變,所以它被規一化使得h33= 1

 

CalibrateCamera2

利用定標來計算攝像機的內參數和外參數

void cvCalibrateCamera2( const CvMat* object_points, const CvMat* image_points,
                         const CvMat* point_counts, CvSize image_size,
                         CvMat* intrinsic_matrix, CvMat* distortion_coeffs,
                         CvMat* rotation_vectors=NULL,
                         CvMat* translation_vectors=NULL,
                         int flags=0 );

object_points

定標點的世界座標,爲3xN或者Nx3的矩陣,這裏N是所有視圖中點的總數。

image_points

定標點的圖像座標,爲2xN或者Nx2的矩陣,這裏N是所有視圖中點的總數。

point_counts

向量,指定不同視圖裏點的數目,1xM或者Mx1向量,M是視圖數目。

image_size

圖像大小,只用在初始化內參數時。

intrinsic_matrix

輸出內參矩陣(A) \begin{bmatrix}fx & 0 & cx\\ 0 & fy & cy \\ 0&0&1\end{bmatrix},如果指定CV_CALIB_USE_INTRINSIC_GUESS和(或)CV_CALIB_FIX_ASPECT_RATION,fx、 fy、 cx和cy部分或者全部必須被初始化。

distortion_coeffs

輸出大小爲4x1或者1x4的向量,裏面爲形變參數[k1, k2, p1, p2]。

rotation_vectors

輸出大小爲3xM或者Mx3的矩陣,裏面爲旋轉向量(旋轉矩陣的緊湊表示方式,具體參考函數cvRodrigues2)

translation_vectors

輸出大小爲3xM或Mx3的矩陣,裏面爲平移向量。

flags

不同的標誌,可以是0,或者下面值的組合:

  • CV_CALIB_USE_INTRINSIC_GUESS - 內參數矩陣包含fx,fy,cx和cy的初始值。否則,(cx, cy)被初始化到圖像中心(這兒用到圖像大小),焦距用最小平方差方式計算得到。注意,如果內部參數已知,沒有必要使用這個函數,使用cvFindExtrinsicCameraParams2則可。
  • CV_CALIB_FIX_PRINCIPAL_POINT - 主點在全局優化過程中不變,一直在中心位置或者在其他指定的位置(當CV_CALIB_USE_INTRINSIC_GUESS設置的時候)。
  • CV_CALIB_FIX_ASPECT_RATIO - 優化過程中認爲fx和fy中只有一個獨立變量,保持比例fx/fy不變,fx/fy的值跟內參數矩陣初始化時的值一樣。在這種情況下, (fx, fy)的實際初始值或者從輸入內存矩陣中讀取(當CV_CALIB_USE_INTRINSIC_GUESS被指定時),或者採用估計值(後者情況中fx和fy可能被設置爲任意值,只有比值被使用)。
  • CV_CALIB_ZERO_TANGENT_DIST – 切向形變參數(p1, p2)被設置爲0,其值在優化過程中保持爲0。

函數cvCalibrateCamera2從每個視圖中估計相機的內參數和外參數。3維物體上的點和它們對應的在每個視圖的2維投影必須被指定。這些可以通過使用一個已知幾何形狀且具有容易檢測的特徵點的物體來實現。這樣的一個物體被稱作定標設備或者定標模式,OpenCV有內建的把棋盤當作定標設備方法(參考cvFindChessboardCorners)。目前,傳入初始化的內參數(當CV_CALIB_USE_INTRINSIC_GUESS不被設置時)只支持平面定標設備(物體點的Z座標必須爲全0或者全1)。不過3維定標設備依然可以用在提供初始內參數矩陣情況。在內參數和外參數矩陣的初始值都計算出之後,它們會被優化用來減小反投影誤差(圖像上的實際座標跟cvProjectPoints2計算出的圖像座標的差的平方和)。

FindExtrinsicCameraParams2

計算指定視圖的攝像機外參數

 

void cvFindExtrinsicCameraParams2( const CvMat* object_points,
                                   const CvMat* image_points,
                                   const CvMat* intrinsic_matrix,
                                   const CvMat* distortion_coeffs,
                                   CvMat* rotation_vector,
                                   CvMat* translation_vector );

object_points

定標點的座標,爲3xN或者Nx3的矩陣,這裏N是視圖中的個數。

image_points

定標點在圖像內的座標,爲2xN或者Nx2的矩陣,這裏N是視圖中的個數。

intrinsic_matrix

內參矩陣(A) \begin{bmatrix}fx & 0 & cx\\ 0 & fy & cy \\ 0&0&1\end{bmatrix}

distortion_coeffs

大小爲4x1或者1x4的向量,裏面爲形變參數[k1,k2,p1,p2]。如果是NULL,所有的形變係數都爲0。

rotation_vector

輸出大小爲3x1或者1x3的矩陣,裏面爲旋轉向量(旋轉矩陣的緊湊表示方式,具體參考函數cvRodrigues2)。

translation_vector

大小爲3x1或1x3的矩陣,裏面爲平移向量。

 

函數cvFindExtrinsicCameraParams2使用已知的內參數和某個視圖的外參數來估計相機的外參數。3維物體上的點座標和相應的2維投影必須被指定。這個函數也可以用來最小化反投影誤差。

 

Rodrigues2

進行旋轉矩陣和旋轉向量間的轉換

int  cvRodrigues2( const CvMat* src, CvMat* dst, CvMat* jacobian=0 );

src

輸入的旋轉向量(3x1或者1x3)或者旋轉矩陣(3x3)。

dst

輸出的旋轉矩陣(3x3)或者旋轉向量(3x1或者1x3)

jacobian

可選的輸出雅可比矩陣(3x9或者9x3),關於輸入部分的輸出數組的偏導數。

函數轉換旋轉向量到旋轉矩陣,或者相反。旋轉向量是旋轉矩陣的緊湊表示形式。旋轉向量的方向是旋轉軸,向量的長度是圍繞旋轉軸的旋轉角。旋轉矩陣R,與其對應的旋轉向量r,通過下面公式轉換:

\theta \leftarrow norm(r)

r \leftarrow r/\theta

R = \cos(\theta)I + (1-\cos(\theta))rr^T + \sin(\theta) \begin{bmatrix}0&-r_z&r_y\\ r_z&0&-r_x\\ -r_y&r_x&0\end{bmatrix}

反變換也可以很容易的通過如下公式實現:

\sin(\theta) \begin{bmatrix}0&-r_z&r_y\\ r_z&0&-r_x\\ -r_y&r_x&0\end{bmatrix} = \frac{R-R^T}{2}

旋轉向量是隻有3個自由度的旋轉矩陣一個方便的表示,這種表示方式被用在函數cvFindExtrinsicCameraParams2和cvCalibrateCamera2內部的全局最優化中。

 

Undistort2

校正圖像因相機鏡頭引起的變形

void cvUndistort2( const CvArr* src, CvArr* dst,
                   const CvMat* intrinsic_matrix,
                   const CvMat* distortion_coeffs );

src

原始圖像(已經變形的圖像)。只能變換32fC1的圖像。

dst

結果圖像(已經校正的圖像)。

intrinsic_matrix

相機內參數矩陣,格式爲 \begin{bmatrix}fx & 0 & cx\\ 0 & fy & cy\\ 0&0&1\end{bmatrix}

distortion_coeffs

四個變形係數組成的向量,大小爲4x1或者1x4,格式爲[k1,k2,p1,p2]。

函數cvUndistort2對圖像進行變換來抵消徑向和切向鏡頭變形。相機參數和變形參數可以通過函數cvCalibrateCamera2取得。使用本節開始時提到的公式,對每個輸出圖像像素計算其在輸入圖像中的位置,然後輸出圖像的像素值通過雙線性插值來計算。如果圖像得分辨率跟定標時用得圖像分辨率不一樣,fx、fy、cx和cy需要相應調整,因爲形變並沒有變化。

 

InitUndistortMap

計算形變和非形變圖像的對應(map)

void cvInitUndistortMap( const CvMat* intrinsic_matrix,
                         const CvMat* distortion_coeffs,
                         CvArr* mapx, CvArr* mapy );

intrinsic_matrix

攝像機內參數矩陣(A) [fx 0 cx; 0 fy cy; 0 0 1].

distortion_coeffs

形變係數向量[k1, k2, p1, p2],大小爲4x1或者1x4。

mapx

x座標的對應矩陣。

mapy

y座標的對應矩陣。

函數cvInitUndistortMap預先計算非形變對應-正確圖像的每個像素在形變圖像裏的座標。這個對應可以傳遞給cvRemap函數(跟輸入和輸出圖像一起)。

 

FindChessboardCorners

尋找棋盤圖的內角點位置

int cvFindChessboardCorners( const void* image, CvSize pattern_size,
                             CvPoint2D32f* corners, int* corner_count=NULL,
                             int flags=CV_CALIB_CB_ADAPTIVE_THRESH );

image

輸入的棋盤圖,必須是8位的灰度或者彩色圖像。

pattern_size

棋盤圖中每行和每列角點的個數。

corners

檢測到的角點

corner_count

輸出,角點的個數。如果不是NULL,函數將檢測到的角點的個數存儲於此變量。

flags

各種操作標誌,可以是0或者下面值的組合:

  • CV_CALIB_CB_ADAPTIVE_THRESH - 使用自適應閾值(通過平均圖像亮度計算得到)將圖像轉換爲黑白圖,而不是一個固定的閾值。
  • CV_CALIB_CB_NORMALIZE_IMAGE - 在利用固定閾值或者自適應的閾值進行二值化之前,先使用cvNormalizeHist來均衡化圖像亮度。
  • CV_CALIB_CB_FILTER_QUADS - 使用其他的準則(如輪廓面積,周長,方形形狀)來去除在輪廓檢測階段檢測到的錯誤方塊。

函數cvFindChessboardCorners試圖確定輸入圖像是否是棋盤模式,並確定角點的位置。如果所有角點都被檢測到且它們都被以一定順序排布(一行一行地,每行從左到右),函數返回非零值,否則在函數不能發現所有角點或者記錄它們地情況下,函數返回0。例如一個正常地棋盤圖右8x8個方塊和7x7個內角點,內角點是黑色方塊相互聯通地位置。這個函數檢測到地座標只是一個大約地值,如果要精確地確定它們的位置,可以使用函數cvFindCornerSubPix。

 

DrawChessBoardCorners

繪製檢測到的棋盤角點

void cvDrawChessboardCorners( CvArr* image, CvSize pattern_size,
                              CvPoint2D32f* corners, int count,
                              int pattern_was_found );

image

結果圖像,必須是8位彩色圖像。

pattern_size

每行和每列地內角點數目。

corners

檢測到地角點數組。

count

角點數目。

pattern_was_found

指示完整地棋盤被發現(≠0)還是沒有發現(=0)。可以傳輸cvFindChessboardCorners函數的返回值。

當棋盤沒有完全檢測出時,函數cvDrawChessboardCorners以紅色圓圈繪製檢測到的棋盤角點;如果整個棋盤都檢測到,則用直線連接所有的角點。


姿態估計

CreatePOSITObject

初始化包含對象信息的結構

CvPOSITObject* cvCreatePOSITObject( CvPoint3D32f* points, int point_count );

points

指向三維對象模型的指針

point_count

對象的點數

函數 cvCreatePOSITObject 爲對象結構分配內存並計算對象的逆矩陣。

預處理的對象數據存儲在結構CvPOSITObject中,只能在OpenCV內部被調用,即用戶不能直接讀寫數據結構。用戶只可以創建這個結構並將指針傳遞給函數。

對象是在某座標系內的一系列點的集合,函數 cvPOSIT計算從照相機座標系中心到目標點points[0] 之間的向量。

一旦完成對給定對象的所有操作,必須使用函數cvReleasePOSITObject釋放內存。

 

POSIT

執行POSIT算法

void cvPOSIT( CvPOSITObject* posit_object, CvPoint2D32f* image_points, 
              double focal_length,
              CvTermCriteria criteria, CvMatr32f rotation_matrix, 
              CvVect32f translation_vector );

posit_object

指向對象結構的指針

image_points

指針,指向目標像素點在二維平面圖上的投影。

focal_length

使用的攝像機的焦距

criteria

POSIT迭代算法程序終止的條件

rotation_matrix

旋轉矩陣

translation_vector

平移矩陣.

函數 cvPOSIT 執行POSIT算法。圖像座標在攝像機座標系統中給出。焦距可以通過攝像機標定得到。算法每一次迭代都會重新計算在估計位置的透視投影。

兩次投影之間的範式差值是對應點中的最大距離。如果差值過小,參數criteria.epsilon就會終止程序。

 

ReleasePOSITObject

釋放3D對象結構

void cvReleasePOSITObject( CvPOSITObject** posit_object );

posit_object

指向 CvPOSIT 結構指針的指針。

函數 cvReleasePOSITObject 釋放函數 cvCreatePOSITObject分配的內存。

 

CalcImageHomography

計算長方形或橢圓形平面對象(例如胳膊)的Homography矩陣

void cvCalcImageHomography( float* line, CvPoint3D32f* center,
                            float* intrinsic, float* homography );

line

對象的主要軸方向,爲向量(dx,dy,dz).

center

對象座標中心 ((cx,cy,cz)).

intrinsic

攝像機內參數 (3x3 matrix).

homography

輸出的Homography矩陣(3x3).

函數 cvCalcImageHomography 爲從圖像平面到圖像平面的初始圖像變化(defined by 3D oblong object line)計算Homography矩陣。


對極幾何(雙視幾何)

FindFundamentalMat

由兩幅圖像中對應點計算出基本矩陣

int cvFindFundamentalMat( const CvMat* points1,
                          const CvMat* points2,
                          CvMat* fundamental_matrix,
                          int    method=CV_FM_RANSAC,
                          double param1=1.,
                          double param2=0.99,
                          CvMat* status=NULL);

points1

第一幅圖像點的數組,大小爲2xN/Nx2 或 3xN/Nx3 (N 點的個數),多通道的1xN或Nx1也可以。點座標應該是浮點數(雙精度或單精度)。:

points2

第二副圖像的點的數組,格式、大小與第一幅圖像相同。

fundamental_matrix

輸出的基本矩陣。大小是 3x3 或者 9x3 ,(7-點法最多可返回三個矩陣).

method

計算基本矩陣的方法

  • CV_FM_7POINT – 7-點算法,點數目= 7
  • CV_FM_8POINT – 8-點算法,點數目 >= 8
  • CV_FM_RANSAC – RANSAC 算法,點數目 >= 8
  • CV_FM_LMEDS - LMedS 算法,點數目 >= 8

param1

這個參數只用於方法RANSAC 或 LMedS 。它是點到對極線的最大距離,超過這個值的點將被捨棄,不用於後面的計算。通常這個值的設定是0.5 or 1.0 。

param2

這個參數只用於方法RANSAC 或 LMedS 。 它表示矩陣正確的可信度。例如可以被設爲0.99 。

status

具有N個元素的輸出數組,在計算過程中沒有被捨棄的點,元素被被置爲1;否則置爲0。這個數組只可以在方法RANSAC and LMedS 情況下使用;在其它方法的情況下,status一律被置爲1。這個參數是可選參數。

對極幾何可以用下面的等式描述:

p_2^T \cdot F \cdot p_1=0

其中 F 是基本矩陣,p1 和 p2 分別是兩幅圖上的對應點。

函數 FindFundamentalMat 利用上面列出的四種方法之一計算基本矩陣,並返回基本矩陣的值:沒有找到矩陣,返回0,找到一個矩陣返回1,多個矩陣返回3。 計算出的基本矩陣可以傳遞給函數cvComputeCorrespondEpilines來計算指定點的對極線。

例子1:使用 RANSAC 算法估算基本矩陣。
int    numPoints = 100;
CvMat* points1;
CvMat* points2;
CvMat* status;
CvMat* fundMatr;
points1 = cvCreateMat(2,numPoints,CV_32F);
points2 = cvCreateMat(2,numPoints,CV_32F);
status  = cvCreateMat(1,numPoints,CV_32F);

/* 在這裏裝入對應點的數據... */

fundMatr = cvCreateMat(3,3,CV_32F);
int num = cvFindFundamentalMat(points1,points2,fundMatr,CV_FM_RANSAC,1.0,0.99,status);
if( num == 1 )
     printf("Fundamental matrix was found\n");
else
     printf("Fundamental matrix was not found\n");


例子2:7點算法(3個矩陣)的情況。
CvMat* points1;
CvMat* points2;
CvMat* fundMatr;
points1 = cvCreateMat(2,7,CV_32F);
points2 = cvCreateMat(2,7,CV_32F);

/* 在這裏裝入對應點的數據... */

fundMatr = cvCreateMat(9,3,CV_32F);
int num = cvFindFundamentalMat(points1,points2,fundMatr,CV_FM_7POINT,0,0,0);
printf("Found %d matrixes\n",num); 

 

ComputeCorrespondEpilines

爲一幅圖像中的點計算其在另一幅圖像中對應的對極線。

void cvComputeCorrespondEpilines( const CvMat* points,
                                  int which_image,
                                  const CvMat* fundamental_matrix,
                                  CvMat* correspondent_lines);

points

輸入點,是2xN 或者 3xN 數組 (N爲點的個數)

which_image

包含點的圖像指數(1 or 2)

fundamental_matrix

基本矩陣

correspondent_lines

計算對極點, 3xN數組

函數 ComputeCorrespondEpilines 根據外級線幾何的基本方程計算每個輸入點的對應外級線。如果點位於第一幅圖像(which_image=1),對應的對極線可以如下計算 :

l_2=F \cdot p_1

其中F是基本矩陣,p1 是第一幅圖像中的點, l2 - 是與第二幅對應的對極線。如果點位於第二副圖像中 which_image=2),計算如下:

l_1=F^T \cdot p_2

其中p2 是第二幅圖像中的點,l1 是對應於第一幅圖像的對極線,每條對極線都可以用三個係數表示 a, b, c:

a\cdot x + b\cdot y + c = 0

歸一化後的對極線係數存儲在correspondent_lines 中。 

 

 

Freak特徵描述+BruteForceMatcher匹配+RANSAC剔除誤匹配

時間一晃飛快,前幾日的廣州之行讓人難忘,置身炎熱的戶外彷彿蒸桑拿一樣。這一段時間一直在研究一種從一幅圖片中找到給定目標的方法,而基於局部特徵提取並且進行特徵的匹配可以實現。 
目前局部特徵描述子有以下幾種,

  1. 2004年發展成熟的SIFT
  2. 後來改進的SURF,它們的描述符是高維度整型的數字。
  3. 2010年出現的BRIEF是一種二進制的描述符,這種描述符在提高計算速度方面給出了不錯的思路,但是不具備旋轉不變,尺度不變,對噪聲也比較敏感(雖然它採用了高斯平滑來抗噪聲)
  4. 2011年出現的ORB算法同樣是二進制,但是解決了旋轉不變和噪聲敏感的問題,但是不具備尺度不變性。
  5. 2011年同時出現了BRISK算法,解決旋轉不變,尺度不變和抗噪聲問題,網上有視頻,實時性也不錯,先有個直觀認識,爽死。鏈接[(http://v.youku.com/v_show/id_XMTI5MzI3Mzk0OA==.html)]
  6. 2012年提出FREAK算法,其實是在BRISK上的改進。具備BRISK的優點。這篇論文中作者表示FREAK算法可以outperform以前所有的描述子。(注意:這裏是特徵描述子,因爲論文中沒有給出特徵點檢測方法,只有特徵點的描述子) 
    看完了綜述,我決定用FREAK來實現。足足耗了我15天,今天我這一段所得寫下,勉勵自己,服務他人。 
    先貼上代碼
/************************************************************************
* Copyright(c) 2016  唯瘋
* All rights reserved.
*
* Brief: FAST特徵點提取以及FREAK描述子的圖像匹配,基於OpenCV2.4.8
* Version: 1.0
* Author: 唯瘋
* Date: 2016/07/21
* Address: 廣州&北京
************************************************************************/
#include <opencv2/core/core.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/nonfree/features2d.hpp>
#include <opencv2/legacy/legacy.hpp>
#include <iostream>
#include <vector>

using namespace std;
using namespace cv;

int main()
{
    Mat img1_src = imread("im5.jpg",0);
    Mat img2_src = imread("im6.jpg",0);
    //FastFeatureDetector fast(40);
    SurfFeatureDetector fast(2000,4);
    FREAK extractor;
    vector<KeyPoint> keypoints1,keypoints2;
    Mat descriptor1,descriptor2;
    vector<DMatch> final_matches; 
    vector<DMatch> matches;

    double t = (double)getTickCount();
    fast.detect(img1_src,keypoints1);
    fast.detect(img2_src,keypoints2);
    //drawKeypoints(img1_src,keypoints1,img1_src,Scalar(0,255,0));
    //drawKeypoints(img2_src,keypoints2,img2_src,Scalar(0,255,0));//問題在這裏!!!醉了,這裏的問題,浪費了我5天,歐耶,就是整整5天,由於我想在這裏看一看檢測出來的特徵點是啥樣的,就跟父母想第一眼就立刻看到剛出生的嬰兒是一個心情的。結果,後來特徵匹配就幾乎沒有什麼正確匹配,害我還以爲是FREAK這個描述子有問題呢。於是就各種找問題,看論文,翻牆逛論壇。最後一個不經意間發現了。問題就是:這倆個語句是把特徵點又原封不動的畫到了img1_src中,也就是原圖像裏面,而後來我進行特徵點描述的時候,就直接在畫滿了特徵點的圖片下進行描述,而不是原圖!不是原圖啊!是充滿了特徵點的圖片!所以後期再進行匹配的時候,顯然,各種亂匹配,就跟隔壁家小狗似的,見了貓都想幹壞事。於是乎,我直接註釋掉了這兩句!
    extractor.compute(img1_src,keypoints1,descriptor1);
    extractor.compute(img2_src,keypoints2,descriptor2);
    BFMatcher matcher(NORM_HAMMING,true);//暴力匹配,並且進行crosscheck,就是說第二個參數選擇true。
    matcher.match(descriptor1,descriptor2,matches);
    final_matches=matches;
    cout<<"number of total_matches : "<<final_matches.size()<<endl;

//接下來是RANSAC剔除誤匹配
    vector<Point2f> querymatches, trainmatches;
    vector<KeyPoint> p1,p2;
    for(int i=0;i<final_matches.size();i++)
    {
        p1.push_back(keypoints1[final_matches[i].queryIdx]);
        p2.push_back(keypoints2[final_matches[i].trainIdx]);
    }
    for(int i=0;i<p1.size();i++)
    {
        querymatches.push_back(p1[i].pt);
        trainmatches.push_back(p2[i].pt);
    }
    cout<<querymatches[1]<<" and "<<trainmatches[1]<<endl;
    vector<uchar> status;
    Mat h = findHomography(querymatches,trainmatches,status,CV_FM_RANSAC,10);
    int index=0;
    vector<DMatch> super_final_matches;
    for (int i=0;i<final_matches.size();i++)
    {
        cout<<status[i];
        if (status[i] != 0)
        {
        super_final_matches.push_back(final_matches[i]);
            index++;
        }
    }
    cout<<"number of inlier_matches : "<<index<<endl;

    Mat imgMatch;
    drawMatches(img1_src,keypoints1,img2_src,keypoints2,super_final_matches,imgMatch);
    imshow("imgMatch",imgMatch);

    t = ((double)getTickCount()-t)/getTickFrequency();
    cout<<" total time [s] : "<<t<<endl;
    waitKey(0);
    return 0;
}

看看簡單的效果
一: 
首先說說特徵點檢測,爲什麼我用surf,而不用速度很快的fast或者Brisk,我自己做了測試,發現用fast和brisk的話,再用freak描述,最終匹配效果並不好,比sift要差…我其實不太明白爲什麼,因爲有一些後期的論文對比效果說freak綜合效果最好,然而我卻得不到這樣的結果。要繼續鑽研了,大家有想法歡迎交流,哦不,歡迎指點! 
二: 
RANSAC的這個算法,請用opencv庫中的cvFindHomography,不要用cvFindFundamentalMat,因爲這兩個是不一樣的,字面上簡單看一個是找到單應性矩陣,一個是找到基礎矩陣,這兩個到底有神馬區別??? 
《計算機視覺中的多視圖幾何》中這樣描述:

  1. 2D單應:一幅2D圖像中點集到另一幅圖像中點集的射影變換。
  2. 基本矩陣:它使一個隊所有的點都是的x`Fx=0成立的奇異矩陣F。這是一個3*3的奇異矩陣,而且秩爲2,不可逆。它是2維圖像上的點通過對極線束約束的映射,是2維到1維的映射。

具體的大家可以看書或者百度,或者等以後我有空了,再具體描述。其實我也沒有太懂哈哈哈!還要再看看!慎言慎言! 
之前,我曾經自己編寫過一個RANSAC的算法啊,但是效果不是特別理想,由於RANSAC算法本身也比較耗時,各路大神們也都提出了很多改進方法,不知道opencv跟進這些改進算法沒有?推薦綜述型文章《A Universal Framework for Random 
Sample Consensus》,裏面提到的prosac,lo-ransac速度提升很多!大家自行汲取,我想以後再寫一些關於RANSAC的博文。 
三: 
代碼中用到BFMatcher這個類,對象的參數有兩個BFMatcher::BFMatcher(int normType=NORM_L2, bool crossCheck=false )。第一個指的是距離,如果是sift或者surf描述的話,就選擇NORM_L2和NORM_L2;ORB,BRISK,FREAK這種二進制的描述子就用漢明距離,也就是NORM_HAMMING;NORM_HAMMING2是用作ORB的。第二個參數是是否crossCheck,最好選擇true,這個也經過我實驗驗證,選擇true相當於knn匹配中將k置1,也就是隻返回最好的那一組匹配,而不是多個近鄰。(注意:如果要用knnmatch的話,這個crossCheck的參數就選擇false)。這種只返回最好的一組匹配的方式可以作爲ratio test的一種替代選擇。實驗證明,如果選擇false,那麼將會返回一對多匹配,和多對一的匹配,後期用ransac的效果可能非常不理想。crossCheck就是交叉匹配,剔除不好的匹配。有論文支撐,大家感興趣可以去搜。

 

參考博文:https://blog.csdn.net/u011867581/article/details/43818183

 

https://blog.csdn.net/luoshixian099/article/details/50217655

https://blog.csdn.net/lcb_coconut/article/details/52145293?locationNum=4

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章