Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 29 additions & 8 deletions package/PartSeg/_roi_mask/main_window.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import os
from collections.abc import Sequence
from contextlib import suppress
from functools import partial
from typing import Union

import numpy as np
from qtpy.QtCore import QByteArray, Qt, Signal, Slot
Expand All @@ -16,8 +18,10 @@
QMessageBox,
QProgressBar,
QPushButton,
QScrollArea,
QSizePolicy,
QSpinBox,
QSplitter,
QTabWidget,
QTextEdit,
QVBoxLayout,
Expand Down Expand Up @@ -393,7 +397,7 @@ def leaveEvent(self, _event):
self.mouse_leave.emit(self.number)


class ChosenComponents(QWidget):
class ChosenComponents(QScrollArea):
"""
:type check_box: dict[int, ComponentCheckBox]
"""
Expand All @@ -404,6 +408,7 @@ class ChosenComponents(QWidget):

def __init__(self):
super().__init__()
self.setWidget(QWidget(self))
self.check_box = {}
self.check_all_btn = QPushButton("Select all")
self.check_all_btn.clicked.connect(self.check_all)
Expand All @@ -416,11 +421,15 @@ def __init__(self):
self.check_layout = FlowLayout()
main_layout.addLayout(btn_layout)
main_layout.addLayout(self.check_layout)
self.setLayout(main_layout)
self.widget().setLayout(main_layout)
self.setWidgetResizable(True)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)

def other_component_choose(self, num):
check = self.check_box[num]
check.setChecked(not check.isChecked())
self.ensureWidgetVisible(check)

def check_all(self):
for el in self.check_box.values():
Expand All @@ -439,10 +448,12 @@ def remove_components(self):
el.mouse_enter.disconnect()
self.check_box.clear()

def new_choose(self, num, chosen_components):
self.set_chose(range(1, num + 1), chosen_components)
def new_choose(self, num: int, chosen_components: Sequence[int]) -> None:
self.set_components(range(1, num + 1), chosen_components)

def set_chose(self, components_index, chosen_components):
def set_components(self, components_index, chosen_components: Union[Sequence[int], None] = None):
if chosen_components is None:
chosen_components = []
chosen_components = set(chosen_components)
self.blockSignals(True)
self.remove_components()
Expand All @@ -459,11 +470,18 @@ def set_chose(self, components_index, chosen_components):
self.update()
self.check_change_signal.emit()

def set_chosen(self, chosen_components: Sequence[int]):
chosen_components = set(chosen_components)
for num, check in self.check_box.items():
check.setChecked(num in chosen_components)
Comment on lines +473 to +476
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Batch set_chosen() updates into a single refresh.

Each setChecked() here can emit stateChanged, which reaches check_change_signal and image_view.refresh_selected() via Line 544. Since StackSettings now calls set_chosen() after ROI loads at Line 174 and Line 306 in package/PartSeg/_roi_mask/stack_settings.py, loading a segmentation with many selected components will refresh the view once per component.

Proposed fix
     def set_chosen(self, chosen_components: Sequence[int]):
         chosen_components = set(chosen_components)
-        for num, check in self.check_box.items():
-            check.setChecked(num in chosen_components)
+        self.blockSignals(True)
+        try:
+            for num, check in self.check_box.items():
+                check.setChecked(num in chosen_components)
+        finally:
+            self.blockSignals(False)
+        self.check_change_signal.emit()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def set_chosen(self, chosen_components: Sequence[int]):
chosen_components = set(chosen_components)
for num, check in self.check_box.items():
check.setChecked(num in chosen_components)
def set_chosen(self, chosen_components: Sequence[int]):
chosen_components = set(chosen_components)
self.blockSignals(True)
try:
for num, check in self.check_box.items():
check.setChecked(num in chosen_components)
finally:
self.blockSignals(False)
self.check_change_signal.emit()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package/PartSeg/_roi_mask/main_window.py` around lines 473 - 476, The loop in
set_chosen causes many stateChanged signals and repeated image refreshes; wrap
per-checkbox updates with signal blocking and then trigger a single update at
the end: in set_chosen (method name) call check.blockSignals(True) before
check.setChecked(...) and check.blockSignals(False) after the loop (or for each
check), then explicitly emit the existing check_change_signal or call
image_view.refresh_selected() once after the loop to perform a single refresh;
this prevents per-checkbox stateChanged handlers from firing during the batch
update invoked by StackSettings.

self.check_change_signal.emit()

def check_change(self):
self.check_change_signal.emit()

def change_state(self, num, val):
self.check_box[num].setChecked(val)
self.ensureWidgetVisible(self.check_box[num])

def get_state(self, num: int) -> bool:
# TODO Check what situation create report of id ID: af9b57f074264169b4353aa1e61d8bc2
Expand Down Expand Up @@ -527,6 +545,7 @@ def __init__(self, settings: StackSettings, image_view: StackImageView): # noqa
self.choose_components.check_change_signal.connect(image_view.refresh_selected)
self.choose_components.mouse_leave.connect(image_view.component_unmark)
self.choose_components.mouse_enter.connect(image_view.component_mark)

self.chosen_list = []
self.progress_bar2 = QProgressBar()
self.progress_bar2.setHidden(True)
Expand Down Expand Up @@ -566,8 +585,10 @@ def __init__(self, settings: StackSettings, image_view: StackImageView): # noqa
main_layout.addWidget(self.progress_bar2)
main_layout.addWidget(self.progress_bar)
main_layout.addWidget(self.progress_info_lab)
main_layout.addWidget(self.algorithm_choose_widget, 1)
main_layout.addWidget(self.choose_components)
split = QSplitter(Qt.Orientation.Vertical)
split.addWidget(self.algorithm_choose_widget)
split.addWidget(self.choose_components)
main_layout.addWidget(split, 1)
down_layout = QHBoxLayout()
down_layout.addWidget(self.keep_chosen_components_chk)
down_layout.addWidget(self.show_parameters)
Expand Down Expand Up @@ -659,7 +680,7 @@ def segmentation(self, val):

def _image_changed(self):
self.settings.roi = None
self.choose_components.set_chose([], [])
self.choose_components.set_components([], [])

def _execute_in_background_init(self):
if self.batch_process.isRunning():
Expand Down
26 changes: 14 additions & 12 deletions package/PartSeg/_roi_mask/stack_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,17 +170,15 @@ def set_project_info(self, data: typing.Union[MaskProjectTuple, PointsInfo]):
data.selected_components,
self.keep_chosen_components,
)
self.chosen_components_widget.set_chose(
sorted(state2.roi_extraction_parameters.keys()), state2.selected_components
)
self.roi = state2.roi_info
self.chosen_components_widget.set_chosen(state2.selected_components)

self.components_parameters_dict = state2.roi_extraction_parameters
else:
self.set_history(data.history)
self.chosen_components_widget.set_chose(
sorted(data.roi_extraction_parameters.keys()), data.selected_components
)
self.roi = data.roi_info
self.chosen_components_widget.set_chosen(data.selected_components)

self.components_parameters_dict = data.roi_extraction_parameters

@staticmethod
Expand Down Expand Up @@ -304,18 +302,22 @@ def _set_roi_info(
raise ValueError("ROI do not fit to image") from e
if save_chosen:
state2 = self.transform_state(state, new_roi_info, segmentation_parameters, list_of_components, save_chosen)
self.chosen_components_widget.set_chose(
sorted(state2.roi_extraction_parameters.keys()), state2.selected_components
)
self.roi = state2.roi_info
self.chosen_components_widget.set_chosen(state2.selected_components)
self.components_parameters_dict = state2.roi_extraction_parameters
else:
selected_parameters = {i: segmentation_parameters[i] for i in new_roi_info.bound_info}

self.chosen_components_widget.set_chose(sorted(selected_parameters.keys()), list_of_components)
self.roi = new_roi_info
self.chosen_components_widget.set_chosen(list_of_components)
self.components_parameters_dict = segmentation_parameters

def post_roi_set(self):
if self.chosen_components_widget is not None:
prev = self.chosen_components_widget.blockSignals(True)
try:
self.chosen_components_widget.set_components(self.roi_info.bound_info.keys(), [])
finally:
self.chosen_components_widget.blockSignals(prev)


def get_mask(
segmentation: typing.Optional[np.ndarray], mask: typing.Optional[np.ndarray], selected: list[int]
Expand Down
5 changes: 5 additions & 0 deletions package/PartSeg/common_backend/base_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ def roi(self, val: Union[np.ndarray, ROIInfo]):
if val is None:
self._roi_info = ROIInfo(val)
self._additional_layers = {}
self.post_roi_set()
self.roi_clean.emit()
return
try:
Expand All @@ -155,8 +156,12 @@ def roi(self, val: Union[np.ndarray, ROIInfo]):
except ValueError as e:
raise ValueError(ROI_NOT_FIT) from e
self._additional_layers = {}
self.post_roi_set()
self.roi_changed.emit(self._roi_info)

def post_roi_set(self) -> None:
"""called after roi is set, for subclasses to override"""

Comment on lines +159 to +164
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make post_roi_set() run on every ROI update path.

Right now this hook only fires in the non-None branch here. The early return above and BaseSettings.set_segmentation_result() at Line 537 still bypass it, so subclasses overriding post_roi_set() will stay synchronized for some ROI updates but not others.

Proposed fix
     def roi(self, val: Union[np.ndarray, ROIInfo]):
         if val is None:
             self._roi_info = ROIInfo(val)
             self._additional_layers = {}
+            self.post_roi_set()
             self.roi_clean.emit()
             return
         try:
             if isinstance(val, np.ndarray):
                 self._roi_info = ROIInfo(self.image.fit_array_to_image(val))
@@
         if result.points is not None:
             self.points = result.points
         self._roi_info = roi_info
+        self.post_roi_set()
         self.roi_changed.emit(self._roi_info)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package/PartSeg/common_backend/base_settings.py` around lines 158 - 163, The
post_roi_set() hook is only called in one branch, so ensure it runs on every ROI
update path: call self.post_roi_set() before any early returns in the ROI setter
(so it executes even when ROI is set to None) and add a call to
self.post_roi_set() inside BaseSettings.set_segmentation_result (the method
referenced as set_segmentation_result) after the internal ROI/state is updated;
keep the existing self.roi_changed.emit(self._roi_info) behavior but ensure
post_roi_set() is invoked consistently before or right after emitting so
subclasses are always synchronized.

@property
def sizes(self):
return self._roi_info.sizes
Expand Down
2 changes: 1 addition & 1 deletion package/PartSeg/common_gui/napari_image_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ def _remove_worker(self, sender=None):
else:
logging.debug("[_remove_worker] %s", sender)

def _add_layer_util(self, index, layer, filters):
def _add_layer_util(self, index: int, layer: _NapariImage, filters: list[tuple[NoiseFilterType, float]]) -> None:
if layer not in self.viewer.layers:
self.viewer.add_layer(layer)

Expand Down
Loading