牙齿STL网格分割实战:投影栅格化与牙龈轮廓提取全流程

📅 发布时间:2026/8/27 22:20:28
牙齿STL网格分割实战:投影栅格化与牙龈轮廓提取全流程 简介三维网格分割是数字牙科与正畸软件中的基础难题尤其在处理口扫设备导出的STL模型时邻接面连续、曲率特征模糊让传统三维算法频繁失效。投影栅格化算法巧妙地将三维曲面降维为二维深度图利用图像处理技术提取牙龈外轮廓再映射回三维网格完成切割。本文从工程实践出发系统讲解STL预处理、咬合平面拟合、Z-Buffer式栅格化、轮廓检测与凹包平滑等关键技术并针对邻接牙粘连、孔洞伪边缘等临床数据问题给出实用排错策略。该方案不仅适用于牙齿STL模型分割也为逆向工程、3D打印切片等通用网格处理场景提供了高性价比的解决思路。 做数字化正畸相关项目时最烦也最绕不开的一步就是把口扫设备导出的牙齿STL网格模型按单颗牙边界从整副牙列上分割出来。工程上叫“牙齿STL网格模型分割”很多团队落地时首选的方案就是投影算法——更具体地说是先把三维曲面栅格化成二维深度图在二维图像上把牙龈外轮廓算出来再映射回三维网格做切割。这套思路不玄乎但真正实现过的人都知道其中有大量跟“临床数据不完美”相关的坑。这篇文章我用一个做过类似项目的工程师视角把从STL预处理、坐标摆正、曲面栅格化、牙龈外轮廓计算到二维轮廓映回三维做分割的完整链路拆开讲一遍适合正在做数字牙科软件、隐形矫治器设计、种植导板规划或者单纯想了解网格分割和投影栅格化算法怎么结合的人阅读。1. 三维分割难在哪邻接面连续、曲率失效投影法是被临床需求逼出来的1.1 临床上要的“分割”到底分的是什么先明确需求。正畸排牙、隐形矫治器设计、牙冠修复、种植规划这些下游任务需要的不是一整副牙列网格而是两类子网格一是单颗牙冠网格用于单独移动、旋转、测量二是牙龈基底网格用于保持软组织形态、生成矫治器底座。口扫设备比如常见的iTero、3Shape TRIOS一类导出的STL文件本质上就是一堆三角形面片的集合没有拓扑关系也没有语义标签。我们要做的就是给每个三角片或每个顶点打上“属于哪颗牙”或“属于牙龈”的标签。听起来简单但真正动手后会发现这个问题比通用的三维网格分割要难一个等级。原因很现实牙齿是紧密排列在一个牙弓上的相邻两颗牙的邻接面间隙往往只有零点几毫米而且这个间隙在网格上表现为曲率连续变化的曲面并没有明显的“断开”。1.2 为什么三维空间直接切容易翻车很多人一开始的思路是直接用三维网格分割算法比如基于曲率的区域生长、谱聚类、或者深度学习分割网络。这些方法不是不行但都有各自的代价。基于曲率的区域生长在实际临床数据上非常容易误判。牙龈沟牙冠和牙龈交界的那道沟确实是高曲率区域但邻接牙的邻面接触区同样有较高的曲率变化算法分不清这是“牙齿和牙齿的边界”还是“牙齿和牙龈的边界”。我见过不少项目在第三磨牙和邻牙之间直接切歪就是因为这两个位置的曲率特征太像。深度学习分割方案比如PointNet、MeshCNN一类的网络效果确实好但前提是你得有大量标注好的单颗牙训练数据。对于中小团队标注一副全牙弓模型的人工成本非常高而且不同口扫设备的网格质量差异大模型泛化是个头疼问题。所以在实际工程里很多人绕了一圈又回到了传统方案先降维再分割。这就是投影栅格化路线的核心动机。1.3 投影栅格化的核心思路把网格问题降维成图像问题从咬合面也就是牙齿的咀嚼面往下看牙列会呈现出一个非常清晰的空间分布牙冠区域凸起、牙龈区域凹陷牙龈沟在俯视图上是一条连续且相对明确的闭合曲线。这就给了我们一个特别的解剖先验三维空间里很难描述的“牙龈-牙冠边界”在二维俯视图上反而是清晰可见的。所以投影算法的本质是选一个合适的投影方向把三维STL曲面的每个点映射到二维平面网格上再在这个二维网格上记录每个位置的高度或深度信息。这个过程就是标题里说的“曲面的栅格化算法”。栅格化之后三维网格分割问题就变成了一个二维图像分割问题——阈值分割、边缘检测、形态学处理、轮廓提取这些图像处理的成熟方法全都可以用上。最后再把二维轮廓映射回三维网格完成真正的切割。这套降维思路不仅牙科能用工业上做逆向工程、3D打印切片、零件自动拆分的场景里也是常见的处理路径。理解了这一点再看后面的实现步骤就会很清楚每一步到底是为了什么。2. 数据摆正是第一步咬合平面拟合与STL预处理2.1 STL网格质量检查连通分量、孔洞、孤立碎片不要把口扫设备导出的STL想象成理想数据。真实扫描模型经常包含扫描头进出时产生的小碎片、托槽或金属修复体造成的孔洞、以及模型边缘的破损三角片。这些脏数据如果不处理会在后续栅格化时投影出一堆假边缘轮廓提取必然出错。我习惯的预处理顺序是这样的读取STL后先做连通分量分析把主牙列模型和离散小碎片区别开只保留顶点数最多的那个连通分量。检查网格边界half-edge结构中的边界边统计孔洞数量和位置。孔洞面积小的可以自动补洞面积大的先标记出来后面再做针对性处理。做一遍轻量级Laplacian平滑滤掉扫描噪声。平滑迭代次数不要多1到2次就够了否则牙尖细节会被磨平。重建顶点法向量和平均边长统计。这些数据后面栅格化分辨率选取、曲率计算都要用。这一步没有太高深的算法但非常影响后续稳定性。我见过同事省掉孤立碎片剔除结果投影图上多出一个莫名的小连通域轮廓追踪时花了半天排查。2.2 咬合平面拟合PCA与RANSAC两种做法投影方向的选择直接决定栅格化质量。最合理的投影方向是咬合平面的法向量方向。什么是咬合平面简单理解就是牙齿咬合面整体分布所拟合出的那个平面。对上颌牙弓可以近似看作一个过牙尖顶点的平面。拟合咬合平面有两种常见做法。第一种是PCA方法。把模型所有顶点坐标去均值后做奇异值分解SVD最小奇异值对应的特征向量就是平面法向量平面过所有顶点的质心。这个做法简单粗暴缺点是如果牙弓形态特别弯或者模型包含了牙根区域拟合出的平面会和真实的咬合面有一定偏差。第二种是RANSAC拟合。随机采样三个顶点确定平面统计所有顶点到平面的距离落在阈值内的点视为内点迭代多次选内点最多的那个平面。RANSAC对模型中的托槽、龋坏、修复体等离群点不敏感在临床数据上通常比PCA更稳。无论用哪种方法最后都要做一个判定法向量方向统一朝上让牙齿的牙尖方向指向Z轴正方向也就是保证所有顶点经过旋转变换后Z轴坐标值最大的是牙尖而不是牙龈底。旋转操作本身不复杂计算当前模型顶点到新坐标系的旋转矩阵用四元数或欧拉角实现都可以。关键是这一步做完之后要检查一下结果——把模型视角切到Z轴俯视牙列应该大致水平展开左右对称而不是歪着的。2.3 牙弓主轴对齐给投影一个统一的坐标系咬合平面法向量对齐到Z轴之后还要确定牙弓在XY平面上的朝向。牙弓通常呈“U”形或“V”形长轴方向大概是从一侧第二磨牙到另一侧第二磨牙的方向。实际做法是把模型顶点投影到XY平面后再做一次PCA取最大特征值对应的特征向量作为牙弓长轴通过旋转变换把它对齐到X轴方向。这一步的意义有两点。一是后续如果需要在二维深度图上按牙位划分区域、做左右对称分析统一的坐标系会方便很多。二是当一次投影不够、需要做局部牙位重投影时统一坐标可以保证局部坐标系和全局坐标系之间的转换不混乱。我建议把“坐标系摆正”单独封装成一个模块因为它在整个项目里会被反复调用——不仅栅格化前要用下游排牙软件、测量模块也需要基于同一个坐标系读取模型。数据准备做完下面才进入标题的重头戏曲面栅格化。3. 曲面栅格化实现细节从三角片到二维深度图3.1 分辨率选择0.05mm还是0.1mm栅格化就是把三维模型沿着Z轴方向垂直投影到一个二维像素网格上。每个像素记录一个或多个数值相当于给三维模型拍了一张带有深度信息的“俯视图”。第一步是确定分辨率。栅格分辨率如果太粗牙缝、牙龈沟这些关键特征会被抹掉太细则计算量大而且像素尺寸小于三角片尺度时会出现大量像素落在三角形间隙里反而产生噪点。我实际项目里的经验值是这样的一副完整牙弓模型长宽大概是60mm×50mm左右口扫网格的平均边长通常在0.02mm到0.1mm之间。栅格分辨率设为0.05mm到0.1mm比较合适对应的像素网格约600×500到1200×1000。如果用两个float数组存储最小深度和最大深度内存占用只有几MB完全不是瓶颈。建议用网格平均边长的2到4倍作为栅格分辨率。这个经验值既不会丢失三角片细节又能保证每个三角片投影后至少覆盖2到4个像素不会出现大量半像素覆盖率。3.2 Z-Buffer式栅格化伪代码与数学原理栅格化本身可以借用图形学里Z-Buffer深度缓冲的思想。基本逻辑是遍历所有三角片把每个三角片投影到二维像素网格上如果这个像素被三角形覆盖就计算该像素位置对应的Z坐标写入深度数组。这里给出一个可以直接实现的C风格伪代码// depthMin和depthMax初始化为大值和小值 for (auto tri : mesh.triangles) { // 将三角片三个顶点投影到XY平面得到二维包围盒 int xmin floor(min(tri.v[0].x, tri.v[1].x, tri.v[2].x) / res); int xmax floor(max(tri.v[0].x, tri.v[1].x, tri.v[2].x) / res); int ymin floor(min(tri.v[0].y, tri.v[1].y, tri.v[2].y) / res); int ymax floor(max(tri.v[0].y, tri.v[1].y, tri.v[2].y) / res); // 限制在图像边界内 for (int py ymin; py ymax; py) { for (int px xmin; px xmax; px) { // 像素中心坐标 float cx (px 0.5f) * res; float cy (py 0.5f) * res; // 判断像素中心是否在三角形投影内 if (!pointInTriangle2D(cx, cy, tri)) continue; // 通过三角形平面方程插值出该点Z值 float z interpolateZ(cx, cy, tri); depthMin[py][px] min(depthMin[py][px], z); depthMax[py][px] max(depthMax[py][px], z); } } }这个实现里的核心是“判断像素中心是否在三角形投影内”我推荐用图形学里的Edge Function方法计算像素中心点与三角形三条边的有向面积。如果三次计算的符号一致说明点在三角形内部这种方法比重心坐标法快很多。另一个重要细节是Z值插值。三角形三个顶点都有(x,y,z)坐标投影到二维后我们求的是像素中心点(x,y)对应的Z值。这里可以直接用三维平面方程由三个顶点算出平面法向量n (a,b,c)满足a·x b·y c·z d代入像素中心(x,y)求出z (d - a·x - b·y) / c。3.3 深度图的min/max双通道与可见性标记为什么我同时记录depthMin和depthMax两个值从咬合面往下投影时牙冠表面是起伏的曲面而牙龈底部的凹陷区域、舌侧的凹腔区域可能被牙冠遮挡。如果只记录最近深度min咬合面下方的凹陷区域会完全丢失如果只记录最远深度max牙尖和牙龈边缘的细节又会模糊。depthMin代表“从投影方向看过去最先碰到的表面”相当于可见表面depthMax代表“这个栅格柱体内最远的表面”。两者之差depthMax - depthMin可以看作这个位置的深度跨度。如果深度跨度突然增大说明这个位置存在遮挡断崖很可能是牙冠边缘或牙龈沟所在的位置这个特征对后续轮廓提取非常有用。实际使用中我会把两个通道都存下来高度图heightMap depthMax从牙龈底部向上的高度直观且平滑深度差图depthSpan depthMax - depthMin用于检测遮挡和边缘如果项目对精度要求更高还可以在栅格化时加一个可见性标记遍历三角片时如果某个像素已经被更近的三角形覆盖则当前三角形不写入颜色通道。这相当于把标准Z-Buffer和最小深度图结合起来。不过对于牙齿分割这个场景min/max双通道已经足够强行加可见性标记反而会增加耗时。到这里三维网格已经被转换成了二维深度图像。接下来的任务就是在这个二维图上计算牙龈外轮廓。4. 牙龈外轮廓怎么算图像阈值、梯度检测与凹包平滑4.1 深度图到前景掩膜阈值的坑拿到高度图之后最朴素的想法是牙冠区域比牙龈区域高所以设定一个高度阈值高于阈值的像素就是牙冠掩膜轮廓就是牙龈外轮廓。但这个思路有一个坑牙龈边缘的高度是渐变的不是突变的。固定阈值选高了牙冠边缘的一部分会被误判为牙龈选低了牙龈的一部分会被划进牙冠。所以阈值的作用不是直接给出精确边界而是给一个“初始前景区域”。我常用的做法是先用Otsu算法自动计算一个初始阈值再根据实际分割效果微调。Otsu在多峰分布下不一定稳定所以我会再加一个先验修正——牙齿高度约占整个模型高度的50%到70%如果某副模型Otsu算出来的阈值明显偏离这个区间就手动调整。得到二值掩膜后先做形态学处理用3×3或5×5的结构元素做一次开运算去除小的噪点再做一次闭运算把牙冠边缘细小的断裂处连接起来。4.2 牙龈沟检测梯度与拉普拉斯如果模型数据比较好阈值掩膜已经能给出大致轮廓。但口扫模型往往有局部噪点或者牙龈和牙冠在部分区域高度差很小仅靠阈值不够。这时就要用梯度信息。牙龈沟在高度图上表现为一个深色凹陷带其两侧高度变化剧烈反映在图像上是梯度幅值局部极大值。具体做法是对高度图计算Sobel梯度幅值然后寻找梯度幅值局部极大值连成的闭合曲线。这个曲线在临床上对应牙龈沟的位置比单纯高度阈值可靠得多。还可以叠加一个拉普拉斯算子响应图拉普拉斯过零点对应图像的二阶导数符号变化位置也就是拐点位置而这正是牙龈沟这类凹槽的数学特征。拿拉普拉斯梯度图再和阈值掩膜叠加能够把牙龈沟断裂处补全。这一步做完我们得到的是一张“可能性图”每个像素表示它是牙龈边界的概率而不是一个干净的轮廓。在下节中我们把它变成真正的闭合轮廓。4.3 轮廓提取、凹包与样条平滑从可能性图提取闭合轮廓标准路线是对梯度图再做一次阈值化得到边缘掩膜。用形态学闭运算把断裂的边连接起来使边缘线连续。使用图像库中的轮廓追踪提取单像素宽轮廓。如果是在C工程里直接用OpenCV的findContours非常方便。在自研轻量管线里也可以用Moore-Neighbor追踪算法手写一个代码量不大效果也稳定。这里需要提醒一个关键问题轮廓提取出来后初始轮廓是一串二维像素点锯齿非常明显不能直接用。所以下一步是平滑。平滑之前先做一个“凹包”处理。牙齿轮廓不是凸的——牙缝位置会向内凹陷牙弓整体呈弧形。如果直接用凸包会把牙缝位置填平外轮廓就会越过牙缝这在分割邻接牙时是致命的。推荐的方案是Alpha Shape算法。简单理解就是用一个半径可调的圆沿着点集滚动凡是被圆滚过且内部没有其他点的边界段才保留这样既能保留凹形特征又能过滤掉噪点产生的细小锯齿。Alpha参数的推荐值大约是2到4个栅格分辨率的长度。轮廓平滑可以用样条拟合或者傅里叶描述子低通滤波。我更推荐二维B样条拟合控制点数量根据轮廓复杂度自动决定拟合结果可以直接输出成连续曲线。到这里牙龈外轮廓已经在二维图像上闭合了。但我们要的不是二维轮廓本身而是用它来指导三维网格分割。5. 从二维轮廓回到三维网格顶点分类、区域生长与边界闭合5.1 轮廓内部的顶点如何判定这是整个流程里最容易被新手忽略、却最关键的环节二维图像上算出来的轮廓怎么映射回三维网格基本思路是把所有网格顶点往XY平面投影判断每个投影点是否落在轮廓多边形内部。如果落在轮廓内这个顶点就初步标记为“牙冠区域”落在轮廓外的标记为“牙龈区域”。点是否在多边形内部用经典的射线法即可。从目标点向右发射一条水平射线统计射线与多边形边界的交点个数奇数个在内部偶数个在外部。这个算法在二维计算几何里已经很成熟实现时注意处理射线正好穿过顶点或与边重合的退化情况即可。但这里有一个精度问题需要重点讨论二维轮廓是一条连续曲线而三维网格顶点是离散采样的。一个顶点投影在轮廓内不代表它对应的三维位置就真的是牙冠靠近轮廓边界的那一圈顶点分类很可能是模糊的。所以我一般不直接拿轮廓做硬分类而是把轮廓当作“初始种子区域”。轮廓内部的顶点集合作为可靠的种子轮廓边界附近一个带状区域内的顶点则标记为“待定”后续交给区域生长阶段处理。这个“先粗分类再细分精化”的思路比一刀切要稳得多。5.2 曲率约束的区域生长优化有了初始种子和待定区接下来用三维网格上的区域生长算法优化边界。基本过程是从轮廓内部的种子顶点出发沿着网格边向外扩张。每扩张一步计算当前访问顶点的曲率或者顶点法向量与种子区域平均法向量的夹角。如果曲率超过设定阈值或者法向夹角过大说明这个位置很可能进入了牙龈区域停止生长。为什么需要这一步因为二维轮廓是把三维形状投影到平面后得到的投影过程一定会丢失高度方向的信息。牙冠边缘在投影图上可能歪斜1到2个像素而区域生长的曲率约束可以直接在三维网格上修正这个误差。实际实现时曲率可以用顶点邻域内的法向量变化率来近似无需精确计算主曲率。也可以用简单的“高度梯度约束”——判断某顶点的高度与当前种子区域平均高度的差值是否过大。区域生长的停止条件非常关键。我遇到过很多次生长过度把牙龈也吞进去了。后来总结的经验是不要把单一曲率阈值作为唯一停止条件要和二维轮廓约束配合使用。具体做法是区域生长允许越过二维轮廓边界一个有限的带宽比如1到2毫米但不能无限扩张这样既保留了三维修正能力又防止过度生长。5.3 边界平滑与三角片修补分割完成后牙冠子网格的边界必然是不平整的。原因有两层一是区域生长本身走的是网格边边界会沿三角形边呈锯齿状二是二维轮廓的投影误差会导致部分边界三角片被误分类。我常用的后处理步骤是对边界顶点做3到5轮Laplacian平滑只移动边界带内的顶点不改变内部顶点防止牙尖被拉变形。删除长宽比过大的狭长三角片通常用三角形最小角或边长比来判断必要时做局部重网格化。检查牙冠网格的孔洞如果存在因为分割造成的边界缺口用补洞算法补上。输出前统一检查网格法线方向一致性确保STL文件在其他软件里打开时不会出现黑面。此外牙龈基底网格也需要处理把标为牙龈的顶点保留标为牙冠的顶点删除然后补上切割面的封口。如果目标只是单颗牙冠这一步可以省略。6. 实战排错邻接牙粘连、孔洞假边缘与投影盲区6.1 邻接牙粘连分水岭与局部重投影实际运行这套流程时出现频率最高的问题就是邻接牙粘连。邻接牙之间的牙缝在二维投影图上往往只有1到2个像素宽阈值法和边缘检测都很难稳定分开。我处理这类问题有两条路线。一条是图像学路线在拿到前景掩膜后先做距离变换再用水分水岭算法做分割。距离变换后的图像里每颗牙的中心是一个局部极大值水到山脊也就是牙缝位置就停。这个方法对接触面积小、牙缝相对清晰的模型很好用。另一条是几何学路线针对粘连区域把该牙位的包围盒单独提取出来以该牙位的局部咬合平面方向重新做一次高分辨率栅格化。因为局部投影时邻牙的干扰会大幅降低牙缝处的深度信息会更明显。两条路线可以结合使用先用分水岭在全局图上预分割如果发现某一颗牙的掩膜面积异常明显偏大或者形状奇怪就用局部重投影单独处理这一颗牙。这种“全局粗分局部精修”的策略在临床数据上非常有效。6.2 孔洞造成的伪轮廓口扫模型在托槽、金属修复体、邻间隙处经常有孔洞。如果孔洞位于牙冠区域在二维投影图上会表现为高度图上的凹陷边缘检测会在孔洞边界产生一条假轮廓。这个问题最好的解决时机是预处理阶段。我之前提过预处理时要检测孔洞并补洞。但补洞不能一概而论大的邻接面孔洞如果强行补平会把牙缝填没如果补成一个凸起又会产生新的假边缘。我的经验是分情况处理直径小于1毫米的小孔直接补直径大于1毫米的孔洞先标记出来在栅格化时对深度图做插值填充而不是直接改网格。这样既不会破坏网格原始形态又能让图像处理阶段得到干净的轮廓。另一种更稳妥的方法是在栅格化之后对深度图做“孔洞掩膜”将STL中的孔洞边界投影到深度图上得到一个不可信区域掩膜计算轮廓时忽略这些区域最后用曲线插值把轮廓跨过不可信区域。6.3 严重错位牙的投影盲区这是投影方案的天花板之一。当一颗牙严重舌倾或唇倾时牙冠会贴近牙龈甚至被邻牙遮挡从单纯俯视图投影后这颗牙的轮廓会和邻牙或牙龈重叠二维图上根本看不出完整边界。我能给出的实用建议是首先判断该牙位的倾斜程度。可以通过计算该牙位局部主方向与全局Z轴的夹角来判断一般夹角超过45度投影方案就会失效。对这种牙位改用该牙位局部坐标系单独做投影而不是使用全局咬合平面方向。如果局部投影后仍然遮挡严重就得切换到三维方案兜底比如基于测地线距离的区域生长或者手动标注一个初始轮廓作为种子。不要指望单一投影方向解决所有病例。成熟的工程系统一定是“全局投影 局部重投影 三维兜底”三层结构。6.4 参数速查表最后给一份我在实际项目中调过的参数参考值不同设备、不同精度要求下需要微调。参数项推荐值说明栅格分辨率网格平均边长的2~4倍口扫模型一般取0.05~0.1mmAlpha Shape半径2~4个栅格分辨率太小保留噪声太大丢失凹形区域生长曲率阈值法向夹角30~45度需要根据数据噪声情况调整形态学结构元素3×3到5×5太大容易抹掉牙缝细节轮廓平滑样条控制点数轮廓像素数的5%~10%太少会过度平滑丢失生理形态预处理Laplacian迭代次数1~2次过多会磨平牙尖这些参数的共同原则是先从保守值开始跑一遍看结果再逐步放宽。不要一次性把所有参数都调到很激进否则问题出现时根本不知道是哪个参数造成的。做牙齿STL网格模型分割投影栅格化绝不是唯一方案但它是投入产出比最高、最容易在工程里落地的一套思路。从我自己实践的角度看整个流程里最值得花时间打磨的不是算法本身而是数据预处理和参数调试。口扫模型千奇百怪同一个算法在不同患者数据上表现差异非常大所以搭建一套能快速可视化中间结果的调试工具比单纯调算法更关键。如果你也在做类似项目建议先把预处理和数据摆正做扎实再逐步跑通栅格化、轮廓提取和三维映射每一步都保留中间图像和网格输出。这样即使出问题也能一眼定位是哪一层丢了精度。本文还有配套的精品资源点击获取