当前位置: 首页 > 编程笔记 >

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

齐俊达
2023-03-14
本文向大家介绍详解如何在pyqt中通过OpenCV实现对窗口的透视变换,包括了详解如何在pyqt中通过OpenCV实现对窗口的透视变换的使用技巧和注意事项,需要的朋友参考一下

窗口的透视变换效果

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

具体代码

   1.下面先定义一个类,它的作用是将传入的 QPixmap 转换为numpy 数组,然后用 opencv 的 warpPerspective 对数组进行透视html" target="_blank">变换,最后再将 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差不多长这个样子,可以很明显看出白色的文字围绕着一圈黑色的描边。

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

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

 类似资料:
  • 问题内容: 我正在尝试实现文件夹查看器以查看特定路径的结构。并且此文件夹视图应该看起来像PyQT中的树小部件,我知道文件对话框可以提供帮助,但我需要将其放在主窗口中。 我试图使用QTreeWidget来实现这一点,并且使用了递归函数在文件夹内循环,但这太慢了。因为它需要围绕大量文件夹进行递归。这是正确的方法吗? 问题答案: 使用模型和视图。

  • 问题内容: 我在Xcode中创建了一个空项目。 当我按下运行按钮时,它显示一个窗口。如何更改其外观,例如窗口的透明度等。 我已经搜索了很多,但是每个人都使用变量来像这里一样进行更改,但是如何创建实例? 我是Mac应用程序开发的新手。有人可以写一个详细的答案吗? 谢谢! 问题答案: 您可以按如下方式获取应用程序窗口的实例: 从NSApp获取窗口(共享应用程序实例的全局常量): 或覆盖或访问视图的wi

  • 我有一个问题,正在寻找一个很好的解决方案。 我有这些db表: 游戏对象 游戏对象属性 游戏\对象\游戏\对象\属性(轴) 游戏对象。php: GameObjectAttribute.php: 现在,我试图获得所有GameObjectType的GameObject,并用透视列值过滤结果。 我在这里使用了一个连接,但是有没有办法用雄辩的关系来做呢? 这将返回给我所有“军队”类型的游戏对象: “game

  • 我的目标是通过Scratch创建一个扫描仪应用程序。为了做到这一点,我正在使用Java android中的CameraX库。假设我已经检测到了页面的边角。图像捕获后,我如何对图像执行变换矩阵乘法?(除了尝试在Java中这样做外,我遵循了这里提到的算法)我需要一个矩阵中的图像,这样我就可以用我的3x3变换矩阵乘以每个像素位置,比如[I,j,1]来得到新的位置。然后,我会有另一个矩阵,在那里我记录像素

  • 本文向大家介绍在Java Swing中创建渐变半透明窗口,包括了在Java Swing中创建渐变半透明窗口的使用技巧和注意事项,需要的朋友参考一下 使用JDK 7,我们可以非常容易地使用swing创建基于渐变的半透明窗口。以下是制作基于渐变的半透明窗口所需的步骤。  首先使JFrame的背景透明。  创建渐变颜料,然后填充面板。  将面板分配为框架的内容窗格。 示例 请参阅下面带有基于梯度的半透明

  • 我对流中的事件进行了键控,我希望通过键来累积,直到超时(例如,5分钟),然后处理累积到该点的事件(忽略该键之后的所有内容,但首先是第一件事)。 我是一个新的Flink,但从概念上来说,我认为我需要一些类似下面代码的东西。 如何在Flink中完成键控窗口超时?

  • 问题内容: 我正在尝试将用相机捕获的实时视频帧发送到服务器并进行处理。我使用usig opencv进行图像处理,使用python进行语言处理。这是我的代码 这段代码给了我文件结尾错误,这是合乎逻辑的,因为数据总是不断到达服务器,而pickle不知道何时完成。我在互联网上进行的搜索使我使用了泡菜,但到目前为止还不能用。 注意 :我设置为80,因为那是我说时得到的数字。 问题答案: 几样东西: 使用而

  • 问题内容: 在angularJs中可以观看全局变量吗? 我从旧版代码中设置了一个window.test变量,然后我需要观察该变量以了解它是否存在。 我尝试过类似的东西 问题答案: 有些。您可以包含Angular 服务(如文档所述,比直接访问更安全): 然后使用watch函数作为您的第一个参数,如下所示: 演示小提琴 但是请注意,只有在触发Angular进行$ digest操作后,才会执行。一种可行