File size: 7,133 Bytes
7ef2f88
13a9d7d
7ef2f88
294d14f
13a9d7d
 
 
 
7ef2f88
13a9d7d
7ef2f88
294d14f
13a9d7d
294d14f
13a9d7d
 
 
294d14f
13a9d7d
 
7ef2f88
13a9d7d
 
 
 
7ef2f88
 
13a9d7d
 
7ef2f88
294d14f
13a9d7d
 
 
 
 
 
 
 
 
 
294d14f
13a9d7d
 
 
 
294d14f
13a9d7d
 
 
294d14f
13a9d7d
 
294d14f
13a9d7d
7ef2f88
 
13a9d7d
7ef2f88
 
13a9d7d
294d14f
 
13a9d7d
 
 
294d14f
 
7ef2f88
13a9d7d
 
294d14f
7ef2f88
13a9d7d
294d14f
 
13a9d7d
 
 
 
 
 
 
 
 
 
294d14f
7ef2f88
 
13a9d7d
294d14f
 
 
 
 
 
13a9d7d
294d14f
 
13a9d7d
294d14f
 
13a9d7d
294d14f
 
13a9d7d
 
 
 
 
294d14f
13a9d7d
294d14f
13a9d7d
 
 
 
294d14f
 
 
 
13a9d7d
294d14f
 
 
 
13a9d7d
 
 
 
 
 
 
 
 
 
 
294d14f
13a9d7d
294d14f
13a9d7d
294d14f
 
 
 
13a9d7d
 
7ef2f88
13a9d7d
7ef2f88
 
13a9d7d
 
 
294d14f
13a9d7d
294d14f
7ef2f88
 
 
294d14f
13a9d7d
 
294d14f
13a9d7d
294d14f
7ef2f88
 
13a9d7d
 
294d14f
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
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)