如何在pyqt中通过OpenCV实现对窗口的透视变换
窗口的透视变换效果
当我们点击UWP应用中的小部件时,会发现小部件会朝着鼠标点击位置凹陷下去,而且不同的点击位置对应着不同的凹陷情况,看起来就好像小部件在屏幕上不只有x轴和y轴,甚至还有一个z轴。要做到这一点,其实只要对窗口进行透视变换即可。下面是对Qt的窗口和按钮进行透视变换的效果:
具体代码
PixmapPerspectiveTransform 类
它的作用是将传入的 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):
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
Parameters
----------
imWidth: int
变换后的图像宽度
imHeight: int
变换后的图像高度
borderMode: int
边框插值方式
borderValue: list
边框颜色
"""
# 如果是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
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)
# 多次点击时不响应,防止小部件的再次隐藏
if self.__pressedPos:
return
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(
[7, 6], [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(
[0, 1], [self.__perspectiveTrans.width - 3, 0],
[6, self.__perspectiveTrans.height - 5],
[self.__perspectiveTrans.width - 2, self.__perspectiveTrans.height - 2])
elif self.__pressedPos == 'top':
self.__perspectiveTrans.setDstPoints(
[4, 5], [self.__perspectiveTrans.width - 5, 5],
[0, self.__perspectiveTrans.height - 1],
[self.__perspectiveTrans.width - 1, self.__perspectiveTrans.height - 1])
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(
[0, 0], [self.__perspectiveTrans.width - 1, 0],
[4, self.__perspectiveTrans.height - 4],
[self.__perspectiveTrans.width - 5, self.__perspectiveTrans.height - 4])
elif self.__pressedPos == 'right-bottom':
self.__perspectiveTrans.setDstPoints(
[1, 0], [self.__perspectiveTrans.width - 3, 2],
[1, self.__perspectiveTrans.height - 2],
[self.__perspectiveTrans.width - 6, self.__perspectiveTrans.height - 5])
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
。要对按钮也进行透视变换,只要按代码中所做的那样重写mousePressEvent
、mouseReleaseEvent
和 paintEvent
即可,如果有对按钮使用qss,记得在 paintEvent
中加上super().paintEvent(e)
,这样样式表才会起作用。总之框架已经给出,具体操作取决于你。如果你喜欢这篇博客的话,记得点个赞哦~~
如何在pyqt中通过OpenCV实现对窗口的透视变换的更多相关文章
- 如何在pyqt中在实现无边框窗口的同时保留Windows窗口动画效果(一)
无边框窗体的实现思路 在pyqt中只要 self.setWindowFlags(Qt.FramelessWindowHint) 就可以实现边框的去除,但是没了标题栏也意味着窗口大小无法改变.窗口无法拖 ...
- 如何在pyqt中自定义无边框窗口
前言 之前写过很多关于无边框窗口并给窗口添加特效的博客,按照时间线罗列如下: 如何在pyqt中实现窗口磨砂效果 如何在pyqt中实现win10亚克力效果 如何在pyqt中通过调用SetWindowCo ...
- 如何在pyqt中给无边框窗口添加DWM环绕阴影
前言 在之前的博客<如何在pyqt中通过调用SetWindowCompositionAttribute实现Win10亚克力效果>中,我们实现了窗口的亚克力效果,同时也用SetWindowC ...
- 如何在Android中使用OpenCV
如何在Android中使用OpenCV 2011-09-21 10:22:35 标签:Android 移动开发 JNI OpenCV NDK 原创作品,允许转载,转载时请务必以超链接形式标明文章 原始 ...
- 如何在pyqt中实现窗口磨砂效果
磨砂效果的实现思路 这两周一直在思考怎么在pyqt上实现窗口磨砂效果,网上搜了一圈,全都是 C++ 的实现方法.正好今天查python的官方文档的时候看到了 ctypes 里面的 HWND,想想倒不如 ...
- 如何在pyqt中实现win10亚克力效果
亚克力效果的实现思路 上一篇博客<如何在pyqt中实现窗口磨砂效果> 中实现了win7中的Aero效果,但是和win10的亚克力效果相比,Aero还是差了点内味.所以今天早上又在网上搜了一 ...
- 如何在pyqt中通过调用 SetWindowCompositionAttribute 实现Win10亚克力效果
亚克力效果 在<如何在pyqt中实现窗口磨砂效果>和<如何在pyqt中实现win10亚克力效果>中,我们调用C++ dll来实现窗口效果,这种方法要求电脑上必须装有MSVC.V ...
- 如何在pyqt中实现带动画的动态QMenu
弹出菜单的视觉效果 QLineEdit 原生的菜单弹出效果十分生硬,而且样式很丑.所以照着Groove中单行输入框弹出菜单的样式和动画效果写了一个可以实现动态变化Item的弹出菜单,根据剪贴板的内容是 ...
- 如何在 pyqt 中捕获并处理 Alt+F4 快捷键
前言 如果在 Windows 系统的任意一个窗口中按下 Alt+F4,默认行为是关闭窗口(或者最小化到托盘).对于使用了亚克力效果的窗口,使用 Alt+F4 最小化到托盘,再次弹出窗口的时候可能出现亚 ...
随机推荐
- 关于python常用的命令
os模块: os.system os.system("cat/cmd等...") os.popen print os.popen("adb shell ls ../aa. ...
- 带你熟悉鸿蒙轻内核Kconfig使用指南
摘要:本文介绍了Kconfig的基础知识,和鸿蒙轻内核的图形化配置及进阶的使用方法. 本文分享自华为云社区<鸿蒙轻内核Kconfig使用笔记>,作者: zhushy. 1. Kconfig ...
- 第四十九个知识点:描述在IPsec和TLS后的基本想法
第四十九个知识点:描述在IPsec和TLS后的基本想法 网络安全协议(Internet Protocol Security,IPsec)和安全传输层协议(Transport Layer Securit ...
- Feign动态调用,结合Ribbon
代码如下,三种方法: import org.springframework.beans.factory.annotation.Autowired;import org.springframework. ...
- WHT, SLANT, Haar
目录 基本 酉变换 WALSH-HADAMARD TRANSFORMS sequency-ordered WHT SLANT TRANSFORM Haar Transform Gonzalez R. ...
- [转]opencv2早期调用函数
1.cvLoadImage:将图像文件加载至内存: 2.cvNamedWindow:在屏幕上创建一个窗口: 3.cvShowImage:在一个已创建好的窗口中显示图像: 4.cvWaitKey:使程序 ...
- Spring企业级程序设计 • 【第4章 Spring持久化层和事务管理】
全部章节 >>>> 本章目录 4.1 配置数据源资源 4.1.1 JdbcTemplate介绍 4.1.2通过ComboPooledDataSource创建数据源 4.1. ...
- 20道JavaScript经典面试题
该篇文章整理了一些前端经典面试题,附带详解,涉及到JavaScript多方面知识点,满满都是干货-建议收藏阅读 前言 如果这篇文章有帮助到你,️关注+点赞️鼓励一下作者,文章公众号首发,关注 前端南玖 ...
- Canvas原生API(纯CPU)计算并渲染三维图
Canvas原生API(纯CPU)计算并渲染三维图 前端工程师学图形学:Games101 第三次作业 利用Canvas画三维中的三角形并使用超采样实现抗锯齿 最终完成功能 Canvas 原生API实现 ...
- CentOS 7 连接不到网络解决方法(设置静态ip)
使用VM12创建虚拟机并安装CentOS 7,但是安装完成后发现连接不到网络. ping jd.com发现不通 因为在创建虚拟机的时候 我们选择的是NAT模式 这里给出NAT模式下对应的的解决方法: ...