3. 数据结构优化
大家好,我是你们的老朋友。今天咱们来聊聊点云处理里一个绕不开的话题——空间索引。
说实话,我刚入行那会儿,处理几十万点的点云,直接暴力搜索,CPU跑得嗷嗷叫,风扇转得跟直升机似的。后来带我的老工程师看了一眼代码,就说了句:「你这是在拿大炮打蚊子,建个树吧。」
嗯,从那以后,我才真正开始理解数据结构优化的威力。今天这一章,咱们就把Kd-Tree和Octree这两个最常用的空间索引结构,掰开了揉碎了讲清楚。
为什么需要空间索引?
你想想看,点云里动辄几百万个点。如果我想找某个点附近最近的邻居,最笨的办法是什么?挨个算距离,找最小的。复杂度O(n²),百万级别的点,算一次要几分钟。
这在实际项目中根本没法用。比如做激光SLAM的时候,每一帧点云都要做帧间匹配,你要是每帧等几分钟,那车都开出去两公里了。
所以我们需要一种「聪明」的数据结构,把点云组织起来,让搜索变得高效。说白了,就是用空间换时间。
Kd-Tree:原理与实现
Kd-Tree,全称K-Dimensional Tree,是一种对k维空间进行分割的二叉树。用在点云里,k通常就是3(x, y, z)。
它的核心思想很简单:每次选一个维度,找一个切分点,把空间切成两半。左边放小于切分点的点,右边放大于切分点的点。然后递归下去,直到每个叶子节点包含的点数少于某个阈值。
我在项目中遇到过一个问题:切分维度的选择很关键。如果每次都选x轴,遇到一个沿着y轴拉得很长的点云,树就会变得很不平衡,搜索效率大打折扣。
我个人习惯的做法是:每次选方差最大的那个维度来切分。这样能保证空间被尽量均匀地分割。
下面是一个简单的Kd-Tree构建代码,我用C++写的,注释都加上了:
struct KdNode {
Point3D point;
KdNode* left;
KdNode* right;
int split_axis; // 0:x, 1:y, 2:z
};
KdNode* buildKdTree(std::vector<Point3D>& points, int depth = 0) {
if (points.empty()) return nullptr;
// 选切分轴:按深度轮转,或者算方差
int axis = depth % 3;
// 按axis排序,取中位数作为切分点
std::nth_element(points.begin(),
points.begin() + points.size()/2,
points.end(),
[axis](const Point3D& a, const Point3D& b) {
return a.data[axis] < b.data[axis];
});
int mid = points.size() / 2;
KdNode* node = new KdNode{points[mid], nullptr, nullptr, axis};
// 递归构建左右子树
std::vector<Point3D> left_pts(points.begin(), points.begin() + mid);
std::vector<Point3D> right_pts(points.begin() + mid + 1, points.end());
node->left = buildKdTree(left_pts, depth + 1);
node->right = buildKdTree(right_pts, depth + 1);
return node;
}
Octree:原理与实现
Octree,八叉树,是另一种常用的空间索引结构。它把三维空间递归地分成8个相等的子立方体,直到每个子立方体包含的点数少于阈值,或者达到最大深度。
Kd-Tree和Octree最大的区别是什么?Kd-Tree每次只切一刀,切出来的两个子空间大小不一定相等。Octree每次切三刀(x, y, z各一刀),切出来的8个子立方体大小完全相等。
Octree的好处是结构规整,插入和删除操作比Kd-Tree简单。而且对于均匀分布的点云,Octree的搜索效率非常稳定。
我记得有一次做室内SLAM,点云分布比较均匀,用Octree做近邻搜索,每帧只要2毫秒。换成Kd-Tree反而要5毫秒,因为树不太平衡。
struct OctreeNode {
Point3D center;
float half_size;
std::vector<Point3D> points; // 叶子节点存储的点
OctreeNode* children[8]; // 8个子节点
bool is_leaf;
};
void insertPoint(OctreeNode* node, const Point3D& pt) {
if (node->is_leaf) {
node->points.push_back(pt);
// 如果点数超过阈值,分裂
if (node->points.size() > MAX_POINTS_PER_NODE) {
subdivide(node);
}
return;
}
// 计算点落在哪个子节点
int idx = 0;
if (pt.x >= node->center.x) idx |= 1;
if (pt.y >= node->center.y) idx |= 2;
if (pt.z >= node->center.z) idx |= 4;
insertPoint(node->children[idx], pt);
}
空间索引对比与选型
好了,两种结构都讲完了。那实际项目中到底选哪个?我给大家整理了一个对比表:
| 特性 | Kd-Tree | Octree |
|---|---|---|
| 构建速度 | 中等(需要排序) | 快(直接计算位置) |
| 最近邻搜索 | 快(平衡树时) | 中等 |
| 范围搜索 | 中等 | 快(空间规整) |
| 动态插入/删除 | 困难(需要重建) | 容易 |
| 内存占用 | 低 | 高(空节点多) |
| 适用场景 | 静态点云、精确搜索 | 动态点云、范围查询 |
我个人习惯是:
- 做SLAM的帧间匹配(比如ICP),用Kd-Tree。因为点云是静态的,建一次树用很多次,搜索精度高。
- 做点云滤波、降采样,用Octree。因为经常要动态插入删除,而且范围搜索多。
- 如果点云分布极其不均匀(比如有很远的离群点),Kd-Tree容易退化,这时候Octree更稳。
下面这张图是我自己画的,把两种结构的核心逻辑放在一起对比,你看一眼就明白了:
嗯,看到这里,你应该对这两种结构有了比较清晰的认识。其实选哪个没有绝对的对错,关键看你的数据特点和应用场景。
最后说一句:别光看理论,回去一定自己动手实现一遍。我当年就是手写了一遍Kd-Tree和Octree,才真正理解了它们的区别。代码跑起来,你才会发现那些藏在细节里的魔鬼。
可可精品资料库