diff --git a/engines/ceus b/engines/ceus
index 97d3d3a..b8044e4 160000
--- a/engines/ceus
+++ b/engines/ceus
@@ -1 +1 @@
-Subproject commit 97d3d3a8b03ee02bc5bda8dca2f1f316151d8210
+Subproject commit b8044e4a88136d642c0cc180c6f77e8a1d36ecec
diff --git a/engines/qus b/engines/qus
index d5531ed..ac6e2f0 160000
--- a/engines/qus
+++ b/engines/qus
@@ -1 +1 @@
-Subproject commit d5531ed6f48149931fb83cd33960b273517dd4c8
+Subproject commit ac6e2f0b8d13b6065a4cda9e889f3d48c5f070c9
diff --git a/requirements.txt b/requirements.txt
index 63bcec3..97b7a07 100644
Binary files a/requirements.txt and b/requirements.txt differ
diff --git a/src/ceus/application_model.py b/src/ceus/application_model.py
index 2ae47fc..b98621b 100644
--- a/src/ceus/application_model.py
+++ b/src/ceus/application_model.py
@@ -88,6 +88,7 @@ class ApplicationModel(BaseModel):
# Additional signals for application-specific events
image_loaded = pyqtSignal(UltrasoundImage)
+ bmode_image_loaded = pyqtSignal(UltrasoundImage)
segmentation_loaded = pyqtSignal(CeusSeg)
def __init__(self):
@@ -97,7 +98,10 @@ def __init__(self):
self._scan_loaders: Dict[str, Any] = {}
self._selected_scan_type: Optional[str] = None
self._image_data: Optional[UltrasoundImage] = None
+ self._bmode_image_data: Optional[UltrasoundImage] = None
self._scan_worker: Optional[ScanLoadingWorker] = None
+ self._bmode_scan_worker: Optional[ScanLoadingWorker] = None
+ self._pending_bmode_load: bool = False # True when B-mode load is in progress
# Segmentation loading state
self._seg_loaders: Dict[str, Any] = {}
@@ -148,6 +152,11 @@ def image_data(self) -> Optional[UltrasoundImage]:
"""Get the currently loaded image data."""
return self._image_data
+ @property
+ def bmode_image_data(self) -> Optional[UltrasoundImage]:
+ """Get the currently loaded B-mode image data."""
+ return self._bmode_image_data
+
def set_scan_type(self, scan_type_display_name: str) -> bool:
"""
Set the selected scan type.
@@ -211,7 +220,7 @@ def get_image_loading_options(self) -> list:
def load_image(self, image_path: str, scan_loader_kwargs: Dict[str, Any] = None) -> None:
"""
Load scan image data.
-
+
Args:
image_path: Path to image file
scan_loader_kwargs: Additional loader arguments (optional)
@@ -219,7 +228,12 @@ def load_image(self, image_path: str, scan_loader_kwargs: Dict[str, Any] = None)
if not self._selected_scan_type:
self._emit_error("No scan type selected")
return
-
+
+ # Reset state for new load cycle
+ self._image_data = None
+ self._bmode_image_data = None
+ self._pending_bmode_load = False
+
if scan_loader_kwargs is None:
scan_loader_kwargs = {}
@@ -337,6 +351,31 @@ def enhance_image(self, image: UltrasoundImage, func_configs: List[Dict[str, Any
print(f"DEBUG: enhance_image error: {e}")
return image
+ def compute_ceus_noise_floor(self, image_data: UltrasoundImage, n_ref_frames: int, noise_std_multiplier: float) -> float:
+ """
+ Compute the noise floor from pre-contrast frames.
+
+ Args:
+ image_data: UltrasoundImage object
+ n_ref_frames: Number of reference frames
+ noise_std_multiplier: Multiplier for standard deviation
+
+ Returns:
+ float: The computed noise floor
+ """
+ try:
+ funcs = self.get_preprocessing_options()
+ if 'compute_ceus_noise_floor' in funcs:
+ return funcs['compute_ceus_noise_floor'](
+ image_data,
+ n_ref_frames=n_ref_frames,
+ noise_std_multiplier=noise_std_multiplier
+ )
+ return 0.0
+ except Exception as e:
+ print(f"WARNING: compute_ceus_noise_floor failed in model: {e}")
+ return 0.0
+
def _validate_image_input(self, input_data: Dict[str, Any]) -> bool:
"""
Validate input data for scan loading.
@@ -370,16 +409,14 @@ def _validate_image_input(self, input_data: Dict[str, Any]) -> bool:
def _on_image_loading_complete(self, image_data: UltrasoundImage) -> None:
"""
Handle completion of scan loading.
-
+
Args:
image_data: Loaded ultrasound image data
"""
- self._set_loading(False)
-
# Check if loading was successful
if isinstance(image_data, UltrasoundImage):
self._image_data = image_data
-
+
# Print NIfTI information if applicable
scan_path = getattr(image_data, 'scan_path', '')
if scan_path and scan_path.lower().endswith(('.nii', '.nii.gz')):
@@ -389,9 +426,14 @@ def _on_image_loading_complete(self, image_data: UltrasoundImage) -> None:
print(f"Pixel Dimensions: {getattr(image_data, 'pixdim', 'Unknown')}")
print(f"Frame Rate: {getattr(image_data, 'frame_rate', 'Unknown')}")
print(f"----------------------------------------\n")
-
+
+ # If B-mode is still loading, wait for it before emitting
+ if self._pending_bmode_load:
+ return
+ self._set_loading(False)
self.image_loaded.emit(image_data)
else:
+ self._set_loading(False)
print(f"DEBUG: Image loading failed - invalid image data:")
print(f" - scan_path: {getattr(image_data, 'scan_path', 'Missing')}")
print(f" - has pixel_data: {hasattr(image_data, 'pixel_data')}")
@@ -399,7 +441,66 @@ def _on_image_loading_complete(self, image_data: UltrasoundImage) -> None:
print(f" - has intensity: {hasattr(image_data, 'intensities_for_analysis')}")
print(f" - intensities_for_analysis is None: {getattr(image_data, 'intensities_for_analysis', None) is None}")
self._emit_error("Failed to load image data - image loading was unsuccessful")
-
+
+ def load_bmode_image(self, bmode_path: str, scan_loader_kwargs: Dict[str, Any] = None) -> None:
+ """
+ Load B-mode image data in the background.
+
+ Args:
+ bmode_path: Path to B-mode image file
+ scan_loader_kwargs: Additional loader arguments (optional)
+ """
+ if not self._selected_scan_type:
+ self._emit_error("No scan type selected for B-mode loading")
+ return
+
+ if scan_loader_kwargs is None:
+ scan_loader_kwargs = {}
+
+ if not os.path.exists(bmode_path):
+ self._emit_error(f"B-mode file not found: {bmode_path}")
+ return
+
+ self._pending_bmode_load = True
+
+ # Stop any existing B-mode worker
+ if self._bmode_scan_worker and self._bmode_scan_worker.isRunning():
+ self._bmode_scan_worker.quit()
+ self._bmode_scan_worker.wait()
+
+ self._bmode_scan_worker = ScanLoadingWorker(
+ self._selected_scan_type,
+ bmode_path,
+ scan_loader_kwargs
+ )
+
+ self._bmode_scan_worker.finished.connect(self._on_bmode_loading_complete)
+ self._bmode_scan_worker.error_msg.connect(self._on_bmode_loading_error)
+ self._bmode_scan_worker.start()
+
+ def _on_bmode_loading_complete(self, image_data: UltrasoundImage) -> None:
+ """Handle completion of B-mode image loading."""
+ self._pending_bmode_load = False
+ if isinstance(image_data, UltrasoundImage):
+ self._bmode_image_data = image_data
+ self.bmode_image_loaded.emit(image_data)
+ else:
+ self._emit_error("Failed to load B-mode image data")
+
+ # If CEUS already finished loading, emit image_loaded now
+ if self._image_data is not None:
+ self._set_loading(False)
+ self.image_loaded.emit(self._image_data)
+
+ def _on_bmode_loading_error(self, error_msg: str) -> None:
+ """Handle B-mode loading error — proceed with CEUS only."""
+ self._pending_bmode_load = False
+ self._emit_error(error_msg)
+ # Still allow proceeding with CEUS image if it loaded successfully
+ if self._image_data is not None:
+ self._set_loading(False)
+ self.image_loaded.emit(self._image_data)
+
# Segmentation Loading Properties and Methods
@property
def seg_loaders(self) -> Dict[str, Any]:
@@ -551,7 +652,12 @@ def cleanup(self) -> None:
self._scan_worker.quit()
self._scan_worker.wait()
self._scan_worker = None
-
+
+ if self._bmode_scan_worker and self._bmode_scan_worker.isRunning():
+ self._bmode_scan_worker.quit()
+ self._bmode_scan_worker.wait()
+ self._bmode_scan_worker = None
+
if self._seg_worker and self._seg_worker.isRunning():
self._seg_worker.quit()
self._seg_worker.wait()
diff --git a/src/ceus/image_loading/image_loading_controller.py b/src/ceus/image_loading/image_loading_controller.py
index e8ff825..977dfd1 100644
--- a/src/ceus/image_loading/image_loading_controller.py
+++ b/src/ceus/image_loading/image_loading_controller.py
@@ -61,7 +61,7 @@ def _handle_scan_type_selection(self, scan_type_name: str) -> None:
def _handle_image_loading(self, load_data: dict) -> None:
"""
Handle image loading request.
-
+
Args:
load_data: Dictionary with loading parameters
"""
@@ -70,7 +70,15 @@ def _handle_image_loading(self, load_data: dict) -> None:
image_path=load_data['image_path'],
scan_loader_kwargs=load_data['scan_loader_kwargs']
)
-
+
+ # Load B-mode image if path was provided
+ bmode_path = load_data.get('bmode_path')
+ if bmode_path:
+ self.model.load_bmode_image(
+ bmode_path=bmode_path,
+ scan_loader_kwargs=load_data['scan_loader_kwargs']
+ )
+
except Exception as e:
print(f"DEBUG: Error in image loading: {e}")
import traceback
diff --git a/src/ceus/image_loading/ui/file_selection.ui b/src/ceus/image_loading/ui/file_selection.ui
index a926a8c..4ea3e81 100644
--- a/src/ceus/image_loading/ui/file_selection.ui
+++ b/src/ceus/image_loading/ui/file_selection.ui
@@ -474,7 +474,7 @@
-
-
+
20
@@ -656,6 +656,119 @@
+ -
+
+
+ 20
+
+
+ 20
+
+
-
+
+
+ QLabel {
+ background-color: rgba(255, 255, 255, 0);
+ color: white;
+ font-size: 17px;
+}
+
+
+ Input Path to B-mode Image (optional)
+
+
+ Qt::AlignCenter
+
+
+ Qt::NoTextInteraction
+
+
+
+ -
+
+
+
+ 201
+ 31
+
+
+
+
+ 401
+ 31
+
+
+
+ QLineEdit {
+ background-color: rgb(249, 249, 249);
+ color: black;
+}
+
+
+
+ -
+
+
+ 6
+
+
-
+
+
+
+ 131
+ 41
+
+
+
+
+ 131
+ 41
+
+
+
+ QPushButton {
+ color: white;
+ font-size: 16px;
+ background: rgb(90, 37, 255);
+ border-radius: 15px;
+}
+
+
+ Choose File
+
+
+
+ -
+
+
+
+ 131
+ 41
+
+
+
+
+ 131
+ 41
+
+
+
+ QPushButton {
+ color: white;
+ font-size: 16px;
+ background: rgb(90, 37, 255);
+ border-radius: 15px;
+}
+
+
+ Clear Path
+
+
+
+
+
+
+
-
diff --git a/src/ceus/image_loading/views/file_selection_widget.py b/src/ceus/image_loading/views/file_selection_widget.py
index 5b095c6..b2c1af1 100644
--- a/src/ceus/image_loading/views/file_selection_widget.py
+++ b/src/ceus/image_loading/views/file_selection_widget.py
@@ -53,6 +53,8 @@ def _setup_ui(self) -> None:
self._loading_widgets = [
self._ui.choose_image_path_button,
self._ui.clear_image_path_button,
+ self._ui.choose_bmode_path_button,
+ self._ui.clear_bmode_path_button,
self._ui.generate_image_button,
self._ui.back_button
]
@@ -76,6 +78,8 @@ def _connect_signals(self) -> None:
"""Connect UI signals to internal handlers."""
self._ui.choose_image_path_button.clicked.connect(self._on_choose_image_path)
self._ui.clear_image_path_button.clicked.connect(self._ui.image_path_input.clear)
+ self._ui.choose_bmode_path_button.clicked.connect(self._on_choose_bmode_path)
+ self._ui.clear_bmode_path_button.clicked.connect(self._ui.bmode_path_input.clear)
self._ui.generate_image_button.clicked.connect(self._on_generate_image)
self._ui.back_button.clicked.connect(self._on_back_clicked)
@@ -122,6 +126,15 @@ def _on_choose_image_path(self) -> None:
self._ui.image_path_input.setText(dir_name)
else:
self._select_file_helper(self._ui.image_path_input, self._file_extensions)
+
+ def _on_choose_bmode_path(self) -> None:
+ """Handle B-mode image file selection."""
+ if self._file_extensions == ["FOLDER"]:
+ dir_name = QFileDialog.getExistingDirectory(self, "Select B-mode Directory")
+ if dir_name:
+ self._ui.bmode_path_input.setText(dir_name)
+ else:
+ self._select_file_helper(self._ui.bmode_path_input, self._file_extensions)
def _on_generate_image(self) -> None:
"""Handle image generation request."""
@@ -160,10 +173,20 @@ def _on_generate_image(self) -> None:
scan_loader_kwargs[option] = value
- self.files_selected.emit({
+ file_data = {
'image_path': image_path,
'scan_loader_kwargs': scan_loader_kwargs
- })
+ }
+
+ # Include B-mode path if provided (optional)
+ bmode_path = self._ui.bmode_path_input.text().strip()
+ if bmode_path:
+ if not os.path.exists(bmode_path):
+ self.show_error(f"B-mode file does not exist: {os.path.basename(bmode_path)}")
+ return
+ file_data['bmode_path'] = bmode_path
+
+ self.files_selected.emit(file_data)
def _on_back_clicked(self) -> None:
"""Handle back button click."""
diff --git a/src/ceus/seg_loading/seg_loading_controller.py b/src/ceus/seg_loading/seg_loading_controller.py
index eab66f6..874df85 100644
--- a/src/ceus/seg_loading/seg_loading_controller.py
+++ b/src/ceus/seg_loading/seg_loading_controller.py
@@ -31,7 +31,7 @@ def __init__(self, model: Optional[ApplicationModel] = None, custom_view=None):
image_data = model.image_data
if not image_data:
raise ValueError("No image loaded in ApplicationModel")
- view = SegLoadingViewCoordinator(image_data)
+ view = SegLoadingViewCoordinator(image_data, bmode_image_data=model.bmode_image_data)
super().__init__(model, view)
@@ -66,6 +66,10 @@ def handle_user_action(self, action_name: str, action_data: Any) -> None:
self._handle_segmentation_loading(action_data)
elif action_name == 'apply_preprocs_preview':
self._handle_preprocs_preview(action_data)
+ elif action_name == 'compute_noise_floor':
+ self._handle_compute_noise_floor(action_data)
+ elif action_name == 'enhance_image_request':
+ self._handle_enhance_image_request(action_data)
elif action_name == 'segmentation_confirmed':
pass # Handle confirmation action in the application controller
else:
@@ -89,6 +93,26 @@ def _handle_preprocs_preview(self, preproc_data_list: list) -> None:
self.view.preview_modified_image(image_data, frame_ix)
+ def _handle_compute_noise_floor(self, data: dict) -> None:
+ """Handle noise floor computation request."""
+ value = self.model.compute_ceus_noise_floor(
+ data['image_data'],
+ data['n_ref_frames'],
+ data['noise_std_multiplier']
+ )
+ self.view.set_noise_floor(value)
+
+ def _handle_enhance_image_request(self, data: dict) -> None:
+ """Handle image enhancement request."""
+ enhanced = self.model.enhance_image(
+ data['image_data'],
+ data['func_configs']
+ )
+ # The enhanced image is returned; the view caller should handle usage
+ # This is primarily for the export worker which calls it synchronously
+ # through the coordinator's routed action.
+ data['callback'](enhanced)
+
def _handle_seg_type_selection(self, seg_type_name: str) -> None:
"""
Handle segmentation type selection.
diff --git a/src/ceus/seg_loading/seg_loading_view_coordinator.py b/src/ceus/seg_loading/seg_loading_view_coordinator.py
index 4f6d9d5..9635378 100644
--- a/src/ceus/seg_loading/seg_loading_view_coordinator.py
+++ b/src/ceus/seg_loading/seg_loading_view_coordinator.py
@@ -40,9 +40,10 @@ class SegLoadingViewCoordinator(QStackedWidget):
# ============================================================================
- def __init__(self, image_data: UltrasoundImage, parent: Optional[QWidget] = None):
+ def __init__(self, image_data: UltrasoundImage, bmode_image_data: Optional[UltrasoundImage] = None, parent: Optional[QWidget] = None):
super().__init__(parent)
self._image_data = image_data
+ self._bmode_image_data = bmode_image_data
# Widget instances
self._seg_type_widget: Optional[SegTypeSelectionWidget] = None
@@ -178,18 +179,42 @@ def show_file_selection(self, file_extensions: list) -> None:
def show_voi_drawing(self) -> None:
"""Show the VOI drawing widget."""
- self._voi_drawing_widget = DrawVOIWidget(self._image_data)
+ self._voi_drawing_widget = DrawVOIWidget(self._image_data, bmode_image_data=self._bmode_image_data)
# Connect signals to handle user actions
self._voi_drawing_widget.back_requested.connect(self.reset_to_seg_type_selection)
self._voi_drawing_widget.close_requested.connect(self.close_requested.emit)
self._voi_drawing_widget.apply_preprocs_preview.connect(self._on_preprocs_preview_requested)
+ self._voi_drawing_widget.compute_noise_floor_requested.connect(self._on_noise_floor_requested)
+ self._voi_drawing_widget.enhance_image_requested.connect(self._on_enhance_image_requested)
# Add to stack and show
self.addWidget(self._voi_drawing_widget)
self.setCurrentWidget(self._voi_drawing_widget)
+ def set_noise_floor(self, value: float) -> None:
+ """Route computed noise floor to the VOI drawing widget."""
+ if self._voi_drawing_widget:
+ self._voi_drawing_widget.set_noise_floor(value)
+
+ def _on_noise_floor_requested(self, image_data, n_ref, std_mult) -> None:
+ """Emit signal to controller to compute noise floor."""
+ self._emit_user_action('compute_noise_floor', {
+ 'image_data': image_data,
+ 'n_ref_frames': n_ref,
+ 'noise_std_multiplier': std_mult
+ })
+
+ def _on_enhance_image_requested(self, image_data, func_configs, callback=None) -> None:
+ """Emit signal to controller to enhance an image (used by export)."""
+ self._emit_user_action('enhance_image_request', {
+ 'image_data': image_data,
+ 'func_configs': func_configs,
+ 'callback': callback
+ })
+
def preview_modified_image(self, modified_image: UltrasoundImage, frame: int) -> None:
+
"""Show the preprocessed data in the VOI drawing widget."""
if self._voi_drawing_widget:
self._voi_drawing_widget.update_enhancement_cache(modified_image.pixel_data, frame)
diff --git a/src/ceus/seg_loading/views/draw_voi_widget.py b/src/ceus/seg_loading/views/draw_voi_widget.py
index 858a869..fa6374b 100644
--- a/src/ceus/seg_loading/views/draw_voi_widget.py
+++ b/src/ceus/seg_loading/views/draw_voi_widget.py
@@ -16,6 +16,7 @@
from scipy.spatial import ConvexHull
from PyQt6.QtWidgets import QWidget, QLabel, QHBoxLayout, QSizePolicy, QFileDialog, QSlider, QVBoxLayout, QFrame, QCheckBox
from PyQt6.QtCore import QEvent, pyqtSignal, Qt, QThread
+from PyQt6.QtWidgets import QPushButton
from ...mvc.base_view import BaseViewMixin
from ..ui.draw_voi_ui import Ui_voi_drawer
@@ -117,6 +118,23 @@ def run(self):
self.finished.emit("VOI saved successfully.")
except Exception as e:
self.error_msg.emit(str(e))
+
+class ExportVideoWorker(QThread):
+ """Worker thread for exporting the postprocessed CEUS video and VOI overlay as MP4."""
+ finished = pyqtSignal(str)
+ error_msg = pyqtSignal(str)
+ progress = pyqtSignal(int) # 0-100
+
+ def __init__(self, parent_widget):
+ super().__init__()
+ self.parent_widget = parent_widget
+
+ def run(self):
+ try:
+ self.parent_widget._export_video(self.progress)
+ self.finished.emit("Video exported successfully.")
+ except Exception as e:
+ self.error_msg.emit(str(e))
class DrawVOIWidget(QWidget, BaseViewMixin):
@@ -131,26 +149,48 @@ class DrawVOIWidget(QWidget, BaseViewMixin):
file_selected = pyqtSignal(dict) # {'seg_path': str, 'seg_type': str}
back_requested = pyqtSignal()
close_requested = pyqtSignal()
+ # B-mode/CEUS Toggle & Paramap Signals
apply_preprocs_preview = pyqtSignal(list) # List of dicts with 'name' and 'kwargs' keys
+ compute_noise_floor_requested = pyqtSignal(object, int, float) # image_data, n_ref, std_mult
+ enhance_image_requested = pyqtSignal(object, list) # image_data, func_configs
- def __init__(self, image_data: UltrasoundImage, parent: Optional[QWidget] = None):
+ def __init__(self, image_data: UltrasoundImage, bmode_image_data: Optional[UltrasoundImage] = None, parent: Optional[QWidget] = None):
QWidget.__init__(self, parent)
self.__init_base_view__(parent)
self._ui = Ui_voi_drawer()
self._image_data = image_data
- self._pix_data = image_data.pixel_data
-
+
+ # B-mode overlay data
+ self._bmode_image_data = bmode_image_data
+ self._bmode_pix_data = bmode_image_data.pixel_data if bmode_image_data else None
+ self._bmode_alpha = 0.5 # 0.0 = invisible, 1.0 = fully opaque
+ self._ax_sag_cor_bmode_artists = [None, None, None]
+
# Enhancement parameters
- self._clahe_clip_limit = 1.2
- self._gamma = 1.5
+ # self._clahe_clip_limit = 1.2
+ # self._gamma = 1.5
+
+ self._p_low_percentile = 4.0
+ self._p_high_percentile = 98.0
+
+ self._n_ref_frames = 5
+ self._noise_std_multiplier = 0.5
+ self._ceus_p_high_percentile = 100
+ self._last_noise_floor = 0.0
+
self._width_scale_axial = 1.0
self._width_scale_sagittal = 1.0
self._width_scale_coronal = 1.0
self._use_philips_ceus = False
+ self._pix_data = image_data.pixel_data # keep raw data untouched
+ self._ceus_p_low = self._compute_noise_floor(image_data) # single scalar
+
# Cache for enhanced volume
- self._enhanced_cache = None
- self._enhanced_cache_frame = -1
+ # self._enhanced_cache = None
+ # self._enhanced_cache_frame = -1
+
+ self._slice_cache: dict = {}
# State collections
self._drawing_widgets = []
@@ -199,13 +239,26 @@ def __init__(self, image_data: UltrasoundImage, parent: Optional[QWidget] = None
self._update_scan_display() # Initial UI update
self._refresh_frames() # Mark all planes for first update
- def update_enhancement_cache(self, enhanced_frame: np.ndarray, frame: int) -> None:
- """Update the displayed image data, e.g. after preprocessing."""
- assert enhanced_frame.shape[:-1] == self._pix_data.shape[:-1], "Enhanced pixel data must have the same shape as original"
- self._enhanced_cache = enhanced_frame[:, :, :, 0] # Store only the current time frame in cache
- self._enhanced_cache_frame = frame
- self._refresh_frames()
+ # def update_enhancement_cache(self, enhanced_frame: np.ndarray, frame: int) -> None:
+ # """Update the displayed image data, e.g. after preprocessing."""
+ # self._frame_cache = enhanced_frame[:, :, :, 0] if enhanced_frame.ndim == 4 else enhanced_frame
+ # self._frame_cache_t = frame
+ # self._refresh_frames()
+
+ def _compute_noise_floor(self, image_data: UltrasoundImage) -> float:
+ """Request the CEUS noise floor scalar from the model."""
+ self._last_noise_floor = 0.0
+ self.compute_noise_floor_requested.emit(
+ image_data,
+ self._n_ref_frames,
+ self._noise_std_multiplier
+ )
+ return self._last_noise_floor
+ def set_noise_floor(self, value: float) -> None:
+ """Callback from controller to set the computed noise floor."""
+ self._last_noise_floor = value
+
# ======================= Matplotlib Mouse Interaction ===================
def _connect_matplotlib_events(self):
"""Connect motion and click events on each plane's matplotlib canvas.
@@ -397,12 +450,13 @@ def create_enh_column(label_text, min_val, max_val, current_val, callback):
return col_widget, slider, val_lbl
# Create the columns
- clahe_col, self.clahe_slider, self.clahe_val_lbl = create_enh_column(
- "CLAHE", 1, 100, int(self._clahe_clip_limit * 10), self._on_clahe_changed
+ p_low_col, self.p_low_slider, self.p_low_val_lbl = create_enh_column(
+ "P LOW", 1, 200, int(self._p_low_percentile * 10), self._on_p_low_changed
)
- gamma_col, self.gamma_slider, self.gamma_val_lbl = create_enh_column(
- "GAMMA", 1, 40, int(self._gamma * 10), self._on_gamma_changed
+ p_high_col, self.p_high_slider, self.p_high_val_lbl = create_enh_column(
+ "P HIGH", 500, 999, int(self._p_high_percentile * 10), self._on_p_high_changed
)
+
width_ax_col, self.width_ax_slider, self.width_ax_val_lbl = create_enh_column(
"WIDTH (AX)", 1, 50, int(self._width_scale_axial * 10), self._on_width_axial_changed
)
@@ -413,8 +467,8 @@ def create_enh_column(label_text, min_val, max_val, current_val, callback):
"WIDTH (COR)", 1, 50, int(self._width_scale_coronal * 10), self._on_width_coronal_changed
)
- row1_layout.addWidget(clahe_col)
- row1_layout.addWidget(gamma_col)
+ row1_layout.addWidget(p_low_col)
+ row1_layout.addWidget(p_high_col)
# Philips CEUS Toggle (Pseudocoloring) - now in row 1
self.philips_check = QCheckBox("Pseudocoloring")
@@ -434,8 +488,52 @@ def create_enh_column(label_text, min_val, max_val, current_val, callback):
container_layout.addLayout(row1_layout)
container_layout.addLayout(row2_layout)
+ # B-mode alpha slider (only shown when B-mode data is loaded)
+ if self._bmode_pix_data is not None:
+ row3_layout = QHBoxLayout()
+ row3_layout.setSpacing(20)
+ bmode_alpha_col, self.bmode_alpha_slider, self.bmode_alpha_val_lbl = create_enh_column(
+ "B-MODE ALPHA", 0, 100, int(self._bmode_alpha * 100), self._on_bmode_alpha_changed
+ )
+ self.bmode_alpha_val_lbl.setText(f"{self._bmode_alpha:.2f}")
+ row3_layout.addWidget(bmode_alpha_col)
+ container_layout.addLayout(row3_layout)
+
# Add to the layout below the current slice slider
self._ui.verticalLayout_2.addWidget(enh_group)
+
+ self.export_video_button = QPushButton("Export Video")
+ self.export_video_button.setStyleSheet(
+ "QPushButton { color: white; font-size: 14px; font-weight: bold; "
+ "background: rgb(90, 37, 255); border-radius: 10px; padding: 6px 12px; }"
+ "QPushButton:disabled { background: rgb(100, 100, 100); }"
+ )
+ self.export_video_button.clicked.connect(self._on_export_video_clicked)
+ self._ui.verticalLayout_2.addWidget(self.export_video_button)
+
+ def _on_export_video_clicked(self) -> None:
+ """Handle export video button click."""
+ if not Path(self._ui.save_folder_input.text()).is_dir():
+ self.show_error("Please select a valid save folder first (use the VOI save panel).")
+ return
+
+ self.export_video_button.setEnabled(False)
+ self.export_video_button.setText("Exporting...")
+
+ self._export_video_worker = ExportVideoWorker(self)
+ self._export_video_worker.finished.connect(self._on_export_video_finished)
+ self._export_video_worker.error_msg.connect(self._on_export_video_error)
+ self._export_video_worker.start()
+
+ def _on_export_video_finished(self, msg: str) -> None:
+ self.export_video_button.setEnabled(True)
+ self.export_video_button.setText("Export Video")
+ print(msg)
+
+ def _on_export_video_error(self, err: str) -> None:
+ self.export_video_button.setEnabled(True)
+ self.export_video_button.setText("Export Video")
+ self.show_error(f"Export failed: {err}")
def _on_philips_toggled(self, state: int) -> None:
"""Handle Philips CEUS pseudocolor toggle."""
@@ -447,18 +545,26 @@ def _on_philips_toggled(self, state: int) -> None:
artist.set_cmap(new_cmap)
self._refresh_frames()
- 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}")
+ def _on_bmode_alpha_changed(self, value: int) -> None:
+ """Handle B-mode overlay alpha change."""
+ self._bmode_alpha = value / 100.0
+ if hasattr(self, 'bmode_alpha_val_lbl'):
+ self.bmode_alpha_val_lbl.setText(f"{self._bmode_alpha:.2f}")
+ for artist in self._ax_sag_cor_bmode_artists:
+ if artist is not None:
+ artist.set_alpha(self._bmode_alpha)
+ self._refresh_frames()
+
+ def _on_p_low_changed(self, value: int) -> None:
+ self._p_low_percentile = value / 10.0
+ if hasattr(self, 'p_low_val_lbl'):
+ self.p_low_val_lbl.setText(f"{self._p_low_percentile:.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}")
+ def _on_p_high_changed(self, value: int) -> None:
+ self._p_high_percentile = value / 10.0
+ if hasattr(self, 'p_high_val_lbl'):
+ self.p_high_val_lbl.setText(f"{self._p_high_percentile:.1f}")
self._invalidate_enhancement_cache()
def _on_width_axial_changed(self, value: int) -> None:
@@ -524,15 +630,14 @@ def _update_aspect_ratios(self) -> None:
def _reset_enhancement(self, _=None) -> None:
"""Reset enhancement parameters to defaults."""
- self.clahe_slider.setValue(12) # 1.2
- self.gamma_slider.setValue(15) # 1.5
+ self.p_low_slider.setValue(20) # 2.0
+ self.p_high_slider.setValue(980) # 98.0
self.width_ax_slider.setValue(10) # 1.0
self.width_sag_slider.setValue(10) # 1.0
self.width_cor_slider.setValue(10) # 1.0
def _invalidate_enhancement_cache(self) -> None:
- """Invalidate the cache and trigger a refresh of all planes."""
- self._enhanced_cache = None
+ self._slice_cache.clear()
self._refresh_frames()
def _setup_matplotlib_canvases(self):
@@ -587,13 +692,21 @@ def _initialize_plane_displays(self) -> None:
mask_arr = self._get_mask_slice(plane_ix)
current_cmap = philips_cmap if self._use_philips_ceus else 'gray'
- artist = ax.imshow(slice_arr, cmap=current_cmap, aspect=float(aspect), zorder=1, animated=True) # add vmin and vmax for the 0 - 255 show
+ artist = ax.imshow(slice_arr, cmap=current_cmap, aspect=float(aspect), zorder=1, animated=True)
+
+ # B-mode overlay layer (zorder=2, above CEUS, below masks)
+ if self._bmode_pix_data is not None:
+ bmode_slice = self._get_bmode_plane_slice(plane_ix)
+ bmode_artist = ax.imshow(bmode_slice, cmap='gray', aspect=float(aspect),
+ zorder=2, animated=True, alpha=self._bmode_alpha)
+ self._ax_sag_cor_bmode_artists[plane_ix] = bmode_artist
+
v_line = ax.axvline(x=0, color='yellow', lw=0.8, animated=True, zorder=11)
h_line = ax.axhline(y=0, color='yellow', lw=0.8, animated=True, zorder=11)
seg_mask = ax.imshow(mask_arr, zorder=8, aspect=float(aspect), animated=True)
roi_plot = ax.plot([], [], c='cyan', lw=1, zorder=9, animated=True)
point_scatter = ax.scatter([], [], c='red', s=5, marker='o', zorder=10, animated=True)
-
+
self._ax_sag_cor_plane_artists[plane_ix] = artist
self._ax_sag_cor_crosshair_lines[plane_ix] = (v_line, h_line)
self._ax_sag_cor_point_scatters[plane_ix] = point_scatter
@@ -606,62 +719,70 @@ def _initialize_plane_displays(self) -> None:
self.show_error(f"Error initializing plane display {plane_ix}: {e}")
def _get_plane_slice(self, plane_ix: int, initializing=False):
- """Return 2D numpy slice for given plane index based on current crosshair."""
- idx = self._get_plane_indices(plane_ix)
current_t = self._crosshair_xyzt[3]
-
- # Check if we need to enhance a new frame
- if not initializing and (self._enhanced_cache is None or self._enhanced_cache_frame != current_t):
- # Get the 3D volume for current time frame
- current_frame_3d = self._pix_data[:, :, :, current_t]
-
- # Enhance the entire 3D volume ONCE per frame
- self._enhance_volume(current_frame_3d) # performs enhancement SYNCHRONOUSLY
- self._enhanced_cache_frame = current_t
- elif initializing:
- self._enhanced_cache = self._image_data.pixel_data[:, :, :, current_t] # Cache the initial frame without enhancement for faster startup
-
- # Extract the 2D slice from cached enhanced volume
- slice_idx = list(idx[:3]) # Remove time dimension
- arr = self._enhanced_cache[tuple(slice_idx)]
-
- if arr.ndim != 2:
- arr = arr.squeeze()
- # Axial plane (index 0) needs transpose for correct orientation
- if plane_ix == 0:
- arr = arr.T
- return arr
+ # Include the fixed-dimension index in the cache key
+ cx, cy, cz = self._crosshair_xyzt[0], self._crosshair_xyzt[1], self._crosshair_xyzt[2]
+ fixed_indices = [cz, cx, cy] # axial fixed by z, sagittal by x, coronal by y
+ fixed_idx = fixed_indices[plane_ix]
+ key = ('ceus', current_t, plane_ix, fixed_idx)
+
+ if key not in self._slice_cache:
+ idx = self._get_plane_indices(plane_ix)
+ raw = np.asarray(self._pix_data[:, :, :, current_t])
+ arr = raw[tuple(list(idx[:3]))].astype(np.float32)
+ if arr.ndim != 2:
+ arr = arr.squeeze()
+ if plane_ix == 0:
+ arr = arr.T
+ if not initializing:
+ arr = self._enhance_slice_ceus(arr)
+ self._slice_cache[key] = arr.astype(np.uint8)
+ return self._slice_cache[key]
+
+ def _get_bmode_plane_slice(self, plane_ix: int):
+ if self._bmode_pix_data is None:
+ return None
+ current_t = min(self._crosshair_xyzt[3], self._bmode_pix_data.shape[3] - 1)
+ cx, cy, cz = self._crosshair_xyzt[0], self._crosshair_xyzt[1], self._crosshair_xyzt[2]
+ fixed_indices = [cz, cx, cy]
+ fixed_idx = fixed_indices[plane_ix]
+ key = ('bmode', current_t, plane_ix, fixed_idx)
+
+ if key not in self._slice_cache:
+ idx = self._get_plane_indices(plane_ix)
+ raw = np.asarray(self._bmode_pix_data[:, :, :, current_t])
+ arr = raw[tuple(list(idx[:3]))].astype(np.float32)
+ if arr.ndim != 2:
+ arr = arr.squeeze()
+ if plane_ix == 0:
+ arr = arr.T
+ arr = self._enhance_slice_bmode(arr)
+ self._slice_cache[key] = arr.astype(np.uint8)
+ return self._slice_cache[key]
- def _enhance_volume(self, volume_3d: np.ndarray) -> None:
- """Enhance a 3D image volume using predefined enhancement methods in the backend engine."""
- # Create a temporary UltrasoundImage for the current frame
- temp_im = UltrasoundImage(self._image_data.scan_path)
- temp_im.pixel_data = volume_3d.T[None].T.copy() # Add back time dimension for processing
- temp_im.pixdim = self._image_data.pixdim
- temp_im.frame_rate = self._image_data.frame_rate
-
- clahe_preproc_dict = {
- 'name': 'enhance_clahe',
- 'image_data': temp_im,
- 'frame_ix': self._crosshair_xyzt[3],
- 'kwargs': {
- 'clip_limit': self._clahe_clip_limit,
- 'tile_grid_size': (8, 8),
- }
- }
-
- gamma_preproc_dict = {
- 'name': 'enhance_gamma',
- 'image_data': None, # signal to reuse the already CLAHE-enhanced image (all preprocs in the same batch share the same image input)
- 'frame_ix': self._crosshair_xyzt[3],
- 'kwargs': {
- 'gamma': self._gamma,
- }
- }
-
- preproc_dicts = [clahe_preproc_dict, gamma_preproc_dict]
- self.apply_preprocs_preview.emit(preproc_dicts) # synchronous call to apply the enhancements and update the cache via the connected slot
-
+ def _enhance_slice_ceus(self, arr: np.ndarray) -> np.ndarray:
+ """Inline enhance_ceus_noise_norm on a single 2D slice."""
+ nonzero = arr[arr != 0]
+ if nonzero.size == 0:
+ return np.zeros_like(arr)
+ p_high = np.percentile(nonzero, self._ceus_p_high_percentile)
+ if p_high <= self._ceus_p_low:
+ return np.zeros_like(arr)
+ clipped = np.clip(arr, self._ceus_p_low, p_high)
+ return ((clipped - self._ceus_p_low) / (p_high - self._ceus_p_low) * 255)
+
+ def _enhance_slice_bmode(self, arr: np.ndarray) -> np.ndarray:
+ """Inline enhance_bmode_noise on a single 2D slice."""
+ nonzero = arr[arr != 0]
+ if nonzero.size == 0:
+ return np.zeros_like(arr)
+ p_low = np.percentile(nonzero, self._p_low_percentile)
+ p_high = np.percentile(nonzero, self._p_high_percentile)
+ if p_high <= p_low:
+ return np.zeros_like(arr)
+ clipped = np.clip(arr, p_low, p_high)
+ return ((clipped - p_low) / (p_high - p_low) * 255)
+
def _get_mask_slice(self, plane_ix: int):
"""Return RGBA numpy slice for the mask of the given plane index."""
idx = self._get_plane_indices(plane_ix)[:-1] # no time dimension
@@ -698,6 +819,12 @@ def _update(_frame):
try:
slice_arr = self._get_plane_slice(plane_ix)
self._ax_sag_cor_plane_artists[plane_ix].set_array(slice_arr)
+ # Update B-mode overlay if present
+ bmode_artist = self._ax_sag_cor_bmode_artists[plane_ix]
+ if bmode_artist is not None:
+ bmode_slice = self._get_bmode_plane_slice(plane_ix)
+ if bmode_slice is not None:
+ bmode_artist.set_array(bmode_slice)
self._update_crosshair_lines(plane_ix)
except Exception as e:
self.show_error(f"Plane {plane_ix} update error: {e}")
@@ -707,12 +834,14 @@ def _update(_frame):
self._update_roi_plot(plane_ix)
self._update_point_scatter(plane_ix)
self._update_seg_masks(plane_ix)
-
+
v_line, h_line = self._ax_sag_cor_crosshair_lines[plane_ix]
roi_plot = self._ax_sag_cor_roi_plots[plane_ix]
scatter = self._ax_sag_cor_point_scatters[plane_ix]
mask = self._ax_sag_cor_seg_masks[plane_ix]
artists = [self._ax_sag_cor_plane_artists[plane_ix]]
+ bmode_artist = self._ax_sag_cor_bmode_artists[plane_ix]
+ if bmode_artist is not None: artists.append(bmode_artist)
if v_line: artists.append(v_line)
if h_line: artists.append(h_line)
if roi_plot: artists.append(roi_plot[0])
@@ -1039,6 +1168,167 @@ def _on_export_voi_clicked(self):
self._save_worker.finished.connect(self._on_save_voi_finished)
self._save_worker.error_msg.connect(self._on_save_voi_error)
self._save_worker.start()
+
+ def _export_video(self, progress_signal=None) -> None:
+ """Export postprocessed CEUS and B-mode with VOI border overlay as a 2x3 MP4."""
+ import cv2
+
+ out_folder = self._ui.save_folder_input.text()
+ out_name = Path(self._ui.save_name_input.text() or self._image_data.scan_name or "export").stem
+ if not Path(out_folder).is_dir():
+ raise ValueError("Please select a valid output folder first.")
+
+ out_path = str(Path(out_folder) / f"{out_name}_ceus.mp4")
+
+ cx, cy, cz = self._crosshair_xyzt[0], self._crosshair_xyzt[1], self._crosshair_xyzt[2]
+
+ # --- Helpers ---
+
+ def _enhance_ceus_frame(raw_3d: np.ndarray) -> np.ndarray:
+ """Enhance a raw CEUS 3D frame, returns H x W x Z uint8."""
+ temp_im = UltrasoundImage(self._image_data.scan_path)
+ temp_im.pixel_data = raw_3d.T[None].T.copy()
+ temp_im.pixdim = self._image_data.pixdim
+ temp_im.frame_rate = self._image_data.frame_rate
+
+ # Use controller to enhance via signals
+ self._temp_enhanced = None
+ self.enhance_image_requested.emit(temp_im, [
+ {'name': 'enhance_ceus_noise_norm', 'kwargs': {
+ 'p_low': self._ceus_p_low,
+ 'p_high_percentile': self._ceus_p_high_percentile
+ }}
+ ], lambda img: setattr(self, '_temp_enhanced', img))
+
+ # Wait for callback (this is synchronous because the action handler calls callback immediately)
+ if self._temp_enhanced:
+ return self._temp_enhanced.pixel_data[:, :, :, 0]
+ return raw_3d
+
+ def _enhance_bmode_frame_3d(raw_3d: np.ndarray) -> np.ndarray:
+ """Enhance a raw B-mode 3D frame, returns H x W x Z uint8."""
+ temp_im = UltrasoundImage(self._image_data.scan_path)
+ temp_im.pixel_data = raw_3d.T[None].T.copy()
+ temp_im.pixdim = self._image_data.pixdim
+ temp_im.frame_rate = self._image_data.frame_rate
+
+ self._temp_enhanced = None
+ self.enhance_image_requested.emit(temp_im, [
+ {'name': 'enhance_bmode_noise', 'kwargs': {
+ 'p_low_percentile': self._p_low_percentile,
+ 'p_high_percentile': self._p_high_percentile,
+ }}
+ ], lambda img: setattr(self, '_temp_enhanced', img))
+
+ if self._temp_enhanced:
+ return self._temp_enhanced.pixel_data[:, :, :, 0]
+ return raw_3d
+
+ def _voi_border_2d(mask_2d: np.ndarray) -> np.ndarray:
+ binary = (mask_2d > 0).astype(np.uint8) * 255
+ if not binary.any():
+ return np.zeros_like(binary)
+ edges = cv2.Canny(binary, 100, 200)
+ kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
+ return cv2.dilate(edges, kernel, iterations=1)
+
+ def _apply_border(gray_2d: np.ndarray, border_2d: np.ndarray) -> np.ndarray:
+ bgr = cv2.cvtColor(gray_2d, cv2.COLOR_GRAY2BGR)
+ bgr[border_2d > 0] = [0, 0, 255]
+ return bgr
+
+ def _resize_to_height(img_bgr: np.ndarray, target_h: int) -> np.ndarray:
+ h, w = img_bgr.shape[:2]
+ if h == 0 or w == 0:
+ return np.zeros((target_h, target_h, 3), dtype=np.uint8)
+ scale = target_h / h
+ new_w = max(1, int(w * scale))
+ return cv2.resize(img_bgr, (new_w, target_h), interpolation=cv2.INTER_LINEAR)
+
+ def _build_row(enhanced_3d: np.ndarray, target_h: int) -> np.ndarray:
+ """Build a 1x3 BGR row (axial | sagittal | coronal) from an enhanced 3D volume."""
+ # Axial: slice at z=cz, transpose to match widget orientation
+ ax_gray = enhanced_3d[:, :, cz].T
+ ax_mask = self._roi_masks_overlap[:, :, cz, 0].T
+ # Sagittal: slice at x=cx → (y_len, z_len)
+ sag_gray = enhanced_3d[cx, :, :]
+ sag_mask = self._roi_masks_overlap[cx, :, :, 0]
+ # Coronal: slice at y=cy → (x_len, z_len)
+ cor_gray = enhanced_3d[:, cy, :]
+ cor_mask = self._roi_masks_overlap[:, cy, :, 0]
+
+ panels = []
+ for gray, mask in [(ax_gray, ax_mask), (sag_gray, sag_mask), (cor_gray, cor_mask)]:
+ border = _voi_border_2d(mask)
+ bgr = _apply_border(gray, border)
+ panels.append(_resize_to_height(bgr, target_h))
+
+ return np.concatenate(panels, axis=1)
+
+ def _add_label(row: np.ndarray, text: str) -> np.ndarray:
+ """Add a text label in the top-left corner of a row."""
+ import cv2
+ labeled = row.copy()
+ cv2.putText(labeled, text, (10, 24),
+ cv2.FONT_HERSHEY_SIMPLEX, 0.3, (255, 255, 255), 2, cv2.LINE_AA)
+ return labeled
+
+ # --- Determine composite dimensions from first frame ---
+
+ sample_ceus_raw = np.array(self._pix_data[:, :, :, 0])
+ sample_ceus_enh = _enhance_ceus_frame(sample_ceus_raw)
+ target_h = sample_ceus_enh[:, :, cz].T.shape[0]
+
+ has_bmode = self._bmode_pix_data is not None
+ if has_bmode:
+ sample_bmode_raw = np.array(self._bmode_pix_data[:, :, :, 0])
+ sample_bmode_enh = _enhance_bmode_frame_3d(sample_bmode_raw)
+ bmode_target_h = sample_bmode_enh[:, :, cz].T.shape[0]
+
+ ceus_row_sample = _build_row(sample_ceus_enh, target_h)
+ comp_w = ceus_row_sample.shape[1]
+
+ if has_bmode:
+ bmode_row_sample = _build_row(sample_bmode_enh, bmode_target_h)
+ # Match bmode row width to ceus row width
+ if bmode_row_sample.shape[1] != comp_w:
+ bmode_row_sample = cv2.resize(bmode_row_sample, (comp_w, bmode_target_h))
+ comp_h = ceus_row_sample.shape[0] + bmode_row_sample.shape[0]
+ else:
+ comp_h = ceus_row_sample.shape[0]
+
+ fps = max(1, int(round(1.0 / self._image_data.frame_rate))) if self._image_data.frame_rate else 10
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
+ writer = cv2.VideoWriter(out_path, fourcc, fps, (comp_w, comp_h))
+
+ # --- Write frames ---
+ try:
+ for t in range(self._num_slices):
+ # CEUS row
+ ceus_raw = np.array(self._pix_data[:, :, :, t])
+ ceus_enh = _enhance_ceus_frame(ceus_raw)
+ ceus_row = _add_label(_build_row(ceus_enh, target_h), "CEUS")
+
+ if has_bmode:
+ # B-mode: clamp t to available frames
+ bmode_t = min(t, self._bmode_pix_data.shape[3] - 1)
+ bmode_raw = np.array(self._bmode_pix_data[:, :, :, bmode_t])
+ bmode_enh = _enhance_bmode_frame_3d(bmode_raw)
+ bmode_row = _build_row(bmode_enh, bmode_target_h)
+ # Match width before stacking
+ if bmode_row.shape[1] != comp_w:
+ bmode_row = cv2.resize(bmode_row, (comp_w, bmode_target_h))
+ bmode_row = _add_label(bmode_row, "B-mode")
+ composite = np.concatenate([ceus_row, bmode_row], axis=0)
+ else:
+ composite = ceus_row
+
+ writer.write(composite)
+
+ if progress_signal:
+ progress_signal.emit(int((t + 1) / self._num_slices * 100))
+ finally:
+ writer.release()
def _on_save_voi_finished(self, msg):
self._ui.saving_voi_label.hide()
diff --git a/src/qus/seg_loading/ui/roi_drawing.ui b/src/qus/seg_loading/ui/roi_drawing.ui
index 4d73186..fb3989b 100644
--- a/src/qus/seg_loading/ui/roi_drawing.ui
+++ b/src/qus/seg_loading/ui/roi_drawing.ui
@@ -528,64 +528,47 @@
background: rgb(90, 37, 255);
border-radius: 15px;
}
-
-
- 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 {
+
+
+ Back
+
+
+
+
+
+
+
+
+ -
+
+
+ 10
+
+
+ 30
+
+
+ 10
+
+
+ 30
+
+
+ 10
+
+
-
+
+
+ 5
+
+
-
+
+
-
+
+
-
+
+
+ QLabel {
font-size: 18px;
color: rgb(255, 255, 255);
background-color: rgba(255, 255, 255, 0);
@@ -872,337 +855,50 @@
color: rgb(255, 255, 255);
background-color: rgba(255, 255, 255, 0);
}
-
-
- 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 {
+
+
+ 0
+
+
+ Qt::TextFormat::AutoText
+
+
+ false
+
+
+ Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 241
+ 41
+
+
+
+
+ 241
+ 41
+
+
+
+ QPushButton {
color: white;
font-size: 16px;
background: rgb(90, 37, 255);