【WSL2】【PyQt6】カメラアプリを作る

はじめに

以前にこのような記事を書きました。
touch-sp.hatenablog.com
touch-sp.hatenablog.com
今回は二つの記事を合体させて簡単なカメラアプリを作ります。

完成図


ズームと首振りと背景ぼかしに対応しています。


ズームと首振りはなんちゃってです。
touch-sp.hatenablog.com
touch-sp.hatenablog.com

PythonスクリプトとYAMLファイル

Pythonスクリプト

import numpy as np
import torch
from torchvision import transforms
import cv2
from PyQt6.QtCore import pyqtSignal, pyqtSlot, QThread
from PyQt6.QtWidgets import QWidget, QApplication, QLabel, QSlider, QGridLayout
from PyQt6.QtGui import QImage, QPixmap

from constructGUI import construct

device = 'cuda' if torch.cuda.is_available() else 'cpu'

transform_fn = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize([.485, .456, .406], [.229, .224, .225]),
])

model = torch.hub.load('pytorch/vision:v0.10.0', 'deeplabv3_resnet50', pretrained=True)
model.eval().to(device)

class VideoThread(QThread):

    change_pixmap_signal = pyqtSignal(QImage)
    playing = True

    cap = cv2.VideoCapture(0)
    cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G'))
    cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)

    crop = 0
    leftside = 280
    bokashi = 0

    def run(self):
        
        while self.playing:
            ret, frame = self.cap.read()
            
            frame = frame[:, self.leftside:self.leftside+720, :]
            
            if not self.crop == 0:
                frame = cv2.resize(frame[self.crop:-self.crop, self.crop:-self.crop, :], dsize = (720, 720))
            
            if not self.bokashi == 0:
                frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                input_tensor = transform_fn(frame_rgb)
                input_batch = input_tensor.unsqueeze(0)
            
                with torch.no_grad():
                    output = model(input_batch.to(device))['out'][0]
                
                predict = output.argmax(0).to('cpu').numpy()

                mask_1 = np.where(predict == 15, 1, 0)[...,np.newaxis]
                mask_2 = np.where(predict == 15, 0, 1)[...,np.newaxis]
                if self.bokashi ==0:
                    blurred_img = cv2.blur(frame, (1, 1))
                else:
                    blurred_img = cv2.blur(frame, (self.bokashi, self.bokashi))
                frame = (frame * mask_1 + blurred_img * mask_2).astype('uint8')

            h, w, ch = frame.shape
            bytesPerLine = ch * w
            image = QImage(frame.copy(), w, h, bytesPerLine, QImage.Format.Format_BGR888)
            
            self.change_pixmap_signal.emit(image)

        self.cap.release()

    def stop(self):
        self.playing = False
        self.wait()
    
    def set_zoom_scale(self, x):    
        self.crop = x
    
    def set_move_scale(self, x):    
        self.leftside = x

    def set_bokashi_scale(self, x):    
        self.bokashi = x

class Window(QWidget):

    def __init__(self):
        super().__init__()    
        self.initUI()
        self.thread = VideoThread()
        self.thread.change_pixmap_signal.connect(self.update_image)
        self.thread.start()

    def initUI(self):
        self.setWindowTitle("OpenCV-Python sample")
        
        self.img_label1 = construct(QLabel(), 'settings.yaml', 'img_label')

        self.zoom_label = construct(QLabel('ズーム'), 'settings.yaml', 'text_label')
        self.move_label = construct(QLabel('首振り'), 'settings.yaml', 'text_label')
        self.bokashi_label = construct(QLabel('背景ぼかし'), 'settings.yaml', 'text_label')

        self.zoom_slider = construct(QSlider(), 'settings.yaml', 'zoom_slider')
        self.zoom_slider.valueChanged.connect(self.zoom_slider_change)

        self.move_slider = construct(QSlider(), 'settings.yaml', 'move_slider')
        self.move_slider.valueChanged.connect(self.move_slider_change)

        self.bokashi_slider = construct(QSlider(), 'settings.yaml', 'bokashi_slider')
        self.bokashi_slider.valueChanged.connect(self.bokashi_slider_change)

        self.grid = QGridLayout()
        
        self.grid.addWidget(self.img_label1, 0, 0, 1, 5)
        self.grid.addWidget(self.zoom_label, 1, 0)
        self.grid.addWidget(self.zoom_slider, 1, 1, 1, 4)
        self.grid.addWidget(self.move_label, 2, 0)
        self.grid.addWidget(self.move_slider, 2, 1, 1, 4)
        self.grid.addWidget(self.bokashi_label, 3, 0)
        self.grid.addWidget(self.bokashi_slider, 3, 1, 1, 4)

        self.setLayout(self.grid)

    def closeEvent(self, e):
       self.thread.stop()
       e.accept()
    
    def zoom_slider_change(self):
        self.thread.set_zoom_scale(self.zoom_slider.value() * 30)
    
    def move_slider_change(self):
        self.thread.set_move_scale(self.move_slider.value() * 70)
    
    def bokashi_slider_change(self):
        self.thread.set_bokashi_scale(self.bokashi_slider.value() * 5)

    @pyqtSlot(QImage)
    def update_image(self, image):
        self.img_label1.setPixmap(QPixmap.fromImage(image))

if __name__ == "__main__":
    app = QApplication([])
    ex =Window()
    ex.show()
    app.exec()

YAMLファイル

YAMLファイルを読み込む方法はこちらを参照して下さい。
touch-sp.hatenablog.com

img_label:
  type: QLabel
  width: 720
  height: 720

text_label:
  type: QLabel
  alignment: center
  fontFamily: times
  fontPoint: 16
  fontBold: True

button:
  type: QPushButton
  fontFamily: times
  fontPoint: 20
  fontBold: True

zoom_slider:
  type: QSlider
  orientation: h  #(h or v)
  max: 6
  min: 0
  default: 0
  tickposition: both  #(above, below, left, right or both)  

move_slider:
  type: QSlider
  orientation: h  #(h or v)
  max: 8
  min: 0
  default: 4
  tickposition: both  #(above, below, left, right or both)  

bokashi_slider:
  type: QSlider
  orientation: h  #(h or v)
  max: 3
  min: 0
  default: 0
  tickposition: both  #(above, below, left, right or both)  

環境

Ubuntu 20.04 on WSL2 (Windows 11)
WSL バージョン: 0.56.2.0
カーネル バージョン: 5.10.102.1
WSLg バージョン: 1.0.30
MSRDC バージョン: 1.2.2924
Direct3D バージョン: 1.601.0
Windows バージョン: 10.0.22000.593
Python 3.9.5
certifi==2021.10.8
charset-normalizer==2.0.12
idna==3.3
numpy==1.22.3
opencv-python==4.5.5.64
Pillow==9.0.1
pkg_resources==0.0.0
PyQt6==6.2.3
PyQt6-Qt6==6.2.4
PyQt6-sip==13.2.1
PyYAML==6.0
requests==2.27.1
torch==1.11.0+cu113
torchvision==0.12.0+cu113
typing_extensions==4.1.1
urllib3==1.26.9

カーネルは独自にビルドしています。こちらを参照して下さい。
touch-sp.hatenablog.com

つづき

つづきを書きました。
touch-sp.hatenablog.com