Androidで縦向き(Portrait)でカメラを使う方法 (主にAndroid2.x向け)

縦向きカメラを使うのは意外に難しい。

要点

  • Androidのカメラは横向きが基本。
  • Android2.1以前では、縦向きカメラが使用できるかどうかは実装依存Camera.Parameters.set("rotation", 90); とすると、一部端末で縦向き表示になる。
  • Android2.2以降では、Camera.setDisplayOrientation(int)を使うことで縦向きにすることができる。
  • 縦向きカメラを使ってもCamera.takePicture()を使って得られた画像は横向きのままなので、手動で変換する必要がある。

カメラを使う

Androidでカメラを使うには、以下のようなコードを書く。詳しくはAPI DemosCameraPreview.javaとかを見よう。
        mCamera = Camera.open(); //カメラを用意
        mCamera.setPreviewDisplay(holder); //カメラ画像を表示先を設定
        mCamera.startPreview(); // カメラ画像の表示を始める
        mCamera.takePicture(listener, listener, listener, listener); //写真を撮る

カメラを縦向きにする

上記のやり方で得られるカメラ画像は、横向きである。これを画面が縦向きのときに表示させることもできるが、アスペクト比が狂った画像が表示されてしまう。
 
これを縦向きにするには、Camera.setDisplayOrientation(int)でカメラプレビューの角度を設定すればいい。
角度は、リファレンスページにあるコード(下記のもの)をコピペすれば得られる。(ただし要API Level 9。API Level 8向けに修正したコードは後述
 public static void setCameraDisplayOrientation(Activity activity,
         int cameraId, android.hardware.Camera camera) {
     android.hardware.Camera.CameraInfo info =
             new android.hardware.Camera.CameraInfo();
     android.hardware.Camera.getCameraInfo(cameraId, info);
     int rotation = activity.getWindowManager().getDefaultDisplay()
             .getRotation();
     int degrees = 0;
     switch (rotation) {
         case Surface.ROTATION_0: degrees = 0; break;
         case Surface.ROTATION_90: degrees = 90; break;
         case Surface.ROTATION_180: degrees = 180; break;
         case Surface.ROTATION_270: degrees = 270; break;
     }

     int result;
     if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
         result = (info.orientation + degrees) % 360;
         result = (360 - result) % 360;  // compensate the mirror
     } else {  // back-facing
         result = (info.orientation - degrees + 360) % 360;
     }
     camera.setDisplayOrientation(result);
 }
 
Android2.1以前では、setCameraDisplayOrientation()は使えない。パラメーターを取ってきてparameters.set("rotation", 90);とすると一部端末で縦向きになるが、実装依存なので端末によっては動作しない。

撮影した写真を縦向きにする

で、これでカメラのデータを表示する部分は縦向きになったのだが、実際に写真を撮ってみる(Camera.takePicture()を使う)と横長になる。
これを解決するには以下の4つの方法がある。
 
 
下の項目になるほど動作が速いけど、やり方がよくわからないのでここでは上の2つについて説明します。
 
撮った写真を編集する必要がないなら、2番目のイミュータブルなBitmapを作る方法でやる。こちらの方が1番目の方法よりわかりやすい。
     int degrees = getCameraDisplayOrientation(this); // 後述のメソッド
     Matrix m = new Matrix();
     m.postRotate(degrees);
     Bitmap rotatedBitmap = Bitmap.createBitmap(origBitmap, 0, 0, origBitmap.getWidth(), origBitmap.getHeight(), m, false);
 
ここでgetCameraDisplayOrientation()はCameraクラスのリファレンスページにあるsetCameraDisplayOrientation()から抜き出したもの。
以下のようになる。
このコードはAPI level 8向けに修正しているので、インカメラなどを使う場合には間違った数値を返すので注意。
    public static int getCameraDisplayOrientation(Activity activity) {
        int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
        int degrees = 0;
        switch (rotation) {
        case Surface.ROTATION_0:
            degrees = 0;
            break;
        case Surface.ROTATION_90:
            degrees = 90;
            break;
        case Surface.ROTATION_180:
            degrees = 180;
            break;
        case Surface.ROTATION_270:
            degrees = 270;
            break;
        }
        return (90 + 360 - degrees) % 360;
    }
撮った写真に後から他の図形や画像を書き込む場合は、この方法は不適。
イミュータブルなBitmapを生成後にそれをコピーするという方法もあるが、それはメモリ効率が悪い。
そこで1番目の方法を使う。これは動作原理が微妙にわかりにくいし、コードが長い。
     // dataはonPictureTaken(byte, Camera);で定義
     Bitmap origBitmap = BitmapFactory.decodeByteArray(
             data, 0, data.length);
            
     int degrees = getCameraDisplayOrientation(this); // 前述のメソッド
     int rotatedWidth, rotatedHeight;
     if (degrees % 180 == 0) {
          rotatedWidth = origBitmap.getWidth();
          rotatedHeight = origBitmap.getHeight();
     }
     else {
          rotatedWidth = origBitmap.getHeight();
          rotatedHeight = origBitmap.getWidth();
     }
     Bitmap rotatedBitmap = Bitmap.createBitmap(rotatedWidth, rotatedHeight, Bitmap.Config.ARGB_8888);
     Canvas canvas = new Canvas(rotatedBitmap);
     canvas.save();
    
     canvas.rotate(degrees, rotatedWidth / 2, rotatedHeight / 2);
     int offset = (rotatedHeight - rotatedWidth) / 2 * ((degrees - 180) % 180) / 90;
     canvas.translate(offset, -offset);
     canvas.drawBitmap(origBitmap, 0, 0, null);
     canvas.restore();
     origBitmap.recycle();
    
     // ここからrotatedBitmapに何か描き足したり
 
ndkやExifを使う方法はやったことない。。。
ndkでやるには、AndroidBitmapInfoをいじって転置行列を作る要領でピクセルを書き換えればいいはず。

サンプルコード

上記のようにして作った実際のAndroidプロジェクトを以下においています。