# This Python file uses the following encoding: utf-8 import functools import os import sys from math import sqrt from pathlib import Path from typing import Callable import cv2 import matplotlib.pyplot as plt import numpy as np from PySide6.QtCore import Slot, QUrl, QPoint, QFileInfo from PySide6.QtGui import QImage, QPainter, QColor from PySide6.QtQml import QQmlApplicationEngine, QmlElement from PySide6.QtQuick import QQuickPaintedItem from PySide6.QtWidgets import QApplication from numba import jit from lapstyle import LapStylePredictor QML_IMPORT_NAME = "Util" QML_IMPORT_MAJOR_VERSION = 1 QML_IMPORT_MINOR_VERSION = 0 @QmlElement class ImageItem(QQuickPaintedItem): matrices: list[np.ndarray] = [] matrix: np.ndarray = None image: QImage = None xpos = 0 ypos = 0 def __init__(self, parent=None): super(ImageItem, self).__init__(parent) def paint(self, painter: QPainter): if self.image is None: painter.fillRect(self.boundingRect(), QColor("transparent")) else: x = self.width() / 2 - self.image.width() / 2 if x < 0: x = 0 y = self.height() / 2 - self.image.height() / 2 if y < 0: y = 0 painter.drawImage(x, y, self.image, self.xpos, self.ypos) def setImage(self, matrix: np.ndarray): self.matrix = matrix # 彩色图片 if len(matrix.shape) > 2: height, width, channel = matrix.shape self.image = QImage(matrix.data, width, height, width * channel, QImage.Format_RGB888) # 灰度图片 else: height, width = matrix.shape self.image = QImage(matrix.data, width, height, width, QImage.Format_Grayscale8) self.update() @Slot(float, float, result="QPoint") def getPoint(self, x, y): ox = np.round(x + self.xpos) oy = np.round(y + self.ypos) return QPoint(ox, oy) def appendMatrix(self): if self.matrix is None: return if len(self.matrices) >= 1 and np.array_equal(self.matrices[-1], self.matrix): return self.matrices.append(self.matrix) @Slot() def withdraw(self): if self.matrix is None: return if np.array_equal(self.matrix, self.matrices[-1]) and len(self.matrices) >= 2: self.matrices.pop() self.setImage(self.matrices[-1]) @staticmethod def process(isGray=False): def decorator(func: Callable): @functools.wraps(func) def wrapper(self, *args): image = self.getMatrix(isGray) if image is None: return out = func(self, *args, src=image) if out is not None: self.setImage(out) self.appendMatrix() return wrapper return decorator def getMatrix(self, isGray=False): if len(self.matrices) < 1: return None matrix = self.matrices[-1] if not isGray or len(matrix.shape) < 3: return matrix.copy() return cv2.cvtColor(matrix, cv2.COLOR_RGB2GRAY) @Slot(QUrl) def readFile(self, filepath: QUrl): image = cv2.imdecode(np.fromfile(filepath.toLocalFile(), dtype=np.uint8), cv2.IMREAD_COLOR) matrix = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) self.setImage(matrix) self.matrices.clear() self.appendMatrix() @Slot(QUrl) def saveFile(self, filepath: QUrl): if self.matrix is None: return if len(self.matrix.shape) > 2: img = cv2.cvtColor(self.matrix, cv2.COLOR_RGB2BGR) else: img = self.matrix fileInfo = QFileInfo(filepath.toLocalFile()) cv2.imencode("." + fileInfo.suffix(), img)[1].tofile(filepath.toLocalFile()) @Slot(int, int) def moveBy(self, x: int, y: int): if self.image is None: return if x + self.xpos + self.width() <= self.image.width(): ox = x + self.xpos else: ox = self.image.width() - self.width() if ox < 0: ox = 0 if y + self.ypos + self.height() <= self.image.height(): oy = y + self.ypos else: oy = self.image.height() - self.height() if oy < 0: oy = 0 if ox is not self.xpos or oy is not self.ypos: self.xpos = ox self.ypos = oy self.update() # 图像变换 @Slot(int) @process() def removeChannel(self, channel, src): img = src img[..., channel] = 0 return img @Slot(int, int, QColor) @process() def translation(self, dx, dy, border: QColor, src): img = src # 读取彩色图像(BGR) rows, cols = img.shape[:2] mat = np.float32([[1, 0, dx], [0, 1, dy]]) # 构造平移变换矩阵 (r, g, b, a) = border.getRgb() dst = cv2.warpAffine(img, mat, (cols, rows), borderValue=(r, g, b, a)) return dst @Slot(float) @process() def centralRotation(self, theta, src): # 1.36 图像旋转 (以任意点 (x0,y0) 为中心旋转) img = src height, width = img.shape[:2] # 图片的高度和宽度 x0, y0 = width // 2, height // 2 # 以图像中心作为旋转中心 mat = cv2.getRotationMatrix2D((x0, y0), theta, 1.0) dst = cv2.warpAffine(img, mat, (width, height)) # 设置白色填充 return dst @Slot(int) @process() def rightAngleRotation(self, angle, src): img = src img_r = None if angle == 1: img_r = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE) elif angle == 2: img_r = cv2.rotate(img, cv2.ROTATE_180) if angle == 3: img_r = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE) return img_r @Slot(int) @process() def flip(self, method, src): img = src img_flip = None if method == 1: img_flip = cv2.flip(img, 0) # 垂直翻转 if method == 2: img_flip = cv2.flip(img, 1) # 水平翻转 if method == 3: img_flip = cv2.flip(img, -1) # 水平和垂直翻转 return img_flip @Slot(float, float) @process() def resize(self, width_size, height_size, src): img = src dst = cv2.resize(img, None, fx=width_size, fy=height_size, interpolation=cv2.INTER_AREA) return dst @Slot() @process() def affine(self, src): image = src rows, cols = image.shape[:2] pts1 = np.float32([[50, 50], [200, 50], [50, 200]]) # 初始位置 pts2 = np.float32([[50, 100], [200, 50], [100, 250]]) # 终止位置 mtr = cv2.getAffineTransform(pts1, pts2) # 计算 2x3 变换矩阵 MA dst = cv2.warpAffine(image, mtr, (cols, rows)) # 实现仿射变换 return dst @Slot() @process() def shear(self, src): # 1.41 图像的错切 img = src height, width = img.shape[:2] # 图片的高度和宽度 mat = np.float32([[1, 0.2, 0], [0, 1, 0]]) # 构造错切变换矩阵 img_shear = cv2.warpAffine(img, mat, (width, height)) return img_shear @Slot() @process() def projective(self, src): img = src h, w = img.shape[:2] # 图片的高度和宽度 point_src = np.float32([[0, 0], [w, 0], [0, h], [w, h]]) # 原始图像中 4点坐标 point_dst = np.float32( [[int(w / 3), int(h / 3)], [int(w * 2 / 3), int(h / 3)], [0, h], [w, h]]) # 变换图像中 4点坐标 mp = cv2.getPerspectiveTransform(point_src, point_dst) # 计算投影变换矩阵 M img_p = cv2.warpPerspective(img, mp, (w, h), flags=cv2.INTER_AREA, borderMode=cv2.BORDER_WRAP) return img_p # 图形绘制 @Slot(QPoint, QPoint, QColor, int) @process() def line(self, start: QPoint, end: QPoint, color: QColor, width, src): img = src if start.isNull() or end.isNull(): return (r, g, b, a) = color.getRgb() cv2.line(img, start.toTuple(), end.toTuple(), (r, g, b, a), width) return img @Slot(QPoint, QPoint, QColor, int) @process() def rectangle(self, start: QPoint, end: QPoint, color: QColor, width, src): img = src if start.isNull() or end.isNull(): return (r, g, b, a) = color.getRgb() cv2.rectangle(img, start.toTuple(), end.toTuple(), (r, g, b, a), width) return img @Slot(QPoint, QPoint, QColor, int) @process() def circle(self, center: QPoint, side: QPoint, color: QColor, width, src): img = src if center.isNull() or side.isNull(): return (center_x, center_y) = center.toTuple() (side_x, side_y) = side.toTuple() radius = np.round(sqrt(np.power(center_x - side_x, 2) + np.power(center_y - side_y, 2))) (r, g, b, a) = color.getRgb() cv2.circle(img, center.toTuple(), int(radius), (r, g, b, a), width) return img @Slot(QPoint, QColor, int, int, str) @process() def text(self, start: QPoint, color: QColor, fontsize, width, text, src): img = src if start.isNull(): return (r, g, b, a) = color.getRgb() cv2.putText(img, text, start.toTuple(), cv2.FONT_HERSHEY_SCRIPT_COMPLEX, fontsize, (r, g, b, a), width, cv2.LINE_AA, 0) return img # 灰度运算 @Slot(int) @process(isGray=True) def binaryTransformation(self, thresh, src): img_gray = src if img_gray is None: return _, img = cv2.threshold(img_gray, thresh, 255, cv2.THRESH_BINARY) # 转换为二值图像, thresh=191 return img @Slot() @process(isGray=True) def linearGrayscaleTransformation(self, src): # 通过遍历对不同像素范围内进行分段线性变化,在这里分三段函数进行分段线性变化, 主要还是考察for循环的应用 # y=0.5*x(x<50) # y=3.6*x-310(50<=x<150) # y=0.238*x+194(x>=150) img = src @jit(nopython=True) def transform(image): out = np.zeros(image.shape, np.uint8) for pos, pix in np.ndenumerate(image): if pix < 50: out[pos] = 0.5 * pix elif pix < 150: out[pos] = 3.6 * pix - 310 else: out[pos] = 0.238 * pix + 194 return out res = transform(img) return res @Slot(int, int) @process(isGray=True) def grayLayered(self, low_thresh, high_thresh, src): # # 1.53 分段线性灰度变换 (灰度级分层) # Gray layered img_gray = src ''' 另一种方法 img_layer1 = img_gray.copy() img_layer1[(img_layer1[:, :] < low_thresh) | (img_layer1[:, :] > high_thresh)] = 0 # 其它区域:黑色 img_layer1[(img_layer1[:, :] >= low_thresh) & (img_layer1[:, :] <= high_thresh)] = 255 # 灰度级窗口:白色 ''' img_layer = img_gray.copy() img_layer[(img_layer[:, :] >= low_thresh) & (img_layer[:, :] <= high_thresh)] = 255 # 灰度级窗口:白色,其它区域不变 return img_layer @Slot() @process(isGray=True) def logTrans(self, src): # 1.55 图像的非线性灰度变换:对数变换 img = src img_log = np.log(img + 1.0) cv2.normalize(img_log, img_log, 0, 255, cv2.NORM_MINMAX) img_log_abs = cv2.convertScaleAbs(img_log) return img_log_abs @Slot(float) @process(isGray=True) def gammaTrans(self, gamma, src): # 1.56 图像的非线性灰度变换: 幂律变换 (伽马变换) img = src img_gamma = np.power(img, gamma) cv2.normalize(img_gamma, img_gamma, 0, 255, cv2.NORM_MINMAX) img_gamma_abs = cv2.convertScaleAbs(img_gamma) return img_gamma_abs @Slot() @process(isGray=True) def grayHistogram(self, src): # 1.57 图像的灰度直方图 img = src hist_cv = cv2.calcHist([img], [0], None, [256], [0, 256]) # OpenCV 函数 cv2.calcHist plt.bar(range(256), hist_cv[:, 0]) ''' 用opencv和numpy两种方法得到直方图 hist_np, bins = np.histogram(img.flatten(), 256) plt.subplot(132,xticks=[], yticks=[]) plt.axis([0,255,0,np.max(hist_cv)]) plt.bar(range(256), hist_cv[:,0]) plt.title("Gray Hist(cv2.calcHist)") plt.subplot(133,xticks=[], yticks=[]) plt.axis([0,255,0,np.max(hist_cv)]) plt.bar(bins[:-1], hist_np) plt.title("Gray Hist(np.histogram)") ''' plt.show() # 边缘检测 @Slot() @process(isGray=True) def roberts(self, src): img = src # Roberts 边缘算子 kernel_Roberts_x = np.array([[1, 0], [0, -1]]) kernel_Roberts_y = np.array([[0, -1], [1, 0]]) imgRoberts_x = cv2.filter2D(img, cv2.CV_16S, kernel_Roberts_x) imgRoberts_y = cv2.filter2D(img, cv2.CV_16S, kernel_Roberts_y) imgRoberts_x_abs = cv2.convertScaleAbs(imgRoberts_x) imgRoberts_y_abs = cv2.convertScaleAbs(imgRoberts_y) imgRoberts = cv2.addWeighted(imgRoberts_x_abs, 0.5, imgRoberts_y_abs, 0.5, 0) return imgRoberts @Slot() @process(isGray=True) def sobel(self, src): img = src # kernel_Sobel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]) # kernel_Sobel_y = np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]]) imgSobel_x = cv2.Sobel(img, cv2.CV_16S, 1, 0) imgSobel_y = cv2.Sobel(img, cv2.CV_16S, 0, 1) imgSobel_x_abs = cv2.convertScaleAbs(imgSobel_x) imgSobel_y_abs = cv2.convertScaleAbs(imgSobel_y) imgSobel = cv2.addWeighted(imgSobel_x_abs, 0.5, imgSobel_y_abs, 0.5, 0) return imgSobel @Slot() @process(isGray=True) def prewitt(self, src): img = src kernel_Prewitt_x = np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]]) kernel_Prewitt_y = np.array([[1, 1, 1], [0, 0, 0], [-1, -1, -1]]) imgPrewitt_x = cv2.filter2D(img, cv2.CV_16S, kernel_Prewitt_x) imgPrewitt_y = cv2.filter2D(img, cv2.CV_16S, kernel_Prewitt_y) imgPrewitt_x_abs = cv2.convertScaleAbs(imgPrewitt_x) imgPrewitt_y_abs = cv2.convertScaleAbs(imgPrewitt_y) imgPrewitt = cv2.addWeighted(imgPrewitt_x_abs, 0.5, imgPrewitt_y_abs, 0.5, 0) return imgPrewitt @Slot() @process(isGray=True) def laplacian(self, src): img = src # kernel_Laplacian_K1 = np.array([[0, 1, 0], [1, -4, 1], [0, 1, 0]]) # imgLaplacian = cv2.filter2D(img, -1, kernel_Laplacian_K1) imgLaplacian = cv2.Laplacian(img, cv2.CV_16S, ksize=3) imgLaplacian_abs = cv2.convertScaleAbs(imgLaplacian) return imgLaplacian_abs @Slot(int, float) @process(isGray=True) def loG(self, size, sigma, src): img = src imgGaussBlur = cv2.GaussianBlur(img, (size, size), sigma) imgLoG = cv2.Laplacian(imgGaussBlur, cv2.CV_16S, ksize=size) imgLoG_abs = cv2.convertScaleAbs(imgLoG) return imgLoG_abs @Slot(int, int, int) @process(isGray=True) def canny(self, size, low_thresh, high_thresh, src): img = src kSize = (size, size) imgGauss = cv2.GaussianBlur(img, kSize, sigmaX=1.0) # sigma=1.0 sobel_x = cv2.Sobel(imgGauss, cv2.CV_16S, 1, 0) sobel_y = cv2.Sobel(imgGauss, cv2.CV_16S, 0, 1) imgCanny = cv2.Canny(sobel_x, sobel_y, low_thresh, high_thresh) return imgCanny # 图像平滑 @Slot(int) @process() def blur(self, size, src): img = src imgBlur = cv2.blur(img, (size, size)) return imgBlur @Slot(int) @process() def medianBlur(self, size, src): # 1.73:图像的非线性滤波 (中值滤波器) img = src imgMedianBlur = cv2.medianBlur(img, size) return imgMedianBlur @Slot(int) @process(isGray=True) def lowPassIdealBlur(self, D0, src): img = src height, width = img.shape[:2] centerX, centerY = int(height / 2), int(width / 2) x, y = np.mgrid[0:height, 0:width] D = np.sqrt(np.power(x - centerX, 2) + np.power(y - centerY, 2)) H = np.where(D > D0, 0, 1) fft = np.fft.fft2(img) # 傅里叶变换 fft_shift = np.fft.fftshift(fft) # 中心化 fft_mask = fft_shift * H ifft_shift = np.fft.ifftshift(fft_mask) ifft = np.fft.ifft2(ifft_shift) ifft_abs = np.abs(ifft) return cv2.convertScaleAbs(ifft_abs) @Slot(int, int) @process(isGray=True) def lowPassButterworthBlur(self, D0, n, src): img = src height, width = img.shape[:2] centerX, centerY = int(height / 2), int(width / 2) x, y = np.mgrid[0:height, 0:width] D = np.sqrt(np.power(x - centerX, 2) + np.power(y - centerY, 2)) H = 1.0 / (1.0 + np.power(D / D0, 2 * n)) fft = np.fft.fft2(img) # 傅里叶变换 fft_shift = np.fft.fftshift(fft) # 中心化 fft_mask = fft_shift * H ifft_shift = np.fft.ifftshift(fft_mask) ifft = np.fft.ifft2(ifft_shift) ifft_abs = np.abs(ifft) return cv2.convertScaleAbs(ifft_abs) @Slot(int) @process(isGray=True) def lowPassGaussianBlur(self, D0, src): img = src height, width = img.shape[:2] centerX, centerY = int(height / 2), int(width / 2) x, y = np.mgrid[0:height, 0:width] D = np.sqrt(np.power(x - centerX, 2) + np.power(y - centerY, 2)) H = np.exp(-1 * np.power(D, 2) / (2 * np.power(D0, 2))) fft = np.fft.fft2(img) # 傅里叶变换 fft_shift = np.fft.fftshift(fft) # 中心化 fft_mask = fft_shift * H ifft_shift = np.fft.ifftshift(fft_mask) ifft = np.fft.ifft2(ifft_shift) ifft_abs = np.abs(ifft) return cv2.convertScaleAbs(ifft_abs) # 形态处理 @Slot(int, int) @process() def erode(self, shape, size, src): # 读取原始图像 img = src kernel = cv2.getStructuringElement(shape, (size, size)) imgErode = cv2.erode(img, kernel=kernel) # 图像腐蚀 return imgErode @Slot(int, int) @process() def dilate(self, shape, size, src): # 读取原始图像 img = src kernel = cv2.getStructuringElement(shape, (size, size)) imgDilate = cv2.dilate(img, kernel=kernel) # 图像膨胀 return imgDilate @Slot(int, int) @process() def open(self, shape, size, src): # 读取原始图像 img = src kernel = cv2.getStructuringElement(shape, (size, size)) imgOpen = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel) return imgOpen @Slot(int, int) @process() def close(self, shape, size, src): # 读取原始图像 img = src kernel = cv2.getStructuringElement(shape, (size, size)) imgClose = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel) return imgClose # 噪声添加 @Slot(float, float) @process() def gaussNoise(self, mu, sigma, src): img = src noiseGauss = np.random.normal(mu, sigma, img.shape) imgGaussNoise = img + noiseGauss return cv2.convertScaleAbs(imgGaussNoise) @Slot(float) @process() def expNoise(self, a, src): # 指数噪声 (Exponential noise) img = src noiseExponent = np.random.exponential(scale=a, size=img.shape) imgExponentNoise = img + noiseExponent return cv2.convertScaleAbs(imgExponentNoise) @Slot(float, float) @process() def uniformNoise(self, mean, sigma, src): img = src a = 2 * mean - np.sqrt(12 * sigma) b = 2 * mean + np.sqrt(12 * sigma) noiseUniform = np.random.uniform(a, b, img.shape) imgUniformNoise = img + noiseUniform return cv2.convertScaleAbs(imgUniformNoise) @Slot(float, float) @process() def pepperSaltNoise(self, ps, pp, src): img = src mask = np.random.choice((0, 0.5, 1), size=img.shape[:2], p=[pp, (1 - ps - pp), ps]) imgChoiceNoise = img.copy() imgChoiceNoise[mask == 1] = 255 imgChoiceNoise[mask == 0] = 0 return imgChoiceNoise # 风格迁移 @Slot(str) @process() def styleTransfer(self, style, src): predictor = LapStylePredictor(style) img = predictor.run(src) return img if __name__ == "__main__": app = QApplication(sys.argv) engine = QQmlApplicationEngine() engine.load(os.fspath(Path(__file__).resolve().parent / "main.qml")) if not engine.rootObjects(): sys.exit(-1) sys.exit(app.exec())