طرق التنفيذ

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

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

النتيجة

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

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

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


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

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

نشكر نيك بوتشر وأليكس فانيو وتريفور ماكغواير ودون تيرنر و"لورين وارد" على مراجعة هذه المقالة وتقديم الملاحظات. تمّت إتاحة هذه الميزة بفضل الجهود الحثيثة التي بذلها ياسيث فيداناراشتش.

 

المؤلف:
متابعة القراءة