def compute_map(self): if self.map is None: start = time() self.map_button.setText( self.tr('Computing heatmap, please wait...')) modify_font(self.map_button, bold=False, italic=True) QCoreApplication.processEvents() mapp, valid, range0, range1, imgsize, other = noiseprint_blind_post( self.noise, self.image0) if mapp is None: QMessageBox.critical(self, self.tr('Error'), self.tr('Too many invalid blocks!')) return self.map = cv.applyColorMap( genMappUint8(mapp, valid, range0, range1, imgsize), cv.COLORMAP_JET) self.map_viewer.update_processed(self.map) elapsed = time() - start self.map_button.setText( self.tr('Heatmap computed ({:.1f} s)'.format(elapsed))) modify_font(self.map_button, bold=False, italic=False) self.map_button.setCheckable(True) self.map_button.setChecked(True) self.noise_viewer.viewChanged.connect(self.map_viewer.changeView) self.map_viewer.viewChanged.connect(self.noise_viewer.changeView)
def __init__(self, filename, parent=None): super(LocationWidget, self).__init__(parent) self.temp_dir = QTemporaryDir() if self.temp_dir.isValid(): with exiftool.ExifTool(exiftool_exe()) as et: temp_file = os.path.join(self.temp_dir.path(), "geo.html") metadata = et.get_metadata(filename) try: lat = metadata["Composite:GPSLatitude"] long = metadata["Composite:GPSLongitude"] except KeyError: label = QLabel(self.tr("Geolocation data not found!")) modify_font(label, bold=True) label.setStyleSheet("color: #FF0000") label.setAlignment(Qt.AlignCenter) layout = QVBoxLayout() layout.addWidget(label) self.setLayout(layout) return html = '<iframe src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2948.532014673314!2d{}!3d{}!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x0%3A0x0!2zNDLCsDIxJzA5LjAiTiA3McKwMDUnMjguMiJX!5e0!3m2!1sit!2sit!4v1590074026898!5m2!1sit!2sit" width="600" height="450" frameborder="0" style="border:0;" allowfullscreen="" aria-hidden="false" tabindex="0"></iframe>'.format( long, lat) with open(temp_file, "w") as file: file.write(html) web_view = QWebEngineView() web_view.load(QUrl("file://" + temp_file)) layout = QVBoxLayout() layout.addWidget(web_view) self.setLayout(layout)
def __init__(self, image, parent=None): super(SplicingWidget, self).__init__(parent) self.image = image self.image0 = cv.cvtColor(self.image, cv.COLOR_BGR2GRAY).astype( np.float32) / 255 self.noise = self.map = None self.noise_button = QPushButton(self.tr('(1/2) Estimate noise')) modify_font(self.noise_button, bold=True) gray = np.full_like(self.image, 127) self.noise_viewer = ImageViewer(self.image, gray, self.tr('Estimated noise print'), export=True) self.map_button = QPushButton(self.tr('(2/2) Compute heatmap')) modify_font(self.map_button, bold=True) self.map_button.setEnabled(False) self.map_viewer = ImageViewer(self.image, gray, self.tr('Splicing probability heatmap')) self.noise_button.clicked.connect(self.estimate_noise) self.noise_button.toggled.connect(self.estimate_noise) self.map_button.clicked.connect(self.compute_map) self.map_button.toggled.connect(self.compute_map) main_layout = QGridLayout() main_layout.addWidget(self.noise_viewer, 0, 0) main_layout.addWidget(self.noise_button, 1, 0) main_layout.addWidget(self.map_viewer, 0, 1) main_layout.addWidget(self.map_button, 1, 1) self.setLayout(main_layout)
def process(self): if self.prob is None: return mask = self.var < self.variance_spin.value() if self.filter_check.isChecked(): prob = cv.medianBlur(self.prob.astype(np.float32), 3) else: prob = self.prob.astype(np.float32) if self.showprob_check.isChecked(): output = np.repeat(prob[:, :, np.newaxis], 3, axis=2) output[mask] = 0 else: thr = self.threshold_spin.value() output = np.zeros((prob.shape[0], prob.shape[1], 3)) blue, green, red = cv.split(output) blue[mask] = 1 green[prob < thr] = 1 green[mask] = 0 red[prob >= thr] = 1 red[mask] = 0 output = cv.merge((blue, green, red)) output = cv.convertScaleAbs(output, None, 255) output = cv.resize(output, None, None, self.block, self.block, cv.INTER_LINEAR) self.viewer.update_processed( np.copy(output[:self.image.shape[0], :self.image.shape[1]])) avgprob = cv.mean(prob, 1 - mask.astype(np.uint8))[0] * 100 self.avgprob_label.setText(self.tr( 'Average = {:.2f}%'.format(avgprob))) modify_font(self.avgprob_label, italic=False, bold=True) self.process_button.setEnabled(False)
def show_error(self, message): error_label = QLabel(message) 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)
def search(self, pattern, row, col, direction): nocase = not self.case_button.isChecked() word = self.word_button.isChecked() regex = self.regex_button.isChecked() matches = 0 index = 0 if direction > 0: row_range = range(self.table_widget.rowCount() - 1, -1, -1) col_range = range(self.table_widget.columnCount() - 1, -1, -1) else: row_range = range(self.table_widget.rowCount()) col_range = range(self.table_widget.columnCount()) for i in row_range: for j in col_range: item = self.table_widget.item(i, j) if item is not None: text = item.text() if regex: match = QRegularExpression(pattern).match( text).hasMatch() else: if nocase: text = text.lower() pattern = pattern.lower() if word: match = text == pattern else: match = pattern in text if match and pattern: self.table_widget.item(i, j).setBackground(Qt.yellow) if (direction > 0 and (i > row or i == row and j > col)) or ( direction < 0 and (i < row or i == row and j < col)): self.table_widget.setCurrentCell(i, j) index = matches matches += 1 else: self.table_widget.item(i, j).setBackground(Qt.transparent) if pattern: self.matches_label.setVisible(True) if matches > 0: match = matches - index if direction > 0 else index + 1 self.matches_label.setText( self.tr(f"match #{match}/{matches}")) self.matches_label.setStyleSheet("color: #000000") modify_font(self.matches_label, bold=True) else: self.matches_label.setText(self.tr("not found!")) self.matches_label.setStyleSheet("color: #FF0000") modify_font(self.matches_label, italic=True) else: self.matches_label.setText("")
def create_table(matrix): table_widget = QTableWidget(DCT_SIZE, DCT_SIZE) hsv = np.array([[[0, 192, 255]]]) maximum = clip_value(np.max(matrix) - 1, minv=1) for i in range(DCT_SIZE): for j in range(DCT_SIZE): value = matrix[i, j] item = QTableWidgetItem(str(value)) item.setTextAlignment(Qt.AlignCenter) hsv[0, 0, 0] = 64 - 64 * ((value - 1) / maximum) rgb = cv.cvtColor(hsv.astype(np.uint8), cv.COLOR_HSV2RGB) item.setBackgroundColor( QColor(rgb[0, 0, 0], rgb[0, 0, 1], rgb[0, 0, 2])) table_widget.setItem(i, j, item) table_widget.resizeRowsToContents() table_widget.resizeColumnsToContents() table_widget.setEditTriggers(QAbstractItemView.NoEditTriggers) table_widget.setSelectionMode(QAbstractItemView.SingleSelection) modify_font(table_widget, mono=True) return table_widget
def estimate_noise(self): if self.noise is None: start = time() self.noise_button.setText( self.tr('Estimating noise, please wait...')) modify_font(self.noise_button, bold=False, italic=True) QCoreApplication.processEvents() qf = estimate_qf(self.image) self.noise = genNoiseprint(self.image0, qf, model_name='net') vmin, vmax, _, _ = cv.minMaxLoc(self.noise[34:-34, 34:-34]) self.noise_viewer.update_processed( norm_mat(self.noise.clip(vmin, vmax), to_bgr=True)) elapsed = time() - start self.noise_button.setText( self.tr('Noise estimated ({:.1f} s)'.format(elapsed))) modify_font(self.noise_button, bold=False, italic=False) self.map_button.setEnabled(True) self.noise_button.setCheckable(True) self.noise_button.setChecked(True)
def __init__(self, interval, step, ticks, reset=0, suffix='', parent=None): super(ParamSlider, self).__init__(parent) self.slider = QSlider(Qt.Horizontal) self.slider.setRange(interval[0], interval[1]) self.slider.setTickPosition(QSlider.TicksBelow) self.slider.setTickInterval((interval[1] - interval[0] + 1) / ticks) self.slider.setSingleStep(1) # self.slider.setPageStep(step) self.slider.setPageStep(1) self.slider.setValue(reset) self.slider.mouseDoubleClickEvent = self.double_click self.label = QLabel() modify_font(self.label, bold=True) self.suffix = suffix self.reset = reset self.sync(reset) self.slider.valueChanged.connect(self.sync) layout = QHBoxLayout() layout.addWidget(self.slider) layout.addWidget(self.label) self.setLayout(layout)
def __init__(self, parent=None): super(ToolTree, self).__init__(parent) group_names = [] tool_names = [] tool_infos = [] tool_progress = [ ] # 0 = da fare, 1 = iniziato, 2 = funzionante, 3 = completo # [0] group_names.append(self.tr('[General]')) tool_names.append([ self.tr('Original Image'), self.tr('File Digest'), self.tr('Hex Editor'), self.tr('Reverse Search') ]) tool_infos.append([ self.tr( 'Display the unaltered reference image for visual inspection'), self. tr('Retrieve file information and compute many hashes and ballistics' ), self. tr('Open an external hexadecimal editor to show and edit raw bytes' ), self.tr( 'Use online search services to find visually similar images') ]) tool_progress.extend([3, 3, 2, 2]) # [1] group_names.append(self.tr('[Metadata]')) tool_names.append([ self.tr('Header Structure'), self.tr('Metadata Extraction'), self.tr('Thumbnail Analysis'), self.tr('Geolocation data') ]) tool_infos.append([ self. tr('Dump the physical EXIF structure and display an interactive view' ), self. tr('Scan through file metadata and gather all available information' ), self.tr( 'Extract optional embedded thumbnail and compare with original' ), self. tr('Retrieve optional geo-location data and show it on a world map' ) ]) tool_progress.extend([3, 3, 3, 2]) # [2] group_names.append(self.tr('[Inspection]')) tool_names.append([ self.tr('Enhancing Magnifier'), self.tr('Reference Comparison'), self.tr('Global Adjustments'), self.tr('Fourier Transform') ]) tool_infos.append([ self. tr('Use a loupe with visual enhancement for better identifying forgeries' ), self. tr('Open a synchronized double view to compare two different pictures' ), self. tr('Apply standard adjustments (contrast, brightness, hue, saturation)' ), self. tr('Compute amplitude and phase components of the 2D Fourier Transform' ) ]) tool_progress.extend([2, 2, 2, 3]) # [3] group_names.append(self.tr('[JPEG]')) tool_names.append([ self.tr('Quality Estimation'), self.tr('Error Level Analysis'), self.tr('Multiple Compression'), self.tr('DCT Dimples Map') ]) tool_infos.append([ self. tr('Extract quantization tables and estimate last saved JPEG quality' ), self. tr('Show pixel-level difference against different compression levels' ), self. tr('Use residuals to detect multiple compressions at different levels' ), self. tr('Analyze periodic quantization artifacts to detect manipulations' ) ]) tool_progress.extend([2, 3, 1, 0]) # [4] group_names.append(self.tr('[Colors]')) tool_names.append([ self.tr('RGB/HSV Plots'), self.tr('PCA Projection'), self.tr('Pixel Statistics'), self.tr('Space Conversion') ]) tool_infos.append([ self.tr( 'Display interactive 2D and 3D plots of RGB and HSV pixel data' ), self. tr('Use color PCA to project RGB values onto reduced vector spaces' ), self.tr( 'Compute minimum/maximum/average RGB values for every pixel'), self. tr('Convert color channels into RGB/HSV/YCbCr/Lab/Luv/CMYK spaces') ]) tool_progress.extend([0, 2, 3, 2]) # [5] group_names.append(self.tr('[Tonality]')) tool_names.append([ self.tr('Luminance Gradient'), self.tr('Echo Edge Filter'), self.tr('Correlation Plot'), self.tr('Wavelet Threshold') ]) tool_infos.append([ self. tr('Analyze horizontal/vertical brightness variations across the image' ), self. tr('Use derivative filters to reveal artificial out-of-focus zones' ), self.tr( 'Exploit spatial correlation patterns among neighboring pixels' ), self. tr('Reconstruct image with different wavelet coefficient thresholds' ) ]) tool_progress.extend([2, 3, 0, 0]) # [6] group_names.append(self.tr('[Noise]')) tool_names.append([ self.tr('Noise Estimation'), self.tr('Min/Max Deviation'), self.tr('Image Bit Planes'), self.tr('Frequency Separation') ]) tool_infos.append([ self.tr( 'Estimate and visualize gaussian noise components of the image' ), self. tr('Highlight pixels deviating from block-based min/max statistics' ), self. tr('Visualize bit planes values to find different noise patterns'), self. tr('Estimate high/low frequency components of the luminance channel' ) ]) tool_progress.extend([3, 2, 3, 0]) # [7] group_names.append(self.tr('[Tampering]')) tool_names.append([ self.tr('Contrast Enhancement'), self.tr('Region Cloning'), self.tr('Image Resampling'), self.tr('Composite Splicing') ]) tool_infos.append([ self.tr( 'Analyze color distribuions to detect contrast enhancements'), self. tr('Use feature descriptors for copy/rotate clone area detection'), self. tr('Analyze 2D pixel interpolation for detecting resampling traces' ), self.tr( 'Exploit DCT statistics for automatic splicing zone detection') ]) tool_progress.extend([0, 0, 0, 0]) count = 0 for i, group in enumerate(group_names): group_item = QTreeWidgetItem() group_item.setText(0, group) font = group_item.font(0) font.setBold(True) group_item.setFont(0, font) group_item.setData(0, Qt.UserRole, False) group_item.setIcon(0, QIcon('icons/{}.svg'.format(i))) for j, tool in enumerate(tool_names[i]): tool_item = QTreeWidgetItem(group_item) tool_item.setText(0, tool) tool_item.setData(0, Qt.UserRole, True) tool_item.setData(0, Qt.UserRole + 1, i) tool_item.setData(0, Qt.UserRole + 2, j) tool_item.setToolTip(0, tool_infos[i][j]) if tool_progress[count] == 0: modify_font(tool_item, italic=True) count += 1 self.addTopLevelItem(group_item) self.expandAll() self.setColumnCount(1) self.header().setVisible(False) self.setMaximumWidth(300) self.version = '{:.2f}a'.format(sum(tool_progress) / (count * 3))
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 __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)
def __init__(self, parent=None): super(ToolTree, self).__init__(parent) group_names = [] tool_names = [] tool_infos = [] tool_progress = [ ] # 0 = da fare, 1 = debug, 2 = funzionante, 3 = completo # [0] group_names.append(self.tr("[General]")) tool_names.append([ self.tr("Original Image"), self.tr("File Digest"), self.tr("Hex Editor"), self.tr("Similar Search") ]) tool_infos.append([ self.tr( "Display the unaltered reference image for visual inspection"), self. tr("Retrieve physical file information, crypto and perceptual hashes" ), self. tr("Open an external hexadecimal editor to show and edit raw bytes" ), self.tr( "Browse online search services to find visually similar images" ), ]) tool_progress.extend([3, 3, 2, 2]) # [1] group_names.append(self.tr("[Metadata]")) tool_names.append([ self.tr("Header Structure"), self.tr("EXIF Full Dump"), self.tr("Thumbnail Analysis"), self.tr("Geolocation data"), ]) tool_infos.append([ self. tr("Dump the file header structure and display an interactive view" ), self. tr("Scan through file metadata and gather all available information" ), self.tr( "Extract optional embedded thumbnail and compare with original" ), self.tr( "Retrieve optional geolocation data and show it on a world map" ), ]) tool_progress.extend([3, 3, 3, 2]) # [2] group_names.append(self.tr("[Inspection]")) tool_names.append([ self.tr("Enhancing Magnifier"), self.tr("Channel Histogram"), self.tr("Global Adjustments"), self.tr("Reference Comparison"), ]) tool_infos.append([ self. tr("Magnifying glass with enhancements for better identifying forgeries" ), self. tr("Display single color channels or RGB composite interactive histogram" ), self. tr("Apply standard image adjustments (brightness, hue, saturation, ...)" ), self. tr("Open a synchronized double view for comparison with another picture" ), ]) tool_progress.extend([3, 3, 3, 3]) # [3] group_names.append(self.tr("[Detail]")) tool_names.append([ self.tr("Luminance Gradient"), self.tr("Echo Edge Filter"), self.tr("Wavelet Threshold"), self.tr("Frequency Split"), ]) tool_infos.append([ self. tr("Analyze horizontal/vertical brightness variations across the image" ), self. tr("Use derivative filters to reveal artificial out-of-focus regions" ), self. tr("Reconstruct image with different wavelet coefficient thresholds" ), self.tr( "Divide image luminance into high and low frequency components" ), ]) tool_progress.extend([3, 3, 3, 3]) # [4] group_names.append(self.tr("[Colors]")) tool_names.append([ self.tr("RGB/HSV Plots"), self.tr("Space Conversion"), self.tr("PCA Projection"), self.tr("Pixel Statistics"), ]) tool_infos.append([ self. tr("Display interactive 2D and 3D plots of RGB and HSV pixel values" ), self. tr("Convert RGB channels into HSV/YCbCr/Lab/Luv/CMYK/Gray spaces"), self.tr( "Use color PCA to project pixel onto most salient components"), self.tr( "Compute minimum/maximum/average RGB values for every pixel"), ]) tool_progress.extend([3, 3, 3, 3]) # [5] group_names.append(self.tr("[Noise]")) tool_names.append([ self.tr("Noise Separation"), self.tr("Min/Max Deviation"), self.tr("Bit Plane Values"), self.tr("PRNU Identification"), ]) tool_infos.append([ self.tr( "Estimate and extract different kind of image noise components" ), self. tr("Highlight pixels deviating from block-based min/max statistics" ), self. tr("Show individual bit planes to find inconsistent noise patterns" ), self. tr("Exploit sensor pattern noise introduced by different cameras"), ]) tool_progress.extend([3, 3, 3, 0]) # [6] group_names.append(self.tr("[JPEG]")) tool_names.append([ self.tr("Quality Estimation"), self.tr("Error Level Analysis"), self.tr("Multiple Compression"), self.tr("JPEG Ghost Maps"), ]) tool_infos.append([ self. tr("Extract quantization tables and estimate last saved JPEG quality" ), self.tr( "Show pixel-wise differences against a fixed compression level" ), self.tr( "Use a machine learning model to detect multiple compression"), self. tr("Highlight traces of different compressions in difference images" ), ]) tool_progress.extend([3, 3, 0, 0]) # [7] group_names.append(self.tr("[Tampering]")) tool_names.append([ self.tr("Contrast Enhancement"), self.tr("Copy-Move Forgery"), self.tr("Composite Splicing"), self.tr("Image Resampling"), ]) tool_infos.append([ self.tr( "Analyze color distributions to detect contrast enhancements"), self.tr( "Use invariant feature descriptors to detect cloned regions"), self. tr("Exploit DCT statistics for automatic splicing zone detection"), self. tr("Estimate 2D pixel interpolation for detecting resampling traces" ), ]) tool_progress.extend([3, 2, 3, 0]) # [8] group_names.append(self.tr("[Various]")) tool_names.append([ self.tr("Median Filtering"), self.tr("Illuminant Map"), self.tr("Dead/Hot Pixels"), self.tr("Stereogram Decoder"), ]) tool_infos.append([ self.tr( "Detect nonlinear processing traces left by median filtering"), self.tr( "Estimate scene local light direction on estimated 3D surfaces" ), self.tr( "Detect and fix dead/hot pixels caused by sensor imperfections" ), self.tr( "Decode 3D images concealed inside crossed-eye autostereograms" ), ]) tool_progress.extend([2, 0, 0, 3]) count = 0 for i, group in enumerate(group_names): group_item = QTreeWidgetItem() group_item.setText(0, group) font = group_item.font(0) font.setBold(True) group_item.setFont(0, font) group_item.setData(0, Qt.UserRole, False) group_item.setIcon(0, QIcon(f"icons/{i}.svg")) for j, tool in enumerate(tool_names[i]): tool_item = QTreeWidgetItem(group_item) tool_item.setText(0, tool) tool_item.setData(0, Qt.UserRole, True) tool_item.setData(0, Qt.UserRole + 1, i) tool_item.setData(0, Qt.UserRole + 2, j) tool_item.setToolTip(0, tool_infos[i][j]) if tool_progress[count] == 0: modify_font(tool_item, italic=True) count += 1 self.addTopLevelItem(group_item) self.expandAll() self.setColumnCount(1) self.header().setVisible(False) self.setMaximumWidth(300) self.version = f"{sum(tool_progress) / 100:.2f}a"
def __init__(self, image, parent=None): super(PcaWidget, self).__init__(parent) self.comp_combo = QComboBox() self.comp_combo.addItems([self.tr('#{}'.format(i + 1)) for i in range(3)]) self.distvect_radio = QRadioButton(self.tr('Vector Distance')) self.cross_radio = QRadioButton(self.tr('Cross Correlation')) self.distvect_radio.setChecked(True) self.last_radio = self.distvect_radio self.image = image self.components = [] rows, cols, dims = self.image.shape bgr = np.reshape(self.image, (rows * cols, dims)).astype(np.float32) m, eigen_vec, eigen_val = cv.PCACompute2(bgr, np.array([])) p = self.image.astype(np.float32) - m for v in eigen_vec: c = np.cross(p, v) d = np.linalg.norm(c, axis=2) / np.linalg.norm(v) distance = normalize_mat(d, to_bgr=True) cross = cv.merge([normalize_mat(x) for x in cv.split(c)]) self.components.extend([distance, cross]) table_data = [[m[0, 2], m[0, 1], m[0, 0]], [eigen_vec[0, 2], eigen_vec[0, 1], eigen_vec[0, 0]], [eigen_vec[1, 2], eigen_vec[1, 1], eigen_vec[1, 0]], [eigen_vec[2, 2], eigen_vec[2, 1], eigen_vec[2, 0]], [eigen_val[2, 0], eigen_val[1, 0], eigen_val[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 color'))) table_widget.setItem(1, 0, QTableWidgetItem(self.tr('Eigen vect 1'))) table_widget.setItem(2, 0, QTableWidgetItem(self.tr('Eigen vect 2'))) table_widget.setItem(3, 0, QTableWidgetItem(self.tr('Eigen vect 3'))) table_widget.setItem(4, 0, QTableWidgetItem(self.tr('Eigen values'))) 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(m[0, 2], m[0, 1], m[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(self.image, self.image, None) self.process() self.comp_combo.currentIndexChanged.connect(self.process) self.distvect_radio.clicked.connect(self.process) self.cross_radio.clicked.connect(self.process) top_layout = QHBoxLayout() top_layout.addWidget(QLabel(self.tr('Component:'))) top_layout.addWidget(self.comp_combo) top_layout.addWidget(QLabel(self.tr('Projection:'))) top_layout.addWidget(self.distvect_radio) top_layout.addWidget(self.cross_radio) 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 __init__(self, image, parent=None): super(CloningWidget, self).__init__(parent) self.detector_combo = QComboBox() self.detector_combo.addItems( [self.tr('BRISK'), self.tr('ORB'), self.tr('AKAZE')]) self.detector_combo.setCurrentIndex(0) self.detector_combo.setToolTip( self.tr('Algorithm used for localization and description')) self.response_spin = QSpinBox() self.response_spin.setRange(0, 100) self.response_spin.setSuffix(self.tr('%')) self.response_spin.setValue(90) self.response_spin.setToolTip( self.tr('Maximum keypoint response to perform matching')) self.matching_spin = QSpinBox() self.matching_spin.setRange(1, 100) self.matching_spin.setSuffix(self.tr('%')) self.matching_spin.setValue(20) self.matching_spin.setToolTip( self.tr('Maximum metric difference to accept matching')) self.distance_spin = QSpinBox() self.distance_spin.setRange(1, 100) self.distance_spin.setSuffix(self.tr('%')) self.distance_spin.setValue(15) self.distance_spin.setToolTip( self.tr('Maximum distance between matches in the same cluster')) self.cluster_spin = QSpinBox() self.cluster_spin.setRange(1, 20) self.cluster_spin.setValue(5) self.cluster_spin.setToolTip( self.tr('Minimum number of keypoints to create a new cluster')) self.kpts_check = QCheckBox(self.tr('Show keypoints')) self.kpts_check.setToolTip(self.tr('Show keypoint coverage')) self.nolines_check = QCheckBox(self.tr('Hide lines')) self.nolines_check.setToolTip(self.tr('Disable match line drawing')) self.process_button = QToolButton() self.process_button.setText(self.tr('Process')) self.process_button.setToolTip(self.tr('Perform automatic detection')) modify_font(self.process_button, bold=True) self.status_label = QLabel() self.mask_label = QLabel() self.mask_button = QToolButton() self.mask_button.setText(self.tr('Load mask...')) self.mask_button.setToolTip( self.tr('Load an image to be used as mask')) self.onoff_button = QToolButton() self.onoff_button.setText(self.tr('OFF')) self.onoff_button.setCheckable(True) self.onoff_button.setToolTip(self.tr('Toggle keypoint detection mask')) self.image = image self.viewer = ImageViewer(self.image, self.image) self.gray = cv.cvtColor(self.image, cv.COLOR_BGR2GRAY) self.total = self.kpts = self.desc = self.matches = self.clusters = self.mask = None self.canceled = False self.detector_combo.currentIndexChanged.connect(self.update_detector) self.response_spin.valueChanged.connect(self.update_detector) self.matching_spin.valueChanged.connect(self.update_matching) self.distance_spin.valueChanged.connect(self.update_cluster) self.cluster_spin.valueChanged.connect(self.update_cluster) self.nolines_check.stateChanged.connect(self.process) self.kpts_check.stateChanged.connect(self.process) self.process_button.clicked.connect(self.process) self.mask_button.clicked.connect(self.load_mask) self.onoff_button.toggled.connect(self.toggle_mask) self.onoff_button.setEnabled(False) top_layout = QHBoxLayout() top_layout.addWidget(QLabel(self.tr('Detector:'))) top_layout.addWidget(self.detector_combo) top_layout.addWidget(QLabel(self.tr('Response:'))) top_layout.addWidget(self.response_spin) top_layout.addWidget(QLabel(self.tr('Matching:'))) top_layout.addWidget(self.matching_spin) top_layout.addWidget(QLabel(self.tr('Distance:'))) top_layout.addWidget(self.distance_spin) top_layout.addWidget(QLabel(self.tr('Cluster:'))) top_layout.addWidget(self.cluster_spin) top_layout.addWidget(self.nolines_check) top_layout.addWidget(self.kpts_check) top_layout.addStretch() bottom_layout = QHBoxLayout() bottom_layout.addWidget(self.process_button) bottom_layout.addWidget(self.status_label) bottom_layout.addStretch() bottom_layout.addWidget(self.mask_button) bottom_layout.addWidget(self.onoff_button) main_layout = QVBoxLayout() main_layout.addLayout(top_layout) main_layout.addLayout(bottom_layout) main_layout.addWidget(self.viewer) self.setLayout(main_layout)
def cancel(self): self.canceled = True self.status_label.setText(self.tr('Processing interrupted!')) modify_font(self.status_label, bold=False, italic=False)
def __init__(self, filename, image, parent=None): super(QualityWidget, self).__init__(parent) x = np.arange(1, 101) y = loss_curve(image) tail = 5 qm = np.argmin(y[:-tail]) + 1 if qm == 100 - tail: qm = 100 figure = Figure() canvas = FigureCanvas(figure) axes = canvas.figure.subplots() axes.plot(x, y * 100, label="compression loss") axes.fill_between(x, y * 100, alpha=0.2) axes.axvline(qm, linestyle=":", color="k", label=f"min error (q = {qm})") xt = axes.get_xticks() xt = np.append(xt, 1) axes.set_xticks(xt) axes.set_xlim([1, 100]) axes.set_ylim([0, 100]) axes.set_xlabel(self.tr("JPEG quality (%)")) axes.set_ylabel(self.tr("average error (%)")) axes.grid(True, which="both") axes.legend(loc="upper center") axes.figure.canvas.draw() figure.set_tight_layout(True) main_layout = QVBoxLayout() main_layout.addWidget(canvas) MRK = b"\xFF" SOI = b"\xD8" DQT = b"\xDB" # DHT = b'\xC4' MSK = b"\x0F" PAD = b"\x00" MAX_TABLES = 2 LEN_OFFSET = 2 LUMA_IDX = 0 CHROMA_IDX = 1 luma = np.zeros((DCT_SIZE, DCT_SIZE), dtype=int) chroma = np.zeros((DCT_SIZE, DCT_SIZE), dtype=int) temp_file = QTemporaryFile() try: if temp_file.open(): copyfile(filename, temp_file.fileName()) subprocess.run( [ exiftool_exe(), "-all=", "-overwrite_original", temp_file.fileName() ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) found = False with open(temp_file.fileName(), "rb") as file: first = file.read(1) if first not in [MRK, SOI]: raise ValueError(self.tr("File is not a JPEG image!")) while True: if not self.find_next(file, [MRK, DQT, PAD]): break length = file.read(1)[0] - LEN_OFFSET if length <= 0 or length % (TABLE_SIZE + 1) != 0: continue while length > 0: mode = file.read(1) if not mode: break index = mode[0] & MSK[0] if index >= MAX_TABLES: break length -= 1 for k in range(TABLE_SIZE): b = file.read(1)[0] if not b: break length -= 1 i, j = ZIG_ZAG[k] if index == LUMA_IDX: luma[i, j] = b elif index == CHROMA_IDX: chroma[i, j] = b else: found = True if not found: raise ValueError(self.tr("Unable to find JPEG tables!")) levels = [(1 - (np.mean(t.ravel()[1:]) - 1) / 254) * 100 for t in [luma, chroma]] distance = np.zeros(101) for qm in range(101): lu, ch = cv.split(get_tables(qm)) lu_diff = np.mean(cv.absdiff(luma, lu)) ch_diff = np.mean(cv.absdiff(chroma, ch)) distance[qm] = (lu_diff + 2 * ch_diff) / 3 closest = np.argmin(distance) deviation = distance[closest] if deviation == 0: quality = closest message = "(standard tables)" else: quality = int(np.round(closest - deviation)) message = f"(deviation from standard tables = {deviation:.4f})" if quality == 0: quality = 1 quality_label = QLabel( self.tr( f"[JPEG FORMAT] Last saved quality: {quality}% {message}")) modify_font(quality_label, bold=True) luma_label = QLabel( self. tr(f"Luminance Quantization Table (level = {levels[0]:.2f}%)\n" )) luma_label.setAlignment(Qt.AlignCenter) modify_font(luma_label, underline=True) luma_table = self.create_table(luma) luma_table.setFixedSize(420, 190) chroma_label = QLabel( self. tr(f"Chrominance Quantization Table (level = {levels[1]:.2f}%)\n" )) chroma_label.setAlignment(Qt.AlignCenter) modify_font(chroma_label, underline=True) chroma_table = self.create_table(chroma) chroma_table.setFixedSize(420, 190) table_layout = QGridLayout() table_layout.addWidget(luma_label, 0, 0) table_layout.addWidget(luma_table, 1, 0) table_layout.addWidget(chroma_label, 0, 1) table_layout.addWidget(chroma_table, 1, 1) table_layout.addWidget(quality_label, 2, 0, 1, 2) main_layout.addLayout(table_layout) except ValueError: modelfile = "models/jpeg_qf.mdl" try: model = load(modelfile) limit = model.best_ntree_limit if hasattr( model, "best_ntree_limit") else None # f = self.get_features(image) # p = model.predict_proba(f, ntree_limit=limit)[0, 0] qp = model.predict(np.reshape(y, (1, len(y))), ntree_limit=limit)[0] # if p > 0.5: # p = 2 * (p - 0.5) * 100 # output = self.tr('Uncompressed image (p = {:.2f}%)'.format(p)) # else: # p = (1 - 2 * p) * 100 # output = self.tr('Compressed image (p = {:.2f}%) ---> Estimated JPEG quality = {}%'.format(p, qm)) message = self.tr( f"[LOSSLESS FORMAT] Estimated last saved quality = {qp:.1f}%{'' if qp <= 99 else ' (uncompressed)'}" ) if qp == 100: message += " (uncompressed)" prob_label = QLabel(message) modify_font(prob_label, bold=True) main_layout.addWidget(prob_label) except FileNotFoundError: QMessageBox.critical( self, self.tr("Error"), self.tr(f'Model not found ("{modelfile}")!')) main_layout.addStretch() self.setLayout(main_layout)
def __init__(self, image, parent=None): super(ToolWidget, self).__init__(parent) self.rgb_radio = QRadioButton(self.tr('RGB')) self.rgb_radio.setChecked(True) self.last_radio = self.rgb_radio self.red_radio = QRadioButton(self.tr('Red')) self.green_radio = QRadioButton(self.tr('Green')) self.blue_radio = QRadioButton(self.tr('Blue')) self.value_radio = QRadioButton(self.tr('Value')) self.log_check = QCheckBox(self.tr('Log scale')) self.grid_check = QCheckBox(self.tr('Show grid')) self.marker_check = QCheckBox(self.tr('Show markers')) self.marker_check.setToolTip( self.tr('Show plot markers for min(--), avg(-), max(-.)')) self.start_slider = ParamSlider([0, 255], 8, 0, label='Start:', bold=True) self.end_slider = ParamSlider([0, 255], 8, 255, label='End:', bold=True) channels = cv.split(cv.cvtColor(image, cv.COLOR_BGR2RGB)) channels.append(cv.cvtColor(image, cv.COLOR_BGR2GRAY)) self.hist = [compute_hist(c) for c in channels] rows, cols, chans = image.shape pixels = rows * cols unique = np.unique(np.reshape(image, (pixels, chans)), axis=0).shape[0] unique_ratio = unique / pixels * 100 unique_label = QLabel( self.tr('total pixels = {}, unique colors = {} ({:.2f}%) '.format( pixels, unique, unique_ratio))) modify_font(unique_label, italic=True) self.rgb_radio.clicked.connect(self.redraw) self.red_radio.clicked.connect(self.redraw) self.green_radio.clicked.connect(self.redraw) self.blue_radio.clicked.connect(self.redraw) self.value_radio.clicked.connect(self.redraw) self.log_check.stateChanged.connect(self.redraw) self.grid_check.stateChanged.connect(self.redraw) self.marker_check.stateChanged.connect(self.redraw) self.start_slider.valueChanged.connect(self.redraw) self.end_slider.valueChanged.connect(self.redraw) self.table_widget = QTableWidget(8, 2) self.table_widget.setHorizontalHeaderLabels( [self.tr('Property'), self.tr('Value')]) self.table_widget.setItem(0, 0, QTableWidgetItem(self.tr('Least frequent'))) self.table_widget.setItem(1, 0, QTableWidgetItem(self.tr('Most frequent'))) self.table_widget.setItem(2, 0, QTableWidgetItem(self.tr('Average level'))) self.table_widget.setItem(3, 0, QTableWidgetItem(self.tr('Median level'))) self.table_widget.setItem(4, 0, QTableWidgetItem(self.tr('Deviation'))) self.table_widget.setItem(5, 0, QTableWidgetItem(self.tr('Pixel count'))) self.table_widget.setItem(6, 0, QTableWidgetItem(self.tr('Percentile'))) self.table_widget.setItem(7, 0, QTableWidgetItem(self.tr('Smoothness'))) 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.table_widget.setMaximumWidth(200) figure = Figure() plot_canvas = FigureCanvas(figure) # plot_canvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.axes = plot_canvas.figure.subplots() self.redraw() figure.set_tight_layout(True) right_layout = QVBoxLayout() table_label = QLabel(self.tr('Range properties')) modify_font(table_label, bold=True) table_label.setAlignment(Qt.AlignCenter) right_layout.addWidget(table_label) right_layout.addWidget(self.table_widget) right_layout.addWidget(self.marker_check) right_layout.addWidget(self.start_slider) right_layout.addWidget(self.end_slider) center_layout = QHBoxLayout() center_layout.addWidget(plot_canvas) center_layout.addLayout(right_layout) bottom_layout = QHBoxLayout() bottom_layout.addWidget(self.rgb_radio) bottom_layout.addWidget(self.red_radio) bottom_layout.addWidget(self.green_radio) bottom_layout.addWidget(self.blue_radio) bottom_layout.addWidget(self.value_radio) bottom_layout.addWidget(self.log_check) bottom_layout.addWidget(self.grid_check) bottom_layout.addStretch() bottom_layout.addWidget(unique_label) main_layout = QVBoxLayout() main_layout.addLayout(center_layout) main_layout.addLayout(bottom_layout) self.setLayout(main_layout)
def cancel(self): self.canceled = True self.total = self.kpts = self.desc = self.matches = self.clusters = None self.status_label.setText(self.tr('Processing interrupted!')) modify_font(self.status_label, bold=False, italic=False)
def __init__(self, table, headers, bold=True, mono=True, tooltips=None, align=False, search=True, parent=None): super(TableWidget, self).__init__(parent) self.table_widget = QTableWidget(len(table), len(table[0])) for i, row in enumerate(table): for j, item in enumerate(row): if item is not None: self.table_widget.setItem(i, j, QTableWidgetItem(str(item))) if tooltips is not None: self.table_widget.setToolTip(tooltips[i][j]) modify_font(self.table_widget.item(i, j), bold=bold and j == 0, mono=mono) if align: self.table_widget.item(i, j).setTextAlignment( Qt.AlignRight) self.table_headers = headers self.table_widget.setHorizontalHeaderLabels(self.table_headers) self.table_widget.setSelectionMode(QAbstractItemView.SingleSelection) self.table_widget.setEditTriggers(QAbstractItemView.NoEditTriggers) self.table_widget.resizeColumnsToContents() self.table_widget.setAlternatingRowColors(True) self.table_widget.itemDoubleClicked.connect(self.copy) search_layout = QHBoxLayout() search_layout.addWidget(QLabel(self.tr('Search:'))) self.search_edit = QLineEdit() self.search_edit.textChanged.connect(self.start) self.search_edit.returnPressed.connect(self.next) search_layout.addWidget(self.search_edit) clear_button = QToolButton() clear_button.setIcon(QIcon('icons/clear.svg')) clear_button.setShortcut(QKeySequence.DeleteCompleteLine) clear_button.setToolTip(self.tr('Clear pattern')) clear_button.clicked.connect(self.search_edit.clear) search_layout.addWidget(clear_button) self.case_button = QToolButton() self.case_button.setText(self.tr('Aa')) self.case_button.setCheckable(True) self.case_button.toggled.connect(self.start) self.case_button.setToolTip(self.tr('Case sensitive')) search_layout.addWidget(self.case_button) self.word_button = QToolButton() self.word_button.setText(self.tr('W')) self.word_button.setCheckable(True) self.word_button.toggled.connect(self.start) self.word_button.setToolTip(self.tr('Whole words')) search_layout.addWidget(self.word_button) self.regex_button = QToolButton() self.regex_button.setText(self.tr('.*')) self.regex_button.setCheckable(True) self.regex_button.toggled.connect(self.start) self.regex_button.setToolTip(self.tr('Regular expression')) search_layout.addWidget(self.regex_button) prev_button = QToolButton() prev_button.setIcon(QIcon('icons/up.svg')) prev_button.setShortcut(QKeySequence.FindPrevious) prev_button.clicked.connect(self.previous) prev_button.setToolTip(self.tr('Previous occurence')) search_layout.addWidget(prev_button) next_button = QToolButton() next_button.setIcon(QIcon('icons/down.svg')) next_button.setShortcut(QKeySequence.FindNext) next_button.clicked.connect(self.next) next_button.setToolTip(self.tr('Next occurence')) search_layout.addWidget(next_button) self.matches_label = QLabel() search_layout.addWidget(self.matches_label) search_layout.addStretch() export_button = QToolButton() export_button.setText(self.tr('Export...')) export_button.clicked.connect(self.export) search_layout.addWidget(export_button) main_layout = QVBoxLayout() main_layout.addWidget(self.table_widget) if search: main_layout.addLayout(search_layout) self.setLayout(main_layout)
def __init__(self, image, parent=None): super(ToolWidget, self).__init__(parent) self.value_radio = QRadioButton(self.tr('Value')) self.value_radio.setChecked(True) self.last_radio = self.value_radio self.red_radio = QRadioButton(self.tr('Red')) self.green_radio = QRadioButton(self.tr('Green')) self.blue_radio = QRadioButton(self.tr('Blue')) self.rgb_radio = QRadioButton(self.tr('RGB')) self.smooth_check = QCheckBox(self.tr('Smooth line')) self.smooth_check.setToolTip(self.tr('Interpolated values plot')) self.log_check = QCheckBox(self.tr('Log scale')) self.log_check.setToolTip(self.tr('Y-axes logarithmic scale')) self.grid_check = QCheckBox(self.tr('Show grid')) self.grid_check.setToolTip(self.tr('Display XY main grid lines')) self.marker_check = QCheckBox(self.tr('Show markers')) self.marker_check.setToolTip( self.tr('Show plot markers for min(--), avg(-), max(-.)')) self.start_slider = ParamSlider([0, 255], 8, 0, bold=True) self.end_slider = ParamSlider([0, 255], 8, 255, bold=True) channels = cv.split(cv.cvtColor(image, cv.COLOR_BGR2RGB)) channels.append(cv.cvtColor(image, cv.COLOR_BGR2GRAY)) self.hist = [compute_hist(c) for c in channels] rows, cols, chans = image.shape pixels = rows * cols self.unique_colors = np.unique(np.reshape(image, (pixels, chans)), axis=0).shape[0] self.unique_ratio = np.round(self.unique_colors / pixels * 100, 2) self.value_radio.clicked.connect(self.redraw) self.red_radio.clicked.connect(self.redraw) self.green_radio.clicked.connect(self.redraw) self.blue_radio.clicked.connect(self.redraw) self.rgb_radio.clicked.connect(self.redraw) self.smooth_check.stateChanged.connect(self.redraw) self.log_check.stateChanged.connect(self.redraw) self.grid_check.stateChanged.connect(self.redraw) self.marker_check.stateChanged.connect(self.redraw) self.start_slider.valueChanged.connect(self.redraw) self.end_slider.valueChanged.connect(self.redraw) self.table_widget = QTableWidget(13, 2) self.table_widget.setHorizontalHeaderLabels( [self.tr('Property'), self.tr('Value')]) self.table_widget.setItem(0, 0, QTableWidgetItem(self.tr('Least frequent'))) self.table_widget.item(0, 0).setToolTip( self.tr('Value that appears less')) self.table_widget.setItem(1, 0, QTableWidgetItem(self.tr('Most frequent'))) self.table_widget.item(1, 0).setToolTip( self.tr('Value that appears more')) self.table_widget.setItem(2, 0, QTableWidgetItem(self.tr('Average level'))) self.table_widget.item(2, 0).setToolTip(self.tr('Histogram mean value')) self.table_widget.setItem(3, 0, QTableWidgetItem(self.tr('Median level'))) self.table_widget.item(3, 0).setToolTip(self.tr('Histogram median value')) self.table_widget.setItem(4, 0, QTableWidgetItem(self.tr('Deviation'))) self.table_widget.item(4, 0).setToolTip( self.tr('Histogram standard deviation')) self.table_widget.setItem(5, 0, QTableWidgetItem(self.tr('Pixel count'))) self.table_widget.item(5, 0).setToolTip( self.tr('Total values in current range')) self.table_widget.setItem(6, 0, QTableWidgetItem(self.tr('Percentile'))) self.table_widget.item(6, 0).setToolTip( self.tr('Percentage of total pixels')) self.table_widget.setItem(7, 0, QTableWidgetItem(self.tr('Nonzero range'))) self.table_widget.item(7, 0).setToolTip( self.tr('Minimal range without empty bins')) self.table_widget.setItem(8, 0, QTableWidgetItem(self.tr('Empty bins'))) self.table_widget.item(8, 0).setToolTip( self.tr('Number of missing values')) self.table_widget.setItem(9, 0, QTableWidgetItem(self.tr('Unique colors'))) self.table_widget.item(9, 0).setToolTip(self.tr('Unique RGB color count')) self.table_widget.setItem(10, 0, QTableWidgetItem(self.tr('Unique ratio'))) self.table_widget.item(10, 0).setToolTip( self.tr('Unique colors vs total pixels')) self.table_widget.setItem(11, 0, QTableWidgetItem(self.tr('Smoothness'))) self.table_widget.item(11, 0).setToolTip( self.tr('Estimated correlation among bin values')) self.table_widget.setItem(12, 0, QTableWidgetItem(self.tr('Fullness'))) self.table_widget.item(12, 0).setToolTip( self.tr('Area covered vs total size')) 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.setAlternatingRowColors(True) self.table_widget.setMinimumWidth(200) self.table_widget.resizeColumnsToContents() figure = Figure() plot_canvas = FigureCanvas(figure) self.axes = plot_canvas.figure.subplots() self.redraw() figure.set_tight_layout(True) range_layout = QGridLayout() range_layout.addWidget(QLabel(self.tr('Start:')), 0, 0) range_layout.addWidget(self.start_slider, 0, 1) range_layout.addWidget(QLabel(self.tr('End:')), 1, 0) range_layout.addWidget(self.end_slider, 1, 1) right_frame = QFrame() right_layout = QVBoxLayout() right_layout.addWidget(self.table_widget) right_layout.addLayout(range_layout) right_frame.setLayout(right_layout) center_split = QSplitter() center_split.addWidget(plot_canvas) center_split.addWidget(right_frame) bottom_layout = QHBoxLayout() bottom_layout.addWidget(QLabel(self.tr('Channel:'))) bottom_layout.addWidget(self.value_radio) bottom_layout.addWidget(self.red_radio) bottom_layout.addWidget(self.green_radio) bottom_layout.addWidget(self.blue_radio) bottom_layout.addWidget(self.rgb_radio) bottom_layout.addStretch() bottom_layout.addWidget(self.smooth_check) bottom_layout.addWidget(self.log_check) bottom_layout.addWidget(self.grid_check) bottom_layout.addWidget(self.marker_check) main_layout = QVBoxLayout() main_layout.addWidget(center_split) main_layout.addLayout(bottom_layout) self.setLayout(main_layout)
def process(self): start = time() self.canceled = False self.status_label.setText(self.tr('Processing, please wait...')) algorithm = self.detector_combo.currentIndex() response = 100 - self.response_spin.value() matching = self.matching_spin.value() / 100 * 255 distance = self.distance_spin.value() / 100 cluster = self.cluster_spin.value() modify_font(self.status_label, bold=False, italic=True) QCoreApplication.processEvents() if self.kpts is None: if algorithm == 0: detector = cv.BRISK_create() elif algorithm == 1: detector = cv.ORB_create() elif algorithm == 2: detector = cv.AKAZE_create() else: return mask = self.mask if self.onoff_button.isChecked() else None self.kpts, self.desc = detector.detectAndCompute(self.gray, mask) self.total = len(self.kpts) responses = np.array([k.response for k in self.kpts]) strongest = (cv.normalize(responses, None, 0, 100, cv.NORM_MINMAX) >= response).flatten() self.kpts = list(compress(self.kpts, strongest)) if len(self.kpts) > 30000: QMessageBox.warning( self, self.tr('Warning'), self. tr('Too many keypoints found ({}), please reduce response value' .format(self.total))) self.kpts = self.desc = None self.total = 0 self.status_label.setText('') return self.desc = self.desc[strongest] if self.matches is None: matcher = cv.BFMatcher_create(cv.NORM_HAMMING, True) self.matches = matcher.radiusMatch(self.desc, self.desc, matching) if self.matches is None: self.status_label.setText( self.tr('No keypoint match found with current settings')) modify_font(self.status_label, italic=False, bold=True) return self.matches = [ item for sublist in self.matches for item in sublist ] self.matches = [ m for m in self.matches if m.queryIdx != m.trainIdx ] if not self.matches: self.clusters = [] elif self.clusters is None: self.clusters = [] min_dist = distance * np.min(self.gray.shape) / 2 kpts_a = np.array([p.pt for p in self.kpts]) ds = np.linalg.norm([ kpts_a[m.queryIdx] - kpts_a[m.trainIdx] for m in self.matches ], axis=1) self.matches = [ m for i, m in enumerate(self.matches) if ds[i] > min_dist ] total = len(self.matches) progress = QProgressDialog(self.tr('Clustering matches...'), self.tr('Cancel'), 0, total, self) progress.canceled.connect(self.cancel) progress.setWindowModality(Qt.WindowModal) for i in range(total): match0 = self.matches[i] d0 = ds[i] query0 = match0.queryIdx train0 = match0.trainIdx group = [match0] for j in range(i + 1, total): match1 = self.matches[j] query1 = match1.queryIdx train1 = match1.trainIdx if query1 == train0 and train1 == query0: continue d1 = ds[j] if np.abs(d0 - d1) > min_dist: continue a0 = np.array(self.kpts[query0].pt) b0 = np.array(self.kpts[train0].pt) a1 = np.array(self.kpts[query1].pt) b1 = np.array(self.kpts[train1].pt) aa = np.linalg.norm(a0 - a1) bb = np.linalg.norm(b0 - b1) ab = np.linalg.norm(a0 - b1) ba = np.linalg.norm(b0 - a1) if not (0 < aa < min_dist and 0 < bb < min_dist or 0 < ab < min_dist and 0 < ba < min_dist): continue for g in group: if g.queryIdx == train1 and g.trainIdx == query1: break else: group.append(match1) if len(group) >= cluster: self.clusters.append(group) progress.setValue(i) if self.canceled: self.update_detector() return progress.close() output = np.copy(self.image) hsv = np.zeros((1, 1, 3)) nolines = self.nolines_check.isChecked() show_kpts = self.kpts_check.isChecked() if show_kpts: for kpt in self.kpts: cv.circle(output, (int(kpt.pt[0]), int(kpt.pt[1])), 2, (250, 227, 72)) angles = [] for c in self.clusters: for m in c: ka = self.kpts[m.queryIdx] pa = tuple(map(int, ka.pt)) sa = int(np.round(ka.size)) kb = self.kpts[m.trainIdx] pb = tuple(map(int, kb.pt)) sb = int(np.round(kb.size)) angle = np.arctan2(pb[1] - pa[1], pb[0] - pa[0]) if angle < 0: angle += np.pi angles.append(angle) hsv[0, 0, 0] = angle / np.pi * 180 hsv[0, 0, 1] = 255 hsv[0, 0, 2] = m.distance / matching * 255 rgb = cv.cvtColor(hsv.astype(np.uint8), cv.COLOR_HSV2BGR) rgb = tuple([int(x) for x in rgb[0, 0]]) cv.circle(output, pa, sa, rgb, 1, cv.LINE_AA) cv.circle(output, pb, sb, rgb, 1, cv.LINE_AA) if not nolines: cv.line(output, pa, pb, rgb, 1, cv.LINE_AA) regions = 0 if angles: angles = np.reshape(np.array(angles, dtype=np.float32), (len(angles), 1)) if np.std(angles) < 0.1: regions = 1 else: criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 10, 1.0) attempts = 10 flags = cv.KMEANS_PP_CENTERS compact = [ cv.kmeans(angles, k, None, criteria, attempts, flags)[0] for k in range(1, 11) ] compact = cv.normalize(np.array(compact), None, 0, 1, cv.NORM_MINMAX) regions = np.argmax(compact < 0.005) + 1 self.viewer.update_processed(output) self.process_button.setEnabled(False) modify_font(self.status_label, italic=False, bold=True) self.status_label.setText( self. tr('Keypoints: {} --> Filtered: {} --> Matches: {} --> Clusters: {} --> Regions: {}' .format(self.total, len(self.kpts), len(self.matches), len(self.clusters), regions))) self.info_message.emit( self.tr('Copy-Move Forgery = {}'.format(elapsed_time(start))))
def __init__(self, parent=None): super(MainWindow, self).__init__(parent) QApplication.setApplicationName('Sherloq') QApplication.setOrganizationName('Guido Bartoli') QApplication.setOrganizationDomain('www.guidobartoli.com') QApplication.setApplicationVersion(ToolTree().version) QApplication.setWindowIcon(QIcon('icons/sherloq_white.png')) self.setWindowTitle('{} {}'.format(QApplication.applicationName(), QApplication.applicationVersion())) self.mdi_area = QMdiArea() self.setCentralWidget(self.mdi_area) self.filename = None self.image = None modify_font(self.statusBar(), bold=True) tree_dock = QDockWidget(self.tr('TOOLS'), self) tree_dock.setObjectName('tree_dock') tree_dock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) self.addDockWidget(Qt.LeftDockWidgetArea, tree_dock) self.tree_widget = ToolTree() self.tree_widget.setObjectName('tree_widget') self.tree_widget.itemDoubleClicked.connect(self.open_tool) tree_dock.setWidget(self.tree_widget) tools_action = tree_dock.toggleViewAction() tools_action.setText(self.tr('Show tools')) tools_action.setToolTip(self.tr('Toggle toolset visibility')) tools_action.setShortcut(QKeySequence(Qt.Key_Tab)) tools_action.setObjectName('tools_action') tools_action.setIcon(QIcon('icons/tools.svg')) help_action = QAction(self.tr('Show help'), self) help_action.setToolTip(self.tr('Toggle online help')) help_action.setShortcut(QKeySequence.HelpContents) help_action.setObjectName('help_action') help_action.setIcon(QIcon('icons/help.svg')) help_action.setCheckable(True) help_action.setEnabled(False) load_action = QAction(self.tr('&Load image...'), self) load_action.setToolTip(self.tr('Load an image to analyze')) load_action.setShortcut(QKeySequence.Open) load_action.triggered.connect(self.load_file) load_action.setObjectName('load_action') load_action.setIcon(QIcon('icons/load.svg')) quit_action = QAction(self.tr('&Quit'), self) quit_action.setToolTip(self.tr('Exit from Sherloq')) quit_action.setShortcut(QKeySequence.Quit) quit_action.triggered.connect(self.close) quit_action.setObjectName('quit_action') quit_action.setIcon(QIcon('icons/quit.svg')) tabbed_action = QAction(self.tr('&Tabbed'), self) tabbed_action.setToolTip(self.tr('Toggle tabbed view for window area')) tabbed_action.setShortcut(QKeySequence(Qt.Key_F10)) tabbed_action.setCheckable(True) tabbed_action.triggered.connect(self.toggle_view) tabbed_action.setObjectName('tabbed_action') tabbed_action.setIcon(QIcon('icons/tabbed.svg')) prev_action = QAction(self.tr('&Previous'), self) prev_action.setToolTip(self.tr('Select the previous tool window')) prev_action.setShortcut(QKeySequence.PreviousChild) prev_action.triggered.connect(self.mdi_area.activatePreviousSubWindow) prev_action.setObjectName('prev_action') prev_action.setIcon(QIcon('icons/previous.svg')) next_action = QAction(self.tr('&Next'), self) next_action.setToolTip(self.tr('Select the next tool window')) next_action.setShortcut(QKeySequence.NextChild) next_action.triggered.connect(self.mdi_area.activateNextSubWindow) next_action.setObjectName('next_action') next_action.setIcon(QIcon('icons/next.svg')) tile_action = QAction(self.tr('&Tile'), self) tile_action.setToolTip( self.tr('Arrange windows into non-overlapping views')) tile_action.setShortcut(QKeySequence(Qt.Key_F11)) tile_action.triggered.connect(self.mdi_area.tileSubWindows) tile_action.setObjectName('tile_action') tile_action.setIcon(QIcon('icons/tile.svg')) cascade_action = QAction(self.tr('&Cascade'), self) cascade_action.setToolTip( self.tr('Arrange windows into overlapping views')) cascade_action.setShortcut(QKeySequence(Qt.Key_F12)) cascade_action.triggered.connect(self.mdi_area.cascadeSubWindows) cascade_action.setObjectName('cascade_action') cascade_action.setIcon(QIcon('icons/cascade.svg')) close_action = QAction(self.tr('Close &All'), self) close_action.setToolTip(self.tr('Close all open tool windows')) close_action.setShortcut(QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_W)) close_action.triggered.connect(self.mdi_area.closeAllSubWindows) close_action.setObjectName('close_action') close_action.setIcon(QIcon('icons/close.svg')) self.full_action = QAction(self.tr('Full screen'), self) self.full_action.setToolTip(self.tr('Switch to full screen mode')) self.full_action.setShortcut(QKeySequence.FullScreen) self.full_action.triggered.connect(self.change_view) self.full_action.setObjectName('full_action') self.full_action.setIcon(QIcon('icons/full.svg')) self.normal_action = QAction(self.tr('Normal view'), self) self.normal_action.setToolTip(self.tr('Back to normal view mode')) self.normal_action.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_F12)) self.normal_action.triggered.connect(self.change_view) self.normal_action.setObjectName('normal_action') self.normal_action.setIcon(QIcon('icons/normal.svg')) about_action = QAction(self.tr('&About...'), self) about_action.setToolTip(self.tr('Information about this program')) about_action.triggered.connect(self.show_about) about_action.setObjectName('about_action') about_action.setIcon(QIcon('icons/sherloq_alpha.png')) about_qt_action = QAction(self.tr('About &Qt'), self) about_qt_action.setToolTip( self.tr('Information about the Qt Framework')) about_qt_action.triggered.connect(QApplication.aboutQt) about_qt_action.setIcon(QIcon('icons/Qt.svg')) file_menu = self.menuBar().addMenu(self.tr('&File')) file_menu.addAction(load_action) file_menu.addSeparator() self.recent_actions = [None] * self.max_recent for i in range(len(self.recent_actions)): self.recent_actions[i] = QAction(self) self.recent_actions[i].setVisible(False) self.recent_actions[i].triggered.connect(self.open_recent) file_menu.addAction(self.recent_actions[i]) file_menu.addSeparator() file_menu.addAction(quit_action) view_menu = self.menuBar().addMenu(self.tr('&View')) view_menu.addAction(tools_action) view_menu.addAction(help_action) view_menu.addSeparator() view_menu.addAction(self.full_action) view_menu.addAction(self.normal_action) window_menu = self.menuBar().addMenu(self.tr('&Window')) window_menu.addAction(prev_action) window_menu.addAction(next_action) window_menu.addSeparator() window_menu.addAction(tile_action) window_menu.addAction(cascade_action) window_menu.addAction(tabbed_action) window_menu.addSeparator() window_menu.addAction(close_action) help_menu = self.menuBar().addMenu(self.tr('&Help')) help_menu.addAction(help_action) help_menu.addSeparator() help_menu.addAction(about_action) help_menu.addAction(about_qt_action) main_toolbar = self.addToolBar(self.tr('&Toolbar')) main_toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) main_toolbar.addAction(load_action) main_toolbar.addSeparator() main_toolbar.addAction(tools_action) main_toolbar.addAction(help_action) main_toolbar.addSeparator() main_toolbar.addAction(prev_action) main_toolbar.addAction(next_action) main_toolbar.addSeparator() main_toolbar.addAction(tile_action) main_toolbar.addAction(cascade_action) main_toolbar.addAction(tabbed_action) main_toolbar.addAction(close_action) # main_toolbar.addSeparator() # main_toolbar.addAction(self.normal_action) # main_toolbar.addAction(self.full_action) main_toolbar.setAllowedAreas(Qt.TopToolBarArea | Qt.BottomToolBarArea) main_toolbar.setObjectName('main_toolbar') settings = QSettings() settings.beginGroup('main_window') self.restoreGeometry(settings.value('geometry')) self.restoreState(settings.value('state')) self.recent_files = settings.value('recent_files') if self.recent_files is None: self.recent_files = [] elif not isinstance(self.recent_files, list): self.recent_files = [self.recent_files] self.update_recent() settings.endGroup() prev_action.setEnabled(False) next_action.setEnabled(False) tile_action.setEnabled(False) cascade_action.setEnabled(False) close_action.setEnabled(False) tabbed_action.setEnabled(False) self.tree_widget.setEnabled(False) self.showNormal() self.normal_action.setEnabled(False) self.show_message(self.tr('Ready'))
def __init__(self, parent=None): super(MainWindow, self).__init__(parent) QApplication.setApplicationName("Sherloq") QApplication.setOrganizationName("Guido Bartoli") QApplication.setOrganizationDomain("www.guidobartoli.com") QApplication.setApplicationVersion(ToolTree().version) QApplication.setWindowIcon(QIcon("icons/sherloq_white.png")) self.setWindowTitle( f"{QApplication.applicationName()} {QApplication.applicationVersion()}" ) self.mdi_area = QMdiArea() self.setCentralWidget(self.mdi_area) self.filename = None self.image = None modify_font(self.statusBar(), bold=True) tree_dock = QDockWidget(self.tr("TOOLS"), self) tree_dock.setObjectName("tree_dock") tree_dock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) self.addDockWidget(Qt.LeftDockWidgetArea, tree_dock) self.tree_widget = ToolTree() self.tree_widget.setObjectName("tree_widget") self.tree_widget.itemDoubleClicked.connect(self.open_tool) tree_dock.setWidget(self.tree_widget) tools_action = tree_dock.toggleViewAction() tools_action.setText(self.tr("Show tools")) tools_action.setToolTip(self.tr("Toggle toolset visibility")) tools_action.setShortcut(QKeySequence(Qt.Key_Tab)) tools_action.setObjectName("tools_action") tools_action.setIcon(QIcon("icons/tools.svg")) help_action = QAction(self.tr("Show help"), self) help_action.setToolTip(self.tr("Toggle online help")) help_action.setShortcut(QKeySequence.HelpContents) help_action.setObjectName("help_action") help_action.setIcon(QIcon("icons/help.svg")) help_action.setCheckable(True) help_action.setEnabled(False) load_action = QAction(self.tr("&Load image..."), self) load_action.setToolTip(self.tr("Load an image to analyze")) load_action.setShortcut(QKeySequence.Open) load_action.triggered.connect(self.load_file) load_action.setObjectName("load_action") load_action.setIcon(QIcon("icons/load.svg")) quit_action = QAction(self.tr("&Quit"), self) quit_action.setToolTip(self.tr("Exit from Sherloq")) quit_action.setShortcut(QKeySequence.Quit) quit_action.triggered.connect(self.close) quit_action.setObjectName("quit_action") quit_action.setIcon(QIcon("icons/quit.svg")) tabbed_action = QAction(self.tr("&Tabbed"), self) tabbed_action.setToolTip(self.tr("Toggle tabbed view for window area")) tabbed_action.setShortcut(QKeySequence(Qt.Key_F10)) tabbed_action.setCheckable(True) tabbed_action.triggered.connect(self.toggle_view) tabbed_action.setObjectName("tabbed_action") tabbed_action.setIcon(QIcon("icons/tabbed.svg")) prev_action = QAction(self.tr("&Previous"), self) prev_action.setToolTip(self.tr("Select the previous tool window")) prev_action.setShortcut(QKeySequence.PreviousChild) prev_action.triggered.connect(self.mdi_area.activatePreviousSubWindow) prev_action.setObjectName("prev_action") prev_action.setIcon(QIcon("icons/previous.svg")) next_action = QAction(self.tr("&Next"), self) next_action.setToolTip(self.tr("Select the next tool window")) next_action.setShortcut(QKeySequence.NextChild) next_action.triggered.connect(self.mdi_area.activateNextSubWindow) next_action.setObjectName("next_action") next_action.setIcon(QIcon("icons/next.svg")) tile_action = QAction(self.tr("&Tile"), self) tile_action.setToolTip( self.tr("Arrange windows into non-overlapping views")) tile_action.setShortcut(QKeySequence(Qt.Key_F11)) tile_action.triggered.connect(self.mdi_area.tileSubWindows) tile_action.setObjectName("tile_action") tile_action.setIcon(QIcon("icons/tile.svg")) cascade_action = QAction(self.tr("&Cascade"), self) cascade_action.setToolTip( self.tr("Arrange windows into overlapping views")) cascade_action.setShortcut(QKeySequence(Qt.Key_F12)) cascade_action.triggered.connect(self.mdi_area.cascadeSubWindows) cascade_action.setObjectName("cascade_action") cascade_action.setIcon(QIcon("icons/cascade.svg")) close_action = QAction(self.tr("Close &All"), self) close_action.setToolTip(self.tr("Close all open tool windows")) close_action.setShortcut(QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_W)) close_action.triggered.connect(self.mdi_area.closeAllSubWindows) close_action.setObjectName("close_action") close_action.setIcon(QIcon("icons/close.svg")) self.full_action = QAction(self.tr("Full screen"), self) self.full_action.setToolTip(self.tr("Switch to full screen mode")) self.full_action.setShortcut(QKeySequence.FullScreen) self.full_action.triggered.connect(self.change_view) self.full_action.setObjectName("full_action") self.full_action.setIcon(QIcon("icons/full.svg")) self.normal_action = QAction(self.tr("Normal view"), self) self.normal_action.setToolTip(self.tr("Back to normal view mode")) self.normal_action.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_F12)) self.normal_action.triggered.connect(self.change_view) self.normal_action.setObjectName("normal_action") self.normal_action.setIcon(QIcon("icons/normal.svg")) about_action = QAction(self.tr("&About..."), self) about_action.setToolTip(self.tr("Information about this program")) about_action.triggered.connect(self.show_about) about_action.setObjectName("about_action") about_action.setIcon(QIcon("icons/sherloq_alpha.png")) about_qt_action = QAction(self.tr("About &Qt"), self) about_qt_action.setToolTip( self.tr("Information about the Qt Framework")) about_qt_action.triggered.connect(QApplication.aboutQt) about_qt_action.setIcon(QIcon("icons/Qt.svg")) file_menu = self.menuBar().addMenu(self.tr("&File")) file_menu.addAction(load_action) file_menu.addSeparator() self.recent_actions = [None] * self.max_recent for i in range(len(self.recent_actions)): self.recent_actions[i] = QAction(self) self.recent_actions[i].setVisible(False) self.recent_actions[i].triggered.connect(self.open_recent) file_menu.addAction(self.recent_actions[i]) file_menu.addSeparator() file_menu.addAction(quit_action) view_menu = self.menuBar().addMenu(self.tr("&View")) view_menu.addAction(tools_action) view_menu.addAction(help_action) view_menu.addSeparator() view_menu.addAction(self.full_action) view_menu.addAction(self.normal_action) window_menu = self.menuBar().addMenu(self.tr("&Window")) window_menu.addAction(prev_action) window_menu.addAction(next_action) window_menu.addSeparator() window_menu.addAction(tile_action) window_menu.addAction(cascade_action) window_menu.addAction(tabbed_action) window_menu.addSeparator() window_menu.addAction(close_action) help_menu = self.menuBar().addMenu(self.tr("&Help")) help_menu.addAction(help_action) help_menu.addSeparator() help_menu.addAction(about_action) help_menu.addAction(about_qt_action) main_toolbar = self.addToolBar(self.tr("&Toolbar")) main_toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) main_toolbar.addAction(load_action) main_toolbar.addSeparator() main_toolbar.addAction(tools_action) main_toolbar.addAction(help_action) main_toolbar.addSeparator() main_toolbar.addAction(prev_action) main_toolbar.addAction(next_action) main_toolbar.addSeparator() main_toolbar.addAction(tile_action) main_toolbar.addAction(cascade_action) main_toolbar.addAction(tabbed_action) main_toolbar.addAction(close_action) # main_toolbar.addSeparator() # main_toolbar.addAction(self.normal_action) # main_toolbar.addAction(self.full_action) main_toolbar.setAllowedAreas(Qt.TopToolBarArea | Qt.BottomToolBarArea) main_toolbar.setObjectName("main_toolbar") settings = QSettings() settings.beginGroup("main_window") self.restoreGeometry(settings.value("geometry")) self.restoreState(settings.value("state")) self.recent_files = settings.value("recent_files") if self.recent_files is None: self.recent_files = [] elif not isinstance(self.recent_files, list): self.recent_files = [self.recent_files] self.update_recent() settings.endGroup() prev_action.setEnabled(False) next_action.setEnabled(False) tile_action.setEnabled(False) cascade_action.setEnabled(False) close_action.setEnabled(False) tabbed_action.setEnabled(False) self.tree_widget.setEnabled(False) self.showNormal() self.normal_action.setEnabled(False) self.show_message(self.tr("Ready"))