Tutoriais

Criar um efeito de destaque com o CameraX e o Jetpack Compose

Leitura de 8 minutos
Jolanda Verhoef
Engenheira de relações com desenvolvedores

Olá! É muito bom ter você de volta na nossa série sobre o CameraX e o Jetpack Compose. Nos posts anteriores, abordamos os fundamentos da configuração de uma visualização da câmera e adicionamos a funcionalidade de tocar para focar.

🧱 Parte 1:como criar uma prévia básica da câmera usando o novo artefato camera-compose. Abordamos o processamento de permissões e a integração básica.

👆 Parte 2:usar o sistema de gestos, gráficos e corrotinas do Compose para implementar um toque para focar visual.

🔦 Parte 3 (esta postagem): como sobrepor elementos da interface do Compose na visualização da câmera para uma experiência do usuário mais rica.

📂 Parte 4: como usar APIs adaptáveis e o framework de animação do Compose para animar suavemente para e do modo de mesa em smartphones dobráveis.

Nesta postagem, vamos abordar algo um pouco mais interessante visualmente: a implementação de um efeito de destaque na parte de cima da prévia da câmera, usando a detecção de rosto como base para o efeito. Por quê? Não tenho uma resposta exata. Mas fica muito legal 🙂. E, mais importante, demonstra como podemos converter coordenadas de sensores em coordenadas de interface para usá-las no Compose.

face-detection.gif

Ativar a detecção facial

Primeiro, vamos modificar o CameraPreviewViewModel para ativar a detecção de rosto. Vamos usar a API Camera2Interop, que permite interagir com a API Camera2 subjacente da CameraX. Isso nos dá a oportunidade de usar recursos da câmera que não são expostos diretamente pela CameraX. Precisamos fazer as seguintes mudanças:

  • Crie um StateFlow que contenha os limites do rosto como uma lista de Rects.
  • Defina a opção de solicitação de captura STATISTICS_FACE_DETECT_MODE como "FULL", o que ativa a detecção de rostos.
  • Defina um CaptureCallback para receber as informações do rosto do resultado da captura.
  class CameraPreviewViewModel : ViewModel() {
    ...
    private val _sensorFaceRects = MutableStateFlow(listOf<Rect>())
    val sensorFaceRects: StateFlow<List<Rect>> = _sensorFaceRects.asStateFlow()

    private val cameraPreviewUseCase = Preview.Builder()
        .apply {
            Camera2Interop.Extender(this)
                .setCaptureRequestOption(
                    CaptureRequest.STATISTICS_FACE_DETECT_MODE,
                    CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL
                )
                .setSessionCaptureCallback(object : CameraCaptureSession.CaptureCallback() {
                    override fun onCaptureCompleted(
                        session: CameraCaptureSession,
                        request: CaptureRequest,
                        result: TotalCaptureResult
                    ) {
                        super.onCaptureCompleted(session, request, result)
                        result.get(CaptureResult.STATISTICS_FACES)
                            ?.map { face -> face.bounds.toComposeRect() }
                            ?.toList()
                            ?.let { faces -> _sensorFaceRects.update { faces } }
                    }
                })
        }
        .build().apply {
    ...
}

Com essas mudanças, nosso modelo de visualização agora emite uma lista de objetos Rect que representam as caixas delimitadoras dos rostos detectados nas coordenadas do sensor.

Traduzir coordenadas do sensor para coordenadas da interface

As caixas delimitadoras dos rostos detectados que armazenamos na última seção usam coordenadas no sistema de coordenadas do sensor. Para desenhar as caixas delimitadoras na nossa interface, precisamos transformar essas coordenadas para que elas estejam corretas no sistema de coordenadas do Compose. Precisamos:

  • Transformar as coordenadas do sensor em coordenadas do buffer de prévia
  • Transformar as coordenadas do buffer de visualização em coordenadas da interface do Compose

Essas transformações são feitas usando matrizes de transformação. Cada uma das transformações tem uma matriz própria:

Podemos criar um método auxiliar que faça a transformação para nós:

  private fun List<Rect>.transformToUiCoords(
    transformationInfo: SurfaceRequest.TransformationInfo?,
    uiToBufferCoordinateTransformer: MutableCoordinateTransformer
): List<Rect> = this.map { sensorRect ->
    val bufferToUiTransformMatrix = Matrix().apply {
        setFrom(uiToBufferCoordinateTransformer.transformMatrix)
        invert()
    }

    val sensorToBufferTransformMatrix = Matrix().apply {
        transformationInfo?.let {
            setFrom(it.sensorToBufferTransform)
        }
    }

    val bufferRect = sensorToBufferTransformMatrix.map(sensorRect)
    val uiRect = bufferToUiTransformMatrix.map(bufferRect)

    uiRect
}
  • Iteramos a lista de rostos detectados e executamos a transformação para cada um deles.
  • O CoordinateTransformer.transformMatrix que recebemos do nosso CameraXViewfinder transforma as coordenadas da interface em coordenadas do buffer por padrão. No nosso caso, queremos que a matriz funcione ao contrário, transformando coordenadas de buffer em coordenadas de UI. Portanto, usamos o método invert() para inverter a matriz.
  • Primeiro, transformamos o rosto de coordenadas do sensor em coordenadas de buffer usando o sensorToBufferTransformMatrix e, em seguida, transformamos essas coordenadas de buffer em coordenadas de interface usando o bufferToUiTransformMatrix.

Implementar o efeito de destaque

Agora, vamos atualizar o elemento combinável CameraPreviewContent para desenhar o efeito de destaque. Vamos usar um elemento combinável Canvas para desenhar uma máscara de gradiente sobre a prévia, tornando os rostos detectados visíveis:

  @Composable
fun CameraPreviewContent(
    viewModel: CameraPreviewViewModel,
    modifier: Modifier = Modifier,
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
) {
    val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle()
    val sensorFaceRects by viewModel.sensorFaceRects.collectAsStateWithLifecycle()
    val transformationInfo by
        produceState<SurfaceRequest.TransformationInfo?>(null, surfaceRequest) {
            try {
                surfaceRequest?.setTransformationInfoListener(Runnable::run) { transformationInfo ->
                    value = transformationInfo
                }
                awaitCancellation()
            } finally {
                surfaceRequest?.clearTransformationInfoListener()
            }
        }
    val shouldSpotlightFaces by remember {
        derivedStateOf { sensorFaceRects.isNotEmpty() && transformationInfo != null} 
    }
    val spotlightColor = Color(0xDDE60991)
    ..

    surfaceRequest?.let { request ->
        val coordinateTransformer = remember { MutableCoordinateTransformer() }
        CameraXViewfinder(
            surfaceRequest = request,
            coordinateTransformer = coordinateTransformer,
            modifier = ..
        )

        AnimatedVisibility(shouldSpotlightFaces, enter = fadeIn(), exit = fadeOut()) {
            Canvas(Modifier.fillMaxSize()) {
                val uiFaceRects = sensorFaceRects.transformToUiCoords(
                    transformationInfo = transformationInfo,
                    uiToBufferCoordinateTransformer = coordinateTransformer
                )

                // Fill the whole space with the color
                drawRect(spotlightColor)
                // Then extract each face and make it transparent

                uiFaceRects.forEach { faceRect ->
                    drawRect(
                        Brush.radialGradient(
                            0.4f to Color.Black, 1f to Color.Transparent,
                            center = faceRect.center,
                            radius = faceRect.minDimension * 2f,
                        ),
                        blendMode = BlendMode.DstOut
                    )
                }
            }
        }
    }
}

Veja como esse processo funciona:

  • Coletamos a lista de rostos do modelo de visualização.
  • Para garantir que não vamos recompor a tela inteira sempre que a lista de rostos detectados mudar, usamos derivedStateOf para acompanhar se algum rosto foi detectado. Isso pode ser usado com AnimatedVisibility para animar a sobreposição colorida.
  • O surfaceRequest contém as informações necessárias para transformar as coordenadas do sensor em coordenadas de buffer no SurfaceRequest.TransformationInfo. Usamos a função produceState para configurar um listener na solicitação de superfície e limpar esse listener quando o elemento combinável sai da árvore de composição.
  • Usamos um Canvas para desenhar um retângulo rosa translúcido que cobre toda a tela.
  • Adiamos a leitura da variável sensorFaceRects até estarmos dentro do bloco de desenho Canvas. Em seguida, transformamos as coordenadas em coordenadas da interface.
  • Iteramos sobre os rostos detectados e, para cada um deles, desenhamos um gradiente radial que torna o interior do retângulo transparente.
  • Usamos BlendMode.DstOut para garantir que estamos cortando o gradiente do retângulo rosa, criando o efeito de destaque.

Observação: quando você muda a câmera para DEFAULT_FRONT_CAMERA, o destaque é espelhado. Esse é um problema conhecido, rastreado no Issue Tracker do Google.

Resultado

Com esse código, temos um efeito de destaque totalmente funcional que destaca os rostos detectados. Confira o snippet de código completo aqui.

Esse efeito é apenas o começo. Ao usar o poder do Compose, você pode criar inúmeras experiências de câmera visualmente impressionantes. A capacidade de transformar coordenadas de sensor e buffer em coordenadas da interface do Compose e vice-versa significa que podemos usar todos os recursos da interface do Compose e integrá-los perfeitamente ao sistema de câmera subjacente. Com animações, gráficos avançados de interface, gerenciamento simples de estado da interface e controle total de gestos, sua imaginação é o limite.

Na postagem final da série, vamos explicar como usar APIs adaptáveis e o framework de animação do Compose para fazer transições perfeitas entre diferentes interfaces de câmera em dispositivos dobráveis. Não perca!


Os snippets de código neste blog têm a seguinte licença:

// Copyright 2024 Google LLC. SPDX-License-Identifier: Apache-2.0

Agradecemos a Nick Butcher, Alex Vanyo, Trevor McGuire, Don Turner e Lauren Ward por revisarem e enviarem feedback. Possível graças ao trabalho de Yasith Vidanaarachch.

 

Escrito por:

Continuar lendo