方向性形态学开运算:用 Clipper 拆解不规则多边形
引言
在几何处理领域,我们经常需要将一个不规则多边形拆分成多个满足特定尺寸要求的规则子多边形。比如,一个凹多边形可能在某个方向收缩到很窄,如果直接使用,会对后续处理造成问题。理想的方案是:沿着特定方向识别并切断窄区域,同时尽可能保留其他方向的几何特征。
本文将介绍一种"方向性形态学开运算"算法,核心依赖 Clipper 库(本文使用其整数 API,沿袭自 Clipper1 的经典接口风格)完成 Union、Difference、Intersection、Offset 等布尔运算。我会详细拆解其中的几何编排思想。
1. 问题建模
给定一个多边形(可能带孔洞、凹凸不规则),以及两个阈值:
- 最小宽度(垂直于主方向)
- 最小长度(平行于主方向)
目标是将其拆分为若干子多边形,每块在两个方向上都满足最小尺寸。不满足的窄区域直接丢弃。
这里的"主方向"可能是任意角度(比如 30°、45°、90°),但算法内部会做转化:判断主方向更接近 X 轴还是 Y 轴,然后将坐标系对齐到"X = 长度轴,Y = 宽度轴"的统一框架里处理。
absX >= absY → 主方向沿 X 轴,X=长度、Y=宽度
absX < absY → 主方向沿 Y 轴,自动交换宽/长阈值后 X=长度、Y=宽度
45° 时 absX == absY,默认视为沿 X 轴,避免歧义。
2. 核心流程
算法分三个阶段:
输入多边形
│
▼
[预处理] 去重合顶点、去共线中间点、闭合多边形
│
▼
[第一轮] Y 向方向性形态学开运算(消除 Y 向窄特征)
│ 如果无结果 → 返回空
▼
[第二轮] X 向方向性形态学开运算(消除 X 向窄特征)
│ 如果无结果 → 返回空
▼
[后处理] 多边形修复 → 包围盒二次检查 → 输出满足尺寸的子板
为什么是宽度先、长度后?因为先消除宽度方向的窄特征后,原先被窄桥连接的两块宽区域会自然分开。如果反过来先处理长度,某些窄桥可能因为宽度方向的关系被保留,导致无法正确断裂。
3. 方向性形态学开运算详解
这是整个算法的心脏。每一轮方向性形态学开运算包含以下步骤:
3.1 Union 合并
var unionClipper = ClipperWrapperBase.GetClipperWrapperInt(scale);
unionClipper.Set(paths);
unionClipper.Execute(BoolOpType.Union);
首先将所有路径做并集(Union),消除重叠区域,将零散的连通分量合并为尽可能少的外轮廓。这一步同时处理了孔洞——孔洞路径以顺时针(CW)方向参与 Union,Clipper 会自动从外轮廓中减去它们。
关于路径方向:Clipper 遵循偶数-奇/非零填充规则。在本算法中,外轮廓统一为逆时针(CCW),孔洞统一为顺时针(CW)。Union 后得到的 ClipperShape 对象已自动将外轮廓和孔洞分离开。
3.2 开重建(Opening by Reconstruction)
这是与传统数学形态学"开运算"最关键的区别。
标准开运算 = 先腐蚀再膨胀(Erode → Dilate)。操作过程:先向内偏移 delta,消除宽度 < 2×delta 的区域;再向外偏移 delta,将剩余区域的边界恢复到原始尺寸附近。
但标准开运算有个致命问题:ClipperOffset 在凹角处会产生人工线段。膨胀操作不会精确恢复到原始凹角的形状,而是在凹角内侧产生一道斜边或弧边,这些人工引入的几何对后续运算会造成连锁干扰。
开重建的解决方案:
// Step 1: 腐蚀
var co = new ClipperOffset { MiterLimit = 20 };
co.AddPath(intPath, JoinType.jtMiter, EndType.etClosedPolygon);
co.Execute(ref eroded, -deltaInt);
// Step 2: 膨胀
var co2 = new ClipperOffset { MiterLimit = 20 };
co2.AddPaths(eroded, JoinType.jtMiter, EndType.etClosedPolygon);
co2.Execute(ref dilated, deltaInt);
// Step 3: 原始多边形 ∩ 膨胀结果 → 精确恢复边界
var clip = new Clipper();
clip.AddPath(intPath, PolyType.ptSubject, true);
clip.AddPaths(dilated, PolyType.ptClip, true);
clip.Execute(ClipType.ctIntersection, polyTree, ...);
第三步用原始多边形与膨胀结果求交集,将膨胀"溢出"原始边界之外的区域精确裁剪掉。这样最终结果的边界完全来自原始多边形,不会引入任何人工线段。
MiterLimit = 20 的考量:这是 ClipperOffset 的斜接限制,对应最小内角约 5.73°。当多边形存在锐角时,miter 连接(保持尖角)比 round 连接(弧线近似)更能忠实于原始几何。20 是一个足够大的值,确保在大多数工程场景下不会触发尖角裁剪。
3.3 整数缩放
Clipper 的整数 API 使用 IntPoint(64 位整数坐标),因此浮点坐标需要缩放:
int scale = GeometryTolerance.MathScale; // 1E+8(一亿)
long x = (long)Math.Round(coord.X * scale);
long y = (long)Math.Round(coord.Y * scale);
1E+8 的缩放因子意味着:1 毫米精度的坐标可以表示到亚纳米级别(约 0.01 纳米),完全覆盖工程精度的需求。64 位整数能表达的范围约 9.22×10^18,缩放后仍能覆盖约 9.22×10^10 单位的坐标范围,对于绝大多数几何场景绰绰有余。
计算结果再反向缩放:
double x = (double)intPoint.X / scale;
double y = (double)intPoint.Y / scale;
浮点舍入误差通过容差控制(BusinessTolerance = 1E-3)在后续步骤中消除。
3.4 方向性恢复
各向同性的 ClipperOffset 对 X 和 Y 方向一视同仁。但如果本轮目标是"只消除 Y 向窄特征,保留 X 向特征",那么被误删的 X 向几何需要恢复。
恢复逻辑:
-
Boolean Difference:原始外轮廓 ∪ 开运算结果 → 得到被 MorphologicalOpen 移除的片段。
-
邻接度判定(核心创新):对每个被移除片段,计算它与保留片段之间的"边共线重叠比例"。
具体做法:遍历保留多边形的每条边,找出被移除多边形中与之共线的边,计算在保留边上的投影重叠区间。多条重叠区间合并后(避免重复计算),重叠总长度除以保留边自身长度,取所有保留边中的最大值作为邻接度。
// 区间合并算法 intervals.Sort((a, b) => a.tMin.CompareTo(b.tMin)); double curMin = intervals[0].tMin, curMax = intervals[0].tMax; for (int i = 1; i < intervals.Count; i++) { if (intervals[i].tMin <= curMax + tol) curMax = Math.Max(curMax, intervals[i].tMax); // 合并 else { mergedTotal += curMax - curMin; // 累加 curMin = intervals[i].tMin; curMax = intervals[i].tMax; } }若邻接度 ≥ 60%(
AdjacencyThreshold = 0.6),判定该片段是保留几何的自然延伸(比如一块从主体上略微凸出的结构),整片恢复。 -
跨度恢复:邻接度不达标的片段,进一步按非过滤方向细分为子区间。对每个子区间,用轴对齐矩形与原片段求交集,检查子区间在过滤方向的跨度是否 ≥ minExtent,满足则恢复。
最后,所有恢复的片段与开运算结果再次 Union 合并,得到最终外轮廓。
3.5 方向性孔洞破开
如果多边形内部有孔洞,孔洞与外轮廓之间的窄桥也需要处理。
算法对每个孔洞,分别计算过滤方向两侧到外轮廓的间隙:
上方间隙 = outerFMax - holeFMax
下方间隙 = holeFMin - outerFMin
仅当 0 < 间隙 < minExtent 时才处理——间隙恰好为 0 说明孔洞与外轮廓相切,不需要破开;间隙 ≥ minExtent 说明足够宽,也不需要处理。
破开方式很直接:构造一个矩形覆盖间隙区域,用 BoolOpType.Difference 从外轮廓中减去:
diffClipper.Set(outerPaths);
diffClipper.SetClips(gapRect);
diffClipper.Execute(BoolOpType.Difference);
这个矩形在过滤方向覆盖间隙(从外轮廓边缘到孔洞边缘),在非过滤方向覆盖孔洞在该方向的完整范围。矩形总是以 CCW 方向构造,与外轮廓做 Difference 会自动产生正确的破开效果。
双向破开:不仅对当前轮的过滤方向(如 Y)破开,同时用非过滤方向(X)的阈值再执行一次。这是为了防止先破开一个方向导致外轮廓分裂后,另一个方向的窄桥变得无法处理。
破开后,孔洞需要重新裁剪到新的外轮廓范围内(BoolOpType.Intersection),防止孔洞飘移到轮廓外部。
4. Clipper 操作全景
总结一下算法中使用的全部 Clipper 布尔操作:
| 操作 | 用途 | 出现位置 |
|---|---|---|
| Union | 合并重叠多边形、从路径重建 ClipperShape、最终合并恢复片段 | 多处在合并和重构中 |
| Difference | 孔洞窄桥破开(外轮廓减间隙矩形)、找出被 MorphologicalOpen 移除的片段 | 孔洞处理、方向性恢复 |
| Intersection | 开重建(原始 ∩ 膨胀)、孔洞裁剪到外轮廓内、被移除片段按子区间细分求交 | 开重建、孔洞裁剪、跨度恢复 |
| Offset(腐蚀/膨胀) | 开重建的前两步 | 形态学开运算 |
还有非 Clipper 但同样关键的几何运算:
- 鞋带公式(SignedArea):计算有向面积,用于判断路径是 CW 还是 CCW,确保方向标准化。
- 共线线段重叠区间:通过向量投影将共线检查转化为参数空间 [0,1] 上的区间计算,支撑邻接度算法。
- 容差去重:排序后扫描,相邻差值 ≤ 容差的视为同一点。
5. 为什么选择形态学开运算而非其他方案
初版方案尝试过"条带-交集-合并"思路:按顶点坐标生成水平/垂直条带,逐条带与原多边形求交后合并。这个方案的问题在于:
- 条带生成策略高度依赖顶点分布,非轴对齐的多边形会产生大量无效条带。
- 窄区域可能需要跨越多个顶点才能被正确识别,单次条带扫描可能遗漏。
- 时间复杂度和条带数量成正比,对于复杂多边形性能较差。
形态学开运算的优势:
- 各向同性 + 方向性恢复的组合,确保过滤方向精准消除、非过滤方向充分保留。
- 开重建保证了边界的几何保真度。
- 算法复杂度由 Clipper 内部控制,Union/Difference/Intersection 操作已被充分优化。
6. 边界情况与鲁棒性
实际工程中,算法需要处理各种退化情况:
- 多边形退化:顶点数 < 3 的直接返回空,RepairPolygon 预先过滤重合点和共线中间点。
- 零阈值:minWidth 或 minLength 为 0 时,delta = max(0, BusinessTolerance) = 0.001,实际上该轮过滤近乎空操作,但不影响算法结构。
- 腐蚀后完全消失:MorphologicalOpen 返回 null,外层跳过该形状。
- 孔洞与外轮廓相切:间隙为 0,不触发破开。
- 破开后外轮廓消失:Difference 返回空集合,直接短路返回空列表。
- 无结果保护:每轮之后检查结果是否为空,空则直接终止,不产生部分结果(防止不可用的碎片被输出)。
7. 总结
这篇文章介绍了一个完整的、生产级的不规则多边形拆分算法。核心思路围绕三大几何编排:
- 方向性形态学开运算:用 ClipperOffset 消除窄特征,用原始多边形 ∩ 膨胀结果实现开重建保证边界保真度,再通过方向性恢复纠正误删。
- 邻接度判定:用区间合并算法精确量化被移除片段与保留几何的附着关系,以 60% 阈值决定整片恢复或逐区间恢复。
- 孔洞窄桥破开:检测孔洞各侧间隙,仅对不够宽的间隙方向做定向破开,然后用 Union 重构确保外轮廓与孔洞的一致性。
其中涉及的所有布尔运算——Union、Difference、Intersection、Offset——全部由 Clipper 整数 API 承担,配合 1E+8 的缩放因子,在工程精度和数值稳定性之间取得了良好平衡。

浙公网安备 33010602011771号