窗口的透视变换效果
当我们点击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: """ 透视变换图像,返回qpixmapn 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窗口透视变换内容请搜索<计算机技术网(www.ctvol.com)!!>以前的文章或继续浏览下面的相关文章希望大家以后多多支持<计算机技术网(www.ctvol.com)!!>!
本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。
ctvol管理联系方式QQ:251552304
本文章地址:https://www.ctvol.com/c-cdevelopment/597500.html