简介:scikit-geometry 是一套基于成熟计算几何库 CGAL 的 Python 封装,面向需要计算几何、地理信息、计算机图形学与机器人路径规划等领域的 Python 开发者与算法研究人员。该库以 skgeom 模块为核心,提供 Point2、Segment2、Polygon、Plane3、Polyhedron3 等基础几何类型,并支持线段相交测试、凸包、多边形集合运算、可见性计算、Delaunay 三角剖分等常用算法,使研究者无需直接接触 C++ 即可快速完成几何建模与算法验证。资源包内含 61 个文件,整体仅 273KB,结构紧凑但体系完整:18 个 C++ 源文件承担底层算法封装,12 个 Python 模块提供调用接口,10 个 Jupyter Notebook 示例便于交互式学习,另有测试用例、构建配置与 API 文档工程文件,覆盖从源码阅读到二次开发的完整链路。目前已有 2121 人学习,适合正在入门计算几何、或希望借助现成几何算法快速搭建原型项目的开发者深度参考。 做计算几何相关的开发,最烦的不是算法本身,而是圆、多边形、线段、三角剖分这些基础几何对象在代码里没有一个统一又好用的归宿。我以前长期在Shapely和CGAL的C++接口之间来回折腾,直到接触到scikit-geometry这个Python几何算法库,很多以前绕远路才能完成的事情,十几行代码就解决了。这篇不是模块说明书,而是一次完整复盘:从项目定位、环境准备、核心功能到踩坑记录,把我实际项目里使用scikit-geometry的经验一次性讲清楚,给正在做几何计算选型的朋友一点参考。
1. scikit-geometry到底是什么
1.1 一句话定位:CGAL的Python门面
scikit-geometry并不是从零写一套几何算法,它更像一个封装层,把计算几何领域的重型武器CGAL(Computational Geometry Algorithms Library)包装成Python风格的API,底层还借用了OpenCV的部分图像处理能力。因此你拿到的是:经过工业界几十年验证的算法实现,加上Python相对友好的调用体验。这个组合让它天然适合做二维多边形布尔运算、凸包、Delaunay三角剖分、Voronoi图、多边形的缓冲和偏移、点定位,以及三维网格的基础操作。
一开始我以为它跟Shapely差不多,顶多是“另一个几何库”而已,但真正跑起来才发现定位完全不一样。Shapely的抽象建立在图斑和空间关系上,scikit-geometry的抽象则更贴近计算几何本身:顶点、半边、面、Nef多边形、三角剖分结构。这意味着如果你要做的是算法级操作,而不是单纯的空间数据查询,它给你的东西要底层得多,也强大得多。
1.2 和Shapely到底差在哪
很多人第一反应是:Python几何库不是有Shapely吗?确实,Shapely在GIS领域几乎是事实标准,它的核心是GEOS,主要面向二维空间关系与拓扑运算。如果只是判断点是否在多边形内、算两个面相交面积,Shapely轻量好用;一旦涉及更复杂的计算几何问题,比如带洞多边形的布尔差、点的三角剖分、三维网格的遍历和编辑,Shapely就力不从心了。
scikit-geometry恰好补上这些能力。因为底层是C++的CGAL,算法性能明显更好,而且在处理浮点误差时采用了精确内核,不会出现两个多边形明明相切却因为误差导致拓扑判断出错的情况。当然,代价是安装相对麻烦,接口也没有Shapely那么“群众路线”。两者更像是互补关系,而不是替代关系,我现在的项目里两个库经常混着用。
1.3 典型场景与适合人群
从我自己的经验看,以下场景特别适合用scikit-geometry:地理信息里的地块合并与面积统计、机器人路径规划中的碰撞区域合并、点云处理中的凸包提取、CAD简化中的多边形偏移,以及做算法验证时临时需要Voronoi图又不想从头写。
适合人群也很明确:GIS开发、计算机图形学方向的学生、做激光雷达点云后处理的工程师,还有任何需要在Python里做严肃几何计算的人。如果你只是偶尔查一下点面关系,Shapely就够了,不必折腾这个库;但如果你发现自己频繁被Shapely的拓扑报错和性能卡住,那scikit-geometry大概率能把你从泥潭里拉出来。
2. 环境安装与第一行代码
2.1 先把系统依赖装好
我在Ubuntu 22.04上安装比较顺利,前提是先把系统级的依赖补齐。scikit-geometry依赖CGAL、Eigen、OpenCV,编译时还需要Boost和GMP/MPFR支持。先跑:
sudo apt-get install libcgal-dev libeigen3-dev libopencv-dev如果后面编译报缺Boost或GMP,再补上对应开发包。然后直接:
pip install scikit-geometry如果运气好,pip会直接拉取预编译的wheel包,几分钟装完。如果走到源码编译,一般是Python版本或平台不匹配,那就还要确保环境里有CMake和较新的C++编译器。
2.2 Windows与macOS的差异化处理
Windows上我折腾过两次,每次都卡在编译环节。后来学乖了:直接用WSL装Ubuntu,再走上面的流程,省掉一堆MSVC和CGAL版本匹配问题。如果你一定要在原生Windows环境用,可以试conda-forge源:
conda install -c conda-forge scikit-geometrymacOS用户则建议先通过Homebrew安装依赖:
brew install cgal eigen opencv之后再pip install。总体来说,这个库对Linux环境的支持最友好,这也是它在我这里成为“重活专用库”的原因。如果你团队里有人用Windows原生环境,要做好帮他解决编译问题的心理准备。
2.3 安装后的冒烟测试
装完之后,不要直接开始写业务代码,先做一次最简单的冒烟测试:
import skgeom as sg p = sg.Point2(1.0, 2.0) print(p.x(), p.y())这个能跑通,说明基础绑定没问题。接着可以打印一下库的顶层接口,确认当前版本里有没有你要用的功能:
names = [name for name in dir(sg) if not name.startswith("_")] print(names)不同版本的API会有差异,提前看一眼能避免很多“AttributeError”的惊吓。我遇到过好几个朋友直接照老版本的教程写,结果新版里函数挪了模块,排查半天才发现是版本问题。
3. 核心功能实操与底层原理
3.1 构造多边形与读取基础属性
Polygon是scikit-geometry里出场率最高的类。构造方式很直接,按顺序传入Point2列表即可。注意顶点的顺序决定了多边形是逆时针还是顺时针,这会影响面积的符号和后续布尔运算的方向判断。
poly = sg.Polygon([ sg.Point2(0, 0), sg.Point2(4, 0), sg.Point2(4, 3), sg.Point2(0, 3), ]) # 常见的基础属性,具体是属性还是方法建议在交互环境里确认一下 print(poly.area())提示:多边形的顶点顺序很重要。逆时针为正向,signed area为正;顺时针会得到负面积。虽然很多算法不care方向,但布尔运算和偏移可能会受影响。
这里最值钱的地方是底层用了精确内核,不是普通浮点运算,在处理大坐标系时不容易积累误差。这一点在多边形相交、差集这种拓扑运算里尤其关键。
3.2 带洞多边形与拓扑组合
真实的地理数据和CAD数据里,带洞多边形太常见了。scikit-geometry的Polygon本身是简单多边形,没有直接暴露“外环带内洞”的构造语法,但通过布尔运算可以组合出带洞区域。我在做老旧小区地块合并时遇到一个地块内部有公共绿地,本质是外环加一个内洞,用带洞概念思考问题、用布尔差集去实现,比自己拆分要省心太多。
这个“理解带洞区域但用简单多边形实现”的设计在工程上很实用,因为不用为每种特殊情况发明新API。对新手来说,一开始容易绕晕,熟悉之后就明白CGAL这种设计是有道理的:所有复杂区域都是简单多边形按照几何代数关系运算出来的。
3.3 布尔运算背后的Nef机制
布尔运算是这个库的招牌功能。它基于CGAL的Nef多边形实现,和普通多边形布尔运算的最大区别是支持非正则化运算:能正确处理边与边重叠、退化成点线这些边界情况,不会出现“两个多边形明明只碰了一个点,结果程序崩了”的尴尬。
调用方式比较直观,下面是一个差集的例子:
# 不同版本中,布尔运算可能在 BooleanOperations 子模块下 from skgeom import BooleanOperations a = sg.Polygon([ sg.Point2(0, 0), sg.Point2(4, 0), sg.Point2(4, 3), sg.Point2(0, 3), ]) b = sg.Polygon([ sg.Point2(2, 1), sg.Point2(6, 1), sg.Point2(6, 5), sg.Point2(2, 5), ]) diff = BooleanOperations.difference(a, b)如果你打开源码就会发现,它本质上把多边形转成Nef多边形,计算完成再转回来。理解这一点对排查问题很有帮助:如果结果不符合预期,优先怀疑输入多边形是否规范。自相交多边形在Nef运算里会自动正则化,输出可能不是你直觉上认为的形状。
3.4 凸包与Delaunay三角剖分
凸包是计算几何里最常见的基础算法,CGAL的凸包实现复杂度是O(n log n)。scikit-geometry里调用很简洁:
pts = [ sg.Point2(0, 0), sg.Point2(1, 2), sg.Point2(2, 0), sg.Point2(1, 1), sg.Point2(3, 1), ] hull = sg.convex_hull(pts)Delaunay三角剖分则常用于点云重建和网格生成。它有一个非常重要的性质:所有三角形的最小角最大化。也就是说,它不会产生极窄的三角形,这是很多后续算法的理想前置条件。使用方式大致是:
dt = sg.delaunay(pts)剖分结果内部是一个复杂的数据结构,遍历三角形时需要拿到face对应的vertex,再组成坐标三元组。第一次用的人经常在这里绕晕。我的建议是:先关注“点数”和“三角形个数”是否符合预期,再逐步做可视化验证,不要一上来就试图理解全部拓扑结构。
3.5 三维网格的底子:SurfaceMesh
3D部分是scikit-geometry区别于多数Python几何库的地方,它提供了SurfaceMesh来承载带拓扑关系的三角网格。和普通三角形顶点数组不同,SurfaceMesh保存了顶点、半边和面的连接关系,做网格简化、区域增长、拓扑修改都会方便很多。
不过说实话,3D部分的Python API相对2D部分还粗糙一些。如果你只是做简单的网格显示,Open3D会更顺手;但要做拓扑层面的算法实验,scikit-geometry的网格结构值得研究。我在做一个碰撞检测预处理的demo时,就是用它把一组三角网格做合并,再去重顶点,确实省了不少事。
4. 实战案例:批量合并地块多边形并清理重叠
4.1 需求与坑点
我实际遇到过一个需求:几十个相邻的地块多边形,有些互相重叠,有些共边,需要把它们合并成若干个不重叠的区域,并统计合并后的总面积。用Shapely也能做,但数据量到了几千个多边形时,性能差距就出来了,个别极窄的共边情况还会让Shapely产生无效几何。
这个需求的核心难点不在“合并”本身,而在“边界的处理”。有的地块只共享一条边界,有的地块重叠了一点点,有的地块被另一个完全包含。如果用简单的串行合并逻辑,处理不好就会出现重复边或小碎面。
4.2 合并策略与代码骨架
我的策略是维护一个“当前结果集合”,对每个新多边形,如果它和结果集合里的多边形相交,就先做差集去掉重叠部分,再跟并集融合。代码骨架类似这样:
import skgeom as sg from skgeom import BooleanOperations def merge_polygons(polygons): result = [] for p in polygons: if not p.is_simple(): continue acc = p kept = [] for r in result: inter = BooleanOperations.intersection(acc, r) # 空判断需要根据API返回值调整,有的版本返回空多边形,有的返回None if inter is not None and not inter.is_empty(): acc = BooleanOperations.union(acc, r) else: kept.append(r) kept.append(acc) result = kept return result注意:共边和共点也属于“相交”的一种。如果你的业务里允许这种接触,需要在判断时额外处理,否则会把本不该合到一起的地块强行合并了。
4.3 面积统计与精度收益
合并完成后,用sum(p.area() for p in result)计算总面积。我在实际数据上对比过:用CGAL后端的scikit-geometry算出的总面积,比Shapely在多边形自相交边界上“能算但会告警”的结果更稳定。这个稳定不是玄学,而是精确内核把计算中遇到的数值误差都用有理数表示绕开了。
代价是极少数大数运算时内存和CPU会略高。有一次我处理一个超大坐标系的建筑轮廓,单个坐标达到了百万级别,计算耗时确实比Shapely高一些,但结果可信度高,几乎没有出现“面与面之间有了奇怪的缝隙”这种问题。
4.4 性能取舍经验
这里也提醒一下:如果只是两个多边形求个交集,直接用CGAL重武器有点浪费,Shapely更轻更快。我现在的习惯是:轻量查询用Shapely,批量合并、三角剖分、凸包这类算法密集任务交给scikit-geometry。
另外,如果要对一万个多边形两两求交,不管什么库都扛不住。一定要先用空间索引做粗筛,只对可能相交的候选对做精确布尔运算。scikit-geometry本身不提供索引结构,但可以配合R-tree库或GeoPandas的Spatial Index使用,把候选集缩小一个数量级,性能问题就解决一大半。
5. 常见问题与排查技巧
5.1 导入报共享库缺失
在Linux上常见的问题是:
ImportError: libCGAL.so: cannot open shared object file这个一般是系统里装了CGAL但路径没被找到,可以临时设置LD_LIBRARY_PATH指向CGAL的lib目录,或者干脆重新跑一遍pip install scikit-geometry,让它在编译时把依赖路径绑定好。千万别只是把libCGAL.so拷到/usr/lib,容易和其他版本冲突,后面会有更诡异的报错。
5.2 自相交多边形的“正则化”陷阱
输入多边形如果有自相交,布尔运算和面积计算都会出现不符合预期的结果。CGAL在算法层面能处理这类输入,但它倾向于先做“正则化”,把自相交部分拆掉,输出的是正则化后的结果。这和我们常说的“原样修复自相交多边形”不是一回事。
我的经验是:在进入任何几何计算之前,先调用is_simple()做一次检查,发现不简单就修复或过滤,而不是依赖下游算法去兜底。这个习惯帮我避免了很多莫名其妙的面积负数和边界错乱问题。
5.3 大批量计算慢怎么办
除了前面说的空间索引粗筛之外,还有一个容易被忽略的点:选择合适的内核。scikit-geometry默认使用精确构造的CGAL内核,对绝大多数场景是好事,但当数据量特别大且对精度要求没有那么苛刻时,可以尝试切换到只做精确谓词、构造用浮点的内核,性能会明显提升。不过这需要你对计算结果的容错有把握,我不敢无脑推荐,建议先做小规模验证再切换。
5.4 与GeoJSON和Shapely的数据互转
Shapely的坐标是(lon, lat)浮点对,scikit-geometry是Point2对象,转换时最方便的方式是用列表推导:
coords = [(pt.x(), pt.y()) for pt in polygon.vertices]反过来构造时也类似。注意GeoJSON的坐标顺序是经度在前、纬度在后,别在转换时把经纬度调了,这种错位在几何计算里最难查,因为你画图看是正常的,但算法会认为你在南半球。
5.5 两个库混用的最佳姿势
我在项目里经常是两者混用:Shapely负责和PostGIS、GeoPandas对接,做数据查询和展示;需要算法型计算时,把坐标传到scikit-geometry算完,再转回Shapely对象输出。
刚开始时我担心这种混用会有数据一致性问题,实际用了大半年,只要做好坐标格式转换,没有遇到问题。对团队来说,也不需要强制所有人都学CGAL概念,只需要在算法密集模块里封装一层几何计算服务,对上层透明即可。
6. 写在最后:我的一些体会
6.1 这个库最打动我的地方
用了大半年scikit-geometry,最大的感受是:计算几何的算法是真的难实现,但有了CGAL这样的工业级库,调用起来又真的可以变得很简单。精确内核带来的稳定性是我最看重的,以前用浮点几何库,时不时会因为坐标尺度问题出现“把两个相邻多边形算成重叠”的事情,排查起来极其痛苦。scikit-geometry至少在算法正确性上给了我足够的信心。
还有一个容易忽略的好处:它其实是学习CGAL概念的绝佳入口。很多同学看到CGAL的C++模板代码就头大,但在scikit-geometry里,你可以用Python直观地操作Nef多边形、Delaunay剖分、SurfaceMesh这些数据结构,把概念吃透之后再去读C++代码,难度低了很多。
6.2 什么时候不要用它
如果你想用它替代Shapely处理日常GIS查询,我不建议。scikit-geometry的对象模型并不是空间索引友好的,也没有GeoPandas那种现成的数据框集成。如果只是做轻量Web服务的空间查询,Shapely加PostGIS的组合更合适。它也不适合做实时三维渲染,SurfaceMesh虽然带拓扑,但并不是为性能渲染设计的。
最后分享一个小经验:不要等出bug了才去研究底层Nef多边形和精确内核。花一个下午把CGAL官网的核心概念过一遍,再回来用scikit-geometry,你会发现API变得顺眼很多,踩坑概率也低非常多。这个库不完美,但在“严肃几何计算”这个细分赛道上,目前Python生态里我没有找到比它更省心的替代品。
本文还有配套的精品资源,点击获取