CameraX を使用して Jetpack Compose で写真を撮ったりビデオを録画したりする

Android 開発の歴史の中で、Camera API は常に批判されてきましたが、使ったことのある人なら誰でも直感的に感じられるのは、構成が複雑で肥大化し、使いにくく、理解しにくいということであることがわかります。カメラに関する API の反復ルート 公式は、カメラの開発者エクスペリエンスを継続的に改善することにも努めており、カメラ API はこれまでにの 3 つのバージョンCameraX、およびCamera2(非推奨)、Camera

ここに画像の説明を挿入

第一世代のCamera API は 5.0 から廃止が宣言されており、特にCamera2の API が使いにくく、多くの人が使った結果、以前のCameraに及ばないため、実際にベースになっているCameraXがあります。Camera2 ですが、使用されています。Jetpack コンポーネント ライブラリの一部である、より人道的な最適化が現在、公式のカメラ ソリューションです。したがって、カメラ API を含む新しいプロジェクトがある場合、または古いカメラ API をアップグレードする予定がある場合は、CameraX を直接使用することをお勧めします。

この記事では、Jetpack Compose で CameraX を使用する方法について説明します。

CameraXの準備

まず依存関係を追加します。

dependencies {
    
     
  def camerax_version = "1.3.0-alpha04" 
  // implementation "androidx.camera:camera-core:${camerax_version}" // 可选,因为camera-camera2 包含了camera-core
  implementation "androidx.camera:camera-camera2:${camerax_version}" 
  implementation "androidx.camera:camera-lifecycle:${camerax_version}" 
  implementation "androidx.camera:camera-video:${camerax_version}" 
  implementation "androidx.camera:camera-view:${camerax_version}"  
  implementation "androidx.camera:camera-extensions:${camerax_version}"
}

上記のライブラリの最新バージョン情報は、https: //developer.android.com/jetpack/androidx/releases/camera? hl=zh-cn で確認できます。

カメラを使用するにはカメラ許可アプリケーションが必要であるため、accompanist-permissionsCompose で許可を申請するには依存関係を追加する必要があります。

val accompanist_version = "0.31.2-alpha"
implementation "com.google.accompanist:accompanist-permissions:$accompanist_version"

上記のライブラリの最新バージョン情報は、https: //github.com/google/accompanist/releasesで確認できます。

次に、AndroidManifest.xml次のファイルに権限宣言を忘れずに追加してください。

<manifest .. >
    <uses-permission android:name="android.permission.CAMERA" />
    ..
</manifest>

CameraX には次の最小バージョン要件があります。

  • Android API レベル 21
  • Android アーキテクチャ コンポーネント 1.1.1

ライフサイクル対応アクティビティの場合は、FragmentActivity または AppCompatActivity を使用します。

CameraX カメラのプレビュー

主に、CameraX がカメラ プレビューをどのように実行するかを見てみましょう

プレビューの作成 PreviewView

Jetpack Compose はカメラ プレビュー用の別個のコンポーネントを直接提供しないため、解決策は、AndroidViewこのコンポーザブルを使用してネイティブ プレビュー コントロールを Compose に統合して表示することです。コードは以下のように表示されます:

@Composable
private fun CameraPreviewExample() {
    
    
    Scaffold(modifier = Modifier.fillMaxSize()) {
    
     innerPadding: PaddingValues ->
        AndroidView(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding),
            factory = {
    
     context ->
                PreviewView(context).apply {
    
    
                    setBackgroundColor(Color.White.toArgb())
                    layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
                    scaleType = PreviewView.ScaleType.FILL_START
                    implementationMode = PreviewView.ImplementationMode.COMPATIBLE
                }
            }
        )
    }
}

これはライブラリ内のネイティブ View コントロールPreviewViewです。その設定メソッドをいくつか見てみましょう。camera-view

1.PreviewView.setImplementationMode() : このメソッドは、アプリケーションに適した特定の実装モードを設定するために使用されます。

実装モード

PreviewViewプレビュー ストリームは、次のいずれかのモードを使用してターゲット上にレンダリングできますView

  • PERFORMANCEはデフォルトのモードで、PreviewViewを使用してビデオ ストリームを表示しますSurfaceViewが、場合によっては を使用するようにフォールバックしますTextureViewSurfaceView専用の描画インターフェイスを使用すると、特にプレビュー ビデオの上にボタンなどの他のインターフェイス要素がない場合、オブジェクトは内部ハードウェア コンポジターを介してハードウェア オーバーレイを実装する可能性が高くなります。ハードウェア オーバーレイを使用してレンダリングすると、ビデオ フレームが GPU パスからバイパスされ、プラットフォームの消費電力と遅延が低減されます。

  • COMPATIBLEこのモードでは、 モード がPreviewView使用されますTextureViewとは異なりSurfaceView、このオブジェクトには専用の描画面がありません。したがって、ビデオはブレンディングを通じてレンダリングされるまで表示できません。この追加のステップ中に、アプリはビデオの拡大縮小や回転などの追加の処理を制限なく実行できます。

注: はPERFORMANCEデフォルト モードです。デバイスがこれをサポートしていない場合はSurfaceViewPreviewViewを使用するようにフォールバックされますTextureViewAPIレベルが24以下、カメラのハードウェアサポートレベルが表示回転CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY以上の場合に戻ります任意の回転はサポートされていないため、モニターの回転以外の値に設定されている場合は、このモードを使用しないでください。プレビュー ビューをアニメーション化する必要がある場合は、このモードを使用しないでください。アニメーションはAPI以下のレベルではサポートされていません。また、で提供されるプレビュー ストリームの状態については、このモードを使用すると、状態が早く発生する可能性があります。Preview.getTargetRotation()PreviewViewPreviewViewTextureView
Preview.Builder.setTargetRotation(int)SurfaceView24SurfaceViewgetPreviewStreamStatePreviewView.StreamState.streaming

パフォーマンスを考慮する場合は、明らかにこのモードを使用する必要がありますが、互換性を考慮する場合は、このモードPERFORMANCEを使用するのが最善です。COMPATIBLE

2.PreviewView.setScaleType() : このメソッドは、アプリケーションに最適なスケーリング タイプを設定するために使用されます。

ズームタイプ

プレビュー ビデオの解像度がPreviewViewターゲットのサイズと異なる場合、ビューに合わせてビデオ コンテンツをトリミングまたはレターボックス化する必要があります (元のアスペクト比を維持)。この目的のために、PreviewView以下が提供されますScaleTypes

  • FIT_CENTER、、、FIT_STARTおよびレターボックスを追加しFIT_ENDますビデオ コンテンツ全体が、ターゲットで表示できる最大サイズにリサイズ (拡大または縮小) されます。ただし、ビデオ フレーム全体が完全に表示されますが、スクリーン ショットには空白の部分が存在する場合があります。ビデオ フレームは、上で選択した 3 つのズーム タイプのどれに応じて、ターゲット ビューの中心、開始、または終了に位置合わせされます。PreviewView

  • FILL_CENTERFILL_STARTおよびクリッピングFILL_ENDビデオのアスペクト比がと一致しない場合、コンテンツの一部のみが表示されますが、ビデオは全体を占めますPreviewViewPreviewView

CameraX使用されるデフォルトのスケーリング タイプは ですFILL_CENTER

注: ズーム タイプの主な目的は、プレビューが伸びたり変形したりしないようにすることです。以前の Camera または Camera2 API を使用する場合、私の一般的なアプローチは、カメラでサポートされているプレビュー解像度のリストを取得し、プレビュー解像度を選択することです。次に、SurfaceViewまたはTextureViewコントロールのアスペクト比を、選択したプレビュー解像度のアスペクト比に合わせます。これにより、プレビュー中に伸縮や変形の問題が発生せず、最終的な効果は実際には上記のズーム タイプとまったく同じになります。幸いなことに、現在は正式な API レベルのサポートにより、開発者はこれらの面倒な作業を手動で行う必要がなくなりました。

たとえば、下の左の図は通常のプレビュー表示効果であり、右の図はストレッチされた変形のプレビュー表示効果です。

ここに画像の説明を挿入

この種のエクスペリエンスは非常に好ましくなく、最大の問題は、表示されているものがそのまま得られることです(保存された画像またはビデオ ファイルがプレビューで見られる効果と一致しない)。

16:9 のプレビュー画面に表示された 4:3 の画像を例に挙げると、何も処理しないと 100% の確率で伸縮と変形が発生します。

ここに画像の説明を挿入

次の図は、適用されたさまざまなスケーリング タイプの効果を示しています。

ここに画像の説明を挿入

の使用にはいくつかの制限がありますPreviewViewを使用する場合PreviewView、次のことはできません。

  • に設定するSurfaceTextureために作成されました。TextureViewPreview.SurfaceProvider
  • からTextureView取得されSurfaceTexturePreview.SurfaceProviderに設定されます。
  • からSurfaceView取得してSurfacePreview.SurfaceProviderに設定します。

上記のいずれかの状況が発生した場合、 へのPreviewフレームのストリーミングは停止しますPreviewView

ライフサイクル CameraController のバインディング

作成後のPreviewView次のステップは、作成したインスタンスCameraController(その実装が である抽象クラス) を設定し、作成したインスタンスを現在のライフサイクル ホルダーにバインドLifecycleCameraControllerすることですコードは以下のように表示されます:CameraControllerlifecycleOwner

@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun CameraPreviewExample() {
    
    
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraController = remember {
    
     LifecycleCameraController(context) }

    Scaffold(modifier = Modifier.fillMaxSize()) {
    
     innerPadding: PaddingValues ->
        AndroidView(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding),
            factory = {
    
     context ->
                PreviewView(context).apply {
    
    
                    setBackgroundColor(Color.White.toArgb())
                    layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
                    scaleType = PreviewView.ScaleType.FILL_START
                    implementationMode = PreviewView.ImplementationMode.COMPATIBLE
                }.also {
    
     previewView ->
                    previewView.controller = cameraController
                    cameraController.bindToLifecycle(lifecycleOwner)
                }
            },
            onReset = {
    
    },
            onRelease = {
    
    
                cameraController.unbind()
            }
        )
    }
}

上記のコードでは、onReleaseコールバックでコントローラーPreviewViewのバインドを解除していることに注意してください。AndroidViewこうすることで、カメラ リソースが使用されなくなったときに確実に解放することができます。

アクセスのリクエスト

プレビューのコンポーザブル インターフェイスは、アプリケーションがカメラの承認を取得した後にのみ表示されます。それ以外の場合は、プレースホルダーのコンポーザブル インターフェイスが表示されます。認可を取得するための参照コードは以下のとおりです。

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun ExampleCameraScreen() {
    
    
    val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
    LaunchedEffect(key1 = Unit) {
    
    
        if (!cameraPermissionState.status.isGranted && !cameraPermissionState.status.shouldShowRationale) {
    
    
            cameraPermissionState.launchPermissionRequest()
        }
    }
    if (cameraPermissionState.status.isGranted) {
    
     // 相机权限已授权, 显示预览界面
        CameraPreviewExample()
    } else {
    
     // 未授权,显示未授权页面
        NoCameraPermissionScreen(cameraPermissionState = cameraPermissionState)
    }
}

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun NoCameraPermissionScreen(cameraPermissionState: PermissionState) {
    
    
    // In this screen you should notify the user that the permission
    // is required and maybe offer a button to start another camera perission request
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
    
    
        val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
    
    
            // 如果用户之前选择了拒绝该权限,应当向用户解释为什么应用程序需要这个权限
            "未获取相机授权将导致该功能无法正常使用。"
        } else {
    
    
            // 首次请求授权
            "该功能需要使用相机权限,请点击授权。"
        }
        Text(textToShow)
        Spacer(Modifier.height(8.dp))
        Button(onClick = {
    
     cameraPermissionState.launchPermissionRequest() }) {
    
     Text("请求权限") }
    }
}

Compose で動的アクセス許可を適用する方法の詳細については、「Jetpack Compose の Accompanist 」を参照してください。そのため、ここでは詳しく説明しません。

全画面設定

プレビュー時に上部のステータス バーを表示せずにカメラを全画面で表示するには、ActivityメソッドonCreate()setContent前に次のコードを追加します。

if (isFullScreen) {
    
    
   requestWindowFeature(Window.FEATURE_NO_TITLE)
    //这个必须设置,否则不生效。
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    
    
        window.attributes.layoutInDisplayCutoutMode =
            WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
    }
    WindowCompat.setDecorFitsSystemWindows(window, false)
    val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
    windowInsetsController.hide(WindowInsetsCompat.Type.statusBars()) // 隐藏状态栏
    windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars()) // 隐藏导航栏
    //将底部的navigation操作栏弄成透明,滑动显示,并且浮在上面
    windowInsetsController.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
}

通常、このコードは機能するはずですが、機能しない場合は、テーマのテーマを変更してみてください。

// themes.xml
<?xml version="1.0" encoding="utf-8"?>
<resources> 
    <style name="Theme.MyComposeApplication" parent="android:Theme.Material.Light.NoActionBar.Fullscreen" >
        <item name="android:statusBarColor">@android:color/transparent</item>
        <item name="android:navigationBarColor">@android:color/transparent</item>
        <item name="android:windowTranslucentStatus">true</item>
    </style> 
</resources>

写真を撮る CameraX

CameraX で写真を撮るには、主に 2 つのオーバーロード メソッドが提供されます。

  • takePicture(Executor, OnImageCapturedCallback): このメソッドは、キャプチャされた画像にメモリ バッファを提供します。
  • takePicture(OutputFileOptions, Executor, OnImageSavedCallback): このメソッドは、キャプチャされた画像を指定されたファイルの場所に保存します。

クリックするとカメラ機能をトリガーするFloatingActionButtonボタンを追加しましょう。CameraPreviewExampleコードは以下のように表示されます:

@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun CameraPreviewExample() {
    
    
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraController = remember {
    
     LifecycleCameraController(context) }

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        floatingActionButton = {
    
    
            FloatingActionButton(onClick = {
    
     takePhoto(context, cameraController) }) {
    
    
                Icon(
                    imageVector = ImageVector.vectorResource(id = R.drawable.ic_camera_24),
                    contentDescription = "Take picture"
                )
            }
        },
        floatingActionButtonPosition = FabPosition.Center,
    ) {
    
     innerPadding: PaddingValues ->
        AndroidView(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding),
            factory = {
    
     context -> 
                PreviewView(context).apply {
    
    
                    setBackgroundColor(Color.White.toArgb())
                    layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
                    scaleType = PreviewView.ScaleType.FILL_START
                    implementationMode = PreviewView.ImplementationMode.COMPATIBLE
                }.also {
    
     previewView ->
                    previewView.controller = cameraController
                    cameraController.bindToLifecycle(lifecycleOwner)
                }
            },
            onReset = {
    
    },
            onRelease = {
    
    
                cameraController.unbind() 
            }
        )
    }
}
fun takePhoto(context: Context, cameraController: LifecycleCameraController) {
    
    
    val mainExecutor = ContextCompat.getMainExecutor(context)
    // Create time stamped name and MediaStore entry.
    val name = SimpleDateFormat(FILENAME, Locale.CHINA)
        .format(System.currentTimeMillis())
    val contentValues = ContentValues().apply {
    
    
        put(MediaStore.MediaColumns.DISPLAY_NAME, name)
        put(MediaStore.MediaColumns.MIME_TYPE, PHOTO_TYPE)
        if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
    
    
            val appName = context.resources.getString(R.string.app_name)
            put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/${
      
      appName}")
        }
    }
    // Create output options object which contains file + metadata
    val outputOptions = ImageCapture.OutputFileOptions
        .Builder(context.contentResolver, MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            contentValues)
        .build()
    cameraController.takePicture(outputOptions, mainExecutor, object : ImageCapture.OnImageSavedCallback {
    
    
            override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
    
    
                val savedUri = outputFileResults.savedUri
                Log.d(TAG, "Photo capture succeeded: $savedUri")
                context.notifySystem(savedUri)
            }
            override fun onError(exception: ImageCaptureException) {
    
    
                Log.e(TAG, "Photo capture failed: ${
      
      exception.message}", exception)
            }
        }
    )
    context.showFlushAnimation()
}

OnImageSavedCallbackのメソッドコールバックでは、保存された画像ファイルを取得onImageSaved、その後の業務処理を行うことができます。outputFileResultsUri

写真を撮った後に保存ロジックを自分で実行したい場合、または保存せずに表示するだけの場合は、別のコールバックを使用できますOnImageCapturedCallback

fun takePhoto2(context: Context, cameraController: LifecycleCameraController) {
    
    
    val mainExecutor = ContextCompat.getMainExecutor(context)
    cameraController.takePicture(mainExecutor,  object : ImageCapture.OnImageCapturedCallback() {
    
    
        override fun onCaptureSuccess(image: ImageProxy) {
    
    
            Log.e(TAG, "onCaptureSuccess: ${
      
      image.imageInfo}")
            // Process the captured image here
            try {
    
    
                // The supported format is ImageFormat.YUV_420_888 or PixelFormat.RGBA_8888.
                val bitmap = image.toBitmap()
                Log.e(TAG, "onCaptureSuccess bitmap: ${
      
      bitmap.width} x ${
      
      bitmap.height}")
            } catch (e: Exception) {
    
    
                Log.e(TAG, "onCaptureSuccess Exception: ${
      
      e.message}")
            }
        }
    })
    context.showFlushAnimation()
}

このコールバックでは、このImageProxy#toBitmapメソッドを使用して、写真を撮った後の生データをBitmap表示用に簡単に変換できます。ただし、ここで取得したデフォルトの形式ではメソッド変換が失敗するImageFormat.JPEGためtoBitmap、次のコードを参照して解決できます。

fun takePhoto2(context: Context, cameraController: LifecycleCameraController) {
    
    
    val mainExecutor = ContextCompat.getMainExecutor(context)
    cameraController.takePicture(mainExecutor,  object : ImageCapture.OnImageCapturedCallback() {
    
    
        override fun onCaptureSuccess(image: ImageProxy) {
    
    
            Log.e(TAG, "onCaptureSuccess: ${
      
      image.format}")
            // Process the captured image here
            try {
    
    
                var bitmap: Bitmap? = null
                // The supported format is ImageFormat.YUV_420_888 or PixelFormat.RGBA_8888.
                if (image.format == ImageFormat.YUV_420_888 || image.format == PixelFormat.RGBA_8888) {
    
    
                    bitmap = image.toBitmap()
                } else if (image.format == ImageFormat.JPEG) {
    
    
                    val planes = image.planes
                    val buffer = planes[0].buffer // 因为是ImageFormat.JPEG格式,所以 image.getPlanes()返回的数组只有一个,也就是第0个。
                    val size = buffer.remaining()
                    val bytes = ByteArray(size)
                    buffer.get(bytes, 0, size)
                    // ImageFormat.JPEG格式直接转化为Bitmap格式。
                    bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
                }
                if (bitmap != null) {
    
    
                    Log.e(TAG, "onCaptureSuccess bitmap: ${
      
      bitmap.width} x ${
      
      bitmap.height}")
                }
            } catch (e: Exception) {
    
    
                Log.e(TAG, "onCaptureSuccess Exception: ${
      
      e.message}")
            }
        }
    })
    context.showFlushAnimation()
}

image.toBitmap()ここでYUV形式を取得した場合は、メソッドを直接呼び出すだけでなく、YUV_420_888形式をRGBその形式のオブジェクトに変換できるツールクラスも公式で提供されていますので、 YuvToRgbConverter.ktBitmapを参照してください

上記の例の完全なコードは次のとおりです。

@Composable
fun ExampleCameraNavHost() {
    
    
    val navController = rememberNavController()
    NavHost(navController, startDestination = "CameraScreen") {
    
    
        composable("CameraScreen") {
    
    
            ExampleCameraScreen(navController = navController)
        }
        composable("ImageScreen") {
    
    
            ImageScreen(navController = navController)
        }
    }
}

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun ExampleCameraScreen(navController: NavHostController) {
    
    
    val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
    LaunchedEffect(key1 = Unit) {
    
    
        if (!cameraPermissionState.status.isGranted && !cameraPermissionState.status.shouldShowRationale) {
    
    
            cameraPermissionState.launchPermissionRequest()
        }
    }
    if (cameraPermissionState.status.isGranted) {
    
     // 相机权限已授权, 显示预览界面
        CameraPreviewExample(navController)
    } else {
    
     // 未授权,显示未授权页面
        NoCameraPermissionScreen(cameraPermissionState = cameraPermissionState)
    }
}

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun NoCameraPermissionScreen(cameraPermissionState: PermissionState) {
    
    
    // In this screen you should notify the user that the permission
    // is required and maybe offer a button to start another camera perission request
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
    
    
        val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
    
    
            // 如果用户之前选择了拒绝该权限,应当向用户解释为什么应用程序需要这个权限
            "未获取相机授权将导致该功能无法正常使用。"
        } else {
    
    
            // 首次请求授权
            "该功能需要使用相机权限,请点击授权。"
        }
        Text(textToShow)
        Spacer(Modifier.height(8.dp))
        Button(onClick = {
    
     cameraPermissionState.launchPermissionRequest() }) {
    
     Text("请求权限") }
    }
}

private const val TAG = "CameraXBasic"
private const val FILENAME = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val PHOTO_TYPE = "image/jpeg"

@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun CameraPreviewExample(navController: NavHostController) {
    
    
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraController = remember {
    
     LifecycleCameraController(context) }

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        floatingActionButton = {
    
    
            FloatingActionButton(onClick = {
    
    
                 takePhoto(context, cameraController, navController)
                // takePhoto2(context, cameraController, navController)
                // takePhoto3(context, cameraController, navController)
            }) {
    
    
                Icon(
                    imageVector = ImageVector.vectorResource(id = R.drawable.ic_camera_24),
                    contentDescription = "Take picture"
                )
            }
        },
        floatingActionButtonPosition = FabPosition.Center,
    ) {
    
     innerPadding: PaddingValues ->
        AndroidView(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding),
            factory = {
    
     context ->
                cameraController.imageCaptureMode = CAPTURE_MODE_MINIMIZE_LATENCY
                PreviewView(context).apply {
    
    
                    setBackgroundColor(Color.White.toArgb())
                    layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
                    scaleType = PreviewView.ScaleType.FILL_CENTER
                    implementationMode = PreviewView.ImplementationMode.COMPATIBLE
                }.also {
    
     previewView ->
                    previewView.controller = cameraController
                    cameraController.bindToLifecycle(lifecycleOwner)
                }
            },
            onReset = {
    
    },
            onRelease = {
    
    
                cameraController.unbind()
            }
        )
    }
}

fun takePhoto(context: Context, cameraController: LifecycleCameraController, navController: NavHostController) {
    
    
    val mainExecutor = ContextCompat.getMainExecutor(context)
    // Create time stamped name and MediaStore entry.
    val name = SimpleDateFormat(FILENAME, Locale.CHINA)
        .format(System.currentTimeMillis())
    val contentValues = ContentValues().apply {
    
    
        put(MediaStore.MediaColumns.DISPLAY_NAME, name)
        put(MediaStore.MediaColumns.MIME_TYPE, PHOTO_TYPE)
        if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
    
    
            val appName = context.resources.getString(R.string.app_name)
            put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/${
      
      appName}")
        }
    }
    // Create output options object which contains file + metadata
    val outputOptions = ImageCapture.OutputFileOptions
        .Builder(context.contentResolver, MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            contentValues)
        .build()
    cameraController.takePicture(outputOptions, mainExecutor, object : ImageCapture.OnImageSavedCallback {
    
    
            override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
    
    
                val savedUri = outputFileResults.savedUri
                Log.d(TAG, "Photo capture succeeded: $savedUri")
                context.notifySystem(savedUri)

                navController.currentBackStackEntry?.savedStateHandle?.set("savedUri", savedUri)
                navController.navigate("ImageScreen")
            }
            override fun onError(exception: ImageCaptureException) {
    
    
                Log.e(TAG, "Photo capture failed: ${
      
      exception.message}", exception)
            }
        }
    )
    context.showFlushAnimation()
}

fun takePhoto2(context: Context, cameraController: LifecycleCameraController, navController: NavHostController) {
    
    
    val mainExecutor = ContextCompat.getMainExecutor(context)
    cameraController.takePicture(mainExecutor,  object : ImageCapture.OnImageCapturedCallback() {
    
    
        override fun onCaptureSuccess(image: ImageProxy) {
    
    
            Log.e(TAG, "onCaptureSuccess: ${
      
      image.format}")
            // Process the captured image here
            val scopeWithNoEffect = CoroutineScope(SupervisorJob())
            scopeWithNoEffect.launch {
    
    
                val savedUri = withContext(Dispatchers.IO) {
    
    
                    try {
    
    
                        var bitmap: Bitmap? = null
                        // The supported format is ImageFormat.YUV_420_888 or PixelFormat.RGBA_8888.
                        if (image.format == ImageFormat.YUV_420_888 || image.format == PixelFormat.RGBA_8888) {
    
    
                            bitmap = image.toBitmap()
                        } else if (image.format == ImageFormat.JPEG) {
    
    
                            val planes = image.planes
                            val buffer = planes[0].buffer // 因为是ImageFormat.JPEG格式,所以 image.getPlanes()返回的数组只有一个,也就是第0个。
                            val size = buffer.remaining()
                            val bytes = ByteArray(size)
                            buffer.get(bytes, 0, size)
                            // ImageFormat.JPEG格式直接转化为Bitmap格式。
                            bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
                        }
                        bitmap?.let {
    
    
                            // 保存bitmap到文件中
                            val photoFile = File(
                                context.getOutputDirectory(),
                                SimpleDateFormat(FILE_FORMAT, Locale.CHINA).format(System.currentTimeMillis()) + ".jpg"
                            )
                            BitmapUtilJava.saveBitmap(bitmap, photoFile.absolutePath, 100)
                            val savedUri = Uri.fromFile(photoFile)
                            savedUri
                        }
                    } catch (e: Exception) {
    
    
                        if (e is CancellationException) throw e
                        Log.e(TAG, "onCaptureSuccess Exception: ${
      
      e.message}")
                        null
                    }
                }
                mainExecutor.execute {
    
    
                	context.notifySystem(savedUri)
                    navController.currentBackStackEntry?.savedStateHandle?.set("savedUri", savedUri)
                    navController.navigate("ImageScreen")
                }
            }

        }
    })
    context.showFlushAnimation()
}
fun takePhoto3(context: Context, cameraController: LifecycleCameraController, navController: NavHostController) {
    
    
    val photoFile = File(
        context.getOutputDirectory(),
        SimpleDateFormat(FILENAME, Locale.CHINA).format(System.currentTimeMillis()) + ".jpg"
    )
    val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
    val mainExecutor = ContextCompat.getMainExecutor(context)
    cameraController.takePicture(outputOptions, mainExecutor, object: ImageCapture.OnImageSavedCallback {
    
    
        override fun onError(exception: ImageCaptureException) {
    
    
            Log.e(TAG, "Take photo error:", exception)
            onError(exception)
        }

        override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
    
    
            val savedUri = Uri.fromFile(photoFile)
            Log.d(TAG, "Photo capture succeeded: $savedUri")
            context.notifySystem(savedUri)

            navController.currentBackStackEntry?.savedStateHandle?.set("savedUri", savedUri)
            navController.navigate("ImageScreen")
        }
    })
    context.showFlushAnimation()
}

// flash 动画
private fun Context.showFlushAnimation() {
    
    
    // We can only change the foreground Drawable using API level 23+ API
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    
    
        // Display flash animation to indicate that photo was captured
        if (this is Activity) {
    
    
            val decorView = window.decorView
            decorView.postDelayed({
    
    
                decorView.foreground = ColorDrawable(android.graphics.Color.WHITE)
                decorView.postDelayed({
    
     decorView.foreground = null }, ANIMATION_FAST_MILLIS)
            }, ANIMATION_SLOW_MILLIS)
        }
    }
}
// 发送系统广播
private fun Context.notifySystem(savedUri: Uri?) {
    
    
    // 对于运行API级别>=24的设备,将忽略隐式广播,因此,如果您只针对24+级API,则可以删除此语句
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
    
    
        sendBroadcast(Intent(Camera.ACTION_NEW_PICTURE, savedUri))
    }
}
private fun Context.getOutputDirectory(): File {
    
    
    val mediaDir = externalMediaDirs.firstOrNull()?.let {
    
    
        File(it, resources.getString(R.string.app_name)).apply {
    
     mkdirs() }
    }

    return if (mediaDir != null && mediaDir.exists()) mediaDir else filesDir
}
// ImageScreen.kt 用于展示拍照结果的屏幕
@Composable
fun ImageScreen(navController: NavHostController) {
    
    
    val context = LocalContext.current
    var imageBitmap by remember {
    
     mutableStateOf<ImageBitmap?>(null) }
    val scope = rememberCoroutineScope()
    val savedUri = navController.previousBackStackEntry?.savedStateHandle?.get<Uri>("savedUri")
    savedUri?.run {
    
    
        scope.launch {
    
    
            withContext(Dispatchers.IO){
    
    
                val bitmap = BitmapUtilJava.getBitmapFromUri(context, savedUri)
                imageBitmap = BitmapUtilJava.scaleBitmap(bitmap, 1920, 1080).asImageBitmap()
            }
         }
        imageBitmap?.let {
    
    
            Image(it,
                contentDescription = null,
                modifier = Modifier.fillMaxWidth(),
                contentScale = ContentScale.Crop
            )
        }
    }
}
// 用到的几个工具类
public static void saveBitmap(Bitmap mBitmap, String filePath, int quality) {
    
    
	File f = new File(filePath);
	FileOutputStream fOut = null;
	try {
    
    
		fOut = new FileOutputStream(f);
	} catch (FileNotFoundException e) {
    
    
		e.printStackTrace();
	}
	mBitmap.compress(Bitmap.CompressFormat.JPEG, quality, fOut);
	try {
    
    
		if (fOut != null) {
    
    
			fOut.flush();
		}
	} catch (IOException e) {
    
    
		e.printStackTrace();
	}
}
/**
 * 宽高比取最大值缩放图片.
 *
 * @param bitmap     加载的图片
 * @param widthSize  缩放之后的图片宽度,一般就是屏幕的宽度.
 * @param heightSize 缩放之后的图片高度,一般就是屏幕的高度.
 */
public static Bitmap scaleBitmap(Bitmap bitmap, int widthSize, int heightSize) {
    
    
	int bmpW = bitmap.getWidth();
	int bmpH = bitmap.getHeight();
	float scaleW = ((float) widthSize) / bmpW;
	float scaleH = ((float) heightSize) / bmpH;
	//取宽高最大比例来缩放图片
	float max = Math.max(scaleW, scaleH);
	Matrix matrix = new Matrix();
	matrix.postScale(max, max);
	return Bitmap.createBitmap(bitmap, 0, 0, bmpW, bmpH, matrix, true);
}
/**
 * 根据Uri返回Bitmap对象
 * @param context
 * @param uri
 * @return
 */
public static Bitmap getBitmapFromUri(Context context, Uri uri){
    
    
	try {
    
    
		// 这种方式也可以
		// BitmapFactory.decodeStream(context.getContentResolver().openInputStream(uri));
		return MediaStore.Images.Media.getBitmap(context.getContentResolver(), uri);
	}catch (Exception e){
    
    
		e.printStackTrace();
		return null;
	}
}

注: ビットマップ関連の操作は時間のかかるタスクなので、コルーチンを使用してメインスレッドから分離されたコルーチン スケジューラで実行する必要があります。実際には、運用プロジェクトで使用する場合は、それ自体を改善する必要があります。

CameraProvider と CameraController

すべての公式 CameraX コードは、実際には1CameraControllerCameraProvider2 つの実装を提供します。CameraX を最も簡単に使用したい場合は選択しCameraController、より柔軟な方法が必要な場合は選択してくださいCameraProvider

どの実装が最適であるかを判断するには、それぞれの利点を以下に示します。

カメラコントローラー カメラプロバイダー
セットアップコードはほとんど必要ありません より優れた制御を可能にする
CameraX がより多くのセットアップ プロセスを処理できるようになり、タップしてフォーカスしたり、ピンチしてズームしたりする機能が自動的に動作します。 アプリ開発者が設定を処理するため、出力画像の回転を有効にしたり、ImageAnalysis で出力画像形式を設定したりするなど、構成をカスタマイズする機会が増えます。
カメラのプレビューには PreviewView が必要です。これにより、機械学習モデルの結果の座標 (顔の境界ボックスなど) をプレビュー座標に直接マッピングする ML Suite 統合のように、CameraX がシームレスなエンドツーエンドの統合を提供できるようになります。 カメラ プレビューにカスタムの「Surface」を使用できるため、アプリの他の部分への入力として使用できる既存の「Surface」コードを使用するなど、より柔軟な対応が可能になります。

CameraProviderを利用してカメラ機能を実現する

簡単にアクセスできるようにCameraProvider、最初に拡張関数を定義します。

private suspend fun Context.getCameraProvider(): ProcessCameraProvider {
    
    
    return ProcessCameraProvider.getInstance(this).await()
}

を使用してCameraProvider写真を撮ることは を使用することと非常に似ていますが、唯一の違いは、メソッドを呼び出すオブジェクトCameraControllerが必要であることですImageCapturetakePicture

private fun takePhoto(
    context: Context,
    imageCapture: ImageCapture,
    onImageCaptured: (Uri) -> Unit,
    onError: (ImageCaptureException) -> Unit
) {
    
    
    val photoFile = File(
        context.getOutputDirectory(),
        SimpleDateFormat(FILE_FORMAT, Locale.CHINA).format(System.currentTimeMillis()) + ".jpg"
    )
    val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
    val mainExecutor = ContextCompat.getMainExecutor(context)
    imageCapture.takePicture(outputOptions, mainExecutor, object: ImageCapture.OnImageSavedCallback {
    
    
        override fun onError(exception: ImageCaptureException) {
    
    
            Log.e(TAG, "Take photo error:", exception)
            onError(exception)
        }

        override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
    
    
            val savedUri = Uri.fromFile(photoFile)
            onImageCaptured(savedUri)
            context.notifySystem(savedUri)
        }
    })
    context.showFlushAnimation()
}

通話コード:

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun CameraPreviewExample2(navController: NavHostController) {
    
    
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current

    val previewView = remember {
    
     PreviewView(context) }
    // Create Preview UseCase.
    val preview = remember {
    
    
        Preview.Builder().build().apply {
    
    
            setSurfaceProvider(previewView.surfaceProvider)
        }
    }
    val imageCapture: ImageCapture = remember {
    
     ImageCapture.Builder().build() }
    val cameraSelector = remember {
    
     CameraSelector.DEFAULT_BACK_CAMERA } // Select default back camera.
    var pCameraProvider: ProcessCameraProvider? = null
    
    LaunchedEffect(cameraSelector) {
    
    
        val cameraProvider = context.getCameraProvider()
        cameraProvider.unbindAll()  // Unbind UseCases before rebinding. 
        // Bind UseCases to camera. This function returns a camera
        // object which can be used to perform operations like zoom, flash, and focus.
        cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageCapture)
        pCameraProvider = cameraProvider
    }

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        floatingActionButton = {
    
    
            FloatingActionButton(onClick = {
    
    
                takePhoto(
                    context,
                    imageCapture = imageCapture,
                    onImageCaptured = {
    
     savedUri ->
                        Log.d(TAG, "Photo capture succeeded: $savedUri")
                        context.notifySystem(savedUri)
                        navController.currentBackStackEntry?.savedStateHandle?.set("savedUri", savedUri)
                        navController.navigate("ImageScreen")
                    },
                    onError = {
    
    
                        Log.e(TAG, "Photo capture failed: ${
      
      it.message}", it)
                    }
                )
            }) {
    
    
                Icon(
                    imageVector = ImageVector.vectorResource(id = R.drawable.ic_camera_24),
                    contentDescription = "Take picture"
                )
            }
        },
        floatingActionButtonPosition = FabPosition.Center,
    ) {
    
     innerPadding: PaddingValues ->
        AndroidView(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding),
            factory = {
    
     previewView },
            onReset = {
    
    },
            onRelease = {
    
    
                pCameraProvider?.unbindAll()
            }
        )
    }
}

ここでの最大の違いは、LaunchedEffect副作用 API が使用され、cameraProviderバインディング関連の操作を設定するためにコルーチンが開始されることです。これは、cameraProviderここで取得するときにサスペンド関数が使用されるためです。また、ここでPreviewViewと の両方がImageCaptureを使用して記憶されるrememberため、Composable再編成されるたびに新しいオブジェクトを再作成するのではなく、再編成後も存続することに注意してください。

CameraX の共通設定

撮影モードを設定する

または のどちらでもCameraControllerの方法で撮影モードを設定CameraProviderできます。setCaptureMode()

CameraX でサポートされている撮影モードは次のとおりです。

  • CAPTURE_MODE_MINIMIZE_LATENCY: 撮影の遅延時間を短縮します。
  • CAPTURE_MODE_MAXIMIZE_QUALITY: 画像キャプチャの画質を向上させます。
  • CAPTURE_MODE_ZERO_SHUTTER_LAG:ゼロシャッターラグモード、1.2以降で利用可能。ゼロシャッターラグを有効にすると、デフォルトの撮影モードCAPTURE_MODE_MINIMIZE_LATENCYに比べてラグタイムが大幅に短縮されるため、シャッターを逃すことがなくなります。

撮影モードの初期値は ですCAPTURE_MODE_MINIMIZE_LATENCY詳細については、setCaptureMode()リファレンス ドキュメントを参照してください。

ゼロ シャッター ラグは、リング バッファーを使用して、最近キャプチャされた 3 つのフレームを保存します。ユーザーがキャプチャ ボタンを押すと、CameraX が呼び出されtakePicture()、リング バッファはボタンが押された時間に最も近いタイムスタンプを持つキャプチャ フレームを取得します。次に、CameraX はキャプチャ セッションを再処理して、そのフレームから画像を生成し、JPEG 形式でディスクに保存します。

ゼロ シャッター ラグを有効にする前に、 を使用して、isZslSupported()問題のデバイスが次の要件を満たしているかどうかを判断します。

  • Android 6.0以降(APIレベル23以降)を対象とします。
  • PRIVATE 再処理がサポートされています

デバイスが最小要件を満たしていない場合、CameraX は CAPTURE_MODE_MINIMIZE_LATENCY に戻ります。

ゼロ シャッター ラグは、写真キャプチャの使用例でのみ使用できます。ビデオ キャプチャのユースケースやカメラ拡張機能に対してこれを有効にすることはできません。最後に、フラッシュを使用すると遅延時間が増加するため、フラッシュがオンまたは自動モードの場合、ゼロ シャッター ラグは機能しません。

撮影モードの設定例CameraController

cameraController.imageCaptureMode = CAPTURE_MODE_MINIMIZE_LATENCY

撮影モードの設定例CameraProvider

val imageCapture: ImageCapture = remember {
    
    
   ImageCapture.Builder().setCaptureMode(ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG).build()
}

フラッシュを設定する

デフォルトのフラッシュ モードは ですFLASH_MODE_OFFフラッシュ モードを設定するには、次を使用しますsetFlashMode()

  • FLASH_MODE_ON:フラッシュは常時発光します。
  • FLASH_MODE_AUTO: 暗い場所で撮影すると、自動的にフラッシュが発光します。

CameraControllerフラッシュモードの設定例:

cameraController.imageCaptureFlashMode = ImageCapture.FLASH_MODE_AUTO

CameraProviderフラッシュモードの設定例:

ImageCapture.Builder() 
   .setFlashMode(FLASH_MODE_AUTO)
   .build()

カメラを選択

CameraX では、カメラの選択はCameraSelectorクラスを通じて処理されます。CameraX を使用すると、デフォルトのカメラを使用する一般的なケースがはるかに簡単になります。デフォルトの前面カメラを使用するか、デフォルトの背面カメラを使用するかを指定できます。

以下は、CameraControllerデフォルトの背面カメラを使用した CameraX コードです。

var cameraController = LifecycleCameraController(baseContext)
// val selector = CameraSelector.Builder()
//    .requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
val selector = CameraSelector.DEFAULT_BACK_CAMERA // 等价上面的代码
cameraController.cameraSelector = selector

以下は、CameraProviderデフォルトのフロントカメラを選択するための CameraX コードです。

val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
cameraProvider.unbindAll() 
var camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCases)

タップして焦点を合わせる

カメラのプレビューが画面に表示されている場合、一般的なコントロールは、ユーザーがプレビューをタップしたときにフォーカスを設定することです。

CameraControllerタッチ イベントをリッスンしてPreviewView、タップ フォーカスを自動的に処理します。setTapToFocusEnabled()タップしてフォーカスを有効または無効にすることができ、getter isTapToFocusEnabled()対応する でその値を確認できます。

getTapToFocusState()メソッドは、上のフォーカス状態の変化をLiveData追跡するオブジェクトを返します。CameraController

// CameraX: track the state of tap-to-focus over the Lifecycle of a PreviewView,
// with handlers you can define for focused, not focused, and failed states.

val tapToFocusStateObserver = Observer {
    
     state ->
    when (state) {
    
    
        CameraController.TAP_TO_FOCUS_NOT_STARTED ->
            Log.d(TAG, "tap-to-focus init")
        CameraController.TAP_TO_FOCUS_STARTED ->
            Log.d(TAG, "tap-to-focus started")
        CameraController.TAP_TO_FOCUS_FOCUSED ->
            Log.d(TAG, "tap-to-focus finished (focus successful)")
        CameraController.TAP_TO_FOCUS_NOT_FOCUSED ->
            Log.d(TAG, "tap-to-focus finished (focused unsuccessful)")
        CameraController.TAP_TO_FOCUS_FAILED ->
            Log.d(TAG, "tap-to-focus failed")
    }
}

cameraController.getTapToFocusState().observe(this, tapToFocusStateObserver)

を使用する場合CameraProvider、タップしてフォーカスを正しく機能させるには、いくつかの設定が必要です。この例では、 を使用していることを前提としていますPreviewView使用しない場合は、カスタム Surface に適用するロジックを調整する必要があります。

を使用する場合はPreviewView、次の手順に従います。

  1. タップイベントの処理に使用されるジェスチャ検出器を設定します。
  2. タップ イベントの場合は、MeteringPointFactory.createPoint()を使用して作成しますMeteringPoint
  3. の場合はMeteringPoint、 を作成しますFocusMeteringAction
  4. Camera 上のCameraControl( から返された) オブジェクトの場合はbindToLifecycle()、それを呼び出しstartFocusAndMetering()て に渡しますFocusMeteringAction
  5. (オプション) 応答FocusMeteringResult
  6. PreviewView.setOnTouchListener()のタッチ イベントに応答するようにジェスチャ検出器を設定します。
// CameraX: implement tap-to-focus with CameraProvider.

// Define a gesture detector to respond to tap events and call
// startFocusAndMetering on CameraControl. If you want to use a
// coroutine with await() to check the result of focusing, see the
// "Android development concepts" section above.
val gestureDetector = GestureDetectorCompat(context,
    object : SimpleOnGestureListener() {
    
    
        override fun onSingleTapUp(e: MotionEvent): Boolean {
    
    
            val previewView = previewView ?: return
            val camera = camera ?: return
            val meteringPointFactory = previewView.meteringPointFactory
            val focusPoint = meteringPointFactory.createPoint(e.x, e.y)
            val meteringAction = FocusMeteringAction
                .Builder(meteringPoint).build()
            lifecycleScope.launch {
    
    
                val focusResult = camera.cameraControl
                    .startFocusAndMetering(meteringAction).await()
                if (!result.isFocusSuccessful()) {
    
    
                    Log.d(TAG, "tap-to-focus failed")
                }
            }
        }
    }
)

...

// Set the gestureDetector in a touch listener on the PreviewView.
previewView.setOnTouchListener {
    
     _, event ->
    // See pinch-to-zooom scenario for scaleGestureDetector definition.
    var didConsume = scaleGestureDetector.onTouchEvent(event)
    if (!scaleGestureDetector.isInProgress) {
    
    
        didConsume = gestureDetector.onTouchEvent(event)
    }
    didConsume
}

ピンチしてズーム

プレビューのズームも、カメラ プレビューの一般的な直接操作です。デバイス上のカメラの数が増えるにつれ、ユーザーはズーム後に最適なフォーカスを持つカメラを自動的に選択したいと考えています。

タップしてフォーカスする場合と同様に、タッチ イベントがCameraController監視されPreviewView、ピンチしてズームする操作が自動的に処理されます。setPinchToZoomEnabled()でピンチズームを有効または無効にし、対応getter isPinchToZoomEnabled()する でその値を確認できます。

getZoomState()メソッドは、の変更をLiveData追跡するオブジェクトを返しますCameraControllerZoomState

// CameraX: track the state of pinch-to-zoom over the Lifecycle of
// a PreviewView, logging the linear zoom ratio.

val pinchToZoomStateObserver = Observer {
    
     state ->
    val zoomRatio = state.getZoomRatio()
    Log.d(TAG, "ptz-zoom-ratio $zoomRatio")
}

cameraController.getZoomState().observe(this, pinchToZoomStateObserver)

を使用する場合CameraProvider、ピンチとズームが正しく機能するには、いくつかの設定が必要です。を使用していない場合はPreviewView、カスタム を適用するようにロジックを調整する必要がありますSurface

を使用する場合はPreviewView、次の手順に従います。

  1. ピンチ イベントの処理に使用されるズーム ジェスチャ検出器を設定します。
  2. オブジェクトからCamera.CameraInfo取得されたインスタンスは、ZoomStateを呼び出すbindToLifecycle()と返されますCamera
  3. 値がZoomStateある場合は、それを現在のズームとして保存します。NonezoomRatioの場合、カメラのデフォルトのズーム ( ) が使用されます。ZoomStatezoomRatio1.0
  4. 現在のズーム倍率を取得しscaleFactorて乗算して新しいズーム倍率を決定し、それを に渡しますCameraControl.setZoomRatio()
  5. PreviewView.setOnTouchListener()のタッチ イベントに応答するようにジェスチャ検出器を設定します。
// CameraX: implement pinch-to-zoom with CameraProvider.

// Define a scale gesture detector to respond to pinch events and call
// setZoomRatio on CameraControl.
val scaleGestureDetector = ScaleGestureDetector(context,
    object : SimpleOnGestureListener() {
    
    
        override fun onScale(detector: ScaleGestureDetector): Boolean {
    
    
            val camera = camera ?: return
            val zoomState = camera.cameraInfo.zoomState
            val currentZoomRatio: Float = zoomState.value?.zoomRatio ?: 1f
            camera.cameraControl.setZoomRatio(
                detector.scaleFactor * currentZoomRatio
            )
        }
    }
)

...

// Set the scaleGestureDetector in a touch listener on the PreviewView.
previewView.setOnTouchListener {
    
     _, event ->
    var didConsume = scaleGestureDetector.onTouchEvent(event)
    if (!scaleGestureDetector.isInProgress) {
    
    
        // See pinch-to-zooom scenario for gestureDetector definition.
        didConsume = gestureDetector.onTouchEvent(event)
    }
    didConsume
}

CameraX はビデオをキャプチャします

通常、キャプチャ システムはビデオ ストリームとオーディオ ストリームを記録し、それらを圧縮し、2 つのストリームを多重化し、結果のストリームをディスクに書き込みます。

ここに画像の説明を挿入

VideoCapture API の概要

CameraX では、ビデオ キャプチャのソリューションは次のようVideoCaptureなユースケースです。

ここに画像の説明を挿入

CameraX ビデオ キャプチャは、いくつかの高レベルのアーキテクチャ コンポーネントで構成されています。

  • SurfaceProvider、ビデオ ソースを表します。
  • AudioSource、オーディオソースを示します。
  • ビデオ/オーディオをエンコードおよび圧縮するための 2 つのエンコーダー。
  • 2 つのストリームを多重化するメディア マルチプレクサー。
  • 結果を書き出すためのファイル セーバー。

VideoCaptureAPI は複雑なキャプチャ エンジンを抽象化し、よりシンプルで直感的な API をアプリケーションに提供します。

VideoCaptureは、単独で使用することも、他のユースケースと組み合わせて使用​​することもできる CameraX ユースケースです。サポートされる正確な組み合わせはカメラのハードウェア機能によって異なりますが、この使用例のPreviewとの組み合わせはすべてのデバイスに適用されます。VideoCapture

注:はVideoCaptureCameraX のライブラリcamera-videoに実装されており、1.1.0-alpha10以降で利用可能です。CameraX VideoCaptureAPI は最終的なものではなく、時間の経過とともに変更される可能性があります。

VideoCaptureAPI は、アプリケーションと通信できる次のオブジェクトで構成されます。

  • VideoCapture最上位のユースケースクラスです。VideoCaptureviaおよびその他の CameraX ユースケースCameraSelectorにバインドしますLifecycleOwner
  • RecorderVideoCaptureは と密接に結合された実装ですVideoOutputRecorderビデオおよびオーディオのキャプチャ操作を実行するために使用されます。Recorder記録オブジェクトを作成して適用します。
  • PendingRecordingオーディオの有効化やイベント リスナーの設定などのオプションを使用して、録音オブジェクトが構成されます。Recorderを作成するにはを使用する必要がありますPendingRecordingPendingRecording何も記録されません。
  • Recording実際の録音動作が行われます。PendingRecordingを作成するにはを使用する必要がありますRecording

次の図は、これらのオブジェクト間の関係を示しています。

ここに画像の説明を挿入
伝説:

  1. QualitySelectorcreateを使用しますRecorder
  2. OutputOptionsこれらの構成のいずれかを使用しますRecorder
  3. 必要に応じてwithAudioEnabled()、オーディオを有効にするために使用します。
  4. VideoRecordEventリスナーと通話してstart()録音を開始します。
  5. Recording使用してpause()/resume()/stop()録音操作を制御します。
  6. イベント リスナー内で応答しますVideoRecordEvents

詳細な API リストは、ソース コード内の current-txt にあります

CameraProvider でビデオを撮影する

CameraProviderバインドされたユースケースを使用する場合は、 UseCase を作成してオブジェクトを渡すVideoCapure必要がありますビデオ品質は、デバイスが必要な品質仕様を満たしていない場合に備え、オプションで設定できます。最後に、インスタンスは他の UseCases とともにバインドされますVideoCaptureRecorderRecorder.BuilderFallbackStrategyVideoCaptureCameraProvider

QualitySelector オブジェクトを作成する

QualitySelectorアプリはオブジェクトを介してビデオ解像度を構成できますRecorder

CameraX は、Recorder次の事前定義されたビデオ解像度品質をサポートしています。

  • 品質.UHD ( 4K Ultra HD ビデオ サイズ ( 2160p ))
  • フル HD ビデオ サイズ ( 1080p )の場合は、 Quality.FHD
  • HD ビデオ サイズ ( 720p )の場合は、 Quality.HD
  • 標準解像度ビデオ サイズの場合は、Quality.SD ( 480p )

アプリの認証により、CameraX では他の解像度も利用できることに注意してください。各オプションの正確なビデオ サイズは、カメラとエンコーダの機能によって異なります。詳細については、 CamcorderProfileのドキュメントを参照してください

次のいずれかの方法を使用して作成できますQualitySelector

  1. 使用には、fromOrderedList()いくつかの優先解像度が用意されており、優先解像度がいずれもサポートされていない場合のフォールバック戦略が含まれています。

    CameraX は、選択したカメラの機能に基づいて最適なフォールバック マッチを決定できます。QualitySelector詳細については、 FallbackStrategy仕様を参照してくださいたとえば、次のコードは、サポートされている最高の録画解像度を要求します。要求された解像度がいずれもサポートされていない場合、CameraX は最も近いQuality.SD解像度を選択することが許可されます。

val qualitySelector = QualitySelector.fromOrderedList(
         listOf(Quality.UHD, Quality.FHD, Quality.HD, Quality.SD),
         FallbackStrategy.lowerQualityOrHigherThan(Quality.SD))
  1. まず、カメラのサポートされている解像度をクエリし、次に次のQualitySelector::from()コマンドを使用してサポートされている解像度から選択します。
val cameraInfo = cameraProvider.availableCameraInfos.filter {
    
    
    Camera2CameraInfo
    .from(it)
    .getCameraCharacteristic(CameraCharacteristics.LENS\_FACING) == CameraMetadata.LENS_FACING_BACK
}

val supportedQualities = QualitySelector.getSupportedQualities(cameraInfo[0])
val filteredQualities = arrayListOf (Quality.UHD, Quality.FHD, Quality.HD, Quality.SD)
                       .filter {
    
     supportedQualities.contains(it) }

// Use a simple ListView with the id of simple_quality_list_view
viewBinding.simpleQualityListView.apply {
    
    
    adapter = ArrayAdapter(context,
                           android.R.layout.simple_list_item_1,
                           filteredQualities.map {
    
     it.qualityToString() })

    // Set up the user interaction to manually show or hide the system UI.
    setOnItemClickListener {
    
     _, _, position, _ ->
        // Inside View.OnClickListener,
        // convert Quality.* constant to QualitySelector
        val qualitySelector = QualitySelector.from(filteredQualities[position])

        // Create a new Recorder/VideoCapture for the new quality
        // and bind to lifecycle
        val recorder = Recorder.Builder()
            .setQualitySelector(qualitySelector).build()

         // ...
    }
}

// A helper function to translate Quality to a string
fun Quality.qualityToString() : String {
    
    
    return when (this) {
    
    
        Quality.UHD -> "UHD"
        Quality.FHD -> "FHD"
        Quality.HD -> "HD"
        Quality.SD -> "SD"
        else -> throw IllegalArgumentException()
    }
}

QualitySelector.getSupportedQualities()返される関数は、VideoCaptureユースケースまたはVideoCaptureユースケースの組み合わせに適用できる必要があることに注意してくださいPreviewImageCaptureまたはユースケースでバインドするときImageAnalysis、要求されたカメラが目的の組み合わせをサポートしていない場合、CameraX は依然としてバインドに失敗する可能性があります。

VideoCapture オブジェクトを作成してバインドする

を取得したらQualitySelectorVideoCaptureオブジェクトを作成してバインドを実行できます。このバインディングは他の使用例と同じであることに注意してください。

val recorder = Recorder.Builder()
    .setExecutor(cameraExecutor)
    .setQualitySelector(QualitySelector.from(Quality.FHD))
    .build()
val videoCapture = VideoCapture.withOutput(recorder)

try {
    
    
    // Bind use cases to camera
    cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, videoCapture)
} catch(exc: Exception) {
    
    
    Log.e(TAG, "Use case binding failed", exc)
}

Recorderシステムに最適な形式が選択されます。最も一般的なビデオ コーデックは でH.264 AVC、そのコンテナ形式は ですMPEG-4

注: 現在、最終的なビデオ コーデックとコンテナ形式を構成することはできません。

録音オブジェクトの構成と生成

その後、videoCapture.outputプロパティに対してさまざまな構成を実行して、Recording記録の一時停止、再開、または停止に使用できるオブジェクトを生成できます。最後に、を呼び出してstart()録画を開始し、コンテキストとConsumer<VideoRecordEvent>ビデオ録画イベントを処理するイベント リスナーを渡します。

val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
    .format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
    
    
    put(MediaStore.MediaColumns.DISPLAY_NAME, name)
    put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
    
    
        put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
    }
}
// Create MediaStoreOutputOptions for our recorder
val mediaStoreOutputOptions = MediaStoreOutputOptions
    .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
    .setContentValues(contentValues)
    .build()
    
// 2. Configure Recorder and Start recording to the mediaStoreOutput.
val recording = videoCapture.output
    .prepareRecording(context, mediaStoreOutputOptions)
    .withAudioEnabled() // 启用音频
    .start(ContextCompat.getMainExecutor(this), captureListener) // 启动并注册录制事件监听

出力オプション

Recorder次のタイプの構成がサポートされていますOutputOptions

  • FileDescriptorOutputOptionsに取り込むためFileDescriptor
  • FileOutputOptions、ファイルにキャプチャする場合。
  • MediaStoreOutputOptionsMediaStore にキャプチャするためのものです。

のタイプに関係なくOutputOptionssetFileSizeLimit()を通じて最大ファイル サイズを設定できます。ParcelFileDescriptor他のオプションは、 に固有など、個々の出力タイプに固有ですFileDescriptorOutputOptions

一時停止、再開、停止

start()関数を呼び出すと、オブジェクトRecorderが返されますRecordingアプリはこのRecordingオブジェクトを使用して、キャプチャを完了したり、一時停止や再開などの他のアクションを実行したりできます。次のコマンドを使用して、進行中のセッションを一時停止、再開、停止できますRecording

  • pause()、現在アクティブな録音を一時停止します。
  • resume()、一時停止したアクティブな録音を再開します。
  • stop()これにより、記録が完了し、関連付けられたすべての記録オブジェクトがクリアされます。

録音が一時停止されているかアクティブであるかに関係なく、呼び出してstop()終了できることに注意してくださいRecording

RecorderRecording一度にサポートされるオブジェクトは1 つです。前のオブジェクトでまたはRecordingを呼び出した後、新しい録音を開始できます。Recording.stop()Recording.close()

 if (recording != null) {
    
    
      // Stop the current recording session.
      recording.stop()
      recording = null
      return
  }
  ..
  recording = ..

イベントリスナー

に登録している場合はPendingRecording.start()、を使用して通信EventListenerますRecordingVideoRecordEvent

CameraX は、対応するカメラ デバイスで録画が開始されるたびにイベントを送信しますVideoRecordEvent.Start

  • VideoRecordEvent.Status現在のファイルのサイズや記録の期間などの統計を記録するため。
  • VideoRecordEvent.Finalize結果を記録する場合、最終ファイルURIと関連するエラーが含まれます。

アプリが録画セッションの成功を示す を受信するとFinalizeOutputOptionsで。

 recording = videoCapture.output
     .prepareRecording(context, mediaStoreOutputOptions)
     .withAudioEnabled()
     .start(ContextCompat.getMainExecutor(context)) {
    
     recordEvent ->
         when(recordEvent) {
    
    
             is VideoRecordEvent.Start -> {
    
    

             }
             is VideoRecordEvent.Status -> {
    
    

             }
             is VideoRecordEvent.Pause -> {
    
    

             }
             is VideoRecordEvent.Resume -> {
    
    

             }
             is VideoRecordEvent.Finalize -> {
    
    
                 if (!recordEvent.hasError()) {
    
    
                     val msg = "Video capture succeeded: ${
      
      recordEvent.outputResults.outputUri}"
                     context.showToast(msg)
                     Log.d(TAG, msg)
                 } else {
    
    
                     recording?.close()
                     recording = null
                     Log.e(TAG, "video capture ends with error", recordEvent.cause)
                 }
             }
         }
     }

完全なサンプルコード

Compose で使用してCameraProviderビデオをキャプチャするための完全なサンプル コードを次に示します。

// CameraProvider 拍摄视频示例
private const val TAG = "CameraXVideo"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

@Composable
fun CameraVideoExample(navController: NavHostController) {
    
    
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current

    val previewView = remember {
    
     PreviewView(context) }
    // Create Preview UseCase.
    val preview = remember {
    
    
        Preview.Builder().build().apply {
    
     setSurfaceProvider(previewView.surfaceProvider) }
    }
    var cameraSelector by remember {
    
     mutableStateOf(CameraSelector.DEFAULT_BACK_CAMERA) }
    // Create VideoCapture UseCase.
    val videoCapture = remember(cameraSelector) {
    
    
        val qualitySelector = QualitySelector.from(Quality.FHD)
        val recorder = Recorder.Builder()
            .setExecutor(ContextCompat.getMainExecutor(context))
            .setQualitySelector(qualitySelector)
            .build()
        VideoCapture.withOutput(recorder)
    }
    // Bind UseCases
    LaunchedEffect(cameraSelector) {
    
    
        try {
    
    
            val cameraProvider = context.getCameraProvider()
            cameraProvider.unbindAll()
            cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, videoCapture)
        } catch(exc: Exception) {
    
    
            Log.e(TAG, "Use case binding failed", exc)
        }
    }

    var recording: Recording? =  null
    var isRecording by remember {
    
     mutableStateOf(false) }
    var time by remember {
    
     mutableStateOf(0L) }

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        floatingActionButton = {
    
    
            FloatingActionButton(onClick = {
    
    
                 if (!isRecording) {
    
    
                     isRecording = true
                     recording?.stop()
                     time = 0L
                     recording = startRecording(context, videoCapture,
                        onFinished = {
    
     savedUri ->
                            if (savedUri != Uri.EMPTY) {
    
    
                                val msg = "Video capture succeeded: $savedUri"
                                context.showToast(msg)
                                Log.d(TAG, msg)
                                navController.currentBackStackEntry?.savedStateHandle?.set("savedUri", savedUri)
                                navController.navigate("VideoPlayerScreen")
                            }
                        },
                        onProgress = {
    
     time = it },
                        onError = {
    
    
                            isRecording = false
                            recording?.close()
                            recording = null
                            time = 0L
                            Log.e(TAG, "video capture ends with error", it)
                        }
                    )
                 } else {
    
    
                     isRecording = false
                     recording?.stop()
                     recording = null
                     time = 0L
                 }
            }) {
    
    
                val iconId = if (!isRecording) R.drawable.ic_start_record_36
                    else R.drawable.ic_stop_record_36
                Icon(
                    imageVector = ImageVector.vectorResource(id = iconId),
                    tint = Color.Red,
                    contentDescription = "Capture Video"
                )
            }
        },
        floatingActionButtonPosition = FabPosition.Center,
    ) {
    
     innerPadding: PaddingValues ->
        Box(modifier = Modifier
            .padding(innerPadding)
            .fillMaxSize()) {
    
    
            AndroidView(
                modifier = Modifier.fillMaxSize(),
                factory = {
    
     previewView },
            )
            if (time > 0 && isRecording) {
    
    
                Text(text = "${
      
      SimpleDateFormat("mm:ss", Locale.CHINA).format(time)} s",
                    modifier = Modifier.align(Alignment.TopCenter),
                    color = Color.Red,
                    fontSize = 16.sp
                )
            }
            if (!isRecording) {
    
    
                IconButton(
                    onClick = {
    
    
                        cameraSelector = when(cameraSelector) {
    
    
                            CameraSelector.DEFAULT_BACK_CAMERA -> CameraSelector.DEFAULT_FRONT_CAMERA
                            else -> CameraSelector.DEFAULT_BACK_CAMERA
                        }
                    },
                    modifier = Modifier
                        .align(Alignment.TopEnd)
                        .padding(bottom = 32.dp)
                ) {
    
    
                    Icon(
                        painter = painterResource(R.drawable.ic_switch_camera),
                        contentDescription = "",
                        tint = Color.Green,
                        modifier = Modifier.size(36.dp)
                    )
                }
            }
        }
    }
}

@SuppressLint("MissingPermission")
private fun startRecording(
    context: Context,
    videoCapture: VideoCapture<Recorder>,
    onFinished: (Uri) -> Unit,
    onProgress: (Long) -> Unit,
    onError: (Throwable?) -> Unit
): Recording{
    
    
    // Create and start a new recording session.
    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.CHINA)
        .format(System.currentTimeMillis())
    val contentValues = ContentValues().apply {
    
    
        put(MediaStore.MediaColumns.DISPLAY_NAME, name)
        put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
    
    
            put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
        }
    }

    val mediaStoreOutputOptions = MediaStoreOutputOptions
        .Builder(context.contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
        .setContentValues(contentValues)
        .build()

    return videoCapture.output
        .prepareRecording(context, mediaStoreOutputOptions)
        .withAudioEnabled()  // 启用音频
        .start(ContextCompat.getMainExecutor(context)) {
    
     recordEvent ->
        when(recordEvent) {
    
    
            is VideoRecordEvent.Start -> {
    
    }
            is VideoRecordEvent.Status -> {
    
    
                val duration = recordEvent.recordingStats.recordedDurationNanos / 1000 / 1000
                onProgress(duration)
            }
            is VideoRecordEvent.Pause -> {
    
    }
            is VideoRecordEvent.Resume -> {
    
    }
            is VideoRecordEvent.Finalize -> {
    
    
                if (!recordEvent.hasError()) {
    
    
                    val savedUri = recordEvent.outputResults.outputUri
                    onFinished(savedUri)
                } else {
    
    
                    onError(recordEvent.cause)
                }
            }
        }
    }
}

private suspend fun Context.getCameraProvider(): ProcessCameraProvider {
    
    
    return ProcessCameraProvider.getInstance(this).await()
}

ルーティングと権限の構成:

@Composable
fun CameraVideoCaptureNavHost() {
    
    
    val navController = rememberNavController()
    NavHost(navController, startDestination = "CameraVideoScreen") {
    
    
        composable("CameraVideoScreen") {
    
    
            CameraVideoScreen(navController = navController)
        }
        composable("VideoPlayerScreen") {
    
    
            VideoPlayerScreen(navController = navController)
        }
    }

}

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraVideoScreen(navController: NavHostController) {
    
    
    val multiplePermissionsState = rememberMultiplePermissionsState(
        listOf(
            Manifest.permission.CAMERA,
            Manifest.permission.RECORD_AUDIO,
        )
    )
    LaunchedEffect(Unit) {
    
    
        if (!multiplePermissionsState.allPermissionsGranted) {
    
    
            multiplePermissionsState.launchMultiplePermissionRequest()
        }
    }
    if (multiplePermissionsState.allPermissionsGranted) {
    
    
        CameraVideoExample(navController)
    } else {
    
    
        NoCameraPermissionScreen(multiplePermissionsState)
    }
}

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun NoCameraPermissionScreen(permissionState: MultiplePermissionsState) {
    
    
    Column(modifier = Modifier.padding(10.dp)) {
    
    
        Text(
            getTextToShowGivenPermissions(
                permissionState.revokedPermissions, // 被拒绝/撤销的权限列表
                permissionState.shouldShowRationale
            ),
            fontSize = 16.sp
        )
        Spacer(Modifier.height(8.dp))
        Button(onClick = {
    
     permissionState.launchMultiplePermissionRequest() }) {
    
    
            Text("请求权限")
        }
    }
}

@OptIn(ExperimentalPermissionsApi::class)
private fun getTextToShowGivenPermissions(
    permissions: List<PermissionState>,
    shouldShowRationale: Boolean
): String {
    
    
    val size = permissions.size
    if (size == 0) return ""
    val textToShow = StringBuilder().apply {
    
     append("以下权限:\n") }
    for (i in permissions.indices) {
    
    
        textToShow.append(permissions[i].permission).apply {
    
    
            if (i == size - 1) append(" \n") else append(", ")
        }
    }
    textToShow.append(
        if (shouldShowRationale) {
    
    
            " 需要被授权,以保证应用功能正常使用."
        } else {
    
    
            " 未获得授权. 应用功能将不能正常使用."
        }
    )
    return textToShow.toString()
}

録画したビデオを表示するには、Google のExoPlayerライブラリを使用して別のルーティング画面でビデオを再生し、依存関係を追加します。

implementation "com.google.android.exoplayer:exoplayer:2.18.7"
// 展示拍摄视频
@Composable
fun VideoPlayerScreen(navController: NavHostController) {
    
    
    val savedUri = navController.previousBackStackEntry?.savedStateHandle?.get<Uri>("savedUri")
    val context = LocalContext.current

    val exoPlayer = savedUri?.let {
    
    
        remember(context) {
    
    
            ExoPlayer.Builder(context).build().apply {
    
    
                setMediaItem(MediaItem.fromUri(savedUri))
                prepare()
            }
        }
    }

    DisposableEffect(
        Box(
            modifier = Modifier.fillMaxSize()
        ) {
    
    
            AndroidView(
                factory = {
    
     context ->
                    StyledPlayerView(context).apply {
    
    
                        player = exoPlayer
                        setShowFastForwardButton(false)
                        setShowNextButton(false)
                        setShowPreviousButton(false)
                        setShowRewindButton(false)
                        controllerHideOnTouch = true
                        controllerShowTimeoutMs = 200
                    }
                },
                modifier = Modifier.fillMaxSize()
            )
        }
    ) {
    
    
        onDispose {
    
    
            exoPlayer?.release()
        }
    }
}

CameraControllerで動画を撮影する

CameraX では、 UseCase を独立して切り替えることができます (これらCameraControllerUseCaseを同時に使用できる場合)とUseCase はデフォルトで有効になっているため、写真を撮るために電話する必要はありません。ImageCaptureVideoCaptureImageAnalysisImageCaptureImageAnalysissetEnabledUseCases()

を使用してビデオを録画する場合は、まず を使用したユースケースを許可するCameraController必要がありますsetEnabledUseCases()VideoCapture

// CameraX: Enable VideoCapture UseCase on CameraController.
cameraController.setEnabledUseCases(VIDEO_CAPTURE);

ビデオの録画を開始したい場合は、CameraController.startRecording()関数を呼び出すことができます。Fileこの関数は、次の例に示すように、録画したビデオを に保存します。さらに、成功およびエラーのコールバックを処理するには、Executorおよび の実装クラスを渡す必要があります。OnVideoSavedCallback

からCameraX 1.3.0-alpha02ビデオ録画の一時停止、再開、停止に使用できるオブジェクトをstartRecording()返します。パラメータを通じてビデオ録画機能を有効または無効にするRecordingこともできます。AudioConfig録音を有効にするには、マイクの使用許可があることを確認する必要があります。と同様にCameraProviderConsumer<VideoRecordEvent>ビデオ イベントを監視するために、一種のコールバックも渡されます。

@SuppressLint("MissingPermission")
@androidx.annotation.OptIn(ExperimentalVideo::class)
private fun startStopVideo(context: Context, cameraController: LifecycleCameraController): Recording {
    
    
    // Define the File options for saving the video.
    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.CHINA)
        .format(System.currentTimeMillis())+".mp4"

    val outputFileOptions = FileOutputOptions
        .Builder(File(context.filesDir, name))
        .build()

    // Call startRecording on the CameraController.
    return cameraController.startRecording(
        outputFileOptions,
        AudioConfig.create(true), // 开启音频
        ContextCompat.getMainExecutor(context),
    ) {
    
     videoRecordEvent ->
        when(videoRecordEvent) {
    
    
            is VideoRecordEvent.Start -> {
    
    }
            is VideoRecordEvent.Status -> {
    
    }
            is VideoRecordEvent.Pause -> {
    
    }
            is VideoRecordEvent.Resume -> {
    
    }
            is VideoRecordEvent.Finalize -> {
    
    
                if (!videoRecordEvent.hasError()) {
    
    
                    val savedUri = videoRecordEvent.outputResults.outputUri
                    val msg = "Video capture succeeded: $savedUri"
                    context.showToast(msg) 
                    Log.d(TAG, msg) 
                } else {
    
     
                    Log.d(TAG, "video capture ends with error", videoRecordEvent.cause)
                }
            }
        }
    }
}

ご覧のとおり、CameraControllerを使用してビデオを録画するのは、 を使用するよりもはるかに簡単ですCameraProvider

完全なサンプルコード

Compose で使用してCameraControllerビデオをキャプチャするための完全なサンプル コードを次に示します。

// CameraController 拍摄视频示例
private const val TAG = "CameraXVideo"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

@androidx.annotation.OptIn(ExperimentalVideo::class)
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun CameraVideoExample2(navController: NavHostController) {
    
    
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraController = remember {
    
     LifecycleCameraController(context) }

    var recording: Recording? =  null
    var time by remember {
    
     mutableStateOf(0L) }

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        floatingActionButton = {
    
    
            FloatingActionButton(onClick = {
    
    
                if (!cameraController.isRecording) {
    
    
                    recording?.stop()
                    time = 0L
                    recording = startRecording(context, cameraController,
                        onFinished = {
    
     savedUri ->
                            if (savedUri != Uri.EMPTY) {
    
    
                                val msg = "Video capture succeeded: $savedUri"
                                context.showToast(msg)
                                Log.d(TAG, msg)
                                navController.currentBackStackEntry?.savedStateHandle?.set("savedUri", savedUri)
                                navController.navigate("VideoPlayerScreen")
                            }
                        },
                        onProgress = {
    
     time = it },
                        onError = {
    
    
                            recording?.close()
                            recording = null
                            time = 0L
                            Log.e(TAG, "video capture ends with error", it)
                        }
                    )
                } else {
    
    
                    recording?.stop()
                    recording = null
                    time = 0L
                }
            }) {
    
    
                val iconId = if (!cameraController.isRecording) R.drawable.ic_start_record_36
                    else R.drawable.ic_stop_record_36
                Icon(
                    imageVector = ImageVector.vectorResource(id = iconId),
                    tint = Color.Red,
                    contentDescription = "Capture Video"
                )
            }
        },
        floatingActionButtonPosition = FabPosition.Center,
    ) {
    
     innerPadding: PaddingValues ->
        Box(modifier = Modifier
            .padding(innerPadding)
            .fillMaxSize()) {
    
    
            AndroidView(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(innerPadding),
                factory = {
    
     context ->
                    PreviewView(context).apply {
    
    
                        setBackgroundColor(Color.White.toArgb())
                        layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
                        scaleType = PreviewView.ScaleType.FILL_CENTER
                        implementationMode = PreviewView.ImplementationMode.COMPATIBLE
                    }.also {
    
     previewView ->
                        cameraController.cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
                        previewView.controller = cameraController
                        cameraController.bindToLifecycle(lifecycleOwner)
                        // cameraController.cameraInfo?.let {
    
    
                        //    val supportedQualities = QualitySelector.getSupportedQualities(it)
                        // }
                        cameraController.setEnabledUseCases(VIDEO_CAPTURE) // 启用 VIDEO_CAPTURE UseCase
                        cameraController.videoCaptureTargetQuality = Quality.FHD
                    }
                },
                onReset = {
    
    },
                onRelease = {
    
    
                    cameraController.unbind()
                }
            )
            if (time > 0 && cameraController.isRecording) {
    
    
                Text(text = "${
      
      SimpleDateFormat("mm:ss", Locale.CHINA).format(time)} s",
                    modifier = Modifier.align(Alignment.TopCenter),
                    color = Color.Red,
                    fontSize = 16.sp
                )
            }
            if (!cameraController.isRecording) {
    
    
                IconButton(
                    onClick = {
    
    
                        cameraController.cameraSelector = when(cameraController.cameraSelector) {
    
    
                            CameraSelector.DEFAULT_BACK_CAMERA -> CameraSelector.DEFAULT_FRONT_CAMERA
                            else -> CameraSelector.DEFAULT_BACK_CAMERA
                        }
                    },
                    modifier = Modifier
                        .align(Alignment.TopEnd)
                        .padding(bottom = 32.dp)
                ) {
    
    
                    Icon(
                        painter = painterResource(R.drawable.ic_switch_camera),
                        contentDescription = "",
                        tint = Color.Green,
                        modifier = Modifier.size(36.dp)
                    )
                }
            }
        }
    }
}

@SuppressLint("MissingPermission")
@androidx.annotation.OptIn(ExperimentalVideo::class)
private fun startRecording(
    context: Context,
    cameraController: LifecycleCameraController,
    onFinished: (Uri) -> Unit,
    onProgress: (Long) -> Unit,
    onError: (Throwable?) -> Unit,
): Recording {
    
    
    // Define the File options for saving the video.
    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.CHINA)
        .format(System.currentTimeMillis())+".mp4"

    val outputFileOptions = FileOutputOptions
        .Builder(File(context.getOutputDirectory(), name))
        .build()

    // Call startRecording on the CameraController.
    return cameraController.startRecording(
        outputFileOptions,
        AudioConfig.create(true), // 开启音频
        ContextCompat.getMainExecutor(context),
    ) {
    
     videoRecordEvent ->
        when(videoRecordEvent) {
    
    
            is VideoRecordEvent.Start -> {
    
    }
            is VideoRecordEvent.Status -> {
    
    
                val duration = videoRecordEvent.recordingStats.recordedDurationNanos / 1000 / 1000
                onProgress(duration)
            }
            is VideoRecordEvent.Pause -> {
    
    }
            is VideoRecordEvent.Resume -> {
    
    }
            is VideoRecordEvent.Finalize -> {
    
    
                if (!videoRecordEvent.hasError()) {
    
    
                    val savedUri = videoRecordEvent.outputResults.outputUri
                    onFinished(savedUri)
                    context.notifySystem(savedUri, outputFileOptions.file)
                } else {
    
    
                    onError(videoRecordEvent.cause)
                }
            }
        }
    }
}
private fun Context.getOutputDirectory(): File {
    
    
    val mediaDir = externalMediaDirs.firstOrNull()?.let {
    
    
        File(it, resources.getString(R.string.app_name)).apply {
    
     mkdirs() }
    }

    return if (mediaDir != null && mediaDir.exists()) mediaDir else filesDir
}
// 发送系统广播
private fun Context.notifySystem(savedUri: Uri?, file: File) {
    
    
    // 对于运行API级别>=24的设备,将忽略隐式广播,因此,如果您只针对24+级API,则可以删除此语句
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
    
    
        sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, savedUri)) //刷新单个文件
    } else {
    
    
        MediaScannerConnection.scanFile(this, arrayOf(file.absolutePath), null, null)
    }
}

画像解析

画像分析のユースケースは、画像処理、コンピューター ビジョン、または機械学習推論を実行できる CPU からアクセス可能な画像をアプリケーションに提供します。アプリケーションはanalyze()フレームごとに実行するメソッドを実装します。

アプリケーションで Image Analytics を使用するには、次の手順に従います。

  • ユースケースを構築しますImageAnalysis
  • 作成しますImageAnalysis.Analyzer
  • ImageAnalysisアナライザーを設定します
  • lifecycleOwner、 、cameraSelectorおよびImageAnalysisユースケースをライフサイクルにバインドします。( ProcessCameraProvider.bindToLifecycle())

バインド直後、CameraX は登録されたアナライザーに画像を送信します。分析が完了したら、ユースケースを呼び出すImageAnalysis.clearAnalyzer()かバインドを解除してImageAnalysis分析を停止します。

ImageAnalysis のユースケースを構築する

ImageAnalysisアナライザー (画像コンシューマー) は CameraX (画像プロデューサー) に接続できます。ImageAnalysis.Builderアプリケーションはオブジェクトを構築するために使用できますImageAnalysisを使用するとImageAnalysis.Builder、アプリケーションを次のように構成できます。

画像出力パラメータ:

  • 形式: CameraX はおよび を介してsetOutputImageFormat(int)サポートされますデフォルトの形式は ですYUV_420_888RGBA_8888YUV_420_888
  • 解像度アスペクト比: これらのパラメータのいずれかを設定できますが、両方の値を同時に設定できないことに注意してください。
  • 回転角度
  • ターゲット名: このパラメータはデバッグに使用します。

画像フロー制御:

ビルドのImageAnalysisサンプルコードは次のとおりです。

private fun getImageAnalysis(): ImageAnalysis{
    
    
    val imageAnalysis = ImageAnalysis.Builder() 
        .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
        .setTargetResolution(Size(1280, 720))
        .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
        .build()
    val executor = Executors.newSingleThreadExecutor()
    imageAnalysis.setAnalyzer(executor, ImageAnalysis.Analyzer {
    
     imageProxy ->
        val rotationDegrees = imageProxy.imageInfo.rotationDegrees
        Log.e(TAG, "ImageAnalysis.Analyzer: imageProxy.format = ${
      
      imageProxy.format}")
        // insert your code here.
        if (imageProxy.format == ImageFormat.YUV_420_888 || imageProxy.format == PixelFormat.RGBA_8888) {
    
    
            val bitmap = imageProxy.toBitmap()
        }
        // ...
        // after done, release the ImageProxy object
        imageProxy.close()
    })
    return imageAnalysis
}
val imageAnalysis = getImageAnalysis() 
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageCapture, imageAnalysis)

注:Analyzerのコールバック メソッドは、プレビュー時にフレームごとにコールバックされます。

アプリは解像度またはアスペクト比のいずれかを設定できますが、両方を設定することはできません。正確な出力解像度は、アプリの要求されたサイズ (またはアスペクト比) とハードウェアの機能によって異なり、要求されたサイズまたはアスペクト比とは異なる場合があります。解像度マッチングアルゴリズムについては、 setTargetResolution()のドキュメントを参照してください。

アプリケーションは、出力画像ピクセルを YUV (デフォルト) または RGBA 色空間で構成できます。RGBA 出力形式を設定すると、CameraX は内部で画像を YUV カラー スペースから RGBA カラー スペースに変換し、画像ビットを最初のプレーンにパックしますImageProxy(他の 2 つのプレーンは使用されません) ByteBuffer。シーケンスは次のとおりです。

ImageProxy.getPlanes()[0].buffer[0]: alpha
ImageProxy.getPlanes()[0].buffer[1]: red
ImageProxy.getPlanes()[0].buffer[2]: green
ImageProxy.getPlanes()[0].buffer[3]: blue
...

動作モード

アプリケーションの分析パイプラインが CameraX のフレーム レート要件を満たせない場合、次のいずれかの方法でフレームをドロップするように CameraX を構成できます。

  • ノンブロッキング(デフォルト): このモードでは、アプリケーションが前のイメージを分析している間、実行プログラムは常に最新のイメージをイメージ バッファー (深さ 1 のキューと同様) にキャッシュします。アプリの処理が完了する前に CameraX が新しい画像を受信した場合、新しい画像は同じバッファに保存され、前の画像が上書きされます。この場合、ImageAnalysis.Builder.setImageQueueDepth()効果はなく、バッファの内容は常に上書きされることに注意してください。このノンブロッキング モードは、STRATEGY_KEEP_ONLY_LATESTcallを使用して有効にできます。エグゼキューター関連の効果の詳細については、 STRATEGY_KEEP_ONLY_LATESTsetBackpressureStrategy()のリファレンス ドキュメントを参照してください

  • ブロッキング: このモードでは、内部エグゼキュータは複数の画像を内部画像キューに追加し、キューがいっぱいになった場合にのみフレームのドロップを開始できます。システムはカメラ デバイス全体をブロックします。カメラ デバイスに複数のバインドされたユースケースがある場合、CameraX がそれらの画像を処理するときに、システムはそれらすべてをブロックします。たとえば、プレビューと画像分析の両方がカメラ デバイスにバインドされている場合、CameraX が画像を処理している間、対応するプレビューもブロックされます。を にSTRATEGY_BLOCK_PRODUCER渡すことで、ブロッキング モードを有効にできます。setBackpressureStrategy()さらに、ImageAnalysis.Builder.setImageQueueDepth()を使用してイメージ キューの深さを設定できます。

アナライザーの遅延が短く、パフォーマンスが高い場合、つまり画像の分析に費やされる合計時間が CameraX フレームの持続時間よりも短い場合 (たとえば、60fps で 16 ミリ秒)、上記の両方の操作モードで全体的にスムーズな処理を実現できます。経験。ブロッキング モードは、非常に短いシステム ジッターに対処する場合など、場合によっては依然として役立ちます。

プロファイラーの遅延が長く、パフォーマンスが高い場合、遅延を補うためにブロッキング モードと長いキューを組み合わせる必要があります。ただし、この場合でもアプリケーションはすべてのフレームを処理できることに注意してください。

アナライザーのレイテンシが高くて時間がかかる場合 (アナライザーがすべてのフレームを処理できない場合)、ノンブロッキング モードの方が適している可能性があります。この場合、システムは分析パスのフレームをドロップする必要がありますが、同時にバインドされている他のユースケースにはすべてのフレームがそのまま残されるためです。見える。

ML キット アナライザー (機械学習キット アナライザー)

Google のMachine Learning Suite は、顔の検出、バーコードのスキャン、画像のラベル付けなどを行うためのオンデバイス機械学習 Vision API を提供します。ML Kit Analyzer を利用すると、ML Kit を CameraX アプリケーションとより簡単に統合できます。

ML Kit アナライザーはImageAnalysis.Analyzerインターフェイスの実装です。プロファイラーは、デフォルトのターゲット解像度をオーバーライドすることで (必要に応じて) ML Suite での使用を最適化し、座標変換を処理して、集約された分析結果を返す ML Suite にフレームを渡します。

ML Kit Analyzerの実装

CameraControllerML Kit プロファイラーを実装するには、UI 要素を表示するために使用できるクラスを使用することをお勧めしますPreviewViewを使用して実装する場合CameraController、ML Kit アナライザーは、生のImageAnalysisストリームとPreviewViewの間の座標変換を処理します。アナライザーは、CameraX からターゲット座標系を受け取り、座標変換を計算し、それをDetector分析のために ML Kit クラスに転送します。

で ML Kit アナライザーを使用するにはCameraControllersetImageAnalysisAnalyzer()コンストラクターに以下を含む新しい ML Kit アナライザー オブジェクトを呼び出して渡します。

  • 機械学習スイートのリストDetector、CameraX が順番に呼び出されます。

  • ML Kit の出力座標を決定するために使用されるターゲット座標系:

    COORDINATE_SYSTEM_VIEW_REFERENCED: 変換されたPreviewView座標。
    COORDINATE_SYSTEM_ORIGINAL: 元のImageAnalysisストリーム座標。

  • Consumerコールバックを呼び出してMlKitAnalyzer.Resultアプリケーションに渡すために使用されますExecutor

  • 新しい ML Kit 出力があるときに CameraX によって呼び出されますConsumer

ML Kit プロファイラーを使用するには、依存関係を追加する必要があります。

def camerax_version = "1.3.0-alpha04"
implementation "androidx.camera:camera-mlkit-vision:${camerax_version}"

QRコード/バーコード認識

ML Kit バーコード依存関係ライブラリを追加します。

implementation 'com.google.mlkit:barcode-scanning:17.1.0'

使用例を次に示します。

private const val TAG = "MLKitAnalyzer"

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun MLKitAnalyzerCameraExample(navController: NavHostController) {
    
    
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraController = remember {
    
     LifecycleCameraController(context) }

    AndroidView(
        modifier = Modifier.fillMaxSize() ,
        factory = {
    
     context ->
            PreviewView(context).apply {
    
    
                setBackgroundColor(Color.White.toArgb())
                layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
                scaleType = PreviewView.ScaleType.FILL_CENTER
                implementationMode = PreviewView.ImplementationMode.COMPATIBLE
            }.also {
    
     previewView ->
                previewView.controller = cameraController
                cameraController.bindToLifecycle(lifecycleOwner)
                cameraController.imageAnalysisBackpressureStrategy = ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST
                cameraController.setBarcodeAnalyzer(context) {
    
     result ->
                    navController.currentBackStackEntry?.savedStateHandle?.set("result", result)
                    navController.navigate("ResultScreen")
                }
            }
        },
        onReset = {
    
    },
        onRelease = {
    
    
            cameraController.unbind()
        }
    )
}

private fun LifecycleCameraController.setBarcodeAnalyzer(
    context: Context,
    onFound: (String?) -> Unit
) {
    
    
    // create BarcodeScanner object
    val options = BarcodeScannerOptions.Builder()
        .setBarcodeFormats(Barcode.FORMAT_QR_CODE,
            Barcode.FORMAT_AZTEC, Barcode.FORMAT_DATA_MATRIX, Barcode.FORMAT_PDF417,
            Barcode.FORMAT_CODABAR, Barcode.FORMAT_CODE_39, Barcode.FORMAT_CODE_93,
            Barcode.FORMAT_EAN_8, Barcode.FORMAT_EAN_13, Barcode.FORMAT_ITF,
            Barcode.FORMAT_UPC_A, Barcode.FORMAT_UPC_E
        )
        .build()
    val barcodeScanner = BarcodeScanning.getClient(options)

    setImageAnalysisAnalyzer(
        ContextCompat.getMainExecutor(context),
        MlKitAnalyzer(
            listOf(barcodeScanner),
            COORDINATE_SYSTEM_VIEW_REFERENCED,
            ContextCompat.getMainExecutor(context)
        ) {
    
     result: MlKitAnalyzer.Result? ->
            val value = result?.getValue(barcodeScanner)
            value?.let {
    
     list ->
                if (list.size > 0) {
    
    
                    list.forEach {
    
     barCode ->
                        Log.e(TAG, "format:${
      
      barCode.format}, displayValue:${
      
      barCode.displayValue}")
                        context.showToast("识别到:${
      
      barCode.displayValue}")
                    }
                    val res = list[0].displayValue
                    if (!res.isNullOrEmpty()) onFound(res) 
                }
            }
        }
    )
}

上記のコード例では、ML Kit アナライザーは次のものを のクラスに渡しBarcodeScannerますDetector

  • ターゲット座標系の表現に基づくCOORDINATE_SYSTEM_VIEW_REFERENCED変換Matrix
  • カメラのフレーム。

BarcodeScanner問題が発生するとエラーDetectorがスローされ、ML Kit アナライザーはそのエラーをアプリに伝播します。成功すると、ML Kit アナライザーは を返しますMLKitAnalyzer.Result#getValue()(この場合はBarcodeオブジェクト)。

barcode.valueType次の方法で値の型を取得することもできます。

for (barcode in barcodes) {
    
    
    val bounds = barcode.boundingBox
    val corners = barcode.cornerPoints

    val rawValue = barcode.rawValue

    val valueType = barcode.valueType
    // See API reference for complete list of supported types
    when (valueType) {
    
    
        Barcode.TYPE_WIFI -> {
    
    
            val ssid = barcode.wifi!!.ssid
            val password = barcode.wifi!!.password
            val type = barcode.wifi!!.encryptionType
        }
        Barcode.TYPE_URL -> {
    
    
            val title = barcode.url!!.title
            val url = barcode.url!!.url
        }
    }
}

camera-coreImageAnalysisクラスを使用して ML Kit アナライザーを実装することもできます。ただし、ImageAnalysisは と統合されていないためPreviewView、座標変換を手動で処理する必要があります。詳細については、「ML Kit Analyzer Reference Documentation」を参照してください。

ルーティングと権限の構成:

@Composable
fun ExampleMLKitAnalyzerNavHost() {
    
    
    val navController = rememberNavController()
    NavHost(navController, startDestination = "MLKitAnalyzerCameraScreen") {
    
    
        composable("MLKitAnalyzerCameraScreen") {
    
    
            MLKitAnalyzerCameraScreen(navController = navController)
        }
        composable("ResultScreen") {
    
    
            ResultScreen(navController = navController)
        }
    }
}

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun MLKitAnalyzerCameraScreen(navController: NavHostController) {
    
    
    val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
    LaunchedEffect(Unit) {
    
    
        if (!cameraPermissionState.status.isGranted && !cameraPermissionState.status.shouldShowRationale) {
    
    
            cameraPermissionState.launchPermissionRequest()
        }
    }
    if (cameraPermissionState.status.isGranted) {
    
     // 相机权限已授权, 显示预览界面
        MLKitAnalyzerCameraExample(navController)
    } else {
    
     // 未授权,显示未授权页面
        NoCameraPermissionScreen(cameraPermissionState = cameraPermissionState)
    }
}

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun NoCameraPermissionScreen(cameraPermissionState: PermissionState) {
    
    
    // In this screen you should notify the user that the permission
    // is required and maybe offer a button to start another camera perission request
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
    
    
        val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
    
    
            // 如果用户之前选择了拒绝该权限,应当向用户解释为什么应用程序需要这个权限
            "未获取相机授权将导致该功能无法正常使用。"
        } else {
    
    
            // 首次请求授权
            "该功能需要使用相机权限,请点击授权。"
        }
        Text(textToShow)
        Spacer(Modifier.height(8.dp))
        Button(onClick = {
    
     cameraPermissionState.launchPermissionRequest() }) {
    
     Text("请求权限") }
    }
}
// 展示识别结果
@Composable
fun ResultScreen(navController: NavHostController) {
    
    
    val result = navController.previousBackStackEntry?.savedStateHandle?.get<String>("result")
    result?.let {
    
    
        Box(modifier = Modifier.fillMaxSize()) {
    
    
            Text("$it", fontSize = 18.sp, modifier = Modifier.align(Alignment.Center))
        }
    }
}

関連コンテンツの詳細については、https: //developers.google.cn/ml-kit/vision/barcode-scanning/android?hl =zh-cn を参照してください。

テキスト認識

依存関係を追加します。

dependencies {
    
    
  // To recognize Latin script
  implementation 'com.google.mlkit:text-recognition:16.0.0'
  // To recognize Chinese script
  implementation 'com.google.mlkit:text-recognition-chinese:16.0.0'
  // To recognize Devanagari script
  implementation 'com.google.mlkit:text-recognition-devanagari:16.0.0'
  // To recognize Japanese script
  implementation 'com.google.mlkit:text-recognition-japanese:16.0.0'
  // To recognize Korean script
  implementation 'com.google.mlkit:text-recognition-korean:16.0.0'
}

サンプルコード:

private fun LifecycleCameraController.setTextAnalyzer(
    context: Context,
    onFound: (String) -> Unit
) {
    
    
    var called = false
    // val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS) // 拉丁文
    val recognizer = TextRecognition.getClient(ChineseTextRecognizerOptions.Builder().build()) // 中文
    setImageAnalysisAnalyzer(
        ContextCompat.getMainExecutor(context),
        MlKitAnalyzer(
            listOf(recognizer),
            COORDINATE_SYSTEM_VIEW_REFERENCED,
            ContextCompat.getMainExecutor(context)
        ) {
    
     result: MlKitAnalyzer.Result? ->
            val value = result?.getValue(recognizer)
            value?.let {
    
     resultText ->
                val sb = StringBuilder()
                for (block in resultText.textBlocks) {
    
    
                    val blockText = block.text
                    sb.append(blockText).append("\n")
                    val blockCornerPoints = block.cornerPoints
                    val blockFrame = block.boundingBox
                    for (line in block.lines) {
    
    
                        val lineText = line.text
                        val lineCornerPoints = line.cornerPoints
                        val lineFrame = line.boundingBox
                        for (element in line.elements) {
    
    
                            val elementText = element.text
                            val elementCornerPoints = element.cornerPoints
                            val elementFrame = element.boundingBox
                        }
                    }
                }
                val res = sb.toString()
                if (res.isNotEmpty() && !called) {
    
    
                    Log.e(TAG, "$res")
                    context.showToast("识别到:$res")
                    onFound(res)
                    called = true
                }
            }
        }
    )
}

テキスト認識機能は、テキストをブロック、行、要素、記号に分割します。大ざっぱに言えば:

  • ブロックは、段落や列などの連続したテキスト行のグループです。
  • ラインは同じ軸上にある連続した単語のグループであり、
  • 要素は、ラテン アルファベットのほとんどの列の他の文字を参照する、ラテン アルファベットの連続する英数字 (「単語」) のグループです。
  • シンボルは、ほとんどのラテン語では同じ軸上の単一の英数字、または他の言語では文字です

次の図では、これらの例を降順で強調表示しています。最初の強調表示されたブロック (シアンで示されている) はテキストのブロックです。強調表示されている青いブロックの 2 番目のセットはテキスト行です。最後に、濃い青色のテキストで強調表示されている 3 番目の単語セットは Word です。

ここに画像の説明を挿入

検出されたすべてのブロック、線、要素、記号について、API は境界ボックス、コーナー、回転情報、信頼スコア、認識された言語、および認識されたテキストを返します。

関連コンテンツの詳細については、https: //developers.google.cn/ml-kit/vision/text-recognition/v2/android? hl=zh-cn を参照してください。

顔認識

依存関係を追加します。

dependencies {
    
     
  // Use this dependency to bundle the model with your app
  implementation 'com.google.mlkit:face-detection:16.1.5'
}

写真に顔検出を適用する前に、顔検出器のデフォルト設定を変更する場合は、FaceDetectorOptions オブジェクトを使用してそれらの設定を指定します。次の設定を変更できます。

設定 関数
setPerformanceMode PERFORMANCE_MODE_FAST(デフォルト) /PERFORMANCE_MODE_ACCURATE顔検出の速度または精度を優先します。
setLandmarkMode LANDMARK_MODE_NONE(デフォルト) /LANDMARK_MODE_ALL顔の「ランドマーク」(目、耳、鼻、頬、口など) を識別しようとするかどうか。
setContourMode CONTOUR_MODE_NONE(デフォルト) /CONTOUR_MODE_ALL顔の輪郭を検出するかどうか。写真内で最も目立つ顔の輪郭のみを検出します。
setClassificationMode CLASSIFICATION_MODE_NONE(デフォルト) /CLASSIFICATION_MODE_ALL顔をさまざまなカテゴリに分類するかどうか (例: 「笑顔」と「目を開けている」)。
setMinFaceSize float(デフォルト: 0.1f) 頭の幅と画像の幅の比率として表される最小の顔サイズを設定します。
enableTracking false(デフォルト) /true画像全体で顔を追跡する際に使用する ID を顔に割り当てるかどうか。
輪郭検出を有効にすると、顔が 1 つだけ検出されるため、顔追跡では有用な結果が得られないことに注意してください。このため、検出を高速化するには、輪郭検出と顔追跡を同時に有効にしないでください。

例えば:

// High-accuracy landmark detection and face classification
val highAccuracyOpts = FaceDetectorOptions.Builder()
        .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
        .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
        .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
        .build()

// Real-time contour detection
val realTimeOpts = FaceDetectorOptions.Builder()
        .setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL)
        .build()

// 获取 FaceDetector 实例
val detector = FaceDetection.getClient(highAccuracyOpts)
// Or, to use the default option:
// val detector = FaceDetection.getClient();

顔検出操作が成功すると、システムはオブジェクトの配列を成功リスナーに渡しますFaceFaceオブジェクトは、画像内で検出された顔を表します。顔ごとに、入力画像内の境界座標と、顔検出器が検索するように構成したその他の情報を取得します。例えば:

for (face in faces) {
    
    
    val bounds = face.boundingBox
    val rotY = face.headEulerAngleY // Head is rotated to the right rotY degrees
    val rotZ = face.headEulerAngleZ // Head is tilted sideways rotZ degrees

    // If landmark detection was enabled (mouth, ears, eyes, cheeks, and nose available):
    val leftEar = face.getLandmark(FaceLandmark.LEFT_EYE)
    leftEar?.let {
    
    
        val leftEarPos = leftEar.position
    }

    // If contour detection was enabled:
    val leftEyeContour = face.getContour(FaceContour.LEFT_EYE)?.points
    val upperLipBottomContour = face.getContour(FaceContour.UPPER_LIP_BOTTOM)?.points

    // If classification was enabled:
    if (face.smilingProbability != null) {
    
    
        val smileProb = face.smilingProbability
    }
    if (face.rightEyeOpenProbability != null) {
    
    
        val rightEyeOpenProb = face.rightEyeOpenProbability
    }

    // If face tracking was enabled:
    if (face.trackingId != null) {
    
    
        val id = face.trackingId
    }
}

サンプルコード:

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun MLKitFaceDetectorExample() {
    
    
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraController = remember {
    
     LifecycleCameraController(context) }
    var faces by remember {
    
     mutableStateOf(listOf<Face>()) }

    val bounds = remember(faces) {
    
    
        faces.map {
    
     face -> face.boundingBox }
    }

    val points = remember(faces) {
    
     getPoints(faces) }

    Box(modifier = Modifier.fillMaxSize()) {
    
    
        AndroidView(
            modifier = Modifier.fillMaxSize(),
            factory = {
    
     context ->
                PreviewView(context).apply {
    
    
                    setBackgroundColor(Color.White.toArgb())
                    layoutParams = LinearLayout.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.MATCH_PARENT
                    )
                    scaleType = PreviewView.ScaleType.FILL_CENTER
                    implementationMode = PreviewView.ImplementationMode.COMPATIBLE
                }.also {
    
     previewView ->
                    previewView.controller = cameraController
                    cameraController.bindToLifecycle(lifecycleOwner)
                    cameraController.imageAnalysisBackpressureStrategy =
                        ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST
                    cameraController.setFaceDetectorAnalyzer(context) {
    
     faces = it }
                }
            },
            onReset = {
    
    },
            onRelease = {
    
    
                cameraController.unbind()
            }
        )
        Canvas(modifier = Modifier.fillMaxSize()) {
    
    
            bounds.forEach {
    
     rect ->
                drawRect(
                    Color.Red,
                    size = Size(rect.width().toFloat(), rect.height().toFloat()),
                    topLeft = Offset(x = rect.left.toFloat(), y = rect.top.toFloat()),
                    style = Stroke(width = 5f)
                )
            }
            points.forEach {
    
     point ->
                drawCircle(
                    Color.Green,
                    radius = 2.dp.toPx(),
                    center = Offset(x = point.x, y = point.y),
                )
            }
        }
    }
}

private fun LifecycleCameraController.setFaceDetectorAnalyzer(
    context: Context,
    onFound: (List<Face>) -> Unit
) {
    
    
    // High-accuracy landmark detection and face classification
    val highAccuracyOpts = FaceDetectorOptions.Builder()
        .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
        .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
        .enableTracking()
        .build()
    // Real-time contour detection
    val realTimeOpts = FaceDetectorOptions.Builder()
        .setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL)
        .build()
    val detector = FaceDetection.getClient(highAccuracyOpts)
    setImageAnalysisAnalyzer(
        ContextCompat.getMainExecutor(context),
        MlKitAnalyzer(
            listOf(detector),
            COORDINATE_SYSTEM_VIEW_REFERENCED,
            ContextCompat.getMainExecutor(context)
        ) {
    
     result: MlKitAnalyzer.Result? ->
            val value = result?.getValue(detector)
            value?.let {
    
     onFound(it) }
        }
    )
}

// All landmarks
private val landMarkTypes = intArrayOf(
    FaceLandmark.MOUTH_BOTTOM,
    FaceLandmark.MOUTH_RIGHT,
    FaceLandmark.MOUTH_LEFT,
    FaceLandmark.RIGHT_EYE,
    FaceLandmark.LEFT_EYE,
    FaceLandmark.RIGHT_EAR,
    FaceLandmark.LEFT_EAR,
    FaceLandmark.RIGHT_CHEEK,
    FaceLandmark.LEFT_CHEEK,
    FaceLandmark.NOSE_BASE
)

private fun getPoints(faces: List<Face>) : List<PointF> {
    
    
    val points = mutableListOf<PointF>()
    for (face in faces) {
    
    
        landMarkTypes.forEach {
    
     landMarkType ->
            face.getLandmark(landMarkType)?.let {
    
    
                points.add(it.position)
            }
        }
    }
    return points
}

効果:

ここに画像の説明を挿入

関連コンテンツの詳細については、https: //developers.google.cn/ml-kit/vision/face-detection/android?hl =zh-cn を参照してください。

CameraX のその他の高度な構成オプション

カメラXConfig

わかりやすくするために、CameraX には、ほとんどの使用シナリオに適したデフォルト構成 (内部エグゼキューターやハンドラーなど) が用意されています。ただし、アプリケーションに特別な要件がある場合、またはこれらの構成をカスタマイズしたい場合は、CameraXConfigこの目的のためにインターフェイスを使用できます。

help を使用するとCameraXConfig、アプリは次のことを実行できます。

  • 起動遅延時間を最適化するために使用しますsetAvailableCameraLimiter()
  • を使用して、setCameraExecutor()アプリケーション実行プログラムを CameraX に提供します。
  • デフォルトのスケジューラ ハンドラを に置き換えますsetSchedulerHandler()
  • setMinimumLoggingLevel()ログレベルを変更するために使用します。

次の手順は、 の使用方法を示していますCameraXConfig

  1. カスタム構成を使用してオブジェクトを作成しますCameraXConfig
  2. のインターフェイスApplicationを実装しCameraXConfig.ProviderのオブジェクトgetCameraXConfig()を返します。CameraXConfig
  3. ApplicationクラスをAndroidManifest.xmlファイルに追加します。

たとえば、次のコード サンプルでは、​​CameraX のログ記録をエラー メッセージのみに制限しています。

class CameraApplication : Application(), CameraXConfig.Provider {
    
    
   override fun getCameraXConfig(): CameraXConfig {
    
    
       return CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig())
           .setMinimumLoggingLevel(Log.ERROR).build()
   }
}

CameraX の設定後にアプリがそれについて知る必要がある場合は、CameraXConfigオブジェクトのローカル コピーを保存してください。

カメラリミッター

最初の呼び出し中にProcessCameraProvider.getInstance()、CameraX はデバイス上で使用可能なカメラの特性を列挙し、クエリします。CameraX はハードウェア コンポーネントと通信する必要があるため、特にローエンド デバイスでは、各カメラでこのプロセスに時間がかかることがあります。アプリケーションがデバイス上の特定のカメラ (デフォルトの前面カメラなど) のみを使用する場合、他のカメラを無視するように CameraX を設定すると、アプリケーションで使用されるカメラの起動遅延が短縮されます。

カメラを除外するために が渡された場合、CameraX は実行時にカメラが存在しないものとみなしCameraXConfig.Builder.setAvailableCamerasLimiter()ますCameraSelectorたとえば、次のコードは、アプリがデバイスのデフォルトの背面カメラのみを使用するように制限します。

class MainApplication : Application(), CameraXConfig.Provider {
    
    
   override fun getCameraXConfig(): CameraXConfig {
    
    
       return CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig())
              .setAvailableCamerasLimiter(CameraSelector.DEFAULT_BACK_CAMERA)
              .build()
   }
}

CameraX が構築されているプラ​​ットフォーム API の多くは、ハードウェアとのプロセス間通信 (IPC) をブロックする必要があり、場合によっては数百ミリ秒の応答時間が必要になる場合があります。したがって、CameraX はこれらの API をバックグラウンド スレッドからのみ呼び出すことで、メイン スレッドのブロックを回避し、インターフェイスをスムーズに保ちます。CameraX はこれらのバックグラウンド スレッドを内部で管理するため、この種の動作は透過的に表示されます。ただし、一部のアプリケーションでは、スレッドを厳密に制御する必要があります。CameraXConfigアプリケーションがCameraXConfig.Builder.setCameraExecutor()とでCameraXConfig.Builder.setSchedulerHandler()使用されるバックグラウンド スレッドを設定できるようにします。

自動的に選択します

CameraX は、アプリが実行されているデバイスに基づいて専用の機能を自動的に提供します。たとえば、解像度を指定しなかった場合、またはサポートされていない解像度を指定した場合、CameraX は使用する最適な解像度を自動的に決定します。これらの操作はすべてライブラリによって処理され、デバイス固有のコードを記述する必要はありません。

CameraX の目標は、カメラ セッションを正常に初期化することです。これは、CameraX がデバイスの機能に応じて解像度とアスペクト比を下げることを意味します。これは次の理由で発生します。

  • デバイスは要求された解像度をサポートしていません。
  • 古いデバイスが適切に機能するには特定の解像度が必要であるなど、デバイスとの互換性の問題があります。
  • 一部のデバイスでは、一部の形式は特定のアスペクト比でのみ利用できます。
  • JPEG またはビデオ エンコードの場合、デバイスは「最も近い mod16」を優先します。詳細については、 「SCALER_STREAM_CONFIGURATION_MAP」を参照してください

CameraX はセッションを作成して管理しますが、ユースケースの出力によって返される画像のサイズを常にコードでチェックし、それに応じて調整する必要があります。

回転する

デフォルトでは、ユースケースの作成中に、カメラの回転がデフォルトのディスプレイの回転と一致するように設定されます。デフォルトでは、CameraX は、アプリがプレビューで期待どおりのものであることを保証する出力を生成します。回転角度をカスタム値に変更してマルチディスプレイ デバイスをサポートするには、ユース ケース オブジェクトの構成時に現在のディスプレイの向きを渡すか、ユース ケース オブジェクトの作成後にディスプレイの向きを動的に渡します。

アプリは構成設定を使用してターゲットの回転を設定できます。その後、ライフサイクルの実行中であっても、アプリは ImageAnalysis.setTargetRotation() などのユースケース API のメソッドを使用して回転設定を更新できます。アプリがポートレート モードでロックされているときに上記の操作を実行できるため、回転を再構成する必要はありませんが、写真や分析のユースケースでは、デバイスの現在の回転に関する知識が必要です。たとえば、ユースケースでは、正しい向きで顔検出を行うため、または写真を横向きまたは縦向きに設定するために、回転角度を知る必要がある場合があります。

撮影した写真のデータは回転情報とともに保存されない場合があります。Exif データには、保存後にギャラリー アプリが画像を正しい画面方向で表示するための回転情報が含まれています。

プレビュー データを正しい方向で表示するには、Preview.PreviewOutput()のメタデータ出力を使用して変換を作成できます。

次のコード サンプルは、向きイベントの回転角度を設定する方法を示しています。

override fun onCreate() {
    
    
    val imageCapture = ImageCapture.Builder().build()

    val orientationEventListener = object : OrientationEventListener(this as Context) {
    
    
        override fun onOrientationChanged(orientation : Int) {
    
    
            // Monitors orientation values to determine the target rotation value
            val rotation : Int = when (orientation) {
    
    
                in 45..134 -> Surface.ROTATION_270
                in 135..224 -> Surface.ROTATION_180
                in 225..314 -> Surface.ROTATION_90
                else -> Surface.ROTATION_0
            }

            imageCapture.targetRotation = rotation
        }
    }
    orientationEventListener.enable()
}

各ユースケースは、設定された回転角度に従って画像データを直接回転するか、回転されていない画像データの回転メタデータをユーザーに提供します。

  • プレビュー:Preview.getTargetRotation()ターゲット解像度の回転設定を知るために使用するメタデータ出力を提供します。
  • ImageAnalysis : 画像バッファー座標が表示座標を基準にしてどこにあるかを理解するためのメタデータ出力を提供します。
  • ImageCapture : 画像のExifメタデータ、バッファ、またはその両方を変更して、回転設定を反映します。変更される値は HAL の実装によって異なります。

回転の詳細については、https: //developer.android.google.cn/training/camerax/orientation-rotation ?hl=zh-cn を参照してください。

クリッピング長方形

デフォルトでは、クリッピング四角形は完全なバッファー四角形であり、ViewPortと を使用してUseCaseGroupカスタマイズできます。ユース ケースをグループ化し、ビューポートを設定することにより、CameraX は、グループ内のすべてのユース ケースのクリッピング四角形がカメラ センサーの同じ領域を指すことを保証します。

次のコード スニペットは、これら 2 つのクラスの使用方法を示しています。

val viewPort =  ViewPort.Builder(Rational(width, height), display.rotation).build()
val useCaseGroup = UseCaseGroup.Builder()
    .addUseCase(preview)
    .addUseCase(imageAnalysis)
    .addUseCase(imageCapture)
    .setViewPort(viewPort)
    .build()
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCaseGroup)

ViewPortエンドユーザーに表示されるバッファ四角形を指定するために使用されます。CameraX は、ビューポートのプロパティと付属のユースケースに基づいて、可能な最大のクリッピング四角形を計算します。一般に、表示どおりの結果を実現するには、プレビューの使用例に従ってビューポートを構成する必要があります。ビューポートを取得する簡単な方法は、 を使用することですPreviewView

ViewPort次のコード スニペットは、オブジェクトを取得する方法を示しています。

val viewport = findViewById<PreviewView>(R.id.preview_view).viewPort

前述の例では、アプリは、エンド ユーザーが表示するのとImageAnalysis同じコンテンツを取得します(ズーム タイプ がデフォルト値 に設定されていると仮定します)。クリッピング四角形と回転が出力バッファーに適用されると、解像度は異なる場合がありますが、画像はすべてのユースケースで一貫したものになります。変換情報を適用する方法の詳細については、「変換の出力」を参照してください。ImageCapturePreviewViewPreviewViewFILL_CENTER

利用可能なカメラを選択してください

CameraX は、アプリケーションの要件とユースケースに基づいて、最適なカメラ デバイスを自動的に選択します。自動的に選択されたデバイス以外のデバイスを使用したい場合は、いくつかのオプションがあります。

  • CameraSelector.DEFAULT_FRONT_CAMERAデフォルトのフロントカメラを要求するために使用します。
  • CameraSelector.DEFAULT_BACK_CAMERAデフォルトの背面カメラを要求するために使用します。
  • 使用可能なデバイスのリストをフィルタリングするCameraSelector.Builder.addCameraFilter()ために使用します。CameraCharacteristics

CameraManager.getCameraIdList()注: カメラ デバイスは、使用する前にシステムによって認識され、 に表示される必要があります。

さらに、各 OEM は外部カメラ デバイスをサポートするかどうかを選択する必要があります。したがって、外部カメラを使用する前に、PackageManager.FEATURE_CAMERA_EXTERNALが有効になっていることを必ず確認してください。

次のコード サンプルは、デバイスの選択に影響を与える を作成する方法を示していますCameraSelector

fun selectExternalOrBestCamera(provider: ProcessCameraProvider):CameraSelector? {
    
    
   val cam2Infos = provider.availableCameraInfos.map {
    
    
       Camera2CameraInfo.from(it)
   }.sortedByDescending {
    
    
       // HARDWARE_LEVEL is Int type, with the order of:
       // LEGACY < LIMITED < FULL < LEVEL_3 < EXTERNAL
       it.getCameraCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
   }

   return when {
    
    
       cam2Infos.isNotEmpty() -> {
    
    
           CameraSelector.Builder()
               .addCameraFilter {
    
    
                   it.filter {
    
     camInfo ->
                       // cam2Infos[0] is either EXTERNAL or best built-in camera
                       val thisCamId = Camera2CameraInfo.from(camInfo).cameraId
                       thisCamId == cam2Infos[0].cameraId
                   }
               }.build()
       }
       else -> null
    }
}

// create a CameraSelector for the USB camera (or highest level internal camera)
val selector = selectExternalOrBestCamera(processCameraProvider)
processCameraProvider.bindToLifecycle(this, selector, preview, analysis)

複数のカメラを同時に選択する

CameraX 1.3以降では、複数のカメラを同時に選択することもできます。たとえば、フロントカメラとリアカメラを装備して、両方の視点から同時に写真を撮ったり、ビデオを録画したりできます。

同時カメラ機能を使用すると、デバイスはレンズの向きが異なる 2 台のカメラを同時に実行したり、2 台の背面カメラを同時に実行したりできます。次のコード ブロックは、bindToLifecycle呼び出し時に 2 つのカメラを設定し、返されたオブジェクトからConcurrentCamera2 つのオブジェクトを取得する方法を示しています。Camera

// Build ConcurrentCameraConfig
val primary = ConcurrentCamera.SingleCameraConfig(
    primaryCameraSelector,
    useCaseGroup,
    lifecycleOwner
)

val secondary = ConcurrentCamera.SingleCameraConfig(
    secondaryCameraSelector,
    useCaseGroup,
    lifecycleOwner
)

val concurrentCamera = cameraProvider.bindToLifecycle(
    listOf(primary, secondary)
)

val primaryCamera = concurrentCamera.cameras[0]
val secondaryCamera = concurrentCamera.cameras[1]

カメラの解像度

デバイスの機能、デバイスがサポートするハードウェア レベル、使用例、提供されたアスペクト比の組み合わせに基づいて、CameraX に画像解像度を設定させるように選択できます。あるいは、対応する構成がサポートされているユースケースでは、特定のターゲット解像度または特定のアスペクト比を設定できます。

自動解決

CameraX は、cameraProcessProvider.bindToLifecycle()で指定された使用例に基づいて、最適な解像度設定を自動的に決定できます。可能な限り、bindToLifecycle()1 つの呼び出しの 1 つのセッションで同時に実行する必要があるすべてのユースケースを指定します。CameraX は、デバイスがサポートするハードウェア レベルとデバイス固有の変動 (デバイスが利用可能なストリーミング構成を超えるか満たさない) を考慮して、バンドルされたユース ケースのセットに基づいて解像度を決定します。これは、アプリが最小限のデバイス固有のコード パスでさまざまなデバイス上で実行されるようにするために行われます。

画像キャプチャおよび画像分析の使用例のデフォルトのアスペクト比は です4:3

アスペクト比を構成できるユースケースの場合は、UI デザインに基づいてアプリに希望のアスペクト比を指定させます。CameraX は、デバイスでサポートされているアスペクト比に可能な限り一致する、要求されたアスペクト比で出力を生成します。サポートされている完全に一致する解像度がない場合は、最も多くの基準を満たす解像度が選択されます。つまり、アプリケーションでカメラがどのように表示されるかはアプリケーションが決定し、CameraX はさまざまなデバイスの特定の要件を満たす最適なカメラ解像度設定を決定します。

たとえば、アプリは次のいずれかを実行できます。

  • ユースケースの4:3または16:9のターゲット解像度を指定します
  • カスタム解像度を指定します。CameraX はこの解像度に最も近い解像度を見つけようとします。
  • ImageCaptureトリミングのアスペクト比を指定します

CameraX はCamera2内部インターフェイスの解像度を自動的に選択します。以下の表に、これらの解像度を示します。

ここに画像の説明を挿入

解像度を指定する

このメソッドを使用してユースケースを構築する場合setTargetResolution(Size resolution)、次のコード例に示すように、特定の解像度を設定できます。

val imageAnalysis = ImageAnalysis.Builder()
    .setTargetResolution(Size(1280, 720))
    .build()

同じユースケースに対してターゲット アスペクト比とターゲット解像度を設定することはできません。存在する場合、構成オブジェクトの構築時にスローされますIllegalArgumentException

対応サイズを目標回転角度だけ回転させた後、解像度を座標系で表現しますSizeたとえば、自然な画面の向きが縦であり、自然なターゲット回転角度を使用するデバイスは、縦の画像を要求するかどうかを指定できますが、同じ480x640デバイスが回転され90、横の画面の向きをターゲットにする場合は、それを指定できます640x480

ターゲット解像度は、画像解像度の下限を設定しようとします。実際の画像解像度は利用可能な最も近い解像度であり、そのサイズはカメラの実装によって決定されるターゲット解像度より小さくありません。

ただし、ターゲット解像度以上の解像度が存在しない場合は、ターゲット解像度より小さい、最も近い解像度が選択されます。提供されたものと同じアスペクト比の解像度Sizeが、異なるアスペクト比の解像度よりも優先されます。

CameraX は、リクエストに応じて最適な解像度を適用します。アスペクト比要件を満たすことが主なニーズである場合にのみ指定すると、setTargetAspectRatioCameraX がデバイスに基づいて適切な特定の解像度を決定します。アプリケーションの主な要件が、画像処理効率を向上させるために解像度を指定することである場合に使用します (デバイスの処理能力に応じて小型または中サイズの画像を処理するなど) setTargetResolution(Size resolution)

注: を使用するとsetTargetResolution()、他の使用例と一致しないアスペクト比のバッファが得られる可能性があります。アスペクト比を一致させる必要がある場合は、両方のユースケースから返されるバッファーのサイズを確認し、一方をもう一方に一致するようにクリップまたはスケールします。
アプリケーションが正確な解像度を必要とする場合は、createCaptureSession()表を参照して各ハードウェア レベルでサポートされる最大解像度を確認してください。現在のデバイスでサポートされている特定の解像度を確認するには、「 」を参照してくださいStreamConfigurationMap.getOutputSizes(int)

アプリがAndroid 10以降で実行されている場合は、isSessionConfigurationSupported()認証固有の を使用できますSessionConfiguration

カメラ出力の制御

CameraX では、個別のユース ケースごとにカメラ出力をオプションで構成できるだけでなく、バ​​ンドルされているすべてのユース ケースにわたる共通のカメラ操作をサポートする次のインターフェイスも実装しています。

  • を使用するとCameraControl、一般的なカメラ機能を設定できます。
  • を使用するとCameraInfo、これらの一般的なカメラ機能のステータスを照会できます。

次のCameraControlカメラ機能がサポートされています。

  • ズーム
  • 懐中電灯
  • フォーカスと測光(タップしてフォーカス)
  • 露出補正

CameraControl と CameraInfo のインスタンスを取得する

ProcessCameraProvider.bindToLifecycle()によって返されたCameraオブジェクトを使用してCameraControl、のインスタンスを取得しますCameraInfo次のコードは例を示しています。

val camera = processCameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview)

// For performing operations that affect all outputs.
val cameraControl = camera.cameraControl
// For querying information and states.
val cameraInfo = camera.cameraInfo

たとえば、を呼び出した後にbindToLifecycle()ズーム操作やその他のCameraControl操作を送信できます。カメラのバインドに使用されたインスタンスを停止または破棄するとactivityCameraControlそれ以上の操作は実行できなくなり、失敗が返されますListenableFuture

注:LifecycleOwnerが停止または破壊された場合、Cameraオフになり、その後、ズーム、トーチ、フォーカス、測光のすべての状態変化と露出補正コントロールがデフォルト値に戻ります。

ズーム

CameraControlズーム レベルを変更する 2 つの方法が提供されています。

  • setZoomRatio() は、ズーム率でズームを設定するために使用されます。

    比率はCameraInfo.getZoomState().getValue().getMinZoomRatio()からの範囲内である必要がありますCameraInfo.getZoomState().getValue().getMaxZoomRatio()それ以外の場合、関数は failed を返しますListenableFuture

  • setLinearZoom() は、と の間の線形ズーム値で現在のズーム操作を設定します01.0

リニア ズームの利点は、ズームの変化に応じて視野 (FOV) を拡大できることです。したがって、リニア ズームはSliderビューで適切に機能します。

CameraInfo.getZoomState()現在のズーム状態を返しますLiveDataこの値は、カメラが初期化されたとき、またはsetZoomRatio()またはを使用してズームレベルが設定されたときに変化します。setLinearZoom()いずれかのメソッドを呼び出すと、サポートされている値ZoomState.getZoomRatio()ZoomState.getLinearZoom()値が設定されます。これは、ズーム スライダーの横にズーム スケールのテキストを表示する場合に便利です。どちらもキャストせずに単に観察するだけで更新できますZoomState LiveData

これら 2 つの API によって返される により、ListenableFuture指定されたズーム値で繰り返されたリクエストが完了したときに通知を受け取るオプションがアプリに与えられます。また、前のズーム操作がまだ進行中に新しいズーム値を設定すると、前のズーム操作はListenableFutureすぐに失敗します。

懐中電灯

CameraControl.enableTorch(boolean)懐中電灯は有効または無効にできます (懐中電灯アプリ)。

CameraInfo.getTorchState()現在の懐中電灯のステータスを照会するために使用できます。CameraInfo.hasFlashUnit()トーチ機能が利用可能かどうかは、 によって返される値を確認することで判断できます。トーチが利用できない場合、呼び出しによりCameraControl.enableTorch(boolean)返された はListenableFuture失敗の結果で直ちに完了し、トーチの状態を に設定しますTorchState.OFF

有効にすると、フラッシュ モードの設定に関係なく、写真やビデオの撮影時にトーチが点灯したままになります。ImageCapture懐中電灯が無効な場合にのみ機能しますflashMode

フォーカスと測光

CameraControl.startFocusAndMetering()AF/AE/AWB測光エリアは、オートフォーカスと露出測光をトリガーする指定に従って設定できますFocusMeteringActionこの方法で「タップしてフォーカス」機能を実装するカメラ アプリは数多くあります。

メータリングポイント

まず、MeteringPointFactory.createPoint(float x, float y, float size)Createを使用しますMeteringPointMeteringPointカメラSurface上の単一点を表します。正規化された形式で保存されるため、AF/AE/AWB エリアを指定するためのセンサー座標に簡単に変換できます。

MeteringPointのサイズは0との間で1、デフォルトのサイズは です0.15fを呼び出すとMeteringPointFactory.createPoint(float x, float y, float size)、CameraX は提供された を中心とするsize長方形の領域を作成します。(x, y)

次のコードは、 の作成方法を示していますMeteringPoint

// Use PreviewView.getMeteringPointFactory if PreviewView is used for preview.
previewView.setOnTouchListener((view, motionEvent) ->  {
    
    
val meteringPoint = previewView.meteringPointFactory
    .createPoint(motionEvent.x, motionEvent.y)}

// Use DisplayOrientedMeteringPointFactory if SurfaceView / TextureView is used for
// preview. Please note that if the preview is scaled or cropped in the View,
// it’s the application's responsibility to transform the coordinates properly
// so that the width and height of this factory represents the full Preview FOV.
// And the (x,y) passed to create MeteringPoint might need to be adjusted with
// the offsets.
val meteringPointFactory = DisplayOrientedMeteringPointFactory(
     surfaceView.display,
     camera.cameraInfo,
     surfaceView.width,
     surfaceView.height
)

// Use SurfaceOrientedMeteringPointFactory if the point is specified in
// ImageAnalysis ImageProxy.
val meteringPointFactory = SurfaceOrientedMeteringPointFactory(
     imageWidth,
     imageHeight,
     imageAnalysis)

startFocusAndMetering および FocusMeteringAction
を呼び出す必要がある場合は、 1 つ以上の を含むstartFocusAndMetering()アプリケーションを構築する必要があります。後者は FLAG_AF、FLAG_AE、FLAG_AWB などのオプションのメータリング モードで構成されます。次のコードは、この使用法を示しています。FocusMeteringActionMeteringPoints

val meteringPoint1 = meteringPointFactory.createPoint(x1, x1)
val meteringPoint2 = meteringPointFactory.createPoint(x2, y2)
val action = FocusMeteringAction.Builder(meteringPoint1) // default AF|AE|AWB
      // Optionally add meteringPoint2 for AF/AE.
      .addPoint(meteringPoint2, FLAG_AF | FLAG_AE)
      // The action is canceled in 3 seconds (if not set, default is 5s).
      .setAutoCancelDuration(3, TimeUnit.SECONDS)
      .build()

val result = cameraControl.startFocusAndMetering(action)
// Adds listener to the ListenableFuture if you need to know the focusMetering result.
result.addListener({
    
    
   // result.get().isFocusSuccessful returns if the auto focus is successful or not.
}, ContextCompat.getMainExecutor(this)

上のコードに示すように、AF/AE/AWB 測光ゾーン用の 1 つと、AF および AE のみ用の 1 つstartFocusAndMetering()を受け入れますFocusMeteringActionMeteringPointMeteringPoint

内部的には、CameraX はそれを Camera2 に変換しMeteringRectangles、対応するCONTROL_AF_REGIONS/CONTROL_AE_REGIONS/CONTROL_AWB_REGIONSパラメーターをキャプチャ リクエストに設定します。

すべてのデバイスが AF/AE/AWB および複数のゾーンをサポートしているわけではないため、CameraX は最善を尽くしますFocusMeteringActionCameraX はサポートされている最大数を使用しMeteringPoint、測光ポイントが追加された順序で順番に使用します。MeteringPointCameraX は、サポートされている最大数を超えるすべての追加を無視します。たとえば、 2 のみをサポートするMeteringPointプラットフォームでFocusMeteringAction3 を指定するとMeteringPoint、CameraX は最初の 2 のみを使用しMeteringPoint、最後の は無視しますMeteringPoint

露出補正

露出補正は、自動露出 (AE) 出力の結果以外に、露出値 (EV) の微調整が必​​要なアプリケーションの場合に役立ちます。CameraX は、次のように露出補正値を組み合わせて、現在の画像条件に望ましい露出を決定します。

Exposure = ExposureCompensationIndex * ExposureCompensationStep

CameraX はCamera.CameraControl.setExposureCompensationIndex()露出補正をインデックス値に設定する機能を提供します。

インデックス値が正の場合、画像は明るくなり、インデックス値が負の場合、画像は暗くなります。次のセクションで説明するように、アプリはCameraInfo.ExposureState.exposureCompensationRange()サポートされている範囲をクエリできます。返された ListenableFuture は、対応する値がサポートされている場合はキャプチャ リクエストで値が正常に有効になったときに完了し、指定されたインデックスがサポートされている範囲外である場合は、返された ListenableFuture がsetExposureCompensationIndex()失敗ListenableFutureしてただちに完了します。

CameraX は、最新の未処理のsetExposureCompensationIndex()リクエストのみを保持します。前のリクエストが実行されていない間にこの関数を複数回呼び出すと、リクエストはキャンセルされます。

次のスニペットは、露出補正インデックスを設定し、露出変更リクエストがいつ実行されたかを知るためのコールバックを登録します。

camera.cameraControl.setExposureCompensationIndex(exposureCompensationIndex)
   .addListener({
    
    
      // Get the current exposure compensation index, it might be
      // different from the asked value in case this request was
      // canceled by a newer setting request.
      val currentExposureIndex = camera.cameraInfo.exposureState.exposureCompensationIndex
      …
   }, mainExecutor)

Camera.CameraInfo.getExposureState()ExposureState以下を含む現在のものを取得できます。

  • 露出補正コントロールのサポート。
  • 現在の露出補正インデックス。
  • 露出補正指数範囲。
  • 露出補正値の計算に使用される露出補正ステップ。

たとえば、次のコードは現在のExposureState値で露出設定を初期化しますSeekBar

val exposureState = camera.cameraInfo.exposureState
binding.seekBar.apply {
    
    
   isEnabled = exposureState.isExposureCompensationSupported
   max = exposureState.exposureCompensationRange.upper
   min = exposureState.exposureCompensationRange.lower
   progress = exposureState.exposureCompensationIndex
}

CameraX 拡張 API

CameraX は、デバイス メーカーによってさまざまな Android デバイスに実装された拡張機能にアクセスするための拡張機能 API を提供します。サポートされている拡張モードのリストについては、 「カメラ拡張機能」を参照してください

この拡張機能をサポートするデバイスのリストについては、「サポートされるデバイス」を参照してください。

拡張アーキテクチャ

次の図は、カメラ拡張アーキテクチャを示しています。

ここに画像の説明を挿入

CameraX アプリケーションは、CameraX Extensions API を通じて拡張機能を使用できます。CameraX Extensions API を使用して、利用可能な拡張機能のクエリを管理し、拡張機能のカメラ セッションを構成し、カメラ拡張機能の OEM ライブラリと通信することができます。これにより、アプリで夜間、HDR、自動、ボケ、顔写真修正などの機能を使用できるようになります。

依存関係

CameraX Extensions API はcamera-extensionsライブラリに実装されています。これらの拡張機能は、CameraX コア モジュール ( corecamera2lifecycle) に依存します。

dependencies {
    
    
  def camerax_version = "1.3.0-alpha04"
  implementation "androidx.camera:camera-core:${camerax_version}"
  implementation "androidx.camera:camera-camera2:${camerax_version}"
  implementation "androidx.camera:camera-lifecycle:${camerax_version}"
  //the CameraX Extensions library
  implementation "androidx.camera:camera-extensions:${camerax_version}"
    ...
}

画像のキャプチャとプレビューを可能にする拡張機能

Extensions API を使用する前に、ExtensionsManager#getInstanceAsync(Context, CameraProvider)メソッドを使用してExtensionsManagerインスタンスを取得します。このようにして、拡張可用性情報をクエリできます。次に、有効な拡張機能を取得しますCameraSelectorCameraSelector拡張機能を有効にしてメソッドを呼び出すとbindToLifecycle()、拡張モードが画像キャプチャとプレビューのユースケースに適用されます。

注:ImageCaptureとで拡張機能が有効になっている場合、とを の引数としてPreview使用すると、選択できるカメラの数が制限される可能性があります。拡張機能をサポートするカメラが見つからない場合は、例外がスローされますImageCapturePreviewbindToLifecycle()ExtensionsManager#getExtensionEnabledCameraSelector()

画像のキャプチャとプレビューの使用例を実装するための拡張機能については、次のコード サンプルを参照してください。

import androidx.camera.extensions.ExtensionMode
import androidx.camera.extensions.ExtensionsManager

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

    val lifecycleOwner = this

    val cameraProviderFuture = ProcessCameraProvider.getInstance(applicationContext)
    cameraProviderFuture.addListener({
    
    
        // Obtain an instance of a process camera provider
        // The camera provider provides access to the set of cameras associated with the device.
        // The camera obtained from the provider will be bound to the activity lifecycle.
        val cameraProvider = cameraProviderFuture.get()

        val extensionsManagerFuture =
            ExtensionsManager.getInstanceAsync(applicationContext, cameraProvider)
        extensionsManagerFuture.addListener({
    
    
            // Obtain an instance of the extensions manager
            // The extensions manager enables a camera to use extension capabilities available on
            // the device.
            val extensionsManager = extensionsManagerFuture.get()

            // Select the camera
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            // Query if extension is available.
            // Not all devices will support extensions or might only support a subset of
            // extensions.
            if (extensionsManager.isExtensionAvailable(cameraSelector, ExtensionMode.NIGHT)) {
    
    
                // Unbind all use cases before enabling different extension modes.
                try {
    
    
                    cameraProvider.unbindAll()

                    // Retrieve a night extension enabled camera selector
                    val nightCameraSelector =
                        extensionsManager.getExtensionEnabledCameraSelector(
                            cameraSelector,
                            ExtensionMode.NIGHT
                        )

                    // Bind image capture and preview use cases with the extension enabled camera
                    // selector.
                    val imageCapture = ImageCapture.Builder().build()
                    val preview = Preview.Builder().build()
                    // Connect the preview to receive the surface the camera outputs the frames
                    // to. This will allow displaying the camera frames in either a TextureView
                    // or SurfaceView. The SurfaceProvider can be obtained from the PreviewView.
                    preview.setSurfaceProvider(surfaceProvider)

                    // Returns an instance of the camera bound to the lifecycle
                    // Use this camera object to control various operations with the camera
                    // Example: flash, zoom, focus metering etc.
                    val camera = cameraProvider.bindToLifecycle(
                        lifecycleOwner,
                        nightCameraSelector,
                        imageCapture,
                        preview
                    )
                } catch (e: Exception) {
    
    
                    Log.e(TAG, "Use case binding failed", e)
                }
            }
        }, ContextCompat.getMainExecutor(this))
    }, ContextCompat.getMainExecutor(this))
}

拡張機能を無効にする

ベンダー拡張機能を無効にするには、すべてのユース ケースのバインドを解除してから、通常のカメラ ピッカーを使用してイメージ キャプチャとプレビュー ユース ケースを再バインドします。たとえば、CameraSelector.DEFAULT_BACK_CAMERA背面カメラに再バインドするために使用します。

CameraXとCamera1の比較まとめ

CameraX と Camera1 のコードは異なっているように見えますが、CameraX と Camera1 の基本概念は非常に似ています。CameraX では、さまざまな一般的なカメラ関数タスク、PreviewImageCaptureVideoCaptureおよび はImageAnalysisすべてUseCaseユース ケースとして抽象化されているため、Camera1 で開発者に任されている多くのタスクを CameraX で自動的に処理できます。

ここに画像の説明を挿入

CameraX が開発者向けに低レベルの詳細をどのように処理するかの例は、有効なUseCaseの間で共有されますViewPortこれにより、UseCase表示されるすべてのピクセルがまったく同じであることが保証されます。Camera1 では、これらの詳細を自分で管理する必要があります。また、デバイスのカメラ センサーや画面ごとにアスペクト比が異なることを考慮すると、プレビューがキャプチャされた写真やビデオと一致することを確認するのが難しい場合があります。

別の例として、CameraX は、渡された Lifecycle インスタンスの Lifecycle コールバックを自動的に処理します。これは、CameraX が Android アクティビティのライフサイクル全体を通じてアプリのカメラへの接続を処理することを意味します。これには、アプリがバックグラウンドに移行した後にカメラを閉じる、画面に必要がなくなったときにカメラのプレビューを削除する、カメラを一時停止するなどのケースが含まれます。プレビューしながら、他のアクティビティ (ビデオ通話への招待など) がフォアグラウンドで優先されます。

また、CameraX は、追加のコードを実行することなく、回転とスケーリングを処理します。UseCaseアクティビティの向きがロックされていない場合、画面の向きが変わるとシステムがアクティビティを破棄して再作成するため、デバイスが回転するたびにシステムによって設定されます。こうすることで、ユースケースは画面の向きに合わせて毎回ターゲットの回転をデフォルトに設定します。

ここに画像の説明を挿入


参考:https://developer.android.google.cn/training/camerax?hl=zh-cn

おすすめ

転載: blog.csdn.net/lyabc123456/article/details/131181550