Skip to content

Commit 9db4e17

Browse files
committed
feat(signal): split peak detection into XY-markers + sticks rebuild
1 parent affe2a4 commit 9db4e17

12 files changed

Lines changed: 656 additions & 113 deletions

File tree

datalab/gui/actionhandler.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1443,10 +1443,14 @@ def cra_fit(title, fitdlgfunc, tip: str | None = None):
14431443
self.action_for("extract_pulse_features")
14441444
self.action_for("extract_peak_positions")
14451445
self.new_action(
1446-
_("Peak detection"),
1447-
separator=True,
1446+
_("Peak detection..."),
14481447
triggered=self.panel.processor.compute_peak_detection,
14491448
icon_name="peak_detect.svg",
1449+
tip=_(
1450+
"Interactive peak detection: adjust threshold and minimum "
1451+
"distance visually, then store detected peaks as an "
1452+
"XY-markers table result"
1453+
),
14501454
)
14511455
self.action_for("sampling_rate_period", separator=True)
14521456
self.action_for("dynamic_parameters", context_menu_pos=-1)
@@ -1457,6 +1461,16 @@ def create_last_actions(self):
14571461
"""Create actions that are added to the menus in the end"""
14581462
super().create_last_actions()
14591463
with self.new_category(ActionCategory.OPERATION):
1464+
self.new_action(
1465+
_("Create signal from markers table..."),
1466+
separator=True,
1467+
triggered=self.panel.processor.compute_markers_to_signal,
1468+
icon_name="peak_detect.svg",
1469+
tip=_(
1470+
"Build a sticks signal from an XY-markers table result "
1471+
"(e.g. from 'Extract peak positions')"
1472+
),
1473+
)
14601474
self.action_for("signals_to_image", separator=True)
14611475

14621476
with self.new_category(ActionCategory.VIEW):

datalab/gui/processor/signal.py

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111
import re
1212
from collections.abc import Callable
1313

14+
import guidata.dataset as gds
1415
import numpy as np
1516
import sigima.params
1617
import sigima.proc.base as sigima_base
1718
import sigima.proc.signal as sips
1819
from guidata.qthelpers import exec_dialog
20+
from qtpy import QtWidgets as QW
1921
from sigima.objects import (
2022
NormalDistributionParam,
2123
PoissonDistributionParam,
@@ -27,6 +29,7 @@
2729
)
2830
from sigima.objects.scalar import GeometryResult, TableResult
2931

32+
from datalab.adapters_metadata.table_adapter import TableAdapter
3033
from datalab.config import _
3134
from datalab.gui.processor.base import BaseProcessor
3235
from datalab.utils.qthelpers import qt_try_except
@@ -329,12 +332,6 @@ def register_processing(self) -> None:
329332
paramclass=sigima.params.PowerParam,
330333
icon_name="power.svg",
331334
)
332-
self.register_1_to_1(
333-
sips.peak_detection,
334-
_("Peak detection"),
335-
paramclass=sigima.params.PeakDetectionParam,
336-
icon_name="peak_detect.svg",
337-
)
338335
# Frequency filters
339336
self.register_1_to_1(
340337
sips.lowpass,
@@ -610,8 +607,18 @@ def compute_all_stability(
610607
def compute_peak_detection(
611608
self, param: sigima.params.PeakDetectionParam | None = None
612609
) -> None:
613-
"""Detect peaks from data
614-
with :py:func:`sigima.proc.signal.peak_detection`"""
610+
"""Interactive peak detection.
611+
612+
Opens :class:`~datalab.widgets.signalpeak.SignalPeakDetectionDialog`
613+
to set the detection threshold and minimum distance visually, then
614+
stores the detected peak positions as an XY-markers
615+
:class:`~sigima.objects.scalar.TableResult` attached to the signal
616+
(via :py:func:`sigima.proc.signal.extract_peak_positions`).
617+
618+
To rebuild the historical *sticks* signal from the detected peaks,
619+
use :py:meth:`compute_markers_to_signal` afterwards (menu
620+
*Operations ▸ Create signal from markers table…*).
621+
"""
615622
obj = self.panel.objview.get_sel_objects(include_groups=True)[0]
616623
edit, param = self.init_param(
617624
param, sips.PeakDetectionParam, _("Peak detection")
@@ -623,7 +630,57 @@ def compute_peak_detection(
623630
param.min_dist = dlg.get_min_dist()
624631
else:
625632
return
626-
self.run_feature("peak_detection", param)
633+
self.run_feature("extract_peak_positions", param)
634+
635+
@qt_try_except()
636+
def compute_markers_to_signal(self) -> None:
637+
"""Build a sticks signal from an XY-markers table result
638+
with :py:func:`sigima.proc.signal.markers_table_to_signal`.
639+
"""
640+
selected = self.panel.objview.get_sel_objects(include_groups=True)
641+
if not selected:
642+
return
643+
title = _("Create signal from markers table")
644+
last_choice: str | None = None
645+
for obj in selected:
646+
adapters = [
647+
a
648+
for a in TableAdapter.iterate_from_obj(obj)
649+
if a.result.is_xy_markers()
650+
]
651+
if not adapters:
652+
QW.QMessageBox.information(
653+
self.mainwindow,
654+
title,
655+
_(
656+
"Signal '%s' has no XY-markers table result. "
657+
"Run 'Extract peak positions' (or another XY-markers "
658+
"analysis) first."
659+
)
660+
% obj.title,
661+
)
662+
continue
663+
if len(adapters) == 1:
664+
adapter = adapters[0]
665+
else:
666+
titles = [a.result.title for a in adapters]
667+
default_idx = titles.index(last_choice) if last_choice in titles else 0
668+
choices = list(enumerate(titles))
669+
670+
class _MarkersChoice(gds.DataSet):
671+
"""Markers table selection."""
672+
673+
index = gds.ChoiceItem(
674+
_("Markers table"), choices, default=default_idx
675+
)
676+
677+
choice = _MarkersChoice(title)
678+
if not choice.edit(self.mainwindow):
679+
continue
680+
adapter = adapters[choice.index]
681+
last_choice = titles[choice.index]
682+
signal = sips.markers_table_to_signal(adapter.result, ref=obj)
683+
self.panel.add_object(signal)
627684

628685
@qt_try_except()
629686
def compute_polyfit(

datalab/locale/fr/LC_MESSAGES/datalab.po

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -694,8 +694,17 @@ msgstr "Ordonnée à x=..."
694694
msgid "Compute the ordinate at a given x value (linear interpolation)"
695695
msgstr "Calcule l'ordonnée à une valeur x donnée (interpolation linéaire)"
696696

697-
msgid "Peak detection"
698-
msgstr "Détection de pics"
697+
msgid "Peak detection..."
698+
msgstr "Détection de pics..."
699+
700+
msgid "Interactive peak detection: adjust threshold and minimum distance visually, then store detected peaks as an XY-markers table result"
701+
msgstr "Détection de pics interactive : ajustez visuellement le seuil et la distance minimale, puis stockez les pics détectés sous la forme d'une table de marqueurs XY"
702+
703+
msgid "Create signal from markers table..."
704+
msgstr "Créer un signal à partir d'une table de marqueurs..."
705+
706+
msgid "Build a sticks signal from an XY-markers table result (e.g. from 'Extract peak positions')"
707+
msgstr "Crée un signal de type bâtons à partir d'une table de marqueurs XY (p.ex. issue de la commande 'Extraire les positions des pics')"
699708

700709
msgid "Curve anti-aliasing"
701710
msgstr "Anticrénelage des courbes"
@@ -2066,6 +2075,9 @@ msgstr "Détection de contours"
20662075
msgid "Compute contour shape fit"
20672076
msgstr "Ajustement d'une forme géométrique à un contour"
20682077

2078+
msgid "Peak detection"
2079+
msgstr "Détection de pics"
2080+
20692081
msgid "Detect peaks in the image"
20702082
msgstr "Détection de pics dans l'image"
20712083

@@ -2320,6 +2332,16 @@ msgstr "Contraste"
23202332
msgid "Compute contrast of a signal, i.e. (max-min)/(max+min), e.g. for an image profile"
23212333
msgstr "Calcule le contraste d'un signal, c'est-à-dire (max-min)/(max+min), p.ex. pour un profil d'image"
23222334

2335+
msgid "Create signal from markers table"
2336+
msgstr "Créer un signal à partir d'une table de marqueurs"
2337+
2338+
#, python-format
2339+
msgid "Signal '%s' has no XY-markers table result. Run 'Extract peak positions' (or another XY-markers analysis) first."
2340+
msgstr "Le signal '%s' ne possède aucun résultat de type table de marqueurs XY. Exécutez d'abord 'Extraire les positions des pics' (ou une autre analyse produisant une table de marqueurs XY)."
2341+
2342+
msgid "Markers table"
2343+
msgstr "Table de marqueurs"
2344+
23232345
msgid "Full width at y"
23242346
msgstr "Largeur à y=..."
23252347

@@ -3655,3 +3677,51 @@ msgstr "Merci de sélectionner le fichier à importer."
36553677

36563678
msgid "Example Wizard"
36573679
msgstr "Assistant exemple"
3680+
3681+
msgid "Minimum value"
3682+
msgstr "Valeur minimum"
3683+
3684+
msgid "Maximum value"
3685+
msgstr "Valeur maximum"
3686+
3687+
msgid "Step between levels"
3688+
msgstr "Pas entre les niveaux"
3689+
3690+
msgid "Overlay image"
3691+
msgstr "Image de fond"
3692+
3693+
msgid "Show level labels"
3694+
msgstr "Afficher les étiquettes de niveaux"
3695+
3696+
msgid "Contour isoline plot"
3697+
msgstr "Tracé de lignes de contour"
3698+
3699+
msgid "Display isolines (contour lines) overlaid on the selected image, with configurable level range, step, and optional value labels"
3700+
msgstr "Afficher les isolignes (lignes de contour) superposées à l'image sélectionnée, avec une plage de niveaux configurable, un pas entre les niveaux, et des étiquettes de valeurs optionnelles"
3701+
3702+
msgid "Annotation"
3703+
msgstr "Annotation"
3704+
3705+
msgid "Contour"
3706+
msgstr "Contour"
3707+
3708+
msgid "Select a single image first."
3709+
msgstr "Sélectionnez d'abord une seule image."
3710+
3711+
msgid "Selected image does not contain finite data."
3712+
msgstr "L'image sélectionnée ne contient pas de données finies."
3713+
3714+
msgid "Selected image is constant: no contour can be drawn."
3715+
msgstr "L'image sélectionnée est constante : aucun contour ne peut être tracé."
3716+
3717+
msgid "Contour plot parameters"
3718+
msgstr "Paramètres du tracé de contours"
3719+
3720+
msgid "Maximum value must be strictly greater than minimum."
3721+
msgstr "La valeur maximale doit être strictement supérieure à la valeur minimale."
3722+
3723+
msgid "No contour was found for the selected level range."
3724+
msgstr "Aucun contour n'a été trouvé pour la plage de niveaux sélectionnée."
3725+
3726+
msgid "Show contour plot..."
3727+
msgstr "Afficher le tracé de contours..."

datalab/tests/scenarios/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ def run_signal_computations(
187187

188188
param = sigima.params.PeakDetectionParam()
189189
panel.processor.compute_peak_detection(param)
190+
panel.processor.compute_markers_to_signal()
190191

191192
panel.processor.compute_multigaussianfit()
192193

datalab/tests/scenarios/demo.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ def test_signal_features(win: DLMainWindow, data_size: int = 500) -> None:
7272
sig3 = panel.objview.get_current_object()
7373

7474
param = sigima.params.PeakDetectionParam()
75-
panel.processor.run_feature("peak_detection", param)
75+
panel.processor.compute_peak_detection(param)
76+
panel.processor.compute_markers_to_signal()
7677
sig4 = panel.objview.get_current_object()
7778
panel.objview.select_objects([sig3, sig4])
7879

doc/features/advanced/migration_v020_to_v100.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -709,7 +709,15 @@ Some complex features still have dedicated methods:
709709
# These still use dedicated methods in v1.0
710710
proc.compute_roi_extraction(roi)
711711
proc.compute_multigaussianfit()
712-
proc.compute_peak_detection(param)
712+
713+
# Peak detection (signal): ``compute_peak_detection`` still exists and
714+
# opens the interactive threshold dialog as before, but it now stores
715+
# the result as an XY-markers table (no sticks signal is created
716+
# automatically). To rebuild the legacy sticks signal, chain with
717+
# ``compute_markers_to_signal`` (or call ``run_feature("extract_peak_positions",
718+
# param)`` directly when no interactive dialog is needed):
719+
proc.compute_peak_detection(param) # interactive, XY-markers result
720+
proc.compute_markers_to_signal() # optional: rebuild sticks signal
713721
714722
Testing your plugin
715723
-------------------

doc/features/signal/menu_analysis.rst

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,36 @@ If there are multiple solutions, the displayed result is the smallest value.
113113
Peak detection
114114
^^^^^^^^^^^^^^
115115

116-
Create a new signal from semi-automatic peak detection of each selected signal.
116+
DataLab offers two complementary ways to detect peaks. Both produce the
117+
same result: an *XY-markers* :class:`~sigima.objects.scalar.TableResult`
118+
attached to the signal (visible from *View ▸ Show results* and overlayed
119+
on the curve).
120+
121+
.. list-table::
122+
:header-rows: 1
123+
:widths: 30, 70
124+
125+
* - Entry point
126+
- Description
127+
* - **Extract peak positions**
128+
- Parametric entry: fill the standard parameter dialog
129+
(threshold, minimum distance) and run.
130+
* - **Peak detection...**
131+
- Interactive entry: opens a dedicated dialog where the threshold
132+
is adjusted by moving a horizontal marker and the minimum
133+
distance by a slider. Detected peaks update live as vertical
134+
markers.
117135

118136
.. figure:: /images/shots/s_peak_detection.png
119137

120-
Peak detection dialog: threshold is adjustable by moving the
138+
*Peak detection* dialog: threshold is adjustable by moving the
121139
horizontal marker, peaks are detected automatically (see vertical
122-
markers with labels indicating peak position)
140+
markers with labels indicating peak position).
141+
142+
.. tip::
143+
144+
To rebuild the historical *sticks* signal from the detected peaks, use
145+
*Operations ▸ Create signal from markers table…* on the selected signal.
123146

124147
Sampling rate and period
125148
^^^^^^^^^^^^^^^^^^^^^^^^

doc/features/signal/menu_operations.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,11 @@ Other mathematical functions
273273
* - |signal_to_image| Assemble signals into image
274274
- Create a 2D image by assembling selected 1D signals as rows or columns,
275275
with optional normalization.
276+
* - |peak_detect| Create signal from markers table...
277+
- Build a sticks signal from an XY-markers table result attached to the
278+
selected signal — typically produced by *Analysis ▸ Extract peak
279+
positions*. When several XY-markers tables are stored on the signal,
280+
a dialog lets you pick the source table.
276281

277282

278283
.. |power| image:: ../../../datalab/data/icons/operations/power.svg
@@ -289,3 +294,8 @@ Other mathematical functions
289294
:width: 24px
290295
:height: 24px
291296
:class: dark-light no-scaled-link
297+
298+
.. |peak_detect| image:: ../../../datalab/data/icons/processing/peak_detect.svg
299+
:width: 24px
300+
:height: 24px
301+
:class: dark-light no-scaled-link

doc/intro/tutorials/spectrum.rst

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,10 @@ and fit multiple peaks in the spectrum by selecting
197197
spectrum and the fit in the "Signals" panel on the right, so both are displayed
198198
in the visualization panel on the left.
199199

200-
Alternatively, we could use the "Peak detection" feature from the "Analysis" menu to
201-
detect peaks in the spectrum. This is the first step of the "Multi-Gaussian fit"
202-
function and can be used independently to detect peaks without performing a fit,
203-
creating a signal with a delta function at each detected peak position.
200+
Alternatively, we could use the "Peak detection" feature from the "Analysis" menu
201+
to detect peaks in the spectrum. This is the first step of the "Multi-Gaussian fit"
202+
function and can be used independently to extract peak positions as a result table
203+
attached to the signal.
204204

205205
.. figure:: ../../images/tutorials/spectrum/19.png
206206

@@ -209,9 +209,13 @@ creating a signal with a delta function at each detected peak position.
209209
.. figure:: ../../images/tutorials/spectrum/21.png
210210

211211
After adjusting the peak detection parameters (using the same dialog as
212-
for the multi-Gaussian fit), click "OK". Then select both the
213-
"peak_detection" result and the original spectrum in the "Signals" panel
214-
to display them together in the visualization panel on the left.
212+
for the multi-Gaussian fit), click "OK". The detected peak positions are
213+
stored as an XY-markers result and overlayed on the spectrum.
214+
215+
To rebuild the historical *sticks* signal — a delta function at each detected
216+
peak position — apply *Operations ▸ Create signal from markers table…* to the
217+
spectrum: this creates a child signal whose samples are the ``(x, y)`` couples
218+
of the markers table, rendered as sticks.
215219

216220
Saving the workspace
217221
--------------------

0 commit comments

Comments
 (0)