Используйте несколько потоков камер одновременно

Примечание: Эта страница относится к пакету Camera2 . Если вашему приложению не требуются специфические низкоуровневые функции Camera2, мы рекомендуем использовать CameraX . И CameraX, и Camera2 поддерживают Android 5.0 (уровень API 21) и выше.

Приложение камеры может одновременно использовать несколько потоков кадров. В некоторых случаях для разных потоков даже требуется разное разрешение кадра или формат пикселей. Типичные примеры использования включают:

  • Видеозапись : один поток для предварительного просмотра, другой кодируется и сохраняется в файл.
  • Сканирование штрихкодов : один поток для предварительного просмотра, другой для распознавания штрихкода.
  • Вычислительная фотография : один поток для предварительного просмотра, другой для распознавания лиц/сцен.

При обработке кадров возникают существенные потери производительности, которые многократно возрастают при параллельной потоковой или конвейерной обработке.

Такие ресурсы, как ЦП, ГП и ЦОС, могут воспользоваться возможностями переобработки, предоставляемыми фреймворком, но ресурсы, такие как память, будут расти линейно.

Несколько целей на один запрос

Несколько видеопотоков с камер можно объединить в один запрос CameraCaptureRequest . Следующий фрагмент кода демонстрирует, как настроить сеанс камеры с одним потоком для предварительного просмотра изображения и другим потоком для обработки изображений:

Котлин

val session: CameraCaptureSession = ...  // from CameraCaptureSession.StateCallback

// You will use the preview capture template for the combined streams
// because it is optimized for low latency; for high-quality images, use
// TEMPLATE_STILL_CAPTURE, and for a steady frame rate use TEMPLATE_RECORD
val requestTemplate = CameraDevice.TEMPLATE_PREVIEW
val combinedRequest = session.device.createCaptureRequest(requestTemplate)

// Link the Surface targets with the combined request
combinedRequest.addTarget(previewSurface)
combinedRequest.addTarget(imReaderSurface)

// In this simple case, the SurfaceView gets updated automatically. ImageReader
// has its own callback that you have to listen to in order to retrieve the
// frames so there is no need to set up a callback for the capture request
session.setRepeatingRequest(combinedRequest.build(), null, null)

Java

CameraCaptureSession session = ;  // from CameraCaptureSession.StateCallback

// You will use the preview capture template for the combined streams
// because it is optimized for low latency; for high-quality images, use
// TEMPLATE_STILL_CAPTURE, and for a steady frame rate use TEMPLATE_RECORD
        CaptureRequest.Builder combinedRequest = session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);

// Link the Surface targets with the combined request
        combinedRequest.addTarget(previewSurface);
        combinedRequest.addTarget(imReaderSurface);

// In this simple case, the SurfaceView gets updated automatically. ImageReader
// has its own callback that you have to listen to in order to retrieve the
// frames so there is no need to set up a callback for the capture request
        session.setRepeatingRequest(combinedRequest.build(), null, null);

Если правильно настроить целевые поверхности, этот код будет создавать только потоки, соответствующие минимальному количеству кадров в секунду, определяемому функциями StreamComfigurationMap.GetOutputMinFrameDuration(int, Size) и StreamComfigurationMap.GetOutputStallDuration(int, Size) . Фактическая производительность варьируется от устройства к устройству, хотя Android предоставляет некоторые гарантии поддержки определенных комбинаций в зависимости от трех переменных: типа вывода , размера вывода и уровня оборудования .

Использование неподдерживаемой комбинации переменных может сработать при низкой частоте кадров; если этого не произойдет, сработает один из обработчиков ошибок. В документации к createCaptureSession описано, что гарантированно будет работать.

Тип вывода

Тип выходных данных указывает формат кодирования кадров. Возможные значения: PRIV, YUV, JPEG и RAW. Описание этих значений приведено в документации к createCaptureSession .

При выборе типа выходных данных для вашего приложения, если цель — максимальная совместимость, используйте ImageFormat.YUV_420_888 для анализа кадров и ImageFormat.JPEG для статичных изображений. Для сценариев предварительного просмотра и записи вы, скорее всего, будете использовать SurfaceView , TextureView , MediaRecorder , MediaCodec или RenderScript.Allocation . В этих случаях не указывайте формат изображения. Для обеспечения совместимости он будет считаться ImageFormat.PRIVATE , независимо от фактического формата, используемого внутри. Чтобы запросить поддерживаемые устройством форматы по его CameraCharacteristics , используйте следующий код:

Котлин

val characteristics: CameraCharacteristics = ...
val supportedFormats = characteristics.get(
    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).outputFormats

Java

CameraCharacteristics characteristics = ;
        int[] supportedFormats = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).getOutputFormats();

Размер выходных данных

Все доступные размеры выходных файлов перечислены в методе StreamConfigurationMap.getOutputSizes() , но только два из них связаны с совместимостью: PREVIEW и MAXIMUM . Эти размеры выступают в качестве верхних границ. Если работает файл размером PREVIEW , то будет работать и любой файл меньшего PREVIEW . То же самое относится и к MAXIMUM . Документация для CameraDevice объясняет эти размеры.

Доступные размеры выходных файлов зависят от выбранного формата. Имея CameraCharacteristics и формат, вы можете запросить доступные размеры выходных файлов следующим образом:

Котлин

val characteristics: CameraCharacteristics = ...
val outputFormat: Int = ...  // such as ImageFormat.JPEG
val sizes = characteristics.get(
    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
    .getOutputSizes(outputFormat)

Java

CameraCharacteristics characteristics = ;
        int outputFormat = ;  // such as ImageFormat.JPEG
Size[] sizes = characteristics.get(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
                .getOutputSizes(outputFormat);

В сценариях предварительного просмотра и записи с камеры используйте целевой класс для определения поддерживаемых размеров. Формат будет обрабатываться самой платформой камеры:

Котлин

val characteristics: CameraCharacteristics = ...
val targetClass: Class <T> = ...  // such as SurfaceView::class.java
val sizes = characteristics.get(
    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
    .getOutputSizes(targetClass)

Java

CameraCharacteristics characteristics = ;
   int outputFormat = ;  // such as ImageFormat.JPEG
   Size[] sizes = characteristics.get(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
                .getOutputSizes(outputFormat);

Чтобы получить MAXIMUM размер, отсортируйте размеры выходных данных по площади и верните наибольшее значение:

Котлин

fun <T>getMaximumOutputSize(
    characteristics: CameraCharacteristics, targetClass: Class <T>, format: Int? = null):
    Size {
  val config = characteristics.get(
      CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)

  // If image format is provided, use it to determine supported sizes; or else use target class
  val allSizes = if (format == null)
    config.getOutputSizes(targetClass) else config.getOutputSizes(format)
  return allSizes.maxBy { it.height * it.width }
}

Java

 @RequiresApi(api = Build.VERSION_CODES.N)
    <T> Size getMaximumOutputSize(CameraCharacteristics characteristics,
                                            Class <T> targetClass,
                                            Integer format) {
        StreamConfigurationMap config = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

        // If image format is provided, use it to determine supported sizes; else use target class
        Size[] allSizes;
        if (format == null) {
            allSizes = config.getOutputSizes(targetClass);
        } else {
            allSizes = config.getOutputSizes(format);
        }
        return Arrays.stream(allSizes).max(Comparator.comparing(s -> s.getHeight() * s.getWidth())).get();
    }

PREVIEW обозначает наилучшее соответствие размера изображения разрешению экрана устройства или разрешению 1080p (1920x1080), в зависимости от того, какое из них меньше. Соотношение сторон может не совсем точно соответствовать соотношению сторон экрана, поэтому для отображения потока в полноэкранном режиме может потребоваться обрезка или добавление черных полос сверху и снизу. Чтобы получить правильный размер предварительного просмотра, сравните доступные размеры вывода с размером экрана, учитывая при этом возможность поворота экрана.

Приведённый ниже код определяет вспомогательный класс SmartSize , который немного упростит сравнение размеров:

Котлин

/** Helper class used to pre-compute shortest and longest sides of a [Size] */
class SmartSize(width: Int, height: Int) {
    var size = Size(width, height)
    var long = max(size.width, size.height)
    var short = min(size.width, size.height)
    override fun toString() = "SmartSize(${long}x${short})"
}

/** Standard High Definition size for pictures and video */
val SIZE_1080P: SmartSize = SmartSize(1920, 1080)

/** Returns a [SmartSize] object for the given [Display] */
fun getDisplaySmartSize(display: Display): SmartSize {
    val outPoint = Point()
    display.getRealSize(outPoint)
    return SmartSize(outPoint.x, outPoint.y)
}

/**
 * Returns the largest available PREVIEW size. For more information, see:
 * https://d.android.com/reference/android/hardware/camera2/CameraDevice
 */
fun <T>getPreviewOutputSize(
        display: Display,
        characteristics: CameraCharacteristics,
        targetClass: Class <T>,
        format: Int? = null
): Size {

    // Find which is smaller: screen or 1080p
    val screenSize = getDisplaySmartSize(display)
    val hdScreen = screenSize.long >= SIZE_1080P.long || screenSize.short >= SIZE_1080P.short
    val maxSize = if (hdScreen) SIZE_1080P else screenSize

    // If image format is provided, use it to determine supported sizes; else use target class
    val config = characteristics.get(
            CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
    if (format == null)
        assert(StreamConfigurationMap.isOutputSupportedFor(targetClass))
    else
        assert(config.isOutputSupportedFor(format))
    val allSizes = if (format == null)
        config.getOutputSizes(targetClass) else config.getOutputSizes(format)

    // Get available sizes and sort them by area from largest to smallest
    val validSizes = allSizes
            .sortedWith(compareBy { it.height * it.width })
            .map { SmartSize(it.width, it.height) }.reversed()

    // Then, get the largest output size that is smaller or equal than our max size
    return validSizes.first { it.long <= maxSize.long && it.short <= maxSize.short }.size
}

Java

/** Helper class used to pre-compute shortest and longest sides of a [Size] */
    class SmartSize {
        Size size;
        double longSize;
        double shortSize;

        public SmartSize(Integer width, Integer height) {
            size = new Size(width, height);
            longSize = max(size.getWidth(), size.getHeight());
            shortSize = min(size.getWidth(), size.getHeight());
        }

        @Override
        public String toString() {
            return String.format("SmartSize(%sx%s)", longSize, shortSize);
        }
    }

    /** Standard High Definition size for pictures and video */
    SmartSize SIZE_1080P = new SmartSize(1920, 1080);

    /** Returns a [SmartSize] object for the given [Display] */
    SmartSize getDisplaySmartSize(Display display) {
        Point outPoint = new Point();
        display.getRealSize(outPoint);
        return new SmartSize(outPoint.x, outPoint.y);
    }

    /**
     * Returns the largest available PREVIEW size. For more information, see:
     * https://d.android.com/reference/android/hardware/camera2/CameraDevice
     */
    @RequiresApi(api = Build.VERSION_CODES.N)
    <T> Size getPreviewOutputSize(
            Display display,
            CameraCharacteristics characteristics,
            Class <T> targetClass,
            Integer format
    ){

        // Find which is smaller: screen or 1080p
        SmartSize screenSize = getDisplaySmartSize(display);
        boolean hdScreen = screenSize.longSize >= SIZE_1080P.longSize || screenSize.shortSize >= SIZE_1080P.shortSize;
        SmartSize maxSize;
        if (hdScreen) {
            maxSize = SIZE_1080P;
        } else {
            maxSize = screenSize;
        }

        // If image format is provided, use it to determine supported sizes; else use target class
        StreamConfigurationMap config = characteristics.get(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
        if (format == null)
            assert(StreamConfigurationMap.isOutputSupportedFor(targetClass));
        else
            assert(config.isOutputSupportedFor(format));
        Size[] allSizes;
        if (format == null) {
            allSizes = config.getOutputSizes(targetClass);
        } else {
            allSizes = config.getOutputSizes(format);
        }

        // Get available sizes and sort them by area from largest to smallest
        List <Size> sortedSizes = Arrays.asList(allSizes);
        List <SmartSize> validSizes =
                sortedSizes.stream()
                        .sorted(Comparator.comparing(s -> s.getHeight() * s.getWidth()))
                        .map(s -> new SmartSize(s.getWidth(), s.getHeight()))
                        .sorted(Collections.reverseOrder()).collect(Collectors.toList());

        // Then, get the largest output size that is smaller or equal than our max size
        return validSizes.stream()
                .filter(s -> s.longSize <= maxSize.longSize && s.shortSize <= maxSize.shortSize)
                .findFirst().get().size;
    }

Проверьте поддерживаемый уровень оборудования.

Чтобы определить доступные возможности во время выполнения, проверьте поддерживаемый уровень оборудования с помощью CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL .

С помощью объекта CameraCharacteristics можно получить информацию об аппаратном уровне всего одним оператором:

Котлин

val characteristics: CameraCharacteristics = ...

// Hardware level will be one of:
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
val hardwareLevel = characteristics.get(
        CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)

Java

CameraCharacteristics characteristics = ...;

// Hardware level will be one of:
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
Integer hardwareLevel = characteristics.get(
                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);

Собирая все кусочки воедино

С помощью типа выходного сигнала, размера выходного сигнала и уровня оборудования можно определить, какие комбинации потоков являются допустимыми. На следующей диаграмме представлен снимок конфигураций, поддерживаемых устройством CameraDevice с аппаратным уровнем LEGACY .

Цель 1 Цель 2 Цель 3 Пример(ы) варианта(ов) использования
Тип Максимальный размер Тип Максимальный размер Тип Максимальный размер
PRIV MAXIMUM Простой предварительный просмотр, обработка видео на графическом процессоре или запись видео без предварительного просмотра.
JPEG MAXIMUM Фотосъемка без видоискателя.
YUV MAXIMUM Обработка видео/изображений непосредственно в приложении.
PRIV PREVIEW JPEG MAXIMUM Стандартная фотосъемка.
YUV PREVIEW JPEG MAXIMUM Обработка изображений в приложении плюс фотосъемка.
PRIV PREVIEW PRIV PREVIEW Стандартная запись.
PRIV PREVIEW YUV PREVIEW Предварительный просмотр плюс обработка в приложении.
PRIV PREVIEW YUV PREVIEW JPEG MAXIMUM Фотосъемка плюс обработка в приложении.

LEGACY — это самый низкий возможный уровень аппаратного обеспечения. В этой таблице показано, что каждое устройство, поддерживающее Camera2 (уровень API 21 и выше), может выводить до трех одновременных потоков при правильной конфигурации и при отсутствии слишком больших накладных расходов, ограничивающих производительность, таких как память, процессор или тепловые ограничения.

Вашему приложению также необходимо настроить целевые выходные буферы. Например, для устройства с аппаратным уровнем LEGACY можно настроить две целевые выходные поверхности: одну с использованием ImageFormat.PRIVATE , а другую — с использованием ImageFormat.YUV_420_888 . Это поддерживаемая комбинация при использовании размера PREVIEW . Для получения необходимых размеров предварительного просмотра для идентификатора камеры с помощью функции, описанной ранее в этом разделе, потребуется следующий код:

Котлин

val characteristics: CameraCharacteristics = ...
val context = this as Context  // assuming you are inside of an activity

val surfaceViewSize = getPreviewOutputSize(
    context, characteristics, SurfaceView::class.java)
val imageReaderSize = getPreviewOutputSize(
    context, characteristics, ImageReader::class.java, format = ImageFormat.YUV_420_888)

Java

CameraCharacteristics characteristics = ...;
        Context context = this; // assuming you are inside of an activity

        Size surfaceViewSize = getPreviewOutputSize(
                context, characteristics, SurfaceView.class);
        Size imageReaderSize = getPreviewOutputSize(
                context, characteristics, ImageReader.class, format = ImageFormat.YUV_420_888);

Для этого необходимо дождаться готовности SurfaceView , используя предоставленные функции обратного вызова:

Котлин

val surfaceView = findViewById <SurfaceView>(...)
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
  override fun surfaceCreated(holder: SurfaceHolder) {
    // You do not need to specify image format, and it will be considered of type PRIV
    // Surface is now ready and you could use it as an output target for CameraSession
  }
  ...
})

Java

SurfaceView surfaceView = findViewById <SurfaceView>(...);

surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
                // You do not need to specify image format, and it will be considered of type PRIV
                // Surface is now ready and you could use it as an output target for CameraSession
            }
            ...
        });

Вы можете принудительно установить SurfaceView в соответствии с размером выходного изображения камеры, вызвав метод SurfaceHolder.setFixedSize() , или же использовать подход, аналогичный AutoFitSurfaceView из модуля Common в примерах работы с камерой на GitHub, который устанавливает абсолютный размер, учитывая как соотношение сторон, так и доступное пространство, и автоматически корректирует его при изменении активности.

Настройка другой поверхности из ImageReader с нужным форматом проще, поскольку не нужно ждать обратных вызовов:

Котлин

val frameBufferCount = 3  // just an example, depends on your usage of ImageReader
val imageReader = ImageReader.newInstance(
    imageReaderSize.width, imageReaderSize.height, ImageFormat.YUV_420_888,
    frameBufferCount)

Java

int frameBufferCount = 3;  // just an example, depends on your usage of ImageReader
ImageReader imageReader = ImageReader.newInstance(
                imageReaderSize.width, imageReaderSize.height, ImageFormat.YUV_420_888,
                frameBufferCount);

При использовании блокирующего целевого буфера, такого как ImageReader , кадры следует отбрасывать после их использования:

Котлин

imageReader.setOnImageAvailableListener({
  val frame =  it.acquireNextImage()
  // Do something with "frame" here
  it.close()
}, null)

Java

imageReader.setOnImageAvailableListener(listener -> {
            Image frame = listener.acquireNextImage();
            // Do something with "frame" here
            listener.close();
        }, null);

Аппаратный уровень LEGACY ориентирован на устройства с наименьшим общим знаменателем. Вы можете добавить условное ветвление и использовать размер RECORD для одной из целевых поверхностей вывода в устройствах с аппаратным уровнем LIMITED , или даже увеличить его до MAXIMUM размера для устройств с аппаратным уровнем FULL .