Utilizzare più stream della videocamera contemporaneamente

Nota: questa pagina si riferisce al pacchetto Camera2. A meno che la tua app non richieda funzionalità specifiche di basso livello di Camera2, ti consigliamo di usare CameraX. Sia CameraX che Camera2 supportano Android 5.0 (livello API 21) e versioni successive.

Un'applicazione fotocamera può utilizzare più di un flusso di fotogrammi contemporaneamente. Nella In alcuni casi, stream diversi richiedono addirittura una risoluzione del fotogramma o un pixel differente formato. Alcuni casi d'uso tipici includono:

  • Registrazione video: uno stream per l'anteprima, un altro codificato e salvato in un file.
  • Scansione dei codici a barre: uno stream per l'anteprima, un altro per il rilevamento del codice a barre.
  • Fotografia computazionale: uno stream per l'anteprima, un altro per il volto o la scena il rilevamento automatico.

Le prestazioni hanno un costo non banale durante l'elaborazione dei frame, moltiplicati durante l'elaborazione parallela di flussi di dati o pipeline.

Risorse come CPU, GPU e DSP potrebbero sfruttare i vantaggi dalla rielaborazione del framework ma le risorse come la memoria cresceranno in modo lineare.

Più target per richiesta

Gli stream di più videocamere possono essere combinati in un unico CameraCaptureRequest Il seguente snippet di codice illustra come configurare una sessione della videocamera con uno stream per l'anteprima della fotocamera e un altro per l'elaborazione delle immagini:

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

Se configuri correttamente le piattaforme di destinazione, questo codice produrrà solo stream che soddisfano il valore f/s minimo determinato StreamComfigurationMap.GetOutputMinFrameDuration(int, Size) e StreamComfigurationMap.GetOutputStallDuration(int, Size). Le prestazioni effettive variano da dispositivo a dispositivo, sebbene Android offra garantisce il supporto di combinazioni specifiche in base a tre variabili: tipo di output, dimensione output e livello hardware.

L'utilizzo di una combinazione di variabili non supportata potrebbe funzionare con una frequenza fotogrammi bassa. se non lo fa, attiverà uno dei callback di errore. La documentazione per createCaptureSession descrive cosa garantisce il funzionamento corretto.

Tipo output

Il tipo di output fa riferimento al formato in cui vengono codificati i frame. La i valori possibili sono PRIV, YUV, JPEG e RAW. La documentazione per createCaptureSession le descrivono.

Quando scegli il tipo di output dell'applicazione, se l'obiettivo è massimizzare compatibilità, quindi usa ImageFormat.YUV_420_888 per l'analisi dei frame ImageFormat.JPEG per in formato Docker. Per gli scenari di anteprima e registrazione, è probabile che utilizzerai uno SurfaceView, TextureView, MediaRecorder, MediaCodec oppure RenderScript.Allocation. Nella in questi casi, non specificare un formato dell'immagine. Per la compatibilità, verrà considerato come ImageFormat.PRIVATE, a prescindere dal formato effettivamente utilizzato internamente. Per eseguire query sui formati supportati da un dispositivo, dato che CameraCharacteristics, utilizza il seguente codice:

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

Dimensioni output

Tutte le dimensioni di output disponibili sono elencate in base a StreamConfigurationMap.getOutputSizes(), ma solo due sono correlate alla compatibilità: PREVIEW e MAXIMUM. Le dimensioni come limiti superiori. Se qualcosa di dimensioni PREVIEW funziona, allora qualsiasi cosa con un possono essere utilizzate anche di dimensioni inferiori a PREVIEW. Lo stesso vale per MAXIMUM. La documentazione di CameraDevice spiega queste dimensioni.

Le dimensioni di output disponibili dipendono dalla scelta del formato. Dato il CameraCharacteristics e un formato, puoi eseguire query sulle dimensioni di output disponibili nel seguente modo:

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

Nei casi d'uso relativi all'anteprima della fotocamera e alla registrazione, utilizza la classe target per determinare dimensioni supportate. Il formato verrà gestito dal framework della fotocamera stessa:

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

Per ottenere la dimensione MAXIMUM, ordina le dimensioni di output per area e restituisce il valore più grande uno:

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 indica l'abbinamento ideale tra le dimensioni e la risoluzione dello schermo del dispositivo oppure per 1080p (1920 x 1080), a seconda di quale delle due opzioni è più piccola. Le proporzioni potrebbero non corrispondere esattamente le proporzioni dello schermo, quindi potrebbe essere necessario applicare ritagliare lo stream per visualizzarlo in modalità a schermo intero. Per ottenere la risposta la dimensione di anteprima, confronta le dimensioni di output disponibili con quelle di visualizzazione tenendo presente che il display può essere ruotato.

Il seguente codice definisce una classe helper, SmartSize, che renderà le dimensioni confronti un po' più semplici:

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

Controllare il livello di hardware supportato

Per determinare le funzionalità disponibili in fase di runtime, controlla l'hardware supportato livello con CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL

Con un CameraCharacteristics è possibile recuperare il livello hardware con una singola istruzione:

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

Stiamo mettendo insieme tutti i pezzi

Attraverso il tipo, le dimensioni di output e il livello hardware, è possibile combinazioni di flussi di dati sono valide. Il grafico riportato di seguito è un'istantanea configurazioni supportate da un CameraDevice con LEGACY a livello di hardware.

Obiettivo 1 Obiettivo 2 Obiettivo 3 Esempi di casi d'uso
Tipo Dimensione massima Tipo Dimensione massima Tipo Dimensione massima
PRIV MAXIMUM Anteprima semplice, elaborazione video GPU o registrazione video senza anteprima.
JPEG MAXIMUM Acquisizione di immagini fisse senza mirino.
YUV MAXIMUM Elaborazione di video/immagini all'interno dell'applicazione.
PRIV PREVIEW JPEG MAXIMUM Immagini fisse standard.
YUV PREVIEW JPEG MAXIMUM elaborazione in-app e acquisizione continua.
PRIV PREVIEW PRIV PREVIEW Registrazione standard.
PRIV PREVIEW YUV PREVIEW Anteprima più elaborazione in-app.
PRIV PREVIEW YUV PREVIEW Anteprima più elaborazione in-app.
PRIV PREVIEW YUV PREVIEW JPEG MAXIMUM Acquisizione ancora più elaborazione in-app.

LEGACY è il livello hardware più basso possibile. Questa tabella mostra che ogni in un dispositivo che supporta Camera2 (livello API 21 e superiore) può inviare fino a tre simultanei usando la giusta configurazione e nel caso in cui non dovessero esserci troppa l'overhead limita le prestazioni, come i vincoli di memoria, CPU o termiche.

L'app deve anche configurare il targeting dei buffer di output. Ad esempio, per scegliere come target un dispositivo con livello hardware LEGACY, potresti configurare due output di destinazione piattaforme, una con ImageFormat.PRIVATE e un'altra con ImageFormat.YUV_420_888. Questa è una combinazione supportata durante l'utilizzo di Dimensioni PREVIEW. Utilizzando la funzione definita in precedenza in questo argomento, per ottenere le dimensioni di anteprima richieste per un ID fotocamera richiedono il seguente codice:

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

Richiede di attendere fino a quando SurfaceView non è pronto utilizzando i callback forniti:

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

Puoi forzare l'allineamento di SurfaceView alle dimensioni di output della fotocamera chiamando SurfaceHolder.setFixedSize() oppure puoi adottare un approccio simile AutoFitSurfaceView del Common modulo dei campioni di videocamera su GitHub, che imposta una dimensione assoluta, sia le proporzioni che lo spazio disponibile, mentre automaticamente modificando quando vengono attivate le modifiche alle attività.

La configurazione dell'altra piattaforma ImageReader con il formato desiderato è più semplice, poiché non ci sono callback da attendere:

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

Se utilizzi un buffer di destinazione di blocco come ImageReader, ignora i frame dopo utilizzandole:

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

LEGACY livello hardware ha come target i dispositivi con il più basso comun denominatore. Puoi aggiungi la diramazione condizionale e utilizza la dimensione RECORD per una delle destinazioni di output piattaforme in dispositivi con livello hardware LIMITED oppure aumentalo a Dimensione MAXIMUM per i dispositivi con livello hardware FULL.