Bundle Adjustment(光束平差法、捆集调整等)是指从视觉重建中计算出最优的 3D 模型和相机参数(内参和外参)。从每个特征点反射出来的几束光线(bundles of light rays),在把相机姿态和特征点的位置做出最优的调整(adjustment)之后,最后收束到光心的这个过程,简称 BA。
本文总结网络资源,对 BA 进行了总结。
Bundle Adjustment
1 介绍
Bundle Adjustment(光束平差法、捆集调整等)是指从视觉重建中计算出最优的 3D 模型和相机参数(内参和外参)。从每个特征点反射出来的几束光线(bundles of light rays),在把相机姿态和特征点的位置做出最优的调整(adjustment)之后,最后收束到光心的这个过程,简称 BA。
图-1 BA 示意
2 原理
假设空间位置的 3D 点为:X = { x 1 , x 2 , . . . , x n } X = \{ x_1, x_2, ..., x_n \} X = { x 1 , x 2 , . . . , x n } ,相机中心位姿为 P = { p 1 , p 2 , . . . , p m } P =\{ p_1, p_2, ..., p_m \} P = { p 1 , p 2 , . . . , p m } 。u i u_i u i 为 x i x_i x i 对应的像素位置,K K K 为相机的内参矩阵,s i s_i s i 为 u i u_i u i 对应的深度值。如图,BA 要计算的误差是观测值和估计值之间的误差。
图-2 BA 的计算目标
对于单个相机的 BA,可以构建重投影误差最小二乘如下:
ε ( X , T ) = ∑ i = 1 n ∣ ∣ u i − 1 s i K T x i ∣ ∣ 2 \varepsilon (X, T) = \sum_{i=1}^{n} || u_i - \frac{1}{s_i} K T x_i ||^2
ε ( X , T ) = i = 1 ∑ n ∣ ∣ u i − s i 1 K T x i ∣ ∣ 2
其中,变换矩阵 T T T 满足约束:
T = [ R t 0 T 1 ] ∈ S E ( 3 ) , R T R = I , d e t ( R ) = 1 , t ∈ R 3 T =
\left[
\begin{matrix}
R &t \\
\pmb{0}^T &1
\end{matrix}
\right]
\in SE(3),\
R^T R = I, \ det(R) = 1,\ t \in \mathbb{R}^3
T = [ R 0 0 0 T t 1 ] ∈ S E ( 3 ) , R T R = I , d e t ( R ) = 1 , t ∈ R 3
对于有约束的变换矩阵在最小二乘中不便求解,因此转换为无约束的李群求解:
ε ( X , T ) = ∑ i = 1 n ∣ ∣ u i − 1 s i K ⋅ exp ( ξ ∧ ) ⋅ x i ∣ ∣ 2 \varepsilon (X, T) = \sum_{i=1}^{n} || u_i - \frac{1}{s_i} K \cdot \exp (\xi^{\wedge}) \cdot x_i ||^2
ε ( X , T ) = i = 1 ∑ n ∣ ∣ u i − s i 1 K ⋅ exp ( ξ ∧ ) ⋅ x i ∣ ∣ 2
定义误差函数为:
f ( x i , ξ ) = u i − K ⋅ exp ( ξ ∧ ) ⋅ x i f (x_i, \xi) = u_i - K \cdot \exp (\xi^{\wedge}) \cdot x_i
f ( x i , ξ ) = u i − K ⋅ exp ( ξ ∧ ) ⋅ x i
对于高斯牛顿法,函数 f ( x ) f(x) f ( x ) 有增量方程:
J ( x ) T ⋅ J ( x ) ⋅ Δ x = − J ( x ) T ⋅ f ( x ) J(x)^T \cdot J(x) \cdot \Delta x = -J(x)^T \cdot f(x)
J ( x ) T ⋅ J ( x ) ⋅ Δ x = − J ( x ) T ⋅ f ( x )
仅需要求 f ( x i , ξ ) f(x_i, \xi) f ( x i , ξ ) 的一阶导(Jacobian 矩阵)。
3 C++ 代码
优化相机内参及畸变系数,相机的位姿(6dof)和 landmark 代价函数写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 template <typename CameraModel>class BundleAdjustmentCostFunction {public : explicit BundleAdjustmentCostFunction (const Eigen::Vector2d& point2D) :observed_x_(point2D(0 )), observed_y_(point2D(1 )) { } static ceres::CostFunction* Create (const Eigen::Vector2d& point2D) { return (new ceres::AutoDiffCostFunction< BundleAdjustmentCostFunction<CameraModel>, 2 , 4 , 3 , 3 , CameraModel::kNumParams>(new BundleAdjustmentCostFunction (point2D))); } template <typename T> bool operator () (const T* const qvec, const T* const tvec, const T* const point3D, const T* const camera_params, T* residuals) const { T projection[3 ]; ceres::UnitQuaternionRotatePoint (qvec, point3D, projection); projection[0 ] += tvec[0 ]; projection[1 ] += tvec[1 ]; projection[2 ] += tvec[2 ]; projection[0 ] /= projection[2 ]; projection[1 ] /= projection[2 ]; CameraModel::WorldToImage (camera_params, projection[0 ], projection[1 ], &residuals[0 ], &residuals[1 ]); residuals[0 ] -= T (observed_x_); residuals[1 ] -= T (observed_y_); return true ; } private : const double observed_x_; const double observed_y_; };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ceres::Problem problem; ceres::CostFunction* cost_function = nullptr ; ceres::LossFunction * p_LossFunction = ceres_options_.bUse_loss_function_ ? new ceres::HuberLoss (Square (4.0 )) : nullptr ; cost_function = BundleAdjustmentCostFunction<CameraModel>::Create (point2D.XY ()); problem_->AddResidualBlock (cost_function, p_LossFunction, qvec_data, tvec_data, point3D.XYZ ().data (), camera_params_data);
优化相机内参及畸变系数,pose subset parameterization(pose 信息部分参数优化)和 3D landmark,当只优化姿态信息时,problem 需要添加的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 map_poses[indexPose] = {angleAxis[0 ], angleAxis[1 ], angleAxis[2 ], t (0 ), t (1 ), t (2 )}; double * parameter_block = &map_poses.at (indexPose)[0 ];problem.AddParameterBlock (parameter_block, 6 ); std::vector<int > vec_constant_extrinsic; vec_constant_extrinsic.insert (vec_constant_extrinsic.end (), {3 ,4 ,5 }); if (!vec_constant_extrinsic.empty ()) { ceres::SubsetParameterization *subset_parameterization = new ceres::SubsetParameterization (6 , vec_constant_extrinsic); problem.SetParameterization (parameter_block, subset_parameterization); }
优化相机内参及畸变系数,pose subset parameterization(pose 信息部分参数优化)和 3D landmark,当只优化位置信息时,problem 需要添加的代码如下(同上,只需修改一行):
1 2 3 4 5 6 7 8 9 10 11 map_poses[indexPose] = {angleAxis[0 ], angleAxis[1 ], angleAxis[2 ], t (0 ), t (1 ), t (2 )}; double * parameter_block = &map_poses.at (indexPose)[0 ];problem.AddParameterBlock (parameter_block, 6 ); std::vector<int > vec_constant_extrinsic; vec_constant_extrinsic.insert (vec_constant_extrinsic.end (), {1 ,2 ,3 }); if (!vec_constant_extrinsic.empty ()) { ceres::SubsetParameterization *subset_parameterization = new ceres::SubsetParameterization (6 , vec_constant_extrinsic); problem.SetParameterization (parameter_block, subset_parameterization); }
优化相机内参及畸变系数,pose 是常量不优化和 3D landmark,代价函数写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 template <typename CameraModel> class BundleAdjustmentConstantPoseCostFunction {public : BundleAdjustmentConstantPoseCostFunction (const Eigen::Vector4d& qvec, const Eigen::Vector3d& tvec, const Eigen::Vector2d& point2D) : qw_ (qvec (0 )), qx_ (qvec (1 )), qy_ (qvec (2 )), qz_ (qvec (3 )), tx_ (tvec (0 )), ty_ (tvec (1 )), tz_ (tvec (2 )), observed_x_ (point2D (0 )), observed_y_ (point2D (1 )) {} static ceres::CostFunction* Create (const Eigen::Vector4d& qvec, const Eigen::Vector3d& tvec, const Eigen::Vector2d& point2D) { return (new ceres::AutoDiffCostFunction< BundleAdjustmentConstantPoseCostFunction<CameraModel>, 2 , 3 , CameraModel::kNumParams>( new BundleAdjustmentConstantPoseCostFunction (qvec, tvec, point2D))); } template <typename T> bool operator () (const T* const point3D, const T* const camera_params, T* residuals) const { const T qvec[4 ] = {T (qw_), T (qx_), T (qy_), T (qz_)}; T projection[3 ]; ceres::UnitQuaternionRotatePoint (qvec, point3D, projection); projection[0 ] += T (tx_); projection[1 ] += T (ty_); projection[2 ] += T (tz_); projection[0 ] /= projection[2 ]; projection[1 ] /= projection[2 ]; CameraModel::WorldToImage (camera_params, projection[0 ], projection[1 ], &residuals[0 ], &residuals[1 ]); residuals[0 ] -= T (observed_x_); residuals[1 ] -= T (observed_y_); return true ; } private : const double qw_; const double qx_; const double qy_; const double qz_; const double tx_; const double ty_; const double tz_; const double observed_x_; const double observed_y_; };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ceres::Problem problem; ceres::CostFunction* cost_function = nullptr ; ceres::LossFunction * p_LossFunction = ceres_options_.bUse_loss_function_ ? new ceres::HuberLoss (Square (4.0 )) : nullptr ; cost_function = BundleAdjustmentConstantPoseCostFunction<CameraModel>::Create ( image.Qvec (), image.Tvec (), point2D.XY ()); problem_->AddResidualBlock (cost_function, loss_function, point3D.XYZ ().data (), camera_params_data);
参考