مقالات إرشادية

إنشاء تأثير تسليط الضوء باستخدام CameraX وJetpack Compose

مدّة القراءة: 8 دقائق
عرض الملف الشخصي لـ "جولاندا فيرهوف"
Jolanda Verhoef مهندسة علاقات المطوّرين

مرحبًا مرحبًا بكم من جديد في سلسلتنا حول CameraX وJetpack Compose. في المنشورَين السابقَين، تناولنا أساسيات إعداد معاينة الكاميرا وأضفنا وظيفة النقر للتركيز.

🧱 الجزء 1: إنشاء معاينة أساسية للكاميرا باستخدام العنصر الجديد camera-compose تناولنا معالجة الأذونات والتكامل الأساسي.

👆 الجزء 2: استخدام نظام الإيماءات والرسومات والروتينات الفرعية في Compose لتنفيذ النقر المرئي للتركيز

🔦 الجزء 3 (هذا المنشور): استكشاف كيفية تراكب عناصر واجهة مستخدم Compose على معاينة الكاميرا للحصول على تجربة مستخدم أفضل

📂 الجزء 4: استخدام واجهات برمجة التطبيقات التكيُّفية وإطار عمل الرسوم المتحركة في Compose للانتقال بسلاسة من وضع التثبيت على سطح مستوٍ وإليه على الهواتف القابلة للطي

في هذا المنشور، سنتناول موضوعًا أكثر جاذبية من الناحية المرئية، وهو تنفيذ تأثير تسليط الضوء على معاينة الكاميرا، باستخدام ميزة "التعرّف على الوجوه" كأساس للتأثير. قد تتساءلون عن السبب. لست متأكدًا. لكنّ هذا التأثير يبدو رائعًا 🙂. والأهم من ذلك أنّه يوضّح كيف يمكننا بسهولة ترجمة إحداثيات المستشعر إلى إحداثيات واجهة المستخدم، ما يسمح لنا باستخدامها في Compose.

face-detection.gif

تفعيل ميزة "التعرّف على الوجوه"

أولاً، لنعدّل CameraPreviewViewModel لتفعيل ميزة "التعرّف على الوجوه". سنستخدم واجهة برمجة التطبيقات Camera2Interop التي تسمح لنا بالتفاعل مع واجهة برمجة التطبيقات Camera2 الأساسية من CameraX. يتيح لنا ذلك استخدام ميزات الكاميرا التي لا تعرضها CameraX مباشرةً. علينا إجراء التغييرات التالية:

  • إنشاء StateFlow يحتوي على حدود الوجه كقائمة من Rects.
  • ضبط خيار طلب الالتقاط 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 لرسم تأثير تسليط الضوء. سنستخدم العنصر القابل للإنشاء 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 لرسم مستطيل وردي شفّاف يغطي الشاشة بأكملها.
  • نؤجّل قراءة المتغيّر sensorFaceRects إلى أن نكون داخل كتلة الرسم Canvas. ثم نحوّل الإحداثيات إلى إحداثيات واجهة المستخدم.
  • نكرّر الوجوه التي تم رصدها، ولكل وجه، نرسم تدرّجًا شعاعيًا سيجعل الجزء الداخلي من مستطيل الوجه شفّافًا.
  • نستخدم BlendMode.DstOut للتأكّد من أنّنا نقطع التدرّج من المستطيل الوردي، ما يؤدي إلى إنشاء تأثير تسليط الضوء.

ملاحظة: عند تغيير الكاميرا إلى DEFAULT_FRONT_CAMERA ستلاحظون أنّ تأثير تسليط الضوء معكوس. هذه مشكلة معروفة، ويتم تتبُّعها في أداة تتبُّع المشاكل من Google.

النتيجة

باستخدام هذا الرمز، نحصل على تأثير تسليط ضوء يعمل بكامل وظائفه ويبرز الوجوه التي تم رصدها. يمكنكم العثور على مقتطف الرمز الكامل هنا.

هذا التأثير هو مجرد بداية، فباستخدام إمكانات Compose، يمكنكم إنشاء عدد لا يحصى من تجارب الكاميرا المذهلة من الناحية المرئية. إنّ القدرة على تحويل إحداثيات المستشعر والمخزن المؤقت إلى إحداثيات واجهة مستخدم Compose والعودة إليها تعني أنّه يمكننا استخدام جميع ميزات واجهة مستخدم Compose ودمجها بسلاسة مع نظام الكاميرا الأساسي. باستخدام الرسوم المتحركة والرسومات المتقدّمة لواجهة المستخدم وإدارة حالة واجهة المستخدم البسيطة والتحكّم الكامل بالإيماءات، لا حدود لإبداعكم.

في المنشور الأخير من السلسلة، سنتناول كيفية استخدام واجهات برمجة التطبيقات التكيُّفية وإطار الرسوم المتحركة في Compose للانتقال بسلاسة بين واجهات مستخدم الكاميرا المختلفة على الأجهزة القابلة للطي. يُرجى متابعة أخبارنا باستمرار.


تخضع مقتطفات الرموز البرمجية في هذه المدونة للترخيص التالي:

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

نشكر Nick Butcher وAlex Vanyo وTrevor McGuire وDon Turner وLauren Ward على مراجعة المنشور وتقديم الملاحظات. وقد تم إنجاز هذا المنشور بفضل العمل الجاد الذي قام به Yasith Vidanaarachch.

 

من تأليف:
متابعة القراءة