Dans l'histoire du développement d'Android, l'API de l'appareil photo a toujours été critiquée. Quiconque l'a utilisée sait que le sentiment intuitif est que la configuration est complexe, gonflée, difficile à utiliser et difficile à comprendre. On peut le voir sur le site officiel. Itinéraire d'itération de l'API à propos de Camera L'officiel essaie également d'améliorer continuellement l'expérience des développeurs sur Camera. L'API Camera est passée par CameraXetCamera2(obsolète),Cameratrois versions :
L'API Camera de première génération a été déclarée obsolète depuis la version 5.0, et l'API de Camera2 est particulièrement difficile à utiliser. Après que de nombreuses personnes l'utilisent, elle n'est pas aussi bonne que la précédente Camera , il y a donc CameraX , qui est en fait basé sur Camera2, mais il est utilisé Certaines optimisations plus humaines, qui font partie de la bibliothèque de composants Jetpack, sont actuellement la solution officielle de Camera. Par conséquent, si vous avez un nouveau projet impliquant l'API Camera ou prévoyez de mettre à niveau l'ancienne API Camera, il est recommandé d'utiliser CameraX directement.
Cet article explique comment utiliser CameraX dans Jetpack Compose.
Préparations CameraX
Ajoutez d'abord les dépendances :
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}"
}
Remarque, les dernières informations de version des bibliothèques ci-dessus peuvent être trouvées ici : https://developer.android.com/jetpack/androidx/releases/camera?hl=zh-cn
Étant donné que l'application d'autorisation de caméra est requise pour utiliser la caméra, une dépendance doit être ajoutée accompanist-permissions
pour demander une autorisation dans Compose :
val accompanist_version = "0.31.2-alpha"
implementation "com.google.accompanist:accompanist-permissions:$accompanist_version"
Notez que les dernières informations sur la version des bibliothèques ci-dessus peuvent être trouvées ici : https://github.com/google/accompanist/releases
Pensez ensuite AndroidManifest.xml
à ajouter la déclaration de permission dans le :
<manifest .. >
<uses-permission android:name="android.permission.CAMERA" />
..
</manifest>
CameraX a les exigences de version minimales suivantes :
- API Android niveau 21
- Composants d'architecture Android 1.1.1
Pour une activité sensible au cycle de vie, utilisez FragmentActivity ou AppCompatActivity.
Aperçu de la caméra CameraX
Regardons principalement comment CameraX effectue l'aperçu de la caméra
Créer un aperçu PreviewView
Étant donné que Jetpack Compose ne fournit pas directement un composant distinct pour l'aperçu de l'appareil photo, la solution consiste à utiliser AndroidView
ce Composable pour intégrer le contrôle d'aperçu natif dans Compose pour l'affichage. code afficher comme ci-dessous:
@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
}
}
)
}
}
Voici un contrôle View PreviewView
natif camera-view
de la bibliothèque, examinons plusieurs de ses méthodes de paramétrage :
1. : Cette méthode est utilisée pour définir le PreviewView.setImplementationMode()
mode de mise en œuvre spécifique adapté à l'application
Mode de mise en œuvre
PreviewView
Le flux de prévisualisation peut être rendu sur la cible en utilisant l'un des modes suivantsView
:
-
PERFORMANCE
est le mode par défaut et affiche le flux vidéoPreviewView
en utilisantSurfaceView
, mais revient à utiliser dans certains casTextureView
.SurfaceView
Avec une interface de dessin dédiée, l'objet est plus susceptible d'implémenter une superposition matérielle via un compositeur matériel interne, surtout s'il n'y a pas d'autres éléments d'interface tels que des boutons au-dessus de la vidéo de prévisualisation. Grâce au rendu à l'aide d'une superposition matérielle, les images vidéo sont contournées du chemin GPU, ce qui réduit la consommation d'énergie de la plate-forme et la latence. -
COMPATIBLE
mode, dans ce mode,PreviewView
sera utiliséTextureView
. Contrairement àSurfaceView
, cet objet n'a pas de surface de dessin dédiée. Par conséquent, la vidéo ne peut pas être affichée tant qu'elle n'a pas été rendue par fusion. Au cours de cette étape supplémentaire, l'application peut effectuer des traitements supplémentaires, tels que la mise à l'échelle et la rotation de la vidéo sans restriction.
Remarque : Car
PERFORMANCE
est le mode par défaut, si l'appareil ne le prend pas en chargeSurfaceView
,PreviewView
il reviendra à utiliserTextureView
. Revient à lorsque le niveau d'API est24
ou inférieur et que le niveau de prise en charge du matériel de la caméra estCameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
ou différent de laPreview.getTargetRotation()
rotation de l'affichage . N'utilisez pas ce mode s'il est défini sur une valeur autre que la rotation du moniteur, car les rotations arbitraires ne sont pas prises en charge. N'utilisez pas ce mode si la vue d'aperçu doit être animée. Les animations ne sont pas prises en charge au niveau de l'API ou inférieur . En outre, pour l'état de flux d'aperçu fourni dans , l'état peut se produire plus tôt si ce mode est utilisé.PreviewView
PreviewView
TextureView
Preview.Builder.setTargetRotation(int)
SurfaceView
24
SurfaceView
getPreviewStreamState
PreviewView.StreamState.streaming
Évidemment, si c'est pour des raisons de performances, vous devez utiliser PERFORMANCE
le mode, mais si c'est pour des raisons de compatibilité, il est préférable d'utiliser COMPATIBLE
le mode.
2.PreviewView.setScaleType()
: Cette méthode est utilisée pour définir le type de mise à l'échelle le plus adapté à l'application .
type de zoom
Lorsque la résolution de la vidéo d'aperçu PreviewView
diffère des dimensions cibles, le contenu vidéo doit être recadré ou mis en boîte aux lettres pour s'adapter à la vue (en conservant le format d'image d'origine). A cet effet, PreviewView
sont prévus ScaleTypes
:
-
FIT_CENTER
,FIT_START
, etFIT_END
pour ajouter la boîte aux lettres . L'intégralité du contenu vidéo est redimensionnée (mise à l'échelle vers le haut ou vers le bas) àPreviewView
la plus grande taille pouvant être affichée dans la cible . Cependant, bien que l'intégralité de l'image vidéo soit affichée dans son intégralité, il peut y avoir des parties vierges de la capture d'écran. L'image vidéo est alignée sur le centre, le début ou la fin de la vue cible, selon celui des trois types de zoom que vous avez sélectionné ci-dessus. -
FILL_CENTER
,FILL_START
etFILL_END
pour le découpage . Si le rapport d'aspect de la vidéoPreviewView
ne correspond pas au , seule une partie du contenu sera affichée, mais la vidéo remplira toujours l'intégralité duPreviewView
.
CameraX
Le type de mise à l'échelle par défaut utilisé est FILL_CENTER
.
Remarque : L'objectif principal du type de zoom est d'empêcher l'étirement et la déformation de l'aperçu. Si vous utilisez l'API Camera ou Camera2 précédente, mon approche générale consiste à obtenir une liste des résolutions d'aperçu prises en charge par l'appareil photo et à sélectionner une résolution d'aperçu. . Alignez ensuite le rapport d'aspect de la commande SurfaceView
ou TextureView
sur le rapport d'aspect de la résolution d'aperçu sélectionnée, de sorte qu'il n'y ait pas de problèmes d'étirement et de déformation pendant l'aperçu, et que l'effet final soit exactement le même que le type de zoom ci-dessus. Heureusement, maintenant avec la prise en charge officielle au niveau de l'API, les développeurs n'ont plus à faire ces choses gênantes manuellement.
Par exemple, l'image de gauche ci-dessous est l'effet d'affichage d'aperçu normal, tandis que l'image de droite est l'effet d'affichage d'aperçu de déformation étirée :
Ce genre d'expérience est très mauvais. Le plus gros problème est que ce que vous voyez est ce que vous obtenez (l'image ou le fichier vidéo enregistré est incohérent avec l'effet vu dans l'aperçu).
Prenez l'image 4:3 affichée sur l'écran d'aperçu 16:9 comme exemple, si aucun traitement n'est effectué, l'étirement et la déformation se produiront 100 % du temps :
L'image suivante montre l'effet des différents types de mise à l'échelle appliqués :
Il existe certaines restrictions d'utilisation PreviewView
. Lorsque vous utilisez PreviewView
, vous ne pouvez effectuer aucune des opérations suivantes :
- Créé
SurfaceTexture
pour être réglé surTextureView
et .Preview.SurfaceProvider
TextureView
Extrait deSurfaceTexture
etPreview.SurfaceProvider
défini sur le .SurfaceView
Obtenez-le à partir deSurface
etPreview.SurfaceProvider
activez-le.
Si l'une des conditions ci-dessus se produit, Preview
la diffusion d'images vers s'arrêtera PreviewView
.
Liaison Lifecycle CameraController
Après la création PreviewView
, l' étape suivante consiste à en définir un pour l' instance que nous avons créée CameraController
, qui est une classe abstraite dont l' implémentation est LifecycleCameraController
, puis nous pouvons CameraController
lier l' instance créée au détenteur actuel du cycle de vie lifecycleOwner
. code afficher comme ci-dessous:
@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()
}
)
}
}
Notez que dans le code ci-dessus, nous dissocions le contrôleur de celui-cionRelease
dans le callback . PreviewView
De cette façon, nous pouvons nous assurer que AndroidView
les ressources de la caméra sont libérées lorsqu'elles ne sont plus utilisées.
demande d'accès
L'interface Composable de prévisualisation s'affiche uniquement après que l'application a obtenu l'autorisation de l'appareil photo, sinon une interface Composable d'espace réservé s'affiche. Le code de référence pour l'obtention de l'autorisation est le suivant :
@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("请求权限") }
}
}
Pour plus d'informations sur la façon de demander des autorisations dynamiques dans Compose, veuillez vous référer à Accompanist in Jetpack Compose , donc je n'entrerai pas dans les détails ici.
paramètres plein écran
Afin d'afficher la caméra en plein écran lors de la prévisualisation sans la barre d'état supérieure, vous pouvez ajouter le code suivant avant Activity
la onCreate()
méthode :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;
}
Habituellement ce code devrait fonctionner, sinon, essayez de modifier le thème theme :
// 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 pour prendre des photos
Prendre des photos dans CameraX fournit principalement deux méthodes de surcharge :
takePicture(Executor, OnImageCapturedCallback)
: Cette méthode fournit une mémoire tampon pour l'image capturée.takePicture(OutputFileOptions, Executor, OnImageSavedCallback)
: Cette méthode enregistre l'image capturée à l'emplacement de fichier fourni.
Ajoutons un FloatingActionButton
bouton CameraPreviewExample
qui déclenchera la fonction caméra lorsqu'il sera cliqué. code afficher comme ci-dessous:
@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()
}
Dans le rappel de méthode OnImageSavedCallback
de , le fichier image enregistré onImageSaved
peut être obtenu , puis un traitement commercial supplémentaire peut être effectué.outputFileResults
Uri
Si vous souhaitez exécuter vous-même la logique de sauvegarde après avoir pris une photo, ou juste pour l'affichage sans sauvegarde, vous pouvez utiliser un autre callback 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()
}
Dans ce rappel, ImageProxy#toBitmap
la méthode peut être utilisée pour convertir facilement les données brutes après avoir pris des photos en Bitmap
vue de les afficher. Cependant, le format par défaut obtenu ici est ImageFormat.JPEG
que toBitmap
la conversion de la méthode échouera. Vous pouvez vous référer au code suivant pour le résoudre :
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()
}
Si le format YUV est obtenu ici, en plus d'appeler directement image.toBitmap()
la méthode, l'officiel fournit également une classe d'outils qui peut YUV_420_888
convertir le format en objet RGB
du format Bitmap
, veuillez vous référer à YuvToRgbConverter.kt .
Le code complet de l'exemple ci-dessus :
@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;
}
}
Remarque : les opérations liées aux bitmaps sont des tâches chronophages et doivent être exécutées dans un planificateur de coroutines séparé du thread principal à l'aide de coroutines. En pratique, il doit être amélioré par lui-même s'il est utilisé dans un projet de production.
CameraProvider 与 CameraController
Tous les codes officiels de CameraX fournissent en fait CameraController
deux CameraProvider
implémentations. Choisissez si vous voulez la manière la plus simple d'utiliser CameraX CameraController
, choisissez si vous avez besoin de plus de flexibilité CameraProvider
.
Pour déterminer quelle implémentation vous convient le mieux, les avantages de chacune sont répertoriés ci-dessous :
CaméraContrôleur | Fournisseur de caméra |
---|---|
Nécessite peu de code de configuration | permettre un plus grand contrôle |
Permet à CameraX de gérer une plus grande partie du processus de configuration, ce qui signifie que des fonctionnalités telles que toucher pour faire la mise au point et pincer pour zoomer fonctionnent automatiquement | Étant donné que le développeur de l'application gère les paramètres, il existe davantage de possibilités de personnaliser la configuration, comme l'activation de la rotation de l'image de sortie ou la définition du format de l'image de sortie dans ImageAnalysis. |
Nécessite PreviewView pour l'aperçu de la caméra, permettant à CameraX de fournir une intégration transparente de bout en bout, comme dans notre intégration ML Suite, qui prend les coordonnées de résultat du modèle d'apprentissage automatique (telles que les boîtes englobantes de visage) directement mappées aux coordonnées de prévisualisation | Possibilité d'utiliser une "Surface" personnalisée pour l'aperçu de la caméra, permettant plus de flexibilité, comme l'utilisation de votre code "Surface" existant qui peut être utilisé comme entrée dans d'autres parties de l'application |
Utilisez CameraProvider pour réaliser la fonction caméra
Pour un accès facile CameraProvider
, vous pouvez d'abord définir une fonction d'extension :
private suspend fun Context.getCameraProvider(): ProcessCameraProvider {
return ProcessCameraProvider.getInstance(this).await()
}
Prendre CameraProvider
une photo en utilisant CameraController
est très similaire à utiliser, la seule différence est qu'il nécessite un ImageCapture
objet pour appeler takePicture
des méthodes sur :
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()
}
Indicatif d'appel :
@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()
}
)
}
}
La plus grande différence ici est que LaunchedEffect
l'API d'effet secondaire est utilisée et qu'une coroutine est lancée pour configurer cameraProvider
les opérations liées à la liaison, car cameraProvider
une fonction de suspension est utilisée lors de son obtention ici. Notez également qu'ici PreviewView
et sont mémorisés ImageCapture
à l'aide de remember
, afin qu'ils Composable
survivent à travers les réorganisations, plutôt que de recréer un nouvel objet à chaque fois qu'ils sont réorganisés.
Paramètres communs de CameraX
Définir le mode de prise de vue
Que vous utilisiez CameraController
ou , CameraProvider
vous pouvez setCaptureMode()
définir le mode de prise de vue via la méthode .
Les modes de prise de vue pris en charge par CameraX sont :
CAPTURE_MODE_MINIMIZE_LATENCY
: Réduisez le temps de retard pour prendre des photos.CAPTURE_MODE_MAXIMIZE_QUALITY
: Améliorer la qualité d'image de la capture d'image.CAPTURE_MODE_ZERO_SHUTTER_LAG
: Mode Zero shutter lag, disponible depuis la 1.2.CAPTURE_MODE_MINIMIZE_LATENCY
Lorsque Zero Shutter Lag est activé, le temps de latence est considérablement réduit par rapport au mode de prise de vue par défaut , de sorte que vous ne manquez jamais une photo.
Le mode de prise de vue par défaut est CAPTURE_MODE_MINIMIZE_LATENCY
. Voir la documentation de référence setCaptureMode() pour plus de détails .
Zero Shutter Lag utilise une mémoire tampon circulaire pour stocker les trois dernières images capturées. Lorsque l'utilisateur appuie sur le bouton de capture, CameraX appelle takePicture()
et le tampon circulaire récupère l'image de capture dont l'horodatage est le plus proche de l'heure à laquelle le bouton a été enfoncé. CameraX retraite ensuite la session de capture pour générer une image à partir de cette image qui est enregistrée sur le disque au format JPEG.
Avant d'activer Zero Shutter Lag, utilisez pour isZslSupported()
déterminer si l'appareil en question répond aux exigences suivantes :
- Cible Android 6.0 et supérieur (API niveau 23 et supérieur).
- Le retraitement PRIVÉ est pris en charge .
Si l'appareil ne répond pas aux exigences minimales, CameraX reviendra à CAPTURE_MODE_MINIMIZE_LATENCY.
Le délai d'obturation zéro n'est disponible que pour les cas d'utilisation de capture d'image . Vous ne pouvez pas l'activer pour les cas d'utilisation de capture vidéo ou les extensions de caméra. Enfin, le décalage d'obturation zéro ne fonctionnera pas lorsque le flash est allumé ou en mode automatique, car l'utilisation du flash ajoute au temps de latence.
Exemple d'utilisation CameraController
pour définir le mode de prise de vue :
cameraController.imageCaptureMode = CAPTURE_MODE_MINIMIZE_LATENCY
Exemple d'utilisation CameraProvider
pour définir le mode de prise de vue :
val imageCapture: ImageCapture = remember {
ImageCapture.Builder().setCaptureMode(ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG).build()
}
régler le flash
Le mode de flash par défaut est FLASH_MODE_OFF
. Pour régler le mode flash, utilisez setFlashMode()
:
FLASH_MODE_ON
: Le flash est toujours activé.FLASH_MODE_AUTO
: Lors d'une prise de vue dans un environnement peu éclairé, le flash s'active automatiquement.
Exemple d'utilisation CameraController
pour régler le mode flash :
cameraController.imageCaptureFlashMode = ImageCapture.FLASH_MODE_AUTO
Exemple d'utilisation CameraProvider
pour régler le mode flash :
ImageCapture.Builder()
.setFlashMode(FLASH_MODE_AUTO)
.build()
sélectionner la caméra
Dans CameraX, la sélection de la caméra est CameraSelector
gérée par la classe. CameraX rend le cas courant d'utilisation de la caméra par défaut beaucoup plus facile. Vous pouvez spécifier si vous souhaitez utiliser la caméra frontale par défaut ou la caméra arrière par défaut.
Voici CameraController
le code CameraX en utilisant la caméra arrière par défaut :
var cameraController = LifecycleCameraController(baseContext)
// val selector = CameraSelector.Builder()
// .requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
val selector = CameraSelector.DEFAULT_BACK_CAMERA // 等价上面的代码
cameraController.cameraSelector = selector
Voici CameraProvider
le code CameraX pour sélectionner la caméra frontale par défaut via :
val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
cameraProvider.unbindAll()
var camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCases)
appuyez pour faire la mise au point
Lorsqu'un aperçu de la caméra est affiché à l'écran, un contrôle courant consiste à définir la mise au point lorsque l'utilisateur appuie sur l'aperçu.
CameraController
écoutera PreviewView
les événements tactiles pour gérer automatiquement la mise au point du robinet. Vous pouvez setTapToFocusEnabled()
activer et désactiver le tap-to-focus via et getter isTapToFocusEnabled()
vérifier sa valeur avec le correspondant.
getTapToFocusState()
La méthode renvoie un LiveData
objet qui suit CameraController
les changements d'état du focus sur le .
// 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)
Lors de l'utilisation de CameraProvider
, une configuration est nécessaire pour que la mise au point par pression fonctionne correctement. Cet exemple suppose que vous utilisez PreviewView
. S'il n'est pas utilisé, vous devrez ajuster la logique à appliquer à une surface personnalisée.
Lorsque vous utilisez PreviewView
, suivez ces étapes :
- Définit le détecteur de geste utilisé pour gérer les événements tactiles.
- Pour les événements tactiles, utilisez pour
MeteringPointFactory.createPoint()
en créer unMeteringPoint
. - Pour
MeteringPoint
, créez-en unFocusMeteringAction
. CameraControl
Pour les objets (renvoyés de ) sur CamerabindToLifecycle()
, appelez-lestartFocusAndMetering()
afin qu'il soit transmis àFocusMeteringAction
.- (Facultatif) Réponse
FocusMeteringResult
. - Configurez un détecteur de gestes pour
PreviewView.setOnTouchListener()
répondre aux événements tactiles dans un fichier .
// 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
}
Pincer pour zoomer
Zoomer l'aperçu est une autre manipulation directe courante de l'aperçu de la caméra. Avec de plus en plus de caméras sur un appareil, les utilisateurs souhaitent également sélectionner automatiquement la caméra avec la meilleure mise au point après le zoom.
Comme pour le toucher pour la mise au point, les événements tactiles CameraController
sont surveillés PreviewView
pour gérer automatiquement le pincement pour zoomer. Vous pouvez setPinchToZoomEnabled()
activer et désactiver le zoom par pincement avec et getter isPinchToZoomEnabled()
vérifier sa valeur avec le correspondant.
getZoomState()
La méthode renvoie un LiveData
objet qui suit les modifications CameraController
sur le 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)
Lors de l'utilisation de CameraProvider
, une configuration est nécessaire pour que le pincement et le zoom fonctionnent correctement. Si vous n'utilisez pas PreviewView
, vous devrez ajuster la logique pour appliquer custom Surface
.
Lorsque vous utilisez PreviewView
, suivez ces étapes :
- Définit le détecteur de mouvement de zoom utilisé pour gérer les événements de pincement.
Camera.CameraInfo
Obtenue à partir de l'objet, une instance est renvoyée lorsqueZoomState
vous appelez .bindToLifecycle()
Camera
- Si
ZoomState
azoomRatio
une valeur, enregistrez-la en tant que zoom actuel. SiZoomState
AucunzoomRatio
, le zoom par défaut de la caméra (1.0
) est utilisé. - Obtient le facteur de zoom actuel
scaleFactor
multiplié par pour déterminer le nouveau facteur de zoom et le transmet àCameraControl.setZoomRatio()
. - Configurez un détecteur de gestes pour
PreviewView.setOnTouchListener()
répondre aux événements tactiles dans un fichier .
// 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 capture la vidéo
Un système de capture enregistre généralement un flux vidéo et un flux audio, les compresse, multiplexe les deux flux et écrit les flux résultants sur le disque.
Présentation de l'API VideoCapture
Dans CameraX, la solution pour la capture vidéo est VideoCapture
le cas d'utilisation :
La capture vidéo CameraX se compose de plusieurs composants architecturaux de haut niveau :
- SurfaceProvider , représentant la source vidéo.
- AudioSource , indiquant la source audio.
- Deux encodeurs pour l'encodage et la compression vidéo/audio.
- Un multiplexeur multimédia pour multiplexer deux flux.
- Économiseur de fichiers pour écrire les résultats.
VideoCapture
L'API fait abstraction du moteur de capture complexe pour fournir une API plus simple et plus intuitive pour les applications.
VideoCapture
est un cas d'utilisation CameraX qui peut être utilisé seul ou en combinaison avec d'autres cas d'utilisation. Les combinaisons exactes prises en charge dépendent des capacités matérielles de la caméra, mais la combinaison de Preview
et VideoCapture
pour ce cas d'utilisation s'applique à tous les appareils.
Remarque : est implémenté dans la bibliothèque
VideoCapture
de CameraX , disponible dans et versions ultérieures. L'API CameraX n'est pas définitive et peut changer au fil du temps.camera-video
1.1.0-alpha10
VideoCapture
VideoCapture
L'API se compose des objets suivants qui peuvent communiquer avec l'application :
VideoCapture
est la classe de cas d'utilisation de niveau supérieur. Lier àVideoCapture
via et à d'autres cas d'utilisation de CameraX .CameraSelector
LifecycleOwner
Recorder
est une implémentationVideoCapture
étroitement couplée àVideoOutput
.Recorder
Utilisé pour effectuer des opérations de capture vidéo et audio. Appliquer enRecorder
créant un objet d'enregistrement.PendingRecording
L'objet d'enregistrement est configuré, avec des options telles que l'activation de l'audio et la configuration des écouteurs d'événement. Vous devez utiliserRecorder
pour créerPendingRecording
.PendingRecording
Rien ne sera enregistré.Recording
L'opération d'enregistrement proprement dite sera effectuée. Vous devez utiliserPendingRecording
pour créerRecording
.
La figure suivante montre la relation entre ces objets :
légende:
- Utilisez
QualitySelector
créerRecorder
. - Utilisez l'une de ces
OutputOptions
configurationsRecorder
. withAudioEnabled()
Utilisez pour activer l'audio si vous le souhaitez .- Appelé avec
VideoRecordEvent
l'auditeurstart()
pour commencer l'enregistrement. Recording
Utilisez pourpause()/resume()/stop()
contrôler les opérations d'enregistrement.- Répondre à l'intérieur de l'écouteur d'événement
VideoRecordEvents
.
Une liste détaillée des API se trouve dans current-txt dans le code source .
Tourner une vidéo avec CameraProvider
Si vous utilisez CameraProvider
un VideoCapure
cas d'utilisation lié, vous devez créer un VideoCapture
UseCase et transmettre un Recorder
objet. La qualité vidéo peut ensuite Recorder.Builder
être définie via et éventuellement définie FallbackStrategy
au cas où l'appareil ne répondrait pas aux spécifications de qualité requises. Enfin, VideoCapture
l'instance est liée à avec d'autres UseCases CameraProvider
.
Créer un objet QualitySelector
Les applications peuvent configurer la résolution vidéo via QualitySelector
l'objet Recorder
.
CameraX Recorder
prend en charge les qualités de résolution vidéo prédéfinies suivantes :
- Qualité.UHD pour la taille vidéo 4K Ultra HD ( 2160p )
- Qualité.FHD pour la taille vidéo Full HD ( 1080p )
- Qualité.HD pour la taille de la vidéo HD ( 720p )
- Qualité.SD pour les tailles de vidéo en définition standard ( 480p )
Veuillez noter que d'autres résolutions sont disponibles pour CameraX sur autorisation de l'application. La taille exacte de la vidéo pour chaque option dépend des capacités de votre caméra et de votre encodeur. Voir la documentation de CamcorderProfile pour plus de détails .
Vous pouvez le créer en utilisant l'une des méthodes suivantes QualitySelector
:
-
L'utilisation
fromOrderedList()
fournit plusieurs résolutions préférées et inclut une stratégie de secours au cas où aucune des résolutions préférées n'est prise en charge.CameraX peut déterminer la meilleure correspondance de secours en fonction des capacités de la caméra sélectionnée. Voir la spécification
QualitySelector
FallbackStrategy pour plus de détails . Par exemple, le code suivant demande la résolution d'enregistrement la plus élevée prise en charge ; si aucune des résolutions demandées n'est prise en charge, CameraX est autorisé à choisir la résolution la plus proche :Quality.SD
val qualitySelector = QualitySelector.fromOrderedList(
listOf(Quality.UHD, Quality.FHD, Quality.HD, Quality.SD),
FallbackStrategy.lowerQualityOrHigherThan(Quality.SD))
- Interrogez d'abord les résolutions prises en charge par la caméra, puis
QualitySelector::from()
choisissez parmi les résolutions prises en charge avec :
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()
}
}
Notez que QualitySelector.getSupportedQualities()
la fonction renvoyée doit être applicable à VideoCapture
usecase ou à une combinaison de VideoCapture
et Preview
usecase. Lors de la liaison avec ImageCapture
le cas d'utilisation ou ImageAnalysis
, CameraX peut toujours échouer à se lier si la caméra demandée ne prend pas en charge la combinaison souhaitée.
Créer et lier un objet VideoCapture
Une fois que vous avez QualitySelector
un , vous pouvez créer VideoCapture
l'objet et effectuer la liaison. Notez que cette liaison est la même que pour les autres cas d'utilisation :
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
Le format le plus approprié pour le système sera choisi. Le codec vidéo le plus courant est H.264 AVC
, et son format de conteneur est MPEG-4
.
Remarque : Il n'est actuellement pas possible de configurer le codec vidéo final et le format de conteneur.
Configurer et générer un objet d'enregistrement
Diverses configurations peuvent ensuite videoCapture.output
être effectuées sur la propriété pour générer un Recording
objet qui peut être utilisé pour mettre en pause, reprendre ou arrêter l'enregistrement. Enfin, appelez start()
pour démarrer l'enregistrement, en passant dans le contexte et Consumer<VideoRecordEvent>
un écouteur d'événement pour gérer les événements d'enregistrement vidéo.
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) // 启动并注册录制事件监听
Options de sortie
Recorder
Les types de configuration suivants sont pris en charge OutputOptions
:
FileDescriptorOutputOptions
pour capturer dansFileDescriptor
un fichier .FileOutputOptions
, pour la capture dans un fichier.MediaStoreOutputOptions
pour la capture dans le MediaStore.
Quel que soit le OutputOptions
type de , vous pouvez setFileSizeLimit()
définir une taille de fichier maximale via . D'autres options sont spécifiques aux types de sortie individuels, telles que ParcelFileDescriptor
spécifiques à FileDescriptorOutputOptions
.
Mettre en pause, reprendre et arrêter
Lorsque vous appelez start()
la fonction, l'objet Recorder
est renvoyé Recording
. L'application peut utiliser cet Recording
objet pour terminer la capture ou effectuer d'autres actions telles que la suspension ou la reprise. Vous pouvez mettre en pause, reprendre et arrêter une session en cours avec Recording
:
pause()
, pour mettre en pause l'enregistrement actuellement actif.resume()
, pour reprendre un enregistrement actif mis en pause.stop()
, qui termine l'enregistrement et efface tous les objets d'enregistrement associés.
stop()
Notez que vous pouvez appeler pour terminer si l'enregistrement est en pause ou actif Recording
.
Recorder
Un Recording
objet est pris en charge à la fois. Recording
Après avoir appelé Recording.stop()
ou sur l'objet précédent Recording.close()
, vous pouvez démarrer un nouvel enregistrement.
if (recording != null) {
// Stop the current recording session.
recording.stop()
recording = null
return
}
..
recording = ..
écouteur d'événement
Si vous vous êtes enregistré avec PendingRecording.start()
, EventListener
vous Recording
communiquerez VideoRecordEvent
en utilisant .
CameraX envoie un événement chaque fois que l'enregistrement démarre sur l'appareil photo correspondant VideoRecordEvent.Start
.
VideoRecordEvent.Status
Pour enregistrer des statistiques, telles que la taille du fichier actuel et la durée de l'enregistrement.VideoRecordEvent.Finalize
Pour l'enregistrement des résultats, contiendra le fichier finalURI
et toutes les erreurs associées.
Une fois que votre application reçoit un indiquant une session d'enregistrement réussie Finalize
, vous pouvez accéder à la vidéo capturée à partir de l'emplacement spécifié dans OutputOptions
le fichier .
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)
}
}
}
}
Exemple de code complet
CameraProvider
Voici un exemple de code complet pour capturer une vidéo à l'aide de Compose :
// 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()
}
Configuration du routage et des autorisations :
@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()
}
Afin de visualiser la vidéo enregistrée, nous utilisons la bibliothèque ExoPlayer de Google pour lire la vidéo dans un autre écran de routage , en ajoutant des dépendances :
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()
}
}
}
Tourner une vidéo avec CameraController
Avec CameraX CameraController
, vous pouvez basculer indépendamment ImageCapture
, VideoCapture
et ImageAnalysis
UseCase, à condition que ces UseCase puissent être utilisés en même temps . ImageCapture
et ImageAnalysis
UseCase sont activés par défaut, vous n'avez donc pas besoin d'appeler setEnabledUseCases()
pour prendre une photo.
Si CameraController
vous enregistrez une vidéo à l'aide de , vous devez d'abord setEnabledUseCases()
autoriser VideoCapture
UseCase à l'aide de .
// CameraX: Enable VideoCapture UseCase on CameraController.
cameraController.setEnabledUseCases(VIDEO_CAPTURE);
Si vous souhaitez commencer à enregistrer une vidéo, vous pouvez appeler CameraController.startRecording()
la fonction. Cette fonction enregistre la vidéo enregistrée sur File
, comme illustré dans l'exemple suivant. De plus, vous devez transmettre a Executor
et une OnVideoSavedCallback
classe d'implémentation de pour gérer les rappels de réussite et d'erreur.
À partir de CameraX 1.3.0-alpha02
, startRecording()
renvoie un Recording
objet qui peut être utilisé pour mettre en pause, reprendre et arrêter l'enregistrement vidéo. Vous pouvez également AudioConfig
activer ou désactiver la fonction d'enregistrement vidéo via le paramètre. Pour activer l'enregistrement, vous devez vous assurer que vous avez l'autorisation d'utiliser le microphone. Semblable à CameraProvider
, un Consumer<VideoRecordEvent>
type de rappel est également transmis pour surveiller les événements vidéo.
@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)
}
}
}
}
}
Comme vous pouvez le voir, CameraController
enregistrer une vidéo avec est beaucoup plus simple que d'utiliser CameraProvider
.
Exemple de code complet
CameraController
Voici un exemple de code complet pour capturer une vidéo à l'aide de Compose :
// 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)
}
}
L'analyse d'image
Le cas d'utilisation d'analyse d'image fournit à votre application des images accessibles au processeur sur lesquelles vous pouvez effectuer un traitement d'image, une vision par ordinateur ou une inférence d'apprentissage automatique. L'application implémente analyze()
une méthode qui exécute chaque image.
Pour utiliser Image Analytics dans votre application, procédez comme suit :
- Construire
ImageAnalysis
des cas d'utilisation. - créer
ImageAnalysis.Analyzer
. - Pour
ImageAnalysis
configurer l'analyseur. - Liez
lifecycleOwner
,cameraSelector
etImageAnalysis
cas d'utilisation à des cycles de vie. (ProcessCameraProvider.bindToLifecycle()
)
Immédiatement après la liaison, CameraX enverra l'image à l'analyseur enregistré. Lorsque l'analyse est terminée, appelez ImageAnalysis.clearAnalyzer()
ou dissociez le ImageAnalysis
cas d'utilisation pour arrêter l'analyse.
Construire un cas d'utilisation ImageAnalysis
ImageAnalysis
Des analyseurs (consommateurs d'images) peuvent être connectés à CameraX (producteurs d'images). Les applications peuvent utiliser ImageAnalysis.Builder
pour créer ImageAnalysis
des objets. A l'aide de ImageAnalysis.Builder
, l'application peut être configurée comme suit :
Paramètres de sortie d'image :
- Format : CameraX est
setOutputImageFormat(int)
pris en charge viaYUV_420_888
etRGBA_8888
. Le format par défaut estYUV_420_888
. - Résolution et AspectRatio : Vous pouvez définir l'un de ces paramètres, mais notez que vous ne pouvez pas définir les deux valeurs en même temps.
- angle de rotation .
- Nom cible : utilisez ce paramètre pour le débogage.
Contrôle du flux d'images :
- Exécuteur d'arrière-plan Exécuteur
- profondeur de la file d'attente d'images
- stratégie de contre-pression
Voici l' ImageAnalysis
exemple de code de construction :
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)
Remarque : Analyzer
La méthode de rappel dans sera rappelée à chaque image lors de la prévisualisation.
Les applications peuvent définir la résolution ou le format d'image, mais pas les deux. La résolution de sortie exacte dépend de la taille (ou du rapport d'aspect) et des capacités matérielles demandées par l'application, et peut différer de la taille ou du rapport d'aspect demandé. Voir la documentation sur setTargetResolution() pour les algorithmes de correspondance de résolution
Les applications peuvent configurer les pixels de l'image de sortie pour qu'ils soient dans l'espace colorimétrique YUV (par défaut) ou RGBA. Lors du réglage du format de sortie RGBA, CameraX convertira en interne l'image de l'espace colorimétrique YUV vers l'espace colorimétrique RGBA, et regroupera les bits de l'image dans le ImageProxy
premier plan (les deux autres plans ne sont pas utilisés) ByteBuffer
, la séquence est la suivante :
ImageProxy.getPlanes()[0].buffer[0]: alpha
ImageProxy.getPlanes()[0].buffer[1]: red
ImageProxy.getPlanes()[0].buffer[2]: green
ImageProxy.getPlanes()[0].buffer[3]: blue
...
mode de fonctionnement
Lorsque le pipeline d'analyse de votre application ne peut pas répondre aux exigences de fréquence d'images de CameraX, vous pouvez configurer CameraX pour supprimer des images de l'une des manières suivantes :
-
Non bloquant (par défaut) : dans ce mode, l'exécuteur met toujours en cache la dernière image dans le tampon d'image (similaire à une file d'attente de profondeur 1), tandis que l'application analyse l'image précédente. Si CameraX reçoit une nouvelle image avant que l'application n'ait terminé le traitement, la nouvelle image est enregistrée dans le même tampon, écrasant la précédente. Notez que dans ce cas,
ImageAnalysis.Builder.setImageQueueDepth()
cela n'a aucun effet, le contenu du tampon est toujours écrasé. Vous pouvez activer ce mode non bloquant en utilisantSTRATEGY_KEEP_ONLY_LATEST
l'appel .setBackpressureStrategy()
Consultez la documentation de référence pour STRATEGY_KEEP_ONLY_LATEST pour plus d'informations sur les effets liés à l'exécuteur . -
Blocage : Dans ce mode, l'exécuteur interne peut ajouter plusieurs images à la file d'attente d'images interne et ne commencer à supprimer des images que lorsque la file d'attente est pleine. Le système bloque l'intégralité de l'appareil photo : si un appareil photo a plusieurs cas d'utilisation liés, lorsque CameraX traite ces images, le système les bloque toutes. Par exemple, si l'aperçu et l'analyse d'image sont liés à un appareil photo, l'aperçu correspondant sera également bloqué pendant que CameraX traite l'image. Vous pouvez activer le mode de blocage en passant
STRATEGY_BLOCK_PRODUCER
asetBackpressureStrategy()
à . De plus, vous pouvezImageAnalysis.Builder.setImageQueueDepth()
configurer la profondeur de la file d'attente d'images à l'aide de .
Si l'analyseur a une faible latence et des performances élevées, auquel cas le temps total passé à analyser l'image est inférieur à la durée d'une image CameraX (par exemple, 16 ms à 60 ips), les deux modes de fonctionnement ci-dessus peuvent fournir un ensemble fluide expérience. Le mode de blocage est toujours utile dans certains cas, par exemple lorsqu'il s'agit de très brèves instabilités du système.
Si le profileur a une latence élevée et des performances élevées, une combinaison de mode de blocage et de longues files d'attente sera nécessaire pour compenser le retard. Notez cependant que l'application peut toujours traiter toutes les trames dans ce cas.
Le mode non bloquant peut être plus approprié si la latence de l'analyseur est élevée et prend du temps (l'analyseur ne peut pas traiter toutes les trames), car dans ce cas, le système doit supprimer des trames pour le chemin d'analyse, mais laisser d'autres cas d'utilisation liés simultanément Toutes les trames sont toujours visible.
Analyseur de kit ML (analyseur de kit d'apprentissage automatique)
La suite d'apprentissage automatique de Google fournit des API de vision d'apprentissage automatique sur l'appareil pour détecter les visages, scanner les codes-barres, étiqueter les images, etc. Avec l'aide de ML Kit Analyzer, vous pouvez intégrer plus facilement ML Kit aux applications CameraX.
Les analyseurs ML Kit sont ImageAnalysis.Analyzer
des implémentations de l'interface. Le profileur optimise l'utilisation avec ML Suite en remplaçant la résolution cible par défaut (si nécessaire), gère les transformations de coordonnées et transmet le cadre à ML Suite qui renvoie les résultats d'analyse agrégés.
Implémentation de l'analyseur de kit ML
Pour implémenter les profileurs ML Kit, il est recommandé d'utiliser CameraController
la classe, qui peut être PreviewView
utilisée avec pour afficher les éléments de l'interface utilisateur. Lors de l'implémentation avec CameraController
, l'analyseur ML Kit gère la transformation des coordonnées entre le ImageAnalysis
flux brut et le pour vous. PreviewView
L'analyseur reçoit le système de coordonnées cible de CameraX, calcule la transformation des coordonnées et le transmet à la classe ML Kit Detector
pour analyse.
Pour utiliser un analyseur ML Kit avec CameraController
, appelez setImageAnalysisAnalyzer()
et transmettez-lui un nouvel objet analyseur ML Kit, incluant les éléments suivants dans son constructeur :
-
Une liste de suites d'apprentissage automatique
Detector
, CameraX sera appelée séquentiellement. -
Le système de coordonnées cible utilisé pour déterminer les coordonnées de sortie de ML Kit :
COORDINATE_SYSTEM_VIEW_REFERENCED
PreviewView
: Les coordonnées transformées .
COORDINATE_SYSTEM_ORIGINAL
: lesImageAnalysis
coordonnées du flux d'origine. -
Utilisé pour appeler
Consumer
le rappel etMlKitAnalyzer.Result
transmettre le à l'applicationExecutor
. -
Appelé par CameraX lorsqu'il y a une nouvelle sortie de kit ML
Consumer
.
L'utilisation du profileur ML Kit nécessite l'ajout de dépendances :
def camerax_version = "1.3.0-alpha04"
implementation "androidx.camera:camera-mlkit-vision:${camerax_version}"
Reconnaissance de code QR/code-barres
Ajoutez la bibliothèque de dépendances de codes-barres ML Kit :
implementation 'com.google.mlkit:barcode-scanning:17.1.0'
Voici un exemple d'utilisation :
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)
}
}
}
)
}
Dans l'exemple de code ci-dessus, l'analyseur ML Kit transmettra ce qui suit à la classe BarcodeScanner
deDetector
:
COORDINATE_SYSTEM_VIEW_REFERENCED
Une transformation basée sur la représentation du système de coordonnées cibleMatrix
.- cadre de la caméra.
S'il BarcodeScanner
rencontre des problèmes, il Detector
générera une erreur et l'analyseur ML Kit propagera cette erreur à votre application. En cas de succès, l'analyseur ML Kit renverra MLKitAnalyzer.Result#getValue()
, dans ce cas Barcode
un objet.
Vous pouvez également barcode.valueType
obtenir le type de la valeur via :
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
}
}
}
Vous pouvez également implémenter des analyseurs ML Kit à l'aide de la classe camera-core
dans . ImageAnalysis
Cependant, comme ImageAnalysis
n'est pas PreviewView
intégré à , vous devez gérer manuellement les transformations de coordonnées. Pour plus d'informations, consultez la documentation de référence de ML Kit Analyzer .
Configuration du routage et des autorisations :
@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))
}
}
}
Pour plus de contenu connexe, veuillez vous référer à : https://developers.google.cn/ml-kit/vision/barcode-scanning/android?hl=zh-cn
reconnaissance de texte
Ajoutez des dépendances :
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'
}
Exemple de code :
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
}
}
}
)
}
Un outil de reconnaissance de texte décompose le texte en blocs, lignes, éléments et symboles. Grosso modo:
- Un bloc est un groupe de lignes de texte contiguës, telles que des paragraphes ou des colonnes.
- Une ligne est un groupe de mots consécutifs sur le même axe,
- L'élément est un groupe d'alphanumériques ("mots") consécutifs de l'alphabet latin qui fait référence à d'autres caractères dans la plupart des colonnes de l'alphabet latin
- Le symbole est un caractère alphanumérique unique sur le même axe dans la plupart des langues latines, ou un caractère dans d'autres langues
La figure suivante présente ces exemples par ordre décroissant. Le premier bloc en surbrillance (indiqué en cyan) est un bloc de texte. Le deuxième ensemble de blocs bleus en surbrillance sont des lignes de texte. Enfin, le troisième ensemble de mots surlignés en bleu foncé est Word.
Pour tous les blocs, lignes, éléments et symboles détectés, l'API renvoie les cadres de délimitation, les coins, les informations de rotation, les scores de confiance, les langues reconnues et le texte reconnu.
Pour plus de contenu connexe, veuillez vous référer à : https://developers.google.cn/ml-kit/vision/text-recognition/v2/android?hl=zh-cn
reconnaissance de visage
Ajoutez des dépendances :
dependencies {
// Use this dependency to bundle the model with your app
implementation 'com.google.mlkit:face-detection:16.1.5'
}
Avant d'appliquer la détection de visage à une image, si vous souhaitez modifier les paramètres par défaut du détecteur de visage, utilisez l'objet FaceDetectorOptions pour spécifier ces paramètres. Vous pouvez modifier les paramètres suivants :
installation | Fonction |
---|---|
setPerformanceMode |
PERFORMANCE_MODE_FAST (par défaut) / PERFORMANCE_MODE_ACCURATE Prioriser la vitesse ou la précision lors de la détection des visages. |
setLandmarkMode |
LANDMARK_MODE_NONE (par défaut) / LANDMARK_MODE_ALL S'il faut essayer d'identifier les "repères" du visage : yeux, oreilles, nez, joues, bouche, etc. |
setContourMode |
CONTOUR_MODE_NONE (par défaut) / CONTOUR_MODE_ALL S'il faut détecter le contour des traits du visage. Détecte uniquement les contours des visages les plus proéminents de l'image. |
setClassificationMode |
CLASSIFICATION_MODE_NONE (par défaut) / CLASSIFICATION_MODE_ALL S'il faut classer les visages dans différentes catégories (par exemple, "sourire" vs "yeux ouverts"). |
setMinFaceSize |
float (Par défaut : 0.1f ) Définit la taille de visage minimale souhaitée exprimée sous la forme du rapport entre la largeur de la tête et la largeur de l'image. |
enableTracking |
false (par défaut) / true Attribuer ou non des identifiants aux visages pour les utiliser dans le suivi des visages à travers les images. Notez que lorsque la détection des contours est activée, un seul visage est détecté, donc le suivi des visages ne produira pas de résultats utiles. Pour cette raison, pour accélérer la détection, n'activez pas la détection des contours et le suivi du visage en même temps. |
Par exemple:
// 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();
Si l'opération de détection de visage réussit, le système transmet un tableau d'objets au success listener Face
. Chaque Face
objet représente un visage détecté dans l'image. Pour chaque visage, vous obtenez ses coordonnées de délimitation dans l'image d'entrée, ainsi que toute autre information que vous avez configurée pour rechercher le détecteur de visage. Par exemple:
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
}
}
Exemple de code :
@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
}
Effet:
Pour plus de contenu connexe, veuillez vous référer à : https://developers.google.cn/ml-kit/vision/face-detection/android?hl=zh-cn
CameraX autres options de configuration avancées
CaméraXConfig
Pour plus de simplicité, CameraX a des configurations par défaut (telles que des exécuteurs et des gestionnaires internes) adaptées à la plupart des scénarios d'utilisation. Cependant, si votre application a des besoins particuliers ou souhaite personnaliser ces configurations, CameraXConfig
l'interface peut être utilisée à cet effet.
Avec help CameraXConfig
, les applications peuvent effectuer les opérations suivantes :
- Utilisez pour
setAvailableCameraLimiter()
optimiser le délai de démarrage. setCameraExecutor()
Fournissez un exécuteur d'application à CameraX en utilisant .- Remplacez le gestionnaire de planificateur par défaut par
setSchedulerHandler()
. - Utilisez
setMinimumLoggingLevel()
pour modifier le niveau de journalisation.
La procédure suivante montre comment utiliser CameraXConfig
:
- Créez un objet avec votre configuration personnalisée
CameraXConfig
. Application
Implémente l'interface dansCameraXConfig.Provider
etgetCameraXConfig()
renvoieCameraXConfig
l'objet dans .- Ajoutez
Application
la classe auAndroidManifest.xml
fichier.
Par exemple, l'exemple de code suivant limite la journalisation de CameraX aux messages d'erreur uniquement :
class CameraApplication : Application(), CameraXConfig.Provider {
override fun getCameraXConfig(): CameraXConfig {
return CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig())
.setMinimumLoggingLevel(Log.ERROR).build()
}
}
Si votre application a besoin de connaître la configuration de CameraX après sa configuration, conservez CameraXConfig
une copie locale de l'objet.
limiteur de caméra
Lors du premier appel ProcessCameraProvider.getInstance()
, CameraX énumère et interroge les caractéristiques des caméras disponibles sur l'appareil. Étant donné que CameraX doit communiquer avec des composants matériels, ce processus peut prendre beaucoup de temps pour chaque caméra, en particulier sur les appareils bas de gamme. Si votre application utilise uniquement une caméra spécifique sur l'appareil (telle que la caméra frontale par défaut), vous pouvez configurer CameraX pour qu'elle ignore les autres caméras, réduisant ainsi le délai de démarrage des caméras utilisées par votre application.
CameraXConfig.Builder.setAvailableCamerasLimiter()
Si a est passé pour CameraSelector
filtrer une caméra, CameraX supposera que la caméra n'existe pas au moment de l'exécution. Par exemple, le code suivant interdit à l'application d'utiliser uniquement la caméra arrière par défaut de l'appareil :
class MainApplication : Application(), CameraXConfig.Provider {
override fun getCameraXConfig(): CameraXConfig {
return CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig())
.setAvailableCamerasLimiter(CameraSelector.DEFAULT_BACK_CAMERA)
.build()
}
}
fil
De nombreuses API de plate-forme sur lesquelles CameraX est construit nécessitent le blocage de la communication inter-processus (IPC) avec le matériel, ce qui peut parfois nécessiter des temps de réponse de centaines de millisecondes. Par conséquent, CameraX n'appelle ces API qu'à partir d'un thread d'arrière-plan, évitant ainsi le blocage du thread principal et gardant l'interface fluide. CameraX gère ces threads d'arrière-plan en interne, de sorte que ce type de comportement semble transparent. Cependant, certaines applications nécessitent un contrôle strict des threads. CameraXConfig
Permet à une application de définir le thread d'arrière-plan utilisé par CameraXConfig.Builder.setCameraExecutor()
et .CameraXConfig.Builder.setSchedulerHandler()
choisir automatiquement
CameraX fournit automatiquement des fonctionnalités dédiées en fonction de l'appareil sur lequel votre application est exécutée. Par exemple, si vous ne spécifiez pas de résolution ou si vous spécifiez une résolution non prise en charge, CameraX détermine automatiquement la meilleure résolution à utiliser. Toutes ces opérations sont gérées par la bibliothèque sans que vous ayez à écrire de code spécifique à l'appareil.
L'objectif de CameraX est d'initialiser avec succès une session de caméra. Cela signifie que CameraX réduit la résolution et le format d'image en fonction des capacités de l'appareil. Cela se produit pour les raisons suivantes :
- L'appareil ne prend pas en charge la résolution demandée.
- Il existe des problèmes de compatibilité avec l'appareil, comme un appareil plus ancien qui nécessite une certaine résolution pour fonctionner correctement.
- Sur certains appareils, certains formats ne sont disponibles que dans certains formats d'image.
- Pour l'encodage JPEG ou vidéo, l'appareil préfère le "mod16 le plus proche". Voir SCALER_STREAM_CONFIGURATION_MAP pour plus de détails .
Bien que CameraX crée et gère la session, vous devez toujours vérifier dans votre code la taille de l'image renvoyée par la sortie du cas d'utilisation et l'ajuster en conséquence.
Faire tourner
Par défaut, lors de la création du cas d'utilisation, la rotation de la caméra est définie pour correspondre à la rotation d'affichage par défaut. Par défaut, CameraX génère une sortie qui garantit que l'application correspond à ce que vous vous attendez à voir dans l'aperçu. Vous pouvez modifier l'angle de rotation en une valeur personnalisée pour prendre en charge les appareils à plusieurs affichages en transmettant l'orientation d'affichage actuelle lors de la configuration de l'objet de cas d'utilisation ou en transmettant l'orientation d'affichage dynamiquement après la création de l'objet de cas d'utilisation.
Votre application peut utiliser un paramètre de configuration pour définir la rotation cible. Ensuite, même pendant l'exécution du cycle de vie, l'application peut mettre à jour le paramètre de rotation à l'aide de méthodes dans l'API de cas d'utilisation telles que ImageAnalysis.setTargetRotation(). Vous pouvez faire ce qui précède pendant que l'application est verrouillée en mode portrait afin que vous n'ayez pas besoin de reconfigurer la rotation, mais les cas d'utilisation de photos ou d'analyses nécessitent une connaissance de la rotation actuelle de l'appareil. Par exemple, un cas d'utilisation peut nécessiter de connaître l'angle de rotation pour effectuer la détection de visage dans la bonne orientation ou pour définir une photo en orientation paysage ou portrait.
Les données des images capturées peuvent ne pas être stockées avec les informations de rotation. Les données Exif incluent des informations de rotation afin que l'application de la galerie affiche l'image dans l'orientation correcte de l'écran après l'enregistrement.
Pour afficher les données d'aperçu dans le bon sens, vous pouvez Preview.PreviewOutput()
créer une transformation à l'aide de la sortie de métadonnées du fichier .
L'exemple de code suivant montre comment définir l'angle de rotation pour un événement d'orientation :
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()
}
Chaque cas d'utilisation fera soit directement pivoter les données d'image en fonction de l'angle de rotation défini, soit fournira à l'utilisateur des métadonnées de rotation des données d'image non pivotées.
- Aperçu : fournit une sortie de métadonnées à utiliser en
Preview.getTargetRotation()
connaissant les paramètres de rotation pour la résolution cible. - ImageAnalysis : Fournit une sortie de métadonnées pour comprendre où les coordonnées du tampon d'image sont relatives aux coordonnées d'affichage.
- ImageCapture : modifie les métadonnées Exif de l'image , le tampon ou les deux, pour refléter le paramètre de rotation. La valeur modifiée dépend de l'implémentation HAL.
Pour plus d'informations sur la rotation, veuillez vous référer à : https://developer.android.google.cn/training/camerax/orientation-rotation?hl=zh-cn
rectangle de découpage
Par défaut, le rectangle de découpage est le rectangle de tampon complet, qui peut être personnalisé avec ViewPort
et . UseCaseGroup
En regroupant les cas d'utilisation et en définissant la fenêtre d'affichage, CameraX garantit que les rectangles de découpage de tous les cas d'utilisation d'un groupe pointent vers la même zone du capteur de la caméra.
L'extrait de code suivant montre comment utiliser ces deux classes :
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
Utilisé pour spécifier le rectangle de tampon visible par l'utilisateur final. CameraX calcule le plus grand rectangle de découpage possible en fonction des propriétés de la fenêtre et du cas d'utilisation associé. En général, pour obtenir ce que vous voyez, vous devez configurer la fenêtre d'affichage en fonction de votre cas d'utilisation de l'aperçu. Un moyen simple d'obtenir la fenêtre consiste à utiliser PreviewView
.
L'extrait de code suivant montre comment obtenir ViewPort
un objet :
val viewport = findViewById<PreviewView>(R.id.preview_view).viewPort
Dans l'exemple précédent, l'application obtient le même contenu via ImageAnalysis
et ImageCapture
que l'utilisateur final voit dans PreviewView
(en supposant que PreviewView
le type de zoom de est défini sur la valeur par défaut FILL_CENTER
). Une fois que le rectangle d'écrêtage et la rotation sont appliqués au tampon de sortie, l'image sera cohérente dans tous les cas d'utilisation, bien que la résolution puisse varier. Pour plus d'informations sur l'application des informations de transformation, consultez Sortie de transformation .
Sélectionnez une caméra disponible
CameraX sélectionne automatiquement le meilleur appareil photo en fonction des exigences de l'application et du cas d'utilisation. Si vous souhaitez utiliser un autre appareil que celui sélectionné automatiquement, plusieurs possibilités s'offrent à vous :
- Utilisez
CameraSelector.DEFAULT_FRONT_CAMERA
pour demander la caméra frontale par défaut. - Utilisez
CameraSelector.DEFAULT_BACK_CAMERA
pour demander la caméra arrière par défaut. - Utilisez
CameraSelector.Builder.addCameraFilter()
pourCameraCharacteristics
filtrer la liste des appareils disponibles.
Remarque : Les appareils photo doivent être reconnus par le système et apparaître dans
CameraManager.getCameraIdList()
avant de pouvoir être utilisés.
En outre, chaque fabricant d'équipement d'origine (OEM) doit choisir de prendre en charge ou non les appareils photo externes. PackageManager.FEATURE_CAMERA_EXTERNAL
Par conséquent, assurez-vous de vérifier qu'il est activé avant d'essayer d'utiliser une caméra externe .
L'exemple de code suivant montre comment créer un CameraSelector
pour affecter la sélection de périphérique :
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)
Sélectionnez plusieurs caméras en même temps
À partir de CameraX 1.3 , vous pouvez également sélectionner plusieurs caméras en même temps. Par exemple, vous pouvez configurer les caméras avant et arrière pour prendre des photos ou enregistrer des vidéos des deux points de vue simultanément.
Lors de l'utilisation de la fonction de caméra simultanée, l'appareil peut exécuter deux caméras avec des orientations d'objectif différentes en même temps, ou exécuter deux caméras arrière en même temps. Le bloc de code suivant montre comment bindToLifecycle
configurer deux caméras lors de l'appel et ConcurrentCamera
obtenir deux Camera
objets à partir de l'objet renvoyé.
// 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]
résolution de la caméra
Vous pouvez choisir de laisser CameraX définir la résolution de l'image en fonction des capacités de l'appareil, du niveau matériel pris en charge par l'appareil, du cas d'utilisation et de la combinaison de rapport d'aspect fournie. Vous pouvez également définir une résolution cible spécifique ou un format d'image spécifique dans les cas d'utilisation où la configuration correspondante est prise en charge.
résolution automatique
CameraX peut cameraProcessProvider.bindToLifecycle()
déterminer automatiquement le meilleur paramètre de résolution en fonction du cas d'utilisation spécifié dans . Dans la mesure du possible, bindToLifecycle()
spécifiez tous les cas d'utilisation qui doivent s'exécuter simultanément dans une seule session d'un seul appel. CameraX détermine la résolution en fonction de l'ensemble de cas d'utilisation groupés, en tenant compte du niveau matériel pris en charge par l'appareil, ainsi que des variations spécifiques à l'appareil (l'appareil dépasse ou ne respecte pas la configuration de diffusion disponible). Ceci est fait pour garantir que l'application s'exécute sur une variété d'appareils avec un minimum de chemins de code spécifiques à l'appareil.
Le format d'image par défaut pour les cas d'utilisation de capture d'image et d'analyse d'image est 4:3
.
Pour les cas d'utilisation avec des rapports d'aspect configurables, laissez l'application spécifier le rapport d'aspect souhaité en fonction de la conception de l'interface utilisateur. CameraX génère une sortie dans le rapport d'aspect demandé, correspondant aussi étroitement que possible au rapport d'aspect pris en charge par l'appareil. Si aucune résolution de correspondance exacte n'est prise en charge, la résolution qui satisfait le plus de critères est choisie. C'est-à-dire que l'application déterminera comment la caméra est affichée dans l'application, et CameraX déterminera le réglage de résolution optimal de la caméra pour répondre aux exigences spécifiques des différents appareils.
Par exemple, une application peut effectuer l'une des actions suivantes :
- Spécifiez la résolution cible de
4:3
ou pour le cas d'utilisation16:9
- Spécifiez une résolution personnalisée, CameraX essaiera de trouver la résolution la plus proche de cette résolution
ImageCapture
Spécifie le rapport d'aspect de recadrage pour
CameraX sélectionne automatiquement Camera2
la résolution de l'interface interne. Le tableau ci-dessous montre ces résolutions :
spécifier la résolution
Lors de la création d'un cas d'utilisation à l'aide setTargetResolution(Size resolution)
de la méthode, vous pouvez définir une résolution spécifique, comme illustré dans l'exemple de code suivant :
val imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(Size(1280, 720))
.build()
Vous ne pouvez pas définir le format d'image cible et la résolution cible pour le même cas d'utilisation. Si c'est le cas, il sera lancé lors de la construction de l'objet de configuration IllegalArgumentException
.
Après avoir fait pivoter la taille prise en charge par l'angle de rotation cible, exprimez la résolution dans un système de coordonnées Size
. Par exemple, un appareil dont l'orientation naturelle de l'écran est portrait et utilise un angle de rotation cible naturel peut spécifier s'il demande une image portrait, 480x640
tandis que le même appareil lorsqu'il est tourné 90
et cible une orientation d'écran paysage peut le spécifier 640x480
.
La résolution cible tente de définir une limite inférieure sur la résolution de l'image. La résolution réelle de l'image est la résolution disponible la plus proche et sa taille n'est pas inférieure à la résolution cible déterminée par la mise en œuvre de la caméra.
Cependant, s'il n'y a pas de résolution égale ou supérieure à la résolution cible, la résolution disponible la plus proche inférieure à la résolution cible est choisie. Les résolutions avec le même rapport d'aspect que celui fourni Size
ont priorité sur les résolutions avec des rapports d'aspect différents.
CameraX applique la résolution la plus appropriée sur demande. Spécifiez uniquement si le besoin principal est de répondre aux exigences de rapport d'aspect, et setTargetAspectRatio
CameraX déterminera une résolution spécifique appropriée en fonction de l'appareil. À utiliser si l'exigence principale de votre application est de spécifier une résolution pour améliorer l'efficacité du traitement d'image (telle que le traitement d'images de petite ou moyenne taille en fonction des capacités de traitement de l'appareil) setTargetResolution(Size resolution)
.
Remarque : Si vous utilisez
setTargetResolution()
, vous pouvez obtenir des tampons avec des proportions qui ne correspondent pas à d'autres cas d'utilisation. Si les proportions doivent correspondre, vérifiez les dimensions de la mémoire tampon renvoyées par les deux cas d'utilisation, et découpez ou mettez à l'échelle l'une pour qu'elle corresponde à l'autre.
Si votre application nécessite des résolutions exactes, consultezcreateCaptureSession()
le tableau ci-dessous pour déterminer les résolutions maximales prises en charge pour chaque niveau matériel. Pour voir les résolutions spécifiques prises en charge par votre appareil actuel, voirStreamConfigurationMap.getOutputSizes(int)
.
Si votre application s'exécute sur Android 10 ou version ultérieure, vous pouvez utiliser isSessionConfigurationSupported()
des fichiers SessionConfiguration
.
Contrôler la sortie de la caméra
Non seulement CameraX vous permet de configurer en option la sortie de la caméra pour chaque cas d'utilisation individuel, mais il implémente également les interfaces suivantes pour prendre en charge les opérations de caméra courantes dans tous les cas d'utilisation groupés :
- Avec
CameraControl
, vous pouvez configurer les fonctionnalités courantes de la caméra. - Avec
CameraInfo
, vous pouvez interroger l'état de ces fonctions génériques de caméra.
Les CameraControl
fonctionnalités de caméra suivantes sont prises en charge :
- Zoom
- lampe de poche
- Mise au point et mesure (appuyez pour faire la mise au point)
- la compensation d'exposition
Obtenir des instances de CameraControl et CameraInfo
Récupère les instances de et en utilisant l'objet ProcessCameraProvider.bindToLifecycle()
renvoyé par . Le code suivant montre un exemple :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
Par exemple, vous pouvez bindToLifecycle()
soumettre une opération de zoom et d'autres CameraControl
opérations après avoir appelé . Si vous arrêtez ou détruisez l'instance utilisée pour lier la caméra activity
, CameraControl
aucune autre opération ne peut être effectuée et un échec sera renvoyé ListenableFuture
.
REMARQUE : Si
LifecycleOwner
est arrêté ou détruit,Camera
il sera éteint, après quoi tous les changements d'état pour le zoom, la torche, la mise au point et la mesure, et les commandes de compensation d'exposition reviendront à leurs valeurs par défaut.
Zoom
CameraControl
Deux méthodes de modification du niveau de zoom sont proposées :
-
setZoomRatio() est utilisé pour définir le zoom par taux de zoom.
Le rapport doit être compris entre
CameraInfo.getZoomState().getValue().getMinZoomRatio()
etCameraInfo.getZoomState().getValue().getMaxZoomRatio()
. Sinon, la fonction renvoie failedListenableFuture
. -
setLinearZoom() définit l'opération de zoom actuelle avec une valeur de zoom linéaire comprise entre et
0
.1.0
L'avantage du zoom linéaire est qu'il permet au champ de vision (FOV) de s'adapter à mesure que le zoom change. Par conséquent, le zoom linéaire fonctionne bien avec Slider
la vue.
CameraInfo.getZoomState()
renverra l'état actuel du zoom LiveData
. Cette valeur change lorsque la caméra est initialisée ou lorsque le niveau de zoom est défini à l'aide setZoomRatio()
de ou . setLinearZoom()
L'appel de l'une ou l'autre méthode définit les valeurs ZoomState.getZoomRatio()
et prises en charge ZoomState.getLinearZoom()
. Ceci est utile si vous souhaitez que le texte de l'échelle de zoom apparaisse à côté du curseur de zoom. Les deux peuvent être mis à jour en observant simplement ZoomState LiveData
, sans cast.
Le renvoyé par ces deux API ListenableFuture
donne à l'application la possibilité de recevoir des notifications lorsque des demandes répétées avec la valeur de zoom spécifiée sont terminées. De plus, si vous définissez une nouvelle valeur de zoom alors que l'opération de zoom précédente est toujours en cours, l'opération de zoom précédente ListenableFuture
échouera immédiatement.
lampe de poche
CameraControl.enableTorch(boolean)
La torche peut être activée ou désactivée (application torche).
CameraInfo.getTorchState()
Peut être utilisé pour interroger l'état actuel de la lampe de poche. Vous pouvez CameraInfo.hasFlashUnit()
déterminer si la fonction torche est disponible en vérifiant la valeur renvoyée par . Si la torche n'est pas disponible, l'appel CameraControl.enableTorch(boolean)
entraînera la ListenableFuture
fin immédiate du retour avec un résultat d'échec, définissant l'état de la torche sur TorchState.OFF
.
Lorsqu'elle est activée, la torche reste allumée lors de la prise de photos et de vidéos, quel que soit le réglage du mode flash. ImageCapture
ne fonctionnera que lorsque la lampe de poche est désactivée flashMode
.
Mise au point et mesure
CameraControl.startFocusAndMetering()
La zone de mesure AF/AE/AWB peut être définie en fonction des FocusMeteringAction
paramètres spécifiés pour déclencher la mise au point automatique et la mesure de l'exposition. Il existe de nombreuses applications d'appareil photo qui implémentent la fonctionnalité "appuyer pour faire la mise au point" de cette façon.
Point de mesure
Tout d'abord, utilisez MeteringPointFactory.createPoint(float x, float y, float size)
Créer MeteringPoint
. Représente un seul point sur MeteringPoint
la caméra . Surface
Il est stocké sous forme normalisée afin de pouvoir être facilement converti en coordonnées de capteur pour spécifier les zones AF/AE/AWB.
MeteringPoint
a une taille comprise entre 0
et 1
, avec une taille par défaut de 0.15f
. Lors de l'appel MeteringPointFactory.createPoint(float x, float y, float size)
, CameraX size
crée une zone rectangulaire (x, y)
centrée sur le fourni.
Le code suivant montre comment créer 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)
Si startFocusAndMetering et FocusMeteringAction
doivent être appelés startFocusAndMetering()
, l'application doit être construite FocusMeteringAction
, qui contient un ou plusieurs MeteringPoints
, ce dernier est composé de modes de mesure optionnels tels que FLAG_AF, FLAG_AE et FLAG_AWB. Le code suivant illustre cette utilisation :
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)
startFocusAndMetering()
Accepte un , comme indiqué dans le code ci-dessus FocusMeteringAction
, qui en contient un pour les zones de mesure AF/AE/AWB MeteringPoint
, et un autre pour AF et AE uniquement MeteringPoint
.
En interne, CameraX le convertira en Camera2 MeteringRectangles
et définira les CONTROL_AF_REGIONS/CONTROL_AE_REGIONS/CONTROL_AWB_REGIONS
paramètres correspondants à la demande de capture.
Étant donné que tous les appareils ne prennent pas en charge AF/AE/AWB et plusieurs zones, CameraX fait de son mieux FocusMeteringAction
. CameraX utilisera le nombre maximum pris en charge MeteringPoint
et les utilisera séquentiellement dans l'ordre dans lequel les points de mesure ont été ajoutés. MeteringPoint
CameraX ignore tous les ajouts au-delà du nombre maximum pris en charge . Par exemple, si vous fournissez 3 pour MeteringPoint
sur une plate-forme qui ne prend en charge que 2 , CameraX n'utilisera que les 2 premiers et ignorera le dernier .FocusMeteringAction
MeteringPoint
MeteringPoint
MeteringPoint
la compensation d'exposition
La compensation d'exposition est utile lorsque l'application nécessite un réglage fin de la valeur d'exposition (EV) autre que le résultat de la sortie d'exposition automatique (AE). CameraX combinera les valeurs de compensation d'exposition comme suit pour déterminer l'exposition souhaitée pour les conditions d'image actuelles :
Exposure = ExposureCompensationIndex * ExposureCompensationStep
CameraX fournit Camera.CameraControl.setExposureCompensationIndex()
une fonction pour régler la compensation d'exposition sur une valeur indexée.
Lorsque la valeur de l'index est positive, l'image sera éclaircie ; lorsque la valeur de l'index est négative, l'image sera assombrie. CameraInfo.ExposureState.exposureCompensationRange()
Les applications peuvent interroger les plages prises en charge, comme décrit dans la section suivante . setExposureCompensationIndex()
Le ListenableFuture renvoyé se termine lorsque la valeur est activée avec succès dans une demande de capture si la valeur correspondante est prise en charge, ou entraîne l' ListenableFuture
exécution immédiate du retour avec un échec si l'index spécifié est en dehors de la plage prise en charge.
CameraX ne conserve que la dernière setExposureCompensationIndex()
requête en attente. Appeler cette fonction plusieurs fois alors que la requête précédente n'a pas été exécutée entraînera l'annulation de la requête.
L'extrait de code suivant définit l'indice de compensation d'exposition et enregistre un rappel pour savoir quand une demande de modification d'exposition est exécutée :
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()
Les actuels peuvent être récupérés ExposureState
, notamment :
- Prise en charge des contrôles de compensation d'exposition.
- L'indice de correction d'exposition actuel.
- Plage d'index de compensation d'exposition.
- L'étape de compensation d'exposition utilisée pour calculer la valeur de compensation d'exposition.
Par exemple, le code suivant initialise le paramètre Exposition avec la ExposureState
valeur courante SeekBar
:
val exposureState = camera.cameraInfo.exposureState
binding.seekBar.apply {
isEnabled = exposureState.isExposureCompensationSupported
max = exposureState.exposureCompensationRange.upper
min = exposureState.exposureCompensationRange.lower
progress = exposureState.exposureCompensationIndex
}
API d'extensions CameraX
CameraX fournit une API d'extensions pour accéder aux extensions mises en œuvre par les fabricants d'appareils sur divers appareils Android. Voir Extensions de caméra pour une liste des modes d'extension pris en charge .
Pour obtenir la liste des appareils prenant en charge l'extension, consultez Appareils pris en charge .
Architecture étendue
Le schéma suivant montre l'architecture de l'extension de caméra.
Les applications CameraX peuvent utiliser des extensions via l'API CameraX Extensions. L'API CameraX Extensions peut être utilisée pour gérer les requêtes pour les extensions disponibles, configurer les sessions de caméra d'extension et communiquer avec les bibliothèques OEM d'extensions de caméra. Cela permet à votre application d'utiliser des fonctionnalités telles que Nuit, HDR, Auto, Bokeh ou Face Photo Fix.
dépendances
L'API CameraX Extensions est camera-extensions
implémentée dans la bibliothèque. Ces extensions dépendent des modules principaux de 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}"
...
}
Une extension qui permet la capture et la prévisualisation d'images
Avant d'utiliser l'API Extensions, utilisez ExtensionsManager#getInstanceAsync(Context, CameraProvider)
la méthode pour récupérer ExtensionsManager
une instance. De cette façon, vous pouvez demander des informations sur la disponibilité étendue. Récupérez ensuite les extensions activées CameraSelector
. Si la méthode est appelée CameraSelector
avec l'extension enabled bindToLifecycle()
, le mode étendu sera appliqué pour les cas d'utilisation de capture d'image et de prévisualisation.
Remarque : Avec les extensions activées sur
ImageCapture
etPreview
, si vous utilisez etImageCapture
comme arguments de , vous pouvez être limité dans le nombre de caméras que vous pouvez choisir. Une exception est levée si aucune caméra prenant en charge l'extension n'est trouvée .Preview
bindToLifecycle()
ExtensionsManager#getExtensionEnabledCameraSelector()
Pour une extension permettant d'implémenter le cas d'utilisation de capture et d'aperçu d'image, consultez l'exemple de code suivant :
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))
}
Désactiver l'extension
Pour désactiver les extensions de fournisseur, dissociez tous les cas d'utilisation, puis reliez la capture d'image et prévisualisez les cas d'utilisation à l'aide du sélecteur de caméra habituel. Par exemple, utilisez pour CameraSelector.DEFAULT_BACK_CAMERA
relier à la caméra arrière.
Résumé de la comparaison entre CameraX et Camera1
Bien que le code de CameraX et Camera1 puisse sembler différent, les concepts de base de CameraX et Camera1 sont très similaires. Dans CameraX, diverses tâches de fonction de caméra courantes : Preview
, ImageCapture
, VideoCapture
et ImageAnalysis
sont toutes résumées en tant que cas d'utilisation UseCase , de sorte que de nombreuses tâches laissées aux développeurs dans Camera1 peuvent être automatiquement gérées par CameraX.
Un exemple de la façon dont CameraX gère les détails de bas niveau pour le développeur est UseCase
partagé entre valid ViewPort
. Cela garantit que tous UseCase
les pixels vus sont exactement les mêmes. Dans Camera1, vous devrez gérer ces détails vous-même, et étant donné que les appareils ont des rapports d'aspect différents pour chaque capteur et écran d'appareil photo, il peut être difficile de s'assurer que l'aperçu correspond aux photos et vidéos capturées.
Comme autre exemple, CameraX gère automatiquement les rappels Lifecycle pour l'instance Lifecycle que vous lui transmettez. Cela signifie que CameraX gère la connexion de votre application à l'appareil photo tout au long du cycle de vie de l'activité Android, y compris dans les cas suivants : fermeture de l'appareil photo après que votre application passe en arrière-plan, suppression de l'aperçu de l'appareil photo lorsque l'écran n'en a plus besoin et mise en pause de l'appareil photo. aperçu tandis que d'autres activités (telles qu'une invitation à un appel vidéo) sont prioritaires au premier plan.
De plus, CameraX gère la rotation et la mise à l'échelle sans que vous ayez à faire de code supplémentaire. Si l'orientation de l'activité n'est pas verrouillée, le système la définira à chaque rotation de l'appareil, UseCase
car le système détruit et recrée l'activité lorsque l'orientation de l'écran change. De cette façon, les cas d'utilisation utiliseront par défaut leur rotation cible à chaque fois pour correspondre à l'orientation de l'écran.
Référence : https://developer.android.google.cn/training/camerax?hl=zh-cn