Google OS実験室 ~Moonlight 明日香~

GoogleのAndroidで遊び始めて, すでに6年以上が経った. Androidは思った以上の発展を遂げている. この技術を使って, 新しいことにチャレンジだ!!

アプリ

脈拍センシングにチャレンジ(3)

前回, FFTを使って脈波データから脈拍数を求められそうと記したが, 生体信号の分野は素人なのでいろいろと調べていると, 私設研究所ネオテックラボ[1]の上田氏が, 以下のように記している. [2]

   脈拍は非定常な安定しない波です. 脈拍に限らず生体の信号は全部周期が定常ではありません. 
   ですから『FFT(Fast Fourier Transform)の解析で周波数成分を抽出する』という考え方は全く無意味な行為であり, 正しい結果が得られません.
   何故ならFFTは同じ状態が永遠に継続する事を前提とした変換であるからです.


そこで, 脈拍数をFFTを使って求めることはやめ, 脈波のピーク抽出を行い, ピーク間隔から脈拍数を算出することを検討してみる.

1. ボトム間隔
 カメラの輝度変化は血管拡張時に入射光が少なくなり暗くなるので, 脈波データのピーク間隔ではなく, ボトム間隔から脈拍数を算出する.

sensor01

ボトム間隔から脈拍数を求める式は, 以下の通り.
        
  脈拍数 = ( FPS * 60 ) / Tf        
  ただし,  FPS : フレーム/秒, Tf : ボトム間フレーム数

FPS=30として, 各ボトム間隔から脈拍数を計算すると, 以下のようになる.

間隔[frame]脈拍数
12475
22475
32378.3

2. ボトム位置抽出
 脈波データからボトム位置を抽出するために, まず矩形波相関フィルタ[3]を使って脈波データのゆらぎの影響を抑え, ボトム位置を検出する.
矩形波相関フィルタを使用することによりボトム位置の時間的ずれが生じるが, 今回は同期位置検出とかではなく脈拍数の算出であり, 特に問題ない.

2.1 相互相関処理
 具体的には, 輝度値に対して, 以下のような矩形パルス窓を使って, まずは相互相関値を求める.
window01

輝度値から相互相関値を求める式は, 以下の通り.

  y(t) = (-1) * (x(t) + x(t-1) + ・・・ + x(t-n+1))
       + (+1) * (x(t-n) + x(t-n-1) + ・・・ + x(t-3n+1) + (-1) * (x(t-3n) + x(t-3n-1) + ・・・ + x(t-4n+1))
  ただし, y(t) : 相互相関値, x(t) : 輝度値, n : 1/4窓幅のフレーム数

実際に測定値でやってみると, 以下の通り.

window02

2.2 ボトム位置検出
 ボトム位置は, とりあえず相互相関値で以下の条件を満たすフレーム位置を探索する方法で試してみるが, 実際にいろいろなデータで試してみる必要がある.

   y(t) - y(t-1) > 0 && y(t-1) - y(t-2) ≦ 0

上記で検出されたボトム位置間隔から脈波数を求める.

次回はこれを実際にAndroid上に実装して動かしてみる.
安定して脈波データが取得できる場合は, 多分これで脈拍数の測定が可能と思われる.
ただ, 実際にはスマフォのカメラに指をあてて輝度を測定するので, 常に安定して脈波データが取得できるわけではない.

----
参照URL:
[1] 私設研究所ネオテックラボ
[2] 【俺センシング】『PCのカメラで非接触バイタル・センシングができる』
[3] 特公平7-67440

脈拍センシングにチャレンジ(2)

今回は, カメラで撮影した指の映像から心拍数が正しく求められるか, オムロンの血圧計での測定とカメラでの指の撮影を交互に5回行い, カメラで撮影した映像から心拍数を算出し比較してみた.

1. 輝度変化
 まずは, カメラで撮影した画像から安定して輝度の変化が得られるか調べてみた.
5回の撮影データについて, R/G/Bプレーン毎に輝度変化を調べてみた.

graph01
graph02
graph03
その結果, 測定(1)や(2)のようにR/G/Bすべて安定して脈波が観測できる場合と, 測定(3)や(5)のようにG以外はあまり脈波が観測できない場合があった.
そこで, 心拍数の計測にはGプレーンの輝度変化を用いることにした.

2. 周波数分析
 FFT(高速フーリエ変換)を用いて輝度変化の周波数分析を行い, 心拍数が正しく求められるか確認してみる.
心拍変化は0.8Hz~3Hz程度なので, バンドパスフィルタにより分析する周波数帯域を制限してからFFTすべきだが, 簡易的に差分(y[n] = x[n]-x[n-1])と移動平均(y[n]=(x[n]+x[n-1])/2)を使用することにする.
*注) FFTにはAkiyama氏のFFT-PLOT(フリー版) [1]を使用.
graph03
graph04

FFT
解析サンプル数128256512
基本周波数(Hz)1.4171.2941.350
心拍数85.077.681.0

波形データを目視でチェックしたところ80拍だったので, FFTの解析サンプル数は心拍数が最も近い512を採用する.

3. 測定結果
5回の測定結果は以下の通り.

回数目視カメラオムロン
1808183
2787474
3828175
4818175
5828176

波形データの目視チェックによる心拍数とカメラによる心拍数はかなり近い値となったが, オムロンでの測定値とは少しずれがあった.
一応, 心拍数が測れそうなので, この考え方をベースにプログラミングすることにする.
また, オムロンの測定値とのずれは, 同時測定や他の機器との比較などして引き続き検証はやっていく予定.

----
参照URL:
[1] 高速フーリエ変換(FFT-PLOT)

 

脈拍センシングにチャレンジ(1)

最近, ヘルスケア関連の業務をするようになり, 血圧, 脈拍や心電図などの測定に少し興味を持つようになってきた.
いろいろと調査している中で, 例えば「
Instant Heart Rate」のように, AndroidやiPhoneのカメラを使って心拍数を測定するようなアプリもいくつか出てきている.

そこで, スマフォのカメラを使ってどのように心拍(脈拍)数を測定しているか調べてみた.


1. 脈拍計測の原理[1]
 脈拍計測には, 血液中のヘモグロビンが光を吸収するという性質を利用しているようで, 血管が収縮しているときはヘモグロビンが少なく受光素子への入射光が多くなり, 血管が拡張しているときはヘモグロビンが多く受光素子への入射光が少なくなる.
 この入射光の変化から血中のヘモグロビン量の変化を測定することができ, 脈拍数を求めることができる.

sensor01
                出典:EPSONのHP[1]

 スマフォアプリも同じような原理で測定しており, LEDの光とカメラを使って輝度の変化から脈拍数を測定しているようである.

2. 脈波の測定
Androidのカメラを使って, 指を撮影すると, 以下のように見える.



映像を見ると, 鼓動に合わせて多少明るさが変化しているのがわかる.
そこで, プレビュー画像の中央付近の輝度を測定するプログラムを作成し, 輝度の変化を観測してみた.

sensor01

一応脈波のような波形が取得できた.

[コード]
package com.moonlight_aska.android.preview01;

import java.io.IOException;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.PorterDuff.Mode;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.os.Build;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class CameraView extends SurfaceView 
    implements SurfaceHolder.Callback, Camera.PreviewCallback {
  private static final int PREVIEW_WIDTH = 640;
  private static final int PREVIEW_HEIGHT = 480;
  private static final int FRAME_WIDTH = 50;
  private static final int SCALE = 20;
  private static final int VIEW_POINTS = 200;
  private static final float PEN_WIDTH = 3.0F;
  private int mBufSize;
  private float[] mVal;
  private int mSamples;
  private int mTop;
  private int mStep;
  private Camera mCamera = null;
  private SurfaceHolder mHolder = null;
  private SurfaceTexture mSurfaceTexture = null;
  private Paint mLinePaint = new Paint();
 
  public CameraView(Context context) {
    super(context);
        
    mLinePaint.setStyle(Style.STROKE);
    mLinePaint.setColor(Color.GREEN);
    mLinePaint.setStrokeWidth(PEN_WIDTH);
    mHolder = getHolder();
    mHolder.addCallback(this);
  }
   
  public void surfaceCreated(SurfaceHolder holder) {
    // TODO Auto-generated method stub
    mBufSize = getWidth();
    mVal = new int[mBufSize];
    mStep = (mBufSize - FRAME_WIDTH*2) / VIEW_POINTS;
       
    // カメラオープン
    mCamera = Camera.open();
    try {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        mSurfaceTexture = new SurfaceTexture(0);
        mCamera.setPreviewTexture(mSurfaceTexture);
      }
      else {
        mCamera.setPreviewDisplay(null);
      }
    } catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }
    
  public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
    // TODO Auto-generated method stub
    stopPreview();
    // プレビュー画面のサイズ設定
    Camera.Parameters params = mCamera.getParameters();
    params.setPreviewSize(PREVIEW_WIDTH, PREVIEW_HEIGHT);
    params.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
    mCamera.setParameters(params);
    // プレビュー開始
    mTop = 0;
    mSamples = 0;
    startPreview();
  }
    
  public void surfaceDestroyed(SurfaceHolder holder) {
    // TODO Auto-generated method stub
    stopPreview();
    mCamera.release();
    mCamera = null;
  }
   
  @Override
  public void onPreviewFrame(byte[] data, Camera camera) {
    // TODO Auto-generated method stub
    mVal[mSamples%mBufSize] = calcLuminance(data);
    mSamples++;
    drawLuminance();
  }
  
  // プレビュー開始
  private void startPreview(){
    mCamera.setPreviewCallback(this);
    mCamera.startPreview();
  }

   
  // プレビュー停止
  private void stopPreview(){
    mCamera.setPreviewCallback(null);
    mCamera.stopPreview();
  }
    
  // 輝度値の計算
  private float calcLuminance(byte[] data) {
    float sumVal = 0.0f;
    int cnt = 0;

    for (int y=PREVIEW_HEIGHT/4; y<PREVIEW_HEIGHT*3/4; y++) {
      for (int x=PREVIEW_WIDTH/4; x<PREVIEW_WIDTH*3/4; x++) {
        sumVal += (float)(data[y*PREVIEW_WIDTH+x] & 0xff);
        cnt++;
      }
    }
    sumVal /= cnt;
    return sumVal;
  }
    
  // 輝度グラフの描画
  private void drawLuminance() {
    int idx, idx_1;
  
    Canvas canvas = mHolder.lockCanvas();
    if (canvas != null) {
      canvas.drawColor(0, Mode.CLEAR);
      if (mSamples >= VIEW_POINTS) {
        mTop++;
      }
      for (int i=mTop, x=0; i<mSamples-1; i++, x++) {
        idx = i % mBufSize;
        idx_1 = (i+1) % mBufSize;
        canvas.drawLine(x*mStep+FRAME_WIDTH, (mVal[idx]-80.0)*SCALE, (x+1)*mStep+FRAME_WIDTH, (mVal[idx_1]-80.0)*SCALE, mLinePaint);
      }
      mHolder.unlockCanvasAndPost(canvas);
    } 
  }
}

観測されたデータの周期性を調べることで, 脈拍数を求めることができる.
脈拍数の求め方については, 次回以降に.....

----
参照URL:
 [1]
腕で測る! 高精度脈拍計測技術 | 技術・イノベーション | EPSON

WebAPIにチャレンジ中!!

Web上にはGoogle MapをはじめさままざまなWebサービスがあり, これらのWebサービスをアプリで活用できればアプリのアイデアもグッと広がりそうである.
これまで作成したアプリは端末単体アプリがほとんどでWebAPIをちゅんと使ったことがなかったので, Webサービスから得られるデータをどのように処理すべきかよくわかっていなかった.
そこで, 特にXMLやJSONの解析について少し勉強しようと, 「Google Android Web APIプログラミング入門」[1][2]って本を参考にチャレンジ中である.



中身は, 以下の通り.
 - Chapter 1 はじめに
 - Chapter 2 WebAPI入門
 - Chapter 3 Google Chart APIを使ったアプリケーションを作成する
 - Chapter 4 Social Feedback(GREE)を使ったアプリケーションを作る
 - Chapter 5 空き室検索アプリケーションを作る
 - Chapter 6 駅情報検索アプリケーションを作る
 - Chapter 7 YouTube Data API
 - Chapter 8 Twitter 4j
 - Chapter 9 Facebook SDK
 - Chapter 10 広告配信ライブラリ

私も, この本のサンプルを試しながら読み進めているが, 一部サンプルコードに不具合があるようなので, 同じように勉強されている方のために少しメモを残しておく. (typoもいくつかあるがそれは省略)

1. Chapter 5
1.1  5-6-4 "Activityから非同期処理を実行し, 画面に表示する"
5-6-4でこれまでのコードを実行しようとビルドに失敗するので, 5-5-2 ”レスポンスデータ用のオブジェクトを定義する"の以下の箇所を修正する.
1) p91:Plan.javaのStayに関する記述
   public class Plan {
     private String planName;    // プラン名
       (省略)
     private String rateType;    // 料金タイプ
     private int sampleRate;     // 参考料金
     
private Stay stay;     // 宿泊日情報
     
private ArrayList<Stay> stay;   // 宿泊日情報
     private Hotel hotel;     // ホテル情報

     
public Stay getStay() {
       
return stay;
     
}
     public ArrayList<Stay> getStay() {
       return stay;
     }

     
public void setStay(Stay stay) {
       
this.stay = stay;
     
}
     public void setStay(ArrayList<Stay> stay) {
       this.stay = stay;
     }

     public Hotel getHotel() {
       return hotel;
     }

  原因は, p97のonStartTagメソッド内のmPlan.setStay(new ArrayList<Stay>());との齟齬.

2) p92:RoomSearchResult.javaのPlanに関する記述.
   public class RoomSearchResults extends BaseData {
     private int numberOfResults;  // 該当件数
     private int displayPerPage;   // 表示件数
     private int displayFrom;   // 表示From
     private String apiVersion;   // APIバージョン
     
private Plan plan;    // 料金プラン情報
     
private ArrayList<Plan> plan;  // 料金プラン情報
 
     public Plan getPlan() {
       
return plan;
     
}
     public ArrayList<Plan> getPlan() {
       return plan;
     }
     
public void setPlan(Plan plan) {
       
this.plan = plan;
     
}
     public void setPlan(ArrayList<Plan> plan) {
       this.plan = plan;
     }

     public int getNumberOfResults() {
        return numberOfResults;
     }

  原因は, p98のonEndTagメソッド内のmPlan.setStay(new ArrayList<Stay>());との齟齬.

引き続き, 大きな不具合等が見つかった場合, ここに追記していく予定!!

この本は, 具体例をあげて分かりやすく解説してあるので, WebAPIを勉強したいという方にはお薦めである.
ただ, Androidのプログラミングがある程度できることを前提に書かれているので, 初心者の方には少し難しいかも...

----
参照URL:
 [1] Google Android WebAPI プログラミング入門(秀和システム)
 [2] T.Yokoyamaのブログ


livedoor プロフィール
アクセスカウンター
  • 今日:
  • 昨日:
  • 累計:

記事検索



  • ライブドアブログ