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-permissions
Compose で許可を申請するには依存関係を追加する必要があります。
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
が、場合によっては を使用するようにフォールバックしますTextureView
。SurfaceView
専用の描画インターフェイスを使用すると、特にプレビュー ビデオの上にボタンなどの他のインターフェイス要素がない場合、オブジェクトは内部ハードウェア コンポジターを介してハードウェア オーバーレイを実装する可能性が高くなります。ハードウェア オーバーレイを使用してレンダリングすると、ビデオ フレームが GPU パスからバイパスされ、プラットフォームの消費電力と遅延が低減されます。 -
COMPATIBLE
このモードでは、 モード がPreviewView
使用されますTextureView
。とは異なりSurfaceView
、このオブジェクトには専用の描画面がありません。したがって、ビデオはブレンディングを通じてレンダリングされるまで表示できません。この追加のステップ中に、アプリはビデオの拡大縮小や回転などの追加の処理を制限なく実行できます。
注: は
PERFORMANCE
デフォルト モードです。デバイスがこれをサポートしていない場合はSurfaceView
、PreviewView
を使用するようにフォールバックされますTextureView
。APIレベルが24
以下、カメラのハードウェアサポートレベルが表示回転CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
以上の場合に戻ります。任意の回転はサポートされていないため、モニターの回転以外の値に設定されている場合は、このモードを使用しないでください。プレビュー ビューをアニメーション化する必要がある場合は、このモードを使用しないでください。アニメーションはAPI以下のレベルではサポートされていません。また、で提供されるプレビュー ストリームの状態については、このモードを使用すると、状態が早く発生する可能性があります。Preview.getTargetRotation()
PreviewView
PreviewView
TextureView
Preview.Builder.setTargetRotation(int)
SurfaceView
24
SurfaceView
getPreviewStreamState
PreviewView.StreamState.streaming
パフォーマンスを考慮する場合は、明らかにこのモードを使用する必要がありますが、互換性を考慮する場合は、このモードPERFORMANCE
を使用するのが最善です。COMPATIBLE
2.PreviewView.setScaleType()
: このメソッドは、アプリケーションに最適なスケーリング タイプを設定するために使用されます。
ズームタイプ
プレビュー ビデオの解像度がPreviewView
ターゲットのサイズと異なる場合、ビューに合わせてビデオ コンテンツをトリミングまたはレターボックス化する必要があります (元のアスペクト比を維持)。この目的のために、PreviewView
以下が提供されますScaleTypes
。
-
FIT_CENTER
、、、FIT_START
およびレターボックスを追加しFIT_END
ます。ビデオ コンテンツ全体が、ターゲットで表示できる最大サイズにリサイズ (拡大または縮小) されます。ただし、ビデオ フレーム全体が完全に表示されますが、スクリーン ショットには空白の部分が存在する場合があります。ビデオ フレームは、上で選択した 3 つのズーム タイプのどれに応じて、ターゲット ビューの中心、開始、または終了に位置合わせされます。PreviewView
-
FILL_CENTER
、FILL_START
およびクリッピングFILL_END
用。ビデオのアスペクト比がと一致しない場合、コンテンツの一部のみが表示されますが、ビデオは全体を占めます。PreviewView
PreviewView
CameraX
使用されるデフォルトのスケーリング タイプは ですFILL_CENTER
。
注: ズーム タイプの主な目的は、プレビューが伸びたり変形したりしないようにすることです。以前の Camera または Camera2 API を使用する場合、私の一般的なアプローチは、カメラでサポートされているプレビュー解像度のリストを取得し、プレビュー解像度を選択することです。次に、SurfaceView
またはTextureView
コントロールのアスペクト比を、選択したプレビュー解像度のアスペクト比に合わせます。これにより、プレビュー中に伸縮や変形の問題が発生せず、最終的な効果は実際には上記のズーム タイプとまったく同じになります。幸いなことに、現在は正式な API レベルのサポートにより、開発者はこれらの面倒な作業を手動で行う必要がなくなりました。
たとえば、下の左の図は通常のプレビュー表示効果であり、右の図はストレッチされた変形のプレビュー表示効果です。
この種のエクスペリエンスは非常に好ましくなく、最大の問題は、表示されているものがそのまま得られることです(保存された画像またはビデオ ファイルがプレビューで見られる効果と一致しない)。
16:9 のプレビュー画面に表示された 4:3 の画像を例に挙げると、何も処理しないと 100% の確率で伸縮と変形が発生します。
次の図は、適用されたさまざまなスケーリング タイプの効果を示しています。
の使用にはいくつかの制限がありますPreviewView
。を使用する場合PreviewView
、次のことはできません。
- とに設定する
SurfaceTexture
ために作成されました。TextureView
Preview.SurfaceProvider
- から
TextureView
取得されSurfaceTexture
、Preview.SurfaceProvider
に設定されます。 - から
SurfaceView
取得してSurface
、Preview.SurfaceProvider
に設定します。
上記のいずれかの状況が発生した場合、 へのPreview
フレームのストリーミングは停止しますPreviewView
。
ライフサイクル CameraController のバインディング
作成後のPreviewView
次のステップは、作成したインスタンスCameraController
(その実装が である抽象クラス) を設定し、作成したインスタンスを現在のライフサイクル ホルダーにバインドLifecycleCameraController
することです。コードは以下のように表示されます:CameraController
lifecycleOwner
@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
し、その後の業務処理を行うことができます。outputFileResults
Uri
写真を撮った後に保存ロジックを自分で実行したい場合、または保存せずに表示するだけの場合は、別のコールバックを使用できます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 コードは、実際には1CameraController
とCameraProvider
2 つの実装を提供します。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
が必要であることです。ImageCapture
takePicture
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
、次の手順に従います。
- タップイベントの処理に使用されるジェスチャ検出器を設定します。
- タップ イベントの場合は、
MeteringPointFactory.createPoint()
を使用して作成しますMeteringPoint
。 - の場合は
MeteringPoint
、 を作成しますFocusMeteringAction
。 - Camera 上の
CameraControl
( から返された) オブジェクトの場合はbindToLifecycle()
、それを呼び出しstartFocusAndMetering()
て に渡しますFocusMeteringAction
。 - (オプション) 応答
FocusMeteringResult
。 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
追跡するオブジェクトを返します。CameraController
ZoomState
// 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
、次の手順に従います。
- ピンチ イベントの処理に使用されるズーム ジェスチャ検出器を設定します。
- オブジェクトから
Camera.CameraInfo
取得されたインスタンスは、ZoomState
を呼び出すbindToLifecycle()
と返されますCamera
。 - 値が
ZoomState
ある場合は、それを現在のズームとして保存します。NonezoomRatio
の場合、カメラのデフォルトのズーム ( ) が使用されます。ZoomState
zoomRatio
1.0
- 現在のズーム倍率を取得し
scaleFactor
て乗算して新しいズーム倍率を決定し、それを に渡しますCameraControl.setZoomRatio()
。 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 つのストリームを多重化するメディア マルチプレクサー。
- 結果を書き出すためのファイル セーバー。
VideoCapture
API は複雑なキャプチャ エンジンを抽象化し、よりシンプルで直感的な API をアプリケーションに提供します。
VideoCapture
は、単独で使用することも、他のユースケースと組み合わせて使用することもできる CameraX ユースケースです。サポートされる正確な組み合わせはカメラのハードウェア機能によって異なりますが、この使用例のPreview
との組み合わせはすべてのデバイスに適用されます。VideoCapture
注:は
VideoCapture
CameraX のライブラリcamera-video
に実装されており、1.1.0-alpha10
以降で利用可能です。CameraXVideoCapture
API は最終的なものではなく、時間の経過とともに変更される可能性があります。
VideoCapture
API は、アプリケーションと通信できる次のオブジェクトで構成されます。
VideoCapture
最上位のユースケースクラスです。VideoCapture
viaおよびその他の CameraX ユースケースCameraSelector
にバインドしますLifecycleOwner
。Recorder
VideoCapture
は と密接に結合された実装ですVideoOutput
。Recorder
ビデオおよびオーディオのキャプチャ操作を実行するために使用されます。Recorder
記録オブジェクトを作成して適用します。PendingRecording
オーディオの有効化やイベント リスナーの設定などのオプションを使用して、録音オブジェクトが構成されます。Recorder
を作成するにはを使用する必要がありますPendingRecording
。PendingRecording
何も記録されません。Recording
実際の録音動作が行われます。PendingRecording
を作成するにはを使用する必要がありますRecording
。
次の図は、これらのオブジェクト間の関係を示しています。
伝説:
QualitySelector
createを使用しますRecorder
。OutputOptions
これらの構成のいずれかを使用しますRecorder
。- 必要に応じて
withAudioEnabled()
、オーディオを有効にするために使用します。 VideoRecordEvent
リスナーと通話してstart()
録音を開始します。- を
Recording
使用してpause()/resume()/stop()
録音操作を制御します。 - イベント リスナー内で応答します
VideoRecordEvents
。
詳細な API リストは、ソース コード内の current-txt にあります。
CameraProvider でビデオを撮影する
CameraProvider
バインドされたユースケースを使用する場合は、 UseCase を作成してオブジェクトを渡すVideoCapure
必要があります。ビデオ品質は、デバイスが必要な品質仕様を満たしていない場合に備えて、オプションで設定できます。最後に、インスタンスは他の UseCases とともにバインドされます。VideoCapture
Recorder
Recorder.Builder
FallbackStrategy
VideoCapture
CameraProvider
QualitySelector オブジェクトを作成する
QualitySelector
アプリはオブジェクトを介してビデオ解像度を構成できますRecorder
。
CameraX は、Recorder
次の事前定義されたビデオ解像度品質をサポートしています。
- 品質.UHD ( 4K Ultra HD ビデオ サイズ ( 2160p ))
- フル HD ビデオ サイズ ( 1080p )の場合は、 Quality.FHD
- HD ビデオ サイズ ( 720p )の場合は、 Quality.HD
- 標準解像度ビデオ サイズの場合は、Quality.SD ( 480p )
アプリの認証により、CameraX では他の解像度も利用できることに注意してください。各オプションの正確なビデオ サイズは、カメラとエンコーダの機能によって異なります。詳細については、 CamcorderProfileのドキュメントを参照してください。
次のいずれかの方法を使用して作成できますQualitySelector
。
-
使用には、
fromOrderedList()
いくつかの優先解像度が用意されており、優先解像度がいずれもサポートされていない場合のフォールバック戦略が含まれています。CameraX は、選択したカメラの機能に基づいて最適なフォールバック マッチを決定できます。
QualitySelector
詳細については、 FallbackStrategy仕様を参照してください。たとえば、次のコードは、サポートされている最高の録画解像度を要求します。要求された解像度がいずれもサポートされていない場合、CameraX は最も近いQuality.SD
解像度を選択することが許可されます。
val qualitySelector = QualitySelector.fromOrderedList(
listOf(Quality.UHD, Quality.FHD, Quality.HD, Quality.SD),
FallbackStrategy.lowerQualityOrHigherThan(Quality.SD))
- まず、カメラのサポートされている解像度をクエリし、次に次の
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
ユースケースの組み合わせに適用できる必要があることに注意してくださいPreview
。ImageCapture
またはユースケースでバインドするときImageAnalysis
、要求されたカメラが目的の組み合わせをサポートしていない場合、CameraX は依然としてバインドに失敗する可能性があります。
VideoCapture オブジェクトを作成してバインドする
を取得したらQualitySelector
、VideoCapture
オブジェクトを作成してバインドを実行できます。このバインディングは他の使用例と同じであることに注意してください。
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
、ファイルにキャプチャする場合。MediaStoreOutputOptions
MediaStore にキャプチャするためのものです。
のタイプに関係なくOutputOptions
、setFileSizeLimit()
を通じて最大ファイル サイズを設定できます。ParcelFileDescriptor
他のオプションは、 に固有など、個々の出力タイプに固有ですFileDescriptorOutputOptions
。
一時停止、再開、停止
start()
関数を呼び出すと、オブジェクトRecorder
が返されますRecording
。アプリはこのRecording
オブジェクトを使用して、キャプチャを完了したり、一時停止や再開などの他のアクションを実行したりできます。次のコマンドを使用して、進行中のセッションを一時停止、再開、停止できますRecording
。
pause()
、現在アクティブな録音を一時停止します。resume()
、一時停止したアクティブな録音を再開します。stop()
これにより、記録が完了し、関連付けられたすべての記録オブジェクトがクリアされます。
録音が一時停止されているかアクティブであるかに関係なく、呼び出してstop()
終了できることに注意してくださいRecording
。
Recorder
Recording
一度にサポートされるオブジェクトは1 つです。前のオブジェクトでまたはRecording
を呼び出した後、新しい録音を開始できます。Recording.stop()
Recording.close()
if (recording != null) {
// Stop the current recording session.
recording.stop()
recording = null
return
}
..
recording = ..
イベントリスナー
に登録している場合はPendingRecording.start()
、を使用して通信しEventListener
ます。Recording
VideoRecordEvent
CameraX は、対応するカメラ デバイスで録画が開始されるたびにイベントを送信しますVideoRecordEvent.Start
。
VideoRecordEvent.Status
現在のファイルのサイズや記録の期間などの統計を記録するため。VideoRecordEvent.Finalize
結果を記録する場合、最終ファイルURI
と関連するエラーが含まれます。
アプリが録画セッションの成功を示す を受信するとFinalize
、OutputOptions
で。
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 を独立して切り替えることができます (これらのCameraController
UseCaseを同時に使用できる場合)。とUseCase はデフォルトで有効になっているため、写真を撮るために電話する必要はありません。ImageCapture
VideoCapture
ImageAnalysis
ImageCapture
ImageAnalysis
setEnabledUseCases()
を使用してビデオを録画する場合は、まず を使用したユースケースを許可する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
録音を有効にするには、マイクの使用許可があることを確認する必要があります。と同様にCameraProvider
、Consumer<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_888
RGBA_8888
YUV_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_LATEST
callを使用して有効にできます。エグゼキューター関連の効果の詳細については、 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の実装
CameraController
ML Kit プロファイラーを実装するには、UI 要素を表示するために使用できるクラスを使用することをお勧めしますPreviewView
。を使用して実装する場合CameraController
、ML Kit アナライザーは、生のImageAnalysis
ストリームとPreviewView
の間の座標変換を処理します。アナライザーは、CameraX からターゲット座標系を受け取り、座標変換を計算し、それをDetector
分析のために ML Kit クラスに転送します。
で ML Kit アナライザーを使用するにはCameraController
、setImageAnalysisAnalyzer()
コンストラクターに以下を含む新しい 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-core
のImageAnalysis
クラスを使用して 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();
顔検出操作が成功すると、システムはオブジェクトの配列を成功リスナーに渡しますFace
。各Face
オブジェクトは、画像内で検出された顔を表します。顔ごとに、入力画像内の境界座標と、顔検出器が検索するように構成したその他の情報を取得します。例えば:
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
。
- カスタム構成を使用してオブジェクトを作成します
CameraXConfig
。 - のインターフェイス
Application
を実装しCameraXConfig.Provider
、のオブジェクトgetCameraXConfig()
を返します。CameraXConfig
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
同じコンテンツを取得します(ズーム タイプ がデフォルト値 に設定されていると仮定します)。クリッピング四角形と回転が出力バッファーに適用されると、解像度は異なる場合がありますが、画像はすべてのユースケースで一貫したものになります。変換情報を適用する方法の詳細については、「変換の出力」を参照してください。ImageCapture
PreviewView
PreviewView
FILL_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 つのカメラを設定し、返されたオブジェクトからConcurrentCamera
2 つのオブジェクトを取得する方法を示しています。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 は、リクエストに応じて最適な解像度を適用します。アスペクト比要件を満たすことが主なニーズである場合にのみ指定すると、setTargetAspectRatio
CameraX がデバイスに基づいて適切な特定の解像度を決定します。アプリケーションの主な要件が、画像処理効率を向上させるために解像度を指定することである場合に使用します (デバイスの処理能力に応じて小型または中サイズの画像を処理するなど) 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
操作を送信できます。カメラのバインドに使用されたインスタンスを停止または破棄するとactivity
、CameraControl
それ以上の操作は実行できなくなり、失敗が返されますListenableFuture
。
注:
LifecycleOwner
が停止または破壊された場合、Camera
オフになり、その後、ズーム、トーチ、フォーカス、測光のすべての状態変化と露出補正コントロールがデフォルト値に戻ります。
ズーム
CameraControl
ズーム レベルを変更する 2 つの方法が提供されています。
-
setZoomRatio() は、ズーム率でズームを設定するために使用されます。
比率は
CameraInfo.getZoomState().getValue().getMinZoomRatio()
からの範囲内である必要がありますCameraInfo.getZoomState().getValue().getMaxZoomRatio()
。それ以外の場合、関数は failed を返しますListenableFuture
。 -
setLinearZoom() は、と の間の線形ズーム値で現在のズーム操作を設定します
0
。1.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を使用しますMeteringPoint
。MeteringPoint
カメラ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 などのオプションのメータリング モードで構成されます。次のコードは、この使用法を示しています。FocusMeteringAction
MeteringPoints
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()
を受け入れます。FocusMeteringAction
MeteringPoint
MeteringPoint
内部的には、CameraX はそれを Camera2 に変換しMeteringRectangles
、対応するCONTROL_AF_REGIONS/CONTROL_AE_REGIONS/CONTROL_AWB_REGIONS
パラメーターをキャプチャ リクエストに設定します。
すべてのデバイスが AF/AE/AWB および複数のゾーンをサポートしているわけではないため、CameraX は最善を尽くしますFocusMeteringAction
。CameraX はサポートされている最大数を使用しMeteringPoint
、測光ポイントが追加された順序で順番に使用します。MeteringPoint
CameraX は、サポートされている最大数を超えるすべての追加を無視します。たとえば、 2 のみをサポートするMeteringPoint
プラットフォームでFocusMeteringAction
3 を指定すると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 コア モジュール ( core
、camera2
、lifecycle
) に依存します。
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
インスタンスを取得します。このようにして、拡張可用性情報をクエリできます。次に、有効な拡張機能を取得しますCameraSelector
。CameraSelector
拡張機能を有効にしてメソッドを呼び出すとbindToLifecycle()
、拡張モードが画像キャプチャとプレビューのユースケースに適用されます。
注:
ImageCapture
とで拡張機能が有効になっている場合、とを の引数としてPreview
使用すると、選択できるカメラの数が制限される可能性があります。拡張機能をサポートするカメラが見つからない場合は、例外がスローされます。ImageCapture
Preview
bindToLifecycle()
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 では、さまざまな一般的なカメラ関数タスク、Preview
、ImageCapture
、VideoCapture
および は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