實驗目標

我們學的內容都是跑在命令行中的,並沒有界面,那么”腳本語言”Python如何搭建GUI界面呢?
其實Python支持多種圖形界面庫,如Tk(Tkinter)、wxPython、PyQt等,雖然Python自帶Tkinter,無需額外安裝包,但我更推薦使用PyQt,一是因為它完全基於Qt,跨平台,功能強大,有助於了解Qt的語法,二是Qt提供了Designer設計工具,界面設計上可以拖控件搞定,非常方便,大大節省時間。
pyqt5
安裝pyqt5:
pip install pyqt5
我推薦使用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_())

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

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


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

這樣大致界面就出來了,很簡單:

按鈕事件
我們知道GUI是通過事件驅動的,什么意思呢?比如前面我們已經設計好了界面,接下來就需要實現”打開攝像頭”到”閾值分割”這5個按鈕的功能,也就是給每個按鈕指定一個”函數”,邏輯代碼寫在這個函數里面。這種函數就稱為事件,Qt中稱為槽連接。
點擊Designer工具欄的”Edit Signals/Slots”按鈕,進入槽函數編輯界面,點擊旁邊的”Edit Widgets”可以恢復正常視圖:

然后點擊按鈕並拖動,當產生類似於電路中的接地符號時釋放鼠標,參看下面動圖:

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

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

到此,我們就完成了界面設計的所有工作,按下Ctrl+S保存當前窗口為.ui文件。.ui文件其實是按照XML格式標記的內容,可以用文本編輯器將.ui文件打開看看。
ui文件轉py代碼
因為我們是用Designer工具設計出的界面,並不是用Python代碼敲出來的,所以要想真正運行,需要使用pyuic5將ui文件轉成py文件。pyuic5.exe默認在%\Scripts\下,比如我的是在:D:\ProgramData\Anaconda3\Scripts\。
打開cmd命令行,切換到ui文件的保存目錄。Windows下有個小技巧,可以在目錄的地址欄輸入cmd,一步切換到當前目錄:
然后執行這條指令:
pyuic5 -o mainForm.py using_pyqt_create_ui.ui
如果出現pyuic5不是內部命令的錯誤,說明pyuic5的路徑沒有在環境變量里,添加下就好了。執行正常的話,就會生成mainForm.py文件,里面應該包含一個名為”Ui_MainWindow”的類。
編寫邏輯代碼
在同一工作目錄下新建一個”mainEntry.py”的文件,存放邏輯代碼。代碼中的每部分我都寫得比較獨立,沒有封裝成函數,便於理解。代碼看上去很長,但很簡單,可以每個模塊單獨看。
import sys
import cv2 as cv
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 = cv.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 = cv.imread(str(filename))
# OpenCV圖像以BGR通道存儲,顯示時需要從BGR轉到RGB
self.captured = cv.cvtColor(self.captured, cv.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 = cv.cvtColor(self.captured, cv.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 = cv.threshold(
self.cpatured, 0, 255, cv.THRESH_BINARY + cv.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
cv.cvtColor(self.frame, cv.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_())
程序運行情況

