วิธีการ

สร้างเอฟเฟกต์สปอตไลท์ด้วย CameraX และ Jetpack Compose

ใช้เวลาอ่าน 8 นาที
ดูโปรไฟล์ของ Jolanda Verhoef
Jolanda Verhoef วิศวกรนักพัฒนาซอฟต์แวร์สัมพันธ์

สวัสดี ยินดีต้อนรับกลับสู่ซีรีส์เกี่ยวกับ CameraX และ Jetpack Compose ในโพสต์ก่อนหน้านี้ เราได้พูดถึงพื้นฐานของการตั้งค่าการแสดงตัวอย่างกล้องและเพิ่มฟังก์ชันการทำงานแบบแตะเพื่อโฟกัส

🧱 ส่วนที่ 1: สร้างการแสดงตัวอย่างกล้องพื้นฐานโดยใช้ Artifact ใหม่ของ camera-compose เราได้พูดถึงการจัดการสิทธิ์และการผสานรวมพื้นฐาน

👆 ส่วนที่ 2: ใช้ระบบท่าทางสัมผัส กราฟิก และโครูทีนของ Compose เพื่อใช้ฟังก์ชันการทำงานแบบแตะเพื่อโฟกัสแบบภาพ

🔦 ส่วนที่ 3 (โพสต์นี้): สำรวจวิธีซ้อนทับองค์ประกอบ UI ของ Compose ไว้เหนือการแสดงตัวอย่างกล้องเพื่อประสบการณ์การใช้งานที่ดียิ่งขึ้น

📂 ส่วนที่ 4: ใช้ API แบบปรับอัตโนมัติและเฟรมเวิร์กภาพเคลื่อนไหวของ Compose เพื่อสร้างภาพเคลื่อนไหวที่ราบรื่นในการเปลี่ยนไปมาระหว่างโหมดตั้งโต๊ะในโทรศัพท์แบบพับได้

ในโพสต์นี้ เราจะมาเจาะลึกเรื่องที่น่าสนใจมากขึ้นในเชิงภาพ นั่นคือการใช้เอฟเฟกต์สปอตไลท์ไว้เหนือการแสดงตัวอย่างกล้อง โดยใช้การตรวจจับใบหน้าเป็นพื้นฐานของเอฟเฟกต์ คุณอาจสงสัยว่าทำไม ฉันไม่แน่ใจ แต่ดูเท่ดี 🙂 และที่สำคัญกว่านั้นคือแสดงให้เห็นว่าเราสามารถแปลงพิกัดเซ็นเซอร์เป็นพิกัด UI ได้อย่างง่ายดาย ซึ่งช่วยให้เราใช้พิกัดเหล่านั้นใน Compose ได้

face-detection.gif

เปิดใช้การตรวจจับใบหน้า

ก่อนอื่น เรามาแก้ไข CameraPreviewViewModel เพื่อเปิดใช้การตรวจจับใบหน้ากัน เราจะใช้ Camera2Interop API ซึ่งช่วยให้เราโต้ตอบกับ Camera2 API ที่อยู่เบื้องหลังได้จาก CameraX ซึ่งจะช่วยให้เราใช้ฟีเจอร์กล้องที่ CameraX ไม่ได้แสดงโดยตรงได้ เราต้องทำการเปลี่ยนแปลงต่อไปนี้

  • สร้าง StateFlow ที่มีขอบเขตใบหน้าเป็นรายการ Rect
  • ตั้งค่าตัวเลือกคำขอการจับภาพ 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 {
    ...
}

เมื่อทำการเปลี่ยนแปลงเหล่านี้แล้ว ตอนนี้ View Model จะแสดงรายการออบเจ็กต์ Rect ที่แสดงถึงกรอบล้อมรอบของใบหน้าที่ตรวจพบในพิกัดเซ็นเซอร์

แปลงพิกัดเซ็นเซอร์เป็นพิกัด UI

กรอบล้อมรอบของใบหน้าที่ตรวจพบซึ่งเราจัดเก็บไว้ในส่วนที่แล้วใช้พิกัดในระบบพิกัดเซ็นเซอร์ หากต้องการวาดกรอบล้อมรอบใน UI เราต้องแปลงพิกัดเหล่านี้เพื่อให้ถูกต้องในระบบพิกัดของ Compose โดยต้องทำดังนี้

  • แปลงพิกัดเซ็นเซอร์ เป็นพิกัดบัฟเฟอร์การแสดงตัวอย่าง
  • แปลงพิกัดบัฟเฟอร์การแสดงตัวอย่าง เป็นพิกัด UI ของ Compose

การแปลงเหล่านี้ทำได้โดยใช้เมทริกซ์การแปลง การแปลงแต่ละรายการจะมีเมทริกซ์ของตัวเอง ดังนี้

  • SurfaceRequest ของเราเก็บอินสแตนซ์ TransformationInfo ซึ่งมีเมทริกซ์ sensorToBufferTranform
  • CameraXViewfinder ของเรามี CoordinateTransformer ที่เชื่อมโยงอยู่ คุณอาจจำได้ว่าเราได้ใช้ Transformer นี้ในโพสต์บล็อกก่อนหน้านี้เพื่อแปลงพิกัดแบบแตะเพื่อโฟกัส

เราสามารถสร้างเมธอด Helper ที่ทำการแปลงให้เราได้ ดังนี้

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
}
  • เราจะวนซ้ำรายการใบหน้าที่ตรวจพบ และทำการแปลงสำหรับแต่ละใบหน้า
  • CoordinateTransformer.transformMatrix ที่เราได้รับจาก CameraXViewfinder จะแปลงพิกัดจาก UI เป็นพิกัดบัฟเฟอร์โดยค่าเริ่มต้น ในกรณีของเรา เราต้องการให้เมทริกซ์ทำงานในทางกลับกัน นั่นคือแปลงพิกัดบัฟเฟอร์เป็นพิกัด UI ดังนั้น เราจึงใช้เมธอด invert() เพื่อกลับเมทริกซ์
  • ก่อนอื่น เราจะแปลงใบหน้าจากพิกัดเซ็นเซอร์เป็นพิกัดบัฟเฟอร์โดยใช้ sensorToBufferTransformMatrix แล้วแปลงพิกัดบัฟเฟอร์เหล่านั้นเป็นพิกัด UI โดยใช้ bufferToUiTransformMatrix

ใช้เอฟเฟกต์สปอตไลท์

ตอนนี้ เรามาอัปเดต Composible CameraPreviewContent เพื่อวาดเอฟเฟกต์สปอตไลท์กัน เราจะใช้ Canvas Composible เพื่อวาดมาสก์การไล่ระดับสีไว้เหนือการแสดงตัวอย่าง ซึ่งจะทำให้ใบหน้าที่ตรวจพบมองเห็นได้

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

วิธีการทำงานมีดังนี้

  • เรารวบรวมรายการใบหน้าจาก View Model
  • เพื่อให้แน่ใจว่าเราจะไม่ทำการคอมโพสใหม่ทั้งหน้าจอทุกครั้งที่รายการใบหน้าที่ตรวจพบมีการเปลี่ยนแปลง เราจะใช้ derivedStateOf เพื่อติดตามว่ามีการตรวจพบใบหน้าหรือไม่ จากนั้นเราจะใช้ฟังก์ชันนี้กับ AnimatedVisibility เพื่อสร้างภาพเคลื่อนไหวให้การซ้อนทับสีเข้าและออก
  • surfaceRequest มีข้อมูลที่เราต้องใช้ในการแปลงพิกัดเซ็นเซอร์เป็นพิกัดบัฟเฟอร์ใน SurfaceRequest.TransformationInfo เราใช้ฟังก์ชัน produceState เพื่อตั้งค่า Listener ในคำขอ Surface และล้าง Listener นี้เมื่อ Composible ออกจากแผนผังการคอมโพส
  • เราใช้ Canvas เพื่อวาดสี่เหลี่ยมผืนผ้าสีชมพูโปร่งแสงที่ครอบคลุมทั้งหน้าจอ
  • เราจะเลื่อนการอ่านตัวแปร sensorFaceRects ออกไปจนกว่าจะอยู่ในบล็อกการวาด Canvas จากนั้นเราจะแปลงพิกัดเป็นพิกัด UI
  • เราจะวนซ้ำใบหน้าที่ตรวจพบ และสำหรับแต่ละใบหน้า เราจะวาดการไล่ระดับสีแบบรัศมีที่จะทำให้ด้านในของสี่เหลี่ยมผืนผ้าใบหน้าโปร่งใส
  • เราใช้ BlendMode.DstOut เพื่อให้แน่ใจว่าเราจะตัดการไล่ระดับสีออกจากสี่เหลี่ยมผืนผ้าสีชมพู ซึ่งจะสร้างเอฟเฟกต์สปอตไลท์

หมายเหตุ: เมื่อเปลี่ยนกล้องเป็น DEFAULT_FRONT_CAMERA คุณจะสังเกตเห็นว่าสปอตไลท์กลับด้าน ซึ่งเป็นปัญหาที่ทราบอยู่แล้วและมีการติดตามใน Google Issue Tracker.

ผลลัพธ์

โค้ดนี้จะสร้างเอฟเฟกต์สปอตไลท์ที่ทำงานได้อย่างสมบูรณ์ ซึ่งจะไฮไลต์ใบหน้าที่ตรวจพบ ดูข้อมูลโค้ดทั้งหมดได้ที่นี่ ที่นี่

เอฟเฟกต์นี้เป็นเพียงจุดเริ่มต้นเท่านั้น คุณสามารถสร้างประสบการณ์การใช้งานกล้องที่น่าทึ่งมากมายได้โดยใช้ขุมพลังของ Compose ความสามารถในการแปลงพิกัดเซ็นเซอร์และบัฟเฟอร์เป็นพิกัด UI ของ Compose และแปลงกลับหมายความว่าเราสามารถใช้ฟีเจอร์ UI ทั้งหมดของ Compose และผสานรวมฟีเจอร์เหล่านั้นเข้ากับระบบกล้องที่อยู่เบื้องหลังได้อย่างราบรื่น ด้วยภาพเคลื่อนไหว กราฟิก 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 ที่ทุ่มเททำงานจนเกิดเป็นบล็อกนี้

 

เขียนโดย:
อ่านต่อ