マルチカメラ API

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

マルチカメラは Android 9(API レベル 28)で導入されました。リリース以来、 API に対応したデバイスが市場に 投入されていますマルチカメラのユースケースが多い 特定のハードウェア構成と密接に関連しています。つまり、 あらゆるユースケースがすべてのデバイスと互換性があるため、マルチカメラが可能になります。 Google Play の機能を利用することをおすすめします 提供

一般的なユースケースは次のとおりです。

  • ズーム: 切り抜き範囲や目的のフォーカスに応じてカメラを切り替えます あります。
  • Depth: 複数のカメラを使用して奥行きマップを作成します。
  • ボケ: 推定される奥行き情報を使用して、デジタル一眼レフのような狭い範囲をシミュレートします。 焦点を当てます

論理カメラと物理カメラの違い

multi-camera API を理解するには、 1 台の論理カメラと物理カメラです参考として、3 つのデバイスを持つ 背面カメラ。この例では、3 台の背面カメラのそれぞれが、 物理カメラと見なされます論理カメラは 2 つ以上のカメラを 利用できます論理関数の出力は、 カメラは、基盤となる物理カメラのいずれかからのストリームで、 基盤となる複数の物理カメラからの 融合ストリームです できます。いずれの場合も、ストリーミングはカメラ ハードウェアによって処理されます。 抽象化レイヤ(HAL)。

多くのスマートフォン メーカーがファースト パーティのカメラアプリを開発しており、 デバイスにプリインストールされています。ハードウェアの機能をすべて使用するには プライベートまたは非表示の API を使用したり、Google Cloud からの特別な扱いを ドライバ実装のみをサポートしています。一部 デバイスは論理カメラの概念を実装するために、論理カメラの融合されたストリームを提供します。 割り当てられていますが、特定の特権レベルにのみ関連付けられます。 説明します。多くの場合、外部に公開される物理カメラは 1 つだけです。 説明します。Android 9 より前のサードパーティ デベロッパーは、 これを次の図に示します。

図 1. カメラ機能は通常、 特権アプリ

Android 9 以降、Android アプリでは非公開 API を使用できなくなりました。 フレームワークにマルチカメラ サポートが含まれているため、 おすすめの方法です スマートフォン メーカーは すべての物理カメラが同じ方向を向いている場合です次は サードパーティ デベロッパーは、Android 9 以降のデバイスで 高い:

図 2. すべてのカメラデバイスに対するデベロッパーの完全なアクセス権 Android 9 以降

論理カメラの機能は OEM の実装によって異なります。 使用できます。たとえば、Pixel 3 のようなデバイスは、その論理的実装が 基づいて物理カメラを 1 つ選択し、 リクエストされた焦点距離と切り抜き領域です。

multi-camera API

新しい API では、次の定数、クラス、メソッドを新たに追加しています。

Android 互換性定義ドキュメント(CDD)の変更により、 マルチカメラ API には、デベロッパーから一定の期待が寄せられています。デバイス Android 9 より前ではデュアルカメラを搭載していたが、複数のカメラを起動していた 試行錯誤を重ねましたAndroid 9 以降の場合: マルチカメラ は、物理デバイス ペアを開くことができるタイミングを指定する一連のルールを提供します。 複数台構成されています。

ほとんどの場合、Android 9 以降を搭載したデバイスは、 カメラ(赤外線など、あまり一般的でないタイプのセンサーを除く) 使い勝手の良い論理カメラです受信されたストリームの組み合わせごとに 動作が保証されています。論理カメラに属する 1 つのストリームを、 2 つのストリームを 1 つにまとめます。

同時に複数のストリーム

複数のカメラ ストリームを同時に使用する 1 台のカメラで複数のストリームを同時に使用するためのルールを説明しています。 重要な変更点が 1 つありますが、複数のカメラに同じルールが適用されます。 CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA 論理ストリーム YUV_420_888 または raw ストリームを 物理ストリームですつまり、YUV 型または RAW 型の各ストリームは、 同じタイプとサイズのストリームを 2 つ作成します。カメラ ストリームから開始できます。 シングルカメラ デバイスの場合、保証される構成は次のとおりです。

  • ストリーム 1: 論理カメラ id = 0 からの YUV 型、MAXIMUM サイズ

その後、マルチカメラをサポートするデバイスでセッションを作成できます。 論理 YUV ストリームを 2 つの物理ストリームに置き換えます

  • ストリーム 1: YUV タイプ、物理カメラからの MAXIMUM サイズ id = 1
  • ストリーム 2: YUV タイプ、物理カメラからの MAXIMUM サイズ id = 2

次の場合にのみ、YUV ストリームまたは RAW ストリームを同等の 2 つのストリームに置き換えることができます。 この 2 台のカメラは、論理カメラ グループに属しており、 CameraCharacteristics.getPhysicalCameraIds()

このフレームワークによって提供される保証は、 複数の物理カメラのフレームを同時に取得できる追加のストリーム サポートされ、場合によっては複数の物理コンピュータを カメラデバイスを個別に選択できます。外部 IP アドレスの使用を そのためには、Terraform を使用してデバイスごとのテストと調整を行う必要があります。 試行錯誤を繰り返しています

複数の物理カメラを使用するセッションの作成

マルチカメラ対応デバイスで物理カメラを使用する場合は、 CameraDevice(論理カメラ)であり、1 つのカメラ内で あります。API を使用して単一のセッションを作成する CameraDevice.createCaptureSession(SessionConfiguration config): API レベル 28 で追加されました。セッション構成には多数の出力が 各構成には、一連の出力ターゲットと、必要に応じて 物理カメラ ID を指定します。

図 3. SessionConfiguration モデルと OutputConfiguration モデル

キャプチャ リクエストには出力ターゲットが関連付けられています。フレームワーク リクエストの送信先となる物理(または論理)カメラを 関連付けられる出力ターゲットを定義します。出力ターゲットが 物理的な証明書とともに出力構成として送信された その物理カメラがリクエストを受信して処理します。

物理カメラのペアの使用

マルチカメラ用の Camera API には、 背面にある物理カメラを見つけます。「新規顧客の獲得」目標を 使用できる物理カメラのペアを特定できます。 論理カメラストリームの 1 つを置き換えます。

KotlinJava
/**
     * Helper class used to encapsulate a logical camera and two underlying
     * physical cameras
     */

   
data class DualCamera(val logicalId: String, val physicalId1: String, val physicalId2: String)

   
fun findDualCameras(manager: CameraManager, facing: Int? = null): List
/**
     * Helper class used to encapsulate a logical camera and two underlying
     * physical cameras
     */

   
final class DualCamera {
       
final String logicalId;
       
final String physicalId1;
       
final String physicalId2;

       
DualCamera(String logicalId, String physicalId1, String physicalId2) {
           
this.logicalId = logicalId;
           
this.physicalId1 = physicalId1;
           
this.physicalId2 = physicalId2;
       
}
   
}
   
List

物理カメラの状態の処理は論理カメラによって制御されます。宛先 「デュアルカメラ」を開くか、該当する物理カメラに対応する論理カメラ カメラ:

KotlinJava
fun openDualCamera(cameraManager: CameraManager,
                       dualCamera
: DualCamera,
       
// AsyncTask is deprecated beginning API 30
                       executor
: Executor = AsyncTask.SERIAL_EXECUTOR,
                       callback
: (CameraDevice) -> Unit) {

       
// openCamera() requires API >= 28
        cameraManager
.openCamera(
            dualCamera
.logicalId, executor, object : CameraDevice.StateCallback() {
               
override fun onOpened(device: CameraDevice) = callback(device)
               
// Omitting for brevity...
               
override fun onError(device: CameraDevice, error: Int) = onDisconnected(device)
               
override fun onDisconnected(device: CameraDevice) = device.close()
           
})
   
}
void openDualCamera(CameraManager cameraManager,
                       
DualCamera dualCamera,
                       
Executor executor,
                       
CameraDeviceCallback cameraDeviceCallback
   
) {

       
// openCamera() requires API >= 28
        cameraManager
.openCamera(dualCamera.logicalId, executor, new CameraDevice.StateCallback() {
           
@Override
           
public void onOpened(@NonNull CameraDevice cameraDevice) {
               cameraDeviceCallback
.callback(cameraDevice);
           
}

           
@Override
           
public void onDisconnected(@NonNull CameraDevice cameraDevice) {
                cameraDevice
.close();
           
}

           
@Override
           
public void onError(@NonNull CameraDevice cameraDevice, int i) {
                onDisconnected
(cameraDevice);
           
}
       
});
   
}

どのカメラを開くかを選択するだけでなく、 以前の Android バージョンではカメラに接続できません。新しい セッション構成 API がフレームワークに指示し、特定のターゲットを 特定の物理カメラ ID:

KotlinJava
/**
 * Helper type definition that encapsulates 3 sets of output targets:
 *
 *   1. Logical camera
 *   2. First physical camera
 *   3. Second physical camera
 */

typealias DualCameraOutputs =
       
Triple
/**
 * Helper class definition that encapsulates 3 sets of output targets:
 *


 * 1. Logical camera
 * 2. First physical camera
 * 3. Second physical camera
 */

final class DualCameraOutputs {
   
private final List

詳しくは、 createCaptureSession サポートされているストリームの組み合わせについては、こちらをご覧ください。ストリームの組み合わせ 単一の論理カメラ上の複数のストリームに使用します。互換性は 同じ構成を使用し、一方のストリームを 2 つのストリームに置き換える 同じ論理カメラの一部である 2 台の物理カメラから 接続されます

カメラ セッション 必要なメッセージをディスパッチする キャプチャ リクエスト。各 関連する物理サーバーからデータを受信し、 使用したり、論理カメラにフォールバックしたりできます。

Zoom のユースケースの例

複数の物理カメラを単一のストリームに統合して、 物理カメラを切り替えて 異なる「ズームレベル」を効果的に捉えています。

<ph type="x-smartling-placeholder">
</ph>
図 4.ズームレベルのユースケースでのカメラの切り替えの例(Pixel 3 広告から)

最初に物理カメラのペアを選択して、ユーザーが切り替えられるようにします。 あります。最大限の効果を得るには、 最短 / 最大焦点距離。

KotlinJava
fun findShortLongCameraPair(manager: CameraManager, facing: Int? = null): DualCamera? {

   
return findDualCameras(manager, facing).map {
       
val characteristics1 = manager.getCameraCharacteristics(it.physicalId1)
       
val characteristics2 = manager.getCameraCharacteristics(it.physicalId2)

       
// Query the focal lengths advertised by each physical camera
       
val focalLengths1 = characteristics1.get(
           
CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(0F)
       
val focalLengths2 = characteristics2.get(
           
CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(0F)

       
// Compute the largest difference between min and max focal lengths between cameras
       
val focalLengthsDiff1 = focalLengths2.maxOrNull()!! - focalLengths1.minOrNull()!!
       
val focalLengthsDiff2 = focalLengths1.maxOrNull()!! - focalLengths2.minOrNull()!!

       
// Return the pair of camera IDs and the difference between min and max focal lengths
       
if (focalLengthsDiff1 < focalLengthsDiff2) {
           
Pair(DualCamera(it.logicalId, it.physicalId1, it.physicalId2), focalLengthsDiff1)
       
} else {
           
Pair(DualCamera(it.logicalId, it.physicalId2, it.physicalId1), focalLengthsDiff2)
       
}

       
// Return only the pair with the largest difference, or null if no pairs are found
   
}.maxByOrNull { it.second }?.first
}
// Utility functions to find min/max value in float[]
   
float findMax(float[] array) {
       
float max = Float.NEGATIVE_INFINITY;
       
for(float cur: array)
            max
= Math.max(max, cur);
       
return max;
   
}
   
float findMin(float[] array) {
       
float min = Float.NEGATIVE_INFINITY;
       
for(float cur: array)
            min
= Math.min(min, cur);
       
return min;
   
}

DualCamera findShortLongCameraPair(CameraManager manager, Integer facing) {
       
return findDualCameras(manager, facing).stream()
               
.map(c -> {
                   
CameraCharacteristics characteristics1;
                   
CameraCharacteristics characteristics2;
                   
try {
                        characteristics1
= manager.getCameraCharacteristics(c.physicalId1);
                        characteristics2
= manager.getCameraCharacteristics(c.physicalId2);
                   
} catch (CameraAccessException e) {
                        e
.printStackTrace();
                       
return null;
                   
}

                   
// Query the focal lengths advertised by each physical camera
                   
float[] focalLengths1 = characteristics1.get(
                           
CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);
                   
float[] focalLengths2 = characteristics2.get(
                           
CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);

                   
// Compute the largest difference between min and max focal lengths between cameras
                   
Float focalLengthsDiff1 = findMax(focalLengths2) - findMin(focalLengths1);
                   
Float focalLengthsDiff2 = findMax(focalLengths1) - findMin(focalLengths2);

                   
// Return the pair of camera IDs and the difference between min and max focal lengths
                   
if (focalLengthsDiff1 < focalLengthsDiff2) {
                       
return new Pair<>(new DualCamera(c.logicalId, c.physicalId1, c.physicalId2), focalLengthsDiff1);
                   
} else {
                       
return new Pair<>(new DualCamera(c.logicalId, c.physicalId2, c.physicalId1), focalLengthsDiff2);
                   
}

               
}) // Return only the pair with the largest difference, or null if no pairs are found
               
.max(Comparator.comparing(pair -> pair.second)).get().first;
   
}

そのための理にかなったアーキテクチャは、 SurfaceViews - ストリームごとに 1 つ。 これらの SurfaceViews は、ユーザー操作に基づいてスワップされ、一方のみが 表示できます。

次のコードは、論理カメラを開いてカメラを設定する方法を示しています。 カメラ セッションの作成、2 つのプレビュー ストリームの開始:

KotlinJava
val cameraManager: CameraManager = ...

// Get the two output targets from the activity / fragment
val surface1 = ...  // from SurfaceView
val surface2 = ...  // from SurfaceView

val dualCamera = findShortLongCameraPair(manager)!!
val outputTargets = DualCameraOutputs(
   
null, mutableListOf(surface1), mutableListOf(surface2))

// Here you open the logical camera, configure the outputs and create a session
createDualCameraSession
(manager, dualCamera, targets = outputTargets) { session ->

 
// Create a single request which has one target for each physical camera
 
// NOTE: Each target receive frames from only its associated physical camera
 
val requestTemplate = CameraDevice.TEMPLATE_PREVIEW
 
val captureRequest = session.device.createCaptureRequest(requestTemplate).apply {
    arrayOf
(surface1, surface2).forEach { addTarget(it) }
 
}.build()

 
// Set the sticky request for the session and you are done
  session
.setRepeatingRequest(captureRequest, null, null)
}
CameraManager manager = ...;

       
// Get the two output targets from the activity / fragment
       
Surface surface1 = ...;  // from SurfaceView
       
Surface surface2 = ...;  // from SurfaceView

       
DualCamera dualCamera = findShortLongCameraPair(manager, null);
               
DualCameraOutputs outputTargets = new DualCameraOutputs(
               
null, Collections.singletonList(surface1), Collections.singletonList(surface2));

       
// Here you open the logical camera, configure the outputs and create a session
        createDualCameraSession
(manager, dualCamera, outputTargets, null, (session) -> {
           
// Create a single request which has one target for each physical camera
           
// NOTE: Each target receive frames from only its associated physical camera
           
CaptureRequest.Builder captureRequestBuilder;
           
try {
                captureRequestBuilder
= session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
               
Arrays.asList(surface1, surface2).forEach(captureRequestBuilder::addTarget);

               
// Set the sticky request for the session and you are done
                session
.setRepeatingRequest(captureRequestBuilder.build(), null, null);
           
} catch (CameraAccessException e) {
                e
.printStackTrace();
           
}
       
});

あとはユーザーが 2 つのモードを切り替える UI を用意するだけです。 ボタンなどのサーフェスや、SurfaceView のダブルタップ。さらに、 なんらかのシーン分析を実行し、2 つのストリームを切り替える 自動的に適用されます。

レンズの歪み

どのレンズにもある程度の歪みが生じます。Android では、 レンズによって生じる歪み CameraCharacteristics.LENS_DISTORTION これは、サポートが終了した CameraCharacteristics.LENS_RADIAL_DISTORTION。 論理カメラの場合、歪みは最小限で、アプリケーションで カメラからのフレームが増減します。物理カメラの場合は 特に広角レンズでは、レンズ構成が大きく異なる可能性があります。 。

デバイスによっては、以下を使用して自動歪み補正を実装している場合があります。 CaptureRequest.DISTORTION_CORRECTION_MODE。 ほとんどのデバイスで、歪み補正はデフォルトでオンになっています。

KotlinJava
val cameraSession: CameraCaptureSession = ...

       
// Use still capture template to build the capture request
       
val captureRequest = cameraSession.device.createCaptureRequest(
           
CameraDevice.TEMPLATE_STILL_CAPTURE
       
)

       
// Determine if this device supports distortion correction
       
val characteristics: CameraCharacteristics = ...
       
val supportsDistortionCorrection = characteristics.get(
           
CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES
       
)?.contains(
           
CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY
       
) ?: false

       
if (supportsDistortionCorrection) {
            captureRequest
.set(
               
CaptureRequest.DISTORTION_CORRECTION_MODE,
               
CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY
           
)
       
}

       
// Add output target, set other capture request parameters...

       
// Dispatch the capture request
        cameraSession
.capture(captureRequest.build(), ...)
CameraCaptureSession cameraSession = ...;

       
// Use still capture template to build the capture request
       
CaptureRequest.Builder captureRequestBuilder = null;
       
try {
            captureRequestBuilder
= cameraSession.getDevice().createCaptureRequest(
                   
CameraDevice.TEMPLATE_STILL_CAPTURE
           
);
       
} catch (CameraAccessException e) {
            e
.printStackTrace();
       
}

       
// Determine if this device supports distortion correction
       
CameraCharacteristics characteristics = ...;
       
boolean supportsDistortionCorrection = Arrays.stream(
                        characteristics
.get(
                               
CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES
                       
))
               
.anyMatch(i -> i == CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY);
       
if (supportsDistortionCorrection) {
            captureRequestBuilder
.set(
                   
CaptureRequest.DISTORTION_CORRECTION_MODE,
                   
CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY
           
);
       
}

       
// Add output target, set other capture request parameters...

       
// Dispatch the capture request
        cameraSession
.capture(captureRequestBuilder.build(), ...);

このモードでキャプチャ リクエストを設定すると、フレームレートに影響する可能性があります。 画像が生成されます。歪み補正機能は、 使用できます。