Skip to content

Commit f9b7107

Browse files
committed
Added "Plot results" entry in "Computing" menu
1 parent 1591bc3 commit f9b7107

File tree

15 files changed

+936
-557
lines changed

15 files changed

+936
-557
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ for future and past milestones.
5151
* This concerns both 1D (FHWM, ...) and 2D computing results (contours, blobs, ...):
5252
* Segment results now also show length (L) and center coordinates (Xc, Yc)
5353
* Circle and ellipse results now also show area (A)
54+
* Added "Plot results" entry in "Computing" menu:
55+
* This feature allows to plot computing results (1D or 2D)
56+
* It creates a new signal with X and Y axes corresponding to user-defined
57+
parameters (e.g. X = indexes and Y = radius for circle results)
5458
* Increased default width of the object selection dialog box:
5559
* The object selection dialog box is now wider by default, so that the full
5660
signal/image/group titles may be more easily readable

cdl/core/computation/image/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
)
4545
from cdl.core.model.base import BaseProcParam
4646
from cdl.core.model.image import ImageObj, RoiDataGeometries, RoiDataItem
47-
from cdl.core.model.signal import SignalObj
47+
from cdl.core.model.signal import SignalObj, create_signal
4848

4949
VALID_DTYPES_STRLIST = ImageObj.get_valid_dtypenames()
5050

@@ -114,8 +114,9 @@ def dst_11_signal(src: ImageObj, name: str, suffix: str | None = None) -> Signal
114114
SignalObj: output signal object
115115
"""
116116
title = f"{name}({src.short_id})"
117-
dst = SignalObj(title=title)
118-
dst.title = title
117+
dst = create_signal(
118+
title=title, units=(src.xunit, src.zunit), labels=(src.xlabel, src.zlabel)
119+
)
119120
if suffix is not None:
120121
dst.title += "|" + suffix
121122
dst.metadata["source"] = src.metadata["source"] # Keep track of the source image

cdl/core/gui/actionhandler.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,21 @@ def create_last_actions(self):
669669
_("Swap X/Y axes"), triggered=self.panel.processor.compute_swap_axes
670670
)
671671

672+
with self.new_category(ActionCategory.COMPUTING):
673+
self.new_action(
674+
_("Show results") + "...",
675+
triggered=self.panel.show_results,
676+
icon=get_icon("show_results.svg"),
677+
separator=True,
678+
select_condition=SelectCond.at_least_one_group_or_one_object,
679+
)
680+
self.new_action(
681+
_("Plot results") + "...",
682+
triggered=self.panel.plot_results,
683+
icon=get_icon("plot_results.svg"),
684+
select_condition=SelectCond.at_least_one_group_or_one_object,
685+
)
686+
672687

673688
class SignalActionHandler(BaseActionHandler):
674689
"""Object handling signal panel GUI interactions: actions, menus, ..."""

cdl/core/gui/panel/base.py

Lines changed: 99 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import warnings
5252
from typing import TYPE_CHECKING
5353

54+
import guidata.dataset as gds
5455
import guidata.dataset.qtwidgets as gdq
5556
import numpy as np
5657
import plotpy.io
@@ -68,6 +69,7 @@
6869
from cdl.core.gui import actionhandler, objectmodel, objectview, roieditor
6970
from cdl.core.io.base import IOAction
7071
from cdl.core.model.base import ResultShape, items_to_json
72+
from cdl.core.model.signal import create_signal
7173
from cdl.env import execenv
7274
from cdl.utils.qthelpers import (
7375
create_progress_bar,
@@ -77,7 +79,6 @@
7779
)
7880

7981
if TYPE_CHECKING: # pragma: no cover
80-
import guidata.dataset as gds
8182
from plotpy.plot import PlotWidget
8283
from plotpy.tools import GuiTool
8384

@@ -236,6 +237,15 @@ def remove_all_objects(self):
236237
self.SIG_OBJECT_REMOVED.emit()
237238

238239

240+
@dataclasses.dataclass
241+
class ResultData:
242+
"""Result data associated to a shapetype"""
243+
244+
results: list[ResultShape] = None
245+
xlabels: list[str] = None
246+
ylabels: list[str] = None
247+
248+
239249
class BaseDataPanel(AbstractPanel):
240250
"""Object handling the item list, the selected item properties and plot"""
241251

@@ -1010,37 +1020,49 @@ def add_results_button(self) -> None:
10101020
select_condition=actionhandler.SelectCond.at_least_one,
10111021
)
10121022

1013-
def show_results(self) -> None:
1014-
"""Show results"""
1015-
1016-
@dataclasses.dataclass
1017-
class ResultData:
1018-
"""Result data associated to a shapetype"""
1019-
1020-
results: list[ResultShape] = None
1021-
xlabels: list[str] = None
1022-
ylabels: list[str] = None
1023-
1023+
def __get_resultdata_dict(
1024+
self, objs: list[SignalObj | ImageObj]
1025+
) -> dict[ShapeTypes, ResultData]:
1026+
"""Return result data dictionary"""
10241027
rdatadict: dict[ShapeTypes, ResultData] = {}
1025-
objs = self.objview.get_sel_objects(include_groups=True)
10261028
for obj in objs:
10271029
for result in obj.iterate_resultshapes():
10281030
rdata = rdatadict.setdefault(result.shapetype, ResultData([], None, []))
1029-
title = f"{result.label}"
10301031
rdata.results.append(result)
10311032
rdata.xlabels = result.shown_xlabels
10321033
for _i_row_res in range(result.array.shape[0]):
10331034
ylabel = f"{obj.short_id}: {result.label}"
10341035
rdata.ylabels.append(ylabel)
1036+
return rdatadict
1037+
1038+
def __show_no_result_warning(self):
1039+
"""Show no result warning"""
1040+
msg = "<br>".join(
1041+
[
1042+
_("No result currently available for this object."),
1043+
"",
1044+
_(
1045+
"This feature leverages the results of previous computations "
1046+
"performed on the selected object(s).<br><br>"
1047+
"To compute results, select one or more objects and choose "
1048+
"a computing feature in the <u>Compute</u> menu."
1049+
),
1050+
]
1051+
)
1052+
QW.QMessageBox.information(self, APP_NAME, msg)
1053+
1054+
def show_results(self) -> None:
1055+
"""Show results"""
1056+
objs = self.objview.get_sel_objects(include_groups=True)
1057+
rdatadict = self.__get_resultdata_dict(objs)
10351058
if rdatadict:
10361059
with warnings.catch_warnings():
10371060
warnings.simplefilter("ignore", RuntimeWarning)
10381061
for rdata in rdatadict.values():
10391062
dlg = ArrayEditor(self.parent())
1040-
title = _("Results")
10411063
dlg.setup_and_check(
10421064
np.vstack([result.shown_array for result in rdata.results]),
1043-
title,
1065+
_("Results"),
10441066
readonly=True,
10451067
xlabels=rdata.xlabels,
10461068
ylabels=rdata.ylabels,
@@ -1049,17 +1071,68 @@ class ResultData:
10491071
dlg.resize(750, 300)
10501072
exec_dialog(dlg)
10511073
else:
1052-
msg = "<br>".join(
1053-
[
1054-
_("No result currently available for this object."),
1055-
"",
1074+
self.__show_no_result_warning()
1075+
1076+
def plot_results(self) -> None:
1077+
"""Plot results"""
1078+
objs = self.objview.get_sel_objects(include_groups=True)
1079+
rdatadict = self.__get_resultdata_dict(objs)
1080+
if rdatadict:
1081+
for shapetype, rdata in rdatadict.items():
1082+
xchoices = (("indexes", _("Indexes")),)
1083+
for xlabel in rdata.xlabels[1:]:
1084+
xchoices += ((xlabel, xlabel),)
1085+
ychoices = xchoices[1:]
1086+
1087+
class PlotResultParam(gds.DataSet):
1088+
"""Plot results parameters"""
1089+
1090+
xaxis = gds.ChoiceItem(_("X axis"), xchoices, default="indexes")
1091+
yaxis = gds.ChoiceItem(
1092+
_("Y axis"), ychoices, default=ychoices[0][0]
1093+
)
1094+
1095+
comment = (
10561096
_(
1057-
"This feature shows result arrays as displayed after "
1058-
'calling one of the computing feature (see "Compute" menu).'
1059-
),
1060-
]
1061-
)
1062-
QW.QMessageBox.information(self, APP_NAME, msg)
1097+
"Plot results obtained from previous computations.<br><br>"
1098+
"This plot is based on the results under the form of '%s'."
1099+
)
1100+
% shapetype.name.lower()
1101+
)
1102+
param = PlotResultParam(_("Plot results"), comment=comment)
1103+
if not param.edit(parent=self):
1104+
return
1105+
1106+
# Regrouping ResultShape results by their `label` attribute:
1107+
grouped_results: dict[str, list[ResultShape]] = {}
1108+
for result in rdata.results:
1109+
grouped_results.setdefault(result.label, []).append(result)
1110+
1111+
# Plotting each group of results:
1112+
for label, results in grouped_results.items():
1113+
x, y = [], []
1114+
for index, result in enumerate(results):
1115+
if param.xaxis == "indexes":
1116+
x.append(index)
1117+
else:
1118+
x.append(
1119+
result.shown_array[0][rdata.xlabels.index(param.xaxis)]
1120+
)
1121+
y.append(
1122+
result.shown_array[0][rdata.xlabels.index(param.yaxis)]
1123+
)
1124+
xdata = np.array(x, dtype=float)
1125+
ydata = np.array(y, dtype=float)
1126+
1127+
obj = create_signal(
1128+
title=f"{label}: {param.yaxis} = f({param.xaxis})",
1129+
x=xdata,
1130+
y=ydata,
1131+
labels=[param.xaxis, param.yaxis],
1132+
)
1133+
self.mainwindow.signalpanel.add_object(obj)
1134+
else:
1135+
self.__show_no_result_warning()
10631136

10641137
def add_label_with_title(self, title: str | None = None) -> None:
10651138
"""Add a label with object title on the associated plot

cdl/core/model/image.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,7 @@ def create_image(
717717
"""
718718
assert isinstance(title, str)
719719
assert data is None or isinstance(data, np.ndarray)
720-
image = ImageObj()
720+
image = ImageObj(title=title)
721721
image.title = title
722722
image.data = data
723723
if units is not None:

cdl/core/model/signal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ def create_signal(
475475
SignalObj: signal object
476476
"""
477477
assert isinstance(title, str)
478-
signal = SignalObj()
478+
signal = SignalObj(title=title)
479479
signal.title = title
480480
signal.set_xydata(x, y, dx=dx, dy=dy)
481481
if units is not None:

0 commit comments

Comments
 (0)