LoginSignup
3
1

More than 1 year has passed since last update.

PySide2とQt Quickで、OCRで郵便物を抽出し、宛先人に通知するアプリケーションを作る

Posted at

はじめに

この記事は以下の続きになります。

Google Cloud Vision APIとGiNZAで抽出した郵便物の宛先を使って、宛先人に通知するアプリケーションを作りましたので紹介です。
GUIにはPySide2とQt Quickを使用しました。

PySide2について

Qt5をPythonにバインディングしたライブラリになります。
pipでインストールしておきましょう。pysideとpyside2がありますので注意してください。

pip install pyside2

インストールできたら、簡単な画面を表示してみましょう。
以下のコードだけで画面が表示されます。

sample.py
import sys
from PySide2.QtWidgets import QApplication, QLabel

app = QApplication(sys.argv)
label = QLabel("Hello World!")
label.show()
app.exec_()

PySide2でHello World

Qt Quickについて

Qtでは、コードに直接書くか、.uiファイルというものを使用してUIを定義していましたが、Qt QuickではQML(Qt Meta-Object Language)というものを使用してUIを定義します。
CSSのようなシンタックスでUIを宣言的に記述できることと、JavaScriptを使うことができるのが特徴です。

詳細はこちらの記事が参考になります。

QML

PySide2でQMLを使用するには、以下のようにQMLとPythonを用意します。

sample.qml
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"
        }
    }
}
sample.py
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はマテリアルデザインが用意されていて、簡単におしゃれにできるところがいいなと思っています。

Qt QuickでHelloWorld

QMLからPythonを使う

もちろんQMLからPython側のコードを使うこともできます。
クラスとして定義しておき、QMLエンジン作成時に以下のようにContextPropertyとして登録します。

sample.py
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側のメソッドにバインドします。

sample.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で作成した郵便物通知アプリですが、以下のような構成になります。

構成.png

ざっくり以下のような流れです。

  1. ノートPCに接続したUSBカメラで郵便物を撮影する
  2. Vision APIでOCRを行う
  3. OCR結果からGiNZAで宛先人を抽出する
  4. 宛先人にメール通知する

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を使用しますが、リストのセルはmodeldelegateというもので記述します。
model: ListModelでセルで表示するデータを定義し、delegate: Itemでそのデータの表示方法を定義します。
コンポーネントのインスタンスが生成されたタイミングで、CSVから従業員一覧を取得し、このmodelに追加することで従業員一覧が表示されます。

onClickedにセルがクリックされたときの処理を書き、メール通知を行います。

まとめ

PySide2を使うことで、PythonでGUIアプリケーションを作ることができました。
マテリアルデザインにも対応していますし、公式サイトにはチュートリアルも豊富に用意されています。
機械学習の推論コードなど、お手持ちのPythonをGUI化したいときにぜひ使ってみてください。


3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1