【搞事情】利用PyQt爲目標檢測SSD300添加界面(四)

【原創文章】歡迎正常授權轉載(聯繫作者)
【反對惡意複製粘貼,如有發現必維權】
【微信公衆號原文傳送門


​這篇文章將詳細介紹利用多進程的實現—方案3(代碼獲取見文章末尾)。相比之前的稍微複雜一點,先看看demo的最終效果(視頻)。


1 需求分析

首先看一下UI界面,界面上各個控件的詳細信息如下表所示。
在這裏插入圖片描述

控件序號 控件的類別 Qobject_name 功能
1 QLabel label_imgshow 實時顯示視頻
2 QLabel label_imgshaow_res 顯示抽幀檢測效果
3 QTextEdit textEdit 顯示檢測的目標信息
4 QPushButton pushButton_open 打開視頻文件
5 QLineEdit lineEdit_cameraIndex 設置視頻流URL;支持IP攝像頭;數字爲當前設備中攝像頭索引(例:本人筆記本自帶攝像頭爲0)
6 QPushButton pushButton_start 開始檢測並播放
7 QPushButton pushButton_pause 暫停檢測及播放
8 QPushButton pushButton_end 停止檢測及播放(全部重設)

結合上面的控件分析一下需求。方案3最初的設計需求是電腦的性能有限,無法做到實時檢測並顯示,我們希望做一個折中,不檢測視頻流中的每一幀圖像,只是從中抽取部分來檢測,檢測時不打斷畫面的實時顯示,在"後臺"中儘可能多的檢測視頻流中的圖像。之前的文章中也介紹過神經網絡的預測過程是無法在線程中實現的,因此設計了一個如下圖所示的方案,將圖像檢測神經網絡放到一個子進程中負責在“後臺”檢測目標,主進程(UI進程)負責採集圖像並實時顯示在相應的控件上。
解決方案3流程
在實際的實現過程中面臨以下幾個關鍵點。

(1) 主進程(UI進程)採集到的圖像數據如何傳遞給子進程檢測?

Python多進程之間有一些簡單的通訊方式,例如:Queue,好處是它是一個進程安全的隊列,用戶不需要關注變量的管理,但是實際用這種方式來傳遞圖片,你就會發現速度慢呀!簡直崩潰,最簡單有效的還是通過“共享內存”,速度快很多(但是我還是覺得慢,我覺得主要是數據轉來轉去導致的),但需要對內存進行管理。

(2) 子進程檢測的結果如何告知主進程並將結果顯示出來?

最簡單的就和上面一樣,再使用一塊“共享內存”來將繪製好檢測結果的圖片傳遞迴主進程中,但是這顯然不是最有效率的做法,畢竟使用“共享內存”耗時也挺長的,同時圖片數據也挺大的,檢測結果其實就是幾個簡單的數,沒必要將整個圖像都傳遞回去。我的處理方式是:主進程在“抽幀”時保存一個圖像備份,子進程通過“Queue”將檢測結果(相對圖片數據小的多)傳遞迴主進程,主進程收到結果後,將結果繪製在備份圖像上並顯示出來。

(3) 共享內存的管理。

使用共享內存時一定要注意這部分內存的管理,不能主進程“寫”的同時你子進程在"讀",否則數據不就錯了嘛。嚴格點來說這裏需要一個“互斥鎖”(有興趣的同學可以試試),我實在是比較懶,不想研究,直接創建了一個狀態變量(“共享的”)來控制,主進程和子進程在讀寫“共享內存”前,通過判斷狀態變量的值來確定是否有“權利”使用該“共享內存”。


2 代碼詳解

(1) 構造函數

這部分需要注意的是:建立共享內存、檢測子進程、消息接收線程,代碼裏面有詳細的註釋,這裏不贅述。

def __init__(self, parent=None):
    super(MainWindow, self).__init__(parent)
    self.setupUi(self)# 圖像大小
    self.img_shape = (480, 720)# 初始化界面
    self.label_imgshow.setScaledContents(True)          # 圖片自適應顯示
    self.label_imgshow_res.setScaledContents(True)      # 檢測結果圖片自適應顯示
    self.img_none = np.ones((480, 720, 3), dtype=np.uint8)*255
    self.show_img(self.img_none)# SSD檢測初始化
    self.weight_path = './ssd/weights/weights_SSD300.hdf5'
    self.weight_path = os.path.normpath(os.path.abspath(self.weight_path))
    self.obj_names = ['Aeroplane', 'Bicycle', 'Bird', 'Boat', 'Bottle',
                      'Bus', 'Car', 'Cat', 'Chair', 'Cow', 'Diningtable',
                      'Dog', 'Horse', 'Motorbike', 'Person', 'Pottedplant',
                      'Sheep', 'Sofa', 'Train', 'Tvmonitor']
    # 需要顯示的目標list, 用於過濾
    self.include_class = self.obj_names
    
# -----------檢測子進程--------------
    # 子進程返回結果使用
    self.queue = Queue()
    # 多進程之間的共享圖片內存,參數‘I’表示數據類型爲 int 
    # 後一個參數爲內存的大小,這裏Python不提供多個維度數據的共享內存
    # 只有數組類型滿足使用需求,因此主進程中需先將圖像數據變爲數組的樣子,在子進程中再恢復
    self.img_share = RawArray('I', self.img_shape[0] * self.img_shape[1] * 3)
    # 標識當前進程的狀態,非0:保持檢測;0:停止檢測
    self.process_flg = RawValue('I', 1)
    # 當前圖像共享內存 img_share 的狀態,非0:主進程使用中;0:子進程使用中
    self.img_get_flg = RawValue('I', 1)
    # 創建檢測子進程
    self.detector_process = Process(target=detector_process,
                                    args=(self.img_share,
                                          self.img_shape,
                                          self.process_flg,
                                          self.img_get_flg,
                                          self.queue))
    self.detector_process.start()    # 進程開始
# -------------------------------------------------------------# -----------接收檢測結果的線程--------------
    # 主要考慮到Queue的get方法可能會阻塞,如果直接在計時器函數中調用
    # get會導致UI“假死”,卡着不動。
    # 雖然也可以設置阻塞時間,但是建議還是建立線程接收處理Queue中的結果
    # 接收檢測結果的線程
    self.recv_thread = Recv_res(parent=self, queue=self.queue)
    # 連接信號        
    # 這個信號用於通知UI響應顯示,接收線程中只負責接收轉發結果,後面代碼中有詳細介紹
    self.recv_thread.res_signal.connect(self.show_res)
    self.recv_thread.start()
# -------------------------------------------------------------# 視頻文件路徑
    self.camera_index = 0
    self.FPS = None# 初始化計時器
    self.timer = QTimer(self)               # 更新計時器
    self.timer.timeout.connect(self.timer_update)       # 超時信號連接對應的槽函數# 等待加載模型
    self.textEdit.setText('正在加載模型,請稍後......')
    self.pushButton_start.setEnabled(False)
    self.pushButton_open.setEnabled(False)
    self.pushButton_pause.setEnabled(False)
    self.lineEdit_cameraIndex.setEnabled(False)# 暫停初始化爲不暫停
    self.pause = False

(2) 檢測子進程目標函數

下面是檢測子進程的目標函數,檢測子進程開始後執行的就是這個函數,首先是初始化SSD並加載權重,之後進入幀循環檢測,子進程的消息(包括檢測結果)通過Queue傳遞返回,包括兩部分:狀態量和消息內容,設置狀態量的目的是爲了下一步針對不同的消息做相應的處理。

def detector_process(img_share, img_shape, process_flg, img_get_flg, res_queue):
    """
    SSD檢測子進程目標函數
    :param img_share: 待檢測圖像數據,共享內存
    :param img_shape: 待檢測圖像的大小 (h, w)
    :param process_flg: 子進程狀態量  0:退出檢測進程;非0:保持檢測
    :param img_get_flg: 共享圖像內存 的狀態量     0:子進程佔用共享內存   1:主進程佔有內存
    :param res_queue: 返回檢測結果的通道
    :return:
    """
    # 初始化SSD
    weight_path = './ssd/weights/weights_SSD300.hdf5'
    weight_path = os.path.normpath(os.path.abspath(weight_path))
    obj_names = ['Aeroplane', 'Bicycle', 'Bird', 'Boat', 'Bottle',
                      'Bus', 'Car', 'Cat', 'Chair', 'Cow', 'Diningtable',
                      'Dog', 'Horse', 'Motorbike', 'Person', 'Pottedplant',
                      'Sheep', 'Sofa', 'Train', 'Tvmonitor']
    include_class = obj_names
    ssd = SSD_test(weight_path=weight_path, class_nam_list=obj_names)# 通知UI 模型加載成功
    res_queue.put((3, '模型加載成功'))# 構建檢測循環
    while True:
        # print('process_flg:{}  img_get_flg:{}'.format(process_flg.value, img_get_flg.value))
        # 判斷檢測器狀態,是否退出
        if process_flg.value == 0:
            print('安全退出檢測進程!')
            res_queue.put((0, '檢測進程已安全退出!'))
            break# 判斷共享內存當前狀態是否可以安全讀取數據
        if img_get_flg.value == 0:
            # print('開始檢測!')
            try:
                img = np.array(img_share[:], dtype=np.uint8)
                img_scr = np.reshape(img, (img_shape[0], img_shape[1], 3))# SSD檢測
                preds = ssd.Predict(img_scr)
                # 結果過濾
                preds = filter(obj_names, preds, inclued_class=include_class)
​
                h, w = img_shape[:2]
                res = decode_preds(obj_names, preds, w=w, h=h)  # 列表# 管道返回檢測結果
                res_queue.put((1, res))except:
                print('圖片檢測失敗')
                res_queue.put((2, '當前圖像檢測失敗!'))
            finally:
                # 釋放圖像共享內存佔用,讓主進程寫入新的圖像
                img_get_flg.value = 1

(3) 消息接收線程類

線程創建後會先進入構造函數,啓動後執行run函數。主要的功能就是接收子進程通過Queue傳遞回來的消息,並通知UI做出相應的處理。接收到檢測子進程退出的消息後,該線程也跳出循環結束生命週期。

class Recv_res(QThread):
    """
    檢測結果接收線程
    """
    res_signal = pyqtSignal(list)
    @debug_class('Recv_res')
    def __init__(self, parent, queue:Queue):
        """
        構造函數
        :param parent: 父實例 QObj ,Qt中父實例析構相應的子線程會安全退出,不用人工處理
        :param queue: 管道
        """
        super(Recv_res, self).__init__(parent=parent)
        self.queue = queue
​
    def run(self):
        while True:
            flg, res = self.queue.get()
            print(flg, res)
            if flg == 0:    # 對應檢測子進程已安全退出
                print('接收線程已安全退出!')
                self.res_signal.emit([0, res])
                break
            else:
                self.res_signal.emit([flg, res])

(4) 計時器超時槽函數

該函數主要是按時讀取視頻流中的圖像並顯示在控件上,每次判斷檢測共享內存的狀態,如果子進程釋放則將當前幀數據寫入共享內存中,之後改變狀態變量的值(釋放對共享內存的佔有)。

def timer_update(self):
    """
    計時器槽函數
    :return:
    """
    if self.cap.isOpened():
        # 讀取圖像
        ret, self.img_scr = self.cap.read()
        # ### 視頻讀取完畢
        if not ret:
            # 計時器停止計時
            self.timer.stop()
            # 不檢測
            self.img_get_flg.value = 1
            # 對話框提示
            QMessageBox.information(self, '播放提示', '視頻已播放完畢!')
            # 釋放攝像頭
            if hasattr(self, 'cap'):
                self.cap.release()
                del self.cap
            # 釋放‘開始’按鈕
            self.pushButton_start.setEnabled(True)
            # 禁止暫停並初始化其功能
            self.pause = False
            self.pushButton_pause.setText('暫停')
            self.pushButton_pause.setEnabled(False)
            # 釋放視頻流選擇
            self.pushButton_open.setEnabled(True)
            self.lineEdit_cameraIndex.setEnabled(True)
            return# 圖像預處理
        self.img_scr = cv2.resize(self.img_scr, (self.img_shape[1], self.img_shape[0]))
        # 轉爲RGB
        self.img_scr = cv2.cvtColor(self.img_scr, cv2.COLOR_BGR2RGB)if hasattr(self, 'detector_process'):
            # ### 抽幀
            if self.img_get_flg.value == 1:
                # print('開始抽幀')
                self.img_temp = self.img_scr.copy()         # 用於顯示檢測結果
                self.img_share[:] = self.img_scr.reshape(-1).tolist()  # 抽幀保存在中間緩存
                self.img_get_flg.value = 0  # 不再抽取 直到檢測完成
                # print('結束抽幀')# 顯示圖像
        self.show_img(self.img_scr)# 響應UI
        QApplication.processEvents()
    else:
        self.textEdit.setText('數據流未打開!!!\n請檢查')
        self.resst_detector()

(5) 窗口關閉事件函數

在關閉窗口之前需要關閉子進程,否則子進程會一直在後臺運行,開一次軟件創建一個,多次重複後電腦越來越卡。打開任務管理器後後發現有好多名叫“Python”的進程,這些就是創建後卻沒關閉的子進程。因此,在窗口關閉事件函數下改變檢測進程的狀態變量值,使子進程能夠正常退出。

def closeEvent(self, a0):
    """
    關閉窗口時間函數
    :param a0:
    :return:
    """
    self.process_flg.value = 0          # 退出子進程
    self.detector_process.join()

其他函數就不寫了,非常簡單。


由於本人能力有限,歡迎批評指正。
可以加我的QQ(115229182)交流,請註明來意。

關注下方公衆號,回覆關鍵字即可獲取下載地址。
  • 本文配套源代碼下載地址:

    回覆“SSD界面3”獲取。


如果你讀後有收穫,歡迎關注我的微信公衆號
上面有更多完全免費教程,我也會不定期更新
ღ ღ ღ 打開微信掃描下方二維碼關注 ღ ღ ღ

在這裏插入圖片描述

發佈了13 篇原創文章 · 獲贊 24 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章