woshidan's blog

そんなことよりコードにダイブ。

AndroidのCamera2 APIを触ってみた

ちょっと面白そうだったので調べました。

概要

https://developer.android.com/reference/android/hardware/camera2/package-summary.html からざっくり読み取った結果、Camera2 APIでカメラ経由で画像を取得したりする流れをかなり大雑把にいうと

  1. CameraManager を利用し、CameraDevice.StateCallback 経由で CameraDeviceインスタンスを取得
  2. Surface系のViewやMediaCodecなどの出力先とカメラデバイスを利用して CameraDeviceインスタンスから createCaptureSession(List, CameraCaptureSession.StateCallback, Handler) メソッドにより CameraCaptureSession を生成
  3. 1フレーム分の入力を取得するための入力リクエスト( CaptureRequestインスタンス)を作る
  4. CaptureSession にリクエストをセットする
  5. リクエストが処理されて、 TotalCaptureResult (カメラデバイスの現在の状態など)オブジェクトの生成や、出力先への1フレーム分への画像データの送信が行われる

となるようです。それぞれ見ていきましょう。

なお、ただでさえややこしいので、今回のサンプルコードは意図的にtargetSDKVersionを21にして、パーミッション周りやエラー対応の処理はビルドが通るギリギリくらいまで省いています。。

CameraManager を利用し、CameraDevice.StateCallback 経由で CameraDeviceインスタンスを取得

  1. CanmeraManager を利用してそのデバイスで利用できるカメラのIdを取得
  2. カメラのIdと CameraDevice.StateCallbackインスタンスを利用して、CameraManager.openCamera()メソッドで CameraDevice をオープン
  3. CameraDevice.StateCallbackonOpened コールバックで 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 を生成します。

  1. 出力先として TextureView を用意する
  2. TextureView から SurfaceTexture を取得して Surface を生成する
  3. 2のSurface をListに入れて、CameraCaptureSession.createCaptureSession() メソッドを呼び出す

2の Surface は、 ImageReaderMediaCodec など他の出力先クラスからも生成/取得が可能です*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にしているので少々注意してお試しください。

Camera2_API_Sample · GitHub

現場からは以上です。

参考