第四章 多边形偏移操作(C#版)
4.1 引言
多边形偏移(Polygon Offsetting)也称为多边形膨胀(Inflate)或收缩(Deflate),是指将多边形的边界向内或向外移动指定的距离。这是一个在实际应用中极为常见的操作,从CNC加工的刀具补偿到地图制作的缓冲区分析,从3D打印的轮廓生成到游戏开发的碰撞边界创建,多边形偏移操作无处不在。
Clipper2提供了强大而灵活的偏移功能,支持多种连接类型和端点类型,能够处理复杂的多边形结构。本章将详细介绍Clipper2 C#版本中多边形偏移的使用方法和高级技巧。
4.2 偏移的基本概念
4.2.1 什么是多边形偏移
多边形偏移是将多边形的每条边沿其法线方向移动指定距离的操作。当偏移距离为正值时,多边形向外扩张;当偏移距离为负值时,多边形向内收缩。
原始多边形 正向偏移(膨胀) 负向偏移(收缩)
┌─────────┐ ┌───────────────┐ ┌───────┐
│ │ │ │ │ │
│ │ → │ ┌─────┐ │ │ │
│ │ │ │ │ │ └───────┘
└─────────┘ └───┴─────┴─────┘
4.2.2 偏移中的几何问题
在多边形偏移过程中,会遇到几个重要的几何问题:
拐角处理
当两条边在顶点相遇时,它们的偏移边会在拐角处产生间隙(外角)或重叠(内角)。处理这些拐角的方式称为”连接类型”(Join Type)。
原始拐角 圆角连接 斜角连接 方角连接
(Round) (Miter) (Square)
╱ ╱ ‾‾‾ ╱ ╲ ╱──────╲
╱ ╱ ╲ ╱ ╲ ╱ ╲
────╱ ──────╱ ╲ ─────╱ ╲ ─────╱ ╲
端点处理
对于开放路径(折线),需要决定如何处理两端的端点。处理端点的方式称为”端点类型”(End Type)。
原始折线 方角端点 圆角端点 平头端点
───── ┌─────┐ (─────) ═════
└─────┘
自交处理
当偏移距离过大时,偏移后的多边形可能会自交。Clipper2能够自动检测并正确处理这些自交情况。
4.2.3 偏移方向与符号约定
在Clipper2中:
- 正值偏移:使多边形区域增大(向外扩张)
- 负值偏移:使多边形区域减小(向内收缩)
这个约定与多边形的顶点方向无关——无论多边形是顺时针还是逆时针,正值始终表示扩张。
4.3 使用简化API进行偏移
Clipper2提供了简化的偏移函数,可以用一行代码完成偏移操作。
4.3.1 InflatePaths函数
using System;
using Clipper2Lib;
class InflatePathsExample
{
static void Main()
{
// 创建一个正方形
Paths64 paths = new Paths64();
paths.Add(Clipper.MakePath(new long[] { 100, 100, 200, 100, 200, 200, 100, 200 }));
// 向外偏移10个单位,使用圆角连接
Paths64 inflated = Clipper.InflatePaths(paths, 10, JoinType.Round, EndType.Polygon);
Console.WriteLine($"膨胀后顶点数: {inflated[0].Count}");
// 向内偏移10个单位
Paths64 deflated = Clipper.InflatePaths(paths, -10, JoinType.Round, EndType.Polygon);
Console.WriteLine($"收缩后顶点数: {deflated[0].Count}");
}
}
4.3.2 方法签名
public static Paths64 InflatePaths(
Paths64 paths, // 输入路径
double delta, // 偏移距离(正值膨胀,负值收缩)
JoinType joinType, // 连接类型
EndType endType, // 端点类型
double miterLimit = 2.0, // 斜角限制(仅对Miter连接有效)
double arcTolerance = 0.0 // 弧度容差(仅对Round连接有效)
)
4.3.3 使用浮点数坐标
using Clipper2Lib;
class FloatOffsetExample
{
static void Main()
{
// 使用浮点数坐标
PathsD pathsD = new PathsD();
pathsD.Add(Clipper.MakePath(new double[] { 1.5, 1.5, 2.5, 1.5, 2.5, 2.5, 1.5, 2.5 }));
// 偏移0.1个单位
PathsD result = Clipper.InflatePaths(pathsD, 0.1, JoinType.Round, EndType.Polygon, 2.0, 0.01);
Console.WriteLine($"结果顶点数: {result[0].Count}");
}
}
4.4 连接类型(JoinType)
连接类型决定了在多边形的拐角处如何连接偏移后的边。
4.4.1 Round(圆角连接)
圆角连接在拐角处创建圆弧形的过渡。这是最常用的连接类型,产生的结果最为平滑。
Paths64 result = Clipper.InflatePaths(paths, 10, JoinType.Round, EndType.Polygon);
特点:
- 拐角处为圆弧形
- 视觉效果最平滑
- 顶点数量较多
- 适合CNC加工、3D打印等需要圆滑轮廓的应用
弧度容差(Arc Tolerance)
arcTolerance参数控制圆弧的精度。值越小,圆弧越精确,但顶点数量越多:
// 高精度圆弧(更多顶点)
Paths64 highQuality = Clipper.InflatePaths(paths, 10, JoinType.Round, EndType.Polygon, 2.0, 0.1);
// 低精度圆弧(较少顶点)
Paths64 lowQuality = Clipper.InflatePaths(paths, 10, JoinType.Round, EndType.Polygon, 2.0, 1.0);
如果arcTolerance为0或负值,Clipper2会根据偏移距离自动选择一个合适的值。
4.4.2 Miter(斜角连接)
斜角连接延长相邻的偏移边直到它们相交。这会在锐角处产生尖锐的突出。
Paths64 result = Clipper.InflatePaths(paths, 10, JoinType.Miter, EndType.Polygon);
特点:
- 锐角处会产生尖锐的尖角
- 顶点数量与原始多边形相同
- 在非常锐的角度时,尖角可能延伸得非常远
斜角限制(Miter Limit)
miter_limit参数用于限制斜角的最大长度。当斜角延伸超过此限制时,会被截断为平头:
// miter_limit = 2.0(默认值)
Paths64 result1 = Clipper.InflatePaths(paths, 10, JoinType.Miter, EndType.Polygon, 2.0);
// miter_limit = 4.0(允许更长的斜角)
Paths64 result2 = Clipper.InflatePaths(paths, 10, JoinType.Miter, EndType.Polygon, 4.0);
// miter_limit = 1.0(斜角很快被截断)
Paths64 result3 = Clipper.InflatePaths(paths, 10, JoinType.Miter, EndType.Polygon, 1.0);
斜角限制的计算方式:当斜角长度超过 delta * miter_limit 时,斜角会被截断。
4.4.3 Square(方角连接)
方角连接在拐角处创建矩形的突出,每个拐角固定向外延伸一个偏移距离的长度。
Paths64 result = Clipper.InflatePaths(paths, 10, JoinType.Square, EndType.Polygon);
特点:
- 每个拐角处添加固定大小的矩形突出
- 产生的形状较为规则
- 顶点数量是原始多边形的两倍
- 适合需要均匀边界厚度的应用
4.4.4 Bevel(斜切连接)
斜切连接(也称为倒角)在拐角处创建一个平面截断,而不是尖角或圆弧。
Paths64 result = Clipper.InflatePaths(paths, 10, JoinType.Bevel, EndType.Polygon);
特点:
- 拐角处为斜切平面
- 类似于miter_limit=1.0时的Miter连接
- 顶点数量是原始多边形的两倍
- 视觉效果介于Miter和Square之间
4.4.5 连接类型对比
// 创建一个三角形来展示不同连接类型的效果
Paths64 triangle = new Paths64();
triangle.Add(Clipper.MakePath(new long[] { 0, 100, 50, 0, 100, 100 }));
// 分别使用不同的连接类型偏移
Paths64 roundResult = Clipper.InflatePaths(triangle, 10, JoinType.Round, EndType.Polygon);
Paths64 miterResult = Clipper.InflatePaths(triangle, 10, JoinType.Miter, EndType.Polygon);
Paths64 squareResult = Clipper.InflatePaths(triangle, 10, JoinType.Square, EndType.Polygon);
Paths64 bevelResult = Clipper.InflatePaths(triangle, 10, JoinType.Bevel, EndType.Polygon);
| 连接类型 | 锐角表现 | 顶点数量 | 适用场景 |
|---|---|---|---|
| Round | 圆弧过渡 | 多 | 平滑外观、CNC加工 |
| Miter | 尖锐延伸 | 等于原始 | 保持尖角外观 |
| Square | 矩形突出 | 2倍原始 | 规则边界 |
| Bevel | 斜切平面 | 2倍原始 | 倒角效果 |
4.5 端点类型(EndType)
端点类型决定了开放路径(折线)的两端如何处理。对于闭合路径(多边形),端点类型应设为Polygon或Joined。
4.5.1 Polygon端点
用于闭合的多边形,路径会自动闭合。
Paths64 paths = new Paths64();
paths.Add(Clipper.MakePath(new long[] { 100, 100, 200, 100, 200, 200, 100, 200 }));
Paths64 result = Clipper.InflatePaths(paths, 10, JoinType.Round, EndType.Polygon);
4.5.2 Joined端点
类似于Polygon,但专门用于开放路径的处理,路径两端会连接起来形成闭合的偏移区域。
Paths64 openPath = new Paths64();
openPath.Add(Clipper.MakePath(new long[] { 100, 100, 200, 100, 200, 200 })); // 一条折线
Paths64 result = Clipper.InflatePaths(openPath, 10, JoinType.Round, EndType.Joined);
// 结果是一个闭合的管状区域
4.5.3 Butt端点(平头端点)
在路径端点处创建垂直于路径方向的平头截断。
Paths64 openPath = new Paths64();
openPath.Add(Clipper.MakePath(new long[] { 100, 100, 200, 100, 200, 200 }));
Paths64 result = Clipper.InflatePaths(openPath, 10, JoinType.Round, EndType.Butt);
// 路径两端是平头的
特点:
- 端点处与路径方向垂直
- 产生平整的端面
- 偏移后的长度与原始路径相同
4.5.4 Square端点(方角端点)
类似于Butt端点,但在端点处向外延伸一个偏移距离的长度。
Paths64 openPath = new Paths64();
openPath.Add(Clipper.MakePath(new long[] { 100, 100, 200, 100, 200, 200 }));
Paths64 result = Clipper.InflatePaths(openPath, 10, JoinType.Round, EndType.Square);
// 路径两端是方形的,向外延伸
特点:
- 端点处创建方形突出
- 偏移后的长度增加 2 * delta
4.5.5 Round端点(圆角端点)
在路径端点处创建半圆形的端帽。
Paths64 openPath = new Paths64();
openPath.Add(Clipper.MakePath(new long[] { 100, 100, 200, 100, 200, 200 }));
Paths64 result = Clipper.InflatePaths(openPath, 10, JoinType.Round, EndType.Round);
// 路径两端是圆形的
特点:
- 端点处为半圆形
- 视觉效果平滑
- 偏移后的长度增加 2 * delta
4.5.6 端点类型对比
| 端点类型 | 描述 | 长度变化 | 适用场景 |
|---|---|---|---|
| Polygon | 闭合多边形 | 不适用 | 闭合图形 |
| Joined | 开放路径闭合 | 不适用 | 创建管状区域 |
| Butt | 平头截断 | 无变化 | 精确边界 |
| Square | 方形延伸 | +2*delta | 方形端帽 |
| Round | 圆形端帽 | +2*delta | 圆滑端帽 |
4.6 使用ClipperOffset类
对于需要更多控制的场景,可以使用ClipperOffset类。
4.6.1 基本使用
using Clipper2Lib;
class Program
{
static void Main()
{
// 创建ClipperOffset对象
ClipperOffset offsetter = new ClipperOffset();
// 设置属性
offsetter.MiterLimit = 2.0;
offsetter.ArcTolerance = 0.25;
// 添加路径
Paths64 paths = new Paths64();
paths.Add(Clipper.MakePath(new long[] { 100, 100, 200, 100, 200, 200, 100, 200 }));
offsetter.AddPaths(paths, JoinType.Round, EndType.Polygon);
// 执行偏移
Paths64 result = new Paths64();
offsetter.Execute(10, result); // 偏移10个单位
}
}
4.6.2 多次偏移
使用ClipperOffset类可以对同一组路径执行多次不同距离的偏移:
ClipperOffset offsetter = new ClipperOffset();
offsetter.AddPaths(paths, JoinType.Round, EndType.Polygon);
// 执行多个不同距离的偏移
Paths64 offset5 = new Paths64(), offset10 = new Paths64(), offset15 = new Paths64();
offsetter.Execute(5, offset5);
offsetter.Execute(10, offset10);
offsetter.Execute(15, offset15);
// 所有偏移都基于原始路径
4.6.3 使用PolyTree输出
ClipperOffset offsetter = new ClipperOffset();
offsetter.AddPaths(paths, JoinType.Round, EndType.Polygon);
PolyTree64 tree = new PolyTree64();
offsetter.Execute(10, tree);
// 遍历PolyTree获取层次结构
// 这对于负向偏移产生的孔洞很有用
4.6.4 清空和复用
ClipperOffset offsetter = new ClipperOffset();
for (int i = 0; i < 100; i++) {
// 清空之前的数据
offsetter.Clear();
// 添加新的路径
offsetter.AddPaths(GetPaths(i), JoinType.Round, EndType.Polygon);
// 执行偏移
Paths64 result = new Paths64();
offsetter.Execute(10, result);
}
4.6.5 属性设置
ClipperOffset offsetter = new ClipperOffset();
// 设置斜角限制(用于Miter连接)
offsetter.MiterLimit = 4.0;
// 设置弧度容差(用于Round连接)
offsetter.ArcTolerance = 0.1;
// 设置是否保留共线点
offsetter.PreserveCollinear = true;
// 设置是否反转解决方案
offsetter.ReverseSolution = false;
4.7 处理复杂情况
4.7.1 带孔洞的多边形
当对带孔洞的多边形进行偏移时,外边界和孔洞会以相反的方向偏移:
// 创建带孔洞的多边形
Paths64 paths = new Paths64();
// 外边界(逆时针)
paths.Add(Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 }));
// 孔洞(顺时针)
paths.Add(Clipper.MakePath(new long[] { 25, 25, 25, 75, 75, 75, 75, 25 }));
// 正向偏移:外边界扩张,孔洞收缩
Paths64 inflated = Clipper.InflatePaths(paths, 10, JoinType.Round, EndType.Polygon);
// 负向偏移:外边界收缩,孔洞扩张
Paths64 deflated = Clipper.InflatePaths(paths, -10, JoinType.Round, EndType.Polygon);
4.7.2 自交处理
当偏移距离较大时,可能会产生自交。Clipper2会自动检测并处理这些情况:
// 创建一个凹多边形
Paths64 paths = new Paths64();
paths.Add(Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 50, 50, 50, 50, 100, 0, 100 }));
// 大距离负向偏移可能导致多边形消失或分裂
Paths64 result = Clipper.InflatePaths(paths, -20, JoinType.Round, EndType.Polygon);
// result 可能为空或包含多个分离的多边形
4.7.3 多边形消失
当负向偏移距离足够大时,多边形可能完全消失:
// 一个小正方形
Paths64 paths = new Paths64();
paths.Add(Clipper.MakePath(new long[] { 0, 0, 20, 0, 20, 20, 0, 20 }));
// 偏移距离大于宽度的一半
Paths64 result = Clipper.InflatePaths(paths, -15, JoinType.Round, EndType.Polygon);
// result 为空,因为多边形已完全收缩消失
4.7.4 岛屿生成
对于带孔洞的多边形,负向偏移可能导致孔洞扩张并与外边界相遇,产生新的岛屿:
// 创建一个环形(外边界加孔洞)
Paths64 ring = new Paths64();
ring.Add(Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 })); // 外边界
ring.Add(Clipper.MakePath(new long[] { 25, 25, 25, 75, 75, 75, 75, 25 })); // 孔洞
// 负向偏移使孔洞扩张
Paths64 deflated = Clipper.InflatePaths(ring, -20, JoinType.Round, EndType.Polygon);
// 当偏移距离足够大时,可能产生复杂的结果
4.7.5 使用PolyTree追踪层次
使用PolyTree输出可以追踪偏移后多边形的层次关系:
ClipperOffset offsetter = new ClipperOffset();
offsetter.AddPaths(paths, JoinType.Round, EndType.Polygon);
PolyTree64 tree = new PolyTree64();
offsetter.Execute(-10, tree);
// 遍历PolyTree
static void ProcessTree(PolyPath64 node, int depth) {
if (node.Polygon != null && node.Polygon.Count > 0) {
Console.WriteLine(new string(' ', depth * 2)
+ (node.IsHole ? "孔洞" : "多边形")
+ " 顶点数: " + node.Polygon.Count);
}
foreach (PolyPath64 child in node) {
ProcessTree(child, depth + 1);
}
}
foreach (PolyPath64 child in tree) {
ProcessTree(child, 0);
}
4.8 实际应用示例
4.8.1 CNC刀具路径补偿
// 零件轮廓
Paths64 partContour = new Paths64();
partContour.Add(Clipper.MakePath(new long[] { /* 轮廓坐标 */ }));
// 刀具半径(例如5mm刀具)
double toolRadius = 5.0 * 1000; // 假设使用0.001mm作为单位
// 外轮廓加工:向外偏移刀具半径
Paths64 outerToolPath = Clipper.InflatePaths(partContour, toolRadius,
JoinType.Round, EndType.Polygon);
// 内轮廓(口袋)加工:向内偏移刀具半径
Paths64 innerToolPath = Clipper.InflatePaths(partContour, -toolRadius,
JoinType.Round, EndType.Polygon);
4.8.2 3D打印轮廓生成
// 切片轮廓
Paths64 sliceContour = new Paths64();
sliceContour.Add(Clipper.MakePath(new long[] { /* 轮廓坐标 */ }));
// 挤出线宽(例如0.4mm)
double lineWidth = 0.4 * 1000; // 使用0.001mm作为单位
// 生成多层偏移轮廓
List<Paths64> perimeterLayers = new List<Paths64>();
for (int i = 0; i < 3; i++) {
double offset = -lineWidth * (0.5 + i); // 第一层偏移半个线宽
Paths64 perimeter = Clipper.InflatePaths(sliceContour, offset,
JoinType.Miter, EndType.Polygon);
if (perimeter.Count > 0) {
perimeterLayers.Add(perimeter);
}
}
4.8.3 地图缓冲区分析
// 道路中心线
Paths64 roadCenterlines = new Paths64();
roadCenterlines.Add(Clipper.MakePath(new long[] { /* 中心线坐标 */ }));
// 创建道路缓冲区(道路宽度的一半)
double halfRoadWidth = 7.5 * 1000; // 15米宽道路
Paths64 roadBuffer = Clipper.InflatePaths(roadCenterlines, halfRoadWidth,
JoinType.Round, EndType.Round);
// 创建噪音影响区域(道路两侧50米)
double noiseZone = 50.0 * 1000;
Paths64 noiseBuffer = Clipper.InflatePaths(roadCenterlines, noiseZone,
JoinType.Round, EndType.Round);
4.8.4 安全边界生成
// 建筑物轮廓
Paths64 buildings = new Paths64();
foreach (var building in buildingList) {
buildings.Add(building.getContour());
}
// 创建安全间距(5米)
double safetyDistance = 5.0 * 1000;
Paths64 safetyZones = Clipper.InflatePaths(buildings, safetyDistance,
JoinType.Round, EndType.Polygon);
// 合并重叠的安全区域
Paths64 mergedSafety = Clipper.Union(safetyZones, FillRule.NonZero);
4.8.5 字体描边效果
// 文字轮廓(已转换为多边形)
Paths64 textContour = new Paths64();
textContour.Add(Clipper.MakePath(new long[] { /* 字符轮廓 */ }));
// 创建描边效果
double strokeWidth = 2.0 * 1000; // 2mm描边
// 外描边
Paths64 outerStroke = Clipper.InflatePaths(textContour, strokeWidth,
JoinType.Round, EndType.Polygon);
// 描边区域 = 外描边 - 原始轮廓
Paths64 strokeOnly = Clipper.Difference(outerStroke, textContour, FillRule.NonZero);
// 或者创建内描边
Paths64 innerStroke = Clipper.InflatePaths(textContour, -strokeWidth,
JoinType.Round, EndType.Polygon);
Paths64 innerStrokeOnly = Clipper.Difference(textContour, innerStroke, FillRule.NonZero);
4.8.6 碰撞边界扩展
// 游戏中的角色边界
Paths64 characterBounds = new Paths64();
characterBounds.Add(Clipper.MakePath(new long[] { /* 边界坐标 */ }));
// 扩展碰撞检测范围(预警区域)
double warningZone = 10.0; // 10像素
Paths64 collisionWarning = Clipper.InflatePaths(characterBounds, warningZone,
JoinType.Round, EndType.Polygon);
// 检查是否有物体进入预警区域
Paths64 obstacles = GetObstacles();
Paths64 potentialCollisions = Clipper.Intersect(collisionWarning, obstacles, FillRule.NonZero);
if (potentialCollisions.Count > 0) {
// 有潜在碰撞风险
}
4.9 性能优化
4.9.1 控制圆弧精度
圆弧精度是影响性能的主要因素之一:
// 低精度(快速)
Paths64 fast = Clipper.InflatePaths(paths, 10, JoinType.Round, EndType.Polygon, 2.0, 2.0);
// 高精度(慢速)
Paths64 precise = Clipper.InflatePaths(paths, 10, JoinType.Round, EndType.Polygon, 2.0, 0.1);
选择合适的精度
// 根据偏移距离自动计算合适的弧度容差
double arcTolerance = delta / 50.0; // 经验值
if (arcTolerance < 0.1) arcTolerance = 0.1;
if (arcTolerance > 1.0) arcTolerance = 1.0;
Paths64 result = Clipper.InflatePaths(paths, delta, JoinType.Round, EndType.Polygon, 2.0, arcTolerance);
4.9.2 使用Miter连接
当不需要圆滑的拐角时,使用Miter连接可以显著提高性能:
// Round连接需要生成多个圆弧顶点
Paths64 roundResult = Clipper.InflatePaths(paths, 10, JoinType.Round, EndType.Polygon);
// 顶点数量可能增加很多
// Miter连接保持相同的顶点数量
Paths64 miterResult = Clipper.InflatePaths(paths, 10, JoinType.Miter, EndType.Polygon);
// 顶点数量与原始相同
4.9.3 预处理路径
在偏移之前简化路径可以提高性能:
// 简化路径,移除共线点和接近的点
Paths64 simplified = Clipper.SimplifyPaths(paths, 0.5);
// 然后进行偏移
Paths64 result = Clipper.InflatePaths(simplified, 10, JoinType.Round, EndType.Polygon);
4.9.4 批量处理
复用ClipperOffset对象:
ClipperOffset offsetter = new ClipperOffset();
offsetter.ArcTolerance = 0.25;
List<Paths64> results = new List<Paths64>();
for (int i = 0; i < inputs.Count; i++) {
results.Add(new Paths64());
offsetter.Clear();
offsetter.AddPaths(inputs[i], JoinType.Round, EndType.Polygon);
offsetter.Execute(10, results[i]);
}
4.10 常见问题与解决方案
4.10.1 偏移结果为空
问题:负向偏移后结果为空。
解决方案:
// 检查偏移距离是否过大
double minDistance = GetMinimumOffset(paths); // 计算最大可能的内偏移距离
if (Math.Abs(delta) > minDistance) {
// 警告用户偏移距离过大
}
4.10.2 意外的自交
问题:偏移后出现意外的自交或孔洞。
解决方案:
// 使用Union清理结果
Paths64 result = Clipper.InflatePaths(paths, delta, JoinType.Round, EndType.Polygon);
Paths64 cleaned = Clipper.Union(result, FillRule.NonZero);
4.10.3 性能问题
问题:偏移操作非常慢。
解决方案:
// 1. 降低圆弧精度
double arcTol = delta / 20.0;
// 2. 使用Miter连接代替Round
Paths64 result = Clipper.InflatePaths(paths, delta, JoinType.Miter, EndType.Polygon);
// 3. 预先简化路径
Paths64 simplified = Clipper.SimplifyPaths(paths, 1.0);
Paths64 result2 = Clipper.InflatePaths(simplified, delta, JoinType.Round, EndType.Polygon);
4.10.4 圆弧质量问题
问题:圆弧看起来不够圆滑。
解决方案:
// 降低弧度容差
Paths64 result = Clipper.InflatePaths(paths, delta, JoinType.Round, EndType.Polygon, 2.0, 0.1);
// 或者使用自动计算
Paths64 result = Clipper.InflatePaths(paths, delta, JoinType.Round, EndType.Polygon, 2.0, 0.0);
4.11 形态学操作
形态学(Morphology)运算源自图像处理,但对矢量多边形同样适用。它通过一个称为”结构元素”(Structuring Element)的简单形状,对目标多边形执行膨胀(Dilate)、腐蚀(Erode),以及由二者组合得到的开运算(Open)、闭运算(Close)和形态学梯度(Gradient)。在 GIS 与 CAD 中,形态学操作常用于清理矢量数据:剔除细小图斑、弥合窄缝、填充凹口、提取边界带等。
当结构元素是以原点为中心的圆盘时,形态学膨胀恰好等价于圆角(Round)连接的正向偏移,腐蚀等价于负向偏移。因此借助本章前面介绍的 Clipper.InflatePaths,可以用极简代码实现圆盘形态学运算。若需要矩形、线段等任意形状的结构元素,则必须改用第五章的闵可夫斯基运算实现。
4.11.1 膨胀与腐蚀
膨胀使多边形区域向外扩张,可以填充凹陷、弥合窄缝;腐蚀使多边形区域向内收缩,可以剔除细小突起、断开窄桥。用偏移函数表达:
using Clipper2Lib;
// 膨胀:正 delta;JoinType.Round 等价于圆盘结构元素
Paths64 dilated = Clipper.InflatePaths(paths, r, JoinType.Round, EndType.Polygon);
// 腐蚀:负 delta(Clipper2 没有 DeflatePaths,收缩用负 delta)
Paths64 eroded = Clipper.InflatePaths(paths, -r, JoinType.Round, EndType.Polygon);
Clipper2 的静态 API 只提供
Clipper.InflatePaths,并不存在名为DeflatePaths的函数。收缩(Deflate)通过传入负的delta实现,例如上面的-r。
这里 r 为圆盘结构元素的半径;JoinType.Round 让拐角以圆弧过渡,从而近似圆盘沿形状边界滚动扫过的区域。
4.11.2 开运算与闭运算
开运算是先腐蚀后膨胀(先 -r 后 +r);闭运算是先膨胀后腐蚀(先 +r 后 -r)。
using Clipper2Lib;
// 开运算:先 -r 后 +r
Paths64 opened = Clipper.InflatePaths(
Clipper.InflatePaths(paths, -r, JoinType.Round, EndType.Polygon),
r, JoinType.Round, EndType.Polygon);
// 闭运算:先 +r 后 -r
Paths64 closed = Clipper.InflatePaths(
Clipper.InflatePaths(paths, r, JoinType.Round, EndType.Polygon),
-r, JoinType.Round, EndType.Polygon);
几何效果:
- 开运算:先腐蚀会剔除比结构元素还细小的突起、断开窄桥,再膨胀时这些被剔除的部分无法恢复,因而能平滑轮廓、分离粘连图斑。
- 闭运算:先膨胀会填平凹陷、弥合窄缝,再腐蚀时主体轮廓基本保持,因而能填补内部小孔与窄缝,而不显著改变整体大小。
4.11.3 形态学梯度
形态学梯度定义为膨胀结果减去腐蚀结果,得到沿原多边形边界的环形”边界带”。
using Clipper2Lib;
Paths64 dilated = Clipper.InflatePaths(paths, r, JoinType.Round, EndType.Polygon);
Paths64 eroded = Clipper.InflatePaths(paths, -r, JoinType.Round, EndType.Polygon);
// 梯度 = 膨胀 - 腐蚀
Paths64 gradient = Clipper.Difference(dilated, eroded, FillRule.NonZero);
gradient 是宽度约为 2r 的环形区域,可用于边界高亮或缓冲区边界提取。
4.11.4 迭代收缩与碎屑清理
对复杂多边形做多轮负向偏移(迭代收缩)时,每轮之间可能产生细小的碎屑多边形。官方 README 中的”兔子”示例给出了一种清理模式:每轮收缩后用 Clipper.SimplifyPaths(paths, 0.25) 清除碎屑,避免它们累积影响后续运算。
using Clipper2Lib;
Paths64 paths = originalPaths;
for (int i = 0; i < iterations; i++)
{
paths = Clipper.InflatePaths(paths, -step, JoinType.Round, EndType.Polygon);
// 清除本轮收缩产生的碎屑
paths = Clipper.SimplifyPaths(paths, 0.25);
}
SimplifyPaths 的第二个参数 epsilon 是简化容差:间距小于该值的顶点会被合并,细小到无法形成有效多边形的碎屑会被整体移除。
4.11.5 应用场景
小图斑剔除
开运算(先腐蚀后膨胀)先缩小所有图斑,比结构元素还小的图斑会在腐蚀阶段彻底消失,随后膨胀恢复大图斑轮廓。
// 开运算剔除细小图斑
Paths64 cleaned = Clipper.InflatePaths(
Clipper.InflatePaths(paths, -r, JoinType.Round, EndType.Polygon),
r, JoinType.Round, EndType.Polygon);
窄缝弥合
闭运算(先膨胀后腐蚀)先弥合窄缝、填充小孔,再腐蚀恢复主体轮廓。
// 闭运算弥合窄缝
Paths64 sealedResult = Clipper.InflatePaths(
Clipper.InflatePaths(paths, r, JoinType.Round, EndType.Polygon),
-r, JoinType.Round, EndType.Polygon);
凹口填充
对凹多边形做一次正向膨胀,凹陷处即被填平,得到更”凸”的轮廓。
// 膨胀填充凹口
Paths64 filled = Clipper.InflatePaths(paths, r, JoinType.Round, EndType.Polygon);
边界带提取
用形态学梯度(膨胀减腐蚀)提取沿边界的环形带,常用于地块边界可视化或缓冲区边界分析。
// 梯度提取边界带
Paths64 band = Clipper.Difference(
Clipper.InflatePaths(paths, r, JoinType.Round, EndType.Polygon),
Clipper.InflatePaths(paths, -r, JoinType.Round, EndType.Polygon),
FillRule.NonZero);
以上均为圆盘结构元素。若需要矩形、线段等任意形状的结构元素,应改用闵可夫斯基运算实现,参见第五章。
4.12 本章小结
本章我们深入学习了Clipper2的多边形偏移功能:
- 基本概念:偏移、膨胀、收缩的含义及方向约定
- 连接类型:Round、Miter、Square、Bevel及其特点
- 端点类型:Polygon、Joined、Butt、Square、Round及其应用场景
- ClipperOffset类:高级控制选项和多次偏移
- 复杂情况处理:孔洞、自交、消失、岛屿
- 实际应用:CNC加工、3D打印、地图缓冲区、碰撞检测
- 性能优化:弧度精度、连接类型选择、路径预处理
- 形态学操作:膨胀、腐蚀、开运算、闭运算、形态学梯度及其应用
在下一章中,我们将学习Clipper2的矩形裁剪和闵可夫斯基运算功能。