当前位置: 首页 > news >正文

【实战演练】基于VTK的散点凹包计算实战:从代码逻辑到实现思路

实战问题背景:

在地理信息系统、计算机视觉、机器人路径规划等领域,凹包(Concave Hull)是描述点集边界的重要形态,相比凸包能更精确地贴合非凸分布的点集。本文将基于VTK(Visualization Toolkit)库,解析一段散点凹包计算代码的设计思路与实现逻辑,帮助开发者理解如何利用VTK的几何处理能力快速构建凹包算法。

凹包计算的核心需求与VTK优势

凹包的核心需求是:对于给定的散点集,生成一个能包裹所有点且尽可能贴合点集分布的非凸多边形边界。与凸包(Convex Hull)不同,凹包允许边界向内凹陷,更适合描述“有凹痕”的点集形态(如湖泊轮廓、复杂区域边界等)。

VTK作为开源可视化库,提供了丰富的几何处理工具(如三角化、边界提取、拓扑操作等),无需从零实现基础几何算法,可大幅简化凹包计算的开发流程。本文解析的代码正是基于VTK的这些特性,实现了从3D散点到凹包边界的自动化计算。

代码整体设计思路

代码核心函数为CalcConvexhull(注:函数名可能存在笔误,实际功能为凹包计算),输入为3D点集(vtkPoints*)、输出结果(vtkPolyData*)和容差参数(tol),整体流程可分为5个关键步骤:

  1. 3D点集降维到2D平面:将输入的3D点投影到指定平面(此处为Z=0平面),因为凹包计算通常在2D空间中进行;
  2. Delaunay三角化构建初始网格:通过三角化将散点连接为三角形网格,为后续边界提取提供基础;
  3. 边界边筛选与标记:从三角化网格中提取边界边,并基于容差参数筛选出“有效边界边”;
  4. 迭代剔除内部三角形:通过删除与有效边界边关联的内部三角形,逐步暴露凹包边界;
  5. 边界整合与输出:清理冗余数据,连接连续边界线段,生成最终的凹包多边形。

代码逻辑分步解析

步骤1:3D点集投影到2D平面

double normz[] = { 0,0,1 }; // Z轴方向法向量(对应Z=0平面)
vtkNew<vtkTransform> trans;
RotateToPlane(pts, trans, normz); // 将点集旋转到Z=0平面

VTK中凹包计算依赖2D几何处理,因此需先将3D点集投影到平面。这里通过RotateToPlane函数(未展示实现,推测为坐标变换)将点集旋转至Z=0平面,并用vtkTransform记录变换矩阵,后续处理将基于该平面进行。

步骤2:Delaunay三角化生成初始网格

vtkNew<vtkPolyData> ptsData;
ptsData->SetPoints(pts); // 包装点集为PolyData
vtkNew<vtkDelaunay2D> del;
del->SetTransform(trans); // 应用平面变换
del->SetInputData(ptsData);
del->Update(); // 执行Delaunay三角化

Delaunay三角化是将散点连接为无重叠、无交叉三角形网格的经典算法,其生成的网格边界是点集的凸包候选。这里通过vtkDelaunay2D对投影后的2D点集进行三角化,得到包含所有点的三角形网格,作为后续处理的“原始画布”。

步骤3:提取边界边并筛选有效边

// 为点添加原始ID标记,便于后续追踪
vtkNew<vtkGenerateIds> idsFilter;
idsFilter->SetInputData(del->GetOutput());
idsFilter->SetPointIdsArrayName("origin");
idsFilter->Update();
auto mesh = vtkPolyData::SafeDownCast(idsFilter->GetOutput());// 提取网格的边界边(非共享边)
vtkNew<vtkFeatureEdges> edgeFilter;
edgeFilter->SetInputData(mesh);
edgeFilter->SetBoundaryEdges(1); // 只提取边界边
edgeFilter->SetFeatureEdges(0);
edgeFilter->SetManifoldEdges(0);
edgeFilter->SetNonManifoldEdges(0);
edgeFilter->Update();
auto edges = edgeFilter->GetOutput();

三角化网格的“边界边”是指仅属于一个三角形的边(凸包的初始边界)。通过vtkFeatureEdges可快速提取这类边。同时,vtkGenerateIds为点添加原始ID,避免后续处理中坐标变换导致的ID混乱。

// 基于容差筛选有效边界边(长度≥tol的边)
vtkNew<vtkCellArray> InvalidEdgeLines;
auto idArr = edges->GetPointData()->GetArray("origin");
for (int i = 0; i < nbound; i++) {vtkNew<vtkIdList> eIds;edges->GetCellPoints(i, eIds); // 获取边的两个端点ID// 计算两点距离double pt0[3], pt1[3];edges->GetPoint(pid0, pt0);edges->GetPoint(pid1, pt1);if (sqrt(vtkMath::Distance2BetweenPoints(pt0, pt1)) >= tol) {// 转换为原始ID并保存for (int e = 0; e < eIds->GetNumberOfIds(); e++) {eIds->SetId(e, idArr->GetVariantValue(eIds->GetId(e)).ToInt());}InvalidEdgeLines->InsertNextCell(eIds);}
}

容差参数tol用于过滤短边界边(可能是噪声或局部凸起导致),仅保留长度足够的边作为“有效边界边”,存入InvalidEdgeLines(命名可能为“待处理边界边”)。

步骤4:迭代剔除内部三角形,暴露凹包边界

// 循环处理有效边界边,删除关联的内部三角形
while (InvalidEdgeLines->GetNumberOfCells() > 0) {vtkNew<vtkCellArray> tempEdges;InvalidEdgeLines->InitTraversal();vtkNew<vtkIdList> eIds;while (InvalidEdgeLines->GetNextCell(eIds)) {// 找到边的两个端点共同所属的三角形(唯一)int pid0 = eIds->GetId(0);int pid1 = eIds->GetId(1);vtkNew<vtkIdList> triangleIds0, triangleIds1;mesh->GetPointCells(pid0, triangleIds0); // 包含pid0的三角形mesh->GetPointCells(pid1, triangleIds1); // 包含pid1的三角形triangleIds0->IntersectWith(triangleIds1); // 求交集(共同三角形)int delCellId = triangleIds0->GetId(0); // 待删除的内部三角形// 提取该三角形的其他边,若长度≥tol则加入下一轮处理vtkCell* delCell = mesh->GetCell(delCellId);for (int e = 0; e < delCell->GetNumberOfEdges(); e++) {auto edCell = delCell->GetEdge(e);vtkIdList* edgeIds = edCell->GetPointIds();if (edgeIds->IsId(pid0) != -1 && edgeIds->IsId(pid1) != -1) {continue; // 跳过当前边}// 计算边长度,符合条件则加入tempEdgesdouble p0[3], p1[3];mesh->GetPoint(edgeIds->GetId(0), p0);mesh->GetPoint(edgeIds->GetId(1), p1);if (sqrt(vtkMath::Distance2BetweenPoints(p0, p1)) >= tol) {tempEdges->InsertNextCell(edgeIds);}}mesh->DeleteCell(delCellId); // 删除内部三角形}InvalidEdgeLines->ShallowCopy(tempEdges); // 更新待处理边mesh->RemoveDeletedCells(); // 清理已删除的三角形
}

这是凹包计算的核心步骤:通过迭代删除与有效边界边关联的三角形,逐步“剥离”凸包内部的三角形,暴露凹进去的边界。每次删除一个三角形后,其未被处理的边(长度≥tol)将作为新的待处理边,直到没有更多边需要处理,此时剩余的网格边界即为凹包。

步骤5:边界整合与输出

// 清理冗余数据(重复点、无效单元)
vtkNew<vtkCleanPolyData> clean;
clean->SetInputData(mesh);
clean->Update();// 提取最终边界并连接为连续线段
vtkNew<vtkFeatureEdges> edgeFilter;
edgeFilter->SetInputData(clean->GetOutput());
edgeFilter->Update();
vtkNew<vtkStripper> strip;
strip->SetJoinContiguousSegments(true); // 连接连续线段
strip->SetInputData(edgeFilter->GetOutput());
strip->Update();// 筛选最长连续线段作为凹包边界
auto temp = strip->GetOutput();
int nmaxCell = temp->GetMaxCellSize();
for (int pc = 0; pc < temp->GetNumberOfCells(); pc++) {if (temp->GetCellSize(pc) != nmaxCell)temp->DeleteCell(pc);
}
temp->RemoveDeletedCells();// 输出结果
clean->SetInputData(temp);
clean->Update();
out->ShallowCopy(clean->GetOutput());

最后通过vtkCleanPolyData清理冗余点和单元,vtkStripper将分散的边界线段连接为连续多边形,再筛选出最长的连续线段(即凹包的完整边界),最终输出到out

完整代码

void CalcConvexhull(vtkPoints* pts, vtkPolyData* out, double tol)
{double normz[] = { 0,0,1 };vtkNew<vtkTransform> trans;RotateToPlane(pts, trans, normz);//计算变化vtkNew<vtkPolyData> ptsData;ptsData->SetPoints(pts);vtkNew<vtkDelaunay2D> del;del->SetTransform(trans);del->SetInputData(ptsData);del->Update();//out->ShallowCopy(del->GetOutput());//return;vtkNew<vtkGenerateIds> idsFilter;idsFilter->SetInputData(del->GetOutput());idsFilter->SetCellIds(0);idsFilter->SetFieldData(0);idsFilter->SetPointIdsArrayName("origin");idsFilter->Update();auto mesh = vtkPolyData::SafeDownCast(idsFilter->GetOutput());vtkNew<vtkFeatureEdges> edgeFilter;edgeFilter->SetInputData(mesh);edgeFilter->SetBoundaryEdges(1);edgeFilter->SetFeatureEdges(0);edgeFilter->SetManifoldEdges(0);edgeFilter->SetNonManifoldEdges(0);edgeFilter->Update();auto edges = edgeFilter->GetOutput();int nbound = edges->GetNumberOfLines();vtkNew<vtkCellArray> InvalidEdgeLines;auto idArr = edges->GetPointData()->GetArray("origin");//第一次计算所有无效边界边for (int i = 0; i < nbound; i++) {vtkNew<vtkIdList> eIds;edges->GetCellPoints(i, eIds);assert(eIds->GetNumberOfIds() == 2);int pid0 = eIds->GetId(0);int pid1 = eIds->GetId(1);double pt0[3], pt1[3];edges->GetPoint(pid0, pt0);edges->GetPoint(pid1, pt1);//如果边界边长度超过阈值,加入无效集合if (sqrt(vtkMath::Distance2BetweenPoints(pt0, pt1)) >= tol) {//转换idfor (int e = 0; e < eIds->GetNumberOfIds(); e++) {eIds->SetId(e, idArr->GetVariantValue(eIds->GetId(e)).ToInt());}InvalidEdgeLines->InsertNextCell(eIds);}}//遍历所有无效边边界while (InvalidEdgeLines->GetNumberOfCells() > 0) {vtkNew<vtkCellArray> tempEdges;InvalidEdgeLines->InitTraversal();vtkNew<vtkIdList> eIds;while (InvalidEdgeLines->GetNextCell(eIds)) {//查找edge关联的单元delCellIdassert(eIds->GetNumberOfIds() == 2);int pid0 = eIds->GetId(0);int pid1 = eIds->GetId(1);vtkNew<vtkIdList> triangleIds0, triangleIds1;mesh->GetPointCells(pid0, triangleIds0);mesh->GetPointCells(pid1, triangleIds1);triangleIds0->IntersectWith(triangleIds1);int nlinkCell = triangleIds0->GetNumberOfIds();if (nlinkCell == 0)continue;assert(nlinkCell == 1);int delCellId = triangleIds0->GetId(0);vtkCell* delCell = mesh->GetCell(delCellId);int nedge = delCell->GetNumberOfEdges();for (int e = 0; e < nedge; e++) {auto edCell = delCell->GetEdge(e);vtkIdList* edgeIds = edCell->GetPointIds();if (edgeIds->IsId(pid0) != -1 && edgeIds->IsId(pid1) != -1) {continue;}double p0[3], p1[3];mesh->GetPoint(edgeIds->GetId(0), p0);mesh->GetPoint(edgeIds->GetId(1), p1);//如果边界边长度不超过阈值,继续下一条if (sqrt(vtkMath::Distance2BetweenPoints(p0, p1)) < tol) {continue;}//将新的无效边加入tempEdges->InsertNextCell(edgeIds);}mesh->DeleteCell(delCellId);}InvalidEdgeLines->ShallowCopy(tempEdges);//更新mesh的拓扑mesh->RemoveDeletedCells();}vtkNew<vtkCleanPolyData> clean;clean->SetInputData(mesh);clean->Update();//重新提取边界edgeFilter->SetInputData(clean->GetOutput());edgeFilter->Update();vtkNew<vtkStripper> strip;strip->SetJoinContiguousSegments(true);strip->SetInputData(edgeFilter->GetOutput());strip->Update();auto temp = (strip->GetOutput());//抽取最大Cellint nmaxCell = temp->GetMaxCellSize();for (int pc = 0; pc < temp->GetNumberOfCells(); pc++) {if (temp->GetCellSize(pc) != nmaxCell)temp->DeleteCell(pc);}temp->RemoveDeletedCells();//清理clean->SetInputData(temp);clean->Update();out->ShallowCopy(clean->GetOutput());vtkNew<vtkIdList> lids;out->GetLines()->GetCell(0, lids);int npt = out->GetNumberOfPoints();//保证点不闭合if (npt < lids->GetNumberOfIds())lids->Resize(npt);out->GetLines()->Initialize();out->GetLines()->InsertNextCell(lids);out->Modified();return;
}

关键参数与注意事项

  1. 容差tol的设置tol决定了边界边的最小长度,过小会保留过多噪声导致边界杂乱,过大会丢失细节,需根据点集密度调整(建议设为点平均间距的1-2倍)。

  2. 3D到2D的投影平面:代码中固定投影到Z=0平面,实际应用中可根据点集分布动态选择投影平面(如通过主成分分析确定最佳投影方向)。

  3. 效率考量:Delaunay三角化和迭代删除步骤的时间复杂度与点数量正相关,对于大规模点集(10万+),建议先降采样再计算。

总结

本文解析的代码基于VTK实现了一套简洁的凹包计算流程,核心思路是“三角化构建网格→边界提取与筛选→迭代剔除内部三角形→整合边界”。VTK的几何处理工具大幅简化了底层算法实现,开发者可聚焦于业务逻辑(如参数调优、投影平面选择)。

该方法适用于中等规模3D散点集的凹包计算,可直接应用于地理信息、机器人导航等场景,也可通过扩展支持更复杂的点集形态(如带孔洞的凹包)。

http://www.dtcms.com/a/481606.html

相关文章:

  • Flink 状态设计理念(附源码)
  • 23种设计模式——备忘录模式(Memento Pattern)
  • 【LeetCode】73. 矩阵置零
  • 网站开发教材男通网站哪个好用
  • 《3D草原场景技术拆解:植被物理碰撞与多系统协同的6个实战方案》
  • 软件测试—BUG篇
  • OpenAI系列模型介绍、API使用
  • 做网站的可以信吗深圳商城网站建设
  • 关于使用docker部署srs服务器的相关指令
  • 基于M序列编码的水下微弱目标检测方法
  • Ubuntu SSH 免密码登陆
  • vue前端面试题——记录一次面试当中遇到的题(8)
  • FastbuildAI后端WebModule模块注册分析
  • 南昌网站排名网站站群建设方案
  • day9 cpp:运算符重载
  • Qoder上线提示词增强功能,将开发者从 “提示词“ 的负担中解放出来
  • 「机器学习笔记15」深度学习全面解析:从MLP到LSTM的Python实战指南
  • 在ARM版MacBook上构建lldb-mi
  • php网站后台搭建html代码大全简单
  • 零基础新手小白快速了解掌握服务集群与自动化运维(十一)MySQL数据库主从复制
  • 云手机的真实体验感怎么样
  • 广州微网站建设信息电商如何做
  • 架设一个网站腾讯网站建设推广
  • 【数据结构】:链表的核心实现与操作解析
  • 【Verilog】系统任务和编译指令
  • 辅助分类器GAN(ACGAN)
  • 交流网站建设心得体会wordpress首页固定页面
  • 专门做有机食品的网站dedecms怎么部署网站
  • 大学生个体创业的网站建设百度搭建wordpress
  • 自己公司网站维护上海动易 网站