Skip to content

Commit 49410ef

Browse files
Add confirmation request in case of ROI override
1 parent df0a7ca commit 49410ef

4 files changed

Lines changed: 275 additions & 0 deletions

File tree

datalab/gui/processor/base.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,30 @@ def register_computations(self) -> None:
792792
self.register_processing()
793793
self.register_analysis()
794794

795+
# pylint: disable=unused-argument
796+
def preprocess_1_to_0(
797+
self,
798+
func: Callable,
799+
param: gds.DataSet | None,
800+
objs: list[SignalObj | ImageObj],
801+
) -> bool:
802+
"""Pre-check hook for 1-to-0 operations (hook method).
803+
804+
This method is called before a 1-to-0 computation starts, before the
805+
progress dialog is opened. Subclasses can override this method to perform
806+
pre-checks or ask for user confirmation. Return ``False`` to abort the
807+
computation.
808+
809+
Args:
810+
func: The computation function that will be called
811+
param: Optional parameter set
812+
objs: List of objects that will be processed
813+
814+
Returns:
815+
True to proceed with the computation, False to abort
816+
"""
817+
return True
818+
795819
# pylint: disable=unused-argument
796820
def postprocess_1_to_0_result(
797821
self, obj: SignalObj | ImageObj, result: GeometryResult | TableResult
@@ -1401,6 +1425,8 @@ def compute_1_to_0(
14011425
if target_objs is not None
14021426
else self.panel.objview.get_sel_objects(include_groups=True)
14031427
)
1428+
if not self.preprocess_1_to_0(func, param, objs):
1429+
return None
14041430
current_obj = self.panel.objview.get_current_object()
14051431
title = func.__name__ if title is None else title
14061432
refresh_needed = False

datalab/gui/processor/image.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from __future__ import annotations
1010

11+
import guidata.dataset as gds
1112
import numpy as np
1213
import sigima.params
1314
import sigima.proc.base as sipb
@@ -25,6 +26,7 @@
2526
)
2627
from sigima.objects.scalar import GeometryResult, TableResult
2728

29+
from datalab import env
2830
from datalab.config import APP_NAME, _
2931
from datalab.gui.processor.base import BaseProcessor
3032
from datalab.gui.processor.geometry_postprocess import (
@@ -55,6 +57,50 @@ def _wrap_geometric_transform(self, func, operation: str):
5557
"""
5658
return GeometricTransformWrapper(func, operation)
5759

60+
def preprocess_1_to_0(
61+
self,
62+
func,
63+
param: gds.DataSet | None,
64+
objs: list[ImageObj],
65+
) -> bool:
66+
"""Override to confirm ROI replacement before the progress bar opens.
67+
68+
When the parameter has ``create_rois=True`` and at least one selected
69+
image already has ROIs, the user is warned that the existing ROIs will
70+
be replaced.
71+
72+
Args:
73+
func: The computation function that will be called
74+
param: Optional parameter set
75+
objs: List of image objects that will be processed
76+
77+
Returns:
78+
True to proceed with the computation, False to abort
79+
"""
80+
if (
81+
param is not None
82+
and getattr(param, "create_rois", False)
83+
and not env.execenv.unattended
84+
and any(obj.roi is not None and not obj.roi.is_empty() for obj in objs)
85+
):
86+
return (
87+
QW.QMessageBox.question(
88+
self.mainwindow,
89+
_("Warning"),
90+
_(
91+
"Regions of interest are already defined for this "
92+
"image.<br><br>"
93+
"Creating new ROIs from detection will replace the "
94+
"existing ones, which will be lost.<br><br>"
95+
"Do you want to continue?"
96+
),
97+
QW.QMessageBox.Yes | QW.QMessageBox.No,
98+
QW.QMessageBox.No,
99+
)
100+
== QW.QMessageBox.Yes
101+
)
102+
return True
103+
58104
def postprocess_1_to_0_result(
59105
self, obj: ImageObj, result: GeometryResult | TableResult
60106
) -> bool:

datalab/locale/fr/LC_MESSAGES/datalab.po

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2430,6 +2430,9 @@ msgstr "Détection de taches basée sur SimpleBlobDetector d'OpenCV"
24302430
msgid "Creating a ROI grid will overwrite any existing ROI.<br><br>Do you want to continue?"
24312431
msgstr "La création d'une grille de ROI écrasera toute ROI existante.<br><br>Voulez-vous continuer ?"
24322432

2433+
msgid "Regions of interest are already defined for this image.<br><br>Creating new ROIs from detection will replace the existing ones, which will be lost.<br><br>Do you want to continue?"
2434+
msgstr "Des régions d'intérêt sont déjà définies pour cette image.<br><br>La création de nouvelles ROI par détection remplacera les existantes, qui seront perdues.<br><br>Voulez-vous continuer ?"
2435+
24332436
msgid "Extract ROI"
24342437
msgstr "Extraire une ROI"
24352438

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2+
3+
"""
4+
Detection ROI replacement confirmation test
5+
6+
Testing the following:
7+
- When create_rois=True and image already has ROIs, the preprocess hook
8+
runs before the progress bar and can abort the computation
9+
- In unattended mode (automated tests) the dialog is skipped and ROIs
10+
are always replaced
11+
- When create_rois=False, existing ROIs are left untouched
12+
- When no existing ROIs are present, ROI creation proceeds normally
13+
- When the user cancels the confirmation dialog, existing ROIs are preserved
14+
- The confirmation dialog is shown only when ROIs already exist
15+
"""
16+
17+
# guitest: show
18+
19+
from __future__ import annotations
20+
21+
from unittest.mock import patch
22+
23+
import sigima.params
24+
import sigima.proc.image as sipi
25+
from qtpy import QtWidgets as QW
26+
from sigima.objects import NewImageParam, create_image_roi
27+
from sigima.tests.data import create_multigaussian_image, create_peak_image
28+
29+
from datalab.env import execenv
30+
from datalab.tests import datalab_test_app_context
31+
from datalab.tests.features.image.roi_app_test import IROI1, IROI2
32+
33+
34+
def _create_image_with_roi():
35+
"""Return a multigaussian image that already has ROIs defined."""
36+
newparam = NewImageParam.create(height=200, width=200)
37+
ima = create_multigaussian_image(newparam)
38+
roi = create_image_roi("rectangle", IROI1)
39+
roi.add_roi(create_image_roi("circle", IROI2))
40+
ima.roi = roi
41+
return ima
42+
43+
44+
def test_create_rois_no_existing_roi():
45+
"""ROIs are created normally when the image has no pre-existing ROI."""
46+
with datalab_test_app_context() as win:
47+
panel = win.imagepanel
48+
ima = create_peak_image()
49+
assert ima.roi is None
50+
panel.add_object(ima)
51+
52+
param = sigima.params.Peak2DDetectionParam.create(create_rois=True)
53+
result = panel.processor.compute_peak_detection(param)
54+
assert result is not None, "Peak detection should return results"
55+
56+
obj = panel.objview.get_current_object()
57+
assert obj.roi is not None, "ROI should be created when create_rois=True"
58+
assert not obj.roi.is_empty(), "Created ROI should not be empty"
59+
60+
61+
def test_create_rois_with_existing_roi_unattended():
62+
"""In unattended mode, existing ROIs are silently replaced (no dialog)."""
63+
with datalab_test_app_context() as win:
64+
panel = win.imagepanel
65+
ima = _create_image_with_roi()
66+
initial_roi = ima.roi
67+
panel.add_object(ima)
68+
69+
# execenv.unattended is True in the test suite: the dialog is skipped
70+
assert execenv.unattended, "This test requires unattended mode"
71+
72+
param = sigima.params.Peak2DDetectionParam.create(create_rois=True)
73+
result = panel.processor.compute_peak_detection(param)
74+
assert result is not None, "Peak detection should return results"
75+
76+
obj = panel.objview.get_current_object()
77+
assert obj.roi is not None, "ROI should be present after detection"
78+
assert obj.roi != initial_roi, (
79+
"Existing ROI should have been replaced in unattended mode"
80+
)
81+
82+
83+
def test_create_rois_false_preserves_existing_roi():
84+
"""When create_rois=False, existing ROIs are never touched."""
85+
with datalab_test_app_context() as win:
86+
panel = win.imagepanel
87+
ima = _create_image_with_roi()
88+
panel.add_object(ima)
89+
90+
obj = panel.objview.get_current_object()
91+
roi_before = obj.roi
92+
93+
param = sigima.params.Peak2DDetectionParam.create(create_rois=False)
94+
panel.processor.compute_peak_detection(param)
95+
96+
obj = panel.objview.get_current_object()
97+
assert obj.roi == roi_before, (
98+
"Existing ROI must not be modified when create_rois=False"
99+
)
100+
101+
102+
def test_dialog_shown_only_when_roi_exists():
103+
"""The confirmation dialog is triggered only when the image already has ROIs.
104+
105+
- With existing ROIs and create_rois=True: preprocess_1_to_0 calls the dialog
106+
- Without existing ROIs and create_rois=True: preprocess_1_to_0 returns True
107+
directly, without opening any dialog
108+
"""
109+
with datalab_test_app_context() as win:
110+
panel = win.imagepanel
111+
param = sigima.params.Peak2DDetectionParam.create(create_rois=True)
112+
113+
# Case 1: image with no ROI — dialog must NOT be shown
114+
ima_no_roi = create_peak_image()
115+
panel.add_object(ima_no_roi)
116+
objs_no_roi = panel.objview.get_sel_objects(include_groups=True)
117+
118+
with patch.object(QW.QMessageBox, "question") as mock_question:
119+
execenv.unattended = False
120+
try:
121+
result = panel.processor.preprocess_1_to_0(
122+
sipi.peak_detection, param, objs_no_roi
123+
)
124+
finally:
125+
execenv.unattended = True
126+
127+
assert result is True, "Should proceed when no existing ROIs"
128+
mock_question.assert_not_called()
129+
130+
# Case 2: image with existing ROI — dialog MUST be shown
131+
ima_with_roi = _create_image_with_roi()
132+
panel.add_object(ima_with_roi)
133+
objs_with_roi = panel.objview.get_sel_objects(include_groups=True)
134+
135+
with patch.object(
136+
QW.QMessageBox, "question", return_value=QW.QMessageBox.Yes
137+
) as mock_question:
138+
execenv.unattended = False
139+
try:
140+
result = panel.processor.preprocess_1_to_0(
141+
sipi.peak_detection, param, objs_with_roi
142+
)
143+
finally:
144+
execenv.unattended = True
145+
146+
assert result is True, "Should proceed when user confirms"
147+
mock_question.assert_called_once()
148+
149+
150+
def test_cancel_dialog_preserves_existing_roi():
151+
"""When the user cancels the confirmation dialog, existing ROIs are preserved."""
152+
with datalab_test_app_context() as win:
153+
panel = win.imagepanel
154+
ima = _create_image_with_roi()
155+
panel.add_object(ima)
156+
157+
obj = panel.objview.get_current_object()
158+
roi_before = obj.roi
159+
160+
param = sigima.params.Peak2DDetectionParam.create(create_rois=True)
161+
162+
# Simulate the user clicking "No" in the confirmation dialog
163+
with patch.object(QW.QMessageBox, "question", return_value=QW.QMessageBox.No):
164+
execenv.unattended = False
165+
try:
166+
result = panel.processor.compute_peak_detection(param)
167+
finally:
168+
execenv.unattended = True
169+
170+
assert result is None, "Computation should be aborted when user cancels"
171+
obj = panel.objview.get_current_object()
172+
assert obj.roi == roi_before, (
173+
"Existing ROI must be preserved when user cancels the dialog"
174+
)
175+
176+
177+
def test_preprocess_hook_abort_skipped_in_unattended():
178+
"""preprocess_1_to_0 returns True in unattended mode (no blocking dialog)."""
179+
with datalab_test_app_context() as win:
180+
panel = win.imagepanel
181+
ima = _create_image_with_roi()
182+
panel.add_object(ima)
183+
184+
assert execenv.unattended, "This test requires unattended mode"
185+
186+
param = sigima.params.Peak2DDetectionParam.create(create_rois=True)
187+
objs = panel.objview.get_sel_objects(include_groups=True)
188+
189+
# In unattended mode the hook must always return True (no dialog shown)
190+
result = panel.processor.preprocess_1_to_0(sipi.peak_detection, param, objs)
191+
assert result is True, "preprocess_1_to_0 must return True in unattended mode"
192+
193+
194+
if __name__ == "__main__":
195+
test_create_rois_no_existing_roi()
196+
test_create_rois_with_existing_roi_unattended()
197+
test_create_rois_false_preserves_existing_roi()
198+
test_dialog_shown_only_when_roi_exists()
199+
test_cancel_dialog_preserves_existing_roi()
200+
test_preprocess_hook_abort_skipped_in_unattended()

0 commit comments

Comments
 (0)