在 PyVista 中将参数对象绘制为网格

Plotting parametric objects as a grid in PyVista

我可能遇到了一个简单的问题,但在阅读了 pyvista 文档后,我仍在寻找答案。我正在尝试绘制一个网格,其中每个单元格都是一个定义为参数形状的网格,即 supertorus。在pyvista的早期版本中,我定义了"my own" supertorus如下:

def supertorus(yScale, xScale, Height, InternalRadius, Vertical, Horizontal,
           deltaX=0, deltaY=0, deltaZ=0):

#  initial range for values used in parametric equation
n = 100
u = np.linspace(-np.pi, np.pi, n)
t = np.linspace(-np.pi, np.pi, n)
u, t = np.meshgrid(u, t)

# a1: Y Scale <0, 2>
a1 = yScale
# a2: X Scale <0, 2>
a2 = xScale
# a3: Height <0, 5>
a3 = Height
# a4: Internal radius <0, 5>
a4 = InternalRadius
# e1: Vertical squareness <0.25, 1>
e1 = Vertical
# e2: Horizontal squareness <0.25, 1>
e2 = Horizontal

# Definition of parametric equation for supertorus
x = a1 * (a4 + np.sign(np.cos(u)) * np.abs(np.cos(u)) ** e1) *\
    np.sign(np.cos(t)) * np.abs(np.cos(t)) ** e2
y = a2 * (a4 + np.sign(np.cos(u)) * np.abs(np.cos(u)) ** e1) *\
    np.sign(np.sin(t)) * np.abs(np.sin(t)) ** e2
z = a3 * np.sign(np.sin(u)) * np.abs(np.sin(u)) ** e1

grid = pyvista.StructuredGrid(x + deltaX + 5, y + deltaY + 5, z + deltaZ)
return grid 

我可以使用 deltaXdeltaYdeltaZ 将 supertori 定位在我选择的位置。 不幸的是,这种方法效率不高,我打算使用 PyVista 提供的超环形网格 (https://docs.pyvista.org/examples/00-load/create-parametric-geometric-objects.html?highlight=supertoroid)。我的问题是:如何在坐标 xyz?

定义的位置放置多个网格(如 supertori)

我相信您正在寻找的是 glyphs。您可以将自己的数据集作为字形几何图形传递,该几何图形将依次在超级网格的每个点中绘制数据集。无需详细介绍字形的方向、根据标量为它们着色等等,这里有一个简单的 "alien invasion" 场景作为示例:

import numpy as np
import pyvista as pv

# get dataset for the glyphs: supertoroid in xy plane
saucer = pv.ParametricSuperToroid(ringradius=0.5, n2=1.5, zradius=0.5)
saucer.rotate_y(90)
# saucer.plot()  #  <-- check how a single saucer looks like

# get dataset where to put glyphs
x,y,z = np.mgrid[-1:2, -1:2, :2]
mesh = pv.StructuredGrid(x, y, z)

# construct the glyphs on top of the mesh
glyphs = mesh.glyph(geom=saucer, factor=0.3)
# glyphs.plot()  #  <-- simplest way to plot it

# create Plotter and add our glyphs with some nontrivial lighting
plotter = pv.Plotter(window_size=(1000, 800))
plotter.add_mesh(glyphs, color=[0.2, 0.2, 0.2], specular=1, specular_power=15)

plotter.show()

我添加了 some strong specular lighting 以使碟子看起来更具威胁性:

但是您的问题的关键点是通过将超级网格作为 the geom keyword of mesh.glyph 传递来创建字形。 orientscale 等其他关键字对于类似箭头的字形非常有用,您可以在其中使用字形来表示数据集的矢量信息。


您在评论中询问是否可以沿数据集改变字形。我确信这是不可能的,但是 the VTK docs 清楚地提到了定义要使用的字形集合的可能性:

More than one glyph may be used by creating a table of source objects, each defining a different glyph. If a table of glyphs is defined, then the table can be indexed into by using either scalar value or vector magnitude.

事实证明 PyVista 还没有公开此功能,但是基础 vtk 包让我们亲自动手。这是一个概念证明 based on DataSetFilters.glyph,我将由 PyVista 开发人员提供,看看是否有兴趣公开此功能。

import numpy as np
import pyvista as pv
from pyvista.core.filters import _get_output  # just for this standalone example
import vtk
pyvista = pv  # just for this standalone example

# below: adapted from core/filters.py
def multiglyph(dataset, orient=True, scale=True, factor=1.0,
          tolerance=0.0, absolute=False, clamping=False, rng=None,
          geom_datasets=None, geom_values=None):
    """Copy a geometric representation (called a glyph) to every point in the input dataset.
    The glyphs may be oriented along the input vectors, and they may be scaled according to scalar
    data or vector magnitude.
    Parameters
    ----------
    orient : bool
        Use the active vectors array to orient the glyphs
    scale : bool
        Use the active scalars to scale the glyphs
    factor : float
        Scale factor applied to sclaing array
    tolerance : float, optional
        Specify tolerance in terms of fraction of bounding box length.
        Float value is between 0 and 1. Default is 0.0. If ``absolute``
        is ``True`` then the tolerance can be an absolute distance.
    absolute : bool, optional
        Control if ``tolerance`` is an absolute distance or a fraction.
    clamping: bool
        Turn on/off clamping of "scalar" values to range.
    rng: tuple(float), optional
        Set the range of values to be considered by the filter when scalars
        values are provided.
    geom_datasets : tuple(vtk.vtkDataSet), optional
        The geometries to use for the glyphs in table mode
    geom_values : tuple(float), optional
        The value to assign to each geometry dataset, optional
    """
    # Clean the points before glyphing
    small = pyvista.PolyData(dataset.points)
    small.point_arrays.update(dataset.point_arrays)
    dataset = small.clean(point_merging=True, merge_tol=tolerance,
                          lines_to_points=False, polys_to_lines=False,
                          strips_to_polys=False, inplace=False,
                          absolute=absolute)
    # Make glyphing geometry
    if not geom_datasets:
        arrow = vtk.vtkArrowSource()
        arrow.Update()
        geom_datasets = arrow.GetOutput(),
        geom_values = 0,
    # check if the geometry datasets are consistent
    if not len(geom_datasets) == len(geom_values):
        raise ValueError('geom_datasets and geom_values must have the same length!')
        # TODO: other kinds of sanitization, e.g. check for sequences etc.
    # Run the algorithm
    alg = vtk.vtkGlyph3D()
    if len(geom_values) == 1:
        # use a single glyph
        alg.SetSourceData(geom_datasets[0])
    else:
        alg.SetIndexModeToScalar()
        # TODO: index by vectors?
        # TODO: SetInputArrayToProcess for arbitrary arrays, maybe?
        alg.SetRange(min(geom_values), max(geom_values))
        # TODO: different Range?
        for val, geom in zip(geom_values, geom_datasets):
            alg.SetSourceData(val, geom)
    if isinstance(scale, str):
        dataset.active_scalars_name = scale
        scale = True
    if scale:
        if dataset.active_scalars is not None:
            if dataset.active_scalars.ndim > 1:
                alg.SetScaleModeToScaleByVector()
            else:
                alg.SetScaleModeToScaleByScalar()
    else:
        alg.SetScaleModeToDataScalingOff()
    if isinstance(orient, str):
        dataset.active_vectors_name = orient
        orient = True
    if rng is not None:
        alg.SetRange(rng)
    alg.SetOrient(orient)
    alg.SetInputData(dataset)
    alg.SetVectorModeToUseVector()
    alg.SetScaleFactor(factor)
    alg.SetClamping(clamping)
    alg.Update()
    return _get_output(alg)

def example():
    """Small glyph example"""

    rng = np.random.default_rng()

    # get dataset for the glyphs: supertoroid in xy plane
    # use N random kinds of toroids over a mesh with 27 points
    N = 5
    values = np.arange(N)  # values for scalars to look up glyphs by
    geoms = [pv.ParametricSuperToroid(n1=n1, n2=n2) for n1,n2 in rng.uniform(0.5, 2, size=(N, 2))]
    for geom in geoms:
        # make the disks horizontal for aesthetics
        geom.rotate_y(90)

    # get dataset where to put glyphs
    x,y,z = np.mgrid[-1:2, -1:2, -1:2]
    mesh = pv.StructuredGrid(x, y, z)

    # add random scalars
    mesh.point_arrays['scalars'] = rng.integers(0, N, size=x.size)

    # construct the glyphs on top of the mesh; don't scale by scalars now
    glyphs = multiglyph(mesh, geom_datasets=geoms, geom_values=values, scale=False, factor=0.3)

    # create Plotter and add our glyphs with some nontrivial lighting
    plotter = pv.Plotter(window_size=(1000, 800))
    plotter.add_mesh(glyphs, specular=1, specular_power=15)

    plotter.show()

if __name__ == "__main__":
    example()

上面的multiglyph函数与mesh.glyph大部分相同,但我用两个关键字geom_datasets和[=替换了geom关键字23=]。这些定义了一个索引 -> 几何映射,然后用于根据数组标量查找每个字形。

您询问是否可以独立为字形着色:可以。在上面的概念证明中,字形的选择与标量相关(选择向量同样容易;我不太确定任意数组)。然而,当你调用 pv.Plotter.add_mesh 时,你可以很容易地选择要着色的数组,所以我的建议是使用适当标量以外的东西来为你的字形着色。

这是一个典型的输出:

我保留了标量用于着色,以便更容易看出字形之间的差异。您可以看到根据随机标量随机选择了五种不同类型的字形。如果您设置非整数标量,它仍然有效;我怀疑 vtk 选择最接近的标量或类似的东西进行查找。