چگونه‌ها

با CameraX و Jetpack Compose جلوه‌ای نورانی ایجاد کنید

۸ دقیقه مطالعه
Jolanda Verhoef
مهندس روابط توسعه‌دهنده

سلام! به مجموعه مقالات ما در مورد CameraX و Jetpack Compose خوش آمدید. در پست‌های قبلی، اصول اولیه تنظیم پیش‌نمایش دوربین و اضافه کردن قابلیت فوکوس با لمس را پوشش دادیم.

🧱 بخش ۱ : ساخت یک پیش‌نمایش اولیه دوربین با استفاده از آرتیفکت جدید camera-compose. ما مدیریت مجوزها و یکپارچه‌سازی‌های اولیه را پوشش دادیم.

👆 بخش ۲ : استفاده از سیستم ژست نوشتن، گرافیک و کوروتین‌ها برای پیاده‌سازی قابلیت لمس برای فوکوس بصری.

🔦 بخش ۳ (این پست): بررسی نحوه‌ی همپوشانی عناصر رابط کاربری Compose روی پیش‌نمایش دوربین برای تجربه‌ی کاربری غنی‌تر.

📂 بخش ۴ : استفاده از APIهای تطبیقی ​​و چارچوب انیمیشن Compose برای انتقال روان انیمیشن به حالت رومیزی و از آن در گوشی‌های تاشو.

در این پست، به سراغ چیزی خواهیم رفت که از نظر بصری کمی جذاب‌تر است - پیاده‌سازی یک افکت نورافکن در بالای پیش‌نمایش دوربین، با استفاده از تشخیص چهره به عنوان پایه این افکت. می‌گویید چرا؟ مطمئن نیستم. اما مطمئناً جالب به نظر می‌رسد 🙂. و مهمتر از آن، نشان می‌دهد که چگونه می‌توانیم به راحتی مختصات حسگر را به مختصات رابط کاربری تبدیل کنیم و از آنها در Compose استفاده کنیم!

تشخیص چهره.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 را منتشر می‌کند که نشان‌دهنده‌ی کادرهای مرزی چهره‌های شناسایی‌شده در مختصات حسگر هستند.

تبدیل مختصات حسگر به مختصات رابط کاربری

کادرهای مرزی چهره‌های شناسایی‌شده که در بخش قبل ذخیره کردیم، از مختصات موجود در سیستم مختصات حسگر استفاده می‌کنند. برای ترسیم کادرهای مرزی در رابط کاربری خود، باید این مختصات را طوری تغییر دهیم که در سیستم مختصات Compose صحیح باشند. ما باید:

  • تبدیل مختصات حسگر به مختصات بافر پیش‌نمایش
  • مختصات بافر پیش‌نمایش را به مختصات رابط کاربری Compose تبدیل کنید

این تبدیلات با استفاده از ماتریس‌های تبدیل انجام می‌شوند. هر یک از تبدیلات، ماتریس مخصوص به خود را دارد:

  • SurfaceRequest ما یک نمونه TransformationInfo را نگه می‌دارد که شامل یک ماتریس sensorToBufferTranform است.
  • CameraXViewfinder ما یک CoordinateTransformer مرتبط دارد. شاید به یاد داشته باشید که ما قبلاً در پست وبلاگ قبلی از این مبدل برای تبدیل مختصات tap-to-focus استفاده کردیم.

ما می‌توانیم یک متد کمکی ایجاد کنیم که بتواند تبدیل را برای ما انجام دهد:

  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 چهره را از مختصات حسگر به مختصات بافر تبدیل می‌کنیم و سپس آن مختصات بافر را با استفاده از 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 برای تنظیم یک شنونده در درخواست سطحی استفاده می‌کنیم و این شنونده را هنگامی که composable از درخت ترکیب خارج می‌شود، پاک می‌کنیم.
  • ما از یک Canvas برای رسم یک مستطیل صورتی شفاف که کل صفحه را پوشش می‌دهد، استفاده می‌کنیم.
  • ما خواندن متغیر sensorFaceRects را تا زمانی که داخل بلوک رسم Canvas باشیم، به تعویق می‌اندازیم. سپس مختصات را به مختصات UI تبدیل می‌کنیم.
  • ما روی چهره‌های شناسایی‌شده تکرار می‌کنیم و برای هر چهره، یک گرادیان شعاعی رسم می‌کنیم که داخل مستطیل چهره را شفاف می‌کند.
  • ما از BlendMode.DstOut استفاده می‌کنیم تا مطمئن شویم که گرادیان را از مستطیل صورتی برش می‌دهیم و جلوه نورافکن را ایجاد می‌کنیم.

نکته: وقتی دوربین را به DEFAULT_FRONT_CAMERA تغییر می‌دهید ، متوجه خواهید شد که نورافکن منعکس می‌شود! این یک مشکل شناخته شده است که در ردیاب مشکلات گوگل ردیابی می‌شود .

نتیجه

با این کد، ما یک افکت نورافکن کاملاً کاربردی داریم که چهره‌های شناسایی‌شده را برجسته می‌کند. می‌توانید قطعه کد کامل را اینجا پیدا کنید.

این افکت تازه شروع کار است - با استفاده از قدرت Compose، می‌توانید هزاران تجربه دوربین خیره‌کننده از نظر بصری ایجاد کنید. توانایی تبدیل مختصات حسگر و بافر به مختصات رابط کاربری Compose و برعکس، به این معنی است که می‌توانیم از تمام ویژگی‌های رابط کاربری Compose استفاده کنیم و آنها را به طور یکپارچه با سیستم دوربین زیرین ادغام کنیم. با انیمیشن‌ها، گرافیک پیشرفته رابط کاربری، مدیریت ساده وضعیت رابط کاربری و کنترل کامل حرکات، تخیل شما حد و مرز دارد!

در آخرین پست این مجموعه، به نحوه استفاده از APIهای تطبیقی ​​و چارچوب انیمیشن Compose برای انتقال یکپارچه بین رابط‌های کاربری دوربین مختلف در دستگاه‌های تاشو خواهیم پرداخت. با ما همراه باشید!


قطعه کدهای موجود در این وبلاگ دارای مجوز زیر هستند:

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

با تشکر فراوان از نیک بوچر ، الکس وانیو ، ترور مک‌گوایر ، دان ترنر و لورن وارد برای بررسی و ارائه بازخورد. این امر با تلاش سخت یاسیت ویدانا آراچ امکان‌پذیر شده است.

نوشته شده توسط:

ادامه مطلب