طرق التنفيذ

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

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

النتيجة

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

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

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


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

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

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

 

تأليف:

متابعة القراءة