詳解如何在pyqt中通過OpenCV實(shí)現(xiàn)對(duì)窗口的透視變換
窗口的透視變換效果
當(dāng)我們點(diǎn)擊Win10的UWP應(yīng)用中的小部件時(shí),會(huì)發(fā)現(xiàn)小部件會(huì)朝著鼠標(biāo)點(diǎn)擊位置凹陷下去,而且不同的點(diǎn)擊位置對(duì)應(yīng)著不同的凹陷情況,看起來就好像小部件在屏幕上不只有x軸和y軸,甚至還有一個(gè)z軸。要做到這一點(diǎn),其實(shí)只要對(duì)窗口進(jìn)行透視變換即可。下面是對(duì)Qt的窗口和按鈕進(jìn)行透視變換的效果:

具體代碼
1.下面先定義一個(gè)類,它的作用是將傳入的 QPixmap 轉(zhuǎn)換為numpy 數(shù)組,然后用 opencv 的 warpPerspective 對(duì)數(shù)組進(jìn)行透視變換,最后再將 numpy 數(shù)組轉(zhuǎn)為 QPixmap 并返回;
# coding:utf-8
import cv2 as cv
import numpy
from PyQt5.QtGui import QImage, QPixmap
class PixmapPerspectiveTransform:
""" 透視變換基類 """
def __init__(self, pixmap=None):
""" 實(shí)例化透視變換對(duì)象\n
Parameter
---------
src : numpy數(shù)組 """
self.pixmap = pixmap
def setPixmap(self, pixmap: QPixmap):
""" 設(shè)置被變換的QPixmap """
self.pixmap = QPixmap
self.src=self.transQPixmapToNdarray(pixmap)
self.height, self.width = self.src.shape[:2]
# 變換前后的邊角坐標(biāo)
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):
""" 設(shè)置變換后的邊角坐標(biāo) """
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需要加上一個(gè)透明通道
if self.src.shape[-1] == 3:
self.src = cv.cvtColor(self.src, cv.COLOR_BGR2BGRA)
# 透視變換矩陣
perspectiveMatrix = cv.getPerspectiveTransform(
self.srcPoints, self.dstPoints)
# 執(zhí)行變換
self.dst = cv.warpPerspective(self.src, perspectiveMatrix, (
imWidth, imHeight), borderMode=borderMode, borderValue=borderValue)
# 將ndarray轉(zhuǎn)換為QPixmap
return self.transNdarrayToQPixmap(self.dst)
def transQPixmapToNdarray(self, pixmap: QPixmap):
""" 將QPixmap轉(zhuǎn)換為numpy數(shù)組 """
width, height = pixmap.width(), pixmap.height()
channels_count = 4
image = pixmap.toImage() # type:QImage
s = image.bits().asstring(height * width * channels_count)
# 得到BGRA格式數(shù)組
array = numpy.fromstring(s, numpy.uint8).reshape(
(height, width, channels_count))
return array
def transNdarrayToQPixmap(self, array):
""" 將numpy數(shù)組轉(zhuǎn)換為QPixmap """
height, width, bytesPerComponent = array.shape
bytesPerLine = 4 * width
# 默認(rèn)數(shù)組維度為 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,當(dāng)我們的鼠標(biāo)單擊這個(gè)類實(shí)例化出來的窗口時(shí),窗口會(huì)先通過 self.grab() 被渲染為QPixmap,然后調(diào)用 PixmapPerspectiveTransform 中的方法對(duì)QPixmap進(jìn)行透視變換,拿到透視變換的結(jié)果后只需隱藏窗口內(nèi)的小部件并通過 PaintEvent 將結(jié)果繪制到窗口上即可。雖然思路很通順,但是實(shí)際操作起來會(huì)發(fā)現(xiàn)對(duì)于透明背景的窗口進(jìn)行透視變換時(shí),與透明部分交界的部分會(huì)被插值上半透明的像素。對(duì)于本來就屬于深色的像素來說這沒什么,但是如果像素是淺色的就會(huì)帶來很大的視覺干擾,你會(huì)發(fā)現(xiàn)這些淺色部分旁邊被描上了一圈黑邊,我們先將這個(gè)圖像記為img_1。img_1差不多長(zhǎng)這個(gè)樣子,可以很明顯看出白色的文字圍繞著一圈黑色的描邊。

為了解決這個(gè)煩人的問題,我又對(duì)桌面上的窗口進(jìn)行截屏,再次透視變換。注意是桌面上看到的窗口,這時(shí)的窗口肯定是會(huì)有背景的,這時(shí)的透視變換就不會(huì)存在上述問題,記這個(gè)透視變換完的圖像為img_2。但實(shí)際上我們本來是不想要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):
""" 可進(jìn)行透視變換的窗口 """
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:
""" 返回鼠標(biāo)點(diǎn)擊位置 """
return self.__pressedPos
def mousePressEvent(self, e):
""" 鼠標(biāo)點(diǎn)擊窗口時(shí)進(jìn)行透視變換 """
super().mousePressEvent(e)
self.grabMouse()
pixmap = self.grab()
self.__perspectiveTrans.setPixmap(pixmap)
# 根據(jù)鼠標(biāo)點(diǎn)擊位置的不同設(shè)置背景封面的透視變換
self.__setDstPointsByPressedPos(getPressedPos(self,e))
# 獲取透視變換后的QPixmap
self.__pressedPix = self.__getTransformPixmap()
# 對(duì)桌面上的窗口進(jìn)行截圖
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):
""" 鼠標(biāo)松開時(shí)顯示小部件 """
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):
""" 通過鼠標(biāo)點(diǎn)擊位置設(shè)置透視變換的四個(gè)邊角坐標(biāo) """
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:
""" 對(duì)窗口口所在的桌面區(qū)域進(jìn)行截圖 """
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):
""" 對(duì)窗口截圖再次進(jìn)行透視變換并將兩張圖融合,消除可能存在的黑邊 """
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中調(diào)用了一個(gè)全局函數(shù) getPressedPos(widget,e) ,如果將窗口分為九宮格,它就是用來獲取判斷鼠標(biāo)的點(diǎn)擊位置落在九宮格的哪個(gè)格子的,因?yàn)槲以谄渌胤接杏玫剿詻]將其設(shè)置為PerspectiveWidget的方法成員。下面是這個(gè)函數(shù)的代碼:
# coding:utf-8 from PyQt5.QtGui import QMouseEvent def getPressedPos(widget, e: QMouseEvent) -> str: """ 檢測(cè)鼠標(biāo)并返回按下的方位 """ 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 # 獲取點(diǎn)擊位置 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
使用方法
很簡(jiǎn)單,只要將代碼中的QWidget替換為PerspectiveWidget就可以享受透視變換帶來的無盡樂趣。要想向gif中那樣對(duì)按鈕也進(jìn)行透視變換,只要按代碼中所做的那樣重寫mousePressEvent、mouseReleaseEvent 和 paintEvent 即可,如果有對(duì)按鈕使用qss,記得在paintEvent中加上super().paintEvent(e),這樣樣式表才會(huì)起作用??傊蚣芤呀?jīng)給出,具體操作取決于你。如果你喜歡這篇博客的話,記得點(diǎn)個(gè)贊哦(o゚▽゚)o 。順便做個(gè)下期預(yù)告:在gif中可以看到界面切換時(shí)帶了彈入彈出的動(dòng)畫,在下一篇博客中我會(huì)對(duì)如何實(shí)現(xiàn)QStackedWidget的界面切換動(dòng)畫進(jìn)行介紹,敬請(qǐng)期待~~
到此這篇關(guān)于詳解如何在pyqt中通過OpenCV實(shí)現(xiàn)對(duì)窗口的透視變換的文章就介紹到這了,更多相關(guān)pyqt OpenCV窗口透視變換內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python 使用input同時(shí)輸入多個(gè)數(shù)的操作
這篇文章主要介紹了Python 使用input同時(shí)輸入多個(gè)數(shù)的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-03-03
Python tkinter之Bind(綁定事件)的使用示例
這篇文章主要介紹了Python tkinter之Bind(綁定事件)的使用詳解,幫助大家更好的理解和學(xué)習(xí)python的gui開發(fā),感興趣的朋友可以了解下2021-02-02
淺談python圖片處理Image和skimage的區(qū)別
這篇文章主要介紹了淺談python圖片處理Image和skimage的區(qū)別,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08
教你使用Pandas直接核算Excel中的快遞費(fèi)用
文中仔細(xì)說明了怎么根據(jù)賬單核算運(yùn)費(fèi).首先要確定運(yùn)費(fèi)規(guī)則,然后根據(jù)運(yùn)費(fèi)規(guī)則編寫代碼,生成核算列(快遞費(fèi) = 省份*重量),最后輸入賬單,進(jìn)行核算.將腳本件生成EXE文件,就可以使用啦,需要的朋友可以參考下2021-05-05
Python多進(jìn)程multiprocessing用法實(shí)例分析
這篇文章主要介紹了Python多進(jìn)程multiprocessing用法,結(jié)合實(shí)例形式分析了Python多線程的概念以及進(jìn)程的創(chuàng)建、守護(hù)進(jìn)程、終止、退出進(jìn)程、進(jìn)程間消息傳遞等相關(guān)操作技巧,需要的朋友可以參考下2017-08-08
python實(shí)現(xiàn)單鏈表中刪除倒數(shù)第K個(gè)節(jié)點(diǎn)的方法
這篇文章主要為大家詳細(xì)介紹了python實(shí)現(xiàn)單鏈表中刪除倒數(shù)第K個(gè)節(jié)點(diǎn)的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-09-09
python3.6 實(shí)現(xiàn)AES加密的示例(pyCryptodome)
本篇文章主要介紹了python3.6 實(shí)現(xiàn)AES加密的示例(pyCryptodome),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-01-01

