PySide2でQtQuick(qml)使うメモ2

前回に引き続き今回もPySide2+Qtquick(qml)のメモ

GUIだと結構定番の電卓っぽい奴の実装

f:id:gsmcustomeffects:20190818201337g:plain


Button押下でイベント発生させてTextInputからデータもらってTextInputに返すサンプルだと思ってくれればいい

Pythonコード

import sys
import os
from PySide2 import QtCore, QtWidgets, QtQml

class Connect(QtCore.QObject):
    def __init__(self, parent=None):
        super(Connect, self).__init__(parent)

    @QtCore.Slot(int,int,result = float)
    def sum(self,arg1,arg2):
        return arg1 + arg2

    @QtCore.Slot(int, int, result=float)
    def sub(self, arg1, arg2):
        return arg1 - arg2

    @QtCore.Slot(int, int, result=float)
    def mul(self, arg1, arg2):
        return arg1 * arg2

    @QtCore.Slot(int, int, result=float)
    def div(self, arg1, arg2):
        return arg1 / arg2

if __name__ == "__main__":
    os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
    app = QtWidgets.QApplication(sys.argv)
    myconnect = Connect()
    engine = QtQml.QQmlApplicationEngine()
    ctx = engine.rootContext()
    ctx.setContextProperty("Connect", myconnect)
    engine.load('mypyside2.qml')
    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec_())

qmlコード

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.3
import QtQuick.Controls.Material 2.0

Window {
    objectName: "a"
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    TextInput {
        id: textInput1
        x: 104
        y: 108
        width: 80
        height: 37
        text: qsTr("12")
        font.pixelSize: 20
    }

    TextInput {

        id: textInput2
        x: 179
        y: 108
        width: 80
        height: 37
        text: qsTr("2")
        font.pixelSize: 20
    }

    TextInput {

        id: textInput3
        x: 270
        y: 108
        width: 80
        height: 37
        text: qsTr("1")
        font.pixelSize: 20
    }

    Row {
        id: row
        x: 104
        y: 186
        width: 427
        height: 119
        spacing: 9

        Button {
            objectName:"button"
            id: button1
            width: 100
            height: 50
            text: qsTr("add")
            font.pointSize: 14
            onClicked:function(){
                textInput3.text = Connect.sum(textInput1.text,textInput2.text)
            }
        }

        Button {
            id: button2
            width: 100
            height: 50
            text: qsTr("sub")
            font.pointSize: 13
            objectName: "button"
            onClicked:function(){
                textInput3.text = Connect.sub(textInput1.text,textInput2.text)
            }
        }

        Button {
            id: button3
            width: 100
            height: 50
            text: qsTr("mul")
            font.pointSize: 12
            objectName: "button"
            onClicked:function(){
                textInput3.text = Connect.mul(textInput1.text,textInput2.text)
            }
        }

        Button {
            id: button4
            width: 100
            height: 50
            text: qsTr("div")
            font.pointSize: 12
            objectName: "button"
            onClicked:function(){
                textInput3.text = Connect.div(textInput1.text,textInput2.text)
            }
        }
    }
}

解説的なやつ

python

@QtCore.Slot(int,int,result = float)
    def sum(self,arg1,arg2):
        return arg1 + arg2

addの例で解説する。
@QtCore.Slot(int,int,result = float)で引数の型と返り値の型をqmlに対して公開する。
あとはdefで関数を実装するだけ前回との違いはSlotでreturnしているとこ。
ここで注意が必要なのが書かなくてもエラーは出ないがresult = floatは必須ということ
書かないと何も値が返ってこない

qml側

TextInput {

        id: textInput3
        x: 270
        y: 108
        width: 80
        height: 37
        text: qsTr("1")
        font.pixelSize: 20
    }

Button {
            objectName:"button"
            id: button1
            width: 100
            height: 50
            text: qsTr("add")
            font.pointSize: 14
            onClicked:function(){
                textInput3.text = Connect.sum(textInput1.text,textInput2.text)
            }
        }

こちらもaddの例で説明する。

まずqmlオブジェクトに対してはtextInput3.textのようにid.propertyでアクセスできる。(qmlではオブジェクト以下のパラメータ類のことをPropertyと呼ぶ

次にonclicked部分で押下時の動作を定義している。
textInput3.textにpython側のsumの返り値を代入するということをしているだけだ。

あくまでqmlオブジェクトへの代入はqml側でやるというとこがQtの方針
PythonC++側からも子オブジェクトにアクセスできるがQt公式は推奨していない(参照関係がごちゃごちゃになるため

この関係を守るとUI設計者と内部実装側が完全分業可能なので合理的ではある。

PySide2でQtQuick(qml)使うメモ1

自分用のメモです。

自分の動作環境は

  • PyCharm 無料版
  • Python 3.7.4 or 3.6(仮想で両方で試した)
  • Pycharm内臓のVenvでパッケージ管理

導入

PythonでQtを扱うにはPyQtとPysideの2つがある。

verごとに書くとこんな感じ

  • Qt4 : PyQt4,PyiSde
  • Qt5 : PyQt5,PySide2

という感じになる。
現行で使うならQt5系を利用するほうがいいだろうということでPyQt5,PySide2を選ぶことになる。
選定にあたりいろいろググった結果PySide2ってのがQt公式がサポートするQt5バインディング(Qt for Pythonと呼ばれてるのがこれにあたる)らしいのでこれを使うことにする。

ui -> pyをする使い方

QtDesignerというソフトを使ってグラフィカルにUIをデザインしてそのファイル(.ui)をpyside2-uic.exeを使って.py拡張子ファイルに変換してインポートする手法のこと。
この方法が一番情報も多くて使いやすいと思う。

いろんな人が情報を公開しているのでその辺を読むといいだろう
note.mu

qiita.com

UIファイルの更新のたびにPyCharm側でpyside2-uic.exeを自動で呼び出すこともできる。
qiita.com

QtQuickについて

これについては公式の説明が分かりやすい
Qt Quick 入門 第1回: Qt Quick とは - Qt Japanese Blog

ようはuiファイルではなくqmlというjson風の表記を使ってUIをデザインしていく手法であり比較的楽にかっこいいUIを作成できる。

f:id:gsmcustomeffects:20190818004444p:plain

画像は私が作成したUIであるが5分ぐらいでこのぐらいのGUIを作成できる。

PySide2導入とテスト

早速本題に入る。
まずはPyCharmで新規プロジェクトをつくる。
f:id:gsmcustomeffects:20190818010410p:plain

上部メニューのFile/settingでこの画面が開くのでproject interpreterの画面まで持ってくる。
f:id:gsmcustomeffects:20190818010741p:plain

+ボタンをクリックして必要なモジュールをインストールしてくる。
ここではPySide2を検索して持ってくる。
f:id:gsmcustomeffects:20190818011621p:plain

インストールができたら.pyファイルを作成して以下のリンクのコードを実行してみる。
https://doc.qt.io/qtforpython/tutorials/basictutorial/dialog.html

このような画面が出ていればPySide2の導入はOK
f:id:gsmcustomeffects:20190818012746p:plain

PySide2でQtQuick(qml)ファイルを扱う

次にqmlファイルを作るためにQt Creatorをダウンロードしてくる。
Download Qt: Choose commercial or open source

アカウントとか聞かれますけどスキップで何とかなります。
適当に自分の欲しいものをインストールしてください

CreatorとQt5.x系があればOKです。

Qt Creatorをインストール出来たら開いて新規プロジェクトでUI Prototypeを選ぶ
f:id:gsmcustomeffects:20190818021459p:plain

キットに関しては適当に選ぶといいです。
するとこのような画面になるのでデザインをクリックします。
f:id:gsmcustomeffects:20190818021603p:plain

UIデザイン画面が開くので次にインポートタブを開きます。
f:id:gsmcustomeffects:20190818022028p:plain

QtQuick.ControlsとQtQuick.Controls.Materialsを追加
f:id:gsmcustomeffects:20190818022236p:plain

そうするとエレメントタブにボタン類が入ってくる
f:id:gsmcustomeffects:20190818022427p:plain

ボタンをドラッグしてUIに追加する。
f:id:gsmcustomeffects:20190818022542p:plain

次にできたqmlをPycharmプロジェクトに入れてあげる
f:id:gsmcustomeffects:20190818022837p:plain

そしたらこのコードを実行

import sys
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine
from PySide2.QtCore import QUrl

if __name__ == '__main__':
    app = QGuiApplication(sys.argv)
    engine = QQmlApplicationEngine()
    engine.load(QUrl("mypyside2.qml"))

    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec_())

先ほど作ったUIが表示できていればOK
f:id:gsmcustomeffects:20190818023149p:plain

ボタンへのイベント追加

次に配置したウィジェットPythonコードをつないでいく。

python code

import sys
import os
from PySide2 import QtCore, QtWidgets, QtQml

class Connect(QtCore.QObject):
    def __init__(self, parent=None):
        super(Connect, self).__init__(parent)

    @QtCore.Slot()
    def button_clicked(self):
        print("button clicked")



if __name__ == "__main__":
    os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
    app = QtWidgets.QApplication(sys.argv)
    myconnect = Connect()
    engine = QtQml.QQmlApplicationEngine()
    ctx = engine.rootContext()
    ctx.setContextProperty("Connect", myconnect)
    engine.load('mypyside2.qml')
    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec_())

qml code

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.3
import QtQuick.Controls.Material 2.0

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    Button {
        id: button
        x: 270
        y: 220
        text: qsTr("Button")
        onClicked:Connect.button_clicked()//python側のクラスとつなぐ
    }
}


実行してボタンをクリックするとprint文が実行されるはず。
f:id:gsmcustomeffects:20190818025353p:plain

説明(Python側)

まずQtCore.QObjectを継承したクラス(Connect)を作成する。
@QtCore.Slotでqml側にメソッドの存在を通知する。これによりqml側でdef以下を呼べるようになる。
@表記はPythont的に言うとデコレータってやつらしい。

os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"

Materialテーマを有効にする記述で特に必要はない
有効にしていると以下のリンクのようなことができる。
https://doc.qt.io/qt-5/qtquickcontrols2-material.html

ctx = engine.rootContext()
ctx.setContextProperty("Connect", myconnect)

engine内部の子ウィジェットを検索可能にする記述(あってるかわかんないけど
QMLにアクセスしたりQMLからアクセスしたりする場合はこの二行が必須っぽい

説明(qml側)

説明するといってもonClickedぐらい

クリックされた時の動作を記述するメンバー
ウィジェットごとにイベントが用意されてるSliderならonValueChangedみたいな感じで書ける。
Ctrl+Spaceで補完できるので何があるかは分かると思う。

メンバーにカーソルを合わせてF1を押すとウィジェットの説明を表示できる
f:id:gsmcustomeffects:20190818031331p:plain


参考文献

rootcontextの記述はC++も同様なので参考になった。
その他qmlとの相互イベント通知の参考になる。
QMLとC++のバインディング - Qiita

今回の記事はこの問題の劣化版みたいなものなのでこっちのスレッド読むともっといいのがつくれると思う
Connect python signal to QML ui slot with PySide2 - Stack Overflow

公式のサンプルとチュートリアル
QMLの例が2個しかないけど書き方の参考にはなる。
https://doc.qt.io/qtforpython/tutorials/index.html

CMSIS DSP とコンパイル最適化オプション



色々試してたらGCCの場合最適化しないとFPU命令呼んでくれないっぽい
f:id:gsmcustomeffects:20190207231605p:plain

optimize入れるとちゃんと呼ばれてる
f:id:gsmcustomeffects:20190207231530p:plain

i.MX RT1020(CortrexM7 DP FPU)のSQRTしか試してないのであれですが入れとくに越したことはないかと。
尚、消されたくない変数にはvolatileを付けましょう。

以上

Python:スペクトログラムの描画



今回は信号の時系列変化を可視化できるスペクトログラムについてです。
使うメソッドはPython:scipy.signal.spectrogramです。

オプションパラメータがたくさんありますが

f,t,Sxx = signal.spectrogram(data, fs, nperseg=512)

のように書いてあげればいいです。

spectrogramの描画

今回は前回作成したSwept-sine信号のスペクトラムの描画をやってみたいと思う
信号の生成は前回の記事を参考にしてほしい
gsmcustomeffects.hatenablog.com

今回やることとしてはsignal.spectrogramメソッド呼ぶだけ

import matplotlib.pylab as plt
import numpy as np
import scipy.signal as signal

f1 = 10        # start frequency
f2 = 12000       # end frequency
fs = 96000      # sampling frequency
T = 10          # time duration of the sweep
fade = [48000, 480]   # samlpes to fade-in and fade-out the input signal


'''generation of the signal'''
L = T/np.log(f2/f1)                    # parametre of exp.swept-sine
t = np.arange(0,np.round(fs*T-1)/fs,1/fs)  # time axis
s = np.sin(2*np.pi*f1*L*np.exp(t/L))       # generated swept-sine signal

# fade-in the input signal
if fade[0]>0:
    s[0:fade[0]] = s[0:fade[0]] * ((-np.cos(np.arange(fade[0])/fade[0]*np.pi)+1) / 2)

# fade-out the input signal
if fade[1]>0:
    s[-fade[1]:] = s[-fade[1]:] *  ((np.cos(np.arange(fade[1])/fade[1]*np.pi)+1) / 2)
    
f,t,Sxx = signal.spectrogram(s, fs, nperseg=512)

plt.figure()
plt.pcolormesh(t,f,Sxx,cmap="GnBu")
plt.xlim([0,10])
plt.ylim([0,10000])
plt.xlabel("Time [sec]")
plt.ylabel("Freq [Hz]")
plt.colorbar()
plt.show()

matplotのcmapの設定で好みの色に設定できる


f:id:gsmcustomeffects:20190117192936p:plain
Swept-sine信号のスペクトログラム
f:id:gsmcustomeffects:20190117193200p:plain
色変更後[cmap="Spectral"]

Python:ボード線図の描画



今回はScipyのsignal.bodeを使って周波数応答を描画してみる。
使うメソッドは

  • scipy.signal.lti
  • scipy.signal.bode

である。

scipy.signal.lti

線形時不変システムを作成できるメソッド。引数の渡し方によって伝達関数だったり状態空間モデルとかいろいろ定義できる。
詳しくは公式ドキュメントが参考になる。
scipy.signal.lti — SciPy v1.8.0 Manual

G(s) = \frac{10000}{s+10000}

というのを作りたかったら

from scipy import signal

# transferfunc = 10000 / s+10000
s = signal.lti([10000], [1.0,10000])

とすれば良い

scipy.signal.bode

次にボード線図の方だ
これもドキュメント読んでもらえればわかるんだけど(scipy.signal.bode — SciPy v1.8.0 Manual

f:id:gsmcustomeffects:20190113112621p:plain
bodeメソッドの引数、返り値

基本的にLTIで作ったインスタンスを入れてあげればw,mag,phaseが帰ってくる感じ.
見た感じARMAモデルそのまま打ち込んでも表示してくれるみたい

w[radian/sec]はグラフのx軸に当たる部分で正規化周波数で帰って来るのでfにしたい場合は2piで割れば良い

実際の使い方はこうなる

w, mag, phase = signal.bode(s)

bode plot

最後に全体のコードを張っておく

import numpy as np
from scipy import signal
import matplotlib.pyplot as plt

# transferfunc = 10000 / s+10000
s = signal.lti([10000], [1.0,10000])
w, mag, phase = signal.bode(s)

f = w/(2.0*np.pi)
fig, (ax0, ax1) = plt.subplots(nrows=2, sharex=True)
ax0.semilogx(f, mag, 'b-')
ax0.set_ylabel("Magnitude[dB]")
ax1.semilogx(f, phase, 'r-')
ax1.set_ylabel("Phase[degree]")
ax1.set_xlabel("Freq[Hz]")
plt.show()

f:id:gsmcustomeffects:20190113113807p:plain
bode plot

Python:pysoundfileを使って読み込んだwavデータをFFTをする

自分用メモです。

これ一つで取りあえずはwav読み込みとFFTのサンプルかねてるので参考になれば

import soundfile as sf
import numpy as np
import matplotlib.pyplot as plt
from scipy import fftpack

fname = 'whitenoise.wav' # mono
data, samplerate = sf.read(fname)


X = fftpack.fft(data)
freqList = fftpack.fftfreq(len(data), d=1.0/ samplerate)

amplitude = [np.sqrt(c.real ** 2 + c.imag ** 2) for c in X]  # 振幅スペクトル

# 波形を描画

# 振幅スペクトルを描画
plt.plot(freqList, amplitude, marker='.', linestyle='-',label = "fft plot")
plt.axis([0, 25000, 0, 2000])
plt.xlabel("frequency [Hz]")
plt.ylabel("amplitude")

f:id:gsmcustomeffects:20190107141330p:plain
ホワイトノイズ.wav(44100kHz)