intensity.py 19.9 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
from silx.gui.dialog.DataFileDialog import DataFileDialog
43
from tomwer.core.scan.normalization import (Method, _MethodMode, _ValueCalculationMode)
44
45
46
47
48
49
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
import weakref
50
51
52
53
54
55
import typing


class NormIntensityWindow(qt.QMainWindow):
    def __init__(self, parent):
        qt.QMainWindow.__init__(self, parent)
56
        self.setWindowFlags(qt.Qt.Widget)
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

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

80
81
82
        # connect signal / slot
        self._optsWidget.sigModeChanged.connect(self._modeChanged)

83
84
85
        # set up
        self._centralWidget._updateSinogramROI()

86
87
88
89
90
91
    def getConfiguration(self) -> dict:
        pass

    def setConfiguration(self, config: dict):
        pass

92
93
94
95
96
    def getCurrentMode(self):
        return self._optsWidget.getCurrentMode()

    def _modeChanged(self):
        self._centralWidget.setManualROIVisible(
97
            self.getCurrentMode() == Method.MANUAL_ROI
98
99
        )

100
101
    def setScan(self, scan: typing.Union[None, TomwerScanBase]):
        self._centralWidget.setScan(scan=scan)
102
103
104
105
        self._optsWidget.setScan(scan=scan)

    def setMode(self, mode):
        self._optsWidget.setCurrentMode(mode)
106
107
108


class _Viewer(qt.QTabWidget):
109
110
111
    def __init__(self, parent):
        if not isinstance(parent, NormIntensityWindow):
            raise TypeError("Expect a NormIntensityWindow as parrent")
112
113
114
115
116
117
118
119
        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)
120
121
122
123
124
        self._sinoView.sigSinoLoadEnded.connect(self._updateSinogramROI)
        self._projView.sigROIChanged.connect(self._updateSinogramROI)

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

126
127
128
        :param scan: scan to handle
        :return:
        """
129
130
131
        self._projView.setScan(scan)
        self._sinoView.setScan(scan)

132
    def _updateSinogramROI(self):
133
        display_sino_roi = self.parent().getCurrentMode()
134
        if display_sino_roi != Method.MANUAL_ROI:
135
            self._sinoView.setROIVisible(False)
136
137
138
139
140
141
142
143
144
145
146
147
        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)
148
149
150
151

    def setManualROIVisible(self, visible):
        self._projView.setManualROIVisible(visible=visible)
        self._sinoView.setROIVisible(visible=visible)
152
153
154


class _ProjPlotWithROI(DataViewer):
155
156
157
158
159
    """DataViewer specialized on projections. Embed a RectangleROI"""

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

160
161
162
    def __init__(self, *args, **kwargs):
        DataViewer.__init__(self, *args, **kwargs)
        self._sinogramLine = 0
163
        self._roiVisible = False
164
165
166
167
168
169
170
171
        self.setScanInfoVisible(False)
        self.setDisplayMode("radios")
        self.setDisplayModeVisible(False)

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

172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
        # 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

    def setManualROIVisible(self, visible):
        self._roiVisible = visible
        self._updateROIVisibility()

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

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

200
201
202
203
    def setSinogramLine(self, line):
        self._sinogramLine = line
        self._updateSinogramLine()

204
205
206
207
208
209
210
211
    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)

212
    def _updateSinogramLine(self):
213
214
215
216
        self._roiManager = RegionOfInterestManager(self.getPlotWidget())

        self.getPlotWidget().addYMarker(
            y=self._sinogramLine,
217
218
219
220
221
            legend="sinogram_line",
            text="sinogram line",
            color="blue",
            selectable=False,
        )
222
223
224
        sino_marker = self.getPlotWidget()._getMarker("sinogram_line")
        if sino_marker:
            sino_marker.setLineStyle("--")
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249

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


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

250
251
252
253
254
255
256
257
258
259
        # 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)

260
261
        # connect signal / Slots
        self._options._lineSB.valueChanged.connect(self._sinogramLineChanged)
262
263
        self.sigSinogramLineChanged.connect(self._plot.clear)
        self.getPlotWidget().sigActiveImageChanged.connect(self._updateROI)
264
265
266
267

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

268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
    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()

285
286
287
288
289

class _NormIntensityOptions(qt.QWidget):

    sigValueCanBeLocked = qt.Signal(bool)

290
291
292
    sigModeChanged = qt.Signal()
    """signal emitted when the mode change"""

293
294
295
296
297
    def __init__(self, parent):
        qt.QWidget.__init__(self, parent)
        self.setLayout(qt.QFormLayout())
        # mode
        self._modeCB = qt.QComboBox(self)
298
        for mode in Method:
299
300
301
302
303
304
305
306
307
            self._modeCB.addItem(mode.value)
        self.layout().addRow("mode:", self._modeCB)

        # method
        self._optsMethod = qt.QGroupBox(self)
        self._optsMethod.setTitle("options")
        self._optsMethod.setLayout(qt.QVBoxLayout())
        self.layout().addRow(self._optsMethod)

308
309
        self._intensityCalcOpts = _NormIntensityCalcOpts(self)
        self._optsMethod.layout().addWidget(self._intensityCalcOpts)
310
311
312
313
314
315
316
317
318
319

        self._datasetWidget = _NormIntensityDatasetWidget(self)
        self._optsMethod.layout().addWidget(self._datasetWidget)

        self._modeChanged()

        # connect signal / slot
        self._modeCB.currentIndexChanged.connect(self._modeChanged)

    def getCurrentMode(self):
320
        return Method.from_value(self._modeCB.currentText())
321

322
    def setCurrentMode(self, mode):
323
        mode = Method.from_value(mode)
324
325
326
        idx = self._modeCB.findText(mode.value)
        self._modeCB.setCurrentIndex(idx)

327
328
    def _modeChanged(self, *args, **kwargs):
        mode = self.getCurrentMode()
329
330
331
332
333
        self._intensityCalcOpts.setVisible(mode in (Method.MANUAL_ROI, Method.DATASET))
        self._datasetWidget.setVisible(mode == Method.DATASET)
        self.setManualROIVisible(mode == Method.MANUAL_ROI)
        self._optsMethod.setVisible(mode != Method.MANUAL_SCALAR)
        self.sigValueCanBeLocked.emit(mode == Method.MANUAL_SCALAR)
334
        self.sigModeChanged.emit()
335
336
337
338
339
340
341
342
343
344

    def setManualROIVisible(self, visible):
        pass

    def getConfiguration(self) -> dict:
        pass

    def setConfiguration(self, config: dict):
        pass

345
346
347
348
349
350
    def setScan(self, scan):
        self._datasetWidget.setScan(scan=scan)


class _NormIntensityCalcOpts(qt.QWidget):
    """Options to compute the norm intensity"""
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373

    def __init__(self, parent):
        qt.QWidget.__init__(self, parent)
        self.setLayout(qt.QFormLayout())
        # norm mode
        self._normModeCB = qt.QComboBox(self)
        for mode in _MethodMode:
            self._normModeCB.addItem(mode.value)
        self.layout().addRow("mode", self._normModeCB)
        # calculation method
        self._normCalculCB = qt.QComboBox(self)
        for vcm in _ValueCalculationMode:
            self._normCalculCB.addItem(vcm.value)
        self.layout().addRow("value calculation mode", self._normCalculCB)
        self._modeChanged()

        # connect signal / slot
        self._normModeCB.currentIndexChanged.connect(self._modeChanged)

    def getCurrentMode(self):
        return _MethodMode.from_value(self._normModeCB.currentText())

    def _modeChanged(self, *args, **kwargs):
374
        pass
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412

    def getConfiguration(self) -> dict:
        pass

    def setConfiguration(self, config: dict):
        pass


class _NormIntensityControl(ControlWidget):
    def __init__(self, parent=None):
        ControlWidget.__init__(self, parent)
        self._valueWidget = qt.QWidget(self)
        self._valueWidget.setLayout(qt.QFormLayout())
        self._valueQLE = qt.QLineEdit("", self)
        self._valueWidget.layout().addRow("value:", self._valueQLE)
        self.layout().insertWidget(0, self._valueWidget)
        self._value = None

    def getValue(self) -> typing.Union[None, float, typing.Iterable]:
        return self._value

    def setValue(self, value: typing.Union[None, float, list]):
        if value is None:
            self._valueQLE.setText("")
            self._valueQLE.setToolTip("No value defined")
        elif isinstance(value, float):
            self._valueQLE.setText(str(value))
            self._valueQLE.setToolTip("Single float value")
        elif isinstance(value, list):
            self._valueQLE.setText(str(value[0]) + "...")
            self._valueQLE.setToolTip("array of value: {}".format(value))
        else:
            raise TypeError(
                "value is expected to be None or an instance of " "float or an Iterable"
            )


class _NormIntensityDatasetWidget(qt.QWidget):
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559

    _FILE_PATH_LOCAL_VALUE = "scan master file"

    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)
        self._globalRB = qt.QRadioButton("global", self)
        self._buttonGrpBox.addButton(self._globalRB)
        self._globalRB.setToolTip("Global dataset. Will be constant with time")
        self._fileScopeGB.layout().addWidget(self._globalRB)
        self._localRB = qt.QRadioButton("local", self)
        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)
        self._updateFilePathSisibility()

        # connect signal / slot
        self._buttonGrpBox.buttonToggled.connect(self._updateFilePathSisibility)
        self._selectFileButton.released.connect(self._selectFile)
        self._selectDataPathButton.released.connect(self._selectDataPath)

    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

    def getMode(self) -> str:
        if self._localRB.isChecked():
            return "local"
        else:
            return "global"

    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)

    def _updateFilePathSisibility(self):
        self._filePathQLE.setReadOnly(self.getMode() == "local")
        self._filePathQLE.setEnabled(self.getMode() == "global")
        self._selectFileButton.setEnabled(self.getMode() == "global")
        if (
            self.getMode() == "local"
            and self.getGlobalFilePath() != self._FILE_PATH_LOCAL_VALUE
        ):
            self._lastGlobalPath = self.getGlobalFilePath()
            self.setGlobalFilePath(self._FILE_PATH_LOCAL_VALUE)
        elif (
            self.getMode() == "global"
            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."""
        if self.getMode() == "local":
            if self.getScan() is not None:
                scan = self.getScan()
                if not isinstance(scan, HDF5TomoScan):
                    mess = qt.QMessageBox(
                        parent=self,
                        icon=qt.QMessageBox.Warning,
                        text="local mode is only available for EDF acquisitions",
                    )
                    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
        else:
            file_ = self.getGlobalFilePath()

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