天天看點

OpenCV教程拓展挑戰:PyQt編寫GUI界面

PyQt編寫GUI界面

推薦人群:

初級入門、機器學習小白、技術愛好者

OpenCV教程拓展挑戰:PyQt編寫GUI界面

1、簡介

前面我們學習的OpenCV内容都是運作在指令行中的,沒有界面,是以本次的拓展挑戰内容便是:

了解Python編寫GUI界面的方法,使用PyQt5編寫如下的圖像處理應用程式,實作打開攝像頭、捕獲圖檔、讀取本地圖檔、灰階化和Otsu自動門檻值分割的功能。

最新版本:PyQt 5.x

官網:

https://www.riverbankcomputing.com/software/pyqt/

大家感興趣的話,除去官網,下面是一些可參考的資源:

Python Wiki: PyQt

http://t.cn/R3XvLpk

PyQt/Tutorials

http://t.cn/EiE97YM

PyQt5 tutorial:英文原版

http://zetcode.com/gui/pyqt5/

PyQt4 tutorial:中文版

http://t.cn/EiE9fv8

、英文原版

http://t.cn/RzPrRin

Qt5 Documentation

https://doc.qt.io/qt-5/

中文參考書:PyQt5快速開發與實戰

http://t.cn/RlwsiHL

基于Qt的Python IDE Eric

http://t.cn/hGLo4a

2、安裝

pip install pyqt5

下載下傳速度慢的話,可以到PyPI上下載下傳離線版安裝。另外我推薦使用Qt Designer來設計界面,如果你裝的是Anaconda的話,就已經自帶了designer.exe,例如我的是在:D:\ProgramData\Anaconda3\Library\bin\,如果是普通的Python環境,則需要自行安裝:

pip install pyqt5-tools

安裝完成後,designer.exe應該在Python安裝目錄下:xxx\Lib\site-packages\pyqt5_tools\。

可以使用下面的代碼生成一個簡單的界面:

import sys
from PyQt5.QtWidgets import QApplication, QWidget

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = QWidget()
    window.setWindowTitle('Hello World!')
    window.show()
    sys.exit(app.exec_())           
OpenCV教程拓展挑戰:PyQt編寫GUI界面

3、界面設計

根據我們的挑戰内容,解決思路是使用Qt Designer來設計界面,使用Python完成代碼邏輯。打開designer.exe,會彈出建立新窗體的視窗,我們直接點選“create”:

OpenCV教程拓展挑戰:PyQt編寫GUI界面

界面的左側是Qt的常用控件”Widget Box”,右側有一個控件屬性視窗”Property Editor”,其餘暫時用不到。本例中我們隻用到了”Push Button”控件和”Label”控件:最上面的三個Label控件用于顯示圖檔,可以在屬性視窗調整它的大小,我們統一調整到150×150:

OpenCV教程拓展挑戰:PyQt編寫GUI界面
OpenCV教程拓展挑戰:PyQt編寫GUI界面

另外,控件上顯示的文字”text”屬性和控件的名字”objectName”屬性需要修改,便于顯示和代碼調用。可以按照下面我推薦的命名:

OpenCV教程拓展挑戰:PyQt編寫GUI界面

4、按鈕事件

如果你之前有過一些GUI開發經驗,比如MFC,WinForm等,就知道GUI是通過事件驅動的,什麼意思呢?比如前面我們已經設計好了界面,接下來就需要實作”打開攝像頭”到”門檻值分割”這5個按鈕的功能,也就是給每個按鈕指定一個”函數”,邏輯代碼寫在這個函數裡面。這種函數就稱為事件,Qt中稱為槽連接配接。

點選Designer工具欄的”Edit Signals/Slots”按鈕,進入槽函數編輯界面,點選旁邊的”Edit Widgets”可以恢複正常視圖:

OpenCV教程拓展挑戰:PyQt編寫GUI界面

然後點選按鈕并拖動,當産生類似于電路中的接地符号時釋放滑鼠,參看下面動圖:

OpenCV教程拓展挑戰:PyQt編寫GUI界面

在彈出的配置視窗中,可以看到左側是按鈕的常用事件,我們選擇點選事件”clicked()”,然後添加一個名為”btnOpenCamera_Clicked()”的槽函數:

OpenCV教程拓展挑戰:PyQt編寫GUI界面

重複上面的步驟,給五個按鈕添加五個槽函數,最終結果如下:

OpenCV教程拓展挑戰:PyQt編寫GUI界面

到此,我們就完成了界面設計的所有工作,按下Ctrl+S儲存目前視窗為.ui檔案。.ui檔案其實是按照XML格式标記的内容,可以用文本編輯器将.ui檔案打開看看。

5、ui檔案轉py代碼

因為我們是用Designer工具設計出的界面,并不是用Python代碼敲出來的,是以要想真正運作,需要使用pyuic5将ui檔案轉成py檔案。pyuic5.exe預設在%\Scripts\下,比如我的是在:D:\ProgramData\Anaconda3\Scripts\。

打開cmd指令行,切換到ui檔案的儲存目錄。Windows下有個小技巧,可以在目錄的位址欄輸入cmd,一步切換到目前目錄:

OpenCV教程拓展挑戰:PyQt編寫GUI界面

然後執行這條指令:

pyuic5 -o mainForm.py using_pyqt_create_ui.ui

如果出現pyuic5不是内部指令的錯誤,說明pyuic5的路徑沒有在環境變量裡,添加下就好了。執行正常的話,就會生成mainForm.py檔案,裡面應該包含一個名為”Ui_MainWindow”的類。

6、小結

mainForm.py檔案是根據ui檔案生成的,也就是說重新生成會覆寫掉。是以為了使界面與邏輯分離,我們需要建立一個邏輯檔案。

在同一工作目錄下建立一個”mainEntry.py”的檔案,存放邏輯代碼。代碼中的每部分我都寫得比較獨立,沒有封裝成函數,便于了解。代碼看上去很長,但很簡單,可以每個子產品單獨看,有幾個需要注意的地方我做了注釋:

import sys
import cv2
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import QFileDialog, QMainWindow
from mainForm import Ui_MainWindow

class PyQtMainEntry(QMainWindow, Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        
        self.camera = cv2.VideoCapture(0)       
        self.is_camera_opened = False  # 攝像頭有沒有打開标記
        
        # 定時器:30ms捕獲一幀
        self._timer = QtCore.QTimer(self)
        self._timer.timeout.connect(self._queryFrame)
        self._timer.setInterval(30)
        
        
    def btnOpenCamera_Clicked(self):
        '''
        打開和關閉攝像頭
        '''
        self.is_camera_opened = ~self.is_camera_opened
        if self.is_camera_opened:
            self.btnOpenCamera.setText("關閉攝像頭")
            self._timer.start()
        else:
            self.btnOpenCamera.setText("打開攝像頭")
            self._timer.stop()
            
            
    def btnCapture_Clicked(self):
        '''
        捕獲圖檔
        '''
        # 攝像頭未打開,不執行任何操作
        if not self.is_camera_opened:
            return
        
        self.captured = self.frame
        # 後面這幾行代碼幾乎都一樣,可以嘗試封裝成一個函數
        rows, cols, channels = self.captured.shape
        bytesPerLine = channels * cols
        # Qt顯示圖檔時,需要先轉換成QImgage類型
        QImg = QImage(self.captured.data, cols, rows, bytesPerLine, QImage.Format_RGB888)
        self.labelCapture.setPixmap(QPixmap.fromImage(QImg).scaled(
            self.labelCapture.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))
        
    def btnReadImage_Clicked(self):
        '''
        從本地讀取圖檔
        '''
        # 打開檔案選取對話框
        filename,  _ = QFileDialog.getOpenFileName(self, '打開圖檔')
        if filename:
            self.captured = cv2.imread(str(filename))
            # OpenCV圖像以BGR通道存儲,顯示時需要從BGR轉到RGB
            self.captured = cv2.cvtColor(self.captured, cv2.COLOR_BGR2RGB)
            
            rows, cols, channels = self.captured.shape
            bytesPerLine = channels * cols
            QImg = QImage(self.captured.data, cols, rows, bytesPerLine, QImage.Format_RGB888)
            self.labelCapture.setPixmap(QPixmap.fromImage(QImg).scaled(
                self.labelCapture.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))
            
    def btnGray_Clicked(self):
        '''
        灰階化
        '''
        # 如果沒有捕獲圖檔,則不執行操作
        if not hasattr(self, "captured"):
            return
        self.cpatured = cv2.cvtColor(self.captured, cv2.COLOR_RGB2GRAY)
        rows, columns = self.cpatured.shape
        bytesPerLine = columns
        # 灰階圖是單通道,是以需要用Format_Indexed8
        QImg = QImage(self.cpatured.data, columns, rows, bytesPerLine, QImage.Format_Indexed8)
        self.labelResult.setPixmap(QPixmap.fromImage(QImg).scaled(
            self.labelResult.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))
        
    def btnThreshold_Clicked(self):
        '''
        Otsu自動門檻值分割
        '''
        if not hasattr(self, "captured"):
            return
        
        
        _, self.cpatured = cv2.threshold(
            self.cpatured, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
        rows, columns = self.cpatured.shape
        bytesPerLine = columns
        # 門檻值分割圖也是單通道,也需要用Format_Indexed8
        QImg = QImage(self.cpatured.data, columns, rows, bytesPerLine, QImage.Format_Indexed8)
        self.labelResult.setPixmap(QPixmap.fromImage(QImg).scaled(
            self.labelResult.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))
        
        
    @QtCore.pyqtSlot()    
    def _queryFrame(self):
        '''
        循環捕獲圖檔
        '''
        ret, self.frame = self.camera.read()
        img_rows, img_cols, channels = self.frame.shape
        bytesPerLine = channels * img_cols
        
        
        cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB, self.frame)
        QImg = QImage(self.frame.data, img_cols, img_rows, bytesPerLine, QImage.Format_RGB888)
        self.labelCamera.setPixmap(QPixmap.fromImage(QImg).scaled(
            self.labelCamera.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))
        
        
if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    window = PyQtMainEntry()
    window.show()
    sys.exit(app.exec_())
           
OpenCV教程拓展挑戰:PyQt編寫GUI界面

本文隻是抛磚引玉,介紹了PyQt5的簡單使用,想要深入學習,可以參考本文開頭的參考資料哦

引用

本節源碼

http://t.cn/EiEI3XL
OpenCV教程拓展挑戰:PyQt編寫GUI界面

繼續閱讀