スポンサーリンク

Python3.11+PySide6を使ってテンプレートマッチング支援ツールを作った

ちょっと前からPythonを使ってアイドルマスターシャイニーカラーズ(シャニマス)のノウハウブックをxlsxに転記するっていう取り組みにテンプレートマッチングを使って挑戦しているのですが、その派生としてPySide6を使ってテンプレートマッチングで使用する画像を作成する支援ツールを作りました。

完全なコードは今のところ非公開の予定。シャニマスのツールの方は別途書くかも。

このツールで出来ること

  • Chromeで起動中のシャニマスについて任意のタイミングのスクリーンショットを取得できる
    • サイズは1200×676固定
  • 取得したスクリーンショットについてマウスのドラッグ&ドロップの操作で自由に切り抜ける
    • 切り抜いた画像はカラーのまま or グレースケールに変換して保存できる
    • 切り抜きの範囲はスライダー/スピンボックスで微調整できる
    • スクリーンショット自体も保存できる

実際の画面とコードの断片について

  • 画像左は起動直後のウィンドウになります(赤字はUIについての説明で普段は表示されません)。
  • 画像右はキャプチャボタンを押したときの状態です。メインのグラフィックビューにselenium経由で取得したスクリーンショット画像を描画しています。
    • メインのグラフィックビューは後述する画像の切り抜きに関する実装を行う過程で格上げと呼ばれる操作が必要でした。
      • QGraphicsView → ExQGraphicsViewというクラスを作成し格上げ
      • QGraphicsScene → ExQGraphicsSceneというクラスを作成し格上げ
    • 作成した固有のウィジェットクラスはデザイナーの方で格上げしたいUIを選択して右クリック→「格上げ先を指定」から設定することでui→pyへの変換を行う際の入力補完についても効くようになる。

seleniumのWebElementから画像を取得してQGraphicsViewに書き込むまでのコードは以下のような感じ

# canvasはseleniumのWebElement
# スクリーンショットのデータをPIL(Pillow)で扱う画像形式に変換
pillow_image = Image.open(BytesIO(canvas.screenshot_as_png))

# PySide6で扱う画像形式に変換
image_qt = ImageQt.ImageQt(pillow_image)
pixmap = QtGui.QPixmap.fromImage(image_qt)

# sceneはQGraphicsScene
scene.addPixmap(pixmap)

# viewはQGraphicsView
view.setScene(scene)

 


  • メインのグラフィックビューはマウスのドラッグ&ドロップの操作で指定した範囲の画像を切り抜ける機能を追加しています。
    • クリックを開始した地点と離した地点の座標を取得したい&それぞれのタイミングで行いたい制御があるため固有のシグナルを作成して飛ばしています。
  • 真ん中の画像は切り抜いた画像をスライダーで調整しているところになります。
    • スライダーは直感的に切り抜き範囲の調整ができますが、1px単位といった細かい調整はできません。
  • 右側の画像は切り抜いた画像をスピンボックスで微調整しているところになります。
    • 先ほどのスライダーの方と違い細かい微調整が可能となってます。ただし、直感的に操作しにくいです。。

今回作成した固有の格上げ先ウィジェットは以下のような感じ

class ExQGraphicsView(QtWidgets.QGraphicsView):
    def __init__(self, *argv, **keywords):
        super(ExQGraphicsView, self).__init__(*argv, **keywords)
    # クリックを開始したときのイベントをオーバーライド
    # クリックを開始したら左クリックのときだけドラッグモードについて範囲選択に切り替える
    def mousePressEvent(self, event):
        if event.button() == QtCore.Qt.LeftButton:
            self.setDragMode(QtWidgets.QGraphicsView.RubberBandDrag)

        QtWidgets.QGraphicsView.mousePressEvent(self, event)

    # クリックを離したときのイベントをオーバーライド
    # クリックを離したらドラッグモードについてデフォルトに戻す
    def mouseReleaseEvent(self, event):
        QtWidgets.QGraphicsView.mouseReleaseEvent(self, event)
        self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
class ExQGraphicsScene(QtWidgets.QGraphicsScene):
    # 固有のシグナルを定義(クリックを開始したらgraphics_view_clickingを通知、クリックを離したらgraphics_view_releasedを通知)
    graphics_view_clicking = QtCore.Signal(bool)
    graphics_view_released = QtCore.Signal(list)

    def __init__(self, *argv, **keywords):
        super(ExQGraphicsScene, self).__init__(*argv, **keywords)

    # クリックを開始したときのイベントをオーバーライド
    def mousePressEvent(self, event):
        if event.button() == QtCore.Qt.LeftButton:
            self.graphics_view_clicking.emit(True)
        QtWidgets.QGraphicsScene.mousePressEvent(self, event)

    # クリックを離したときのイベントをオーバーライド
    # クリックを開始したときと離したときの座標についてemitで通知する
    def mouseReleaseEvent(self, event):
        QtWidgets.QGraphicsScene.mouseReleaseEvent(self, event)
        if event.button() == QtCore.Qt.LeftButton:
            ~~~長いので中略~~~
            self.graphics_view_released.emit([x1, y1, x2, y2])

 


  • 画像左はresourcesというディレクトリの中身を表示したタブになります
    • ダブルクリック時に選択したファイルを開きます
  • 画像右はtempというディレクトリの中身を表示したタブになります
    • ダブルクリック時に選択したファイルを開きます
  • この辺はPySide6でタブとツリービュー使ってみたかったから実装してみただけで汎用的なツールを目指した場合は不要となるUIかもしれない
    • resourcesディレクトリの中身のファイルはnpzファイルに変換して実際のテンプレートマッチング処理の方で使用しています
    • tempディレクトリの中身のファイルはテンプレート画像を作成するときなどのエビデンス用に取得した画像を置いとくディレクトリです
      • 今回作成したツールでは画面右上の「オリジナルを保存」ボタンをクリックしたとき、tempディレクトリに画像を保存しています

GUIライブラリ選定について

いくつか試した中でPySide6を使ったのですが、使ってみたライブラリについてメモ。

ライブラリ名メモ
tkinter
  • Pythonにビルトインで入ってるライブラリ
  • pipでのインストールが不要なので一番導入が楽と言えるかもしれない
  • UIを作るのが大変な気がして使わなかった
Flexx
  • tkinterの後に試してみたライブラリ
  • ウィンドウに描画した画像を弄るという目的は達成できそうだった
  • UIを作るのが大変な気がして使わなかった
PySide6(PySide2)
  • Flexxの後に試してみたライブラリ
  • PyQtというライセンスが異なるが使い勝手が似たライブラリが存在する
  • GUIを作るデザイナーツールが同梱している点がとても便利
  • 独自のui(中身はxml)という拡張子のファイルにGUIの定義を保存し、pyファイルへの変換を行うことでエディタ上での入力補完がしっかり効いてくれる(uiファイルをメインのpyファイルからロードする形でも動かせる)
  • QtというC++用のGUIライブラリのPythonラッパーとのこと。分からない箇所についてググるとC++のコードではあるけど参考になる情報がちょいちょい見つかったりする
  • コンストラクタ内の処理について何が正しい実装なのかがいまいちわかっていない
  • ネットで調べものするときはPyQt、PySide2などワードを適当に切り替えて調べる必要がある

他にもPython用のGUIライブラリ結構あるみたいでどれがいいのか迷いました。
他のGUIライブラリも何か機会があれば挑戦してみたい気もする。

PySide6について

PySide6はPython3.11で使用可能なGUIライブラリ。
PySideやPySide2という情報も見当たるのでPyPIより互換性について確認しました。

ライブラリ名互換性
PySidePython::2.6、Python::2.7、Python::3.2、Python::3.3、Python::3.4
PySide
Python bindings for the Qt cross-platform application and UI framework
PySide2Python::2.7、Python::3.5、Python::3.6、Python::3.7、Python::3.8、Python::3.9、Python::3.10
PySide2
Python bindings for the Qt cross-platform application and UI framework
PySide6Python::3.7、Python::3.8、Python::3.9、Python::3.10、Python::3.11
PySide6
Python bindings for the Qt cross-platform application and UI framework
  • バージョンの違いはあれど、PySide2とPySide6は同じようなコードにはなった
  • Python3.8で開発を進めてて途中Python3.11に移行したとき、venv仮想環境を作ったあとpip install -r requirements.txtを実行したらエラー出た。上に書いた互換性情報にもある通りPySide2はPython3.11には対応しておらずPySide6への移行が必要だった。
    • コードはインポート周りを変えるくらいでほぼ動いた
    • コードの一部でウィジェットのパラメータだったかを弄る定数がQtWidgetsからQtGuiに移動してたりしたかも

その他、対応予定

  • 画像を保存するときに指定の色範囲についてまとめる処理機能(ノイズ除去機能?)を追加したい
    • 切り抜いた画像の(200, 200, 200)~(255, 255, 255)について(255, 255, 255)に変換して保存するみたいなイメージ。実際にテンプレートマッチングツールを作るときのコードにも組み込む必要があるため、コードとかもラベル表示できると良いかもしれないと思った。
  • 現状メインのグラフィックビューについて 1200 x 676 を想定した作りとなっているが、最初に取得したスクリーンショットを元に動的にGUIを配置したい
    • 汎用的なツールになるので他の用途でも使えるかも
    • 試してないけどGUIのウィジェットについてストレッチって仕組みを導入することで対応はできそう?
  • アイコンとか作る
  • バイナリ化

参考リンク

【Python】GUIライブラリ13個を比較【初心者へのオススメあり】│しみゅろぐ
YMT Lab | PyQt5 QGraphicsViewでホイールでドラッグ、ドラッグで範囲選択する
PyQt5 vs. Qt for Python(PySide2) – Qiita

タイトルとURLをコピーしました