详解如何在pyqt中通过OpenCV实现对窗口的透视变换
窗口的透视变换效果
当我们点击Win10的UWP应用中的小部件时,会发现小部件会朝着鼠标点击位置凹陷下去,而且不同的点击位置对应着不同的凹陷情况,看起来就好像小部件在屏幕上不只有x轴和y轴,甚至还有一个z轴。要做到这一点,其实只要对窗口进行透视变换即可。下面是对Qt的窗口和按钮进行透视变换的效果:
具体代码
1.下面先定义一个类,它的作用是将传入的 QPixmap
转换为numpy
数组,然后用 opencv
的 warpPerspective
对数组进行透视变换,最后再将 numpy
数组转为 QPixmap
并返回;
# coding:utf-8 import cv2 as cv import numpy from PyQt5.QtGui import QImage, QPixmap class PixmapPerspectiveTransform: """ 透视变换基类 """ def __init__(self, pixmap=None): """ 实例化透视变换对象\n Parameter --------- src : numpy数组 """ self.pixmap = pixmap def setPixmap(self, pixmap: QPixmap): """ 设置被变换的QPixmap """ self.pixmap = QPixmap self.src=self.transQPixmapToNdarray(pixmap) self.height, self.width = self.src.shape[:2] # 变换前后的边角坐标 self.srcPoints = numpy.float32( [[0, 0], [self.width - 1, 0], [0, self.height - 1], [self.width - 1, self.height - 1]]) def setDstPoints(self, leftTop: list, rightTop, leftBottom, rightBottom): """ 设置变换后的边角坐标 """ self.dstPoints = numpy.float32( [leftTop, rightTop, leftBottom, rightBottom]) def getPerspectiveTransform(self, imWidth, imHeight, borderMode=cv.BORDER_CONSTANT, borderValue=[255, 255, 255, 0]) -> QPixmap: """ 透视变换图像,返回QPixmap\n Parameters ---------- imWidth : 变换后的图像宽度\n imHeight : 变换后的图像高度\n borderMode : 边框插值方式\n borderValue : 边框颜色 """ # 如果是jpg需要加上一个透明通道 if self.src.shape[-1] == 3: self.src = cv.cvtColor(self.src, cv.COLOR_BGR2BGRA) # 透视变换矩阵 perspectiveMatrix = cv.getPerspectiveTransform( self.srcPoints, self.dstPoints) # 执行变换 self.dst = cv.warpPerspective(self.src, perspectiveMatrix, ( imWidth, imHeight), borderMode=borderMode, borderValue=borderValue) # 将ndarray转换为QPixmap return self.transNdarrayToQPixmap(self.dst) def transQPixmapToNdarray(self, pixmap: QPixmap): """ 将QPixmap转换为numpy数组 """ width, height = pixmap.width(), pixmap.height() channels_count = 4 image = pixmap.toImage() # type:QImage s = image.bits().asstring(height * width * channels_count) # 得到BGRA格式数组 array = numpy.fromstring(s, numpy.uint8).reshape( (height, width, channels_count)) return array def transNdarrayToQPixmap(self, array): """ 将numpy数组转换为QPixmap """ height, width, bytesPerComponent = array.shape bytesPerLine = 4 * width # 默认数组维度为 m*n*4 dst = cv.cvtColor(array, cv.COLOR_BGRA2RGBA) pix = QPixmap.fromImage( QImage(dst.data, width, height, bytesPerLine, QImage.Format_RGBA8888)) return pix
2.接下来就是这篇博客的主角——PerspectiveWidget
,当我们的鼠标单击这个类实例化出来的窗口时,窗口会先通过 self.grab()
被渲染为QPixmap,然后调用 PixmapPerspectiveTransform
中的方法对QPixmap进行透视变换,拿到透视变换的结果后只需隐藏窗口内的小部件并通过 PaintEvent
将结果绘制到窗口上即可。虽然思路很通顺,但是实际操作起来会发现对于透明背景的窗口进行透视变换时,与透明部分交界的部分会被插值上半透明的像素。对于本来就属于深色的像素来说这没什么,但是如果像素是浅色的就会带来很大的视觉干扰,你会发现这些浅色部分旁边被描上了一圈黑边,我们先将这个图像记为img_1
。img_1差不多长这个样子,可以很明显看出白色的文字围绕着一圈黑色的描边。
为了解决这个烦人的问题,我又对桌面上的窗口进行截屏,再次透视变换。注意是桌面上看到的窗口,这时的窗口肯定是会有背景的,这时的透视变换就不会存在上述问题,记这个透视变换完的图像为img_2
。但实际上我们本来是不想要img_2中的背景的,所以只要将img_2中的背景替换完img_1中的透明背景,下面是具体代码:
# coding:utf-8 import numpy as np from PyQt5.QtCore import QPoint, Qt from PyQt5.QtGui import QPainter, QPixmap, QScreen, QImage from PyQt5.QtWidgets import QApplication, QWidget from my_functions.get_pressed_pos import getPressedPos from my_functions.perspective_transform_cv import PixmapPerspectiveTransform class PerspectiveWidget(QWidget): """ 可进行透视变换的窗口 """ def __init__(self, parent=None, isTransScreenshot=False): super().__init__(parent) self.__visibleChildren = [] self.__isTransScreenshot = isTransScreenshot self.__perspectiveTrans = PixmapPerspectiveTransform() self.__screenshotPix = None self.__pressedPix = None self.__pressedPos = None @property def pressedPos(self) -> str: """ 返回鼠标点击位置 """ return self.__pressedPos def mousePressEvent(self, e): """ 鼠标点击窗口时进行透视变换 """ super().mousePressEvent(e) self.grabMouse() pixmap = self.grab() self.__perspectiveTrans.setPixmap(pixmap) # 根据鼠标点击位置的不同设置背景封面的透视变换 self.__setDstPointsByPressedPos(getPressedPos(self,e)) # 获取透视变换后的QPixmap self.__pressedPix = self.__getTransformPixmap() # 对桌面上的窗口进行截图 if self.__isTransScreenshot: self.__adjustTransformPix() # 隐藏本来看得见的小部件 self.__visibleChildren = [ child for child in self.children() if hasattr(child, 'isVisible') and child.isVisible()] for child in self.__visibleChildren: if hasattr(child, 'hide'): child.hide() self.update() def mouseReleaseEvent(self, e): """ 鼠标松开时显示小部件 """ super().mouseReleaseEvent(e) self.releaseMouse() self.__pressedPos = None self.update() # 显示小部件 for child in self.__visibleChildren: if hasattr(child, 'show'): child.show() def paintEvent(self, e): """ 绘制背景 """ super().paintEvent(e) painter = QPainter(self) painter.setRenderHints(QPainter.Antialiasing | QPainter.HighQualityAntialiasing | QPainter.SmoothPixmapTransform) painter.setPen(Qt.NoPen) # 绘制背景图片 if self.__pressedPos: painter.drawPixmap(self.rect(), self.__pressedPix) def __setDstPointsByPressedPos(self,pressedPos:str): """ 通过鼠标点击位置设置透视变换的四个边角坐标 """ self.__pressedPos = pressedPos if self.__pressedPos == 'left': self.__perspectiveTrans.setDstPoints( [5, 4], [self.__perspectiveTrans.width - 2, 1], [3, self.__perspectiveTrans.height - 3], [self.__perspectiveTrans.width - 2, self.__perspectiveTrans.height - 1]) elif self.__pressedPos == 'left-top': self.__perspectiveTrans.setDstPoints( [6, 5], [self.__perspectiveTrans.width - 1, 1], [1, self.__perspectiveTrans.height - 2], [self.__perspectiveTrans.width - 2, self.__perspectiveTrans.height - 1]) elif self.__pressedPos == 'left-bottom': self.__perspectiveTrans.setDstPoints( [2, 3], [self.__perspectiveTrans.width - 3, 0], [4, self.__perspectiveTrans.height - 4], [self.__perspectiveTrans.width - 2, self.__perspectiveTrans.height - 2]) elif self.__pressedPos == 'top': self.__perspectiveTrans.setDstPoints( [3, 5], [self.__perspectiveTrans.width - 4, 5], [1, self.__perspectiveTrans.height - 2], [self.__perspectiveTrans.width - 2, self.__perspectiveTrans.height - 2]) elif self.__pressedPos == 'center': self.__perspectiveTrans.setDstPoints( [3, 4], [self.__perspectiveTrans.width - 4, 4], [3, self.__perspectiveTrans.height - 3], [self.__perspectiveTrans.width - 4, self.__perspectiveTrans.height - 3]) elif self.__pressedPos == 'bottom': self.__perspectiveTrans.setDstPoints( [2, 2], [self.__perspectiveTrans.width - 3, 3], [3, self.__perspectiveTrans.height - 3], [self.__perspectiveTrans.width - 4, self.__perspectiveTrans.height - 3]) elif self.__pressedPos == 'right-bottom': self.__perspectiveTrans.setDstPoints( [1, 0], [self.__perspectiveTrans.width - 3, 2], [1, self.__perspectiveTrans.height - 2], [self.__perspectiveTrans.width - 5, self.__perspectiveTrans.height - 4]) elif self.__pressedPos == 'right-top': self.__perspectiveTrans.setDstPoints( [0, 1], [self.__perspectiveTrans.width - 7, 5], [2, self.__perspectiveTrans.height - 1], [self.__perspectiveTrans.width - 2, self.__perspectiveTrans.height - 2]) elif self.__pressedPos == 'right': self.__perspectiveTrans.setDstPoints( [1, 1], [self.__perspectiveTrans.width - 6, 4], [2, self.__perspectiveTrans.height - 1], [self.__perspectiveTrans.width - 4, self.__perspectiveTrans.height - 3]) def __getTransformPixmap(self) -> QPixmap: """ 获取透视变换后的QPixmap """ pix = self.__perspectiveTrans.getPerspectiveTransform( self.__perspectiveTrans.width, self.__perspectiveTrans.height).scaled( self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) return pix def __getScreenShot(self) -> QPixmap: """ 对窗口口所在的桌面区域进行截图 """ screen = QApplication.primaryScreen() # type:QScreen pos = self.mapToGlobal(QPoint(0, 0)) # type:QPoint pix = screen.grabWindow( 0, pos.x(), pos.y(), self.width(), self.height()) return pix def __adjustTransformPix(self): """ 对窗口截图再次进行透视变换并将两张图融合,消除可能存在的黑边 """ self.__screenshotPix = self.__getScreenShot() self.__perspectiveTrans.setPixmap(self.__screenshotPix) self.__screenshotPressedPix = self.__getTransformPixmap() # 融合两张透视图 img_1 = self.__perspectiveTrans.transQPixmapToNdarray(self.__pressedPix) img_2 = self.__perspectiveTrans.transQPixmapToNdarray(self.__screenshotPressedPix) # 去除非透明背景部分 mask = img_1[:, :, -1] == 0 img_2[mask] = img_1[mask] self.__pressedPix = self.__perspectiveTrans.transNdarrayToQPixmap(img_2)
在mousePressEvent
中调用了一个全局函数 getPressedPos(widget,e)
,如果将窗口分为九宫格,它就是用来获取判断鼠标的点击位置落在九宫格的哪个格子的,因为我在其他地方有用到它,所以没将其设置为PerspectiveWidget
的方法成员。下面是这个函数的代码:
# coding:utf-8 from PyQt5.QtGui import QMouseEvent def getPressedPos(widget, e: QMouseEvent) -> str: """ 检测鼠标并返回按下的方位 """ pressedPos = None width = widget.width() height = widget.height() leftX = 0 <= e.x() <= int(width / 3) midX = int(width / 3) < e.x() <= int(width * 2 / 3) rightX = int(width * 2 / 3) < e.x() <= width topY = 0 <= e.y() <= int(height / 3) midY = int(height / 3) < e.y() <= int(height * 2 / 3) bottomY = int(height * 2 / 3) < e.y() <= height # 获取点击位置 if leftX and topY: pressedPos = 'left-top' elif midX and topY: pressedPos = 'top' elif rightX and topY: pressedPos = 'right-top' elif leftX and midY: pressedPos = 'left' elif midX and midY: pressedPos = 'center' elif rightX and midY: pressedPos = 'right' elif leftX and bottomY: pressedPos = 'left-bottom' elif midX and bottomY: pressedPos = 'bottom' elif rightX and bottomY: pressedPos = 'right-bottom' return pressedPos
使用方法
很简单,只要将代码中的QWidget替换为PerspectiveWidget就可以享受透视变换带来的无尽乐趣。要想向gif中那样对按钮也进行透视变换,只要按代码中所做的那样重写mousePressEvent
、mouseReleaseEvent
和 paintEven
t 即可,如果有对按钮使用qss,记得在paintEvent中加上super().paintEvent(e)
,这样样式表才会起作用。总之框架已经给出,具体操作取决于你。如果你喜欢这篇博客的话,记得点个赞哦(o゚▽゚)o 。顺便做个下期预告:在gif中可以看到界面切换时带了弹入弹出的动画,在下一篇博客中我会对如何实现QStackedWidget的界面切换动画进行介绍,敬请期待~~
到此这篇关于详解如何在pyqt中通过OpenCV实现对窗口的透视变换的文章就介绍到这了,更多相关pyqt OpenCV窗口透视变换内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!