מדריכים

יצירת אפקט של זרקור באמצעות CameraX ו-Jetpack פיתוח נייטיב

משך הקריאה: 8 דקות
Jolanda Verhoef
מהנדס קשרי מפתחים

שלום! אנחנו שמחים שחזרתם לסדרה שלנו בנושא CameraX ו-Jetpack Compose. בפוסטים הקודמים הסברנו את היסודות של הגדרת תצוגה מקדימה של המצלמה והוספנו את הפונקציונליות של הקשה למיקוד.

🧱 חלק 1: יצירת תצוגה מקדימה בסיסית של המצלמה באמצעות ארטיפקט חדש של מצלמה. הסברנו איך לטפל בהרשאות ואיך לבצע שילוב בסיסי.

‫👆 חלק 2: שימוש במערכת מחוות ההלחנה, בגרפיקה ובקורוטינות להטמעה של הקשה ויזואלית למיקוד.

🔦 חלק 3 (הפוסט הזה): הסבר על שכבת-על של רכיבי ממשק משתמש של Compose על גבי התצוגה המקדימה של המצלמה, כדי לשפר את חוויית המשתמש.

📂 חלק 4: שימוש בממשקי API דינמיים ובמסגרת האנימציה של Compose כדי ליצור מעבר חלק בין מצב שולחן לבין מצב רגיל בטלפונים מתקפלים.

בפוסט הזה נתעמק בנושא מעניין יותר מבחינה ויזואלית – הטמעה של אפקט של אור זרקורים על גבי התצוגה המקדימה של המצלמה, באמצעות זיהוי פנים כבסיס לאפקט. למה, אתם שואלים? קשה לי לומר בוודאות. אבל זה נראה מגניב 🙂. וחשוב מכך, זה מדגים איך אפשר לתרגם בקלות קואורדינטות של חיישן לקואורדינטות של ממשק משתמש, וכך להשתמש בהן ב-Compose.

face-detection.gif

הפעלת זיהוי פנים

קודם נשנה את CameraPreviewViewModel כדי להפעיל זיהוי פנים. נשתמש ב-API ‏Camera2Interop, שמאפשר לנו ליצור אינטראקציה עם 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 שמייצגים את התיבות התוחמות של הפנים שזוהו בקואורדינטות של החיישן.

תרגום קואורדינטות של חיישנים לקואורדינטות של ממשק משתמש

תיבות התוחמות של הפנים שזוהו ואוחסנו בקטע הקודם משתמשות בקואורדינטות במערכת הקואורדינטות של החיישן. כדי לצייר את תיבות התוחמות בממשק המשתמש שלנו, אנחנו צריכים להמיר את הקואורדינטות האלה כך שהן יהיו נכונות במערכת הקואורדינטות של Compose. אנחנו צריכים:

  • המרת קואורדינטות החיישן לקואורדינטות של מאגר התצוגה המקדימה
  • המרת קואורדינטות של מאגר התצוגה המקדימה לקואורדינטות של ממשק משתמש מבוסס Compose

השינויים האלה מתבצעים באמצעות מטריצות טרנספורמציה. לכל אחת מהטרנספורמציות יש מטריצה משלה:

אנחנו יכולים ליצור שיטת עזר שתבצע את ההמרה בשבילנו:

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 שלנו משנה כברירת מחדל את הקואורדינטות מממשק המשתמש לקואורדינטות של מאגר הנתונים הזמני. במקרה שלנו, אנחנו רוצים שהמטריצה תפעל בכיוון ההפוך, ותמיר קואורדינטות של מאגר לקואורדינטות של ממשק משתמש. לכן אנחנו משתמשים בשיטה invert() כדי להפוך את המטריצה.
  • קודם אנחנו משנים את הקואורדינטות של הפנים מהחיישן לקואורדינטות של המאגר באמצעות sensorToBufferTransformMatrix, ואז משנים את הקואורדינטות של המאגר לקואורדינטות של ממשק המשתמש באמצעות bufferToUiTransformMatrix.

הטמעה של אפקט ההתמקדות במישהו אחד

עכשיו נעדכן את רכיב ה-CameraPreviewContent composable כדי לצייר את אפקט ההדגשה. נשתמש ב-Canvas composable כדי לצייר מסכת מעבר צבעים על התצוגה המקדימה, וכך להפוך את הפנים המזוהות לגלויות:

@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 כדי לצייר מלבן ורוד שקוף שמכסה את כל המסך.
  • אנחנו דוחים את הקריאה של המשתנה sensorFaceRects עד שאנחנו בתוך בלוק הציור Canvas. לאחר מכן אנחנו משנים את הקואורדינטות לקואורדינטות של ממשק המשתמש.
  • אנחנו מבצעים איטרציה על הפנים שזוהו, ולכל פנים אנחנו מציירים מעבר צבע רדיאלי שיהפוך את החלק הפנימי של מלבן הפנים לשקוף.
  • אנחנו משתמשים ב-BlendMode.DstOut כדי לוודא שאנחנו חותכים את הגרדיאנט מהמלבן הוורוד, וכך יוצרים את אפקט הזרקור.

הערה: כשמשנים את המצלמה ל DEFAULT_FRONT_CAMERA, תראו שההדגשה הפוכה! זו בעיה מוכרת, ואנחנו עוקבים אחריה ב Issue Tracker של Google.

תוצאה

בעזרת הקוד הזה, יש לנו אפקט של זרקור שפועל במלואו ומדגיש את הפנים שמזוהות. קטע הקוד המלא זמין כאן.

האפקט הזה הוא רק ההתחלה – באמצעות היכולות של Compose, אפשר ליצור מגוון רחב של חוויות מצלמה מדהימות מבחינה ויזואלית. היכולת להמיר קואורדינטות של חיישנים ושל מאגרים לקואורדינטות של ממשק המשתמש של Compose ובחזרה מאפשרת לנו להשתמש בכל התכונות של ממשק המשתמש של Compose ולשלב אותן בצורה חלקה עם מערכת המצלמה הבסיסית. אנימציות, גרפיקה מתקדמת בממשק המשתמש, ניהול פשוט של מצב ממשק המשתמש ושליטה מלאה במחוות – אין גבול לדמיון!

בפוסט האחרון בסדרה נסביר איך להשתמש בממשקי API אדפטיביים ובמסגרת האנימציה של פיתוח נייטיב כדי ליצור מעברים חלקים בין ממשקי משתמש שונים של מצלמות במכשירים מתקפלים. עדכונים נוספים בקרוב!


קטעי הקוד בבלוג הזה כפופים לרישיון הבא:

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

תודה רבה לניק באצ'ר, אלכס ואניו, טרוור מקגווייר, דון טרנר ולורן וורד על הבדיקה ועל המשוב. האפליקציה הזו נוצרה הודות לעבודה הקשה של Yasith Vidanaarachch.

 

נכתב על ידי:

להמשך הקריאה