コード例 #1
0
ファイル: comparison.py プロジェクト: weskerfoot/sherloq
class ComparisonWidget(ToolWidget):
    def __init__(self, filename, image, parent=None):
        super(ComparisonWidget, self).__init__(parent)

        load_button = QPushButton(self.tr('Load reference image...'))
        self.comp_label = QLabel(self.tr('Comparison:'))
        self.normal_radio = QRadioButton(self.tr('Normal'))
        self.normal_radio.setToolTip(self.tr('Show reference (raw pixels)'))
        self.normal_radio.setChecked(True)
        self.difference_radio = QRadioButton(self.tr('Difference'))
        self.difference_radio.setToolTip(
            self.tr('Show evidence/reference difference'))
        self.ssim_radio = QRadioButton(self.tr('SSIM Map'))
        self.ssim_radio.setToolTip(self.tr('Structure similarity quality map'))
        self.butter_radio = QRadioButton(self.tr('Butteraugli'))
        self.butter_radio.setToolTip(
            self.tr('Butteraugli spatial changes heatmap'))
        self.gray_check = QCheckBox(self.tr('Grayscale'))
        self.gray_check.setToolTip(self.tr('Show desaturated output'))
        self.equalize_check = QCheckBox(self.tr('Equalized'))
        self.equalize_check.setToolTip(self.tr('Apply histogram equalization'))
        self.last_radio = self.normal_radio
        self.metric_button = QPushButton(self.tr('Compute metrics'))
        self.metric_button.setToolTip(
            self.tr('Image quality assessment metrics'))

        self.evidence = image
        self.reference = self.difference = self.ssim_map = self.butter_map = None
        basename = os.path.basename(filename)
        self.evidence_viewer = ImageViewer(
            self.evidence, None, self.tr('Evidence: {}'.format(basename)))
        self.reference_viewer = ImageViewer(np.full_like(self.evidence, 127),
                                            None, self.tr('Reference'))

        self.table_widget = QTableWidget(21, 3)
        self.table_widget.setHorizontalHeaderLabels(
            [self.tr('Metric'),
             self.tr('Value'),
             self.tr('Better')])
        self.table_widget.setItem(0, 0, QTableWidgetItem(self.tr('RMSE')))
        self.table_widget.setItem(
            0, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)'))
        self.table_widget.item(0, 0).setToolTip(
            self.
            tr('Root Mean Square Error (RMSE) is commonly used to compare \n'
               'the difference between the reference and evidence images \n'
               'by directly computing the variation in pixel values. \n'
               'The combined image is close to the reference image when \n'
               'RMSE value is zero. RMSE is a good indicator of the spectral \n'
               'quality of the reference image.'))
        self.table_widget.setItem(1, 0, QTableWidgetItem(self.tr('SAM')))
        self.table_widget.setItem(
            1, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)'))
        self.table_widget.item(1, 0).setToolTip(
            self.
            tr('It computes the spectral angle between the pixel, vector of the \n'
               'evidence image and reference image. It is worked out in either \n'
               'degrees or radians. It is performed on a pixel-by-pixel base. \n'
               'A SAM equal to zero denotes the absence of spectral distortion.'
               ))
        self.table_widget.setItem(2, 0, QTableWidgetItem(self.tr('ERGAS')))
        self.table_widget.setItem(
            2, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)'))
        self.table_widget.item(2, 0).setToolTip(
            self.
            tr('It is used to compute the quality of reference image in terms \n'
               'of normalized average error of each band of the reference image. \n'
               'Increase in the value of ERGAS indicates distortion in the \n'
               'reference image, lower value of ERGAS indicates that it is \n'
               'similar to the reference image.'))
        self.table_widget.setItem(3, 0, QTableWidgetItem(self.tr('MB')))
        self.table_widget.setItem(
            3, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)'))
        self.table_widget.item(3, 0).setToolTip(
            self.
            tr('Mean Bias is the difference between the mean of the evidence \n'
               'image and reference image. The ideal value is zero and indicates \n'
               'that the evidence and reference images are similar. Mean value \n'
               'refers to the grey level of pixels in an image.'))
        self.table_widget.setItem(4, 0, QTableWidgetItem(self.tr('PFE')))
        self.table_widget.setItem(
            4, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)'))
        self.table_widget.item(4, 0).setToolTip(
            self.
            tr('It computes the norm of the difference between the corresponding \n'
               'pixels of the reference and fused image to the norm of the reference \n'
               'image. When the calculated value is zero, it indicates that both the \n'
               'reference and fused images are similar and value will be increased \n'
               'when the merged image is not similar to the reference image.'))
        self.table_widget.setItem(5, 0, QTableWidgetItem(self.tr('PSNR')))
        self.table_widget.setItem(
            5, 2,
            QTableWidgetItem(QIcon('gui/icons/high.svg'),
                             '(+' + u'\u221e' + ')'))
        self.table_widget.item(5, 0).setToolTip(
            self.
            tr('It is widely used metric it is computed by the number of gray levels \n'
               'in the image divided by the corresponding pixels in the evidence and \n'
               'the reference images. When the value is high, both images are similar.'
               ))
        self.table_widget.setItem(6, 0, QTableWidgetItem(self.tr('PSNR-B')))
        self.table_widget.setItem(
            6, 2,
            QTableWidgetItem(QIcon('gui/icons/high.svg'),
                             '(+' + u'\u221e' + ')'))
        self.table_widget.item(6, 0).setToolTip(
            self.tr('PSNR with Blocking Effect Factor.'))
        self.table_widget.setItem(7, 0, QTableWidgetItem(self.tr('SSIM')))
        self.table_widget.setItem(
            7, 2, QTableWidgetItem(QIcon('gui/icons/high.svg'), '(1)'))
        self.table_widget.item(7, 0).setToolTip(
            self.
            tr('SSIM is used to compare the local patterns of pixel intensities between \n'
               ' the reference and fused images. The range varies between -1 to 1. \n'
               'The value 1 indicates the reference and fused images are similar.'
               ))
        self.table_widget.setItem(8, 0, QTableWidgetItem(self.tr('MS-SSIM')))
        self.table_widget.setItem(
            8, 2, QTableWidgetItem(QIcon('gui/icons/high.svg'), '(1)'))
        self.table_widget.item(8, 0).setToolTip(
            self.tr('Multiscale version of SSIM.'))
        self.table_widget.setItem(9, 0, QTableWidgetItem(self.tr('RASE')))
        self.table_widget.setItem(
            9, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)'))
        self.table_widget.item(9, 0).setToolTip(
            self.tr('Relative average spectral error'))
        self.table_widget.setItem(10, 0, QTableWidgetItem(self.tr('SCC')))
        self.table_widget.setItem(
            10, 2, QTableWidgetItem(QIcon('gui/icons/high.svg'), '(1)'))
        self.table_widget.item(10, 0).setToolTip(
            self.tr('Spatial Correlation Coefficient'))
        self.table_widget.setItem(11, 0, QTableWidgetItem(self.tr('UQI')))
        self.table_widget.setItem(
            11, 2, QTableWidgetItem(QIcon('gui/icons/high.svg'), '(1)'))
        self.table_widget.item(11, 0).setToolTip(
            self.tr('Universal Image Quality Index'))
        self.table_widget.setItem(12, 0, QTableWidgetItem(self.tr('VIF-P')))
        self.table_widget.setItem(
            12, 2, QTableWidgetItem(QIcon('gui/icons/high.svg'), '(1)'))
        self.table_widget.item(12, 0).setToolTip(
            self.tr('Pixel-based Visual Information Fidelity'))
        self.table_widget.setItem(13, 0,
                                  QTableWidgetItem(self.tr('SSIMulacra')))
        self.table_widget.setItem(
            13, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)'))
        self.table_widget.item(13, 0).setToolTip(
            self.tr('Structural SIMilarity Unveiling Local '
                    'And Compression Related Artifacts'))
        self.table_widget.setItem(14, 0,
                                  QTableWidgetItem(self.tr('Butteraugli')))
        self.table_widget.setItem(
            14, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)'))
        self.table_widget.item(14, 0).setToolTip(
            self.tr('Estimate psychovisual error'))
        self.table_widget.setItem(15, 0,
                                  QTableWidgetItem(self.tr('Correlation')))
        self.table_widget.setItem(
            15, 2, QTableWidgetItem(QIcon('gui/icons/high.svg'), '(1)'))
        self.table_widget.item(15,
                               0).setToolTip(self.tr('Histogram correlation'))
        self.table_widget.setItem(16, 0,
                                  QTableWidgetItem(self.tr('Chi-Square')))
        self.table_widget.setItem(
            16, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)'))
        self.table_widget.item(16,
                               0).setToolTip(self.tr('Histogram Chi-Square'))
        self.table_widget.setItem(17, 0,
                                  QTableWidgetItem(self.tr('Chi-Square 2')))
        self.table_widget.setItem(
            17, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)'))
        self.table_widget.item(17,
                               0).setToolTip(self.tr('Alternative Chi-Square'))
        self.table_widget.setItem(18, 0,
                                  QTableWidgetItem(self.tr('Intersection')))
        self.table_widget.setItem(
            18, 2,
            QTableWidgetItem(QIcon('gui/icons/high.svg'),
                             '(+' + u'\u221e' + ')'))
        self.table_widget.item(18,
                               0).setToolTip(self.tr('Histogram intersection'))
        self.table_widget.setItem(19, 0,
                                  QTableWidgetItem(self.tr('Hellinger')))
        self.table_widget.setItem(
            19, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)'))
        self.table_widget.item(19, 0).setToolTip(
            self.tr('Histogram Hellinger distance'))
        self.table_widget.setItem(20, 0,
                                  QTableWidgetItem(self.tr('Divergence')))
        self.table_widget.setItem(
            20, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)'))
        self.table_widget.item(20, 0).setToolTip(
            self.tr('Kullback-Leibler divergence'))

        for i in range(self.table_widget.rowCount()):
            modify_font(self.table_widget.item(i, 0), bold=True)
        self.table_widget.setSelectionMode(QAbstractItemView.SingleSelection)
        self.table_widget.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.table_widget.resizeColumnsToContents()
        self.table_widget.setMaximumWidth(250)
        self.table_widget.setAlternatingRowColors(True)
        self.stopped = False

        self.comp_label.setEnabled(False)
        self.normal_radio.setEnabled(False)
        self.difference_radio.setEnabled(False)
        self.ssim_radio.setEnabled(False)
        self.butter_radio.setEnabled(False)
        self.gray_check.setEnabled(False)
        self.equalize_check.setEnabled(False)
        self.metric_button.setEnabled(False)
        self.table_widget.setEnabled(False)

        load_button.clicked.connect(self.load)
        self.normal_radio.clicked.connect(self.change)
        self.difference_radio.clicked.connect(self.change)
        self.butter_radio.clicked.connect(self.change)
        self.gray_check.stateChanged.connect(self.change)
        self.equalize_check.stateChanged.connect(self.change)
        self.ssim_radio.clicked.connect(self.change)
        self.evidence_viewer.viewChanged.connect(
            self.reference_viewer.changeView)
        self.reference_viewer.viewChanged.connect(
            self.evidence_viewer.changeView)
        self.metric_button.clicked.connect(self.metrics)

        top_layout = QHBoxLayout()
        top_layout.addWidget(load_button)
        top_layout.addStretch()
        top_layout.addWidget(self.comp_label)
        top_layout.addWidget(self.normal_radio)
        top_layout.addWidget(self.difference_radio)
        top_layout.addWidget(self.ssim_radio)
        top_layout.addWidget(self.butter_radio)
        top_layout.addWidget(self.gray_check)
        top_layout.addWidget(self.equalize_check)

        metric_layout = QVBoxLayout()
        index_label = QLabel(self.tr('Image Quality Assessment'))
        index_label.setAlignment(Qt.AlignCenter)
        modify_font(index_label, bold=True)
        metric_layout.addWidget(index_label)
        metric_layout.addWidget(self.table_widget)
        metric_layout.addWidget(self.metric_button)

        center_layout = QHBoxLayout()
        center_layout.addWidget(self.evidence_viewer)
        center_layout.addWidget(self.reference_viewer)
        center_layout.addLayout(metric_layout)

        main_layout = QVBoxLayout()
        main_layout.addLayout(top_layout)
        main_layout.addLayout(center_layout)
        self.setLayout(main_layout)

    def load(self):
        filename, basename, reference = load_image(self)
        if filename is None:
            return
        if reference.shape != self.evidence.shape:
            QMessageBox.critical(
                self, self.tr('Error'),
                self.tr('Evidence and reference must have the same size!'))
            return
        self.reference = reference
        self.reference_viewer.set_title(
            self.tr('Reference: {}'.format(basename)))
        self.difference = norm_mat(cv.absdiff(self.evidence, self.reference))

        self.comp_label.setEnabled(True)
        self.normal_radio.setEnabled(True)
        self.difference_radio.setEnabled(True)
        self.ssim_radio.setEnabled(False)
        self.butter_radio.setEnabled(False)
        self.gray_check.setEnabled(True)
        self.equalize_check.setEnabled(True)
        self.metric_button.setEnabled(True)
        for i in range(self.table_widget.rowCount()):
            self.table_widget.setItem(i, 1, QTableWidgetItem())
        self.normal_radio.setChecked(True)
        self.table_widget.setEnabled(False)
        self.change()

    def change(self):
        if self.normal_radio.isChecked():
            result = self.reference
            self.gray_check.setEnabled(False)
            self.equalize_check.setEnabled(False)
            self.last_radio = self.normal_radio
        elif self.difference_radio.isChecked():
            result = self.difference
            self.gray_check.setEnabled(True)
            self.equalize_check.setEnabled(True)
            self.last_radio = self.difference_radio
        elif self.ssim_radio.isChecked():
            result = self.ssim_map
            self.gray_check.setEnabled(False)
            self.equalize_check.setEnabled(True)
            self.last_radio = self.ssim_radio
        elif self.butter_radio.isChecked():
            result = self.butter_map
            self.gray_check.setEnabled(True)
            self.equalize_check.setEnabled(False)
            self.last_radio = self.butter_radio
        else:
            self.last_radio.setChecked(True)
            return
        if self.equalize_check.isChecked():
            result = equalize_img(result)
        if self.gray_check.isChecked():
            result = desaturate(result)
        self.reference_viewer.update_original(result)

    def metrics(self):
        progress = QProgressDialog(self.tr('Computing metrics...'),
                                   self.tr('Cancel'), 1,
                                   self.table_widget.rowCount(), self)
        progress.canceled.connect(self.cancel)
        progress.setWindowModality(Qt.WindowModal)
        img1 = cv.cvtColor(self.evidence, cv.COLOR_BGR2GRAY)
        img2 = cv.cvtColor(self.reference, cv.COLOR_BGR2GRAY)
        x = img1.astype(np.float64)
        y = img2.astype(np.float64)

        rmse = self.rmse(x, y)
        progress.setValue(1)
        if self.stopped:
            return
        sam = sewar.sam(img1, img2)
        progress.setValue(2)
        if self.stopped:
            return
        ergas = sewar.ergas(img1, img2)
        progress.setValue(3)
        if self.stopped:
            return
        mb = self.mb(x, y)
        progress.setValue(4)
        if self.stopped:
            return
        pfe = self.pfe(x, y)
        progress.setValue(5)
        if self.stopped:
            return
        psnr = self.psnr(x, y)
        progress.setValue(6)
        if self.stopped:
            return
        try:
            psnrb = sewar.psnrb(img1, img2)
        except NameError:
            # FIXME: C'\`e un bug in psnrb (https://github.com/andrewekhalel/sewar/issues/17)
            psnrb = 0
        progress.setValue(7)
        if self.stopped:
            return
        ssim, self.ssim_map = self.ssim(x, y)
        progress.setValue(8)
        if self.stopped:
            return
        mssim = sewar.msssim(img1, img2).real
        progress.setValue(9)
        if self.stopped:
            return
        rase = sewar.rase(img1, img2)
        progress.setValue(10)
        if self.stopped:
            return
        scc = sewar.scc(img1, img2)
        progress.setValue(11)
        if self.stopped:
            return
        uqi = sewar.uqi(img1, img2)
        progress.setValue(12)
        if self.stopped:
            return
        vifp = sewar.vifp(img1, img2)
        progress.setValue(13)
        if self.stopped:
            return
        ssimul = self.ssimul(img1, img2)
        progress.setValue(14)
        if self.stopped:
            return
        butter, self.butter_map = self.butter(img1, img2)
        progress.setValue(15)
        if self.stopped:
            return

        sizes = [256, 256, 256]
        ranges = [0, 256] * 3
        channels = [0, 1, 2]
        hist1 = cv.calcHist([self.evidence], channels, None, sizes, ranges)
        hist2 = cv.calcHist([self.reference], channels, None, sizes, ranges)
        correlation = cv.compareHist(hist1, hist2, cv.HISTCMP_CORREL)
        progress.setValue(16)
        if self.stopped:
            return
        chi_square = cv.compareHist(hist1, hist2, cv.HISTCMP_CHISQR)
        progress.setValue(17)
        if self.stopped:
            return
        chi_square2 = cv.compareHist(hist1, hist2, cv.HISTCMP_CHISQR_ALT)
        progress.setValue(18)
        if self.stopped:
            return
        intersection = cv.compareHist(hist1, hist2, cv.HISTCMP_INTERSECT)
        progress.setValue(19)
        if self.stopped:
            return
        hellinger = cv.compareHist(hist1, hist2, cv.HISTCMP_HELLINGER)
        progress.setValue(20)
        if self.stopped:
            return
        divergence = cv.compareHist(hist1, hist2, cv.HISTCMP_KL_DIV)
        progress.setValue(21)

        self.table_widget.setItem(0, 1,
                                  QTableWidgetItem('{:.2f}'.format(rmse)))
        self.table_widget.setItem(1, 1, QTableWidgetItem('{:.4f}'.format(sam)))
        self.table_widget.setItem(2, 1,
                                  QTableWidgetItem('{:.2f}'.format(ergas)))
        self.table_widget.setItem(3, 1, QTableWidgetItem('{:.4f}'.format(mb)))
        self.table_widget.setItem(4, 1, QTableWidgetItem('{:.2f}'.format(pfe)))
        if psnr > 0:
            self.table_widget.setItem(
                5, 1, QTableWidgetItem('{:.2f} dB'.format(psnr)))
        else:
            self.table_widget.setItem(
                5, 1, QTableWidgetItem('+' + u'\u221e' + ' dB'))
        self.table_widget.setItem(6, 1,
                                  QTableWidgetItem('{:.2f}'.format(psnrb)))
        self.table_widget.setItem(7, 1,
                                  QTableWidgetItem('{:.4f}'.format(ssim)))
        self.table_widget.setItem(8, 1,
                                  QTableWidgetItem('{:.4f}'.format(mssim)))
        self.table_widget.setItem(9, 1,
                                  QTableWidgetItem('{:.2f}'.format(rase)))
        self.table_widget.setItem(10, 1,
                                  QTableWidgetItem('{:.4f}'.format(scc)))
        self.table_widget.setItem(11, 1,
                                  QTableWidgetItem('{:.4f}'.format(uqi)))
        self.table_widget.setItem(12, 1,
                                  QTableWidgetItem('{:.4f}'.format(vifp)))
        self.table_widget.setItem(13, 1,
                                  QTableWidgetItem('{:.4f}'.format(ssimul)))
        self.table_widget.setItem(14, 1,
                                  QTableWidgetItem('{:.2f}'.format(butter)))
        self.table_widget.setItem(
            15, 1, QTableWidgetItem('{:.2f}'.format(correlation)))
        self.table_widget.setItem(
            16, 1, QTableWidgetItem('{:.2f}'.format(chi_square)))
        self.table_widget.setItem(
            17, 1, QTableWidgetItem('{:.2f}'.format(chi_square2)))
        self.table_widget.setItem(
            18, 1, QTableWidgetItem('{:.2f}'.format(intersection)))
        self.table_widget.setItem(19, 1,
                                  QTableWidgetItem('{:.2f}'.format(hellinger)))
        self.table_widget.setItem(
            20, 1, QTableWidgetItem('{:.2f}'.format(divergence)))
        self.table_widget.resizeColumnsToContents()
        self.table_widget.setEnabled(True)
        self.metric_button.setEnabled(False)
        self.ssim_radio.setEnabled(True)
        self.butter_radio.setEnabled(True)

    def cancel(self):
        self.stopped = True

    @staticmethod
    def rmse(x, y):
        return np.sqrt(np.mean(np.square(x - y)))

    @staticmethod
    def mb(x, y):
        mx = np.mean(x)
        my = np.mean(y)
        return (mx - my) / mx

    @staticmethod
    def pfe(x, y):
        return np.linalg.norm(x - y) / np.linalg.norm(x) * 100

    @staticmethod
    def ssim(x, y):
        c1 = 6.5025
        c2 = 58.5225
        k = (11, 11)
        s = 1.5
        x2 = x**2
        y2 = y**2
        xy = x * y
        mu_x = cv.GaussianBlur(x, k, s)
        mu_y = cv.GaussianBlur(y, k, s)
        mu_x2 = mu_x**2
        mu_y2 = mu_y**2
        mu_xy = mu_x * mu_y
        s_x2 = cv.GaussianBlur(x2, k, s) - mu_x2
        s_y2 = cv.GaussianBlur(y2, k, s) - mu_y2
        s_xy = cv.GaussianBlur(xy, k, s) - mu_xy
        t1 = 2 * mu_xy + c1
        t2 = 2 * s_xy + c2
        t3 = t1 * t2
        t1 = mu_x2 + mu_y2 + c1
        t2 = s_x2 + s_y2 + c2
        t1 *= t2
        ssim_map = cv.divide(t3, t1)
        ssim = cv.mean(ssim_map)[0]
        return ssim, 255 - norm_mat(ssim_map, to_bgr=True)

    @staticmethod
    def corr(x, y):
        return np.corrcoef(x, y)[0, 1]

    @staticmethod
    def psnr(x, y):
        k = np.mean(np.square(x - y))
        if k == 0:
            return -1
        return 20 * math.log10((255**2) / k)

    @staticmethod
    def butter(x, y):
        try:
            exe = butter_exe()
            if exe is None:
                raise FileNotFoundError
            temp_dir = QTemporaryDir()
            if temp_dir.isValid():
                filename1 = os.path.join(temp_dir.path(), 'img1.png')
                cv.imwrite(filename1, x)
                filename2 = os.path.join(temp_dir.path(), 'img2.png')
                cv.imwrite(filename2, y)
                filename3 = os.path.join(temp_dir.path(), 'map.ppm')
                p = run([exe, filename1, filename2, filename3], stdout=PIPE)
                value = float(p.stdout)
                heatmap = cv.imread(filename3, cv.IMREAD_COLOR)
                return value, heatmap
        except FileNotFoundError:
            return -1, cv.cvtColor(np.full_like(x, 127), cv.COLOR_GRAY2BGR)

    @staticmethod
    def ssimul(x, y):
        try:
            exe = ssimul_exe()
            if exe is None:
                raise FileNotFoundError
            temp_dir = QTemporaryDir()
            if temp_dir.isValid():
                filename1 = os.path.join(temp_dir.path(), 'img1.png')
                cv.imwrite(filename1, x)
                filename2 = os.path.join(temp_dir.path(), 'img2.png')
                cv.imwrite(filename2, y)
                p = run([exe, filename1, filename2], stdout=PIPE)
                value = float(p.stdout)
                return value
        except FileNotFoundError:
            return -1
コード例 #2
0
class LCAResultsTab(NewAnalysisTab):
    """Class for the 'LCA Results' sub-tab.

    This tab allows the user to get a basic overview of the results of the calculation setup.

    Shows:
        'Overview' and 'by LCIA method' options for different plots/graphs
        Plots/graphs
        Export buttons
    """
    def __init__(self, parent=None):
        super().__init__(parent)
        self.parent = parent
        self.lca_scores_widget = LCAScoresTab(parent)
        self.lca_overview_widget = LCIAResultsTab(parent)

        self.layout.setAlignment(QtCore.Qt.AlignTop)
        self.layout.addLayout(get_header_layout('LCA Results'))

        # buttons
        button_layout = QHBoxLayout()
        self.button_group = QButtonGroup()
        self.button_overview = QRadioButton("Overview")
        self.button_overview.setToolTip(
            "Show a matrix of all functional units and all impact categories")
        button_layout.addWidget(self.button_overview)
        self.button_by_method = QRadioButton("by LCIA method")
        self.button_by_method.setToolTip(
            "Show the impacts of each functional unit for the selected impact categories"
        )
        self.button_by_method.setChecked(True)
        self.scenario_label = QLabel("Scenario:")
        self.button_group.addButton(self.button_overview, 0)
        self.button_group.addButton(self.button_by_method, 1)
        button_layout.addWidget(self.button_by_method)
        button_layout.addWidget(self.scenario_label)
        button_layout.addWidget(self.scenario_box)
        button_layout.addStretch(1)
        self.layout.addLayout(button_layout)

        self.layout.addWidget(self.lca_scores_widget)
        self.layout.addWidget(self.lca_overview_widget)

        self.button_clicked(False)
        self.connect_signals()

    def connect_signals(self):
        self.button_overview.toggled.connect(self.button_clicked)
        if self.using_presamples:
            self.scenario_box.currentIndexChanged.connect(
                self.parent.update_scenario_data)
            self.parent.update_scenario_box_index.connect(
                lambda index: self.set_combobox_index(self.scenario_box, index
                                                      ))
            self.button_by_method.toggled.connect(
                lambda on_lcia: self.scenario_box.setHidden(on_lcia))
            self.button_by_method.toggled.connect(
                lambda on_lcia: self.scenario_label.setHidden(on_lcia))

    @QtCore.Slot(bool, name="overviewToggled")
    def button_clicked(self, is_overview: bool):
        self.lca_overview_widget.setVisible(is_overview)
        self.lca_scores_widget.setHidden(is_overview)

    def configure_scenario(self):
        """Allow scenarios options to be visible when used."""
        super().configure_scenario()
        self.scenario_box.setHidden(self.button_by_method.isChecked())
        self.scenario_label.setHidden(self.button_by_method.isChecked())

    def update_tab(self):
        """Update the tab."""
        self.lca_scores_widget.update_tab()
        self.lca_overview_widget.update_plot()
        self.lca_overview_widget.update_table()
コード例 #3
0
class PcaWidget(ToolWidget):
    def __init__(self, image, parent=None):
        super(PcaWidget, self).__init__(parent)

        self.component_combo = QComboBox()
        self.component_combo.addItems([self.tr(f"#{i + 1}") for i in range(3)])
        self.distance_radio = QRadioButton(self.tr("Distance"))
        self.distance_radio.setToolTip(self.tr("Distance from the closest point on selected component"))
        self.project_radio = QRadioButton(self.tr("Projection"))
        self.project_radio.setToolTip(self.tr("Projection onto the selected principal component"))
        self.crossprod_radio = QRadioButton(self.tr("Cross product"))
        self.crossprod_radio.setToolTip(self.tr("Cross product between input and selected component"))
        self.distance_radio.setChecked(True)
        self.last_radio = self.distance_radio
        self.invert_check = QCheckBox(self.tr("Invert"))
        self.invert_check.setToolTip(self.tr("Output bitwise complement"))
        self.equalize_check = QCheckBox(self.tr("Equalize"))
        self.equalize_check.setToolTip(self.tr("Apply histogram equalization"))

        rows, cols, chans = image.shape
        x = np.reshape(image, (rows * cols, chans)).astype(np.float32)
        mu, ev, ew = cv.PCACompute2(x, np.array([]))
        p = np.reshape(cv.PCAProject(x, mu, ev), (rows, cols, chans))
        x0 = image.astype(np.float32) - mu
        self.output = []
        for i, v in enumerate(ev):
            cross = np.cross(x0, v)
            distance = np.linalg.norm(cross, axis=2) / np.linalg.norm(v)
            project = p[:, :, i]
            self.output.extend([norm_mat(distance, to_bgr=True), norm_mat(project, to_bgr=True), norm_img(cross)])

        table_data = [
            [mu[0, 2], mu[0, 1], mu[0, 0]],
            [ev[0, 2], ev[0, 1], ev[0, 0]],
            [ev[1, 2], ev[1, 1], ev[1, 0]],
            [ev[2, 2], ev[2, 1], ev[2, 0]],
            [ew[2, 0], ew[1, 0], ew[0, 0]],
        ]
        table_widget = QTableWidget(5, 4)
        table_widget.setHorizontalHeaderLabels([self.tr("Element"), self.tr("Red"), self.tr("Green"), self.tr("Blue")])
        table_widget.setItem(0, 0, QTableWidgetItem(self.tr("Mean vector")))
        table_widget.setItem(1, 0, QTableWidgetItem(self.tr("Eigenvector 1")))
        table_widget.setItem(2, 0, QTableWidgetItem(self.tr("Eigenvector 2")))
        table_widget.setItem(3, 0, QTableWidgetItem(self.tr("Eigenvector 3")))
        table_widget.setItem(4, 0, QTableWidgetItem(self.tr("Eigenvalues")))
        for i in range(len(table_data)):
            modify_font(table_widget.item(i, 0), bold=True)
            for j in range(len(table_data[i])):
                table_widget.setItem(i, j + 1, QTableWidgetItem(str(table_data[i][j])))
        # item = QTableWidgetItem()
        # item.setBackgroundColor(QColor(mu[0, 2], mu[0, 1], mu[0, 0]))
        # table_widget.setItem(0, 4, item)
        # table_widget.resizeRowsToContents()
        # table_widget.resizeColumnsToContents()
        table_widget.setEditTriggers(QAbstractItemView.NoEditTriggers)
        table_widget.setSelectionMode(QAbstractItemView.SingleSelection)
        table_widget.setMaximumHeight(190)

        self.viewer = ImageViewer(image, image, None)
        self.process()

        self.component_combo.currentIndexChanged.connect(self.process)
        self.distance_radio.clicked.connect(self.process)
        self.project_radio.clicked.connect(self.process)
        self.crossprod_radio.clicked.connect(self.process)
        self.invert_check.stateChanged.connect(self.process)
        self.equalize_check.stateChanged.connect(self.process)

        top_layout = QHBoxLayout()
        top_layout.addWidget(QLabel(self.tr("Component:")))
        top_layout.addWidget(self.component_combo)
        top_layout.addWidget(QLabel(self.tr("Mode:")))
        top_layout.addWidget(self.distance_radio)
        top_layout.addWidget(self.project_radio)
        top_layout.addWidget(self.crossprod_radio)
        top_layout.addWidget(self.invert_check)
        top_layout.addWidget(self.equalize_check)
        top_layout.addStretch()
        bottom_layout = QHBoxLayout()
        bottom_layout.addWidget(table_widget)

        main_layout = QVBoxLayout()
        main_layout.addLayout(top_layout)
        main_layout.addWidget(self.viewer)
        main_layout.addLayout(bottom_layout)
        self.setLayout(main_layout)

    def process(self):
        index = 3 * self.component_combo.currentIndex()
        if self.distance_radio.isChecked():
            output = self.output[index]
            self.last_radio = self.distance_radio
        elif self.project_radio.isChecked():
            output = self.output[index + 1]
            self.last_radio = self.project_radio
        elif self.crossprod_radio.isChecked():
            output = self.output[index + 2]
            self.last_radio = self.crossprod_radio
        else:
            self.last_radio.setChecked(True)
            return
        if self.invert_check.isChecked():
            output = cv.bitwise_not(output)
        if self.equalize_check.isChecked():
            output = equalize_img(output)
        self.viewer.update_processed(output)
コード例 #4
0
ファイル: magnifier.py プロジェクト: nsidere/sherloq
class MagnifierWidget(ToolWidget):
    def __init__(self, image, parent=None):
        super(MagnifierWidget, self).__init__(parent)

        self.equalize_radio = QRadioButton(self.tr("Equalization"))
        self.equalize_radio.setToolTip(self.tr("RGB histogram equalization"))
        self.contrast_radio = QRadioButton(self.tr("Auto Contrast"))
        self.contrast_radio.setToolTip(self.tr("Compress luminance tonality"))
        self.centile_spin = QSpinBox()
        self.centile_spin.setRange(0, 100)
        self.centile_spin.setValue(20)
        self.centile_spin.setSuffix(self.tr(" %"))
        self.centile_spin.setToolTip(self.tr("Histogram percentile amount"))
        self.channel_check = QCheckBox(self.tr("By channel"))
        self.channel_check.setToolTip(self.tr("Independent RGB compression"))
        self.equalize_radio.setChecked(True)
        self.last_radio = self.equalize_radio

        self.image = image
        self.viewer = ImageViewer(self.image, self.image)
        self.change()

        self.viewer.viewChanged.connect(self.process)
        self.equalize_radio.clicked.connect(self.change)
        self.contrast_radio.clicked.connect(self.change)
        self.centile_spin.valueChanged.connect(self.change)
        self.channel_check.stateChanged.connect(self.change)

        top_layout = QHBoxLayout()
        top_layout.addWidget(QLabel(self.tr("Mode:")))
        top_layout.addWidget(self.equalize_radio)
        top_layout.addWidget(self.contrast_radio)
        top_layout.addWidget(self.centile_spin)
        top_layout.addWidget(self.channel_check)
        top_layout.addStretch()

        main_layout = QVBoxLayout()
        main_layout.addLayout(top_layout)
        main_layout.addWidget(self.viewer)
        self.setLayout(main_layout)

    def process(self, rect):
        y1 = rect.top()
        y2 = rect.bottom()
        x1 = rect.left()
        x2 = rect.right()
        roi = self.image[y1:y2, x1:x2]
        if self.equalize_radio.isChecked():
            self.centile_spin.setEnabled(False)
            self.channel_check.setEnabled(False)
            roi = equalize_img(roi)
            self.last_radio = self.equalize_radio
        elif self.contrast_radio.isChecked():
            self.centile_spin.setEnabled(True)
            self.channel_check.setEnabled(True)
            centile = self.centile_spin.value() / 200
            if self.channel_check.isChecked():
                roi = cv.merge(
                    [cv.LUT(c, auto_lut(c, centile)) for c in cv.split(roi)])
            else:
                roi = cv.LUT(
                    roi, auto_lut(cv.cvtColor(roi, cv.COLOR_BGR2GRAY),
                                  centile))
            self.last_radio = self.contrast_radio
        else:
            self.last_radio.setChecked(True)
            return
        processed = np.copy(self.image)
        processed[y1:y2, x1:x2] = roi
        self.viewer.update_processed(processed)

    def change(self):
        self.process(self.viewer.get_rect())
コード例 #5
0
ファイル: stereogram.py プロジェクト: nsidere/sherloq
class StereoWidget(ToolWidget):
    def __init__(self, image, parent=None):
        super(StereoWidget, self).__init__(parent)

        gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
        small = cv.resize(gray, None, None, 1, 0.5)
        start = 10
        end = small.shape[1] // 3
        diff = np.fromiter([
            cv.mean(cv.absdiff(small[:, i:], small[:, :-i]))[0]
            for i in range(start, end)
        ], np.float32)
        _, maximum, _, argmax = cv.minMaxLoc(np.ediff1d(diff))
        if maximum < 2:
            error_label = QLabel(self.tr("Unable to detect stereogram!"))
            modify_font(error_label, bold=True)
            error_label.setStyleSheet("color: #FF0000")
            error_label.setAlignment(Qt.AlignCenter)
            main_layout = QVBoxLayout()
            main_layout.addWidget(error_label)
            self.setLayout(main_layout)
            return

        offset = argmax[1] + start
        a = image[:, offset:]
        b = image[:, :-offset]
        self.pattern = norm_img(cv.absdiff(a, b))
        temp = cv.cvtColor(self.pattern, cv.COLOR_BGR2GRAY)
        thr, _ = cv.threshold(temp, 0, 255, cv.THRESH_TRIANGLE)
        self.silhouette = cv.medianBlur(
            gray_to_bgr(cv.threshold(temp, thr, 255, cv.THRESH_BINARY)[1]), 3)
        a = cv.cvtColor(a, cv.COLOR_BGR2GRAY)
        b = cv.cvtColor(b, cv.COLOR_BGR2GRAY)
        flow = cv.calcOpticalFlowFarneback(a, b, None, 0.5, 5, 15, 5, 5, 1.2,
                                           cv.OPTFLOW_FARNEBACK_GAUSSIAN)[:, :,
                                                                          0]
        self.depth = gray_to_bgr(norm_mat(flow))
        flow = np.repeat(cv.normalize(flow, None, 0, 1,
                                      cv.NORM_MINMAX)[:, :, np.newaxis],
                         3,
                         axis=2)
        self.shaded = cv.normalize(
            self.pattern.astype(np.float32) * flow, None, 0, 255,
            cv.NORM_MINMAX).astype(np.uint8)
        self.viewer = ImageViewer(self.pattern, None, export=True)

        self.pattern_radio = QRadioButton(self.tr("Pattern"))
        self.pattern_radio.setChecked(True)
        self.pattern_radio.setToolTip(
            self.tr("Difference between raw and aligned image"))
        self.silhouette_radio = QRadioButton(self.tr("Silhouette"))
        self.silhouette_radio.setToolTip(
            self.tr("Apply threshold to discovered pattern"))
        self.depth_radio = QRadioButton(self.tr("Depth"))
        self.depth_radio.setToolTip(
            self.tr("Estimate 3D depth using optical flow"))
        self.shaded_radio = QRadioButton(self.tr("Shaded"))
        self.shaded_radio.setToolTip(
            self.tr("Combine pattern and depth information"))

        self.silhouette_radio.clicked.connect(self.process)
        self.pattern_radio.clicked.connect(self.process)
        self.depth_radio.clicked.connect(self.process)
        self.shaded_radio.clicked.connect(self.process)

        top_layout = QHBoxLayout()
        top_layout.addWidget(QLabel(self.tr("Mode:")))
        top_layout.addWidget(self.pattern_radio)
        top_layout.addWidget(self.silhouette_radio)
        top_layout.addWidget(self.depth_radio)
        top_layout.addWidget(self.shaded_radio)
        top_layout.addStretch()
        main_layout = QVBoxLayout()
        main_layout.addLayout(top_layout)
        main_layout.addWidget(self.viewer)
        self.setLayout(main_layout)

    def process(self):
        if self.pattern_radio.isChecked():
            output = self.pattern
        elif self.silhouette_radio.isChecked():
            output = self.silhouette
        elif self.depth_radio.isChecked():
            output = self.depth
        elif self.shaded_radio.isChecked():
            output = self.shaded
        else:
            return
        self.viewer.update_original(output)
コード例 #6
0
class SCOUTS(QMainWindow):
    """Main Window Widget for SCOUTS."""
    style = {
        'title': 'QLabel {font-size: 18pt; font-weight: 600}',
        'header': 'QLabel {font-size: 12pt; font-weight: 520}',
        'label': 'QLabel {font-size: 10pt}',
        'button': 'QPushButton {font-size: 10pt}',
        'md button': 'QPushButton {font-size: 12pt}',
        'run button': 'QPushButton {font-size: 18pt; font-weight: 600}',
        'line edit': 'QLineEdit {font-size: 10pt}',
        'checkbox': 'QCheckBox {font-size: 10pt}',
        'radio button': 'QRadioButton {font-size: 10pt}'
    }

    def __init__(self) -> None:
        """SCOUTS Constructor. Defines all aspects of the GUI."""

        # ###
        # ### Main Window setup
        # ###

        # Inherits from QMainWindow
        super().__init__()
        self.rootdir = get_project_root()
        self.threadpool = QThreadPool()
        # Sets values for QMainWindow
        self.setWindowTitle("SCOUTS")
        self.setWindowIcon(
            QIcon(
                os.path.abspath(os.path.join(self.rootdir, 'src',
                                             'scouts.ico'))))
        # Creates StackedWidget as QMainWindow's central widget
        self.stacked_pages = QStackedWidget(self)
        self.setCentralWidget(self.stacked_pages)
        # Creates Widgets for individual "pages" and adds them to the StackedWidget
        self.main_page = QWidget()
        self.samples_page = QWidget()
        self.gating_page = QWidget()
        self.pages = (self.main_page, self.samples_page, self.gating_page)
        for page in self.pages:
            self.stacked_pages.addWidget(page)
        # ## Sets widget at program startup
        self.stacked_pages.setCurrentWidget(self.main_page)

        # ###
        # ### MAIN PAGE
        # ###

        # Main page layout
        self.main_layout = QVBoxLayout(self.main_page)

        # Title section
        # Title
        self.title = QLabel(self.main_page)
        self.title.setText('SCOUTS - Single Cell Outlier Selector')
        self.title.setStyleSheet(self.style['title'])
        self.title.adjustSize()
        self.main_layout.addWidget(self.title)

        # ## Input section
        # Input header
        self.input_header = QLabel(self.main_page)
        self.input_header.setText('Input settings')
        self.input_header.setStyleSheet(self.style['header'])
        self.main_layout.addChildWidget(self.input_header)
        self.input_header.adjustSize()
        self.main_layout.addWidget(self.input_header)
        # Input frame
        self.input_frame = QFrame(self.main_page)
        self.input_frame.setFrameShape(QFrame.StyledPanel)
        self.input_frame.setLayout(QFormLayout())
        self.main_layout.addWidget(self.input_frame)
        # Input button
        self.input_button = QPushButton(self.main_page)
        self.input_button.setStyleSheet(self.style['button'])
        self.set_icon(self.input_button, 'x-office-spreadsheet')
        self.input_button.setObjectName('input')
        self.input_button.setText(' Select input file (.xlsx or .csv)')
        self.input_button.clicked.connect(self.get_path)
        # Input path box
        self.input_path = QLineEdit(self.main_page)
        self.input_path.setObjectName('input_path')
        self.input_path.setStyleSheet(self.style['line edit'])
        # Go to sample naming page
        self.samples_button = QPushButton(self.main_page)
        self.samples_button.setStyleSheet(self.style['button'])
        self.set_icon(self.samples_button, 'preferences-other')
        self.samples_button.setText(' Name samples...')
        self.samples_button.clicked.connect(self.goto_samples_page)
        # Go to gating page
        self.gates_button = QPushButton(self.main_page)
        self.gates_button.setStyleSheet(self.style['button'])
        self.set_icon(self.gates_button, 'preferences-other')
        self.gates_button.setText(' Gating && outlier options...')
        self.gates_button.clicked.connect(self.goto_gates_page)
        # Add widgets above to input frame Layout
        self.input_frame.layout().addRow(self.input_button, self.input_path)
        self.input_frame.layout().addRow(self.samples_button)
        self.input_frame.layout().addRow(self.gates_button)

        # ## Analysis section
        # Analysis header
        self.analysis_header = QLabel(self.main_page)
        self.analysis_header.setText('Analysis settings')
        self.analysis_header.setStyleSheet(self.style['header'])
        self.analysis_header.adjustSize()
        self.main_layout.addWidget(self.analysis_header)
        # Analysis frame
        self.analysis_frame = QFrame(self.main_page)
        self.analysis_frame.setFrameShape(QFrame.StyledPanel)
        self.analysis_frame.setLayout(QVBoxLayout())
        self.main_layout.addWidget(self.analysis_frame)
        # Cutoff text
        self.cutoff_text = QLabel(self.main_page)
        self.cutoff_text.setText('Type of outlier to select:')
        self.cutoff_text.setToolTip(
            'Choose whether to select outliers using the cutoff value from a reference\n'
            'sample (OutR) or by using the cutoff value calculated for each sample\n'
            'individually (OutS)')
        self.cutoff_text.setStyleSheet(self.style['label'])
        # Cutoff button group
        self.cutoff_group = QButtonGroup(self)
        # Cutoff by sample
        self.cutoff_sample = QRadioButton(self.main_page)
        self.cutoff_sample.setText('OutS')
        self.cutoff_sample.setObjectName('sample')
        self.cutoff_sample.setStyleSheet(self.style['radio button'])
        self.cutoff_sample.setChecked(True)
        self.cutoff_group.addButton(self.cutoff_sample)
        # Cutoff by reference
        self.cutoff_reference = QRadioButton(self.main_page)
        self.cutoff_reference.setText('OutR')
        self.cutoff_reference.setObjectName('ref')
        self.cutoff_reference.setStyleSheet(self.style['radio button'])
        self.cutoff_group.addButton(self.cutoff_reference)
        # Both cutoffs
        self.cutoff_both = QRadioButton(self.main_page)
        self.cutoff_both.setText('both')
        self.cutoff_both.setObjectName('sample ref')
        self.cutoff_both.setStyleSheet(self.style['radio button'])
        self.cutoff_group.addButton(self.cutoff_both)
        # Markers text
        self.markers_text = QLabel(self.main_page)
        self.markers_text.setStyleSheet(self.style['label'])
        self.markers_text.setText('Show results for:')
        self.markers_text.setToolTip(
            'Individual markers: for each marker, select outliers\n'
            'Any marker: select cells that are outliers for AT LEAST one marker'
        )
        # Markers button group
        self.markers_group = QButtonGroup(self)
        # Single marker
        self.single_marker = QRadioButton(self.main_page)
        self.single_marker.setText('individual markers')
        self.single_marker.setObjectName('single')
        self.single_marker.setStyleSheet(self.style['radio button'])
        self.single_marker.setChecked(True)
        self.markers_group.addButton(self.single_marker)
        # Any marker
        self.any_marker = QRadioButton(self.main_page)
        self.any_marker.setText('any marker')
        self.any_marker.setObjectName('any')
        self.any_marker.setStyleSheet(self.style['radio button'])
        self.markers_group.addButton(self.any_marker)
        # Both methods
        self.both_methods = QRadioButton(self.main_page)
        self.both_methods.setText('both')
        self.both_methods.setObjectName('single any')
        self.both_methods.setStyleSheet(self.style['radio button'])
        self.markers_group.addButton(self.both_methods)
        # Tukey text
        self.tukey_text = QLabel(self.main_page)
        self.tukey_text.setStyleSheet(self.style['label'])
        # Tukey button group
        self.tukey_text.setText('Tukey factor:')
        self.tukey_group = QButtonGroup(self)
        # Low Tukey value
        self.tukey_low = QRadioButton(self.main_page)
        self.tukey_low.setText('1.5')
        self.tukey_low.setStyleSheet(self.style['radio button'])
        self.tukey_low.setChecked(True)
        self.tukey_group.addButton(self.tukey_low)
        # High Tukey value
        self.tukey_high = QRadioButton(self.main_page)
        self.tukey_high.setText('3.0')
        self.tukey_high.setStyleSheet(self.style['radio button'])
        self.tukey_group.addButton(self.tukey_high)
        # Add widgets above to analysis frame layout
        self.analysis_frame.layout().addWidget(self.cutoff_text)
        self.cutoff_buttons = QHBoxLayout()
        for button in self.cutoff_group.buttons():
            self.cutoff_buttons.addWidget(button)
        self.analysis_frame.layout().addLayout(self.cutoff_buttons)
        self.analysis_frame.layout().addWidget(self.markers_text)
        self.markers_buttons = QHBoxLayout()
        for button in self.markers_group.buttons():
            self.markers_buttons.addWidget(button)
        self.analysis_frame.layout().addLayout(self.markers_buttons)
        self.analysis_frame.layout().addWidget(self.tukey_text)
        self.tukey_buttons = QHBoxLayout()
        for button in self.tukey_group.buttons():
            self.tukey_buttons.addWidget(button)
        self.tukey_buttons.addWidget(QLabel())  # aligns row with 2 buttons
        self.analysis_frame.layout().addLayout(self.tukey_buttons)

        # ## Output section
        # Output header
        self.output_header = QLabel(self.main_page)
        self.output_header.setText('Output settings')
        self.output_header.setStyleSheet(self.style['header'])
        self.output_header.adjustSize()
        self.main_layout.addWidget(self.output_header)
        # Output frame
        self.output_frame = QFrame(self.main_page)
        self.output_frame.setFrameShape(QFrame.StyledPanel)
        self.output_frame.setLayout(QFormLayout())
        self.main_layout.addWidget(self.output_frame)
        # Output button
        self.output_button = QPushButton(self.main_page)
        self.output_button.setStyleSheet(self.style['button'])
        self.set_icon(self.output_button, 'folder')
        self.output_button.setObjectName('output')
        self.output_button.setText(' Select output folder')
        self.output_button.clicked.connect(self.get_path)
        # Output path box
        self.output_path = QLineEdit(self.main_page)
        self.output_path.setStyleSheet(self.style['line edit'])
        # Generate CSV checkbox
        self.output_csv = QCheckBox(self.main_page)
        self.output_csv.setText('Export multiple text files (.csv)')
        self.output_csv.setStyleSheet(self.style['checkbox'])
        self.output_csv.setChecked(True)
        # Generate XLSX checkbox
        self.output_excel = QCheckBox(self.main_page)
        self.output_excel.setText('Export multiple Excel spreadsheets (.xlsx)')
        self.output_excel.setStyleSheet(self.style['checkbox'])
        self.output_excel.clicked.connect(self.enable_single_excel)
        # Generate single, large XLSX checkbox
        self.single_excel = QCheckBox(self.main_page)
        self.single_excel.setText(
            'Also save one multi-sheet Excel spreadsheet')
        self.single_excel.setToolTip(
            'After generating all Excel spreadsheets, SCOUTS combines them into '
            'a single\nExcel spreadsheet where each sheet corresponds to an output'
            'file from SCOUTS')
        self.single_excel.setStyleSheet(self.style['checkbox'])
        self.single_excel.setEnabled(False)
        self.single_excel.clicked.connect(self.memory_warning)
        # Add widgets above to output frame layout
        self.output_frame.layout().addRow(self.output_button, self.output_path)
        self.output_frame.layout().addRow(self.output_csv)
        self.output_frame.layout().addRow(self.output_excel)
        self.output_frame.layout().addRow(self.single_excel)

        # ## Run & help-quit section
        # Run button (stand-alone)
        self.run_button = QPushButton(self.main_page)
        self.set_icon(self.run_button, 'system-run')
        self.run_button.setText(' Run!')
        self.run_button.setStyleSheet(self.style['run button'])
        self.main_layout.addWidget(self.run_button)
        self.run_button.clicked.connect(self.run)
        # Help-quit frame (invisible)
        self.helpquit_frame = QFrame(self.main_page)
        self.helpquit_frame.setLayout(QHBoxLayout())
        self.helpquit_frame.layout().setMargin(0)
        self.main_layout.addWidget(self.helpquit_frame)
        # Help button
        self.help_button = QPushButton(self.main_page)
        self.set_icon(self.help_button, 'help-about')
        self.help_button.setText(' Help')
        self.help_button.setStyleSheet(self.style['md button'])
        self.help_button.clicked.connect(self.get_help)
        # Quit button
        self.quit_button = QPushButton(self.main_page)
        self.set_icon(self.quit_button, 'process-stop')
        self.quit_button.setText(' Quit')
        self.quit_button.setStyleSheet(self.style['md button'])
        self.quit_button.clicked.connect(self.close)
        # Add widgets above to help-quit layout
        self.helpquit_frame.layout().addWidget(self.help_button)
        self.helpquit_frame.layout().addWidget(self.quit_button)

        # ###
        # ### SAMPLES PAGE
        # ###

        # Samples page layout
        self.samples_layout = QVBoxLayout(self.samples_page)

        # ## Title section
        # Title
        self.samples_title = QLabel(self.samples_page)
        self.samples_title.setText('Name your samples')
        self.samples_title.setStyleSheet(self.style['title'])
        self.samples_title.adjustSize()
        self.samples_layout.addWidget(self.samples_title)
        # Subtitle
        self.samples_subtitle = QLabel(self.samples_page)
        string = (
            'Please name the samples to be analysed by SCOUTS.\n\nSCOUTS searches the first '
            'column of your data\nand locates the exact string as part of the sample name.'
        )
        self.samples_subtitle.setText(string)
        self.samples_subtitle.setStyleSheet(self.style['label'])
        self.samples_subtitle.adjustSize()
        self.samples_layout.addWidget(self.samples_subtitle)

        # ## Sample addition section
        # Sample addition frame
        self.samples_frame = QFrame(self.samples_page)
        self.samples_frame.setFrameShape(QFrame.StyledPanel)
        self.samples_frame.setLayout(QGridLayout())
        self.samples_layout.addWidget(self.samples_frame)
        # Sample name box
        self.sample_name = QLineEdit(self.samples_page)
        self.sample_name.setStyleSheet(self.style['line edit'])
        self.sample_name.setPlaceholderText('Sample name ...')
        # Reference check
        self.is_reference = QCheckBox(self.samples_page)
        self.is_reference.setText('Reference?')
        self.is_reference.setStyleSheet(self.style['checkbox'])
        # Add sample to table
        self.add_sample_button = QPushButton(self.samples_page)
        QShortcut(QKeySequence("Return"), self.add_sample_button,
                  self.write_to_sample_table)
        self.set_icon(self.add_sample_button, 'list-add')
        self.add_sample_button.setText(' Add sample (Enter)')
        self.add_sample_button.setStyleSheet(self.style['button'])
        self.add_sample_button.clicked.connect(self.write_to_sample_table)
        # Remove sample from table
        self.remove_sample_button = QPushButton(self.samples_page)
        QShortcut(QKeySequence("Delete"), self.remove_sample_button,
                  self.remove_from_sample_table)
        self.set_icon(self.remove_sample_button, 'list-remove')
        self.remove_sample_button.setText(' Remove sample (Del)')
        self.remove_sample_button.setStyleSheet(self.style['button'])
        self.remove_sample_button.clicked.connect(
            self.remove_from_sample_table)
        # Add widgets above to sample addition layout
        self.samples_frame.layout().addWidget(self.sample_name, 0, 0)
        self.samples_frame.layout().addWidget(self.is_reference, 1, 0)
        self.samples_frame.layout().addWidget(self.add_sample_button, 0, 1)
        self.samples_frame.layout().addWidget(self.remove_sample_button, 1, 1)

        # ## Sample table
        self.sample_table = QTableWidget(self.samples_page)
        self.sample_table.setColumnCount(2)
        self.sample_table.setHorizontalHeaderItem(0,
                                                  QTableWidgetItem('Sample'))
        self.sample_table.setHorizontalHeaderItem(
            1, QTableWidgetItem('Reference?'))
        self.sample_table.horizontalHeader().setSectionResizeMode(
            0, QHeaderView.Stretch)
        self.sample_table.horizontalHeader().setSectionResizeMode(
            1, QHeaderView.ResizeToContents)
        self.samples_layout.addWidget(self.sample_table)

        # ## Save & clear buttons
        # Save & clear frame (invisible)
        self.saveclear_frame = QFrame(self.samples_page)
        self.saveclear_frame.setLayout(QHBoxLayout())
        self.saveclear_frame.layout().setMargin(0)
        self.samples_layout.addWidget(self.saveclear_frame)
        # Clear samples button
        self.clear_samples = QPushButton(self.samples_page)
        self.set_icon(self.clear_samples, 'edit-delete')
        self.clear_samples.setText(' Clear table')
        self.clear_samples.setStyleSheet(self.style['md button'])
        self.clear_samples.clicked.connect(self.prompt_clear_data)
        # Save samples button
        self.save_samples = QPushButton(self.samples_page)
        self.set_icon(self.save_samples, 'document-save')
        self.save_samples.setText(' Save samples')
        self.save_samples.setStyleSheet(self.style['md button'])
        self.save_samples.clicked.connect(self.goto_main_page)
        # Add widgets above to save & clear layout
        self.saveclear_frame.layout().addWidget(self.clear_samples)
        self.saveclear_frame.layout().addWidget(self.save_samples)

        # ###
        # ### GATING PAGE
        # ###

        # Gating page layout
        self.gating_layout = QVBoxLayout(self.gating_page)

        # ## Title section
        # Title
        self.gates_title = QLabel(self.gating_page)
        self.gates_title.setText('Gating & outlier options')
        self.gates_title.setStyleSheet(self.style['title'])
        self.gates_title.adjustSize()
        self.gating_layout.addWidget(self.gates_title)

        # ## Gating options section
        # Gating header
        self.gate_header = QLabel(self.gating_page)
        self.gate_header.setText('Gating')
        self.gate_header.setStyleSheet(self.style['header'])
        self.gate_header.adjustSize()
        self.gating_layout.addWidget(self.gate_header)

        # Gating frame
        self.gate_frame = QFrame(self.gating_page)
        self.gate_frame.setFrameShape(QFrame.StyledPanel)
        self.gate_frame.setLayout(QFormLayout())
        self.gating_layout.addWidget(self.gate_frame)
        # Gating button group
        self.gating_group = QButtonGroup(self)
        # Do not gate samples
        self.no_gates = QRadioButton(self.gating_page)
        self.no_gates.setObjectName('no_gate')
        self.no_gates.setText("Don't gate samples")
        self.no_gates.setStyleSheet(self.style['radio button'])
        self.no_gates.setChecked(True)
        self.gating_group.addButton(self.no_gates)
        self.no_gates.clicked.connect(self.activate_gate)
        # CyToF gating
        self.cytof_gates = QRadioButton(self.gating_page)
        self.cytof_gates.setObjectName('cytof')
        self.cytof_gates.setText('Mass Cytometry gating')
        self.cytof_gates.setStyleSheet(self.style['radio button'])
        self.cytof_gates.setToolTip(
            'Exclude cells for which the average expression of all\n'
            'markers is below the selected value')
        self.gating_group.addButton(self.cytof_gates)
        self.cytof_gates.clicked.connect(self.activate_gate)
        # CyToF gating spinbox
        self.cytof_gates_value = QDoubleSpinBox(self.gating_page)
        self.cytof_gates_value.setMinimum(0)
        self.cytof_gates_value.setMaximum(1)
        self.cytof_gates_value.setValue(0.1)
        self.cytof_gates_value.setSingleStep(0.05)
        self.cytof_gates_value.setEnabled(False)
        # scRNA-Seq gating
        self.rnaseq_gates = QRadioButton(self.gating_page)
        self.rnaseq_gates.setText('scRNA-Seq gating')
        self.rnaseq_gates.setStyleSheet(self.style['radio button'])
        self.rnaseq_gates.setToolTip(
            'When calculating cutoff, ignore reads below the selected value')
        self.rnaseq_gates.setObjectName('rnaseq')
        self.gating_group.addButton(self.rnaseq_gates)
        self.rnaseq_gates.clicked.connect(self.activate_gate)
        # scRNA-Seq gating spinbox
        self.rnaseq_gates_value = QDoubleSpinBox(self.gating_page)
        self.rnaseq_gates_value.setMinimum(0)
        self.rnaseq_gates_value.setMaximum(10)
        self.rnaseq_gates_value.setValue(0)
        self.rnaseq_gates_value.setSingleStep(1)
        self.rnaseq_gates_value.setEnabled(False)
        # export gated population checkbox
        self.export_gated = QCheckBox(self.gating_page)
        self.export_gated.setText('Export gated cells as an output file')
        self.export_gated.setStyleSheet(self.style['checkbox'])
        self.export_gated.setEnabled(False)
        # Add widgets above to Gate frame layout
        self.gate_frame.layout().addRow(self.no_gates, QLabel())
        self.gate_frame.layout().addRow(self.cytof_gates,
                                        self.cytof_gates_value)
        self.gate_frame.layout().addRow(self.rnaseq_gates,
                                        self.rnaseq_gates_value)
        self.gate_frame.layout().addRow(self.export_gated, QLabel())

        # ## Outlier options section
        # Outlier header
        self.outlier_header = QLabel(self.gating_page)
        self.outlier_header.setText('Outliers')
        self.outlier_header.setStyleSheet(self.style['header'])
        self.outlier_header.adjustSize()
        self.gating_layout.addWidget(self.outlier_header)
        # Outlier frame
        self.outlier_frame = QFrame(self.gating_page)
        self.outlier_frame.setFrameShape(QFrame.StyledPanel)
        self.outlier_frame.setLayout(QVBoxLayout())
        self.gating_layout.addWidget(self.outlier_frame)
        # Top outliers information
        self.top_outliers = QLabel(self.gating_page)
        self.top_outliers.setStyleSheet(self.style['label'])
        self.top_outliers.setText(
            'By default, SCOUTS selects the top outliers from the population')
        self.top_outliers.setStyleSheet(self.style['label'])
        # Bottom outliers data
        self.bottom_outliers = QCheckBox(self.gating_page)
        self.bottom_outliers.setText('Include results for low outliers')
        self.bottom_outliers.setStyleSheet(self.style['checkbox'])
        # Non-outliers data
        self.not_outliers = QCheckBox(self.gating_page)
        self.not_outliers.setText('Include results for non-outliers')
        self.not_outliers.setStyleSheet(self.style['checkbox'])
        # Add widgets above to Gate frame layout
        self.outlier_frame.layout().addWidget(self.top_outliers)
        self.outlier_frame.layout().addWidget(self.bottom_outliers)
        self.outlier_frame.layout().addWidget(self.not_outliers)

        # ## Save/back button
        self.save_gates = QPushButton(self.gating_page)
        self.set_icon(self.save_gates, 'go-next')
        self.save_gates.setText(' Back to main menu')
        self.save_gates.setStyleSheet(self.style['md button'])
        self.gating_layout.addWidget(self.save_gates)
        self.save_gates.clicked.connect(self.goto_main_page)

        # ## Add empty label to take vertical space
        self.empty_label = QLabel(self.gating_page)
        self.empty_label.setSizePolicy(QSizePolicy.Expanding,
                                       QSizePolicy.Expanding)
        self.gating_layout.addWidget(self.empty_label)

    # ###
    # ### ICON SETTING
    # ###

    def set_icon(self, widget: QWidget, icon: str) -> None:
        """Associates an icon to a widget."""
        i = QIcon()
        i.addPixmap(
            QPixmap(
                os.path.abspath(
                    os.path.join(self.rootdir, 'src', 'default_icons',
                                 f'{icon}.svg'))))
        widget.setIcon(QIcon.fromTheme(icon, i))

    # ###
    # ### STACKED WIDGET PAGE SWITCHING
    # ###

    def goto_main_page(self) -> None:
        """Switches stacked widget pages to the main page."""
        self.stacked_pages.setCurrentWidget(self.main_page)

    def goto_samples_page(self) -> None:
        """Switches stacked widget pages to the samples table page."""
        self.stacked_pages.setCurrentWidget(self.samples_page)

    def goto_gates_page(self) -> None:
        """Switches stacked widget pages to the gating & other options page."""
        self.stacked_pages.setCurrentWidget(self.gating_page)

    # ###
    # ### MAIN PAGE GUI LOGIC
    # ###

    def get_path(self) -> None:
        """Opens a dialog box and sets the chosen file/folder path, depending on the caller widget."""
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        sender_name = self.sender().objectName()
        if sender_name == 'input':
            query, _ = QFileDialog.getOpenFileName(self,
                                                   "Select file",
                                                   "",
                                                   "All Files (*)",
                                                   options=options)
        elif sender_name == 'output':
            query = QFileDialog.getExistingDirectory(self,
                                                     "Select Directory",
                                                     options=options)
        else:
            return
        if query:
            getattr(self, f'{sender_name}_path').setText(query)

    def enable_single_excel(self) -> None:
        """Enables checkbox for generating a single Excel output."""
        if self.output_excel.isChecked():
            self.single_excel.setEnabled(True)
        else:
            self.single_excel.setEnabled(False)
            self.single_excel.setChecked(False)

    # ###
    # ### SAMPLE NAME/SAMPLE TABLE GUI LOGIC
    # ###

    def write_to_sample_table(self) -> None:
        """Writes data to sample table."""
        table = self.sample_table
        ref = 'no'
        sample = self.sample_name.text()
        if sample:
            for cell in range(table.rowCount()):
                item = table.item(cell, 0)
                if item.text() == sample:
                    self.same_sample()
                    return
            if self.is_reference.isChecked():
                for cell in range(table.rowCount()):
                    item = table.item(cell, 1)
                    if item.text() == 'yes':
                        self.more_than_one_reference()
                        return
                ref = 'yes'
            sample = QTableWidgetItem(sample)
            is_reference = QTableWidgetItem(ref)
            is_reference.setFlags(Qt.ItemIsEnabled)
            row_position = table.rowCount()
            table.insertRow(row_position)
            table.setItem(row_position, 0, sample)
            table.setItem(row_position, 1, is_reference)
            self.is_reference.setChecked(False)
            self.sample_name.setText('')

    def remove_from_sample_table(self) -> None:
        """Removes data from sample table."""
        table = self.sample_table
        rows = set(index.row() for index in table.selectedIndexes())
        for index in sorted(rows, reverse=True):
            self.sample_table.removeRow(index)

    def prompt_clear_data(self) -> None:
        """Prompts option to clear all data in the sample table."""
        if self.confirm_clear_data():
            table = self.sample_table
            while table.rowCount():
                self.sample_table.removeRow(0)

    # ###
    # ### GATING GUI LOGIC
    # ###

    def activate_gate(self) -> None:
        """Activates/deactivates buttons related to gating."""
        if self.sender().objectName() == 'no_gate':
            self.cytof_gates_value.setEnabled(False)
            self.rnaseq_gates_value.setEnabled(False)
            self.export_gated.setEnabled(False)
            self.export_gated.setChecked(False)
        elif self.sender().objectName() == 'cytof':
            self.cytof_gates_value.setEnabled(True)
            self.rnaseq_gates_value.setEnabled(False)
            self.export_gated.setEnabled(True)
        elif self.sender().objectName() == 'rnaseq':
            self.cytof_gates_value.setEnabled(False)
            self.rnaseq_gates_value.setEnabled(True)
            self.export_gated.setEnabled(True)

    # ###
    # ### CONNECT SCOUTS TO ANALYTICAL MODULES
    # ###

    def run(self) -> None:
        """Runs SCOUTS as a Worker, based on user input in the GUI."""
        try:
            data = self.parse_input()
        except Exception as error:
            trace = traceback.format_exc()
            self.propagate_error((error, trace))
        else:
            data['widget'] = self
            worker = Worker(func=start_scouts, **data)
            worker.signals.started.connect(self.analysis_has_started)
            worker.signals.finished.connect(self.analysis_has_finished)
            worker.signals.success.connect(self.success_message)
            worker.signals.error.connect(self.propagate_error)
            self.threadpool.start(worker)

    def parse_input(self) -> Dict:
        """Returns user input on the GUI as a dictionary."""
        # Input and output
        input_dict = {
            'input_file': str(self.input_path.text()),
            'output_folder': str(self.output_path.text())
        }
        if not input_dict['input_file'] or not input_dict['output_folder']:
            raise NoIOPathError
        # Set cutoff by reference or by sample rule
        input_dict['cutoff_rule'] = self.cutoff_group.checkedButton(
        ).objectName()  # 'sample', 'ref', 'sample ref'
        # Outliers for each individual marker or any marker in row
        input_dict['marker_rule'] = self.markers_group.checkedButton(
        ).objectName()  # 'single', 'any', 'single any'
        # Tukey factor used for calculating cutoff
        input_dict['tukey_factor'] = float(
            self.tukey_group.checkedButton().text())  # '1.5', '3.0'
        # Output settings
        input_dict['export_csv'] = True if self.output_csv.isChecked(
        ) else False
        input_dict['export_excel'] = True if self.output_excel.isChecked(
        ) else False
        input_dict['single_excel'] = True if self.single_excel.isChecked(
        ) else False
        # Retrieve samples from sample table
        input_dict['sample_list'] = []
        for tuples in self.yield_samples_from_table():
            input_dict['sample_list'].append(tuples)
        if not input_dict['sample_list']:
            raise NoSampleError
        # Set gate cutoff (if any)
        input_dict['gating'] = self.gating_group.checkedButton().objectName(
        )  # 'no_gate', 'cytof', 'rnaseq'
        input_dict['gate_cutoff_value'] = None
        if input_dict['gating'] != 'no_gate':
            input_dict['gate_cutoff_value'] = getattr(
                self, f'{input_dict["gating"]}_gates_value').value()
        input_dict['export_gated'] = True if self.export_gated.isChecked(
        ) else False
        # Generate results for non-outliers
        input_dict['non_outliers'] = False
        if self.not_outliers.isChecked():
            input_dict['non_outliers'] = True
        # Generate results for bottom outliers
        input_dict['bottom_outliers'] = False
        if self.bottom_outliers.isChecked():
            input_dict['bottom_outliers'] = True
        # return dictionary with all gathered inputs
        return input_dict

    def yield_samples_from_table(
            self) -> Generator[Tuple[str, str], None, None]:
        """Yields sample names from the sample table."""
        table = self.sample_table
        for cell in range(table.rowCount()):
            sample_name = table.item(cell, 0).text()
            sample_type = table.item(cell, 1).text()
            yield sample_name, sample_type

    # ###
    # ### MESSAGE BOXES
    # ###

    def analysis_has_started(self) -> None:
        """Disables run button while SCOUTS analysis is underway."""
        self.run_button.setText(' Working...')
        self.run_button.setEnabled(False)

    def analysis_has_finished(self) -> None:
        """Enables run button after SCOUTS analysis has finished."""
        self.run_button.setEnabled(True)
        self.run_button.setText(' Run!')

    def success_message(self) -> None:
        """Info message box used when SCOUTS finished without errors."""
        title = "Analysis finished!"
        mes = "Your analysis has finished. No errors were reported."
        if self.stacked_pages.isEnabled() is True:
            QMessageBox.information(self, title, mes)

    def memory_warning(self) -> None:
        """Warning message box used when user wants to generate a single excel file."""
        if self.sender().isChecked():
            title = 'Memory warning!'
            mes = (
                "Depending on your dataset, this option can consume a LOT of memory and take"
                " a long time to process. Please make sure that your computer can handle it!"
            )
            QMessageBox.information(self, title, mes)

    def same_sample(self) -> None:
        """Error message box used when the user tries to input the same sample twice in the sample table."""
        title = 'Error: sample name already in table'
        mes = (
            "Sorry, you can't do this because this sample name is already in the table. "
            "Please select a different name.")
        QMessageBox.critical(self, title, mes)

    def more_than_one_reference(self) -> None:
        """Error message box used when the user tries to input two reference samples in the sample table."""
        title = "Error: more than one reference selected"
        mes = (
            "Sorry, you can't do this because there is already a reference column in the table. "
            "Please remove it before adding a reference.")
        QMessageBox.critical(self, title, mes)

    def confirm_clear_data(self) -> bool:
        """Question message box used to confirm user action of clearing sample table."""
        title = 'Confirm Action'
        mes = "Table will be cleared. Are you sure?"
        reply = QMessageBox.question(self, title, mes,
                                     QMessageBox.Yes | QMessageBox.No,
                                     QMessageBox.No)
        if reply == QMessageBox.Yes:
            return True
        return False

    # ###
    # ### EXCEPTIONS & ERRORS
    # ###

    def propagate_error(self, error: Tuple[Exception, str]) -> None:
        """Calls the appropriate error message box based on type of Exception raised."""
        if isinstance(error[0], NoIOPathError):
            self.no_io_path_error_message()
        elif isinstance(error[0], NoReferenceError):
            self.no_reference_error_message()
        elif isinstance(error[0], NoSampleError):
            self.no_sample_error_message()
        elif isinstance(error[0], PandasInputError):
            self.pandas_input_error_message()
        elif isinstance(error[0], SampleNamingError):
            self.sample_naming_error_message()
        else:
            self.generic_error_message(error)

    def no_io_path_error_message(self) -> None:
        """Message displayed when the user did not include an input file path, or an output folder path."""
        title = 'Error: no file/folder'
        message = ("Sorry, no input file and/or output folder was provided. "
                   "Please add the path to the necessary file/folder.")
        QMessageBox.critical(self, title, message)

    def no_reference_error_message(self) -> None:
        """Message displayed when the user wants to analyse cutoff based on a reference, but did not specify what
        sample corresponds to the reference."""
        title = "Error: No reference selected"
        message = (
            "Sorry, no reference sample was found on the sample list, but analysis was set to "
            "reference. Please add a reference sample, or change the rule for cutoff calculation."
        )
        QMessageBox.critical(self, title, message)

    def no_sample_error_message(self) -> None:
        """Message displayed when the user did not add any samples to the sample table."""
        title = "Error: No samples selected"
        message = (
            "Sorry, the analysis cannot be performed because no sample names were input. "
            "Please add your sample names.")
        QMessageBox.critical(self, title, message)

    def pandas_input_error_message(self) -> None:
        """Message displayed when the input file cannot be read (likely because it is not a Excel or csv file)."""
        title = 'Error: unexpected input file'
        message = (
            "Sorry, the input file could not be read. Please make sure that "
            "the data is save in a valid format (supported formats are: "
            ".csv, .xlsx).")
        QMessageBox.critical(self, title, message)

    def sample_naming_error_message(self) -> None:
        """Message displayed when none of the sample names passed by the user are found in the input DataFrame."""
        title = 'Error: sample names not in input file'
        message = (
            "Sorry, your sample names were not found in the input file. Please "
            "make sure that the names were typed correctly (case-sensitive).")
        QMessageBox.critical(self, title, message)

    def generic_error_message(self, error: Tuple[Exception, str]) -> None:
        """Error message box used to display any error message (including traceback) for any uncaught errors."""
        title = 'An error occurred!'
        name, trace = error
        QMessageBox.critical(self, title,
                             f"{str(name)}\n\nfull traceback:\n{trace}")

    def not_implemented_error_message(self) -> None:
        """Error message box used when the user accesses a functionality that hasn't been implemented yet."""
        title = "Not yet implemented"
        mes = "Sorry, this functionality has not been implemented yet."
        QMessageBox.critical(self, title, mes)

    # ###
    # ### HELP & QUIT
    # ###

    @staticmethod
    def get_help() -> None:
        """Opens SCOUTS documentation on the browser. Called when the user clicks the "help" button"""
        webbrowser.open('https://scouts.readthedocs.io/en/master/')

    def closeEvent(self, event: QEvent) -> None:
        """Defines the message box for when the user wants to quit SCOUTS."""
        title = 'Quit SCOUTS'
        mes = "Are you sure you want to quit?"
        reply = QMessageBox.question(self, title, mes,
                                     QMessageBox.Yes | QMessageBox.No,
                                     QMessageBox.No)
        if reply == QMessageBox.Yes:
            self.stacked_pages.setEnabled(False)
            message = self.quit_message()
            waiter = Waiter(waiter_func=self.threadpool.activeThreadCount)
            waiter.signals.started.connect(message.show)
            waiter.signals.finished.connect(message.destroy)
            waiter.signals.finished.connect(sys.exit)
            self.threadpool.start(waiter)
        event.ignore()

    def quit_message(self) -> QDialog:
        """Displays a window while SCOUTS is exiting"""
        message = QDialog(self)
        message.setWindowTitle('Exiting SCOUTS')
        message.resize(300, 50)
        label = QLabel('SCOUTS is exiting, please wait...', message)
        label.setStyleSheet(self.style['label'])
        label.adjustSize()
        label.setAlignment(Qt.AlignCenter)
        label.move(int((message.width() - label.width()) / 2),
                   int((message.height() - label.height()) / 2))
        return message
コード例 #7
0
class StatsWidget(ToolWidget):
    def __init__(self, image, parent=None):
        super(StatsWidget, self).__init__(parent)

        self.min_radio = QRadioButton(self.tr('Minimum'))
        self.min_radio.setToolTip(self.tr('RGB channel with smallest value'))
        self.min_radio.setChecked(True)
        self.avg_radio = QRadioButton(self.tr('Average'))
        self.avg_radio.setToolTip(self.tr('RGB channel with average value'))
        self.max_radio = QRadioButton(self.tr('Maximum'))
        self.max_radio.setToolTip(self.tr('RGB channel with largest value'))
        self.incl_check = QCheckBox(self.tr('Inclusive'))
        self.incl_check.setToolTip(self.tr('Use not strict inequalities'))

        self.image = image
        b, g, r = cv.split(self.image)
        blue = np.array([255, 0, 0])
        green = np.array([0, 255, 0])
        red = np.array([0, 0, 255])
        self.minimum = [[], []]
        self.minimum[0] = np.zeros_like(self.image)
        self.minimum[0][np.logical_and(b < g, b < r)] = blue
        self.minimum[0][np.logical_and(g < r, g < b)] = green
        self.minimum[0][np.logical_and(r < b, r < g)] = red
        self.minimum[1] = np.zeros_like(self.image)
        self.minimum[1][np.logical_and(b <= g, b <= r)] = blue
        self.minimum[1][np.logical_and(g <= r, g <= b)] = green
        self.minimum[1][np.logical_and(r <= b, r <= g)] = red
        self.maximum = [[], []]
        self.maximum[0] = np.zeros_like(self.image)
        self.maximum[0][np.logical_and(b > g, b > r)] = blue
        self.maximum[0][np.logical_and(g > r, g > b)] = green
        self.maximum[0][np.logical_and(r > b, r > g)] = red
        self.maximum[1] = np.zeros_like(self.image)
        self.maximum[1][np.logical_and(b >= g, b >= r)] = blue
        self.maximum[1][np.logical_and(g >= r, g >= b)] = green
        self.maximum[1][np.logical_and(r >= b, r >= g)] = red
        self.average = [[], []]
        self.average[0] = np.zeros_like(self.image)
        self.average[0][np.logical_or(np.logical_and(r < b, b < g),
                                      np.logical_and(g < b, b < r))] = blue
        self.average[0][np.logical_or(np.logical_and(r < g, g < b),
                                      np.logical_and(b < g, g < r))] = green
        self.average[0][np.logical_or(np.logical_and(b < r, r < g),
                                      np.logical_and(g < r, r < b))] = red
        self.average[1] = np.zeros_like(self.image)
        self.average[1][np.logical_or(np.logical_and(r <= b, b <= g),
                                      np.logical_and(g <= b, b <= r))] = blue
        self.average[1][np.logical_or(np.logical_and(r <= g, g <= b),
                                      np.logical_and(b <= g, g <= r))] = green
        self.average[1][np.logical_or(np.logical_and(b <= r, r <= g),
                                      np.logical_and(g <= r, r <= b))] = red
        self.viewer = ImageViewer(self.image, self.image)
        self.process()

        self.min_radio.clicked.connect(self.process)
        self.avg_radio.clicked.connect(self.process)
        self.max_radio.clicked.connect(self.process)
        self.incl_check.stateChanged.connect(self.process)

        params_layout = QHBoxLayout()
        params_layout.addWidget(QLabel(self.tr('Mode:')))
        params_layout.addWidget(self.min_radio)
        params_layout.addWidget(self.avg_radio)
        params_layout.addWidget(self.max_radio)
        params_layout.addWidget(self.incl_check)
        params_layout.addStretch()

        main_layout = QVBoxLayout()
        main_layout.addLayout(params_layout)
        main_layout.addWidget(self.viewer)
        self.setLayout(main_layout)

    def process(self):
        inclusive = self.incl_check.isChecked()
        if self.min_radio.isChecked():
            result = self.minimum[1 if inclusive else 0]
        elif self.max_radio.isChecked():
            result = self.maximum[1 if inclusive else 0]
        elif self.avg_radio.isChecked():
            result = self.average[1 if inclusive else 0]
        else:
            return
        self.viewer.update_processed(result)
コード例 #8
0
class ImageViewer(QWidget):
    viewChanged = Signal(QRect, float, int, int)

    def __init__(self,
                 original,
                 processed,
                 title=None,
                 parent=None,
                 export=False):
        super(ImageViewer, self).__init__(parent)
        if original is None and processed is None:
            raise ValueError(
                self.tr('ImageViewer.__init__: Empty image received'))
        if original is None and processed is not None:
            original = processed
        self.original = original
        self.processed = processed
        if self.original is not None and self.processed is None:
            self.view = DynamicView(self.original)
        else:
            self.view = DynamicView(self.processed)

        # view_label = QLabel(self.tr('View:'))
        self.original_radio = QRadioButton(self.tr('Original'))
        self.original_radio.setToolTip(
            self.tr('Show the original image for comparison'))
        self.process_radio = QRadioButton(self.tr('Processed'))
        self.process_radio.setToolTip(
            self.tr('Show result of the current processing'))
        self.zoom_label = QLabel()
        full_button = QToolButton()
        full_button.setText(self.tr('100%'))
        fit_button = QToolButton()
        fit_button.setText(self.tr('Fit'))
        height, width, _ = self.original.shape
        size_label = QLabel(self.tr('[{}x{} px]'.format(height, width)))
        export_button = QToolButton()
        export_button.setToolTip(self.tr('Export current image to PNG'))
        # export_button.setText(self.tr('Export...'))
        export_button.setIcon(QIcon('icons/export.svg'))

        tool_layout = QHBoxLayout()
        tool_layout.addWidget(QLabel(self.tr('Zoom:')))
        tool_layout.addWidget(self.zoom_label)
        # tool_layout.addWidget(full_button)
        # tool_layout.addWidget(fit_button)
        tool_layout.addStretch()
        if processed is not None:
            # tool_layout.addWidget(view_label)
            tool_layout.addWidget(self.original_radio)
            tool_layout.addWidget(self.process_radio)
            tool_layout.addStretch()
        tool_layout.addWidget(size_label)
        if export or processed is not None:
            tool_layout.addWidget(export_button)
        if processed is not None:
            self.original_radio.setChecked(False)
            self.process_radio.setChecked(True)
            self.toggle_mode(False)

        vert_layout = QVBoxLayout()
        if title is not None:
            self.title_label = QLabel(title)
            modify_font(self.title_label, bold=True)
            self.title_label.setAlignment(Qt.AlignCenter)
            vert_layout.addWidget(self.title_label)
        else:
            self.title_label = None
        vert_layout.addWidget(self.view)
        vert_layout.addLayout(tool_layout)
        self.setLayout(vert_layout)

        self.original_radio.toggled.connect(self.toggle_mode)
        fit_button.clicked.connect(self.view.zoom_fit)
        full_button.clicked.connect(self.view.zoom_full)
        export_button.clicked.connect(self.export_image)
        self.view.viewChanged.connect(self.forward_changed)

        # view_label.setVisible(processed is not None)
        # self.original_radio.setVisible(processed is not None)
        # self.process_radio.setVisible(processed is not None)
        # export_button.setVisible(processed is not None)
        # if processed is not None:
        #
        # self.adjustSize()

    def update_processed(self, image):
        if self.processed is None:
            return
        self.processed = image
        self.toggle_mode(self.original_radio.isChecked())

    def update_original(self, image):
        self.original = image
        self.toggle_mode(True)

    def changeView(self, rect, scaling, horizontal, vertical):
        self.view.change_view(rect, scaling, horizontal, vertical)

    def forward_changed(self, rect, scaling, horizontal, vertical):
        self.zoom_label.setText('{:.2f}%'.format(scaling * 100))
        modify_font(self.zoom_label, scaling == 1)
        self.viewChanged.emit(rect, scaling, horizontal, vertical)

    def get_rect(self):
        return self.view.get_rect()

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Space:
            if self.original_radio.isChecked():
                self.process_radio.setChecked(True)
            else:
                self.original_radio.setChecked(True)
        QWidget.keyPressEvent(self, event)

    def toggle_mode(self, toggled):
        if toggled:
            self.view.set_image(self.original)
        elif self.processed is not None:
            self.view.set_image(self.processed)

    def export_image(self):
        settings = QSettings()
        filename = QFileDialog.getSaveFileName(
            self, self.tr('Export image...'), settings.value('save_folder'),
            self.tr('PNG images (*.png)'))[0]
        if not filename:
            return
        if not filename.endswith('.png'):
            filename += '.png'
        cv.imwrite(
            filename,
            self.processed if self.processed is not None else self.original)

    def set_title(self, title):
        if self.title_label is not None:
            self.title_label.setText(title)
コード例 #9
0
ファイル: settings.py プロジェクト: cernyd/newnigma
    def generate_components(self, reflectors, rotors, rotor_n, charset):
        """Generates currently displayed components based on Enigma model
        :param reflectors: {str} Reflector labels
        :param rotors: {[str, str, str]} Rotor labels
        :param rotor_n: {int} Number of rotors the Enigma model has
        """
        # REFLECTOR SETTINGS ===================================================
        spacing = 15
        style = "font-size: 18px; text-align: center;"

        reflector_frame = QFrame(self.__settings_frame)
        reflector_frame.setFrameStyle(QFrame.StyledPanel | QFrame.Plain)

        reflector_layout = QVBoxLayout(reflector_frame)
        reflector_layout.setSpacing(spacing)
        reflector_layout.addWidget(
            QLabel("REFLECTOR", reflector_frame, styleSheet=style),
            alignment=Qt.AlignHCenter,
        )

        self.__reflector_group = QButtonGroup(reflector_frame)
        reflector_layout.setAlignment(Qt.AlignTop)

        for i, model in enumerate(reflectors):
            radio = QRadioButton(model, reflector_frame)
            radio.setToolTip(
                "Reflector is an Enigma component that \nreflects "
                "letters from the rotors back to the lightboard")
            self.__reflector_group.addButton(radio)
            self.__reflector_group.setId(radio, i)
            reflector_layout.addWidget(radio, alignment=Qt.AlignTop)

        reflector_layout.addStretch()
        reflector_layout.addWidget(self.__ukwd_button)

        self.__reflector_group.button(0).setChecked(True)
        self.__reflector_group.buttonClicked.connect(self.refresh_ukwd)
        self.__settings_layout.addWidget(reflector_frame)

        # ROTOR SETTINGS =======================================================

        self.__rotor_selectors = []
        self.__ring_selectors = []
        self.__rotor_frames = []

        for rotor in range(rotor_n):
            rotor_frame = QFrame(self.__settings_frame)
            rotor_frame.setFrameStyle(QFrame.StyledPanel | QFrame.Plain)
            rotor_layout = QVBoxLayout(rotor_frame)
            rotor_layout.setAlignment(Qt.AlignTop)
            rotor_layout.setSpacing(spacing)
            rotor_frame.setLayout(rotor_layout)

            # ROTOR RADIOS =====================================================

            label = QLabel(SELECTOR_LABELS[-rotor_n:][rotor],
                           rotor_frame,
                           styleSheet=style)
            label.setToolTip(SELECTOR_TOOLTIPS[-rotor_n:][rotor])

            rotor_layout.addWidget(label, alignment=Qt.AlignHCenter)

            button_group = QButtonGroup(rotor_frame)

            final_rotors = rotors

            if "Beta" in rotors:
                logging.info(
                    "Enigma M4 rotors detected, adjusting radiobuttons...")
                if rotor == 0:
                    final_rotors = ["Beta", "Gamma"]
                else:
                    final_rotors.remove("Beta")
                    final_rotors.remove("Gamma")

            for i, model in enumerate(final_rotors):
                radios = QRadioButton(model, rotor_frame)
                button_group.addButton(radios)
                button_group.setId(radios, i)
                rotor_layout.addWidget(radios, alignment=Qt.AlignTop)

            button_group.button(0).setChecked(True)

            # RINGSTELLUNG =====================================================

            combobox = QComboBox(rotor_frame)
            for i, label in enumerate(LABELS[:len(charset)]):
                combobox.addItem(label, i)

            h_rule = QFrame(rotor_frame)
            h_rule.setFrameShape(QFrame.HLine)
            h_rule.setFrameShadow(QFrame.Sunken)

            self.__ring_selectors.append(combobox)
            self.__rotor_selectors.append(button_group)

            rotor_layout.addStretch()
            rotor_layout.addWidget(h_rule)
            rotor_layout.addWidget(
                QLabel("RING SETTING", rotor_frame, styleSheet=style),
                alignment=Qt.AlignHCenter,
            )
            rotor_layout.addWidget(combobox)

            self.__settings_layout.addWidget(rotor_frame)
            self.__rotor_frames.append(rotor_frame)
コード例 #10
0
    def __init__(self, type, index, parent=None):
        super(DocumentTableHeaderDialog, self).__init__(parent)

        self.setWindowFlags(self.windowFlags()
                            & ~Qt.WindowContextHelpButtonHint)

        # Group box
        text = 'Binary Number' if index >= 0 else 'Binary Numbers'
        toolTip = 'Change label to a binary number' if index >= 0 else 'Change all labels to binary numbers'
        rdbBinary = QRadioButton(text)
        rdbBinary.setToolTip(toolTip)

        text = 'With prefix 0b'
        toolTip = 'Change label to a binary number with prefix 0b otherwise without prefix' if index >= 0 else 'Change all labels to binary numbers with prefix 0b otherwise without prefix'
        self.chkBinary = QCheckBox(text)
        self.chkBinary.setChecked(True)
        self.chkBinary.setEnabled(False)
        self.chkBinary.setToolTip(toolTip)
        rdbBinary.toggled.connect(
            lambda checked: self.chkBinary.setEnabled(checked))

        text = 'Octal Number' if index >= 0 else 'Octal Numbers'
        toolTip = 'Change label to a octal number' if index >= 0 else 'Change all labels to octal numbers'
        rdbOctal = QRadioButton(text)
        rdbOctal.setToolTip(toolTip)

        text = 'With prefix 0o'
        toolTip = 'Change label to a octal number with prefix 0o otherwise without prefix' if index >= 0 else 'Change all labels to octal numbers with prefix 0o otherwise without prefix'
        self.chkOctal = QCheckBox(text)
        self.chkOctal.setChecked(True)
        self.chkOctal.setEnabled(False)
        self.chkOctal.setToolTip(toolTip)
        rdbOctal.toggled.connect(
            lambda checked: self.chkOctal.setEnabled(checked))

        text = 'Decimal Number' if index >= 0 else 'Decimal Numbers'
        toolTip = 'Change label to a decimal number' if index >= 0 else 'Change all labels to decimal numbers'
        rdbDecimal = QRadioButton(text)
        rdbDecimal.setToolTip(toolTip)

        text = 'Enumeration starting with 1'
        toolTip = 'Change label to a decimal number with the enumeration starting with 1 otherwise with 0' if index >= 0 else 'Change all labels to decimal numbers with the enumeration starting with 1 otherwise with 0'
        self.chkDecimal = QCheckBox(text)
        self.chkDecimal.setChecked(True)
        self.chkDecimal.setEnabled(False)
        self.chkDecimal.setToolTip(toolTip)
        rdbDecimal.toggled.connect(
            lambda checked: self.chkDecimal.setEnabled(checked))

        text = 'Hexadecimal Number' if index >= 0 else 'Hexadecimal Numbers'
        toolTip = 'Change label to a hexadecimal number' if index >= 0 else 'Change all labels to hexadecimal numbers'
        rdbHexadecimal = QRadioButton(text)
        rdbHexadecimal.setToolTip(toolTip)

        text = 'With prefix 0x'
        toolTip = 'Change label to a hexadecimal number with prefix 0x otherwise without prefix' if index >= 0 else 'Change all labels to hexadecimal numbers with prefix 0x otherwise without prefix'
        self.chkHexadecimal = QCheckBox(text)
        self.chkHexadecimal.setChecked(True)
        self.chkHexadecimal.setEnabled(False)
        self.chkHexadecimal.setToolTip(toolTip)
        rdbHexadecimal.toggled.connect(
            lambda checked: self.chkHexadecimal.setEnabled(checked))

        text = 'Capital Letter' if index >= 0 else 'Capital Letters'
        toolTip = 'Change label to a capital letter' if index >= 0 else 'Change all labels to capital letters'
        rdbLetter = QRadioButton(text)
        rdbLetter.setToolTip(toolTip)

        text = 'Letter as uppercase letter' if index >= 0 else 'Letters as uppercase letters'
        toolTip = 'Change label to a letter as uppercase letter otherwise lowercase letter' if index >= 0 else 'Change all labels to letters as uppercase letters otherwise lowercase letters'
        self.chkLetter = QCheckBox(text)
        self.chkLetter.setChecked(True)
        self.chkLetter.setEnabled(False)
        self.chkLetter.setToolTip(toolTip)
        rdbLetter.toggled.connect(
            lambda checked: self.chkLetter.setEnabled(checked))

        text = 'User-defined Text' if index >= 0 else 'User-defined Texts'
        toolTip = 'Change label to a user-defined text' if index >= 0 else 'Change all labels to user-defined texts'
        rdbCustom = QRadioButton(text)
        rdbCustom.setToolTip(toolTip)

        toolTip = 'Change label to a user-defined text' if index >= 0 else 'Change all labels to user-defined texts'
        self.ledCustom = QLineEdit()
        self.ledCustom.setEnabled(False)
        self.ledCustom.setToolTip(toolTip)
        rdbCustom.toggled.connect(
            lambda checked: self.ledCustom.setEnabled(checked))

        text = '# will be replaced with column index' if type == 'horizontal' else '# will be replaced with row index'
        lblCustom = QLabel(text)
        lblCustom.setEnabled(False)
        rdbCustom.toggled.connect(
            lambda checked: lblCustom.setEnabled(checked))

        self.grpHeaderLabel = QButtonGroup(self)
        self.grpHeaderLabel.addButton(rdbBinary,
                                      Preferences.HeaderLabel.Binary.value)
        self.grpHeaderLabel.addButton(rdbOctal,
                                      Preferences.HeaderLabel.Octal.value)
        self.grpHeaderLabel.addButton(rdbDecimal,
                                      Preferences.HeaderLabel.Decimal.value)
        self.grpHeaderLabel.addButton(
            rdbHexadecimal, Preferences.HeaderLabel.Hexadecimal.value)
        self.grpHeaderLabel.addButton(rdbLetter,
                                      Preferences.HeaderLabel.Letter.value)
        self.grpHeaderLabel.addButton(rdbCustom,
                                      Preferences.HeaderLabel.Custom.value)
        self.grpHeaderLabel.buttonClicked.connect(self.onSettingChanged)

        groupLayout = QGridLayout()
        groupLayout.addWidget(rdbBinary, 0, 0)
        groupLayout.addWidget(self.chkBinary, 0, 1)
        groupLayout.addWidget(rdbOctal, 1, 0)
        groupLayout.addWidget(self.chkOctal, 1, 1)
        groupLayout.addWidget(rdbDecimal, 2, 0)
        groupLayout.addWidget(self.chkDecimal, 2, 1)
        groupLayout.addWidget(rdbHexadecimal, 3, 0)
        groupLayout.addWidget(self.chkHexadecimal, 3, 1)
        groupLayout.addWidget(rdbLetter, 4, 0)
        groupLayout.addWidget(self.chkLetter, 4, 1)
        groupLayout.addWidget(rdbCustom, 5, 0)
        groupLayout.addWidget(self.ledCustom, 5, 1)
        groupLayout.addWidget(lblCustom, 6, 1)
        groupLayout.setRowStretch(7, 1)

        text = 'Change label to a …' if index >= 0 else 'Change all labels to …'
        groupBox = QGroupBox(text)
        groupBox.setLayout(groupLayout)

        # Button box
        buttonBox = QDialogButtonBox(QDialogButtonBox.Ok
                                     | QDialogButtonBox.Cancel)
        self.buttonOk = buttonBox.button(QDialogButtonBox.Ok)
        self.buttonOk.setEnabled(False)
        buttonBox.accepted.connect(self.accept)
        buttonBox.rejected.connect(self.reject)

        # Main layout
        layout = QVBoxLayout()
        layout.addWidget(groupBox)
        layout.addWidget(buttonBox)

        self.setLayout(layout)
コード例 #11
0
    def __init__(self, parent=None):
        super().__init__(parent)

        # Title
        title = QLabel(
            self.tr("<strong style=\"font-size:large;\">{0}</strong>").format(
                self.title()))

        #
        # Content: Header Labels

        rdbDefaultHeaderLabelHorizontalLetters = QRadioButton(
            self.tr("Letters"))
        rdbDefaultHeaderLabelHorizontalLetters.setToolTip(
            self.
            tr("Capital letters as default horizontal header labels of new documents"
               ))

        rdbDefaultHeaderLabelHorizontalNumbers = QRadioButton(
            self.tr("Numbers"))
        rdbDefaultHeaderLabelHorizontalNumbers.setToolTip(
            self.
            tr("Decimal numbers as default horizontal header labels of new documents"
               ))

        self._grpDefaultHeaderLabelHorizontal = QButtonGroup(self)
        self._grpDefaultHeaderLabelHorizontal.addButton(
            rdbDefaultHeaderLabelHorizontalLetters,
            Preferences.HeaderLabel.Letter.value)
        self._grpDefaultHeaderLabelHorizontal.addButton(
            rdbDefaultHeaderLabelHorizontalNumbers,
            Preferences.HeaderLabel.Decimal.value)
        self._grpDefaultHeaderLabelHorizontal.buttonClicked.connect(
            self._onPreferencesChanged)

        defaultHeaderLabelHorizontalBox = QHBoxLayout()
        defaultHeaderLabelHorizontalBox.addWidget(
            rdbDefaultHeaderLabelHorizontalLetters)
        defaultHeaderLabelHorizontalBox.addWidget(
            rdbDefaultHeaderLabelHorizontalNumbers)

        rdbDefaultHeaderLabelVerticalLetters = QRadioButton(self.tr("Letters"))
        rdbDefaultHeaderLabelVerticalLetters.setToolTip(
            self.
            tr("Capital letters as default vertical header labels of new documents"
               ))

        rdbDefaultHeaderLabelVerticalNumbers = QRadioButton(self.tr("Numbers"))
        rdbDefaultHeaderLabelVerticalNumbers.setToolTip(
            self.
            tr("Decimal numbers as default vertical header labels of new documents"
               ))

        self._grpDefaultHeaderLabelVertical = QButtonGroup()
        self._grpDefaultHeaderLabelVertical.addButton(
            rdbDefaultHeaderLabelVerticalLetters,
            Preferences.HeaderLabel.Letter.value)
        self._grpDefaultHeaderLabelVertical.addButton(
            rdbDefaultHeaderLabelVerticalNumbers,
            Preferences.HeaderLabel.Decimal.value)
        self._grpDefaultHeaderLabelVertical.buttonClicked.connect(
            self._onPreferencesChanged)

        defaultHeaderLabelVerticalBox = QHBoxLayout()
        defaultHeaderLabelVerticalBox.addWidget(
            rdbDefaultHeaderLabelVerticalLetters)
        defaultHeaderLabelVerticalBox.addWidget(
            rdbDefaultHeaderLabelVerticalNumbers)

        defaultHeaderLabelLayout = QFormLayout()
        defaultHeaderLabelLayout.addRow(
            self.tr("Labels of the horizontal header"),
            defaultHeaderLabelHorizontalBox)
        defaultHeaderLabelLayout.addRow(
            self.tr("Labels of the vertical header"),
            defaultHeaderLabelVerticalBox)

        defaultHeaderLabelGroup = QGroupBox(self.tr("Header Labels"))
        defaultHeaderLabelGroup.setLayout(defaultHeaderLabelLayout)

        #
        # Content: Cell Counts

        self._spbDefaultCellCountColumn = QSpinBox()
        self._spbDefaultCellCountColumn.setRange(1, 1000)
        self._spbDefaultCellCountColumn.setToolTip(
            self.tr("Default number of columns of new documents"))
        self._spbDefaultCellCountColumn.valueChanged.connect(
            self._onPreferencesChanged)

        self._spbDefaultCellCountRow = QSpinBox()
        self._spbDefaultCellCountRow.setRange(1, 1000)
        self._spbDefaultCellCountRow.setToolTip(
            self.tr("Default number of rows of new documents"))
        self._spbDefaultCellCountRow.valueChanged.connect(
            self._onPreferencesChanged)

        defaultCellCountLayout = QFormLayout()
        defaultCellCountLayout.addRow(self.tr("Number of columns"),
                                      self._spbDefaultCellCountColumn)
        defaultCellCountLayout.addRow(self.tr("Number of rows"),
                                      self._spbDefaultCellCountRow)

        defaultCellCountGroup = QGroupBox(self.tr("Cell Counts"))
        defaultCellCountGroup.setLayout(defaultCellCountLayout)

        # Main layout
        self._layout = QVBoxLayout(self)
        self._layout.addWidget(title)
        self._layout.addWidget(defaultHeaderLabelGroup)
        self._layout.addWidget(defaultCellCountGroup)
        self._layout.addStretch(1)