はじめに
この記事は以下の続きになります。
Google Cloud Vision APIとGiNZAで抽出した郵便物の宛先を使って、宛先人に通知するアプリケーションを作りましたので紹介です。
GUIにはPySide2とQt Quickを使用しました。
PySide2について
Qt5をPythonにバインディングしたライブラリになります。
pipでインストールしておきましょう。pysideとpyside2がありますので注意してください。
pip install pyside2
インストールできたら、簡単な画面を表示してみましょう。
以下のコードだけで画面が表示されます。
import sys
from PySide2.QtWidgets import QApplication, QLabel
app = QApplication(sys.argv)
label = QLabel("Hello World!")
label.show()
app.exec_()
Qt Quickについて
Qtでは、コードに直接書くか、.uiファイルというものを使用してUIを定義していましたが、Qt QuickではQML(Qt Meta-Object Language)というものを使用してUIを定義します。
CSSのようなシンタックスでUIを宣言的に記述できることと、JavaScriptを使うことができるのが特徴です。
詳細はこちらの記事が参考になります。
QML
PySide2でQMLを使用するには、以下のようにQMLとPythonを用意します。
import QtQuick 2.15
import QtQuick.Controls 2.4
import QtQuick.Layouts 1.13
import QtQuick.Controls.Material 2.12
ApplicationWindow {
id: applicationWindow
// Theme
Material.theme: Material.Light
Material.primary: Material.Red
Material.accent: Material.Orange
Material.foreground: '#000000'
Material.background: Material.LightGrey
width: 200
height: 200
visible: true
visibility: "Windowed"
minimumHeight: 200
minimumWidth: 200
Rectangle {
Text {
x: 66
y: 93
text: "Hello World"
}
}
}
import sys
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine
from PySide2.QtCore import QUrl
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.load(QUrl("sample.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec_())
これを実行するとQMLで記述したUIが表示されました。
Qt Quickはマテリアルデザインが用意されていて、簡単におしゃれにできるところがいいなと思っています。
QMLからPythonを使う
もちろんQMLからPython側のコードを使うこともできます。
クラスとして定義しておき、QMLエンジン作成時に以下のようにContextPropertyとして登録します。
import sys
from PySide2 import QtCore
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine
from PySide2.QtCore import QUrl
class SampleViewModel(QtCore.QObject):
pythonEvent = QtCore.Signal(int)
def __init__(self):
super(SampleViewModel, self).__init__()
@QtCore.Slot(int, result='QVariant')
def on_click_button(self, arg):
self.pythonEvent.emit(f"on_click_button:{arg}")
return f"on_click_button:{arg}"
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
sample_view_model = SampleViewModel()
ctx = engine.rootContext()
ctx.setContextProperty("SampleViewModel", sample_view_model)
engine.load(QUrl("sample.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec_())
QML-Python間のイベント通知には、Qtのシグナル・スロットを使用します。シグナル・スロットについてはこちらを参照してください。
QMLからPythonに通知する場合は、上記のように@QtCore.Slot
でQMLに公開するメソッドを定義します。引数を受け取ったり、返り値を返すことも可能です。QML側からはsetContextPropertyで登録した名前を使用して、以下のように、SampleViewModel.on_click_button(1)
とします。
PythonからQMLに通知する場合は、QtCore.Signal
でシグナルを定義します。このシグナルをQML側のメソッドにバインドします。
ApplicationWindow {
id: applicationWindow
width: 200
height: 200
visible: true
visibility: "Windowed"
minimumHeight: 200
minimumWidth: 200
Rectangle {
Button {
onClicked: {
const ret = SampleViewModel.on_click_button(1);
console.log(ret);
}
}
}
Component.onCompleted: {
SampleViewModel.pythonEvent.connect(onPythonEvent);
}
function onPythonEvent(value) {
console.log(value)
}
}
上記の場合、ボタンがクリックされると、pythonのon_click_button
を呼び出して値が返ってくるのと同時に、Signal経由でQMLのonPythonEvent
も呼ばれます。
郵便物通知アプリケーション
本題のPySide2とQt Quickで作成した郵便物通知アプリですが、以下のような構成になります。
ざっくり以下のような流れです。
- ノートPCに接続したUSBカメラで郵便物を撮影する
- Vision APIでOCRを行う
- OCR結果からGiNZAで宛先人を抽出する
- 宛先人にメール通知する
OCR結果が間違ったときのために、手動で宛先人を選択できるようにもしておきます。
ソースは以下になります。
上記からかいつまんで実装を紹介します。
撮影画面
撮影画面ではカメラの映像領域と撮影ボタンを配置します。
カメラ映像を表示するには以下のカスタムViewを作成し、映像を表示できるようにしました。カメラからの画像取得にはOpenCVを使用しています。
import datetime
import os
import cv2
import qimage2ndarray
import numpy as np
from PySide2 import QtCore, QtQuick
class Singleton(type(QtCore.QObject), type):
def __init__(cls, name, bases, dict):
super().__init__(name, bases, dict)
cls.instance = None
def __call__(cls, *args, **kw):
if cls.instance is None:
cls.instance = super().__call__(*args, **kw)
return cls.instance
class CameraView(QtQuick.QQuickPaintedItem, metaclass=Singleton):
def __init__(self):
super(CameraView, self).__init__()
self.is_visible = True
self.video_size = QtCore.QSize(1280, 720)
self._canvas = np.zeros((720, 1280, 3), dtype=np.uint8)
self.capture = None
self.current_frame = None
self.init_camera()
def __del__(self):
self.capture.release()
def get_camera_capture(self):
for num in range(0, 10):
cap = cv2.VideoCapture(num, apiPreference=cv2.CAP_DSHOW)
ret, _ = cap.read()
if ret is True:
print("camera_number", num, "Find!")
return cap
else:
print("camera_number", num, "None")
exec("1")
def init_camera(self):
self.capture = self.get_camera_capture()
def paint(self, painter):
_, self.current_frame = self.capture.read()
self.current_frame = cv2.cvtColor(self.current_frame, cv2.COLOR_BGR2RGB)
image = qimage2ndarray.array2qimage(self.current_frame)
painter.drawImage(0, 0, image)
@QtCore.Slot(result='QVariant')
def save_image(self):
now = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
filename = f'data/history/{now}.jpg'
os.makedirs(os.path.dirname(filename), exist_ok=True)
cv2.imwrite(filename, cv2.cvtColor(self.current_frame, cv2.COLOR_RGB2BGR))
return filename
これをmain.pyで登録しておきます。
QtQml.qmlRegisterType(CameraView, 'CameraView', 1, 0, 'CameraView')
撮影ボタンが押されたら、所定のディレクトリに画像を保存しておき、後続の宛先抽出処理にて使用します。
宛先抽出
宛先抽出は冒頭で紹介した前回記事の通りです。
Vision APIでOCRした結果から、宛先人の名前を抽出します。
宛先人が抽出できたら、従業員名簿(CSV)と照合し、メールアドレスを取得します。お好みでチャット等への通知も可能ですので、この辺はお使いのシステムに変えていただければと思います。
手動宛先選択画面
OCRで正しく宛先が抽出できなかったときの画面になります。
QMLでのリストUIの記述方法は少し特殊に感じたので紹介しておきます。(長いので配置オプションは省略しています)
Rectangle {
ListView {
id: membersListView
ScrollBar.vertical: ScrollBar {
active: true
}
model: ListModel {
id: model
}
delegate: Item {
required property string identifier
required property string name
required property string kana
required property string address
ScrollBar.vertical: ScrollBar { visible: true }
Pane {
property bool pressed
id: card
pressed: false
Text {
id: nameText
elide: Text.ElideRight
font.pixelSize: 28
text: name
}
Text {
id: emailText
color: "#888888"
horizontalAlignment: Text.AlignRight
elide: Text.ElideRight
font.pixelSize: 24
text: address
}
MouseArea {
onClicked: {
card.pressed = true;
ResultViewModel.set_manual_result(name, address);
MainViewModel.on_selected_member();
}
}
}
}
}
Component.onCompleted: {
init();
}
function init() {
const result = MembersViewModel.get_sorted_members();
const members = result[0].map((member, idx) => ({
identifier: "" + idx,
name: member[0],
kana: member[1],
address: member[2]
}));
for (const member of members) {
membersListView.model.append(member);
}
}
}
リスト形式のUIはListView
を使用しますが、リストのセルはmodel
とdelegate
というもので記述します。
model: ListModel
でセルで表示するデータを定義し、delegate: Item
でそのデータの表示方法を定義します。
コンポーネントのインスタンスが生成されたタイミングで、CSVから従業員一覧を取得し、このmodelに追加することで従業員一覧が表示されます。
onClicked
にセルがクリックされたときの処理を書き、メール通知を行います。
まとめ
PySide2を使うことで、PythonでGUIアプリケーションを作ることができました。
マテリアルデザインにも対応していますし、公式サイトにはチュートリアルも豊富に用意されています。
機械学習の推論コードなど、お手持ちのPythonをGUI化したいときにぜひ使ってみてください。