粗大メモ置き場

個人用,たまーに来訪者を意識する雑記メモ

matplotlibのインタラクティブなプロットを作る覚書(スクロールでズーム、ドラッグで移動)

背景

オレオレGUIを作る際にインタラクティブなPlotがしたいという動機です。

インタラクティブなPlotについては下記が結構ボリュームがあって良いと思います。

qiita.com

サンプルコード①:スクロールで拡大縮小、ドラッグで移動

下記StackOverflowの議論からコードをもらってPython3用に改変しました。

stackoverflow.com

動作は下記の2つで、それぞれ関数が割り当てられています。

  • スクロールで拡大縮小
  • クリック&ドラッグで移動
from matplotlib.pyplot import figure, show
import numpy

class ZoomPan:
    def __init__(self):
        self.press = None
        self.cur_xlim = None
        self.cur_ylim = None
        self.x0 = None
        self.y0 = None
        self.x1 = None
        self.y1 = None
        self.xpress = None
        self.ypress = None


    def zoom_factory(self, ax, base_scale = 2.):
        def zoom(event):
            cur_xlim = ax.get_xlim()
            cur_ylim = ax.get_ylim()

            xdata = event.xdata # get event x location
            ydata = event.ydata # get event y location

            if event.button == 'down':
                # deal with zoom in
                scale_factor = 1 / base_scale
            elif event.button == 'up':
                # deal with zoom out
                scale_factor = base_scale
            else:
                # deal with something that should never happen
                scale_factor = 1
                print(event.button)

            new_width = (cur_xlim[1] - cur_xlim[0]) * scale_factor
            new_height = (cur_ylim[1] - cur_ylim[0]) * scale_factor

            relx = (cur_xlim[1] - xdata)/(cur_xlim[1] - cur_xlim[0])
            rely = (cur_ylim[1] - ydata)/(cur_ylim[1] - cur_ylim[0])

            ax.set_xlim([xdata - new_width * (1-relx), xdata + new_width * (relx)])
            ax.set_ylim([ydata - new_height * (1-rely), ydata + new_height * (rely)])
            ax.figure.canvas.draw()

        fig = ax.get_figure() # get the figure of interest
        fig.canvas.mpl_connect('scroll_event', zoom)

        return zoom

    def pan_factory(self, ax):
        def onPress(event):
            if event.inaxes != ax: return
            self.cur_xlim = ax.get_xlim()
            self.cur_ylim = ax.get_ylim()
            self.press = self.x0, self.y0, event.xdata, event.ydata
            self.x0, self.y0, self.xpress, self.ypress = self.press

        def onRelease(event):
            self.press = None
            ax.figure.canvas.draw()

        def onMotion(event):
            if self.press is None: return
            if event.inaxes != ax: return
            dx = event.xdata - self.xpress
            dy = event.ydata - self.ypress
            self.cur_xlim -= dx
            self.cur_ylim -= dy
            ax.set_xlim(self.cur_xlim)
            ax.set_ylim(self.cur_ylim)

            ax.figure.canvas.draw()

        fig = ax.get_figure() # get the figure of interest

        # attach the call back
        fig.canvas.mpl_connect('button_press_event',onPress)
        fig.canvas.mpl_connect('button_release_event',onRelease)
        fig.canvas.mpl_connect('motion_notify_event',onMotion)

        #return the function
        return onMotion


fig = figure()

ax = fig.add_subplot(111, xlim=(0,1), ylim=(0,1), autoscale_on=False)

ax.set_title('Click to zoom')
x,y,s,c = numpy.random.rand(4,200)
s *= 200

ax.scatter(x,y,s,c)
scale = 1.1
zp = ZoomPan()
figZoom = zp.zoom_factory(ax, base_scale = scale)
figPan = zp.pan_factory(ax)
show()

出力は下記のようになります。

f:id:ossyaritoori:20210726210806p:plain
サンプルとして用いる散布図グラフ。ホイールで拡大縮小、ドラッグで移動ができます。

ズーム動作

関数の最初にzoom用の関数を定義して、下記のコードでコールバック関数として渡しているのがわかります。

fig.canvas.mpl_connect('scroll_event', zoom)

そして、受け手の関数では変数eventから得られるスクロールの上下に関する情報をもとに図の拡大縮小を行っています。

ドラッグ動作

一方ドラッグ動作では、クリックされたときと離されたとき、マウスをドラッグしたときの動作にそれぞれ関数を割り当てて、 コールバックを呼んでいます。

# attach the call back
        fig.canvas.mpl_connect('button_press_event',onPress)
        fig.canvas.mpl_connect('button_release_event',onRelease)
        fig.canvas.mpl_connect('motion_notify_event',onMotion)

その他の動作

つまり、どんなイベントがあるかだけ把握すれば適切なコールバック関数を渡してあげることでいろいろな動作ができるということです。

では、実際にどんなイベントがあるかについては下記公式ページを参照してください。

matplotlib.org

次のサンプルではkey_press_eventを使います。

GUI動作は組み合わせで行うことが多いのでサンプルコードのようにクラスを定義してその中でどのボタンがホールドされているかなどの変数を保持しておくと捗ると思います。

サンプルコード②:スクロールで左右ズーム、Ctrl押しながらのスクロールで上下ズーム

先程のコードを時系列Plot用に改良しました。

主な動作としては下記の通りになります。

  • スクロール:X軸のみズーム
  • Ctrl+スクロール:Y軸のみズーム
  • ドラッグ:並行移動
  • 「r」キー:描画範囲リセット
from matplotlib.pyplot import figure, show
import numpy
import matplotlib
matplotlib.use('TKAgg')

class ZoomPan:
    def __init__(self,ax):
        self.press = None
        self.cur_xlim = None
        self.cur_ylim = None
        self.x0 = None
        self.y0 = None
        self.x1 = None
        self.y1 = None
        self.xpress = None
        self.ypress = None

        self.ctrl_press = False

        self.ax = ax
        self.orig_xlim = ax.get_xlim()
        self.orig_ylim = ax.get_ylim()


        self.zoom_factory(ax,base_scale=1.1)
        self.ctrl_key(ax)
        self.pan_factory(ax)

    def zoom_factory(self, ax, base_scale = 2.):

        def zoomX(event,scale_factor):
            cur_xlim = ax.get_xlim()
            xdata = event.xdata # get event x location
            new_width = (cur_xlim[1] - cur_xlim[0]) * scale_factor
            relx = (cur_xlim[1] - xdata)/(cur_xlim[1] - cur_xlim[0])

            ax.set_xlim([xdata - new_width * (1-relx), xdata + new_width * (relx)])
            ax.figure.canvas.draw()

        def zoomY(event,scale_factor):
            cur_ylim = ax.get_ylim()
            ydata = event.ydata # get event y location
            new_height = (cur_ylim[1] - cur_ylim[0]) * scale_factor
            rely = (cur_ylim[1] - ydata)/(cur_ylim[1] - cur_ylim[0])

            ax.set_ylim([ydata - new_height * (1-rely), ydata + new_height * (rely)])
            ax.figure.canvas.draw()

        def zoom(event):
            if event.button == 'down':
                # deal with zoom in
                scale_factor = 1 / base_scale
            elif event.button == 'up':
                # deal with zoom out
                scale_factor = base_scale
            else:
                # deal with something that should never happen
                scale_factor = 1
                print(event.button)

            ####### Switch zoom X or Y #########
            if self.ctrl_press:
                zoomY(event,scale_factor)
            else:
                zoomX(event,scale_factor)

        fig = ax.get_figure() # get the figure of interest
        fig.canvas.mpl_connect('scroll_event', zoom)

        return zoom

    def ctrl_key(self,ax):
        def onPress(event):
            #print(event.key)
            if event.inaxes != ax: return
            if event.key == "control":
                self.ctrl_press = True
            elif event.key == "r": # reset zoom
                ax.set_xlim(self.orig_xlim)
                ax.set_ylim(self.orig_ylim)
                ax.figure.canvas.draw()

        def onRelease(event):
            #print(event.key)
            if event.inaxes != ax: return
            if event.key == "control":
                self.ctrl_press = False
        
        fig = ax.get_figure() # get the figure of interest

        # attach the call back
        fig.canvas.mpl_connect('key_press_event',onPress)
        fig.canvas.mpl_connect('key_release_event',onRelease)

    def pan_factory(self, ax):
        def onPress(event):
            if event.inaxes != ax: return
            self.cur_xlim = ax.get_xlim()
            self.cur_ylim = ax.get_ylim()
            self.press = self.x0, self.y0, event.xdata, event.ydata
            self.x0, self.y0, self.xpress, self.ypress = self.press

        def onRelease(event):
            self.press = None
            ax.figure.canvas.draw()

        def onMotion(event):
            if self.press is None: return
            if event.inaxes != ax: return
            dx = event.xdata - self.xpress
            dy = event.ydata - self.ypress
            self.cur_xlim -= dx
            self.cur_ylim -= dy
            ax.set_xlim(self.cur_xlim)
            ax.set_ylim(self.cur_ylim)

            ax.figure.canvas.draw()

        fig = ax.get_figure() # get the figure of interest

        # attach the call back
        fig.canvas.mpl_connect('button_press_event',onPress)
        fig.canvas.mpl_connect('button_release_event',onRelease)
        fig.canvas.mpl_connect('motion_notify_event',onMotion)

        #return the function
        return onMotion


fig = figure()

ax = fig.add_subplot(111, xlim=(0,1), ylim=(0,1), autoscale_on=False)

ax.set_title('Click to zoom')
x,y,s,c = numpy.random.rand(4,200)
s *= 200

ax.scatter(x,y,s,c)
scale = 1.1
zp = ZoomPan(ax)
show()

key_press_event を使ったフラグ管理と注意点

コントロールキーを押しているか管理するためにkey_press_eventを使っています。

キーが押されたらフラグを立てて、キーを話したらフラグを下ろすという2つのコールバックを定義しています。 その他に「r」を押した際に描画範囲をリセットする機能も同時に書いています。

ハマったバグ
Macで開発しているときにハマったのがOS Xのバックエンドだとcontrolキーが押されたかチェックできない問題です。

python - Close pyplot figure using the keyboard on Mac OS X - Stack Overflow

上記の質疑のようにバックエンドをTKAggに変えてことなきを得ました。ちょっと画質が荒くなる感じがしてあまり好きではありませんが。。。

import matplotlib
matplotlib.use('TKAgg')

これ以外にもキーの同時押しのときはバックエンドによって出てくる値が変わるなどこのあたりは結構気をつけることがありそうです。

スマホ外付けの望遠レンズで月は撮れるか(OpenCVで実倍率を検証)

概要

スマホの外付けレンズというのが果たして実用に堪えるのか前から気になっていたので夏休みに買ってみて検証してみました。

先に所感をまとめると以下のとおりです。

  • クリップは固定に不安
  • 望遠とマクロはそこそこ楽しい
  • 真面目にやるなら三脚は必須
  • 中華のズーム倍率は信用してはいけない

また,一応スマホでも月は撮れます。

f:id:ossyaritoori:20210723185405p:plain
Pixel4,3.7倍と望遠レンズ(約10倍)で撮影した上弦の月

スマホと外付けレンズ

検証に用いたのは下記のレンズキットです。

自称望遠22倍,マクロ,広角,魚眼レンズを備えているということでした。

また,スマホとしてPixel4とHuaweiのNova3を用いました。

レンズを使ってみての感想

100円レンズだと曇ったり周辺がぼやけたりするそうですが,全般的にレンズをつけて著しく画質が劣化するということはなかったです。

ただ,広角も魚眼レンズもほしい場面があまりなく後述のクリップの手間を考えても持ち運んで気軽にスマホにつけるというような運用ではないと思いました。

レンズ おすすめ度 感想
広角 広角で撮りたいシーンが自撮りくらいしか思いつかない。
魚眼 ちゃんと魚眼になるけど何に使うのか不明。
マクロ スマホでマクロ撮影できるのは意外と楽しい。位置合わせも楽。
望遠 ちゃんとセッティングすれば結構遊びがいがある。が,準備がだるい。

f:id:ossyaritoori:20210723190338p:plain
昔買ったルースをマクロで撮影。生活感のない撮影対象が他になかった。

クリップの使用感

商品写真の通り,クリップにレンズをはめてスマホのカメラと位置合わせをすることで撮影ができます。 これは正直慣れが必要で,

  • 位置合わせが難しい(特に望遠レンズ)
  • 望遠レンズ着用時は自重でズレが起きやすい

という問題があるため,特に望遠レンズで「正しくはめて」「正しく目標物に向ける」というプロセスが非常に撮影において時間がかかります。

もう少しきちんとした三脚があるとこのあたりの安定度が全然違うので,望遠レンズを使う方は今後のカメラ購入も見越して用意しておいたほうがいいと思います。

冒頭の画像はセッティングに5分程度かかっており,初めての場合はもっと掛かると思ったほうが良さそうです。

望遠倍率が表記と違う問題

商品説明では22倍ズームとあるのですが正直そんなにズームしている感じはしませんでした。 口コミでも倍率が低めとあったのでOpenCVを使って確認してみます。

Gistソースコード

特徴点のマッチングを用いてレンズの有無の画像の間の倍率を計算すると大体9.72倍と出ました。 22倍とは…

f:id:ossyaritoori:20210723183532p:plain
望遠レンズなし(左)と有り(右)。SIFT特徴点のマッチング結果を線で示しています。

この手の商品あるあるとして,1つのコア技術の製造元に対して外装をちょっと変更していろんな業者が売るという構造になっている事が多いので他の似た製品でも望遠レンズの倍率はせいぜい10倍弱になっているというのはあると思います。

製品仕様くらいはきちんと記述してほしいです。

参考:一眼レフで撮るとこうなる

いろいろあってSONYのα6400というカメラを手に入れました。

で、これで月を撮るとこんな感じになりました。

f:id:ossyaritoori:20210723010837p:plain
α6400の付属レンズで撮った月(3.8倍、JPEG画質)。一眼レフはやはり違った。

一番感動したのは手ブレ補正か倍率低さのおかげか三脚なしの手持ちでちゃんと撮影できたことです。 スマホの設定だと基本的に露光が長くなるので三脚なしはありえないのでこのあたりの撮影の手間は断然こちらの方が楽でした。

ちなみにコンデジで撮ると下記のような感じになります。

ossyaritoori.hatenablog.com

まとめ

ということで今回の所感です。

  • 安い外付けレンズでもスマホで月を撮れるが設備投資(特に三脚)が必要。
  • マクロと望遠意外は特に用がなさそう。
  • 望遠倍率だいたいサバ読んでる。
  • 一眼レフはいいぞ。

Python,PyQtで簡単にGUIを作る ② Splitterを使った配置とcsvのPlotter

目標

PythonでちゃっちゃとGUIを作ることを目標にしています。

前回に引き続き下記の2つを満たすようなものを作成していきます。

  • 複数ある要素を選択
  • 選択した要素をグラフに随時プロット

ossyaritoori.hatenablog.com

f:id:ossyaritoori:20210718014237p:plain
今回つくるやつです。左でcsvのHeaderを選択して右の領域に該当する値がPlotされます。

今回やること

ざっくりと下記のことをやっていきます。

  • Splitterを用いた配置
  • pandasと組み合わせて数値をPlot

Splitterを用いた配置

SetGeometryなどを用いずにSplitterという仕切りを使ってWidgetを配置する方法を用います。

下記のチュートリアルを見てみましょう。

www.finddevguides.com

使い方はVboxたちと似通っていて定義後はaddWidgetでWidgetを追加できます。

#from PyQt5.QtWidgets import *
      splitter1 = QSplitter(Qt.Horizontal) #横向きにStackする
      textedit = QTextEdit()
      textedit2 = QTextEdit()
      splitter1.addWidget(textedit2)
      splitter1.addWidget(textedit)
# 中略
# 最後に予め作成したQVboxlayoutに登録
      vbox.addWidget(splitter1)

また,SplitterにSplitterを追加できるため下記のようにプログラムを書けばいろいろ追加できます。

f:id:ossyaritoori:20210718103855p:plain
上がSplitter1で分けた領域,そしてSplitter2ではSplitter1と下のWidgetを分けている

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *

"""
Qt splitter のチュートリアル
https://www.finddevguides.com/Pyqt-qsplitter-widget

Example:Qwidgetから継承
SplitterはaddWidgetで部品を追加可能。Splitter自身も追加可能
基本的に一つの領域に一つの部品を配置できる。
"""

class Example(QWidget):

   def __init__(self):
      super(Example, self).__init__()

      self.initUI()

   def initUI(self):

      hbox = QHBoxLayout(self)
      
      # フレームを作る
      topleft = QFrame()
      topleft.setFrameShape(QFrame.StyledPanel)
      bottom = QFrame()
      bottom.setFrameShape(QFrame.StyledPanel)

      # 水平に分割
      splitter1 = QSplitter(Qt.Horizontal)
      textedit = QTextEdit()
      textedit2 = QTextEdit()
      textedit3 = QTextEdit()
      splitter1.addWidget(textedit2)
      splitter1.addWidget(textedit)
      splitter1.setSizes([100,200])

      splitter2 = QSplitter(Qt.Vertical)
      splitter2.addWidget(splitter1)
      splitter2.addWidget(textedit3)

      # Widgetを追加
      hbox.addWidget(splitter2)

      # レイアウト適用
      self.setLayout(hbox)
      QApplication.setStyle(QStyleFactory.create('Cleanlooks'))

      # Windowサイズを規定
      self.setGeometry(300, 300, 300, 200)
      self.setWindowTitle('QSplitter demo')
      self.show()

def main():
   app = QApplication(sys.argv)
   ex = Example()
   sys.exit(app.exec_())

if __name__ == '__main__':
   main()

csvファイルを読んでPlotする

前回の記事と合わせると冒頭で示したようなcsvファイルから数値列を読んでPlotするGUIが作れます。

流れは下記のとおりです。

  • csvを読み込んでPandasのDataFrameに落とす
  • 数値列のみを抽出してColumn名をリスト化
  • GUIを作成,選択したColumnリストをもとにPlot

データとして使うCSVはAtushi Sakai大先生のリポジトリから拝借してきました。

github.com

pandasで非数値列を弾く方法

CSVを読んでPlotするのは良いですがその際,非数値が混じってエラー終了するのは避けたいです。

下記のようにselect_dtypesを使えば非数値列を排除できて安全です。

        # dfの非数値の列のみ選択
        self.df = df.select_dtypes(include='number')

コード

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import pandas as pd 
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas


"""
Qt splitter のチュートリアル
https://www.finddevguides.com/Pyqt-qsplitter-widget

Example:Qwidgetから継承
SplitterはaddWidgetで部品を追加可能。Splitter自身も追加可能
基本的に一つの領域に一つの部品を配置できる。
"""

class Example(QWidget):
    def __init__(self,filename=None):
        super(Example, self).__init__()
        
        self.loadcsv(filename)
        self.initUI()
    
    def loadcsv(self,filename):
        df = pd.read_csv(filename)
        # 非数値の列のみ選択
        self.df = df.select_dtypes(include='number')
        print(self.df)
        self.columnlist = self.df.columns # list

    def initUI(self):
        hbox = QHBoxLayout(self)
    
        # list/figure作成
        self.create_listwidget()
        self.create_figure()

        # 左側作成
        splitter_l = QSplitter(Qt.Vertical)
        splitter_l.addWidget(self.add_label("Plotする要素を選択"))
        splitter_l.addWidget(self.listWidget)
        # 右側作成
        splitter = QSplitter(Qt.Horizontal)
        splitter.addWidget(splitter_l)
        splitter.addWidget(self.FigureCanvas)
        #splitter.setSizes([100,200])

        # Widgetを追加
        hbox.addWidget(splitter)

        # レイアウト適用
        self.setLayout(hbox)
        QApplication.setStyle(QStyleFactory.create('Cleanlooks'))

        # Windowサイズを規定
        #self.setGeometry(300, 300, 300, 200)
        self.setWindowTitle('CSV plotter demo')
        self.show()

    def create_listwidget(self):
        # リスト作成
        self.listWidget = QListWidget()
        self.listWidget.setSelectionMode( #複数選択可能にする
            QAbstractItemView.MultiSelection
        )
        for col in self.columnlist:
            item = QListWidgetItem(col)
            self.listWidget.addItem(item)
        self.listWidget.itemClicked.connect(self.printItemText)
    
    def create_figure(self):
        # Figureを作成
        self.Figure = plt.figure()
        self.FigureCanvas = FigureCanvas(self.Figure)  # FigureをFigureCanvasに追加
        self.axis = self.Figure.add_subplot(1,1,1) # axを持っておく



    def add_label(self,text):
        # ラベルを作る
        label=QLabel(self)
        label.setText(text)
        label.adjustSize()
        return label


    def printItemText(self):
        # 選択したアイテムを表示
        items = self.listWidget.selectedItems()
        self.selected_columns = []
        for i in range(len(items)):
            self.selected_columns.append(self.listWidget.selectedItems()[i].text())

        self.plot_selected_columns()

    # Plotをアップデート
    def plot_selected_columns(self):
        self.axis.cla() # リセットを掛ける必要がある。
        df = self.df[self.selected_columns]
        df.plot(ax=self.axis)
        plt.grid(); 
        self.FigureCanvas.draw()


def main():
   app = QApplication(sys.argv)
   ex = Example("samplecsv.csv")
   sys.exit(app.exec_())

if __name__ == '__main__':
   main()

余談:Checkbox形式への変更

Listwidgetは結構見た目があれなので自分的にはCheckboxのほうが好きだったりします。 下記のように設定すればCheckboxを導入できます。

    # Checkbox Widgetの作成
    def add_checkbox(self,name):
      chxlist = QCheckBox(name, self)
      chxlist.stateChanged.connect(self.checklist_clicked)
      return chxlist
    
    #  クリックされたCheckboxをみてリストに追加 or 削除
    def checklist_clicked(self, state):
        checked_name = str(self.sender().text())
        if state == Qt.Checked: # if checked
            # add to the list
            self.clicked_list.append(checked_name)
        else: # if unchecked
            # remove from list
            self.clicked_list = [x for x in self.clicked_list if not x == checked_name]

余談2: Checkboxをまとめて導入

Checkboxをまとめて導入したいときは,上記のように一個一個Widgetを作るのではなく,VboxLayoutを用意してその中に入れていくのが良いです。

ちょっと長めの記述になります。

    def create_checkboxes(self):
        vbox = QVBoxLayout()
        for col in self.columnlist:
            vbox.addWidget(self.add_checkbox(col))
        # 箱を用意する
        frame = QFrame(self) # Frame定義
        frame.setFrameShape(QFrame.StyledPanel) # 形状決定。パネル。
        vbox.addStretch(1) # 行間を定義
        frame.setLayout(vbox)
        return frame

補足:Checkboxに変更した版のGUI

以上をまとめると下記のような感じになります。

f:id:ossyaritoori:20210718115306p:plain
記述量は増えますが自分はこっちのほうがスッキリしていて好みです。

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import pandas as pd 
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas


"""
Qt splitter のチュートリアル
https://www.finddevguides.com/Pyqt-qsplitter-widget

Example:Qwidgetから継承
SplitterはaddWidgetで部品を追加可能。Splitter自身も追加可能
基本的に一つの領域に一つの部品を配置できる。
"""

class Example(QWidget):
    def __init__(self,filename=None):
        super(Example, self).__init__()
        
        self.loadcsv(filename)
        self.initUI()
    
    def loadcsv(self,filename):
        df = pd.read_csv(filename)
        # 非数値の列のみ選択
        self.df = df.select_dtypes(include='number')
        print(self.df)
        self.columnlist = self.df.columns # list

    def initUI(self):
        hbox = QHBoxLayout(self)
    
        # list/figure作成
        #self.create_listwidget()
        self.create_figure()

        # 左側作成
        splitter_l = QSplitter(Qt.Vertical)
        splitter_l.addWidget(self.add_label("Plotする要素を選択"))
        #splitter_l.addWidget(self.listWidget)
        splitter_l.addWidget(self.create_checkboxes())
        # 右側作成
        splitter = QSplitter(Qt.Horizontal)
        splitter.addWidget(splitter_l)
        splitter.addWidget(self.FigureCanvas)
        #splitter.setSizes([100,200])

        # Widgetを追加
        hbox.addWidget(splitter)

        # レイアウト適用
        self.setLayout(hbox)
        QApplication.setStyle(QStyleFactory.create('Cleanlooks'))

        # Windowサイズを規定
        #self.setGeometry(300, 300, 300, 200)
        self.setWindowTitle('CSV plotter demo')
        self.show()

    def create_listwidget(self):
        # リスト作成
        self.listWidget = QListWidget()
        self.listWidget.setSelectionMode( #複数選択可能にする
            QAbstractItemView.MultiSelection
        )
        for col in self.columnlist:
            item = QListWidgetItem(col)
            self.listWidget.addItem(item)
        self.listWidget.itemClicked.connect(self.printItemText)
    
    def create_figure(self):
        # Figureを作成
        self.Figure = plt.figure()
        self.FigureCanvas = FigureCanvas(self.Figure)  # FigureをFigureCanvasに追加
        self.axis = self.Figure.add_subplot(1,1,1) # axを持っておく

    def create_checkboxes(self):
        vbox = QVBoxLayout()
        for col in self.columnlist:
            vbox.addWidget(self.add_checkbox(col))
        # 箱を用意してはる
        frame = QFrame(self) # Frame定義
        frame.setFrameShape(QFrame.StyledPanel) # 形状決定。パネル。
        vbox.addStretch(1) # 行間を定義
        frame.setLayout(vbox)
        return frame

    def add_checkbox(self,name):
        chxlist = QCheckBox(name, self)
        chxlist.stateChanged.connect(self.checklist_clicked)
        return chxlist

    #  クリックされたCheckboxをみてリストに追加 or 削除
    def checklist_clicked(self, state):
        checked_name = str(self.sender().text())
        
        try:
            self.clicked_list
        except:
            self.clicked_list = []

        if state == Qt.Checked: # if checked
            # add to the list
            self.clicked_list.append(checked_name)
        else: # if unchecked
            # remove from list
            self.clicked_list = [x for x in self.clicked_list if not x == checked_name]
        
        self.selected_columns = self.clicked_list
        self.plot_selected_columns()


    def add_label(self,text):
        # ラベルを作る
        label=QLabel(self)
        label.setText(text)
        label.adjustSize()
        return label


    def printItemText(self):
        # 選択したアイテムを表示
        items = self.listWidget.selectedItems()
        self.selected_columns = []
        for i in range(len(items)):
            self.selected_columns.append(self.listWidget.selectedItems()[i].text())

        self.plot_selected_columns()

    # Plotをアップデート
    def plot_selected_columns(self):
        self.axis.cla() # リセットを掛ける必要がある。
        df = self.df[self.selected_columns]
        df.plot(ax=self.axis)
        plt.grid(); 
        self.FigureCanvas.draw()


def main():
   app = QApplication(sys.argv)
   ex = Example("samplecsv.csv")
   sys.exit(app.exec_())

if __name__ == '__main__':
   main()

TODO

いくつか目処は立っていますが下記の要素を徐々に追加していくとそれっぽくなるものと思われます。

Python,PyQtで簡単にGUIを作る ① リスト表示とグラフPlot

モチベーション

身の回りのありとあらゆる便利パッケージにGUIがあったらな,と思うことが多いためやり方をメモします。

初回はひとまず

  • リストの複数を選択
  • matplotlibの図をリストの選択に応じて表示

ができるようにします。

サンプルを通した理解

わかりやすいサンプルコードを介して理解を深めるのが手っ取り早いです。 今回の目的には下記の内容が大部分で合致しているので参考にしました。

qiita.com

配置

構造としてはまずはQwidgetというキャンバスを用意して,QVboxlayoutというコンテナの中にグラフのプロットであったり,チェックボックスであったり具体的な要素を詰めていくという構造をとります。(理解がUpdateされ次第Updateします。)

f:id:ossyaritoori:20210717170350p:plain

超最低限に書くと下記のようになります。

# 初期化のサンプル
class Test(QtWidgets): # QWidgetを継承
    def __init__(self):
        super().__init__() # 継承元のInit
        vbox = QtWidgets.QVBoxLayout(self) #Vboxlayoutの定義。縦にオブジェクトを並べる。(QHboxなら横に) 
        vbox.addWidget( QtWidgets.QListWidget() ) # リスト表示Widgetを追加。
        vbox.addWidget( QtWidgets.QLabel(self) ) # ラベルWidgetを追加。(テキスト設定していないからこの段階では無)
        # ...このように追加していって
        self.setLayout(vbox) # VboxlayoutをWidgetにはめ込む

他にオブジェクトを配置する手法としては最後の部分はsetGeometryなどでゴリゴリ配置しても良さそうですし,

        # 配置
        self.setGeometry(0,0,900,600)
        self.FigureWidget.setGeometry(200,0,700,600)
        self.FileList.setGeometry(0,0,200,600)

配置をこりたい人は,Qt Designer というGUIがあるのでそれでレイアウトを作ったほうがいいと思います。

www7a.biglobe.ne.jp

自分は今はSplitterという機能を使って画面を分割して配置するようにしています。また次回にメモします。

イベントの設定

配置したオブジェクトたちにはイベントが起きたときにどうするかという設定をすることができます。

# インデント略
self.listWidget = QtWidgets.QListWidget()
self.listWidget.itemClicked.connect(self.printItemText) #クリックされたらprintItemText関数を呼ぶ

# 呼ばれる側の関数
    def printItemText(self):
        # 選択したアイテムを表示
        items = self.listWidget.selectedItems()
        x = []
        for i in range(len(items)):
            x.append(str(self.listWidget.selectedItems()[i].text()))
        print (x)

実行

Qtにおいては、QApplicationというクラスがメインのアプリケーションを定義するので, 最後に作ったWidgetクラスを表示する際には下記のようにします。

#アプリケーション本体のインスタンスを作る
app = QtWidgets.QApplication(sys.argv)    #アプリケーションのインスタンスを作る
Test.show()                        #作ったWidgetを表示する
app.exec()                          #アプリケーションのメッセージループを開始する

小ネタ

大体のオブジェクトは配置してから初期設定などをする必要があります。 そうするとaddWidgetでの配置が見づらくなるので関数化して一行で表せるようにしたほうがはかどります。

ラベルテキスト追加

ラベルオブジェクトを配置するならこうします。

    def add_label(self,text):
        # ラベルを作る
        label1=QtWidgets.QLabel(self)
        label1.setText(text)
        label1.adjustSize()
        return label1

リストでの複数選択

QListWidgetでSelectionModeを設定すると複数選択可能にできます。

毎回リセットするなら ExtendedSelection,トグルするなら MultiSelectionがおすすめです。 下記のように書きます。

        self.listWidget = QtWidgets.QListWidget()
        self.listWidget.setSelectionMode( #複数選択可能にする https://doc.qt.io/qt-5/qabstractitemview.html
            QtWidgets.QAbstractItemView.MultiSelection # QtWidgets.QAbstractItemView.ExtendedSelection 
        )

参考: QAbstractItemView Class | Qt Widgets 5.15.5

リストで番号を選択してPlotするサンプル

ということでリストで番号を選択してPlotするサンプルを下記に載せます。

f:id:ossyaritoori:20210717182134p:plain
選択した順番に下の図にPlotされます。

# -*- coding: utf-8 -*-
"""
Stackoverflowの文章より原型を拝借
QtのListwidgetで複数選択をしてPlotする。
"""

from PyQt5 import QtWidgets, QtCore
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas

class Test(QtWidgets.QDialog):
    def __init__(self, parent=None):
        super(Test, self).__init__(parent)
        self.layout = QtWidgets.QVBoxLayout()
        # リスト作成
        self.listWidget = QtWidgets.QListWidget()
        self.listWidget.setSelectionMode( #複数選択可能にする
            QtWidgets.QAbstractItemView.MultiSelection
        )
        self.listWidget.setGeometry(QtCore.QRect(10, 10, 211, 291))
        for i in range(10):
            item = QtWidgets.QListWidgetItem("%i" % i)
            self.listWidget.addItem(item)
        self.listWidget.itemClicked.connect(self.printItemText)

        # Figureを作成
        self.Figure = plt.figure()
        self.FigureCanvas = FigureCanvas(self.Figure)  # FigureをFigureCanvasに追加
        self.axis = self.Figure.add_subplot(1,1,1) # axを持っておく

        self.layout.addWidget(self.add_label("Ctrlを押しながらで複数選択"))
        self.layout.addWidget(self.listWidget)
        self.layout.addWidget(self.FigureCanvas)
        self.setLayout(self.layout)


    def add_label(self,text):
        # ラベルを作る
        label1=QtWidgets.QLabel(self)
        label1.setText(text)
        label1.adjustSize()
        return label1


    def printItemText(self):
        # 選択したアイテムを表示
        items = self.listWidget.selectedItems()
        x = []
        for i in range(len(items)):
            x.append(int(self.listWidget.selectedItems()[i].text()))

        print(x)
        self.update_Figure(x)

    # Figure
    def update_Figure(self,x):
        self.axis.cla() # リセットを掛ける必要がある。
        self.axis.plot(x,'-o')
        plt.grid(); plt.legend(["selected number"])
        self.FigureCanvas.draw()

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    form = Test()
    form.show()
    sys.exit(app.exec_())

m1 mac 環境構築メモ① 入力の設定やRemap、基本操作など

3月末にM1 macを買ってウキウキだったものの、なんだかんだWSL2にハマってしまって放置していました。 とりあえずまともにタイピングできる環境を整えたのでメモします。

機体と環境について

  • 2021年度版 m1 macbook air
    • 16GB, 512GB SSD
    • JIS配列キーボード

電池の持ちが一昔前のガラケーレベルなのが最も顕著に恩恵を感じます。 今はGoogle Colabなどで作業環境共同化もできるので回線さえあれば外出時でもそれなりに色々できます。

記事内での設定内容

  • Terminalをショートカットで開くようにする
  • キーボード設定
    • ファンクションキーをFnなしで動かす
    • JISキーボードをUS入力風に変更する
    • 単語移動を変更
  • 装飾キーまわりのメモ

ターミナルをショートカットで開く。

⌘スペースでWin+rのように呼び出しメニューを出せるのでそこでターミナルとうてばデフォルトでもキーボードだけでターミナルを起動できます。

ただ、やっぱCtrl+Alt+T的なので一発で開きたい場合はちょっと回り道をする必要があります。

Automatorというアプリでサービスというのを作成してそれにショートカットを割り振るというのがよく使われているようです。

https://ry0.github.io/blog/2015/10/21/open-terminal-shortcut/#gsc.tab=0

コンフリクトがない限りいくつでもショートカットは設定できます。 まっさらな環境だと普通にcontrol+command+Tを割り振れました。

キーボード設定

基本的なところ

基本的なところは箇条書きにて失礼します。

KarabinerでJISキーボードをUSキーボードに設定

下記の記事の通りにKarabinerをインストールします。

https://qiita.com/canonrock16/items/0c090276b2f1fd8eb0ab

初回はキーボードの識別に時間を取られてちょっとビビります。 その後、権限を求められるのでそのあたりをきちんと設定したらOKです。

f:id:ossyaritoori:20210605164845p:plain
Karabinerの設定画面

私の設定は以下の通り。

変換前 JISの該当キー 変換後 USの該当キー
international1 grave_accent_and_tilde(`)
international3 backslash \
caps_lock 左下の奴 left_command 左⌘
right_command 右⌘ right_control 右^

右のシフトも使わない気がしているので適当に変換しようかなとは思っています。

Karabinerでその他ショートカット作成

Karabinerでは複数キーの組み合わせもRemapすることができます。 これはComplex modificationsという箇所から設定できインターネット上からインポートしてくることができます。

https://ke-complex-modifications.pqrs.org/?q=option%20%2B%20arrows

例えばWindowsでやっていた操作を一通りMacで動かせるようにするには下記の設定検索からWindows shortcuts on macOS というのを検索してImportすれば良いです。

自分の場合単語移動をcontrolでできるように下記を入れました。

Exchange control + arrows keys with option + arrows keys

装飾キーまわり

Macを使うにあたりちょっと混乱したのがWindowsであったCtrl/Alt/Winキーの挙動がMacではあまり対応していないことです。

⌘はAltとCtrlの機能を兼ねたような挙動を持っていますが肝心なときにどっちかわからなかったりします。
Pythonを雑に起動して終了しようと思ったら画面分割されたりとか、Chromeで新規タブは⌘Tで出すのにタブ移動は^Tabだったりするのでちょっと困ります。

単語移動関連

WindowsやLinudxではCtrlで単語移動できますが、macではOptionで移動する必要があります。(自分はKarabinerの設定で変更しました。)

とりあえずデフォルトでのvscodeでの挙動を下記メモします。

option 上下 行を保持して移動
option 左右 単語移動
command 左右 行頭行末移動
command 上下 文頭文末移動
control 左右 ワークスペース切り替え
control ウィンドウ選択
control 最近開いたファイル表示?

使いにくいと思った部分は混乱がない範囲でRemapして行けばいいと思います。

また、日本語の単語移動を実現するにはその都度設定が必要のようです。 VScodeの場合はJapanese Word Handerを入れれば良さそうでした。

スクリーンショット

スクリーンショット自分の場合、ファイルに保存する必要はなくてクリップボードに入れば良いことが多いです。 下記を参考にしました。

https://tamoc.com/mac-screenshot-window/#i-3

結論、特定領域・Windowをクリップボードにコピーするには下記のとおりやればいいです。

  • control ⌃+command ⌘+shift ⇧+4 で自由領域Cropを起動
  • Spaceを押してWindowの選択をする

controlを押さなければ普通にデスクトップに保存されるようです。

その他

プログラム関係 - ターミナルの名前を変更 - https://code-graffiti.com/how-to-change-the-prompt-display-on-the-mac-terminal/ - Xcodeをインストール - homebrewのインストール - VSCODE - https://stackoverflow.com/questions/30065227/run-open-vscode-from-mac-terminal

今後もなにかあれば追加していくと思います。

2021年4月のよかったもの

誘われたので。4月は特になにかしてないのであまり書くことはありません。 (以下からである調)

まんてん

下記の記事にも紹介されている「まんてん鮨」の日暮里店に食べに行った。

r.gnavi.co.jp

6000円コースを頼んだのだが結論から言うと超腹一杯うまい鮨を食える。 おまかせなのでメニューは選べないが私が行ったときはこんな感じ。恥を忍んでメモをとった。

個人的にはキンメダイの炙りがうまかった。

## 6000円コース 28品目 *付きはシャリ付き
しじみ汁
*サクラマス にぎり
宮城めかぶ
ほたて こぶじめ
かつお たまり醤油のくぐり
湯葉くず粉
あわび
*コハダ にぎり
*キンメダイ あぶり にぎり
みずだこ煮
*しろえび にぎり
*くるまえびにぎり
うめ茶碗蒸し
*赤みにぎり
*トロにぎり
子持ち昆布
ほたるイカあぶり
*いくら + 茎山葵(お椀)
たらこ西洋わさびつけ
*青森ムラサキウニ
*北海道バフンウニ
ひょうたん柴漬け
*ねぎとろ巻
しじみ味噌汁
*穴子にぎり
卵焼き
*かんぴょう巻
すいか

写真は控えていたがウニだけは初めてちゃんと食べたので写真を撮った。

f:id:ossyaritoori:20210502010907p:plain
(上)北海道産バフンウニ,(下)青森産ムラサキウニ。 バフンウニは癖が少なくムラサキウニは味が濃い。

注:予約しないと基本入れなさそう。カウンターで大将の握るのを待つ時間が長いので友達と行くべし。

葬送のフリーレン

サンデーで連載されている漫画。よくあるRPG世界観風ファンタジーかなと侮っていたが思っていた以上に良かった。

読むとすごく優しい気持ちになれる。

1話から魔王を倒した後のエピローグで始まる異色の構成だが, 今を生きる若者と旅しながらかつての旅の思い出を振り返る主人公の描写がアラサーには結構刺さる。これ少年向けか??

ジャンプの「チェンソーマン」「アンデッドアンラック」もそうだが,新しい世代の漫画はタメ回のようなものが少なく(特に序盤)ストーリー展開により無駄がない様に感じる。

感想として今の若手の子たち漫画うますぎない?となっている。

その他

書こうか迷っているが別に書くほどでもないなと思っているやつら。

  • m1 macbook air
    • 電池が持つだけでこんなにも使用感がいいのか!となる。現状開発はWSL2に寄ってるのでやや持て余し気味。
  • vivy(2021年4月開始 SFアニメ)
    • ターミネーター的な王道テーマをきれいな絵柄と丁寧な表情描写で描いているオリジナルアニメ。SFに飢えていたので今後に期待。
  • 鎌倉 オクシロモン
    • お洒落なカレー屋さん。うまい。以上。
    • カレーつながりだと日吉のホアホアというアジアンダイニングのカレーが滅茶うまい。
  • ふるさと納税全般
    • 美味い。神。ありがとう我がふるさと。
  • Oisix
    • 献立を考えないというのがいかに楽かというのがわかる。でも高いのでやめた。

GitHub ActionsとMarkdownで書類作成 ~ Asciidoc編~

概要

下記記事のGitHub Actions版だと思えば良いです。

ossyaritoori.hatenablog.com

下記に出来上がった書類のサンプルを載せます。

https://yoshiri.github.io/MarkdownToAsciidocToHTML_CI/sample.html

Markdownからの変換手法

ここではMarkdownで書いた文をAsciidocに変換します。

qiita.com

MarkdownからAsciidocへの変換はPandocとKramdocの2択があります。

  • Pandoc
    • 言わずとしれた万能変換ツール
    • 🙆数式などの変換がちゃんとしている
    • 🙅タイトルへの変換
  • Kramdoc
    • Asciidoctorの開発側が開発した変換ツール
    • 🙆章立てなどがきれいに変換される
    • 🙅数式や一部書式が正しく変換されない

ここではPandocを採用します。(数式の書きにくいドキュメントはありえないので。)

ここで,Pandocで用いるコマンドを下記に示します。(DockerコマンドなのでPandoc環境がある人は適当に読み替えてください)

wsl docker run --rm -v $(pwd):/data pandoc/core: sample.md --to asciidoctor -o sample_pandoc.adoc  --shift-heading-level-by=-1

引数の詳細などは下記を参照すると良いです。(本当は英語版サイトのほうがもっと良い。)

sky-y.github.io

変換前提のMarkdownの書き方について

基本的な書き方はうまいこと変換されますが,気をつけなければ行けないのは章立てです。

通常の書き方をすると下記のようになると思います。

# 1章
## 1-1節
## 1-2 節
# 2章
## 2-1節
### 2-1-1節
...

本記事で紹介する変換法を用いる場合文章は下記のように書くことを推奨します。 AsciidocのLevel1はHTMLのタイトル用であり,章がLevel2からスタートしていることに起因します。

# 文書タイトル
## 1章
### 1-1節
### 1-2 節
## 2章
...
このように一つずつずらす。

または,マークダウンで見た時に不格好になるのが気にならなければ下記のようにかくのも良いでしょう。

= 文書タイトル
## 1章
### 1-1節
### 1-2 節
## 2章
...
(上記は`--shift-heading-level-by=-1`のときの書き方)

理由は次で説明します。

--shift-heading-level-by=-1を用いた時の挙動について pandocで下記のようなマークダウンを変換した場合,レベルを1段下げます。

# 文書タイトル
## 1章
### 1-1節

下記のようにAsciidocでは=#のような役割を持ちます。

== 文書タイトル
=== 1章
==== 1-1節

これに対して--shift-heading-level-by=-1を引数に加えることでレベル下げをキャンセルできます。 できるのですがタイトルのブッキングを避けるために下記のような結果を生成します。

文書タイトル   <- 本当は"="から始まってほしい 
== 1章
=== 1-1節

これを避けるもっとも手っ取り早い方法は最初からタイトルの装飾を"#"から"="に書き換えて置くことです。

置換でタイトルの"#"を"="に変更

タイトルの装飾を"#"から"="に書き換えて置くことで上記の問題は解決しますが,Markdownでこれを書くのは面倒。

ということで,GithubなどのCIの時点でタイトルの装飾を変えれば良いです。

具体的には,sedコマンドを用いて 文章の最初に出てきた"# "のを"= "に変えます。(タイトルはほぼ文章の最初に来るはずなので)

コマンドはこちら。

sed -ie '0,/# / s/# /= /' sample.md

workflowの定義

サンプルリポジトリをおいておきます。

github.com

workflowは下記のような内容です。

# This is a basic workflow to help you get started with Actions

name: CI

# Controls when the action will run. 
on:
  # Triggers the workflow on push or pull request events but only for the master branch
  push:
    branches: [ master ]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # asciidoc to html
  asciidoctor_job:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest
    name: Build AsciiDoctor
    steps:
    # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
    - name: Check out code
      uses: actions/checkout@v2
    - name: Fix Title Level Issue
      run: sed -ie '0,/# / s/# /= /' sample.md
    - name: Markdown To Asciidoc
      uses: docker://pandoc/core:2.11
      with:
        args: -f markdown+east_asian_line_breaks sample.md --to asciidoctor -o sample.adoc --wrap=preserve --verbose  --shift-heading-level-by=-1
    # Output command using asciidoctor-action
    - name: Build AsciiDoc step
      id: documents
      uses: Analog-inc/asciidoctor-action@master
      with:
        shellcommand: "asciidoctor sample.adoc -r asciidoctor-diagram -a allow-uri-read -a data-uri -a toc=left" 
    # Use the output from the documents step
    - name: move deploy files
      run: |
        mkdir build/
        mv sample* build/
        ls build
    - name: deploy
      uses: peaceiris/actions-gh-pages@v3
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./build
        publish_branch: gh-pages

以上!