Skip to content
Open

28 #67

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
e94b4ec
pyradiomics fix + docs update
akannan05 Dec 18, 2025
96a8aae
readme update for new repo link
HaseebGarfinkel Dec 18, 2025
3c1d857
Merge pull request #11 from QuantUS-OpenSource/docs-update
davidspector67 Dec 21, 2025
0d2636e
Updated readme for windows users
HaseebGarfinkel Jan 18, 2026
ff88ced
Updated gui for qus to qus analysis on sidebar
HaseebGarfinkel Jan 18, 2026
81efdfe
Merge pull request #33 from QuantUS-OpenSource/17-change-RF-analysis-…
davidspector67 Jan 19, 2026
df5f563
Merge branch 'feature/Philips_Quality' into 3-nifti-file-scaling-and-…
OmidChaghaneh Jan 22, 2026
19be9e4
Merge feature/Philips_Quality and cleanup redundant logic in Images/
OmidChaghaneh Jan 22, 2026
987f4cf
Fix module path in options.py and uncomment functions in functions.py
OmidChaghaneh Jan 22, 2026
f89a85d
Fix CEUS import error and refactor image enhancement logic
OmidChaghaneh Jan 22, 2026
e6caaf5
Feature: add interactive enhancement sliders to 3D VOI drawing widget
OmidChaghaneh Jan 22, 2026
5a60511
Refactor: improve 3D stability of image enhancement by using global n…
OmidChaghaneh Jan 22, 2026
da0a2c7
Feature: add interactive Philips-style pseudocoloring to VOI drawer w…
OmidChaghaneh Jan 22, 2026
ef8c04b
feat: display NIfTI metadata on load
OmidChaghaneh Jan 23, 2026
77a6b1d
feat: add dynamic aspect ratio and width control in VOI menu
OmidChaghaneh Jan 23, 2026
96d4bf0
feat(ceus): add independent plane-specific width aspect ratio controls
OmidChaghaneh Jan 29, 2026
985ca96
Feat: Add preprocessing interface to ApplicationModel and connect to …
OmidChaghaneh Feb 9, 2026
7826605
Refactor: Move image preprocessing to backend engine and remove redun…
OmidChaghaneh Feb 9, 2026
f63b4be
Refactor: Update DrawVOIWidget to use standardized backend preprocess…
OmidChaghaneh Feb 9, 2026
43ffb23
Chore: Update QUS submodule pointer state
OmidChaghaneh Feb 9, 2026
6e51da6
refactored to support MVC architecture
davidspector67 Feb 16, 2026
29ab979
Added changes from PR#53
davidspector67 Feb 16, 2026
2712322
Update CEUS submodule pointer state to reflect merged PRs
davidspector67 Feb 16, 2026
cc25554
Adding in the Overlay feature of B mode on top of CEUS image
Yuanshan-Wu Feb 19, 2026
0ec1fe4
Fix AttributeError in FileSelectionWidget and ensure UI files are reg…
OmidChaghaneh Feb 20, 2026
9a54f45
fix performance issue
Yuanshan-Wu Mar 5, 2026
57629ec
Update Image quality improvement and Save Video button
Yuanshan-Wu Mar 9, 2026
64c3617
Update the correct submoule
Yuanshan-Wu Mar 9, 2026
6453af8
TIC demo
Yuanshan-Wu Mar 11, 2026
5739273
Fix AttributeError in FileSelectionWidget and rearrange CEUS/B-mode s…
OmidChaghaneh Mar 18, 2026
e8f5764
Restored B-mode selection logic and added missing UI files across CEU…
OmidChaghaneh Mar 18, 2026
ee28871
Enhance the planes rather than volumes
Yuanshan-Wu Mar 19, 2026
e0b12a0
Fixing bug of the slice selection
Yuanshan-Wu Mar 19, 2026
fa2324c
Restore .gitignore settings: exclude auto-generated _ui.py files
OmidChaghaneh Mar 19, 2026
fd5dc6b
git commit -m "Stop tracking auto-generated _ui.py files"
Yuanshan-Wu Mar 20, 2026
81f2350
Merge main into 28-Bmode-CEUS-Image-Toggle and resolve conflicts favo…
OmidChaghaneh Apr 27, 2026
5b7ca19
Update ceus engine submodule with noise floor export fix
OmidChaghaneh Apr 27, 2026
594e876
Refactor B-mode/CEUS toggle and preprocessing to follow MVC convention
OmidChaghaneh May 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion engines/ceus
Submodule ceus updated from 97d3d3 to b8044e
2 changes: 1 addition & 1 deletion engines/qus
Submodule qus updated 56 files
+418 −0 CLI-Demos/bmode_radiomics_demo.ipynb
+5 −3 README.md
+3 −3 quantus/analysis/README.md
+0 −24 quantus/analysis/bmode/analysis_methods/bmode_intensity.py
+0 −25 quantus/analysis/bmode/analysis_methods/bmode_snr.py
+3 −3 quantus/analysis/bmode/framework.py
+372 −0 quantus/analysis/bmode/functions.py
+297 −0 quantus/analysis/bmode/radiomics_utils.py
+147 −0 quantus/analysis/bmode/test_bmode_radiomics.py
+12 −15 quantus/analysis/options.py
+0 −85 quantus/analysis/paramap/analysis_methods/attenuation_coef.py
+0 −64 quantus/analysis/paramap/analysis_methods/bsc.py
+0 −368 quantus/analysis/paramap/analysis_methods/bsc_stft.py
+0 −75 quantus/analysis/paramap/analysis_methods/central_freq_shift.py
+0 −31 quantus/analysis/paramap/analysis_methods/compute_power_spectra.py
+0 −159 quantus/analysis/paramap/analysis_methods/hscan.py
+0 −76 quantus/analysis/paramap/analysis_methods/lizzi_feleppa.py
+0 −37 quantus/analysis/paramap/analysis_methods/nakagami_params.py
+3 −4 quantus/analysis/paramap/framework.py
+849 −0 quantus/analysis/paramap/functions.py
+3 −3 quantus/analysis_config/README.md
+0 −30 quantus/analysis_config/utc_config/config_loaders/clarius_C3_config.py
+0 −31 quantus/analysis_config/utc_config/config_loaders/clarius_L15_config.py
+0 −57 quantus/analysis_config/utc_config/config_loaders/custom.py
+0 −31 quantus/analysis_config/utc_config/config_loaders/philips_3d_config.py
+0 −27 quantus/analysis_config/utc_config/config_loaders/pkl_rf.py
+168 −0 quantus/analysis_config/utc_config/functions.py
+8 −9 quantus/analysis_config/utc_config/options.py
+3 −3 quantus/data_export/README.md
+10 −0 quantus/data_export/bmode_csv/framework.py
+3 −0 quantus/data_export/bmode_csv/functions.py
+0 −93 quantus/data_export/csv/export_funcs/bsc_stft_arr.py
+0 −26 quantus/data_export/csv/export_funcs/descr_vals.py
+0 −40 quantus/data_export/csv/export_funcs/hscan_arr.py
+0 −70 quantus/data_export/csv/export_funcs/hscan_stats.py
+0 −38 quantus/data_export/csv/export_funcs/paramap_arr.py
+0 −54 quantus/data_export/csv/export_funcs/radiomics_stats.py
+2 −3 quantus/data_export/csv/framework.py
+288 −0 quantus/data_export/csv/functions.py
+13 −15 quantus/data_export/options.py
+2 −2 quantus/seg_loading/README.md
+34 −4 quantus/seg_loading/functions.py
+10 −14 quantus/seg_loading/options.py
+0 −0 quantus/seg_loading/seg_loaders/__init__.py
+0 −37 quantus/seg_loading/seg_loaders/nifti_voi.py
+2 −2 quantus/visualizations/README.md
+18 −0 quantus/visualizations/bmode/framework.py
+8 −0 quantus/visualizations/bmode/functions.py
+14 −16 quantus/visualizations/options.py
+2 −3 quantus/visualizations/paramap/framework.py
+491 −0 quantus/visualizations/paramap/functions.py
+0 −228 quantus/visualizations/paramap/visualization_funcs/plot_bsc_stft.py
+0 −164 quantus/visualizations/paramap/visualization_funcs/plot_hscan_result.py
+0 −66 quantus/visualizations/paramap/visualization_funcs/plot_hscan_wavelets.py
+0 −53 quantus/visualizations/paramap/visualization_funcs/plot_ps_window_data.py
+ requirements.txt
Binary file modified requirements.txt
Binary file not shown.
124 changes: 115 additions & 9 deletions src/ceus/application_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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] = {}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -211,15 +220,20 @@ 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)
"""
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 = {}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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')):
Expand All @@ -389,17 +426,81 @@ 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')}")
print(f" - pixel_data is None: {getattr(image_data, 'pixel_data', None) is 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]:
Expand Down Expand Up @@ -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()
Expand Down
12 changes: 10 additions & 2 deletions src/ceus/image_loading/image_loading_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand All @@ -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
Expand Down
115 changes: 114 additions & 1 deletion src/ceus/image_loading/ui/file_selection.ui
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="img_selection_layout" stretch="2,0,0,0,3,0,2">
<layout class="QVBoxLayout" name="img_selection_layout" stretch="2,0,0,0,0,3,0,2">
<property name="spacing">
<number>20</number>
</property>
Expand Down Expand Up @@ -656,6 +656,119 @@
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="chooseBmodeLayout">
<property name="leftMargin">
<number>20</number>
</property>
<property name="rightMargin">
<number>20</number>
</property>
<item>
<widget class="QLabel" name="bmode_path_label">
<property name="styleSheet">
<string notr="true">QLabel {
background-color: rgba(255, 255, 255, 0);
color: white;
font-size: 17px;
}</string>
</property>
<property name="text">
<string>Input Path to B-mode Image (optional)</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="textInteractionFlags">
<set>Qt::NoTextInteraction</set>
</property>
</widget>
</item>
<item alignment="Qt::AlignHCenter|Qt::AlignVCenter">
<widget class="QLineEdit" name="bmode_path_input">
<property name="minimumSize">
<size>
<width>201</width>
<height>31</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>401</width>
<height>31</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QLineEdit {
background-color: rgb(249, 249, 249);
color: black;
}</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="chooseBmodeButtonsLayout">
<property name="spacing">
<number>6</number>
</property>
<item alignment="Qt::AlignRight|Qt::AlignVCenter">
<widget class="QPushButton" name="choose_bmode_path_button">
<property name="minimumSize">
<size>
<width>131</width>
<height>41</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>131</width>
<height>41</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: white;
font-size: 16px;
background: rgb(90, 37, 255);
border-radius: 15px;
}</string>
</property>
<property name="text">
<string>Choose File</string>
</property>
</widget>
</item>
<item alignment="Qt::AlignLeft|Qt::AlignVCenter">
<widget class="QPushButton" name="clear_bmode_path_button">
<property name="minimumSize">
<size>
<width>131</width>
<height>41</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>131</width>
<height>41</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: white;
font-size: 16px;
background: rgb(90, 37, 255);
border-radius: 15px;
}</string>
</property>
<property name="text">
<string>Clear Path</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="loading_options_label">
<property name="styleSheet">
Expand Down
Loading