Matlab通过GUI实现点云的均值滤波(附最简版)

本节我们分享使用Matlab进行点云的均值滤波。均值滤波(Moving-Average Filter)是点云预处理中最基础、最常用的线性平滑方法之一。其思路是对每个点在其 k-邻域(或 r-半径邻域)内计算几何坐标的算术平均,用该均值替代原始点位置,达到抑制零均值随机噪声、平滑表面的目的。与高斯滤波相比,均值滤波模板权重均匀、计算量小;与中值滤波相比,它对高密度小幅度噪声更敏感,但保持边缘能力稍弱,因此常作为"粗去噪"或"预平滑"步骤出现。

一、MATLAB 主要实现流程

  1. 读取与可视化

    ptCloud = pcread('scene.ply'); % 支持 ply/pcd 等格式
    pcshow(ptCloud); title('原始点云');

  2. 添加噪声(可选,用于算法验证)

    noise = normrnd(0,0.002,size(ptCloud.Location)); % N(0,σ)
    noisyXYZ = ptCloud.Location + noise;
    ptCloud = pointCloud(noisyXYZ);

  3. 直接调用内置平滑函数(最快)

    % 按"movmean"方式对 x/y/z 分别做滑动平均
    smoothXYZ = smoothdata(ptCloud.Location,'movmean',k); % k 为窗宽
    ptSmooth = pointCloud(smoothXYZ);

  4. 自定义邻域均值(精度高、参数灵活)

    % 构建 KD-Tree 加速邻域搜索
    [ids,~] = knnsearch(ptCloud.Location,ptCloud.Location,'K',k);
    neighborMean = zeros(size(ptCloud.Location));
    for i = 1:ptCloud.Count
    idx = ids(i,:); % k 个最近序号
    neighborMean(i,:) = mean(ptCloud.Location(idx,:));
    end
    ptSmooth = pointCloud(neighborMean);

  5. 结果可视化与保存

    figure;
    pcshow(ptSmooth); title('均值滤波后');
    pcwrite(ptSmooth,'scene_meanFilter.ply');

二、关键参数与调优建议

  • k(邻域点数)或 r(搜索半径):越大则平滑越强,但会削弱细节;一般先取 20--50 或半径 5--10 mm 做试验,再按几何分辨率与噪声幅度微调。

  • 距离阈值:可叠加"统计离群剔除",对邻域平均距离超过 μ+α·σ 的点先剔除再求均值,兼顾去噪与保边。

  • GPU/并行:若点云>10^6,使用 `knnsearch` 的 `'NSMethod', 'exhaustive', 'Distance', 'euclidean'` 并打开 `parpool` 可显著提高速度。

三、典型应用领域

  1. 三维重建:平滑无序扫描数据,减少网格重建后的"毛刺"与伪孔洞。

  2. 工业检测:平滑铸件、机加件点云,提高后续配准与尺寸测量精度。

  3. 自动驾驶:对车载 LiDAR 原始扫描做"粗去噪",为地面分割、目标聚类提供更干净输入。

  4. 室内/地下测绘:抑制手持 SLAM 点云的抖动噪声,增强可视化与制图效果。

  5. 文物与医学:在保留关键纹理的前提下削弱设备电子噪声,便于曲面拟合与特征提取。

四、小结

均值滤波因其原理简单、实现方便,在 MATLAB 环境下既可一行代码快速调用,又能基于 KD-Tree 自定义精细控制,是点云预处理链中"去随机噪、平滑表面"的首选工具;后续可串联统计滤波、体素下采样或法向重估,为配准、分割、重建等高级算法奠定高质量数据基础。

本次使用的数据是兔兔兔------------窗帘!

一、点云均值滤波的程序

1、最简版

复制代码
%% 0. 清空环境
clear; clc; close all;

%% 1. 读入点云
[file, path] = uigetfile({'*.ply;*.pcd;*.xyz', '点云文件 (*.ply,*.pcd,*.xyz)'}, ...
                         '请选择点云');
if file == 0; return; end
fname = fullfile(path, file);
ptCloud = pcread(fname);
N = ptCloud.Count;
fprintf('原始点云有 %d 个点\n', N);

%% 2. 均值滤波(半径 50 mm)
radius = 50;
cloudMean = pcdMeanFilter(ptCloud, radius);
fprintf('均值滤波(半径=%d mm)后 %d 个点\n', radius, cloudMean.Count);

%% 3. 可视化
figure('Name', '原始点云', 'NumberTitle', 'off');
pcshow(ptCloud); axis on; view(3);

figure('Name', '均值滤波', 'NumberTitle', 'off');
pcshow(cloudMean); axis on; view(3);

function out = pcdMeanFilter(pc, radiusMm)
% 均值滤波(模仿 block-wise 风格)
% pc        : pointCloud 对象
% radiusMm  : 邻域半径,单位 mm
xyz = pc.Location;
radius = radiusMm; 
n = size(xyz, 1);
newXYZ = zeros(n, 3, 'single');

kd = createns(xyz, 'NSMethod', 'kdtree');
block = 5000;                   % 按内存可调
for i = 1:block:n
    ir = min(i + block - 1, n);
    idxCell = rangesearch(kd, xyz(i:ir, :), radius);
    for j = i:ir
        idx = idxCell{j-i+1};
        newXYZ(j, :) = mean(xyz(idx, :), 1);   % 均值坐标
    end
end
% 重建 pointCloud,保留颜色等信息
out = pointCloud(newXYZ, 'Color', pc.Color, ...
                 'Normal', pc.Normal);
end

2、GUI版本

复制代码
function meanFilterGUI
% 均值滤波 GUI ------ 2020a 兼容
% 1. 浏览选点云  2. 邻域半径/mm 滑块  3. 实时均值滤波  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. radius/mm
uicontrol('Parent',pnlCtrl,'Style','text','String','邻域半径 / mm',...
          'FontSize',12,'FontWeight','bold','Units','normalized','Position',[0.05 yTop-txtH 0.90 txtH],...
          'HorizontalAlignment','left');
yTop = yTop - txtH - gap;

sliderR = uicontrol('Parent',pnlCtrl,'Style','slider','Min',1,'Max',100,'Value',50,...
                    'FontSize',16,'Units','normalized','Position',[0.05 yTop-btnH 0.65 btnH],...
                    'Callback',@refreshFilter);
txtR    = uicontrol('Parent',pnlCtrl,'Style','edit','String','50',...
                    'FontSize',16,'Units','normalized','Position',[0.75 yTop-btnH 0.20 btnH],...
                    'Callback',@editRCB);
yTop = yTop - btnH - gap;

% 3. 保存
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
        r = get(sliderR,'Value');
        ptCloudFilt = pcdMeanFilter(ptCloudOrig,r);
        showPointCloud(axFilt,ptCloudFilt);
        set(txtR,'String',num2str(r));
    end

    function editRCB(src,~)
        v = str2double(get(src,'String'));
        if isnan(v), v = 50; end
        v = max(1,min(100,v));
        set(sliderR,'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 暖启动
        pcshow(pc,'Parent',ax,'MarkerSize',35);
        axis(ax,'tight'); grid(ax,'on'); view(ax,3);
    end

    %% ---------- 均值滤波核心 ----------
    function out = pcdMeanFilter(pc,radiusMm)
    xyz  = pc.Location;
    n    = size(xyz,1);
    radius = radiusMm;          % 转米

    kd = createns(xyz,'NSMethod','kdtree');
    block = 10000;                   % 批查询
    xyzMean = zeros(n,3,'like',xyz);

    for i = 1:block:n
        ir = min(i+block-1,n);
        idxCell = rangesearch(kd, xyz(i:ir,:), radius);
        for k = i:ir
            idx = idxCell{k-i+1};
            if numel(idx)>0
                xyzMean(k,:) = mean(xyz(idx,:),1);
            else
                xyzMean(k,:) = xyz(k,:);   % 无邻域保持原值
            end
        end
    end

    out = pointCloud(xyzMean, ...
                     'Color', pc.Color, ...
                     'Normal', pc.Normal);
    end
end

二、点云均值滤波的结果

使用依旧是GUI(真香!!),通过调节均值半径,可以看到窗帘明显边沿噪声部分被抑制删除,感兴趣的童鞋可以自己再试试别的点云。

就酱,下次见^-^

相关推荐
不枯石2 小时前
Matlab通过GUI实现点云的双边(Bilateral)滤波(附最简版)
开发语言·图像处理·算法·计算机视觉·matlab
二十雨辰3 小时前
vite如何处理项目中的资源
开发语言·javascript
聆风吟º3 小时前
远程录制新体验:Bililive-go与cpolar的无缝协作
开发语言·后端·golang
白水先森4 小时前
C语言作用域与数组详解
java·数据结构·算法
想唱rap4 小时前
直接选择排序、堆排序、冒泡排序
c语言·数据结构·笔记·算法·新浪微博
豆浆whisky4 小时前
netpoll性能调优:Go网络编程的隐藏利器|Go语言进阶(8)
开发语言·网络·后端·golang·go
蓝天白云下遛狗4 小时前
go环境的安装
开发语言·后端·golang
CAir25 小时前
go协程的前世今生
开发语言·golang·协程
@大迁世界5 小时前
Go 会成为“老生态”的新引擎吗?
开发语言·后端·golang