实用指南:Matlab通过GUI实现点云的快速全局配准(FGR)

        本节分享关于点云的FGR。FGR(Fast Global Registration,快速全局配准)是三维点云配准领域的关键算法,专门解决传统 ICP(迭代最近点)算法的三大痛点:

        1、依赖初始变换矩阵,若初始对齐偏差大则易陷入局部最优;

        2、处理大规模点云时迭代次数多,效率低下;

        3、对噪声点与重叠区域缺失敏感,鲁棒性不足。

        其核心逻辑是通过鲁棒特征匹配建立点云对应关系,结合全局优化模型直接求解最优刚性变换(旋转 + 平移),无需提前估计初始对齐状态,实现 “快速 + 全局最优” 的配准效果。

一. MATLAB 中的 FGR 技术实现

        MATLAB 2020a 及以上版本在Computer Vision Toolbox中集成pcregisterfgr函数,为 FGR 配准提供便捷工具,核心优势包括:

  • 零初始依赖:无需手动或通过其他算法(如 SAC-IA)估计初始变换矩阵,直接启动全局配准;

  • 高效处理:采用特征降维与稀疏匹配策略,10 万级点云配准时间可控制在 1-5 秒,满足快速处理需求;

  • 强鲁棒性:内置 RANSAC 外点剔除机制,支持 30% 以上重叠区域的点云配准,可容忍一定噪声(如激光扫描噪声);

  • 灵活可调:支持自定义特征类型(FPFH/SHOT)、匹配阈值(MatchThreshold)、内点比例(InlierRatio)等参数,适配不同场景。

二、MATLAB 点云 FGR 配准完整操作流程

        基于pcregisterfgr函数,点云 FGR 配准可分为 5 个关键步骤,每步均附 MATLAB 代码与参数说明:

步骤 1:点云数据加载与预处理

(1)数据加载

        通过pcread函数读取主流点云文件(PLY/PCD/XYZ 等),或对接传感器(Kinect、激光雷达)实时获取数据:

% 加载源点云(待配准)与目标点云(参考基准)
ptCloudSource = pcread('source_cloud.ply'); % 待配准点云
ptCloudTarget = pcread('target_cloud.ply'); % 参考点云

(2)预处理操作

        目的:降低噪声干扰、减少点云数量(平衡效率与精度),核心操作包括:

  • 去噪:用pcdenoise去除孤立噪声点(基于局部点密度判断);

  • 下采样:用pcdownsample通过体素网格或随机采样减少点数,通常保留 30%-50% 原点数;

  • 法向量估计:若提取 FPFH/SHOT 特征,需用pcnormals估计点云法向量(特征计算依赖法向量方向)。

示例代码:(2020a版本以后)

% 1. 去噪(默认参数,可通过'Sigma'调整噪声敏感度)
ptCloudSource = pcdenoise(ptCloudSource);
ptCloudTarget = pcdenoise(ptCloudTarget);
% 2. 体素下采样(体素大小=0.02m,需根据点云实际尺度调整,如小零件设为0.005m)
ptCloudSource = pcdownsample(ptCloudSource, 'gridAverage', 0.02);
ptCloudTarget = pcdownsample(ptCloudTarget, 'gridAverage', 0.02);
% 3. 估计法向量(为特征提取做准备,邻域点数默认10,可通过'NumNeighbors'调整)
ptCloudSource = pcnormals(ptCloudSource);
ptCloudTarget = pcnormals(ptCloudTarget);

步骤 2:点云特征提取

FGR 配准依赖局部特征描述子建立点云对应关系,MATLAB 支持两种主流特征:

特征类型

特点

适用场景

FPFH(默认)

计算速度快,对旋转 / 尺度鲁棒,适用于无序点云

通用场景(如建筑建模、文物重建)

SHOT

对噪声更鲁棒,特征区分度高

纹理丰富点云(如工业零件、医疗影像)

通过pcreatefeature函数手动提取特征(pcregisterfgr内部会自动提取,手动提取可优化特征参数):

% 提取FPFH特征(邻域半径=0.1m,需与点云尺度匹配)
featureSource = pcreatefeature(ptCloudSource, 'FeatureType', 'fpfh', 'Radius', 0.1);
featureTarget = pcreatefeature(ptCloudTarget, 'FeatureType', 'fpfh', 'Radius', 0.1);
% 若提取SHOT特征,仅需修改'FeatureType'参数
% featureSource = pcreatefeature(ptCloudSource, 'FeatureType', 'shot', 'Radius', 0.1);

步骤 3:特征匹配与外点剔除

该步骤由pcregisterfgr自动完成,核心逻辑包括:

        1、特征匹配:采用 K 近邻(K=2)匹配策略,对源点云每个特征,在目标点云中寻找 Top-2 相似特征;

        2、外点剔除:通过 RANSAC 算法筛选内点 —— 随机抽取 3 对匹配点估计变换矩阵,计算其他匹配点的重投影误差,保留误差小于阈值的内点,迭代优化直至内点集稳定。

可通过参数控制剔除效果:

  • InlierRatio:预期内点比例(如 0.5 表示预期 50% 匹配对为内点,需根据点云重叠度调整);

  • MatchThreshold:特征匹配阈值(值越小匹配越严格,默认 0.9)。

步骤 4:全局变换矩阵估计与配准执行

        基于筛选后的内点集,pcregisterfgr通过最小二乘全局优化求解刚性变换矩阵(rigidtform3d对象),目标函数为最小化内点间欧氏距离平方和:

\(\min_{R,t} \sum_{i=1}^{N} \| R \cdot p_i + t - q_i \|^2\)

        其中,\(p_i\)为源点云内点,\(q_i\)为目标点云对应内点,\(R\)为 3×3 旋转矩阵,\(t\)为 3×1 平移向量。

执行配准代码:

% 调用FGR配准函数,获取配准后点云与变换矩阵
[ptCloudRegistered, tform] = pcregisterfgr(ptCloudSource, ptCloudTarget, ...
'FeatureType', 'fpfh', % 特征类型
'InlierRatio', 0.5, % 预期内点比例
'MatchThreshold', 0.9, % 匹配阈值
'Radius', 0.1); % 特征计算邻域半径
% 提取变换矩阵中的旋转矩阵R与平移向量t
R = tform.Rotation; % 3×3旋转矩阵
t = tform.Translation; % 3×1平移向量(单位:m)

步骤 5:配准结果评估与可视化

(1)定量评估

通过关键指标判断配准精度:

  • 平均距离误差:配准后点云与目标点云的平均欧氏距离(值越小精度越高,通常要求 < 0.01m);

  • 内点比例:实际内点数量 / 总匹配对数量(反映匹配质量,接近InlierRatio说明参数设置合理)。

评估代码:

% 计算平均距离误差
distances = pcdist(ptCloudRegistered, ptCloudTarget); % 逐点距离
meanError = mean(distances); % 平均误差
fprintf('配准平均距离误差:%.4f m\n', meanError);
% 计算内点比例(需先获取匹配对信息,通过辅助函数实现)
function inlierRatio = calcInlierRatio(ptCloudSource, ptCloudTarget, tform)
% 转换源点云到目标点云坐标系
ptCloudTransformed = pctransform(ptCloudSource, tform);
% 计算匹配对并筛选内点(误差<0.005m为内点)
[~, distances] = pcmatchpair(ptCloudTransformed, ptCloudTarget, 0.005);
inlierRatio = length(distances)/size(ptCloudTransformed.Location, 1);
end
inlierRatio = calcInlierRatio(ptCloudSource, ptCloudTarget, tform);
fprintf('实际内点比例:%.2f\n', inlierRatio);

(2)可视化展示

通过pcshowpair函数对比配准前后效果,用不同颜色区分点云:

figure('Position', [100, 100, 1200, 500]); % 设置窗口大小
% 配准前对比(源点云:红,目标点云:绿)
subplot(1, 2, 1);
pcshowpair(ptCloudSource, ptCloudTarget, 'VerticalAxis', 'Y', 'HorizontalAxis', 'X');
title('配准前:源点云(红) vs 目标点云(绿)', 'FontSize', 12);
xlabel('X轴(m)'), ylabel('Y轴(m)'), zlabel('Z轴(m)');
% 配准后对比(配准点云:红,目标点云:绿)
subplot(1, 2, 2);
pcshowpair(ptCloudRegistered, ptCloudTarget, 'VerticalAxis', 'Y', 'HorizontalAxis', 'X');
title('配准后:配准点云(红) vs 目标点云(绿)', 'FontSize', 12);
xlabel('X轴(m)'), ylabel('Y轴(m)'), zlabel('Z轴(m)');

三、点云 FGR 配准的典型应用领域

        基于 “无需初始变换、速度快、鲁棒性强” 的特点,FGR 配准在多领域落地应用:

1. 三维重建领域

        核心需求:将多视角点云拼接为完整三维模型,避免逐帧迭代对齐的繁琐流程。

  • 文物数字化:通过激光扫描获取文物不同角度点云(如青铜器、石雕),FGR 直接实现全局拼接,保留毫米级细节特征,避免 ICP 因初始偏差导致的拼接错位;

  • 建筑建模:无人机激光雷达获取建筑外立面多帧点云,FGR 快速对齐各帧数据,生成完整建筑三维模型,支撑 BIM(建筑信息模型)构建。

2. 工业检测领域

        核心需求:将实际零件点云与 CAD 设计模型配准,计算尺寸偏差,实现质量检测。

  • 汽车零件检测:扫描发动机缸体、变速箱壳体等零件,FGR 无需手动对齐零件与 CAD 模型,直接全局配准,检测关键孔位、平面的位置偏差(精度要求 < 0.005m);

  • 3C 产品组装验证:手机外壳、笔记本机身的扫描点云与设计模型配准,通过 FGR 外点剔除机制排除扫描噪声,精准识别组装间隙超差区域。

3. 自动驾驶与机器人领域

        核心需求:实时将传感器点云与地图配准,实现定位与导航。

  • 自动驾驶定位:激光雷达(LiDAR)获取当前帧点云,与高精度预建地图(点云格式)通过 FGR 快速配准(耗时 < 0.5 秒),获取车辆实时位置(定位精度 < 0.1m),辅助路径规划;

  • 机器人导航:仓储机器人通过深度相机获取环境点云,FGR 将实时点云与预建仓库地图配准,实现自主避障与货架定位,提升导航效率。

4. 医疗影像领域

        核心需求:不同模态医疗点云配准,辅助手术规划与治疗评估。

  • 骨科手术规划:患者骨骼 CT 点云(高精度)与标准骨骼模型(参考)通过 FGR 配准,定位骨折、畸形部位,设计个性化内固定方案;

  • 牙科修复:口腔扫描仪获取患者牙床点云,FGR 将其与假牙设计模型配准,确保修复体与牙床贴合度(误差 < 0.002m),减少后续调整次数。

本次使用的数据。。。。。。兔砸呗

一、点云FGR的程序

1、最简版

clc; clear; close all;
%% 1. 读取点云
[file, path] = uigetfile({'*.pcd;*.ply','点云文件 (*.pcd,*.ply)'}, ...
                         '请选择点云(例如 bunny.pcd)');
if file==0; return; end
sourcePtCloud = pcread(fullfile(path,file));
targetPtCloud = copy(sourcePtCloud);          % 先复制一份
%% 2. 对目标点云做刚性变换 + 噪声
% 2.1 绕 Z 轴旋转 30°
ang = deg2rad(10);
R = [cos(ang) -sin(ang) 0;
     sin(ang)  cos(ang) 0;
     0         0        1];
% 2.2 平移向量
t = [0.01 0.015 0.02];
% 2.3 手工变换
P = targetPtCloud.Location;          % N×3
P = (R * P')';                       % 旋转
P = P + t;                           % 平移
% 2.4 加高斯噪声(降低噪声水平)
sigma = 0.0005;  % 从0.001降低,减少特征干扰
P = P + sigma * randn(size(P));
% 2.5 重新封装成 pointCloud
targetPtCloud = pointCloud(P, ...
                           'Normal', targetPtCloud.Normal, ...
                           'Color',  targetPtCloud.Color);
% 2.6 上色可视化
sourcePtCloud.Color = repmat(uint8([255 0 0]), size(sourcePtCloud.Location,1), 1);
targetPtCloud = pointCloud(P, ...
                           'Normal', targetPtCloud.Normal, ...
                           'Color',  repmat(uint8([0 255 0]), size(P,1), 1));
%% 3. 可视化初始位姿
figure('Name','初始位姿','Color','w');
pcshowpair(sourcePtCloud, targetPtCloud);
title('红色=source,绿色=target');
%% 4. 改进的 Fast Global Registration(平衡匹配质量与数量)
% 4.1 计算更高质量的 FPFH 特征(增加区分度)
sourceFPFH = computeFPFH_improved(sourcePtCloud);
targetFPFH = computeFPFH_improved(targetPtCloud);
% 4.2 更合理的距离阈值估算(动态调整)
dist = sqrt(sum( (sourcePtCloud.Location - targetPtCloud.Location).^2 ,2 ));
avgDist = mean(dist);
stdDist = std(dist);
maxCorrDist = max(avgDist + 1.2*stdDist, 0.01);  % 限制最小阈值
fprintf('平均距离 %.4f,标准差 %.4f → 阈值 %.4f\n', avgDist, stdDist, maxCorrDist);
% 4.3 稳健版 FGR 主流程(抗噪能力增强)
T = fastGlobalRegistration_improved( ...
        sourcePtCloud.Location, targetPtCloud.Location, ...
        sourceFPFH, targetFPFH, maxCorrDist);
fprintf('估计的变换矩阵:\n'); disp(T);
%% 5. 应用变换矩阵
R = T(1:3,1:3);
% 确保旋转矩阵正交性(修正数值误差)
[U,~,V] = svd(R);
R = U * V';  % 正交化处理
t_vec = T(1:3,4).';
sourceLoc = sourcePtCloud.Location;
sourceRegLoc = (R * sourceLoc')' + t_vec;
sourceRegPtCloud = pointCloud(sourceRegLoc, ...
    'Color', sourcePtCloud.Color, ...
    'Normal', sourcePtCloud.Normal);
%% 6. 可视化配准结果
figure('Name','改进的 FGR 配准结果','Color','w');
pcshowpair(sourceRegPtCloud, targetPtCloud);
title('红色=registered,绿色=target');
%% 7. 数值评价
rmsBefore = sqrt(mean(sum( (sourcePtCloud.Location - targetPtCloud.Location).^2 ,2)));
rmsAfter  = sqrt(mean(sum( (sourceRegLoc - targetPtCloud.Location).^2 ,2)));
fprintf('RMS 误差 配准前: %.4f  配准后: %.4f  改善: %.1f%%\n', ...
        rmsBefore, rmsAfter, (rmsBefore-rmsAfter)/rmsBefore*100);
% 计算配准后重叠率(调整阈值)
inlierDist = sqrt(sum( (sourceRegLoc - targetPtCloud.Location).^2 ,2));
overlapRate = mean(inlierDist < 1.2*maxCorrDist);
fprintf('配准后重叠率: %.1f%%\n', overlapRate*100);
%% -------------------------------------------------------------------------
%% 子函数 1:改进的 FPFH 计算(增强特征区分度)
function fpfh = computeFPFH_improved(ptCloud)
P = ptCloud.Location;
N = size(P,1);
[k, d2] = knnsearch(P,P,'K',2);
density    = mean(sqrt(d2(:,2)));
radNormal  = 2.0 * density;  % 增大半径获取更稳定法向
radFPFH    = 4.0 * density;  % 扩大特征支持域
%% 2. 法向计算(更稳健)
K = 40;  % 更多邻域点提高法向可靠性
[nnIndices, dists] = knnsearch(P, P, 'K', K+1);
nnIndices = nnIndices(:, 2:end);  % 去除自身点
dists = dists(:, 2:end);          % 保存距离用于加权
normal = zeros(N,3);
curv   = zeros(N,1);
for i = 1:N
    idx = nnIndices(i, :);
    if numel(idx) < 8  % 要求更多邻域点
        normal(i,:) = [0 0 1];
        continue;
    end
    % 基于距离的加权法向计算(修复维度问题)
    % 将权重转换为列向量,确保与点坐标矩阵维度匹配
    weights = exp(-dists(i,:).^2 / (2*(radNormal/2)^2));
    weights = weights';  % 关键修复:将行向量转为列向量
    pts = P(idx,:) - P(i,:);  % M×3矩阵(M为邻域点数量)
    % 现在weights是M×1列向量,repmat后为M×3,与pts维度匹配
    weighted_pts = pts .* repmat(weights, 1, 3);  % 距离近的点权重高
    [~,S,V] = svd(weighted_pts,'econ');
    n = V(:,3)';
    % 法向一致性增强
    if n*[0 0 1]' < 0, n = -n; end
    normal(i,:) = n;
    curv(i) = S(3,3)/sum(diag(S));
end
%% 3. 建立邻接关系(自适应密度)
K_fpfh = min(60, max(30, round(N/50)));  % 自适应邻域点数
[adjIndices, adjDists] = knnsearch(P, P, 'K', K_fpfh+1);
adjIndices = adjIndices(:, 2:end);
adjDists = adjDists(:, 2:end);
adj = num2cell(adjIndices, 2);
adjWeights = num2cell(exp(-adjDists.^2 / (2*(radFPFH/2)^2)), 2);  % 邻域权重
%% 4. 计算 SPFH(增强区分度)
spfh = zeros(N,44);
for i = 1:N
    nei = adj{i};
    if isempty(nei), continue; end
    u = normal(i,:);
    p = P(i,:);
    weights = adjWeights{i};  % 预计算的权重
    v = P(nei,:) - p;
    v = v ./ sqrt(sum(v.^2, 2));
    n2 = normal(nei,:);
    % 特征计算
    f1 = sum(v .* repmat(u, size(v,1), 1), 2);
    f2 = sum(v .* n2, 2);
    f3 = sum(repmat(u, size(n2,1), 1) .* n2, 2) + 1;
    % 更精细的分箱
    b1 = min(max(floor((f1+1)/2 * 11) + 1, 1), 11);
    b2 = min(max(floor(f2+1) + 1, 1), 2);
    b3 = min(max(floor(f3/2 * 2) + 1, 1), 2);
    % 加权直方图
    hist = zeros(1,44);
    for j = 1:length(b1)
        bin = sub2ind([11 2 2], b1(j), b2(j), b3(j));
        hist(bin) = hist(bin) + weights(j);
    end
    % 防止除以零
    histSum = sum(hist);
    if histSum > 0
        spfh(i,:) = hist ./ histSum;
    end
end
%% 5. FPFH 计算(增强鲁棒性)
fpfh = zeros(N,44);
for i = 1:N
    nei = [i; adj{i}'];
    dists = sqrt(sum((P(nei,:) - P(i,:)).^2, 2));
    w = 1 ./ (1 + dists.^2);  % 距离越近权重越高
    fpfh(i,:) = sum(spfh(nei,:) .* w, 1) / sum(w);
end
fpfh = fpfh';
end
%% -------------------------------------------------------------------------
%% 子函数 2:改进的 Fast Global Registration(抗错误匹配)
function T = fastGlobalRegistration_improved(src, tgt, srcFPFH, tgtFPFH, maxCorrDist)
    % 1. 多阶段特征匹配(先严后宽)
    [idxS, idxT, ratios] = featureMatching_improved(srcFPFH, tgtFPFH);
    % 2. 建立分级对应集
    matchS = src(idxS,:);
    matchT = tgt(idxT,:);
    % 3. 基于比率的自适应过滤
    initDists = sqrt(sum((matchS - matchT).^2, 2));
    % 比率越小,匹配越可靠,可适当放宽距离限制
    adaptiveTh = maxCorrDist .* (1 + ratios(idxS));
    goodMatchIdx = initDists < adaptiveTh;
    matchS = matchS(goodMatchIdx,:);
    matchT = matchT(goodMatchIdx,:);
    numMatches = size(matchS,1);
    % 确保最低匹配点数量
    minRequired = 15;
    if numMatches < minRequired
        warning(['匹配点数量不足,使用低阈值过滤 (', num2str(numMatches), ')']);
        % 逐步降低阈值直到获得足够匹配点
        for factor = 1.1:0.1:2.0
            goodMatchIdx = initDists < factor * maxCorrDist;
            matchS = matchS(goodMatchIdx,:);
            matchT = matchT(goodMatchIdx,:);
            numMatches = size(matchS,1);
            if numMatches >= minRequired
                break;
            end
        end
        if numMatches < 3
            error('匹配点数量不足3个,无法计算刚体变换');
        end
    end
    % 4. 增强版 RANSAC(抗噪优化)
    numSamples = 3;
    maxIter    = 20000;  % 增加迭代次数提高找到正确模型的概率
    inlierTh   = 0.6 * maxCorrDist;  % 更严格的初始内点阈值
    bestInl    = 0;
    bestT = eye(4);
    matchS_T = matchS';  % 预转置加速计算
    % 带概率终止的RANSAC
    p = 0.99;  % 期望成功率
    outlierRatio = 0.5;  % 初始异常值比例估计
    iter = 0;
    while iter < maxIter
        % 动态调整迭代次数
        neededIter = log(1-p)/log(1-(1-outlierRatio)^numSamples);
        maxIter = max(maxIter, ceil(neededIter));
        rnd = randi(numMatches, numSamples,1);
        A = matchS(rnd,:);  B = matchT(rnd,:);
        % 计算变换并验证
        [R, t] = kabsch(A, B);
        transformed = (R * matchS_T)';
        transformed = transformed + repmat(t, numMatches, 1);
        dist = sqrt(sum( (transformed - matchT).^2 ,2));
        inlierCount = sum(dist <= inlierTh);
        % 更新最佳模型
        if inlierCount > bestInl
            bestInl = inlierCount;
            bestT  = [R t'; 0 0 0 1];
            % 更新异常值比例估计
            outlierRatio = 1 - bestInl/numMatches;
            if outlierRatio < 0.1  % 内点足够多则提前退出
                break;
            end
        end
        iter = iter + 1;
    end
    % 5. 两阶段精修
    % 第一阶段:用宽松阈值提取内点
    transformed = (bestT(1:3,1:3) * matchS_T)';
    transformed = transformed + repmat(bestT(1:3,4)', numMatches, 1);
    dist = sqrt(sum( (transformed - matchT).^2 ,2));
    inlIdx = dist <= 1.2*inlierTh;
    if sum(inlIdx) < 5
        warning('内点太少,使用初始变换');
        T = bestT;
        return;
    end
    % 第二阶段:用内点重新估计并精修
    [R_refine, t_refine] = kabsch(matchS(inlIdx,:), matchT(inlIdx,:));
    % 对精修结果进行正交化(确保是刚体变换)
    [U,~,V] = svd(R_refine);
    R_refine = U * V';
    if det(R_refine) < 0
        U(:,3) = -U(:,3);
        R_refine = U * V';
    end
    T = [R_refine t_refine'; 0 0 0 1];
end
%% -------------------------------------------------------------------------
%% 子函数 3:改进的特征匹配(平衡质量与数量)
function [idxS, idxT, ratios] = featureMatching_improved(featS, featT)
    T = featT';  % N2×44
    S = featS';  % N1×44
    [idx, dist] = knnsearch(T, S, 'NSMethod', 'kdtree', 'K', 2);
    % 计算比率并保存
    ratios = dist(:,1) ./ dist(:,2);
    % 动态阈值:根据比率分布自动调整
    ratioTh = min(0.9, max(0.7, prctile(ratios, 30)));  % 取30%分位值
    goodMatch = ratios < ratioTh;
    fprintf('使用比率阈值 %.2f,保留 %d 个匹配点\n', ratioTh, sum(goodMatch));
    idxS = find(goodMatch);
    idxT = idx(goodMatch,1).';
end
%% -------------------------------------------------------------------------
%% 子函数 4:加权 Kabsch 算法
function [R, t] = weightedKabsch(A, B, weights)
    weights = weights / sum(weights);
    W = diag(weights);
    muA = sum(A .* repmat(weights, 1, 3), 1);
    muB = sum(B .* repmat(weights, 1, 3), 1);
    A0 = A - repmat(muA, size(A,1), 1);
    B0 = B - repmat(muB, size(B,1), 1);
    H = A0' * W * B0;
    [U,~,V] = svd(H);
    d = sign(det(V' * U));
    if d < 0
        V(:,3) = -V(:,3);
    end
    R = V * U';
    t = muB - muA * R;
end
%% -------------------------------------------------------------------------
%% 子函数 5:标准 Kabsch 算法
function [R, t] = kabsch(A, B)
    muA = mean(A,1);  muB = mean(B,1);
    A0  = A - muA;    B0  = B - muB;
    H   = A0' * B0;
    [U,~,V] = svd(H);
    d = sign(det(V' * U));
    S = eye(3); S(3,3) = d;
    R = V * S * U';
    t = muB - muA * R;
end

2、GUI版本

function FGR_Register_GUI
%FGR_Register_GUI  基于改进FGR算法的点云配准GUI(修复索引超出问题)
clc; clear; close all;
%% ---------------- 主窗口初始化 ----------------
fig = figure('Name','改进FGR配准工具', 'NumberTitle','off', ...
             'MenuBar','none', 'ToolBar','none', ...
             'Position',[100 100 1366 768], 'Color','w');
%% ---------------- 布局常量定义 ----------------
imgWidth = 0.78;               % 三栏显示区总宽度(归一化)
panelW   = imgWidth/3 - 0.005; % 单个显示面板宽度
gap      = 0.005;              % 面板间距
ctrlW    = 1 - imgWidth;       % 右侧控制面板宽度
%% ---------------- 三栏显示面板(原始/目标/配准后) ----------------
% 1. 原始点云面板(红色)
pnlSrc = uipanel('Parent',fig, 'Units','normalized', ...
                 'FontSize',14, 'Position',[0.01 0.01 panelW 0.98], ...
                 'Title','原始点云(红)');
drawnow;
titleObj = findobj(pnlSrc, 'Type', 'text');
if ~isempty(titleObj)
    set(titleObj, 'FontWeight', 'bold');
end
axSrc = axes('Parent',pnlSrc, 'Units','normalized', 'Position',[0.05 0.05 0.9 0.9]);
% 2. 目标点云面板(绿色)
pnlTgt = uipanel('Parent',fig, 'Units','normalized', ...
                 'FontSize',14, 'Position',[0.01+panelW+gap 0.01 panelW 0.98], ...
                 'Title','目标点云(绿,含变换+噪声)');
drawnow;
titleObj = findobj(pnlTgt, 'Type', 'text');
if ~isempty(titleObj)
    set(titleObj, 'FontWeight', 'bold');
end
axTgt = axes('Parent',pnlTgt, 'Units','normalized', 'Position',[0.05 0.05 0.9 0.9]);
% 3. 配准后点云面板
pnlReg = uipanel('Parent',fig, 'Units','normalized', ...
                 'FontSize',14, 'Position',[0.01+2*(panelW+gap) 0.01 panelW 0.98], ...
                 'Title','FGR配准后(红→绿)');
drawnow;
titleObj = findobj(pnlReg, 'Type', 'text');
if ~isempty(titleObj)
    set(titleObj, 'FontWeight', 'bold');
end
axReg = axes('Parent',pnlReg, 'Units','normalized', 'Position',[0.05 0.05 0.9 0.9]);
%% ---------------- 右侧控制面板 ----------------
pnlCtrl = uipanel('Parent',fig, 'Units','normalized', ...
                  'FontSize',14, 'Position',[imgWidth 0 ctrlW 1], ...
                  'Title','控制与参数');
drawnow;
titleObj = findobj(pnlCtrl, 'Type', 'text');
if ~isempty(titleObj)
    set(titleObj, 'FontWeight', 'bold');
end
% 控制面板位置参数
txtH  = 0.03;  btnH  = 0.05;  sliderH = 0.03;
yPos  = 0.95;  margin = 0.02;
%% ---------------- 控制面板组件:1. 点云加载 ----------------
uicontrol('Parent',pnlCtrl, 'Style','pushbutton', 'String','浏览加载点云', ...
          'FontSize',12, 'Units','normalized', ...
          'Position',[0.05 yPos-btnH 0.9 0.06], ...
          'Callback',@loadPointCloud, 'FontWeight','bold');
yPos = yPos - btnH - margin;
% 状态提示
lblStatus = uicontrol('Parent',pnlCtrl, 'Style','text', ...
                      'String','状态:未加载点云', 'FontSize',10, ...
                      'Units','normalized', 'ForegroundColor','red', ...
                      'Position',[0.05 yPos-txtH 0.9 txtH], ...
                      'HorizontalAlignment','left');
yPos = yPos - txtH - margin/2;
%% ---------------- 控制面板组件:2. 目标点云参数 ----------------
% 2.1 绕Z轴旋转
uicontrol('Parent',pnlCtrl, 'Style','text', 'String','1. 绕Z轴旋转 (度)', ...
          'FontSize',11, 'FontWeight','bold', 'Units','normalized', ...
          'Position',[0.05 yPos-txtH 0.9 txtH], 'HorizontalAlignment','left');
yPos = yPos - txtH - margin/2;
sliderRot = uicontrol('Parent',pnlCtrl, 'Style','slider', ...
                      'Min',0, 'Max',180, 'Value',10, 'SliderStep',[1/180,5/180], ...
                      'Units','normalized', 'Position',[0.05 yPos-sliderH 0.65 sliderH], ...
                      'Callback',@refreshTargetCloud);
txtRot = uicontrol('Parent',pnlCtrl, 'Style','edit', 'String','10', ...
                   'FontSize',11, 'Units','normalized', ...
                   'Position',[0.75 yPos-sliderH 0.2 sliderH], ...
                   'Callback',@editRotation);
yPos = yPos - sliderH - margin/2;
% 2.2 平移偏移
uicontrol('Parent',pnlCtrl, 'Style','text', 'String','2. 平移偏移 (毫米)', ...
          'FontSize',11, 'FontWeight','bold', 'Units','normalized', ...
          'Position',[0.05 yPos-txtH 0.9 txtH], 'HorizontalAlignment','left');
yPos = yPos - txtH - margin/2;
% X轴平移
uicontrol('Parent',pnlCtrl, 'Style','text', 'String','X:', ...
          'FontSize',10, 'Units','normalized', ...
          'Position',[0.05 yPos-txtH 0.1 txtH]);
editTx = uicontrol('Parent',pnlCtrl, 'Style','edit', 'String','10', ...
                   'FontSize',11, 'Units','normalized', ...
                   'Position',[0.15 yPos-txtH 0.25 txtH], 'Callback',@refreshTargetCloud);
% Y轴平移
uicontrol('Parent',pnlCtrl, 'Style','text', 'String','Y:', ...
          'FontSize',10, 'Units','normalized', ...
          'Position',[0.45 yPos-txtH 0.1 txtH]);
editTy = uicontrol('Parent',pnlCtrl, 'Style','edit', 'String','15', ...
                   'FontSize',11, 'Units','normalized', ...
                   'Position',[0.55 yPos-txtH 0.25 txtH], 'Callback',@refreshTargetCloud);
yPos = yPos - txtH - margin/2;
% Z轴平移
uicontrol('Parent',pnlCtrl, 'Style','text', 'String','Z:', ...
          'FontSize',10, 'Units','normalized', ...
          'Position',[0.05 yPos-txtH 0.1 txtH]);
editTz = uicontrol('Parent',pnlCtrl, 'Style','edit', 'String','20', ...
                   'FontSize',11, 'Units','normalized', ...
                   'Position',[0.15 yPos-txtH 0.25 txtH], 'Callback',@refreshTargetCloud);
yPos = yPos - txtH - margin/2;
% 2.3 噪声强度
uicontrol('Parent',pnlCtrl, 'Style','text', 'String','3. 高斯噪声 (毫米)', ...
          'FontSize',11, 'FontWeight','bold', 'Units','normalized', ...
          'Position',[0.05 yPos-txtH 0.9 txtH], 'HorizontalAlignment','left');
yPos = yPos - txtH - margin/2;
sliderNoise = uicontrol('Parent',pnlCtrl, 'Style','slider', ...
                        'Min',0, 'Max',5, 'Value',0.5, 'SliderStep',[0.1/5,0.5/5], ...
                        'Units','normalized', 'Position',[0.05 yPos-sliderH 0.65 sliderH], ...
                        'Callback',@refreshTargetCloud);
txtNoise = uicontrol('Parent',pnlCtrl, 'Style','edit', 'String','0.5', ...
                     'FontSize',11, 'Units','normalized', ...
                     'Position',[0.75 yPos-sliderH 0.2 sliderH], ...
                     'Callback',@editNoise);
yPos = yPos - sliderH - margin;
%% ---------------- 控制面板组件:3. 配准执行 ----------------
uicontrol('Parent',pnlCtrl, 'Style','pushbutton', 'String','运行FGR配准', ...
          'FontSize',12, 'Units','normalized', 'BackgroundColor',[0.2 0.8 0.2], ...
          'Position',[0.05 yPos-btnH 0.9 0.06], 'FontWeight','bold', ...
          'Callback',@runFGRRegistration);
yPos = yPos - btnH - margin;
%% ---------------- 控制面板组件:4. 结果评价 ----------------
uicontrol('Parent',pnlCtrl, 'Style','text', 'String','配准结果评价', ...
          'FontSize',11, 'FontWeight','bold', 'Units','normalized', ...
          'Position',[0.05 yPos-txtH 0.9 txtH], 'HorizontalAlignment','left');
yPos = yPos - txtH - margin/2;
% 评价指标显示
lblRMSBefore = uicontrol('Parent',pnlCtrl, 'Style','text', ...
                         'String','配准前RMS:-- mm', 'FontSize',10, ...
                         'Units','normalized', 'Position',[0.05 yPos-txtH 0.9 txtH]);
yPos = yPos - txtH - margin/4;
lblRMSAfter = uicontrol('Parent',pnlCtrl, 'Style','text', ...
                        'String','配准后RMS:-- mm', 'FontSize',10, ...
                        'Units','normalized', 'Position',[0.05 yPos-txtH 0.9 txtH]);
yPos = yPos - txtH - margin/4;
lblImprove = uicontrol('Parent',pnlCtrl, 'Style','text', ...
                       'String','误差改善:-- %', 'FontSize',10, ...
                       'Units','normalized', 'Position',[0.05 yPos-txtH 0.9 txtH]);
yPos = yPos - txtH - margin/4;
lblOverlap = uicontrol('Parent',pnlCtrl, 'Style','text', ...
                       'String','配准重叠率:-- %', 'FontSize',10, ...
                       'Units','normalized', 'Position',[0.05 yPos-txtH 0.9 txtH]);
yPos = yPos - txtH - margin;
%% ---------------- 控制面板组件:5. 结果保存 ----------------
uicontrol('Parent',pnlCtrl, 'Style','pushbutton', 'String','保存配准后点云', ...
          'FontSize',12, 'Units','normalized', 'BackgroundColor',[0.8 0.8 0.2], ...
          'Position',[0.05 yPos-btnH 0.9 0.06], 'FontWeight','bold', ...
          'Callback',@saveRegisteredCloud);
yPos = yPos - btnH - margin;
%% ---------------- 数据容器 ----------------
sourcePtCloud = pointCloud.empty;   % 原始点云
targetPtCloud = pointCloud.empty;   % 目标点云
sourceRegPtCloud = pointCloud.empty;% 配准后点云
T = eye(4);                         % 变换矩阵
rmsBefore = 0; rmsAfter = 0;        % 误差指标
overlapRate = 0;
lastRot = 10; lastNoise = 0.5;      % 用于控制刷新频率
%% =========================================================================
%% ---------------- 回调函数:1. 加载点云 ----------------
%% =========================================================================
    function loadPointCloud(~,~)
        [file, path] = uigetfile({'*.pcd;*.ply','点云文件 (*.pcd,*.ply)'}, ...
                                 '选择原始点云文件');
        if isequal(file,0); return; end
        try
            sourcePtCloud = pcread(fullfile(path,file));
            % 降采样优化(大型点云提速)
            if sourcePtCloud.Count > 10000
                sourcePtCloud = pcdownsample(sourcePtCloud, 'gridAverage', 0.005);
            end
            % 检查点云数量是否足够(至少100个点)
            if sourcePtCloud.Count < 100
                error('点云数量过少(需至少100个点)');
            end
            set(lblStatus, 'String', sprintf('状态:已加载 %s(%d个点)', ...
                file, sourcePtCloud.Count), 'ForegroundColor','green');
            refreshTargetCloud();
        catch ME
            errordlg(ME.message, '读取失败');
            set(lblStatus, 'String','状态:读取失败,请重新选择', 'ForegroundColor','red');
            sourcePtCloud = pointCloud.empty;
        end
    end
%% =========================================================================
%% ---------------- 回调函数:2. 刷新目标点云 ----------------
%% =========================================================================
    function refreshTargetCloud(~,~)
        if isempty(sourcePtCloud); return; end
        % 控制刷新频率(微小变化不刷新)
        currentRot = get(sliderRot, 'Value');
        currentNoise = get(sliderNoise, 'Value');
        if abs(currentRot - lastRot) < 0.5 && abs(currentNoise - lastNoise) < 0.1
            return;
        end
        lastRot = currentRot;
        lastNoise = currentNoise;
        % 读取参数
        rotAngle = currentRot;
        tx = str2double(get(editTx, 'String')) / 1000;
        ty = str2double(get(editTy, 'String')) / 1000;
        tz = str2double(get(editTz, 'String')) / 1000;
        sigma = currentNoise / 1000;
        % 参数校验
        if any(isnan([tx, ty, tz]))
            [tx, ty, tz] = deal(0.01, 0.015, 0.02);
            set(editTx, 'String','10'); set(editTy, 'String','15'); set(editTz, 'String','20');
        end
        sigma = max(0, sigma);
        % 生成目标点云
        srcLoc = sourcePtCloud.Location;
        angRad = deg2rad(rotAngle);
        R = [cos(angRad) -sin(angRad) 0;
             sin(angRad)  cos(angRad) 0;
             0            0           1];
        tVec = [tx, ty, tz];
        tgtLoc = (R * srcLoc')' + repmat(tVec, size(srcLoc,1), 1);
        tgtLoc = tgtLoc + sigma * randn(size(tgtLoc));
        % 上色并显示
        targetPtCloud = pointCloud(tgtLoc, ...
                                   'Normal', sourcePtCloud.Normal, ...
                                   'Color', repmat(uint8([0 255 0]), size(tgtLoc,1), 1));
        sourcePtCloud.Color = repmat(uint8([255 0 0]), size(srcLoc,1), 1);
        showPointCloud(axSrc, sourcePtCloud, '原始点云(红)');
        showPointCloud(axTgt, targetPtCloud, '目标点云(绿)');
        % 清空配准结果
        sourceRegPtCloud = pointCloud.empty;
        cla(axReg); title(axReg, 'FGR配准后(红→绿)');
        set(lblRMSBefore, 'String', sprintf('配准前RMS:%.3f mm', ...
            computeRMS(srcLoc, tgtLoc)*1000));
        set(lblRMSAfter, 'String','配准后RMS:-- mm');
        set(lblImprove, 'String','误差改善:-- %');
        set(lblOverlap, 'String','配准重叠率:-- %');
        % 同步显示
        set(txtRot, 'String', sprintf('%.0f', rotAngle));
        set(txtNoise, 'String', sprintf('%.1f', sigma*1000));
    end
%% =========================================================================
%% ---------------- 回调函数:3. 编辑旋转角度 ----------------
%% =========================================================================
    function editRotation(~,~)
        rotVal = str2double(get(txtRot, 'String'));
        rotVal = max(0, min(180, rotVal));
        if isnan(rotVal); rotVal = 10; end
        set(txtRot, 'String', sprintf('%.0f', rotVal));
        set(sliderRot, 'Value', rotVal);
        refreshTargetCloud();
    end
%% =========================================================================
%% ---------------- 回调函数:4. 编辑噪声强度 ----------------
%% =========================================================================
    function editNoise(~,~)
        noiseVal = str2double(get(txtNoise, 'String'));
        noiseVal = max(0, min(5, noiseVal));
        if isnan(noiseVal); noiseVal = 0.5; end
        set(txtNoise, 'String', sprintf('%.1f', noiseVal));
        set(sliderNoise, 'Value', noiseVal);
        refreshTargetCloud();
    end
%% =========================================================================
%% ---------------- 回调函数:5. 运行FGR配准 ----------------
%% =========================================================================
    function runFGRRegistration(~,~)
        if isempty(sourcePtCloud) || isempty(targetPtCloud)
            errordlg('请先加载原始点云并生成目标点云', '提示');
            return;
        end
        set(lblStatus, 'String','状态:FGR配准中...', 'ForegroundColor','#FFA500');
        drawnow;
        try
            srcLoc = sourcePtCloud.Location;
            tgtLoc = targetPtCloud.Location;
            % 检查点云是否为空
            if isempty(srcLoc) || isempty(tgtLoc)
                error('点云数据为空,无法配准');
            end
            % 计算FPFH特征
            sourceFPFH = computeFPFH_improved(sourcePtCloud);
            targetFPFH = computeFPFH_improved(targetPtCloud);
            % 计算匹配阈值
            dist = sqrt(sum((srcLoc - tgtLoc).^2, 2));
            avgDist = mean(dist);
            stdDist = std(dist);
            maxCorrDist = max(avgDist + 1.2*stdDist, 0.01);
            % 执行FGR配准
            T = fastGlobalRegistration_improved(srcLoc, tgtLoc, ...
                                                sourceFPFH, targetFPFH, maxCorrDist);
            % 应用变换
            R = T(1:3,1:3);
            [U,~,V] = svd(R);
            R = U * V';
            tVec = T(1:3,4).';
            srcRegLoc = (R * srcLoc')' + repmat(tVec, size(srcLoc,1), 1);
            % 封装配准结果
            sourceRegPtCloud = pointCloud(srcRegLoc, ...
                                          'Color', sourcePtCloud.Color, ...
                                          'Normal', sourcePtCloud.Normal);
            % 显示结果
            showPointCloud(axReg, sourceRegPtCloud, '配准后点云(红)');
            hold(axReg, 'on');
            pcshow(targetPtCloud, 'Parent', axReg, 'MarkerSize',30);
            hold(axReg, 'off');
            title(axReg, 'FGR配准结果(红=配准后,绿=目标)');
            % 计算评价指标
            rmsBefore = computeRMS(srcLoc, tgtLoc) * 1000;
            rmsAfter = computeRMS(srcRegLoc, tgtLoc) * 1000;
            improveRate = (rmsBefore - rmsAfter) / rmsBefore * 100;
            inlierDist = sqrt(sum((srcRegLoc - tgtLoc).^2, 2));
            overlapRate = mean(inlierDist < 1.2*maxCorrDist) * 100;
            % 更新显示
            set(lblRMSBefore, 'String', sprintf('配准前RMS:%.3f mm', rmsBefore));
            set(lblRMSAfter, 'String', sprintf('配准后RMS:%.3f mm', rmsAfter));
            set(lblImprove, 'String', sprintf('误差改善:%.1f %%', improveRate));
            set(lblOverlap, 'String', sprintf('配准重叠率:%.1f %%', overlapRate));
            set(lblStatus, 'String','状态:FGR配准完成', 'ForegroundColor','green');
        catch ME
            errordlg(ME.message, '配准失败');
            set(lblStatus, 'String','状态:配准失败,请检查点云', 'ForegroundColor','red');
            sourceRegPtCloud = pointCloud.empty;
        end
    end
%% =========================================================================
%% ---------------- 回调函数:6. 保存配准结果 ----------------
%% =========================================================================
    function saveRegisteredCloud(~,~)
        if isempty(sourceRegPtCloud)
            errordlg('请先运行FGR配准生成结果', '提示');
            return;
        end
        [file, path] = uiputfile({'*.pcd;*.ply;*.xyz','点云文件'},'保存配准点云');
        if isequal(file,0); return; end
        try
            pcwrite(sourceRegPtCloud, fullfile(path,file), 'Precision','double');
            msgbox('保存成功!','提示');
        catch ME
            errordlg(ME.message, '保存失败');
        end
    end
%% =========================================================================
%% ---------------- 辅助函数:显示点云 ----------------
%% =========================================================================
    function showPointCloud(ax, pc, titleStr)
        cla(ax); set(ax, 'Color','white');
        pcshow(pc, 'Parent', ax, 'MarkerSize',30);
        axis(ax, 'tight'); grid(ax, 'on'); view(ax, 3);
        title(ax, titleStr);
        xlabel(ax, 'X (m)'); ylabel(ax, 'Y (m)'); zlabel(ax, 'Z (m)');
        drawnow;
    end
%% =========================================================================
%% ---------------- 辅助函数:计算RMS误差 ----------------
%% =========================================================================
    function rms = computeRMS(p1, p2)
        % 确保输入数组大小一致
        if size(p1,1) ~= size(p2,1)
            error('输入点云数量不一致,无法计算RMS');
        end
        distSq = sum((p1 - p2).^2, 2);
        rms = sqrt(mean(distSq));
    end
%% =========================================================================
%% ---------------- 核心子函数:改进版FPFH计算(添加索引检查) ----------------
%% =========================================================================
    function fpfh = computeFPFH_improved(ptCloud)
        P = ptCloud.Location;
        N = size(P,1);
        % 检查点云数量是否足够
        if N < 10
            error('点云数量过少,无法计算特征');
        end
        [k, d2] = knnsearch(P,P,'K',2);
        density    = mean(sqrt(d2(:,2)));
        radNormal  = 1.5 * density;
        radFPFH    = 3.0 * density;
        % 法向计算(减少邻域点)
        K = min(20, N-1);  % 确保不超过点云总数
        [nnIndices, dists] = knnsearch(P, P, 'K', K+1);
        nnIndices = nnIndices(:, 2:end);  % 去除自身
        dists = dists(:, 2:end);
        normal = zeros(N,3);
        curv   = zeros(N,1);
        for i = 1:N
            idx = nnIndices(i, :);
            % 过滤无效索引(确保不超出范围)
            idx = idx(idx > 0 & idx <= N);
            if numel(idx) < 3  % 降低要求,避免索引不足
                normal(i,:) = [0 0 1];
                continue;
            end
            weights = exp(-dists(i,1:numel(idx)).^2 / (2*(radNormal/2)^2));
            weights = weights';
            pts = P(idx,:) - P(i,:);
            weighted_pts = pts .* repmat(weights, 1, 3);
            [~,S,V] = svd(weighted_pts,'econ');
            n = V(:,3)';
            if n*[0 0 1]' < 0, n = -n; end
            normal(i,:) = n;
            curv(i) = S(3,3)/sum(diag(S));
        end
        % 简化SPFH计算
        K_fpfh = min(40, max(5, round(N/80)));  % 确保至少5个邻域点
        [adjIndices, adjDists] = knnsearch(P, P, 'K', K_fpfh+1);
        adjIndices = adjIndices(:, 2:end);
        adjDists = adjDists(:, 2:end);
        adj = num2cell(adjIndices, 2);
        adjWeights = num2cell(exp(-adjDists.^2 / (2*(radFPFH/2)^2)), 2);
        spfh = zeros(N,33);
        for i = 1:N
            nei = adj{i};
            % 过滤无效邻域索引
            if isempty(nei)
                continue;
            end
            nei = nei(nei > 0 & nei <= N);  % 确保索引在有效范围内
            if numel(nei) < 3
                continue;
            end
            u = normal(i,:);
            p = P(i,:);
            weights = adjWeights{i}(1:numel(nei));  % 截断权重数组
            v = P(nei,:) - p;
            v = v ./ sqrt(sum(v.^2, 2));
            n2 = normal(nei,:);
            f1 = sum(v .* repmat(u, size(v,1), 1), 2);
            f2 = sum(v .* n2, 2);
            f3 = sum(repmat(u, size(n2,1), 1) .* n2, 2) + 1;
            b1 = min(max(floor((f1+1)/2 * 9) + 1, 1), 9);
            b2 = min(max(floor(f2+1) + 1, 1), 2);
            b3 = min(max(floor(f3/2 * 2) + 1, 1), 2);
            hist = zeros(1,33);
            for j = 1:length(b1)
                bin = sub2ind([9 2 2], b1(j), b2(j), b3(j));
                if bin >= 1 && bin <= 33  % 检查分箱索引有效性
                    hist(bin) = hist(bin) + weights(j);
                end
            end
            histSum = sum(hist);
            if histSum > 0
                spfh(i,:) = hist ./ histSum;
            end
        end
        % FPFH计算(简化权重)
        fpfh = zeros(N,33);
        for i = 1:N
            nei = [i; adj{i}'];
            nei = nei(nei > 0 & nei <= N);  % 过滤无效索引
            if numel(nei) < 1
                fpfh(i,:) = zeros(1,33);
                continue;
            end
            fpfh(i,:) = mean(spfh(nei,:), 1);
        end
        fpfh = fpfh';
    end
%% =========================================================================
%% ---------------- 核心子函数:改进版FGR配准(添加索引检查) ----------------
%% =========================================================================
    function T = fastGlobalRegistration_improved(src, tgt, srcFPFH, tgtFPFH, maxCorrDist)
        % 检查输入特征维度是否匹配
        if size(srcFPFH,2) ~= size(tgtFPFH,2)
            error('特征维度不匹配,无法进行匹配');
        end
        [idxS, idxT, ratios] = featureMatching_improved(srcFPFH, tgtFPFH);
        % 过滤无效索引
        idxS = idxS(idxS > 0 & idxS <= size(src,1));
        idxT = idxT(idxT > 0 & idxT <= size(tgt,1));
        % 确保索引数量一致
        minLen = min(length(idxS), length(idxT));
        idxS = idxS(1:minLen);
        idxT = idxT(1:minLen);
        if isempty(idxS) || isempty(idxT)
            error('未找到有效匹配点,无法配准');
        end
        matchS = src(idxS,:);
        matchT = tgt(idxT,:);
        initDists = sqrt(sum((matchS - matchT).^2, 2));
        adaptiveTh = maxCorrDist .* (1 + ratios(1:minLen));  % 确保 ratios 索引有效
        goodMatchIdx = initDists < adaptiveTh;
        matchS = matchS(goodMatchIdx,:);
        matchT = matchT(goodMatchIdx,:);
        numMatches = size(matchS,1);
        minRequired = 10;
        if numMatches < minRequired
            warning(['匹配点数量不足,使用低阈值过滤 (', num2str(numMatches), ')']);
            for factor = 1.1:0.2:2.0
                goodMatchIdx = initDists < factor * maxCorrDist;
                matchS = matchS(goodMatchIdx,:);
                matchT = matchT(goodMatchIdx,:);
                numMatches = size(matchS,1);
                if numMatches >= minRequired
                    break;
                end
            end
            if numMatches < 3
                error('匹配点数量不足3个,无法计算刚体变换');
            end
        end
        % 优化RANSAC参数
        numSamples = min(2, numMatches);  % 确保采样数不超过匹配点数量
        maxIter    = 5000;
        inlierTh   = 0.8 * maxCorrDist;
        bestInl    = 0;
        bestT = eye(4);
        matchS_T = matchS';
        p = 0.95;
        outlierRatio = 0.5;
        iter = 0;
        while iter < maxIter && numMatches >= numSamples
            neededIter = log(1-p)/log(1-(1-outlierRatio)^numSamples);
            maxIter = max(maxIter, ceil(neededIter));
            rnd = randi(numMatches, numSamples,1);
            A = matchS(rnd,:);  B = matchT(rnd,:);
            [R, t] = kabsch(A, B);
            transformed = (R * matchS_T)';
            transformed = transformed + repmat(t, numMatches, 1);
            dist = sqrt(sum( (transformed - matchT).^2 ,2));
            inlierCount = sum(dist <= inlierTh);
            if inlierCount > bestInl
                bestInl = inlierCount;
                bestT  = [R t'; 0 0 0 1];
                outlierRatio = 1 - bestInl/numMatches;
                if outlierRatio < 0.05
                    break;
                end
            end
            iter = iter + 1;
        end
        % 精修步骤
        if numMatches >= 1
            transformed = (bestT(1:3,1:3) * matchS_T)';
            transformed = transformed + repmat(bestT(1:3,4)', numMatches, 1);
            dist = sqrt(sum( (transformed - matchT).^2 ,2));
            inlIdx = dist <= 1.5*inlierTh;
            if sum(inlIdx) >= 3
                [R_refine, t_refine] = kabsch(matchS(inlIdx,:), matchT(inlIdx,:));
                T = [R_refine t_refine'; 0 0 0 1];
                return;
            end
        end
        % 若精修失败,返回最佳变换
        T = bestT;
    end
%% =========================================================================
%% ---------------- 核心子函数:改进版特征匹配(添加索引检查) ----------------
%% =========================================================================
    function [idxS, idxT, ratios] = featureMatching_improved(featS, featT)
        % 确保特征非空
        if isempty(featS) || isempty(featT)
            error('输入特征为空,无法匹配');
        end
        T = featT';
        S = featS';
        [idx, dist] = knnsearch(T, S, 'NSMethod', 'kdtree', 'K', 2);
        % 过滤无效距离(避免除以零)
        valid = dist(:,2) > 1e-9;
        idx = idx(valid,:);
        dist = dist(valid,:);
        if isempty(dist)
            idxS = [];
            idxT = [];
            ratios = [];
            return;
        end
        ratios = dist(:,1) ./ dist(:,2);
        ratioTh = min(0.9, max(0.7, prctile(ratios, 30)));
        goodMatch = ratios < ratioTh;
        fprintf('特征匹配:比率阈值 %.2f,保留 %d 个匹配点\n', ratioTh, sum(goodMatch));
        idxS = find(valid);
        idxS = idxS(goodMatch);
        idxT = idx(goodMatch,1).';
    end
%% =========================================================================
%% ---------------- 核心子函数:标准Kabsch算法 ----------------
%% =========================================================================
    function [R, t] = kabsch(A, B)
        % 确保输入点数量一致
        if size(A,1) ~= size(B,1)
            error('输入点数量不一致,无法计算变换');
        end
        muA = mean(A,1);  muB = mean(B,1);
        A0  = A - muA;    B0  = B - muB;
        H   = A0' * B0;
        [U,~,V] = svd(H);
        d = sign(det(V' * U));
        S = eye(3); S(3,3) = d;
        R = V * S * U';
        t = muB - muA * R;
    end
end

二、点云FGR的结果

        可以看到,点云大致能配准上,谁让它是粗配准呢。不过这不是最要紧的,最要紧的是,本期的FGR使用的是matlab2020a版本,没使用pcregisterfgr函数,这样就意味着它不依赖于matlab版本,为FGR多元化做出了一定的贡献。

就酱,下次见^-6

posted on 2025-10-04 20:33  ljbguanli  阅读(0)  评论(0)    收藏  举报