dosemetrics / src /dosemetrics_app /tabs /conformity_tab.py
amithjkamath's picture
Deploy dosemetrics app - 2025-12-28 17:28:50
5d2df83
Raw
History Blame Contribute Delete
8.93 kB
"""
Conformity analysis tab for the Streamlit app.
"""
import streamlit as st
import pandas as pd
import plotly.graph_objects as go
from io import BytesIO
from dosemetrics_app.utils import read_byte_data
from dosemetrics import Dose, StructureSet
from dosemetrics.metrics import conformity
from dosemetrics_app.utils import get_example_datasets, load_example_files
def request_dose_and_target(instruction_text):
"""Helper function to request dose and target file uploads or example selection"""
st.markdown(instruction_text)
st.markdown("Check instructions on the sidebar for more information.")
# Add option to use example data
data_source = st.radio(
"Data source:", ["Upload your own files", "Use example data"], horizontal=True
)
dose_file = None
target_file = None
if data_source == "Upload your own files":
dose_file = st.file_uploader(
"Upload a dose distribution volume (in .nii.gz)", type=["gz"]
)
target_file = st.file_uploader(
"Upload target mask volume (in .nii.gz)", type=["gz"]
)
else:
# Load example data
example_datasets = get_example_datasets()
if example_datasets:
dataset_names = list(example_datasets.keys())
default_index = (
dataset_names.index("test_subject")
if "test_subject" in dataset_names
else 0
)
selected_dataset = st.selectbox(
"Select example dataset:", options=dataset_names, index=default_index
)
if selected_dataset:
dataset_path = example_datasets[selected_dataset]
with st.spinner("Loading example data..."):
dose_path, mask_paths = load_example_files(dataset_path)
if dose_path:
# Read dose file
with open(dose_path, "rb") as f:
dose_bytes = BytesIO(f.read())
dose_bytes.name = dose_path.name
dose_file = dose_bytes
# Find target file (look for PTV, GTV, CTV, or Target)
target_path = None
for mask_path in mask_paths:
if any(
t in mask_path.name.upper()
for t in ["PTV", "GTV", "CTV", "TARGET"]
):
target_path = mask_path
break
if target_path:
with open(target_path, "rb") as f:
target_bytes = BytesIO(f.read())
target_bytes.name = target_path.name
target_file = target_bytes
st.success(
f"Loaded dose and target ({target_path.name}) from {selected_dataset}"
)
else:
st.warning(
"No target structure found in example data. Please upload your own target file."
)
else:
st.warning("Example data not available. Please upload your own files.")
data_source = "Upload your own files"
return dose_file, target_file
def panel():
"""Main panel function for Conformity Analysis tab"""
st.sidebar.success("Select an option above.")
instruction_text = "## Step 1: Upload dose distribution volume and target mask"
dose_file, target_file = request_dose_and_target(instruction_text)
files_uploaded = (dose_file is not None) and (target_file is not None)
if files_uploaded:
st.divider()
st.markdown("## Step 2: Specify prescription dose")
prescription_dose = st.number_input(
"Prescription dose (Gy):",
min_value=0.1,
max_value=200.0,
value=60.0,
step=0.1,
help="The prescribed dose to the target volume in Gray (Gy)",
)
st.divider()
st.markdown("## Step 3: Compute conformity indices")
if st.button("Compute Conformity Indices"):
with st.spinner("Loading data and computing conformity indices..."):
# Load data
dose_volume, structure_masks = read_byte_data(dose_file, [target_file])
# Create Dose object
dose = Dose(dose_volume)
# Get target structure
target_name = list(structure_masks.keys())[0]
target_mask = structure_masks[target_name]
structure_set = StructureSet()
structure_set.add_structure(
target_name, target_mask, structure_type="target"
)
target = structure_set.structures[target_name]
# Compute conformity indices
ci = conformity.compute_conformity_index(
dose, target, prescription_dose
)
cn = conformity.compute_conformation_number(
dose, target, prescription_dose
)
gi = conformity.compute_gradient_index(dose, target, prescription_dose)
st.success("Conformity indices computed successfully")
# Display results
st.markdown("### Results")
results_df = pd.DataFrame(
{
"Metric": [
"Conformity Index (CI)",
"Conformation Number (CN)",
"Gradient Index (GI)",
],
"Value": [ci, cn, gi],
"Interpretation": [
"Ratio of prescription isodose volume to target volume (optimal: 1.0)",
"Product of target coverage and dose selectivity (optimal: 1.0)",
"Measure of dose fall-off outside target (lower is better)",
],
}
)
st.dataframe(results_df, use_container_width=True)
# Visualize results
st.markdown("### Visualization")
fig = go.Figure()
fig.add_trace(
go.Bar(
x=["Conformity Index", "Conformation Number"],
y=[ci, cn],
text=[f"{ci:.3f}", f"{cn:.3f}"],
textposition="auto",
marker_color=["#1f77b4", "#ff7f0e"],
)
)
fig.update_layout(
title="Conformity Metrics",
yaxis_title="Value",
yaxis_range=[0, max(1.5, ci * 1.2, cn * 1.2)],
showlegend=False,
)
# Add reference line at 1.0
fig.add_hline(
y=1.0,
line_dash="dash",
line_color="green",
annotation_text="Optimal value = 1.0",
)
st.plotly_chart(fig, use_container_width=True)
# Gradient index separately
fig_gi = go.Figure()
fig_gi.add_trace(
go.Bar(
x=["Gradient Index"],
y=[gi],
text=[f"{gi:.3f}"],
textposition="auto",
marker_color="#d62728",
)
)
fig_gi.update_layout(
title="Gradient Index (lower is better)",
yaxis_title="Value",
showlegend=False,
)
st.plotly_chart(fig_gi, use_container_width=True)
# Download results
csv = results_df.to_csv(index=False)
st.download_button(
label="Download results as CSV",
data=csv,
file_name="conformity_analysis.csv",
mime="text/csv",
)
# Explanation
st.markdown(
"""
### Metric Definitions
- **Conformity Index (CI)**: Ratio of the prescription isodose volume to the target volume.
An ideal CI is 1.0, indicating the prescription isodose perfectly conforms to the target.
- **Conformation Number (CN)**: Product of target coverage fraction and dose selectivity.
Accounts for both target underdosage and normal tissue overdosage. Optimal value is 1.0.
- **Gradient Index (GI)**: Ratio of the 50% isodose volume to the prescription isodose volume.
Measures dose fall-off outside the target. Lower values indicate steeper dose gradients.
"""
)