class FrequencyWidget(ToolWidget): def __init__(self, image, parent=None): super(FrequencyWidget, self).__init__(parent) self.split_spin = QSpinBox() self.split_spin.setRange(0, 100) self.split_spin.setValue(15) self.split_spin.setSuffix(self.tr(" %")) self.smooth_spin = QSpinBox() self.smooth_spin.setRange(0, 100) self.smooth_spin.setValue(25) self.smooth_spin.setSuffix(self.tr(" %")) self.smooth_spin.setSpecialValueText(self.tr("Off")) self.thr_spin = QSpinBox() self.thr_spin.setRange(0, 100) self.thr_spin.setValue(0) self.thr_spin.setSuffix(self.tr(" %")) self.thr_spin.setSpecialValueText(self.tr("Off")) self.zero_label = QLabel() modify_font(self.zero_label, italic=True) self.filter_spin = QSpinBox() self.filter_spin.setRange(0, 15) self.filter_spin.setValue(0) self.filter_spin.setSuffix(self.tr(" px")) self.filter_spin.setSpecialValueText(self.tr("Off")) self.split_spin.valueChanged.connect(self.process) self.smooth_spin.valueChanged.connect(self.process) self.thr_spin.valueChanged.connect(self.process) self.filter_spin.valueChanged.connect(self.postprocess) self.image = image gray = cv.cvtColor(self.image, cv.COLOR_BGR2GRAY) rows, cols = gray.shape height = cv.getOptimalDFTSize(rows) width = cv.getOptimalDFTSize(cols) padded = cv.copyMakeBorder(gray, 0, height - rows, 0, width - cols, cv.BORDER_CONSTANT) self.dft = np.fft.fftshift(cv.dft(padded.astype(np.float32), flags=cv.DFT_COMPLEX_OUTPUT)) self.magnitude0, self.phase0 = cv.cartToPolar(self.dft[:, :, 0], self.dft[:, :, 1]) self.magnitude0 = cv.normalize(cv.log(self.magnitude0), None, 0, 255, cv.NORM_MINMAX) self.phase0 = cv.normalize(self.phase0, None, 0, 255, cv.NORM_MINMAX) self.magnitude = self.phase = None self.low_viewer = ImageViewer(self.image, self.image, self.tr("Low frequency"), export=True) self.high_viewer = ImageViewer(self.image, self.image, self.tr("High frequency"), export=True) self.mag_viewer = ImageViewer(self.image, None, self.tr("DFT Magnitude"), export=True) self.phase_viewer = ImageViewer(self.image, None, self.tr("DFT Phase"), export=True) self.process() self.low_viewer.viewChanged.connect(self.high_viewer.changeView) self.high_viewer.viewChanged.connect(self.low_viewer.changeView) self.mag_viewer.viewChanged.connect(self.phase_viewer.changeView) self.phase_viewer.viewChanged.connect(self.mag_viewer.changeView) top_layout = QHBoxLayout() top_layout.addWidget(QLabel(self.tr("Separation:"))) top_layout.addWidget(self.split_spin) top_layout.addWidget(QLabel(self.tr("Smooth:"))) top_layout.addWidget(self.smooth_spin) top_layout.addWidget(QLabel(self.tr("Threshold:"))) top_layout.addWidget(self.thr_spin) top_layout.addWidget(QLabel(self.tr("Filter:"))) top_layout.addWidget(self.filter_spin) top_layout.addWidget(self.zero_label) top_layout.addStretch() center_layout = QGridLayout() center_layout.addWidget(self.low_viewer, 0, 0) center_layout.addWidget(self.high_viewer, 0, 1) center_layout.addWidget(self.mag_viewer, 1, 0) center_layout.addWidget(self.phase_viewer, 1, 1) main_layout = QVBoxLayout() main_layout.addLayout(top_layout) main_layout.addLayout(center_layout) self.setLayout(main_layout) def process(self): start = time() rows, cols, _ = self.dft.shape mask = np.zeros((rows, cols), np.float32) half = np.sqrt(rows ** 2 + cols ** 2) / 2 radius = int(half * self.split_spin.value() / 100) mask = cv.circle(mask, (cols // 2, rows // 2), radius, 1, cv.FILLED) kernel = 2 * int(half * self.smooth_spin.value() / 100) + 1 mask = cv.GaussianBlur(mask, (kernel, kernel), 0) mask /= np.max(mask) threshold = int(self.thr_spin.value() / 100 * 255) if threshold > 0: mask[self.magnitude0 < threshold] = 0 zeros = (mask.size - np.count_nonzero(mask)) / mask.size * 100 else: zeros = 0 self.zero_label.setText(self.tr("(zeroed coefficients = {:.2f}%)").format(zeros)) mask2 = np.repeat(mask[:, :, np.newaxis], 2, axis=2) rows0, cols0, _ = self.image.shape low = cv.idft(np.fft.ifftshift(self.dft * mask2), flags=cv.DFT_SCALE) low = norm_mat(cv.magnitude(low[:, :, 0], low[:, :, 1])[:rows0, :cols0], to_bgr=True) self.low_viewer.update_processed(low) high = cv.idft(np.fft.ifftshift(self.dft * (1 - mask2)), flags=cv.DFT_SCALE) high = norm_mat(cv.magnitude(high[:, :, 0], high[:, :, 1]), to_bgr=True) self.high_viewer.update_processed(np.copy(high[: self.image.shape[0], : self.image.shape[1]])) self.magnitude = (self.magnitude0 * mask).astype(np.uint8) self.phase = (self.phase0 * mask).astype(np.uint8) self.postprocess() self.info_message.emit(self.tr(f"Frequency Split = {elapsed_time(start)}")) def postprocess(self): kernel = 2 * self.filter_spin.value() + 1 if kernel >= 3: magnitude = cv.GaussianBlur(self.magnitude, (kernel, kernel), 0) phase = cv.GaussianBlur(self.phase, (kernel, kernel), 0) # phase = cv.medianBlur(self.phase, kernel) else: magnitude = self.magnitude phase = self.phase self.mag_viewer.update_original(cv.cvtColor(magnitude, cv.COLOR_GRAY2BGR)) self.phase_viewer.update_original(cv.cvtColor(phase, cv.COLOR_GRAY2BGR))
class FrequencyWidget(ToolWidget): def __init__(self, image, parent=None): super(FrequencyWidget, self).__init__(parent) self.ampl_radio = QRadioButton(self.tr('Amplitude')) self.ampl_radio.setChecked(True) self.phase_radio = QRadioButton(self.tr('Phase')) self.dct_radio = QRadioButton(self.tr('DCT Map')) self.last_radio = self.ampl_radio self.thr_spin = QSpinBox() self.thr_spin.setRange(0, 255) self.thr_spin.setSpecialValueText(self.tr('Off')) self.ratio_label = QLabel() self.filter_check = QCheckBox(self.tr('Filter')) self.ampl_radio.clicked.connect(self.process) self.phase_radio.clicked.connect(self.process) self.dct_radio.clicked.connect(self.process) self.thr_spin.valueChanged.connect(self.process) self.filter_check.stateChanged.connect(self.process) gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY) rows, cols = gray.shape height = cv.getOptimalDFTSize(rows) width = cv.getOptimalDFTSize(cols) padded = cv.copyMakeBorder(gray, 0, height - rows, 0, width - cols, cv.BORDER_CONSTANT).astype(np.float32) planes = cv.merge([padded, np.zeros_like(padded)]) dft = cv.split(np.fft.fftshift(cv.dft(planes))) mag, phase = cv.cartToPolar(dft[0], dft[1]) dct = cv.dct(padded) self.result = [ normalize_mat(img) for img in [cv.log(mag), phase, cv.log(dct)] ] self.image = image self.viewer = ImageViewer(self.image, None) self.process() top_layout = QHBoxLayout() top_layout.addWidget(QLabel(self.tr('Coefficients:'))) top_layout.addWidget(self.ampl_radio) top_layout.addWidget(self.phase_radio) top_layout.addWidget(self.dct_radio) top_layout.addWidget(self.filter_check) top_layout.addStretch() top_layout.addWidget(QLabel(self.tr('Threshold:'))) top_layout.addWidget(self.thr_spin) top_layout.addWidget(self.ratio_label) main_layout = QVBoxLayout() main_layout.addLayout(top_layout) main_layout.addWidget(self.viewer) self.setLayout(main_layout) def process(self): if self.ampl_radio.isChecked(): output = self.result[0] self.last_radio = self.ampl_radio elif self.phase_radio.isChecked(): output = self.result[1] self.last_radio = self.phase_radio elif self.dct_radio.isChecked(): output = self.result[2] self.last_radio = self.dct_radio else: self.last_radio.setChecked(True) return if self.filter_check.isChecked(): output = cv.medianBlur(output, 3) thr = self.thr_spin.value() if thr > 0: _, output = cv.threshold(output, thr, 0, cv.THRESH_TOZERO) zeros = (1.0 - cv.countNonZero(output) / output.size) * 100 else: zeros = 0 self.ratio_label.setText('(masked = {:.1f}%)'.format(zeros)) self.viewer.update_original(cv.cvtColor(output, cv.COLOR_GRAY2BGR))
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 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(f"Evidence: {basename}")) self.reference_viewer = ImageViewer(np.full_like(self.evidence, 127), None, self.tr("Reference")) self.table_widget = QTableWidget(20, 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("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("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("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("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("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("icons/high.svg"), "(+" + "\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('icons/high.svg'), '(+' + u'\u221e' + ')')) # self.table_widget.item(6, 0).setToolTip(self.tr('PSNR with Blocking Effect Factor.')) self.table_widget.setItem(6, 0, QTableWidgetItem(self.tr("SSIM"))) self.table_widget.setItem( 6, 2, QTableWidgetItem(QIcon("icons/high.svg"), "(1)")) self.table_widget.item(6, 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(7, 0, QTableWidgetItem(self.tr("MS-SSIM"))) self.table_widget.setItem( 7, 2, QTableWidgetItem(QIcon("icons/high.svg"), "(1)")) self.table_widget.item(7, 0).setToolTip( self.tr("Multiscale version of SSIM.")) self.table_widget.setItem(8, 0, QTableWidgetItem(self.tr("RASE"))) self.table_widget.setItem( 8, 2, QTableWidgetItem(QIcon("icons/low.svg"), "(0)")) self.table_widget.item(8, 0).setToolTip( self.tr("Relative average spectral error")) self.table_widget.setItem(9, 0, QTableWidgetItem(self.tr("SCC"))) self.table_widget.setItem( 9, 2, QTableWidgetItem(QIcon("icons/high.svg"), "(1)")) self.table_widget.item(9, 0).setToolTip( self.tr("Spatial Correlation Coefficient")) self.table_widget.setItem(10, 0, QTableWidgetItem(self.tr("UQI"))) self.table_widget.setItem( 10, 2, QTableWidgetItem(QIcon("icons/high.svg"), "(1)")) self.table_widget.item(10, 0).setToolTip( self.tr("Universal Image Quality Index")) self.table_widget.setItem(11, 0, QTableWidgetItem(self.tr("VIF-P"))) self.table_widget.setItem( 11, 2, QTableWidgetItem(QIcon("icons/high.svg"), "(1)")) self.table_widget.item(11, 0).setToolTip( self.tr("Pixel-based Visual Information Fidelity")) self.table_widget.setItem(12, 0, QTableWidgetItem(self.tr("SSIMulacra"))) self.table_widget.setItem( 12, 2, QTableWidgetItem(QIcon("icons/low.svg"), "(0)")) self.table_widget.item(12, 0).setToolTip( self.tr("Structural SIMilarity Unveiling Local " "And Compression Related Artifacts")) self.table_widget.setItem(13, 0, QTableWidgetItem(self.tr("Butteraugli"))) self.table_widget.setItem( 13, 2, QTableWidgetItem(QIcon("icons/low.svg"), "(0)")) self.table_widget.item(13, 0).setToolTip( self.tr("Estimate psychovisual error")) self.table_widget.setItem(14, 0, QTableWidgetItem(self.tr("Correlation"))) self.table_widget.setItem( 14, 2, QTableWidgetItem(QIcon("icons/high.svg"), "(1)")) self.table_widget.item(14, 0).setToolTip(self.tr("Histogram correlation")) self.table_widget.setItem(15, 0, QTableWidgetItem(self.tr("Chi-Square"))) self.table_widget.setItem( 15, 2, QTableWidgetItem(QIcon("icons/low.svg"), "(0)")) self.table_widget.item(15, 0).setToolTip(self.tr("Histogram Chi-Square")) self.table_widget.setItem(16, 0, QTableWidgetItem(self.tr("Chi-Square 2"))) self.table_widget.setItem( 16, 2, QTableWidgetItem(QIcon("icons/low.svg"), "(0)")) self.table_widget.item(16, 0).setToolTip(self.tr("Alternative Chi-Square")) self.table_widget.setItem(17, 0, QTableWidgetItem(self.tr("Intersection"))) self.table_widget.setItem( 17, 2, QTableWidgetItem(QIcon("icons/high.svg"), "(+" + "\u221e" + ")")) self.table_widget.item(17, 0).setToolTip(self.tr("Histogram intersection")) self.table_widget.setItem(18, 0, QTableWidgetItem(self.tr("Hellinger"))) self.table_widget.setItem( 18, 2, QTableWidgetItem(QIcon("icons/low.svg"), "(0)")) self.table_widget.item(18, 0).setToolTip( self.tr("Histogram Hellinger distance")) self.table_widget.setItem(19, 0, QTableWidgetItem(self.tr("Divergence"))) self.table_widget.setItem( 19, 2, QTableWidgetItem(QIcon("icons/low.svg"), "(0)")) self.table_widget.item(19, 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(f"Reference: {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(f"{rmse:.2f}")) self.table_widget.setItem(1, 1, QTableWidgetItem(f"{sam:.4f}")) self.table_widget.setItem(2, 1, QTableWidgetItem(f"{ergas:.2f}")) self.table_widget.setItem(3, 1, QTableWidgetItem(f"{mb:.4f}")) self.table_widget.setItem(4, 1, QTableWidgetItem(f"{pfe:.2f}")) if psnr > 0: self.table_widget.setItem(5, 1, QTableWidgetItem(f"{psnr:.2f} dB")) else: self.table_widget.setItem(5, 1, QTableWidgetItem("+" + "\u221e" + " dB")) # self.table_widget.setItem(6, 1, QTableWidgetItem('{:.2f}'.format(psnrb))) self.table_widget.setItem(6, 1, QTableWidgetItem(f"{ssim:.4f}")) self.table_widget.setItem(7, 1, QTableWidgetItem(f"{mssim:.4f}")) self.table_widget.setItem(8, 1, QTableWidgetItem(f"{rase:.2f}")) self.table_widget.setItem(9, 1, QTableWidgetItem(f"{scc:.4f}")) self.table_widget.setItem(10, 1, QTableWidgetItem(f"{uqi:.4f}")) self.table_widget.setItem(11, 1, QTableWidgetItem(f"{vifp:.4f}")) self.table_widget.setItem(12, 1, QTableWidgetItem(f"{ssimul:.4f}")) self.table_widget.setItem(13, 1, QTableWidgetItem(f"{butter:.2f}")) self.table_widget.setItem(14, 1, QTableWidgetItem(f"{correlation:.2f}")) self.table_widget.setItem(15, 1, QTableWidgetItem(f"{chi_square:.2f}")) self.table_widget.setItem(16, 1, QTableWidgetItem(f"{chi_square2:.2f}")) self.table_widget.setItem(17, 1, QTableWidgetItem(f"{intersection:.2f}")) self.table_widget.setItem(18, 1, QTableWidgetItem(f"{hellinger:.2f}")) self.table_widget.setItem(19, 1, QTableWidgetItem(f"{divergence:.2f}")) 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) if p.returncode == 0: value = float(p.stdout) heatmap = cv.imread(filename3, cv.IMREAD_COLOR) else: raise ValueError return value, heatmap except (FileNotFoundError, ValueError) as _: 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) if p.returncode == 0: value = float(p.stdout) else: raise ValueError return value except (FileNotFoundError, ValueError) as _: return -1
class ComparisonWidget(ToolWidget): def __init__(self, filename, image, parent=None): super(ComparisonWidget, self).__init__(parent) load_button = QPushButton(self.tr('Load reference...')) self.comp_label = QLabel(self.tr('Comparison:')) self.normal_radio = QRadioButton(self.tr('Normal')) self.normal_radio.setChecked(True) self.diff_radio = QRadioButton(self.tr('Difference')) self.ssim_radio = QRadioButton(self.tr('SSIM Map')) self.butter_radio = QRadioButton(self.tr('Butteraugli')) self.gray_check = QCheckBox(self.tr('Grayscale')) self.equal_check = QCheckBox(self.tr('Equalized')) self.last_radio = self.normal_radio self.metric_button = QPushButton(self.tr('Compute')) 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(15, 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('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('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('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('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('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('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('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('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('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('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('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('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('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('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('icons/low.svg'), '(0)')) self.table_widget.item(14, 0).setToolTip( self.tr('Estimate psychovisual error')) 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.setAlternatingRowColors(True) self.stopped = False self.comp_label.setEnabled(False) self.normal_radio.setEnabled(False) self.diff_radio.setEnabled(False) self.ssim_radio.setEnabled(False) self.butter_radio.setEnabled(False) self.gray_check.setEnabled(False) self.equal_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.diff_radio.clicked.connect(self.change) self.butter_radio.clicked.connect(self.change) self.gray_check.stateChanged.connect(self.change) self.equal_check.stateChanged.connect(self.change) self.ssim_radio.clicked.connect(self.change) self.evidence_viewer.viewChanged.connect( self.reference_viewer.change_view) self.reference_viewer.viewChanged.connect( self.evidence_viewer.change_view) 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.diff_radio) top_layout.addWidget(self.ssim_radio) top_layout.addWidget(self.butter_radio) top_layout.addWidget(self.gray_check) top_layout.addWidget(self.equal_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 = normalize_mat( cv.absdiff(self.evidence, self.reference)) self.comp_label.setEnabled(True) self.normal_radio.setEnabled(True) self.diff_radio.setEnabled(True) self.ssim_radio.setEnabled(False) self.butter_radio.setEnabled(False) self.gray_check.setEnabled(True) self.equal_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.equal_check.setEnabled(False) self.last_radio = self.normal_radio elif self.diff_radio.isChecked(): result = self.difference self.gray_check.setEnabled(True) self.equal_check.setEnabled(True) self.last_radio = self.diff_radio elif self.ssim_radio.isChecked(): result = self.ssim_map self.gray_check.setEnabled(False) self.equal_check.setEnabled(True) self.last_radio = self.ssim_radio elif self.butter_radio.isChecked(): result = self.butter_map self.gray_check.setEnabled(True) self.equal_check.setEnabled(False) self.last_radio = self.butter_radio else: self.last_radio.setChecked(True) return if self.equal_check.isChecked(): result = equalize_image(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 psnrb = sewar.psnrb(img1, img2) 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) self.table_widget.setItem(0, 1, QTableWidgetItem('{:.4f}'.format(rmse))) self.table_widget.setItem(1, 1, QTableWidgetItem('{:.4f}'.format(sam))) self.table_widget.setItem(2, 1, QTableWidgetItem('{:.4f}'.format(ergas))) self.table_widget.setItem(3, 1, QTableWidgetItem('{:.4f}'.format(mb))) self.table_widget.setItem(4, 1, QTableWidgetItem('{:.4f}'.format(pfe))) if psnr > 0: self.table_widget.setItem( 5, 1, QTableWidgetItem('{:.4f} dB'.format(psnr))) else: self.table_widget.setItem( 5, 1, QTableWidgetItem('+' + u'\u221e' + ' dB')) self.table_widget.setItem(6, 1, QTableWidgetItem('{:.4f}'.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('{:.4f}'.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('{:.4f}'.format(butter))) 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 - normalize_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 ComparisonWidget(ToolWidget): def __init__(self, image, parent=None): super(ComparisonWidget, self).__init__(parent) load_button = QPushButton(self.tr('Load reference...')) self.file_label = QLabel() modify_font(self.file_label, bold=True) self.comp_label = QLabel(self.tr('Comparison:')) self.normal_radio = QRadioButton(self.tr('Normal')) self.normal_radio.setChecked(True) self.diff_radio = QRadioButton(self.tr('Difference')) self.gray_check = QCheckBox(self.tr('Grayscale')) self.equal_check = QCheckBox(self.tr('Equalized')) self.map_radio = QRadioButton(self.tr('SSIM Map')) self.last_radio = self.normal_radio self.comp_label.setEnabled(False) self.normal_radio.setEnabled(False) self.diff_radio.setEnabled(False) self.gray_check.setEnabled(False) self.equal_check.setEnabled(False) self.map_radio.setEnabled(False) self.evidence = image self.reference = np.full_like(image, 127) self.difference = np.full_like(image, 127) self.equalized = np.full_like(image, 127) self.indexmap = np.full_like(image, 127) self.evidence_viewer = ImageViewer(self.evidence, None, self.tr('Evidence')) self.reference_viewer = ImageViewer(self.reference, None, self.tr('Reference')) self.table_widget = QTableWidget(5, 2) self.table_widget.setHorizontalHeaderLabels( [self.tr('Index'), self.tr('Value')]) self.table_widget.setItem(0, 0, QTableWidgetItem(self.tr('MSE'))) self.table_widget.setItem(1, 0, QTableWidgetItem(self.tr('COVAR'))) self.table_widget.setItem(2, 0, QTableWidgetItem(self.tr('PSNR'))) self.table_widget.setItem(3, 0, QTableWidgetItem(self.tr('SSIM'))) self.table_widget.setItem(4, 0, QTableWidgetItem(self.tr('CORR'))) for i in range(self.table_widget.rowCount()): modify_font(self.table_widget.item(i, 0), bold=True) self.table_widget.setEnabled(False) load_button.clicked.connect(self.load) self.normal_radio.toggled.connect(self.change) self.diff_radio.toggled.connect(self.change) self.gray_check.stateChanged.connect(self.change) self.equal_check.stateChanged.connect(self.change) self.map_radio.toggled.connect(self.change) self.evidence_viewer.view_changed.connect( self.reference_viewer.change_view) self.reference_viewer.view_changed.connect( self.evidence_viewer.change_view) top_layout = QHBoxLayout() top_layout.addWidget(load_button) top_layout.addWidget(self.file_label) top_layout.addStretch() top_layout.addWidget(self.comp_label) top_layout.addWidget(self.normal_radio) top_layout.addWidget(self.map_radio) top_layout.addWidget(self.diff_radio) top_layout.addWidget(self.gray_check) top_layout.addWidget(self.equal_check) index_layout = QVBoxLayout() index_label = QLabel(self.tr('Image Quality Assessment')) modify_font(index_label, bold=True) index_layout.addWidget(index_label) index_layout.addWidget(self.table_widget) center_layout = QHBoxLayout() center_layout.addWidget(self.evidence_viewer) center_layout.addWidget(self.reference_viewer) center_layout.addLayout(index_layout) main_layout = QVBoxLayout() main_layout.addLayout(top_layout) main_layout.addLayout(center_layout) self.setLayout(main_layout) def load(self): filename, basename, image = load_image(self) if filename is None: return if image.shape != self.evidence.shape: QMessageBox.critical( self, self.tr('Error'), self.tr('Evidence and reference must have the same size!')) return self.file_label.setText(basename) self.reference = image self.difference = normalize_mat( cv.absdiff(self.evidence, self.reference)) self.equalized = cv.merge( [cv.equalizeHist(c) for c in cv.split(self.difference)]) x = cv.cvtColor(self.evidence, cv.COLOR_BGR2GRAY).astype(np.float32) y = cv.cvtColor(self.reference, cv.COLOR_BGR2GRAY).astype(np.float32) mse = self.mse(x, y) covar = self.covar(x, y) psnr = self.psnr(mse) ssim, self.indexmap = self.ssim(x, y) corr = self.corr(x, y) self.table_widget.setItem(0, 1, QTableWidgetItem('{:.4f}'.format(mse))) self.table_widget.setItem(1, 1, QTableWidgetItem('{:.4f}'.format(covar))) if psnr > 0: self.table_widget.setItem( 2, 1, QTableWidgetItem('{:.2f} dB'.format(psnr))) else: self.table_widget.setItem( 2, 1, QTableWidgetItem('+' + u'\u221e' + ' dB')) self.table_widget.setItem(3, 1, QTableWidgetItem('{:.4f}'.format(ssim))) self.table_widget.setItem(4, 1, QTableWidgetItem('{:.4f}'.format(corr))) self.table_widget.setEnabled(True) self.comp_label.setEnabled(True) self.normal_radio.setEnabled(True) self.diff_radio.setEnabled(True) self.gray_check.setEnabled(True) self.equal_check.setEnabled(True) self.map_radio.setEnabled(True) self.change() def change(self): self.gray_check.setEnabled(False) self.equal_check.setEnabled(False) if self.normal_radio.isChecked(): self.reference_viewer.update_original(self.reference) self.last_radio = self.normal_radio elif self.diff_radio.isChecked(): self.gray_check.setEnabled(True) self.equal_check.setEnabled(True) if self.equal_check.isChecked(): result = self.equalized else: result = self.difference if self.gray_check.isChecked(): result = desaturate(result) self.reference_viewer.update_original(result) self.last_radio = self.diff_radio elif self.map_radio.isChecked(): self.reference_viewer.update_original(self.indexmap) self.last_radio = self.map_radio else: self.last_radio.setChecked(True) @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 indexmap = cv.divide(t3, t1) ssim = cv.mean(indexmap)[0] return ssim, 255 - normalize_mat(indexmap, to_bgr=True) @staticmethod def corr(x, y): return np.corrcoef(x, y)[0, 1] @staticmethod def covar(x, y): return np.std(cv.absdiff(x, y)) @staticmethod def mse(x, y): return cv.mean(cv.pow(x - y, 2))[0] @staticmethod def psnr(mse): k = math.sqrt(mse) if k == 0: return -1 return 20 * math.log10((255**2) / k)