您好,登錄后才能下訂單哦!
窗口的透視變換效果
當我們點擊Win10的UWP應用中的小部件時,會發現小部件會朝著鼠標點擊位置凹陷下去,而且不同的點擊位置對應著不同的凹陷情況,看起來就好像小部件在屏幕上不只有x軸和y軸,甚至還有一個z軸。要做到這一點,其實只要對窗口進行透視變換即可。下面是對Qt的窗口和按鈕進行透視變換的效果:
具體代碼
1.下面先定義一個類,它的作用是將傳入的 QPixmap
轉換為numpy
數組,然后用 opencv
的 warpPerspective
對數組進行透視變換,最后再將 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
和 paintEven
t 即可,如果有對按鈕使用qss,記得在paintEvent中加上super().paintEvent(e)
,這樣樣式表才會起作用。總之框架已經給出,具體操作取決于你。如果你喜歡這篇博客的話,記得點個贊哦(o゚▽゚)o 。順便做個下期預告:在gif中可以看到界面切換時帶了彈入彈出的動畫,在下一篇博客中我會對如何實現QStackedWidget的界面切換動畫進行介紹,敬請期待~~
到此這篇關于詳解如何在pyqt中通過OpenCV實現對窗口的透視變換的文章就介紹到這了,更多相關pyqt OpenCV窗口透視變換內容請搜索億速云以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持億速云!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。