跳转至

基于表面的刚体配准

1 点云配准

点云配准的核心任务,是将两个在不同坐标系或视角下扫描得到的点云(通常称为源点云 P 和目标点云 Q)对齐。这需要找到一个最优的刚体变换(旋转矩阵R和平移向量t),使得两片点云在重叠区域内的整体误差最小。

常见点云配准算法框架有两类:ICP(Iterative Closest Point,迭代最近点) 和 NDT(Normal Distributions Transform,正态分布变换)。

特性 ICP (迭代最近点) NDT (正态分布变换)
核心
思想
最小化点对距离:迭代地寻找最近点并最小化对应点间的距离。 最大化概率似然:将目标点云表示为概率场,寻找使源点云在该场中概率最大的变换。
数据
结构
依赖KD-Tree等加速最近邻搜索。 将空间划分为固定网格(体素),每个网格存储一个概率分布参数。
对应
关系
显式对应:为每个源点找到目标点云中的一个对应点。 隐式对应:源点通过概率密度函数与目标点云的局部区域相关联,无一一对应的点。
初值
要求
较高。容易因错误的最近点对应而陷入局部最优。 相对较低。概率表示更为平滑,对初始位置的容忍度更好。
计算
速度
每次迭代需搜索最近点,计算量较大。 预处理(建网格、计算分布)后,配准过程通常更快,因为无需动态搜索最近邻。
抗噪性 对噪声和离群点较敏感,通常需要搭配鲁棒策略。 抗噪性较好,因为概率分布本身对离群点不敏感。
对点
密度
敏感,点密度差异大会影响匹配质量。 不敏感,只要网格内有足够点来估算分布即可。
适用
场景
1. 高精度工业检测。
2. 计算机辅助医疗。
3. 三维重建后期精配准。
1. 机器人定位与SLAM。
2. 自动驾驶环境感知。
  • ICP 更直观,讲究“精准对位”,适合数据干净、目标明确、有大致初始位置的场景,比如:医疗手术导航(把实时影像对准术前CT)和工业零件检测(把扫描的工件和CAD模型对齐)中。

  • NDT 擅长处理“不可控、嘈杂、大范围、初值未知”,通过概率表示,提供了更平滑、更鲁棒的配准场,适合在环境“不可控”、数据“又吵又乱”、范围还很大的场景,比如:自动驾驶的激光雷达定位和机器人同步建图与定位(SLAM)。

2 基于表面的刚体配准

在计算机辅助医疗导航中,一个非常典型的配准问题是术前影像与术中物理空间之间的对齐。

  • 术前 → 通过 CT 扫描重建出患者骨骼的三维表面模型(通常以 STL 或三角网格形式表示)

  • 术中 → 通过带有定位系统的探针,在真实骨表面上采集一组离散的空间点。

这类问题通常被建模为 基于表面的刚体配准(surface-based rigid registration):在刚体假设下,估计一个空间变换,使术中采集的点集能够与术前重建的骨表面在几何意义上最佳对齐。

由于术中探针只能接触到骨表面的局部区域,且采集到的是稀疏、不规则分布的点集(点数稀疏、覆盖范围有限、噪声较大),而术前模型则为高分辨率的完整骨表面,二者在初始状态下往往存在较大的位姿差异。在此条件下,单一的局部配准算法(如 ICP)容易陷入局部最优,难以保证稳定收敛。

因此,实际系统中通常采用分层式配准策略:首先通过解剖标志点获得一个几何意义明确的粗配准,将两坐标系对齐到相同数量级;随后使用对初始误差更具鲁棒性的 NDT 方法进行概率意义下的粗到中等精度配准;最后在较小误差范围内,利用 ICP 算法进行高精度的局部优化,从而实现稳定而精确的刚体配准结果。

2.1 术前模型构建

通过 CT 扫描重建患者骨骼的三维表面模型(STL 或三角网格),并在术前影像空间中完成必要的标注与模型准备,包括解剖标志点的选取与定义,为后续的术中初始配准提供参考。

2.2 基于标志点的初始配准

通过在术前模型与术中物理空间中分别采集对应的解剖标志点,建立两坐标系之间的初始对应关系。基于这些稀疏但具有明确语义一致性的点对,可直接求解一个刚体变换,用作后续表面配准的初始位姿估计。

解剖标志点仅用于初始位姿估计,其数量有限且不参与后续基于表面误差的优化过程。

bool SurgaceReg::CoarseRegistration(
  vtkSmartPointer<vtkPoints> sourcePoints, vtkSmartPointer<vtkPoints> targetPoints,
  vtkSmartPointer<vtkMatrix4x4> coarseMatrix)
{
    int nbSourcePoints = sourcePoints->GetNumberOfPoints();
    int nbTargetPoints = targetPoints->GetNumberOfPoints();

    if (nbSourcePoints != nbTargetPoints || nbSourcePoints < 3) {
        return false;
    }

    vtkNew<vtkLandmarkTransform> landmarkTrans;
    landmarkTrans->SetModeToRigidBody();
    landmarkTrans->SetSourceLandmarks(sourcePoints);
    landmarkTrans->SetTargetLandmarks(targetPoints);
    landmarkTrans->Update();

    coarseMatrix->DeepCopy(landmarkTrans->GetMatrix());

    return true;
}

2.3 术中表面点采集

使用带定位系统的探针在真实骨表面进行扫描,采集一组稀疏、局部的三维表面点,作为待配准的源点云。

2.4 预处理

  1. 对术前模型进行平移归一化,将其置于计算中心。

  2. 根据术中点的范围裁剪感兴趣区域(ROI),排除无关结构干扰。

  3. 对裁剪后的网格进行表面加密采样,生成用于配准的稠密表面点云。

  4. 基于探针点到最近表面点的方向关系,进行若干次仅含平移的对齐,减小初始位姿误差并提升后续算法的收敛稳定性。

// 在粗配准结果基础上,通过最近邻搜索估计点云间的平均平移偏差,并迭代修正位姿中的平移分量,以降低初始配准的系统性偏移。
Eigen::Matrix4f RegistrationMathUtils::RefineTranslationByNearestNeighbor(
  pcl::PointCloud<pcl::PointXYZ>::Ptr source, pcl::PointCloud<pcl::PointXYZ>::Ptr target,
  double distanceThreshold, int maxIterations)
{
    Eigen::Matrix4f initTransform = Eigen::Matrix4f::Identity();

    if (!source || !target || source->empty() || target->empty()) {
        return initTransform;
    }

    // KD-tree on target cloud
    pcl::search::KdTree<pcl::PointXYZ> targetTree;
    targetTree.setInputCloud(target);

    std::vector<int> indices(1);
    std::vector<float> sqrDistances(1);

    // 当前变换(只更新平移)
    Eigen::Matrix4f currentTransform = initTransform;

    pcl::PointCloud<pcl::PointXYZ> transformedSource;
    pcl::transformPointCloud(*source, transformedSource, currentTransform);

    int iter = 0;
    double meanDistance = std::numeric_limits<double>::max();

    while (meanDistance > distanceThreshold && iter < maxIterations) {

        pcl::PointCloud<pcl::PointXYZ> directionCloud;
        directionCloud.reserve(transformedSource.size());

        std::vector<float> distances(transformedSource.size());

        // 最近邻 + 位移向量
        for (size_t i = 0; i < transformedSource.size(); ++i) {
            if (targetTree.nearestKSearch(transformedSource[i], 1, indices, sqrDistances) > 0) {

                const pcl::PointXYZ & nearest = target->points[indices[0]];

                pcl::PointXYZ dir;
                dir.x = nearest.x - transformedSource[i].x;
                dir.y = nearest.y - transformedSource[i].y;
                dir.z = nearest.z - transformedSource[i].z;
                directionCloud.push_back(dir);

                distances[i] = std::sqrt(sqrDistances[0]);
            }
        }

        if (directionCloud.empty()) {
            break;
        }

        // 平均位移
        Eigen::Vector4f meanShift;
        pcl::compute3DCentroid(directionCloud, meanShift);

        // 只更新 translation
        currentTransform(0, 3) += meanShift[0];
        currentTransform(1, 3) += meanShift[1];
        currentTransform(2, 3) += meanShift[2];

        // 重新变换 source
        pcl::transformPointCloud(*source, transformedSource, currentTransform);

        // 计算 mean NN distance
        meanDistance = std::accumulate(distances.begin(), distances.end(), 0.0) / distances.size();

        ++iter;
    }

    return currentTransform;
}

2.5 基于概率模型的中等精度配准(NDT)

采用正态分布变换(NDT)方法,在概率意义下对两组点云进行对齐,实现对初始误差较为鲁棒的全局或半全局配准。为减小参数选择对结果的影响,可在不同模型尺度下并行执行 NDT,并筛选误差较小的若干候选位姿。

QList<RegistrationCandidate> NDTMultiScaleRegistrar::Register()
{
    qInfo() << "NDTMultiScaleRegistrar start.";

    QElapsedTimer timer;
    timer.start();

    double sourceCloudResolution = ComputeCloudResolution(m_SourceCloud, m_Config.kForResolution);
    double targetCloudResolution = ComputeCloudResolution(m_TargetCloud, m_Config.kForResolution);

    double minResolutionRatio = m_Config.minResolution;
    double maxResolutionRatio = m_Config.maxResolution;

    minResolutionRatio = std::floor(targetCloudResolution * 10.0) / 10.0 + 0.2;
    if (minResolutionRatio > maxResolutionRatio) {
        maxResolutionRatio = minResolutionRatio + 1.0;
    }

    double resolutionStep = m_Config.resolutionStep;
    int resolutionNum = static_cast<int>(std::floor((maxResolutionRatio - minResolutionRatio) / resolutionStep));
    resolutionNum = std::max(1, resolutionNum + 1);

    qInfo() << "Source Cloud Resolution: " << sourceCloudResolution;
    qInfo() << "Target Cloud Resolution: " << targetCloudResolution;
    qInfo() << "NDT Multi-Scale minResolution: " << minResolutionRatio
            << ", maxResolution: " << maxResolutionRatio
            << ", resolutionStep: " << resolutionStep
            << ", resolutionNum: " << resolutionNum;

    QList<NDTJob> jobList;
    for (int i = 0; i < resolutionNum; i++) {
        NDTJob job;
        job.resolution = minResolutionRatio + i * resolutionStep;
        job.stepSize = job.resolution / 10.0;
        pcl::copyPointCloud(*m_SourceCloud, job.sourceCloud);
        pcl::copyPointCloud(*m_TargetCloud, job.targetCloud);
        job.initMatrix = m_InitialTransform;
        jobList.append(job);
    }

    QFuture<QList<NDTResult>> future = QtConcurrent::mappedReduced(jobList, RunNDTRegistration, CollectValidNDTResult);
    future.waitForFinished();

    QList<NDTResult> ndtResults = future.result();
    std::sort(ndtResults.begin(), ndtResults.end(), CompareNDTResultByScore);

    QList<RegistrationCandidate> candidates;
    double minScore = ndtResults[0].score;
    for (int i = 0; i < m_Config.acceptResultMaxNum; i++) {
        if (ndtResults[i].score < minScore * 1.5) {
            RegistrationCandidate candidate;
            candidate.matrix = ndtResults[i].matrix;
            candidate.score = ndtResults[i].score;
            candidates.append(candidate);
        } else {
            break;
        }
    }

    qint64 elapsedMs = timer.elapsed();
    qInfo() << "NDTMultiScaleRegistrar time(ms): " << elapsedMs;

    return candidates;
}

NDTResult RunNDTRegistration(NDTJob paras)
{
    NDTResult result;

    pcl::PointCloud<pcl::PointXYZ>::Ptr outputCloud(new pcl::PointCloud<pcl::PointXYZ>);

    try {
        pcl::NormalDistributionsTransform<pcl::PointXYZ, pcl::PointXYZ> ndt;
        ndt.setTransformationEpsilon(1e-10); // 为终止条件设置最小转换差异
        ndt.setTransformationRotationEpsilon(1 - 1e-5);
        ndt.setMaxCorrespondenceDistance(1.0);
        ndt.setOulierRatio(0.9);
        ndt.setEuclideanFitnessEpsilon(1e-5);
        ndt.setMaximumIterations(10000);
        ndt.setInputSource(paras.sourceCloud.makeShared()); // 源点云
        ndt.setInputTarget(paras.targetCloud.makeShared()); // 目标点云
        ndt.setStepSize(paras.stepSize); // more-thuente 线搜索最大步长
        ndt.setResolution(paras.resolution); // NDT网格网格结构的分辨率
        ndt.align(*outputCloud, paras.initMatrix);

        result.valid = true;
        result.matrix = ndt.getFinalTransformation();
        result.resolution = paras.resolution;
        result.stepSize = paras.stepSize;
        result.score = ndt.getFitnessScore();
    } catch (const std::exception &) {
        result.valid = false;
    }

    return result;
}

2.6 基于最近邻优化的高精度配准(ICP)

在 NDT 输出的候选位姿基础上,使用基于最近邻的点到点 ICP 算法进行局部精配准。通过对多个初始位姿并行优化,进一步降低陷入局部最优的风险,从而获得高精度的刚体配准结果。

ICPResult RunICPRegistration(const ICPJob & job, const ICPMultiInitRegistrar::ICPConfig & cfg)
{
    ICPResult result;

    try {
        pcl::IterativeClosestPoint<pcl::PointXYZ, pcl::PointXYZ> icp;
        pcl::PointCloud<pcl::PointXYZ> aligned;

        icp.setInputSource(job.sourceCloud.makeShared());
        icp.setInputTarget(job.targetCloud.makeShared());

        icp.setMaxCorrespondenceDistance(cfg.maxCorrespondenceDistance);
        icp.setMaximumIterations(cfg.maxIterations);
        icp.setTransformationEpsilon(cfg.transformationEpsilon);
        icp.setTransformationRotationEpsilon(cfg.rotationEpsilon);
        icp.setEuclideanFitnessEpsilon(cfg.fitnessEpsilon);

        icp.align(aligned, job.initMatrix);

        if (icp.hasConverged()) {
            result.valid = true;
            result.matrix = icp.getFinalTransformation();
            result.score = icp.getFitnessScore();
        }
    } catch (const std::exception &) {
        result.valid = false;
    }

    return result;
}

RegistrationCandidate ICPMultiInitRegistrar::Register()
{
    QElapsedTimer timer;
    timer.start();

    QList<ICPJob> jobs;
    for (const auto & init : m_InitialTransforms) {
        ICPJob job;
        pcl::copyPointCloud(*m_SourceCloud, job.sourceCloud);
        pcl::copyPointCloud(*m_TargetCloud, job.targetCloud);
        job.initMatrix = init;
        jobs.append(job);
    }

    auto mapper = [&](const ICPJob & job) {
        return RunICPRegistration(job, m_Config);
    };

    QFuture<QList<ICPResult>> future =
      QtConcurrent::mappedReduced(jobs, mapper, CollectValidICPResult);

    future.waitForFinished();

    QList<ICPResult> results = future.result();
    std::sort(results.begin(), results.end(), CompareICPResultByScore);

    RegistrationCandidate candidate;
    if (results.size() > 0) {
        candidate.matrix = results[0].matrix;
        candidate.score = results[0].score;
    }

    qInfo() << "ICPMultiInitRegistrar time(ms): " << timer.elapsed();
    return candidate;
}

2.7 配准误差评估

通过计算探针点到骨表面最近点的均方根误差(RMS),对最终配准结果的精度进行定量评估。

double RegistrationMathUtils::ComputeRMSDistance(
  pcl::PointCloud<pcl::PointXYZ>::Ptr source, pcl::PointCloud<pcl::PointXYZ>::Ptr target,
  const Eigen::Matrix4f & matrix, double trimRatio)
{
    pcl::search::KdTree<pcl::PointXYZ> tree;
    tree.setInputCloud(source);

    pcl::PointCloud<pcl::PointXYZ> transformed;
    pcl::transformPointCloud(*target, transformed, matrix);

    std::vector<double> dists;
    dists.reserve(transformed.size());

    std::vector<int> idx(1);
    std::vector<float> sqr(1);

    for (const auto & p : transformed) {
        if (!pcl::isFinite(p)) {
            continue;
        }
        if (tree.nearestKSearch(p, 1, idx, sqr) > 0) {
            dists.push_back(sqr[0]);
        }
    }

    if (dists.empty()) {
        return std::numeric_limits<double>::infinity();
    }

    std::sort(dists.begin(), dists.end());

    size_t keep = static_cast<size_t>(dists.size() * (1.0 - trimRatio));
    double sum = 0.0;
    for (size_t i = 0; i < keep; ++i) {
        sum += dists[i];
    }

    return std::sqrt(sum / keep);
}