【MSCKF】UpdaterHelper 学习备注

UpdaterHelper 学习备注

      • 一、特征点参数化与雅可比矩阵推导
        • [1. 全局3D表示(`GLOBAL_3D`)](#1. 全局3D表示(GLOBAL_3D))
        • [2. 全局逆深度表示`GLOBAL_FULL_INVERSE_DEPTH`](#2. 全局逆深度表示GLOBAL_FULL_INVERSE_DEPTH)
        • [3. 锚点坐标系下的表示](#3. 锚点坐标系下的表示)
          • (1)锚点3D表示(`ANCHORED_3D`)
          • [(2)锚点完整逆深度 `ANCHORED_FULL_INVERSE_DEPTH`](#(2)锚点完整逆深度 ANCHORED_FULL_INVERSE_DEPTH)
          • [(3)MSCKF风格逆深度 `ANCHORED_MSCKF_INVERSE_DEPTH`](#(3)MSCKF风格逆深度 ANCHORED_MSCKF_INVERSE_DEPTH)
          • [(4)单逆深度表示 `ANCHORED_INVERSE_DEPTH_SINGLE`](#(4)单逆深度表示 ANCHORED_INVERSE_DEPTH_SINGLE)
      • 二、重投影误差与残差计算
        • [1. 坐标转换链](#1. 坐标转换链)
        • [2. 投影与畸变](#2. 投影与畸变)
        • [3. Huber鲁棒核](#3. Huber鲁棒核)
      • 三、雅可比矩阵的链式组合
      • [四、零空间投影(Nullspace Projection)](#四、零空间投影(Nullspace Projection))
        • [1. 数学原理](#1. 数学原理)
        • [2. 算法实现](#2. 算法实现)
      • [五、测量压缩(Measurement Compression)](#五、测量压缩(Measurement Compression))
        • [1. 数学原理](#1. 数学原理)
        • [2. 算法实现](#2. 算法实现)
      • 六、算法流程梳理

一、特征点参数化与雅可比矩阵推导

特征点的不同参数化方式决定了其雅可比矩阵的形式,代码中支持全局3D全局逆深度锚点3D锚点逆深度(含MSCKF风格)等表示。

1. 全局3D表示(GLOBAL_3D

特征点直接用全局坐标系下的笛卡尔坐标表示:
p F G = [ x , y , z ] T p_F^G = [x, y, z]^T pFG=[x,y,z]T

雅可比矩阵为单位矩阵(特征点参数即全局坐标本身):
H f = ∂ p F G ∂ p F G = I 3 × 3 H_f = \frac{\partial p_F^G}{\partial p_F^G} = I_{3 \times 3} Hf=∂pFG∂pFG=I3×3

2. 全局逆深度表示GLOBAL_FULL_INVERSE_DEPTH

参数化: λ = [ θ , ϕ , ρ ] T \lambda = [\theta, \phi, \rho]^T λ=[θ,ϕ,ρ]T,其中:

  • θ = arctan ⁡ 2 ( y , x ) \theta = \arctan2(y, x) θ=arctan2(y,x):方位角(x-y平面投影与x轴夹角)
  • ϕ = arccos ⁡ ( z / ∥ p F G ∥ ) \phi = \arccos(z/\|p_F^G\|) ϕ=arccos(z/∥pFG∥):极角(与z轴夹角)
  • ρ = 1 / ∥ p F G ∥ \rho = 1/\|p_F^G\| ρ=1/∥pFG∥:逆深度
(1)参数到全局坐标的转换

球坐标转笛卡尔坐标( r = 1 / ρ r = 1/\rho r=1/ρ为特征点到原点的距离):
p F G = 1 ρ [ cos ⁡ θ sin ⁡ ϕ sin ⁡ θ sin ⁡ ϕ cos ⁡ ϕ ] p_F^G = \frac{1}{\rho} \begin{bmatrix} \cos\theta \sin\phi \\ \sin\theta \sin\phi \\ \cos\phi \end{bmatrix} pFG=ρ1 cosθsinϕsinθsinϕcosϕ

(2)雅可比矩阵推导

对参数 λ \lambda λ求偏导,组合为雅可比矩阵 H f = [ ∂ p F G ∂ θ , ∂ p F G ∂ ϕ , ∂ p F G ∂ ρ ] H_f = \left[ \frac{\partial p_F^G}{\partial \theta}, \frac{\partial p_F^G}{\partial \phi}, \frac{\partial p_F^G}{\partial \rho} \right] Hf=[∂θ∂pFG,∂ϕ∂pFG,∂ρ∂pFG]:

  • 对 θ \theta θ的偏导:
    ∂ p F G ∂ θ = 1 ρ [ − sin ⁡ θ sin ⁡ ϕ cos ⁡ θ sin ⁡ ϕ 0 ] \frac{\partial p_F^G}{\partial \theta} = \frac{1}{\rho} \begin{bmatrix} -\sin\theta \sin\phi \\ \cos\theta \sin\phi \\ 0 \end{bmatrix} ∂θ∂pFG=ρ1 −sinθsinϕcosθsinϕ0

  • 对 ϕ \phi ϕ的偏导:
    ∂ p F G ∂ ϕ = 1 ρ [ cos ⁡ θ cos ⁡ ϕ sin ⁡ θ cos ⁡ ϕ − sin ⁡ ϕ ] \frac{\partial p_F^G}{\partial \phi} = \frac{1}{\rho} \begin{bmatrix} \cos\theta \cos\phi \\ \sin\theta \cos\phi \\ -\sin\phi \end{bmatrix} ∂ϕ∂pFG=ρ1 cosθcosϕsinθcosϕ−sinϕ

  • 对 ρ \rho ρ的偏导:
    ∂ p F G ∂ ρ = − 1 ρ 2 [ cos ⁡ θ sin ⁡ ϕ sin ⁡ θ sin ⁡ ϕ cos ⁡ ϕ ] \frac{\partial p_F^G}{\partial \rho} = -\frac{1}{\rho^2} \begin{bmatrix} \cos\theta \sin\phi \\ \sin\theta \sin\phi \\ \cos\phi \end{bmatrix} ∂ρ∂pFG=−ρ21 cosθsinϕsinθsinϕcosϕ

展开后得到代码中的雅可比矩阵:
H f = [ − sin ⁡ θ sin ⁡ ϕ ρ cos ⁡ θ cos ⁡ ϕ ρ − cos ⁡ θ sin ⁡ ϕ ρ 2 cos ⁡ θ sin ⁡ ϕ ρ sin ⁡ θ cos ⁡ ϕ ρ − sin ⁡ θ sin ⁡ ϕ ρ 2 0 − sin ⁡ ϕ ρ − cos ⁡ ϕ ρ 2 ] H_f = \begin{bmatrix} -\frac{\sin\theta \sin\phi}{\rho} & \frac{\cos\theta \cos\phi}{\rho} & -\frac{\cos\theta \sin\phi}{\rho^2} \\ \frac{\cos\theta \sin\phi}{\rho} & \frac{\sin\theta \cos\phi}{\rho} & -\frac{\sin\theta \sin\phi}{\rho^2} \\ 0 & -\frac{\sin\phi}{\rho} & -\frac{\cos\phi}{\rho^2} \end{bmatrix} Hf= −ρsinθsinϕρcosθsinϕ0ρcosθcosϕρsinθcosϕ−ρsinϕ−ρ2cosθsinϕ−ρ2sinθsinϕ−ρ2cosϕ

3. 锚点坐标系下的表示

锚点为某帧相机 C a C_a Ca,特征点在 C a C_a Ca下的坐标为 p F C a p_F^{C_a} pFCa,需转换到全局坐标系 p F G p_F^G pFG:
p F G = R I a G ( R C a I a ( p F C a − p I a C a ) ) + p I a G p_F^G = R_{I_a}^G (R_{C_a}^{I_a} (p_F^{C_a} - p_{I_a}^{C_a}) ) + p_{I_a}^G pFG=RIaG(RCaIa(pFCa−pIaCa))+pIaG

其中:

  • R C a I a R_{C_a}^{I_a} RCaIa:IMU到锚点相机的旋转矩阵, p I a C a p_{I_a}^{C_a} pIaCa:IMU在锚点相机中的位置;
  • R I a G R_{I_a}^G RIaG:全局到锚点IMU的旋转矩阵, p I a G p_{I_a}^G pIaG:锚点IMU的全局位置。

记 R C G = R I a G R C a I a R_C^G = R_{I_a}^G R_{C_a}^{I_a} RCG=RIaGRCaIa(锚点相机到全局的旋转),则 p F G = R C G ( p F C a − p I a C a ) + p I a G p_F^G = R_C^G (p_F^{C_a} - p_{I_a}^{C_a}) + p_{I_a}^G pFG=RCG(pFCa−pIaCa)+pIaG。

(1)锚点3D表示(ANCHORED_3D

特征参数为 p F C a p_F^{C_a} pFCa,雅可比矩阵为:
H f = ∂ p F G ∂ p F C a = R C G H_f = \frac{\partial p_F^G}{\partial p_F^{C_a}} = R_C^G Hf=∂pFCa∂pFG=RCG

(2)锚点完整逆深度 ANCHORED_FULL_INVERSE_DEPTH

参数为锚点相机下的逆深度 λ a = [ θ a , ϕ a , ρ a ] T \lambda_a = [\theta_a, \phi_a, \rho_a]^T λa=[θa,ϕa,ρa]T,转换关系与全局逆深度一致:
p F C a = 1 ρ a [ cos ⁡ θ a sin ⁡ ϕ a sin ⁡ θ a sin ⁡ ϕ a cos ⁡ ϕ a ] p_F^{C_a} = \frac{1}{\rho_a} \begin{bmatrix} \cos\theta_a \sin\phi_a \\ \sin\theta_a \sin\phi_a \\ \cos\phi_a \end{bmatrix} pFCa=ρa1 cosθasinϕasinθasinϕacosϕa

雅可比矩阵为全局坐标对锚点参数的偏导(链式法则):
H f = ∂ p F G ∂ λ a = ∂ p F G ∂ p F C a ⋅ ∂ p F C a ∂ λ a = R C G ⋅ J λ a H_f = \frac{\partial p_F^G}{\partial \lambda_a} = \frac{\partial p_F^G}{\partial p_F^{C_a}} \cdot \frac{\partial p_F^{C_a}}{\partial \lambda_a} = R_C^G \cdot J_{\lambda_a} Hf=∂λa∂pFG=∂pFCa∂pFG⋅∂λa∂pFCa=RCG⋅Jλa

其中 J λ a J_{\lambda_a} Jλa是锚点相机下逆深度参数的雅可比(与全局逆深度形式相同)。

(3)MSCKF风格逆深度 ANCHORED_MSCKF_INVERSE_DEPTH

参数为 λ m = [ α , β , ρ ] T \lambda_m = [\alpha, \beta, \rho]^T λm=[α,β,ρ]T,其中 α = x / z \alpha = x/z α=x/z, β = y / z \beta = y/z β=y/z, ρ = 1 / z \rho = 1/z ρ=1/z(锚点相机下的z分量逆)。转换关系:
p F C a = 1 ρ [ α β 1 ] p_F^{C_a} = \frac{1}{\rho} \begin{bmatrix} \alpha \\ \beta \\ 1 \end{bmatrix} pFCa=ρ1 αβ1

对 λ m \lambda_m λm的偏导:
∂ p F C a ∂ λ m = [ 1 ρ 0 − α ρ 2 0 1 ρ − β ρ 2 0 0 − 1 ρ 2 ] \frac{\partial p_F^{C_a}}{\partial \lambda_m} = \begin{bmatrix} \frac{1}{\rho} & 0 & -\frac{\alpha}{\rho^2} \\ 0 & \frac{1}{\rho} & -\frac{\beta}{\rho^2} \\ 0 & 0 & -\frac{1}{\rho^2} \end{bmatrix} ∂λm∂pFCa= ρ1000ρ10−ρ2α−ρ2β−ρ21

最终雅可比:
H f = R C G ⋅ ∂ p F C a ∂ λ m H_f = R_C^G \cdot \frac{\partial p_F^{C_a}}{\partial \lambda_m} Hf=RCG⋅∂λm∂pFCa

(4)单逆深度表示 ANCHORED_INVERSE_DEPTH_SINGLE

仅参数化锚点相机下的逆深度 ρ = 1 / z \rho = 1/z ρ=1/z,方向向量 u = [ x / z , y / z , 1 ] T \mathbf{u} = [x/z, y/z, 1]^T u=[x/z,y/z,1]T固定,因此:
p F C a = 1 ρ u p_F^{C_a} = \frac{1}{\rho} \mathbf{u} pFCa=ρ1u

偏导:
∂ p F C a ∂ ρ = − 1 ρ 2 u = − p F C a ρ \frac{\partial p_F^{C_a}}{\partial \rho} = -\frac{1}{\rho^2} \mathbf{u} = -\frac{p_F^{C_a}}{\rho} ∂ρ∂pFCa=−ρ21u=−ρpFCa

最终雅可比:
H f = R C G ⋅ ∂ p F C a ∂ ρ H_f = R_C^G \cdot \frac{\partial p_F^{C_a}}{\partial \rho} Hf=RCG⋅∂ρ∂pFCa

二、重投影误差与残差计算

重投影误差是特征点观测坐标与预测坐标的差值,核心步骤如下:

1. 坐标转换链

全局坐标 p F G p_F^G pFG → 当前IMU坐标 p F I i p_F^{I_i} pFIi → 当前相机坐标 p F C i p_F^{C_i} pFCi:
p F I i = R I i G ( p F G − p I i G ) p_F^{I_i} = R_{I_i}^G (p_F^G - p_{I_i}^G) pFIi=RIiG(pFG−pIiG)
p F C i = R C i I i p F I i + p I i C i p_F^{C_i} = R_{C_i}^{I_i} p_F^{I_i} + p_{I_i}^{C_i} pFCi=RCiIipFIi+pIiCi

2. 投影与畸变
  • 归一化投影: u n o r m = [ x z , y z ] T \mathbf{u}_{norm} = \left[ \frac{x}{z}, \frac{y}{z} \right]^T unorm=[zx,zy]T( x = p F C i ( 0 ) , y = p F C i ( 1 ) , z = p F C i ( 2 ) x=p_F^{C_i}(0), y=p_F^{C_i}(1), z=p_F^{C_i}(2) x=pFCi(0),y=pFCi(1),z=pFCi(2));
  • 畸变矫正: u d i s t = D ( u n o r m , ζ ) \mathbf{u}{dist} = D(\mathbf{u}{norm}, \zeta) udist=D(unorm,ζ),其中 ζ \zeta ζ为相机内参(含畸变参数), D D D为畸变函数;
  • 残差: r = u m e a s − u d i s t r = \mathbf{u}{meas} - \mathbf{u}{dist} r=umeas−udist。
3. Huber鲁棒核

若残差 ∥ r ∥ 2 > δ \|r\|^2 > \delta ∥r∥2>δ( δ = 1. 5 2 \delta = 1.5^2 δ=1.52),应用Huber核缩放残差和雅可比:
r ′ = δ ∥ r ∥ ⋅ r r' = \sqrt{\frac{\delta}{\|r\|}} \cdot r r′=∥r∥δ ⋅r
H ′ = δ ∥ r ∥ ⋅ H H' = \sqrt{\frac{\delta}{\|r\|}} \cdot H H′=∥r∥δ ⋅H

三、雅可比矩阵的链式组合

残差对状态变量的雅可比需通过链式法则组合各环节的偏导,核心分解如下:

残差 r r r对特征参数 λ \lambda λ的雅可比:
H f = ∂ r ∂ λ = ∂ r ∂ u d i s t ⏟ − I 2 × 2 ⋅ ∂ u d i s t ∂ u n o r m ⏟ J D ⋅ ∂ u n o r m ∂ p F C i ⏟ J Π ⋅ ∂ p F C i ∂ p F G ⏟ R C i G ⋅ ∂ p F G ∂ λ ⏟ d p f g _ d l a m b d a H_f = \frac{\partial r}{\partial \lambda} = \underbrace{\frac{\partial r}{\partial \mathbf{u}{dist}}}{-I_{2×2}} \cdot \underbrace{\frac{\partial \mathbf{u}{dist}}{\partial \mathbf{u}{norm}}}{J_D} \cdot \underbrace{\frac{\partial \mathbf{u}{norm}}{\partial p_F^{C_i}}}{J\Pi} \cdot \underbrace{\frac{\partial p_F^{C_i}}{\partial p_F^G}}{R{C_i}^G} \cdot \underbrace{\frac{\partial p_F^G}{\partial \lambda}}_{dpfg\_dlambda} Hf=∂λ∂r=−I2×2 ∂udist∂r⋅JD ∂unorm∂udist⋅JΠ ∂pFCi∂unorm⋅RCiG ∂pFG∂pFCi⋅dpfg_dlambda ∂λ∂pFG

残差 r r r对系统状态 x x x(IMU位姿、相机标定)的雅可比:
H x = ∂ r ∂ x = − J D ⋅ J Π ⋅ ( ∂ p F C i ∂ x + R C i G ⋅ ∂ p F G ∂ x ) H_x = \frac{\partial r}{\partial x} = -J_D \cdot J_\Pi \cdot \left( \frac{\partial p_F^{C_i}}{\partial x} + R_{C_i}^G \cdot \frac{\partial p_F^G}{\partial x} \right) Hx=∂x∂r=−JD⋅JΠ⋅(∂x∂pFCi+RCiG⋅∂x∂pFG)

其中关键环节的雅可比:

  • 投影雅可比 J Π J_\Pi JΠ:
    J Π = [ 1 z 0 − x z 2 0 1 z − y z 2 ] J_\Pi = \begin{bmatrix} \frac{1}{z} & 0 & -\frac{x}{z^2} \\ 0 & \frac{1}{z} & -\frac{y}{z^2} \end{bmatrix} JΠ=[z100z1−z2x−z2y]

  • 当前IMU位姿的雅可比 ∂ p F C i ∂ ξ I i \frac{\partial p_F^{C_i}}{\partial \xi_{I_i}} ∂ξIi∂pFCi( ξ I i = [ ϕ I i , p I i G ] T \xi_{I_i} = [\phi_{I_i}, p_{I_i}^G]^T ξIi=[ϕIi,pIiG]T,6维):
    ∂ p F C i ∂ ξ I i = [ R C i I i [ p F I i ] × − R C i I i R I i G ] \frac{\partial p_F^{C_i}}{\partial \xi_{I_i}} = \begin{bmatrix} R_{C_i}^{I_i} [p_F^{I_i}]\times & -R{C_i}^{I_i} R_{I_i}^G \end{bmatrix} ∂ξIi∂pFCi=[RCiIi[pFIi]×−RCiIiRIiG]

  • 相机外参的雅可比 ∂ p F C i ∂ ξ C i \frac{\partial p_F^{C_i}}{\partial \xi_{C_i}} ∂ξCi∂pFCi( ξ C i = [ ϕ C i , p I i C i ] T \xi_{C_i} = [\phi_{C_i}, p_{I_i}^{C_i}]^T ξCi=[ϕCi,pIiCi]T,6维):
    ∂ p F C i ∂ ξ C i = [ [ p F C i − p I i C i ] × I 3 × 3 ] \frac{\partial p_F^{C_i}}{\partial \xi_{C_i}} = \begin{bmatrix} [p_F^{C_i} - p_{I_i}^{C_i}]\times & I{3×3} \end{bmatrix} ∂ξCi∂pFCi=[[pFCi−pIiCi]×I3×3]

四、零空间投影(Nullspace Projection)

1. 数学原理

优化目标为 min ⁡ x ∥ r ( x , f ) ∥ 2 \min_x \|r(x, f)\|^2 minx∥r(x,f)∥2,雅可比分块为 J = [ H x , H f ] J = [H_x, H_f] J=[Hx,Hf]。为消去特征参数 f f f,将残差和雅可比投影到 H f H_f Hf的左零空间 (满足 v T H f = 0 v^T H_f = 0 vTHf=0的向量空间)。

对 H f H_f Hf做QR分解: H f = Q [ R 0 ] H_f = Q \begin{bmatrix} R \\ 0 \end{bmatrix} Hf=Q[R0],其中 Q = [ Q 1 , Q 2 ] Q = [Q_1, Q_2] Q=[Q1,Q2], Q 2 Q_2 Q2为左零空间基( Q 2 T H f = 0 Q_2^T H_f = 0 Q2THf=0)。投影后:
r ′ = Q 2 T r , J ′ = Q 2 T H x r' = Q_2^T r, \quad J' = Q_2^T H_x r′=Q2Tr,J′=Q2THx

2. 算法实现

代码中通过Givens旋转 对 H f H_f Hf做QR分解,保留 Q 2 T Q_2^T Q2T对应的行(残差维度 m m m减去 f f f的维度 n n n),得到仅关于 x x x的优化问题。

五、测量压缩(Measurement Compression)

1. 数学原理

当残差维度 m > m > m>状态维度 n n n时, H x H_x Hx的秩最大为 n n n。对 H x H_x Hx做QR分解: H x = Q [ R 0 ] H_x = Q \begin{bmatrix} R \\ 0 \end{bmatrix} Hx=Q[R0],取前 n n n行( Q 1 T Q_1^T Q1T)压缩残差:
r ′ = Q 1 T r , J ′ = R r' = Q_1^T r, \quad J' = R r′=Q1Tr,J′=R

2. 算法实现

通过Givens旋转对 H x H_x Hx做QR分解,保留前 n n n行,将残差维度从 m m m降至 n n n,降低优化计算量。

六、算法流程梳理

  1. 特征参数化选择 :根据配置选择特征点的参数化方式,计算其到全局坐标的雅可比 H f H_f Hf;
  2. 残差计算:遍历所有观测帧,将特征点投影到图像平面,计算重投影残差;
  3. 雅可比组合:通过链式法则计算残差对特征参数和系统状态的雅可比;
  4. 零空间投影:消去特征参数,得到仅关于系统状态的优化问题;
  5. 测量压缩:降低残差维度,提升优化效率;
  6. 非线性优化:最小化投影后的残差,更新系统状态。

该代码实现了SLAM后端中特征点参数化、雅可比计算、约束投影的核心逻辑,通过FEJ(First Estimate Jacobian)保持线性化点一致性,结合鲁棒核和零空间投影提升优化的稳定性与效率。

cpp 复制代码
#ifndef OV_MSCKF_UPDATER_HELPER_H
#define OV_MSCKF_UPDATER_HELPER_H

#include <Eigen/Eigen>
#include <memory>

#include "common/types/LandmarkRepresentation.h"


class Type;




class State;

/**
 * @brief Class that has helper functions for our updaters.
 *
 * Can compute the Jacobian for a single feature representation.
 * This will create the Jacobian based on what representation our state is in.
 * If we are using the anchor representation then we also have additional Jacobians in respect to the anchor state.
 * Also has functions such as nullspace projection and full jacobian construction.
 * For derivations look at @ref update-feat page which has detailed equations.
 *
 */
class UpdaterHelper {
public:
  /**
   * @brief Feature object that our UpdaterHelper leverages, has all measurements and means
   */
  struct UpdaterHelperFeature {

    /// Unique ID of this feature
    size_t featid;

    /// UV coordinates that this feature has been seen from (mapped by camera ID)
    std::vector<std::vector<Eigen::Vector2f>> uvs;

    // UV normalized coordinates that this feature has been seen from (mapped by camera ID)
    std::vector<std::vector<Eigen::Vector2f>> uvs_norm;

    /// Timestamps of each UV measurement (mapped by camera ID)
    std::vector<std::vector<double>> timestamps;

    /// What representation our feature is in
    LandmarkRepresentation::Representation feat_representation;

    /// What camera ID our pose is anchored in!! By default the first measurement is the anchor.
    int anchor_cam_id = -1;

    /// Timestamp of anchor clone
    double anchor_clone_timestamp = -1;

    /// Triangulated position of this feature, in the anchor frame
    Eigen::Vector3d p_FinA;

    /// Triangulated position of this feature, in the anchor frame first estimate
    Eigen::Vector3d p_FinA_fej;

    /// Triangulated position of this feature, in the global frame
    Eigen::Vector3d p_FinG;

    /// Triangulated position of this feature, in the global frame first estimate
    Eigen::Vector3d p_FinG_fej;
  };

  /**
   * @brief This gets the feature and state Jacobian in respect to the feature representation
   *
   * @param[in] state State of the filter system
   * @param[in] feature Feature we want to get Jacobians of (must have feature means)
   * @param[out] H_f Jacobians in respect to the feature error state (will be either 3x3 or 3x1 for single depth)
   * @param[out] H_x Extra Jacobians in respect to the state (for example anchored pose)
   * @param[out] x_order Extra variables our extra Jacobian has (for example anchored pose)
   */
  static void get_feature_jacobian_representation(std::shared_ptr<State> state, UpdaterHelperFeature &feature, Eigen::MatrixXd &H_f,
                                                  std::vector<Eigen::MatrixXd> &H_x, std::vector<std::shared_ptr<Type>> &x_order);

  /**
   * @brief Will construct the "stacked" Jacobians for a single feature from all its measurements
   *
   * @param[in] state State of the filter system
   * @param[in] feature Feature we want to get Jacobians of (must have feature means)
   * @param[out] H_f Jacobians in respect to the feature error state
   * @param[out] H_x Extra Jacobians in respect to the state (for example anchored pose)
   * @param[out] res Measurement residual for this feature
   * @param[out] x_order Extra variables our extra Jacobian has (for example anchored pose)
   */
  static void get_feature_jacobian_full(std::shared_ptr<State> state, UpdaterHelperFeature &feature, Eigen::MatrixXd &H_f,
                                        Eigen::MatrixXd &H_x, Eigen::VectorXd &res, std::vector<std::shared_ptr<Type>> &x_order);

  /**
   * @brief This will project the left nullspace of H_f onto the linear system.
   *
   * Please see the @ref update-null for details on how this works.
   * This is the MSCKF nullspace projection which removes the dependency on the feature state.
   * Note that this is done **in place** so all matrices will be different after a function call.
   *
   * @param H_f Jacobian with nullspace we want to project onto the system [res = Hx*(x-xhat)+Hf(f-fhat)+n]
   * @param H_x State jacobian
   * @param res Measurement residual
   */
  static void nullspace_project_inplace(Eigen::MatrixXd &H_f, Eigen::MatrixXd &H_x, Eigen::VectorXd &res);

  /**
   * @brief This will perform measurement compression
   *
   * Please see the @ref update-compress for details on how this works.
   * Note that this is done **in place** so all matrices will be different after a function call.
   *
   * @param H_x State jacobian
   * @param res Measurement residual
   */
  static void measurement_compress_inplace(Eigen::MatrixXd &H_x, Eigen::VectorXd &res);
};



#endif // OV_MSCKF_UPDATER_HELPER_H
cpp 复制代码
#include "UpdaterHelper.h"

#include "common/state/State.h"

#include "common/utils/quat_ops.h"



/**
 * @brief 计算特征点在不同表示形式下的雅可比矩阵
 * 
 * 此函数根据特征点的不同参数化表示形式,计算相应的雅可比矩阵,用于SLAM后端的非线性优化。
 * 支持全局3D坐标、全局逆深度、锚点坐标系下的3D坐标和多种逆深度参数化形式。
 * 
 * @param state 系统状态共享指针,包含IMU状态、相机标定等信息
 * @param feature 当前处理的特征点对象
 * @param H_f 输出参数,特征点参数相对于全局坐标系的雅可比矩阵
 * @param H_x 输出参数,特征点相对于系统状态变量的雅可比矩阵向量
 * @param x_order 输出参数,状态变量的顺序列表,与H_x中的雅可比矩阵一一对应
 */
void UpdaterHelper::get_feature_jacobian_representation(std::shared_ptr<State> state, UpdaterHelperFeature &feature, Eigen::MatrixXd &H_f,
                                                        std::vector<Eigen::MatrixXd> &H_x, std::vector<std::shared_ptr<Type>> &x_order) {

  // Global XYZ representation
  if (feature.feat_representation == LandmarkRepresentation::Representation::GLOBAL_3D) {
    H_f.resize(3, 3);
    H_f.setIdentity();  // 对于全局3D表示,雅可比矩阵是单位矩阵
    return;
  }

  // Global inverse depth representation
  if (feature.feat_representation == LandmarkRepresentation::Representation::GLOBAL_FULL_INVERSE_DEPTH) {

    // 获取特征点的线性化点(FEJ或当前估计值)
    Eigen::Matrix<double, 3, 1> p_FinG = (state->_options.do_fej) ? feature.p_FinG_fej : feature.p_FinG;

    // 计算逆深度参数表示(方位角、极角和逆深度)
    double g_rho = 1 / p_FinG.norm();       // 逆深度
    double g_phi = std::acos(g_rho * p_FinG(2));  // 极角(相对于z轴的角度)
    double g_theta = std::atan2(p_FinG(1), p_FinG(0));  // 方位角(相对于x轴的角度)
    Eigen::Matrix<double, 3, 1> p_invFinG;  // 逆深度参数向量
    p_invFinG(0) = g_theta;  // 方位角
    p_invFinG(1) = g_phi;    // 极角
    p_invFinG(2) = g_rho;    // 逆深度

    // 计算三角函数值以构建雅可比矩阵
    double sin_th = std::sin(p_invFinG(0, 0));  // 方位角的正弦
    double cos_th = std::cos(p_invFinG(0, 0));  // 方位角的余弦
    double sin_phi = std::sin(p_invFinG(1, 0));  // 极角的正弦
    double cos_phi = std::cos(p_invFinG(1, 0));  // 极角的余弦
    double rho = p_invFinG(2, 0);  // 逆深度值

    // 构建全局逆深度参数化的雅可比矩阵
    // 雅可比矩阵表示全局3D坐标相对于逆深度参数的导数
    H_f.resize(3, 3);
    H_f << -(1.0 / rho) * sin_th * sin_phi, (1.0 / rho) * cos_th * cos_phi, -(1.0 / (rho * rho)) * cos_th * sin_phi,
        (1.0 / rho) * cos_th * sin_phi, (1.0 / rho) * sin_th * cos_phi, -(1.0 / (rho * rho)) * sin_th * sin_phi, 0.0,
        -(1.0 / rho) * sin_phi, -(1.0 / (rho * rho)) * cos_phi;
    return;
  }

  //======================================================================
  // 以下代码处理基于锚点的特征表示
  //======================================================================

  // 确保特征点有有效的锚点相机ID
  assert(feature.anchor_cam_id != -1);

  // 获取锚点相机的变换参数和相机标定信息
  Eigen::Matrix3d R_ItoC = state->_calib_IMUtoCAM.at(feature.anchor_cam_id)->Rot();  // IMU到相机的旋转矩阵
  Eigen::Vector3d p_IinC = state->_calib_IMUtoCAM.at(feature.anchor_cam_id)->pos();  // IMU在相机坐标系中的位置
  Eigen::Matrix3d R_GtoI = state->_clones_IMU.at(feature.anchor_clone_timestamp)->Rot();  // 全局坐标系到IMU的旋转矩阵
  Eigen::Vector3d p_IinG = state->_clones_IMU.at(feature.anchor_clone_timestamp)->pos();  // IMU在全局坐标系中的位置
  Eigen::Vector3d p_FinA = feature.p_FinA;  // 特征点在锚点坐标系中的位置

  // 如果启用FEJ(First Estimate Jacobian)技术
  if (state->_options.do_fej) {
    // 计算特征点在全局坐标系中的最佳估计位置(使用当前状态估计)
    // 变换步骤:锚点相机坐标系 -> IMU坐标系 -> 全局坐标系
    Eigen::Vector3d p_FinG_best = R_GtoI.transpose() * R_ItoC.transpose() * (feature.p_FinA - p_IinC) + p_IinG;
    
    // 使用FEJ的锚点状态(旋转和位置)重新计算特征点在锚点相机坐标系中的位置
    // 这样可以保持雅可比矩阵在初始估计处的线性化,提高优化一致性
    R_GtoI = state->_clones_IMU.at(feature.anchor_clone_timestamp)->Rot_fej();  // 使用FEJ旋转矩阵
    p_IinG = state->_clones_IMU.at(feature.anchor_clone_timestamp)->pos_fej();  // 使用FEJ位置
    
    // 将全局坐标系中的特征点位置转换回锚点相机坐标系
    // 变换步骤:全局坐标系 -> IMU坐标系 -> 锚点相机坐标系
    p_FinA = (R_GtoI.transpose() * R_ItoC.transpose()).transpose() * (p_FinG_best - p_IinG) + p_IinC;
  }
  
  // 计算相机坐标系到全局坐标系的旋转矩阵
  Eigen::Matrix3d R_CtoG = R_GtoI.transpose() * R_ItoC.transpose();

  // 计算特征点相对于锚点IMU位姿的雅可比矩阵
  Eigen::Matrix<double, 3, 6> H_anc;  // 3个位置维度对6个IMU位姿参数的雅可比
  H_anc.block(0, 0, 3, 3).noalias() = -R_GtoI.transpose() * skew_x(R_ItoC.transpose() * (p_FinA - p_IinC));  // 旋转部分雅可比
  H_anc.block(0, 3, 3, 3).setIdentity();  // 平移部分雅可比为单位矩阵

  // 将锚点IMU位姿的雅可比添加到输出列表中
  x_order.push_back(state->_clones_IMU.at(feature.anchor_clone_timestamp));
  H_x.push_back(H_anc);

  // 如果需要计算相机标定参数的雅可比
  if (state->_options.do_calib_camera_pose) {
    Eigen::Matrix<double, 3, 6> H_calib;  // 特征点位置对相机标定参数的雅可比
    H_calib.block(0, 0, 3, 3).noalias() = -R_CtoG * skew_x(p_FinA - p_IinC);  // 相机旋转标定参数的雅可比
    H_calib.block(0, 3, 3, 3) = -R_CtoG;  // 相机平移标定参数的雅可比
    
    // 将相机标定参数的雅可比添加到输出列表
    x_order.push_back(state->_calib_IMUtoCAM.at(feature.anchor_cam_id));
    H_x.push_back(H_calib);
  }

  // 处理锚点坐标系下的3D特征表示
  if (feature.feat_representation == LandmarkRepresentation::Representation::ANCHORED_3D) {
    H_f = R_CtoG;  // 雅可比矩阵即为相机到全局的旋转矩阵
    return;
  }

  // 处理锚点坐标系下的完整逆深度表示
  if (feature.feat_representation == LandmarkRepresentation::Representation::ANCHORED_FULL_INVERSE_DEPTH) {

    // 计算锚点坐标系中的逆深度参数表示
    double a_rho = 1 / p_FinA.norm();  // 逆深度
    double a_phi = std::acos(a_rho * p_FinA(2));  // 极角
    double a_theta = std::atan2(p_FinA(1), p_FinA(0));  // 方位角
    Eigen::Matrix<double, 3, 1> p_invFinA;  // 逆深度参数向量
    p_invFinA(0) = a_theta;
    p_invFinA(1) = a_phi;
    p_invFinA(2) = a_rho;

    // 计算三角函数值用于构建雅可比矩阵
    double sin_th = std::sin(p_invFinA(0, 0));
    double cos_th = std::cos(p_invFinA(0, 0));
    double sin_phi = std::sin(p_invFinA(1, 0));
    double cos_phi = std::cos(p_invFinA(1, 0));
    double rho = p_invFinA(2, 0);

    // 构建锚点坐标系下特征点3D位置相对于逆深度参数的雅可比矩阵
    Eigen::Matrix<double, 3, 3> d_pfinA_dpinv;
    d_pfinA_dpinv << -(1.0 / rho) * sin_th * sin_phi, (1.0 / rho) * cos_th * cos_phi, -(1.0 / (rho * rho)) * cos_th * sin_phi,
        (1.0 / rho) * cos_th * sin_phi, (1.0 / rho) * sin_th * cos_phi, -(1.0 / (rho * rho)) * sin_th * sin_phi, 0.0,
        -(1.0 / rho) * sin_phi, -(1.0 / (rho * rho)) * cos_phi;
    
    // 最终雅可比矩阵:全局坐标系中的特征点位置相对于逆深度参数的导数
    H_f = R_CtoG * d_pfinA_dpinv;
    return;
  }

  // 处理MSCKF风格的逆深度表示
  if (feature.feat_representation == LandmarkRepresentation::Representation::ANCHORED_MSCKF_INVERSE_DEPTH) {

    // 计算MSCKF风格的逆深度参数(alpha, beta, rho)
    Eigen::Matrix<double, 3, 1> p_invFinA_MSCKF;
    p_invFinA_MSCKF(0) = p_FinA(0) / p_FinA(2);  // alpha = x/z
    p_invFinA_MSCKF(1) = p_FinA(1) / p_FinA(2);  // beta = y/z
    p_invFinA_MSCKF(2) = 1 / p_FinA(2);  // rho = 1/z

    // 提取参数值
    double alpha = p_invFinA_MSCKF(0, 0);
    double beta = p_invFinA_MSCKF(1, 0);
    double rho = p_invFinA_MSCKF(2, 0);

    // 构建MSCKF逆深度参数化的雅可比矩阵
    Eigen::Matrix<double, 3, 3> d_pfinA_dpinv;
    d_pfinA_dpinv << (1.0 / rho), 0.0, -(1.0 / (rho * rho)) * alpha,
        0.0, (1.0 / rho), -(1.0 / (rho * rho)) * beta,
        0.0, 0.0, -(1.0 / (rho * rho));
    
    // 最终雅可比矩阵:全局坐标系中的特征点位置相对于MSCKF逆深度参数的导数
    H_f = R_CtoG * d_pfinA_dpinv;
    return;
  }

  /// 处理仅估计单一深度值的情况(假设初始方向已知)
  if (feature.feat_representation == LandmarkRepresentation::Representation::ANCHORED_INVERSE_DEPTH_SINGLE) {

    // 计算逆深度和方向向量
    double rho = 1.0 / p_FinA(2);  // 逆深度(z轴分量的倒数)
    Eigen::Vector3d bearing = rho * p_FinA;  // 单位方向向量(乘以rho后)

    // 构建单一深度参数化的雅可比矩阵
    Eigen::Vector3d d_pfinA_drho;
    d_pfinA_drho << -(1.0 / (rho * rho)) * bearing;  // 位置对逆深度参数的导数
    
    // 最终雅可比矩阵:全局坐标系中的特征点位置相对于单一逆深度参数的导数
    H_f = R_CtoG * d_pfinA_drho;
    return;
  }

  // 如果执行到这里,说明特征表示类型无效
  assert(false);
}


double huberA = 1.5;
double huberB = huberA * huberA;
bool use_Loss = true;

/**
 * @brief 计算特征点的完整雅可比矩阵和重投影误差残差
 * 
 * 该函数是SLAM后端优化的核心组件,负责计算特征点在所有观测帧中的重投影误差,
 * 以及这些误差对特征参数和系统状态变量的雅可比矩阵,为非线性优化提供梯度信息。
 * 
 * @param state 系统状态,包含IMU克隆状态、相机内外参等信息
 * @param feature 特征点信息,包含多相机的观测、时间戳、特征表示类型等
 * @param H_f 输出参数,残差对特征点参数的雅可比矩阵
 * @param H_x 输出参数,残差对系统状态变量的雅可比矩阵
 * @param res 输出参数,重投影误差残差向量
 * @param x_order 输出参数,状态变量的排序顺序,用于雅可比矩阵列索引映射
 */
void UpdaterHelper::get_feature_jacobian_full(std::shared_ptr<State> state, UpdaterHelperFeature &feature, Eigen::MatrixXd &H_f,
                                              Eigen::MatrixXd &H_x, Eigen::VectorXd &res, std::vector<std::shared_ptr<Type>> &x_order) {

  // 统计当前特征点在所有相机中的总观测次数
  int total_meas = 0;
  const std::vector<std::vector<double>>& ts_all = feature.timestamps;
  for (size_t cam_id = 0; cam_id < ts_all.size(); ++cam_id) {
    total_meas += (int)ts_all[cam_id].size();
  }

  // 计算与当前特征点相关的所有状态变量的总大小
  int total_hx = 0;
  // 建立状态变量到雅可比矩阵列索引的映射,用于快速定位状态变量在雅可比中的位置
  std::unordered_map<std::shared_ptr<Type>, size_t> map_hx;
  // 遍历所有相机的观测
  for (size_t cam_id = 0; cam_id < ts_all.size(); ++cam_id) {

    // 获取当前相机与IMU之间的外参和内参
    std::shared_ptr<PoseJPL> calibration = state->_calib_IMUtoCAM.at(cam_id);
    std::shared_ptr<Vec> distortion = state->_cam_intrinsics.at(cam_id);

    // 如果启用相机外参在线标定,将外参添加到状态变量中
    if (state->_options.do_calib_camera_pose) {
      map_hx.insert({calibration, total_hx});
      x_order.push_back(calibration);
      total_hx += calibration->size(); // 外参大小为6(3旋转+3平移)
    }

    // 如果启用相机内参在线标定,将内参添加到状态变量中
    if (state->_options.do_calib_camera_intrinsics) {
      map_hx.insert({distortion, total_hx});
      x_order.push_back(distortion);
      total_hx += distortion->size(); // 内参大小根据相机模型而定
    }

    // 遍历当前相机的所有观测
    for (size_t m = 0; m < feature.timestamps[cam_id].size(); ++m) {

      // 获取对应时间戳的IMU克隆状态,如果尚未添加到状态变量列表中,则添加
      std::shared_ptr<PoseJPL> clone_Ci = state->_clones_IMU.at(feature.timestamps[cam_id][m]);
      if (map_hx.find(clone_Ci) == map_hx.end()) {
        map_hx.insert({clone_Ci, total_hx});
        x_order.push_back(clone_Ci);
        total_hx += clone_Ci->size(); // IMU位姿大小为6(3旋转+3平移)
      }
    }
  }

  // 如果特征点使用基于锚点的相对表示,确保锚点状态也被添加到状态变量中
  if (LandmarkRepresentation::is_relative_representation(feature.feat_representation)) {

    // 确保特征点有有效的锚点相机ID
    assert(feature.anchor_cam_id != -1);

    // 添加锚点IMU克隆状态(如果尚未添加)
    std::shared_ptr<PoseJPL> clone_Ai = state->_clones_IMU.at(feature.anchor_clone_timestamp);
    if (map_hx.find(clone_Ai) == map_hx.end()) {
      map_hx.insert({clone_Ai, total_hx});
      x_order.push_back(clone_Ai);
      total_hx += clone_Ai->size();
    }

    // 如果启用相机外参标定,也需要添加锚点相机的外参
    if (state->_options.do_calib_camera_pose) {
      std::shared_ptr<PoseJPL> clone_calib = state->_calib_IMUtoCAM.at(feature.anchor_cam_id);
      if (map_hx.find(clone_calib) == map_hx.end()) {
        map_hx.insert({clone_calib, total_hx});
        x_order.push_back(clone_calib);
        total_hx += clone_calib->size();
      }
    }
  }

  //=========================================================================
  // 计算特征点在全局坐标系中的位置
  //=========================================================================

  // 获取特征点的全局位置(如果是绝对表示则直接使用)
  Eigen::Vector3d p_FinG = feature.p_FinG;
  // 如果特征点使用基于锚点的相对表示,则需要从锚点相机坐标系转换到全局坐标系
  if (LandmarkRepresentation::is_relative_representation(feature.feat_representation)) {
    // 确保有有效的锚点
    assert(feature.anchor_cam_id != -1);
    // 获取锚点相机与IMU之间的外参
    Eigen::Matrix3d R_ItoC = state->_calib_IMUtoCAM.at(feature.anchor_cam_id)->Rot(); // IMU到相机的旋转矩阵
    Eigen::Vector3d p_IinC = state->_calib_IMUtoCAM.at(feature.anchor_cam_id)->pos(); // IMU在相机坐标系中的位置
    // 获取锚点时刻的IMU位姿
    Eigen::Matrix3d R_GtoI = state->_clones_IMU.at(feature.anchor_clone_timestamp)->Rot(); // 全局到IMU的旋转矩阵
    Eigen::Vector3d p_IinG = state->_clones_IMU.at(feature.anchor_clone_timestamp)->pos(); // IMU在全局坐标系中的位置
    // 将特征点从锚点相机坐标系转换到全局坐标系
    // 转换过程:锚点相机坐标系 → IMU坐标系 → 全局坐标系
    p_FinG = R_GtoI.transpose() * R_ItoC.transpose() * (feature.p_FinA - p_IinC) + p_IinG;
  }

  // 计算特征点在FEJ(First Estimate Jacobian)状态下的全局位置
  // 如果是基于锚点的表示,则FEJ位置与当前计算的位置相同,因为锚点参数不参与FEJ
  Eigen::Vector3d p_FinG_fej = feature.p_FinG_fej;
  if (LandmarkRepresentation::is_relative_representation(feature.feat_representation)) {
    p_FinG_fej = p_FinG;
  }

  //=========================================================================
  // 初始化残差和雅可比矩阵
  //=========================================================================
  // 计数器,用于跟踪当前处理的测量索引
  int c = 0;

  // 根据特征点表示类型确定雅可比矩阵的列数
  // 大多数表示使用3个参数,只有单参数逆深度表示使用1个参数
  int jacobsize = (feature.feat_representation != LandmarkRepresentation::Representation::ANCHORED_INVERSE_DEPTH_SINGLE) ? 3 : 1;

  // 初始化残差向量,每个观测贡献2个残差(x和y坐标)
  res = Eigen::VectorXd::Zero(2 * total_meas);

  // 初始化特征点参数的雅可比矩阵
  H_f = Eigen::MatrixXd::Zero(2 * total_meas, jacobsize);

  // 初始化状态变量的雅可比矩阵
  H_x = Eigen::MatrixXd::Zero(2 * total_meas, total_hx);

  // 计算特征点全局位置对特征参数的导数
  // 这个计算只需要执行一次,因此提取到循环外以提高效率
  Eigen::MatrixXd dpfg_dlambda;
  std::vector<Eigen::MatrixXd> dpfg_dx;
  std::vector<std::shared_ptr<Type>> dpfg_dx_order;
  // 调用辅助函数计算特征表示雅可比(注意:这里原始代码中缺少第二个参数feature)
  UpdaterHelper::get_feature_jacobian_representation(state, feature, dpfg_dlambda, dpfg_dx, dpfg_dx_order);

  // 调试断言:确保所有在 dpfg_dx_order 中的状态变量都在map_hx中有对应的映射
#ifndef NDEBUG
  for (auto &type : dpfg_dx_order) {
    assert(map_hx.find(type) != map_hx.end());
  }
#endif

  //=========================================================================
  // 遍历所有相机观测,计算残差和雅可比矩阵
  //=========================================================================
  // 遍历每个相机
  for (size_t cam_id = 0; cam_id < ts_all.size(); ++cam_id) {

    // 获取当前相机的内参和与IMU的外参
    std::shared_ptr<Vec> distortion = state->_cam_intrinsics.at(cam_id);
    std::shared_ptr<PoseJPL> calibration = state->_calib_IMUtoCAM.at(cam_id);
    Eigen::Matrix3d R_ItoC = calibration->Rot();  // IMU到相机的旋转矩阵
    Eigen::Vector3d p_IinC = calibration->pos();  // IMU在相机坐标系中的位置

    // 遍历当前相机的所有观测
    for (size_t m = 0; m < feature.timestamps[cam_id].size(); ++m) {

      //=====================================================================
      // 计算特征点在当前相机坐标系中的投影并计算残差
      //=====================================================================

      // 获取当前观测时刻的IMU克隆状态
      std::shared_ptr<PoseJPL> clone_Ii = state->_clones_IMU.at(feature.timestamps[cam_id][m]);
      Eigen::Matrix3d R_GtoIi = clone_Ii->Rot(); // 全局到当前IMU的旋转矩阵
      Eigen::Vector3d p_IiinG = clone_Ii->pos(); // 当前IMU在全局坐标系中的位置

      // 将特征点从全局坐标系转换到当前IMU坐标系
      Eigen::Vector3d p_FinIi = R_GtoIi * (p_FinG - p_IiinG);

      // 将特征点从IMU坐标系转换到相机坐标系
      Eigen::Vector3d p_FinCi = R_ItoC * p_FinIi + p_IinC;
      // 计算归一化图像坐标(投影到归一化平面)
      Eigen::Vector2d uv_norm;
      uv_norm << p_FinCi(0) / p_FinCi(2), p_FinCi(1) / p_FinCi(2);

      // 应用相机畸变模型(支持鱼眼等畸变模型)
      Eigen::Vector2d uv_dist;
      float uv_norm_array[2] = {uv_norm.cast<float>()[0], uv_norm.cast<float>()[1]};
      float uv_dist_array[2];
      // 使用相机模型计算畸变后的坐标
      cam_equi_distort_f(state->_cam_intrinsics_cameras.at(cam_id).get(), uv_norm_array, uv_dist_array);
      uv_dist = Eigen::Vector2f(uv_dist_array[0], uv_dist_array[1]).cast<double>();

      // 计算重投影误差残差(观测值减去预测值)
      Eigen::Vector2d uv_m((double)feature.uvs[cam_id][m](0), (double)feature.uvs[cam_id][m](1));
      res.block(2 * c, 0, 2, 1) = uv_m - uv_dist;

      // 应用Huber鲁棒核函数,减小异常值对优化的影响
      double huber_scale = 1.0;
      if (use_Loss) {
          const double r_l2 = res.squaredNorm();  // 计算残差的L2范数平方
          if (r_l2 > huberB) {  // 如果残差大于阈值,应用Huber核
              const double radius = sqrt(r_l2);  // 残差的L2范数
              // 计算Huber缩放因子,避免除零错误
              double rho1 = std::max(std::numeric_limits<double>::min(), huberA / radius);
              huber_scale = sqrt(rho1);  // 对残差应用平方根缩放
              res *= huber_scale;  // 缩放残差
          }
      }

      //=====================================================================
      // 应用FEJ(First Estimate Jacobian)技术
      //=====================================================================

      // 如果启用了FEJ,则使用首次估计的状态来计算雅可比,以保持线性化点的一致性
      if (state->_options.do_fej) {
        // 使用FEJ状态下的IMU旋转和平移
        R_GtoIi = clone_Ii->Rot_fej();  // 首次估计的旋转矩阵
        p_IiinG = clone_Ii->pos_fej();  // 首次估计的位置
        // 注意:注释掉的部分表示相机外参也可以使用FEJ,但在这里未启用
        // R_ItoC = calibration->Rot_fej();
        // p_IinC = calibration->pos_fej();
        // 使用FEJ状态重新计算特征点位置
        p_FinIi = R_GtoIi * (p_FinG_fej - p_IiinG);
        p_FinCi = R_ItoC * p_FinIi + p_IinC;
        // 注意:这里不重新计算uv_norm,因为FEJ只影响雅可比计算
        // uv_norm << p_FinCi(0)/p_FinCi(2),p_FinCi(1)/p_FinCi(2);
      }

      //=====================================================================
      // 计算各层雅可比矩阵
      //=====================================================================

      // 计算畸变对归一化坐标的雅可比,以及对相机内参的雅可比
      float uv_norm_jacobian_array[2] = {uv_norm.cast<float>()[0], uv_norm.cast<float>()[1]};
      float H_dz_dzn[4];  // 畸变对归一化坐标的雅可比
      float H_dz_dzeta[16];  // 畸变对内参的雅可比
      // 调用相机模型计算畸变雅可比
      cam_equi_compute_distort_jacobian(state->_cam_intrinsics_cameras.at(cam_id).get(), 
                                       uv_norm_jacobian_array, H_dz_dzn, H_dz_dzeta);
      
      // 将C风格数组转换为Eigen矩阵
      Eigen::MatrixXd dz_dzn(2, 2);  // 2×2矩阵,畸变对归一化坐标的雅可比
      dz_dzn << H_dz_dzn[0], H_dz_dzn[1],
                H_dz_dzn[2], H_dz_dzn[3];
      
      Eigen::MatrixXd dz_dzeta(2, 8);  // 2×8矩阵,畸变对内参的雅可比(最多8个内参参数)
      dz_dzeta << H_dz_dzeta[0], H_dz_dzeta[1], H_dz_dzeta[2], H_dz_dzeta[3], 
                 H_dz_dzeta[4], H_dz_dzeta[5], H_dz_dzeta[6], H_dz_dzeta[7],
                 H_dz_dzeta[8], H_dz_dzeta[9], H_dz_dzeta[10], H_dz_dzeta[11], 
                 H_dz_dzeta[12], H_dz_dzeta[13], H_dz_dzeta[14], H_dz_dzeta[15];

      // 计算归一化图像坐标对相机坐标系中特征点的雅可比
      Eigen::MatrixXd dzn_dpfc = Eigen::MatrixXd::Zero(2, 3);  // 2×3矩阵
      // 透视投影的导数,z是特征点在相机坐标系中的深度
      dzn_dpfc << 1 / p_FinCi(2), 0, -p_FinCi(0) / (p_FinCi(2) * p_FinCi(2)), 
                  0, 1 / p_FinCi(2), -p_FinCi(1) / (p_FinCi(2) * p_FinCi(2));
      
      // 如果使用了Huber核,也要缩放雅可比矩阵
      if (use_Loss) {
          dzn_dpfc *= huber_scale;
      }

      // 计算相机坐标系中特征点对全局坐标系中特征点的雅可比
      // 这是一个旋转变换的导数
      Eigen::MatrixXd dpfc_dpfg = R_ItoC * R_GtoIi;  // 3×3矩阵

      // 计算相机坐标系中特征点对IMU位姿的雅可比(旋转部分和平移部分)
      Eigen::MatrixXd dpfc_dclone = Eigen::MatrixXd::Zero(3, 6);  // 3×6矩阵(3旋转+3平移)
      // 对旋转部分的导数:使用反对称矩阵表示旋转的扰动
      dpfc_dclone.block(0, 0, 3, 3).noalias() = R_ItoC * skew_x(p_FinIi);
      // 对平移部分的导数:等于负的dpfc_dpfg
      dpfc_dclone.block(0, 3, 3, 3) = -dpfc_dpfg;

      //=====================================================================
      // 应用链式法则组合雅可比矩阵
      //=====================================================================

      // 预计算部分雅可比组合
      Eigen::MatrixXd dz_dpfc = dz_dzn * dzn_dpfc;  // 2×3矩阵,观测对相机坐标系点的雅可比
      Eigen::MatrixXd dz_dpfg = dz_dpfc * dpfc_dpfg;  // 2×3矩阵,观测对全局坐标系点的雅可比

      // 链式法则:计算残差对特征参数的总雅可比
      H_f.block(2 * c, 0, 2, H_f.cols()).noalias() = dz_dpfg * dpfg_dlambda;

      // 链式法则:计算残差对当前IMU位姿的雅可比
      H_x.block(2 * c, map_hx[clone_Ii], 2, clone_Ii->size()).noalias() = dz_dpfc * dpfc_dclone;

      // 链式法则:计算残差对其他相关状态变量的雅可比(如锚点状态)
      // 注意:这里使用加法是因为可能存在多个贡献来源
      for (size_t i = 0; i < dpfg_dx_order.size(); i++) {
        H_x.block(2 * c, map_hx[dpfg_dx_order.at(i)], 2, dpfg_dx_order.at(i)->size()).noalias() += 
            dz_dpfg * dpfg_dx.at(i);
      }

      //=====================================================================
      // 计算相机标定参数的雅可比(如果启用)
      //=====================================================================

      // 计算残差对相机外参的雅可比(如果启用外参标定)
      if (state->_options.do_calib_camera_pose) {

        // 计算相机坐标系中特征点对相机外参的雅可比
        Eigen::MatrixXd dpfc_dcalib = Eigen::MatrixXd::Zero(3, 6);  // 3×6矩阵(3旋转+3平移)
        // 对旋转部分的导数:使用反对称矩阵
        dpfc_dcalib.block(0, 0, 3, 3) = skew_x(p_FinCi - p_IinC);
        // 对平移部分的导数:单位矩阵
        dpfc_dcalib.block(0, 3, 3, 3) = Eigen::Matrix<double, 3, 3>::Identity();

        // 链式法则:组合得到残差对外参的雅可比
        H_x.block(2 * c, map_hx[calibration], 2, calibration->size()).noalias() += 
            dz_dpfc * dpfc_dcalib;
      }

      // 计算残差对相机内参的雅可比(如果启用内参标定)
      if (state->_options.do_calib_camera_intrinsics) {
        H_x.block(2 * c, map_hx[distortion], 2, distortion->size()) = dz_dzeta;
      }

      // 更新计数器,处理下一个观测
      c++;
    }
  }
}

/**
 * @brief 将雅可比矩阵和残差向量投影到特征点参数雅可比H_f的左零空间中
 * 
 * 此函数实现了一个重要的投影操作,用于MSCKF或滑动窗口优化中消除特征点参数化带来的不确定性。
 * 通过将优化问题投影到H_f的左零空间,我们可以移除特征点参数相关的自由度,直接优化系统状态。
 * 
 * 数学原理:对于观测方程r = h(x, f),其中x是系统状态,f是特征点参数,我们可以将雅可比矩阵分块为[H_x H_f]。
 * 左零空间投影可以构建一个新的优化问题,仅包含系统状态x的信息,同时保留原始问题的所有约束。
 * 
 * @param H_f 特征点参数的雅可比矩阵,形状为[残差维度×特征参数维度]
 * @param H_x 系统状态变量的雅可比矩阵,形状为[残差维度×状态变量维度]
 * @param res 重投影误差残差向量,形状为[残差维度×1]
 */
void UpdaterHelper::nullspace_project_inplace(Eigen::MatrixXd &H_f, Eigen::MatrixXd &H_x, Eigen::VectorXd &res) {

  // 对H_f应用左零空间投影到所有变量
  // 基于《Matrix Computations》第4版(Golub和Van Loan著)
  // 详见第252页,算法5.2.4了解这两个循环的工作原理
  // 注意:书中使用matlab索引表示法,因此我们需要从所有索引中减去1
  Eigen::JacobiRotation<double> tempHo_GR;  // 用于存储Givens旋转矩阵
  
  // 使用Givens旋转对 H_f 进行QR分解
  for (int n = 0; n < H_f.cols(); ++n) {  // 遍历每一列
    for (int m = (int)H_f.rows() - 1; m > n; m--) {  // 从下往上遍历每一行
      // 创建一个Givens旋转矩阵,用于消除H_f(m, n)元素
      tempHo_GR.makeGivens(H_f(m - 1, n), H_f(m, n));
      
      // 将Givens旋转应用到H_f、H_x和res的相应行
      // 注意:我们只需要将G应用到非零列[n:Ho.cols()-n-1],
      // 这等效于将G应用到整个列[0:Ho.cols()-1]
      (H_f.block(m - 1, n, 2, H_f.cols() - n)).applyOnTheLeft(0, 1, tempHo_GR.adjoint());
      (H_x.block(m - 1, 0, 2, H_x.cols())).applyOnTheLeft(0, 1, tempHo_GR.adjoint());
      (res.block(m - 1, 0, 2, 1)).applyOnTheLeft(0, 1, tempHo_GR.adjoint());
    }
  }

  // H_f雅可比矩阵的最大秩为3(如果它表示3D位置),因此左零空间的大小为Hf.rows()-3
  // 注意:这里需要使用eigen3的eval()方法来避免别名问题!
  // H_f = H_f.block(H_f.cols(),0,H_f.rows()-H_f.cols(),H_f.cols()).eval();  // 注释掉的代码:特征参数雅可比在投影后通常不需要保留
  
  // 提取H_x和res中对应左零空间的部分
  // 投影后的H_x只包含与系统状态相关且独立于特征参数的信息
  H_x = H_x.block(H_f.cols(), 0, H_x.rows() - H_f.cols(), H_x.cols()).eval();
  res = res.block(H_f.cols(), 0, res.rows() - H_f.cols(), res.cols()).eval();

  // 验证代码:确保投影后的H_x和res行数一致
  assert(H_x.rows() == res.rows());
}

/**
 * @brief 执行测量压缩操作,减小雅可比矩阵和残差向量的维度
 * 
 * 此函数实现了MSCKF等算法中的测量压缩技术,通过QR分解将高维测量信息压缩为低维等价形式,
 * 同时保留原始测量的所有约束信息。这可以显著降低后续优化计算的计算复杂度。
 * 
 * 测量压缩的核心思想是:当残差维度(H_x.rows())大于状态变量维度(H_x.cols())时,
 * 我们可以通过投影操作将问题转换为等价的低维形式,而不会丢失任何信息。
 * 
 * @param H_x 系统状态变量的雅可比矩阵,形状为[残差维度×状态变量维度],将被原地修改
 * @param res 重投影误差残差向量,形状为[残差维度×1],将被原地修改
 */
void UpdaterHelper::measurement_compress_inplace(Eigen::MatrixXd &H_x, Eigen::VectorXd &res) {

  // 如果H_x是一个宽矩阵(行数<=列数),则不需要压缩,直接返回
  // 只有当观测残差维度大于状态变量维度时,压缩才会带来计算效率提升
  if (H_x.rows() <= H_x.cols())
    return;

  // 使用Givens旋转执行测量压缩
  // 基于《Matrix Computations》第4版(Golub和Van Loan著)
  // 详见第252页,算法5.2.4了解这两个循环的工作原理
  // 注意:书中使用matlab索引表示法,因此我们需要从所有索引中减去1
  Eigen::JacobiRotation<double> tempHo_GR;  // 用于存储Givens旋转矩阵
  
  // 使用Givens旋转对H_x进行QR分解
  for (int n = 0; n < H_x.cols(); n++) {  // 遍历每一列
    for (int m = (int)H_x.rows() - 1; m > n; m--) {  // 从下往上遍历每一行
      // 创建一个Givens旋转矩阵,用于消除H_x(m, n)元素
      tempHo_GR.makeGivens(H_x(m - 1, n), H_x(m, n));
      
      // 将Givens旋转应用到H_x和res的相应行
      // 注意:我们只需要将G应用到非零列[n:Ho.cols()-n-1],
      // 这等效于将G应用到整个列[0:Ho.cols()-1]
      (H_x.block(m - 1, n, 2, H_x.cols() - n)).applyOnTheLeft(0, 1, tempHo_GR.adjoint());
      (res.block(m - 1, 0, 2, 1)).applyOnTheLeft(0, 1, tempHo_GR.adjoint());
    }
  }

  // 确定压缩后的矩阵大小
  // 如果H是宽矩阵,则使用所有行;否则使用与状态变量维度相同的大小
  int r = std::min(H_x.rows(), H_x.cols());

  // 构建压缩后的雅可比矩阵和残差向量
  // 经过QR分解后,只有前r行包含有效信息,其余行可以丢弃
  assert(r <= H_x.rows());  // 验证r不超过原始行数
  H_x.conservativeResize(r, H_x.cols());  // 保留前r行,其余行被移除
  res.conservativeResize(r, res.cols());   // 相应地调整残差向量大小
}
相关推荐
西岸行者13 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意13 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码13 天前
嵌入式学习路线
学习
毛小茛13 天前
计算机系统概论——校验码
学习
babe小鑫13 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms13 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下13 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。13 天前
2026.2.25监控学习
学习
im_AMBER13 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J13 天前
从“Hello World“ 开始 C++
c语言·c++·学习