详解如何在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学习数据结构实例代码
May 11 Python
举例讲解Django中数据模型访问外键值的方法
Jul 21 Python
Python sqlite3事务处理方法实例分析
Jun 19 Python
Python简单实现socket信息发送与监听功能示例
Jan 03 Python
Python实现七彩蟒蛇绘制实例代码
Jan 16 Python
Django安装配置mysql的方法步骤
Oct 15 Python
Pycharm配置远程调试的方法步骤
Dec 17 Python
python实现大文本文件分割
Jul 22 Python
浅谈django2.0 ForeignKey参数的变化
Aug 06 Python
关于keras.layers.Conv1D的kernel_size参数使用介绍
May 22 Python
python+selenium+chrome实现淘宝购物车秒杀自动结算
Jan 07 Python
Python生成九宫格图片的示例代码
Apr 14 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
用ODBC的分页显示
2006/10/09 PHP
关于页面优化和伪静态
2009/10/11 PHP
在Mac OS的PHP环境下安装配置MemCache的全过程解析
2016/02/15 PHP
PHP对象实例化单例方法
2017/01/19 PHP
jQuery使用之处理页面元素用法实例
2015/01/19 Javascript
JavaScript实现动态添加,删除行的方法实例详解
2015/07/02 Javascript
Javascript验证方法大全
2015/09/21 Javascript
JavaScript时间操作之年月日星期级联操作
2016/01/15 Javascript
JavaScript中的继承之类继承
2016/05/01 Javascript
node.js ws模块搭建websocket服务端的方法示例
2019/04/25 Javascript
微信小程序自定义navigationBar顶部导航栏适配所有机型(附完整案例)
2020/04/26 Javascript
详解微信小程序(Taro)手动埋点和自动埋点的实现
2021/03/02 Javascript
[02:28]DOTA2亚洲邀请赛 LGD战队巡礼
2015/02/03 DOTA
python缩进区别分析
2014/02/15 Python
Python中的列表知识点汇总
2015/04/14 Python
详解python单例模式与metaclass
2016/01/15 Python
Django admin美化插件suit使用示例
2017/12/12 Python
Anaconda下配置python+opencv+contribx的实例讲解
2018/08/06 Python
Python函数装饰器实现方法详解
2018/12/22 Python
详解Python odoo中嵌入html简单的分页功能
2019/05/29 Python
用Python实现最速下降法求极值的方法
2019/07/10 Python
关于python3中setup.py小概念解析
2019/08/22 Python
VSCode中自动为Python文件添加头部注释
2019/11/14 Python
调整Jupyter notebook的启动目录操作
2020/04/10 Python
如何在django中运行scrapy框架
2020/04/22 Python
在echarts中图例legend和坐标系grid实现左右布局实例
2020/05/16 Python
python对一个数向上取整的实例方法
2020/06/18 Python
Python爬虫逆向分析某云音乐加密参数的实例分析
2020/12/04 Python
你可能不熟练的十个前端HTML5经典面试题
2018/07/03 HTML / CSS
H5 meta小结(前端必看篇)
2016/08/24 HTML / CSS
HTML文本属性&amp;颜色控制属性的实现
2019/12/17 HTML / CSS
开业庆典答谢词
2014/01/18 职场文书
施工安全承诺书
2014/05/22 职场文书
2015年世界粮食日演讲稿
2015/03/20 职场文书
刑事上诉状(量刑过重)
2015/05/23 职场文书
2019年销售部季度工作计划3篇
2019/10/09 职场文书