AndroidのCamera2 APIを触ってみた
ちょっと面白そうだったので調べました。
概要
https://developer.android.com/reference/android/hardware/camera2/package-summary.html からざっくり読み取った結果、Camera2 APIでカメラ経由で画像を取得したりする流れをかなり大雑把にいうと
CameraManager
を利用し、CameraDevice.StateCallback
経由でCameraDevice
のインスタンスを取得- Surface系のViewやMediaCodecなどの出力先とカメラデバイスを利用して
CameraDevice
のインスタンスからcreateCaptureSession(List, CameraCaptureSession.StateCallback, Handler)
メソッドによりCameraCaptureSession
を生成 - 1フレーム分の入力を取得するための入力リクエスト(
CaptureRequest
のインスタンス)を作る CaptureSession
にリクエストをセットする- リクエストが処理されて、
TotalCaptureResult
(カメラデバイスの現在の状態など)オブジェクトの生成や、出力先への1フレーム分への画像データの送信が行われる
となるようです。それぞれ見ていきましょう。
なお、ただでさえややこしいので、今回のサンプルコードは意図的にtargetSDKVersionを21にして、パーミッション周りやエラー対応の処理はビルドが通るギリギリくらいまで省いています。。
CameraManager
を利用し、CameraDevice.StateCallback
経由で CameraDevice
のインスタンスを取得
CanmeraManager
を利用してそのデバイスで利用できるカメラのIdを取得- カメラのIdと
CameraDevice.StateCallback
のインスタンスを利用して、CameraManager.openCamera()
メソッドでCameraDevice
をオープン CameraDevice.StateCallback
のonOpened
コールバックでCameraDevice
のインスタンスを取得
利用するカメラの種類を制限したかったりする場合はCameraDevice
をオープンする前に行います。
また、サンプルによるとプレビュー領域のサイズや向きなどを設定したい場合はプレビューを表示する TextureView
に対して行うようです。
CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); String selectedCameraId = ""; try { selectedCameraId = manager.getCameraIdList()[0]; // https://github.com/googlesamples/android-Camera2Basic/blob/5dad16c103715b5e7e3c001cc5f6067f8d23f29e/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java#L499 // あたりにあるのですが、顔用カメラを使いたくないなどがあれば、CameraCharacteristicsを経由して確認可能 // CameraCharacteristics characteristics // = manager.getCameraCharacteristics(selectedCameraId); } catch (CameraAccessException e) { e.printStackTrace(); } try { manager.openCamera(selectedCameraId, mStateCallback, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); }
private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() { @Override public void onOpened(CameraDevice cameraDevice) { mCameraDevice = cameraDevice; } @Override public void onDisconnected(CameraDevice cameraDevice) { cameraDevice.close(); mCameraDevice = null; } @Override public void onError(CameraDevice cameraDevice, int error) { cameraDevice.close(); mCameraDevice = null; } };
CameraDevice
のインスタンスから CameraCaptureSession
を生成
Surface系のViewやMediaCodecなどの出力先とカメラデバイスを利用して CameraDevice
のインスタンスから createCaptureSession(List, CameraCaptureSession.StateCallback, Handler)
メソッドにより CameraCaptureSession
を生成します。
- 出力先として
TextureView
を用意する TextureView
からSurfaceTexture
を取得してSurface
を生成する- 2の
Surface
をListに入れて、CameraCaptureSession.createCaptureSession()
メソッドを呼び出す
2の Surface
は、 ImageReader
や MediaCodec
など他の出力先クラスからも生成/取得が可能です*1。
// Activity.onCreate内 mTextureView = (TextureView) findViewById(R.id.texture); mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { // 先ほどのカメラを開く部分をメソッド化した openCamera(); } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { return true; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { } });
private CameraCaptureSession mCaptureSession = null; // ... private void createCameraPreviewSession() { SurfaceTexture texture = mTextureView.getSurfaceTexture(); texture.setDefaultBufferSize(320, 240); // 自分の手元のデバイスで決めうちしてます Surface surface = new Surface(texture); try { mCameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession session) { // カメラがcloseされている場合 if (null == mCameraDevice) { return; } } mCaptureSession = session; @Override public void onConfigureFailed(CameraCaptureSession session) { } }, null); } catch (CameraAccessException e) { e.printStackTrace(); } }
1フレーム分の入力を取得するための入力リクエスト( CaptureRequest
のインスタンス)を作る
CaptureRequest
はカメラが何ができるかなどの情報が必要なため、 CameraDevice
クラスが CaptureRequest.Builder
のファクトリメソッドを持っていますので利用します。
リクエストには色々ありますが、今回のリクエストは Target
に与えた Surface
によくあるプレビューの表示を作るための画像データを送ってくれ、というものです(たぶん)。
private CaptureRequest.Builder mPreviewRequestBuilder; private CaptureRequest mPreviewRequest; ... // createCameraPreviewSession メソッド内で try { mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); mPreviewRequestBuilder.addTarget(surface); mPreviewRequest = mPreviewRequestBuilder.build(); } catch (CameraAccessException e) { e.printStackTrace(); }
CaptureSession
にリクエストをセットする
先ほど生成したリクエストに加えて、 CameraCaptureSession.CaptureCallback
が必要です。
プレビュー表示のいろいろなエラーハンドリングをする場合は結構書く必要があるみたいですが、今回はその辺は考えないのでnullを与えています。
この時点で TextureView を置いた領域にカメラで撮影した範囲が表示されているのが確認できます。
mCameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession session) { // カメラがcloseされている場合 if (null == mCameraDevice) { return; } mCaptureSession = session; try { session.setRepeatingRequest(mPreviewRequest, null, null); // `CaptureSession` にリクエストをセットする } catch (CameraAccessException e) { e.printStackTrace(); } }
TotalCaptureResult
へ送られてきた1フレーム分への画像データの処理について
前節で TotalCaptureResult
へデータが送られてくるところまでは確認しましたが、 プレビューだけ延々と表示しても仕方ないので、ここへ飛んできたデータを利用する方法の一例を書いておしまいにします。
下記のコードでは、ボタンを押すとプレビューの表示を止めて、プレビューに表示している画像をファイルへ保存しています。
Button capture = (Button) findViewById(R.id.button_capture); capture.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { try { mCaptureSession.stopRepeating(); // プレビューの更新を止める if(mTextureView.isAvailable()) { File file = new File(getFilesDir(), "surface_text.jpg"); FileOutputStream fos = new FileOutputStream(file); Bitmap bitmap = mTextureView.getBitmap(); bitmap.compress(Bitmap.CompressFormat.JPEG, 50, fos); fos.close(); } } catch (CameraAccessException e) { e.printStackTrace(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } });
真剣にやろうと思ったらActivityやFragmentのライフサイクルに合わせてリソースの解放とか色々あるんですが今日は流れを把握するためにこんな感じで。
プレビューの表示止めてファイルに書き込む部分まで含めて全体のコードは以下となります。一番初めに書きましたが、今回はCamera2 APIの流れだけを把握しようと例外処理を省くため、targetSDKVersion=21
にしているので少々注意してお試しください。
現場からは以上です。