Python/Numpy equivalent of MATLAB isosurface functions

Question:

Anyone can suggest the equivalent function of MATLAB’s "isosurface" function in python/numpy. The MATLAB isosurface returns the faces and vertices. I need the faces and vertices to create .stl files. MATLAB isosurface function looks like this:

[f,v] = isosurface(X,Y,Z,V,isovalue)

in python I have found this one method in plotly which works as follows:

import plotly.graph_objects as go
import numpy as np

X, Y, Z = np.mgrid[-5:5:40j, -5:5:40j, -5:5:40j]

# ellipsoid
values = X * X * 0.5 + Y * Y + Z * Z * 2

fig = go.Figure(data=go.Isosurface(
    x=X.flatten(),
    y=Y.flatten(),
    z=Z.flatten(),
    value=values.flatten(),
    isomin=10,
    isomax=40,
    caps=dict(x_show=False, y_show=False)
    ))
fig.show()

the problem with this method is that it only plots isosurfaces and doesn’t return faces and vertices as the MATLAB isosurface function does and I need those faces and vertices.

Any help would be greatly appreciated.

Asked By: Daniel Sempere

||

Answers:

Although it wasn’t among your target libraries, PyVista built on VTK can help you do this easily (disclaimer: I’m one of the devs). Since you seemed receptive of a PyVista-based solution in comments, here’s how you’d do it:

  1. define a mesh, generally as a StructuredGrid for your kind of data, although the equidistant grid in your example could even work with a UniformGrid,
  2. compute its isosurfaces with a contour filter,
  3. save as an .stl file using the save method of the grid containing the isosurfaces.
import numpy as np
import pyvista as pv

# generate data grid for computing the values
X, Y, Z = np.mgrid[-5:5:40j, -5:5:40j, -5:5:40j]
values = X**2 * 0.5 + Y**2 + Z**2 * 2

# create a structured grid
# (for this simple example we could've used an unstructured grid too)
# note the fortran-order call to ravel()!
mesh = pv.StructuredGrid(X, Y, Z)
mesh.point_arrays['values'] = values.ravel(order='F')  # also the active scalars

# compute 3 isosurfaces
isos = mesh.contour(isosurfaces=3, rng=[10, 40])
# or: mesh.contour(isosurfaces=np.linspace(10, 40, 3)) etc.

# plot them interactively if you want to
isos.plot(opacity=0.7)

# save to stl
isos.save('isosurfaces.stl')

The interactive plot looks something like this:
3d plot of three ellipsoidal isosurfaces with partial opacity, coloured according to iso values

The colours correspond to the isovalues, picked from the scalars array and indicated by the scalar bar.

If we load back the mesh from file we’ll get the structure, but not the scalars:

loaded = pv.read('isosurfaces.stl')
loaded.plot(opacity=0.7)

same figure when loaded back from .stl file, but everything is white and there is no scalar bar

The reason why the scalars are missing is that data arrays can’t be exported to .stl files:

>>> isos  # original isosurface mesh
PolyData (0x7fa7245a2220)
  N Cells:  26664
  N Points: 13656
  X Bounds: -4.470e+00, 4.470e+00
  Y Bounds: -5.000e+00, 5.000e+00
  Z Bounds: -5.000e+00, 5.000e+00
  N Arrays: 3

>>> isos.point_arrays
pyvista DataSetAttributes
Association: POINT
Contains keys:
    values
    Normals

>>> isos.cell_arrays
pyvista DataSetAttributes
Association: CELL
Contains keys:
    Normals

>>> loaded  # read back from .stl file
PolyData (0x7fa7118e7d00)
  N Cells:  26664
  N Points: 13656
  X Bounds: -4.470e+00, 4.470e+00
  Y Bounds: -5.000e+00, 5.000e+00
  Z Bounds: -5.000e+00, 5.000e+00
  N Arrays: 0

While the original isosurfaces each had the isovalues bound to them (providing the colour mapping seen in the first figure), as well as point and cell normals (computed by the call to .save() for some reason), there’s no data in the latter case.

Still, since you’re looking for vertices and faces, this should do just fine. In case you need it, you can also access these on the PyVista side, since the isosurface mesh is a PolyData object:

>>> isos.n_points, isos.n_cells
(13656, 26664)

>>> isos.points.shape  # each row is a point
(13656, 3)

>>> isos.faces
array([    3,     0,    45, ..., 13529, 13531, 13530])

>>> isos.faces.shape
(106656,)

Now the logistics of the faces is a bit tricky. They are all encoded in a 1d array of integers. In the 1d array you always have an integer n telling you the size of the given face, and then n zero-based indices corresponding to points in the points array. The above isosurfaces consist entirely of triangles:

>>> isos.faces[::4]  # [3 i1 i2 i3] quadruples encode faces
array([3, 3, 3, ..., 3, 3, 3])

>>> isos.is_all_triangles()
True

Which is why you’ll see

>>> isos.faces.size == 4 * isos.n_cells
True

and you could do isos.faces.reshape(-1, 4) to get a 2d array where each row corresponds to a triangular face (and the first column is constant 3).

There’s no equivalent function of MATLAB’s "isosurface" function in python/numpy. You need Marching Cube from Scikit-image to return faces and vertices.

https://scikit-image.org/docs/stable/api/skimage.measure.html?highlight=marching%20cube#skimage.measure.marching_cubes

Answered By: httpsn
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.