CameraX ユースケースの回転

このトピックでは、アプリ内で CameraX のユースケースを設定して、正しい回転情報が指定された画像を取得する方法を示します。ImageAnalysisImageCapture のどちらのユースケースにも該当します。それによって次のようになります。

  • ImageAnalysis ユースケースの Analyzer は正しい回転が指定されたフレームを受信します。
  • ImageCapture ユースケースは正しい回転で写真を撮影します。

用語

このトピックでは以下の用語を使用します。各用語の意味を理解することが重要です。

ディスプレイの向き
デバイスのどの側が上向きであるかを示します。縦向き、横向き、逆の縦向き、逆の横向きの 4 つのいずれかの値になります。
ディスプレイの回転
これは Display.getRotation() によって返される値であり、デバイスが自然な向きから反時計回りに回転する度数を表します。
ターゲットの回転
これは、自然な向きに達するまでにデバイスを時計回りに回転させる度数を表します。

ターゲットの回転を決定する方法

次の例は、自然な向きに基づいて、デバイスのターゲットの回転を決定する方法を示しています。

例 1: 自然な縦向き

デバイスの例: Pixel 3 XL

自然な向き = 縦向き
現在の向き = 縦向き

ディスプレイの回転 = 0
ターゲットの回転 = 0

自然な向き = 縦向き
現在の向き = 横向き

ディスプレイの回転 = 90
ターゲットの回転 = 90

例 2: 自然な横向き

デバイスの例: Pixel C

自然な向き = 横向き
現在の向き = 横向き

ディスプレイの回転 = 0
ターゲットの回転 = 0

自然な向き = 横向き
現在の向き = 縦向き

ディスプレイの回転 = 270
ターゲットの回転 = 270

画像の回転

どちらの側が上向きでしょうか。Android では、センサーの向きは、デバイスが自然な位置にある状態で、デバイスの上部からセンサーが回転する度数(0、90、180、270)を表す定数値として定義されます。図のすべてのケースで、画像の回転は、画像が縦向きに見えるまで時計回りにどれだけ回転させるかのデータを示しています。

次の例は、カメラセンサーの向きに応じて画像をどれだけ回転させるかを示しています。また、ターゲットの回転はディスプレイの回転に設定されていることが前提になります。

例 1: センサーを 90 度回転させる

デバイスの例: Pixel 3 XL

ディスプレイの回転 = 0
ディスプレイの向き = 縦向き
画像の回転 = 90

ディスプレイの回転 = 90
ディスプレイの向き = 横向き
画像の回転 = 0

例 2: センサーを 270 度回転させる

デバイスの例: Nexus 5X

ディスプレイの回転 = 0
ディスプレイの向き = 縦向き
画像の回転 = 270

ディスプレイの回転 = 90
ディスプレイの向き = 横向き
画像の回転 = 180

例 3: センサーを 0 度回転させる

デバイスの例: Pixel C(タブレット)

ディスプレイの回転 = 0
ディスプレイの向き = 横向き
画像の回転 = 0

ディスプレイの回転 = 270
ディスプレイの向き = 縦向き
画像の回転 = 90

画像の回転を計算する

ImageAnalysis

ImageAnalysisAnalyzer は、カメラから画像を ImageProxy の形式で受け取ります。各画像には回転情報が含まれています。これは次のように確認できます。

val rotation = imageProxy.imageInfo.rotationDegrees

この値は、ImageAnalysis のターゲットの回転に合わせて画像を時計回りに回転させる必要がある度数を表します。Android アプリのコンテキストでは、ImageAnalysis のターゲットの回転は通常、画面の向きと一致します。

ImageCapture

キャプチャ結果が準備完了になったことを通知するために、ImageCapture インスタンスにコールバックが追加されています。それにより、キャプチャされた画像またはエラーが返されます。

写真を撮る場合、提供されるコールバックは次のいずれかのタイプになります。

  • OnImageCapturedCallback: ImageProxy の形式で、メモリ内アクセスが可能な画像を受け取ります。
  • OnImageSavedCallback: キャプチャした画像が、ImageCapture.OutputFileOptions で指定された場所に正常に保存されている場合に呼び出されます。オプションには、FileOutputStream または MediaStore 内のロケーションを指定できます。

キャプチャした画像の回転は、形式(ImageProxyFileOutputStreamMediaStore Uri)にかかわらず、キャプチャした画像を ImageCapture のターゲットの回転に合わせて時計回りに回転させる必要がある度数を表します。Android アプリのコンテキストでは、通常は画面の向きと一致します。

キャプチャした画像の回転は、次のいずれかの方法で取得できます。

ImageProxy

val rotation = imageProxy.imageInfo.rotationDegrees

File

val exif = Exif.createFromFile(file)
val rotation = exif.rotation

OutputStream

val byteArray = outputStream.toByteArray()
val exif = Exif.createFromInputStream(ByteArrayInputStream(byteArray))
val rotation = exif.rotation

MediaStore uri

val inputStream = contentResolver.openInputStream(outputFileResults.savedUri)
val exif = Exif.createFromInputStream(inputStream)
val rotation = exif.rotation

画像の回転を確認する

ImageAnalysisImageCapture のユースケースは、キャプチャが正常にリクエストされた後にカメラから ImageProxy を受け取ります。ImageProxy は、画像とそれに関する情報(回転を含む)をラップします。この回転情報は、ユースケースのターゲット回転に合わせて画像を回転させる必要がある度数を表します。

画像の回転の確認フロー

ImageCapture / ImageAnalysis ターゲット回転ガイドライン

多くのデバイスはデフォルトで逆の縦向きまたは逆の横向きに回転できないため、これらの向きをサポートしていない Android アプリもあります。そのため、ユースケースのターゲット回転の更新方法は、アプリがこれをサポートしているかどうかによって変わります。

以下の 2 つの表は、ユースケースのターゲット回転とディスプレイの回転を同期する方法を示しています。最初の表は、4 つすべての向きをサポートしながらこの同期を行う方法を示しています。2 つ目の表は、デフォルトでデバイスが回転する向きだけを示しています。

アプリ内で従うガイドラインを選択するには:

  1. アプリのカメラ Activity で向きがロックされているかどうか、また向きの設定変更がオーバーライドされるかどうかを確認します。

  2. アプリのカメラの Activity がデバイスの 4 つの向き(縦向き、逆の縦向き、横向き、逆の横向き)すべてを処理するか、デバイスでデフォルトでサポートされている向きだけを処理するかを決定します。

4 つすべての画面の向きをサポートする

この表は、デバイスが逆の縦向きに回転しない場合のガイドラインを示しています。逆の横向きに回転しないデバイスについても同様に適用できます。

シナリオ ガイドライン 単一ウィンドウ モード マルチウィンドウ分割画面モード
画面の向きのロックを解除 Activity が作成されるたびにユースケースを設定します(ActivityonCreate() コールバックなど)。
OrientationEventListeneronOrientationChanged() を使用します。 コールバック内で、ユースケースのターゲット回転を更新します。デバイスが 180 度回転された場合など、向きの変更後も Activity が再作成されない場合に適用されます。 ディスプレイが逆の縦向きであるときに、デバイスがデフォルトで逆の縦向きに回転しない場合にも適用されます。 また、デバイスが(たとえば 90 度)回転しても Activity が再作成されない場合にも適用されます。これは、アプリが画面の半分を占めるようなフォーム ファクタが小さいデバイスや、3 分の 2 を占めるフォーム ファクタが大きいデバイスに該当します。
省略可: AndroidManifest ファイルで、ActivityscreenOrientation プロパティを fullSensor に設定します。 それにより、デバイスが逆の縦向きのときに UI が縦向きになり、デバイスを 90 度回転したときに Activity が再作成されます。 デフォルトで逆の縦向きに回転しないデバイスには影響しません。ディスプレイが逆の縦向きである場合、マルチウィンドウ モードはサポートされません。
画面の向きをロック Activity が最初に作成されたときに、ユースケースを 1 回だけ作成します(ActivityonCreate() コールバックなど)。
OrientationEventListeneronOrientationChanged() を使用します。 コールバック内で、ユースケースのターゲット回転を更新します。 また、デバイスが(たとえば 90 度)回転しても Activity が再作成されない場合にも適用されます。これは、アプリが画面の半分を占めるようなフォーム ファクタが小さいデバイスや、3 分の 2 を占めるフォーム ファクタが大きいデバイスに該当します。
デバイスの向きの configChanges がオーバーライドされる Activity が最初に作成されたときに、ユースケースを 1 回だけ作成します(ActivityonCreate() コールバックなど)。
OrientationEventListeneronOrientationChanged() を使用します。 コールバック内で、ユースケースのターゲット回転を更新します。 また、デバイスが(たとえば 90 度)回転しても Activity が再作成されない場合にも適用されます。これは、アプリが画面の半分を占めるようなフォーム ファクタが小さいデバイスや、3 分の 2 を占めるフォーム ファクタが大きいデバイスに該当します。
省略可: AndroidManifest ファイルで、アクティビティの screenOrientation プロパティを fullSensor に設定します。 デバイスが逆の縦向きのときに UI を縦向きにできるようにします。 デフォルトで逆の縦向きに回転しないデバイスには影響しません。ディスプレイが逆の縦向きである場合、マルチウィンドウ モードはサポートされません。

デバイスでサポートされている画面の向きのみをサポートする

デバイスがデフォルトでサポートする画面の向きのみをサポートします(逆の縦向き / 逆の横向きを含む場合と含まない場合があります)。

シナリオ ガイドライン マルチウィンドウ分割画面モード
画面の向きのロックを解除 Activity が作成されるたびにユースケースを設定します(ActivityonCreate() コールバックなど)。
DisplayListeneronDisplayChanged() を使用します。コールバック内でユースケースのターゲット回転を更新します(デバイスが 180 度回転したときなど)。 また、デバイスが(たとえば 90 度)回転しても Activity が再作成されない場合にも適用されます。これは、アプリが画面の半分を占めるようなフォーム ファクタが小さいデバイスや、3 分の 2 を占めるフォーム ファクタが大きいデバイスに該当します。
画面の向きをロック Activity が最初に作成されたときに、ユースケースを 1 回だけ作成します(ActivityonCreate() コールバックなど)。
OrientationEventListeneronOrientationChanged() を使用します。 コールバック内で、ユースケースのターゲット回転を更新します。 また、デバイスが(たとえば 90 度)回転しても Activity が再作成されない場合にも適用されます。これは、アプリが画面の半分を占めるようなフォーム ファクタが小さいデバイスや、3 分の 2 を占めるフォーム ファクタが大きいデバイスに該当します。
デバイスの向きの configChanges がオーバーライドされる Activity が最初に作成されたときに、ユースケースを 1 回だけ作成します(ActivityonCreate() コールバックなど)。
DisplayListeneronDisplayChanged() を使用します。コールバック内でユースケースのターゲット回転を更新します(デバイスが 180 度回転したときなど)。 また、デバイスが(たとえば 90 度)回転しても Activity が再作成されない場合にも適用されます。これは、アプリが画面の半分を占めるようなフォーム ファクタが小さいデバイスや、3 分の 2 を占めるフォーム ファクタが大きいデバイスに該当します。

画面の向きのロックを解除

ディスプレイの向き(縦向き、横向きなど)とデバイスの物理的な向きが一致する場合に、Activity で向きのロックが解除されます。ただし一部のデバイスでデフォルトでサポートされていない、逆の縦向き / 逆の横向きについては該当しません。デバイスが 4 つすべての向きに回転するように強制するには、ActivityscreenOrientation プロパティを fullSensor に設定します。

マルチウィンドウ モードの場合、逆の縦向き / 逆の横向きをデフォルトでサポートしていないデバイスは、screenOrientation プロパティを fullSensor に設定しても、逆の縦向き / 逆の横向きには回転しません。

<!-- The Activity has an unlocked orientation, but might not rotate to reverse
portrait/landscape in single-window mode if the device doesn't support it by
default. -->
<activity android:name=".UnlockedOrientationActivity" />

<!-- The Activity has an unlocked orientation, and will rotate to all four
orientations in single-window mode. -->
<activity
   android:name=".UnlockedOrientationActivity"
   android:screenOrientation="fullSensor" />

画面の向きをロック

デバイスの物理的な向きにかかわらず、ディスプレイの向き(縦向きまたは横向き)が同じ状態が続くと、その向きにロックされます。これを行うには、AndroidManifest.xml ファイルの宣言内で、ActivityscreenOrientation プロパティを指定します。

ディスプレイの向きがロックされると、デバイスを回転させても Activity が破棄されず、再作成されません。

<!-- The Activity keeps a portrait orientation even as the device rotates. -->
<activity
   android:name=".LockedOrientationActivity"
   android:screenOrientation="portrait" />

画面の向きの設定変更をオーバーライド

Activity によって向きの設定変更がオーバーライドされると、デバイスの物理的な向きが変わっても、設定が破棄されず、再作成されません。 ただし UI は、デバイスの物理的な向きに合わせて更新されます。

<!-- The Activity's UI might not rotate in reverse portrait/landscape if the
device doesn't support it by default. -->
<activity
   android:name=".OrientationConfigChangesOverriddenActivity"
   android:configChanges="orientation|screenSize" />

<!-- The Activity's UI will rotate to all 4 orientations in single-window
mode. -->
<activity
   android:name=".OrientationConfigChangesOverriddenActivity"
   android:configChanges="orientation|screenSize"
   android:screenOrientation="fullSensor" />

カメラのユースケースの設定

上記のシナリオでは、Activity が最初に作成されるときにカメラのユースケースを設定できます。

Activity で向きのロックが解除されている場合は、向きが変わると Activity が破棄されて再作成されるため、この設定はデバイスが回転されるたびに行われます。それによりユースケースでは、ディスプレイの向きに合わせて、デフォルトで毎回ターゲットの回転が設定されます。

Activity で向きがロックされている場合、または向きの設定変更がオーバーライドされる場合には、この設定は Activity が最初に作成されたときに 1 回だけ行われます。

class CameraActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)

       val cameraProcessFuture = ProcessCameraProvider.getInstance(this)
       cameraProcessFuture.addListener(Runnable {
          val cameraProvider = cameraProcessFuture.get()

          // By default, the use cases set their target rotation to match the
          // display’s rotation.
          val preview = buildPreview()
          val imageAnalysis = buildImageAnalysis()
          val imageCapture = buildImageCapture()

          cameraProvider.bindToLifecycle(
              this, cameraSelector, preview, imageAnalysis, imageCapture)
       }, mainExecutor)
   }
}

OrientationEventListener の設定

OrientationEventListener を使用すると、デバイスの向きの変更に合わせて、カメラのユースケースにおけるターゲットの回転を継続的に更新できます。

class CameraActivity : AppCompatActivity() {

    private val orientationEventListener by lazy {
        object : OrientationEventListener(this) {
            override fun onOrientationChanged(orientation: Int) {
                if (orientation == UNKNOWN_ORIENTATION) {
                    return
                }

                val rotation = when (orientation) {
                     in 45 until 135 -> Surface.ROTATION_270
                     in 135 until 225 -> Surface.ROTATION_180
                     in 225 until 315 -> Surface.ROTATION_90
                     else -> Surface.ROTATION_0
                 }

                 imageAnalysis.targetRotation = rotation
                 imageCapture.targetRotation = rotation
            }
        }
    }

    override fun onStart() {
        super.onStart()
        orientationEventListener.enable()
    }

    override fun onStop() {
        super.onStop()
        orientationEventListener.disable()
    }
}

DisplayListener の設定

DisplayListener を使用すると、デバイスが 180 度回転した後で Activity が破棄されず、再作成されないなどの状況で、カメラのユースケースにおけるターゲットの回転を更新できます。

class CameraActivity : AppCompatActivity() {

    private val displayListener = object : DisplayManager.DisplayListener {
        override fun onDisplayChanged(displayId: Int) {
            if (rootView.display.displayId == displayId) {
                val rotation = rootView.display.rotation
                imageAnalysis.targetRotation = rotation
                imageCapture.targetRotation = rotation
            }
        }

        override fun onDisplayAdded(displayId: Int) {
        }

        override fun onDisplayRemoved(displayId: Int) {
        }
    }

    override fun onStart() {
        super.onStart()
        val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
        displayManager.registerDisplayListener(displayListener, null)
    }

    override fun onStop() {
        super.onStop()
        val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
        displayManager.unregisterDisplayListener(displayListener)
    }
}