本次我们分享关于Matlab实现点云的双边滤波。在三维点云数据处理中,噪声去除与特征保留往往是一对矛盾的需求 ------ 传统滤波算法(如高斯滤波)在平滑噪声的同时,容易模糊点云中的边缘、棱角等关键特征。而双边滤波(Bilateral Filtering) 作为一种非线性滤波技术,通过同时考虑空间域权重与灰度(或强度)域权重,能够在抑制噪声的同时有效保留点云的细节特征,成为点云预处理中的核心算法之一。Matlab 作为工程领域常用的数值计算与可视化平台,提供了便捷的点云处理工具箱(PointCloud Toolbox),支持双边滤波的快速实现与优化。
一、点云双边滤波的核心原理与优势
- 基本原理
点云双边滤波的本质是对每个目标点,根据其邻域内点的空间距离 和属性差异(如颜色、法向量、强度值等)计算加权平均,从而更新目标点的坐标。其核心公式可表示为:
\(P_{filtered} = \frac{1}{W} \sum_{q \in N(p)} \left[ G_s(\|p - q\|) \cdot G_r(\|I_p - I_q\|) \right] \cdot q\)
其中:
- \(p\) 为目标点,\(q\) 为 \(p\) 邻域内的点,\(N(p)\) 表示 \(p\) 的邻域集合;
- \(G_s\) 为空间域高斯函数,根据点之间的欧氏距离分配权重,距离越近权重越大;
- \(G_r\) 为属性域高斯函数,根据点的属性差异分配权重,差异越小权重越大;
- \(W\) 为归一化系数,确保滤波后点的坐标范围合理。
- 核心优势
与传统滤波算法相比,Matlab 点云双边滤波具有以下特点:
- 特征保留能力强:通过属性域权重约束,避免边缘、棱角等特征被过度平滑;
- 参数可控性高:可通过调整空间域标准差(控制邻域范围)、属性域标准差(控制属性差异敏感度),适配不同噪声强度的点云;
- 兼容性广:支持 XYZ 坐标点云、带颜色(RGB)点云、带强度(Intensity)点云等多种类型数据,可与 Matlab 点云分割、配准等模块无缝衔接。
二、Matlab 点云双边滤波的主要流程
Matlab 点云双边滤波的实现依赖于pointCloud类与bilateralFilter函数(需安装 PointCloud Toolbox),完整流程可分为数据准备→参数设置→滤波执行→结果评估四步,具体操作如下:
- 数据准备:加载与预处理
首先需加载点云数据,并进行基础预处理(如去除无效点、下采样),减少后续计算量。Matlab 支持多种点云格式(如 PLY、PCD、XYZ),常用函数包括pcread(读取点云)、pcremoveinvalid(去除无效点)、pcdownsample(下采样)。
示例代码:
cpp% 读取PLY格式点云(以桌面场景点云为例) ptCloud = pcread('desk_scene.ply'); % 去除无效点(如NaN坐标点) ptCloud = pcremoveinvalid(ptCloud); % 下采样(体素下采样,体素大小0.01m,减少点云数量) ptCloud = pcdownsample(ptCloud, 'gridAverage', 0.01); % 可视化原始点云 figure; pcshow(ptCloud); title('原始点云'); xlabel('X'); ylabel('Y'); zlabel('Z');
- 参数设置:关键参数选择
BilateralFilter函数的核心参数需根据点云噪声强度与特征需求调整,主要包括:
- sigmaS (空间域标准差):控制邻域搜索范围,单位与点云坐标一致(如米)。噪声越强,需适当增大sigmaS(如 0.02~0.05),但过大易导致特征模糊;
- sigmaR (属性域标准差):控制属性差异敏感度,若点云无颜色 / 强度信息,可基于点云法向量(需提前计算)或坐标差异设置。例如,带颜色点云可设sigmaR=0.05(RGB 值归一化后),无属性点云可基于 XYZ 差异设sigmaR=0.01;
- kernelSize (核大小):邻域搜索的核尺寸,通常设为2*ceil(3*sigmaS)+1(确保覆盖 99.7% 的高斯分布范围),默认自动计算;
- metric (属性度量方式):指定属性域的计算方式,如'color'(基于 RGB)、'intensity'(基于强度)、'normal'(基于法向量),无属性时用'euclidean'(基于坐标)。
参数选择原则:
- 若点云噪声密集(如激光雷达扫描的远距离点云),增大sigmaS(如 0.04),同时减小sigmaR(如 0.005),平衡平滑与特征保留;
- 若点云含精细特征(如机械零件的棱角),减小sigmaS(如 0.02),增大sigmaR(如 0.08),避免特征丢失。
- 滤波执行:调用函数实现
根据点云类型调用bilateralFilter函数,输出滤波后的点云对象。若点云无颜色 / 强度属性,需先计算法向量作为属性输入。
示例代码:
cpp% 情况1:带颜色的点云(直接基于RGB属性滤波) if isfield(ptCloud.Location, 'Color') % 设置参数:空间域标准差0.03m,颜色域标准差0.05(RGB归一化后) sigmaS = 0.03; sigmaR = 0.05; % 执行双边滤波 ptCloudFiltered = bilateralFilter(ptCloud, sigmaS, sigmaR, 'metric', 'color'); else % 情况2:无属性点云(基于法向量滤波,需先计算法向量) % 计算法向量(邻域大小5,用于属性度量) ptCloud = pcnormals(ptCloud, 5); % 设置参数:空间域标准差0.03m,法向量域标准差0.1 sigmaS = 0.03; sigmaR = 0.1; % 执行双边滤波 ptCloudFiltered = bilateralFilter(ptCloud, sigmaS, sigmaR, 'metric', 'normal'); end % 可视化滤波后点云 figure; pcshow(ptCloudFiltered); title('双边滤波后点云'); xlabel('X'); ylabel('Y'); zlabel('Z');
- 结果评估:定量与定性分析
滤波效果需通过定性观察 (特征保留程度)与定量指标(噪声抑制效果)结合评估:
- 定性评估:对比原始点云与滤波后点云的边缘、棱角细节(如机械零件的孔、凸起),观察是否存在过度平滑;
- 定量评估 :计算点云的均方根误差(RMSE) (若有真值点云)、点云密度变化 (滤波后点数量应与原始接近,避免过度删减)、法向量一致性(特征区域法向量方向是否连续)。
定量评估示例代码:
cpp% 计算原始点云与滤波后点云的坐标差异(假设无真值,用自身差异近似噪声) diffXYZ = ptCloud.Location - ptCloudFiltered.Location; rmse = sqrt(mean(diffXYZ(:).^2)); % 均方根误差 fprintf('滤波后点云RMSE:%.6f m\n', rmse); % 计算点云密度变化 originalCount = ptCloud.Count; filteredCount = ptCloudFiltered.Count; densityRatio = filteredCount / originalCount; fprintf('滤波后点数量保留比例:%.2f%%\n', densityRatio * 100);
三、Matlab 点云双边滤波的应用领域
由于其 "噪声抑制 + 特征保留" 的核心优势,Matlab 点云双边滤波广泛应用于工业检测、自动驾驶、文化遗产保护、医疗影像等领域,具体场景如下:
- 工业零件检测
在机械零件三维扫描(如激光扫描、结构光扫描)中,点云易因设备精度、环境振动引入噪声,导致后续尺寸测量(如孔径、壁厚)误差。通过 Matlab 双边滤波,可在去除噪声的同时保留零件的棱角、螺纹等关键特征,为后续的尺寸标注(pcfitcylinder/pcfitsphere) 、缺陷检测提供高精度点云数据。
应用案例:汽车发动机缸体的三维扫描点云处理,滤波后可准确拟合缸体孔的圆柱面,测量孔径公差是否符合标准。
- 自动驾驶环境感知
自动驾驶的激光雷达(LiDAR)点云常包含雨雪噪声、地面杂点,若直接用于障碍物检测(如行人、车辆),易导致误识别。Matlab 双边滤波可基于点云的强度值(LiDAR 反射强度)或法向量(地面与障碍物法向量差异大),过滤雨雪噪声与地面杂点,同时保留障碍物的轮廓特征,提升后续目标分割(pcsegplane) 与跟踪的准确性。
- 文化遗产数字化保护
在文物(如石雕、青铜器)三维重建中,需同时保留文物的纹理细节(如刻痕、花纹)与整体形态。传统滤波易模糊纹理,而 Matlab 双边滤波可通过调整颜色域(纹理颜色差异)或法向量域(纹理凹凸差异)参数,在平滑扫描噪声的同时,完整保留文物的纹理特征,为后续的数字化存档 、虚拟修复提供高保真点云。
- 医疗影像三维重建
在医学 CT、MRI 的三维点云重建(如骨骼、器官)中,点云噪声会影响手术规划的精度。Matlab 双边滤波可基于医学影像的灰度值(CT 值)作为属性域权重,过滤重建过程中的伪影噪声,同时保留骨骼的关节面、器官的轮廓等关键结构,为手术导航 、假体设计提供精准的三维模型。
四、总结与注意事项
Matlab 点云双边滤波凭借其灵活的参数设置与强大的特征保留能力,成为点云预处理的关键工具。在实际应用中,需注意以下事项:
1. 参数调试:sigmaS与sigmaR需根据点云噪声强度迭代调试,建议先从较小sigmaS(如 0.02)与中等sigmaR(如 0.05)开始,逐步优化;
2. 计算效率:双边滤波为非线性操作,点云数量过大时(如百万级)会导致计算缓慢,建议先通过pcdownsample下采样(体素大小 0.01~0.05m),平衡效率与精度;
3. 属性选择:优先使用点云的固有属性(如颜色、强度)作为metric参数,若无属性再使用法向量,避免仅基于 XYZ 坐标导致特征丢失。
本次使用的数据是------------兔砸!!!

一、点云实现双边滤波的程序
1、最简版
cpp
%% 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. 计算平均密度(最近邻距离均值)
kdtree0 = createns(ptCloud.Location,'NSMethod','kdtree');
[~, nndist0] = knnsearch(kdtree0,ptCloud.Location,'K',2); % 第2列=最近邻距离
density = mean(nndist0(:,2));
fprintf('点云平均密度 = %.4f\n',density);
%% 3. 添加高斯噪声
sigmaNoise = density * 2;
noise = sigmaNoise * randn(N,3);
noisyCloud = pointCloud(ptCloud.Location + noise,...
'Color',ptCloud.Color,...
'Normal',ptCloud.Normal);
fprintf('添加噪声后 %d 个点\n',noisyCloud.Count);
%% 4. 双边滤波
K = 30; % 与原脚本一致
srMm = 10; % 与原脚本一致
filtCloud = pcdBilateralFilter(noisyCloud,K,srMm);
fprintf('双边滤波后 %d 个点\n',filtCloud.Count);
%% 5. 可视化
figure('Name','原始点云','NumberTitle','off'); pcshow(ptCloud); axis on; view(3);
figure('Name','噪声点云','NumberTitle','off'); pcshow(noisyCloud); axis on; view(3);
figure('Name','双边滤波','NumberTitle','off'); pcshow(filtCloud); axis on; view(3);
function out = pcdBilateralFilter(pc,K,srMm)
K = max(round(K),2);
sr = srMm/1000;
xyz = pc.Location;
n = size(xyz,1);
kd = createns(xyz,'NSMethod','kdtree');
% 自算法向量
block = 10000;
normals= zeros(n,3,'like',xyz);
h = waitbar(0,'计算法向量...');
for i = 1:block:n
ir = min(i+block-1,n);
[idxMat,~] = knnsearch(kd, xyz(i:ir,:), 'K', K);
for k = i:ir
idx = idxMat(k-i+1,:);
P = xyz(idx,:);
[~,~,V] = svd(P - mean(P,1),0);
nk = V(:,3);
if nk(3)>0, nk=-nk; end
normals(k,:) = nk;
end
waitbar(ir/n,h);
end
close(h);
% 双边滤波
xyzNew = zeros(n,3,'like',xyz);
h = waitbar(0,'双边滤波...');
for i = 1:block:n
ir = min(i+block-1,n);
[idxMat,~] = knnsearch(kd, xyz(i:ir,:), 'K', K);
for k = i:ir
idx = idxMat(k-i+1,:);
p0 = xyz(k,:);
n0 = normals(k,:);
Ws = 0; Z = 0;
for t = 2:numel(idx)
pj = xyz(idx(t),:);
vec = pj - p0;
dd = norm(vec);
dn = dot(vec, n0);
ws = exp(-(dd^2)/(2*(0.05)^2));
wr = exp(-(dn^2)/(2*sr^2));
w = ws*wr;
Ws = Ws + w;
Z = Z + w*dn;
end
xyzNew(k,:) = p0 + (Z/(Ws+eps))*n0;
end
waitbar(ir/n,h);
end
close(h);
out = pointCloud(xyzNew,'Color',pc.Color,'Normal',normals);
end
2、GUI版本
cpp
function bilateralFilterGUI
% 双边滤波 GUI(三栏:原始 | 加噪 | 滤波)------ 2020a 兼容
fig = figure('Name','双边滤波工具','NumberTitle','off',...
'MenuBar','none','ToolBar','none','Position',[100 100 1280 720]);
%% ==================== 界面布局 ====================
imgWidth = 0.78;
panelW = imgWidth/3 - 0.005;
pnlOrig = uipanel('Parent',fig,'Units','normalized',...
'FontSize',16,'Position',[0.02 0.02 panelW 0.96],'Title','原始点云');
pnlNois = uipanel('Parent',fig,'Units','normalized',...
'FontSize',16,'Position',[0.02+panelW+0.005 0.02 panelW 0.96],'Title','加噪声');
pnlFilt = uipanel('Parent',fig,'Units','normalized',...
'FontSize',16,'Position',[0.02+2*(panelW+0.005) 0.02 panelW 0.96],'Title','双边滤波');
axOrig = axes('Parent',pnlOrig,'Units','normalized','Position',[0.05 0.05 0.90 0.90]);
axNois = axes('Parent',pnlNois,'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]);
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;
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;
% 噪声强度
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;
sliderSig = uicontrol('Parent',pnlCtrl,'Style','slider','Min',0,'Max',20,'Value',5,...
'FontSize',16,'Units','normalized','Position',[0.05 yTop-btnH 0.65 btnH],...
'Callback',@refreshNoise);
txtSig = uicontrol('Parent',pnlCtrl,'Style','edit','String','5',...
'FontSize',16,'Units','normalized','Position',[0.75 yTop-btnH 0.20 btnH],...
'Callback',@editSigCB);
yTop = yTop - btnH - gap;
% 邻域点数 K
uicontrol('Parent',pnlCtrl,'Style','text','String','邻域点数 K',...
'FontSize',12,'FontWeight','bold','Units','normalized','Position',[0.05 yTop-txtH 0.90 txtH],...
'HorizontalAlignment','left');
yTop = yTop - txtH - gap;
sliderK = uicontrol('Parent',pnlCtrl,'Style','slider','Min',2,'Max',100,'Value',30,...
'FontSize',16,'Units','normalized','Position',[0.05 yTop-btnH 0.65 btnH],...
'Callback',@refreshFilter);
txtK = uicontrol('Parent',pnlCtrl,'Style','edit','String','30',...
'FontSize',16,'Units','normalized','Position',[0.75 yTop-btnH 0.20 btnH],...
'Callback',@editKCB);
yTop = yTop - btnH - gap;
% 法向权重 σr
uicontrol('Parent',pnlCtrl,'Style','text','String','法向权重 σr',...
'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',50,'Value',10,...
'FontSize',16,'Units','normalized','Position',[0.05 yTop-btnH 0.65 btnH],...
'Callback',@refreshFilter);
txtR = uicontrol('Parent',pnlCtrl,'Style','edit','String','10',...
'FontSize',16,'Units','normalized','Position',[0.75 yTop-btnH 0.20 btnH],...
'Callback',@editRCB);
yTop = yTop - btnH - gap;
% 保存
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;
ptCloudNois = 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
% disp(isvalid(axOrig)) % 必须 =1
% 显示原始点云
showPointCloud(axOrig,ptCloudOrig);
set(lblInfo,'String',sprintf('已加载:%s (%d 点)',file,ptCloudOrig.Count));
refreshNoise(); % 自动生成噪声并滤波
end
function refreshNoise(~,~)
if isempty(ptCloudOrig), return; end
sigma = get(sliderSig,'Value')/1000; % mm → m
xyz = ptCloudOrig.Location;
xyzNois = xyz + sigma*randn(size(xyz));
ptCloudNois = pointCloud(xyzNois,'Color',ptCloudOrig.Color);
showPointCloud(axNois,ptCloudNois);
set(txtSig,'String',num2str(get(sliderSig,'Value')));
refreshFilter();
end
function refreshFilter(~,~)
if isempty(ptCloudNois), return; end
K = round(get(sliderK,'Value'));
sr = get(sliderR,'Value');
ptCloudFilt = pcdBilateralFilter(ptCloudNois,K,sr);
showPointCloud(axFilt,ptCloudFilt);
set(txtK,'String',num2str(K));
set(txtR,'String',num2str(sr));
end
function editSigCB(src,~)
v = str2double(get(src,'String'));
if isnan(v), v = 5; end
v = max(0,min(20,v));
set(sliderSig,'Value',v);
refreshNoise();
end
function editKCB(src,~)
v = str2double(get(src,'String'));
if isnan(v), v = 30; end
v = max(2,min(100,round(v)));
set(sliderK,'Value',v);
refreshFilter();
end
function editRCB(src,~)
v = str2double(get(src,'String'));
if isnan(v), v = 10; end
v = max(1,min(50,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)
%---- 2020a 暖启动 + 强制刷新 ----
cla(ax); set(ax,'Color','w');
pcshow(pointCloud(nan(0,3)),'Parent',ax,'MarkerSize',1); drawnow;
pcshow(pc,'Parent',ax,'MarkerSize',35);
axis(ax,'tight'); grid(ax,'on'); view(ax,3);
drawnow; % 确保刷新
end
%% ==================== 双边滤波核心(零 pcnormals) ====================
function out = pcdBilateralFilter(pc,K,srMm)
K = max(round(K),2);
sr = srMm/1000;
xyz = pc.Location;
n = size(xyz,1);
kd = createns(xyz,'NSMethod','kdtree');
% 自算法向量
block = 10000;
normals= zeros(n,3,'like',xyz);
h = waitbar(0,'计算法向量...');
for i = 1:block:n
ir = min(i+block-1,n);
[idxMat,~] = knnsearch(kd, xyz(i:ir,:), 'K', K);
for k = i:ir
idx = idxMat(k-i+1,:);
P = xyz(idx,:);
[~,~,V] = svd(P - mean(P,1),0);
nk = V(:,3);
if nk(3)>0, nk=-nk; end
normals(k,:) = nk;
end
waitbar(ir/n,h);
end
close(h);
% 双边滤波
xyzNew = zeros(n,3,'like',xyz);
h = waitbar(0,'双边滤波...');
for i = 1:block:n
ir = min(i+block-1,n);
[idxMat,~] = knnsearch(kd, xyz(i:ir,:), 'K', K);
for k = i:ir
idx = idxMat(k-i+1,:);
p0 = xyz(k,:);
n0 = normals(k,:);
Ws = 0; Z = 0;
for t = 2:numel(idx)
pj = xyz(idx(t),:);
vec = pj - p0;
dd = norm(vec);
dn = dot(vec, n0);
ws = exp(-(dd^2)/(2*(0.05)^2));
wr = exp(-(dn^2)/(2*sr^2));
w = ws*wr;
Ws = Ws + w;
Z = Z + w*dn;
end
xyzNew(k,:) = p0 + (Z/(Ws+eps))*n0;
end
waitbar(ir/n,h);
end
close(h);
out = pointCloud(xyzNew,'Color',pc.Color,'Normal',normals);
end
end
二、点云双边滤波的结果

依旧采用GUI布局,添加了噪声设置功能,双边滤波设置了邻域个数控制和法向量权重变量。总体来说性能还比较平稳。小伙伴们快来试试吧!
就酱,下次见^-^