三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

2026-08-13-clipper-tutorials-morphology

2026-08-13-clipper-tutorials-morphology

Clipper1/Clipper2 教程形态学内容优化 实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 在 cad/(教程)与 sci/(源码解读)下的 Clipper1、Clipper2 四个系列中,就地修正形态学相关错误并补齐"膨胀/腐蚀/开运算/闭运算/梯度"完整内容,同步两个 GitHub 仓库的最新 API 事实。

Architecture: 4 个并行内容更新任务(文件集互不相交)+ 1 个全局验证任务。所有代码示例以已核对的仓库源码签名为准(见下)。不新增/删除章节文件、不动 index.md 与 navigation.yml。

Tech Stack: Jekyll 静态站点,Markdown 教程。C# 代码示例(ClipperLib / Clipper2Lib)。

Global Constraints

  • 只修改本任务指定的文件与段落;不新增章节文件;不动 sci/*/index.mdcad/*/index.md_data/navigation.yml
  • 章节内新增小节必须顺延重编号,且同步修正本章小结/练习题中引用的小节编号。
  • 代码语言一律 C#;API 签名必须与本计划附录"已核对 API 参考"一致,不得凭记忆写。
  • 中文写作,无 emoji;风格匹配所在章节现有文风(cad=概念+代码+练习,sci=源码分析+原理+注意事项)。
  • 代码块围栏 ```csharp 成对闭合。
  • 禁止提交 git——任务完成后报告改动文件清单与验证结果,由编排者统一验证提交。
  • 数学正确性红线:腐蚀必须用补集法 A ⊖ B = (Aᶜ ⊕ Bʳ)ᶜ,禁止出现"腐蚀 = MinkowskiDiff"的说法;开运算=先腐蚀后膨胀;闭运算=先膨胀后腐蚀。

已核对 API 参考(2026-08 从仓库源码逐行核对)

Clipper2(AngusJohnson/Clipper2 main 分支,CSharp/Clipper2Lib)

  • 静态工具在 public static class Clipper(Clipper.cs)。
  • public static Paths64 MinkowskiSum(Path64 pattern, Path64 path, bool isClosed);
  • public static PathsD MinkowskiSum(PathD pattern, PathD path, bool isClosed);(注意: precision 参数,旧教程中的 int precision = 2 已移除;PathD 精度由内部 Minkowski.SumdecimalPlaces = 2 默认值处理)
  • public static Paths64 MinkowskiDiff(Path64 pattern, Path64 path, bool isClosed);PathD 同构。
  • public static Paths64 InflatePaths(Paths64 paths, double delta, JoinType joinType, EndType endType, double miterLimit = 2.0, double arcTolerance = 0.0);(PathD 重载多 int precision = 2 参数)。DeflatePaths 不存在——收缩 = 负 delta。
  • public static Path64 SimplifyPath(Path64 path, double epsilon, bool isClosedPath = true);public static Paths64 SimplifyPaths(Paths64 paths, double epsilon, bool isClosedPaths = true);
  • public static PointInPolygonResult PointInPolygon(Point64 pt, Path64 polygon);[Flags] enum PointInPolygonResult { IsOn = 0, IsInside = 1, IsOutside = 2 }
  • public static Rect64 GetBounds(Path64 path);public static Rect64 GetBounds(Paths64 paths);struct Rect64 { public long left; public long top; public long right; public long bottom; ... }(字段小写)
  • public static Path64 MakePath(long[] arr);(另有 int[]、double[] 重载);public static double Area(Path64 path);public static bool IsPositive(Path64 poly);
  • public static Path64 ReversePath(Path64 path);public static Paths64 ReversePaths(Paths64 paths);
  • public static Path64 ScalePath64(PathD path, double scale);public static PathsD ScalePathsD(Paths64 paths, double scale);
  • public static Paths64 Union(Paths64 subject, FillRule fillRule);public static Paths64 Difference(Paths64 subject, Paths64 clip, FillRule fillRule);
  • class Path64 : List<Point64>class Paths64 : List<Path64>struct Point64 { public long X; public long Y; public Point64(long x, long y); ... }
  • enum JoinType { Miter, Square, Bevel, Round }enum EndType { Polygon, Joined, Butt, Square, Round }enum FillRule { EvenOdd, NonZero, Positive, Negative }enum ClipType { NoClip, Intersection, Union, Difference, Xor }
  • 引擎类 Clipper64void AddSubject(Paths64 paths);bool Execute(ClipType clipType, FillRule fillRule, Paths64 solutionClosed);
  • class ClipperOffset { public void AddPath(Path64 path, JoinType joinType, EndType endType); public void AddPaths(Paths64 paths, JoinType joinType, EndType endType); public void Execute(double delta, Paths64 solution); }注意参数顺序:(delta, solution))

Clipper1(znlgis/Clipper1 镜像,v6.4.2,C%23/clipper_library/clipper.cs)

  • 命名空间 ClipperLibusing Path = List<IntPoint>;using Paths = List<List<IntPoint>>;cInt = long
  • 存在public static Paths MinkowskiSum(Path pattern, Path path, bool pathIsClosed);public static Paths MinkowskiSum(Path pattern, Paths paths, bool pathIsClosed);public static Paths MinkowskiDiff(Path poly1, Path poly2);无 isClosed 参数
  • public static IntRect GetBounds(Paths paths);只有 Paths 重载,单路径需包一层 new Paths { path });struct IntRect { public cInt left; public cInt top; public cInt right; public cInt bottom; }struct IntPoint { public cInt X; public cInt Y; }
  • MakePath
  • enum ClipType { ctIntersection, ctUnion, ctDifference, ctXor }enum PolyType { ptSubject, ptClip }enum PolyFillType { pftEvenOdd, pftNonZero, pftPositive, pftNegative }enum JoinType { jtSquare, jtRound, jtMiter }enum EndType { etClosedPolygon, etClosedLine, etOpenButt, etOpenSquare, etOpenRound }
  • Clipper 类:public bool AddPath(Path pg, PolyType polyType, bool Closed);public bool AddPaths(Paths ppg, PolyType polyType, bool closed);public bool Execute(ClipType clipType, Paths solution, PolyFillType FillType = PolyFillType.pftEvenOdd);public bool Execute(ClipType clipType, Paths solution, PolyFillType subjFillType, PolyFillType clipFillType);
  • ClipperOffsetpublic void AddPath(Path path, JoinType joinType, EndType endType);public void AddPaths(Paths paths, JoinType joinType, EndType endType);public void Execute(ref Paths solution, double delta);ref + 参数顺序 (solution, delta)
  • 偏移方向约定:外轮廓需正面积方向(CCW),正 delta 膨胀。

参考实现(任务中嵌入的权威代码)

通用形态学(Clipper2,已按当前 API 编写)

using Clipper2Lib;// 圆盘结构元素(正多边形近似,中心在原点)
public static Path64 MakeDisk(long radius, int edgeCount = 32)
{Path64 disk = new Path64(edgeCount);for (int i = 0; i < edgeCount; i++){double angle = 2 * Math.PI * i / edgeCount;disk.Add(new Point64((long)Math.Round(radius * Math.Cos(angle)),(long)Math.Round(radius * Math.Sin(angle))));}return disk;
}// 补集:包围盒减去 paths
public static Paths64 Complement(Paths64 paths, Rect64 bounds)
{Path64 box = Clipper.MakePath(new long[] {bounds.left, bounds.top,bounds.right, bounds.top,bounds.right, bounds.bottom,bounds.left, bounds.bottom });return Clipper.Difference(new Paths64 { box }, paths, FillRule.NonZero);
}// 结构元素关于原点的反射:B^r = {-b : b in B}
public static Path64 ReflectPath(Path64 pattern)
{Path64 reflected = new Path64(pattern.Count);foreach (Point64 pt in pattern)reflected.Add(new Point64(-pt.X, -pt.Y));return reflected;
}// 形态学膨胀:A ⊕ B = MinkowskiSum(A, B)
public static Paths64 MorphDilate(Path64 shape, Path64 se)=> Clipper.MinkowskiSum(shape, se, true);// 形态学腐蚀:A ⊖ B = (A^c ⊕ B^r)^c
public static Paths64 MorphErode(Path64 shape, Path64 se)
{Rect64 b = Clipper.GetBounds(shape);Rect64 sb = Clipper.GetBounds(se);long w = sb.right - sb.left, h = sb.bottom - sb.top;Rect64 bounds = new Rect64(b.left - w, b.top - h, b.right + w, b.bottom + h);Paths64 comp = Complement(new Paths64 { shape }, bounds);Path64 seReflected = ReflectPath(se);Paths64 grown = new Paths64();foreach (Path64 p in comp)grown.AddRange(Clipper.MinkowskiSum(p, seReflected, true));return Complement(grown, bounds);
}// 开运算:(A ⊖ B) ⊕ B —— 平滑凸角、剔除细小突起、断开窄桥
public static Paths64 MorphOpen(Path64 shape, Path64 se)
{Paths64 eroded = MorphErode(shape, se);Paths64 opened = new Paths64();foreach (Path64 p in eroded)opened.AddRange(Clipper.MinkowskiSum(p, se, true));return Clipper.Union(opened, FillRule.NonZero);
}// 闭运算:(A ⊕ B) ⊖ B —— 平滑凹角、填充细小凹陷、弥合窄缝
public static Paths64 MorphClose(Path64 shape, Path64 se)
{Paths64 dilated = MorphDilate(shape, se);Paths64 closed = new Paths64();foreach (Path64 p in dilated)closed.AddRange(MorphErode(p, se));return Clipper.Union(closed, FillRule.NonZero);
}// 形态学梯度:(A ⊕ B) \ (A ⊖ B) —— 得到边界带
public static Paths64 MorphGradient(Path64 shape, Path64 se)=> Clipper.Difference(MorphDilate(shape, se), MorphErode(shape, se), FillRule.NonZero);

注意事项(写入教程):

  1. 结构元素必须以原点为中心,否则结果整体平移。
  2. 结构元素对称时 Bʳ = B,可直接用原结构元素。
  3. 腐蚀结果可能为空(结构元素比形状大):先判空再继续。
  4. 圆盘结构元素等价于 JoinType.Round 偏移:Clipper.InflatePaths(paths, ±r, JoinType.Round, EndType.Polygon),实现更简单且快——教程应说明:圆盘/方形结构元素优先用偏移,任意形状结构元素才用闵可夫斯基法
  5. 迭代收缩(官方 README 兔子示例模式)每轮调用 Clipper.SimplifyPaths(p, 0.25) 清除碎屑。

偏移版形态学(Clipper2)

// 圆盘结构元素半径 r 的膨胀 / 腐蚀
Paths64 dilated = Clipper.InflatePaths(paths,  r, JoinType.Round, EndType.Polygon);
Paths64 eroded  = Clipper.InflatePaths(paths, -r, JoinType.Round, EndType.Polygon);// 开运算 = 先腐蚀后膨胀;闭运算 = 先膨胀后腐蚀;梯度 = 膨胀 - 腐蚀
Paths64 opened = Clipper.InflatePaths(Clipper.InflatePaths(paths, -r, JoinType.Round, EndType.Polygon),r, JoinType.Round, EndType.Polygon);

Clipper1 版(整数坐标,缩放因子 1e6;无 MakePath、GetBounds 仅 Paths 重载)

using ClipperLib;const double scale = 1e6;// 偏移版膨胀/腐蚀(jtRound = 圆盘结构元素)
ClipperOffset co = new ClipperOffset();
co.AddPaths(paths, JoinType.jtRound, EndType.etClosedPolygon);
Paths dilated = new Paths();
co.Execute(ref dilated, 5.0 * scale);// 补集
public static Paths Complement(Paths paths, IntRect bounds)
{Path box = new Path {new IntPoint(bounds.left, bounds.top),new IntPoint(bounds.right, bounds.top),new IntPoint(bounds.right, bounds.bottom),new IntPoint(bounds.left, bounds.bottom) };Clipper clipper = new Clipper();clipper.AddPath(box, PolyType.ptSubject, true);clipper.AddPaths(paths, PolyType.ptClip, true);Paths result = new Paths();clipper.Execute(ClipType.ctDifference, result,PolyFillType.pftNonZero, PolyFillType.pftNonZero);return result;
}// 任意结构元素腐蚀(补集法)
public static Paths MorphErode(Paths shape, Path se)
{IntRect b = Clipper.GetBounds(shape);IntRect sb = Clipper.GetBounds(new Paths { se });long w = sb.right - sb.left, h = sb.bottom - sb.top;IntRect bounds = new IntRect(b.left - w, b.top - h, b.right + w, b.bottom + h);Paths comp = Complement(shape, bounds);Path seReflected = new Path(se.Count);foreach (IntPoint pt in se)seReflected.Add(new IntPoint(-pt.X, -pt.Y));Paths grown = new Paths();foreach (Path p in comp)grown.AddRange(Clipper.MinkowskiSum(seReflected, p, true));return Complement(grown, bounds);
}

Task 1: sci/Clipper2(第16、18、20章)— 深度实现

Files:

  • Modify: sci/Clipper2/第18章-Minkowski和与差.md
  • Modify: sci/Clipper2/第16章-ClipperOffset偏移类详解.md
  • Modify: sci/Clipper2/第20章-实际应用与最佳实践.md

Interfaces:

  • Consumes: 本计划"已核对 API 参考"(Clipper2 部分)+ "参考实现"。

  • Produces: 修正后的 18.3.1/18.8.3 签名;18.6 完整形态学小节(18.6.3 形态学基础概念 / 18.6.4 形态学膨胀 / 18.6.5 形态学腐蚀(补集法)/ 18.6.6 开运算与闭运算 / 18.6.7 形态学梯度与组合应用);16.13 形态学视角小节;20.6.5 交叉引用段落。

public static PathsD MinkowskiSum(PathD pattern, PathD path, bool isClosed)
{return Minkowski.Sum(pattern, path, isClosed);
}

并加一段说明:当前版本 Clipper 类只保留 3 参包装,内部委托 Minkowski.Sum(PathD 重载带 int decimalPlaces = 2 默认参数);早期版本的 precision 参数已移除。18.8.3 中 Clipper.MinkowskiSum(circleApprox, complexPath, true, 3) 改为 Clipper.MinkowskiSum(circleApprox, complexPath, true)

    • 18.6.3 形态学基础概念:二值形态学定义(膨胀 A ⊕ B = ∪{a+B}、腐蚀 A ⊖ B = {x : x+B ⊆ A})、与闵可夫斯基运算的关系、明确警告:闵可夫斯基差 A ⊕ (−B) 不是腐蚀(它与碰撞检测有关,腐蚀是另一个概念)。
    • 18.6.4 形态学膨胀:MorphDilate(用参考实现),示例(三角形 + 小圆盘)。
    • 18.6.5 形态学腐蚀(补集法):完整推导 A ⊖ B = (Aᶜ ⊕ Bʳ)ᶜ + ComplementReflectPathMorphErode 三个函数(用参考实现),注意事项(结构元素以原点为中心、对称时 Bʳ=B、结果可为空)。
    • 18.6.6 开运算与闭运算:MorphOpen/MorphClose(参考实现)+ 几何效果说明(开:剔突起/断窄桥;闭:填凹陷/弥窄缝)+ 圆盘版等价实现(InflatePaths 先 -r 后 +r / 先 +r 后 -r)。
    • 18.6.7 形态学梯度与组合应用:MorphGradient(参考实现)+ 应用场景(边界带提取、多边形质量检查、地图综合)。

Task 2: sci/Clipper1(第17、19、20章)— 深度实现

Files:

  • Modify: sci/Clipper1/第17章-ClipperOffset详解.md
  • Modify: sci/Clipper1/第19章-辅助函数与工具.md
  • Modify: sci/Clipper1/第20章-实际应用与最佳实践.md

Interfaces:

  • Consumes: 本计划"已核对 API 参考"(Clipper1 部分)+ Clipper1 版参考实现。

  • Produces: 17.12 形态学视角小节;19.x 形态学辅助函数小节(补集法腐蚀);20.4.2 闭运算框架 + 20.x 形态学应用小节。

Task 3: cad/Clipper2(第01、04、05章)— 深度实现

Files:

  • Modify: cad/Clipper2/第01章-Clipper2概述与安装.md
  • Modify: cad/Clipper2/第04章-多边形偏移操作.md
  • Modify: cad/Clipper2/第05章-矩形裁剪与闵可夫斯基操作.md

Interfaces:

  • Consumes: Clipper2 API 参考 + Clipper2 参考实现。

  • Produces: 第01章三角剖分警告;第04章 4.11 形态学操作小节;第05章 5.3.7 C++ 转 C#、全章 API 统一、5.3.10 形态学小节。

Task 4: cad/Clipper1(第01、04、06章)— 轻量实现

Files:

  • Modify: cad/Clipper1/第01章-Clipper1概述与安装.md
  • Modify: cad/Clipper1/第04章-多边形偏移操作.md
  • Modify: cad/Clipper1/第06章-实际应用案例与最佳实践.md

Interfaces:

  • Consumes: Clipper1 API 参考 + Clipper1 版参考实现。

  • Produces: 第01章仓库信息段落;第04章 4.8 形态学操作小节(编号顺延,小结/练习题随动);第06章交叉引用段落。

Task 5: 全局验证与提交(编排者执行)


自审结论

  • Spec 覆盖:设计文档"各文件改动明细"每条均有对应 Task 步骤;仓库事实同步(第01章信息、三角剖分警告)已入 Task 3/4;API 核对已前置完成并写入 Global Constraints。
  • 占位符扫描:无 TBD/TODO;所有插入内容均给出权威参考实现代码。
  • 一致性:Clipper2/Clipper1 两套 API 参考分别标注;腐蚀公式全篇统一为补集法;开/闭运算方向统一(开=先腐后胀,闭=先胀后腐)。
← 返回列表