diff --git a/.gitmodules b/.gitmodules
index 52d9cfe..0edd8b7 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,6 +1,8 @@
[submodule "engines/ceus"]
path = engines/ceus
url = https://github.com/QuantUS-OpenSource/QuantUS-Backend-CEUS
+ branch = main
[submodule "engines/qus"]
path = engines/qus
url = https://github.com/QuantUS-OpenSource/QuantUS-Backend-QUS
+ branch = main
diff --git a/requirements.txt b/requirements.txt
index 97b7a07..63bcec3 100644
Binary files a/requirements.txt and b/requirements.txt differ
diff --git a/src/ceus/image_loading/views/file_selection_widget.py b/src/ceus/image_loading/views/file_selection_widget.py
index 5b095c6..23f8d09 100644
--- a/src/ceus/image_loading/views/file_selection_widget.py
+++ b/src/ceus/image_loading/views/file_selection_widget.py
@@ -116,7 +116,8 @@ def _show_loading_message(self) -> None:
def _on_choose_image_path(self) -> None:
"""Handle image file selection."""
- if self._file_extensions == ["FOLDER"]:
+ is_folder = any(ext.upper() == "FOLDER" for ext in self._file_extensions)
+ if is_folder:
dir_name = QFileDialog.getExistingDirectory(self, "Select Directory")
if dir_name:
self._ui.image_path_input.setText(dir_name)
@@ -133,12 +134,16 @@ def _on_generate_image(self) -> None:
if not os.path.exists(image_path):
self.show_error(f"Image file does not exist: {os.path.basename(image_path)}")
return
- if not image_path.endswith(tuple(self._file_extensions)) and self._file_extensions != ['FOLDER']:
- self.show_error(f"Image file must have one of the following extensions: {', '.join(self._file_extensions)}")
- return
- if self._file_extensions == ["FOLDER"] and not os.path.isdir(image_path):
- self.show_error("Input path must be a folder!")
- return
+
+ is_folder = any(ext.upper() == "FOLDER" for ext in self._file_extensions)
+ if not is_folder:
+ if not image_path.endswith(tuple(self._file_extensions)):
+ self.show_error(f"Image file must have one of the following extensions: {', '.join(self._file_extensions)}")
+ return
+ else:
+ if not os.path.isdir(image_path):
+ self.show_error("Input path must be a folder!")
+ return
self.clear_error()
diff --git a/src/ceus/seg_loading/views/draw_roi_widget.py b/src/ceus/seg_loading/views/draw_roi_widget.py
index f72738f..6ae71a3 100644
--- a/src/ceus/seg_loading/views/draw_roi_widget.py
+++ b/src/ceus/seg_loading/views/draw_roi_widget.py
@@ -11,13 +11,24 @@
from scipy import interpolate
from PIL import Image, ImageDraw
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
+from matplotlib.colors import LinearSegmentedColormap
-from PyQt6.QtWidgets import QWidget, QHBoxLayout, QFileDialog
+from PyQt6.QtWidgets import QWidget, QHBoxLayout, QFileDialog, QSlider, QVBoxLayout, QFrame, QCheckBox, QLabel
from PyQt6.QtCore import pyqtSignal, Qt
from ...mvc.base_view import BaseViewMixin
from ..ui.draw_roi_ui import Ui_constructRoi
from engines.ceus.src.data_objs import UltrasoundImage
+from engines.ceus.src.image_preprocessing.functions import enhance_clahe, enhance_gamma
+
+# Philips CEUS Colormap: Grayscale -> Red -> Yellow
+philips_colors = [
+ (0.0, 0.0, 0.0), # 0% - Black
+ (0.4, 0.4, 0.4), # 40% - Gray
+ (0.8, 0.0, 0.0), # 80% - Red
+ (1.0, 1.0, 0.0) # 100% - Yellow
+]
+philips_cmap = LinearSegmentedColormap.from_list("philips_ceus", philips_colors)
class DrawROIWidget(QWidget, BaseViewMixin):
@@ -59,8 +70,18 @@ def __init__(self, image_data: UltrasoundImage, parent: Optional[QWidget] = None
self._target_frame = 0 # Target frame for smooth transitions
self._frame_update_pending = False
+ # Enhancement parameters
+ self._clahe_clip_limit = 1.2
+ self._gamma = 1.5
self._width_scale = 1.0
+ # Enhancement parameters
+ self._clahe_clip_limit = 1.2
+ self._gamma = 1.5
+ self._use_philips_ceus = False
+ self._enhanced_cache = None # Cache for enhanced current frame
+ self._enhanced_cache_idx = -1
+
self._setup_ui()
self._connect_signals()
self._show_draw_type_selection()
@@ -256,52 +277,112 @@ def _update_aspect_ratio(self) -> None:
self._matplotlib_canvas.draw_idle()
def _setup_enhancement_controls(self) -> None:
- """Add enhancement sliders to the sidebar."""
- from PyQt6.QtWidgets import QVBoxLayout, QLabel, QSlider, QFrame
-
+ """Add enhancement sliders beside the frame slider in a single horizontal line."""
+ # Container frame for enhancement controls
enh_group = QFrame()
enh_group.setStyleSheet("background-color: rgba(255, 255, 255, 0); border: none;")
- container_layout = QVBoxLayout(enh_group)
- container_layout.setContentsMargins(0, 10, 0, 10)
+
+ # Main horizontal layout for the enhancement section
+ container_layout = QHBoxLayout(enh_group)
+ container_layout.setContentsMargins(0, 0, 15, 0)
container_layout.setSpacing(15)
- def create_enh_column(label_text, min_val, max_val, current_val, callback):
- col_widget = QWidget()
- col_layout = QVBoxLayout(col_widget)
- col_layout.setContentsMargins(0, 0, 0, 0)
- col_layout.setSpacing(5)
+ def create_compact_control(label_text, min_val, max_val, current_val, callback):
+ # Widget to hold label, slider, and value in ONE line
+ ctrl_widget = QWidget()
+ ctrl_layout = QHBoxLayout(ctrl_widget)
+ ctrl_layout.setContentsMargins(0, 0, 0, 0)
+ ctrl_layout.setSpacing(5)
lbl = QLabel(label_text)
- lbl.setStyleSheet("font-size: 14px; color: white; font-weight: bold;")
- lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
- col_layout.addWidget(lbl)
+ lbl.setStyleSheet("font-size: 10px; color: white; font-weight: bold;")
+ ctrl_layout.addWidget(lbl)
- row_layout = QHBoxLayout()
+ # Slider
slider = QSlider(Qt.Orientation.Horizontal)
slider.setRange(min_val, max_val)
slider.setValue(current_val)
- slider.setMinimumWidth(100)
- slider.setMaximumWidth(120)
+ slider.setStyleSheet(self._ui.frame_slider.styleSheet())
+ slider.setFixedWidth(70)
+ slider.setFixedHeight(12)
slider.valueChanged.connect(callback)
-
+ ctrl_layout.addWidget(slider)
+
val_lbl = QLabel(f"{current_val/10.0:.1f}")
- val_lbl.setMinimumWidth(40)
- val_lbl.setStyleSheet("color: #3498db; font-weight: bold; font-size: 14px;")
+ val_lbl.setStyleSheet("color: #3498db; font-weight: bold; font-size: 10px;")
+ val_lbl.setMinimumWidth(22)
+ val_lbl.setAlignment(Qt.AlignmentFlag.AlignLeft)
+ ctrl_layout.addWidget(val_lbl)
- row_layout.addWidget(slider)
- row_layout.addWidget(val_lbl)
- col_layout.addLayout(row_layout)
-
- return col_widget, slider, val_lbl
+ return ctrl_widget, slider, val_lbl
- width_col, self.width_slider, self.width_val_lbl = create_enh_column(
+ # Create controls
+ clahe_w, self.clahe_slider, self.clahe_val_lbl = create_compact_control(
+ "CLAHE", 1, 100, int(self._clahe_clip_limit * 10), self._on_clahe_changed
+ )
+ gamma_w, self.gamma_slider, self.gamma_val_lbl = create_compact_control(
+ "GAMMA", 1, 40, int(self._gamma * 10), self._on_gamma_changed
+ )
+ width_w, self.width_slider, self.width_val_lbl = create_compact_control(
"WIDTH", 1, 50, int(self._width_scale * 10), self._on_width_changed
)
- container_layout.addWidget(width_col)
+ # Pseudo colouring toggle nicely aligned
+ self.philips_check = QCheckBox("Pseudo colouring")
+ self.philips_check.setStyleSheet("color: white; font-weight: bold; font-size: 11px;")
+ self.philips_check.stateChanged.connect(self._on_philips_toggled)
+
+ # Add to horizontal layout
+ container_layout.addWidget(clahe_w)
+ container_layout.addWidget(gamma_w)
+ container_layout.addWidget(width_w)
+ container_layout.addWidget(self.philips_check)
+
+ # Add to the layout beside the frame slider (below the image)
+ self._ui.frameControlsLayout.insertWidget(0, enh_group)
+
+ def _on_clahe_changed(self, value: int) -> None:
+ """Handle CLAHE clip limit change."""
+ self._clahe_clip_limit = value / 10.0
+ if hasattr(self, 'clahe_val_lbl'):
+ self.clahe_val_lbl.setText(f"{self._clahe_clip_limit:.1f}")
+ self._invalidate_enhancement_cache()
+
+ def _on_gamma_changed(self, value: int) -> None:
+ """Handle gamma change."""
+ self._gamma = value / 10.0
+ if hasattr(self, 'gamma_val_lbl'):
+ self.gamma_val_lbl.setText(f"{self._gamma:.1f}")
+ self._invalidate_enhancement_cache()
+
+ def _on_philips_toggled(self, state: int) -> None:
+ """Handle Philips CEUS pseudocolor toggle."""
+ self._use_philips_ceus = state == Qt.CheckState.Checked.value
+ # Update colormap on artist
+ if self._im_artist:
+ new_cmap = philips_cmap if self._use_philips_ceus else 'gray'
+ self._im_artist.set_cmap(new_cmap)
+ self._matplotlib_canvas.draw_idle()
- # Add to the layout below the frame slider
- self._ui.side_bar_layout.addWidget(enh_group)
+ def _invalidate_enhancement_cache(self) -> None:
+ """Invalidate the enhancement cache and trigger display update."""
+ self._enhanced_cache = None
+ self._enhanced_cache_idx = -1
+ self._force_frame_update()
+
+ def _enhance_frame(self, frame_2d: np.ndarray) -> np.ndarray:
+ """Enhance a 2D image frame using backend engine functions."""
+ # Create a temporary UltrasoundImage for processing
+ temp_im = UltrasoundImage(self._image_data.scan_path)
+ temp_im.pixel_data = frame_2d
+ temp_im.pixdim = self._image_data.pixdim
+ temp_im.frame_rate = self._image_data.frame_rate
+
+ # Apply enhancements
+ temp_im = enhance_clahe(temp_im, clip_limit=self._clahe_clip_limit)
+ temp_im = enhance_gamma(temp_im, gamma=self._gamma)
+
+ return temp_im.pixel_data
def _on_frame_changed(self, value: int) -> None:
"""Handle frame slider change with optimized performance."""
@@ -312,8 +393,18 @@ def _on_frame_changed(self, value: int) -> None:
def _update_frame_display(self, frame_index: int) -> None:
"""Update the frame display with consistent parameters."""
if self._im_artist:
- self._displayed_im = self._all_frames[frame_index]
+ # Update cache if needed
+ if self._enhanced_cache is None or self._enhanced_cache_idx != frame_index:
+ self._enhanced_cache = self._enhance_frame(self._all_frames[frame_index])
+ self._enhanced_cache_idx = frame_index
+
+ self._displayed_im = self._enhanced_cache
self._im_artist.set_array(self._displayed_im)
+
+ # Ensure correct colormap is applied (e.g. after initialization)
+ new_cmap = philips_cmap if self._use_philips_ceus else 'gray'
+ self._im_artist.set_cmap(new_cmap)
+
self._ui.cur_frame_label.setText(str(np.round(frame_index*self._image_data.frame_rate, decimals=2)))
def _force_frame_update(self) -> None:
diff --git a/src/ceus/seg_loading/views/draw_voi_widget.py b/src/ceus/seg_loading/views/draw_voi_widget.py
index 420da4a..9797fe9 100644
--- a/src/ceus/seg_loading/views/draw_voi_widget.py
+++ b/src/ceus/seg_loading/views/draw_voi_widget.py
@@ -196,6 +196,8 @@ def __init__(self, image_data: UltrasoundImage, parent: Optional[QWidget] = None
self._connect_signals()
self._connect_matplotlib_events()
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
+ self._update_scan_display() # Initial UI update
+ self._refresh_frames() # Mark all planes for first update
# ======================= Matplotlib Mouse Interaction ===================
def _connect_matplotlib_events(self):
@@ -325,6 +327,7 @@ def _setup_ui(self) -> None:
self._ui.scan_name_input.setText(self._image_data.scan_name)
self._ui.toggle_crosshair_visibility_button.setText('Hide Crosshair')
+ self._ui.cur_slice_label.setText("Current Frame:")
self._ui.interp_loading_label.hide(); self._ui.saving_voi_label.hide()
self._ui.navigating_label.hide(); self._ui.undo_last_roi_button.hide()
@@ -685,9 +688,6 @@ def _update(_frame):
if scatter: artists.append(scatter)
if mask: artists.append(mask)
- # Only update frame counters occasionally or when pending refreshed
- if self._ax_sag_cor_pending[plane_ix]:
- self._update_scan_display()
return artists
self._ax_sag_cor_animations[plane_ix] = anim.FuncAnimation(
@@ -745,6 +745,7 @@ def set_crosshair(self, x: Optional[int] = None, y: Optional[int] = None,
if [cx, cy, cz, ct] != self._crosshair_xyzt:
self._crosshair_xyzt = [cx, cy, cz, ct]
self._refresh_frames()
+ self._update_scan_display()
return cx, cy, cz, ct
def _update_seg_masks(self, plane_ix):
@@ -806,11 +807,35 @@ def _connect_signals(self) -> None:
self._ui.toggle_crosshair_visibility_button.clicked.connect(self._on_toggle_crosshair_visibility)
self._ui.save_voi_button.clicked.connect(self._on_save_voi_clicked)
+ # Configure slice/time controls
self._ui.cur_slice_slider.setMinimum(0)
self._ui.cur_slice_slider.setMaximum(max(0, self._num_slices - 1))
self._ui.cur_slice_slider.setValue(self._crosshair_xyzt[3])
self._ui.cur_slice_slider.valueChanged.connect(self._on_time_slider_changed)
+ # Configure spin box for frame-based navigation
+ self._ui.cur_slice_spin_box.setRange(1, self._num_slices)
+ self._ui.cur_slice_spin_box.setSingleStep(1)
+ self._ui.cur_slice_spin_box.setDecimals(0)
+ self._ui.cur_slice_spin_box.setValue(self._crosshair_xyzt[3] + 1)
+ self._ui.cur_slice_spin_box.valueChanged.connect(self._on_time_spin_box_changed)
+
+ # Set initial total frames for all planes
+ self._ui.ax_total_frames.setText(str(self._z_len))
+ self._ui.sag_total_frames.setText(str(self._x_len))
+ self._ui.cor_total_frames.setText(str(self._y_len))
+ self._ui.cur_slice_total.setText(str(self._num_slices))
+
+ def _on_time_spin_box_changed(self, value: float):
+ """Handle user changing the time spin box."""
+ frame_idx = int(value) - 1
+
+ # Clamp to valid range
+ frame_idx = max(0, min(self._num_slices - 1, frame_idx))
+
+ if self._ui.cur_slice_slider.value() != frame_idx:
+ self._ui.cur_slice_slider.setValue(frame_idx)
+
def _on_time_slider_changed(self, value: int): # type: ignore
"""Handle user sliding through time dimension (t)."""
# Clamp safety (though slider should enforce)
@@ -1018,6 +1043,21 @@ def _update_scan_display(self):
self._ui.sag_frame_num.setText(str(self._crosshair_xyzt[0] + 1))
self._ui.cor_frame_num.setText(str(self._crosshair_xyzt[1] + 1))
+ # Update total frame labels
+ self._ui.ax_total_frames.setText(str(self._z_len))
+ self._ui.sag_total_frames.setText(str(self._x_len))
+ self._ui.cor_total_frames.setText(str(self._y_len))
+
+ # Update current slice/frame display
+ current_t = self._crosshair_xyzt[3]
+
+ # Block signals to avoid feedback loop
+ self._ui.cur_slice_spin_box.blockSignals(True)
+ self._ui.cur_slice_spin_box.setValue(current_t + 1)
+ self._ui.cur_slice_spin_box.blockSignals(False)
+
+ self._ui.cur_slice_total.setText(str(self._num_slices))
+
def mousePressEvent(self, a0):
super().mousePressEvent(a0)
self._crosshair_active = not self._crosshair_active
diff --git a/src/qus/application_model.py b/src/qus/application_model.py
index 3a6d558..30f9717 100644
--- a/src/qus/application_model.py
+++ b/src/qus/application_model.py
@@ -6,6 +6,7 @@
"""
import os
+import numpy as np
from typing import Dict, Any, Optional, Tuple
from PyQt6.QtCore import QThread, pyqtSignal
@@ -201,6 +202,10 @@ def __init__(self):
self._analysis_data: Optional[ParamapAnalysis] = None
self._analysis_worker: Optional[AnalysisWorker] = None
+ # DICOM state
+ self._dicom_image: Optional[np.ndarray] = None
+ self._dicom_file_path: Optional[str] = None
+
# Visualization state
self._visualization_types: Dict[str, Any] = {}
self._visualization_functions: Dict[str, Any] = {}
@@ -263,6 +268,48 @@ def image_data(self) -> Optional[UltrasoundRfImage]:
"""Get the currently loaded image data."""
return self._image_data
+ # DICOM Properties and Methods
+ @property
+ def dicom_available(self) -> bool:
+ """Check if DICOM data is available."""
+ return self._dicom_image is not None
+
+ @property
+ def dicom_image(self) -> Optional[np.ndarray]:
+ """Get the processed DICOM image data."""
+ return self._dicom_image
+
+ def load_dicom_file(self, dicom_file_path: str) -> bool:
+ """
+ Load a DICOM file using DicomLoader.
+
+ Args:
+ dicom_file_path: Path to the DICOM file
+
+ Returns:
+ bool: True if loaded successfully, False otherwise
+ """
+ from src.qus.image_loading.dicom_loader import DicomLoader
+
+ try:
+ dicom_pixels = DicomLoader.load_dicom_file(dicom_file_path)
+ if dicom_pixels is not None:
+ self._dicom_image = dicom_pixels
+ self._dicom_file_path = dicom_file_path
+ return True
+ return False
+ except Exception as e:
+ self._emit_error(f"Error loading DICOM: {e}")
+ return False
+
+ def get_dicom_data(self) -> dict:
+ """Get processed DICOM data for display."""
+ return {
+ 'image': self._dicom_image,
+ 'file_path': self._dicom_file_path,
+ 'available': self.dicom_available
+ }
+
def set_scan_type(self, scan_type_display_name: str) -> bool:
"""
Set the selected scan type.
diff --git a/src/qus/image_loading/__init__.py b/src/qus/image_loading/__init__.py
index 56cec46..bd3884d 100644
--- a/src/qus/image_loading/__init__.py
+++ b/src/qus/image_loading/__init__.py
@@ -3,5 +3,6 @@
"""
from .image_loading_controller import ImageLoadingController
+from .dicom_loader import DicomLoader
-__all__ = ['ImageLoadingController']
+__all__ = ['ImageLoadingController', 'DicomLoader']
diff --git a/src/qus/image_loading/dicom_loader.py b/src/qus/image_loading/dicom_loader.py
new file mode 100644
index 0000000..dbeaa42
--- /dev/null
+++ b/src/qus/image_loading/dicom_loader.py
@@ -0,0 +1,97 @@
+import numpy as np
+from pathlib import Path
+from typing import Optional
+
+try:
+ import pydicom
+ PYDICOM_AVAILABLE = True
+except ImportError:
+ PYDICOM_AVAILABLE = False
+
+class DicomLoader:
+ """
+ Utility class for loading and processing DICOM files for ultrasound imaging.
+ """
+
+ @staticmethod
+ def load_dicom_file(dicom_file_path: str) -> Optional[np.ndarray]:
+ """
+ Load a DICOM file and return the processed pixel data.
+
+ Args:
+ dicom_file_path (str): Path to the DICOM file to load
+
+ Returns:
+ np.ndarray: Processed pixel data as a 2D numpy array, or None if failed
+ """
+ if not PYDICOM_AVAILABLE:
+ print("pydicom is not installed. Cannot load DICOM files.")
+ return None
+
+ try:
+ dicom_path = Path(dicom_file_path)
+ if not dicom_path.exists() or not dicom_path.is_file():
+ print(f"DICOM file not found: {dicom_file_path}")
+ return None
+
+ # Read the DICOM file
+ dicom_data = pydicom.dcmread(str(dicom_path))
+
+ # Extract pixel data
+ if hasattr(dicom_data, 'pixel_array'):
+ dicom_pixels = dicom_data.pixel_array
+
+ # Convert to grayscale if needed (handle different DICOM formats)
+ if len(dicom_pixels.shape) == 4:
+ # 4D DICOM (frames, height, width, channels) - take first frame
+ dicom_pixels = dicom_pixels[0]
+ if len(dicom_pixels.shape) == 3:
+ # RGB or multi-frame DICOM - convert to grayscale
+ if dicom_pixels.shape[2] == 3: # RGB
+ dicom_pixels = np.dot(dicom_pixels[...,:3], [0.2989, 0.5870, 0.1140])
+ elif dicom_pixels.shape[0] < dicom_pixels.shape[2]: # Multi-frame
+ dicom_pixels = dicom_pixels[0] # Take first frame
+
+ # Normalize to 0-255 range
+ if dicom_pixels.dtype != np.uint8:
+ dicom_pixels = ((dicom_pixels - dicom_pixels.min()) /
+ (max(1, dicom_pixels.max() - dicom_pixels.min())) * 255).astype(np.uint8)
+
+ # Crop black regions from top and bottom
+ dicom_pixels = DicomLoader.crop_black_regions(dicom_pixels)
+
+ return dicom_pixels
+ else:
+ print("No pixel data found in DICOM file")
+ return None
+
+ except Exception as e:
+ print(f"Failed to load DICOM file: {e}")
+ return None
+
+ @staticmethod
+ def crop_black_regions(image: np.ndarray) -> np.ndarray:
+ """
+ Crop a fixed number of rows from top and bottom of the image.
+
+ Args:
+ image: Input image as numpy array
+
+ Returns:
+ Cropped image with specified rows removed
+ """
+ # Define fixed number of rows to crop from top and bottom
+ crop_top = 175 # Number of rows to crop from top
+ crop_bottom = 175 # Number of rows to crop from bottom
+
+ # Ensure we don't crop more than the image height
+ height = image.shape[0]
+ if crop_top + crop_bottom >= height:
+ # If we try to crop too much, crop half from each side
+ crop_top = crop_bottom = height // 4
+
+ # Crop the image
+ cropped_image = image[crop_top:height - crop_bottom, :]
+
+ print(f"DICOM cropped: {image.shape} -> {cropped_image.shape} (removed {crop_top} from top, {crop_bottom} from bottom)")
+ return cropped_image
diff --git a/src/qus/seg_loading/seg_loading_controller.py b/src/qus/seg_loading/seg_loading_controller.py
index 3ccd4fd..cd760c0 100644
--- a/src/qus/seg_loading/seg_loading_controller.py
+++ b/src/qus/seg_loading/seg_loading_controller.py
@@ -32,6 +32,7 @@ def __init__(self, model: Optional[ApplicationModel] = None, custom_view=None):
if not image_data:
raise ValueError("No image loaded in ApplicationModel")
view = SegLoadingViewCoordinator(image_data)
+ view.controller = self
super().__init__(model, view)
@@ -128,7 +129,23 @@ def get_loaded_segmentation(self) -> BmodeSeg:
BmodeSeg: The loaded segmentation data, or None if no segmentation loaded
"""
return self.model.seg_data
+
+ def load_dicom_file(self, dicom_file_path: str) -> bool:
+ """
+ Load a DICOM file using the DicomLoader utility.
+ Args:
+ dicom_file_path (str): Path to the DICOM file to load
+
+ Returns:
+ bool: True if the DICOM file was loaded successfully, False otherwise
+ """
+ return self.model.load_dicom_file(dicom_file_path)
+
+ def get_dicom_data(self) -> dict:
+ """Get processed DICOM data from the model."""
+ return self.model.get_dicom_data()
+
def cleanup(self) -> None:
"""Clean up resources."""
self.model.cleanup()
diff --git a/src/qus/seg_loading/seg_loading_view_coordinator.py b/src/qus/seg_loading/seg_loading_view_coordinator.py
index 6a46f58..dadbcbe 100644
--- a/src/qus/seg_loading/seg_loading_view_coordinator.py
+++ b/src/qus/seg_loading/seg_loading_view_coordinator.py
@@ -46,6 +46,7 @@ class SegLoadingViewCoordinator(QStackedWidget):
def __init__(self, image_data: UltrasoundRfImage, parent: Optional[QWidget] = None):
super().__init__(parent)
self._image_data = image_data
+ self.controller = None
# Widget instances
self._seg_type_widget: Optional[SegTypeSelectionWidget] = None
@@ -193,6 +194,10 @@ def show_roi_drawing(self, frame=0) -> None:
# Create ROI drawing widget with brightness value
self._roi_drawing_widget = RoiDrawingWidget(self._image_data, frame, self._frame_brightness)
+ # Pass controller reference for DICOM loading via model
+ if hasattr(self, 'controller') and self.controller:
+ self._roi_drawing_widget._parent_controller = self.controller
+
# Connect signals
self._roi_drawing_widget.seg_file_selected.connect(self._on_file_selected)
self._roi_drawing_widget.back_requested.connect(self._on_roi_draw_back)
diff --git a/src/qus/seg_loading/ui/roi_drawing.ui b/src/qus/seg_loading/ui/roi_drawing.ui
index fb3989b..4d73186 100644
--- a/src/qus/seg_loading/ui/roi_drawing.ui
+++ b/src/qus/seg_loading/ui/roi_drawing.ui
@@ -528,47 +528,64 @@
background: rgb(90, 37, 255);
border-radius: 15px;
}
-
-
- Back
-
-
-
-
-
-
-
-
- -
-
-
- 10
-
-
- 30
-
-
- 10
-
-
- 30
-
-
- 10
-
-
-
-
-
- 5
-
-
-
-
-
-
-
-
-
-
-
- QLabel {
+
+
+ Back
+
+
+
+
+
+
+ -
+
+
+ QLabel {
+ color: rgb(0, 255, 0);
+ font-size: 20px;
+ background-color: rgba(255, 255, 255, 0);
+}
+
+
+ LOADING....
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ -
+
+
+ 10
+
+
+ 30
+
+
+ 10
+
+
+ 30
+
+
+ 10
+
+
-
+
+
+ 5
+
+
-
+
+
-
+
+
-
+
+
+ QLabel {
font-size: 18px;
color: rgb(255, 255, 255);
background-color: rgba(255, 255, 255, 0);
@@ -855,50 +872,337 @@
color: rgb(255, 255, 255);
background-color: rgba(255, 255, 255, 0);
}
-
-
- 0
-
-
- Qt::TextFormat::AutoText
-
-
- false
-
-
- Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter
-
-
- true
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
- 241
- 41
-
-
-
-
- 241
- 41
-
-
-
- QPushButton {
+
+
+ 0
+
+
+ Qt::AutoText
+
+
+ false
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+ 10
+
+
-
+
+
+
+ 80
+ 41
+
+
+
+
+ 80
+ 41
+
+
+
+ QLabel {
+ font-size: 15px;
+ color: rgb(255, 255, 255);
+ background-color: rgba(255, 255, 255, 0);
+}
+
+
+ Brightness:
+
+
+ Qt::AlignRight|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 200
+ 41
+
+
+
+
+ 200
+ 41
+
+
+
+ QSlider::groove:horizontal {
+ border: 1px solid #999999;
+ height: 8px;
+ background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #B1B1B1, stop:1 #c4c4c4);
+ margin: 2px 0;
+}
+QSlider::handle:horizontal {
+ background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #b4b4b4, stop:1 #8f8f8f);
+ border: 1px solid #5c5c5c;
+ width: 18px;
+ margin: 2px 0;
+ border-radius: 3px;
+}
+
+
+ 0
+
+
+ 100
+
+
+ 0
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+
+ 40
+ 41
+
+
+
+
+ 40
+ 41
+
+
+
+ QLabel {
+ font-size: 15px;
+ color: rgb(255, 255, 255);
+ background-color: rgba(255, 255, 255, 0);
+}
+
+
+ 0
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ -
+
+
+ 10
+
+
-
+
+
+
+ 200
+ 41
+
+
+
+
+ 200
+ 41
+
+
+
+ QCheckBox {
+ color: rgb(255, 255, 255);
+ font-size: 15px;
+ background-color: rgba(255, 255, 255, 0);
+ border: 0px;
+}
+QCheckBox::indicator {
+ width: 20px;
+ height: 20px;
+ border-radius: 10px;
+ background-color: rgb(90, 37, 255);
+ border: 2px solid rgb(255, 255, 255);
+}
+QCheckBox::indicator:checked {
+ background-color: rgb(90, 37, 255);
+ border: 2px solid rgb(255, 255, 255);
+}
+
+
+ Show DICOM Overlay
+
+
+
+ -
+
+
+
+ 100
+ 41
+
+
+
+
+ 100
+ 41
+
+
+
+ QLabel {
+ font-size: 15px;
+ color: rgb(255, 255, 255);
+ background-color: rgba(255, 255, 255, 0);
+}
+
+
+ Transparency:
+
+
+ Qt::AlignRight|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 200
+ 41
+
+
+
+
+ 200
+ 41
+
+
+
+ QSlider::groove:horizontal {
+ border: 1px solid #999999;
+ height: 8px;
+ background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #B1B1B1, stop:1 #c4c4c4);
+ margin: 2px 0;
+}
+QSlider::handle:horizontal {
+ background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #b4b4b4, stop:1 #8f8f8f);
+ border: 1px solid #5c5c5c;
+ width: 18px;
+ margin: 2px 0;
+ border-radius: 3px;
+}
+
+
+ 0
+
+
+ 100
+
+
+ 50
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+
+ 40
+ 41
+
+
+
+
+ 40
+ 41
+
+
+
+ QLabel {
+ font-size: 15px;
+ color: rgb(255, 255, 255);
+ background-color: rgba(255, 255, 255, 0);
+}
+
+
+ 50
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ -
+
+
+
+ 241
+ 41
+
+
+
+
+ 241
+ 41
+
+
+
+ QPushButton {
+ color: white;
+ font-size: 16px;
+ background: rgb(90, 37, 255);
+ border-radius: 15px;
+}
+QPushButton:hover {
+ background-color: rgb(120, 67, 255);
+}
+QPushButton:pressed {
+ background-color: rgb(60, 17, 195);
+}
+
+
+ Load DICOM File
+
+
+
+ -
+
+
-
+
+
+
+ 241
+ 41
+
+
+
+
+ 241
+ 41
+
+
+
+ QPushButton {
color: white;
font-size: 16px;
background: rgb(90, 37, 255);
diff --git a/src/qus/seg_loading/views/roi_drawing_widget.py b/src/qus/seg_loading/views/roi_drawing_widget.py
index 4667bad..73d93b4 100644
--- a/src/qus/seg_loading/views/roi_drawing_widget.py
+++ b/src/qus/seg_loading/views/roi_drawing_widget.py
@@ -12,7 +12,8 @@
import scipy.interpolate as interpolate
from PyQt6.QtCore import pyqtSignal, Qt
-from PyQt6.QtWidgets import QWidget, QHBoxLayout, QFileDialog, QSlider, QLabel, QVBoxLayout
+from PyQt6.QtWidgets import QApplication
+from PyQt6.QtWidgets import QWidget, QHBoxLayout, QFileDialog, QSlider, QLabel, QCheckBox, QPushButton
from src.qus.mvc.base_view import BaseViewMixin
from src.qus.seg_loading.ui.roi_drawing_ui import Ui_constructRoi
@@ -52,6 +53,7 @@ def __init__(self, image_data: UltrasoundRfImage, frame: int = 0, brightness: in
self._drawing = False # Flag to track if drawing is in progress
self._frame = frame # Frame number for multi-frame images
self._displayed_im: np.ndarray = None # Placeholder for the image to be displayed
+ self._dicom_available = False # Track if DICOM data is available
# Brightness control variables
self._brightness_slider: Optional[QSlider] = None
@@ -65,6 +67,17 @@ def __init__(self, image_data: UltrasoundRfImage, frame: int = 0, brightness: in
# Drawing parameters
self._min_point_distance = 5.0 # Minimum distance between points in pixels
+
+ # DICOM overlay control variables
+ self._dicom_overlay_checkbox: Optional[QCheckBox] = None
+ self._transparency_slider: Optional[QSlider] = None
+ self._transparency_label: Optional[QLabel] = None
+ self._transparency_value_label: Optional[QLabel] = None
+ self._load_dicom_button: Optional[QPushButton] = None
+ self._overlay_enabled = False # Track if overlay is currently enabled
+ self._transparency_value = 50 # Default transparency (0-100)
+ self._original_bmode_im = None # Store original B-mode for overlay blending
+ self._parent_controller = None # Reference to controller for model access
self._setup_ui()
self._connect_signals()
@@ -138,6 +151,18 @@ def set_min_point_distance(self, distance: float) -> None:
raise ValueError("Minimum distance must be non-negative")
self._min_point_distance = distance
+ def show_loading(self) -> None:
+ """Show the loading screen label."""
+ super().show_loading()
+ if hasattr(self._ui, 'loading_screen_label'):
+ self._ui.loading_screen_label.show()
+
+ def hide_loading(self) -> None:
+ """Hide the loading screen label."""
+ super().hide_loading()
+ if hasattr(self._ui, 'loading_screen_label'):
+ self._ui.loading_screen_label.hide()
+
def get_min_point_distance(self) -> float:
"""
Get the current minimum distance between points.
@@ -147,6 +172,75 @@ def get_min_point_distance(self) -> float:
"""
return self._min_point_distance
+ def _apply_dicom_overlay(self, bmode_image: np.ndarray) -> np.ndarray:
+ """
+ Apply DICOM overlay with adjustable transparency to B-mode image.
+
+ Args:
+ bmode_image: The RF-derived B-mode image to blend with DICOM
+
+ Returns:
+ Blended image with DICOM overlay applied if enabled, otherwise original B-mode
+ """
+ if not self._overlay_enabled:
+ return bmode_image
+
+ dicom_data = self._parent_controller.get_dicom_data()
+ self._dicom_available = dicom_data['available']
+
+ if not dicom_data or not self._dicom_available:
+ return bmode_image
+
+ dicom_image = dicom_data.get('image')
+ if dicom_image is None:
+ return bmode_image
+
+ try:
+ # Get transparency value (0-100) and convert to alpha (0.0-1.0)
+ alpha = self._transparency_value / 100.0
+
+ # Ensure both images have the same shape
+ dicom_im = dicom_image
+
+ if dicom_im.shape != bmode_image.shape:
+ # Resize DICOM to match B-mode if needed
+ from skimage.transform import resize
+ dicom_im = resize(dicom_im, bmode_image.shape, preserve_range=True, anti_aliasing=True)
+ dicom_im = dicom_im.astype(np.uint8)
+
+ # Normalize both images to 0-255 range if needed
+ bmode_norm = bmode_image.astype(np.float32)
+ dicom_norm = dicom_im.astype(np.float32)
+
+ # Apply direct alpha blending: output = (1-alpha) * Bmode + alpha * DICOM
+ # At alpha=0 (transparency=0): output = Bmode (only B-mode visible)
+ # At alpha=1 (transparency=100): output = DICOM (only DICOM visible)
+ blended = (1.0 - alpha) * bmode_norm + alpha * dicom_norm
+
+ # Clip and convert back to uint8
+ blended = np.clip(blended, 0, 255).astype(np.uint8)
+
+ return blended
+
+ except Exception as e:
+ print(f"Warning: Failed to apply DICOM overlay: {e}")
+ return bmode_image
+
+ def _update_overlay_display(self) -> None:
+ """
+ Update the display when overlay settings change.
+ This method handles the real-time overlay updates.
+ """
+ if self._original_bmode_im is not None:
+ # 1. Apply overlay to a copy of the original B-mode image
+ blended_im = self._apply_dicom_overlay(self._original_bmode_im.copy())
+
+ # 2. Apply brightness adjustment to the blended result
+ self._displayed_im = self._apply_brightness_adjustment(blended_im)
+
+ # 3. Update the matplotlib display
+ self._update_display()
+
def _update_drawing_status(self) -> None:
"""Update the drawing status to show current settings."""
if self._current_drawing_mode == 'points':
@@ -216,6 +310,11 @@ def _setup_ui(self) -> None:
'physical_roi_dims_label', 'physical_roi_height_label', 'physical_roi_height_val',
'physical_roi_width_label', 'physical_roi_width_val'
]
+ self._dicom_menu_objects = [
+ 'dicom_overlay_checkbox', 'transparency_slider',
+ 'transparency_label', 'transparency_value_label',
+ 'load_dicom_button'
+ ]
# Setup matplotlib canvas for ROI drawing
self._setup_matplotlib_canvas()
@@ -223,9 +322,13 @@ def _setup_ui(self) -> None:
# Add brightness control
self._setup_brightness_control()
+ # Add DICOM overlay control
+ self._setup_dicom_overlay_control()
+
# Display image for ROI drawing
self._display_image_for_roi()
+ self._ui.loading_screen_label.hide()
self._hide_save_menu()
self._show_draw_type_selection()
self._hide_roi_dims()
@@ -245,66 +348,43 @@ def _setup_matplotlib_canvas(self) -> None:
def _setup_brightness_control(self) -> None:
"""Setup brightness control slider and label."""
- # Create a horizontal layout for brightness control
- brightness_layout = QHBoxLayout()
-
- # Create brightness label
- self._brightness_label = QLabel("Brightness:")
- self._brightness_label.setStyleSheet("""
- QLabel {
- font-size: 15px;
- color: rgb(255, 255, 255);
- background-color: rgba(255, 255, 255, 0);
- }
- """)
- self._brightness_label.setMinimumSize(80, 41)
- self._brightness_label.setMaximumSize(80, 41)
- self._brightness_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
-
- # Create brightness slider
- self._brightness_slider = QSlider()
- self._brightness_slider.setOrientation(Qt.Orientation.Horizontal)
- self._brightness_slider.setRange(0, 100)
+ self._brightness_label = self._ui.brightness_label
+ self._brightness_slider = self._ui.brightness_slider
+ self._brightness_value_label = self._ui.brightness_value_label
+
+ # Initialize values
self._brightness_slider.setValue(self._brightness_value)
- self._brightness_slider.setMinimumSize(200, 41)
- self._brightness_slider.setMaximumSize(200, 41)
- self._brightness_slider.setStyleSheet("""
- QSlider::groove:horizontal {
- border: 1px solid #999999;
- height: 8px;
- background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #B1B1B1, stop:1 #c4c4c4);
- margin: 2px 0;
- }
- QSlider::handle:horizontal {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #b4b4b4, stop:1 #8f8f8f);
- border: 1px solid #5c5c5c;
- width: 18px;
- margin: 2px 0;
- border-radius: 3px;
- }
- """)
-
- # Create brightness value label
- self._brightness_value_label = QLabel(str(self._brightness_value))
- self._brightness_value_label.setStyleSheet("""
- QLabel {
- font-size: 15px;
- color: rgb(255, 255, 255);
- background-color: rgba(255, 255, 255, 0);
- }
- """)
- self._brightness_value_label.setMinimumSize(30, 41)
- self._brightness_value_label.setMaximumSize(30, 41)
- self._brightness_value_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
-
- # Add widgets to layout
- brightness_layout.addWidget(self._brightness_label)
- brightness_layout.addWidget(self._brightness_slider)
- brightness_layout.addWidget(self._brightness_value_label)
-
- # Add brightness control to the draw_roi_layout
- # Insert it at the beginning of the layout (after the title)
- self._ui.draw_roi_layout.insertLayout(1, brightness_layout)
+ self._brightness_value_label.setText(str(self._brightness_value))
+
+ def _setup_dicom_overlay_control(self) -> None:
+ """Setup DICOM overlay control checkbox and transparency slider."""
+ # Get references to the UI controls from the .ui file
+ self._dicom_overlay_checkbox = self._ui.dicom_overlay_checkbox
+ self._transparency_slider = self._ui.transparency_slider
+ self._transparency_label = self._ui.transparency_label
+ self._transparency_value_label = self._ui.transparency_value_label
+ self._load_dicom_button = self._ui.load_dicom_button
+
+ if not self._dicom_available:
+ # Hide overlay controls if DICOM is not available
+ self._dicom_overlay_checkbox.hide()
+ self._transparency_slider.hide()
+ self._transparency_label.hide()
+ self._transparency_value_label.hide()
+ # Show Load DICOM button
+ self._load_dicom_button.show()
+ else:
+ # Show overlay controls and set initial values
+ self._dicom_overlay_checkbox.show()
+ self._dicom_overlay_checkbox.setChecked(False) # Start with overlay disabled
+ self._transparency_slider.show()
+ self._transparency_slider.setValue(50) # Default transparency
+ self._transparency_slider.setEnabled(False) # Disabled until overlay is enabled
+ self._transparency_label.show()
+ self._transparency_value_label.show()
+ self._transparency_value_label.setText("50")
+ # Hide Load DICOM button since DICOM is already loaded
+ self._load_dicom_button.hide()
def _connect_signals(self) -> None:
"""Connect UI signals to internal handlers."""
@@ -322,40 +402,120 @@ def _connect_signals(self) -> None:
self._ui.back_from_save_button.clicked.connect(self._show_draw_type_selection)
# Connect brightness slider
- if self._brightness_slider:
- self._brightness_slider.valueChanged.connect(self._on_brightness_changed)
+ self._brightness_slider.valueChanged.connect(self._on_brightness_changed)
+
+ # Connect DICOM overlay controls
+ self._dicom_overlay_checkbox.toggled.connect(self._on_overlay_toggled)
+ self._transparency_slider.valueChanged.connect(self._on_transparency_changed)
+ self._load_dicom_button.clicked.connect(self._on_load_dicom_clicked)
def _on_brightness_changed(self, value: int) -> None:
"""Handle brightness slider change."""
self._brightness_value = value
if self._brightness_value_label:
self._brightness_value_label.setText(str(value))
- self._apply_brightness_adjustment()
- self._update_display()
+
+ # Refresh the entire image display pipeline
+ self._update_overlay_display()
- def _apply_brightness_adjustment(self) -> None:
- """Apply brightness adjustment to the displayed image using exponential function."""
- if self._original_displayed_im is None:
- return
+ def _on_overlay_toggled(self, checked: bool) -> None:
+ """Handle DICOM overlay checkbox toggle."""
+ self._overlay_enabled = checked
+
+ # Enable/disable transparency slider based on overlay state
+ if self._transparency_slider:
+ self._transparency_slider.setEnabled(checked)
+
+ # Update display with new overlay state
+ self._update_overlay_display()
+
+ def _on_transparency_changed(self, value: int) -> None:
+ """Handle transparency slider change."""
+ self._transparency_value = value
+ if self._transparency_value_label:
+ self._transparency_value_label.setText(str(value))
+
+ # Update display if overlay is enabled
+ if self._overlay_enabled:
+ self._update_overlay_display()
+
+ def _on_load_dicom_clicked(self) -> None:
+ """Handle Load DICOM button click."""
+ # Open file dialog to select DICOM file
+ dicom_file, _ = QFileDialog.getOpenFileName(
+ self,
+ "Select DICOM File",
+ "",
+ "DICOM Files (*.dcm *.dicom *.DICOM);;All Files (*)"
+ )
+
+ if dicom_file:
+ # Show loading text
+ self.show_loading()
+ QApplication.processEvents()
+
+ success = self._parent_controller.load_dicom_file(dicom_file)
+
+ # Hide loading text
+ self.hide_loading()
- # Calculate exponential coefficient based on brightness slider (0-100)
- # Map 0-100 to 0.05-5.0 for more intense exponential range
- exp_coefficient = 0.05 + (self._brightness_value / 100.0) * 4.95
+ if success:
+ # Successfully loaded - update UI
+ if self._load_dicom_button:
+ self._load_dicom_button.hide()
+ if self._dicom_overlay_checkbox:
+ self._dicom_overlay_checkbox.show()
+ self._dicom_overlay_checkbox.setChecked(False)
+ if self._transparency_slider:
+ self._transparency_slider.show()
+ self._transparency_slider.setValue(50)
+ self._transparency_slider.setEnabled(False)
+ if self._transparency_label:
+ self._transparency_label.show()
+ if self._transparency_value_label:
+ self._transparency_value_label.show()
+ self._transparency_value_label.setText("50")
+
+ # Update display to refresh overlay visibility state
+ self._update_overlay_display()
+ else:
+ # Failed to load - show error message
+ from PyQt6.QtWidgets import QMessageBox
+ QMessageBox.warning(
+ self,
+ "DICOM Load Error",
+ "Failed to load the selected DICOM file. Please check the file format and try again."
+ )
+
+ def _apply_brightness_adjustment(self, image: np.ndarray) -> np.ndarray:
+ """
+ Apply brightness adjustment to an image using a gamma/power function.
- # Apply exponential brightness adjustment
- adjusted_im = self._original_displayed_im.astype(np.float32)
+ Args:
+ image: Input image (0-255 uint8)
+
+ Returns:
+ Adjusted image (0-255 uint8)
+ """
+ if image is None:
+ return None
+
+ # Map brightness slider (0-100) to gamma value
+ # 50 is the identity (no change)
+ # Higher than 50 increases brightness (lower gamma)
+ # Lower than 50 decreases brightness (higher gamma)
+ # Range: ~0.1 to ~10.0
+ exponent = 10.0 ** ((50 - self._brightness_value) / 50.0)
- # Normalize to 0-1 range for exponential operation
- normalized_im = adjusted_im / 255.0
+ # Normalize to 0-1 range for power operation
+ normalized_im = image.astype(np.float32) / 255.0
- # Apply exponential function: I_out = I_in^exp_coefficient
- # This will make the image brighter as exp_coefficient increases
- adjusted_im = np.power(normalized_im, 1.0 / exp_coefficient)
+ # Apply power function: I_out = I_in^exponent
+ adjusted_im = np.power(normalized_im, exponent)
# Scale back to 0-255 range and clip
- adjusted_im = adjusted_im * 255.0
- adjusted_im = np.clip(adjusted_im, 0, 255)
- self._displayed_im = adjusted_im.astype(np.uint8)
+ adjusted_im = np.clip(adjusted_im * 255.0, 0, 255).astype(np.uint8)
+ return adjusted_im
def _update_display(self) -> None:
"""Update the image display with current brightness."""
@@ -415,13 +575,21 @@ def _display_image_for_roi(self) -> None:
im = self._image_data.sc_bmode if self._image_data.sc_bmode is not None else self._image_data.bmode
if im.ndim == 2:
self._original_displayed_im = im.copy()
+ self._original_bmode_im = im.copy() # Store original B-mode for overlay
else:
if self._frame < 0 or self._frame >= im.shape[0]:
raise ValueError(f"Frame {self._frame} is out of bounds for image with {im.shape[0]} frames")
self._original_displayed_im = im[self._frame].copy()
+ self._original_bmode_im = im[self._frame].copy() # Store original B-mode for overlay
+
+ # Apply DICOM overlay first (if enabled)
+ if self._overlay_enabled:
+ blended_im = self._apply_dicom_overlay(self._original_bmode_im.copy())
+ else:
+ blended_im = self._original_bmode_im.copy()
- # Apply initial brightness adjustment
- self._apply_brightness_adjustment()
+ # Apply brightness adjustment on top
+ self._displayed_im = self._apply_brightness_adjustment(blended_im)
self._plot_im_on_ax(self._ax)
self._matplotlib_canvas.draw()
@@ -715,6 +883,9 @@ def _show_save_menu(self) -> None:
self._hide_draw_freehand_drag()
self._hide_draw_rect_drag()
self._hide_draw_pts()
+ self._hide_draw_type_selection()
+ self._hide_dicom_controls()
+
try:
self._ax.figure.canvas.mpl_disconnect(self._cid_press)
except Exception:
@@ -753,6 +924,15 @@ def _hide_draw_type_selection(self) -> None:
else:
print(f"Warning: Widget '{obj_name}' not found in UI")
+ def _hide_dicom_controls(self) -> None:
+ """Hide the DICOM controls."""
+ for obj_name in self._dicom_menu_objects:
+ widget = getattr(self._ui, obj_name, None)
+ if widget:
+ widget.hide()
+ else:
+ print(f"Warning: Widget '{obj_name}' not found in UI")
+
def _show_draw_type_selection(self) -> None:
"""Show the draw type selection layout."""
# Remove the current ROI
@@ -794,6 +974,7 @@ def _show_draw_type_selection(self) -> None:
self._hide_draw_freehand_drag()
self._hide_draw_rect_drag()
self._hide_draw_pts()
+ self._setup_dicom_overlay_control()
for obj_name in self._draw_types_objects:
widget = getattr(self._ui, obj_name, None)