2011年8月1日月曜日

Cameraの機種依存 (2)

前回、Intent経由での、端末のカメラアプリの利用(起動)での、機種依存を書いてみました。

今回は、SurfaceViewを使った、カメラの利用について、書いてみます。
結論から言うと、出てきた課題をクリアーできませんでしたので、その前提で呼んでくださいorz

---

SurfaceViewを使うと、自分で定義したActivityで、カメラプレビュー画面を使えます。
カメラで写している画像を、Viewとして表示できるようになるわけです。

サンプルコードは、ググれば・・・というより、ApiDemoにあります。

ApiDemos->Graphics->CameraPreviewを、たどってください。

このAPIDemoは、撮影には対応してませんが、撮影のコードも、ググれば出てきます。
(あとでサンプルコードを・・・って、ほとんど、ネット上のコードのコピペだけど)

ここまでできると、プレビュー>撮影ができるのですが・・・ここでも機種依存(だけじゃない)罠が。

1.カメラプレビューのアスペクト比がおかしい。もしくは、プレビューと、撮影サイズが一致しない。
2.Galaxy Sの場合・・・なんかヘン。
またGalaxyですが。(おかしいのは自分のコードかもしれない)

まず、1。

カメラプレビューで表示できる、プレビューサイズというのは、機種ごとに決まっています。
機種ごとにも、幾つか種類があって、そのサイズ一覧は
Camera.Parameters::getSupportedPreviewSizes()で取得できます。(戻り値はListです)
ApiDemoのサンプルコードの場合は、この中から、SurfaceViewのサイズとアスペクト比が
近しいものを選ぶようなアルゴリズムが書いてあります。getOptimalPreviewSize()メソッドです。
これをやらないで、適当に選ぶと、SurfaceViewのアスペクト比に、カメラプレビューがあわされてしまい、
例えば人をプレビューすると、太ったり痩せちゃったりします。

この処理があることで、適切なプレビューが選ばれるのですが、困ったことに、いざ撮影!してみると
またサイズが違っちゃったりします。プレビューに出てない両端が表示されたりとか、また痩せたり細ったりとか。

これは、実際に撮影し、保存された画像と、これまたアスペクト比が異なっちゃっているのが問題みたいです。
(この辺から推測ですが、)結局、「画面のアスペクト比」「プレビューのアスペクト比」「撮影され、保存された画像のアスペクト比」を
考えて、サイズを選ばないといけないっぽい。

アルゴリズム的にはこんな感じ(未検証だけど)

1.撮影後に保存される画像のアスペクト比の設定値を取得する。Camera.Parameters::getPictureSize()←たぶん

2.1と近しいアスペクト比のプレビュー画像サイズを取得するCamera.Parameters::getSupportedPreviewSizes()

3. 2 のアスペクト比をもとに、SurfaceViewのサイズを決める。


おそらく、3がポイント。1は、カメラスペックに依存してしまうので、どうにも変えられない。
最終的に保存される画像(HW依存)が全てなので、それに合わせていくために、2,3を決めていくしかない。
3は、SurfaceViewのマージンやSurfaceView以外の画面パーツ(View)を駆使して調整するしかないかなと。


たぶんこれでうまく行きそうな気がするんですが、実は機種依存にはまってしまって、SurfaceViewを使うのを諦めた(※)ので、検証用のコード書いてません。

(※)諦めたというより、労力に見合わなかった、、、


で、その機種依存ですが。以下の3つ(たぶんもっとある)を乗り越える必要があります。


1.CameraParameterは、フォーカス、ホワイトバランスなど、設定項目が多すぎ。機種ごとに使えるものも違う。

2.ボタンを押す、タッチするなどの、撮影トリガーは、自分のアプリで作ってあげる必要がある。

これはつまり、カメラアプリの動作と異なってきてしまう。
3.撮影後の動作が・・・


1.設定パラメータがたくさんある、つまり、自分で設定しなけりゃいけない。

オートフォーカス、ホワイトバランスとか、端末のカメラアプリなら勝手に設定してくれるものを、
チューニング含めて、自分でやる必要あります。
私がやりたかったのは、カメラで撮った画像から顔を取得する機能なんですが、
SurfaceViewを使った方法だと、(Intentでカメラを起動する方法に比べて)顔認識率が下がってしまいました。
これは、デフォルトのカメラアプリは、パラメータを、センサとかいろんなモノ使って自動で補正しているから。


2.カメラ系のUIは、やはり各メーカ、こだわりがあります。

撮影するのも、画面タッチで撮影するものと、ボタンで撮影するもの・・・などなど。
「自分のアプリはこうだ!」といって、撮影方法決めても、
ユーザから見たら同じカメラ、「なんで操作が違うの?」となります。
つまり、使いづらいわけです。
いっつもカメラボタン押して撮影してるのに、このアプリではカメラボタン効かない、とか。イライラします。


3.大抵の機種は、撮影後、プレビュー表示してくれます。

が、Galaxy Sだけ、なぜか、プレビューが出ず、ベタ塗りの画面に・・・
しかも、何回か起動すると落ちたりとか。端末の向きの検出してくれなかったりとか。
まぁ、これは自分の設定値に問題があるような気もしますが。


ということで、カメラ撮影した画像がほしいという場合、特別な理由がなければ、
IntentでCameraアプリを呼んだほうが、使い勝手とか、労力とかの点で、かなりベターだと思います。


それにしても、Cameraの機種依存は凄まじい。
メーカが力を注力する部分だし、仕方ない部分だとは思うけど、
少なくとも標準動作は決めて欲しいな>Google様。


最後に、SurfaceViewを使って、プレビュー、撮影するコードを晒しておきます。
一応、Orientationにも対応しているつもり。
もちろん、今回記述した問題には対処してないですwww


package test.android.Camera;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.hardware.Camera;
import android.hardware.Camera.Size;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.MotionEvent;
import android.view.OrientationEventListener;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.Window;
import android.view.WindowManager;

public class CameraEntry extends Activity {
private CameraView camView;
private static final String TAG = "CameraEntry";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().clearFlags(
WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);

this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
camView = new CameraView(this);
setContentView(camView);

}

public class CameraView extends SurfaceView implements
SurfaceHolder.Callback {
private SurfaceHolder mHolder; // ホルダー
private Camera mCamera; // カメラ
private Size mSize;
private static final String TAG = "CameraEntry";

private OrientationEventListener orientationListener = null;

public CameraView(Context context) {
super(context);
// サーフェイスホルダーの生成
mHolder = getHolder();
mHolder.addCallback(this);
// プッシュバッッファの指定
mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

orientationListener = new OrientationEventListener(CameraEntry.this) {
@Override
public void onOrientationChanged(int orientation) {
CameraView.this.onOrientationChanged(orientation);

}
};

}

private int prevOrientation = 0;

public void onOrientationChanged(int orientation) {
if (mCamera == null) {
Log.d(TAG, "mCamera == null");
return;
}
if (mInProgress == true) {
return;
}

if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN)
return;

orientation = (orientation + 45) / 90 * 90;
int rotation = 0;
rotation = (orientation + 90) % 360;

if (prevOrientation == rotation)
return;

Log.d(TAG, "Orientation " + prevOrientation + " to " + rotation);
prevOrientation = rotation;
mCamera.stopPreview();
Camera.Parameters params = mCamera.getParameters();
params.setRotation(rotation);
mCamera.setParameters(params);
mCamera.startPreview();
}

public void surfaceCreated(SurfaceHolder holder) {
mCamera = Camera.open();
Camera.Parameters params = mCamera.getParameters();
List supportedSizes = params.getSupportedPreviewSizes();

if (supportedSizes != null && supportedSizes.size() > 0) {

mSize = supportedSizes.get(0);
params.setPreviewSize(mSize.width, mSize.height);
params.setRotation(0);
mCamera.setParameters(params);
}

for (Iterator i = supportedSizes.iterator();i.hasNext();)
{
Size s = i.next();
Log.d(TAG, "Preview/w:" + s.width +  "h:" + s.height);
}

supportedSizes = params.getSupportedPictureSizes();

for (Iterator i = supportedSizes.iterator();i.hasNext();)
{
Size s = i.next();
Log.d(TAG, "Picture/w:" + s.width +  "h:" + s.height);
}

Size s = params.getPictureSize();
Log.d(TAG, "PictureSetting/w:" + s.width +  "h:" + s.height);


try {
mCamera.setPreviewDisplay(holder);
} catch (IOException exception) {
mCamera.release();
mCamera = null;
// TODO: add more exception handling logic here
}
}

public void surfaceChanged(SurfaceHolder holder, int format, int w,
int h) {
Log.d(TAG, "Start preview!");

// カメラの初期化
try {
Log.d(TAG, "Start create!");

Camera.Parameters parameters = mCamera.getParameters();
List sizes = parameters.getSupportedPreviewSizes();
Size pictureSize = parameters.getPictureSize();
Size optimalSize = getOptimalPreviewSize(sizes,
pictureSize.width, pictureSize.height);
parameters
.setPreviewSize(optimalSize.width, optimalSize.height);
// parameters.setPreviewSize(mSize.width, mSize.height);
parameters.setPictureSize(1600, 1200);

mCamera.setParameters(parameters);
orientationListener.enable();
mCamera.startPreview();

} catch (Exception e) {
e.printStackTrace();
}
}

private Size getOptimalPreviewSize(List sizes, int w, int h) {
final double ASPECT_TOLERANCE = 0.05;
double targetRatio = (double) w / h;
if (sizes == null) return null;

Size optimalSize = null;
double minDiff = Double.MAX_VALUE;

int targetHeight = h;
// Try to find an size match aspect ratio and size
Log.d(TAG, "Surface size w:" + w + "h:" + h);
for (Size size : sizes) {
Log.d(TAG, "Optimal charenge w:" + size.width + "h:" + size.height);
double ratio = (double) size.width / size.height;
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue;
//if (size.width > w || size.height > h) continue;
//if (Math.abs(size.height - targetHeight) < minDiff) {
if (Math.abs(ratio - targetRatio) < minDiff){
optimalSize = size;
minDiff = Math.abs(ratio - targetRatio);
}
}


// Cannot find the one match the aspect ratio, ignore the requirement
if (optimalSize == null) {
minDiff = Double.MAX_VALUE;
for (Size size : sizes) {
//if (size.width > w || size.height > h) continue;
if (Math.abs(size.height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}
}

Log.d(TAG, "OptimalSize w:" + optimalSize.width +  "h:" + optimalSize.height);

return optimalSize;
}

  public void surfaceDestroyed(SurfaceHolder holder) {
   // カメラのプレビュー停止
   orientationListener.disable();
   mCamera.setPreviewCallback(null);
   mCamera.stopPreview();
   mCamera.release();
   mCamera = null;
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
   if (event.getAction() == MotionEvent.ACTION_DOWN) {
    if (mCamera != null) {
     mCamera.autoFocus(mAutoFocusListener);
    }
   }
   return true;
  }

  private Camera.AutoFocusCallback mAutoFocusListener = new Camera.AutoFocusCallback() {
   public void onAutoFocus(boolean success, Camera camera) {
    camera.autoFocus(null);
    try {
    } catch (Exception e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
    }
    camera.takePicture(null, null, mPictureListener);
    mInProgress = true;
   }
  };

  private Camera.PictureCallback mPictureListener = new Camera.PictureCallback() {
   @Override
   public void onPictureTaken(byte[] data, Camera camera) {
    Log.i(TAG, "Picture taken");
    if (data != null) {
     Log.i(TAG, "JPEG Picture Taken");
     FileOutputStream fo;
     try {
      fo = new FileOutputStream("/sdcard/test.jpg");
      fo.write(data);
      fo.close();
     } catch (FileNotFoundException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
     } catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
     }
     BitmapFactory.Options options = new BitmapFactory.Options();
     // options.inSampleSize = IN_SAMPLE_SIZE;
     options.inPreferredConfig = Bitmap.Config.RGB_565;
     Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0,
       data.length, options);
     // TODO: Got the picture, add your own code.

    }
   }
  };
  private boolean mInProgress = false;
 };
}

0 件のコメント:

コメントを投稿