Google OS実験室 ~Moonlight 明日香~

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

センシング

FLIR ONEで遊んでみる(3)

今回は, FLIR ONEを使って, 写したものの温度を測定してみようと思う.

2. 温度測定[1][2][3]
ImageTypeに"ThermalRadiometricKelvinImage”を指定すると"Radiometric centikelvin(cK) tmperature data"が取得できる.
セルシウス温度tとそれに等しい絶対温度Tとの間には以下の関係があり, 温度(℃)を求めることができる.
 t/℃ = T/K - 273.15

(1) ImageTypeの設定
[コード]
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_preview);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        mOverlayView = (OverlayView)findViewById(R.id.overlay_view_id);
        mThermalView = (ImageView)findViewById(R.id.thermal_view_id);
        mThermalView.setRotation(180.0f);
        RenderedImage.ImageType defImageType = RenderedImage.ImageType.BlendedMSXRGBA8888Image;
        RenderedImage.ImageType kelvinlImageType = RenderedImage.ImageType.ThermalRadiometricKelvinImage;
        mFrmProcessor = new FrameProcessor(this, this, EnumSet.of(defImageType, kelvinlImageType));
    }

(2) 温度に変換
[コード]
    public void onFrameProcessed(RenderedImage renderedImage) {
        Log.i(LOG_TAG, "Frame processing!");

        if (renderedImage.imageType() == RenderedImage.ImageType.BlendedMSXRGBA8888Image) {
            mThermalBitmap = renderedImage.getBitmap();
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if (mThermalView != null) {
                        mThermalView.setImageBitmap(mThermalBitmap);
                    }
                }
            });
        }
        else if (renderedImage.imageType() == RenderedImage.ImageType.ThermalRadiometricKelvinImage) {
            calcTemperature(renderedImage);  // 画像中央の温度計算

            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if (mOverlayView != null) {
                        mOverlayView.drawTemperature(mOverlayView.getWidth()/2, mOverlayView.getHeight()/2, mAveTemperature);
                    }
                }
            });
        }
    }

    // 画面中央の10x10エリアの平均温度
    private void calcTemperature(RenderedImage renderedImage) {
        final int AREA_SIZE = 10;
        int width = renderedImage.width();
        int x0 = (width - AREA_SIZE) / 2;
        int y0 = (renderedImage.height() - AREA_SIZE) / 2;

        double averageTemp = 0.0;
        short[] shortPixels = new short[renderedImage.pixelData().length / 2];
        ByteBuffer.wrap(renderedImage.pixelData()).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(shortPixels);
        for (int y = y0; y < y0 + AREA_SIZE; y++) {
            for (int x = x0; x < x0 + AREA_SIZE; x++) {
                // 注) データはunsigned shortなのでintに変換して加算
                averageTemp += (int)(shortPixels[width*y + x] & 0xffff);
            }
        }
        averageTemp /= (AREA_SIZE * AREA_SIZE);
        mAveTemperature = averageTemp / 100 - 273.15;
    }


[実行結果]
 - Nexus 7 (2013) / Android 4.4.4

flir04

flir03

とりあえず, 画面中央の位置の物の表面温度を測定できるようになった.
ただ, この表示温度がどこまで正確な温度かは別の問題である.
正しく温度を測定するためには, 測定する物質に合わせて放射率(Emissivity)を正しく設定するなど, いろいろと条件設定が必要そうだ...


----
参照URL:
[1]
ケルビン - Wikipedia
[2] FLIR One SDK Documentation
[3] FLIR One Software Development Kit

FLIR ONEで遊んでみる(2)

FLIR Oneを使ったアプリを考える前に, FLIR Oneについてもう少し理解を深めてみよう.

まずは, FLIR Oneのドキュメント[1]やサンプルコード[2]をベースに, 基本的な機能(サーマル画像表示, 温度測 - 定など)を実装し, FLIR Oneの基本的な動きを確認してみる.

1. サーマル画像表示
基本的な処理の流れは以下の通り.
flir02
[手順]
1) onCreateメソッド
 - 画像タイプを指定して, FrameProcessorのインスタンスを取得する.
2) onResumeメソッド
 - Device#startDiscoveryメソッドで, FLIR Oneとの接続を開始する.
  注) 例外処理(try-catch)をすること.
3) onStopメソッド
 - Device#stopDiscoveryメソッドで, FLIR Oneからのリスニングを中止し接続を停止する.
  注) stopDescoveryメソッド内で, closeメソッドも実行される.
4) onDeviceConnectedメソッド
 - デバイス情報を保持する.
 - Device#startFrameStreamメソッドで,On ストリーミングを開始する.
5) onDeviceDisconnectedメソッド
 - チューニング状態をUnknownにする.
 - Device#stopFrameStreamメソッドで, ストローミングを停止する.
6) onFrameReceivedメソッド
 - チューニング状態がInProgressでない場合に, FrameProcessor#processFrameメソッドで, フレームデータを処理する.
7) onFrameProcessedメソッド
 - RenderImage#getBitmapメソッドで, ビットマップ画像を取得する.
 - ImageView#setImageBitmapメソッドで, ビットマップ画像をビューにセットし, 画像を表示させる.
  注) UI表示はrunOnUiThread内で行うこと.

[コード]
package com.moonlight_aska.android.vision.flironedemo;

import android.graphics.Bitmap;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.ImageView;
import android.widget.Toast;

import com.flir.flironesdk.Device;
import com.flir.flironesdk.Frame;
import com.flir.flironesdk.FrameProcessor;
import com.flir.flironesdk.RenderedImage;

import java.util.EnumSet;

public class PreviewActivity extends AppCompatActivity
        implements Device.Delegate, Device.StreamDelegate, FrameProcessor.Delegate {
    private static final String LOG_TAG = PreviewActivity.class.getSimpleName();

    private ImageView mThermalView = null;  // 表示用View
    private volatile Device mDevice = null;  // Flir Oneデバイス
    private FrameProcessor mFrmProcessor = null;  // フレーム処理プロセッサ
    private Device.TuningState mTuningState = Device.TuningState.Unknown;    // チューニング状態
    private Bitmap mThermalBitmap = null;   // 描画用ビットマップ画像

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_preview);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        mThermalView = (ImageView)findViewById(R.id.thermal_view_id);
        mThermalView.setRotation(180.0f);  // 表示上下反転
        RenderedImage.ImageType defImageType = RenderedImage.ImageType.BlendedMSXRGBA8888Image;  // Thermal + Visual画像
        mFrmProcessor = new FrameProcessor(this, this, EnumSet.of(defImageType));
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_preview, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

   @Override
    protected void onResume() {
        super.onResume();
        try {
            Log.i(LOG_TAG, "onPesume, starting discovery!");
            Device.startDiscovery(this, this);
        } catch(IllegalStateException e) {
            Log.e(LOG_TAG, "onPesume, startDiscovery() error!");
            e.printStackTrace();
        }
    }

    @Override
    protected void onStop() {
        Log.i(LOG_TAG, "onStop, stopping discovery!");
        Device.stopDiscovery();

        super.onStop();
    }

    @Override
    public void onTuningStateChanged(Device.TuningState tuningState) {

    }

    @Override
    public void onAutomaticTuningChanged(boolean b) {

    }

    @Override
    public void onDeviceConnected(Device device) {
        Log.i(LOG_TAG, "Device connected!");

        mDevice = device;
        mDevice.startFrameStream(this);  // ストリーミング開始
        // Debug
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(getApplicationContext(), "Device connected.", Toast.LENGTH_LONG).show();
            }
        });
    }

    @Override
    public void onDeviceDisconnected(Device device) {
        Log.i(LOG_TAG, "Device disconnected!");

        mTuningState = Device.TuningState.Unknown;
        if (mDevice != null) {
            mDevice.stopFrameStream();
            mDevice = null;
            // Debug
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(getApplicationContext(), "Device disconnected.", Toast.LENGTH_LONG).show();
                }
            });
        }
    }

    @Override
    public void onFrameReceived(Frame frame) {
        Log.i(LOG_TAG, "Frame received!");

        if (mFrmProcessor != null) {
            if (mTuningState != Device.TuningState.InProgress) {  // チューン中でない
                mFrmProcessor.processFrame(frame);
            }
        }
    }

    @Override
    public void onFrameProcessed(RenderedImage renderedImage) {
        Log.i(LOG_TAG, "Frame processing!");

        if (renderedImage.imageType() == RenderedImage.ImageType.BlendedMSXRGBA8888Image) {
            mThermalBitmap = renderedImage.getBitmap();
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if (mThermalView != null) {
                        mThermalView.setImageBitmap(mThermalBitmap); // 画像表示
                    }
                }
            });
        }
    }
}

[実行結果]
 - Nexus 7(2013) / Android 4.4.4

flir01

一応, サーマル画像を表示できるようになった.
次は, 温度表示を行ってみようと思う.

----
参照URL:
[1] FLIR One SDK Documentation
[2] FLIR One Software Development Kit

FLIR ONEで遊んでみる(1)

FLIR ONEいうスマフォ用サーマルカメラに触れる機会があったので, 少し遊んでみた.
ちなみに, FLIRとは Foward Looking Infra-Red の略で, 軍用飛行機に搭載される「赤外線前方監視装置」のことである.



1. FLIR ONEを試す
Google Playから「FLIR ONE」というアプリをNexus7にインストールし, サーマルカメラを接続してみた.

端末情報:
 - Nexus 7 (2013) / Android 4.4.4

パソコンのディスプレイを写してみると...

Thermal0

中央部分の温度を測定することができる.
掌の温度が35.0℃ということで, それなりに温度が測定できているようだ.

thermal04

また, 一覧でいろいろな表示方法を確認することができる.

thermal03

2. SDKに触れる[1]
結構面白そうだが, 自分でアプリを作れるのだろうか?

少し調べてみると, FLIR One Developerのサイトでユーザ登録すると, DOWNLOAD THE SDKというところから SDKをダウンロードできる.
SDKには, ライブラリ(flironesdk.aar), ドキュメントの他にサンプルアプリが含まれていたので, さっそくビルドしてみた.

ビルド環境:
 - Android Studio 1.4.1
  - Andorid SDK Build-Tools 21.1.2
  - Target SDK Version 21

ビルド環境に合わせて, build.gradleを若干修正したが, 意外と簡単にビルドが通った.

さっそくサンプルアプリを動かしてみる.

thermal05

thermal06

サンプルアプリは, 単にサーマル画像を表示するだけでなく, 表示や色の切り替えなどライブラリの基本的な使い方がいろいろと実装されていそうだ.

これをベースにすれば, 何か面白いことが割と簡単にできそうな気がする.

----
参照URL:
[1] FLIR One Developer | Developer community  for FLIR One thermal imaging device


脈拍センシングにチャレンジ(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

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

記事検索



  • ライブドアブログ