Инструкции

Создайте эффект прожектора с помощью CameraX и Jetpack Compose.

8 минут чтения
Jolanda Verhoef
Инженер по связям с разработчиками

Привет! Добро пожаловать обратно в нашу серию статей о CameraX и Jetpack Compose. В предыдущих постах мы рассмотрели основы настройки предварительного просмотра камеры и добавили функцию фокусировки касанием.

🧱 Часть 1 : Создание базового предварительного просмотра камеры с использованием нового артефакта camera-compose. Мы рассмотрели обработку разрешений и базовую интеграцию.

👆 Часть 2 : Использование системы жестов Compose, графики и сопрограмм для реализации визуального эффекта фокусировки касанием.

🔦 Часть 3 (в этом посте): Рассмотрим, как наложить элементы пользовательского интерфейса Compose поверх предварительного просмотра камеры для более удобного пользовательского интерфейса.

📂 Часть 4 : Использование адаптивных API и фреймворка анимации Compose для плавной анимации перехода в настольный режим и обратно на складных телефонах.

В этом посте мы углубимся в нечто более визуально привлекательное — реализацию эффекта подсветки поверх предварительного просмотра камеры, используя распознавание лиц в качестве основы для эффекта. Зачем, спросите вы? Я не уверен. Но выглядит это, безусловно, круто 🙂. И, что более важно, это демонстрирует, как мы можем легко преобразовывать координаты сенсора в координаты пользовательского интерфейса, что позволит нам использовать их в Compose!

face-detection.gif

Включить распознавание лиц

Для начала изменим CameraPreviewViewModel, чтобы включить распознавание лиц. Мы будем использовать API Camera2Interop , который позволяет нам взаимодействовать с базовым API 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.

Эти преобразования выполняются с помощью матриц преобразования. Каждое из преобразований имеет свою собственную матрицу:

  • В нашем SurfaceRequest хранится экземпляр TransformationInfo , содержащий матрицу sensorToBufferTranform .
  • В нашем CameraXViewfinder есть связанный с ним CoordinateTransformer . Возможно, вы помните, что мы уже использовали этот преобразователь в предыдущей статье блога для преобразования координат, используемых для фокусировки касанием.

Мы можем создать вспомогательный метод, который выполнит преобразование за нас:

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 и беспрепятственно интегрировать их с базовой системой камеры. Анимация, продвинутая графика пользовательского интерфейса, простое управление состоянием пользовательского интерфейса и полное управление жестами — ваши возможности безграничны!

В заключительной статье этой серии мы подробно рассмотрим, как использовать адаптивные API и фреймворк анимации Compose для плавного перехода между различными интерфейсами камеры на складных устройствах. Следите за обновлениями!


Приведенные в этом блоге фрагменты кода распространяются под следующей лицензией:

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

Большое спасибо Нику Бутчеру , Алексу Ваньо , Тревору Макгуайру , Дону Тернеру и Лорен Уорд за рецензирование и предоставленные отзывы. Это стало возможным благодаря кропотливой работе Ясита Виданаарачча .

Автор:

Продолжить чтение