29 ceus bug report gui cannot load saved nifti voi#64
Conversation
pyradiomics fix + docs update
- Add 'Load DICOM File' button in ROI selection menu - Implement file dialog for DICOM selection - Add transparency slider with 0-100% control - 0% shows pure B-mode, 100% shows pure DICOM - Fix brightness adjustment to preserve overlay - Remove debug print statements
…sidebar-label-to-QUS-analysis 17 change rf analysis sidebar label to qus analysis
…image-quality-issue
…ing signal and implementing preview transition
…d preview widgets
…ronal synchronization
… resizing for NIfTI preview
…vista compatibility
There was a problem hiding this comment.
Pull request overview
Updates the QuantUS GUI to improve segmentation loading/review workflows (including NIfTI/VOI handling) and to introduce DICOM loading support, alongside regenerated PyQt6 UI updates and CEUS analysis plumbing.
Changes:
- Add DICOM loading support (DicomLoader + model/controller wiring) and extra diagnostic logging for NIfTI image/seg loads.
- Extend CEUS segmentation flow to include post-draw preview/confirmation and add a CEUS analysis-loading pipeline.
- Regenerate/normalize multiple Qt
.uifiles (PyQt6 enum scoping, geometry, labels) and update submodules/documentation.
Reviewed changes
Copilot reviewed 72 out of 83 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| src/qus/seg_loading/ui/seg_type_selection.ui | PyQt6 .ui enum scoping + geometry/layout adjustments |
| src/qus/seg_loading/ui/seg_file_selection.ui | PyQt6 .ui enum scoping + geometry/layout adjustments |
| src/qus/seg_loading/ui/roi_preview.ui | PyQt6 .ui enum scoping + geometry/layout adjustments |
| src/qus/seg_loading/ui/frame_selection.ui | PyQt6 .ui enum scoping + geometry/layout adjustments |
| src/qus/seg_loading/seg_loading_view_coordinator.py | Pass controller reference down to ROI drawing widget |
| src/qus/seg_loading/seg_loading_controller.py | Attach controller to view; add DICOM passthrough methods |
| src/qus/image_loading/ui/scan_type_ui.py | New generated PyQt6 UI python for scan type selection |
| src/qus/image_loading/ui/scan_type.ui | PyQt6 .ui enum scoping + label updates |
| src/qus/image_loading/ui/file_selection.ui | PyQt6 .ui enum scoping updates |
| src/qus/image_loading/dicom_loader.py | New DICOM loading + normalization/cropping helper |
| src/qus/image_loading/init.py | Export DicomLoader from package |
| src/qus/export_loading/ui/export_loading_ui.py | New generated PyQt6 export loading UI python |
| src/qus/export_loading/ui/export_loading.ui | PyQt6 .ui enum scoping + geometry/layout tweaks |
| src/qus/config_loading/ui/custom_params.ui | PyQt6 .ui enum scoping updates |
| src/qus/config_loading/ui/config_type_selection.ui | PyQt6 .ui enum scoping updates |
| src/qus/config_loading/ui/config_preview.ui | PyQt6 .ui enum scoping + scroll policy updates |
| src/qus/config_loading/ui/config_file_selection.ui | PyQt6 .ui enum scoping updates |
| src/qus/application_model.py | Add DICOM state/methods; extra NIfTI debug prints |
| src/qus/analysis_loading/ui/analysis_params.ui | PyQt6 .ui enum scoping + sizing tweaks |
| src/qus/analysis_loading/ui/analysis_function_selection_ui.py | New generated PyQt6 analysis function selection UI python |
| src/qus/analysis_loading/ui/analysis_function_selection.ui | PyQt6 .ui enum scoping updates |
| src/ceus/seg_loading/views/spline.py | Avoid spline failures from duplicate points; handle 4D point stripping |
| src/ceus/seg_loading/views/draw_roi_widget.py | Add enhancement controls + confirm/review flow; emit completed segmentation |
| src/ceus/seg_loading/ui/seg_type_selection_ui.py | New generated PyQt6 segmentation type selection UI python |
| src/ceus/seg_loading/seg_loading_view_coordinator.py | Add segmentation preview step and wire completion/confirmation |
| src/ceus/seg_loading/seg_loading_controller.py | Connect model signals; persist confirmed/manual segmentation into model |
| src/ceus/seg_loading/init.py | Export new widgets (preview + ROI widget) |
| src/ceus/image_preprocessing/transforms.py | Removed legacy preprocessing transforms module |
| src/ceus/image_preprocessing/options.py | Removed legacy preprocessing options module |
| src/ceus/image_preprocessing/functions.py | Removed legacy preprocessing functions module |
| src/ceus/image_preprocessing/decorators.py | Removed legacy preprocessing decorators module |
| src/ceus/image_preprocessing/README.md | Removed legacy preprocessing documentation |
| src/ceus/image_loading/views/file_selection_widget.py | Fix folder-selection detection/validation logic |
| src/ceus/image_loading/ui/scan_type_ui.py | New generated PyQt6 UI python for CEUS scan type selection |
| src/ceus/application_model.py | Add CEUS preprocessing API + analysis worker/types + manual segmentation setter |
| src/ceus/application_controller.py | Add CEUS analysis-loading navigation/controller lifecycle |
| src/ceus/analysis_loading/views/analysis_params_widget.py | New CEUS analysis params widget scaffold |
| src/ceus/analysis_loading/views/analysis_function_selection_widget.py | Port imports/types to CEUS engine objects |
| src/ceus/analysis_loading/views/analysis_execution_widget.py | Port types to CurvesAnalysis; update label access |
| src/ceus/analysis_loading/analysis_loading_view_coordinator.py | Port coordinator to CEUS widgets/types; harden error routing |
| src/ceus/analysis_loading/analysis_loading_controller.py | Select available CEUS analysis type; fix completion action name |
| engines/qus | Update QUS submodule pointer |
| engines/ceus | Update CEUS submodule pointer |
| README.md | Update clone URL; add pyradiomics install note and Windows build-tools note |
| Images/functions.py | Removed legacy duplicate code under Images/ |
| Images/application_controller.py | Removed legacy duplicate controller under Images/ |
| Images/README.md | Removed legacy duplicate README under Images/ |
| .gitmodules | Pin submodules to branch = main |
Comments suppressed due to low confidence (1)
src/qus/seg_loading/seg_loading_view_coordinator.py:1
- Avoid setting a private attribute (
_parent_controller) on another widget from the outside; this is brittle and bypasses the widget’s public API. Prefer passing the controller (or a narrower interface/callback) via the RoiDrawingWidget constructor, or add a public setter method (e.g.,set_controller(...)) on RoiDrawingWidget.
"""
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Pass controller reference for DICOM loading via model | ||
| if hasattr(self, 'controller') and self.controller: | ||
| self._roi_drawing_widget._parent_controller = self.controller |
There was a problem hiding this comment.
Avoid setting a private attribute (_parent_controller) on another widget from the outside; this is brittle and bypasses the widget’s public API. Prefer passing the controller (or a narrower interface/callback) via the RoiDrawingWidget constructor, or add a public setter method (e.g., set_controller(...)) on RoiDrawingWidget.
| """Get the processed DICOM image data.""" | ||
| return self._dicom_image | ||
|
|
||
| def load_dicom_file(self, dicom_file_path: str) -> bool: |
There was a problem hiding this comment.
Importing via from src.qus... is fragile and commonly fails when the package is installed/imported outside a 'src' layout context. Use a package-relative import (e.g., from .image_loading.dicom_loader import DicomLoader or from qus.image_loading.dicom_loader import DicomLoader, depending on your package structure) to ensure it works in all execution environments.
| Returns: | ||
| bool: True if loaded successfully, False otherwise | ||
| """ | ||
| from src.qus.image_loading.dicom_loader import DicomLoader |
There was a problem hiding this comment.
Importing via from src.qus... is fragile and commonly fails when the package is installed/imported outside a 'src' layout context. Use a package-relative import (e.g., from .image_loading.dicom_loader import DicomLoader or from qus.image_loading.dicom_loader import DicomLoader, depending on your package structure) to ensure it works in all execution environments.
| from src.qus.image_loading.dicom_loader import DicomLoader | |
| from .image_loading.dicom_loader import DicomLoader |
| if not PYDICOM_AVAILABLE: | ||
| print("pydicom is not installed. Cannot load DICOM files.") | ||
| return None |
There was a problem hiding this comment.
Using print() for missing-dependency and processing diagnostics will spam stdout in GUI usage and makes it difficult to manage verbosity. Prefer the application's logging mechanism (or propagate errors upward so the model/controller can emit user-facing errors) and gate debug output behind a log level.
| # 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)") |
There was a problem hiding this comment.
Using print() for missing-dependency and processing diagnostics will spam stdout in GUI usage and makes it difficult to manage verbosity. Prefer the application's logging mechanism (or propagate errors upward so the model/controller can emit user-facing errors) and gate debug output behind a log level.
| # We need to create a 3D mask where this 2D ROI is on one slice or repeated. | ||
| # However, for consistency with DrawVOIWidget, we probably want a 3D volume. | ||
| # If DrawROIWidget is only for a single frame, z_len should match what is expected. | ||
| x_len, y_len, z_len = self._image_data.pixel_data.shape[:3] | ||
| seg_mask = np.zeros((x_len, y_len, z_len), dtype=np.uint8) | ||
|
|
||
| # Translate 2D mask [y, x] to [x, y, z] slice | ||
| # Assuming the ROI was drawn on a specific slice? | ||
| # Actually DrawROIWidget seems to be for 2D images or a specific frame. | ||
| # If the image is 4D [x, y, z, t], maybe it was drawn on the central slice? | ||
| # Let's assume it was for 2D or we put it on the middle slice of 3D. | ||
| mid_z = z_len // 2 | ||
|
|
||
| # Handle shape mismatch if any | ||
| if mask_2d.shape[1] == x_len and mask_2d.shape[0] == y_len: | ||
| seg_mask[:, :, mid_z] = mask_2d.T | ||
|
|
There was a problem hiding this comment.
This assumes pixel_data.shape[:3] corresponds to (x, y, z), but elsewhere the widget operates on _all_frames shaped like [t, y, x] (per your own comments). If pixel_data is time-first (or otherwise not (x,y,z,...)), the created seg_mask dimensions will be wrong (e.g., x_len could become t). Use the actual spatial dimensions of the frame you drew on (height/width) and construct a mask with a well-defined axis order that matches what CEUS downstream expects.
| # We need to create a 3D mask where this 2D ROI is on one slice or repeated. | |
| # However, for consistency with DrawVOIWidget, we probably want a 3D volume. | |
| # If DrawROIWidget is only for a single frame, z_len should match what is expected. | |
| x_len, y_len, z_len = self._image_data.pixel_data.shape[:3] | |
| seg_mask = np.zeros((x_len, y_len, z_len), dtype=np.uint8) | |
| # Translate 2D mask [y, x] to [x, y, z] slice | |
| # Assuming the ROI was drawn on a specific slice? | |
| # Actually DrawROIWidget seems to be for 2D images or a specific frame. | |
| # If the image is 4D [x, y, z, t], maybe it was drawn on the central slice? | |
| # Let's assume it was for 2D or we put it on the middle slice of 3D. | |
| mid_z = z_len // 2 | |
| # Handle shape mismatch if any | |
| if mask_2d.shape[1] == x_len and mask_2d.shape[0] == y_len: | |
| seg_mask[:, :, mid_z] = mask_2d.T | |
| # Construct a 3D mask using the spatial dimensions of the frame on which | |
| # the ROI was drawn. The ROI is 2D [y, x]; we add a singleton z dimension. | |
| mask_h, mask_w = mask_2d.shape # [y, x] | |
| x_len, y_len, z_len = mask_w, mask_h, 1 | |
| seg_mask = np.zeros((x_len, y_len, z_len), dtype=np.uint8) | |
| # Translate 2D mask [y, x] to [x, y, z] slice | |
| # Place the ROI on the only slice (z = 0) of this 3D volume. | |
| seg_mask[:, :, 0] = mask_2d.T |
| # Position it next to the save button in the layout | ||
| self._ui.chooseImageButtonsLayout_4.addWidget(self.confirm_review_button) |
There was a problem hiding this comment.
This assumes chooseImageButtonsLayout_4 always exists in the loaded UI. If the .ui changes or the name differs across variants, this will raise an AttributeError at runtime. Consider guarding with hasattr(self._ui, 'chooseImageButtonsLayout_4') and failing gracefully (or place the button in a layout that is known to exist).
| # Position it next to the save button in the layout | |
| self._ui.chooseImageButtonsLayout_4.addWidget(self.confirm_review_button) | |
| # Position it next to the save button in the layout, if that layout exists | |
| layout = getattr(self._ui, "chooseImageButtonsLayout_4", None) | |
| if layout is not None: | |
| layout.addWidget(self.confirm_review_button) | |
| else: | |
| # Fallback: add to main_layout if the specific layout is not available | |
| self._ui.main_layout.addWidget(self.confirm_review_button) |
| def _create_parameter_inputs(self) -> None: | ||
| """Create input fields for each required parameter.""" | ||
| # This implementation is simplified compared to QUS for now | ||
| # Ideally would dynamically create inputs based on CEUS requirements | ||
| pass |
There was a problem hiding this comment.
This widget emits params_configured but never creates inputs for required parameters, so the analysis will always run with an empty params dict even when required params exist. Either implement dynamic input creation before enabling 'Run', or block execution and show an error until required parameters are supplied.
| def __init__(self, analysis_type: str, image_data: UltrasoundImage, | ||
| config_data: Any, seg_data: CeusSeg, |
There was a problem hiding this comment.
config_data is accepted and stored on the worker but never used when constructing the analysis object. If the engine's analysis types require config, this will be incorrect; if they don't, remove config_data from the worker signature to avoid confusion and dead parameters.
| self.image_data, | ||
| self.seg_data, | ||
| self.selected_functions, |
There was a problem hiding this comment.
config_data is accepted and stored on the worker but never used when constructing the analysis object. If the engine's analysis types require config, this will be incorrect; if they don't, remove config_data from the worker signature to avoid confusion and dead parameters.
| self.image_data, | |
| self.seg_data, | |
| self.selected_functions, | |
| self.image_data, | |
| self.seg_data, | |
| self.config_data, | |
| self.selected_functions, |
…d resolve conflicts favoring feature branch
…update_enhancement_cache method to DrawVOIWidget
No description provided.