详解如何在pyqt中通过OpenCV实现对窗口的透视变换


Posted in Python onSeptember 20, 2020

窗口的透视变换效果

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

详解如何在pyqt中通过OpenCV实现对窗口的透视变换

具体代码

   1.下面先定义一个类,它的作用是将传入的 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):
  """ 实例化透视变换对象\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差不多长这个样子,可以很明显看出白色的文字围绕着一圈黑色的描边。

详解如何在pyqt中通过OpenCV实现对窗口的透视变换

为了解决这个烦人的问题,我又对桌面上的窗口进行截屏,再次透视变换。注意是桌面上看到的窗口,这时的窗口肯定是会有背景的,这时的透视变换就不会存在上述问题,记这个透视变换完的图像为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中那样对按钮也进行透视变换,只要按代码中所做的那样重写mousePressEventmouseReleaseEventpaintEvent 即可,如果有对按钮使用qss,记得在paintEvent中加上super().paintEvent(e),这样样式表才会起作用。总之框架已经给出,具体操作取决于你。如果你喜欢这篇博客的话,记得点个赞哦(o゚?゚)o 。顺便做个下期预告:在gif中可以看到界面切换时带了弹入弹出的动画,在下一篇博客中我会对如何实现QStackedWidget的界面切换动画进行介绍,敬请期待~~

到此这篇关于详解如何在pyqt中通过OpenCV实现对窗口的透视变换的文章就介绍到这了,更多相关pyqt OpenCV窗口透视变换内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Python 相关文章推荐
Python通过PIL获取图片主要颜色并和颜色库进行对比的方法
Mar 19 Python
Python中文分词实现方法(安装pymmseg)
Jun 14 Python
安装python3的时候就是输入python3死活没有反应的解决方法
Jan 24 Python
python负载均衡的简单实现方法
Feb 04 Python
python实现在pandas.DataFrame添加一行
Apr 04 Python
完美解决Python 2.7不能正常使用pip install的问题
Jun 12 Python
10个Python小技巧你值得拥有
Sep 29 Python
python将一个英文语句以单词为单位逆序排放的方法
Dec 20 Python
利用python修改json文件的value方法
Dec 31 Python
Python实现的栈、队列、文件目录遍历操作示例
May 06 Python
Python qqbot 实现qq机器人的示例代码
Jul 11 Python
python 图像判断,清晰度(明暗),彩色与黑白实例
Jun 04 Python
Python Pillow(PIL)库的用法详解
Sep 19 #Python
Python自动化xpath实现自动抢票抢货
Sep 19 #Python
python 贪心算法的实现
Sep 18 #Python
详解KMP算法以及python如何实现
Sep 18 #Python
python实现二分查找算法
Sep 18 #Python
Python自定义sorted排序实现方法详解
Sep 18 #Python
python爬虫爬取网页数据并解析数据
Sep 18 #Python
You might like
PHP安全防范技巧分享
2011/11/03 PHP
php中实现记住密码下次自动登录的例子
2014/11/06 PHP
php查询whois信息的方法
2015/06/08 PHP
PHP执行shell脚本运行程序不产生core文件的方法
2016/12/28 PHP
详谈PHP中public,private,protected,abstract等关键字的用法
2017/12/31 PHP
Laravel 默认邮箱登录改成用户名登录的实现方法
2019/08/12 PHP
用javascript实现无刷新更新数据的详细步骤 asp
2006/12/26 Javascript
fireworks菜单生成器mm_menu.js在 IE 7.0 显示问题的解决方法
2009/10/20 Javascript
jquery autocomplete自动完成插件的的使用方法
2010/08/07 Javascript
简单了解Backbone.js的Model模型以及View视图的源码
2016/02/14 Javascript
详解jQuery简单的表单应用
2016/12/16 Javascript
微信小程序 switch组件详解及简单实例
2017/01/10 Javascript
基于Bootstrap的标签页组件及bootstrap-tab使用说明
2017/07/25 Javascript
基于AngularJS的简单使用详解
2017/09/10 Javascript
webpack4 SCSS提取和懒加载的示例
2018/09/03 Javascript
深入浅析vue-cli@3.0 使用及配置说明
2019/05/08 Javascript
JavaScript判断数组类型的方法
2019/10/23 Javascript
Javascript数组及类数组相关原理详解
2020/10/29 Javascript
使用python 获取进程pid号的方法
2014/03/10 Python
20个常用Python运维库和模块
2018/02/12 Python
Python Tkinter模块实现时钟功能应用示例
2018/07/23 Python
Python GUI布局尺寸适配方法
2018/10/11 Python
windows7 32、64位下python爬虫框架scrapy环境的搭建方法
2018/11/29 Python
Python获取、格式化当前时间日期的方法
2020/02/10 Python
详解CSS3中nth-child与nth-of-type的区别
2017/01/05 HTML / CSS
HTML5中canvas中的beginPath()和closePath()的重要性
2018/08/24 HTML / CSS
美国著名首饰网站:BaubleBar
2016/08/29 全球购物
KLOOK客路:发现更好玩的世界,预订独一无二的旅行体验
2016/12/16 全球购物
橄榄树药房:OLIVEDA
2019/09/01 全球购物
瑞典网上购买现代和复古家具:Reforma
2019/10/21 全球购物
Wolford法国官网:奥地利奢侈内衣品牌
2020/08/11 全球购物
写一个用矩形法求定积分的通用函数
2012/11/08 面试题
行政部主管岗位职责
2013/12/28 职场文书
计算机通信专业推荐信
2014/02/22 职场文书
我们的节日元宵活动方案
2014/08/23 职场文书
观看焦裕禄观后感
2015/06/09 职场文书