Android4.0 からは OS がスクリーンショット機能をサポートしました。しかし、それ以前の Android では、次のような不便な撮影方法しかありませんでした。
  • AndroidSDK の DDMS のキャプチャ機能
  • AndroidSDK の adb shell から起動したプロセスを介してフレームバッファにアクセスし、Android アプリから画像を作成
  • root化した Android 端末のフレームバッファから画像を作成
どれも一度はPCにUSB接続する必要があり、また、Android の開発環境をPCに入れる必要があり、一般のユーザには敷居が高い画面キャプチャ方法しかありません。

そこで、ここでは、「root化も adb も不要な方法を紹介します。」と言いたいところなのですが、おそらく無理です。root 化不要でフレームバッファにアクセス可能な端末はいくつかあるらしいのですが、調べた限りでは、大抵の端末ではフレームバッファにアクセスするために、root権限かadb権限が必須となります。

問題の解決にはなってませんが、先人の轍を踏むという意味を込めて、今回はフレームバッファのデータを Bitmap 画像化するプログラムをC言語で書いてみました。




  • フレームバッファからの画面データ取得

本来、Android アプリから取得できればいいのですが、調べたら上記のような理由で無理があるので、ここでは adb shell からデータを取得しました。Android のフレームバッファファイルは /dev/graphics の下にあります。

$ adb pull /dev/graphics/fb0 ./

ちなみに、WVGA800(480x800) の画面サイズのエミュレータに対して行ったところ、ファイルサイズ1,536,000Byte(1.536MB)でした。
  • 1.536*10^6 / (480*800) = 4
となるので、1 ピクセルあたり 4 Byte のデータと推測できます。
下の画像は、fb0 を取得した時のエミュの画面です。

 エミュのスクリーンショット
  ※Windowsのスクリーンショットアプリで撮ったんですが、うまく取れていないかもしれません。



  • BMPヘッダ情報の付加

fb0 で取得したデータは生データなので、BMP 画像ファイル用のバイナリデータを付加しないと画像ビューアで見ることはできません。次のサイトを参考に、BMP ヘッダを付加します。

[参考サイト]

BMP ファイルフォーマット

さて、BMP 画像は大まかに分けると、OS/2 タイプと Windows タイプの2種類があります。世間一般で多く用いられているのは Windows タイプとのことなので、Windows タイプのヘッダ情報を付加します。ヘッダ情報を付加しているのは add_bmp_header 関数です。上記で 1 画素あたり 32bit の情報量だったので、32bit のヘッダ情報を付加すると、次のような画像になりました。


画像は上下反転し、色もおかしいですね。更に、4画面分あります。

まず、上下反転しているのはフレームバッファが下から上へ、左から右へ画面データを並べている仕様みたいです。また、Android では色を16bit で表現しているため、32bit のヘッダ情報を付加したためおかしな色になってしまったわけです。




  • フレームバッファのデータ形式について [RGB565]

フレームバッファには画面データが格納されています。Android では RGB565 という 16bit のデータフォーマットで色を表現しており、フレームバッファでもこの形式でデータが保存されています。

RGB565とは (Red, Green, Blue) がそれぞれ (5bit, 6bit, 5bit) のビットサイズで表現される色の表現方法です。何故緑だけが1ビット多いのかというと、緑は人間の視覚的に一番繊細だからだそうです。コンピュータで画像データを扱う際は2の累乗が扱いやすいので、2Byteに合わせる際に緑だけ1bit増やしているというわけですね。

しかし、一般的に出回っているBitmap画像の多くは32bit形式のbmpファイルです。そのため、次のようにして、フレームバッファから取得した各画素のデータを変換します。
/***
 * Convert RGB565 into RGB888
 *   dst:4byte, src:2byte
 */
void convert16to32(byte dst[], byte src[]){
  uint tmp = src[0] + (src[1] << 8);
  byte b = (tmp & 0x001F) >> 0;
  byte g = (tmp & 0x07E0) >> 5;
  byte r = (tmp & 0xF800) >> 11;
  dst[0] = b * 255/32;
  dst[1] = g * 255/64;
  dst[2] = r * 255/32;
  dst[3] = 0;
}
ビットシフトで元データの R, G, B 情報を取得し、255/32 や 255/64 の部分で、情報量の変換に伴う色の表現を正規化しています。

さて、ここでおかしなことに気が付きます。
画面サイズは 480*800 で、1 画素あたり 16bit なので、フレームバッファから取得されたデータは
480*800*2 = 768,000
となるはずです。しかし、実際には2倍あります。何故でしょうか。
原因は、Android ではダブルバッファリングを行っているからです。




  • ダブルバッファリング

さて、フレームバッファは1ピクセルに2バイトのデータが保存されているとわかりました。ここで、先ほど fb0 から拾ってきたフレームバッファのファイルサイズを見てみると、

1,536,000 Byte

でした。理論的には、

768,000 Byte

となるはずですが、実際は2倍のデータ量になっています。これは、Android ではダブルバッファリングを行っているためです。

ダブルバッファリングは画面を表示するための技術で、画面用メモリ領域を2つ用意し、片方を表示用、もう片方を次の状態の画面データ書き込み用として使い、交互に切り替えることで高速かつ滑らかに表示することを可能とする方法です。

これを考慮すると、fb0 には2画面分の情報が入っていることになります。しかし、単に 32bit の BMP ヘッダ情報を付加した画像を見てみると、4画面あります。原因は、32bit のヘッダ情報を付加してしまったことにあります。本来2列分のデータなのですが、32bit のヘッダを付加してしまったがために、一列に表示されてしまい、一列に2画面分の画像が表示されてしまったのです。そのため、厳密には左右の画面は微妙に異なる画面となっています。

このように、4画面になった原因としては
  • 16bit の画像データに 32bit のBMPヘッダを付加してしまった
  • Android ではダブルバッファリングを行っている
ということになります。これらの問題は、
  • 16bit のデータを 32bit のデータに変換
  • 後半のデータを無視

することで、解決できます。

その結果は次のようになりました。


上下反転ですが、良い感じです。




  • 上下反転

いよいよ最後、上下の反転です。左右は入れ替わっていないので、単純にメモリ上のデータをひっくり返すのではなく、行単位で上下をひっくり返します。処理はプログラムの inverse_top_bottom 関数で行っています。

/**
 * Inverse order data between top and bottom.
 */
byte *inverse_top_bottom(byte *data, uint data_size, uint width){
  byte *inversed = (byte *)malloc(sizeof(byte) * data_size);
  uint i = 0;
  uint height = data_size / width / PXL_BYTE;
  for(i = 0; i < height; i++){
    int j = 0;
    for(j = 0; j < width*PXL_BYTE; j++){
      inversed[(height-1-i)*width*PXL_BYTE + j] = data[i*width*PXL_BYTE + j];
    }
  }
  return inversed;
}

処理結果は次のようになりました。


一応、問題なく復元できていると思います。ただし、ヘッダ情報はかなり適当なので、その辺は気を付けないといけません。


以上です。最後にプログラムを載せておきます。何か突っ込みたいところがあればお気軽にどうぞ。プログラムの書き方でも何でも構いません。


fb2bmp.c