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