방법

CameraX 및 Jetpack Compose로 스포트라이트 효과 만들기

전문 길이: 8분
욜란다 페르후프의 프로필 보기
Jolanda Verhoef 개발자 관계 엔지니어

안녕하세요. CameraX 및 Jetpack Compose에 관한 시리즈에 다시 오신 것을 환영합니다. 이전 게시물에서는 카메라 미리보기를 설정하는 기본사항을 다루고 탭하여 초점 맞추기 기능을 추가했습니다.

🧱 파트 1: 새로운 camera-compose 아티팩트를 사용하여 기본 카메라 미리보기 빌드 권한 처리와 기본 통합을 다뤘습니다.

👆 파트 2: Compose 동작 시스템, 그래픽, 코루틴을 사용하여 시각적 탭하여 포커스를 구현합니다.

🔦 3부 (이 게시물): 더 풍부한 사용자 환경을 위해 카메라 미리보기 위에 Compose UI 요소를 오버레이하는 방법을 살펴봅니다.

📂 4부: 적응형 API와 Compose 애니메이션 프레임워크를 사용하여 폴더블 휴대전화에서 탁자 모드로 부드럽게 전환하기

이 게시물에서는 시각적으로 좀 더 흥미로운 주제인 카메라 미리보기에 스포트라이트 효과를 구현하는 방법을 살펴보겠습니다. 얼굴 감지를 효과의 기준으로 사용합니다. 왜냐고요? 확실하지 않습니다. 하지만 멋있어 보이긴 합니다 🙂. 그리고 더 중요한 점은 센서 좌표를 UI 좌표로 쉽게 변환하여 Compose에서 사용할 수 있다는 점을 보여준다는 것입니다.

face-detection.gif

얼굴 인식 사용 설정하기

먼저 얼굴 인식을 사용 설정하도록 CameraPreviewViewModel을 수정해 보겠습니다. CameraX에서 기본 Camera2 API와 상호작용할 수 있는 Camera2Interop API를 사용합니다. 이를 통해 CameraX에서 직접 노출되지 않는 카메라 기능을 사용할 수 있습니다. 다음과 같이 변경해야 합니다.

  • 얼굴 경계를 Rect 목록으로 포함하는 StateFlow를 만듭니다.
  • 얼굴 인식을 사용 설정하는 STATISTICS_FACE_DETECT_MODE 캡처 요청 옵션을 FULL로 설정합니다.
  • 캡처 결과에서 얼굴 정보를 가져오도록 CaptureCallback를 설정합니다.
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 {
    ...
}

이러한 변경사항이 적용되면 뷰 모델은 이제 센서 좌표에서 감지된 얼굴의 경계 상자를 나타내는 Rect 객체 목록을 내보냅니다.

센서 좌표를 UI 좌표로 변환

이전 섹션에 저장된 감지된 얼굴의 경계 상자는 센서 좌표계의 좌표를 사용합니다. UI에 경계 상자를 그리려면 Compose 좌표계에서 올바르도록 이러한 좌표를 변환해야 합니다. 다음과 같은 조치가 필요합니다.

  • 센서 좌표미리보기 버퍼 좌표로 변환
  • 미리보기 버퍼 좌표Compose UI 좌표로 변환

이러한 변환은 변환 행렬을 사용하여 실행됩니다. 각 변환에는 자체 행렬이 있습니다.

변환을 실행할 수 있는 도우미 메서드를 만들 수 있습니다.

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
}
  • 감지된 얼굴 목록을 반복하고 각 얼굴에 변환을 실행합니다.
  • CameraXViewfinder에서 가져온 CoordinateTransformer.transformMatrix는 기본적으로 UI에서 버퍼 좌표로 좌표를 변환합니다. 여기서는 행렬이 반대로 작동하여 버퍼 좌표를 UI 좌표로 변환해야 합니다. 따라서 invert() 메서드를 사용하여 행렬을 반전합니다.
  • 먼저 sensorToBufferTransformMatrix를 사용하여 센서 좌표에서 버퍼 좌표로 얼굴을 변환한 다음 bufferToUiTransformMatrix를 사용하여 버퍼 좌표를 UI 좌표로 변환합니다.

스포트라이트 효과 구현

이제 스포트라이트 효과를 그릴 수 있도록 CameraPreviewContent 컴포저블을 업데이트합니다. Canvas 컴포저블을 사용하여 미리보기에 그라데이션 마스크를 그려 감지된 얼굴을 표시합니다.

@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
                    )
                }
            }
        }
    }
}

작동 방식은 다음과 같습니다.

  • 뷰 모델에서 얼굴 목록을 수집합니다.
  • 감지된 얼굴 목록이 변경될 때마다 전체 화면이 리컴포즈되지 않도록 derivedStateOf를 사용하여 얼굴이 감지되었는지 여부를 추적합니다. 그런 다음 AnimatedVisibility와 함께 사용하여 색상이 지정된 오버레이를 애니메이션으로 표시하거나 숨길 수 있습니다.
  • surfaceRequest에는 센서 좌표를 SurfaceRequest.TransformationInfo의 버퍼 좌표로 변환하는 데 필요한 정보가 포함되어 있습니다. produceState 함수를 사용하여 서피스 요청에 리스너를 설정하고 컴포저블이 컴포지션 트리를 벗어나면 이 리스너를 삭제합니다.
  • Canvas를 사용하여 화면 전체를 덮는 반투명한 분홍색 직사각형을 그립니다.
  • Canvas 그리기 블록 내에 있을 때까지 sensorFaceRects 변수의 읽기를 지연합니다. 그런 다음 좌표를 UI 좌표로 변환합니다.
  • 감지된 얼굴을 반복하고 각 얼굴에 대해 얼굴 사각형의 내부를 투명하게 만드는 방사형 그라데이션을 그립니다.
  • BlendMode.DstOut를 사용하여 분홍색 직사각형에서 그라데이션을 잘라내 스포트라이트 효과를 만듭니다.

참고: 카메라를 DEFAULT_FRONT_CAMERA로 변경하면 스포트라이트가 미러링됩니다. 이는 알려진 문제이며 Google 문제 추적기에서 추적됩니다.

결과

이 코드를 사용하면 감지된 얼굴을 강조하는 완전히 작동하는 스포트라이트 효과가 있습니다. 전체 코드 스니펫은 여기에서 확인할 수 있습니다.

이 효과는 시작에 불과합니다. Compose의 기능을 사용하면 시각적으로 멋진 다양한 카메라 환경을 만들 수 있습니다. 센서 및 버퍼 좌표를 Compose UI 좌표로 변환하고 다시 변환할 수 있다는 것은 모든 Compose UI 기능을 활용하고 기본 카메라 시스템과 원활하게 통합할 수 있다는 의미입니다. 애니메이션, 고급 UI 그래픽, 간단한 UI 상태 관리, 전체 동작 제어를 사용하면 상상하는 모든 것을 만들 수 있습니다.

이 시리즈의 마지막 게시물에서는 적응형 API와 Compose 애니메이션 프레임워크를 사용하여 폴더블 기기에서 다양한 카메라 UI 간에 원활하게 전환하는 방법을 자세히 알아봅니다. 계속해서 지켜봐 주세요.


이 블로그의 코드 스니펫에는 다음 라이선스가 적용됩니다.

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

검토와 의견을 제공해 주신 Nick Butcher, Alex Vanyo, Trevor McGuire, Don Turner, Lauren Ward에게 감사드립니다. Yasith Vidanaarachch의 노력으로 가능해졌습니다.

 

작성자:
계속 읽기