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

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

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

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

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

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

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

Несколько потоков камер можно объединить в один 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)

Ява

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);

Если вы правильно настроите целевые поверхности, этот код будет создавать только потоки, соответствующие минимальному FPS, определенному 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

Ява

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)

Ява

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)

Ява

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 }
}

Ява

 @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
}

Ява

/** 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)

Ява

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 Предварительный просмотр и обработка в приложении.
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)

Ява

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
  }
  ...
})

Ява

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 из общего модуля примеров камер на 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)

Ява

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)

Ява

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

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