Raspberry Piでジャイロを使ってみる:加速度センサーの値をGUIで表示(Python, Tkinter)

 前回ジャイロセンサーの加速度センサーの値を表示するためのクラス(LevelGraphクラス)を作りました:

今回は加速度センサーの値を取得し続けて、その様子をGUIに表示して見える化してみましょう。

加速度センサーの値を取得する

 今回扱っているのはAdafruit社製のジャイロセンサー「LSM9DS1搭載 9軸センサモジュール」です:

 このセンサーから加速度の値を得る方法についてはこちらの記事で取り上げました:

 ありがたい事に専用ライブラリがあるので値を得るのは至極簡単です。が、一つ知っておかないといけない事があります:

import board
import adafruit_lsm9ds1
 
i2c = board.I2C()
sensor = adafruit_lsm9ds1.LSM9DS1_I2C( i2c )
 
acc = sensor.acceleration   # 各軸の加速度を取得

 sensor.accelerationに呼び出した時点での加速度の値があるのですが、それをコピーした上のaccはリストでしょうか?もしくはタプル?実はこれ「mapオブジェクト」というものなんです。

mapオブジェクト

 mapと言うとC++では辞書の事ですが、Pythonのmapというのはそれとは全然違う別物のようです。
 Pythonにはmap関数mapオブジェクトというのがあります。map関数を使うと戻り値でmapオブジェクトを得られます。これらの働きを言語化したい所なんですが…ちょっと言葉で表しにくいんです(-_-;。なので実例で感じて頂ければと思います。

 map関数は次のように引数に「関数」と「イテレータ」を取ります:

def add1( val ):
    return val + 1
 
mapObj = map( add1, [ 10, 20, 30 ] )

 第1引数に渡した関数は、第2引数に渡したイテレータ(リストとかタプルとか辞書とか)の各要素を変更する役目を担います。で、その変更はmap関数の戻り値であるmapオブジェクトに「値をくれ」と要求した時に初めて行われます。

 例えば上の場合では、map関数の戻り値のmapObjに「値をくれ」と要求すると、map関数の第2引数に渡した[10, 20, 30]というリスト内の各数値に対して1足し算するadd1関数が適用され、[11, 21, 31]という並びが出力されます。要はmap関数はそういう一連の加工をするワンセットを作る人で、その加工を実行するのがmapオブジェクトというわけです。

 mapオブジェクトはイテレーターではあるのですが、リストのようにアクセスする事は出来ません:

#アクセスエラー
val = mapObj[ 0 ]

 こんな感じの配列アクセスをしようとすると怒られます。辞書でも無いのでキーによるアクセスもできません。

 mapオブジェクトから数値を取り出すにはlist()やtuple()のようなコレクション系の関数を通して値を変換してもらう必要があります:

#リストとして格納
valList = list( mapObj )
 
#タプルとして格納
valTpl = tuple( mapObj )

 「直接リストでくれればいいのに…」と思うかもしれませんが、値を得る前に加工処理を走らせなければならないため、それを走らせる人(list()とかtuple())が必要なんです。もちろんfor文でも回せますよ。

 adafruit社が提供するAPIのsensor.accelerationがmapオブジェクトなのも取得前に加工が必要だからです。ちょっと公開されているAPI内の該当コードを覗いてみましょう:

@property
    def acceleration(self) -> Tuple[float, float, float]:
        raw = self.read_accel_raw()
        return map(
            lambda x: x * self._accel_mg_lsb / 1000.0 * _SENSORS_GRAVITY_STANDARD, raw
        )

 accelerationにアクセスした時rawという生データ(int型)をセンサーから読み取っています。そしてmap関数を通して生データを各軸の加速度の並びに変換する機構を作っています。この一連の処理をユーザーに意識させないためmapオブジェクトとして返しているんです。

 という事で、加速度の値をタプルとして取るコードはこうなります:

import board
import adafruit_lsm9ds1
 
i2c = board.I2C()
sensor = adafruit_lsm9ds1.LSM9DS1_I2C( i2c )
 
acc = tuple( sensor.acceleration )   # 各軸の加速度をタプルとして取得

移動平均を取る

 センサーが返す値は通常内外の様々な要因によって揺れ動いています。その為値をダイレクトに扱うと駆動部がブルブルと震えてしまいます。これを抑えるには平均値を取るのが一般的です。例えば距離センサーはある距離をじっくり測る事が多い為算術平均が良いのですが、加速度センサーなど常に値が変動する物は移動平均がベターかなと思います。

 移動平均というのは最新のデータに加えある一定数(もしくは一定期間)の過去のデータで算術平均を取る方法です。最新データが刻々と更新されると、過去のデータの範囲も新しい方へシフトしていきます。「移動」の意味合いはこれです:

 移動平均を実現するコードの説明をするのはここではちょっと冗長であるため、今回は以下の移動平均を管理するクラスを活用するという形にさせて下さい:

<average.py>

#移動平均
class MoveAverage:
    __total = 0.0        # データの積算値
    __len = 10           # 移動平均を取るデータの個数
    __topDataIdx = -1    # 現在の最新データ格納位置
    __dataList = [0.0] * __len   # データリスト

    def __init__(self, len = 10):
        self.setLen( len )

    # 移動平均を取るデータの個数を変更
    def setLen( self, len ):
        if ( len <= 0 ):
            return False
        self.__len = len
        self.clear()

    # データを追加
    def add( self, data ):
        # 最新データを追加し最古データを除く
        self.__topDataIdx = ( self.__topDataIdx + 1 ) % self.__len
        self.__total += data - self.__dataList[ self.__topDataIdx ]
        self.__dataList[ self.__topDataIdx ] = data
        return self.average()

    # 現在の平均値を取得
    def average( self ):
        return self.__total / self.__len
    
    # データをクリア
    def clear( self ):
        self.__total = 0.0
        self.__dataList = [0.0] * self.__len
        self.__topDataIdx = -1

 MoveAverageクラスのaddメソッドにデータを渡すと移動平均の情報を更新してくれます。addメソッドの戻り値かaverageメソッドで現在の移動平均値を取得できます。

加速度を取り続けてGUIに表示

 では本丸です。加速度をセンサーから値を取り続けて、都度GUIにその様子を投影してみます:

#水平確認アプリ

import threading
import board
import adafruit_lsm9ds1
import average
import tkinter
import levelgraph

i2c = board.I2C()
sensor = adafruit_lsm9ds1.LSM9DS1_I2C( i2c )

root = tkinter.Tk()
ww = 400
wh = 400
root.geometry( "{0}x{1}".format( ww, wh ) )
levelGraph = levelgraph.LevelGraph( root )
levelGraph.setGraphRect( 0, 0, ww, wh )
levelGraph.circleRadius( 10.0 )
levelGraph.setAxisScale( 1.0, 10.0, 1.0, 10.0 )

# 更新スレッド作成
def startThread():
    # センサーから加速度を取得しグラフに表示(移動平均値)
    aveNum = 100
    accs = [ average.MoveAverage( aveNum ), average.MoveAverage( aveNum ) ]
    while True:
        vals = tuple( sensor.acceleration )
        levelGraph.setPosition( accs[ 0 ].add( vals[ 0 ] ), accs[ 1 ].add( vals[ 1 ] ) )

thread = threading.Thread( target = startThread, daemon = True )
thread.start()

root.mainloop()

 GUIを表示するLevelGraphオブジェクトは軸スケールを10(10ピクセルで加速度1単位)にし、水準点の半径は10ピクセルとしました。

 値を取り続けて図形描画を更新するにはスレッドを回します。この辺りは前回の図形描画の記事で取り上げました:

 スレッド内ではまずsensor.accelerationで得られる加速度のmapオブジェクトをタプルに変換、次に移動平均を計算してくれるMoveAverageオブジェクトに加え、返ってくる平均値をLevelGraph.setPositionメソッドに渡し表示を更新しています。ここまで積み上げてきた仕組みをフルに使っています。

 上のコードを実際に動かしてみた様子がこちらです:

 手がプルプル震えているのはカメラの前にセンサーを映すべく、無理な姿勢でセンサーを摘まんでいるからですww。撮影するのも大変なんですよw。
 それはさておき、動画を見て分かるようにセンサーの傾きに合わせて画面の水準点が上下左右にちゃんと反応していますよね。右に傾けたら右へ、上へ傾けたら上へと水準点が移動しています。原点に合えばセンサーは水平です。水準器ができました!!

終わりに

 ようやくラズパイに搭載したジャイロセンサーの加速度センサーの値をグラフィカルに表示する事が出来ました。ハードウェアの準備(半田付けと配線)に始まりソフトウェア側の諸々の用意(APIのテスト、LevelGraphクラスの作成)などそれなりに手数はかかりましたが、やっぱり見える化するとセンサーの挙動が感覚的に理解出来ます。見える化は偉大です!

 ところで、ここまでの段階でできる事を整理しておきます。加速度センサーの値はセンサーの各軸にかかる加速度です。それが重力加速度のみの場合、すなわちセンサーがその場で停止していて捻り(回転)のみしている場合、その重力加速度からセンサーの傾きを検出する事ができます。これは例えばドローンの初期設定で水平に近い場所に静止させて姿勢をリセットする時などに使えます。

 しかし、もし基板がロボットとか車とかドローンなどの移動体に搭載されていて、前後左右上下に常に揺れ動いている場合、加速度センサーのみではセンサーの傾きを検出する事はできなくなります。動いている方向に重力加速度とは別の加速度が生じてしまうからです。そういう移動体でセンサーの傾きを得るにはまた別の考え方が必要になります。その辺りについても後々検討したい所です。

 今回はZ軸の情報のみを扱いましたが、次回はX軸やY軸の傾きについても表示してみようと思います。

ではまた(^-^)/

<次回>

この記事が気に入ったらサポートをしてみませんか?