intensity.py 33.8 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ###########################################################################*/
"""
contains gui relative to intensity normalization
"""


__authors__ = [
    "H. Payno",
]
__license__ = "MIT"
__date__ = "23/06/2021"


from silx.gui import qt
38
from silx.io.url import DataUrl
39
40
41
from silx.gui.plot.items.roi import RectangleROI
from silx.gui.plot.items.roi import HorizontalRangeROI
from silx.gui.plot.tools.roi import RegionOfInterestManager
42
43
44
45
46
47
from silx.gui.dialog.DataFileDialog import DataFileDialog
from tomwer.core.scan.hdf5scan import HDF5TomoScan
from tomwer.gui.visualization.dataviewer import DataViewer
from tomwer.gui.reconstruction.scores.control import ControlWidget
from tomwer.core.scan.scanbase import TomwerScanBase
from tomwer.gui.visualization.sinogramviewer import SinogramViewer as _SinogramViewer
48
from tomwer.core.process.reconstruction.normalization import params as _normParams
49
from tomwer.core.process.reconstruction.normalization.params import _ValueSource
Henri Payno's avatar
Henri Payno committed
50
from tomoscan.normalization import Method
51
from tomwer.gui.utils.buttons import PadlockButton
52
import weakref
53
54
55
56
import typing


class NormIntensityWindow(qt.QMainWindow):
57
58
59
60

    sigConfigurationChanged = qt.Signal()
    """signal emit when the configuration change"""

61
62
    def __init__(self, parent):
        qt.QMainWindow.__init__(self, parent)
63
        self._scan = None
64
        self.setWindowFlags(qt.Qt.Widget)
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

        # central widget
        self._centralWidget = _Viewer(self)
        self.setCentralWidget(self._centralWidget)

        # control widget (options + ctrl buttons)
        self._dockWidgetWidget = qt.QWidget(self)
        self._dockWidgetWidget.setLayout(qt.QVBoxLayout())
        self._optsWidget = _NormIntensityOptions(self)
        self._dockWidgetWidget.layout().addWidget(self._optsWidget)
        self._spacer = qt.QWidget(self)
        self._spacer.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Expanding)
        self._dockWidgetWidget.layout().addWidget(self._spacer)
        self._crtWidget = _NormIntensityControl(self)
        self._dockWidgetWidget.layout().addWidget(self._crtWidget)

        # dock widget
        self._dockWidgetCtrl = qt.QDockWidget(parent=self)
        self._dockWidgetCtrl.layout().setContentsMargins(0, 0, 0, 0)
        self._dockWidgetCtrl.setFeatures(qt.QDockWidget.DockWidgetMovable)
        self._dockWidgetCtrl.setWidget(self._dockWidgetWidget)
        self.addDockWidget(qt.Qt.RightDockWidgetArea, self._dockWidgetCtrl)

88
        # connect signal / slot
89
        # self._optsWidget.sigModeChanged.connect(self._modeChanged)
90
        self._optsWidget.sigValueUpdated.connect(self.setResult)
91
        self._optsWidget.sigConfigurationChanged.connect(self._configurationChanged)
92
        self._optsWidget.sigSourceChanged.connect(self._sourceChanged)
93
        self._crtWidget.sigValidateRequest.connect(self._validated)
94

95
96
        # set up
        self._centralWidget._updateSinogramROI()
97
        self._modeChanged()
98

Henri Payno's avatar
Henri Payno committed
99
100
101
102
103
    def close(self):
        self._centralWidget.stop()
        self._centralWidget = None
        super().close()

104
105
106
    def _configurationChanged(self):
        self.sigConfigurationChanged.emit()

107
108
109
    def _hideLockButton(self):
        self._optsWidget._hideLockButton()

110
111
112
    def _validated(self):
        pass

113
    def getConfiguration(self) -> dict:
114
        return self._optsWidget.getConfiguration()
115
116

    def setConfiguration(self, config: dict):
117
        self._optsWidget.setConfiguration(config=config)
118

Henri Payno's avatar
Henri Payno committed
119
120
121
    def setCurrentMethod(self, method):
        self._optsWidget.setCurrentMethod(method=method)

122
123
    def getCurrentMethod(self):
        return self._optsWidget.getCurrentMethod()
124

Henri Payno's avatar
Henri Payno committed
125
126
127
    def setCurrentSource(self, source):
        self._optsWidget.setCurrentSource(source=source)

128
129
130
    def getCurrentSource(self):
        return self._optsWidget.getCurrentSource()

131
    def _modeChanged(self):
132
133
134
135
136
137
138
        self._sourceChanged()

    def _sourceChanged(self):
        source = self.getCurrentSource()
        method = self.getCurrentMethod()
        scan = self.getScan()

payno's avatar
payno committed
139
        methods_using_manual_roi = (Method.DIVISION, Method.SUBTRACTION)
140

141
        self._centralWidget.setManualROIVisible(
142
            source is _ValueSource.MANUAL_ROI and method in methods_using_manual_roi
143
        )
144
        if scan:
payno's avatar
payno committed
145
            methods_requesting_calculation = (Method.DIVISION, Method.SUBTRACTION)
146
147
148
149
            if method in methods_requesting_calculation:
                # if the normed sinogram can be obtained `directly`
                if source in (_ValueSource.MANUAL_SCALAR, _ValueSource.DATASET):
                    scan.intensity_normalization = self.getCurrentMethod().value
payno's avatar
payno committed
150
151
152
153
154
155
156
                    extra_info = self.getExtraArgs()
                    extra_info.update(
                        {
                            "tomwer_processing_res_code": True,
                        }
                    )
                    scan.intensity_normalization.set_extra_infos(extra_info)
157
        self._centralWidget._updateSinogramROI()
158
159
160
161
162
163

    def getScan(self):
        if self._scan is not None:
            return self._scan()
        else:
            return None
164

165
    def setScan(self, scan: typing.Union[None, TomwerScanBase]):
166
        self._scan = weakref.ref(scan)
167
        self._centralWidget.setScan(scan=scan)
168
169
        self._optsWidget.setScan(scan=scan)

payno's avatar
payno committed
170
    def stop(self):
171
172
173
        if self._centralWidget is not None:
            self._centralWidget.stop()
            self._centralWidget = None
payno's avatar
payno committed
174

175
    def getExtraArgs(self) -> dict:
176
177
178
179
180
        return self._optsWidget.getExtraInfos()

    def getROI(self):
        return self._centralWidget.getROI()

181
182
183
184
185
    def setROI(self, start_x, end_x, start_y, end_y):
        self._centralWidget.setROI(
            start_x=start_x, end_x=end_x, start_y=start_y, end_y=end_y
        )

186
187
188
189
190
    def setResult(self, result):
        self._crtWidget.setResult(result)

    def clear(self):
        self._crtWidget.clear()
191

192
193
194
    def isLocked(self):
        return self._optsWidget.isLocked()

195
196
197
    def setLocked(self, locked):
        self._optsWidget.setLocked(locked)

198
199

class _Viewer(qt.QTabWidget):
200
201
202
    def __init__(self, parent):
        if not isinstance(parent, NormIntensityWindow):
            raise TypeError("Expect a NormIntensityWindow as parrent")
203
204
205
206
207
208
209
210
        qt.QTabWidget.__init__(self, parent)
        self._projView = _ProjPlotWithROI(parent=self)
        self.addTab(self._projView, "projection view")
        self._sinoView = SinogramViewer(parent=self)
        self.addTab(self._sinoView, "sinogram view")

        # connect signal / Slot
        self._sinoView.sigSinogramLineChanged.connect(self._projView.setSinogramLine)
211
212
213
214
215
        self._sinoView.sigSinoLoadEnded.connect(self._updateSinogramROI)
        self._projView.sigROIChanged.connect(self._updateSinogramROI)

    def setScan(self, scan: typing.Union[None, TomwerScanBase]):
        """
216

217
218
219
        :param scan: scan to handle
        :return:
        """
220
        self._projView.setScan(scan)
221
        self._sinoView.setScan(scan, update=False)
222

223
    def _updateSinogramROI(self):
224
225
226
227
228
        source = self.parent().getCurrentSource()
        method = self.parent().getCurrentMethod()

        display_sino_roi = source is _ValueSource.MANUAL_ROI and method in (
            Method.DIVISION,
payno's avatar
payno committed
229
            Method.SUBTRACTION,
230
231
232
        )

        if display_sino_roi:
233
234
235
236
237
238
239
240
241
242
243
            roi = self._projView.getROI()
            sinogram_line = self._sinoView.getLine()
            y_min = roi.getOrigin()[1]
            y_max = roi.getOrigin()[1] + roi.getSize()[1]
            if y_min <= sinogram_line <= y_max:
                x_min = roi.getOrigin()[0]
                x_max = roi.getOrigin()[0] + roi.getSize()[0]
                self._sinoView.setROIRange(x_min, x_max)
                self._sinoView.setROIVisible(True)
            else:
                self._sinoView.setROIVisible(False)
244
245
        else:
            self._sinoView.setROIVisible(False)
246
247
248
249

    def setManualROIVisible(self, visible):
        self._projView.setManualROIVisible(visible=visible)
        self._sinoView.setROIVisible(visible=visible)
250

payno's avatar
payno committed
251
252
253
254
255
256
    def stop(self):
        self._projView.stop()
        self._projView = None
        self._sinoView.stop()
        self._sinoView = None

257
258
259
    def getROI(self):
        return self._projView.getROI()

260
261
262
263
264
    def setROI(self, start_x, end_x, start_y, end_y):
        self._projView.setROI(
            start_x=start_x, end_x=end_x, start_y=start_y, end_y=end_y
        )

265
266

class _ProjPlotWithROI(DataViewer):
267
268
269
270
271
    """DataViewer specialized on projections. Embed a RectangleROI"""

    sigROIChanged = qt.Signal()
    """signal emit when ROI change"""

272
    def __init__(self, *args, **kwargs):
273
        DataViewer.__init__(self, *args, **kwargs, show_overview=False)
274
        self._sinogramLine = 0
275
        self._roiVisible = False
276
277
278
279
280
281
282
283
        self.setScanInfoVisible(False)
        self.setDisplayMode("radios")
        self.setDisplayModeVisible(False)

        dw = self.getUrlListDockWidget()
        self.addDockWidget(qt.Qt.BottomDockWidgetArea, dw)
        dw.setFeatures(qt.QDockWidget.DockWidgetMovable)

284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
        # add ROI
        self._roiManager = RegionOfInterestManager(self.getPlotWidget())
        self._roi = RectangleROI()
        self._roi.setName("ROI")
        self._roi.setLineStyle("-.")
        self._roi.setGeometry(origin=(0, 0), size=(200, 200))
        self._roi.setEditable(True)
        self._roi.setVisible(True)
        self._roiManager.addRoi(self._roi)

        # connect signal / slot
        self.getPlotWidget().sigActiveImageChanged.connect(self._updateSinogramLine)
        self.getPlotWidget().sigActiveImageChanged.connect(self._updateROI)
        self._roi.sigEditingFinished.connect(self._roiChanged)

    def getROI(self):
        return self._roi

302
303
304
305
    def setROI(self, start_x, end_x, start_y, end_y):
        self._roi.setOrigin((start_x, start_y))
        self._roi.setSize((end_x - start_x, end_y - start_y))

306
307
308
309
310
311
312
313
314
315
    def setManualROIVisible(self, visible):
        self._roiVisible = visible
        self._updateROIVisibility()

    def _updateROIVisibility(self):
        self._roi.setVisible(self._roiVisible)

    def _roiChanged(self):
        self.sigROIChanged.emit()

316
317
318
319
    def setSinogramLine(self, line):
        self._sinogramLine = line
        self._updateSinogramLine()

320
321
322
323
324
325
326
327
    def _updateROI(self):
        """ImageStack clean the plot which bring item removal. This is
        why we need to add them back"""
        if self._roi is not None:
            for item in self._roi.getItems():
                if item not in self.getPlotWidget().getItems():
                    self.getPlotWidget().addItem(item)

328
    def _updateSinogramLine(self):
329
330
331
332
        self._roiManager = RegionOfInterestManager(self.getPlotWidget())

        self.getPlotWidget().addYMarker(
            y=self._sinogramLine,
333
334
335
336
337
            legend="sinogram_line",
            text="sinogram line",
            color="blue",
            selectable=False,
        )
338
339
340
        sino_marker = self.getPlotWidget()._getMarker("sinogram_line")
        if sino_marker:
            sino_marker.setLineStyle("--")
341
342
343
344
345

    def clear(self):
        super().clear()
        self.getPlotWidget().removeMarker("sinogram_line")

payno's avatar
payno committed
346
    def stop(self):
347
        self._viewer._plot.stopUpdateThread()
payno's avatar
payno committed
348

349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368

class SinogramViewer(_SinogramViewer):
    """ "Sinogram viewer but adapated for Intensity normalization"""

    sigSinogramLineChanged = qt.Signal(int)
    """signal emit when the selected sinogram line changes"""

    def __init__(self, *args, **kwargs):
        _SinogramViewer.__init__(self, *args, **kwargs)

        dockWidget = self.getOptionsDockWidget()
        self.addDockWidget(qt.Qt.TopDockWidgetArea, dockWidget)
        dockWidget.setFeatures(qt.QDockWidget.DockWidgetMovable)

        # change ApplyButton name and icon
        self._loadButton = self._options._buttons.button(qt.QDialogButtonBox.Apply)
        self._loadButton.setText("load")
        style = qt.QApplication.style()
        self._loadButton.setIcon(style.standardIcon(qt.QStyle.SP_BrowserReload))

369
370
371
372
373
374
375
376
377
378
        # ROI
        self._roiManager = RegionOfInterestManager(self.getPlotWidget())
        self._roi = HorizontalRangeROI()
        self._roi.setRange(0, 0)
        self._roi.setVisible(True)
        self._roi.setColor((250, 50, 50, 150))
        self._roi.setLineWidth(1.5)
        self._roi.setLineStyle("-.")
        self._roiManager.addRoi(self._roi)

379
380
        # connect signal / Slots
        self._options._lineSB.valueChanged.connect(self._sinogramLineChanged)
381
382
        self.sigSinogramLineChanged.connect(self._plot.clear)
        self.getPlotWidget().sigActiveImageChanged.connect(self._updateROI)
383
384
385
386

    def _sinogramLineChanged(self):
        self.sigSinogramLineChanged.emit(self.getLine())

387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
    def setROIRange(self, x_min, x_max):
        self._roi.setRange(x_min, x_max)

    def setROIVisible(self, visible):
        self._roi.setVisible(visible)

    def _updateROI(self):
        """ImageStack clean the plot which bring item removal. This is
        why we need to add them back"""
        if self._roi is not None:
            for item in self._roi.getItems():
                if item not in self.getPlotWidget().getItems():
                    self.getPlotWidget().addItem(item)

    def getPlotWidget(self):
        return self._plot.getPlotWidget()

404
405
406
407
    def _updatePlot(self, sinogram):
        self._plot.getPlotWidget().addImage(data=sinogram)
        self._plot.getPlotWidget().replot()

payno's avatar
payno committed
408
    def stop(self):
409
        self._plot.stopUpdateThread()
payno's avatar
payno committed
410

411
412
413
414
415

class _NormIntensityOptions(qt.QWidget):

    sigValueCanBeLocked = qt.Signal(bool)

416
417
418
    sigProcessingRequested = qt.Signal()
    """Signal emit when the processing is requested"""

419
420
421
    sigModeChanged = qt.Signal()
    """signal emitted when the mode change"""

422
423
424
    sigSourceChanged = qt.Signal()
    """signal emitted when the source change"""

425
426
427
    sigValueUpdated = qt.Signal(object)
    """Signal emit when user defines manually the value"""

428
429
430
    sigConfigurationChanged = qt.Signal()
    """Signal emit when the configuration changes"""

431
    def __init__(self, parent):
432
433
434
435
        if not isinstance(parent, NormIntensityWindow):
            raise TypeError(
                "parent is expected to be an instance of " "NormIntensityWindow "
            )
436
        qt.QWidget.__init__(self, parent)
437
        self._getROI = self.parent().getROI
438
        self.setLayout(qt.QGridLayout())
439
440
        # mode
        self._modeCB = qt.QComboBox(self)
441
        for mode in Method:
442
            if mode in (Method.LSQR_SPLINE,):
443
444
445
                continue
            else:
                self._modeCB.addItem(mode.value)
446
447
448
449
450
        self.layout().addWidget(qt.QLabel("mode:", self), 0, 0, 1, 1)
        self.layout().addWidget(self._modeCB, 0, 1, 1, 1)
        self._lockButton = PadlockButton(self)
        self._lockButton.setFixedWidth(25)
        self.layout().addWidget(self._lockButton, 0, 2, 1, 1)
451
452
453
454
455
456
        # source
        self._sourceCB = qt.QComboBox(self)
        for mode in _ValueSource:
            if mode == _ValueSource.NONE:
                # filter this value because does not have much sense for the GUI
                continue
Henri Payno's avatar
Henri Payno committed
457
            if mode in (_ValueSource.AUTO_ROI, _ValueSource.MONITOR):
458
459
460
461
462
                continue
            self._sourceCB.addItem(mode.value)
        self._sourceLabel = qt.QLabel("source:", self)
        self.layout().addWidget(self._sourceLabel, 1, 0, 1, 1)
        self.layout().addWidget(self._sourceCB, 1, 1, 1, 1)
463
464
465
466
        # method
        self._optsMethod = qt.QGroupBox(self)
        self._optsMethod.setTitle("options")
        self._optsMethod.setLayout(qt.QVBoxLayout())
467
        self.layout().addWidget(self._optsMethod, 2, 0, 1, 3)
468
        # intensity calculation options
469
470
        self._intensityCalcOpts = _NormIntensityCalcOpts(self)
        self._optsMethod.layout().addWidget(self._intensityCalcOpts)
471
        # dataset widget
472
473
        self._datasetWidget = _NormIntensityDatasetWidget(self)
        self._optsMethod.layout().addWidget(self._datasetWidget)
474
475
        # scalar value
        self._scalarValueWidget = _NormIntensityScalarValue(self)
476
        self.layout().addWidget(self._scalarValueWidget, 3, 0, 1, 3)
477
478
479
480
481
482
483
484
485
        # buttons
        self._buttonsGrp = qt.QWidget(self)
        self._buttonsGrp.setLayout(qt.QGridLayout())
        self._buttonsGrp.layout().setContentsMargins(0, 0, 0, 0)
        self._computeButton = qt.QPushButton("compute", self)
        self._buttonsGrp.layout().addWidget(self._computeButton, 0, 1, 1, 1)
        spacer = qt.QWidget(self)
        spacer.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Minimum)
        self._buttonsGrp.layout().addWidget(spacer)
486
        self.layout().addWidget(self._buttonsGrp, 4, 0, 1, 3)
487
488
489
490
491

        self._modeChanged()

        # connect signal / slot
        self._modeCB.currentIndexChanged.connect(self._modeChanged)
492
        self._modeCB.currentIndexChanged.connect(self._configurationChanged)
493
494
        self._sourceCB.currentIndexChanged.connect(self._sourceChanged)
        self._sourceCB.currentIndexChanged.connect(self._configurationChanged)
495
        self._computeButton.released.connect(self._computationRequested)
496
        self._scalarValueWidget.sigValueChanged.connect(self._valueUpdated)
497
498
499
500
501
502
503
504
505
506
507
        self._scalarValueWidget.sigValueChanged.connect(self._configurationChanged)
        self._datasetWidget.sigConfigurationChanged.connect(self._configurationChanged)
        self._intensityCalcOpts.sigConfigurationChanged.connect(
            self._configurationChanged
        )
        self._lockButton.toggled.connect(self._lockChanged)
        self._lockButton.toggled.connect(self._configurationChanged)

    def _configurationChanged(self):
        self.sigConfigurationChanged.emit()

508
509
510
    def _sourceChanged(self):
        source = self.getCurrentSource()
        method = self.getCurrentMethod()
payno's avatar
payno committed
511
        interactive_methods = (Method.DIVISION, Method.SUBTRACTION)
512
513
514
515
516
517
518
519
520
521
        interactive_sources = (
            _ValueSource.MANUAL_ROI,
            _ValueSource.AUTO_ROI,
            _ValueSource.DATASET,
        )
        self._datasetWidget.setVisible(source == _ValueSource.DATASET)
        self.setManualROIVisible(source == _ValueSource.MANUAL_ROI)
        self._optsMethod.setVisible(
            method in interactive_methods and source in interactive_sources
        )
payno's avatar
payno committed
522
523
524
525
526
        self._intensityCalcOpts.setCalculationFctVisible(
            method in interactive_methods
            and source in interactive_sources
            and source is not _ValueSource.DATASET
        )
527
528
529
530
531
532
533
        self._scalarValueWidget.setVisible(source == _ValueSource.MANUAL_SCALAR)
        self._buttonsGrp.setVisible(
            method in interactive_methods and source in interactive_sources
        )

        self.sigSourceChanged.emit()

534
535
536
537
538
539
    def _lockChanged(self):
        self._scalarValueWidget.setEnabled(not self.isLocked())
        self._datasetWidget.setEnabled(not self.isLocked())
        self._intensityCalcOpts.setEnabled(not self.isLocked())
        self._modeCB.setEnabled(not self.isLocked())
        self._computeButton.setEnabled(not self.isLocked())
540

541
542
543
    def isLocked(self):
        return self._lockButton.isLocked()

544
545
546
    def setLocked(self, locked):
        self._lockButton.setChecked(locked)

547
548
549
    def _hideLockButton(self):
        self._lockButton.hide()

550
    def getCurrentMethod(self):
551
        return Method.from_value(self._modeCB.currentText())
552

553
554
555
    def setCurrentMethod(self, method):
        method = Method.from_value(method)
        idx = self._modeCB.findText(method.value)
556
557
        self._modeCB.setCurrentIndex(idx)

558
    def getCurrentSource(self):
payno's avatar
payno committed
559
        if self.getCurrentMethod() in (Method.DIVISION, Method.SUBTRACTION):
Henri Payno's avatar
Henri Payno committed
560
561
562
            return _ValueSource.from_value(self._sourceCB.currentText())
        else:
            return _ValueSource.NONE
563
564
565
566
567
568

    def setCurrentSource(self, source):
        source = _ValueSource.from_value(source)
        idx = self._sourceCB.findText(source.value)
        self._sourceCB.setCurrentIndex(idx)

569
    def _modeChanged(self, *args, **kwargs):
570
        mode = self.getCurrentMethod()
payno's avatar
payno committed
571
        mode_with_calculations = (Method.DIVISION, Method.SUBTRACTION)
572
573
574
575
        self._intensityCalcOpts.setVisible(mode in mode_with_calculations)
        self._sourceCB.setVisible(mode in mode_with_calculations)
        self._sourceLabel.setVisible(mode in mode_with_calculations)
        self._sourceChanged()
576
        self.sigModeChanged.emit()
577
        self.sigSourceChanged.emit()
578

579
580
581
    def _valueUpdated(self, *args):
        self.sigValueUpdated.emit(args)

582
583
584
585
    def setManualROIVisible(self, visible):
        pass

    def getConfiguration(self) -> dict:
586
587
        return _normParams.IntensityNormalizationParams(
            method=self.getCurrentMethod(),
588
            source=self.getCurrentSource(),
589
590
            extra_infos=self.getExtraInfos(),
        ).to_dict()
591
592

    def setConfiguration(self, config: dict):
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
        params = _normParams.IntensityNormalizationParams.from_dict(config)
        self.setCurrentMethod(params.method)
        extra_infos = params.extra_infos
        if (
            "start_x" in extra_infos
            and "start_y" in extra_infos
            and "end_x" in extra_infos
            and "end_y" in extra_infos
        ):
            start_x = extra_infos["start_x"]
            start_y = extra_infos["start_y"]
            end_x = extra_infos["end_x"]
            end_y = extra_infos["end_y"]
            self._getROI().setOrigin((start_x, start_y))
            self._getROI().setSize((end_x - start_x, end_y - start_y))
        if "calc_fct" in extra_infos:
            self._intensityCalcOpts.setCalculationFct(extra_infos["calc_fct"])
        if "calc_area" in extra_infos:
            self._intensityCalcOpts.setCalculationArea(extra_infos["calc_area"])
        if "calc_method" in extra_infos:
            self._intensityCalcOpts.setCalculationMethod(extra_infos["calc_method"])
614
        if params.source is _ValueSource.MANUAL_SCALAR:
615
616
            if "value" in extra_infos:
                self._scalarValueWidget.setValue(extra_infos["value"])
617

618
619
620
    def setScan(self, scan):
        self._datasetWidget.setScan(scan=scan)

621
622
    def getExtraInfos(self):
        method = self.getCurrentMethod()
623
624
        source = self.getCurrentSource()
        if method in (Method.CHEBYSHEV, Method.NONE):
625
            return {}
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
        else:
            if source is _ValueSource.MANUAL_SCALAR:
                return {"value": self._scalarValueWidget.getValue()}
            elif source is _ValueSource.AUTO_ROI:
                raise NotImplementedError("auto roi not implemented yet")
            elif source is _ValueSource.MANUAL_ROI:
                roi = self._getROI()
                return {
                    "start_x": roi.getOrigin()[0],
                    "end_x": roi.getOrigin()[0] + roi.getSize()[0],
                    "start_y": roi.getOrigin()[1],
                    "end_y": roi.getOrigin()[1] + roi.getSize()[1],
                    "calc_fct": self._intensityCalcOpts.getCalculationFct().value,
                }
            elif source is _ValueSource.DATASET:
                return {
                    "dataset_url": self._datasetWidget.getDatasetUrl().path(),
                }
            else:
                raise ValueError(f"unhandled source: {source} for method {method}")
646

647
648
649
    def _computationRequested(self):
        self.sigProcessingRequested.emit()

650
651
652

class _NormIntensityCalcOpts(qt.QWidget):
    """Options to compute the norm intensity"""
653

654
655
656
    sigConfigurationChanged = qt.Signal()
    """Signal emitted when configuration changes"""

657
658
659
    def __init__(self, parent):
        qt.QWidget.__init__(self, parent)
        self.setLayout(qt.QFormLayout())
660
        # calculation function
661
662
663
        self._calculationModeCB = qt.QComboBox(self)
        for fct in _normParams._ValueCalculationFct.values():
            self._calculationModeCB.addItem(fct)
payno's avatar
payno committed
664
665
        self._calculationModeLabel = qt.QLabel("calculation fct", self)
        self.layout().addRow(self._calculationModeLabel, self._calculationModeCB)
666
667

        # connect signal / slot
668
        self._calculationModeCB.currentIndexChanged.connect(self._configurationChanged)
payno's avatar
payno committed
669
670
671
672

    def setCalculationFctVisible(self, visible):
        self._calculationModeLabel.setVisible(visible)
        self._calculationModeCB.setVisible(visible)
673
674
675

    def _configurationChanged(self):
        self.sigConfigurationChanged.emit()
676
677

    def getCalculationFct(self):
678
679
680
        return _normParams._ValueCalculationFct.from_value(
            self._calculationModeCB.currentText()
        )
681

682
683
684
685
686
    def setCalculationFct(self, fct):
        idx = self._calculationModeCB.findText(
            _normParams._ValueCalculationFct.from_value(fct)
        )
        self._calculationModeCB.setCurrentIndex(idx)
687
688
689
690
691


class _NormIntensityControl(ControlWidget):
    def __init__(self, parent=None):
        ControlWidget.__init__(self, parent)
692
693
        self._resultWidget = qt.QWidget(self)
        self._resultWidget.setLayout(qt.QFormLayout())
694
        self._result = None
695
696
697
698
699
700

        self._resultQLE = qt.QLineEdit("", self)
        self._resultWidget.layout().addRow("value:", self._resultQLE)
        self._resultQLE.setReadOnly(True)
        self.layout().insertWidget(0, self._resultWidget)

701
702
        self._computeBut.hide()

703
    def setResult(self, result):
704
        self._result = result
705
706
        if isinstance(result, tuple):
            result = ",".join([str(element) for element in result])
707
        self._resultQLE.setText(str(result))
708

709
710
711
    def getResult(self):
        return self._result

712
713
    def clear(self):
        self._resultQLE.clear()
714
715


716
717
class _NormIntensityScalarValue(qt.QWidget):

718
    sigValueChanged = qt.Signal(float)
719
720
721
722

    def __init__(self, parent=None):
        qt.QWidget.__init__(self, parent=parent)
        self.setLayout(qt.QFormLayout())
723
        self._QLE = qt.QLineEdit("0.0", self)
724
725
726
727
        validator = qt.QDoubleValidator(parent=self)
        self._QLE.setValidator(validator)
        self.layout().addRow("value", self._QLE)

728
729
730
        # connect signal / slot
        self._QLE.editingFinished.connect(self._valueChanged)

731
732
733
734
735
736
737
738
739
740
    def setValue(self, value):
        self._QLE.setText(str(value))

    def getValue(self):
        return float(self._QLE.text())

    def _valueChanged(self):
        self.sigValueChanged.emit(self.getValue())


741
class _NormIntensityDatasetWidget(qt.QWidget):
742
743
744

    _FILE_PATH_LOCAL_VALUE = "scan master file"

745
746
    sigConfigurationChanged = qt.Signal()

747
748
749
750
751
752
753
754
755
756
    def __init__(self, parent=None):
        qt.QWidget.__init__(self, parent)
        self._lastGlobalPath = None
        self._scan = None

        self.setLayout(qt.QGridLayout())
        # file scope
        self._fileScopeGB = qt.QGroupBox("file scope", self)
        self._fileScopeGB.setLayout(qt.QVBoxLayout())
        self._buttonGrpBox = qt.QButtonGroup(self)
757
        self._globalRB = qt.QRadioButton(_normParams._DatasetScope.GLOBAL.value, self)
758
759
760
        self._buttonGrpBox.addButton(self._globalRB)
        self._globalRB.setToolTip("Global dataset. Will be constant with time")
        self._fileScopeGB.layout().addWidget(self._globalRB)
761
        self._localRB = qt.QRadioButton(_normParams._DatasetScope.LOCAL.value, self)
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
        self._buttonGrpBox.addButton(self._localRB)
        self._localRB.setToolTip(
            "Local dataset."
            "Must be contained in the NXtomo entry "
            "provided (not compatible with EDF)."
        )
        self._fileScopeGB.layout().addWidget(self._localRB)
        self.layout().addWidget(self._fileScopeGB, 0, 0, 3, 3)
        # file_path
        self._filePathLabel = qt.QLabel("file path", self)
        self.layout().addWidget(self._filePathLabel, 3, 0, 1, 1)
        self._filePathQLE = qt.QLineEdit("", self)
        self.layout().addWidget(self._filePathQLE, 3, 1, 1, 1)
        self._selectFileButton = qt.QPushButton("select", self)
        self.layout().addWidget(self._selectFileButton, 3, 2, 1, 1)
        # data path
        self._dataPathLabel = qt.QLabel("data path", self)
        self.layout().addWidget(self._dataPathLabel, 4, 0, 1, 1)
        self._dataPathQLE = qt.QLineEdit("", self)
        self.layout().addWidget(self._dataPathQLE, 4, 1, 1, 1)
        self._selectDataPathButton = qt.QPushButton("select", self)
        self.layout().addWidget(self._selectDataPathButton, 4, 2, 1, 1)

        # set up
        self._localRB.setChecked(True)
787
        self._updateFilePathVisibility()
788
789

        # connect signal / slot
790
        self._buttonGrpBox.buttonToggled.connect(self._updateFilePathVisibility)
791
792
        self._selectFileButton.released.connect(self._selectFile)
        self._selectDataPathButton.released.connect(self._selectDataPath)
793
794
795
796
797
798
        self._buttonGrpBox.buttonReleased.connect(self._configurationChanged)
        self._filePathQLE.editingFinished.connect(self._configurationChanged)
        self._dataPathQLE.editingFinished.connect(self._configurationChanged)

    def _configurationChanged(self, *args, **kwargs):
        self.sigConfigurationChanged.emit()
799
800
801
802
803
804
805
806
807
808
809
810

    def setScan(self, scan):
        if scan is not None:
            self._scan = weakref.ref(scan)
        return None

    def getScan(self):
        if self._scan is not None:
            return self._scan()
        else:
            return None

811
    def getMode(self) -> _normParams._DatasetScope:
812
        if self._localRB.isChecked():
813
            return _normParams._DatasetScope.LOCAL
814
        else:
815
            return _normParams._DatasetScope.GLOBAL
816
817
818
819
820
821
822
823
824
825
826
827
828

    def getDataPath(self):
        return self._dataPathQLE.text()

    def setDataPath(self, path):
        self._dataPathQLE.setText(path)

    def getGlobalFilePath(self) -> str:
        return self._filePathQLE.text()

    def setGlobalFilePath(self, file_path):
        self._filePathQLE.setText(file_path)

829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
    def getDatasetUrl(self) -> DataUrl:
        if self.getMode() is _normParams._DatasetScope.LOCAL:
            if self.getScan() is not None:
                scan = self.getScan()
                file_path = scan.master_file
            else:
                file_path = None
        else:
            file_path = self.getGlobalFilePath()
        data_path = self.getDataPath()
        if file_path is not None and file_path.lower().endswith("edf"):
            scheme = "fabio"
        else:
            scheme = "silx"

        return DataUrl(
            file_path=file_path,
            data_path=data_path,
            scheme=scheme,
        )

    def setDatasetUrl(self, url: DataUrl):
        raise NotImplementedError("")

853
    def _updateFilePathVisibility(self):
854
855
856
857
858
        self._filePathQLE.setReadOnly(self.getMode() == _normParams._DatasetScope.LOCAL)
        self._filePathQLE.setEnabled(self.getMode() == _normParams._DatasetScope.GLOBAL)
        self._selectFileButton.setEnabled(
            self.getMode() == _normParams._DatasetScope.GLOBAL
        )
859
        if (
860
            self.getMode() == _normParams._DatasetScope.LOCAL
861
862
863
864
865
            and self.getGlobalFilePath() != self._FILE_PATH_LOCAL_VALUE
        ):
            self._lastGlobalPath = self.getGlobalFilePath()
            self.setGlobalFilePath(self._FILE_PATH_LOCAL_VALUE)
        elif (
866
            self.getMode() == _normParams._DatasetScope.GLOBAL
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
            and self.getGlobalFilePath() == self._FILE_PATH_LOCAL_VALUE
        ):
            if self._lastGlobalPath is not None:
                self.setGlobalFilePath(self._lastGlobalPath)

    def _selectFile(self):
        dialog = qt.QFileDialog(self)
        dialog.setNameFilters(["HDF5 file *.h5 *.hdf5 *.nx *.nexus"])

        if not dialog.exec_():
            dialog.close()
            return

        filesSelected = dialog.selectedFiles()
        if len(filesSelected) > 0:
            self.setGlobalFilePath(filesSelected[0])

    def _selectDataPath(self):
        """Open a dialog. If from a master file try to open the scan
        master file if any."""
887
        if self.getMode() is _normParams._DatasetScope.LOCAL:
888
889
890
891
892
893
            if self.getScan() is not None:
                scan = self.getScan()
                if not isinstance(scan, HDF5TomoScan):
                    mess = qt.QMessageBox(
                        parent=self,
                        icon=qt.QMessageBox.Warning,
894
                        text="local mode is only available for HDF5 acquisitions",
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
                    )
                    mess.setModal(False)
                    mess.show()
                    return
                else:
                    file_ = scan.master_file
            else:
                mess = qt.QMessageBox(
                    parent=self,
                    icon=qt.QMessageBox.Information,
                    text="No scan set. Unable to find the master file",
                )
                mess.setModal(False)
                mess.show()
                return
910
        elif self.getMode() is _normParams._DatasetScope.GLOBAL:
911
            file_ = self.getGlobalFilePath()
912
913
        else:
            raise ValueError("{} is not handled".format(self.getMode()))
914
915
916
917
918
919
920
921
922
923
924

        dialog = DataFileDialog()
        dialog.selectFile(file_)
        dialog.setFilterMode(DataFileDialog.FilterMode.ExistingDataset)

        if not dialog.exec_():
            dialog.close()
            return
        else:
            selected_url = dialog.selectedUrl()
            self.setDataPath(DataUrl(path=selected_url).data_path())