1. 程式人生 > 實用技巧 >Ceres求解直接法BA實現自動求導

Ceres求解直接法BA實現自動求導

作者:郭田峰

來源:公眾號@3D視覺工坊

連結:Ceres求解直接法BA實現自動求導

BA,即Bundle Adjustment,通常譯為光束法平差,束調整,捆綁調整等。但高翔博士覺得這些譯名不如英文名稱來得直觀,所以保留英文名,簡稱BA。

所謂BA,是指從視覺影象中提煉出最優的3D模型和相機引數。在視覺SLAM裡,BA特徵點法和直接法兩種。前者是最小化重投影誤差作為優化目標,後者是以最小化光度誤差為目標。

對於特徵點法BA,高翔博士所著的《視覺SLAM十四講》第二版第九章作了非常詳細的說明。對於直接法BA,在深藍學院的課程《視覺SLAM理論與實踐》中有用g2o求解的習題,但沒有提到Ceres求解。而且,習題中給出的是雙線性插值來得到影象點的灰度值。我們知道,直接法BA需要判斷影象邊界,而且Ceres對雙線性插值是不能自動求導的。這都會增加程式碼實現的難度。

在課後作業裡有一題要求用g2o實現直接法BA。相對來說g2o來說,我個人更喜歡用Ceres,畢竟Ceres是谷歌出品,而且,谷歌的非線性優化大多是用Ceres來解決的,功能和效率應該是值得我們信任的。

我們知道,Ceres是推薦我們儘可能使用自動求導的,一是準確性更有保障;二是求解更快速。所以,我們要尋找能實現自動求導的實現方法。

Ceres提供的Ceres的Grid2D和BiCubicInterpolator聯合使用可以解決上述兩個問題。Grid2D和BiCubicInterpolator的使用需要包含標頭檔案cubic_interpolation.h。

Grid2D是無限二維網格的物件,可以用來載入影象資料,如果是灰度圖,其值用標量,如果是彩色影象,其值用3維向量。由於網格的輸入資料總是有限的,而網格本身是無限的,因為需要通過使用雙三次插值BiCubicInterpolator來計算網格之間的值。而超出網格範圍,則將返回最近邊緣的值。

將灰度影象資料載入到Grid2D物件,可以避免我們在程式碼中判斷影象邊界的問題。而且,Grid2D還可以利用BiCubicInterpolator實現雙三次插值,它相對於雙線性插值的優點是能實現自動求導。利用它們寫出的程式碼更簡潔,執行效率也更高。

Grid2D物件和BiCubicInterpolator物件的定義:

std::unique_ptr<ceres::Grid2D<資料型別[,資料維數]>>變數名;std::unique_ptr<ceres::BiCubicInterpolator<ceres::Grid2D<資料型別[,資料維數]>>>變數

資料型別一般是簡單型別,如int,float,double等,上面兩個定義的資料型別和資料維數必須相同。資料維數是指值是幾維資料,預設值為1,即函式值為標量時可以不指定該引數。定義好了物件變數,就可以初始化了,格式如下:

Grid2D物件.reset(newceres::Grid2D<資料型別[,資料維數]>(資料首地址,首行號,總行數,首列號,總列數));BiCubicInterpolator物件.reset(newceres::BiCubicInterpolator<ceres::Grid2D<資料型別[,資料維數]>>(*Grid2D物件))

資料一般使用vector型別以及巢狀,對於灰度圖,我使用的是vector<vector<float>>。

Grid2D還有兩個引數,分別是表示資料的儲存順序為行還是列,以及值為向量時向量的每一維值的儲存順序是行還是列,由於在本文中並不重要,所以這裡不作介紹。程式碼中直接採用了預設值。

計算殘差程式碼如下:

struct GetPixelGrayValue {  GetPixelGrayValue(const float pixel_gray_val_in[16],           const int rows,           const int cols,           const std::vector<float>& vec_pixel_gray_values)  {    for (int i = 0; i < 16; i++)      pixel_gray_val_in_[i] = pixel_gray_val_in[i];    rows_ = rows;    cols_ = cols;        // 初始化grid2d    grid2d.reset(new ceres::Grid2D<float>(      &vec_pixel_gray_values[0], 0, rows_, 0, cols_));        //雙三次插值    get_pixel_gray_val.reset(      new ceres::BiCubicInterpolator<ceres::Grid2D<float> >(*grid2d));  }    template <typename T>bool operator()(const T* const so3t,       //模型引數,位姿,6維const T* const xyz,       //模型引數,3D點,3維T* residual ) const       //殘差,4*4影象塊,16維{    // 計算變換後的u和v    T u_out, v_out, pt[3], r[3];    r[0] = so3t[0];    r[1] = so3t[1];    r[2] = so3t[2];    ceres::AngleAxisRotatePoint(r, xyz, pt);    pt[0] += so3t[3];    pt[1] += so3t[4];    pt[2] += so3t[5];    u_out = pt[0] * T(fx) / pt[2] + T(cx);    v_out = pt[1] * T(fy) / pt[2] + T(cy);            for (int i = 0; i < 16; i++)    {      int m = i / 4;      int n = i % 4;      T u, v, pixel_gray_val_out;      u = u_out + T(m - 2);      v = v_out + T(n - 2);      get_pixel_gray_val->Evaluate(u, v, &pixel_gray_val_out);      residual[i] = T(pixel_gray_val_in_[i]) - pixel_gray_val_out;    }        return true;  }
  float pixel_gray_val_in_[16];  int rows_,cols_;  std::unique_ptr<ceres::Grid2D<float> > grid2d;   std::unique_ptr<ceres::BiCubicInterpolator<ceres::Grid2D<float> > > get_pixel_gray_val;};新增殘差塊的程式碼:
for (int ip = 0; ip < poses_num; ip++){  for (int jp = 0; jp < points_num; jp++)  {    double *pose_position = first_poses_pos + ip * 6;    double *point_position = first_point_pos + jp * 3;    float gray_values[16]{};
    for ( int i = 0; i < 16; i++ )      gray_values[i] = patch_gray_values[jp][i];
    problem.AddResidualBlock(      new ceres::AutoDiffCostFunction<GetPixelGrayValue, 16, 6, 3> (        new GetPixelGrayValue ( gray_values,                    multi_img_rows_cols[ip * 2],                    multi_img_rows_cols[ip * 2 + 1],                    multi_img_gray_values[ip]        )      ),      new ceres::HuberLoss(1.0),      pose_position,      point_position    );  }}

新增殘差塊的程式碼:

for (int ip = 0; ip < poses_num; ip++){  for (int jp = 0; jp < points_num; jp++)  {    double *pose_position = first_poses_pos + ip * 6;    double *point_position = first_point_pos + jp * 3;    float gray_values[16]{};
    for ( int i = 0; i < 16; i++ )      gray_values[i] = patch_gray_values[jp][i];
    problem.AddResidualBlock(      new ceres::AutoDiffCostFunction<GetPixelGrayValue, 16, 6, 3> (        new GetPixelGrayValue ( gray_values,                    multi_img_rows_cols[ip * 2],                    multi_img_rows_cols[ip * 2 + 1],                    multi_img_gray_values[ip]        )      ),      new ceres::HuberLoss(1.0),      pose_position,      point_position    );  }}

這裡有必要就說明的是,要判定變換後的u和v是否在影象內,如果超界了,則該組資料棄之不用。在使用Grid2D和BiCubicInterpolator後,超界後使用的值是最接近的邊緣的值。這兩者處理結果看似差別很大,但對結果影響很小的,幾乎可以忽略不計。

下面的原來的題目:

對於相同的資料,g2o和Ceres求解執行結果如下。從截圖資料顯示,Ceres優化一共進行了33次迭代,用時7秒多;g2o優化一共進行了199次迭代,用時64秒左右。在這個優化題目裡,兩者差異非常明顯。

g2o求解直接法BA執行結果截圖

Ceres求解直接法BA執行結果截圖

在公眾號後臺回覆「DirectBA」,獲取g2o和Ceres的求解程式碼。

本文僅做學術分享,如有侵權,請聯絡刪文。