Spaces:
Running
Running
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) | |