Multivolume Rendering Crashing (Python wrap, v8.2.0)

Hi everyone,
I’m a beginner at VTK (using the Python wrapping) and am currently having a bug where the program silently crashes when vtk.vtkRenderWindow.Render() is called.
Currently I’m running the following code on my system (excuse my messy code):
import vtk
import numpy as np
from mmw.utils import mmw_numba

opacity_map_default = np.array([[0, 0],
                                [0.2, 1]
                                ],
                               dtype=np.float32)

color_map_default = np.array([[0, 0, 0, 0],
                              [0.1, 0.8, 0.8, 0.8],
                              [1, 1, 1, 1]
                              ],
                             dtype=np.float32)

background_default = [0.2, 0.2, 0.2]

class vtkTimerCallback():
    def __init__(self, volume_list, renderer, render_window):
        self.volume_list = volume_list
        self.renderer = renderer
        self.render_window = render_window
        self.current_volume = 0
        self.num_volumes = len(self.volume_list)

    def execute(self, obj, event):
        volumeCollection = self.renderer.GetVolumes()
        num_active_volumes = volumeCollection.GetNumberOfItems()
        if num_active_volumes > 0:
            for i in range(num_active_volumes):
                self.renderer.RemoveVolume(volumeCollection.GetItemAsObject(i))
        self.renderer.AddVolume(self.volume_list[self.current_volume])
        self.render_window.Render()
        if self.current_volume >= self.num_volumes - 1:
            self.current_volume = 0
        else:
            self.current_volume += 1


def exitCheck(obj, event):
    '''A simple function to be called when the user decides to quit the application.'''
    if obj.GetEventPending() != 0:
        obj.SetAbortRender(1)

def multi_volume_render(data, voxel_size, dynamic_range, fps=1,
                  color_map=color_map_default,
                  opacity_map=opacity_map_default,
                  background=background_default, attachment_window=None, interactor=None):
    """Render a numpy volume using VTK. The data can be a 3D static volume or a 4D animated volume

    Parameters
    ----------
    data : [numpy 3d/4d float32 or complex64 array]
        The volume(s) to render with VTK. A 4D volume will be animated.
        SHould be shape (nx, ny, nz) or (num frames, nx, ny, nz)

    voxel_size : [float / list[3]]
        This is the voxel size of the volume. If all dimensions are equal pass a
        single float otherwise pass a list of length 3

    dynamic_range : [float]
        The desired dynamic range to render the volume with

    fps : [float] (default 1)
        The desired frames per second to render with vtk. If data is a 3d volume
        this parameter is not used

    color_map : [numpy 2D float array]
        A numpy array mapping the intensity scaled from 0 to 1 to a color map.
        The shape should by (number points, 4). each row is (intensity, R, G, B)
        with everything normalized from 0 to 1

    opacity_map : [numpy 2D float array]
        A numpy array mapping the intensity scaled from 0 to 1 to an opacity map.
        The shape should by (number points, 2). each row is
        (intensity, opacity) with everything normalized from 0 to 1

    background : [list[3]]
        This is the RGB values for the background values range from 0-1
    """

    if data.ndim == 3:
        animated = False
    elif data.ndim == 4:
        animated = True
    else:
        raise ValueError(f'data must be either 3 or 4 dimensions, is : {data.ndim}')

    if data.dtype != np.float32 and data.dtype != np.complex64:
        raise ValueError(f'data must be float32 or complex64, is : {data.dtype}')

    if isinstance(voxel_size, float):
        voxel_size = [voxel_size, voxel_size, voxel_size]
    if isinstance(voxel_size, list) and len(voxel_size) == 3:
        pass
    else:
        raise ValueError('voxel_size must be a float or a list of length 3')

    if not isinstance(background, list) and len(voxel_size) != 3:
        raise ValueError('background must a list of length 3')

    if color_map.ndim != 2 and color_map.shape[1] != 4:
        raise ValueError(f'color_map my be of dimension 2 and the second dimension must'
                         f'be length 4\n color_map.shape = {color_map.hshape}')

    if opacity_map.ndim != 2 and opacity_map.shape[1] != 2:
        raise ValueError(f'opacity_map my be of dimension 2 and the second dimension must'
                         f'be length 2\n opacity_map.shape = {opacity_map.hshape}')

    print(f'Converting to {dynamic_range} dB dynamic range...')
    scale_max = 255
    # convert to u8 image
    data_u8 = mmw_numba.fast_dbscale(data, dynamic_range)
    data_u8 -= data_u8.min()
    data_u8 *= scale_max / data_u8.max()
    data_u8 = data_u8.astype(np.uint8)

    if data.ndim == 3:
        data_u8 = np.expand_dims(data_u8, axis=0)

    num_frames, nx, ny, nz = data_u8.shape

    opacityFunc = vtk.vtkPiecewiseFunction()
    for i in range(opacity_map.shape[0]):
        opacityFunc.AddPoint(scale_max * opacity_map[i, 0], opacity_map[i, 1])

    colorFunc = vtk.vtkColorTransferFunction()
    for i in range(color_map.shape[0]):
        colorFunc.AddRGBPoint(scale_max * color_map[i, 0],  # intensity
                              color_map[i, 1],  # R
                              color_map[i, 2],  # G
                              color_map[i, 3])  # B

    volume_list = []

    for fn in range(num_frames):
        print(f'Creating VTK volume {fn + 1} / {num_frames}')
        dataImporter = vtk.vtkImageImport()
        dataImporter.SetDataSpacing(voxel_size[0], voxel_size[1], voxel_size[2])
        dataImporter.SetDataOrigin(0, 0, 0)
        data_bytes = data_u8[fn].tobytes()
        dataImporter.CopyImportVoidPointer(data_bytes, len(data_bytes))
        dataImporter.SetDataScalarTypeToUnsignedChar()
        dataImporter.SetNumberOfScalarComponents(1)
        dataImporter.SetDataExtent(0, nx - 1, 0, ny - 1, 0, nz - 1)
        dataImporter.SetWholeExtent(0, nx - 1, 0, ny - 1, 0, nz - 1)

        # volume properties
        volumeProperty = vtk.vtkVolumeProperty()
        volumeProperty.SetColor(colorFunc)
        volumeProperty.SetScalarOpacity(opacityFunc)
        volumeProperty.ShadeOn()

        volumeProperty.SetAmbient(0.0)
        volumeProperty.SetDiffuse(1.0)
        volumeProperty.SetSpecular(0.0)
        volumeProperty.SetSpecularPower(0.0)

        volumeProperty.SetInterpolationTypeToLinear()

        # create volume mapper
        volumeMapper = vtk.vtkGPUVolumeRayCastMapper()
        # volumeMapper.SetRequestedRenderModeToGPU()
        volumeMapper.SetInputConnection(0, dataImporter.GetOutputPort())
        volumeMapper.SetBlendModeToComposite()

        # The class vtkVolume is used to pair the previously declared volume as
        # well as the properties to be used when rendering that volume.
        volume = vtk.vtkVolume()
        multivolume = vtk.vtkMultiVolume()
        volume.SetMapper(volumeMapper)
        volume.SetProperty(volumeProperty)
        multivolume.SetVolume(volume, 0)
        multiVolumeMapper = vtk.vtkGPUVolumeRayCastMapper()
        multiVolumeMapper.UseJitteringOn()
        # multiVolumeMapper.SetRequestedRenderModeToGPU()
        multivolume.SetMapper(multiVolumeMapper)

        volume_list.append(multivolume)

    # With almost everything else ready, its time to initialize the renderer
    # and window, as well as creating a method for exiting the application
    renderer = vtk.vtkRenderer()

    if attachment_window == None:
        print("WINDOWMAKER POWER")
        renderWin = vtk.vtkRenderWindow()
    else:
        renderWin = attachment_window

    renderWin.SetSize(600, 1200)
    renderWin.AddRenderer(renderer)
    renderer.SetBackground(background[0], background[1], background[2])
    if interactor == None:
        renderInteractor = vtk.vtkRenderWindowInteractor()
    else:
        renderInteractor = interactor

    # We add the volume to the renderer ...
    renderer.AddVolume(volume_list[0])

    renderInteractor.SetRenderWindow(renderWin)
    
    # Tell the application to use the function as an exit check.
    renderWin.AddObserver("AbortCheckEvent", exitCheck)

    renderInteractor.SetInteractorStyle(vtk.vtkInteractorStyleTrackballCamera())
    try:
        renderInteractor.Initialize()
    except:
        print("What happened here?")

    if animated:
        volume_timer = vtkTimerCallback(volume_list, renderer, renderWin)
        renderInteractor.AddObserver('TimerEvent', volume_timer.execute)
        timerId = renderInteractor.CreateRepeatingTimer(int(1 / fps * 1000))

    # Because nothing will be rendered without any input, we order the first
    # render manually before control is handed over to the main-loop.
    print('Starting Renderer..')

    pos = renderer.GetActiveCamera().GetPosition()
    renderer.GetActiveCamera().SetPosition(5 * nx * voxel_size[0], ny // 2 * voxel_size[1], nz // 2 * voxel_size[2])
    renderer.GetActiveCamera().SetFocalPoint(nx // 2 * voxel_size[0], ny // 2 * voxel_size[1], nz // 2 * voxel_size[2])

    # renderer.GetActiveCamera().Azimuth(90)
    # renderer.GetActiveCamera().Elevation(0)

    # renderer.ResetCameraClippingRange() # crashes program
    # renderer.ResetCamera() # crashes program

    renderWin.Render() # crashes program
    renderInteractor.Start()

sample_volume = np.array(
    [
        [
            [1, 2, 3, 4, 5],
            [1, 2, 3, 4, 5],
            [2, 3, 4, 5, 6]
        ],
        [
            [3, 2, 3, 4, 5],
            [1, 3, 3, 4, 5],
            [2, 3, 8, 5, 6]
        ],
        [
            [1, 9, 3, 4, 5],
            [1, 2, 3, 4, 5],
            [2, 3, 4, 9, 6]
        ],
    ],
dtype=np.float32)


multi_volume_render(sample_volume, [1, 1, 1], 25, 10,
            color_map_default,
            opacity_map_default,
            background_default)

I’ve determined using VSCode debug tools that renderWin.Render() is the exact last line that runs before the program crashes with no error message. Is there a reason why that is, and how would it be solved? I’m using vtk version 8.2.0.