mgbam commited on
Commit
294d14f
·
verified ·
1 Parent(s): eb87d5f

Update ui_components.py

Browse files
Files changed (1) hide show
  1. ui_components.py +54 -86
ui_components.py CHANGED
@@ -1,24 +1,21 @@
1
- # ui_helpers.py (Example filename)
2
-
3
  import streamlit as st
4
  from typing import Optional, Tuple, Dict, Any, List, Union
5
  import pydicom
6
- import pydicom.valuerep # Import for specific types like PersonName
7
  import numpy as np
8
  import logging
9
 
10
- # Assume logger is configured elsewhere
11
  logger = logging.getLogger(__name__)
12
 
13
  # --- DICOM Metadata Display ---
14
 
15
- def display_dicom_metadata(metadata: Optional[Dict[str, Any]]):
16
  """
17
- Displays formatted DICOM metadata in a Streamlit expander, arranged in columns.
18
 
19
  Args:
20
  metadata: A dictionary containing DICOM tags (keys) and their values.
21
- Handles basic formatting for lists, UIDs, and bytes.
22
  If None or empty, displays a placeholder message.
23
  """
24
  with st.expander("View DICOM Metadata", expanded=False):
@@ -31,36 +28,30 @@ def display_dicom_metadata(metadata: Optional[Dict[str, Any]]):
31
  logger.debug(f"Displaying {len(metadata)} metadata items.")
32
 
33
  for key, value in metadata.items():
34
- display_value = "N/A" # Default display value
35
  try:
36
- # Format specific types for better readability
37
  if value is None:
38
  display_value = "N/A"
39
  elif isinstance(value, list):
40
- # Join list elements, ensuring individual elements are strings
41
  display_value = ", ".join(map(str, value))
42
  elif isinstance(value, pydicom.uid.UID):
43
- # Show the descriptive name for UIDs
44
  display_value = value.name
45
  elif isinstance(value, bytes):
46
- # Avoid displaying large byte strings directly
47
  display_value = f"[Binary Data ({len(value)} bytes)]"
48
  elif isinstance(value, pydicom.valuerep.PersonName):
49
- # Potentially sensitive, display generic placeholder or handle based on policy
50
  display_value = "[Person Name]"
51
  else:
52
- # Default to string representation, strip whitespace
53
  display_value = str(value).strip()
54
 
55
- # Truncate very long strings for display
56
  if len(display_value) > 150:
57
  display_value = display_value[:147] + "..."
58
-
59
  except Exception as e:
60
- logger.warning(f"Error formatting metadata key '{key}' for display: {e}", exc_info=False)
61
  display_value = "[Error formatting value]"
62
 
63
- # Use markdown for bold key and plain value
64
  cols[col_idx % 2].markdown(f"**{key}:** {display_value}")
65
  col_idx += 1
66
 
@@ -68,21 +59,21 @@ def display_dicom_metadata(metadata: Optional[Dict[str, Any]]):
68
 
69
  def dicom_wl_sliders(
70
  ds: Optional[pydicom.Dataset],
71
- metadata: Dict[str, Any] # Assumes metadata is already extracted
72
- ) -> Tuple[Optional[float], Optional[float]]:
73
  """
74
  Creates Streamlit sliders for adjusting DICOM Window Center (Level) and Width.
75
 
76
- Derives slider ranges and defaults from the dataset's pixel data and metadata.
77
- Includes a button to reset sliders to their initial default values.
78
 
79
  Args:
80
  ds: The pydicom Dataset object (must contain PixelData).
81
- metadata: Dictionary containing extracted metadata, used for default W/L.
82
 
83
  Returns:
84
- A tuple (window_center, window_width) representing the current slider
85
- values as floats. Returns (None, None) if sliders cannot be created.
86
  """
87
  st.subheader("DICOM Window/Level Adjustment")
88
 
@@ -93,59 +84,48 @@ def dicom_wl_sliders(
93
 
94
  # --- Determine Pixel Range ---
95
  pixel_min: float = 0.0
96
- pixel_max: float = 4095.0 # Fallback default range
97
- pixel_range_determined = False
98
  try:
99
  pixel_array = ds.pixel_array
100
- # Apply Rescale Slope/Intercept if present for accurate range
101
  if 'RescaleSlope' in ds and 'RescaleIntercept' in ds:
102
- slope = float(ds.RescaleSlope)
103
- intercept = float(ds.RescaleIntercept)
104
- logger.debug(f"Applying Rescale Slope ({slope}) / Intercept ({intercept}) for range calculation.")
105
- # Calculate on float copy to avoid modifying original array type if slope/intercept are floats
106
- rescaled_array = pixel_array.astype(np.float64) * slope + intercept
107
- pixel_min = float(rescaled_array.min())
108
- pixel_max = float(rescaled_array.max())
109
  else:
110
- pixel_min = float(pixel_array.min())
111
- pixel_max = float(pixel_array.max())
112
-
113
- pixel_range_determined = True
114
  logger.info(f"Determined pixel value range: Min={pixel_min}, Max={pixel_max}")
 
 
115
  if pixel_max == pixel_min:
116
- logger.warning("Pixel data range is zero (constant image). Sliders may not be meaningful.")
117
- # Adjust range slightly to avoid zero width/division issues
118
- pixel_max += 1.0
119
 
120
- except AttributeError:
121
- st.caption("Pixel data format not directly accessible (e.g., compressed). Using default range.")
122
- logger.warning("Could not directly access pixel_array (possibly compressed), using default range for sliders.")
123
  except Exception as e:
124
  st.caption(f"Could not determine pixel range (Error: {e}). Using default range.")
125
  logger.error(f"Error determining pixel range for sliders: {e}", exc_info=True)
126
 
127
- # --- Get and Validate Default W/L from Metadata ---
128
- default_wc_raw = metadata.get("WindowCenter", None)
129
- default_ww_raw = metadata.get("WindowWidth", None)
130
- default_wc: Optional[float] = None
131
- default_ww: Optional[float] = None
132
-
133
- # Helper to safely convert potential multi-value or string to float
134
  def safe_float_convert(value: Any) -> Optional[float]:
 
135
  if isinstance(value, (list, pydicom.multival.MultiValue)):
136
  val_to_convert = value[0] if len(value) > 0 else None
137
  else:
138
  val_to_convert = value
139
- if val_to_convert is None: return None
140
- try: return float(val_to_convert)
141
- except (ValueError, TypeError): return None
 
142
 
143
- default_wc = safe_float_convert(default_wc_raw)
144
- default_ww = safe_float_convert(default_ww_raw)
 
 
145
 
146
- # Use calculated center/width if defaults are missing or invalid
147
  calculated_center = (pixel_max + pixel_min) / 2.0
148
- # Use 80% of range or a fallback fixed width if range is very small/zero
149
  calculated_width = max(1.0, (pixel_max - pixel_min) * 0.8)
150
 
151
  if default_wc is None:
@@ -155,52 +135,40 @@ def dicom_wl_sliders(
155
  default_ww = calculated_width
156
  logger.debug(f"Using calculated default Window Width: {default_ww:.2f}")
157
 
158
- logger.info(f"Using defaults for sliders - WC: {default_wc:.2f}, WW: {default_ww:.2f}")
159
 
160
- # --- Calculate Sensible Slider Bounds ---
161
  data_range = pixel_max - pixel_min
162
- # Allow sliders to go slightly beyond the data range, but not excessively
163
- slider_min_level = pixel_min - data_range * 0.5 # Extend 50% below min
164
- slider_max_level = pixel_max + data_range * 0.5 # Extend 50% above max
165
- # Cap maximum width to avoid extreme values, e.g., 2x the data range or a fixed large value
166
- slider_max_width = max(1.0, data_range * 2.0)
167
- # Add absolute cap in case data_range is huge (e.g. for float data)
168
- slider_max_width = min(slider_max_width, 65536.0) # Example cap
169
-
170
- # Clamp default values to be within the calculated slider bounds
171
  clamped_default_wc = max(slider_min_level, min(slider_max_level, default_wc))
172
  clamped_default_ww = max(1.0, min(slider_max_width, default_ww))
173
 
174
  # --- Create Sliders ---
175
- # Use unique keys based on session state if these sliders persist across reruns
176
- # For simplicity here, using fixed keys assuming they are recreated each time needed
177
  wc = st.slider(
178
  "Window Center (Level)",
179
  min_value=slider_min_level,
180
  max_value=slider_max_level,
181
  value=clamped_default_wc,
182
- step=max(0.1, data_range / 1000.0), # Dynamic step based on range, minimum 0.1
183
  key="dicom_wc_slider",
184
- help=f"Adjusts the brightness center. Range based on pixel data [{pixel_min:.1f} - {pixel_max:.1f}]"
185
  )
186
  ww = st.slider(
187
  "Window Width",
188
- min_value=1.0, # Width must be positive
189
  max_value=slider_max_width,
190
  value=clamped_default_ww,
191
- step=max(0.1, data_range / 1000.0), # Dynamic step based on range
192
  key="dicom_ww_slider",
193
- help=f"Adjusts the contrast range. Based on pixel data range [{data_range:.1f}]"
194
  )
195
 
196
  # --- Reset Button ---
197
  if st.button("Reset W/L", key="reset_wl_button"):
198
- logger.info("Reset W/L button clicked. Triggering rerun to apply defaults.")
199
- # Clear potentially cached W/L values from session state if they are stored there
200
- # st.session_state.pop('manual_wc', None)
201
- # st.session_state.pop('manual_ww', None)
202
- # Rerun will cause sliders to re-render with their default values calculated above
203
- st.rerun()
204
-
205
- # Return the current values from the sliders
206
- return float(wc), float(ww)
 
 
 
1
  import streamlit as st
2
  from typing import Optional, Tuple, Dict, Any, List, Union
3
  import pydicom
4
+ import pydicom.valuerep
5
  import numpy as np
6
  import logging
7
 
 
8
  logger = logging.getLogger(__name__)
9
 
10
  # --- DICOM Metadata Display ---
11
 
12
+ def display_dicom_metadata(metadata: Optional[Dict[str, Any]]) -> None:
13
  """
14
+ Displays formatted DICOM metadata in a Streamlit expander, arranged in two columns.
15
 
16
  Args:
17
  metadata: A dictionary containing DICOM tags (keys) and their values.
18
+ Handles basic formatting for lists, UIDs, bytes, and sensitive types.
19
  If None or empty, displays a placeholder message.
20
  """
21
  with st.expander("View DICOM Metadata", expanded=False):
 
28
  logger.debug(f"Displaying {len(metadata)} metadata items.")
29
 
30
  for key, value in metadata.items():
31
+ display_value = "N/A" # Default display value
32
  try:
 
33
  if value is None:
34
  display_value = "N/A"
35
  elif isinstance(value, list):
 
36
  display_value = ", ".join(map(str, value))
37
  elif isinstance(value, pydicom.uid.UID):
 
38
  display_value = value.name
39
  elif isinstance(value, bytes):
 
40
  display_value = f"[Binary Data ({len(value)} bytes)]"
41
  elif isinstance(value, pydicom.valuerep.PersonName):
42
+ # Mask sensitive information or display a placeholder.
43
  display_value = "[Person Name]"
44
  else:
 
45
  display_value = str(value).strip()
46
 
47
+ # Truncate very long strings to improve readability.
48
  if len(display_value) > 150:
49
  display_value = display_value[:147] + "..."
 
50
  except Exception as e:
51
+ logger.warning(f"Error formatting metadata key '{key}': {e}", exc_info=True)
52
  display_value = "[Error formatting value]"
53
 
54
+ # Alternate between the two columns.
55
  cols[col_idx % 2].markdown(f"**{key}:** {display_value}")
56
  col_idx += 1
57
 
 
59
 
60
  def dicom_wl_sliders(
61
  ds: Optional[pydicom.Dataset],
62
+ metadata: Dict[str, Any]
63
+ ) -> Tuple[Optional[float], Optional[float]]:
64
  """
65
  Creates Streamlit sliders for adjusting DICOM Window Center (Level) and Width.
66
 
67
+ Derives slider ranges and default values from the dataset's pixel data and metadata.
68
+ Provides a "Reset W/L" button that reruns the app to restore default values.
69
 
70
  Args:
71
  ds: The pydicom Dataset object (must contain PixelData).
72
+ metadata: Dictionary containing extracted metadata, used for default window/level values.
73
 
74
  Returns:
75
+ A tuple (window_center, window_width) as floats.
76
+ Returns (None, None) if sliders cannot be created.
77
  """
78
  st.subheader("DICOM Window/Level Adjustment")
79
 
 
84
 
85
  # --- Determine Pixel Range ---
86
  pixel_min: float = 0.0
87
+ pixel_max: float = 4095.0 # Default fallback range
 
88
  try:
89
  pixel_array = ds.pixel_array
 
90
  if 'RescaleSlope' in ds and 'RescaleIntercept' in ds:
91
+ slope = float(ds.RescaleSlope)
92
+ intercept = float(ds.RescaleIntercept)
93
+ logger.debug(f"Applying Rescale Slope ({slope}) / Intercept ({intercept}) for range calculation.")
94
+ rescaled_array = pixel_array.astype(np.float64) * slope + intercept
95
+ pixel_min = float(rescaled_array.min())
96
+ pixel_max = float(rescaled_array.max())
 
97
  else:
98
+ pixel_min = float(pixel_array.min())
99
+ pixel_max = float(pixel_array.max())
 
 
100
  logger.info(f"Determined pixel value range: Min={pixel_min}, Max={pixel_max}")
101
+
102
+ # Avoid zero-width range.
103
  if pixel_max == pixel_min:
104
+ logger.warning("Pixel data range is zero (constant image). Adjusting range.")
105
+ pixel_max += 1.0
 
106
 
 
 
 
107
  except Exception as e:
108
  st.caption(f"Could not determine pixel range (Error: {e}). Using default range.")
109
  logger.error(f"Error determining pixel range for sliders: {e}", exc_info=True)
110
 
111
+ # --- Get and Validate Default Window/Level from Metadata ---
 
 
 
 
 
 
112
  def safe_float_convert(value: Any) -> Optional[float]:
113
+ """Safely converts a value (or first element of a list) to float."""
114
  if isinstance(value, (list, pydicom.multival.MultiValue)):
115
  val_to_convert = value[0] if len(value) > 0 else None
116
  else:
117
  val_to_convert = value
118
+ try:
119
+ return float(val_to_convert) if val_to_convert is not None else None
120
+ except (ValueError, TypeError):
121
+ return None
122
 
123
+ default_wc_raw = metadata.get("WindowCenter", None)
124
+ default_ww_raw = metadata.get("WindowWidth", None)
125
+ default_wc: Optional[float] = safe_float_convert(default_wc_raw)
126
+ default_ww: Optional[float] = safe_float_convert(default_ww_raw)
127
 
 
128
  calculated_center = (pixel_max + pixel_min) / 2.0
 
129
  calculated_width = max(1.0, (pixel_max - pixel_min) * 0.8)
130
 
131
  if default_wc is None:
 
135
  default_ww = calculated_width
136
  logger.debug(f"Using calculated default Window Width: {default_ww:.2f}")
137
 
138
+ logger.info(f"Slider defaults - WC: {default_wc:.2f}, WW: {default_ww:.2f}")
139
 
140
+ # --- Calculate Slider Bounds ---
141
  data_range = pixel_max - pixel_min
142
+ slider_min_level = pixel_min - data_range * 0.5 # Extend 50% below minimum
143
+ slider_max_level = pixel_max + data_range * 0.5 # Extend 50% above maximum
144
+ slider_max_width = min(max(1.0, data_range * 2.0), 65536.0) # Cap maximum width
145
+
 
 
 
 
 
146
  clamped_default_wc = max(slider_min_level, min(slider_max_level, default_wc))
147
  clamped_default_ww = max(1.0, min(slider_max_width, default_ww))
148
 
149
  # --- Create Sliders ---
 
 
150
  wc = st.slider(
151
  "Window Center (Level)",
152
  min_value=slider_min_level,
153
  max_value=slider_max_level,
154
  value=clamped_default_wc,
155
+ step=max(0.1, data_range / 1000.0),
156
  key="dicom_wc_slider",
157
+ help=f"Adjust brightness center. Range: [{pixel_min:.1f} - {pixel_max:.1f}]"
158
  )
159
  ww = st.slider(
160
  "Window Width",
161
+ min_value=1.0,
162
  max_value=slider_max_width,
163
  value=clamped_default_ww,
164
+ step=max(0.1, data_range / 1000.0),
165
  key="dicom_ww_slider",
166
+ help=f"Adjust contrast range. Data range: {data_range:.1f}"
167
  )
168
 
169
  # --- Reset Button ---
170
  if st.button("Reset W/L", key="reset_wl_button"):
171
+ logger.info("Reset W/L button clicked. Rerunning to apply default values.")
172
+ st.rerun()
173
+
174
+ return float(wc), float(ww)