QSpaceWidget.py 34 KB
Newer Older
Damien Naudet's avatar
Damien Naudet committed
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
# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2016 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.
#
# ###########################################################################*/

from __future__ import absolute_import

__authors__ = ["D. Naudet"]
__license__ = "MIT"
__date__ = "15/09/2016"

Damien Naudet's avatar
Damien Naudet committed
32
33
34

import numpy as np

35
from silx.gui import qt as Qt
36
from silx.gui.dialog.ImageFileDialog import ImageFileDialog
37

38
39
from ...io.XsocsH5 import XsocsH5

40
from ..widgets.AcqParamsWidget import AcqParamsWidget
Damien Naudet's avatar
Damien Naudet committed
41
from ..widgets.Containers import GroupBox, SubGroupBox
42
from ..widgets.Input import StyledLineEdit
43
from ...process.qspace import QSpaceConverter, qspace_conversion
44
from ...process.qspace import QSpaceCoordinates
45

46
47
48
49
_ETA_LOWER = u'\u03B7'

_DEFAULT_IMG_BIN = [1, 1]

Damien Naudet's avatar
Damien Naudet committed
50
51
_DEFAULT_MEDFILT = [3, 3]

52
53

class ConversionParamsWidget(Qt.QWidget):
Damien Naudet's avatar
Damien Naudet committed
54
55
    """
    Widget for conversion parameters input :
56
57
58
        - normalization counter
        - median filter
        - qspace dimensions
Damien Naudet's avatar
Damien Naudet committed
59
    """
Damien Naudet's avatar
Damien Naudet committed
60
61
    def __init__(self,
                 medfiltDims=None,
62
                 normalizers=None,
63
64
65
                 beamEnergy=None,
                 directBeam=None,
                 channelsPerDegree=None,
Damien Naudet's avatar
Damien Naudet committed
66
                 **kwargs):
67
68
69
70
        super(ConversionParamsWidget, self).__init__(**kwargs)
        layout = Qt.QGridLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)

Damien Naudet's avatar
Damien Naudet committed
71
72
        # medfiltDims = np.array(medfiltDims, ndmin=1)

73
74
        self.__mask = None  # Currently selected mask array

75
76
77
78
79
80
81
82
83
84
        # ################
        # parameters
        # ################

        acqParamsGbx = SubGroupBox("Acq. Parameters")
        grpLayout = Qt.QVBoxLayout(acqParamsGbx)

        self.__acqParamWid = AcqParamsWidget()
        # Set default values with provided info
        self.__acqParamWid.beam_energy = beamEnergy
85
86
87
88
        self.__acqParamWid.direct_beam_v = directBeam[0]
        self.__acqParamWid.direct_beam_h = directBeam[1]
        self.__acqParamWid.chperdeg_v = channelsPerDegree[0]
        self.__acqParamWid.chperdeg_h = channelsPerDegree[1]
89
90
91
92
        grpLayout.addWidget(self.__acqParamWid)

        self.layout().addWidget(acqParamsGbx)

93
        # ===========
Damien Naudet's avatar
Damien Naudet committed
94
        # Image pre processing
95
        # ===========
Damien Naudet's avatar
Damien Naudet committed
96
97

        imageGbox = SubGroupBox('Image processing.')
98
99
100
101
102
103
104
105
106
107
108
109
110
111
        imgGboxLayout = Qt.QFormLayout(imageGbox)

        def createCheckBox(title):
            """Create and init a QCheckBox"""
            style = Qt.QApplication.instance().style()
            size = style.pixelMetric(Qt.QStyle.PM_SmallIconSize)

            checkBox = Qt.QCheckBox(title)
            checkBox.toggled.connect(self.__checkboxToggled)
            checkBox.setIconSize(Qt.QSize(size, size))
            checkBox.setIcon(
                style.standardIcon(Qt.QStyle.SP_DialogCancelButton))
            checkBox.setChecked(False)
            return checkBox
Damien Naudet's avatar
Damien Naudet committed
112

113
114
115
116
117
        # Maxipix correction
        self.__maxipixCorrection = createCheckBox('1. Maxipix correction')

        imgGboxLayout.addRow(self.__maxipixCorrection)

118
        # Mask
119
        self.__imgMaskCBox = createCheckBox('2. Mask')
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138

        self.__maskFileLineEdit = StyledLineEdit()
        self.__maskFileLineEdit.setAlignment(Qt.Qt.AlignLeft)
        self.__maskFileLineEdit.setReadOnly(True)
        self.__imgMaskCBox.toggled.connect(self.__maskFileLineEdit.setEnabled)

        maskButton = Qt.QToolButton()
        style = Qt.QApplication.style()
        icon = style.standardIcon(Qt.QStyle.SP_DialogOpenButton)
        maskButton.setIcon(icon)
        maskButton.setEnabled(False)
        maskButton.clicked.connect(self.__maskButtonClicked)
        self.__imgMaskCBox.toggled.connect(maskButton.setEnabled)

        maskLayout = Qt.QHBoxLayout()
        maskLayout.addWidget(self.__maskFileLineEdit, 1)
        maskLayout.addWidget(maskButton)
        imgGboxLayout.addRow(self.__imgMaskCBox, maskLayout)

139
        # Normalization
140
        self.__imgNormCBox = createCheckBox('3. Normalization')
141
142
143
144
145
146
147
148
149
150

        self.__normalizationComboBox = Qt.QComboBox()
        self.__normalizationComboBox.setEnabled(False)

        self.__imgNormCBox.toggled.connect(self.__normalizationComboBox.setEnabled)

        if normalizers is not None:
            self.__normalizationComboBox.addItems(normalizers)
        imgGboxLayout.addRow(self.__imgNormCBox, self.__normalizationComboBox)

Damien Naudet's avatar
Damien Naudet committed
151
        # Median filter
152
        self.__medfiltCBox = createCheckBox('4. Median filter')
Damien Naudet's avatar
Damien Naudet committed
153
154

        inputBase = Qt.QWidget()
155
        inputBase.setEnabled(False)
Damien Naudet's avatar
Damien Naudet committed
156
157
158
159
        inputBase.setContentsMargins(0, 0, 0, 0)
        medfiltLayout = Qt.QHBoxLayout(inputBase)
        medfiltLayout.setContentsMargins(0, 0, 0, 0)

160
        self.__medfiltCBox.toggled.connect(inputBase.setEnabled)
Damien Naudet's avatar
Damien Naudet committed
161

162
163
164
165
        self.__medfiltHEdit = StyledLineEdit(nChar=5)
        self.__medfiltHEdit.setValidator(Qt.QIntValidator(self.__medfiltHEdit))
        self.__medfiltHEdit.setAlignment(Qt.Qt.AlignRight)
        # self.__medfiltHEdit.setText(str(medfiltDims[0]))
Damien Naudet's avatar
Damien Naudet committed
166

167
168
169
170
        self.__medfiltVEdit = StyledLineEdit(nChar=5)
        self.__medfiltVEdit.setValidator(Qt.QIntValidator(self.__medfiltVEdit))
        self.__medfiltVEdit.setAlignment(Qt.Qt.AlignRight)
        # self.__medfiltVEdit.setText(str(medfiltDims[1]))
Damien Naudet's avatar
Damien Naudet committed
171
172

        medfiltLayout.addWidget(Qt.QLabel('w='))
173
        medfiltLayout.addWidget(self.__medfiltHEdit)
Damien Naudet's avatar
Damien Naudet committed
174
        medfiltLayout.addWidget(Qt.QLabel('h='))
175
        medfiltLayout.addWidget(self.__medfiltVEdit)
Damien Naudet's avatar
Damien Naudet committed
176

177
178
        imgGboxLayout.addRow(self.__medfiltCBox, inputBase)

Damien Naudet's avatar
Damien Naudet committed
179
        layout.addWidget(imageGbox)
180
181

        # ===========
182
        # QSpace histogram settings
183
184
        # ===========

185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
        qspaceGbox = SubGroupBox('QSpace')
        qspaceLayout = Qt.QFormLayout(qspaceGbox)
        qspaceLayout.addRow(Qt.QLabel("Grid dimensions:"))

        self.__qDimEdits = {}  # Store line edits for each coordinate system
        self.__qDimWidgets = {}  # Store widget containing labels and line edit

        qDimLayout = Qt.QVBoxLayout()
        qDimLayout.setContentsMargins(0, 0, 0, 0)
        qDimLayout.setSpacing(0)
        qspaceLayout.addRow(qDimLayout)

        for coordinates in QSpaceCoordinates.ALLOWED:
            self.__qDimWidgets[coordinates] = Qt.QWidget()
            qspaceDimLayout = Qt.QHBoxLayout(self.__qDimWidgets[coordinates])

            self.__qDimEdits[coordinates] = (StyledLineEdit(nChar=5),
                                             StyledLineEdit(nChar=5),
                                             StyledLineEdit(nChar=5))
Damien Naudet's avatar
Damien Naudet committed
204

205
206
207
208
209
            for axis, edit in zip(QSpaceCoordinates.axesNames(coordinates),
                                  self.__qDimEdits[coordinates]):
                qspaceDimLayout.addWidget(Qt.QLabel(axis + ':'))
                qspaceDimLayout.addWidget(edit)
                qspaceDimLayout.addStretch(1)
Damien Naudet's avatar
Damien Naudet committed
210

211
            qDimLayout.addWidget(self.__qDimWidgets[coordinates])
Damien Naudet's avatar
Damien Naudet committed
212

213
214
215
        self.__coordinatesComboBox = Qt.QComboBox()
        self.__coordinatesComboBox.currentIndexChanged[int].connect(
            self.__coordinatesChanged)
Damien Naudet's avatar
Damien Naudet committed
216

217
218
219
        for coordinates in QSpaceCoordinates.ALLOWED:
            self.__coordinatesComboBox.addItem(coordinates)
        qspaceLayout.addRow("Coordinates:", self.__coordinatesComboBox)
Damien Naudet's avatar
Damien Naudet committed
220
221

        layout.addWidget(qspaceGbox)
222

Damien Naudet's avatar
Damien Naudet committed
223
224
        self.setMedfiltDims(medfiltDims)

225
226
227
        # ===========
        # size constraints
        # ===========
Damien Naudet's avatar
Damien Naudet committed
228
229
230
        # self.setSizePolicy(Qt.QSizePolicy(Qt.QSizePolicy.Fixed,
        #                                   Qt.QSizePolicy.Fixed))

231
232
233
234
235
236
    def __coordinatesChanged(self, index=None):
        """Handle change of QSpace coordinate change"""
        coordinates = self.__coordinatesComboBox.currentText()
        for coord, widget in self.__qDimWidgets.items():
            widget.setVisible(coordinates == coord)

237
238
239
240
241
242
243
244
245
246
247
    def __checkboxToggled(self, checked):
        """Update image processing check box icons"""
        style = Qt.QApplication.instance().style()
        if checked:
            icon = style.standardIcon(Qt.QStyle.SP_DialogApplyButton)
        else:
            icon = style.standardIcon(Qt.QStyle.SP_DialogCancelButton)

        checkBox = self.sender()
        checkBox.setIcon(icon)

248
249
250
251
252
253
254
    def __maskButtonClicked(self, checked):
        """On mask file dialog"""
        dialog = ImageFileDialog()
        if dialog.exec_():
            self.__maskFileLineEdit.setText(dialog.selectedUrl())
            self.__mask = dialog.selectedImage()

255
256
257
258
259
260
261
    def isMaxipixCorrectionEnabled(self):
        """Returns whether Maxipix correction is enabled or not.

        :rtype: bool
        """
        return self.__maxipixCorrection.isChecked()

262
263
264
265
266
267
268
269
    def getMask(self):
        """Returns the selected mask image or None if not set

        :rtype: Union[numpy.ndarray, None]
        """
        if not self.__imgMaskCBox.isChecked():
            return None
        else:
Thomas Vincent's avatar
Thomas Vincent committed
270
            return self.__mask
271

272
273
274
275
276
277
278
279
280
281
282
283
    def getNormalizer(self):
        """Returns the counter name to use for normalization if any.

        It returns None if normalization is disabled.

        :rtype: Union[str, None]
        """
        if not self.__imgNormCBox.isChecked():
            return None
        else:
            return self.__normalizationComboBox.currentText()

284
285
286
287
288
289
290
291
292
293
294
295
296
    def setNormalizer(self, normalizer):
        """Set the selected normalizer

        :param str normalizer:
        :return:
        """
        hasNormalizer = normalizer is not None
        if hasNormalizer:
            index = self.__normalizationComboBox.findText(normalizer)
            if index >= 0:
                self.__normalizationComboBox.setCurrentIndex(index)
            else:
                raise ValueError('%s is not a valid normalizer' % normalizer)
297
298
        self.__imgNormCBox.setChecked(hasNormalizer)
        self.__normalizationComboBox.setEnabled(hasNormalizer)
299

Damien Naudet's avatar
Damien Naudet committed
300
301
    def getMedfiltDims(self):
        """
Damien Naudet's avatar
Damien Naudet committed
302
303
        Returns the median filter dimensions, a 2 integers array, or None if
        the median filter is not enabled.
Damien Naudet's avatar
Damien Naudet committed
304
305
        :return:
        """
Damien Naudet's avatar
Damien Naudet committed
306
307
308
309

        if not self.__medfiltCBox.isChecked():
            return None

Damien Naudet's avatar
Damien Naudet committed
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
        hMedfilt = self.__medfiltHEdit.text()
        if len(hMedfilt) == 0:
            hMedfilt = None
        else:
            hMedfilt = int(hMedfilt)

        vMedfilt = self.__medfiltVEdit.text()
        if len(vMedfilt) == 0:
            vMedfilt = None
        else:
            vMedfilt = int(vMedfilt)
        return [hMedfilt, vMedfilt]

    def setMedfiltDims(self, medfiltDims):
        """
        Sets the median filter dimensions.
        :param medfiltDims: a 2 integers array.
        :return:
        """
        if medfiltDims is None:
            medfiltDims = (1, 1)
Damien Naudet's avatar
Damien Naudet committed
331
332
            medfiltDims = np.array(medfiltDims, ndmin=1)
        equal = np.array_equal(medfiltDims, [1, 1])
Damien Naudet's avatar
Damien Naudet committed
333
334
        self.__medfiltHEdit.setText(str(medfiltDims[0]))
        self.__medfiltVEdit.setText(str(medfiltDims[1]))
Damien Naudet's avatar
Damien Naudet committed
335
        self.__medfiltCBox.setChecked(not equal)
336

337
338
    def getQspaceDims(self, coordinates=None):
        """Returns the qspace dimensions, a 3 integers (> 1) array if set,
Damien Naudet's avatar
Damien Naudet committed
339
            or [None, None, None].
340
341
342
343
344

        :param Union[None,QSpaceCoordinates] coordinates:
            Either the coordinates system for which to get the value or None
            to get information for the current coordinates system
        :rtype: List[Union[int,None]]
Damien Naudet's avatar
Damien Naudet committed
345
        """
346
347
348
349
350
351
352
353
354
        if coordinates is None:
            coordinates = self.__coordinatesComboBox.currentText()

        sizes = []
        for edit in self.__qDimEdits[coordinates]:
            qsize = edit.text()
            qsize = None if len(qsize) == 0 else int(qsize)
            sizes.append(qsize)
        return sizes
355

356
    def setQSpaceDims(self, coordinates, qspaceDims):
357
358
        """Sets the qspace dimensions.

359
        :param QSpaceCoordinates coordinates:
Damien Naudet's avatar
Damien Naudet committed
360
        :param qspaceDims: A three integers array.
Damien Naudet's avatar
Damien Naudet committed
361
362
        :return:
        """
363
364
        for edit, size in zip(self.__qDimEdits[coordinates], qspaceDims):
            edit.setText(str(int(size)))
365

366
367
368
369
370
371
372
373
374
    def getBeamEnergy(self):
        """Returns beam energy in eV or None if no input"""
        return self.__acqParamWid.beam_energy

    def getDirectBeam(self):
        """Returns direct beam calibration position None if no input

        If one input is missing, it returns None.
        """
375
376
        directBeam = (self.__acqParamWid.direct_beam_v,
                      self.__acqParamWid.direct_beam_h)
377
378
379
380
381
382
383
        return None if None in directBeam else directBeam

    def getChannelsPerDegree(self):
        """Returns channels per degree calibration position None if no input

        If one input is missing, it returns None.
        """
384
385
        channelsPerDegree = (self.__acqParamWid.chperdeg_v,
                             self.__acqParamWid.chperdeg_h)
386
387
        return None if None in channelsPerDegree else channelsPerDegree

388
389
390
391
392
393
394
    def getCoordinates(self):
        """Returns the coordinates system to use.

        :rtype: QSpaceCoordinates
        """
        return self.__coordinatesComboBox.currentText()

395

396
class QSpaceWidget(Qt.QDialog):
Damien Naudet's avatar
WIP    
Damien Naudet committed
397
398
399
400
401
    sigProcessDone = Qt.Signal(object)

    (StatusUnknown, StatusInit,
     StatusRunning, StatusCompleted,
     StatusAborted, StatusCanceled) = StatusList = range(6)
402
403
404
405

    __sigConvertDone = Qt.Signal()

    def __init__(self,
406
407
408
                 xsocH5File,
                 outQSpaceH5,
                 qspaceDims=None,
Damien Naudet's avatar
Damien Naudet committed
409
                 medfiltDims=None,
Damien Naudet's avatar
Damien Naudet committed
410
                 roi=None,
411
                 entries=None,
Damien Naudet's avatar
Damien Naudet committed
412
                 shiftH5File=None,
413
                 normalizer=None,
414
                 **kwargs):
Damien Naudet's avatar
Damien Naudet committed
415
416
417
        """
        Widgets displaying informations about data to be converted to QSpace,
            and allowing the user to input some parameters.
418
419
420
        :param xsocH5File: name of the input XsocsH5 file.
        :param outQSpaceH5: name of the output hdf5 file
        :param qspaceDims: dimensions of the qspace volume
Damien Naudet's avatar
Damien Naudet committed
421
        :param medfiltDims: dimensions of the kernel used when applying a
422
            a median filter to the images (after the mask, if any).
Damien Naudet's avatar
Damien Naudet committed
423
            Set to None or (1, 1) to disable the median filter.
424
425
426
        :param roi: Roi in sample coordinates (xMin, xMax, yMin, yMax)
        :param entries: a list of entry names to convert to qspace. If None,
            all entries found in the xsocsH5File will be used.
Damien Naudet's avatar
Damien Naudet committed
427
        :param shiftH5File: name of a ShiftH5 file to use.
428
429
        :param Union[str, None] normalizer:
            Name of the dataset in measurement to use for normalization.
Damien Naudet's avatar
Damien Naudet committed
430
431
        :param kwargs:
        """
432
        super(QSpaceWidget, self).__init__(**kwargs)
Damien Naudet's avatar
Damien Naudet committed
433

434
        self.__status = QSpaceWidget.StatusInit
435

436
437
438
439
440
441
442
443
444
445
446
447
448
449
        xsocsH5 = XsocsH5(xsocH5File)

        # checking entries
        if entries is None:
            entries = xsocsH5.entries()
        elif len(entries) == 0:
            raise ValueError('At least one entry must be selected.')
        else:
            diff = set(entries) - set(xsocsH5.entries())
            if len(diff) > 0:
                raise ValueError('The following entries were not found in '
                                 'the input file :\n - {0}'
                                 ''.format('\n -'.join(diff)))

Damien Naudet's avatar
Damien Naudet committed
450
451
452
453
454
455
456
        self.__converter = QSpaceConverter(xsocH5File,
                                           output_f=outQSpaceH5,
                                           qspace_dims=qspaceDims,
                                           medfilt_dims=medfiltDims,
                                           roi=roi,
                                           entries=entries,
                                           shiftH5_f=shiftH5File)
457
        self.__converter.normalizer = normalizer
Damien Naudet's avatar
Damien Naudet committed
458

Damien Naudet's avatar
Damien Naudet committed
459
        self.__params = {'roi': roi,
460
461
                         'xsocsH5_f': xsocH5File,
                         'qspaceH5_f': outQSpaceH5}
462

463
        topLayout = Qt.QGridLayout(self)
464

465
466
        # ATTENTION : this is done to allow the stretch
        # of the QTableWidget containing the scans info
Damien Naudet's avatar
Damien Naudet committed
467
        topLayout.setColumnStretch(1, 1)
468
469
470
471
472

        # ################
        # input QGroupBox
        # ################

473
474
475
        inputGbx = GroupBox("Input")
        layout = Qt.QHBoxLayout(inputGbx)
        topLayout.addWidget(inputGbx,
Damien Naudet's avatar
Damien Naudet committed
476
477
                            0, 0,
                            1, 2)
478
479

        # data HDF5 file input
480
481
482
        lab = Qt.QLabel('XsocsH5 file :')
        xsocsFileEdit = StyledLineEdit(nChar=50, readOnly=True)
        xsocsFileEdit.setText(xsocH5File)
483
484
485
        layout.addWidget(lab,
                         stretch=0,
                         alignment=Qt.Qt.AlignLeft)
486
        layout.addWidget(xsocsFileEdit,
487
488
489
490
491
492
493
                         stretch=0,
                         alignment=Qt.Qt.AlignLeft)
        layout.addStretch()

        # ################
        # Scans
        # ################
494
495
        scansGbx = GroupBox("Scans")
        topLayout.addWidget(scansGbx, 1, 1, 2, 1)
Damien Naudet's avatar
Damien Naudet committed
496
        topLayout.setRowStretch(2, 1000)
497

498
499
500
        grpLayout = Qt.QVBoxLayout(scansGbx)
        infoLayout = Qt.QGridLayout()
        grpLayout.addLayout(infoLayout)
Damien Naudet's avatar
Damien Naudet committed
501
        #
502

503
        line = 0
Damien Naudet's avatar
Damien Naudet committed
504

Thomas Vincent's avatar
Thomas Vincent committed
505
        style = Qt.QApplication.instance().style()
Damien Naudet's avatar
Damien Naudet committed
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
        shiftLayout = Qt.QHBoxLayout()
        if shiftH5File is not None:
            shiftText = 'Shift applied.'
            icon = Qt.QStyle.SP_MessageBoxWarning
        else:
            shiftText = 'No shift applied.'
            icon = Qt.QStyle.SP_MessageBoxInformation
        shiftLabel = Qt.QLabel(shiftText)
        size = style.pixelMetric(Qt.QStyle.PM_ButtonIconSize)
        shiftIcon = Qt.QLabel()
        icon = style.standardIcon(icon)
        shiftIcon.setPixmap(icon.pixmap(size))
        shiftLayout.addWidget(shiftIcon)
        shiftLayout.addWidget(shiftLabel)
        infoLayout.addLayout(shiftLayout, line, 0)

        line = 1
523
        label = Qt.QLabel('# Roi :')
524
525
526
527
528
529
530
531
532
533
534
        self.__roiXMinEdit = xMinText = StyledLineEdit(nChar=5, readOnly=True)
        self.__roiXMaxEdit = xMaxText = StyledLineEdit(nChar=5, readOnly=True)
        self.__roiYMinEdit = yMinText = StyledLineEdit(nChar=5, readOnly=True)
        self.__roiYMaxEdit = yMaxText = StyledLineEdit(nChar=5, readOnly=True)
        roiLayout = Qt.QHBoxLayout()
        roiLayout.addWidget(xMinText)
        roiLayout.addWidget(xMaxText)
        roiLayout.addWidget(yMinText)
        roiLayout.addWidget(yMaxText)
        infoLayout.addWidget(label, line, 0)
        infoLayout.addLayout(roiLayout, line, 1, alignment=Qt.Qt.AlignLeft)
535
536
537

        line += 1
        label = Qt.QLabel('# points :')
538
        self.__nImgLabel = nImgLabel = StyledLineEdit(nChar=16, readOnly=True)
539
        nImgLayout = Qt.QHBoxLayout()
540
541
542
        infoLayout.addWidget(label, line, 0)
        infoLayout.addLayout(nImgLayout, line, 1, alignment=Qt.Qt.AlignLeft)
        nImgLayout.addWidget(nImgLabel)
543
544
545
        nImgLayout.addWidget(Qt.QLabel(' (roi / total)'))

        line += 1
546
        label = Qt.QLabel(u'# {0} :'.format(_ETA_LOWER))
547
548
549
550
551
        self.__nAnglesLabel = nAnglesLabel = StyledLineEdit(nChar=5,
                                                            readOnly=True)
        infoLayout.addWidget(label, line, 0)
        infoLayout.addWidget(nAnglesLabel, line, 1, alignment=Qt.Qt.AlignLeft)
        infoLayout.setColumnStretch(2, 1)
552

553
554
555
        self.__scansTable = scansTable = Qt.QTableWidget(0, 2)
        scansTable.verticalHeader().hide()
        grpLayout.addWidget(scansTable, alignment=Qt.Qt.AlignLeft)
556
557
558
559
560

        # ################
        # conversion params
        # ################

561
562
563
        convGbx = GroupBox("Conversion parameters")
        grpLayout = Qt.QVBoxLayout(convGbx)
        topLayout.addWidget(convGbx, 1, 0, alignment=Qt.Qt.AlignTop)
564

565
566
567
568
569
570
571
572
573
        if entries:  # Get default config from first entry
            beamEnergy = xsocsH5.beam_energy(entries[0])
            directBeam = xsocsH5.direct_beam(entries[0])
            channelsPerDegree = xsocsH5.chan_per_deg(entries[0])
        else:  # This should not happen
            beamEnergy = ''
            directBeam = '', ''
            channelsPerDegree = '', ''

574
        self.__paramsWid = ConversionParamsWidget(
575
576
            medfiltDims=self.__converter.medfilt_dims,
            normalizers=xsocsH5.normalizers(),
577
578
579
            beamEnergy=beamEnergy,
            directBeam=directBeam,
            channelsPerDegree=channelsPerDegree)
580
581
        self.__paramsWid.setNormalizer(normalizer)
        grpLayout.addWidget(self.__paramsWid)
582
583
584
585
586

        # ################
        # output
        # ################

587
588
589
590
591
592
        outputGbx = GroupBox("Output")
        layout = Qt.QHBoxLayout(outputGbx)
        topLayout.addWidget(outputGbx, 3, 0, 1, 2)
        lab = Qt.QLabel('Output :')
        outputFileEdit = StyledLineEdit(nChar=50, readOnly=True)
        outputFileEdit.setText(outQSpaceH5)
593
594
595
        layout.addWidget(lab,
                         stretch=0,
                         alignment=Qt.Qt.AlignLeft)
596
        layout.addWidget(outputFileEdit,
597
598
599
600
601
602
603
604
                         stretch=0,
                         alignment=Qt.Qt.AlignLeft)
        layout.addStretch()

        # ################
        # buttons
        # ################

605
606
607
608
        self.__converBn = convertBn = Qt.QPushButton('Convert')
        cancelBn = Qt.QPushButton('Cancel')
        hLayout = Qt.QHBoxLayout()
        topLayout.addLayout(hLayout, 4, 0, 1, 2,
Damien Naudet's avatar
Damien Naudet committed
609
                            Qt.Qt.AlignHCenter | Qt.Qt.AlignTop)
610
611
        hLayout.addWidget(convertBn)
        hLayout.addWidget(cancelBn)
612
613
614
615
616

        # #################
        # setting initial state
        # #################

617
618
619
620
621
        cancelBn.clicked.connect(self.close)
        convertBn.clicked.connect(self.__slotConvertBnClicked)

        self.__fillScansInfos()

622
623
        # Set default qspace bin size
        if entries:
624

625
626
627
628
629
630
631
632
633
634
635
636
637
            # Get angles from all entries
            phi = []
            eta = []
            nu = []
            delta = []
            for entry in entries:
                phi.append(xsocsH5.positioner(entry, 'phi'))
                eta.append(xsocsH5.positioner(entry, 'eta'))
                nu.append(xsocsH5.positioner(entry, 'nu'))
                delta.append(xsocsH5.positioner(entry, 'del'))

            entry = entries[0]  # Get default config from first entry

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
666
667
668
669
            for coordinates in QSpaceCoordinates.ALLOWED:
                # Compute Qspace conversion
                q_array = qspace_conversion(
                    img_size=xsocsH5.image_size(entry),
                    center_chan=xsocsH5.direct_beam(entry),
                    chan_per_deg=xsocsH5.chan_per_deg(entry),
                    beam_energy=xsocsH5.beam_energy(entry),
                    phi=phi,
                    eta=eta,
                    nu=nu,
                    delta=delta,
                    coordinates=coordinates)

                if coordinates == QSpaceCoordinates.CARTESIAN:
                    # Estimate bin numbers based on smallest distance between q vectors
                    maxbins = []
                    for dim in (q_array[..., 0], q_array[..., 1], q_array[..., 2]):
                        maxbin = np.inf
                        for j in range(dim.ndim):
                            step = abs(np.diff(dim, axis=j)).max()
                            bins = (abs(dim).max() - abs(dim).min()) / step
                            maxbin = min(maxbin, bins)
                        maxbins.append(int(maxbin) + 1)
                else:
                    # TODO estimate bins for spherical coordinates
                    maxbins = [0, 0, 0]

                self.__paramsWid.setQSpaceDims(coordinates, maxbins)

        else:  # Set to 0 by default
            for coordinates in QSpaceCoordinates.ALLOWED:
                self.__paramsWid.setQSpaceDims(coordinates, [0, 0, 0])
670

671
    def __slotConvertBnClicked(self):
Damien Naudet's avatar
Damien Naudet committed
672
673
674
675
676
        """
        Slot called when the convert button is clicked. Does some checks
        then starts the conversion if all is OK.
        :return:
        """
677
678
679
680
681
682
683
684
685
686
687
688
        converter = self.__converter
        if converter is None:
            # shouldn't be here
            raise RuntimeError('Converter not found.')
        elif converter.is_running():
            # this part shouldn't even be called, just putting this
            # in case someone decides to modify the code to enable the
            # convert_bn even tho conditions are not met.
            Qt.QMessageBox.critical(self, 'Error',
                                    'A conversion is already in progress!')
            return

689
        output_file = converter.output_f
690
691
692
693
694
695

        if len(output_file) == 0:
            Qt.QMessageBox.critical(self, 'Error',
                                    'Output file field is mandatory.')
            return

696
        normalizer = self.__paramsWid.getNormalizer()
Damien Naudet's avatar
Damien Naudet committed
697
698
        medfiltDims = self.__paramsWid.getMedfiltDims()
        qspaceDims = self.__paramsWid.getQspaceDims()
699
700
701
        beamEnergy = self.__paramsWid.getBeamEnergy()
        directBeam = self.__paramsWid.getDirectBeam()
        channelsPerDegree = self.__paramsWid.getChannelsPerDegree()
702
        mask = self.__paramsWid.getMask()
703
        maxipixCorrection = self.__paramsWid.isMaxipixCorrectionEnabled()
704
        coordinates = self.__paramsWid.getCoordinates()
705
706

        try:
707
            converter.normalizer = normalizer
Damien Naudet's avatar
Damien Naudet committed
708
709
            converter.medfilt_dims = medfiltDims
            converter.qspace_dims = qspaceDims
710
711
712
            converter.beam_energy = beamEnergy
            converter.direct_beam = directBeam
            converter.channels_per_degree = channelsPerDegree
713
            converter.mask = mask
714
            converter.maxipix_correction = maxipixCorrection
715
            converter.coordinates = coordinates
716
717
718
719
720
        except ValueError as ex:
            Qt.QMessageBox.critical(self, 'Error',
                                    str(ex))
            return

721
722
723
724
725
726
        errors = converter.check_parameters()
        if errors:
            msg = 'Invalid parameters.\n{0}'.format('\n'.join(errors))
            Qt.QMessageBox.critical(self, 'Error', msg)
            return

727
728
729
730
731
732
733
734
735
736
737
        if len(converter.check_overwrite()):
            ans = Qt.QMessageBox.warning(self,
                                         'Overwrite?',
                                         ('The output file already exists.'
                                          '\nDo you want to overwrite it?'),
                                         buttons=Qt.QMessageBox.Yes |
                                         Qt.QMessageBox.No)
            if ans == Qt.QMessageBox.No:
                return

        self.__converter = converter
738
        procDialog = _ConversionProcessDialog(converter, parent=self)
739
740
        procDialog.accepted.connect(self.__slotConvertDone)
        procDialog.rejected.connect(self.__slotConvertDone)
Damien Naudet's avatar
WIP    
Damien Naudet committed
741
        self._setStatus(self.StatusRunning)
742
743
744
745
746
747
748
        rc = procDialog.exec_()

        if rc == Qt.QDialog.Accepted:
            self.__slotConvertDone()
        procDialog.deleteLater()

    def __slotConvertDone(self):
Damien Naudet's avatar
Damien Naudet committed
749
750
751
752
        """
        Method called when the conversion has been completed succesfuly.
        :return:
        """
753
754
755
        converter = self.__converter
        if not converter:
            return
756

757
758
759
760
761
762
        self.__qspaceH5 = None
        status = converter.status

        if status == QSpaceConverter.DONE:
            self.__qspaceH5 = converter.results
            self._setStatus(self.StatusCompleted)
Damien Naudet's avatar
Damien Naudet committed
763
            self.hide()
764
765
766
            self.sigProcessDone.emit(self.__qspaceH5)
        elif status == QSpaceConverter.CANCELED:
            self._setStatus(self.StatusAborted)
767
        else:
768
            self._setStatus(self.StatusUnknown)
769
770

    qspaceH5 = property(lambda self: self.__qspaceH5)
Damien Naudet's avatar
Damien Naudet committed
771
    """ Written file (set when the conversion was succesful, None otherwise. """
772

Damien Naudet's avatar
WIP    
Damien Naudet committed
773
    status = property(lambda self: self.__status)
Damien Naudet's avatar
Damien Naudet committed
774
    """ Status of the widget. """
Damien Naudet's avatar
WIP    
Damien Naudet committed
775
776

    def _setStatus(self, status):
Damien Naudet's avatar
Damien Naudet committed
777
778
779
780
781
        """
        Sets the status of the widget.
        :param status:
        :return:
        """
782
        if status not in QSpaceWidget.StatusList:
Damien Naudet's avatar
WIP    
Damien Naudet committed
783
784
785
786
            raise ValueError('Unknown status value : {0}.'
                             ''.format(status))
        self.__status = status

787
788
789
790
791
792
793
794
795
    def __fillScansInfos(self):
        """
        Fills the QTableWidget with info found in the input file
        """
        converter = self.__converter
        if converter is None:
            return

        scans = converter.scans
796
797
        scansTable = self.__scansTable
        scansTable.setRowCount(len(scans))
798
799
800
801
        for row, scan in enumerate(scans):
            params = converter.scan_params(scan)
            item = Qt.QTableWidgetItem(scan)
            item.setFlags(item.flags() ^ Qt.Qt.ItemIsEditable)
802
            scansTable.setItem(row, 0, item)
803
804
            item = Qt.QTableWidgetItem(str(params['angle']))
            item.setFlags(item.flags() ^ Qt.Qt.ItemIsEditable)
805
            scansTable.setItem(row, 1, item)
806

807
808
809
810
        scansTable.resizeColumnsToContents()
        width = (sum([scansTable.columnWidth(i)
                     for i in range(scansTable.columnCount())]) +
                 scansTable.verticalHeader().width() +
811
812
813
814
                 20)
        # TODO : the size is wrong when the
        # verticalScrollBar isnt displayed yet
        # scans_table.verticalScrollBar().width())
815
        size = scansTable.minimumSize()
816
        size.setWidth(width)
817
        scansTable.setMinimumSize(size)
818

819
        # TODO : warning if the ROI is empty (too small to contain images)
820
        params = converter.scan_params(scans[0])
Damien Naudet's avatar
Damien Naudet committed
821
822
        roi = converter.roi
        if roi is None:
823
824
            xMin = xMax = yMin = yMax = 'ns'
        else:
Damien Naudet's avatar
Damien Naudet committed
825
            xMin, xMax, yMin, yMax = roi
826

827
828
829
830
        self.__roiXMinEdit.setText(str(xMin))
        self.__roiXMaxEdit.setText(str(xMax))
        self.__roiYMinEdit.setText(str(yMin))
        self.__roiYMaxEdit.setText(str(yMax))
831

832
        indices = converter.sample_indices
833
834
        nImgTxt = '{0} / {1}'.format(len(indices),
                                     params['n_images'])
835
        self.__nImgLabel.setText(nImgTxt)
836
837
838

        nEntries = len(XsocsH5(self.__params['xsocsH5_f']).entries())
        self.__nAnglesLabel.setText('{0} / {1}'.format(len(scans), nEntries))
839
840
841
842
843
844
845
846


class _ConversionProcessDialog(Qt.QDialog):
    __sigConvertDone = Qt.Signal()

    def __init__(self, converter,
                 parent=None,
                 **kwargs):
Damien Naudet's avatar
Damien Naudet committed
847
848
849
850
851
852
853
        """
        Simple widget displaying a progress bar and a info label during the
            conversion process.
        :param converter:
        :param parent:
        :param kwargs:
        """
854
855
856
857
858
859
860
861
862
863
        super(_ConversionProcessDialog, self).__init__(parent)
        layout = Qt.QVBoxLayout(self)

        progress_bar = Qt.QProgressBar()
        layout.addWidget(progress_bar)
        status_lab = Qt.QLabel('<font color="blue">Conversion '
                               'in progress</font>')
        status_lab.setFrameStyle(Qt.QFrame.Panel | Qt.QFrame.Sunken)
        layout.addWidget(status_lab)

864
        bn_box = Qt.QDialogButtonBox(Qt.QDialogButtonBox.Abort)
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
        layout.addWidget(bn_box)
        bn_box.accepted.connect(self.accept)
        bn_box.rejected.connect(self.__onAbort)

        self.__sigConvertDone.connect(self.__convertDone)

        self.__bn_box = bn_box
        self.__progress_bar = progress_bar
        self.__status_lab = status_lab
        self.__converter = converter
        self.__aborted = False

        self.__qtimer = Qt.QTimer()
        self.__qtimer.timeout.connect(self.__onProgress)

        converter.convert(blocking=False,
                          overwrite=True,
                          callback=self.__sigConvertDone.emit,
                          **kwargs)
Damien Naudet's avatar
Damien Naudet committed
884

885
886
887
        self.__qtimer.start(1000)

    def __onAbort(self):
Damien Naudet's avatar
Damien Naudet committed
888
889
890
891
        """
        Slot called when the abort button is clicked.
        :return:
        """
892
        self.__status_lab.setText('<font color="orange">Cancelling...</font>')
893
        self.__bn_box.button(Qt.QDialogButtonBox.Abort).setEnabled(False)
894
895
896
897
        self.__converter.abort(wait=False)
        self.__aborted = True

    def __onProgress(self):
Damien Naudet's avatar
Damien Naudet committed
898
899
900
901
        """
        Slot called when the progress timer timeouts.
        :return:
        """
902
903
904
905
        progress = self.__converter.progress()
        self.__progress_bar.setValue(progress)

    def __convertDone(self):
Damien Naudet's avatar
Damien Naudet committed
906
907
908
909
910
        """
        Callback called when the conversion is done (whether its successful or
        not).
        :return:
        """
911
912
913
        self.__qtimer.stop()
        self.__qtimer = None
        self.__onProgress()
914
        abortBn = self.__bn_box.button(Qt.QDialogButtonBox.Abort)
Damien Naudet's avatar
Damien Naudet committed
915
916
917
918

        converter = self.__converter

        if converter.status == QSpaceConverter.CANCELED:
919
            self.__bn_box.rejected.disconnect(self.__onAbort)
920
921
            self.__status_lab.setText('<font color="red">Conversion '
                                      'cancelled.</font>')
922
923
924
            abortBn.setText('Close')
            self.__bn_box.rejected.connect(self.reject)
            abortBn.setEnabled(True)
Damien Naudet's avatar
Damien Naudet committed
925
926
927
928
929
930
        elif converter.status == QSpaceConverter.ERROR:
            self.__bn_box.removeButton(abortBn)
            okBn = self.__bn_box.addButton(Qt.QDialogButtonBox.Ok)
            self.__status_lab.setText('<font color="red">Error : {0}.</font>'
                                      ''.format(converter.status_msg))
            okBn.setText('Close')
931
        else:
932
933
            self.__bn_box.removeButton(abortBn)
            okBn = self.__bn_box.addButton(Qt.QDialogButtonBox.Ok)
934
935
            self.__status_lab.setText('<font color="green">Conversion '
                                      'done.</font>')
936
            okBn.setText('Close')
937

938
    status = property(lambda self: 0 if self.__aborted else 1)
Damien Naudet's avatar
Damien Naudet committed
939
    """ Status of the process. """