Import Surface data from Pyvista (Vertices, Face)

I converted point data (arr_3d, Dim:5121283) to the surface using Pyvista libraries:

cloud = pv.PolyData(arr_3d)   
surf = cloud.delaunay_2d()

verts = surf.points  # Dim: (65536, 3)
faces = surf.faces  #Dim: (522924,)

But based on the Napari document, argument faces is an (M, 3) array of int of indices of the mesh triangles.

Is there any pythonic way to convert the face data generated by PyVista to Napari’s add_surface ((vertices, faces, values)) function?
I used the below code, but it connects points from different sides which is not correct.

Hi @Mas_Developer.

PyVista faces are defined according to [cell0_nverts, cell0_v0, cell0_v1, cell0_v2, cell1_nverts, ...] see more info here https://github.com/pyvista/pyvista-support/issues/26#issuecomment-511481860.

As you note napari above is expecting faces in an Nx3 array.

As cells in pyvista can have any number of vertices we should call triangulate on the surface if is_all_triangles is False, and then we know that nverts for each cell will be 3. We can then do a simple reshape operation and drop the first column (which will be all 3) to get the indices of the vertices in the format we need.

As an additional note we can use the point_normals to get nice values for the vertices to improve the shading. We should probably improve napari to calculate these normals under-the-hood if not provided, but as you have them now, you might as well use them! (projected onto some “light source” like vector).

Here’s a worked example showing the airplane from pyvista.

"""
Display a pyvista surface
"""

import pyvista as pv
from pyvista import examples
import numpy as np
import napari

# Load the surface and triangulate just in case
surf = pv.read(examples.planefile).triangulate()

# Convert the data to a simple numpy representation
# that napari understands
vertices = np.asarray(surf.points)
faces = np.asarray(surf.faces).reshape((-1, 4))[:, 1:]
normals = np.asarray(surf.point_normals)
# generate values by projecting on a "lighting vector"
values = np.dot(normals, [1, -1, 1])
print(vertices.shape, faces.shape, values.shape)
# (1335, 3) (2452, 3) (1335,)

with napari.gui_qt():
    # create an empty viewer
    viewer = napari.Viewer()

    # add the surface
    viewer.add_surface((vertices, faces, values))

    # turn on 3D rendering
    viewer.dims.ndisplay = 3

The surface layer of napari might need some additional love, so do let us know if you find things not working or not ideal, but hopefully this gets you going a bit further! This could also be a fun topic to write up on our tutorials repo!!

We could also make a pyvista/ meshio fileIO plugin that did this conversion automatically so you could drag and drop your surface data into the napari viewer and see it like this right away :slight_smile:

3 Likes