本文以 bunny_base.las 点云为例,介绍 MATLAB 中 KD 树的基础使用方法,包括 K 近邻搜索、半径邻域搜索、局部邻域提取、区域提取和局部密度显示。
开篇说明
在点云处理中,经常需要找到某个点周围的一批邻近点。比如法向量估计、曲率计算、边界点提取、点云滤波、局部特征分析等,都离不开邻域搜索。
如果每次都遍历全部点,计算效率会比较低。KD 树可以理解为一种空间索引结构,能够帮助我们更快地查找最近邻点或指定半径范围内的邻域点。
本期使用 bunny_base.las 作为示例数据。相比桥梁这类线状工程点云,兔子点云的轮廓更集中,查询点、邻域点和局部密度的显示效果更直观。
1. 读取 bunny_base.las 点云数据
首先读取 LAS 点云,并提取点云坐标矩阵 XYZ。
clc; clear; close all;
%% ================== 1. 读取 bunny_base.las ==================
fileName = "bunny_base.las";
lasReader = lasFileReader(fileName);
ptCloud = readPointCloud(lasReader);
XYZ = ptCloud.Location;
fprintf('点云数量:%d\n', size(XYZ,1));
其中,XYZ(:,1)、XYZ(:,2)、XYZ(:,3) 分别对应点云的 X、Y、Z 坐标。
2. 建立 KD 树
读取点云坐标后,可以使用 KDTreeSearcher 建立 KD 树。后续的 K 近邻搜索和半径邻域搜索都基于这个模型完成。
%% ================== 2. 建立 KD 树 ==================
Mdl = KDTreeSearcher(XYZ);
disp('KD树建立完成');
3. 选择查询点
为了让示例显示更稳定,这里先计算点云中心,再选择距离中心最近的点作为查询点。这样查询点通常位于模型主体区域,邻域显示效果更清楚。
%% ================== 3. 选择查询点 ==================
centerPoint = mean(XYZ,1);
[queryIndex, ~] = knnsearch(Mdl, centerPoint, 'K',1);
queryPoint = XYZ(queryIndex,:);
4. K近邻搜索
K 近邻搜索的意思是:对于一个查询点,查找距离它最近的 K 个点。这里设置 K=30。
%% ================== 4. K近邻搜索 ==================
K = 30;
[idxKNN, distKNN] = knnsearch(Mdl, queryPoint, 'K', K);
neighborPoints = XYZ(idxKNN,:);
得到的 idxKNN 是邻近点索引,distKNN 是这些邻近点到查询点的距离。可视化时,红色点表示查询点,绿色点表示 K 个最近邻点。
figure;pcshow(XYZ, 'MarkerSize', 10);hold on;
plot3(queryPoint(1),
queryPoint(2), queryPoint(3), ...
'ro', 'MarkerSize', 10, 'LineWidth', 2);
plot3(neighborPoints(:,1),
neighborPoints(:,2), neighborPoints(:,3), ...
'g.', 'MarkerSize', 20);
axis equal;
xlabel('X');
ylabel('Y');
zlabel('Z');
title('K近邻搜索结果');
ax = gca;
set(ax, 'XColor','k',
'YColor','k', 'ZColor','k', 'Color','w');
set(gcf, 'Color','w');
grid off;
box on;
legend('点云', '查询点', 'K近邻点');
view([12.416,17.9678]);

5. 半径邻域搜索
半径邻域搜索的意思是:以查询点为中心,查找指定半径范围内的所有点。这里先根据点云包围盒对角线长度自适应确定半径。
%% ================== 5. 半径邻域搜索 ==================
bboxSize = max(XYZ,[],1) - min(XYZ,[],1);
diagLen = norm(bboxSize);
r = 0.03 * diagLen;
idxRadiusCell = rangesearch(Mdl, queryPoint, r);
idxRadius = idxRadiusCell{1};
radiusPoints = XYZ(idxRadius,:);
需要注意,rangesearch 返回的是 cell 类型结果,因此需要使用 idxRadiusCell{1} 提取第一个查询点对应的邻域索引。
figure;
pcshow(XYZ, 'MarkerSize', 10);
hold on;
plot3(queryPoint(1), queryPoint(2), queryPoint(3), ...
'ro', 'MarkerSize', 10, 'LineWidth', 2);
plot3(radiusPoints(:,1), radiusPoints(:,2), radiusPoints(:,3), ...
'y.', 'MarkerSize', 20);
axis equal;
xlabel('X');
ylabel('Y');
zlabel('Z');
title('半径邻域搜索结果');
ax = gca;
set(ax, 'XColor','k', 'YColor','k', 'ZColor','k', 'Color','w');
set(gcf, 'Color','w');
grid off;
box on;
legend('点云', '查询点', '半径邻域点');
view([12.416,17.9678]);

6. 提取半径邻域点云
通过半径搜索得到邻域点后,可以直接生成新的 pointCloud 对象,用于单独显示或后续处理。
%% ================== 6. 提取半径邻域点云 ==================
ptCloud_radius = pointCloud(radiusPoints);
figure;pcshow(ptCloud_radius.Location, 'MarkerSize', 30);
axis equal;
xlabel('X');
ylabel('Y');
zlabel('Z');
title('提取的局部邻域点云');
ax = gca;
set(ax, 'XColor','k', 'YColor','k', 'ZColor','k', 'Color','w');
set(gcf, 'Color','w');
grid off;
box on;
view([12.416,17.9678]);

7. 指定区域点云提取
除了基于查询点进行邻域搜索,也可以直接按照坐标范围提取点云局部区域。下面示例提取 X 方向后半部分区域。
%% ==================7. 指定区域点云提取 ==================
xRange = range(XYZ(:,1));
xmin = min(XYZ(:,1)) + 0.45 *
xRange;
xmax = max(XYZ(:,1));
ymin = min(XYZ(:,2));
ymax = max(XYZ(:,2));
zmin = min(XYZ(:,3));
zmax = max(XYZ(:,3));
idx_roi = XYZ(:,1) >= xmin
& XYZ(:,1) <= xmax & ...
XYZ(:,2) >= ymin & XYZ(:,2)
<= ymax & ...
XYZ(:,3) >= zmin & XYZ(:,3)
<= zmax;
XYZ_roi =XYZ(idx_roi,:);
figure;
pcshow(XYZ_roi, 'MarkerSize', 20);
axis equal;
xlabel('X');
ylabel('Y');
zlabel('Z');
title('指定区域内的点云');
ax = gca;
set(ax, 'XColor','k',
'YColor','k', 'ZColor','k', 'Color','w');
set(gcf, 'Color','w');
grid off;
box on;
view([12.416,17.9678]);

8. 计算局部密度
局部密度可以简单理解为:在某个半径范围内,邻域点数量越多,说明该位置附近点云越密集。为了提高演示速度,这里随机抽取 5000 个点进行密度显示。
%% ==================8. 计算局部密度 ==================
sampleNum = 5000;
if size(XYZ,1) > sampleNum
sampleIdx = randperm(size(XYZ,1), sampleNum);
else
sampleIdx = 1:size(XYZ,1);
end
XYZ_sample = XYZ(sampleIdx,:);
r_density = 0.03 * diagLen;
idxDensity = rangesearch(Mdl,
XYZ_sample, r_density);
densityValue =zeros(length(idxDensity),1);
for i = 1:length(idxDensity)
densityValue(i) = length(idxDensity{i});
end
figure;
pcshow(XYZ_sample, densityValue,'MarkerSize', 20);
axis equal;
colormap(jet);
colorbar;
xlabel('X');
ylabel('Y');
zlabel('Z');
title('基于邻域点数量的局部密度显示');
ax = gca;
set(ax, 'XColor','k',
'YColor','k', 'ZColor','k', 'Color','w');
set(gcf, 'Color','w');
grid off;
box on;
view([12.416,17.9678]);

9. 完整代码
下面给出本文完整代码,可直接复制运行。
clc; clear; close all;
%% ================== 1. 读取 bunny_base.las ==================
fileName = "bunny_base.las";
lasReader = lasFileReader(fileName);
ptCloud = readPointCloud(lasReader);
XYZ = ptCloud.Location;
fprintf('点云数量:%d\n', size(XYZ,1));
%% ================== 2. 建立 KD 树 ==================
Mdl = KDTreeSearcher(XYZ);
disp('KD树建立完成');
%% ================== 3. 选择查询点 ==================
centerPoint = mean(XYZ,1);
[queryIndex, ~] = knnsearch(Mdl, centerPoint, 'K', 1);
queryPoint = XYZ(queryIndex,:);
%% ================== 4. K近邻搜索 ==================
K = 30;
[idxKNN, distKNN] = knnsearch(Mdl, queryPoint, 'K', K);
neighborPoints = XYZ(idxKNN,:);
figure;
pcshow(XYZ, 'MarkerSize', 10);
hold on;
plot3(queryPoint(1), queryPoint(2), queryPoint(3), ...
'ro', 'MarkerSize', 10, 'LineWidth', 2);
plot3(neighborPoints(:,1), neighborPoints(:,2), neighborPoints(:,3), ...
'g.', 'MarkerSize', 20);
axis equal;
xlabel('X');
ylabel('Y');
zlabel('Z');
title('K近邻搜索结果');
ax = gca;
set(ax, 'XColor','k', 'YColor','k', 'ZColor','k', 'Color','w');
set(gcf, 'Color','w');
grid off;
box on;
legend('点云', '查询点', 'K近邻点');
view(3);
%% ================== 5. 半径邻域搜索 ==================
bboxSize = max(XYZ,[],1) - min(XYZ,[],1);
diagLen = norm(bboxSize);
r = 0.03 * diagLen;
idxRadiusCell = rangesearch(Mdl, queryPoint, r);
idxRadius = idxRadiusCell{1};
radiusPoints = XYZ(idxRadius,:);
figure;
pcshow(XYZ, 'MarkerSize', 10);
hold on;
plot3(queryPoint(1), queryPoint(2), queryPoint(3), ...
'ro', 'MarkerSize', 10, 'LineWidth', 2);
plot3(radiusPoints(:,1), radiusPoints(:,2), radiusPoints(:,3), ...
'y.', 'MarkerSize', 20);
axis equal;
xlabel('X');
ylabel('Y');
zlabel('Z');
title('半径邻域搜索结果');
ax = gca;
set(ax, 'XColor','k', 'YColor','k', 'ZColor','k', 'Color','w');
set(gcf, 'Color','w');
grid off;
box on;
legend('点云', '查询点', '半径邻域点');
view(3);
%% ================== 6. 提取半径邻域点云 ==================
ptCloud_radius = pointCloud(radiusPoints);
figure;
pcshow(ptCloud_radius.Location, 'MarkerSize', 30);
axis equal;
xlabel('X');
ylabel('Y');
zlabel('Z');
title('提取的局部邻域点云');
ax = gca;
set(ax, 'XColor','k', 'YColor','k', 'ZColor','k', 'Color','w');
set(gcf, 'Color','w');
grid off;
box on;
view(3);
%% ================== 7. 指定区域点云提取 ==================
xRange = range(XYZ(:,1));
xmin = min(XYZ(:,1)) + 0.45 * xRange;
xmax = max(XYZ(:,1));
ymin = min(XYZ(:,2));
ymax = max(XYZ(:,2));
zmin = min(XYZ(:,3));
zmax = max(XYZ(:,3));
idx_roi = XYZ(:,1) >= xmin & XYZ(:,1) <= xmax & ...
XYZ(:,2) >= ymin & XYZ(:,2) <= ymax & ...
XYZ(:,3) >= zmin & XYZ(:,3) <= zmax;
XYZ_roi = XYZ(idx_roi,:);
figure;
pcshow(XYZ_roi, 'MarkerSize', 20);
axis equal;
xlabel('X');
ylabel('Y');
zlabel('Z');
title('指定区域内的点云');
ax = gca;
set(ax, 'XColor','k', 'YColor','k', 'ZColor','k', 'Color','w');
set(gcf, 'Color','w');
grid off;
box on;
view(3);
%% ================== 8. 计算局部密度 ==================
sampleNum = 5000;
if size(XYZ,1) > sampleNum
sampleIdx = randperm(size(XYZ,1), sampleNum);
else
sampleIdx = 1:size(XYZ,1);
end
XYZ_sample = XYZ(sampleIdx,:);
r_density = 0.03 * diagLen;
idxDensity = rangesearch(Mdl, XYZ_sample, r_density);
densityValue = zeros(length(idxDensity),1);
for i = 1:length(idxDensity)
densityValue(i) = length(idxDensity{i});
end
figure;
pcshow(XYZ_sample, densityValue, 'MarkerSize', 20);
axis equal;
colormap(jet);
colorbar;
xlabel('X');
ylabel('Y');
zlabel('Z');
title('基于邻域点数量的局部密度显示');
ax = gca;
set(ax, 'XColor','k', 'YColor','k', 'ZColor','k', 'Color','w');
set(gcf, 'Color','w');
grid off;
box on;
view(3);
10. 本文小结
本文以 bunny_base.las 点云为例,介绍了 MATLAB 中 KD 树与邻域搜索的基础流程。主要包括:
使用 lasFileReader 和 readPointCloud 读取 LAS 点云;
使用 KDTreeSearcher 建立 KD 树;
使用 knnsearch 进行 K 近邻搜索;
使用 rangesearch 进行半径邻域搜索;
基于索引提取局部点云和指定区域点云;
通过邻域点数量显示点云局部密度。
KD 树是点云处理中的基础工具。后续很多操作,例如法向量估计、曲率计算、边界点识别和点云分割,都会用到邻域搜索。