ฮาวทู

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

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

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

🧱 ส่วนที่ 1: สร้างตัวอย่างกล้องพื้นฐานโดยใช้ชิ้นงาน 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 {
    ...
}

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

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

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

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

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

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

เราสามารถสร้างเมธอดตัวช่วยที่จะทำการแปลงให้เราได้

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

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

ตอนนี้เรามาอัปเดต 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
                    )
                }
            }
        }
    }
}

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

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

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

ผลลัพธ์

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

เอฟเฟกต์นี้เป็นเพียงจุดเริ่มต้นเท่านั้น คุณสามารถใช้พลังของ 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

 

เขียนโดย

อ่านต่อ