实用指南:Matlab通过GUI实现点云的统计滤波(附最简版)

        本次分享使用Matlab进行点云的统计滤波。点云统计滤波是一种基于邻域统计信息去除离群点、提升数据质量的常用方法,广泛应用于自动驾驶、地形建模、工业检测等领域。下面将从算法原理、实现流程和典型应用三个方面进行系统介绍。

一、算法原理

        统计滤波(Statistical Outlier Removal)假设点云中每个点与其邻域点的距离服从高斯分布。通过计算每个点到其最近 *k* 个邻域点的平均距离,判断其是否显著偏离全局统计特性:

        - 若某点的平均邻域距离超出 **均值 + α × 标准差**(α 为阈值系数),则视为离群点并剔除;
        - 该方法能有效去除稀疏分布的噪声点,同时保留点云的几何结构。

二、MATLAB 实现流程

1. 读取点云数据

ptCloud = pcread('input.pcd');  % 支持 .pcd、.ply 等格式

2. 设置滤波参数

numNeighbors = 50;      % 邻域点数 k
stdThreshold = 1.0;     % 标准差倍数阈值

3. 执行统计滤波

filteredPtCloud = pcfilterstatistical(ptCloud, numNeighbors, stdThreshold);

若需保留强度信息,可结合索引手动筛选:

[~, idx] = pcfilterstatistical(ptCloud, numNeighbors, stdThreshold);
filteredPtCloud = select(ptCloud, idx);  % 同时保留 Location 与 Intensity

4. 可视化对比

figure;
subplot(1,2,1); pcshow(ptCloud); title('原始点云');
subplot(1,2,2); pcshow(filteredPtCloud); title('统计滤波后');

三、应用领域

领域应用示例
自动驾驶去除车载激光雷达采集中的飘移点,提升障碍物检测精度
地形测绘滤除航空点云中的孤立噪声,保留地面与建筑物边缘特征
工业检测清除扫描模型中的离群点,提高后续配准、建模的鲁棒性
文物保护优化文物三维扫描数据,保留细节的同时消除环境噪声

四、小结

        统计滤波以其原理简单、计算高效、结构保持性好的优势,成为 MATLAB 点云预处理的首选方法之一。结合 KD 树加速邻域搜索,可进一步提升处理效率。在实际应用中,建议与半径滤波、直通滤波等方法组合使用,以适应不同场景需求。如需进一步处理,可引入双边滤波或移动最小二乘(MLS)平滑以提升表面质量。

本次使用的数据是——————兔砸!

一、点云统计滤波程序

1、最简版

clc; clear; close all;
%% 1. 读入点云
plyFile = 'E:\CSDN\规则点云\实拍\窗帘.ply';   % <-- 换成你的实际路径
ptCloud = pcread(plyFile);                    % MATLAB 内置函数
fprintf('原始点云有 %d 个点\n', ptCloud.Count);
%% 2. 统计离群滤波
% 参数与 Open3D 对应:
%   nb_neighbors = 100
%   std_ratio    = 2.0
% pcdenoise 的 ‘NumNeighbors’ 就是 K,‘Threshold’ 是倍数 σ
filteredPtCloud = pcdenoise(ptCloud, ...
                            'NumNeighbors', 100, ...
                            'Threshold', 2.0);   % 2*sigma
fprintf('统计滤波有 %d 个点\n', filteredPtCloud.Count);
%% 3. 可视化
figure('Name','原始点云','Position',[50 50 1200 800]);
pcshow(ptCloud);
title('原始点云');
figure('Name','统计滤波','Position',[50 50 1200 800]);
pcshow(filteredPtCloud);
title('统计滤波');

2、GUI版本

function statFilterGUI
% 统计滤波 GUI —— 2020a 兼容
% 1. 浏览选点云  2. 邻域数滑块  3. std 滑块  4. 滤波结果保存
fig = figure('Name','统计滤波工具','NumberTitle','off',...
             'MenuBar','none','ToolBar','none','Position',[100 100 1280 720]);
%% ---------- 左侧图像区(78 %) ----------
imgWidth = 0.78;
panelW   = imgWidth/2 - 0.01;   % 只放两图:原始 + 滤波
pnlOrig = uipanel('Parent',fig,'Units','normalized',...
                  'FontSize',16,...
                  'Position',[0.02 0.02 panelW 0.96],'Title','原始点云');
pnlFilt = uipanel('Parent',fig,'Units','normalized',...
                  'FontSize',16,...
                  'Position',[0.02+panelW+0.01 0.02 panelW 0.96],'Title','统计滤波');
axOrig = axes('Parent',pnlOrig,'Units','normalized','Position',[0.05 0.05 0.90 0.90]);
axFilt = axes('Parent',pnlFilt,'Units','normalized','Position',[0.05 0.05 0.90 0.90]);
%% ---------- 右侧控制区(22 %) ----------
pnlCtrl = uipanel('Parent',fig,'Units','normalized',...
                  'FontSize',16,...
                  'Position',[0.78 0 0.22 1],'Title','控制');
txtH  = 0.04;   % 文字高
btnH  = 0.06;   % 按钮高
gap   = 0.02;   % 间隙
yTop  = 0.94;   % 顶部
% 1. 浏览
uicontrol('Parent',pnlCtrl,'Style','pushbutton','String','浏览…',...
            'FontSize',16,...
          'Units','normalized','Position',[0.05 yTop-btnH 0.90 btnH],...
          'Callback',@loadCloud);
yTop = yTop - btnH - gap;
lblInfo = uicontrol('Parent',pnlCtrl,'Style','text','String','未加载点云',...
                    'FontSize',10,...
                    'Units','normalized','Position',[0.05 yTop-txtH 0.90 txtH],...
                    'HorizontalAlignment','left');
yTop = yTop - txtH - gap;
% 2. 邻域数
uicontrol('Parent',pnlCtrl,'Style','text','String','邻域数',...
          'FontSize',16,...
          'Units','normalized','Position',[0.05 yTop-txtH 0.90 txtH],...
          'FontSize',12,'FontWeight','bold','HorizontalAlignment','left');
yTop = yTop - txtH - gap;
sliderK = uicontrol('Parent',pnlCtrl,'Style','slider','Min',10,'Max',200,'Value',50,...
                    'FontSize',16,...
                    'Units','normalized','Position',[0.05 yTop-btnH 0.65 btnH],...
                    'Callback',@refreshFilter);
txtK    = uicontrol('Parent',pnlCtrl,'Style','edit','String','50',...
                    'FontSize',16,...
                    'Units','normalized','Position',[0.75 yTop-btnH 0.20 btnH],...
                    'Callback',@editKCB);
yTop = yTop - btnH - gap;
% 3. std 倍数
uicontrol('Parent',pnlCtrl,'Style','text','String','std 倍数',...
          'FontSize',16,...
          'Units','normalized','Position',[0.05 yTop-txtH 0.90 txtH],...
          'FontSize',12,'FontWeight','bold','HorizontalAlignment','left');
yTop = yTop - txtH - gap;
sliderStd = uicontrol('Parent',pnlCtrl,'Style','slider','Min',0.5,'Max',3.0,'Value',2.0,...
                      'FontSize',16,...
                      'Units','normalized','Position',[0.05 yTop-btnH 0.65 btnH],...
                      'Callback',@refreshFilter);
txtStd    = uicontrol('Parent',pnlCtrl,'Style','edit','String','2.0',...
                      'FontSize',16,...
                      'Units','normalized','Position',[0.75 yTop-btnH 0.20 btnH],...
                      'Callback',@editStdCB);
yTop = yTop - btnH - gap;
% 4. 保存
uicontrol('Parent',pnlCtrl,'Style','pushbutton','String','保存滤波结果',...
          'FontSize',16,...
          'Units','normalized','Position',[0.05 yTop-btnH 0.90 btnH],...
          'Callback',@(s,e)saveCloud(ptCloudFilt));
%% ---------- 数据 ----------
ptCloudOrig = pointCloud.empty;
ptCloudFilt = pointCloud.empty;
    %% ---------- 回调 ----------
    function loadCloud(~,~)
        [file,path] = uigetfile({'*.pcd;*.ply;*.xyz','点云文件'},'选择点云');
        if isequal(file,0), return; end
        try
            ptCloudOrig = pcread(fullfile(path,file));
        catch ME
            errordlg(ME.message,'读取失败'); return;
        end
        showPointCloud(axOrig,ptCloudOrig);  % ← 必须显式给句柄
        N = ptCloudOrig.Count;
        set(lblInfo,'String',sprintf('已加载:%s  (%d 点)',file,N));
        refreshFilter();
    end
    function refreshFilter(~,~)
        if isempty(ptCloudOrig), return; end
        k   = round(get(sliderK,'Value'));
        std = get(sliderStd,'Value');
        ptCloudFilt = pcdStatFilter(ptCloudOrig,k,std);
        showPointCloud(axFilt,ptCloudFilt);
        set(txtK,'String',num2str(k));
        set(txtStd,'String',num2str(std));
    end
    function editKCB(src,~)
        v = str2double(get(src,'String'));
        if isnan(v), v = 50; end
        v = max(10,min(200,round(v)));
        set(sliderK,'Value',v);
        refreshFilter();
    end
    function editStdCB(src,~)
        v = str2double(get(src,'String'));
        if isnan(v), v = 2.0; end
        v = max(0.5,min(3.0,v));
        set(sliderStd,'Value',v);
        refreshFilter();
    end
    function saveCloud(cloud)
        if isempty(cloud)
            errordlg('请先完成统计滤波','提示'); return;
        end
        [file,path] = uiputfile({'*.pcd','PCD';'*.ply','PLY';'*.xyz','XYZ'},'保存滤波点云');
        if isequal(file,0), return; end
        try
            pcwrite(cloud,fullfile(path,file),'Precision','double');
            msgbox('保存成功!','提示');
        catch ME
            errordlg(ME.message,'保存失败');
        end
    end
    function showPointCloud(ax,pc)
        cla(ax);
        set(ax,'Color','w');
        pcshow(pointCloud(nan(0,3)),'Parent',ax);  % 2020a 暖启动(防 rotateFromCenter)
        pcshow(pc,'Parent',ax,'MarkerSize',35);
        axis(ax,'tight'); grid(ax,'on'); view(ax,3);
    end
    %% ---------- 统计滤波核心 ----------
    function out = pcdStatFilter(pc,k,stdRatio)
    xyz = pc.Location;
    n   = size(xyz,1);
    % 1. 找 k 邻域(去掉自己)
    [idx,~] = knnsearch(xyz,xyz,'K',k+1);
    idx = idx(:,2:end);          % n×k
    % 2. 逐点计算到邻域的平均距离
    dist = zeros(n,1);
    for i = 1:n
        dist(i) = mean(sqrt(sum((xyz(i,:) - xyz(idx(i,:),:)).^2,2)));
    end
    % 3. 统计门限
    mu  = mean(dist);
    sig = std(dist);
    th  = mu + stdRatio * sig;
    % 4. 提取内点
    out = select(pc,find(dist <= th));
    end
end

二、点云统计滤波结果

        可以看到,本次使用的依旧是GUI画面,确实好用啊,统计滤波的邻域和倍数信息也可以自由的调节,总体功能实现完整。有兴趣的同学自己play吧!

就酱,下次见^-^

posted @ 2025-10-24 13:30  yxysuanfa  阅读(7)  评论(0)    收藏  举报