カメラ プレビュー

注: このページでは、Camera2 パッケージについて言及しています。アプリで Camera2 の特定の低レベル機能が必要な場合を除き、CameraX を使用することをおすすめします。CameraX と Camera2 は、どちらも Android 5.0(API レベル 21)以降に対応しています。

Android デバイスでは、カメラとカメラ プレビューの向きが常に同じとは限りません。

カメラは、デバイスがスマートフォン、タブレット、パソコンのいずれであるかにかかわらず、デバイス上の固定位置にあります。デバイスの向きが変わると、カメラの向きも変わります。

そのため、カメラアプリは通常、デバイスの向きとカメラ プレビューのアスペクト比の間に固定された関係があることを前提としています。スマートフォンが縦向きの場合、カメラ プレビューは横長ではなく縦長であると想定されます。スマートフォン(とカメラ)を横向きに回転させると、カメラ プレビューは縦よりも横が長くなります。

しかし、折りたたみ式デバイスなどの新しいフォーム ファクタや、マルチウィンドウやマルチディスプレイなどのディスプレイ モードの出現により、そのような想定は不可能となりました。折りたたみ式デバイスでは、向きを変えずにディスプレイのサイズとアスペクト比を変更できます。マルチ ウィンドウ モードでは、カメラアプリが画面の一部に制限され、デバイスの向きに関係なくカメラ プレビューがスケーリングされます。マルチディスプレイ モードでは、プライマリ ディスプレイと同じ向きではないセカンダリ ディスプレイを使用できます。

カメラの向き

Android 互換性定義では、カメラのイメージ センサーの向きは、カメラの長辺と画面の長辺が平行となる向きにしなければならないと規定されています。つまり、デバイスが横向きで保持されている場合、カメラは横向きで画像をキャプチャしなければなりません。これは、デバイスの自然な向きに関係なく適用されます。つまり、横向き主体のデバイスにも、縦向き主体のデバイスにも適用されます。」

カメラと画面の配置により、カメラアプリのカメラ ビューファインダーの表示領域が最大化されます。また、イメージ センサーは通常、横向きのアスペクト比でデータを出力します。最も一般的なのは 4:3 です。

スマートフォンとカメラセンサーの両方が縦向き。
図 1. スマートフォンとカメラセンサーの向きの一般的な関係。

カメラセンサーの自然な向きは横向きです。図 1 では、前面カメラ(ディスプレイと同じ方向を向いているカメラ)のセンサーが、Android 互換性定義に準拠するために、スマートフォンに対して 270 度回転しています。

センサーの回転をアプリに公開するため、camera2 API には SENSOR_ORIENTATION 定数が含まれています。ほとんどのスマートフォンとタブレットでは、デバイスは前面カメラのセンサーの向きを 270 度、背面カメラのセンサーの向きを 90 度(デバイスの背面からの視点)と報告します。これにより、センサーの長辺がデバイスの長辺と揃います。ノートパソコンのカメラは通常、センサーの向きを 0 度または 180 度として報告します。

カメラのイメージ センサーは、センサーの自然な向き(横向き)でデータ(画像バッファ)を出力するため、カメラ プレビューがデバイスの自然な向きで直立して表示されるように、画像バッファを SENSOR_ORIENTATION で指定された角度だけ回転させる必要があります。前面カメラの場合は反時計回り、背面カメラの場合は時計回りです。

たとえば、図 1 の前面カメラの場合、カメラセンサーによって生成される画像バッファは次のようになります。

カメラセンサーが横向きに回転し、画像が横向き、左上。
図 2. 自然な横向きの前面カメラセンサーからの未加工の画像バッファ。

プレビューの向きがデバイスの向きと一致するように、画像を反時計回りに 270 度回転させる必要があります。

カメラセンサーが縦向きで、画像が直立している。
図 3. 前面カメラの画像バッファを反時計回りに 270 度回転させて、デバイスの縦向きに合わせます。

背面カメラは、前のバッファと同じ向きの画像バッファを生成しますが、SENSOR_ORIENTATION は 90 度です。その結果、バッファは時計回りに 90 度回転します。

デバイスの回転

デバイスの回転とは、デバイスが自然な向きから回転している度数です。たとえば、横向きのスマートフォンの場合、回転方向に応じてデバイスの回転は 90 度または 270 度になります。

カメラ プレビューを直立して表示するには、カメラセンサーの画像バッファを、デバイスの回転と同じ角度(センサーの向きの角度に加えて)だけ回転させる必要があります。

向きの計算

カメラ プレビューの適切な向きは、センサーの向きとデバイスの回転を考慮します。

センサー画像バッファの全体的な回転は、次の式を使用して計算できます。

rotation = (sensorOrientationDegrees - deviceOrientationDegrees * sign + 360) % 360

ここで、sign は前面カメラの場合は 1、背面カメラの場合は -1 です。

前面カメラの場合、画像バッファは(センサーの自然な向きから)反時計回りに回転されます。背面カメラの場合、センサー画像バッファは時計回りに回転します。

式 deviceOrientationDegrees * sign + 360 は、背面カメラのデバイスの回転を反時計回りから時計回りに変換します(たとえば、反時計回りに 270 度を時計回りに 90 度に変換します)。モジュロ演算により、結果が 360 度未満にスケーリングされます(たとえば、540 度の回転を 180 度にスケーリングします)。

API によってデバイスの回転の報告方法が異なります。

  • Display#getRotation() は、デバイスの反時計回りの回転(ユーザーの視点から)を提供します。この値は、前の式にそのまま代入されます。
  • OrientationEventListener#onOrientationChanged() は、デバイスの時計回りの回転(ユーザーの視点から)を返します。前の式で使用する値を否定します。

前面カメラ

カメラ プレビューとセンサーの両方が横向きで、センサーが正しい向きになっています。
図 4. スマートフォンを 90 度回転させて横向きにした場合のカメラ プレビューとセンサー。

図 4 のカメラセンサーによって生成される画像バッファは次のようになります。

横向きのカメラセンサーと、直立した画像。
図 5. スマートフォンを反時計回りに 90 度回転させたときに、前面カメラセンサーによって生成される画像バッファ。

センサーの向きに合わせてバッファを反時計回りに 270 度回転させる必要があります(上記のカメラの向きを参照)。

カメラセンサーが縦向きに回転し、画像が横向き(右上)になっている。
図 6. センサーの向きに合わせて、画像バッファを反時計回りに 270 度回転。

次に、デバイスの回転を考慮して、バッファが反時計回りにさらに 90 度回転されます。その結果、図 4 のカメラ プレビューの向きが正しくなります。

カメラセンサーが横向きに回転し、画像が直立している。
図 7. デバイスの向きに合わせて反時計回りに 90 度回転させた後の画像バッファ。

以下は、カメラを右に回転させて横向きにした場合です。

カメラ プレビューとセンサーはどちらも横向きですが、センサーは上下逆さまです。
図 8. スマートフォンを 270 度(または -90 度)回転させて横向きにした場合のカメラ プレビューとセンサー。

画像バッファは次のとおりです。

カメラセンサーが横向きに回転し、画像が上下逆さまになっている。
図 9. スマートフォンを反時計回りに 270 度回転させたときに、前面カメラセンサーによって生成される画像バッファ。

センサーの向きを調整するには、バッファを反時計回りに 270 度回転させる必要があります。

カメラセンサーが縦向きに回転し、画像が横向き(左上)になっています。
図 10. センサーの向きに合わせて、画像バッファを反時計回りに 270 度回転。

次に、デバイスの回転を考慮して、バッファが反時計回りにさらに 270 度回転されます。

カメラセンサーが横向きに回転し、画像が直立している。
図 11. デバイスの向きに合わせて反時計回りに 270 度回転した後の画像バッファ。

背面カメラ

背面カメラのセンサーの向きは通常 90 度です(デバイスの背面から見た場合)。カメラ プレビューの向きを調整する際、センサー画像バッファはセンサーの回転量だけ時計回りに回転し(前面カメラのように反時計回りではありません)、その後、画像バッファはデバイスの回転量だけ反時計回りに回転します。

カメラ プレビューとセンサーはどちらも横向きですが、センサーは上下逆さまです。
図 12. 背面カメラが横向き(270 度または -90 度回転)のスマートフォン。

図 12 のカメラセンサーからの画像バッファは次のとおりです。

カメラセンサーが横向きに回転し、画像が上下逆さまになっている。
図 13. スマートフォンが横向きの場合に背面カメラセンサーによって生成される画像バッファ。

センサーの向きに合わせて、バッファを時計回りに 90 度回転させる必要があります。

カメラセンサーが縦向きに回転し、画像が横向き(左上)になっています。
図 14. 背面カメラのセンサーの向きに合わせて、画像バッファを時計回りに 90 度回転。

次に、デバイスの回転を考慮して、バッファが反時計回りに 270 度回転されます。

カメラセンサーが横向きに回転し、画像が直立している。
図 15. デバイスの向きに合わせて反時計回りに 270 度回転した後の画像バッファ。

アスペクト比

ディスプレイのアスペクト比は、デバイスの向きが変わったときだけでなく、折りたたみ式デバイスを折りたたんだり開いたりしたとき、マルチウィンドウ環境でウィンドウのサイズが変更されたとき、セカンダリ ディスプレイでアプリが開いたときにも変化します。

カメラセンサーの画像バッファは、UI が動的に向きを変更する際に、ビューファインダー UI 要素の向きとアスペクト比に一致するように向きとスケールを調整する必要があります。このとき、デバイスの向きが変更されるかどうかは関係ありません。

新しいフォーム ファクタや、マルチウィンドウまたはマルチディスプレイ環境で、カメラ プレビューの向きがデバイスと同じ(縦向きまたは横向き)であるとアプリが想定している場合、プレビューの向きやスケーリングが正しく行われない可能性があります。

縦向きのカメラ プレビューが横向きに表示されている、開いた状態の折りたたみ式デバイス。
図 16. 折りたたみ式デバイスが縦向きから横向きのアスペクト比に移行しても、カメラセンサーは縦向きのままです。

図 16 では、アプリがデバイスを反時計回りに 90 度回転したと誤って認識したため、プレビューを同じ角度だけ回転しています。

展開した折りたたみ式デバイスで、カメラ プレビューが直立しているが、スケーリングが正しくないため、押しつぶされている。
図 17. 折りたたみ式デバイスが縦向きから横向きのアスペクト比に移行し、カメラ プレビューが不適切にスケーリングされています。

図 17 では、アプリが画像バッファのアスペクト比を調整していないため、カメラ プレビュー UI 要素の新しいサイズに合わせて適切にスケーリングできません。

向きが固定されたカメラアプリは、通常、折りたたみ式デバイスやノートパソコンなどの大画面デバイスで問題が発生します。

ノートパソコンのカメラ プレビューは直立しているが、アプリの UI が横向きになっている。
図 18. ノートパソコン上の固定縦向きアプリ。

図 18 では、アプリの向きが縦向きのみに制限されているため、カメラアプリの UI が横向きになっています。ビューファインダーの画像が、カメラセンサーに対して正しい向きになっている。

インセット ポートレート モード

マルチ ウィンドウ モード(resizeableActivity="false")をサポートせず、向きを制限する(screenOrientation="portrait" または screenOrientation="landscape")カメラアプリは、大画面デバイスでインセット縦向きモードに配置して、カメラ プレビューの向きを適切に設定できます。

インセット ポートレート モードでは、ディスプレイのアスペクト比が横向きであっても、縦向き専用アプリを縦向きでレターボックス表示(インセット)します。横向き専用アプリは、ディスプレイのアスペクト比が縦向きであっても、横向きでレターボックス表示されます。カメラ画像は、アプリの UI に合わせて回転され、カメラ プレビューのアスペクト比に合わせて切り抜かれ、プレビューに合わせて拡大縮小されます。

インセット ポートレート モードは、カメライメージ センサーのアスペクト比とアプリのメイン アクティビティのアスペクト比が一致しない場合にトリガーされます。

ノートパソコンで、カメラ プレビューとアプリの UI が適切な縦向きで表示されています。
            ワイド プレビュー画像は、縦向きに合わせて拡大縮小され、切り抜かれます。
図 19. ノートパソコンのインセット縦向きモードで表示された、固定向きの縦向きアプリ。

図 19 では、縦向き専用のカメラアプリが回転され、ノートパソコンのディスプレイに UI が直立して表示されています。縦向きのアプリと横向きのディスプレイのアスペクト比の違いにより、アプリはレターボックス表示されます。カメラ プレビュー画像は、アプリの UI の回転(インセット ポートレート モードによる)を補正するために回転され、縦向きに合わせて切り抜かれ、拡大縮小されているため、画角が狭くなっています。

回転、切り抜き、スケーリング

横向きのアスペクト比のディスプレイで、縦向き専用のカメラアプリに対してインセット ポートレート モードが呼び出されます。

ノートパソコンのカメラ プレビューは直立しているが、アプリの UI が横向きになっている。
図 20. ノートパソコン上の固定縦向きアプリ。

アプリは縦向きでレターボックス表示されます。

アプリが縦向きに回転され、レターボックス表示されます。画像が横向きで、上部が右側になっています。
図 21. センサー画像の回転前に、横向きディスプレイで回転され、レターボックス表示された縦向き専用アプリ。

カメラ画像は、アプリの向きの変更に合わせて 90 度回転されます。

センサー画像を 90 度回転させて直立させます。
図 22. カメラの画像が 90 度回転され、向きが変更されたアプリの UI に合わせられます。

画像はカメラ プレビューのアスペクト比に切り抜かれ、プレビューに合わせて拡大縮小されます(画角が狭くなります)。

カメラ プレビューに合わせて拡大縮小された、切り抜かれたカメラ画像。
図 23. カメラ画像が切り抜かれ、縦向きのカメラ プレビューに合わせて拡大縮小されます。

折りたたみ式デバイスでは、ディスプレイのアスペクト比が横向きであるときに、カメラセンサーの向きが縦向きになることがあります。

カメラ プレビューとアプリの UI が、幅の広い展開されたディスプレイの横向きに表示されます。
図 24. 縦向き専用のカメラアプリと、カメラセンサーとディスプレイのアスペクト比が異なる展開状態のデバイス。

カメラ プレビューはセンサーの向きに合わせて回転されるため、ビューファインダーでは画像が正しい向きになりますが、縦向き専用のアプリは横向きになります。

インセット ポートレート モードでは、アプリとカメラ プレビューの向きを正しく設定するために、アプリを縦向きでレターボックス表示するだけで済みます。

折りたたみ式デバイスで、縦向きのレターボックス表示アプリにカメラ プレビューが直立して表示されている。
図 25. 縦向き専用のアプリが、開いた状態の折りたたみ式デバイスでインセット縦向きモードでレターボックス表示されています。

API

Android 12(API レベル 31)以降では、アプリは CaptureRequest クラスの SCALER_ROTATE_AND_CROP プロパティを使用して、インセット縦向きモードを明示的に制御することもできます。

デフォルト値は SCALER_ROTATE_AND_CROP_AUTO で、システムがインセット ポートレート モードを呼び出せるようにします。SCALER_ROTATE_AND_CROP_90 は、前述のインセット ポートレート モードの動作です。

すべてのデバイスがすべての SCALER_ROTATE_AND_CROP 値をサポートしているわけではありません。サポートされている値の一覧を取得するには、CameraCharacteristics#SCALER_AVAILABLE_ROTATE_AND_CROP_MODES をご覧ください。

CameraX

Jetpack CameraX ライブラリを使用すると、センサーの向きとデバイスの回転に対応するカメラ ビューファインダーを簡単に作成できます。

PreviewView レイアウト要素は、センサーの向き、デバイスの回転、スケーリングを自動的に調整して、カメラ プレビューを作成します。PreviewView は、FILL_CENTER スケールタイプを適用して、カメラ画像のアスペクト比を維持します。このスケールタイプは、画像を中央に配置しますが、PreviewView のサイズに合わせて画像を切り抜く場合があります。カメラ画像をレターボックス表示するには、スケールタイプを FIT_CENTER に設定します。

PreviewView を使用してカメラ プレビューを作成する基本については、プレビューを実装するをご覧ください。

完全な実装例については、GitHub の CameraXBasic リポジトリをご覧ください。

CameraViewfinder

Preview ユースケースと同様に、CameraViewfinder ライブラリには、カメラ プレビューの作成を簡素化する一連のツールが用意されています。CameraX Core に依存しないため、既存の Camera2 コードベースにシームレスに統合できます。

Surface を直接使用する代わりに、CameraViewfinder ウィジェットを使用して Camera2 のカメラフィードを表示できます。

CameraViewfinder は、内部で TextureView または SurfaceView を使用してカメラフィードを表示し、必要な変換を適用してビューファインダーを正しく表示します。これには、アスペクト比、スケール、回転の補正が含まれます。

CameraViewfinder オブジェクトからサーフェスをリクエストするには、ViewfinderSurfaceRequest を作成する必要があります。

このリクエストには、CameraCharacteristics からのサーフェス解像度とカメラデバイス情報の要件が含まれています。

requestSurfaceAsync() を呼び出すと、リクエストがサーフェス プロバイダ(TextureView または SurfaceView)に送信され、Surface の ListenableFuture が取得されます。

markSurfaceSafeToRelease() を呼び出すと、サーフェスが不要になり、関連リソースを解放できることがサーフェス プロバイダに通知されます。

fun startCamera() {
    val previewResolution = Size(width, height)
    val viewfinderSurfaceRequest =
        ViewfinderSurfaceRequest(previewResolution, characteristics)
    val surfaceListenableFuture =
        cameraViewfinder.requestSurfaceAsync(viewfinderSurfaceRequest)

    Futures.addCallback(surfaceListenableFuture, object : FutureCallback<Surface> {
        override fun onSuccess(surface: Surface) {
            /* create a CaptureSession using this surface as usual */
        }
        override fun onFailure(t: Throwable) { /* something went wrong */ }
    }, ContextCompat.getMainExecutor(context))
}

SurfaceView

SurfaceView は、プレビューに処理が必要なく、アニメーション化されていない場合に、カメラ プレビューを作成するための簡単なアプローチです。

SurfaceView は、センサーの向きとデバイスの回転の両方を考慮して、ディスプレイの向きに合わせてカメラセンサーの画像バッファを自動的に回転させます。ただし、画像バッファはアスペクト比を考慮せずに SurfaceView のサイズに合わせてスケーリングされます。

画像バッファのアスペクト比が SurfaceView のアスペクト比と一致していることを確認する必要があります。これは、コンポーネントの onMeasure() メソッドで SurfaceView のコンテンツをスケーリングすることで実現できます。

(computeRelativeRotation() のソースコードは、以下の相対回転にあります)。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val width = MeasureSpec.getSize(widthMeasureSpec)
    val height = MeasureSpec.getSize(heightMeasureSpec)

    val relativeRotation = computeRelativeRotation(characteristics, surfaceRotationDegrees)

    if (previewWidth > 0f && previewHeight > 0f) {
        /* Scale factor required to scale the preview to its original size on the x-axis. */
        val scaleX =
            if (relativeRotation % 180 == 0) {
                width.toFloat() / previewWidth
            } else {
                width.toFloat() / previewHeight
            }
        /* Scale factor required to scale the preview to its original size on the y-axis. */
        val scaleY =
            if (relativeRotation % 180 == 0) {
                height.toFloat() / previewHeight
            } else {
                height.toFloat() / previewWidth
            }

        /* Scale factor required to fit the preview to the SurfaceView size. */
        val finalScale = min(scaleX, scaleY)

        setScaleX(1 / scaleX * finalScale)
        setScaleY(1 / scaleY * finalScale)
    }
    setMeasuredDimension(width, height)
}

SurfaceView をカメラ プレビューとして実装する方法について詳しくは、カメラの向きをご覧ください。

TextureView

TextureView は SurfaceView よりもパフォーマンスが低く、作業量も多くなりますが、TextureView を使用すると、カメラ プレビューを最大限に制御できます。

TextureView は、センサーの向きに基づいてセンサー画像バッファを回転させますが、デバイスの回転やプレビューのスケーリングは処理しません。

スケーリングと回転は、Matrix 変換でエンコードできます。TextureView を正しく拡大縮小、回転する方法については、カメラアプリでサイズ変更可能なサーフェスをサポートするをご覧ください。

相対回転

カメラセンサーの相対回転は、カメラセンサーの出力をデバイスの向きに合わせるために必要な回転量です。

相対回転は、SurfaceView や TextureView などのコンポーネントで、プレビュー画像の X と Y のスケーリング ファクタを決定するために使用されます。また、センサー画像バッファの回転を指定するためにも使用されます。

CameraCharacteristics クラスと Surface クラスを使用すると、カメラセンサーの相対的な回転を計算できます。

/**
 * Computes rotation required to transform the camera sensor output orientation to the
 * device's current orientation in degrees.
 *
 * @param characteristics The CameraCharacteristics to query for the sensor orientation.
 * @param surfaceRotationDegrees The current device orientation as a Surface constant.
 * @return Relative rotation of the camera sensor output.
 */
public fun computeRelativeRotation(
    characteristics: CameraCharacteristics,
    surfaceRotationDegrees: Int
): Int {
    val sensorOrientationDegrees =
        characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0

    // Reverse device orientation for back-facing cameras.
    val sign = if (characteristics.get(CameraCharacteristics.LENS_FACING) ==
        CameraCharacteristics.LENS_FACING_FRONT
    ) 1 else -1

    // Calculate desired orientation relative to camera orientation to make
    // the image upright relative to the device orientation.
    return (sensorOrientationDegrees - surfaceRotationDegrees * sign + 360) % 360
}

ウィンドウ指標

画面サイズは、カメラのビューファインダーのサイズを決定するために使用しないでください。カメラアプリは、モバイル デバイスのマルチウィンドウ モードまたは ChromeOS のフリーフォーム モードで、画面の一部で実行されている可能性があります。

WindowManager#getCurrentWindowMetrics()(API レベル 30 で追加)は、画面のサイズではなく、アプリケーション ウィンドウのサイズを返します。Jetpack WindowManager ライブラリ メソッドの WindowMetricsCalculator#computeCurrentWindowMetrics() と WindowInfoTracker#currentWindowMetrics() は、API レベル 14 との下位互換性がある同様のサポートを提供します。

180 度回転

デバイスを 180 度回転させても(たとえば、自然な向きから上下逆の自然な向きに回転させても)、onConfigurationChanged() コールバックはトリガーされません。その結果、カメラのプレビューが上下逆になることがあります。

180 度の回転を検出するには、DisplayListener を実装し、onDisplayChanged() コールバックで Display#getRotation() を呼び出してデバイスの回転を確認します。

限定リソース

Android 10 より前は、マルチ ウィンドウ環境で最上位に表示されるアクティビティのみが RESUMED 状態でした。どのアプリが再開されたのかをシステムが示さないため、ユーザーは混乱していました。

Android 10(API レベル 29)では、表示されているすべてのアクティビティが RESUMED 状態になるマルチレジュームが導入されました。表示されているアクティビティは、たとえば、別の透明なアクティビティが上にある場合や、フォーカス可能でない場合(ピクチャー イン ピクチャー モードなど。ピクチャー イン ピクチャーのサポートをご覧ください)には、PAUSED 状態になることもあります。

API レベル 29 以降でカメラ、マイク、または排他的リソースやシングルトン リソースを使用するアプリは、マルチレジュームをサポートする必要があります。たとえば、再開された 3 つのアクティビティがカメラを使用する場合、この排他的リソースにアクセスできるのは 1 つだけです。各アクティビティは、優先度の高いアクティビティによるカメラへのプリエンプティブ アクセスを認識できるように、onDisconnected() コールバックを実装する必要があります。

詳細については、マルチレジュームをご覧ください。

参考情報