見出し画像

OpenCV4Python06:OpenCVのカメラキャプチャ映像をQtで表示する

【0】はじめに:Qtのイベント処理について

前回はOpenCVで読み込んだ静止画をQtで作成したウィンドウに表示した。今回は、動画を表示してみる。

まずOpenCVでカメラキャプチャした映像をQtで表示するにあたり、Qtのイベント処理について、簡単に説明する。

Qtでは「ボタンを押す、クリックする」などのGUIに対する操作を「Signal(シグナル)オブジェクト」として扱う。

GUIに対して何かしらの操作を行うと「Signal(シグナル)」が発信される。次に発信されたSignalに対して、「あらかじめ紐づくように登録(=Connect:コネクト)」された「Slot(スロット)オブジェクト」が実行される。

要はボタンなどのGUIのパーツへの操作(Signal)に対して、処理(slot)を紐づける(Connectする)仕組みになっている

■例:ボタンを押すとメッセージボックスが表示される時の挙動

画像1

【1】QtでOpenCVのカメラ映像を表示する方法

静止画と違って「カメラの映像は動画像」のため、ウィンドウ内の「描画内容の更新」が必要となる。

 今回は「スレッド(QThreadオブジェクト)」を使って、OpenCVのカメラキャプチャ部分を分離して、カメラ映像を取得したらシグナルを発信する。そして、発信されたシグナルを受け付けてウィンドウ内の画像(フレーム)を更新する。
 この処理を繰り返すことでカメラ映像をQt上で表示することをやってみる。

※イメージとしては以下のような感じ。

画像2

以下PyQt6ベースでコードを作っていく。

【2】カメラ用オブジェクト(QThreadオブジェクト)をつくる

「QThread」を継承してカメラキャプチャ用オブジェクトを作成する。

import cv2 as cv
import numpy as np

from PyQt6.QtCore import pyqtSignal, QThread

... ...


class VideoThread(QThread):
   # シグナル設定
   change_pixmap_signal = pyqtSignal(np.ndarray)

   def __init__(self):
       super().__init__()
       self._run_flag = True


   # QThreadのrunメソッドを定義
   def run(self):
       cap = cv.VideoCapture(0)
       while self._run_flag:
           ret, cv_img = cap.read() # 1フレーム取得
           # 新たなフレームを取得できたら
           # シグナル発信(cv_imgオブジェクトを発信)            
           if ret:
               self.change_pixmap_signal.emit(cv_img)

       # videoCaptureのリリース処理
       cap.release()
   
   
   # スレッドが終了するまでwaitをかける
   def stop(self):
       self._run_flag = False
       self.wait()
 

■pyqtSignalオブジェクトの作成
スレッドを使うことで並行処理になるので終了処理を色々書いているが、注目すべき部分はシグナル設定のところ

change_pixmap_signal = pyqtSignal(np.ndarray)

「OpenCVの画像データはnumpy.ndarrayオブジェクト」。これを踏まえて「numpy.ndarrayオブジェクトに対するpyqtSignal」を作成している。

この「pyqtSignal」に紐づく「numpy.ndarrayオブジェクト」として、「OpenCVの画像オブジェクトをセットしてemit(シグナル発信)」すればよい。

# QThreadのrunメソッドを定義
def run(self):
    #VideoCaptureオブジェクトでウェブカメラ認識
    cap = cv.VideoCapture(0)
    while self._run_flag:
        ret, cv_img = cap.read() # 1フレーム取得
        
        # 新たなフレームを取得できたらシグナル発信(cv_imgオブジェクトを発信)            
        if ret:
            self.change_pixmap_signal.emit(cv_img) # emit(シグナル発信)

    # videoCaptureのリリース処理
    cap.release()

【3】QWidgetオブジェクト(ウィンドウ側)の作成

カメラでキャプチャする画像(1フレーム)自体は前回同様QLabel上にQPixmapとして配置する。

from PyQt6 import QtGui
from PyQt6.QtWidgets import QWidget, QApplication, QLabel, QVBoxLayout
from PyQt6.QtGui import QPixmap

from PyQt6.QtCore import pyqtSignal, pyqtSlot, QThread

import cv2 as cv
import numpy as np

... ...

class App(QWidget):

   def __init__(self):
       super().__init__()

       # 画像を配置するQLabel
       self.image_label = QLabel(self)
       
       # vboxにQLabelをセット
       vbox = QVBoxLayout()
       vbox.addWidget(self.image_label)
       self.setLayout(vbox)  # vboxをレイアウトとして配置


       # ビデオキャプチャ用のスレッドオブジェクトを生成
       self.thread = VideoThread()
       # ビデオスレッド内のchange_pixmap_signalオブジェクトに対するslot
       self.thread.change_pixmap_signal.connect(self.update_image)

       self.thread.start() # スレッドを起動


   # 終了時にスレッドがシングルになるようにする
   def closeEvent(self,event):
       self.thread.stop()
       event.accept()


   @pyqtSlot(np.ndarray)
   def update_image(self, cv_img):
       # img = cv.cvtColor(cv_img, cv.COLOR_BGR2RGB)
       # QT側でチャネル順BGRを指定
       qimg = QtGui.QImage(cv_img.tobytes(),cv_img.shape[1],cv_img.shape[0],cv_img.strides[0],QtGui.QImage.Format.Format_BGR888)
       qpix = QPixmap.fromImage(qimg)
       self.image_label.setPixmap(qpix)

QWidgetへの「レイアウト配置」や「closeEventメソッド(終了処理)」などの部分もざっと記述しているが、connectによるslot紐づけ部分に注目してほしい。

■connectによるslot紐づけ
「カメラ用オブジェクト(QThreadオブジェクト)」で作成した「シグナル:pyqtSignal(np.ndarray)」に対してconnectを使ってslot(関数)を登録する

class App(QWidget):

   def __init__(self):

       ... ...

       # ビデオキャプチャ用のスレッドオブジェクトを生成
       self.thread = VideoThread()
       # ビデオスレッド内のchange_pixmap_signalオブジェクトに対するslot登録
       self.thread.change_pixmap_signal.connect(self.update_image)

       self.thread.start() # スレッドを起動
   
   
   ... ...
   
   # connectで登録されるslot
   @pyqtSlot(np.ndarray)
   def update_image(self, cv_img):
       ... ...

slot側ではデコレータ@pyqtSlot(np.ndarray)をつけている。これにより「明示的にこの関数がslotであること、引数としてnumpy.ndarrayオブジェクトを受け付けること」をpythonに伝えている。

あとはslotの内部で「画像データ(numpy.ndarray)→QImage→QPixmap」と変換してから、QLabel上に配置すればよい。

@pyqtSlot(np.ndarray)
def update_image(self, cv_img):
    #img = cv.cvtColor(cv_img, cv.COLOR_BGR2RGB)
    # QT側でチャネル順BGRに対応。
    qimg = QtGui.QImage(cv_img.tobytes(),cv_img.shape[1],cv_img.shape[0],cv_img.strides[0],QtGui.QImage.Format.Format_BGR888)
    qpix = QPixmap.fromImage(qimg)
    self.image_label.setPixmap(qpix)

QImageに変換する際に、OpenCVの画像チャネル順はBGRなので気をつけることになる。事前に「cv.cvtColor()」で変換してもよいが、今回はQt側に「QtGui.QImage.Format.Format_BGR888」というフラグが存在するので、これを使ってOpenCVのカラーチャネル順:BGRに対応することにする。

【4】全体コード:PyQt6版

from PyQt6 import QtGui
from PyQt6.QtWidgets import QWidget, QApplication, QLabel, QVBoxLayout
from PyQt6.QtGui import QPixmap

from PyQt6.QtCore import pyqtSignal, pyqtSlot, QThread

import cv2 as cv
import numpy as np

class VideoThread(QThread):
   # シグナル設定
   change_pixmap_signal = pyqtSignal(np.ndarray)

   def __init__(self):
       super().__init__()
       self._run_flag = True

   # QThreadのrunメソッドを定義
   def run(self):
       cap = cv.VideoCapture(0)
       while self._run_flag:
           ret, cv_img = cap.read() # 1フレーム取得
           # 新たなフレームを取得できたら
           # シグナル発信(cv_imgオブジェクトを発信)            
           if ret:
               self.change_pixmap_signal.emit(cv_img)

       # videoCaptureのリリース処理
       cap.release()
   
   # スレッドが終了するまでwaitをかける
   def stop(self):
       self._run_flag = False
       self.wait()


class App(QWidget):

   def __init__(self):
       super().__init__()

       self.image_label = QLabel(self)
       
       # vboxにQLabelをセット
       vbox = QVBoxLayout()
       vbox.addWidget(self.image_label)

       # vboxをレイアウトとして配置
       self.setLayout(vbox)

       # ビデオキャプチャ用のスレッドオブジェクトを生成
       self.thread = VideoThread()
       # ビデオスレッド内のchange_pixmap_signalオブジェクトのシグナルに対するslot
       self.thread.change_pixmap_signal.connect(self.update_image)

       self.thread.start() # スレッドを起動


   # 終了時にスレッドがシングルになるようにする
   def closeEvent(self,event):
       self.thread.stop()
       event.accept()


   @pyqtSlot(np.ndarray)
   def update_image(self, cv_img):
       #img = cv.cvtColor(cv_img, cv.COLOR_BGR2RGB)
       # QT側でチャネル順BGRを指定
       qimg = QtGui.QImage(cv_img.tobytes(),cv_img.shape[1],cv_img.shape[0],cv_img.strides[0],QtGui.QImage.Format.Format_BGR888)
       qpix = QPixmap.fromImage(qimg)
       self.image_label.setPixmap(qpix)

if __name__ == "__main__":
   app = QApplication([])
   window = App()
   window.show()
   app.exec()

【おまけ】全体コード:PySide6版

PySide6では名前が「pyqtSignal→Signal」、「pyqtSlot→Slot」にかわることに注意する。

from PySide6 import QtGui
from PySide6.QtWidgets import QWidget, QApplication, QLabel, QVBoxLayout
from PySide6.QtGui import QPixmap

from PySide6.QtCore import Signal, Slot, QThread

import sys
import cv2 as cv
import numpy as np

class VideoThread(QThread):
   # シグナル設定
   change_pixmap_signal = Signal(np.ndarray)

   def __init__(self):
       super().__init__()
       self._run_flag = True

   # QThreadのrunメソッドを定義
   def run(self):
       cap = cv.VideoCapture(0)
       while self._run_flag:
           ret, cv_img = cap.read() # 1フレーム取得
           # 新たなフレームを取得できたら
           # シグナル発信(cv_imgオブジェクトを発信)            
           if ret:
               self.change_pixmap_signal.emit(cv_img)

       # videoCaptureのリリース処理
       cap.release()
   
   # スレッドが終了するまでwaitをかける
   def stop(self):
       self._run_flag = False
       self.wait()


class App(QWidget):

   def __init__(self):
       super().__init__()

       self.image_label = QLabel(self)
       
       # vboxにQLabelをセット
       vbox = QVBoxLayout()
       vbox.addWidget(self.image_label)

       # vboxをレイアウトとして配置
       self.setLayout(vbox)

       # ビデオキャプチャ用のスレッドオブジェクトを生成
       self.thread = VideoThread()
       # ビデオスレッド内のchange_pixmap_signalオブジェクトのシグナルに対するslot
       self.thread.change_pixmap_signal.connect(self.update_image)

       self.thread.start() # スレッドを起動


   # 終了時にスレッドがシングルになるようにする
   def closeEvent(self,event):
       self.thread.stop()
       event.accept()


   @Slot(np.ndarray)
   def update_image(self, cv_img):
       #img = cv.cvtColor(cv_img, cv.COLOR_BGR2RGB)
       # QT側でチャネル順BGRを指定
       qimg = QtGui.QImage(cv_img.data,cv_img.shape[1],cv_img.shape[0],cv_img.strides[0],QtGui.QImage.Format.Format_BGR888)
       qpix = QPixmap.fromImage(qimg)
       self.image_label.setPixmap(qpix)

if __name__ == "__main__":
   app = QApplication([])
   window = App()
   window.show()
   sys.exit(app.exec_())

もっと応援したいなと思っていただけた場合、よろしければサポートをおねがいします。いただいたサポートは活動費に使わせていただきます。