2012年5月19日土曜日

モーションイベントサンプル3 - CoreMotionを使った各種センサーの利用

前回は「モーションイベントサンプル2 - UIAccelerometerを使った傾き検知」でしたが、「Deprecated in iOS 5.0」となっています。そこで今回は、iOS 4.0から用意されている「Core Motion」を利用します。
iOS Developer Libraryの「iOSイベント処理ガイド」(英語版はEvent Handling Guide for iOS)に沿って、サンプルを公開します。なお、日英版のどちらにも「磁力センサー」の利用について書かれていませんが、ここでは記述します。

注意点

今までのイベント処理では、シングルトンを利用してきましたが、Core Motionでは自分でインスタンスを生成することになります。ですが、複数のインスタンスを生成するとせっかくのデータにもズレが生じることがあります。そのため、インスタンスはひとつだけにすることが推奨されています。

Core Motion【iOS 4.0以降】

Core Motionには以下のクラスがあり、複数のセンサーが同時に扱えるようになっています。もちろん、データを取得するためには取得したいセンサーが内蔵されているデバイスでなければなりません。(各種デバイスの対応表については「iOSデバイス間の互換性まとめ」をご覧ください。)
クラス名概要対応OS
CMAccelerometerData加速度センサーのデータにアクセスできる。前回のUIAccelerometerDelegateの代わりとなります。X軸・Y軸・Z軸がどの方向に移動したかが分かります。iOS 4.0以降
CMGyroDataジャイロスコープのデータにアクセスできる。X軸・Y軸・Z軸を中心にしてどの方向に"回転"したかが分かります。iOS 4.0以降
CMMagnetometerData磁力センサーのデータにアクセスできる。X軸・Y軸・Z軸で計測される磁力の強さが分かります。iOS 5.0以降
CMDeviceMotion上記3つの、加速度センサー・ジャイロスコープ・磁力センサーのデータを統合して加工してくれます。iOS 4.0以降
CMAttitudeCMDeviceMotionに含まれるもので、デバイスの姿勢に関する情報をオイラー角(ロール・ピッチ・ヨー)や行列(マトリックス)の形にしてくれます。iOS 4.0以降
CMLogItemセンサーからの情報がいつ取得されたものかを示すタイムスタンプ。デバイスの起動時刻からの差分なので使い所は不明です。iOS 4.0以降
CMMotionManager上記のクラスを管理するクラス。このクラスを起点に様々なデバイスの情報を取り出します。iOS 4.0以降
各クラスの関係図

加速度センサー・磁力センサーで得られる情報の向きジャイロスコープで得られる情報の向き
Attitudeから得られるオイラー角。
pitch:縦方向、roll:横方向、yaw:デバイスの向きとなっている。


センサーからのデータ取得方法

センサーは定期的に情報を取得し続けるのですが、その情報をアプリケーションから利用するためには以下の2つの方法があります。
  1. プル型
    • センサーから取得されたデータを利用する為には、定期的に値を参照するようなプログラムを書かなければなりません。ドキュメントではこちらが推奨されています。しかし、OpenGLなどを使わない限りは、下のプッシュ型の方が使いやすいです。
  2. プッシュ型
    • 特定のハンドラを渡すと、センサーの値が更新される度にそのハンドラを実行してくれるものです。ハンドラはiOS 4.0から導入されたブロック構文となっています。

以下、利用方法について例を示しますが、いずれも共通部分は以下の通りです。
  1. 「CoreMotion.framework」を追加
  2. 必要に応じて、<project-name>-Info.plistファイルにキー「accelerometer」「gyroscope」「magnetometer」を追加する
    • ただし、例えセンサーが無くても他の機能で補うことができ、アプリケーションが正常に動作する場合には追加すべきではありません。(UIRequiredDeviceCapabilitiesについての詳細は「iOSデバイス間の互換性まとめ」をご覧下さい。)
  3. ヘッダの読み込み
    #import <CoreMotion/CoreMotion.h>
  4. インスタンスを生成し、適切な取得間隔の設定をして各種センサーを有効化
    イベント頻度(Hz)使用方法
    10–20デバイスの現在の向きを表すベクトルを確認するのに適しています。
    30-60ゲーム、またはリアルタイムユーザ入力用に加速度センサーを使用するその他のアプリケーションに適しています。
    70-100高い頻度でモーションを検出する必要があるアプリケーションに適しています。たとえば、この間隔を使って、ユーザがデバイスをたたいたり高速でゆすったりすることを検出する場合が考えられます。
  5. 各種センサーからのデータを取得
    • 上述のように、プル型とプッシュ型がある
  6. (不必要になったら)各種センサーを無効化

加速度センサー(Accelerometer)【iOS 4.0以降】

// インスタンスの生成
CMMotionManager *manager = [[CMMotionManager alloc] init];

// 現在、加速度センサー無しのデバイスは存在しないが念のための確認
if (manager.accelerometerAvailable) {
    // センサーの更新間隔の指定
    manager.accelerometerUpdateInterval = 0.01;  // 100Hz

    // ハンドラを指定
    CMAccelerometerHandler handler = ^(CMAccelerometerData *data, NSError *error) {
        double timestamp = data.timestamp;  // 更新時刻
        double x = data.acceleration.x;  // X軸: 加速度G
        double y = data.acceleration.y;  // Y軸: 加速度G
        double z = data.acceleration.z;  // Z軸: 加速度G

        // x, y, zの値を必要に応じて、ローパス・ハイパスなどのフィルタを適用する
    };

    // センサーの利用開始
    [manager startAccelerometerUpdatesToQueue:[NSOperationQueue currentQueue] withHandler:handler];
    
    // (不必要になったら)センサーの停止
    if (manager.accelerometerActive) {
        [manager stopAccelerometerUpdates];
    }
}

ジャイロスコープ(Gyroscope)【iOS 4.0以降】

// インスタンスの生成
CMMotionManager *manager = [[CMMotionManager alloc] init];

// ジャイロスコープ無しのデバイスもあるため、センサーの有無を確認
if (manager.gyroAvailable) {
    // センサーの更新間隔の指定
    manager.gyroUpdateInterval = 0.01;  // 100Hz
    // ハンドラを指定
    CMGyroHandler handler = ^(CMGyroData *data, NSError *error) {
        double timestamp = data.timestamp;  // 更新時刻
        double x = data.rotationRate.x;  // X軸: ラジアン/秒
        double y = data.rotationRate.y;  // Y軸: ラジアン/秒
        double z = data.rotationRate.z;  // Z軸: ラジアン/秒

        // x, y, zの値を適宜加工して利用する
    };

    // センサーの利用開始
    [manager startGyroUpdatesToQueue:[NSOperationQueue currentQueue] withHandler:handler];
        
    // (不必要になったら)センサーの停止
    if (manager.gyroActive) {
        [manager stopGyroUpdates];
    }
}

磁力センサー(Magnetometer)【iOS 5.0以降】

// インスタンスの生成
CMMotionManager *manager = [[CMMotionManager alloc] init];

// 磁力センサー無しのデバイスもあるため、センサーの有無を確認
if (manager.magnetometerAvailable) {
    // センサーの更新間隔の指定
    manager.magnetometerUpdateInterval = 0.01;  // 100Hz

    // ハンドラを指定
    CMMagnetometerHandler handler = ^(CMMagnetometerData *data, NSError *error) {
        double timestamp = data.timestamp;  // 更新時刻
        double x = data.magneticField.x;  // X軸: マイクロテスラ
        double y = data.magneticField.y;  // Y軸: マイクロテスラ
        double z = data.magneticField.z;  // Z軸: マイクロテスラ

        // x, y, zの値を適宜加工して利用する
    };

    // センサーの利用開始
    [manager startMagnetometerUpdatesToQueue:[NSOperationQueue currentQueue] withHandler:handler];
        
    // (不必要になったら)センサーの停止
    if (manager.magnetometerActive) {
        [manager stopMagnetometerUpdates];
    }
}

デバイスモーション(Device motion)【iOS 4.0以降】

// インスタンスの生成
CMMotionManager *manager = [[CMMotionManager alloc] init];

// 実質、ジャイロスコープの有無を確認
if (manager.deviceMotionAvailable) {
    // センサーの更新間隔の指定
    manager.deviceMotionUpdateInterval = 0.01;  // 100Hz

    // ハンドラを指定
    CMDeviceMotionHandler handler = ^(CMDeviceMotion *motion, NSError *error) {
        double timestamp = data.timestamp;  // 更新時刻

        /* 加速度センサー(ローパスフィルタ) */
        double gravityX = motion.gravity.x;  // X軸: 加速度G
        double gravityY = motion.gravity.y;  // Y軸: 加速度G 
        double gravityZ = motion.gravity.z;  // Z軸: 加速度G

        /* 加速度センサー(ハイパスフィルタ) */
        double userAccelerationX = motion.userAcceleration.x;  // X軸: 加速度G
        double userAccelerationY = motion.userAcceleration.y;  // Y軸: 加速度G
        double userAccelerationZ = motion.userAcceleration.z;  // Z軸: 加速度G

        /* ジャイロスコープ */
        double x = motion.rotationRate.x;  // X軸: ラジアン/秒
        double y = motion.rotationRate.y;  // Y軸: ラジアン/秒 
        double z = motion.rotationRate.z;  // Z軸: ラジアン/秒

        /* 磁力センサー */
        /* 下記の startDeviceMotionUpdatesUsingReferenceFrame で以下の値を指定した場合のみ取得可能
                - CMAttitudeReferenceFrameXArbitraryCorrectedZVertical
                - CMAttitudeReferenceFrameXMagneticNorthZVertical
                - CMAttitudeReferenceFrameXTrueNorthZVertical
           startDeviceMotionUpdatesToQueue や CMAttitudeReferenceFrameXArbitraryZVertical を指定した場合は取得できない */
        double x = motion.magneticField.x;  // X軸: マイクロテスラ
        double y = motion.magneticField.y;  // Y軸: マイクロテスラ
        double z = motion.magneticField.z;  // Z軸: マイクロテスラ
        CMMagneticFieldCalibrationAccuracy accuracy = motion.magneticField.accuracy;  // 磁力の強さ

        /* CMAttitude */
        double roll = motion.attitude.roll;  // Y軸中心のラジアン角: -π〜π(-180度〜180度)
        double pitch = motion.attitude.pitch; // X軸中心のラジアン角: -π/2〜π/2(-90度〜90度)
        double yaw = motion.attitude.yaw;  // Z軸中心のラジアン角: -π〜π(-180度〜180度)
    };

    /* 以下、どちらかの方法でセンサーの利用開始 */
    // 特に何も調整せずに開始
    [manager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue] withHandler:handler];
    // Z軸を鉛直として、X軸を横とする。その際、GPSと電子コンパスを利用して、X軸を"真北"に設定
    [manager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXTrueNorthZVertical toQueue:[NSOperationQueue currentQueue] withHandler:handler];

    // (不必要になったら)センサーの停止
    if (manager.deviceMotionActive) {
        [manager stopMagnetometerUpdates];
    }
}
CMDeviceMotionにのみ、センサーの開始時に特別な初期化方法があります。【iOS 5.0以降
定数初期化の方法magneticFieldの利用
CMAttitudeReferenceFrameXArbitraryZVerticalZ軸を鉛直として、X軸を横とする(FaceUp)×
CMAttitudeReferenceFrameXArbitraryCorrectedZVertical上記に加えて、電子コンパスで位置を修正する(ただし、上記のものよりもCPUを消費する)
CMAttitudeReferenceFrameXMagneticNorthZVerticalZ軸を鉛直として、X軸を横とする。その際、電子コンパスを利用して、X軸を"磁北"に設定
CMAttitudeReferenceFrameXTrueNorthZVerticalZ軸を鉛直として、X軸を横とする。その際、GPSと電子コンパスを利用して、X軸を"真北"に設定

利用方法のまとめ

・センサーの有無を確認

// 加速度センサー
@property(readonly, nonatomic, getter=isAccelerometerAvailable) BOOL accelerometerAvailable;
// ジャイロスコープ
@property(readonly, nonatomic, getter=isGyroAvailable) BOOL gyroAvailable;
// 磁力センサー
@property(readonly, nonatomic, getter=isMagnetometerAvailable) BOOL magnetometerAvailable NS_AVAILABLE(NA,5_0);
// デバイスモーション
@property(readonly, nonatomic, getter=isDeviceMotionAvailable) BOOL deviceMotionAvailable;

・センサーで計測中かどうか

// 加速度センサー
@property(readonly, nonatomic, getter=isAccelerometerActive) BOOL accelerometerActive;
// ジャイロスコープ
@property(readonly, nonatomic, getter=isGyroActive) BOOL gyroActive;
// 磁力センサー
@property(readonly, nonatomic, getter=isMagnetometerActive) BOOL magnetometerActive NS_AVAILABLE(NA,5_0);
// デバイスモーション
@property(readonly, nonatomic, getter=isDeviceMotionActive) BOOL deviceMotionActive;

・更新間隔の指定(秒数)

// 加速度センサー
@property(assign, nonatomic) NSTimeInterval accelerometerUpdateInterval;
// ジャイロスコープ
@property(assign, nonatomic) NSTimeInterval gyroUpdateInterval;
// 磁力センサー
@property(assign, nonatomic) NSTimeInterval magnetometerUpdateInterval NS_AVAILABLE(NA,5_0);
// デバイスモーション
@property(assign, nonatomic) NSTimeInterval deviceMotionUpdateInterval;

・センサーの利用開始(プル型)

// 加速度センサー
- (void)startAccelerometerUpdates;
// ジャイロスコープ
- (void)startGyroUpdates;
// 磁力センサー
- (void)startMagnetometerUpdates NS_AVAILABLE(NA,5_0);
// デバイスモーション
- (void)startDeviceMotionUpdates;
- (void)startDeviceMotionUpdatesUsingReferenceFrame:(CMAttitudeReferenceFrame)referenceFrame NS_AVAILABLE(NA,5_0);

・センサーの利用開始(プッシュ型)

// 加速度センサー
- (void)startAccelerometerUpdatesToQueue:(NSOperationQueue *)queue withHandler:(CMAccelerometerHandler)handler;
// ジャイロスコープ
- (void)startGyroUpdatesToQueue:(NSOperationQueue *)queue withHandler:(CMGyroHandler)handler;
// 磁力センサー
- (void)startMagnetometerUpdatesToQueue:(NSOperationQueue *)queue withHandler:(CMMagnetometerHandler)handler NS_AVAILABLE(NA,5_0);
// デバイスモーション
- (void)startDeviceMotionUpdatesToQueue:(NSOperationQueue *)queue withHandler:(CMDeviceMotionHandler)handler;
- (void)startDeviceMotionUpdatesUsingReferenceFrame:(CMAttitudeReferenceFrame)referenceFrame toQueue:(NSOperationQueue *)queue withHandler:(CMDeviceMotionHandler)handler NS_AVAILABLE(NA,5_0);

・ハンドラ

// 加速度センサー
typedef void (^CMAccelerometerHandler)(CMAccelerometerData *accelerometerData, NSError *error);
// ジャイロスコープ
typedef void (^CMGyroHandler)(CMGyroData *gyroData, NSError *error);
// 磁力センサー
typedef void (^CMMagnetometerHandler)(CMMagnetometerData *magnetometerData, NSError *error) NS_AVAILABLE(NA,5_0);
// デバイスモーション
typedef void (^CMDeviceMotionHandler)(CMDeviceMotion *motion, NSError *error);

・データの取得

// 加速度センサー
@property(readonly) CMAccelerometerData *accelerometerData;
// ジャイロスコープ
@property(readonly) CMGyroData *gyroData;
// 磁力センサー
@property(readonly) CMMagnetometerData *magnetometerData NS_AVAILABLE(NA,5_0);
// デバイスモーション
@property(readonly) CMDeviceMotion *deviceMotion;

・センサーの利用停止

// 加速度センサー
- (void)stopAccelerometerUpdates;
// ジャイロスコープ
- (void)stopGyroUpdates;
// 磁力センサー
- (void)stopMagnetometerUpdates NS_AVAILABLE(NA,5_0);
// デバイスモーション
- (void)stopDeviceMotionUpdates;


サンプル

ソースコードはGitHubにて公開しています。
iOS-SampleCodes/CoreMotionSample - GitHub

起動時iPodなどでは
一部正しくセンサー情報が取得できないが、動作します


次回

次は「iOSイベント処理ガイド」の最後で、「リモートコントロールイベントサンプル - マルティメディアの操作」です。

2 件のコメント:

  1. デバイスモーションの利用開始方法に誤字があります。
    startMagnetometerUpdatesToQueue → startDeviceMotionUpdatesToQueue

    返信削除
    返信
    1. iOS4での使用方法を修正いたしました。ご指摘いただきありがとうございます。

      削除