napari / napari

napari: a fast, interactive, multi-dimensional image viewer for python
https://napari.org
BSD 3-Clause "New" or "Revised" License
2.07k stars 410 forks source link

Surface without points in the current time frame raise "AttributeError: 'NoneType' object has no attribute 'shape'" on main branch. #6872

Closed odinsbane closed 1 day ago

odinsbane commented 3 weeks ago

🐛 Bug Report

This is related to the previous bug I submitted https://github.com/napari/napari/issues/6870 and can be demonstrated from that example.

💡 Steps to Reproduce

Run the example in the other bug: https://github.com/napari/napari/issues/6870 select time frame 1 then select another time frame where the second mesh doesn't exist. Rotate the mesh, and the error will occur.

💡 Expected Behavior

I expect no errors, and only one mesh gets shown.

🌎 Environment

WSL2 Ubuntu using Windows 10 on the napari main branch.

💡 Additional Context

The issue appears to happen due in napari/_vispy/layers/surface.py once the mesh gets shown the first time, it has a shading_filter, then when view rotates, it tries to calculate a normal for a mesh without any vertexes. I changed line 207 to

    if self.node.shading_filter is not None and self._meshdata._vertices is not None:

And the problem goes away.

odinsbane commented 3 weeks ago

I forgot the traceback.

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
File ~/linux-desktop/napari/napari-venv/lib/python3.10/site-packages/vispy/app/backends/_qt.py:928, in CanvasBackendDesktop.paintGL(self=<vispy.app.backends._qt.CanvasBackendDesktop object>)
    926 # (0, 0, self.width(), self.height()))
    927 self._vispy_canvas.set_current()
--> 928 self._vispy_canvas.events.draw(region=None)
        self._vispy_canvas = <NapariSceneCanvas (PyQt5) at 0x7f9be9bf0d60>
        self._vispy_canvas.events.draw = <vispy.util.event.EventEmitter object at 0x7f9be9bf2800>
        self = <vispy.app.backends._qt.CanvasBackendDesktop object at 0x7f9be9bdfc70>
        self._vispy_canvas.events = <vispy.util.event.EmitterGroup object at 0x7f9be9bf2740>
    930 # Clear the alpha channel with QOpenGLWidget (Qt >= 5.4), otherwise the
    931 # window is translucent behind non-opaque objects.
    932 # Reference:  MRtrix3/mrtrix3#266
    933 if QT5_NEW_API or PYSIDE6_API or PYQT6_API:

File ~/linux-desktop/napari/napari-venv/lib/python3.10/site-packages/vispy/util/event.py:453, in EventEmitter.__call__(self=<vispy.util.event.EventEmitter object>, *args=(), **kwargs={'region': None})
    450 if self._emitting > 1:
    451     raise RuntimeError('EventEmitter loop detected!')
--> 453 self._invoke_callback(cb, event)
        event = <DrawEvent blocked=False handled=False native=None region=None source=None sources=[] type=draw>
        self = <vispy.util.event.EventEmitter object at 0x7f9be9bf2800>
        cb = <bound method VispyCamera.on_draw of <napari._vispy.camera.VispyCamera object at 0x7f9be9bf0d00>>
    454 if event.blocked:
    455     break

File ~/linux-desktop/napari/napari-venv/lib/python3.10/site-packages/vispy/util/event.py:471, in EventEmitter._invoke_callback(self=<vispy.util.event.EventEmitter object>, cb=<bound method VispyCamera.on_draw of <napari._vispy.camera.VispyCamera object>>, event=<DrawEvent blocked=False handled=False native=None region=None source=None sources=[] type=draw>)
    469     cb(event)
    470 except Exception:
--> 471     _handle_exception(self.ignore_callback_errors,
        self = <vispy.util.event.EventEmitter object at 0x7f9be9bf2800>
        cb = <bound method VispyCamera.on_draw of <napari._vispy.camera.VispyCamera object at 0x7f9be9bf0d00>>
        event = <DrawEvent blocked=False handled=False native=None region=None source=None sources=[] type=draw>
        (cb, event) = (<bound method VispyCamera.on_draw of <napari._vispy.camera.VispyCamera object at 0x7f9be9bf0d00>>, <DrawEvent blocked=False handled=False native=None region=None source=None sources=[] type=draw>)
    472                       self.print_callback_errors,
    473                       self, cb_event=(cb, event))

File ~/linux-desktop/napari/napari-venv/lib/python3.10/site-packages/vispy/util/event.py:469, in EventEmitter._invoke_callback(self=<vispy.util.event.EventEmitter object>, cb=<bound method VispyCamera.on_draw of <napari._vispy.camera.VispyCamera object>>, event=<DrawEvent blocked=False handled=False native=None region=None source=None sources=[] type=draw>)
    467 def _invoke_callback(self, cb, event):
    468     try:
--> 469         cb(event)
        cb = <bound method VispyCamera.on_draw of <napari._vispy.camera.VispyCamera object at 0x7f9be9bf0d00>>
        event = <DrawEvent blocked=False handled=False native=None region=None source=None sources=[] type=draw>
    470     except Exception:
    471         _handle_exception(self.ignore_callback_errors,
    472                           self.print_callback_errors,
    473                           self, cb_event=(cb, event))

File ~/linux-desktop/napari/napari-repo/napari/_vispy/camera.py:188, in VispyCamera.on_draw(self=<napari._vispy.camera.VispyCamera object>, _event=<DrawEvent blocked=False handled=False native=None region=None source=None sources=[] type=draw>)
    183 """Called whenever the canvas is drawn.
    184 
    185 Update camera model angles, center, and zoom.
    186 """
    187 with self._camera.events.angles.blocker(self._on_angles_change):
--> 188     self._camera.angles = self.angles
        self._camera.angles = (0.4350144751843147, 2.893990661923638, 86.80650786529637)
        self._camera = Camera(center=(5.0, 10.0, 0.0), zoom=40.232499999999995, angles=(0.4350144751843147, 2.893990661923638, 86.80650786529637), perspective=0.0, mouse_pan=True, mouse_zoom=True)
        self = <napari._vispy.camera.VispyCamera object at 0x7f9be9bf0d00>
    189 with self._camera.events.center.blocker(self._on_center_change):
    190     self._camera.center = self.center

File ~/linux-desktop/napari/napari-repo/napari/utils/events/evented_model.py:307, in EventedModel.__setattr__(self=Camera(center=(5.0, 10.0, 0.0), zoom=40.23249999...perspective=0.0, mouse_pan=True, mouse_zoom=True), name='angles', value=(0.14731901486049193, 0.4556950725660273, 89.40443463136077))
    305     super().__setattr__(name, value)
    306     return
--> 307 with ComparisonDelayer(self):
        self = Camera(center=(5.0, 10.0, 0.0), zoom=40.232499999999995, angles=(0.4350144751843147, 2.893990661923638, 86.80650786529637), perspective=0.0, mouse_pan=True, mouse_zoom=True)
    308     self._primary_changes.add(name)
    309     self._setattr_impl(name, value)

File ~/linux-desktop/napari/napari-repo/napari/utils/events/evented_model.py:520, in ComparisonDelayer.__exit__(self=<napari.utils.events.evented_model.ComparisonDelayer object>, exc_type=None, exc_val=None, exc_tb=None)
    518 def __exit__(self, exc_type, exc_val, exc_tb):
    519     self._target._delay_check_semaphore -= 1
--> 520     self._target._check_if_values_changed_and_emit_if_needed()
        self._target = Camera(center=(5.0, 10.0, 0.0), zoom=40.232499999999995, angles=(0.4350144751843147, 2.893990661923638, 86.80650786529637), perspective=0.0, mouse_pan=True, mouse_zoom=True)
        self = <napari.utils.events.evented_model.ComparisonDelayer object at 0x7f9b5ab7e530>

File ~/linux-desktop/napari/napari-repo/napari/utils/events/evented_model.py:345, in EventedModel._check_if_values_changed_and_emit_if_needed(self=Camera(center=(5.0, 10.0, 0.0), zoom=40.23249999...perspective=0.0, mouse_pan=True, mouse_zoom=True))
    342 with ComparisonDelayer(self):
    343     # Again delay comparison to avoid having events caused by callback functions
    344     for name, new_value in to_emit:
--> 345         getattr(self.events, name)(value=new_value)
        name = 'angles'
        self = Camera(center=(5.0, 10.0, 0.0), zoom=40.232499999999995, angles=(0.4350144751843147, 2.893990661923638, 86.80650786529637), perspective=0.0, mouse_pan=True, mouse_zoom=True)
        new_value = (0.14731901486049193, 0.4556950725660273, 89.40443463136077)

File ~/linux-desktop/napari/napari-repo/napari/utils/events/event.py:764, in EventEmitter.__call__(self=<napari.utils.events.event.EventEmitter object>, *args=(), **kwargs={'value': (0.14731901486049193, 0.4556950725660273, 89.40443463136077)})
    761     self._block_counter.update([cb])
    762     continue
--> 764 self._invoke_callback(cb, event if pass_event else None)
        event = <Event blocked=False handled=False native=None source=None sources=[] type='angles'>
        self = <napari.utils.events.event.EventEmitter object at 0x7f9be269dd20>
        cb = <bound method VispySurfaceLayer._on_camera_move of <napari._vispy.layers.surface.VispySurfaceLayer object at 0x7f9b604e0970>>
        pass_event = True
    765 if event.blocked:
    766     break

File ~/linux-desktop/napari/napari-repo/napari/utils/events/event.py:802, in EventEmitter._invoke_callback(self=<napari.utils.events.event.EventEmitter object>, cb=<bound method VispySurfaceLayer._on_camera_move ...._vispy.layers.surface.VispySurfaceLayer object>>, event=<Event blocked=False handled=False native=None source=None sources=[] type='angles'>)
    800     self.disconnect(cb)
    801     return
--> 802 _handle_exception(
        self = <napari.utils.events.event.EventEmitter object at 0x7f9be269dd20>
        event = <Event blocked=False handled=False native=None source=None sources=[] type='angles'>
        cb = <bound method VispySurfaceLayer._on_camera_move of <napari._vispy.layers.surface.VispySurfaceLayer object at 0x7f9b604e0970>>
        (cb, event) = (<bound method VispySurfaceLayer._on_camera_move of <napari._vispy.layers.surface.VispySurfaceLayer object at 0x7f9b604e0970>>, <Event blocked=False handled=False native=None source=None sources=[] type='angles'>)
    803     self.ignore_callback_errors,
    804     self.print_callback_errors,
    805     self,
    806     cb_event=(cb, event),
    807 )

File ~/linux-desktop/napari/napari-repo/napari/utils/events/event.py:789, in EventEmitter._invoke_callback(self=<napari.utils.events.event.EventEmitter object>, cb=<bound method VispySurfaceLayer._on_camera_move ...._vispy.layers.surface.VispySurfaceLayer object>>, event=<Event blocked=False handled=False native=None source=None sources=[] type='angles'>)
    787 try:
    788     if event is not None:
--> 789         cb(event)
        event = <Event blocked=False handled=False native=None source=None sources=[] type='angles'>
        cb = <bound method VispySurfaceLayer._on_camera_move of <napari._vispy.layers.surface.VispySurfaceLayer object at 0x7f9b604e0970>>
    790     else:
    791         cb()

File ~/linux-desktop/napari/napari-repo/napari/_vispy/layers/surface.py:207, in VispySurfaceLayer._on_camera_move(self=<napari._vispy.layers.surface.VispySurfaceLayer object>, event=<Event blocked=False handled=False native=None source=None sources=[] type='angles'>)
    205 # combine to get light behind the camera on the top right
    206 self._light_direction = view - up + np.cross(up, view)
--> 207 self.node.shading_filter.light_dir = self._light_direction
        self._light_direction = <class 'numpy.ndarray'> (3,) float64
        self = <napari._vispy.layers.surface.VispySurfaceLayer object at 0x7f9b604e0970>
        self.node.shading_filter = <vispy.visuals.filters.mesh.ShadingFilter object at 0x7f9b5ac13130>
        self.node = <SurfaceVisual at 0x7f9b604a3220>

File ~/linux-desktop/napari/napari-venv/lib/python3.10/site-packages/vispy/visuals/filters/mesh.py:460, in ShadingFilter.light_dir(self=<vispy.visuals.filters.mesh.ShadingFilter object>, direction=<class 'numpy.ndarray'> (3,) float64)
    458     raise ValueError('Invalid direction %s' % direction)
    459 self._light_dir = tuple(direction)
--> 460 self._update_data()
        self = <vispy.visuals.filters.mesh.ShadingFilter object at 0x7f9b5ac13130>

File ~/linux-desktop/napari/napari-venv/lib/python3.10/site-packages/vispy/visuals/filters/mesh.py:552, in ShadingFilter._update_data(self=<vispy.visuals.filters.mesh.ShadingFilter object>)
    547 self.fshader['flat_shading'] = 1 if self._shading == 'flat' else 0
    548 self.fshader['shading_enabled'] = (
    549     1 if self._enabled and self._shading is not None else 0
    550 )
--> 552 normals = self._visual.mesh_data.get_vertex_normals(indexed='faces')
        self = <vispy.visuals.filters.mesh.ShadingFilter object at 0x7f9b5ac13130>
        self._visual = <SurfaceVisual at 0x7f9b604a3220>
    553 self._normals.set_data(normals, convert=True)

File ~/linux-desktop/napari/napari-venv/lib/python3.10/site-packages/vispy/geometry/meshdata.py:380, in MeshData.get_vertex_normals(self=<vispy.geometry.meshdata.MeshData object>, indexed='faces')
    377     raise ValueError("Invalid indexing mode. Accepts: None, 'faces'")
    379 if self._vertex_normals is None:
--> 380     face_normals = self.get_face_normals()
        self = <vispy.geometry.meshdata.MeshData object at 0x7f9b60406080>
    381     faces = self.get_faces()
    382     vertices = self.get_vertices()

File ~/linux-desktop/napari/napari-venv/lib/python3.10/site-packages/vispy/geometry/meshdata.py:351, in MeshData.get_face_normals(self=<vispy.geometry.meshdata.MeshData object>, indexed=None)
    349 if self._face_normals is None:
    350     vertices = self.get_vertices(indexed='faces')
--> 351     self._face_normals = _compute_face_normals(vertices)
        self._face_normals = None
        vertices = None
        self = <vispy.geometry.meshdata.MeshData object at 0x7f9b60406080>
    353 if indexed == 'faces' and self._face_normals_indexed_by_faces is None:
    354     self._face_normals_indexed_by_faces = \
    355         _repeat_face_normals_on_corners(self._face_normals)

File ~/linux-desktop/napari/napari-venv/lib/python3.10/site-packages/vispy/geometry/meshdata.py:23, in _compute_face_normals(vertices=None)
     22 def _compute_face_normals(vertices):
---> 23     if vertices.shape[1:] != (3, 3):
        vertices = None
     24         raise ValueError("Expected (N, 3, 3) array of vertices repeated on"
     25                          f" the triangle corners, got {vertices.shape}.")
     26     edges1 = vertices[:, 1] - vertices[:, 0]

AttributeError: 'NoneType' object has no attribute 'shape'
jni commented 1 day ago

Fixed by #6874