• 正文
  • 相关推荐
申请入驻 产业图谱

十分钟读论文 | DCReg:把退化LiDAR配准拆成可诊断、可求解的三个步骤

4小时前
191
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

转载自公众号:敢敢AUTOHUB

0. 简介

DCReg 面向走廊、隧道、矿洞和空旷停车场中的 LiDAR 点云配准,处理的是几何约束不足时六自由度位姿求解变得病态的问题。它先用 Schur 补把旋转与平移的观测性拆开,再把弱方向映射到 roll、pitch、yaw 与 x、y、z,最后只在预条件子里抬高弱特征值,并用预条件共轭梯度求解。论文报告其在长时定位精度上相对同类退化感知基线提升约 20% 到 50%,在速度上有约 5 到 30 倍加速,个别场景达到 116 倍。开源仓库的 main 分支已经提供完整 C++ 实现,依赖面保持在 Eigen 与 PCL,便于接入现有 SLAM 系统。

这篇论文的价值不只是提出一个更稳的求解器。它把过去经常混在一起的三个问题拆开:退化是否出现、退化具体落在哪个物理运动方向、求解时应该怎样只处理弱方向而不污染良好方向。这里的关键是,DCReg 把“描述”变成流程中的显式模块,因此调试人员能看到一个类似“X 轴贡献约 77%,同时存在 yaw 弱约束”的诊断结果,而不是只得到一个整体条件数或一个布尔值。进一步看,这种诊断信息也能被系统层使用,例如在退化方向上降低权重、等待下一帧观测,或交给 IMU、轮速计与回环模块补充约束。

Github项目地址:https://github.com/JokerJohn/DCReg
论文地址:https://arxiv.org/abs/2509.06285]

1. 研究问题与现有缺口

1.1 退化场景为什么危险

LiDAR 配准通常要在两帧点云或单帧点云与局部地图之间估计六自由度相对位姿。在结构丰富的建筑立面、柱子和地面交界处,法向分布覆盖多个方向,位姿每个自由度都能得到约束。可是在长走廊里,墙面主要约束横向位置和部分旋转,沿走廊方向的平移可能只靠很远的端墙或少量地面起伏约束;在空旷停车场里,大面积平面地面能约束 z、roll 和 pitch,却很难约束 x、y 与 yaw。直观理解是,环境像一组弹簧,某些方向弹簧很硬,某些方向几乎没有弹簧,优化器在这些弱方向上自然容易漂移。

1.2 现有流程的三个缺口

论文把现有退化处理总结为“检测—缓解”两阶段,但这个流程有三个实际缺口。第一,检测不可靠:旋转与平移在 Hessian 中有天然尺度差异,同时存在交叉耦合,直接看全矩阵或只看对角块都会漏检或误检。第二,描述不充分:特征向量是数学基,不会自动对齐 x、y、z 或 roll、pitch、yaw,逐帧解释会受到符号、顺序和基选择的影响。第三,缓解有副作用:常见正则化会直接修改 Hessian 或最小二乘目标,稳定弱方向的同时也可能改变原本约束良好方向的最优解。核心问题在于,检测、描述、缓解三者没有共享同一套结构化坐标,导致每一步都在丢失信息。

2. DCReg 整体框架

2.1 输入输出与数据流

DCReg 的输入并不是额外的传感器,而是每次点到平面 ICP 迭代中已经存在的六维法方程:Hessian、右端项,以及当前位姿线性化后的雅可比。它在每个迭代内执行三步:先从 Hessian 构造旋转与平移的 Schur 补并分析谱;再把特征向量映射到物理运动轴并生成退化掩码、贡献比例与对齐特征值;最后构造块对角预条件子,用 PCG 求解原始法方程。这个设计的关键是,诊断与求解共享同一个解耦后的坐标系统,因此缓解动作可以精确落在弱方向上。

2.2 三个模块的闭环关系

第一个模块回答“是否退化、旋转与平移各自的条件数是多少”。第二个模块回答“弱方向由哪些物理轴组成、每个轴贡献多少”。第三个模块回答“怎样在不修改原始问题的前提下让线性系统更容易求解”。换句话说,检测提供子空间,描述提供物理语义,缓解利用前两者的输出构造预条件子。这里值得强调,Schur 解耦只用于分析,最终仍求解完整六自由度问题,不会把旋转和平移当成两个独立问题分别更新。

直觉理解:可以把 DCReg 想象成修一辆方向跑偏的车。先分别检查方向盘和车轮,而不是只看整车抖动;再确认偏移来自左前轮还是转向柱;最后只调整对应部件,而不是把整辆车的悬挂全部拧紧。

3. 点到平面配准与病态来源

3.1 从残差到法方程

点到平面 ICP 不要求源点与目标点一一重合,而是让源点落在目标局部平面上。设源点在当前位姿下变换为 ,目标局部平面法向为 ,平面偏置为 ,残差可以写成 。配准目标是最小化加权残差平方和,并在当前位姿附近线性化为六维增量  的线性方程。公式如下:

其中  是加权残差对位姿增量的雅可比, 是高斯-牛顿近似下的 Hessian, 由  累加得到。这里的关键是, 的每个方向描述当前点云对位姿增量的曲率或约束强度。若某个方向特征值大,数据在该方向上对误差变化很敏感;若特征值接近零,数据对该方向几乎不敏感。配准器在这个阶段并不会区分“旋转弱”和“平移弱”,它只看到同一个六维线性系统,因此后续必须先做结构化分析。

3.2 条件数与误差放大

把  做特征分解后,位姿更新在每个特征方向上的分量大致等于梯度分量除以对应特征值。特征值越小,测量噪声、线性化误差和对应点误差被放大的倍数越高;当这个值接近数值精度下限时,求解器甚至可能产生方向正确但幅度完全错误的更新。条件数定义为最大特征值与最小特征值之比:

直观理解是, 给出了输入扰动在求解中可能被放大的量级上界。论文关注的是弱约束但仍然局部可观的病态问题,即  很小但仍大于零;如果几何上完全秩亏,单帧数值方法无法凭空创造约束。这意味着,DCReg 的目标是在弱约束中稳定求解,而不是替代缺失的观测。

难点提示:病态与不可观经常被混用。病态像在一根很软的弹簧上测位置,噪声会被放大;不可观则像根本没有这根弹簧,任何算法都无法从同一帧几何中恢复该方向。

4. Schur 补检测

4.1 全矩阵与对角块各自的盲区

六自由度 Hessian 可以按旋转和平移分块。全矩阵条件数会被旋转块的大尺度主导,因为旋转对远处点产生的位移随距离平方增长,而平移块主要取决于法向分布。简单地把两个对角块拆开也不够,因为交叉块  编码了旋转与平移的相互补偿关系。进一步看,即使  和  单独看都健康,强耦合仍可能让完整系统病态。核心问题在于,简单分块只是把矩阵切开,并没有消去另一个子空间的影响。

4.2 Schur 补的数学形式

DCReg 使用 Schur 补在两个三自由度子空间中分析条件数。旋转 Schur 补表示先把平移能够解释的残差变化最优消去后,剩余给旋转的约束;平移 Schur 补则先把旋转的影响消去,再看平移自身还剩多少信息。两者共同避免把“另一个子空间可以补偿的部分”误认为当前子空间的约束:

这里的关键是,交叉块没有被丢掉,而是以最优补偿的形式被吸收进子空间。这样得到的不是“旋转块”或“平移块”,而是已经扣除了另一组参数影响的等效信息矩阵。随后对  和  分别做特征分解,并在各自子空间内计算相对条件数,从而避免全矩阵条件数被另一组参数的尺度主导:

如果  或  超过阈值,论文判定该方向病态。实验与代码中常用阈值为 10 到 50,仓库示例默认取 10;阈值越小,检测越保守,也越容易把普通数值差异识别为退化。下面是仓库中的核心检测函数,节选保留了全矩阵条件数、分块、Schur 补、特征分解和子空间条件数计算:

// Source: DCReg/DCReg/include/dcreg.hpp
inline DegeneracyDetection DetectDegeneracy(const Matrix6d& hessian,
                                            const SolverParameters& params) {
  DegeneracyDetection detection;
  const Eigen::JacobiSVD<Matrix6d> full_svd(
      hessian, Eigen::ComputeFullU | Eigen::ComputeFullV);
  const auto singular_values = full_svd.singularValues();
  detection.cond_full =
      singular_values(5) > 1e-12
          ? singular_values(0) / singular_values(5)
          : std::numeric_limits<double>::infinity();

  const Eigen::Matrix3d h_rr = hessian.block<3, 3>(0, 0);
  const Eigen::Matrix3d h_rt = hessian.block<3, 3>(0, 3);
  const Eigen::Matrix3d h_tr = hessian.block<3, 3>(3, 0);
  const Eigen::Matrix3d h_tt = hessian.block<3, 3>(3, 3);

  const Eigen::FullPivLU<Eigen::Matrix3d> lu_rr(h_rr);
  const Eigen::FullPivLU<Eigen::Matrix3d> lu_tt(h_tt);
  if (!lu_rr.isInvertible() || !lu_tt.isInvertible()) {
    return detection;
  }

  // Eq. (18): decouple rotation and translation observability.
  const Eigen::Matrix3d schur_rot = h_rr - h_rt * lu_tt.inverse() * h_tr;
  const Eigen::Matrix3d schur_trans = h_tt - h_tr * lu_rr.inverse() * h_rt;

  const Eigen::SelfAdjointEigenSolver<Eigen::Matrix3d> rot_solver(schur_rot);
  const Eigen::SelfAdjointEigenSolver<Eigen::Matrix3d> trans_solver(schur_trans);
  if (rot_solver.info() != Eigen::Success ||
      trans_solver.info() != Eigen::Success) {
    return detection;
  }

  detection.factorization_ok = true;
  detection.lambda_schur_rot = rot_solver.eigenvalues();
  detection.lambda_schur_trans = trans_solver.eigenvalues();
  detection.raw_rot_basis = rot_solver.eigenvectors();
  detection.raw_trans_basis = trans_solver.eigenvectors();
  detection.cond_schur_rot =
      detection.lambda_schur_rot.maxCoeff() /
      std::max(detection.lambda_schur_rot.minCoeff(), 1e-12);
  detection.cond_schur_trans =
      detection.lambda_schur_trans.maxCoeff() /
      std::max(detection.lambda_schur_trans.minCoeff(), 1e-12);
  return detection;
}

这段代码没有引入点云特征学习,也没有依赖额外传感器,完全从当前迭代的  出发。工程上先检查  与  是否可逆,避免在严重秩亏时继续构造 Schur 补;随后用 SelfAdjointEigenSolver 处理对称子矩阵,保证特征值是实的。这里的实现细节说明,退化检测被放在线性代数层面,而不是启发式后处理层面。

4.3 四条理论结果

论文为 Schur 检测给出四条支撑。第一,消元与曲率等价: 正是消去平移后旋转子问题的等效信息矩阵,其条件数界定旋转解的误差放大。第二,投影表示:Schur 补可以写成投影形式,几何含义是剔除能被平移解释的部分,保留旋转自身提供的约束。第三,尺度与正交基不变性:平移单位从米换成厘米时,相关缩放因子在 Schur 补中相互抵消。第四,谱界与掩蔽机制:当耦合足够强时,交叉项可能抵消对角块原有曲率,使对角块分析漏检,而 Schur 补能暴露退化。进一步看,这四条结果解释了为什么该检测不是经验阈值游戏。

工程价值:如果把全矩阵条件数当作仪表盘,司机只能看到“车有问题”;Schur 子空间相当于把仪表盘拆成转向、制动、动力三个独立指示,能定位问题来自哪一部分。

5. 物理运动轴描述

5.1 特征向量的三类歧义

检测知道“哪个特征值小”之后,还需要把它翻译成机器人运动语言。特征向量本身有三类歧义。符号歧义表示同一个特征向量取正取负都合法;顺序歧义表示相邻特征值接近时,按大小排序会在帧间跳动;基歧义表示特征值挤在一起时,子空间内基可以旋转,单个特征向量不唯一。核心问题在于,直接把特征向量编号当作物理轴编号,会让同一场景的诊断结果在相邻帧之间发生无意义跳变。

5.2 从数学基到物理轴

DCReg 用最大绝对内积把特征向量匹配到物理轴,然后固定符号,并记录原始特征值顺序。由于原始特征分解按特征值大小排列,而这个顺序与 roll、pitch、yaw 或 x、y、z 没有天然对应关系,对齐步骤必须显式保存索引映射。贡献比例再通过对齐基系数取平方得到:

其中  是第  个谱方向在第  个物理轴上的分量, 表示该物理轴对谱方向的贡献比例。平方运算消除了正负号带来的表观差异,同时保留各分量在能量或信息贡献上的相对大小;所有物理轴的贡献比例相加为一,便于把混合方向拆成可读的百分比。下面的代码节选展示符号与顺序处理:

// Source: DCReg/DCReg/include/dcreg.hpp
inline bool AlignEigenBasisToAxes(const Eigen::Matrix3d& raw_basis,
                                  const std::array<Eigen::Vector3d, 3>& refs,
                                  const char* label,
                                  Eigen::Matrix3d* aligned_basis,
                                  std::array<int, 3>* original_indices) {
  aligned_basis->setZero();
  original_indices->fill(-1);

  std::array<bool, 3> used = {false, false, false};
  for (int axis = 0; axis < 3; ++axis) {
    double best_score = -1.0;
    int best_index = -1;
    for (int candidate = 0; candidate < 3; ++candidate) {
      if (used[candidate]) {
        continue;
      }
      const double score = std::abs(refs[axis].dot(raw_basis.col(candidate)));
      if (score > best_score) {
        best_score = score;
        best_index = candidate;
      }
    }
    if (best_index < 0) {
      return false;
    }
    used[best_index] = true;
    (*original_indices)[axis] = best_index;
    Eigen::Vector3d aligned_column = raw_basis.col(best_index);
    if (refs[axis].dot(aligned_column) < 0.0) {
      aligned_column = -aligned_column;
    }
    (*aligned_basis).col(axis) = aligned_column;
  }
  return true;
}

这段代码的关键不是复杂,而是边界清楚。它不允许同一个特征向量被重复分配给两个物理轴,用 used 保证匹配是一一对应的;再用内积符号把方向固定下来。换句话说,物理轴描述从“排序后的第几个特征向量”变成“与某个运动轴最接近的弱方向”,调试信息因此有了稳定语义。

5.3 描述模块的完整输出

在 CharacterizeDegeneracy 中,代码先对旋转和平移分别调用对齐函数,再计算贡献比例、对齐特征值、退化掩码和钳制后的特征值。这个函数是前两个模块的连接点:它接收检测模块给出的 Schur 谱和原始基,输出物理坐标中的诊断结果与可供求解器使用的谱参数。下面的节选保留了最核心的逻辑:

// Source: DCReg/DCReg/include/dcreg.hpp
characterization.rot_axis_contribution_ratio =
    characterization.aligned_rot_basis.cwiseProduct(
        characterization.aligned_rot_basis);
characterization.trans_axis_contribution_ratio =
    characterization.aligned_trans_basis.cwiseProduct(
        characterization.aligned_trans_basis);

const double rot_max = characterization.aligned_lambda_schur_rot.maxCoeff();
const double trans_max = characterization.aligned_lambda_schur_trans.maxCoeff();
const double min_rot = std::max(rot_max / params.kappa_target, 1e-9);
const double min_trans = std::max(trans_max / params.kappa_target, 1e-9);

for (int axis = 0; axis < 3; ++axis) {
  const double rot_condition =
      rot_max /
      std::max(characterization.aligned_lambda_schur_rot(axis), 1e-12);
  if (rot_condition > params.degeneracy_condition_threshold) {
    characterization.is_degenerate = true;
    characterization.degenerate_mask[axis] = true;
    characterization.clamped_lambda_schur_rot(axis) = min_rot;
  }

  const double trans_condition =
      trans_max /
      std::max(characterization.aligned_lambda_schur_trans(axis), 1e-12);
  if (trans_condition > params.degeneracy_condition_threshold) {
    characterization.is_degenerate = true;
    characterization.degenerate_mask[axis + 3] = true;
    characterization.clamped_lambda_schur_trans(axis) = min_trans;
  }
}

这里要厘清,degenerate_mask 的前三位对应 roll、pitch、yaw,后三位对应 x、y、z。代码把弱特征值钳制到 max / kappa_target,同时保留掩码、对齐基和贡献比例,因此诊断与后续求解使用的不是两套彼此独立的坐标。这意味着,后续求解不仅知道要处理哪些方向,也知道处理强度来自一个显式的目标条件数。

论文中的例子让这个模块很直观。停车场第 2314 帧的弱平移方向约 77% 来自 X 轴,13.6% 来自 Y 轴,9.4% 来自 Z 轴,与 X 轴夹角约 12.1 度;由于帧中存在路缘点,旋转约束较好。第 2924 帧以平坦地面为主,除平移退化外还出现 yaw 退化。走廊第 1096 帧存在 X 平移与 roll 退化;第 1142 帧因端墙点增多, X 退化消失,只剩 roll 退化。这样的输出比“该帧退化”更适合进入系统层策略。

难点提示:特征值排序像考试名次,两个学生分数接近时名次会来回换;物理轴对齐像把成绩按科目归档,即使总分接近,也能看出到底是数学还是物理拖了后腿。

6. 目标预条件与 PCG 求解

6.1 与正则化的本质差异

常见做法会直接修改 Hessian,例如 Tikhonov 给所有方向增加正则,TSVD 剔除弱方向,Solution Remapping 把弱方向更新置零。这些方法能改善数值稳定性,但会改变原始最小二乘问题的目标或最优解。DCReg 的选择不同:它保持  与  不变,只在 PCG 的预条件子中调整弱方向。直观理解是,解方程时在两边同乘一个可逆矩阵,数学解不变,但迭代系统的谱更好,收敛更稳定。

6.2 预条件子的构造

DCReg 在旋转和平移两个子空间内分别构造预条件子,而不是把一个统一阻尼项加到整个六维矩阵上。这种分块方式继承了 Schur 检测的结构:旋转与平移的尺度差异已经在子空间中被处理,弱方向也已经被映射到稳定物理基。设对齐后的特征值为 ,目标条件数为 ,则钳制规则只在特征值低于下界时改变它:

随后用对齐基  组装块对角预条件子。这里可以把  与  理解为把物理运动方向变换回当前 Schur 谱坐标的桥梁,而  决定每个方向在迭代中的等效增益。由于预条件子按旋转和平移分成两个 3x3 块,交叉耦合仍保留在原始  中,求解阶段并没有把六自由度问题拆成两个彼此独立的更新:

这里的关键是,弱特征值只在  的逆谱中被抬高,原始  没有被替换。约束良好方向的  大于下界时保持不变,因此预条件子不会主动压平它们。论文还说明这种操作可解释为在高斯先验下做 MAP 估计,并把预处理后系统的条件数控制在目标值以内。这个解释也提醒使用者:目标条件数是数值稳定参数,不应被误当成环境真实约束强度。

下面的代码是 PCG 主循环的核心节选。它展示了 DCReg 如何在每次迭代中用预条件残差修正搜索方向,同时持续检查分母、步长、残差和更新量是否仍然数值可靠。若出现非有限值、过小分母或无法满足残差阈值,外层函数会转入稠密 QR 回退路径,保证配准主循环仍能得到一个有限位姿增量:

// Source: DCReg/DCReg/include/dcreg.hpp
Vector6d delta = Vector6d::Zero();
Vector6d residual = system.rhs;
Vector6d z = characterization.preconditioner * residual;
Vector6d direction = z;
double rz_old = residual.dot(z);

const double target_residual =
    params.pcg_tolerance * std::max(1.0, rhs_norm);
for (int iteration = 0; iteration < std::max(params.pcg_max_iterations, 6);
     ++iteration) {
  const Vector6d hessian_direction = hessian * direction;
  const double denom = direction.dot(hessian_direction);
  if (!std::isfinite(denom) || std::abs(denom) < 1e-20) {
    break;
  }

  const double alpha = rz_old / denom;
  delta += alpha * direction;
  residual -= alpha * hessian_direction;
  report.iterations = iteration + 1;
  report.relative_residual =
      residual.norm() / std::max(1.0, rhs_norm);

  if (residual.norm() <= target_residual) {
    report.delta = delta;
    report.converged = true;
    return report;
  }

  const Vector6d z_next = characterization.preconditioner * residual;
  const double rz_new = residual.dot(z_next);
  const double beta = rz_new / rz_old;
  direction = z_next + beta * direction;
  z = z_next;
  rz_old = rz_new;
}

这段实现保留了 PCG 的标准结构:残差、预条件方向、步长 、方向更新系数 。工程细节同样重要,代码在进入 PCG 前检查预条件子是否有限,在迭代中检查分母、步长、残差和更新量,一旦数值不可靠就回退到稠密 QR 求解。这里的实现选择说明,论文方法在代码中不是理想化算法,而是带失败边界的工程求解器。

工程价值:预条件子像给不同齿轮换上合适的减速比。大阻力方向保持原速,小阻力方向降低增益,最终机器仍按原始控制目标运动,但运行过程更平稳。

7. 开源实现拆解

7.1 仓库结构与入口

仓库的 main 分支是完整公开实现,早期基线版本保留在 baseline 分支,方便对照论文中的传统检测与缓解基线。核心头文件是 DCReg/include/dcreg.hpp,最小示例是 DCReg/src/dcreg_minimal_example.cpp,真实停车场示例是 DCReg/src/dcreg_parking_lot_example.cpp。构建过程只依赖 CMake、Eigen 与 PCL,可选 TBB 或 OpenMP。构建命令如下:

# Source: DCReg/README.md
cmake -S DCReg -B DCReg/build
cmake --build DCReg/build -j8
./DCReg/build/dcreg_minimal_example
./DCReg/build/dcreg_runner

最小示例先构造一个带弱轴的六维合成系统,再依次调用三个模块,并打印条件数、退化掩码、贡献比例、钳制特征值和 PCG 结果。这个入口的价值在于隔离了点云处理,便于把 DCReg 求解器移植到其他前端中。进一步看,仓库也提供 Open3D 与 PCL 的上游集成 PR,目前处于 open 状态,说明作者希望把退化感知配准推进到主流点云库,而不是只服务于论文复现。

7.2 从点云到对应点

真实配准的第一步不是 Schur 补,而是为目标点云建 KD 树、把源点按当前位姿变换到目标坐标系、寻找近邻并拟合局部平面。只有得到法向、平面偏置和残差后,后续才有资格谈六自由度约束。下面的代码节选展示了对应点过滤、平面拟合调用、鲁棒权重和残差构造:

// Source: DCReg/DCReg/include/dcreg.hpp
ParallelFor(0, static_cast<int>(source.size()), mode, [&](int index) {
  const PointT& source_point = source.points[index];
  const Eigen::Vector3d world = TransformPoint(transform, source_point);
  PointT transformed_point;
  transformed_point.x = static_cast<float>(world.x());
  transformed_point.y = static_cast<float>(world.y());
  transformed_point.z = static_cast<float>(world.z());

  std::vector<int> neighbor_indices(k);
  std::vector<float> neighbor_distances(k);
  if (target_kdtree->nearestKSearch(transformed_point, k, neighbor_indices,
                                    neighbor_distances) != k ||
      neighbor_distances.back() >= radius_sq) {
    return;
  }

  Eigen::Vector3d normal_world;
  double plane_offset = 0.0;
  if (!FitPlaneFromNeighbors(target, neighbor_indices,
                             params.max_plane_thickness, &normal_world,
                             &plane_offset)) {
    return;
  }

  const double residual = normal_world.dot(world) + plane_offset;
  const double weight =
      std::max(0.0, 1.0 - params.weight_slope * std::abs(residual));
  if (weight <= params.min_weight) {
    return;
  }

  Correspondence corr;
  corr.point_body = Eigen::Vector3d(source_point.x, source_point.y,
                                    source_point.z);
  corr.normal_world = normal_world;
  corr.residual = residual;
  corr.weight = weight;
  candidates[index] = corr;
  is_valid[index] = 1;
});

这段代码体现了几个工程约束:近邻数量必须足够,最远近邻不能超出搜索半径,拟合平面的厚度必须小于阈值,残差过大时鲁棒权重会被削弱。这里的关键是,坏对应点在进入 Hessian 前已经被过滤。如果这一步质量很差,后续任何谱分析都只是在诊断一个被噪声污染的系统,而不是在诊断真实环境的几何可观测性。

7.3 从对应点到法方程

得到对应点后,代码根据状态参数化计算点到平面雅可比,并把鲁棒权重对残差的导数也纳入完整雅可比。这个细节容易被忽略:如果权重本身随残差变化,只把常数权重乘进雅可比会得到不一致的法方程,进而在 Hessian 中错误估计各方向的约束强度。对退化检测来说,这种错误可能直接改变特征值和条件数。下面是 BuildWeightedLinearSystem 的核心节选:

// Source: DCReg/DCReg/include/dcreg.hpp
ParallelFor(0, static_cast<int>(correspondences.size()), mode, [&](int index) {
  const Correspondence& corr = correspondences[index];
  const Eigen::Matrix<double, 1, 6> residual_jacobian = jacobian_of(corr);
  const Eigen::Matrix<double, 1, 6> full_jacobian =
      corr.weight * residual_jacobian +
      corr.residual * corr.weight_derivative * residual_jacobian;
  jacobians.row(index) = full_jacobian;
  weighted_residuals(index) = -corr.weight * corr.residual;
});

return ReduceNormalEquations(jacobians, weighted_residuals, mode);

注释中给出的公式是 ,也就是同时考虑残差对位姿的导数和权重对残差的导数。随后 ReduceNormalEquations 将所有行累加成  与 ,并可按串行、OpenMP 或 TBB 路径执行。这意味着,DCReg 不要求用户重写前端残差,只要能输出标准法方程,就能接入三阶段退化处理。

7.4 单帧迭代主循环

真实配准主循环在每次迭代中重复执行对应点搜索、法方程构建、退化检测、物理轴描述和 PCG 求解。这个顺序体现了 DCReg 与普通 ICP 的主要差别:每一步更新都基于当前线性化点重新评估几何约束,而不是沿用第一次迭代得到的诊断。下面的节选来自 RunDcRegRegistration:

// Source: DCReg/DCReg/include/dcreg.hpp
const std::vector<Correspondence> correspondences =
    CollectCorrespondences(source, target, &target_kdtree, current_transform,
                           params, result.resolved_parallel_mode,
                           &near_neighbor_count, &residual_square_sum);

const LinearSystem system = build_system(correspondences, state,
                                         result.resolved_parallel_mode);
const DegeneracyDetection detection =
    DetectDegeneracy(system.hessian, params);
const DegeneracyCharacterization characterization =
    CharacterizeDegeneracy(detection, params);
const LinearSolveReport solve_report =
    SolvePreconditionedUpdate(system, params, characterization);
const Vector6d& delta = solve_report.delta;

update_state(&state, delta);
result.transform = transform_of(state);

这五个调用几乎就是 DCReg 的实现流程:对应点、法方程、检测、描述、求解。与只在第一次迭代修改 Hessian 的做法不同,这里每次迭代都会根据当前线性化点重新诊断。论文强调,在真实 SLAM 系统中,上一帧位姿会影响下一帧匹配,因此逐迭代处理能放大不同退化策略在系统层面的差异。进一步看,这也让方法更适合非线性迭代,因为退化方向会随着位姿和对应点变化而变化。

8. 总结

DCReg 的贡献可以压缩为三点。检测上,它用 Schur 补同时处理旋转与平移的尺度差异和交叉耦合,避免全矩阵与对角块分析的盲区。描述上,它处理符号、顺序和基三类歧义,把弱方向稳定映射到物理运动轴,并给出贡献比例。缓解上,它构造结构感知预条件子,只抬高弱特征值,并用 PCG 求解原始法方程,避免直接修改最小二乘目标。代码实现与论文流程对应得很清楚,DetectDegeneracy、CharacterizeDegeneracy、SolvePreconditionedUpdate 三个函数就是三个模块的直接落点。

DCReg 把退化 LiDAR 配准从“发现异常后硬压”变成“先拆解耦、再说明方向、最后只稳定弱方向的求解流程”。对读者而言,理解这篇文章的入口不是记住 Schur 补公式,而是抓住它的诊断链路:点到平面残差生成 Hessian,Hessian 分块后构造 Schur 补,谱方向对齐物理轴,弱谱方向进入预条件子,PCG 在不改变原始法方程的情况下完成更新。

相关推荐