淺談隨機采樣一致性(RANSAC)算法實現


注:這是我在知乎寫的文章,現搬運至此。原文鏈接:https://zhuanlan.zhihu.com/p/79386524

在基於特征的SLAM中,特征匹配是一個非常關鍵的問題,為了防止錯誤匹配對后端的估計造成影響,工程師們研究出了很多魯棒估計算法,在視覺SLAM中,目前比較流行兩種方式,一種是在SLAM后端優化中添加核函數防止錯誤匹配權重過大,另一種就是本文要介紹的RANSAC,中文翻譯為隨機采樣一致性算法。本文的 RANSAC 算法是利用DLT求解PNP實現的最基礎版本,outlier剔除效果不錯,但是迭代次數過多,編寫的代碼和對RANSAC的理解還有許多不足,僅供參考。

RANSAC 原理簡單,知乎上也有很多老鐵介紹過,在此再簡單說下,要想更加具體的了解可以參閱《機器人學中的狀態估計》五章第3節。

最基礎的RANSAC包括五個步驟:

  1. 從所有原始數據中隨機選取一個最小子集(如果求解PNP問題,那么顯然可以選取3個點(P3P)。但是我使用的是dlt方式,所以選取的是6個點,這種方式僅供參考,不過應該沒什么問題,OpenCV中solvePnPRansac()也可以選擇Epnp解法,需要4個點對)。
  2. 使用這個子集擬合一個模型(本文中就是通過DLT求解PNP問題)。
  3. 根據上一步求解出的模型去判斷所有數據,將數據分為outlier(外點)和inlier(內點),如果內點太少,那么返回第一步。其中本文求解的是PNP問題,使用的是重投影誤差來判斷內外點。
  4. 根據划分出的所有內點,重新擬合模型。
  5. 根據最新擬合出的模型,判斷內外點數目,如果達到最終要求,那么跳出循環,如果內點太少,那么返回第一步。如果內點數目有增加但是還沒有達到最終要求,那么返回第二步。

我的代碼編寫的比較簡單,其中DLT部分參考了此博客:[PnP] PnP問題之DLT解法

我使用cuSift這個庫提取sift特征點,然后使用庫中自帶的暴力匹配,先看原始結果:

再看使用我們的RANSAC剔除后的結果:

可以看出來,雖說我代碼編的不好,不過還是有一定效果的,因為我使用了cuSift庫,這些代碼在大部分人的電腦上應該是運行不了的,所以代碼僅供參考。

首先隨機選取6個點,這可是真真的隨機,沒有加任何額外處理。

 1 deque<int> getMaxScorePoints(SiftData& sift,vector<double>& pt3DDepth , int nums = 6){
 2     deque<int> ans;
 3     while(ans.size()<nums){
 4         struct timeval tv;
 5         gettimeofday(&tv,NULL);
 6         srand((int)(tv.tv_usec));  // 產生隨機種子 
 7         int i = rand()%sift.numPts;
 8         if(ans.size()<nums){
 9             ans.push_back(i);
10          }
11     return ans;
12 }
View Code

 

然后我們進行RANSAC:

 1 void RANSAC(SiftData& sift,vector<double>& pt3DDepth,int nums = 6){
 2     auto start = cv::getTickCount();
 3     Eigen::Matrix3d R ;
 4     Eigen::Vector3d t ;
 5     /// 連續3次滿足條件,那么迭代終止,這里還有需要改進的空間
 6     for(int it=0;it<3;++it) {
 7         deque<int> goodPoints;
 8         /// 如果是第一次迭代,那么隨機選取6個點
 9         if(it==0)
10             goodPoints = getMaxScorePoints(sift, pt3DDepth,nums);
11         else {
12             /// state數組記錄所有特征點的狀態,內點為true,外點為false
13             /// 某次迭代后,使用所有內點重新估計模型
14             for(int i=0;i<sift.numPts;++i){
15                 if (state[i])
16                     goodPoints.push_back(i);
17             }
18         }
19         std::cout << "goodPoints size:" << goodPoints.size() << endl;
20         /// 對內點使用DLT解法求解,R,t為結果
21         solvePnpByDLT(sift,pt3DDepth,goodPoints,R,t);
22         /// 計算重投影誤差,剔除外點
23         double goodPoint = 0.0;
24         for (int i=0;i<sift.numPts;++i) {
25             auto siftPoint = sift.h_data[i];
26             /// 我們觀測的像素坐標
27             const double &u = siftPoint.match_xpos;
28             const double &v = siftPoint.match_ypos;
29             /// 計算3d點的世界坐標
30             const double &z = pt3DDepth[i];
31             const double &x = (siftPoint.xpos - cx) * z / fx;
32             const double &y = (siftPoint.ypos - cy) * z / fy;
33             /// 求得的是特征點世界坐標系下的坐標,要轉化到相機坐標系下
34             Eigen::Vector3d ans(x,y,z);
35             ans = R.inverse()*(ans-t);
36             /// 重投影
37             double u_reprojection = (fx * ans(0) / ans(2)) + cx;
38             double v_reprojection = (fy * ans(1) / ans(2)) + cy;
39             /// 誤差
40             double dis = sqrt((u - u_reprojection)*(u - u_reprojection) 
41                             + (v - v_reprojection)*(v - v_reprojection));
42             /// 誤差小於35.....這個閾值好像有點大,因為DLT估計的姿態特別不准確...
43             /// 根據結果設置state的狀態
44             if (abs(dis) > 35) state[i] = false;
45             else {
46                 goodPoint += 1.0;
47                 state[i] = true;
48             }
49         }
50         cout<<"good count:"<<goodPoint<<endl;
51         /// 如果本次估計的結果滿足一個比率,那么繼續
52         /// 否則重新迭代
53         if(goodPoint / sift.numPts<ee[it]){
54             cout<<"估計錯誤,重新估算!"<<endl;
55             it = -1;
56             for(int i=0;i<sift.numPts;++i){
57                 auto p = sift.h_data[i];
58                 if (pt3DDepth[i] < 0.0 || p.score<0.9 || p.ambiguity>0.9) {
59                     state[i] = false;
60                 } else state[i] = true;
61 
62             }
63         }
64     }
65     cout << "R:" << endl << R << endl;
66     cout << "t:" << endl << t << endl;
67     double time = (cv::getTickCount() - start) / cv::getTickFrequency();
68     std::cout<<"Finish ......t =:"<<time*1000<<"ms"<<std::endl;
69 }
View Code

 

其中的solvePnpByDLT(),參考了這位老鐵寫的文章:[PnP] PnP問題之DLT解法,感謝分享!我就不貼代碼了。

總結與討論

1.RANSAC原理很簡單,代碼編寫更簡單,但是從我的測試來看,如果我不對比配的特征點做任何過濾,真正的隨機取點,那么算法的收斂速度完全就取決於這隨機的6個點的好壞,收斂非常之慢,往往迭代幾百次才收斂。如果我們希望加快收斂速度,這個所謂的“隨機”應該還是有很多方式的,最起碼如果6個特征點非常集中,那么我們肯定要重新取點,因為過於集中的特征點會帶來非常差的估計結果,這次迭代必定是發散的。不過這樣似乎就不叫隨機了。當然,這僅僅代表我自己的觀點,讀者還要選擇性的吸收。我對RANSAC的理解還有待加強。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM