窗口的透视变换效果

当我们点击UWP应用中的小部件时,会发现小部件会朝着鼠标点击位置凹陷下去,而且不同的点击位置对应着不同的凹陷情况,看起来就好像小部件在屏幕上不只有x轴和y轴,甚至还有一个z轴。要做到这一点,其实只要对窗口进行透视变换即可。下面是对Qt的窗口和按钮进行透视变换的效果:

具体代码

PixmapPerspectiveTransform 类

它的作用是将传入的 QPixmap 转换为numpy 数组,然后用 opencvwarpPerspective 对数组进行透视变换,最后再将 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_1img_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。要对按钮也进行透视变换,只要按代码中所做的那样重写mousePressEventmouseReleaseEventpaintEvent 即可,如果有对按钮使用qss,记得在 paintEvent 中加上super().paintEvent(e),这样样式表才会起作用。总之框架已经给出,具体操作取决于你。如果你喜欢这篇博客的话,记得点个赞哦~~

如何在pyqt中通过OpenCV实现对窗口的透视变换的更多相关文章

  1. 如何在pyqt中在实现无边框窗口的同时保留Windows窗口动画效果(一)

    无边框窗体的实现思路 在pyqt中只要 self.setWindowFlags(Qt.FramelessWindowHint) 就可以实现边框的去除,但是没了标题栏也意味着窗口大小无法改变.窗口无法拖 ...

  2. 如何在pyqt中自定义无边框窗口

    前言 之前写过很多关于无边框窗口并给窗口添加特效的博客,按照时间线罗列如下: 如何在pyqt中实现窗口磨砂效果 如何在pyqt中实现win10亚克力效果 如何在pyqt中通过调用SetWindowCo ...

  3. 如何在pyqt中给无边框窗口添加DWM环绕阴影

    前言 在之前的博客<如何在pyqt中通过调用SetWindowCompositionAttribute实现Win10亚克力效果>中,我们实现了窗口的亚克力效果,同时也用SetWindowC ...

  4. 如何在Android中使用OpenCV

    如何在Android中使用OpenCV 2011-09-21 10:22:35 标签:Android 移动开发 JNI OpenCV NDK 原创作品,允许转载,转载时请务必以超链接形式标明文章 原始 ...

  5. 如何在pyqt中实现窗口磨砂效果

    磨砂效果的实现思路 这两周一直在思考怎么在pyqt上实现窗口磨砂效果,网上搜了一圈,全都是 C++ 的实现方法.正好今天查python的官方文档的时候看到了 ctypes 里面的 HWND,想想倒不如 ...

  6. 如何在pyqt中实现win10亚克力效果

    亚克力效果的实现思路 上一篇博客<如何在pyqt中实现窗口磨砂效果> 中实现了win7中的Aero效果,但是和win10的亚克力效果相比,Aero还是差了点内味.所以今天早上又在网上搜了一 ...

  7. 如何在pyqt中通过调用 SetWindowCompositionAttribute 实现Win10亚克力效果

    亚克力效果 在<如何在pyqt中实现窗口磨砂效果>和<如何在pyqt中实现win10亚克力效果>中,我们调用C++ dll来实现窗口效果,这种方法要求电脑上必须装有MSVC.V ...

  8. 如何在pyqt中实现带动画的动态QMenu

    弹出菜单的视觉效果 QLineEdit 原生的菜单弹出效果十分生硬,而且样式很丑.所以照着Groove中单行输入框弹出菜单的样式和动画效果写了一个可以实现动态变化Item的弹出菜单,根据剪贴板的内容是 ...

  9. 如何在 pyqt 中捕获并处理 Alt+F4 快捷键

    前言 如果在 Windows 系统的任意一个窗口中按下 Alt+F4,默认行为是关闭窗口(或者最小化到托盘).对于使用了亚克力效果的窗口,使用 Alt+F4 最小化到托盘,再次弹出窗口的时候可能出现亚 ...

随机推荐

  1. 魔法串(hud4545)

    魔法串 Time Limit: 3000/1000 MS (Java/Others)    Memory Limit: 65535/32768 K (Java/Others)Total Submiss ...

  2. uniapp 兼容H5复制文本功能,亲测可用

    封装copyText函数,具体如下: copyText(val){ let result // #ifndef H5 uni.setClipboardData({ data: val, success ...

  3. CS5211芯片|EDP to LVDS|CS5211应用方案

    CS5211芯片–EDP to LVDSDisplayPort到LVDS转换器双通道DP输入,双链路LVDS输出CS5211是一个显示端口到LVDS转换器设计的PC机,利用GPU和显示端口(DP) 或 ...

  4. 【优雅代码】01-lombok精选注解及原理

    [优雅代码]01-lombok精选注解及原理 欢迎关注b站账号/公众号[六边形战士夏宁],一个要把各项指标拉满的男人.该文章已在github目录收录. 屏幕前的大帅比和大漂亮如果有帮助到你的话请顺手点 ...

  5. 【jvm】03-写了final就是常量池了么

    [jvm]03-写了final就是常量池了么 欢迎关注b站账号/公众号[六边形战士夏宁],一个要把各项指标拉满的男人.该文章已在github目录收录. 屏幕前的大帅比和大漂亮如果有帮助到你的话请顺手点 ...

  6. .NET 云原生架构师训练营(设计原则&&设计模式)--学习笔记

    目录 设计原则 设计模式 设计原则 DRY (Don't repeat yourself 不要重复) KISS (Keep it stupid simple 简单到傻子都能看懂) YAGNI (You ...

  7. SpringCloud创建Config模块

    1.说明 本文详细介绍Spring Cloud创建Config模块的方法, 基于已经创建好的Spring Cloud父工程, 请参考SpringCloud创建项目父工程, 创建Config模块这个子工 ...

  8. CAS学习笔记三:SpringBoot自动配置与手动配置过滤器方式集成CAS客户端

    本文目标 基于SpringBoot + Maven 分别使用自动配置与手动配置过滤器方式集成CAS客户端. 需要提前搭建 CAS 服务端,参考 https://www.cnblogs.com/hell ...

  9. Swoole 中使用 Context 类管理上下文,防止发生数据错乱

    前面的文章中,我们说过:不能使用类静态变量 Class::$array / 全局变量 global $_array / 全局对象属性 $object->array / 其他超全局变量 $GLOB ...

  10. linux分区(挂载)

    主分区: 最多4个扩展分区: 最多一个: 扩展分区+主分区最多4个: 不能存放数据,只能划分逻辑分区逻辑分区: 可格式化.存放数据: 分区编号只能从5开始(1.2.3.4编号为主分区或扩展分区) 所有 ...