Slider-controlled surface plot with intersecting planes (MRI example)

Hi all,

I am trying to visualize slices in a 3d volume similar to the MRI example.
I was able to alter the code such that also vertical slices can be displayed and added a second slider for those slices.

The point where Iā€™m getting stuck is that when a horizontal slice is shown, the vertical one disappears and vice versa. If anyone has an Idea how to display a horizontal and a vertical slide at the same time (e.g. using two separate traces), your help would be very much appreciated.

Here is the modified MRI example as far a I got:

# Import data
import time
import numpy as np

from skimage import io

vol = io.imread("https://s3.amazonaws.com/assets.datacamp.com/blog_assets/attention-mri.tif")
volume = vol.T

nz,ny,nx = volume.shape
print('volume dimensions:', nz, ny, nx)
x = np.linspace(0,nx*0.1, nx)
y = np.linspace(0,ny*0.1, ny)
z = np.linspace(0,nz*0.1, nz)


# Define frames
import plotly.graph_objects as go
nb_frames = nz

# add z-frames
X, Y = np.meshgrid(x,y)
zFrames = [go.Frame(data=go.Surface(
    x=X, y=Y, z=(6.7 - k * 0.1) * np.ones((ny, nz)),
    surfacecolor=np.flipud(volume[nz-1 - k,:,:]),
    cmin=0, cmax=200
    ),
    name=str(k)
    )
    for k in range(nb_frames)]

# add y-frames
X, Z = np.meshgrid(x,z)
yFrames = [go.Frame(data=go.Surface(
    x=X, y = (18.9 - k * 0.1) * np.ones((nz, nx)), z=Z,
    surfacecolor=volume[:, ny-1 - k, :],
    cmin=0, cmax=200,
    name='ySlices'
    ),
    name=str(k+nz)
    )
    for k in range(ny)]

frames = zFrames + yFrames

fig = go.Figure(frames = frames)

# Add data to be displayed before animation starts
# z-plane
X, Y = np.meshgrid(x,y)
fig.add_trace(go.Surface(
    x=X, y=Y, z=6.7 * np.ones((ny, nx)),
    surfacecolor=np.flipud(volume[nz-1,:,:]),
    colorscale='Gray',
    cmin=0, cmax=200,
    colorbar=dict(thickness=20, ticklen=4)
    ))
# y-plane
#fig.add_trace(go.Surface(
#    z=18.9 * np.ones((nz, nx)),
#    surfacecolor=np.flipud(volume[:,ny-1,:]),
#    colorscale='Gray',
#    cmin=0, cmax=200,
#    colorbar=dict(thickness=20, ticklen=4)
#    ))


def frame_args(duration):
    return {
            "frame": {"duration": duration},
            "mode": "immediate",
            "fromcurrent": True,
            "transition": {"duration": duration, "easing": "linear"},
        }

sliders = [
            {
                "pad": {"b": 10, "t": 60},
                "len": 0.9,
                "x": 0.1,
                "y": 0,
                "steps": [
                    {
                        "args": [[f.name], frame_args(0)],
                        "label": str(iz),
                        "method": "animate",
                    }
                    for iz, f in enumerate(fig.frames[:nz])
                ],
            },
            {
                "pad": {"b": 10, "t": 60},
                "len": 0.9,
                "x": 0.1,
                "y": 0.2,
                "steps": [
                    {
                        "args": [[f.name], frame_args(0)],
                        "label": str(iy),
                        "method": "animate",
                    }
                    for iy, f in enumerate(fig.frames[nz:])
                ],
            }
        ]

# Layout
fig.update_layout(
         title='Slices in volumetric data',
         width=600,
         height=600,
         scene=dict(
                    zaxis=dict(range=[-0.1, 6.8], autorange=False),
                    yaxis=dict(range=[-0.1, 18.9], autorange=False),
                    aspectratio=dict(x=1, y=1, z=1),
                    ),
         updatemenus = [
            {
                "buttons": [
                    {
                        "args": [None, frame_args(50)],
                        "label": "▶", # play symbol
                        "method": "animate",
                    },
                    {
                        "args": [[None], frame_args(0)],
                        "label": "◼", # pause symbol
                        "method": "animate",
                    },
                ],
                "direction": "left",
                "pad": {"r": 10, "t": 70},
                "type": "buttons",
                "x": 0.1,
                "y": 0,
            }
         ],
         sliders=sliders
)

fig.show()

@cbmurkel
Could you explain, please, what slices do you want to be displayed simultaneously?
From your frames definition, as a concatenation of the two lists of slices, plotly cannot display simultaneously both a zslice and a yslice.
You should define two initial traces like the initial_slice from this notebook https://github.com/empet/Animating-MRI-scans/blob/master/scanning-the-head-classic.ipynb that contains a more general code than in the first version posted as a plotly tutorial.
The two initial slices correspond to to the initial zslice, respectively yslice, and a frame should update simultaneously both traces (if this is the behaviour you are expecting).

Thanks for your response @empet.
I had already the suspicion with the two initial slices but was not able to realize that with plotly.
The final goal is to produce an image like this where x, y and z planes are shown simultaneously with sliders at the bottom so scan through all three dimensions:

Screenshot from 2024-02-16 09-22-48

I will try your notebook and see if I can adapt it for my needs.