radvisionai / ui_components.py
mgbam's picture
Update ui_components.py
294d14f verified
import streamlit as st
from typing import Optional, Tuple, Dict, Any, List, Union
import pydicom
import pydicom.valuerep
import numpy as np
import logging
logger = logging.getLogger(__name__)
# --- DICOM Metadata Display ---
def display_dicom_metadata(metadata: Optional[Dict[str, Any]]) -> None:
"""
Displays formatted DICOM metadata in a Streamlit expander, arranged in two columns.
Args:
metadata: A dictionary containing DICOM tags (keys) and their values.
Handles basic formatting for lists, UIDs, bytes, and sensitive types.
If None or empty, displays a placeholder message.
"""
with st.expander("View DICOM Metadata", expanded=False):
if not metadata:
st.caption("No metadata extracted or available.")
return
cols = st.columns(2)
col_idx = 0
logger.debug(f"Displaying {len(metadata)} metadata items.")
for key, value in metadata.items():
display_value = "N/A" # Default display value
try:
if value is None:
display_value = "N/A"
elif isinstance(value, list):
display_value = ", ".join(map(str, value))
elif isinstance(value, pydicom.uid.UID):
display_value = value.name
elif isinstance(value, bytes):
display_value = f"[Binary Data ({len(value)} bytes)]"
elif isinstance(value, pydicom.valuerep.PersonName):
# Mask sensitive information or display a placeholder.
display_value = "[Person Name]"
else:
display_value = str(value).strip()
# Truncate very long strings to improve readability.
if len(display_value) > 150:
display_value = display_value[:147] + "..."
except Exception as e:
logger.warning(f"Error formatting metadata key '{key}': {e}", exc_info=True)
display_value = "[Error formatting value]"
# Alternate between the two columns.
cols[col_idx % 2].markdown(f"**{key}:** {display_value}")
col_idx += 1
# --- DICOM Window/Level Sliders ---
def dicom_wl_sliders(
ds: Optional[pydicom.Dataset],
metadata: Dict[str, Any]
) -> Tuple[Optional[float], Optional[float]]:
"""
Creates Streamlit sliders for adjusting DICOM Window Center (Level) and Width.
Derives slider ranges and default values from the dataset's pixel data and metadata.
Provides a "Reset W/L" button that reruns the app to restore default values.
Args:
ds: The pydicom Dataset object (must contain PixelData).
metadata: Dictionary containing extracted metadata, used for default window/level values.
Returns:
A tuple (window_center, window_width) as floats.
Returns (None, None) if sliders cannot be created.
"""
st.subheader("DICOM Window/Level Adjustment")
if ds is None or 'PixelData' not in ds:
st.caption("Cannot create W/L sliders: DICOM data or PixelData missing.")
logger.warning("dicom_wl_sliders called with missing Dataset or PixelData.")
return None, None
# --- Determine Pixel Range ---
pixel_min: float = 0.0
pixel_max: float = 4095.0 # Default fallback range
try:
pixel_array = ds.pixel_array
if 'RescaleSlope' in ds and 'RescaleIntercept' in ds:
slope = float(ds.RescaleSlope)
intercept = float(ds.RescaleIntercept)
logger.debug(f"Applying Rescale Slope ({slope}) / Intercept ({intercept}) for range calculation.")
rescaled_array = pixel_array.astype(np.float64) * slope + intercept
pixel_min = float(rescaled_array.min())
pixel_max = float(rescaled_array.max())
else:
pixel_min = float(pixel_array.min())
pixel_max = float(pixel_array.max())
logger.info(f"Determined pixel value range: Min={pixel_min}, Max={pixel_max}")
# Avoid zero-width range.
if pixel_max == pixel_min:
logger.warning("Pixel data range is zero (constant image). Adjusting range.")
pixel_max += 1.0
except Exception as e:
st.caption(f"Could not determine pixel range (Error: {e}). Using default range.")
logger.error(f"Error determining pixel range for sliders: {e}", exc_info=True)
# --- Get and Validate Default Window/Level from Metadata ---
def safe_float_convert(value: Any) -> Optional[float]:
"""Safely converts a value (or first element of a list) to float."""
if isinstance(value, (list, pydicom.multival.MultiValue)):
val_to_convert = value[0] if len(value) > 0 else None
else:
val_to_convert = value
try:
return float(val_to_convert) if val_to_convert is not None else None
except (ValueError, TypeError):
return None
default_wc_raw = metadata.get("WindowCenter", None)
default_ww_raw = metadata.get("WindowWidth", None)
default_wc: Optional[float] = safe_float_convert(default_wc_raw)
default_ww: Optional[float] = safe_float_convert(default_ww_raw)
calculated_center = (pixel_max + pixel_min) / 2.0
calculated_width = max(1.0, (pixel_max - pixel_min) * 0.8)
if default_wc is None:
default_wc = calculated_center
logger.debug(f"Using calculated default Window Center: {default_wc:.2f}")
if default_ww is None or default_ww <= 0:
default_ww = calculated_width
logger.debug(f"Using calculated default Window Width: {default_ww:.2f}")
logger.info(f"Slider defaults - WC: {default_wc:.2f}, WW: {default_ww:.2f}")
# --- Calculate Slider Bounds ---
data_range = pixel_max - pixel_min
slider_min_level = pixel_min - data_range * 0.5 # Extend 50% below minimum
slider_max_level = pixel_max + data_range * 0.5 # Extend 50% above maximum
slider_max_width = min(max(1.0, data_range * 2.0), 65536.0) # Cap maximum width
clamped_default_wc = max(slider_min_level, min(slider_max_level, default_wc))
clamped_default_ww = max(1.0, min(slider_max_width, default_ww))
# --- Create Sliders ---
wc = st.slider(
"Window Center (Level)",
min_value=slider_min_level,
max_value=slider_max_level,
value=clamped_default_wc,
step=max(0.1, data_range / 1000.0),
key="dicom_wc_slider",
help=f"Adjust brightness center. Range: [{pixel_min:.1f} - {pixel_max:.1f}]"
)
ww = st.slider(
"Window Width",
min_value=1.0,
max_value=slider_max_width,
value=clamped_default_ww,
step=max(0.1, data_range / 1000.0),
key="dicom_ww_slider",
help=f"Adjust contrast range. Data range: {data_range:.1f}"
)
# --- Reset Button ---
if st.button("Reset W/L", key="reset_wl_button"):
logger.info("Reset W/L button clicked. Rerunning to apply default values.")
st.rerun()
return float(wc), float(ww)