Malek kchaou
Malek kchaou

Reputation: 11

How can I store the value of a Dash slider component?

I am developing an MRI file viewer app using dash and plotly. The way it works is I can select a specific MRI file from my dataset and the app will generate a slider that could take you through the different MRI slices. There's also a show ROI (ROI is short for region of interest) check box that if toggled will re-size the slicer to only slide through the MRI slices on which the ROI could be shown. The problem is when I uncheck the Display ROI box and go back to the normal view with the original slider the MRI slice gets reset to the first one and not the one I was viewing last.

Is there a way to store the slider value when viewing the ROI so that when I uncheck the "Display ROI" box, the same slice I was viewing with the ROI remains selected for the full view?

Here's the app's code:

# Define the base directory where subdirectories and files are stored
BASE_DIRECTORY = path


# Function to get subdirectories from the base directory
def get_subdirectories(directory):
    """Returns a sorted list of subdirectories within the given directory."""
    return sorted([d for d in os.listdir(directory) if os.path.isdir(os.path.join(directory, d))])

# Function to get files within a selected subdirectory
def get_files(directory, subdirectory):
    """
    Returns a sorted list of files within a given subdirectory.
    Parameters:
        - directory: The base directory containing subdirectories.
        - subdirectory: The selected subdirectory to list files from.
    """
    subdirectory_path = os.path.join(directory, subdirectory)  # Full path to the selected subdirectory
    return sorted([f for f in os.listdir(subdirectory_path) if os.path.isfile(os.path.join(subdirectory_path, f))])

# Initialize the Dash app
app = dash.Dash(__name__)

# Define the layout of the Dash app
app.layout = html.Div(
    # Styling for the entire page
    style={
        "backgroundColor": "#f4f4f4",  # Light gray background
        "padding": "20px",  # Padding around content
        "borderRadius": "10px",  # Rounded corners for smooth UI
        "fontfamily": "Arial, sans-serif",  # Font styling
        "fontweight": "normal"  # Normal font weight
    },
    children=[  # Contains all UI elements within this Div
            
        # Main header
        html.H1("Interactive File Selector", style={"textAlign": "center", "color": "#333"}),  # Centered header with dark color

        # Container for dropdowns and output
        html.Div(
            # Styling for the container
            style={
                "maxWidth": "600px",  # Restricts width for a neat appearance
                "margin": "auto",  # Centers the container
                "padding": "20px",  # Adds padding inside the container
                "backgroundColor": "white",  # White background for contrast
                "borderRadius": "10px",  # Rounded corners
                "boxShadow": "0 4px 10px rgba(0, 0, 0, 0.1)"  # Light shadow for a modern look
            },
            children=[
                # Label for subdirectory selection
                html.Label("Select a Subdirectory:", style={"fontSize": "16px", "fontWeight": "bold"}),

                # Dropdown for selecting a subdirectory
                dcc.Dropdown(
                    id='subdirectory-dropdown',  # Unique ID to reference this component in callbacks
                    options=[{'label': subdir, 'value': subdir} for subdir in get_subdirectories(BASE_DIRECTORY)],
                    # Generates options dynamically based on available subdirectories
                    placeholder="Select a subdirectory",  # Default text displayed before selection
                    style={"marginBottom": "10px"}  # Adds spacing below the dropdown
                ),

                # Label for file selection
                html.Label("Select a File:", style={"fontSize": "16px", "fontWeight": "bold"}),

                # Dropdown for selecting a file (dynamically updated when a subdirectory is selected)
                dcc.Dropdown(
                    id='file-dropdown',  # Unique ID for referencing in callbacks
                    options=[],  # Initially empty, will be updated dynamically
                    placeholder="Select an MRI file",  # Default text before selection
                    style={"marginBottom": "10px"}  # Adds spacing below the dropdown
                ),

                html.Br(),  # Adds a line break for better spacing

                # Div to display the selected file
                html.Div(
                    id='selected-file-output',  # Unique ID to update output dynamically
                    style={"fontSize": "16px", "fontWeight": "bold", "color": "#007bff"}),

                # Display row data in a nice table format (using pandas DataFrame)
                html.Div(id='row-data-table-output', style={"marginTop": "20px", "marginBottom": "20px"}),

                # Label for file selection
                html.Label("Slide the handle to change the viewed slice:", style={"marginTop": "20px", "marginBottom": "10px", "fontSize": "16px", "fontWeight": "bold"}),

                html.Div(
                    dcc.Slider(
                        id='slice-slider',
                        min=0,
                        max=100,
                        step=1,
                        value=0,
                        marks={i: str(i) for i in range(0, 101, 10)},
                        tooltip={"placement": "bottom", "always_visible": True}
                    ),
                    style={'width': '80%', 'margin': '20px auto'}  # Increase width and center it
                ),

                html.Div([dcc.Graph(id='roi-slice-output')]),

                dcc.Checklist(
                    id='roi-toggle',
                    options=[{'label': 'Show ROI', 'value': 'show_roi'}],
                    value=[],  # Default is unchecked
                    style={'marginTop': '10px'}
                )

            ]
        )
    ]
)

# Callback to update the file dropdown when a subdirectory is selected
@app.callback(
    Output('file-dropdown', 'options'),  # Dynamically updates the file dropdown options
    Input('subdirectory-dropdown', 'value')  # Runs when the user selects a subdirectory
)
def update_file_dropdown(selected_subdirectory):
    """
    Updates the file dropdown based on the selected subdirectory.
    If no subdirectory is selected, it returns an empty list.
    """
    if not selected_subdirectory:
        return []  # Return empty list if no subdirectory is selected

    # Retrieve files from the selected subdirectory
    files = get_files(BASE_DIRECTORY, selected_subdirectory)

    # Returns a list of dictionaries where each file is {'label': file_name, 'value': file_name}
    return [{'label': f, 'value': f} for f in files]

# Callback to display the selected file
@app.callback(
    Output('selected-file-output', 'children'),  # Updates the text display dynamically
    Input('file-dropdown', 'value')  # Runs when a file is selected
)
def display_selected_file(selected_file):
    """
    Updates the text output to display the selected file name.
    If no file is selected, it displays 'No file selected.'
    """
    if not selected_file:
        return "No file selected."  # Default message if no file is chosen

    return f"Selected File: {selected_file}"  # Displays the selected file name

# Callback to update slider min, max, and marks dynamically
@app.callback(
    [Output('slice-slider', 'min'),
     Output('slice-slider', 'max'),
     Output('slice-slider', 'marks'),
     Output('slice-slider', 'value')],
    [Input('file-dropdown', 'value'), 
     Input('roi-toggle', 'value')]
)
def update_slider(selected_file, show_roi):
    if selected_file:
      if 'show_roi' in show_roi:
        first_roi_slice = roi_dict[selected_file][1]['roiZ']
        last_roi_slice = first_roi_slice + roi_dict[selected_file][1]['roiDepth'] - 1 # Get total slices
        marks = {i: str(i) for i in range(0, last_roi_slice - first_roi_slice + 1, 1)}
        return first_roi_slice, last_roi_slice, marks, first_roi_slice  # Reset to first slice
      else:
        mri_data = roi_dict[selected_file][2]
        first_slice = 0
        last_slice = mri_data.shape[0]
        marks = {i: str(i) for i in range(0, last_slice, 1)}
        return first_slice, last_slice, marks, first_slice  # Reset to first slice
    return 0, 10, {0: '0', 10: '10'}, 0  # Default values if no file selected

@app.callback(
    Output('row-data-table-output', 'children'),
    Input('file-dropdown', 'value')
)
def display_mri_row_data(selected_mri_filename):
    """
    This function retrieves the row data from the dictionary and formats it for display.
    It also fetches the corresponding ROI slice to display in the app.
    """
    if not selected_mri_filename:
        return "", "", "" # Default message if no file is chosen

    # Get the corresponding row data from the dictionary roi_dict
    row = roi_dict[selected_mri_filename][1]  # This is a pandas Series

    # Extract individual fields from the pandas Series
    examId = row['examId']
    seriesNo = row['seriesNo']
    aclDiagnosis = row['aclDiagnosis']
    kneeLR = row['kneeLR']
    roiX = row['roiX']
    roiY = row['roiY']
    roiZ = row['roiZ']
    roiHeight = row['roiHeight']
    roiWidth = row['roiWidth']
    roiDepth = row['roiDepth']
    volumeFilename = row['volumeFilename']

    # Format the row data for display
    row_data_str = html.Div([
        f"examId: {examId}",
        html.Br(),
        f"seriesNo: {seriesNo}",
        html.Br(),
        f"aclDiagnosis: {aclDiagnosis}",
        html.Br(),
        f"kneeLR: {kneeLR}",
        html.Br(),
        f"roiX: {roiX}",
        html.Br(),
        f"roiY: {roiY}",
        html.Br(),
        f"roiZ: {roiZ}",
        html.Br(),
        f"roiHeight: {roiHeight}",
        html.Br(),
        f"roiWidth: {roiWidth}",
        html.Br(),
        f"roiDepth: {roiDepth}",
        html.Br(),
        f"volumeFilename: {volumeFilename}"
    ])

    # Create a table to display the row data in a structured format
    row_data_table = html.Table(
        children=[html.Tr([html.Th(col), html.Td(row[col])]) for col in row.index]
    )
    # Return row data, table, and the ROI images
    return row_data_table

# Callback to display the MRI row data and visualization using matplotlib
@app.callback(
    Output('roi-slice-output', 'figure'),
    [Input('file-dropdown', 'value'),
     Input('slice-slider', 'value'),
     Input('roi-toggle', 'value')] 
)
def display_mri_with_mask(selected_mri_filename, selected_slice, show_roi):
    """
    This callback function retrieves the MRI data and mask from the selected MRI file,
    and generates a visualization overlaying the mask on the MRI slice.
    """
    if not selected_mri_filename:
        blank_image = np.ones((256, 256)) * 255  # White image (all pixels set to 255)
        return px.imshow(blank_image, color_continuous_scale='gray')  # Return an empty figure if no file is chosen


    # Check if the selected filename exists in roi_dict
    if selected_mri_filename not in roi_dict:
        return go.Figure()# Return an empty figure if the filename is invalid

    # Retrieve the data and mask from roi_dict
    mri_data = roi_dict[selected_mri_filename][2]
    mask = roi_dict[selected_mri_filename][3]

    # Ensure the arrays are NumPy arrays
    if not isinstance(mri_data, np.ndarray):
        print(f"Error: MRI data for {selected_mri_filename} is not a numpy array")
        return go.Figure()# Return an empty figure if the MRI data is not a numpy array

    if not isinstance(mask, np.ndarray):
        print(f"Error: Mask for {selected_mri_filename} is not a numpy array")
        return go.Figure() # Return an empty figure if the mask is not a numpy array

    # Ensure the slice index is valid (e.g., check for 'roiZ' in metadata)
    if 'roiZ' not in roi_dict[selected_mri_filename][1]:
        print(f"Error: 'roiZ' not found in metadata for {selected_mri_filename}")
        return go.Figure() # Return an empty figure if roiZ is not available

    try:
      mri_slice = mri_data[selected_slice, :, :]
      mask_slice = mask[selected_slice, :, :]
      if 'show_roi' in show_roi:
        updated_mri_slice = mri_slice * mask_slice
        fig = px.imshow(updated_mri_slice, color_continuous_scale='gray', labels={'color': 'MRI Data'})
      else:  
        # Plot the MRI data using Plotly Express
        fig = px.imshow(mri_slice, color_continuous_scale='gray', labels={'color': 'MRI Data'})
    except IndexError:
        print(f"Error: Invalid 'roiZ' index {roi_dict[selected_mri_filename][1]['roiZ']} for {selected_mri_filename}")
        return go.Figure() # Return an empty figure if the slice index is out of bounds
          # Update the layout of the figure

    fig.update_layout(
          title="MRI with Mask Overlay",
          coloraxis_showscale=False,
          template="plotly_white",
          xaxis={'showgrid': False, 'zeroline': False},
          yaxis={'showgrid': False, 'zeroline': False},
          dragmode='pan'
      )
    
    return fig # Return the updated figure and the updated slice index

# Run the Dash app
if __name__ == '__main__':
    app.run_server(debug=True, use_reloader=False, port=8000)  # Runs the app with debug mode enabled for easier troubleshooting

I tried usind dcc.Store() as well as updating the 'value' of 'slice-slider' through my display_mri_with_mask() function by adding Output('slice-slider', 'value') to the callback, but nothing worked.

I'm just stuck and any help would be much appreciated!

Thank you in advance!

Upvotes: 1

Views: 17

Answers (0)

Related Questions