カメラの向き

Android アプリでカメラを使用する場合は、向きを処理する際に特別な考慮事項があります。このドキュメントは、Android camera2 API の基本コンセプトを理解していることを前提としています。camera2 の概要については、ブログ投稿または概要をご覧ください。また、このドキュメントを読む前に、まずカメラアプリを書いてみることをおすすめします。

背景

Android カメラアプリで画面の向きを処理するのは難しく、次の要素を考慮する必要があります。

  • 自然な向き: デバイスがデバイス設計の「通常」の位置にある場合のディスプレイの向き。通常、スマートフォンでは縦向き、ノートパソコンでは横向きです。
  • センサーの向き: デバイスに物理的に取り付けられたセンサーの向き。
  • ディスプレイの回転: 自然な向きからデバイスが物理的にどれだけ回転しているか。
  • ビューファインダーのサイズ: カメラのプレビューを表示するために使用されるビューファインダーのサイズ。
  • カメラが出力する画像サイズ。

これらの要素が組み合わさることで、カメラアプリの UI とプレビューの構成の可能性が大幅に増えます。このドキュメントは、デベロッパーがこれらの問題を回避し、Android アプリでカメラの向きを正しく処理する方法を示すことを目的としています。

わかりやすくするために、特に記載がない限り、すべての例で背面カメラを使用しているとします。また、以下の写真はすべて、説明をわかりやすくするためにシミュレートされています。

向きについて

自然な向き

自然な向きは、デバイスが通常想定される位置にある場合のディスプレイの向きとして定義されます。スマートフォンの場合、自然な向きは縦向きであることが多いです。つまり、スマートフォンの幅は短く、高さは長くなります。ノートパソコンの場合、自然な向きは横向きです。つまり、幅が広く、高さが低くなります。タブレットはこれよりも少し複雑で、縦向きと横向きの両方に対応できます。

スマートフォン、ノートパソコン、オブザーバー側のオブジェクトの自然な向きのイラスト

センサーの向き

正式に言うと、センサーの向きは、センサーからの出力画像をデバイスの自然な向きに合わせるために時計回りに回転させる必要がある度数で測定されます。つまり、センサーの向きとは、センサーがデバイスに取り付けられる前に反時計回りに回転する角度のことです。画面を見ると、回転は時計回りのように見えます。これは、背面カメラのセンサーがデバイスの「背面」に取り付けられているためです。

Android 10 互換性定義 7.5.5 カメラの向きによると、前面カメラと背面カメラは「カメラの長辺が画面の長辺と揃うように向きを調整しなければなりません」。

カメラからの出力バッファは横向きサイズです。スマートフォンの自然な向きは通常縦向きであるため、出力バッファの長辺が画面の長辺と一致するように、センサーの向きは通常、自然な向きから 90 度または 270 度回転しています。Chromebook など、自然な向きが横向きのデバイスでは、センサーの向きが異なります。これらのデバイスでは、出力バッファの長辺が画面の長辺と一致するように、画像センサーが配置されています。どちらも横向きサイズであるため、向きが一致し、センサーの向きは 0 度または 180 度になります。

スマートフォン、ノートパソコン、オブザーバー側のオブジェクトの自然な向きのイラスト

次の図は、デバイスの画面を見ている観察者の視点から見た様子を示しています。

スマートフォン、ノートパソコン、物体を観察者側から見たセンサーの向きのイラスト

次のシーンを考えてみましょう。

かわいい Android フィギュア(バグドロイド)のシーン

スマートフォン ノートパソコン
スマートフォンの背面カメラ センサーを通して見た画像のイラスト ノートパソコンの背面カメラ センサーから見た画像のイラスト

スマートフォンのセンサーの向きは通常 90 度または 270 度であるため、センサーの向きを考慮しないと、次のような画像が取得されます。

スマートフォン ノートパソコン
スマートフォンの背面カメラ センサーを通して見た画像のイラスト ノートパソコンの背面カメラ センサーから見た画像のイラスト

反時計回りのセンサーの向きが変数 sensorOrientation に格納されているとします。センサーの向きを補正するには、出力バッファを `sensorOrientation` 時計回りに回転させて、向きをデバイスの自然な向きに合わせる必要があります。

Android では、アプリは TextureView または SurfaceView を使用してカメラ プレビューを表示できます。アプリが正しく使用すれば、どちらもセンサーの向きを処理できます。センサーの向きを考慮する方法については、以降のセクションで詳しく説明します。

ディスプレイの回転

ディスプレイの回転は、画面に描画されるグラフィックの回転として正式に定義されます。これは、デバイスの自然な向きからの物理的な回転とは逆の方向です。以降のセクションでは、ディスプレイの回転がすべて 90 の倍数であることを前提としています。ディスプレイの回転を絶対角度で取得する場合は、{0, 90, 180, 270} のうち最も近い値に切り上げます。

以下のセクションの「ディスプレイの向き」は、デバイスが物理的に横向きまたは縦向きで保持されているかどうかを指し、「ディスプレイの回転」とは異なります。

次の図に示すように、デバイスを以前の位置から反時計回りに 90 度回転させるとします。

スマートフォン、ノートパソコン、オブザーバー側のオブジェクトが 90 度回転したディスプレイのイラスト

出力バッファがセンサーの向きに基づいてすでに回転されているとすると、出力バッファは次のようになります。

スマートフォン ノートパソコン
スマートフォンの背面カメラ センサーを通して見た画像のイラスト ノートパソコンの背面カメラ センサーから見た画像のイラスト

ディスプレイの回転が変数 displayRotation に保存されている場合、正しい画像を取得するには、出力バッファを displayRotation の値だけ反時計回りに回転させる必要があります。

前面カメラの場合、ディスプレイの回転は画面に対して逆方向に画像バッファに作用します。前面カメラを扱う場合は、バッファを displayRotation の値だけ時計回りに回転させる必要があります。

注意点

ディスプレイの回転は、デバイスの反時計回りの回転を測定します。これは、すべての向き/回転 API に当てはまるわけではありません。

次に例を示します。

ここで重要なのは、ディスプレイの回転は自然な向きを基準にしているということです。たとえば、スマートフォンを 90 度または 270 度回転させると、画面は横長になります。一方、ノートパソコンを同じ角度で回転させると、画面は縦長になります。アプリは常にこの点を考慮し、デバイスの自然な向きについて想定してはなりません。

前の図を使用して、向きと回転について説明します。

スマートフォンとノートパソコンが回転していない、オブジェクトを含む向きの組み合わせのイラスト

スマートフォン ノートパソコン
自然な向き = 縦向き 自然な向き = 横向き
センサーの向き = 90 センサーの向き = 0
ディスプレイの回転 = 0 ディスプレイの回転 = 0
ディスプレイの向き = 縦向き ディスプレイの向き = 横向き

スマートフォンとノートパソコンが回転していない、オブジェクトを含む向きの組み合わせのイラスト

スマートフォン ノートパソコン
自然な向き = 縦向き 自然な向き = 横向き
センサーの向き = 90 センサーの向き = 0
ディスプレイの回転 = 90 ディスプレイの回転 = 90
ディスプレイの向き = 横向き ディスプレイの向き = 縦向き

ビューファインダーのサイズ

アプリは、画面の向き、回転、解像度に基づいて常にビューファインダーのサイズを変更する必要があります。一般に、アプリはビューファインダーの向きを現在のディスプレイの向きと同じにする必要があります。つまり、アプリはビューファインダーの長辺を画面の長辺に合わせる必要があります。

カメラ別の画像出力サイズ

プレビューの画像出力サイズを選択する際は、可能な限り、ファインダーのサイズと同じか、それより少し大きいサイズを選択する必要があります。通常、出力バッファを拡大するとピクセル化が発生するため、拡大しないようにします。また、サイズが大きすぎると、パフォーマンスが低下し、バッテリーの使用量が増える可能性があります。

JPEG の向き

まずは、JPEG 写真をキャプチャするという一般的な状況から始めましょう。camera2 API では、キャプチャ リクエストで JPEG_ORIENTATION を渡して、出力 JPEG を時計回りに回転させる量を指定できます。

前述した内容を簡単にまとめます。

  • センサーの向きを処理するには、画像バッファを時計回りに sensorOrientation 回転させる必要があります。
  • ディスプレイの回転を処理するには、背面カメラの場合はバッファを反時計回りに displayRotation 回転させ、前面カメラの場合は時計回りに回転させる必要があります。

2 つの要素を合計すると、時計回りに回転させる量は

  • 背面カメラの sensorOrientation - displayRotation
  • 前面カメラの場合は sensorOrientation + displayRotation

このロジックのサンプルコードについては、JPEG_ORIENTATION のドキュメントをご覧ください。ドキュメントのサンプルコードの deviceOrientation は、デバイスの時計回りの回転を使用しています。そのため、ディスプレイの回転の符号が反転します。

プレビュー

カメラのプレビューはどうですか?アプリでカメラのプレビューを表示する方法は、SurfaceView と TextureView の 2 つが主な方法です。それぞれで、向きを正しく処理するための異なるアプローチが必要です。

SurfaceView

一般に、プレビュー バッファの処理やアニメーション化が必要ない場合は、カメラ プレビューに SurfaceView を使用することをおすすめします。TextureView よりもパフォーマンスが高く、リソースの消費も少なくなります。

SurfaceView はレイアウトも比較的簡単です。カメラ プレビューを表示する SurfaceView のアスペクト比のみを考慮する必要があります。

ソース

Android プラットフォームは、SurfaceView の下で出力バッファを回転させて、デバイスのディスプレイの向きに合わせます。つまり、センサーの向きディスプレイの回転の両方を考慮します。簡単に言うと、ディスプレイが横向きの場合は横向きのプレビューが表示され、縦向きの場合は縦向きのプレビューが表示されます。

次の表にこれを示します。ここで重要なのは、ディスプレイの回転だけではソースの向きは決まらないということです。

ディスプレイの回転 スマートフォン(自然な向き = 縦向き) ノートパソコン(自然な向き = 横向き)
0 バグドロイドの頭が上を向いている縦長の画像 横長の画像。バグドロイドの頭が上を向いている
90 横長の画像。バグドロイドの頭が上を向いている バグドロイドの頭が上を向いている縦長の画像
180 バグドロイドの頭が上を向いている縦長の画像 横長の画像。バグドロイドの頭が上を向いている
270 横長の画像。バグドロイドの頭が上を向いている バグドロイドの頭が上を向いている縦長の画像

レイアウト

ご覧のとおり、SurfaceView はすでにいくつかの難しい処理を代行してくれます。ただし、ファインダーのサイズ、つまり画面上のプレビューのサイズを考慮する必要があります。SurfaceView は、そのサイズに合わせてソースバッファを自動的にスケーリングします。ビューファインダーのアスペクト比が sourcebuffer のアスペクト比と同一であることを確認する必要があります。たとえば、縦長のプレビューを横長の SurfaceView に合わせようとすると、次のように歪んだ画像になります。

縦長のプレビューを横長のファインダーに収めた結果、伸びたバグドロイドが表示されている様子を示すイラスト

一般的に、ビューファインダーのアスペクト比(幅/高さ)は、ソースのアスペクト比と同一であることが望ましいです。ビューファインダーで画像をクリップしたくない場合(表示を修正するために一部のピクセルを切り取る場合)、aspectRatioActivityaspectRatioSource より大きい場合と aspectRatioActivityaspectRatioSource 以下の場合の 2 つのケースを考慮する必要があります。

aspectRatioActivity > aspectRatioSource

ケースはアクティビティよりも「広範囲」であると考えることができます。以下では、16:9 のアクティビティと 4:3 のソースがある例について説明します。

aspectRatioActivity = 16/9 ≈ 1.78
aspectRatioSource = 4/3 ≈ 1.33

まず、ファインダーも 4:3 にします。次に、ソースとビューファインダーをアクティビティに次のように合わせます。

内部のファインダーのアスペクト比よりも大きいアスペクト比のアクティビティのイラスト

この場合、ビューファインダーの高さはアクティビティの高さに合わせ、ビューファインダーのアスペクト比はソースのアスペクト比と同じにする必要があります。擬似コードは次のとおりです。

viewfinderHeight = activityHeight;
viewfinderWidth = activityHeight * aspectRatioSource;
aspectRatioActivity ≤ aspectRatioSource

もう 1 つのケースは、アクティビティが「狭い」または「高い」場合です。前の例を再利用できます。ただし、次の例ではデバイスを 90 度回転させているため、アクティビティは 9:16、ソースは 3:4 になります。

aspectRatioActivity = 9/16 = 0.5625
aspectRatioSource = 3/4 = 0.75

この場合、ソースとビューファインダーをアクティビティに次のように合わせます。

アスペクト比が内部のファインダーのアスペクト比よりも小さいアクティビティの図

ビューファインダーのアスペクト比をソースのアスペクト比と同じにしながら、ビューファインダーの幅をアクティビティの幅(前のケースの高さとは対照的)に合わせる必要があります。擬似コード:

viewfinderWidth = activityWidth;
viewfinderHeight = activityWidth / aspectRatioSource;
クリッピング

Camera2 サンプルの AutoFitSurfaceView.kt(github)は、SurfaceView をオーバーライドし、両方の次元でアクティビティと同じか「わずかに大きい」画像を使用してアスペクト比の不一致を処理し、オーバーフローしたコンテンツを切り抜きます。これは、プレビューでアクティビティ全体をカバーしたり、固定サイズのビューを画像が歪むことなく完全に埋めたりしたいアプリに便利です。

注意点

上記のサンプルでは、プレビューをアクティビティより少し大きくして、画面のスペースを最大限に活用し、空白が残らないようにしています。これは、オーバーフローした部分がデフォルトで親レイアウト(または ViewGroup)によってクリップされるという事実に基づいています。この動作は RelativeLayout および LinearLayout とは一致しますが、ConstraintLayout とは一致しません。ConstraintLayout は、レイアウト内に収まるように子ビューのサイズを変更することがあります。これにより、意図した「センター クロップ」効果が失われ、プレビューが引き伸ばされてしまいます。このコミットを参照してください。

TextureView

TextureView はカメラ プレビューのコンテンツを最大限に制御できますが、パフォーマンスのコストがかかります。また、カメラのプレビューを適切に表示するには、より多くの作業が必要になります。

ソース

Android プラットフォームは、TextureView の下で、センサーの向きに応じて出力バッファを回転させ、デバイスの自然な向きに合わせます。TextureView はセンサーの向きを処理しますが、ディスプレイの回転は処理しません。出力バッファはデバイスの自然な向きに合わせられるため、表示の回転は自分で処理する必要があります。

次の表にこれを示します。図形を対応するディスプレイの回転で回転させてみてください。SurfaceView で同じ図形が得られます。

ディスプレイの回転 スマートフォン(自然な向き = 縦向き) ノートパソコン(自然な向き = 横向き)
0 バグドロイドの頭が上を向いている縦長の画像 横長の画像。バグドロイドの頭が上を向いている
90 バグドロイドの頭が右を向いている縦長の画像 バグドロイドの頭が右を向いている横長の画像
180 横長の画像。バグドロイドの頭が上を向いている バグドロイドの頭が上を向いている縦長の画像
270 横長の画像。バグドロイドの頭が上を向いている バグドロイドの頭が上を向いている縦長の画像

レイアウト

TextureView の場合、レイアウトは少し複雑になります。以前は TextureView に変換行列を使用することが推奨されていましたが、この方法はすべてのデバイスで機能するわけではありません。代わりに、こちらの手順をお試しください。

TextureView でプレビューを正しくレイアウトする 3 ステップのプロセスは次のとおりです。

  1. TextureView のサイズを、選択したプレビュー サイズと同じになるように設定します。
  2. 引き伸ばされる可能性のある TextureView をプレビューの元の寸法にスケールバックします。
  3. TextureView を反時計回りに displayRotation 回転させます。

ディスプレイの回転が 90 度のスマートフォンがあるとします。

ディスプレイが 90 度回転し、オブジェクトが表示されているスマートフォンのイラスト

1. TextureView のサイズを、選択したプレビュー サイズと同じになるように設定

選択したプレビュー サイズが previewWidth × previewHeight で、previewWidth > previewHeight(センサー出力は本来横長)であるとします。キャプチャ セッションを構成する際は、SurfaceTexture#setDefaultBufferSize(int width, height) を呼び出してプレビュー サイズ(previewWidth × previewHeight)を指定する必要があります。

setDefaultBufferSize を呼び出す前に、View#setLayoutParams(android.view.ViewGroup.LayoutParams) を使用して TextureView のサイズを`previewWidth × previewHeight`に設定することも重要です。これは、TextureView が測定された幅と高さで SurfaceTexture#setDefaultBufferSize(int width, height) を呼び出すためです。TextureView のサイズが事前に明示的に設定されていない場合、競合状態が発生する可能性があります。この問題は、最初に TextureView のサイズを明示的に設定することで軽減されます。

この場合、TextureView のサイズがソースのサイズと一致しないことがあります。スマートフォンの場合、ソースは縦長ですが、先ほど設定した layoutParams により TextureView は横長になります。この場合、プレビューは次のように引き伸ばされます。

選択したプレビュー サイズと同じサイズの TextureView に合わせて引き伸ばされた縦長のプレビューの画像イラスト

2. 引き伸ばされる可能性のある TextureView をプレビューの元の寸法に戻します。

ストレッチされたプレビューを元のサイズに戻すには、次の点を考慮してください。

ソースのディメンション(sourceWidth × sourceHeight)は次のとおりです。

  • previewHeight × previewWidth(自然な向きが縦向きまたは逆縦向きの場合(センサーの向きが 90 度または 270 度の場合))
  • previewWidth × previewHeight(自然な向きが横向きまたは逆横向きの場合(センサーの向きが 0 度または 180 度の場合))。

View#setScaleX(float)View#setScaleY(float) を利用してストレッチを修正

  • setScaleX(sourceWidth / previewWidth)
  • setScaleY(sourceHeight / previewHeight)

ストレッチされたプレビューが元の寸法に縮小される手順を示す画像イラスト

3. プレビューを `displayRotation` 反時計回りに回転します。

前述のとおり、ディスプレイの回転を補正するために、プレビューを displayRotation 反時計回りに回転させる必要があります。

この操作は View#setRotation(float) で行うことができます。

  • 時計回りの回転を行うため、setRotation(-displayRotation) を使用します。

デバイスのディスプレイの向きに合わせてプレビューを回転させる手順を示す画像イラスト

サンプル
  • Jetpack の cameraxPreviewView は、前述のように TextureView のレイアウトを処理します。PreviewCorrector を使用して変換を構成します。

注: コードで TextureView の変換行列を以前に使用したことがある場合、Chromebook などのネイティブな横向きデバイスではプレビューが正しく表示されないことがあります。変換行列がセンサーの向きを 90 度または 270 度と誤って想定している可能性があります。回避策については、GitHub のこのコミットを参照してください。ただし、代わりに、ここで説明する方法を使用するようにアプリを移行することを強くおすすめします。