Reputation: 11
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