pyvista icon indicating copy to clipboard operation
pyvista copied to clipboard

Crash when `compute_normals`

Open Onandon11 opened this issue 4 years ago • 11 comments

Describe the bug, what's wrong, and what you expect:

When calculating normals, the interpreter silently crashes.. No error's or output, not even with the VTKErrorCatcher. can anyone please enlighten me what happens in this minimal example?


To Reproduce

Please include a code snippet to reproduce the bug in the block below:

import pyvista
import numpy as np

xyz = np.random.random(102*3).reshape(-1,3)
faces = np.arange(102*3)
np.random.shuffle(faces)
faces = faces.reshape(-1,3)
faces = np.column_stack((np.ones(len(faces),) * 3, faces)).astype(int)

mesh = pyvista.PolyData()
mesh.points = xyz
mesh.faces = faces
print(mesh.compute_normals(inplace=False))

System Information: Please run the following code wherever you are experiencing the bug and paste the output below. This report helps us track down bugs and it is critical to addressing your bug:

# Get system info
import pyvista as pv
print(pv.Report())
--------------------------------------------------------------------------------
  Date: Wed Feb 02 16:36:41 2022 W. Europe Standard Time

                OS : Windows
            CPU(s) : 8
           Machine : AMD64
      Architecture : 64bit
               RAM : 31.9 GiB
       Environment : Python
       File system : unknown
        GPU Vendor : NVIDIA Corporation
      GPU Renderer : Quadro M1000M/PCIe/SSE2
       GPU Version : 4.5.0 NVIDIA 441.66

  Python 3.9.0 (tags/v3.9.0:9cf6752, Oct  5 2020, 15:34:40) [MSC v.1927 64 bit
  (AMD64)]

           pyvista : 0.33.2
               vtk : 9.1.0
             numpy : 1.22.1
           imageio : 2.14.1
           appdirs : 1.4.4
            scooby : 0.5.11
        matplotlib : 3.5.1
         pyvistaqt : 0.7.0
             PyQt5 : 5.15.6
             scipy : 1.7.3
--------------------------------------------------------------------------------


Onandon11 avatar Feb 02 '22 15:02 Onandon11

The vtkPolyDataNormals (the underlying VTK filter here) filter is specifically for polygonal meshes - for this specific example data, the cells are malformed (hence why you see nothing when trying to plot these data).

From https://vtk.org/doc/nightly/html/classvtkPolyDataNormals.html#details

Normals are computed only for polygons and triangle strips. Normals are not computed for lines or vertices. Triangle strips are broken up into triangle polygons. You may want to restrip the triangles.

Perhaps we should add a warning or error in compute_normals to check the cell types

banesullivan avatar Feb 02 '22 16:02 banesullivan

Thanks for tour quick response!

However the faces are all triangles and thus a Polygonial mesh, aren't they? The filter should then.

Onandon11 avatar Feb 02 '22 16:02 Onandon11

there's only 102 verts but faces is indexing up to 305

darikg avatar Feb 02 '22 17:02 darikg

The faces in this particular mesh (produced randomly) are not valid/illformed - @darikg is exactly right about the indexing

import pyvista
import numpy as np

xyz = np.random.random(102*3).reshape(-1,3)
faces = np.arange(102*3)
np.random.shuffle(faces)
faces = faces.reshape(-1,3)
faces = np.column_stack((np.ones(len(faces),) * 3, faces)).astype(int)

mesh = pyvista.PolyData()
mesh.points = xyz
mesh.faces = faces
print(mesh)
PolyData (0x7ff2e02eb440)
  N Cells:	102
  N Points:	102
  X Bounds:	-5.518e+127, 1.146e+248
  Y Bounds:	0.000e+00, 1.711e+238
  Z Bounds:	0.000e+00, 1.143e+243
  N Arrays:	0

Let's look at the first face:

>>> faces[0,:]
array([  3, 151, 221,  96])

Cell index 0 says that nodes 151, 221, and 96 should make that polygon. The mesh itself only has 102 points, thus that polygon cannot exist

banesullivan avatar Feb 02 '22 17:02 banesullivan

Do you have some valid data where you are seeing this crash?

banesullivan avatar Feb 02 '22 17:02 banesullivan

Sorry you guys are right, the example is invalid. I have some real data however the bug does not occur every time I run the program (due to some randomness). So it's hard to catch the dataset which raises the bug.

Tomorrow I will try, better, to catch the bug. Or make a valid example which raises the problem.

@banesullivan, assuming the bug is valid, is checking for 'is all triangles' a fix?

Onandon11 avatar Feb 02 '22 18:02 Onandon11

is checking for 'is all triangles' a fix?

That's what I'm thinking but it is hard to tell without seeing the problematic data. Perhaps also a clean().

FWIW, we have this check in PyVista with is_all_triangles

If the problem mesh is indeed all triangles, I'd like to do some other investigation

banesullivan avatar Feb 02 '22 18:02 banesullivan

That function is indeed what I mentioned 😄

I will work on reproducing the dataset. I will be back when I have the dataset 😉

Onandon11 avatar Feb 02 '22 21:02 Onandon11

So I managed to reproduce the problematic data bug_merged.zip

When calling compute_normals on the dataset the interpreter crashes without errors just like I described. However giving the dataset a closer look I see that the the indices of the faces are out of bounds

import pyvista

mesh = pyvista.read("bug_merged.vtk")

print(mesh.faces.max())
# Returns 102

print(mesh.points.shape)
# Returns (102, 3)

So I can image this is problematic. To give some background information; the dataset is created by merging n datasets, every dataset should (should) contain 3 points and these point are connected by 1 face (namely faces=[3, 0, 1, 2]). I found a bug in my program that some datasets has only two points. So settings the faces to [3, 0, 1, 2] is obviously wrong on a dataset with n_points=2. However the error is raised pretty late, setting the faces, merging the datasets all go right and no function complains about the malformed faces array. Only at the point of computing the normals the program crashes...

I made a small example to recreate the malformed dataset:

import pyvista
import numpy as np

meshes = []
for i in range(3):
    if i == 0:
        xyz = np.random.random(2*3).reshape(-1, 3)
    else:
        xyz = np.random.random(3*3).reshape(-1, 3)
    faces = np.array([[3, 0, 1, 2]])
    mesh = pyvista.PolyData()
    mesh.points = xyz
    mesh.faces = faces
    mesh.delaunay_2d()
    meshes.append(mesh)

merged = pyvista.PolyData().merge(meshes, merge_points=False)


>>> merged
PolyData (0x18105e45880)
  N Cells:	3
  N Points:	9
  X Bounds:	7.980e-02, 9.152e-01
  Y Bounds:	2.071e-01, 8.445e-01
  Z Bounds:	5.083e-02, 8.073e-01
  N Arrays:	0

>>> merged.faces.reshape(-1, 4)
array([[3, 0, 1, 2],
       [3, 2, 3, 4],
       [3, 5, 6, 7]], dtype=int64)

But look at the faces attribute. Ofcourse the dataset is invalid, but merging should complain about this, or when setting the faces array a check should be done if self.faces.max() < self.n_points. This dataset is even plottable and normals can be calculated.

However if I place the malformed dataset to the end of the for loop:

meshes = []
for i in range(3):
    if i == 2:
        xyz = np.random.random(2*3).reshape(-1, 3)
    else:
        xyz = np.random.random(3*3).reshape(-1, 3)
    faces = np.array([[3, 0, 1, 2]])
    mesh = pyvista.PolyData()
    mesh.points = xyz
    mesh.faces = faces
    mesh.delaunay_2d()
    meshes.append(mesh)

merged = pyvista.PolyData().merge(meshes, merge_points=False)

>>> merged
PolyData (0x18105e7fe20)
  N Cells:	3
  N Points:	8
  X Bounds:	3.444e-01, 7.444e-01
  Y Bounds:	6.473e-02, 8.465e-01
  Z Bounds:	3.494e-02, 9.298e-01
  N Arrays:	0
>>> merged.faces.reshape(-1, 4)
array([[3, 0, 1, 2],
       [3, 3, 4, 5],
       [3, 6, 7, 8]], dtype=int64)

The faces matrix goes up to 8 which is of course out-of-bounds due to n_points=8.

So easiest I think is just add a check when setting faces? PS.: sorry for flagging this as bug, the problem was due to my own fault 😢

Onandon11 avatar Feb 03 '22 09:02 Onandon11

Thank you for this thorough investigative work, @Onandon11! I think the poor handling of this "malformed" mesh resulting in a crash is worthy of the "bug" label.

I think we should use this to add a check to the faces for compute_normals() or at least issue a warning. We could also add some sort of warning in the merge if we detect it is producing a "malformed" mesh

banesullivan avatar Feb 04 '22 05:02 banesullivan

The mesh validation methods in #8164 can help address this. E.g. mesh validation with the original example:

import numpy as np
xyz = np.random.random(102*3).reshape(-1,3)
faces = np.arange(102*3)
np.random.shuffle(faces)
faces = faces.reshape(-1,3)
faces = np.column_stack((np.ones(len(faces),) * 3, faces)).astype(int)
mesh = pyvista.PolyData()
mesh.points = xyz
mesh.faces = faces

mesh.validate_mesh(action='error')
# pyvista.core.errors.InvalidMeshError: PolyData mesh is not valid due to the following problems:
#   - Mesh has 97 cells with invalid point references. Invalid cell ids: [  0   1   2   3   4   5   6   8   9  10  11  12  13  14  15  16  17  18
#   19  20  21  22  23  24  26  27  28  29  30  31  32  33  34  35  36  37
#   38  39  40  41  42  43  44  45  46  47  48  49  51  52  53  54  55  56
#   57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74
#   75  76  77  78  79  80  81  82  83  84  86  87  88  89  90  91  93  94
#   95  96  97  98  99 100 101]

user27182 avatar Dec 09 '25 21:12 user27182