1. 程式人生 > 程式設計 >詳解如何在pyqt中通過OpenCV實現對視窗的透視變換

詳解如何在pyqt中通過OpenCV實現對視窗的透視變換

視窗的透視變換效果

   當我們點選Win10的UWP應用中的小部件時,會發現小部件會朝著滑鼠點選位置凹陷下去,而且不同的點選位置對應著不同的凹陷情況,看起來就好像小部件在螢幕上不只有x軸和y軸,甚至還有一個z軸。要做到這一點,其實只要對視窗進行透視變換即可。下面是對Qt的視窗和按鈕進行透視變換的效果:

在這裡插入圖片描述

具體程式碼

   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,self.height - 1],self.height - 1]])

 def setDstPoints(self,leftTop: list,rightTop,leftBottom,rightBottom):
  """ 設定變換後的邊角座標 """
  self.dstPoints = numpy.float32(
   [leftTop,rightBottom])

 def getPerspectiveTransform(self,imWidth,imHeight,borderMode=cv.BORDER_CONSTANT,borderValue=[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,bytesPerComponent = array.shape
  bytesPerLine = 4 * width
  # 預設陣列維度為 m*n*4
  dst = cv.cvtColor(array,cv.COLOR_BGRA2RGBA)
  pix = QPixmap.fromImage(
   QImage(dst.data,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.height - 1])
  elif self.__pressedPos == 'left-top':
   self.__perspectiveTrans.setDstPoints(
    [6,5],[self.__perspectiveTrans.width - 1,[1,self.__perspectiveTrans.height - 2],self.__perspectiveTrans.height - 1])
  elif self.__pressedPos == 'left-bottom':
   self.__perspectiveTrans.setDstPoints(
    [2,3],[self.__perspectiveTrans.width - 3,[4,self.__perspectiveTrans.height - 4],self.__perspectiveTrans.height - 2])
  elif self.__pressedPos == 'top':
   self.__perspectiveTrans.setDstPoints(
    [3,[self.__perspectiveTrans.width - 4,self.__perspectiveTrans.height - 2])
  elif self.__pressedPos == 'center':
   self.__perspectiveTrans.setDstPoints(
    [3,self.__perspectiveTrans.height - 3])
  elif self.__pressedPos == 'bottom':
   self.__perspectiveTrans.setDstPoints(
    [2,2],self.__perspectiveTrans.height - 3])
  elif self.__pressedPos == 'right-bottom':
   self.__perspectiveTrans.setDstPoints(
    [1,[self.__perspectiveTrans.width - 5,self.__perspectiveTrans.height - 4])
  elif self.__pressedPos == 'right-top':
   self.__perspectiveTrans.setDstPoints(
    [0,[self.__perspectiveTrans.width - 7,[2,self.__perspectiveTrans.height - 1],self.__perspectiveTrans.height - 2])
  elif self.__pressedPos == 'right':
   self.__perspectiveTrans.setDstPoints(
    [1,[self.__perspectiveTrans.width - 6,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視窗透視變換內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!