カメラ プレビュー

注: このページでは、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 の前面カメラの場合、カメラセンサーによって生成される画像バッファは次のようになります。

画像が横向き、左上に回転したカメラセンサー。

プレビューの向きをデバイスの向きに合わせ、画像を反時計回りに 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() は、ユーザーの視点からデバイスの時計回りの回転を返します。上の数式で使用する値を否定します。

前面カメラ

横向きのカメラ プレビューとセンサー、センサーは右向き。
図 2. スマートフォンを 90 度回転させて横向きにしたカメラ プレビューとセンサー。

図 2 のカメラセンサーによって生成された画像バッファは次のとおりです。

横向きのカメラセンサーで、画像が縦向きになっている。

センサーの向きを調整するには、バッファを反時計回りに 270 度回転する必要があります(上記のカメラの向きをご覧ください)。

カメラセンサーが縦向きに回転し、画像が横向きになっている(右上)。

次に、デバイスの回転に合わせて、バッファが反時計回りにさらに 90 度回転します。これにより、図 2 のカメラ プレビューの正しい向きになります。

カメラセンサーが横向きに回転し、画像が縦向きになっている。

ここでは、カメラを右に向け、横向きに向けています。

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

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

カメラセンサーが横向きに回転し、画像が上下が反転している。

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

カメラセンサーの評価が縦向き、画像が横向き、左上。

次に、デバイスの回転に合わせて、バッファが反時計回りにさらに 270 度回転します。

カメラセンサーが横向きに回転し、画像が縦向きになっている。

背面カメラ

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

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

カメラセンサーからの画像バッファを図 4 に示します。

カメラセンサーが横向きに回転し、画像が上下が反転している。

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

カメラセンサーの評価が縦向き、画像が横向き、左上。

次に、デバイスの回転に合わせて、バッファが反時計回りに 270 度回転します。

カメラセンサーが横向きに回転し、画像が縦向きになっている。

アスペクト比

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

カメラセンサーの画像バッファの向きとスケーリングは、デバイスの向きが変わっても UI が動的に向きが変わるため、ビューファインダー UI 要素の向きとアスペクト比に合わせて調整する必要があります。

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

縦向きカメラ プレビューが横向きになっている、広げた折りたたみ式デバイス。
図 5.折りたたみ式デバイスは縦向きから横向きにアスペクト比を移行しますが、カメラセンサーは縦向きのままになります。

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

カメラ プレビューが表示された、広げた状態の折りたたみ式デバイス。ただし、スケーリングが不正確であるために押しつぶされた。
図 6.折りたたみ式デバイスは縦向きから横向きにアスペクト比を移行しますが、カメラセンサーは縦向きのままになります。

図 6 では、カメラ プレビュー UI 要素の新しい寸法に合わせて適切に拡大縮小できるように、アプリで画像バッファのアスペクト比を調整していません。

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

ノートパソコンのカメラ プレビューは縦向きですが、アプリの UI は横向きです。
図 7. ノートパソコン上の固定向きの縦向きアプリ。

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

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

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

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

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

ノートパソコン上の適切な縦向きでのカメラ プレビューとアプリ UI。
            横長のプレビュー画像は、縦向きに合わせて拡大縮小され、トリミングされます。
図 8. ノートパソコンのインセット縦向きモードに固定された向きの縦向きアプリ。

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

回転、切り抜き、拡大縮小

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

ノートパソコンのカメラ プレビューは縦向きですが、アプリの UI は横向きです。
図 9. ノートパソコン上の向きが固定された縦向きアプリ。

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

アプリが縦向きに回転し、レターボックス表示されます。画像は横向き、右上から始まります。

アプリの向きに合わせてカメラの画像が 90 度回転します。

センサー画像が縦向きになるように 90 度回転しました。

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

切り抜かれたカメラ画像を、カメラ プレビューに合わせて拡大縮小しました。

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

広げたワイドなディスプレイの横向き表示で、カメラ プレビューとアプリの UI が表示される。
図 10. 縦向き専用のカメラアプリを備え、アスペクト比が異なるカメラセンサーとディスプレイを備えた、広げた状態のデバイス。

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

インセットの縦向きモードでは、アプリを縦向きにレターボックス表示するだけで、アプリとカメラのプレビューの向きを適切に調整できます。

折りたたみ式デバイスでレターボックス表示のアプリが縦向きで、カメラ プレビューが直立している。

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 ライブラリには、カメラ プレビューの作成を簡素化するツールセットが用意されています。CameraX Core に依存しないため、既存の Camera2 コードベースにシームレスに統合できます。

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

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

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

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

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

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

Kotlin

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

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

Java

    void startCamera(){
        Size previewResolution = new Size(width, height);
        ViewfinderSurfaceRequest viewfinderSurfaceRequest =
                new ViewfinderSurfaceRequest(previewResolution, characteristics);
        ListenableFuture surfaceListenableFuture =
                cameraViewfinder.requestSurfaceAsync(viewfinderSurfaceRequest);

        Futures.addCallback(surfaceListenableFuture, new FutureCallback() {
            @Override
            public void onSuccess(Surface result) {
                /* create a CaptureSession using this surface as usual */
            }
            @Override public void onFailure(Throwable t) { /* something went wrong */}
        },  ContextCompat.getMainExecutor(context));
    }

SurfaceView

プレビューに処理が不要で、アニメーションを使用しない場合は、SurfaceView で簡単にカメラ プレビューを作成できます。

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

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

computeRelativeRotation() ソースコードは、以下の相対ローテーションにあります)。

Kotlin

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)
}

Java

@Override
void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);

    int relativeRotation = computeRelativeRotation(characteristics, surfaceRotationDegrees);

    if (previewWidth > 0f && previewHeight > 0f) {

        /* Scale factor required to scale the preview to its original size on the x-axis. */
        float scaleX = (relativeRotation % 180 == 0)
                       ? (float) width / previewWidth
                       : (float) width / previewHeight;

        /* Scale factor required to scale the preview to its original size on the y-axis. */
        float scaleY = (relativeRotation % 180 == 0)
                       ? (float) height / previewHeight
                       : (float) height / previewWidth;

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

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

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

TextureView

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

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

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

相対回転

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

相対回転は、SurfaceViewTextureView などのコンポーネントで使用され、プレビュー画像の x スケーリング ファクタと y スケーリング ファクタが決定されます。また、センサー イメージ バッファの回転の指定にも使用されます。

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

Kotlin

/**
 * 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)!!

    // 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
}

Java

/**
 * 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 int computeRelativeRotation(
    CameraCharacteristics characteristics,
    int surfaceRotationDegrees
){
    Integer sensorOrientationDegrees =
        characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);

    // Reverse device orientation for back-facing cameras.
    int sign = characteristics.get(CameraCharacteristics.LENS_FACING) ==
        CameraCharacteristics.LENS_FACING_FRONT ? 1 : -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() コールバックを実装する必要があります。

詳細については、複数のアプリの再開をご覧ください。

参考情報