Backend Global Graph Optimization: Pose Parameterization and Jacobian Solution Scheme

Date:

Pose Parameterization and Jacobian Solution for Backend Global Graph Optimization

Implementation Based on Quaternion Rotation and Translation Vector Representation with Automatic Differentiation

Assuming for time \(i\), the vehicle pose is represented as \(x_i=[p_i^T,q_i^T]^T\), where \(p_i\) denotes the three-dimensional position vector and \(q_i\) is a unit quaternion representing the vehicle orientation. For two time instances \(i\) and \(j\), the residual term corresponding to the relative pose between vehicles is defined as

\[r_{ij}= \begin{bmatrix} R(q_i)^T(p_j-p_i)-\hat{p}_{ij} \\ 2\cdot \rm{vec}\left((q_i^{-1}q_j)\hat{q}_{ij}^{-1}\right) \end{bmatrix}\]

where \(\rm{vec}()\) denotes the vector part of a quaternion, specifically \([q_x,q_y,q_z]^T\), and \(R(q)\) represents the rotation matrix corresponding to quaternion \(q\). The corresponding code implementation is in the Functor class EdgePoseError, used in the Ceres CostFunction class for computing the residual vector and its corresponding Jacobian.

	template <typename T>
	bool operator()(const T* const position_i, const T* const orientation_i, 
					const T* const position_j, const T* const orientation_j, 
					T* residual_ptr) const
	{
		Eigen::Map<const Eigen::Matrix<T,3,1>> p_i(position_i);
		Eigen::Map<const Eigen::Quaternion<T>> q_i(orientation_i);

		Eigen::Map<const Eigen::Matrix<T,3,1>> p_j(position_j);
		Eigen::Map<const Eigen::Quaternion<T>> q_j(orientation_j);

		Eigen::Quaternion<T> q_i_inv=q_i.conjugate();
		Eigen::Quaternion<T> q_ij=q_i_inv*q_j;
		Eigen::Matrix<T,3,1> p_ij=q_i_inv*(p_j-p_i);

		Eigen::Quaternion<T> q_ij_meas(pose_ij_meas_.linear().template cast<T>());
		Eigen::Quaternion<T> delta_q=q_ij_meas*q_ij.conjugate();

		Eigen::Map<Eigen::Matrix<T,6,1>> residual(residual_ptr);
		Eigen::Map<Eigen::Matrix<T,3,1>> residual_trs(residual_ptr);
		Eigen::Map<Eigen::Matrix<T,3,1>> residual_rot(residual_ptr+3);

		residual_trs=p_ij-pose_ij_meas_.translation().template cast<T>();
		residual_rot=T(2.0)*delta_q.vec();
		residual.applyOnTheLeft(sqrt_information_.template cast<T>());

		return true;
	}

The Jacobian solving utilizes the automatic differentiation method provided by Ceres.

	static ceres::CostFunction* Create(const Eigen::Isometry3d& pose_ij,const Matrix6d& sqrt_information)
	{
		return new ceres::AutoDiffCostFunction<EdgePoseError,6,3,4,3,4>
			(new EdgePoseError(pose_ij,sqrt_information));
	}

Implementation Based on Pose Lie Algebra Parameterization and Analytical Differentiation

In this approach, the vehicle pose is represented in a minimal parameter form, i.e., \(\xi_i\in{\mathbb R}^6\). The corresponding transformation matrix for the three-dimensional vehicle pose is represented by \(T_i\in\mathbb{SE}(3)\).

\[\xi_i^\wedge\in\mathfrak{se}(3)\] \[T_i=\exp(\xi_i^\wedge)\in\mathbb{SE}(3)\]

The relative pose transformation between two nodes can be represented as

\[\begin{aligned} \delta\xi_{ij} &=\log(\delta T_{ij})^\vee=\log(T_i^{-1}T_j)^\vee \\ &=\log\left(\exp(-\xi_i^\wedge)\exp(\xi_j^\wedge)\right)^\vee \end{aligned}\]

The residual of the corresponding edge is

\[\begin{aligned} r_{ij} &=\log(\delta T_{ij}^{-1}T_i^{-1}T_j)^\vee \\ &=\log(\exp(-\xi_{ij}^\wedge)\exp(-\xi_i^\wedge)\exp(\xi_j^\wedge))^\vee \end{aligned}\]

The Jacobian matrix is calculated by

\[\begin{aligned} & \frac{\partial r_{ij}}{\partial \xi_i} =-{\mathcal J}^{-1}(r_{ij})\rm{Ad}(T_j^{-1})\\ & \frac{\partial r_{ij}}{\partial \xi_j} ={\mathcal J}^{-1}(r_{ij})\rm{Ad}(T_j^{-1}) \end{aligned}\]

In the implementation approach, define the cost function class EdgePoseSE3CostFunction, with the corresponding code for residual and Jacobian matrix calculation as follows.

	virtual bool Evaluate(double const* const* parameters,
						  double *residuals, double **jacobians) const
	{
		Sophus::SE3 pose_i = Sophus::SE3::exp(Vector6d(parameters[0]));
		Sophus::SE3 pose_j = Sophus::SE3::exp(Vector6d(parameters[1]));
		Sophus::SE3 estimate = pose_i.inverse() * pose_j;

		Eigen::Map<Vector6d> residual(residuals);
		residual = sqrt_information_ * ((measurment_.inverse() * estimate).log());

		if(jacobians)
		{
			if(jacobians[0]) {
				Eigen::Map<Eigen::Matrix<double,6,6, Eigen::RowMajor>> jacobian_i(jacobians[0]);
				Matrix6d J = JRInv(Sophus::SE3::exp(residual));
				jacobian_i = (-J) * pose_j.inverse().Adj();
				jacobian_i = sqrt_information_*(-J) * pose_j.inverse().Adj();
			}
			if(jacobians[1]){
				Eigen::Map<Eigen::Matrix<double,6,6, Eigen::RowMajor>> jacobian_j(jacobians[1]);
				Matrix6d J = JRInv(Sophus::SE3::exp(residual));
				jacobian_j = J * pose_j.inverse().Adj();
				jacobian_j = sqrt_information_*J * pose_j.inverse().Adj();
			}
		}
		return true;
	}

Automatic Differentiation Algorithm Based on Dual Numbers and Jets

Dual Numbers and Jets

Dual numbers extend real numbers by introducing a differential unit \(\epsilon\) with the property \(\epsilon^2=0\). A dual number is defined as:

\[a+v\epsilon\]

where it consists of two parts: the real part \(a\) and the differential part \(v\).

Based on the definition of dual numbers, it is possible to compute exact differentials of functions without explicitly providing the differential form. Specifically, for any continuously differentiable function \(f(x)\), considering the Taylor expansion of \(f(x+\epsilon)\) around \(x\), we have:

\[\begin{aligned} f(x+\epsilon)&=f(x)+Df(x)\epsilon +D^2f(x)\frac{\epsilon^2}{2!} +D^3f(x)\frac{\epsilon^3}{3!} +\cdots \\ &=f(x)+Df(x)\epsilon \end{aligned}\]

Next, extend real numbers to \(n\) differential units \(\epsilon_i,i=1,\dots,n\), satisfying \(\forall i,j:\epsilon_i\epsilon_j=0\). Define a Jet as

\[x=a+\sum_j{v_j\epsilon_j}\]

where it includes a real part \(a\) and an \(n\)-dimensional differential part \(\bf v\), represented as

\[x=a+\bf v\]

The Taylor expansion of \(f(a+{\bf v})\) yields

\[f(a+{\bf v})=f(a)+Df(a){\bf v}\]

For a multivariable function \(f:{\mathbb R}^n\rightarrow{\mathbb R}^m\), the Taylor expansion at \(x_i=a_i+{\bf v}_i,\forall i=1,\dots,n\) is given by

\[f(x_1,\dots,x_n)=f(a_1,\dots,a_n)+ \sum_i{D_if(a_1,\dots,a_n){\bf v}_i}\]

Setting \({\bf v}_i=e_i\), the basis vector for each dimension, we have

\[f(x_1,\dots,x_n)=f(a_1,\dots,a_n)+ \sum_i{D_if(a_1,\dots,a_n)\epsilon_i}\]

Based on this, the Jacobian at a given value can be obtained according to the coefficients corresponding to each differential component \(\epsilon_i\).

Ceres Implementation

The Ceres nonlinear optimization library provides an exact derivative solution based on dual numbers and Jets, known as Automatic Differentiation. First, the Jet structure needs to be defined.

template<int N> struct Jet {
  double a;
  Eigen::Matrix<double, 1, N> v;
};

template<int N> Jet<N> operator+(const Jet<N>& f, const Jet<N>& g) {
  return Jet<N>(f.a + g.a, f.v + g.v);
}

template<int N> Jet<N> operator-(const Jet<N>& f, const Jet<N>& g) {
  return Jet<N>(f.a - g.a, f.v - g.v);
}

template<int N> Jet<N> operator*(const Jet<N>& f, const Jet<N>& g) {
  return Jet<N>(f.a * g.a, f.a * g.v + f.v * g.a);
}

template<int N> Jet<N> operator/(const Jet<N>& f, const Jet<N>& g) {
  return Jet<N>(f.a / g.a, f.v / g.a - f.a * g.v / (g.a * g.a));
}

template <int N> Jet<N> exp(const Jet<N>& f) {
  return Jet<T, N>(exp(f.a), exp(f.a) * f.v);
}

// This is a simple implementation for illustration purposes, the
// actual implementation of pow requires careful handling of a number
// of corner cases.
template <int N>  Jet<N> pow(const Jet<N>& f, const Jet<N>& g) {
  return Jet<N>(pow(f.a, g.a),
                g.a * pow(f.a, g.a - 1.0) * f.v +
                pow(f.a, g.a) * log(f.a); * g.v);
}

Next, taking the Rat43 problem as an example, define the functor class as follows.

struct Rat43CostFunctor {
  Rat43CostFunctor(const double x, const double y) : x_(x), y_(y) {}

  template <typename T>
  bool operator()(const T* parameters, T* residuals) const {
    const T b1 = parameters[0];
    const T b2 = parameters[1];
    const T b3 = parameters[2];
    const T b4 = parameters[3];
    residuals[0] = b1 * pow(1.0 + exp(b2 -  b3 * x_), -1.0 / b4) - y_;
    return true;
  }

  private:
    const double x_;
    const double y_;
};

CostFunction* cost_function =
      new AutoDiffCostFunction<Rat43CostFunctor, 1, 4>(
        new Rat43CostFunctor(x, y));

Unlike the implementation of numerical differentiation algorithms, the operator() definition here uses a template form.

template <typename T> bool operator()(const T* parameters, T* residuals) const;

With this definition, the Jet type can be passed into the Rat43CostFunctor function to compute the Jacobian matrix of the error function with respect to the variables.

Performance Comparison

The Ceres nonlinear optimization library provides three methods for Jacobian computation: Analytic Derivatives, Numeric Derivatives, and Automatic Derivatives.

Taking the Rat43 problem as an example, the performance comparison of these three differentiation methods is shown in Table 3.1. Here, Rat43AutomaticDiff represents the Automatic Differentiation solution, Rat43Analytic and Rat43AnalyticOptimized are the Analytic Derivatives methods, whose implementation details can be found in Ceres - Analytic Derivatives. Rat43NumericDiffForward, Rat43NumericDiffCentral, and Rat43NumericDiffRidders represent the Numeric Derivatives methods, with their implementation details available at Ceres - Numeric Derivatives.

CostFunctionTime (ns)
Rat43Analytic255
Rat43AnalyticOptimized92
Rat43NumericDiffForward262
Rat43NumericDiffCentral517
Rat43NumericDiffRidders3760
Rat43AutomaticDiff129

Some Tips

Since this is a large sparse problem (well large for DENSE_QR anyways), one way to solve this problem is to set Solver::Options::linear_solver_type to SPARSE_NORMAL_CHOLESKY and call Solve(). And while this is a reasonable thing to do, bundle adjustment problems have a special sparsity structure that can be exploited to solve them much more efficiently. Ceres provides three specialized solvers (collectively known as Schur-based solvers) for this task. The example code uses the simplest of them DENSE_SCHUR.

Problem by default takes ownership of the cost_function, loss_function and local_parameterization pointers. These objects remain live for the life of the Problem. If the user wishes to keep control over the destruction of these objects, then they can do this by setting the corresponding enums in the Problem::Options struct.

Covariance Calculation and Propagation

Covariance Estimation Method Based on Hessian Matrix

For the linear least squares problem, assuming the objective function is

\[E(\hat{X})=(Y-M\hat{X})^T(Y-M\hat{X})\]

The optimal estimate of variable \(\hat{X}\) and its covariance is given by

\[\begin{aligned} &\hat{X}=(M^TM)^{-1}M^TY \\ &C(\hat{X})=(M^TM)^{-1}\sigma^2 \end{aligned}\]

The above equation provides an unbiased estimate of \(\sigma^2\), where \(N\) is the number of observation data, and \(\dim(\cdot)\) denotes the dimensionality of the variables.

\[s^2= \frac{E_{min}(\hat{X})}{N-\dim(X)}\]

Taking the second derivative of the above equation yields the Hessian matrix:

\[\begin{aligned} &H=\frac{\rm{d}E^2(\hat{X})}{\rm{d}\hat{X}^2}=2M^TM \\ &\rightarrow M^TM=\frac{1}{2}H \end{aligned}\]

Substituting the covariance estimation result, we obtain

\[C(\hat{X})=(\frac{1}{2}H)^{-1}\sigma^2\]

For the point cloud ICP scan matching method, its optimization objective is

\[E(\xi) =\sum^{N}_{i=1}\|\boldsymbol e_i\|^2 =\sum^{N}_{i=1}\|\boldsymbol R\boldsymbol p_i+\boldsymbol t-\boldsymbol p_i'\|^2\]

The first derivative of solving \(E(\xi)\) with respect to \(\xi\) gives

\[\frac{\partial E(\xi)}{\partial\xi} =2\sum^{N}_{i=1}\frac{\partial\boldsymbol e_i}{\partial\xi}^T\boldsymbol e_i\]

where

\[\frac{\partial\boldsymbol e_i}{\partial\xi}= \begin{bmatrix} \boldsymbol I & -\boldsymbol p_i^\wedge \end{bmatrix}\]

Based on this, solving the second derivative of \(E(\xi)\) with respect to \(\xi\) yields the Hessian matrix

\[\boldsymbol H=\frac{\partial^2E}{\partial\xi^2} =2\sum^{N}_{i=1} \begin{bmatrix} \boldsymbol I & \boldsymbol p_i^{\wedge T} \\ \boldsymbol p_i^\wedge & \boldsymbol p_i^\wedge\boldsymbol p_i^{\wedge T} \end{bmatrix}\]

Implementation

Eigen::Matrix<float,6,6> ICPRegistration::GetCovariance()
{
//	CloudData::CLOUD_PTR cloud;
//	cloud=icp_ptr_->getInputSource();
	Eigen::Matrix<float,6,6> hessian;
	hessian.setZero();
	for(int i=0;i<input_source_->size();i++)
	{
		Eigen::Vector3f p(input_source_->at(i).x,input_source_->at(i).y,input_source_->at(i).z);
		Eigen::Matrix3f p_skew=Converter::toSkewSym(p);
		hessian.block<3,3>(0,0)+=Eigen::Matrix3f::Identity();
		hessian.block<3,3>(0,3)+=-p_skew;
		hessian.block<3,3>(3,0)+=p_skew;
		hessian.block<3,3>(3,3)+=-p_skew*p_skew;
	}
	return hessian.inverse() * GetFitnessScore() / (input_source_->size()-6);
}

code: