GNU General Public License v3.0

技术解读

以下内容由 GitRead 中文社区 编辑团队基于项目 README 与官方资料整理, 结合中文开发者的常见使用场景补充技术选型建议。具体以项目官方文档为准。

pygalmesh:CGAL 网格划分工具的 Python 接口

项目背景与痛点

在计算几何领域,网格生成技术一直是一个核心问题。无论是生物医学成像、流体力学模拟还是3D打印,都需要高效且准确的网格划分来保证模拟的精度和效果。然而,网格生成本身是一个极其复杂的过程,常涉及几何学、拓扑学以及数值分析等多种学科知识。过往,开发者通常依赖于借助复杂的 C++ API 来实现网格生成,这不仅增加了代码量,同时在进行复杂编程时也缺乏灵活性和便利性。

为此,meshpro/pygalmesh 项目提供了一个 Python 接口,服务于 CGAL (Computational Geometry Algorithms Library) 的网格划分工具。通过这种方式,开发者可以更加直观、便捷地构建复杂的几何模型,同时结合 Python 生态中丰富的数据处理工具,极大的提升了计算效率和研发速度。

核心技术揭秘

从技术的角度来看,pygalmesh 的实现相对较复杂,主要依托于 Python 对 C++ 库进行绑定,同时使用了 SWIG (Simplified Wrapper and Interface Generator) 工具来实现这种绑定。SWIG 提供了一种相对直观的方法来在 Python 中调用 C++ 代码,从而避免了手动创建繁琐的接口函数。此外,CGAL 提供了一系列经过优化的网格生成算法,如 Delaunay 三角化、曼哈顿距离最小生成树等。

要理解其内部实现原理,我们可以通过一个简单的 Mermaid 数据流图来进行分析:

graph TD A[Python 脚本] --> B[SWIG 绑定层] B --> C[CGAL 网格生成器] C --> D[几何模型生成] D --> E[Python 对象] E --> F[返回 Python]

实际上,代码的运行流程主要是:

  1. 用户通过 Python 脚本调用外部函数。
  2. 通过 SWIG 生成的 Python 绑定层,调用底层 C++ 代码。
  3. CGAL 使用高效的网格生成算法生成几何模型。
  4. 通过绑定层,将几何模型转化为 Python 对象。
  5. Python 对象被返回至用户脚本以供使用。

功能亮点与差异

pygalmesh 的主要亮点在于它简化了 C++ 丰富的网格生成功能到 Python 的无缝衔。与纯粹的 C++ 接口相比,pygalmesh 提供了一个更加友好且易用的编程界面。同时,它在计算复杂度和性能上有显著优势,自带的一系列优化使得网格划分更加高效稳定。

与其他 Python 包(如 scipy.spatial 提供的 Delaunay 三角化)相比,pygalmesh借助 CGAL 的核心算法,提供了更多种类、更高级的网格生成方式,如曲面网格划分、奇异网格处理等,这是单一算法库难以比拟的。

应用场景与落地建议

pygalmesh 主要应用于对计算几何有较多需求的领域,例如 CAD 设计、虚拟现实、以及利用三角剖分进行数据拟合等场景。在使用该项目进行生产部署时,需要注意以下几点:

  1. 数据预处理:不要忽略数据预处理的重要性,尤其是对于复杂的几何模型。数据预处理不当可能会带来不必要的网格量,进而导致计算效率低下。
  2. 缓存机制:合理利用缓存机制,对于同一个模型多次调用相同参数的网格生成时,避免不必要的重复计算。
  3. 性能监控:要注重性能监控和异常处理,方便及时发现并解决运算中的错误。

综合评价

pygalmesh作为 CGAL 库的 Python 接口,展示出了强大的计算几何能力,尤其是在复杂的网格划分方面。其易用性与强大的几何处理能力使其成为一项有价值的技术工具。然而,该项目在灵活性和自定义选项方面尚未达到极致,这意味着对于特定定制需求,开发者仍需深入底层的 C++ 库。理论上来说,随着项目的发展与完善,这些可能会在未来得到改善。

关于

GitHub 原始描述(英文)

:spider_web: A Python interface to CGAL's meshing tools

描述取自 GitHub 官方 API,保持仓库作者书写的原文。

简要分析

热度分
824
价值分
421
活跃状态
活跃

语言占比

C++
CMake
Meson
Python

Release

README

pygalmesh

Create high-quality meshes with ease.

PyPi Version Anaconda Cloud Packaging status PyPI pyversions DOI GitHub stars Downloads

Discord

gh-actions codecov LGTM Code style: black

pygalmesh is a Python frontend to CGAL’s 2D and 3D mesh generation capabilities. pygalmesh makes it easy to create high-quality 2D, 3D volume meshes, periodic volume meshes, and surface meshes.

Examples

2D meshes

CGAL generates 2D meshes from linear constraints.

import numpy as np
import pygalmesh

points = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]])
constraints = [[0, 1], [1, 2], [2, 3], [3, 0]]

mesh = pygalmesh.generate_2d(
    points,
    constraints,
    max_edge_size=1.0e-1,
    num_lloyd_steps=10,
)
# mesh.points, mesh.cells

The quality of the mesh isn’t very good, but can be improved with optimesh.

A simple ball
import pygalmesh

s = pygalmesh.Ball([0, 0, 0], 1.0)
mesh = pygalmesh.generate_mesh(s, max_cell_circumradius=0.2)

# mesh.points, mesh.cells, ...

You can write the mesh with

mesh.write("out.vtk")

You can use any format supported by meshio.

The mesh generation comes with many more options, described here. Try, for example,

mesh = pygalmesh.generate_mesh(
    s, max_cell_circumradius=0.2, odt=True, lloyd=True, verbose=False
)
Other primitive shapes

pygalmesh provides out-of-the-box support for balls, cuboids, ellipsoids, tori, cones, cylinders, and tetrahedra. Try for example

import pygalmesh

s0 = pygalmesh.Tetrahedron(
    [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]
)
mesh = pygalmesh.generate_mesh(
    s0,
    max_cell_circumradius=0.1,
    max_edge_size_at_feature_edges=0.1,
)
Domain combinations

Supported are unions, intersections, and differences of all domains. As mentioned above, however, the sharp intersections between two domains are not automatically handled. Try for example

import pygalmesh

radius = 1.0
displacement = 0.5
s0 = pygalmesh.Ball([displacement, 0, 0], radius)
s1 = pygalmesh.Ball([-displacement, 0, 0], radius)
u = pygalmesh.Difference(s0, s1)

To sharpen the intersection circle, add it as a feature edge polygon line, e.g.,

import numpy as np
import pygalmesh

radius = 1.0
displacement = 0.5
s0 = pygalmesh.Ball([displacement, 0, 0], radius)
s1 = pygalmesh.Ball([-displacement, 0, 0], radius)
u = pygalmesh.Difference(s0, s1)

# add circle
a = np.sqrt(radius**2 - displacement**2)
max_edge_size_at_feature_edges = 0.15
n = int(2 * np.pi * a / max_edge_size_at_feature_edges)
alpha = np.linspace(0.0, 2 * np.pi, n + 1)
alpha[-1] = alpha[0]
circ = a * np.column_stack([np.zeros(n + 1), np.cos(alpha), np.sin(alpha)])

mesh = pygalmesh.generate_mesh(
    u,
    extra_feature_edges=[circ],
    max_cell_circumradius=0.15,
    max_edge_size_at_feature_edges=max_edge_size_at_feature_edges,
    min_facet_angle=25,
    max_radius_surface_delaunay_ball=0.15,
    max_circumradius_edge_ratio=2.0,
)

Note that the length of the polygon legs are kept in sync with max_edge_size_at_feature_edges of the mesh generation. This makes sure that it fits in nicely with the rest of the mesh.

Domain deformations

You can of course translate, rotate, scale, and stretch any domain. Try, for example,

import pygalmesh

s = pygalmesh.Stretch(pygalmesh.Ball([0, 0, 0], 1.0), [1.0, 2.0, 0.0])

mesh = pygalmesh.generate_mesh(s, max_cell_circumradius=0.1)
Extrusion of 2D polygons

pygalmesh lets you extrude any polygon into a 3D body. It even supports rotation alongside!

import pygalmesh

p = pygalmesh.Polygon2D([[-0.5, -0.3], [0.5, -0.3], [0.0, 0.5]])
max_edge_size_at_feature_edges = 0.1
domain = pygalmesh.Extrude(
    p,
    [0.0, 0.0, 1.0],
    0.5 * 3.14159265359,
    max_edge_size_at_feature_edges,
)
mesh = pygalmesh.generate_mesh(
    domain,
    max_cell_circumradius=0.1,
    max_edge_size_at_feature_edges=max_edge_size_at_feature_edges,
    verbose=False,
)

Feature edges are automatically preserved here, which is why an edge length needs to be given to pygalmesh.Extrude.

Rotation bodies

Polygons in the x-z-plane can also be rotated around the z-axis to yield a rotation body.

import pygalmesh

p = pygalmesh.Polygon2D([[0.5, -0.3], [1.5, -0.3], [1.0, 0.5]])
max_edge_size_at_feature_edges = 0.1
domain = pygalmesh.RingExtrude(p, max_edge_size_at_feature_edges)
mesh = pygalmesh.generate_mesh(
    domain,
    max_cell_circumradius=0.1,
    max_edge_size_at_feature_edges=max_edge_size_at_feature_edges,
    verbose=False,
)
Your own custom level set function

If all of the variety is not enough for you, you can define your own custom level set function. You simply need to subclass pygalmesh.DomainBase and specify a function, e.g.,

import pygalmesh


class Heart(pygalmesh.DomainBase):
    def __init__(self):
        super().__init__()

    def eval(self, x):
        return (
            (x[0] ** 2 + 9.0 / 4.0 * x[1] ** 2 + x[2] ** 2 - 1) ** 3
            - x[0] ** 2 * x[2] ** 3
            - 9.0 / 80.0 * x[1] ** 2 * x[2] ** 3
        )

    def get_bounding_sphere_squared_radius(self):
        return 10.0


d = Heart()
mesh = pygalmesh.generate_mesh(d, max_cell_circumradius=0.1)

Note that you need to specify the square of a bounding sphere radius, used as an input to CGAL’s mesh generator.

Local refinement

Use generate_mesh with a function (regular or lambda) as max_cell_circumradius. The same goes for max_edge_size_at_feature_edges, max_radius_surface_delaunay_ball, and max_facet_distance.

import numpy as np
import pygalmesh

mesh = pygalmesh.generate_mesh(
    pygalmesh.Ball([0.0, 0.0, 0.0], 1.0),
    min_facet_angle=30.0,
    max_radius_surface_delaunay_ball=0.1,
    max_facet_distance=0.025,
    max_circumradius_edge_ratio=2.0,
    max_cell_circumradius=lambda x: abs(np.sqrt(np.dot(x, x)) - 0.5) / 5 + 0.025,
)
Surface meshes

If you’re only after the surface of a body, pygalmesh has generate_surface_mesh for you. It offers fewer options (obviously, max_cell_circumradius is gone), but otherwise works the same way:

import pygalmesh

s = pygalmesh.Ball([0, 0, 0], 1.0)
mesh = pygalmesh.generate_surface_mesh(
    s,
    min_facet_angle=30.0,
    max_radius_surface_delaunay_ball=0.1,
    max_facet_distance=0.1,
)

Refer to CGAL’s documentation for the options.

Periodic volume meshes

pygalmesh also interfaces CGAL’s 3D periodic mesh generation. Besides a domain, one needs to specify a bounding box, and optionally the number of copies in the output (1, 2, 4, or 8). Example:

import numpy as np
import pygalmesh


class Schwarz(pygalmesh.DomainBase):
    def __init__(self):
        super().__init__()

    def eval(self, x):
        x2 = np.cos(x[0] * 2 * np.pi)
        y2 = np.cos(x[1] * 2 * np.pi)
        z2 = np.cos(x[2] * 2 * np.pi)
        return x2 + y2 + z2


mesh = pygalmesh.generate_periodic_mesh(
    Schwarz(),
    [0, 0, 0, 1, 1, 1],
    max_cell_circumradius=0.05,
    min_facet_angle=30,
    max_radius_surface_delaunay_ball=0.05,
    max_facet_distance=0.025,
    max_circumradius_edge_ratio=2.0,
    number_of_copies_in_output=4,
    # odt=True,
    # lloyd=True,
    verbose=False,
)
Volume meshes from surface meshes

If you have a surface mesh at hand (like elephant.vtu), pygalmesh generates a volume mesh on the command line via

pygalmesh volume-from-surface elephant.vtu out.vtk --cell-size 1.0 --odt

(See pygalmesh volume-from-surface -h for all options.)

In Python, do

import pygalmesh

mesh = pygalmesh.generate_volume_mesh_from_surface_mesh(
    "elephant.vtu",
    min_facet_angle=25.0,
    max_radius_surface_delaunay_ball=0.15,
    max_facet_distance=0.008,
    max_circumradius_edge_ratio=3.0,
    verbose=False,
)
Meshes from INR voxel files

It is also possible to generate meshes from INR voxel files, e.g., here either on the command line

pygalmesh from-inr skull_2.9.inr out.vtu --cell-size 5.0 --odt

(see pygalmesh from-inr -h for all options) or from Python

import pygalmesh

mesh = pygalmesh.generate_from_inr(
    "skull_2.9.inr",
    max_cell_circumradius=5.0,
    verbose=False,
)
Meshes from numpy arrays representing 3D images

pygalmesh can help generating unstructed meshes from 3D numpy int arrays specifying the subdomains. Subdomains with key 0 are not meshed.

import pygalmesh
import numpy as np

x_ = np.linspace(-1.0, 1.0, 50)
y_ = np.linspace(-1.0, 1.0, 50)
z_ = np.linspace(-1.0, 1.0, 50)
x, y, z = np.meshgrid(x_, y_, z_)

vol = np.empty((50, 50, 50), dtype=np.uint8)
idx = x**2 + y**2 + z**2 < 0.5**2
vol[idx] = 1
vol[~idx] = 0

voxel_size = (0.1, 0.1, 0.1)

mesh = pygalmesh.generate_from_array(
    vol, voxel_size, max_facet_distance=0.2, max_cell_circumradius=0.1
)
mesh.write("ball.vtk")

The code below creates a mesh from the 3D breast phantom from Lou et al available here. The phantom comprises four tissue types (background, fat, fibrograndular, skin, vascular tissues). The generated mesh conforms to tissues interfaces.

import pygalmesh
import meshio

with open("MergedPhantom.DAT", "rb") as fid:
    vol = np.fromfile(fid, dtype=np.uint8)

vol = vol.reshape((722, 411, 284))
voxel_size = (0.2, 0.2, 0.2)

mesh = pygalmesh.generate_from_array(
    vol, voxel_size, max_facet_distance=0.2, max_cell_circumradius=1.0
)
mesh.write("breast.vtk")

In addition, we can specify different mesh sizes for each tissue type. The code below sets the mesh size to 1 mm for the skin tissue (label 4), 0.5 mm for the vascular tissue (label 5), and 2 mm for all other tissues (default).

mesh = pygalmesh.generate_from_array(
    vol,
    voxel_size,
    max_facet_distance=0.2,
    max_cell_circumradius={"default": 2.0, 4: 1.0, 5: 0.5},
)
mesh.write("breast_adapted.vtk")
Surface remeshing

pygalmesh can help remeshing an existing surface mesh, e.g., lion-head.off. On the command line, use

pygalmesh remesh-surface lion-head.off out.vtu -e 0.025 -a 25 -s 0.1 -d 0.001

(see pygalmesh remesh-surface -h for all options) or from Python

import pygalmesh

mesh = pygalmesh.remesh_surface(
    "lion-head.off",
    max_edge_size_at_feature_edges=0.025,
    min_facet_angle=25,
    max_radius_surface_delaunay_ball=0.1,
    max_facet_distance=0.001,
    verbose=False,
)

Installation

For installation, pygalmesh needs CGAL and Eigen installed on your system. They are typically available on your Linux distribution, e.g., on Ubuntu

sudo apt install libcgal-dev libeigen3-dev

On MacOS with homebrew,

brew install cgal eigen

After that, pygalmesh can be installed from the Python Package Index, so with

pip install -U pygalmesh

you can install/upgrade.

Troubleshooting

If pygalmesh fails to build due to fatal error: 'Eigen/Dense' file not found you will need to create a symbolic link for Eigen to be detected, e.g.

cd /usr/local/include
sudo ln -sf eigen3/Eigen Eigen

It’s possible that eigen3 could be in /usr/include instead of /usr/local/install.

Manual installation

For manual installation (if you’re a developer or just really keen on getting the bleeding edge version of pygalmesh), there are two possibilities:

  • Get the sources, type python3 setup.py install. This does the trick most the time.
  • As a fallback, there’s a CMake-based installation. Simply go cmake /path/to/sources/ and make.

Testing

To run the pygalmesh unit tests, check out this repository and type

pytest

Background

CGAL offers two different approaches for mesh generation:

  1. Meshes defined implicitly by level sets of functions.
  2. Meshes defined by a set of bounding planes.

pygalmesh provides a front-end to the first approach, which has the following advantages and disadvantages:

  • All boundary points are guaranteed to be in the level set within any specified residual. This results in smooth curved surfaces.
  • Sharp intersections of subdomains (e.g., in unions or differences of sets) need to be specified manually (via feature edges, see below), which can be tedious.

On the other hand, the bounding-plane approach (realized by mshr), has the following properties:

  • Smooth, curved domains are approximated by a set of bounding planes, resulting in more of less visible edges.
  • Intersections of domains can be computed automatically, so domain unions etc. have sharp edges where they belong.

See here for other mesh generation tools.

License

This software is available under the GPLv3 license as well as a commercial license. If you’d like to use this software commercially, please contact the author.

评论

暂无评论