안녕하세요. CameraX 및 Jetpack Compose에 관한 시리즈에 다시 오신 것을 환영합니다. 이전 게시물에서는 카메라 미리보기를 설정하는 기본사항을 다루고 탭하여 초점 맞추기 기능을 추가했습니다.
🧱 파트 1: 새로운 camera-compose 아티팩트를 사용하여 기본 카메라 미리보기 빌드 권한 처리와 기본 통합을 다뤘습니다.
👆 파트 2: Compose 동작 시스템, 그래픽, 코루틴을 사용하여 시각적 탭하여 포커스를 구현합니다.
🔦 3부 (이 게시물): 더 풍부한 사용자 환경을 위해 카메라 미리보기 위에 Compose UI 요소를 오버레이하는 방법을 살펴봅니다.
📂 4부: 적응형 API와 Compose 애니메이션 프레임워크를 사용하여 폴더블 휴대전화에서 탁자 모드로 부드럽게 전환하기
이 게시물에서는 시각적으로 좀 더 흥미로운 주제인 카메라 미리보기에 스포트라이트 효과를 구현하는 방법을 살펴보겠습니다. 얼굴 감지를 효과의 기준으로 사용합니다. 왜냐고요? 확실하지 않습니다. 하지만 멋있어 보이긴 합니다 🙂. 그리고 더 중요한 점은 센서 좌표를 UI 좌표로 쉽게 변환하여 Compose에서 사용할 수 있다는 점을 보여준다는 것입니다.
얼굴 인식 사용 설정하기
먼저 얼굴 인식을 사용 설정하도록 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 좌표로 변환
이러한 변환은 변환 행렬을 사용하여 실행됩니다. 각 변환에는 자체 행렬이 있습니다.
SurfaceRequest는sensorToBufferTranform행렬이 포함된TransformationInfo인스턴스를 보유합니다.CameraXViewfinder에는 연결된CoordinateTransformer이 있습니다. 이전 블로그 게시물에서 탭하여 초점 맞추기 좌표를 변환하기 위해 이 변환기를 이미 사용한 적이 있습니다.
변환을 실행할 수 있는 도우미 메서드를 만들 수 있습니다.
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의 노력으로 가능해졌습니다.
-
방법이 도움말에서는 Compose에서 waitUntil 테스트 API를 사용하여 특정 조건이 충족될 때까지 기다리는 방법을 알아봅니다.
Jose Alcérreca • 전문 길이: 3분 -
방법앱 성능은 부드러운 UI와 빠른 시작 시간과 동일시되는 경우가 많지만, 메모리는 이러한 눈에 보이는 측정항목이 구축되는 조용한 기반 역할을 합니다. 기기 메모리가 그 어느 때보다 중요해지는 추세가 나타나고 있습니다.
-
방법오늘 Google에서 발급하는 새로운 인증된 이메일 사용자 인증 정보를 발표하게 되어 기쁩니다. 이제 개발자는 Android의 인증 관리자 디지털 사용자 인증 정보 API에서 직접 이 사용자 인증 정보를 가져올 수 있습니다.
Niharika Arora, Jean-Pierre Pralle • 전문 길이: 3분
Android 개발 관련 최신 정보를 이메일로 받아 보세요.