Raspberry Piでジャイロを使ってみる:加速度センサーの値を表示するクラス(Python, Tkinter)
以前ラズパイに搭載したジャイロセンサーから出力される値をPythonで取得する方法を見てきました:
インストール作業に一手間ありましたが、すべてセットアップすればシンプルなコードで加速度や角加速度などを得る事が出来ました。ただそれらの値をターミナルにつらつらと流しても正直何が何やら…。やっぱりこういう値は「見える化」する事でより感覚的に認識できるもんなんです。そこで見える化を図るべく前回Pythonで図形を描画する方法を紹介しました:
ここまで見える化のお膳立てがある程度揃いましたので、今回は加速度センサーの値をTkinterの図形描画でグラフとして表示する仕組みを作ってみようと思います。
加速度センサーの軸方向と意味
今更なのですが、今回のジャイロ基板の加速度センサーで定義されている軸の方向とその意味を整理しておきましょう:
基板の右にXYZ軸の方向が刻印されています。また曲がった矢印で回転の正の向きが表されています。この軸と回転方向の関係はいわゆる「右ネジ」です。右手の親指を立てて軸の方向に向けた時、残り4本の指で巻く回転方向を正とするのが右ネジです。そこからZ軸の向きが「空方向」なのがわかります:
3次元の座標系の分け方に「左手系」「右手系」があります。これは左右の手について親指をX軸、人差し指をY軸とした時に、中指とZ軸の向きがあった手で判断出来ます。上のジャイロ基板はそれで調べると「左手系」ですね。
基板を平らな所に置いてじっとしていると、Z軸の値が+9.7~+9.8位の値を指します。これは重力加速度を検知しています。ただZ軸は空方向でした。つまり加速度センサーは加速度の方向を指すのではなくてその逆方向、感覚的に言えば「人がGを感じる方向」を正とします:
水準表示クラス「LevelGraph」を設計する
今回作る水準計のイメージはこんな感じです:
センサーを傾けると加速度センサーの値を示す水準点がひょろひょろと傾いた方向に対応して動く…みたいな感じです。
この表示イメージを実現するコードをメインコードにつらつら書く事はできますが、それだと再利用性やカスタマイズ性に乏しくなってしまいます。そこで今回は専用の表示クラスであるLevelGraphクラスを作り、表示の仕事を担ってもらおうと思います。
取り敢えず必要最低限の動作&表示が出来れば良いとします。どういう機能が必要になるか、そして仕事の分担をどうするか、設計しつつ考えてみましょう。
水準点の位置(setPosition)
加速度センサーのXYZ値は各軸にかかるGの大きさです。これはセンサーが「ぶん!」と動くと勿論反応しますし、静止していても地球の重力加速度に反応し続けます。センサーが静止してY軸回転して傾いている場合、下図のようにGが各軸に分配されます:
3軸のGベクトルを同時に表示するには3D表示が必要になりますが、それはTkinterだとちょっと面倒なんですね。しかも3Dなグラフは人にとって実は認識し辛いグラフです。上の模式図も最初XYZ3軸で表現しようとしましたが…分かりにく過ぎて2軸に描き直しました(-_-;。そこでGUIのグラフも1軸落として2軸表示にする事にしましょう。
2軸は右方向を第1軸(X)、上方向を第2軸(Y)とします。表示に必要な値はsetPosition(x,y)というインターフェイスで与えることにします。
軸のスケール(setAxisScale)
加速度センサーのXYZ軸の値は、使う環境によりますが、通常扱いならせいぜい0~5Gくらいでしょうか。ざっくり言えば±50(m/s²)くらい。つまりsetPositionメソッドに入る値はその程度なわけです。もしその値をそのままピクセル座標として表示すると、±50くらいの幅で水準点が動く事になります。ラズパイのデスクトップ画面は800~1600ピクセルぐらいの解像度はあるわけでして、この移動幅はちょっと狭すぎます。
つまりsetPositionメソッドで渡される値は何らかの倍率でスケーリングしないといけないんですね。これを「軸スケール」と呼ぶ事にします。setAxisScaleメソッドを設けて軸スケールを設定できるようにします。
引数は(xScale, yScale)と各軸の倍率をダイレクトに指定する方法も考えられます。でももう少し具体的な設定をしたい所です。例えば「画面の100ピクセルで1単位くらい」のようにした方が直感的ですよね。なので(xUnit, xPixelLen, yUnit, yPixelLen)と各軸の単位数と対応するピクセル幅をセットで指定する事にします。
グラフの表示領域(setGraphRect)
表示するグラフの表示位置を長方形の領域で指定するsetGraphRectメソッドも必要ですよね。典型的には(x, y, width, height)でしょうか。すべてピクセル座標です。左上が(x,y)で幅高さが(width, height)です。
…まぁこんなもんでしょうか。これらを軸に実装していく事にしましょう。実装時に必要になったメソッドは都度追加していきます。
LevelGraphクラスを実装する
クラスを定義するlebelgraph.pyファイルを新規追加します。このファイル名がimport名で使われるので全部小文字にしときます。クラスの雛型はこんな感じです:
class LevelGraph:
# コンストラクタ
def __init__( self, ... ):
...
# 水準点の値を設定
def setPositiion( self, x, y ):
...
# 軸スケールを設定
def setAxisScale( self, xUnit, xPixelLen, yUnit, yPixelLen ):
...
# グラフ領域を設定
def setGraphRect( self, x, y, width, height ):
...
以下でコードの内容を細かく説明致しますが、全コードは一番最後に補遺として挙げておきますのでご自由にお使い下さい(^-^)
コンストラクタ
今回はTkinterベースなので、LevelGraphクラスはTkinterについて知っている(=依存する)としてしまいましょう。でもLevelGraphがルート(メインウィンドウ)を兼ねてしまうと流石に仕様が固すぎます。ウィンドウ内に複数のグラフを表示させる事も想定するなら、LevelGraphはCanvas単位が適切かなと思います。なのでコンストラクタでCanvasを作り引数に渡される親レイヤーに紐づける事にしましょう。
他の引数は無しにしときます。僕はコンストラクタの引数が多いのはあまり好みでは無いので(^-^;。表示を変更したいならセッターを呼んで対応です。でも、コンストラクタの中で表示に必要な初期化は全部します:
# 初期化
# parent: 親レイヤー
def __init__( self, parent ):
self.__parent = parent
# 図形初期化
self.__canvas = tkinter.Canvas( self.__parent )
self.__circle = self.__canvas.create_oval( 0.0, 0.0, self.__radius * 2.0, self.__radius * 2.0 )
# グラフ領域初期化
self.setGraphRect( 0, 0, 200, 200 )
# 水準点初期化
self.circleRadius( self.__radius )
最初にCanvasを一つ作成します。親は引数のparentです。水準点となる円もデフォルトのオブジェクトを作っておきます。
次にsetGraphRectメソッドを呼んでグラフ領域の位置や幅高を初期化します。この中で軸の再設定と円の位置が再設定されます(後述)。
水準点の半径も指定します。これはcircleRadiusメソッドで半径を指定する事にしました。
setGraphRectメソッド
グラフ領域を変更するsetGraphRectメソッドは次のようになります:
# グラフの表示領域を設定
def setGraphRect( self, x, y, width, height ):
self.setGraphPosition( x, y )
self.setGraphWH( width, height )
領域の変更は結局位置の変更と幅高の変更の2つに分離出来ます。それぞれをsetGraphPositiion、setGraphWHメソッドとして定義しておけば、後でこれらを個別変更したくなった時に便利です。
setGraphPositionメソッドの実装はこんな感じです:
# グラフの表示位置を設定
def setGraphPosition( self, x, y ):
self.__canvas.place( x = x, y = y )
self.__cvRect[ 0 ] = x
self.__cvRect[ 1 ] = y
グラフ全体である__canvasのplaceメソッドで直接親レイヤーベースの表示座標を指定しています。表示領域の情報は__cvRectというメンバに保存しておくことにします。これは[x, y, width, height]というリストです。
setGraphWHメソッドの実装を見てみましょう:
# グラフの表示幅を設定
def setGraphWH( self, width, height ):
self.__canvas.configure( width = width, height = height )
self.__cvRect[ 2 ] = width
self.__cvRect[ 3 ] = height
# 軸と水準点を再設定
self.__createAxis()
self.setPosition( self.__pos[ 0 ], self.__pos[ 1 ] )
Canvasの幅と高さを変えるにはconfigureというメソッドを使います。configureメソッドは幅高以外にも様々なプロパティーを設定できる汎用メソッドです。ここではwidth、heightオプションにそれぞれの値を指定します。表示幅と高さを変更したら軸と水準点の描画位置が変わってしまうため再描画を促しています。
setAxisScaleメソッド
軸のスケールを設定するsetAxisScaleメソッドは、引数に単位と、その単位に対応した軸のピクセル長を指定します:
# 軸のスケールを変更
# unitX, unitY : 各軸の1単位の値
# pixelLenX, pixelLenY : 1単位に対応する軸のピクセル幅
def setAxisScale( self, unitX, pixelLenX, unitY, pixelLenY ):
self.__pixelPerUnit = [ pixelLenX / unitX, pixelLenY / unitY ]
self.setPosition( self.__pos[ 0 ], self.__pos[ 1 ] )
例えばunitX=5、pixcelLen=100とすると、5単位が100ピクセルなので1単位は20ピクセルとなります。これを__pixcelPerUnit(1単位あたりピクセル数)として保持しています。__pixcelPerUnitがあれば、水準点を描画する時に水準点の値にこれを掛け算すれば表示位置のピクセル数を計算できるわけです。
軸スケールを変えると水準点の表示位置も変更が必要なのでsetPositionメソッドを呼んで更新しています。
setPositionメソッド
水準点の値に対応して表示を更新するのがsetPositionメソッドです:
# 水準器の位置を更新
def setPosition( self, x, y ):
self.__pos[0] = x
self.__pos[1] = y
# 表示位置は軸スケールに依存
ox = self.__cvRect[ 2 ] / 2.0
oy = self.__cvRect[ 3 ] / 2.0
refX = x * self.__pixelPerUnit[ 0 ] - self.__radius
refY = -y * self.__pixelPerUnit[ 1 ] - self.__radius
px = ox + refX
py = oy - refY
self.__canvas.moveto( self.__circle, px, py )
__posに値を保持するのは良いとして、表示位置の計算はちょっと面倒です。まず(ox, oy)は原点の表示位置です。これは今はグラフ領域のど真ん中としています。
円オブジェクトの描画位置の原点からの相対位置(refX, refY)は、軸スケールがあるため__pixelPerUnitに値を掛ける事でピクセル長に変換します。この時y座標を反転する必要があります。CanvasのY軸は下方向なのに対して、グラフのY座標は上向だからです。さらに円オブジェクトの位置は左上なので半径分左上に減算する事で、表示位置が円の中心になります。この辺りは落ち着いて実装すれば大丈夫(^-^)
表示指定位置(px, py)は上の諸々の計算から求まります。最後にCanvas.movetoメソッドで(px,py)の位置に表示して完了です。
circleRadiusメソッド
水準点の円の半径を変更するメソッドです:
# 水準器の半径を設定
def circleRadius( self, r ):
self.__radius = r
self.__canvas.coords( self.__circle, 0, 0, r * 2.0, r * 2.0 )
self.setPosition( self.__pos[ 0 ], self.__pos[ 1 ] )
Canvasから作った図形オブジェクトの表示領域を直接変更するにはCanvas.coordsメソッドを使います。(x0, y0, x1, y1)と左上右下座標をそれぞれ指定します。半径が変わると円の表示時のオフセットが異なる為setPositionメソッドで再描画しています。
__createAxisメソッド
最後__createAxisメソッドは軸を表示します:
# 軸を作成
def __createAxis( self ):
if self.__axisX != None:
self.__canvas.delete( self.__axisX )
self.__canvas.delete( self.__axisY )
w = self.__cvRect[ 2 ]
h = self.__cvRect[ 3 ]
self.__axisX = self.__canvas.create_line( 0.0, h / 2.0, w, h / 2.0 )
self.__axisY = self.__canvas.create_line( w / 2.0, 0.0, w / 2.0, h )
軸は線分で表現しています。グラフの表示領域の真ん中が原点になるようにXY軸の表示位置を計算しています。このメソッドは作り直し時にも呼ばれるため、既に線分オブジェクトが存在している場合はそれを消して作成し直しています。
センサーとの結合とグラフ表示は次回に
これでクラスの実装はとりあえず終了~。さっそく加速度の値を放り込みたい所ですが、ちょっと記事が長くなりましたのでそれは次回に持ち越します。まぁここまで来ればもう出来たようなもんです。
ではまた(^-^)/
<次回>
補遺:LevelGraphクラス
import tkinter
class LevelGraph:
__canvas = None
__cvRect = [ 0.0, 0.0, 200.0, 200.0 ]
__pos = [0.0] * 2 # 水準の値
__parent = None
__radius = 5.0
__circle = None
__axisX = None
__axisY = None
__pixelPerUnit = [ 1.0, 1.0 ]
# 初期化
# parent: 親レイヤー
def __init__( self, parent ):
self.__parent = parent
# 図形初期化
self.__canvas = tkinter.Canvas( self.__parent )
self.__circle = self.__canvas.create_oval( 0.0, 0.0, self.__radius * 2.0, self.__radius * 2.0 )
# グラフ領域初期化
self.setGraphRect( 0, 0, 200, 200 )
# 水準点初期化
self.circleRadius( self.__radius )
# グラフの表示幅を設定
def setGraphWH( self, width, height ):
self.__canvas.configure( width = width, height = height )
self.__cvRect[ 2 ] = width
self.__cvRect[ 3 ] = height
# 軸と水準点を再設定
self.__createAxis()
self.setPosition( self.__pos[ 0 ], self.__pos[ 1 ] )
# グラフの表示位置を設定
def setGraphPosition( self, x, y ):
self.__canvas.place( x = x, y = y )
self.__cvRect[ 0 ] = x
self.__cvRect[ 1 ] = y
# グラフの表示領域を設定
def setGraphRect( self, x, y, width, height ):
self.setGraphPosition( x, y )
self.setGraphWH( width, height )
# 軸のスケールを変更
# unitX, unitY : 各軸の1単位の値
# pixelLenX, pixelLenY : 1単位に対応する軸のピクセル幅
def setAxisScale( self, unitX, pixelLenX, unitY, pixelLenY ):
self.__pixelPerUnit = [ pixelLenX / unitX, pixelLenY / unitY ]
self.setPosition( self.__pos[ 0 ], self.__pos[ 1 ] )
# 水準器の半径を設定
def circleRadius( self, r ):
self.__radius = r
self.__canvas.coords( self.__circle, 0, 0, r * 2.0, r * 2.0 )
self.setPosition( self.__pos[ 0 ], self.__pos[ 1 ] )
# 水準器の位置を更新
def setPosition( self, x, y ):
self.__pos[0] = x
self.__pos[1] = y
# 表示位置は軸スケールに依存
ox = self.__cvRect[ 2 ] / 2.0
oy = self.__cvRect[ 3 ] / 2.0
refX = x * self.__pixelPerUnit[ 0 ] - self.__radius
refY = -y * self.__pixelPerUnit[ 1 ] - self.__radius
px = ox + refX
py = oy - refY
self.__canvas.moveto( self.__circle, px, py )
# 軸を作成
def __createAxis( self ):
if self.__axisX != None:
self.__canvas.delete( self.__axisX )
self.__canvas.delete( self.__axisY )
w = self.__cvRect[ 2 ]
h = self.__cvRect[ 3 ]
self.__axisX = self.__canvas.create_line( 0.0, h / 2.0, w, h / 2.0 )
self.__axisY = self.__canvas.create_line( w / 2.0, 0.0, w / 2.0, h )