eigen和ceres中的四元数

1.ceres中的四元数乘法函数

ceres::QuaternionRotatePoint(quantion, p, result )函数

ceres中定义了ceres::QuaternionRotatePoint(quantion, p, result )函数,就是实现四元数对点的乘法,其内部调用的是UnitQuaternionRotatePoint函数进行计算。

四元数乘法
q = ( w , x , y , z ) , q − 1 = ( w , − x , − y , − z ) , p = ( 0 , a , b , c ) p ′ = q ⊗ p ⊗ q − 1 q = (w, x, y, z), \quad q^{-1} = (w, -x, -y, -z), \quad p = (0, a, b, c) \\ p' = q \otimes p \otimes q^{-1} q=(w,x,y,z),q1=(w,x,y,z),p=(0,a,b,c)p=qpq1

q p = ( w , x , y , z ) ⊗ ( 0 , a , b , c ) = ( 0 − x a − y b − z c , w a + 0 + y c − z b , w b + 0 − x c + z a , w c + 0 + x b − y a ) \begin{aligned} qp &= (w, x, y, z) \otimes (0, a, b, c) \\ &=(0-xa-yb-zc, \quad wa+0+yc-zb, wb+0-xc+za,\quad wc+0+xb-ya) \end{aligned} qp=(w,x,y,z)(0,a,b,c)=(0xaybzc,wa+0+yczb,wb+0xc+za,wc+0+xbya)

{ q ⊗ p ⊗ q − 1 } w = − w ( x a + y b + z c ) + x ( w a + y c − z b ) + y ( w b − x c + z a ) + z ( w c + x b − y a ) = ( − w x + x w + y z − y z ) a + ( − w y − x z + y w + x z ) b + ( − w z + x y − x y + z w ) c = 0 \begin{aligned} \{q \otimes p \otimes q^{-1}\}_w =& -w(xa+yb+zc)+ x(wa+yc-zb) +y(wb-xc+za)+z(wc+xb-ya) \\ =& (-wx+xw + yz-yz)a + (-wy-xz+yw+xz)b +(-wz+xy-xy+zw)c \\ =& 0 \end{aligned} {qpq1}w===w(xa+yb+zc)+x(wa+yczb)+y(wbxc+za)+z(wc+xbya)(wx+xw+yzyz)a+(wyxz+yw+xz)b+(wz+xyxy+zw)c0

{ q ⊗ p ⊗ q − 1 } x = x ( x a + y b + z c ) + w ( w a + y c − z b ) − z ( w b − x c + z a ) + y ( w c + x b − y a ) = ( x 2 + w 2 − y 2 − z 2 ) a + 2 ( x y − w z ) b + 2 ( x z + w y ) c = ( 1 + 2 ( − y 2 − z 2 ) ) a + 2 ( x y − w z ) b + 2 ( x z + w y ) c = 2 ( − y 2 − z 2 ) a + 2 ( x y − w z ) b + 2 ( x z + w y ) c + a \begin{aligned} \{q \otimes p \otimes q^{-1}\}_x =& x(xa+yb+zc)+w(wa+yc-zb) - z(wb-xc+za)+y(wc+xb-ya) \\ =& (x^2+w^2-y^2-z^2)a + 2(xy-wz)b + 2(xz+wy)c \\ =&(1+2(-y^2-z^2))a + 2(xy-wz)b + 2(xz+wy)c \\ =& 2(-y^2-z^2)a + 2(xy-wz)b + 2(xz+wy)c + a \end{aligned} {qpq1}x====x(xa+yb+zc)+w(wa+yczb)z(wbxc+za)+y(wc+xbya)(x2+w2y2z2)a+2(xywz)b+2(xz+wy)c(1+2(y2z2))a+2(xywz)b+2(xz+wy)c2(y2z2)a+2(xywz)b+2(xz+wy)c+a

{ q ⊗ p ⊗ q − 1 } y = y ( x a + y b + z c ) + w ( w b − x c + z a ) − x ( w c + x b − y a ) + z ( w a + y c − z b ) = 2 ( x y + w z ) a + ( y 2 + w 2 − x 2 − z 2 ) b + 2 ( y z − w x ) c = 2 ( x y + w z ) a + ( 1 − 2 x 2 − 2 z 2 ) b + 2 ( y z − w x ) c = 2 ( x y + w z ) a + 2 ( − x 2 − z 2 ) b + 2 ( y z − w x ) c + b \begin{aligned} \{q \otimes p \otimes q^{-1}\}_y =& y(xa+yb+zc) + w(wb-xc+za) -x(wc+xb-ya) + z(wa+yc-zb) \\ =&2(xy+wz)a+(y^2+w^2-x^2-z^2)b + 2(yz-wx)c \\ =&2(xy+wz)a + (1-2x^2-2z^2)b + 2(yz-wx)c \\ =&2(xy+wz)a + 2(-x^2-z^2)b + 2(yz-wx)c + b \end{aligned} {qpq1}y====y(xa+yb+zc)+w(wbxc+za)x(wc+xbya)+z(wa+yczb)2(xy+wz)a+(y2+w2x2z2)b+2(yzwx)c2(xy+wz)a+(12x22z2)b+2(yzwx)c2(xy+wz)a+2(x2z2)b+2(yzwx)c+b

{ q ⊗ p ⊗ q − 1 } z = z ( x a + y b + z c ) + w ( w c + x b − y a ) − y ( w a + y c − z b ) + x ( w b − x c + z a ) = 2 ( x z − y w ) a + 2 ( y z + x w ) b + ( z 2 + w 2 − x 2 − y 2 ) c = 2 ( x z − y w ) a + 2 ( y z + x w ) b + ( 1 − 2 x 2 − 2 y 2 ) c = 2 ( x z − y w ) a + 2 ( y z + x w ) b + 2 ( − x 2 − y 2 ) c + c \begin{aligned} \{q \otimes p \otimes q^{-1}\}_z =& z(xa+yb+zc) + w(wc+xb-ya) -y(wa+yc-zb) + x(wb-xc+za) \\ =&2(xz-yw)a + 2(yz+xw)b + (z^2 + w^2 - x^2 - y^2)c \\ =& 2(xz-yw)a + 2(yz+xw)b + (1-2x^2-2y^2)c \\ =& 2(xz-yw)a + 2(yz+xw)b + 2(-x^2-y^2)c + c \end{aligned} {qpq1}z====z(xa+yb+zc)+w(wc+xbya)y(wa+yczb)+x(wbxc+za)2(xzyw)a+2(yz+xw)b+(z2+w2x2y2)c2(xzyw)a+2(yz+xw)b+(12x22y2)c2(xzyw)a+2(yz+xw)b+2(x2y2)c+c

从上面推导的计算公式可以看出两点

  • 使用虚四元数表示三维坐标点,即坐标作为四元数的虚部,实部为0。对这个坐标点构成的虚四元数执行四元数乘法,最后的结果确实是实部为0,即计算结果仍然是一个虚四元数,其虚部就代表旋转后的点的坐标
  • 表示旋转的四元数必须是单位四元数,否则从计算结果的xyz坐标可以看出来,假设不是单位四元数,那么xyzw每个系数相对单位四元数都有一个放大倍数,那么旋转后的点的坐标自然也就不正确了。

代码

template <typename T> inline
void UnitQuaternionRotatePoint(const T q[4], const T pt[3], T result[3]) {
  const T t2 =  q[0] * q[1];  //; wx
  const T t3 =  q[0] * q[2];  //; wy
  const T t4 =  q[0] * q[3];  //; wz
  const T t5 = -q[1] * q[1];  //; -x^2
  const T t6 =  q[1] * q[2];  //; xy
  const T t7 =  q[1] * q[3];  //; xz
  const T t8 = -q[2] * q[2];  //; -y^2
  const T t9 =  q[2] * q[3];  //; yz
  const T t1 = -q[3] * q[3];  //; -z^2

  //; x部分,和上面公式推导相符
  result[0] = T(2) * ((t8 + t1) * pt[0] + (t6 - t4) * pt[1] + (t3 + t7) * pt[2]) + pt[0];  // NOLINT
  //; y部分,和上面公式推导相符
  result[1] = T(2) * ((t4 + t6) * pt[0] + (t5 + t1) * pt[1] + (t9 - t2) * pt[2]) + pt[1];  // NOLINT
  //; z部分,和上面公式推导相符
  result[2] = T(2) * ((t7 - t3) * pt[0] + (t2 + t9) * pt[1] + (t5 + t8) * pt[2]) + pt[2];  // NOLINT
}

另外,另一篇博客中对这个四元数乘法计算进行了简化加速。
参考:ceres UnitQuaternionRotatePoint 利用单位四元数旋转向量的函数算法原理推导

2.eigen中的四元数初始化

参考:Eigen四元数 初始化的坑!!!一天的教训!

  • 方式1:4个标量
Quaterniond q1(1, 2, 3, 4);        // 第1种方式  实部为1 ,虚部234
  • 方式2:数组
double tmp_q = {1, 2, 3, 4}
Quaterniond q2(tmp_q);              // 第2种方式,double tmp_q[4];   实部为4 ,虚部123

这个属实是坑,看eigen中源码的定义:

/** Constructs and initializes the quaternion \f$ w+xi+yj+zk \f$ from
 * its four coefficients \a w, \a x, \a y and \a z.
 *
 * \warning Note the order of the arguments: the real \a w coefficient first,
 * while internally the coefficients are stored in the following order:
 * [\c x, \c y, \c z, \c w]
 */
Quaternion(const Scalar& w, const Scalar& x, const Scalar& y, const Scalar& z) : m_coeffs(x, y, z, w){}

/** Constructs and initialize a quaternion from the array data */
Quaternion(const Scalar* data) : m_coeffs(data) {}
  • m_coeffs:eigen中四元数存储的系数,在内存中存储的顺序是(x, y, z, w)
  • 上面第1种方式就是把四元数的4个数全给出来赋值,注意注释说的,顺序是(w, x, y, z),但是内部存储的时候系数存储顺序仍然是(x, y, z, w)。也就是这种方式只是提供了一个用户方便使用的接口。
  • 上面第2中方式则是直接给系数m_coeffs赋值,所以传入的时候顺序应该是x, y, z, w

总结

  • 尽量不要使用第二种数组赋值的方式,很容易出错。还是尽量使用显式赋值的方式,比如第1种方式。
  • 还有在vins中看到使用四元数如下,可见已经使用了double数组存储四元数,但是最后还是显式的赋值,这样不容易出错。
for (int i = 0; i < frame_num; i++)
	{
		q[i].w() = c_rotation[i][0]; 
		q[i].x() = c_rotation[i][1]; 
		q[i].y() = c_rotation[i][2]; 
		q[i].z() = c_rotation[i][3]; 
		q[i] = q[i].inverse();
		//cout << "final  q" << " i " << i <<"  " <<q[i].w() << "  " << q[i].vec().transpose() << endl;
	}
  • 如果赋值顺序错误会如何?比如上面用数组,正常数组中应该存储(x, y, z, w),但是实际存的是(w, x, y, z),那么最后看起来转成轴角的话,两个旋转轴会交叉。比如(1, 0, 0)变成(0, 1, 0);旋转角度互补,如一个60度,一个120度。
  • ceres中的四元数和eigen存储顺序不同:ceres中优化四元数的话,其存储顺序是(w, x, y, z),即实部在前;而eigen中的四元数存储顺序是(x, y, z, w),即实部在后。

3.VINS-Mono中使用四元数优化

在VINS-Mono中,VIO初始化和后端优化中都使用了ceres进行了优化,并且二者使用的四元数存储顺序还不同。

3.1.VIO初始化中视觉sfm最后做global BA

initial_sfm.cpp文件中相关部分如下,可以看到这里使用了ceres中定义好的local_parameterization,即QuaternionParameterization,也就是说这里使用的是ceres中默认定义的四元数,即实部在前的四元数,所以内存中存储的待优化的四元数也应该是实部在前的四元数。

//full BA
ceres::Problem problem;
ceres::LocalParameterization* local_parameterization = new ceres::QuaternionParameterization();
//cout << " begin full BA " << endl;
for (int i = 0; i < frame_num; i++)
{
	//double array for ceres
	c_translation[i][0] = c_Translation[i].x();
	c_translation[i][1] = c_Translation[i].y();
	c_translation[i][2] = c_Translation[i].z();
	//; 实部在前的四元数,因为用的是ceres默认的四元数localparameterization
	c_rotation[i][0] = c_Quat[i].w();  //; 给四元数的数组赋值,是wxyz的顺序
	c_rotation[i][1] = c_Quat[i].x();
	c_rotation[i][2] = c_Quat[i].y();
	c_rotation[i][3] = c_Quat[i].z();
	problem.AddParameterBlock(c_rotation[i], 4, local_parameterization);
	problem.AddParameterBlock(c_translation[i], 3);
	if (i == l)
	{
		problem.SetParameterBlockConstant(c_rotation[i]);
	}
	if (i == l || i == frame_num - 1)
	{
		problem.SetParameterBlockConstant(c_translation[i]);
	}
}

3.2.后端优化中自定义的7维位姿变量

estimator.cpp文件中相关部分如下,其中7维的位姿变量分别是3维的位置和4维的四元数。

for (int i = 0; i < WINDOW_SIZE + 1; i++)
 {
     ceres::LocalParameterization *local_parameterization = new PoseLocalParameterization();
     problem.AddParameterBlock(para_Pose[i], SIZE_POSE, local_parameterization);
     problem.AddParameterBlock(para_SpeedBias[i], SIZE_SPEEDBIAS);
 }
 for (int i = 0; i < NUM_OF_CAM; i++)
 {
     ceres::LocalParameterization *local_parameterization = new PoseLocalParameterization();
     problem.AddParameterBlock(para_Ex_Pose[i], SIZE_POSE, local_parameterization);
     if (!ESTIMATE_EXTRINSIC)
     {
         ROS_DEBUG("fix extinsic param");
         problem.SetParameterBlockConstant(para_Ex_Pose[i]);
     }
     else
         ROS_DEBUG("estimate extinsic param");
 }

其中的PoseLocalParameterization是作者自己定义的和para_Pose对应的局部参数,其定义如下。可以看到在定义加法Plus的时候,使用前3维作为平移,然后使用数组赋值的方式把内存中后4维定义为四元数,也就是说此时内存中的四元数应该是实部在后的,即xyzw的形式。和ceres默认的方式不一样

class PoseLocalParameterization : public ceres::LocalParameterization
{
    virtual bool Plus(const double *x, const double *delta, double *x_plus_delta) const;
    virtual bool ComputeJacobian(const double *x, double *jacobian) const;
    virtual int GlobalSize() const { return 7; };
    virtual int LocalSize() const { return 6; };
};

//; 注意这个位姿的排列:优化变量(x)的前3个是位置xyz, 后4个是实部在后的四元数(x, y, z, w)
bool PoseLocalParameterization::Plus(const double *x, const double *delta, double *x_plus_delta) const
{
    Eigen::Map<const Eigen::Vector3d> _p(x);

    //; 看这里四元数赋值就知道了,用的是数组赋值的方法,因此对应的x+3到最后的内存中存储的四元数顺序应该是x, y, z, w,即实部在后
    Eigen::Map<const Eigen::Quaterniond> _q(x + 3);

    Eigen::Map<const Eigen::Vector3d> dp(delta);
    
    Eigen::Quaterniond dq = Utility::deltaQ(Eigen::Map<const Eigen::Vector3d>(delta + 3));

    Eigen::Map<Eigen::Vector3d> p(x_plus_delta);
    Eigen::Map<Eigen::Quaterniond> q(x_plus_delta + 3);

    p = _p + dp;
    q = (_q * dq).normalized();

    return true;
}
bool PoseLocalParameterization::ComputeJacobian(const double *x, double *jacobian) const
{
    Eigen::Map<Eigen::Matrix<double, 7, 6, Eigen::RowMajor>> j(jacobian);
    j.topRows<6>().setIdentity();
    j.bottomRows<1>().setZero();

    return true;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值