Androidでカメラを利用するには

Firemonkeyを用いてAndoroidアプリを開発し、アプリにカメラ機能を追加するには2つの方法がある。

  • TCameraComponentを使って、アプリ内で撮影できるようにする。
  • TTakePhotoFromCameraアクションかIFMXCameraServiceを用いて、他のカメラアプリで撮影する。

A.TCameraComponentを用いる

TCameraComponentを用いた方法は、TCameraComponentのヘルプや、そのサンプルがあるのでそちらを参考にして欲しい。
ただ、なぜだか分からないが、手元にあるAndroidタブレットでは、カメラの解像度を最高にできないなど制限がある。
キャプチャ頻度も良くないのでプレビューではカクカク表示される。性能の高いAndroidタブレットであればそうはならないかもしれない。
得られる画像は、どうもビデオストリーミングのキャプチャー画像となるようでぶれているケースが目立つ。
使用に当たっては目的に合致するかよく考えた方が良いだろう。
TCameraComponentを利用するには、プロジェクトオプションの「使用する権限」で「カメラの利用」にチェックをつける必要がある。アプリのオプションで設定してもデバッグでは権限の確認画面が表示されないので、デバッグインストールの後で、Androidタブレットの設定を開きアプリの権限で設定してやる。

B.TTakePhotoFromCameraアクションを用いる

TTakePhotoFromCameraアクションを用いる場合、通常コントロール(TButtonなど)のActionに、TTakePhotoFromCameraアクションを関連付ける。
こちらの方法では、プレビューもスムーズだし、撮影した画像も気をつけていればぶれない。
行う作業等を以下に示す。

  1. ActionListコンポーネントを追加する
  2. ActionListコンポーネントをダブルクリックするなどしてアクションリストの編集ダイアログを表示する
  3. 標準アクションの作成を選んで、メディアライブラリカテゴリにあるTTakePhotoFromCameraActionを追加する
  4. 追加したアクションのプロパティ(MaxWidth,MaxHeight)に大きめの値を入れておく(5000位)
  5. 追加したアクションのOnDidFinishTakingイベントを作成して引数のTBitmapを処理するルーチンを作成する。
  6. 追加したアクションをコンポーネントのActionに関連付けするなどする
  7. プロジェクトオプションの「資格リスト」で「セキュアファイルの共有」にチェックをつける

7.を行わないで実行すると、カメラアプリ呼び出し時にエラーメッセージが発生する。この情報はTTakePhotoFromCameraActionのヘルプを見ても説明がないので、困る方もいるだろう。なお、ヘルプのトピック→「資格リスト」の該当項目には記述がある。
ここまでして画像を得ることができるが、実はこちらの方法でもカメラの最大解像度で画像を得ることができない場合がある。また、TTakePhotoFromCameraActionのNeedSaveToAlbamプロパティにチェックをつけて、共有に画像を別に保存したものと比較するとサイズがかなり小さくなっており、高圧縮の画像であることが分かる。
実機でのテストでは、4160x3120ピクセルの画像を得られるAndroidタブレットで、2080x1560ピクセルという半端なサイズの画像が得られる。別のAndroidタブレットでは、4032x1960ピクセルの画像が4032x1960ピクセルになる。

いろいろ挙動を確認したところ、Androidでカメラアプリとの画像のやりとりは以下のような手続きになっているようだ。

  1. カメラアプリの呼び出し(インスタンスと最大画像サイズの受け渡し)
  2. シャッターボタンを押して撮影(この瞬間、テンポラリー画像ファイルのデータが作成される)
  3. 画面にはい・いいえボタンが表示される
  4. はいを選ぶとカメラアプリ終了(NeedSaveToAlbamがTrueであれば、このタイミングで保存ファイルが作成される)
  5. テンポラリー画像ファイルが保存される
  6. 呼び出し元のアプリにテンポラリー画像ファイルのパスを通知
  7. 呼び出し元のアプリにテンポラリー画像ファイルのイメージデータを通知
  8. テンポラリー画像ファイルを消去(されない行儀が悪いAndroidタブレットもあり)

見て分かるように、渡される画像は一回ファイルとして保存がされている。
この画像ファイルは、NeedSaveToAlbamフラグで保存される画像ファイルとは別で、Exif情報がなく、また高圧縮である。画像サイズについてはカメラアプリの実装次第で最大の画像サイズでない場合もある。
6.で渡される画像ファイル名はテンポラリー画像ファイルのファイル名で、NeedSaveToAlbamフラグで保存される画像ファイルの名前ではない。

C.IFMXCameraServiceとTMessageReceivedImagePathメッセージを用いる

B.のアクションイベントのかわりにIFMXCameraServiceとTMessageReceivedImagePathメッセージを用いて、撮影保存された画像を得る方法を示す。
このコードはAndroidのみで動くことに注意。
幾つか仕込む必要があるのと、RTLメッセージフックの登録解除を行う必要があるので、慎重に実装して欲しい。

まず、メッセージフックの登録解除のコードを示す。登録はフォーム生成時、解除はフォームクローズのタイミングが良いだろう。
コード中のReceivedImagePathMessageがメッセージを受け取る関数だ。また、FIdは登録時の値を保持して、解除の時に与える必要がある。

//---------------------------------------------------------------------------
#include <Fmx.Platform.Android.hpp>
int FId;
void __fastcall TForm1::FormCreate(TObject *Sender)
{
  // TMessageReceivedImagePathメッセージを受け取る関数の登録
  TMetaClass*            msgCls         = __classid(Fmx::Platform::Android::TMessageReceivedImagePath);
  TMessageListenerMethod listenerMethod = &(this->ReceivedImagePathMessage);
  FId = TMessageManager::DefaultManager->SubscribeToMessage( msgCls, listenerMethod );
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action)
{
  // メッセージ登録解除
  if( FId != 0 )
  {
    TMetaClass* msgCls = __classid(Fmx::Platform::Android::TMessageReceivedImagePath);
    TMessageManager::DefaultManager->Unsubscribe( msgCls, FId );
  }
}
//---------------------------------------------------------------------------

ボタンクリックイベントでのカメラアプリの呼び出し例を以下に示す。

void __fastcall TForm1::Button1Click(TObject *Sender)
{
  _di_IFMXCameraService service;
  TParamsPhotoQuery params;
  if(  TPlatformServices::Current->SupportsPlatformService(__uuidof(IFMXCameraService) )
   &&( service = TPlatformServices::Current->GetPlatformService(__uuidof(IFMXCameraService))) )
  {
    params.Editable           = false;
    params.NeedSaveToAlbum    = true;
    params.RequiredResolution = TSize( 8192, 8192 );
    params.OnDidFinishTaking  = nullptr;
    service->TakePhoto( SnapButton, params );
  }
  else
  {
    ShowMessage( "This device does not support the camera service" );
  }
}

RequiredResolutionには適当な大きい数字を入れる。また、NeedSaveToAlbumはtrueにする。

テンポラリー画像のファイル名を得て、そこから保存ファイル名を検索して、画像を読み込むコードを以下に示す。保存ファイルの生成はテンポラリー画像ファイルと最大でも2秒程度の誤差しかないと仮定している。

constexpr double dEps = 1.0/( 24.0 * 60.0 * 30.0 ); // 2秒
void __fastcall TForm1::ReceivedImagePathMessage( System::TObject* const Sender, System::Messaging::TMessageBase* const M )
{
  TMessage__1<UnicodeString>* Message = dynamic_cast<TMessage__1<UnicodeString>*>(M);
  if( Message == nullptr )
    return;

  // 保存画像ファイル名をファイルのタイムスタンプで検索
  UnicodeString uSrc  = Message->Value;
  UnicodeString uPath = ExtractFilePath( uSrc );

  TDateTime       srcTime = TFile::GetLastWriteTime( uSrc );
  TStringDynArray aFiles  = TDirectory::GetFiles( uPath, u"*.*" );
  for( int i = 0; i < aFiles.Length; ++i )
  {
    if( aFiles[i] == uSrc ) continue;
    TDateTime dTime = TFile::GetLastWriteTime( aFiles[i] );
    if( fabs( dTime.Val - srcTime.Val ) < dEps )
    {
      uSrc    = aFiles[i];
      srcTime = dTime;
      break;
    }
  }
  TBitmap* Image = new TBitmap;
  Image->LoadFromFile( uSrc ); 
}

ボタンクリックコードの替わりにB.のアクションイベントを用いても可能であるが、メッセージフックは自前で書かなければならないのでこちらの方が見通しが良いと思う。