読者です 読者をやめる 読者になる 読者になる

fitbitエクササイズデータ (TCX) をPandas.DataFrameに入れてプロットする

ジョギングデータが入手できたので、次はプロット。

  • XMLオブジェクトのままでは扱いにくいので、Pandas.DataFrameの手習いも兼ねてDataFrameに入れる。
  • matplotlibでプロットする。

まず、Pandasをざっと勉強。知ってはいたがいじっていなかったので。numpyのndarrayを直接利用したときに自分でも拡張していたのと同様に時系列データを扱うときに便利なようなDataFrameが提供されていて、納得。

10 Minutes to pandas — pandas 0.19.2 documentation

以下のようにして、TCXからDataFrameを構築した。

def get_activity_tcx(auth2_client, log_id):
    """
    get TCX data with log_id.

    Parameters
    ----------
    auth2_client : fitbit.Fitbit
        web API client object
    log_id : str
        Activity log ID.

    Return
    ------
    df : pandas.DataFrame
        data frame containing the Activity.
    """
    fitbit_tcx = auth2_client.activity_tcx(log_id=log_id)
    # convert XML elements into DataFrame
    rows = []
    for tp in fitbit_tcx.iter(TCX_NAMESPACE + 'Trackpoint'):
        time_text = tp.find(TCX_NAMESPACE + 'Time').text
        time = pd.to_datetime(time_text, utc=True)
        hr_body = tp.find(TCX_NAMESPACE + 'HeartRateBpm')
        hr_text = hr_body.find(TCX_NAMESPACE+'Value').text
        hr = float(hr_text)
        dist_text = tp.find(TCX_NAMESPACE + 'DistanceMeters').text
        dist = float(dist_text)
        rows.append((time, [dist, hr]))
    df = pd.DataFrame.from_items(rows, orient='index',
                                 columns=['distance_meters', 'heartrate_bpm'])
    return df

あとは、pandasのplotを利用して簡単にプロットできた。

# get TCX data and plot it.
df = get_activity_tcx(auth2_client, LOG_ID)

# replace index with elapsed seconds.
df.index = (df.index - df.index[0]) / pd.Timedelta(1, unit='s')
df.plot(subplots=True, sharex=True)

f:id:ken_miyashita:20170103233053p:plain

ちなみに、同じデータをfitbit.comのWebサイトで表示させると下図のようになり同一データがプロットできていることがわかる。

f:id:ken_miyashita:20170103233228p:plain

fitbitエクササイズデータ (TCX) を列挙する その2

前回で、APIとしてのfitbit.activity_logs_list() は完成した。ただし、以下の点が気になるのでユーティリティ関数でも作ってみよう。

  • activity_logs_list() は Web APIとして一度に巨大なデータを返さないpaginationの考え方に基づいており、一回の呼び出しでは最大20エントリーしか戻らない。これはふつうのプログラミングAPIとしては利用しにくい。
  • 今回の用途では、logType = mobile_run である「明示的に記録したジョギング」エントリの log_id 一覧が欲しい。

よって、以下のような generator を作った。これで20エントリー以上あっても(時間はかかるが)fitbitサーバーから一気にデータを持ってくることができる。

def enumerate_activity_logs(auth2_client, log_type, from_date, to_date):
    """
    enumerate activity logs with a certain log type.
    note this function works as a 'generator' which you can use in
    enumeration loop.
    
    Parameters
    ----------
    auth2_client : fitbit.Fitbit
        web API client object
    log_type : str
        log type such as 'mobile_run'.
        See https://dev.fitbit.com/docs/activity/#get-activity-logs-list
        for more details.
    from_date : str
        The beginning date in the format yyyy-MM-ddTHH:mm:ss. 
        Only yyyy-MM-dd is required.
    to_date : str
        The ending date in the format yyyy-MM-ddTHH:mm:ss. 
        Only yyyy-MM-dd is required.
    
    Return (yield)
    --------------
    act : 'Activity' map object
        map object which contains an Activity data.
        See https://dev.fitbit.com/docs/activity/#get-activity-logs-list
        for more details.
    """
    response = auth2_client.activity_logs_list(after_date=from_date)
    enumerating = True
    while enumerating:
        for act in response['activities']:
            if act['startTime'] > to_date:
                enumerating = False
                break
            if act['logType'] == log_type:
                yield act
        if enumerating:
            next_request = response['pagination']['next']
            response = auth2_client.make_request(next_request)

このユーティリティーは以下のようにして呼び出すことができ、きちんと自分のジョギング日時リストが取得できた。

"""for obtaining Access-token and Refresh-token"""
server = Oauth2.OAuth2Server(USER_ID, CLIENT_SECRET)
server.browser_authorize()
 
"""Authorization"""
auth2_client = fitbit.Fitbit(USER_ID, CLIENT_SECRET, oauth2=True, access_token=server.oauth.token['access_token'], refresh_token=server.oauth.token['refresh_token'])

for run in enumerate_activity_logs(auth2_client, 'mobile_run', '2016-12-01', '2017-01-01'):
    print(run['startTime'])

fitbitエクササイズデータ (TCX) を列挙する

log_idさえ分かればTCXデータが入手できることは分かった。あとはエクササイズデータの一覧の入手ができるか、が問題。

Activity & Exercise Logs — Fitbit Web API Docs で一定期間の log_id が入手できるようだ。

python-fitbit には、この API 呼び出しに対応する python メソッドは定義されていないので拡張しよう。 そのためには requests モジュールの Session.request() というメソッドの仕様を調べないと、どうやってURL末尾パラメータを埋め込むのか分からない。

Requests: 人間のためのHTTP — requests-docs-ja 1.0.4 documentation をざっと調べる。params 引数に key-value ペアを入れればURL末尾にエンコードしてくれるとのことなので、以下のような activity_logs_list() を作ってみた。

    def activity_logs_list(self, user_id=None, before_date=None,
                           after_date=None, limit=20):
        """
        * https://dev.fitbit.com/docs/activity/#get-activity-logs-list

        Parameters
        ----------
        before_date : str
            The date in the format yyyy-MM-ddTHH:mm:ss. 
            Only yyyy-MM-dd is required. 
            Either before_date or after_date must be specified.
        after_date : str
            The date in the format yyyy-MM-ddTHH:mm:ss.
            Only yyyy-MM-dd is required.
            Either before_date or after_date must be specified.
        limit : number
            The max of the number of entries returned (maximum: 20).
            
        Return
        ------
        json : JSON
            JSON object representing the list.
        """
        if not((isinstance(before_date, str) and after_date is None) or \
                (isinstance(after_date, str) and before_date is None)):
            raise ValueError('either before_date or after_date is required')
        if isinstance(before_date, str):
            sort = 'desc'
        else:
            sort = 'asc'
        url = "{0}/{1}/user/{2}/activities/list.json".format(
            *self._get_common_args(user_id)
        )
        params = {
            'beforeDate' : before_date,
            'afterDate' : after_date,
            'sort' : sort,
            'limit' : limit,
            'offset' : 0
        }
        return self.make_request(url, params=params)

テストコードからこのメソッドを呼び出してみる。下図のようにきちんとリストが取得できた。 activities というリストにアクティビティが列挙されている。

さらに、2016/12/21 の朝 7:30 あたりからおこなったジョギングのデータは、下図のように 見えている。この logType が mobile_run となっているものが、明示的にジョギングを記録した エントリーだ。自動計測されたものは logType = auto_detected となっているので、区別できる。

fitbitデータにアクセスしてみる (1秒ごとの心拍数)

初期調査

もともとやりたかったのは、ジョギング時の1秒ごとの心拍数をグラフ化すること。 どうも比較的最近までは1秒ごとの心拍数を外部に出すAPIはなかったようだが、最近できたようだ。

そろそろググって誰かのサンプルをコピーしてくるのではなく、まともにAPIマニュアルを読むことにする。

Web API Documentation — Fitbit Web API Docs

どうも、「1秒ごとの心拍数をとる」といっても、1日分をとるのと、明示的に運動モードにしたときのデータをTCXフォーマットで取り出すのと2通りの話があるようだ。

  • 1日分の心拍データを1秒単位でとる
  • TCX情報を取り出す
    • Activity & Exercise Logs — Fitbit Web API Docs
    • こちらは、スポーツ用をうたっているのだから、実際に1秒ごとの心拍数を計測&記録している、と期待する。
    • REST アクセスするための URL は簡単なのだが、[log-id] というものの取得が人力でアクセスした Web ページからしか分からない?

まず [log-id] は決め打ちだとして、TCXデータを取得できるのかを試してみよう。

この TCX を取り出す機能は python-fitbit でラッピングしているのか? どうも 'tcx' というキーワードがソースコードの中から見つからないので、まだサポートしていないと思われる。とすると、拡張しないと駄目だな。

実験その1

python-fitbit では、以下のURLを用いたアクセスはサポートしていない。

GET https://api.fitbit.com/1/user/[user-id]/activities/[log-id].tcx

よって、activity_stats() を参考にして、実験的に作ってみる。

    def activity_tcx(self, user_id=None, log_id=''):
        """
        * https://dev.fitbit.com/docs/activity/#get-activity-tcx
        
        Return
        ------
        tcx : xml.etree.ElementTree.Element
            root element of TCX data 
        
        Exception
        ---------
        BadResponse
            when HTTP response status is error
        """
        url = "{0}/{1}/user/{2}/activities/{log_id}.tcx".format(
            *self._get_common_args(user_id),
            log_id=log_id
        )
        response = self.client.make_request(url)
        if response.status_code != 200:
            raise BadResponse
        return ET.fromstring(response.content)

あとは、アプリコードで呼び出す。

    """Getting data"""
    fitbit_tcx = auth2_client.activity_tcx(log_id=LOG_ID)
    
    for tp in fitbit_tcx.iter(TCX_NAMESPACE + 'Trackpoint'):
        time = tp.find(TCX_NAMESPACE + 'Time').text
        hr = tp.find(TCX_NAMESPACE + 'HeartRateBpm')
        hr_val = hr.find(TCX_NAMESPACE+'Value').text
        print(time, hr_val)

fitbitデータにアクセスしてみる(1日の心拍数をダウンロードするサンプル)

自分へのクリスマスプレゼントにfitbit Charge2 を購入。1週間の毎朝のインターバル速歩をしてみてデータもたまったので、グラフにしたり遊んでみたい。

Windows で Fitbit の心拍数データを取得する - もみあげあしめ を参照しながら、まずは1分単位の心拍数をとってみるところから。

まず環境整備。

まずはサンプルそのままを試してみる。まあ、動くのは当たり前。コードをざっと見てみる。

2017/1/11 追記:

唯一のハマり場所は、fitbitサイトにCallback URLを登録するときに、"http://127.0.0.1:8080/" の末尾の "/" を忘れないこと。これを忘れるとリダイレクトに失敗した、というわけのわからないエラーが fitbit サイトアクセス時に出て、理由解明に苦しむ。

cherrypyは軽量Webフレームワークなので、ある意味ローカルPCに一瞬Webサーバーを立てて Webサービス間のやりとりして fitbit.comにアクセスしたわけだ。

PythonのWebフレームワーク「CherryPy」 入門用にも適した、枯れた名品 - モジログ

なるほど。あとでWebサービスを試してみるときには、これくらいシンプルなフレームワークからスタートすると試しやすいので覚えておく。

まずは oauth2 まわりのコードはこれで手に入ったので、ここからは欲しい fitbit のデータを取得してみよう。

Windows10を高速化できるか

まず、ざっとやってみる

今日はWindows10 PCがあまりにも遅くなってきたので、世の中に書かれている「高速化」をしてみる。

まず、初期状態。そもそも、先ほど購入してインストールしたセキュリティソフトESETがディスクスキャンしているので、初期状態として不利だが。

f:id:ken_miyashita:20160917112623p:plain

以下のサイトを参考に、Windows Defender を無効にする。

「何だか動作が重い・・・」そんなときに効く!【Windows 10】でWindows Defenderを完全無効化する方法! | ボクシルが運営する法人向けクラウドサービス紹介メディア ボクシルマガジン!

リブートしてみた。まあ、予想どおり変わらない。ESETがあるから無効化しておくことは必要なので、よし。

f:id:ken_miyashita:20160917115528p:plain

次は、余計なサービスを停止。以下のサイトを参考にする。たぶん Windows Searchを止めたのが一番きくと期待。

Windows10を爆速仕様にする高速化設定方法 | WebEssentials

たしかにCPUの負荷は下がったように見える。が、因果関係の検証が困難。今はHDDアクセスが重すぎて全体的に反応がよろしくない。

f:id:ken_miyashita:20160917123407p:plain

ESETによるディスクのフルスキャンが終わった後でも、リブートするとまたディスクアクセスをしまくり100%となる。 これは正しいのか?

下図のように「サービスホスト」というデーモンなのだが、どうもなぜここまでHDDアクセスが頻発する?

f:id:ken_miyashita:20160917161502p:plain

いや、5分弱でやっとHDDアクセスは収まった。

さらにいろいろやってみる

以下のサイトに従い、高速起動をやってみる。

Windows 10を高速化する8つの裏技 「こんなものか」とあきらめないで - ライブドアニュース

「バックグラウンドアプリ」も無効化しておいた。たしかにたくさんのバックグランドが走っていた。

One Drive も起動しないようにした。

f:id:ken_miyashita:20160917163343p:plain

スタートメニュー>設定>通知と操作 から、いろいろなアプリの通知を消した。

Windowsの視覚効果をOFFにした。

メモリサイズの最適化: 初期サイズ、最大サイズとも推奨にしたがって 1396MB とする。大丈夫か?

結局...

いくつか試してみたのだが、どうも体感速度が明らかによくなった、ということは起きない。

残念....

Webカメラで遊ぶための環境セットアップ まとめ

ここまでで、RPi + python2.7 + Webカメラ + OpenCV + VNC という環境ができた。 いつ SDカードが吹っ飛んでもいいように、環境構築方法のまとめをしておく。

基本環境

参考: ラズパイ マガジン 2016年 6月号

書かれているとおりにセットアップ。

  • OS : Jessie
  • ネットワーク : Wi-Fi (DHCP)
  • python : 2.7
  • TightVNC は導入しない。後述の x11vnc を利用しないと OpenCV のウインドウが VNC 経由で見えないため。

VNC

参考: Raspberry Piでリモート操作できるようにするメモ – 1ft-seabass.jp.MEMO

参考: Sakura/VPS/x11vnc - Home Server Journal

vnc server をインストール。

$ sudo apt-get install x11vnc
$ x11vnc -storepasswd

常時起動させる。また、キーリピートをきちんと効かせる。

/home/pi/.config/autostart/x11vnc.desktop 設定ファイルを作成し、以下のように記述。

[Desktop Entry]
Encoding=UTF-8
Type=Application
Name=X11VNC
Comment=
Exec=x11vnc -repeat -usepw -forever -display :0 -ultrafilexfer
StartupNotify=false
Terminal=false
Hidden=false

vnc 接続をした際の画面サイズを指定。

/boot/config.txt の中で、以下の2行がコメントアウトされているので、有効化する。

framebuffer_width=1280
framebuffer_height=720

RPi と Windowsクリップボード共有

参考: raspbian - TightVNC copy/paste between local OS and Raspberry Pi? - Raspberry Pi Stack Exchange

autocutsel をインストール。

$ sudo apt-get install autocutsel

autocutsel を X server と同時に起動。

/home/pi/.vnc/xstartup に以下のように1行追加。

#!/bin/sh

xrdb $HOME/.Xresources
xsetroot -solid grey
autocutsel -fork
#x-terminal-emulator -geometry 80x24+10+10 -ls -title "$VNCDESKTOP Desktop" &
#x-window-manager &
# Fix to make GNOME work
export XKL_XMODMAP_DISABLE=1
/etc/X11/Xsession

Webカメラ

参考: (35連休)4日目:Raspberry Pi(OpenCVのインストール〜リアルタイム顔認識) - hsuetsugu’s diary

USBポートにWebカメラを接続すれば自動認識される。以下のコマンドでUSB認識されたことの確認、およびカメラ映像の確認。

$ lsusb
Bus 001 Device 004: ID 046d:0825 Logitech, Inc. Webcam C270
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp. 
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
$ sudo apt-get install luvcview 
$ luvcview

OpenCV

参考: (35連休)4日目:Raspberry Pi(OpenCVのインストール〜リアルタイム顔認識) - hsuetsugu’s diary

OpenCV をインストール。

$ sudo apt-get install libopencv-dev
$ sudo apt-get install python-opencv