Skip to content

Commit 96434f0

Browse files
committed
Fix: add option to keep analysis results after computation
1 parent c1560d4 commit 96434f0

File tree

12 files changed

+221
-64
lines changed

12 files changed

+221
-64
lines changed

cdl/computation/base.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import guidata.dataset as gds
1818
import numpy as np
1919

20-
from cdl.config import _
20+
from cdl.config import Conf, _
2121
from cdl.obj import ResultProperties, create_signal
2222

2323
if TYPE_CHECKING:
@@ -200,7 +200,8 @@ def dst_11(
200200
if suffix: # suffix may be None or an empty string
201201
title += suffix
202202
dst = src.copy(title=title)
203-
dst.delete_results() # Remove any previous results
203+
if not Conf.proc.keep_results.get():
204+
dst.delete_results() # Remove any previous results
204205
return dst
205206

206207

@@ -228,7 +229,8 @@ def dst_n1n(
228229
if suffix is not None:
229230
title += "|" + suffix
230231
dst = src1.copy(title=title)
231-
dst.delete_results() # Remove any previous results
232+
if not Conf.proc.keep_results.get():
233+
dst.delete_results() # Remove any previous results
232234
return dst
233235

234236

cdl/computation/image/__init__.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
dst_n1n,
4646
new_signal_result,
4747
)
48-
from cdl.config import _
48+
from cdl.config import Conf, _
4949
from cdl.obj import (
5050
BaseProcParam,
5151
ImageObj,
@@ -60,21 +60,26 @@
6060

6161

6262
def restore_data_outside_roi(dst: ImageObj, src: ImageObj) -> None:
63-
"""Restore data outside the Region Of Interest (ROI) of the input image after a
64-
computation, only if the input image has a ROI, if the data types are compatible,
63+
"""Restore data outside the Region Of Interest (ROI) of the input image
64+
after a computation, only if the input image has a ROI,
65+
and if the output image has the same ROI as the input image,
66+
and if the data types are compatible,
6567
and if the shapes are the same.
6668
Otherwise, do nothing.
6769
6870
Args:
6971
dst: output image object
7072
src: input image object
7173
"""
72-
if (
73-
src.maskdata is not None
74-
and (dst.data.dtype == src.data.dtype or not is_integer_dtype(dst.data.dtype))
75-
and dst.data.shape == src.data.shape
76-
):
77-
dst.data[src.maskdata] = src.data[src.maskdata]
74+
if src.maskdata is not None and dst.maskdata is not None:
75+
if (
76+
np.array_equal(src.maskdata, dst.maskdata)
77+
and (
78+
dst.data.dtype == src.data.dtype or not is_integer_dtype(dst.data.dtype)
79+
)
80+
and dst.data.shape == src.data.shape
81+
):
82+
dst.data[src.maskdata] = src.data[src.maskdata]
7883

7984

8085
class Wrap11Func:
@@ -289,7 +294,8 @@ def compute_arithmetic(src1: ImageObj, src2: ImageObj, p: ArithmeticParam) -> Im
289294
initial_dtype = src1.data.dtype
290295
title = p.operation.replace("obj1", src1.short_id).replace("obj2", src2.short_id)
291296
dst = src1.copy(title=title)
292-
dst.delete_results() # Remove any previous results
297+
if not Conf.proc.keep_results.get():
298+
dst.delete_results() # Remove any previous results
293299
o, a, b = p.operator, p.factor, p.constant
294300
# Apply operator
295301
if o in ("×", "/") and a == 0.0:

cdl/computation/signal.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,27 +39,31 @@
3939
dst_n1n,
4040
new_signal_result,
4141
)
42-
from cdl.config import _
42+
from cdl.config import Conf, _
4343
from cdl.obj import ResultProperties, ResultShape, ROI1DParam, SignalObj
4444

4545
VALID_DTYPES_STRLIST = SignalObj.get_valid_dtypenames()
4646

4747

4848
def restore_data_outside_roi(dst: SignalObj, src: SignalObj) -> None:
49-
"""Restore data outside the region of interest, after a computation, only if the
50-
source signal has a ROI, if the data types are the same and if the shapes are the
51-
same. Otherwise, do nothing.
49+
"""Restore data outside the Region Of Interest (ROI) of the input signal
50+
after a computation, only if the input signal has a ROI,
51+
and if the output signal has the same ROI as the input signal,
52+
and if the data types are the same,
53+
and if the shapes are the same.
54+
Otherwise, do nothing.
5255
5356
Args:
5457
dst: destination signal object
5558
src: source signal object
5659
"""
57-
if (
58-
src.maskdata is not None
59-
and dst.xydata.dtype == src.xydata.dtype
60-
and dst.xydata.shape == src.xydata.shape
61-
):
62-
dst.xydata[src.maskdata] = src.xydata[src.maskdata]
60+
if src.maskdata is not None and dst.maskdata is not None:
61+
if (
62+
np.array_equal(src.maskdata, dst.maskdata)
63+
and dst.xydata.dtype == src.xydata.dtype
64+
and dst.xydata.shape == src.xydata.shape
65+
):
66+
dst.xydata[src.maskdata] = src.xydata[src.maskdata]
6367

6468

6569
class Wrap11Func:
@@ -251,7 +255,8 @@ def compute_arithmetic(
251255
initial_dtype = src1.xydata.dtype
252256
title = p.operation.replace("obj1", src1.short_id).replace("obj2", src2.short_id)
253257
dst = src1.copy(title=title)
254-
dst.delete_results() # Remove any previous results
258+
if not Conf.proc.keep_results.get():
259+
dst.delete_results() # Remove any previous results
255260
o, a, b = p.operator, p.factor, p.constant
256261
if o in ("×", "/") and a == 0.0:
257262
dst.y = np.ones_like(src1.y) * b

cdl/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@ class ProcSection(conf.Section, metaclass=conf.SectionMeta):
161161
# - False: extract each ROI in a separate signal or image
162162
extract_roi_singleobj = conf.Option()
163163

164+
# Keep analysis results after processing:
165+
# - True: keep analysis results (dangerous because results may not be valid anymore)
166+
# - False: do not keep analysis results (default)
167+
keep_results = conf.Option()
168+
164169
# FFT shift enabled state for signal/image processing:
165170
# - True: FFT shift is enabled (default)
166171
# - False: FFT shift is disabled
@@ -309,6 +314,7 @@ def initialize():
309314
Conf.proc.operation_mode.get("single")
310315
Conf.proc.fft_shift_enabled.get(True)
311316
Conf.proc.extract_roi_singleobj.get(False)
317+
Conf.proc.keep_results.get(False)
312318
Conf.proc.ignore_warnings.get(False)
313319
# View section
314320
tb_pos = Conf.view.plot_toolbar_position.get("left")

cdl/core/gui/panel/base.py

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
from cdl.core.gui import actionhandler, objectmodel, objectview
3535
from cdl.core.gui.roieditor import TypeROIEditor
3636
from cdl.core.model.base import (
37+
ANN_KEY,
38+
ROI_KEY,
3739
ResultProperties,
3840
ResultShape,
3941
TypeObj,
@@ -247,6 +249,18 @@ def create_resultdata_dict(objs: list[SignalObj | ImageObj]) -> dict[str, Result
247249
return rdatadict
248250

249251

252+
class PasteMetadataParam(gds.DataSet):
253+
"""Paste metadata parameters"""
254+
255+
keep_roi = gds.BoolItem(_("Regions of interest"), default=True)
256+
keep_resultshapes = gds.BoolItem(_("Result shapes"), default=False).set_pos(col=1)
257+
keep_annotations = gds.BoolItem(_("Annotations"), default=True)
258+
keep_resultproperties = gds.BoolItem(_("Result properties"), default=False).set_pos(
259+
col=1
260+
)
261+
keep_other = gds.BoolItem(_("Other metadata"), default=True)
262+
263+
250264
class BaseDataPanel(AbstractPanel, Generic[TypeObj, TypeROI, TypeROIEditor]):
251265
"""Object handling the item list, the selected item properties and plot"""
252266

@@ -535,15 +549,60 @@ def duplicate_object(self) -> None:
535549

536550
def copy_metadata(self) -> None:
537551
"""Copy object metadata"""
538-
obj_cpy = self.objview.get_sel_objects()[0].copy()
539-
obj_cpy.delete_results() # Remove all object-specific analysis results
540-
self.__metadata_clipboard = obj_cpy.metadata
541-
542-
def paste_metadata(self) -> None:
552+
obj = self.objview.get_sel_objects()[0]
553+
self.__metadata_clipboard = obj.metadata.copy()
554+
new_pref = obj.short_id + "_"
555+
for key, value in obj.metadata.items():
556+
if ResultShape.match(key, value):
557+
mshape = ResultShape.from_metadata_entry(key, value)
558+
if not re.match(obj.PREFIX + r"[0-9]{3}[\s]*", mshape.title):
559+
# Handling additional result (e.g. diameter)
560+
for a_key, a_value in obj.metadata.items():
561+
if isinstance(a_key, str) and a_key.startswith(mshape.title):
562+
self.__metadata_clipboard.pop(a_key)
563+
self.__metadata_clipboard[new_pref + a_key] = a_value
564+
mshape.title = new_pref + mshape.title
565+
# Handling result shape
566+
self.__metadata_clipboard.pop(key)
567+
self.__metadata_clipboard[mshape.key] = value
568+
569+
def paste_metadata(self, param: PasteMetadataParam | None = None) -> None:
543570
"""Paste metadata to selected object(s)"""
571+
if param is None:
572+
param = PasteMetadataParam(
573+
_("Paste metadata"),
574+
comment=_(
575+
"Select what to keep from the clipboard.<br><br>"
576+
"Result shapes and annotations, if kept, will be merged with "
577+
"existing ones. <u>All other metadata will be replaced</u>."
578+
),
579+
)
580+
if not param.edit(parent=self.parent()):
581+
return
582+
metadata = {}
583+
if param.keep_roi and ROI_KEY in self.__metadata_clipboard:
584+
metadata[ROI_KEY] = self.__metadata_clipboard[ROI_KEY]
585+
if param.keep_annotations and ANN_KEY in self.__metadata_clipboard:
586+
metadata[ANN_KEY] = self.__metadata_clipboard[ANN_KEY]
587+
if param.keep_resultshapes:
588+
for key, value in self.__metadata_clipboard.items():
589+
if ResultShape.match(key, value):
590+
metadata[key] = value
591+
if param.keep_resultproperties:
592+
for key, value in self.__metadata_clipboard.items():
593+
if ResultProperties.match(key, value):
594+
metadata[key] = value
595+
if param.keep_other:
596+
for key, value in self.__metadata_clipboard.items():
597+
if (
598+
not ResultShape.match(key, value)
599+
and not ResultProperties.match(key, value)
600+
and key not in (ROI_KEY, ANN_KEY)
601+
):
602+
metadata[key] = value
544603
sel_objects = self.objview.get_sel_objects(include_groups=True)
545604
for obj in sorted(sel_objects, key=lambda obj: obj.short_id, reverse=True):
546-
obj.metadata.update(self.__metadata_clipboard)
605+
obj.update_metadata_from(metadata)
547606
# We have to do a manual refresh in order to force the plot handler to update
548607
# all plot items, even the ones that are not visible (otherwise, image masks
549608
# would not be updated after pasting the metadata: see issue #123)
@@ -1324,8 +1383,8 @@ class PlotResultParam(gds.DataSet):
13241383
x.append(index)
13251384
else:
13261385
i_xaxis = rdata.xlabels.index(param.xaxis)
1327-
x.append(float(result.shown_array[mask, i_xaxis]))
1328-
y.append(float(result.shown_array[mask, i_yaxis]))
1386+
x.append(result.shown_array[mask, i_xaxis][0])
1387+
y.append(result.shown_array[mask, i_yaxis][0])
13291388
self.__add_result_signal(
13301389
x, y, f"{title}{roi_suffix}", param.xaxis, param.yaxis
13311390
)

cdl/core/gui/processor/base.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,8 @@ def compute_n1(
624624
src_dtype = src_obj1.data.dtype
625625
dst_dtype = complex if is_complex_dtype(src_dtype) else float
626626
dst_obj = src_obj1.copy(dtype=dst_dtype)
627-
dst_obj.delete_results() # Remove any previous results
627+
if not Conf.proc.keep_results.get():
628+
dst_obj.delete_results() # Remove any previous results
628629
src_objs_pair = [src_obj1]
629630
for src_gid in src_gids[1:]:
630631
src_obj = src_objs[src_gid][i_pair]
@@ -641,12 +642,15 @@ def compute_n1(
641642
)
642643
if dst_obj is None:
643644
break
644-
dst_obj.update_resultshapes_from(src_obj)
645+
if Conf.proc.keep_results.get():
646+
dst_obj.update_resultshapes_from(src_obj)
645647
if src_obj.roi is not None:
646648
if dst_obj.roi is None:
647649
dst_obj.roi = src_obj.roi.copy()
648650
else:
649-
dst_obj.roi.add_roi(src_obj.roi)
651+
roi = dst_obj.roi
652+
roi.add_roi(src_obj.roi)
653+
dst_obj.roi = roi
650654
if func_objs is not None:
651655
func_objs(dst_obj, src_objs_pair)
652656
short_ids = [obj.short_id for obj in src_objs_pair]
@@ -673,7 +677,8 @@ def compute_n1(
673677
src_dtypes[src_gid] = src_dtype = src_obj.data.dtype
674678
dst_dtype = complex if is_complex_dtype(src_dtype) else float
675679
dst_objs[src_gid] = dst_obj = src_obj.copy(dtype=dst_dtype)
676-
dst_obj.delete_results() # Remove any previous results
680+
if not Conf.proc.keep_results.get():
681+
dst_obj.delete_results() # Remove any previous results
677682
dst_obj.roi = None
678683
src_objs[src_gid] = [src_obj]
679684
else:
@@ -691,12 +696,15 @@ def compute_n1(
691696
if dst_obj is None:
692697
break
693698
dst_objs[src_gid] = dst_obj
694-
dst_obj.update_resultshapes_from(src_obj)
699+
if Conf.proc.keep_results.get():
700+
dst_obj.update_resultshapes_from(src_obj)
695701
if src_obj.roi is not None:
696702
if dst_obj.roi is None:
697703
dst_obj.roi = src_obj.roi.copy()
698704
else:
699-
dst_obj.roi.add_roi(src_obj.roi)
705+
roi = dst_obj.roi
706+
roi.add_roi(src_obj.roi)
707+
dst_obj.roi = roi
700708

701709
grps = self.panel.objview.get_sel_groups()
702710
if grps:

cdl/core/gui/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ class ProcSettings(gds.DataSet):
110110
)
111111
fft_shift_enabled = gds.BoolItem("", _("FFT shift"))
112112
extract_roi_singleobj = gds.BoolItem("", _("Extract ROI in single object"))
113+
keep_results = gds.BoolItem("", _("Keep results after computation"))
113114
ignore_warnings = gds.BoolItem("", _("Ignore warnings"))
114115
_g0 = gds.EndGroup("")
115116

0 commit comments

Comments
 (0)