intensity.py 33.2 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
payno's avatar
payno committed
48
from tomwer.core.process.reconstruction.normalization.normalization import Method
49
from tomwer.core.process.reconstruction.normalization import params as _normParams
50
from tomoscan.normalization import Method as TomoScanMethod
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
89
        # connect signal / slot
        self._optsWidget.sigModeChanged.connect(self._modeChanged)
90
        self._optsWidget.sigValueUpdated.connect(self.setResult)
91
        self._optsWidget.sigConfigurationChanged.connect(self._configurationChanged)
92
        self._crtWidget.sigValidateRequest.connect(self._validated)
93

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

98
99
100
    def _configurationChanged(self):
        self.sigConfigurationChanged.emit()

101
102
103
    def _hideLockButton(self):
        self._optsWidget._hideLockButton()

104
105
106
    def _validated(self):
        pass

107
    def getConfiguration(self) -> dict:
108
        return self._optsWidget.getConfiguration()
109
110

    def setConfiguration(self, config: dict):
111
        self._optsWidget.setConfiguration(config=config)
112

113
114
    def getCurrentMethod(self):
        return self._optsWidget.getCurrentMethod()
115
116
117

    def _modeChanged(self):
        self._centralWidget.setManualROIVisible(
118
            self.getCurrentMethod() == Method.MANUAL_ROI
119
        )
120
121
        scan = self.getScan()
        if scan:
122
            # if the normed sinogram can be obtained `directly` from tomoscan
123
124
            if self.getCurrentMethod().value in TomoScanMethod.values():
                scan.intensity_normalization = self.getCurrentMethod().value
125
                scan.intensity_normalization.set_extra_infos(self.getExtraArgs())
payno's avatar
payno committed
126
                scan.intensity_normalization.tomwer_processing_res_code = True
127
128
129
130
131
132

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

134
    def setScan(self, scan: typing.Union[None, TomwerScanBase]):
135
        self._scan = weakref.ref(scan)
136
        self._centralWidget.setScan(scan=scan)
137
138
        self._optsWidget.setScan(scan=scan)

139
    def setCurrentMethod(self, mode):
140
        self._optsWidget.setCurrentMethod(mode)
141

payno's avatar
payno committed
142
    def stop(self):
143
144
145
        if self._centralWidget is not None:
            self._centralWidget.stop()
            self._centralWidget = None
payno's avatar
payno committed
146

147
    def getExtraArgs(self) -> dict:
148
149
150
151
152
        return self._optsWidget.getExtraInfos()

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

153
154
155
156
157
    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
        )

158
    def setResult(self, result):
159
160
161
        # once computed update scan normalization values (
        # update scalar value for example)
        scan = self.getScan()
162
        if scan and self.getCurrentMethod().value in TomoScanMethod.values():
163
164
            scan.intensity_normalization = self.getCurrentMethod().value
            scan.intensity_normalization.set_extra_infos(self.getExtraArgs())
165
166
167
168
        self._crtWidget.setResult(result)

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

170
171
172
    def isLocked(self):
        return self._optsWidget.isLocked()

173
174
175
    def setLocked(self, locked):
        self._optsWidget.setLocked(locked)

176
177

class _Viewer(qt.QTabWidget):
178
179
180
    def __init__(self, parent):
        if not isinstance(parent, NormIntensityWindow):
            raise TypeError("Expect a NormIntensityWindow as parrent")
181
182
183
184
185
186
187
188
        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)
189
190
191
192
193
        self._sinoView.sigSinoLoadEnded.connect(self._updateSinogramROI)
        self._projView.sigROIChanged.connect(self._updateSinogramROI)

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

195
196
197
        :param scan: scan to handle
        :return:
        """
198
        self._projView.setScan(scan)
199
        self._sinoView.setScan(scan, update=False)
200

201
    def _updateSinogramROI(self):
202
        display_sino_roi = self.parent().getCurrentMethod()
203
        if display_sino_roi != Method.MANUAL_ROI:
204
            self._sinoView.setROIVisible(False)
205
206
207
208
209
210
211
212
213
214
215
216
        else:
            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)
217
218
219
220

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

payno's avatar
payno committed
222
223
224
225
226
227
    def stop(self):
        self._projView.stop()
        self._projView = None
        self._sinoView.stop()
        self._sinoView = None

228
229
230
    def getROI(self):
        return self._projView.getROI()

231
232
233
234
235
    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
        )

236
237

class _ProjPlotWithROI(DataViewer):
238
239
240
241
242
    """DataViewer specialized on projections. Embed a RectangleROI"""

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

243
244
245
    def __init__(self, *args, **kwargs):
        DataViewer.__init__(self, *args, **kwargs)
        self._sinogramLine = 0
246
        self._roiVisible = False
247
248
249
250
251
252
253
254
        self.setScanInfoVisible(False)
        self.setDisplayMode("radios")
        self.setDisplayModeVisible(False)

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

255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
        # 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

273
274
275
276
    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))

277
278
279
280
281
282
283
284
285
286
    def setManualROIVisible(self, visible):
        self._roiVisible = visible
        self._updateROIVisibility()

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

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

287
288
289
290
    def setSinogramLine(self, line):
        self._sinogramLine = line
        self._updateSinogramLine()

291
292
293
294
295
296
297
298
    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)

299
    def _updateSinogramLine(self):
300
301
302
303
        self._roiManager = RegionOfInterestManager(self.getPlotWidget())

        self.getPlotWidget().addYMarker(
            y=self._sinogramLine,
304
305
306
307
308
            legend="sinogram_line",
            text="sinogram line",
            color="blue",
            selectable=False,
        )
309
310
311
        sino_marker = self.getPlotWidget()._getMarker("sinogram_line")
        if sino_marker:
            sino_marker.setLineStyle("--")
312
313
314
315
316

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

payno's avatar
payno committed
317
    def stop(self):
318
        self._viewer._plot.stopUpdateThread()
payno's avatar
payno committed
319

320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339

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))

340
341
342
343
344
345
346
347
348
349
        # 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)

350
351
        # connect signal / Slots
        self._options._lineSB.valueChanged.connect(self._sinogramLineChanged)
352
353
        self.sigSinogramLineChanged.connect(self._plot.clear)
        self.getPlotWidget().sigActiveImageChanged.connect(self._updateROI)
354
355
356
357

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

358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
    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()

375
376
377
378
    def _updatePlot(self, sinogram):
        self._plot.getPlotWidget().addImage(data=sinogram)
        self._plot.getPlotWidget().replot()

payno's avatar
payno committed
379
    def stop(self):
380
        self._plot.stopUpdateThread()
payno's avatar
payno committed
381

382
383
384
385
386

class _NormIntensityOptions(qt.QWidget):

    sigValueCanBeLocked = qt.Signal(bool)

387
388
389
    sigProcessingRequested = qt.Signal()
    """Signal emit when the processing is requested"""

390
391
392
    sigModeChanged = qt.Signal()
    """signal emitted when the mode change"""

393
394
395
    sigValueUpdated = qt.Signal(object)
    """Signal emit when user defines manually the value"""

396
397
398
    sigConfigurationChanged = qt.Signal()
    """Signal emit when the configuration changes"""

399
    def __init__(self, parent):
400
401
402
403
        if not isinstance(parent, NormIntensityWindow):
            raise TypeError(
                "parent is expected to be an instance of " "NormIntensityWindow "
            )
404
        qt.QWidget.__init__(self, parent)
405
        self._getROI = self.parent().getROI
406
        self.setLayout(qt.QGridLayout())
407
408
        # mode
        self._modeCB = qt.QComboBox(self)
409
        for mode in Method:
410
411
412
413
414
415
416
417
418
419
            if mode in (
                Method.LSQR_SPLINE,
                Method.MANUAL_SCALAR,
                Method.DATASET,
                Method.AUTO_ROI,
                Method.MANUAL_ROI,
            ):
                continue
            else:
                self._modeCB.addItem(mode.value)
420
421
422
423
424
        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)
425
426
427
428
429

        # method
        self._optsMethod = qt.QGroupBox(self)
        self._optsMethod.setTitle("options")
        self._optsMethod.setLayout(qt.QVBoxLayout())
430
        self.layout().addWidget(self._optsMethod, 1, 0, 1, 3)
431
        # intensity calculation options
432
433
        self._intensityCalcOpts = _NormIntensityCalcOpts(self)
        self._optsMethod.layout().addWidget(self._intensityCalcOpts)
434
        # dataset widget
435
436
        self._datasetWidget = _NormIntensityDatasetWidget(self)
        self._optsMethod.layout().addWidget(self._datasetWidget)
437
438
        # scalar value
        self._scalarValueWidget = _NormIntensityScalarValue(self)
439
        self.layout().addWidget(self._scalarValueWidget, 2, 0, 1, 3)
440
441
442
443
444
445
446
447
448
        # 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)
449
        self.layout().addWidget(self._buttonsGrp, 3, 0, 1, 3)
450
451
452
453
454

        self._modeChanged()

        # connect signal / slot
        self._modeCB.currentIndexChanged.connect(self._modeChanged)
455
        self._modeCB.currentIndexChanged.connect(self._configurationChanged)
456
        self._computeButton.released.connect(self._computationRequested)
457
        self._scalarValueWidget.sigValueChanged.connect(self._valueUpdated)
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
        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()

    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())
475

476
477
478
    def isLocked(self):
        return self._lockButton.isLocked()

479
480
481
    def setLocked(self, locked):
        self._lockButton.setChecked(locked)

482
483
484
    def _hideLockButton(self):
        self._lockButton.hide()

485
    def getCurrentMethod(self):
486
        return Method.from_value(self._modeCB.currentText())
487

488
489
490
    def setCurrentMethod(self, method):
        method = Method.from_value(method)
        idx = self._modeCB.findText(method.value)
491
492
        self._modeCB.setCurrentIndex(idx)

493
    def _modeChanged(self, *args, **kwargs):
494
        mode = self.getCurrentMethod()
495
        self._intensityCalcOpts.setVisible(mode in (Method.MANUAL_ROI, Method.DATASET))
496
497
498
499
500
501
        self._intensityCalcOpts._calculationAreaCB.setVisible(
            mode in (Method.MANUAL_ROI,)
        )
        self._intensityCalcOpts._calculationAreaLabel.setVisible(
            mode in (Method.MANUAL_ROI,)
        )
502
503
        self._datasetWidget.setVisible(mode == Method.DATASET)
        self.setManualROIVisible(mode == Method.MANUAL_ROI)
504
505
506
507
        self._optsMethod.setVisible(
            mode in (Method.MANUAL_ROI, Method.AUTO_ROI, Method.DATASET)
        )
        self._scalarValueWidget.setVisible(mode == Method.MANUAL_SCALAR)
508
509
510
511
        self._buttonsGrp.setVisible(
            mode in (Method.AUTO_ROI, Method.DATASET, Method.MANUAL_ROI)
        )

512
        self.sigValueCanBeLocked.emit(mode == Method.MANUAL_SCALAR)
513
        self.sigModeChanged.emit()
514

515
516
517
    def _valueUpdated(self, *args):
        self.sigValueUpdated.emit(args)

518
519
520
521
    def setManualROIVisible(self, visible):
        pass

    def getConfiguration(self) -> dict:
522
523
524
525
        return _normParams.IntensityNormalizationParams(
            method=self.getCurrentMethod(),
            extra_infos=self.getExtraInfos(),
        ).to_dict()
526
527

    def setConfiguration(self, config: dict):
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
        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"])
549
550
551
        if params.method is Method.MANUAL_SCALAR:
            if "value" in extra_infos:
                self._scalarValueWidget.setValue(extra_infos["value"])
552

553
554
555
    def setScan(self, scan):
        self._datasetWidget.setScan(scan=scan)

556
557
    def getExtraInfos(self):
        method = self.getCurrentMethod()
558
        if method is Method.MANUAL_SCALAR:
559
            return {"value": self._scalarValueWidget.getValue()}
560
        elif method is Method.AUTO_ROI:
561
            raise NotImplementedError("auto roi not implemented yet")
562
        elif method is Method.MANUAL_ROI:
563
564
565
566
567
568
569
570
571
572
            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,
                "calc_area": self._intensityCalcOpts.getCalculationArea().value,
                "calc_method": self._intensityCalcOpts.getCalculationMethod().value,
            }
573
574
575
576
577
578
579
        elif method is Method.DATASET:
            return {
                "calc_fct": self._intensityCalcOpts.getCalculationFct().value,
                "calc_area": self._intensityCalcOpts.getCalculationArea().value,
                "calc_method": self._intensityCalcOpts.getCalculationMethod().value,
                "dataset_url": self._datasetWidget.getDatasetUrl().path(),
            }
580
581
582
        else:
            return {}

583
584
585
    def _computationRequested(self):
        self.sigProcessingRequested.emit()

586
587
588

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

590
591
592
    sigConfigurationChanged = qt.Signal()
    """Signal emitted when configuration changes"""

593
594
595
    def __init__(self, parent):
        qt.QWidget.__init__(self, parent)
        self.setLayout(qt.QFormLayout())
596
        # calculation function
597
598
599
600
601
602
603
604
        self._calculationModeCB = qt.QComboBox(self)
        for fct in _normParams._ValueCalculationFct.values():
            self._calculationModeCB.addItem(fct)
        self.layout().addRow("calculation fct", self._calculationModeCB)
        # calculation depth
        self._calculationAreaCB = qt.QComboBox(self)
        for area in _normParams._CalculationArea.values():
            self._calculationAreaCB.addItem(area)
605
606
        self._calculationAreaLabel = qt.QLabel("calculation area", self)
        self.layout().addRow(self._calculationAreaLabel, self._calculationAreaCB)
607
        # calculation 'axis'
608
609
610
611
612
613
614
        self._calculationMethodCB = qt.QComboBox(self)
        for opt in _normParams._ValueCalculationMethod.values():
            self._calculationMethodCB.addItem(opt)
        self.layout().addRow("calculation method", self._calculationMethodCB)

        # connect signal / slot
        self._calculationAreaCB.currentTextChanged.connect(self._areaChanged)
615
616
617
618
619
620
621
622
        self._calculationModeCB.currentIndexChanged.connect(self._configurationChanged)
        self._calculationAreaCB.currentIndexChanged.connect(self._configurationChanged)
        self._calculationMethodCB.currentIndexChanged.connect(
            self._configurationChanged
        )

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

    def getCalculationFct(self):
625
626
627
        return _normParams._ValueCalculationFct.from_value(
            self._calculationModeCB.currentText()
        )
628

629
630
631
632
633
    def setCalculationFct(self, fct):
        idx = self._calculationModeCB.findText(
            _normParams._ValueCalculationFct.from_value(fct)
        )
        self._calculationModeCB.setCurrentIndex(idx)
634

635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
    def getCalculationArea(self):
        return _normParams._CalculationArea.from_value(
            self._calculationAreaCB.currentText()
        )

    def setCalculationArea(self, area):
        idx = self._calculationAreaCB.findText(
            _normParams._CalculationArea.from_value(area)
        )
        self._calculationAreaCB.setCurrentIndex(idx)

    def getCalculationMethod(self):
        return _normParams._ValueCalculationMethod.from_value(
            self._calculationMethodCB.currentText()
        )

    def setCalculationMethod(self, method):
        idx = self._calculationMethodCB.findText(
            _normParams._ValueCalculationMethod.from_value(method)
        )
        self._calculationMethodCB.setCurrentIndex(idx)

    def _areaChanged(self):
        if self.getCalculationArea() == _normParams._CalculationArea.PROJECTION:
            idx = self._calculationMethodCB.findText(
                _normParams._ValueCalculationMethod.SCALAR_VALUE.value
            )
            self._calculationMethodCB.setCurrentIndex(idx)
            self._calculationMethodCB.setEnabled(False)
        else:
            self._calculationMethodCB.setEnabled(True)
666
667
668
669
670


class _NormIntensityControl(ControlWidget):
    def __init__(self, parent=None):
        ControlWidget.__init__(self, parent)
671
672
673
674
675
676
677
678
        self._resultWidget = qt.QWidget(self)
        self._resultWidget.setLayout(qt.QFormLayout())

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

679
680
        self._computeBut.hide()

681
    def setResult(self, result):
682
683
        if isinstance(result, tuple):
            result = ",".join([str(element) for element in result])
684
        self._resultQLE.setText(str(result))
685
686
687

    def clear(self):
        self._resultQLE.clear()
688
689


690
691
class _NormIntensityScalarValue(qt.QWidget):

692
    sigValueChanged = qt.Signal(float)
693
694
695
696

    def __init__(self, parent=None):
        qt.QWidget.__init__(self, parent=parent)
        self.setLayout(qt.QFormLayout())
697
        self._QLE = qt.QLineEdit("0.0", self)
698
699
700
701
        validator = qt.QDoubleValidator(parent=self)
        self._QLE.setValidator(validator)
        self.layout().addRow("value", self._QLE)

702
703
704
        # connect signal / slot
        self._QLE.editingFinished.connect(self._valueChanged)

705
706
707
708
709
710
711
712
713
714
    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())


715
class _NormIntensityDatasetWidget(qt.QWidget):
716
717
718

    _FILE_PATH_LOCAL_VALUE = "scan master file"

719
720
    sigConfigurationChanged = qt.Signal()

721
722
723
724
725
726
727
728
729
730
    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)
731
        self._globalRB = qt.QRadioButton(_normParams._DatasetScope.GLOBAL.value, self)
732
733
734
        self._buttonGrpBox.addButton(self._globalRB)
        self._globalRB.setToolTip("Global dataset. Will be constant with time")
        self._fileScopeGB.layout().addWidget(self._globalRB)
735
        self._localRB = qt.QRadioButton(_normParams._DatasetScope.LOCAL.value, self)
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
        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)
761
        self._updateFilePathVisibility()
762
763

        # connect signal / slot
764
        self._buttonGrpBox.buttonToggled.connect(self._updateFilePathVisibility)
765
766
        self._selectFileButton.released.connect(self._selectFile)
        self._selectDataPathButton.released.connect(self._selectDataPath)
767
768
769
770
771
772
        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()
773
774
775
776
777
778
779
780
781
782
783
784

    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

785
    def getMode(self) -> _normParams._DatasetScope:
786
        if self._localRB.isChecked():
787
            return _normParams._DatasetScope.LOCAL
788
        else:
789
            return _normParams._DatasetScope.GLOBAL
790
791
792
793
794
795
796
797
798
799
800
801
802

    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)

803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
    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("")

827
    def _updateFilePathVisibility(self):
828
829
830
831
832
        self._filePathQLE.setReadOnly(self.getMode() == _normParams._DatasetScope.LOCAL)
        self._filePathQLE.setEnabled(self.getMode() == _normParams._DatasetScope.GLOBAL)
        self._selectFileButton.setEnabled(
            self.getMode() == _normParams._DatasetScope.GLOBAL
        )
833
        if (
834
            self.getMode() == _normParams._DatasetScope.LOCAL
835
836
837
838
839
            and self.getGlobalFilePath() != self._FILE_PATH_LOCAL_VALUE
        ):
            self._lastGlobalPath = self.getGlobalFilePath()
            self.setGlobalFilePath(self._FILE_PATH_LOCAL_VALUE)
        elif (
840
            self.getMode() == _normParams._DatasetScope.GLOBAL
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
            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."""
861
        if self.getMode() is _normParams._DatasetScope.LOCAL:
862
863
864
865
866
867
            if self.getScan() is not None:
                scan = self.getScan()
                if not isinstance(scan, HDF5TomoScan):
                    mess = qt.QMessageBox(
                        parent=self,
                        icon=qt.QMessageBox.Warning,
868
                        text="local mode is only available for HDF5 acquisitions",
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
                    )
                    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
884
        elif self.getMode() is _normParams._DatasetScope.GLOBAL:
885
            file_ = self.getGlobalFilePath()
886
887
        else:
            raise ValueError("{} is not handled".format(self.getMode()))
888
889
890
891
892
893
894
895
896
897
898

        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())