Mehrere Kamerastreams gleichzeitig verwenden

Hinweis:Diese Seite bezieht sich auf das Paket Camera2. Sofern Ihre App keine bestimmten Low-Level-Funktionen von Camera2 erfordert, empfehlen wir die Verwendung von CameraX. CameraX und Camera2 unterstützen Android 5.0 (API-Level 21) und höher.

In einer Kamera-App können mehrere Frames gleichzeitig verwendet werden. In einigen Fällen erfordern unterschiedliche Streams sogar eine andere Frame-Auflösung oder ein anderes Pixelformat. Einige typische Anwendungsfälle sind:

  • Videoaufzeichnung: Ein Stream wird für die Vorschau codiert und ein anderer wird codiert und in einer Datei gespeichert.
  • Barcode-Scan: ein Stream für die Vorschau, ein weiterer für die Barcodeerkennung
  • Computergestützte Fotografie: ein Stream für die Vorschau, ein anderer für die Gesichts-/Szeneerkennung

Bei der Verarbeitung von Frames entstehen nicht triviale Leistungskosten, die bei einer parallelen Stream- oder Pipelineverarbeitung multipliziert werden.

Ressourcen wie CPU, GPU und DSP können unter Umständen die Neuverarbeitungsfunktionen des Frameworks nutzen, Ressourcen wie Arbeitsspeicher wachsen jedoch linear.

Mehrere Ziele pro Anfrage

Mehrere Kamerastreams können in einem einzigen CameraCaptureRequest kombiniert werden. Das folgende Code-Snippet zeigt, wie Sie eine Kamerasitzung mit einem Stream für die Kameravorschau und einem anderen für die Bildverarbeitung einrichten:

Kotlin

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

Wenn Sie die Zieloberflächen richtig konfigurieren, erzeugt dieser Code nur Streams, die den durch StreamComfigurationMap.GetOutputMinFrameDuration(int, Size) und StreamComfigurationMap.GetOutputStallDuration(int, Size) festgelegten Mindest-fps entsprechen. Die tatsächliche Leistung variiert von Gerät zu Gerät. Android bietet allerdings einige Garantien für die Unterstützung bestimmter Kombinationen, die von drei Variablen abhängen: Ausgabetyp, Ausgabegröße und Hardwareebene.

Die Verwendung einer nicht unterstützten Kombination von Variablen funktioniert möglicherweise bei einer niedrigen Framerate. Andernfalls wird ein Fehler-Callback ausgelöst. In der Dokumentation zu createCaptureSession wird beschrieben, was garantiert funktioniert.

Ausgabetyp

Der Ausgabetyp bezieht sich auf das Format, in dem die Frames codiert sind. Mögliche Werte sind PRIV, YUV, JPEG und RAW. Eine Beschreibung dazu finden Sie in der Dokumentation zu createCaptureSession.

Wenn Sie bei der Auswahl des Ausgabetyps Ihrer Anwendung die Kompatibilität maximieren möchten, verwenden Sie ImageFormat.YUV_420_888 für die Frame-Analyse und ImageFormat.JPEG für Standbilder. Für Vorschau- und Aufzeichnungsszenarien verwenden Sie wahrscheinlich SurfaceView, TextureView, MediaRecorder, MediaCodec oder RenderScript.Allocation. Geben Sie in diesen Fällen kein Bildformat an. Aus Kompatibilitätsgründen zählt er als ImageFormat.PRIVATE, unabhängig vom tatsächlichen intern verwendeten Format. Verwenden Sie den folgenden Code, um die von einem Gerät anhand seiner CameraCharacteristics unterstützten Formate abzufragen:

Kotlin

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

Ausgabegröße

Alle verfügbaren Ausgabegrößen sind nach StreamConfigurationMap.getOutputSizes() aufgelistet, aber nur zwei sind für die Kompatibilität relevant: PREVIEW und MAXIMUM. Die Größen dienen als Obergrenzen. Wenn etwas der Größe PREVIEW funktioniert, funktioniert auch alles mit einer Größe, die kleiner als PREVIEW ist. Dasselbe gilt für MAXIMUM. Eine Erklärung zu diesen Größen finden Sie in der Dokumentation zu CameraDevice.

Die verfügbaren Ausgabegrößen hängen vom Format ab. Mit dem CameraCharacteristics und einem Format können Sie die verfügbaren Ausgabegrößen so abfragen:

Kotlin

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

Verwenden Sie in den Anwendungsfällen für die Kameravorschau und -aufzeichnung die Zielklasse, um die unterstützten Größen festzulegen. Das Format wird von der Kamera selbst gehandhabt:

Kotlin

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

Sortieren Sie die Ausgabegrößen nach Fläche und geben Sie die größte Größe zurück, um die MAXIMUM-Größe zu erhalten:

Kotlin

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 bezieht sich auf die optimale Größe für die Bildschirmauflösung des Geräts oder auf 1080p (1920 × 1080), je nachdem, was kleiner ist. Das Seitenverhältnis stimmt möglicherweise nicht genau mit dem Seitenverhältnis des Bildschirms überein. Daher müssen Sie den Stream eventuell mit Letterbox-Bild versehen oder zuschneiden, damit er im Vollbildmodus angezeigt werden kann. Um die richtige Vorschaugröße zu erhalten, vergleichen Sie die verfügbaren Ausgabegrößen mit der Anzeigegröße. Berücksichtigen Sie dabei, dass der Bildschirm gedreht werden kann.

Mit dem folgenden Code wird die Hilfsklasse SmartSize definiert, die Größenvergleiche etwas einfacher macht:

Kotlin

/** 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;
    }

Unterstützte Hardware prüfen

Prüfen Sie die unterstützte Hardwareebene mit CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, um die während der Laufzeit verfügbaren Funktionen zu ermitteln.

Mit einem CameraCharacteristics-Objekt können Sie die Hardwareebene mit einer einzigen Anweisung abrufen:

Kotlin

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

Indem alle Teile zusammengefügt werden

Mit dem Ausgabetyp, der Ausgabegröße und der Hardwareebene können Sie bestimmen, welche Kombinationen von Streams gültig sind. Das folgende Diagramm zeigt eine Übersicht der Konfigurationen, die von einem CameraDevice mit der Hardwareebene LEGACY unterstützt werden.

Ziel 1 Ziel 2 Ziel 3 Beispielanwendungsfälle
Typ Maximale Größe Typ Maximale Größe Typ Maximale Größe
PRIV MAXIMUM Einfache Vorschau, GPU-Videoverarbeitung oder Videoaufzeichnung ohne Vorschau.
JPEG MAXIMUM Standbild ohne Sucher aufnehmen.
YUV MAXIMUM Video-/Bildverarbeitung in der Anwendung
PRIV PREVIEW JPEG MAXIMUM Standard-Bildverarbeitung.
YUV PREVIEW JPEG MAXIMUM In-App-Verarbeitung und Aufnahme
PRIV PREVIEW PRIV PREVIEW Standardaufzeichnung.
PRIV PREVIEW YUV PREVIEW Vorschau und In-App-Verarbeitung.
PRIV PREVIEW YUV PREVIEW Vorschau und In-App-Verarbeitung.
PRIV PREVIEW YUV PREVIEW JPEG MAXIMUM Trotzdem Erfassung plus In-App-Verarbeitung.

LEGACY ist die niedrigste Hardwareebene. Die folgende Tabelle zeigt, dass jedes Gerät, das Camera2 (API-Level 21 und höher) unterstützt, bei der richtigen Konfiguration bis zu drei gleichzeitige Streams ausgeben kann, wenn der Aufwand nicht zu stark die Leistung beeinträchtigt, z. B. Speicher-, CPU- oder Überhitzungseinschränkungen.

Außerdem müssen in Ihrer App Ausgabezwischenspeicher für das Targeting konfiguriert werden. Für das Targeting auf ein Gerät mit der Hardwareebene LEGACY könnten Sie beispielsweise zwei Zielausgabeoberflächen einrichten, eine mit ImageFormat.PRIVATE und eine mit ImageFormat.YUV_420_888. Diese Kombination wird bei Verwendung der Größe PREVIEW unterstützt. Wenn Sie die zuvor in diesem Thema beschriebene Funktion verwenden, ist der folgende Code erforderlich, um die erforderlichen Vorschaugrößen für eine Kamera-ID abzurufen:

Kotlin

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

Es muss mit den bereitgestellten Callbacks gewartet werden, bis SurfaceView bereit ist:

Kotlin

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

Sie können erzwingen, dass SurfaceView an die Ausgabegröße der Kamera angepasst wird, indem Sie SurfaceHolder.setFixedSize() aufrufen. Sie können auch eine ähnliche Methode wie AutoFitSurfaceView aus dem Common Module der Kamerabeispiele auf GitHub verwenden, das eine absolute Größe festlegt, bei der sowohl das Seitenverhältnis als auch der verfügbare Platz berücksichtigt und automatisch angepasst wird, wenn Aktivitätsänderungen ausgelöst werden.

Das Einrichten der anderen Oberfläche aus ImageReader mit dem gewünschten Format ist einfacher, da keine Callbacks warten müssen:

Kotlin

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

Wenn Sie einen blockierenden Zielpuffer wie ImageReader verwenden, verwerfen Sie die Frames:

Kotlin

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

Die Hardwareebene „LEGACY“ ist auf Geräte mit dem kleinsten gemeinsamen Nenner ausgerichtet. Sie können eine bedingte Verzweigung hinzufügen und die Größe RECORD für eine der Ausgabezieloberflächen auf Geräten mit der Hardwareebene LIMITED verwenden. Sie können die Größe für Geräte mit der Hardwareebene FULL sogar auf die Größe MAXIMUM erhöhen.