Hướng dẫn

Tạo hiệu ứng làm nổi bật bằng CameraX và Jetpack Compose

Đọc trong 8 phút
Jolanda Verhoef
Kỹ sư Quan hệ với nhà phát triển

Chào bạn! Chào mừng bạn quay lại với loạt bài viết của chúng tôi về CameraX và Jetpack Compose. Trong các bài đăng trước, chúng ta đã tìm hiểu những kiến thức cơ bản về cách thiết lập bản xem trước camera và thêm chức năng nhấn để lấy nét.

🧱 Phần 1: Xây dựng bản xem trước cơ bản của camera bằng cấu phần camera-compose mới. Chúng ta đã đề cập đến việc xử lý quyền và tích hợp cơ bản.

👆 Phần 2: Sử dụng hệ thống cử chỉ, đồ hoạ và các coroutine của Compose để triển khai tính năng nhấn để lấy nét trực quan.

🔦 Phần 3 (bài đăng này): Khám phá cách phủ các phần tử giao diện người dùng Compose lên trên bản xem trước camera để mang lại trải nghiệm phong phú hơn cho người dùng.

📂 Phần 4: Sử dụng các API thích ứng và khung ảnh động Compose để tạo ảnh động mượt mà khi chuyển đổi sang và từ chế độ mặt bàn trên điện thoại có thể gập lại.

Trong bài đăng này, chúng ta sẽ tìm hiểu một điều gì đó hấp dẫn hơn về mặt thị giác: triển khai hiệu ứng làm nổi bật trên bản xem trước camera, sử dụng tính năng nhận diện khuôn mặt làm cơ sở cho hiệu ứng này. Bạn hỏi vì sao ư? Tôi không chắc. Nhưng chắc chắn là nó trông rất thú vị 🙂. Và quan trọng hơn, nó minh hoạ cách chúng ta có thể dễ dàng chuyển đổi toạ độ cảm biến thành toạ độ giao diện người dùng, cho phép chúng ta sử dụng các toạ độ đó trong Compose!

face-detection.gif

Bật tính năng phát hiện khuôn mặt

Trước tiên, hãy sửa đổi CameraPreviewViewModel để bật tính năng phát hiện khuôn mặt. Chúng ta sẽ sử dụng API Camera2Interop. API này cho phép chúng ta tương tác với Camera2 API cơ bản từ CameraX. Điều này cho phép chúng ta sử dụng các tính năng của camera mà CameraX không trực tiếp hiển thị. Chúng ta cần thực hiện những thay đổi sau:

  • Tạo một StateFlow chứa ranh giới khuôn mặt dưới dạng danh sách Rect.
  • Đặt lựa chọn yêu cầu chụp STATISTICS_FACE_DETECT_MODE thành FULL (ĐẦY ĐỦ) để bật tính năng phát hiện khuôn mặt.
  • Đặt CaptureCallback để lấy thông tin khuôn mặt từ kết quả chụp.
  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 {
    ...
}

Với những thay đổi này, mô hình chế độ xem của chúng ta hiện phát ra một danh sách các đối tượng Rect đại diện cho các hộp giới hạn của khuôn mặt được phát hiện theo toạ độ cảm biến.

Dịch toạ độ cảm biến sang toạ độ giao diện người dùng

Các hộp giới hạn của những khuôn mặt được phát hiện mà chúng ta đã lưu trữ trong phần trước sử dụng toạ độ trong hệ toạ độ cảm biến. Để vẽ các hộp giới hạn trong giao diện người dùng, chúng ta cần chuyển đổi các toạ độ này sao cho chúng chính xác trong hệ thống toạ độ Compose. Chúng ta cần:

  • Chuyển đổi toạ độ cảm biến thành toạ độ vùng đệm xem trước
  • Chuyển đổi toạ độ vùng đệm xem trước thành toạ độ giao diện người dùng Compose

Các phép biến đổi này được thực hiện bằng cách sử dụng ma trận biến đổi. Mỗi phép biến đổi có ma trận riêng:

  • SurfaceRequest của chúng ta giữ một thực thể TransformationInfo, chứa một ma trận sensorToBufferTranform.
  • CameraXViewfinder của chúng tôi có một CoordinateTransformer được liên kết. Bạn có thể nhớ rằng chúng ta đã sử dụng bộ chuyển đổi này trong bài đăng trên blog trước để chuyển đổi toạ độ nhấn để lấy tiêu điểm.

Chúng ta có thể tạo một phương thức trợ giúp có thể thực hiện việc chuyển đổi cho chúng ta:

  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
}
  • Chúng ta lặp lại danh sách các khuôn mặt được phát hiện và thực hiện quá trình biến đổi cho từng khuôn mặt.
  • CoordinateTransformer.transformMatrix mà chúng ta nhận được từ CameraXViewfinder sẽ chuyển đổi toạ độ từ giao diện người dùng sang toạ độ vùng đệm theo mặc định. Trong trường hợp này, chúng ta muốn ma trận hoạt động theo cách khác, chuyển đổi toạ độ vùng đệm thành toạ độ giao diện người dùng. Do đó, chúng ta sử dụng phương thức invert() để đảo ngược ma trận.
  • Trước tiên, chúng ta biến đổi khuôn mặt từ toạ độ cảm biến sang toạ độ vùng đệm bằng cách dùng sensorToBufferTransformMatrix, sau đó biến đổi các toạ độ vùng đệm đó sang toạ độ giao diện người dùng bằng cách dùng bufferToUiTransformMatrix.

Triển khai hiệu ứng tiêu điểm

Bây giờ, hãy cập nhật thành phần kết hợp CameraPreviewContent để vẽ hiệu ứng tiêu điểm. Chúng ta sẽ dùng thành phần kết hợp Canvas để vẽ một mặt nạ chuyển màu lên bản xem trước, giúp các khuôn mặt được phát hiện xuất hiện:

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

Dưới đây là cách hoạt động:

  • Chúng ta thu thập danh sách khuôn mặt từ mô hình chế độ xem.
  • Để đảm bảo chúng ta không kết hợp lại toàn bộ màn hình mỗi khi danh sách khuôn mặt được phát hiện thay đổi, chúng ta sẽ dùng derivedStateOf để theo dõi xem có khuôn mặt nào được phát hiện hay không. Sau đó, bạn có thể dùng AnimatedVisibility để tạo hiệu ứng ảnh động cho lớp phủ có màu xuất hiện và biến mất.
  • surfaceRequest chứa thông tin chúng ta cần để biến đổi toạ độ cảm biến thành toạ độ vùng đệm trong SurfaceRequest.TransformationInfo. Chúng ta dùng hàm produceState để thiết lập một trình nghe trong yêu cầu về vùng hiển thị và xoá trình nghe này khi thành phần kết hợp rời khỏi cây thành phần.
  • Chúng ta sử dụng Canvas để vẽ một hình chữ nhật màu hồng trong suốt bao phủ toàn bộ màn hình.
  • Chúng ta trì hoãn việc đọc biến sensorFaceRects cho đến khi ở trong khối vẽ Canvas. Sau đó, chúng ta chuyển đổi toạ độ thành toạ độ giao diện người dùng.
  • Chúng ta lặp lại các khuôn mặt được phát hiện và đối với mỗi khuôn mặt, chúng ta vẽ một chuyển màu xuyên tâm để làm cho bên trong hình chữ nhật khuôn mặt trong suốt.
  • Chúng ta sử dụng BlendMode.DstOut để đảm bảo rằng chúng ta đang cắt bỏ hiệu ứng chuyển màu khỏi hình chữ nhật màu hồng, tạo hiệu ứng tiêu điểm.

Lưu ý: Khi chuyển camera sang chế độ DEFAULT_FRONT_CAMERA, bạn sẽ nhận thấy đèn chiếu được phản chiếu! Đây là một vấn đề đã biết và được theo dõi trong Trình theo dõi sự cố của Google.

Kết quả

Với mã này, chúng ta có hiệu ứng làm nổi bật hoàn toàn chức năng, giúp làm nổi bật các khuôn mặt được phát hiện. Bạn có thể xem toàn bộ đoạn mã tại đây.

Hiệu ứng này chỉ là bước khởi đầu. Bằng cách sử dụng sức mạnh của Compose, bạn có thể tạo ra vô số trải nghiệm camera bắt mắt. Khả năng chuyển đổi toạ độ cảm biến và vùng đệm thành toạ độ giao diện người dùng Compose và ngược lại có nghĩa là chúng ta có thể tận dụng tất cả các tính năng của giao diện người dùng Compose và tích hợp chúng một cách liền mạch với hệ thống camera cơ bản. Với ảnh động, đồ hoạ giao diện người dùng nâng cao, khả năng quản lý trạng thái giao diện người dùng đơn giản và khả năng kiểm soát hoàn toàn bằng cử chỉ, giới hạn duy nhất là trí tưởng tượng của bạn!

Trong bài đăng cuối cùng của loạt bài này, chúng ta sẽ tìm hiểu cách sử dụng các API thích ứng và khung ảnh động Compose để chuyển đổi liền mạch giữa các giao diện người dùng camera trên thiết bị có thể gập lại. Hãy chú ý theo dõi!


Các đoạn mã trong blog này có giấy phép sau:

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

Xin chân thành cảm ơn Nick Butcher, Alex Vanyo, Trevor McGuire, Don Turner và Lauren Ward đã xem xét và đưa ra ý kiến phản hồi. Nhờ nỗ lực làm việc miệt mài của Yasith Vidanaarachch.

 

Tác giả:

Tiếp tục đọc