相機擷取工作階段和要求

注意:本頁面是指 Camera2 套件。除非應用程式需要 Camera2 的特定低階功能,否則建議使用 CameraX。CameraX 和 Camera2 均支援 Android 5.0 (API 級別 21) 以上版本。

一部 Android 裝置可以搭載多部相機。每個攝影機都是 CameraDevice,而 CameraDevice 可以同時輸出多個串流。

這麼做的其中一個原因是,從 CameraDevice 產生的一個串流依序鏡頭畫面已最佳化處理特定工作 (例如顯示觀景窗),其他則可用於拍照或錄製影片。串流可做為平行管道,處理相機傳入的原始影格 (一次一個影格):

圖 1 插圖:打造通用相機應用程式 (2018 年 Google I/O 大會)

平行處理功能表示效能上限取決於 CPU、GPU 或其他處理器的可用處理能力。如果管道無法跟上收到的影格,便會開始捨棄這些影格。

每個管道都有自己的輸出格式。傳入的原始資料會透過與每個管道相關聯的隱含邏輯,自動轉換為適當的輸出格式。本頁程式碼範例中使用的 CameraDevice 並非特定類型,因此請先列舉所有可用相機,再繼續操作。

您可以使用 CameraDevice 建立該 CameraDevice 專屬的 CameraCaptureSessionCameraDevice 必須使用 CameraCaptureSession 接收每個原始影格的影格設定。這個設定會指定相機屬性,例如自動對焦、光圈、效果和曝光。由於硬體限制,鏡頭感應器中每次只會啟用一項設定,這稱為「主動」設定。

不過,串流用途可以強化並擴充先前使用 CameraDevice 串流擷取工作階段的方式,這樣可讓您根據特定用途,將相機串流調整至最佳狀態。舉例來說,可在最佳化視訊通話時延長電池續航力。

CameraCaptureSession 說明所有可能繫結至 CameraDevice 的管道。工作階段建立後,您就無法新增或移除管道。CameraCaptureSession 會保留 CaptureRequest 佇列,該佇列將成為啟用中的設定。

CaptureRequest 會將設定新增至佇列,然後選取一個、多個或所有可用管道,以便從 CameraDevice 接收影格。您可以在擷取工作階段的生命週期內傳送許多擷取要求。每項要求都會變更目前啟用的設定,以及接收原始映像檔的輸出管道組合。

使用串流用途提升效能

串流用途是改善 Camera2 擷取工作階段效能的方法。這類元件可讓硬體裝置取得更多資訊來調整參數,為特定工作提供更佳的相機體驗。

如此一來,相機裝置就能根據每個串流的使用者情境,對相機硬體和軟體管道進行最佳化調整。如要進一步瞭解串流用途,請參閱 setStreamUseCase

除了在 CameraDevice.createCaptureRequest() 中設定範本外,串流用途也能讓您指定特定相機串流的使用方式更加詳盡。如此一來,相機硬體就能根據適合特定用途的品質或延遲取捨,將參數最佳化,例如調整、感應器模式或相機感應器設定。

串流用途包括:

  • DEFAULT:涵蓋所有現有的應用程式行為。這相當於不要設定任何串流用途。

  • PREVIEW:建議用於觀景窗或應用程式內圖片分析。

  • STILL_CAPTURE:針對高畫質的擷取進行最佳化,且預期不會維持類似預覽的影格速率。

  • VIDEO_RECORD:針對高畫質影片擷取進行最佳化,包括高畫質圖片防震功能 (如果裝置支援這項功能且應用程式已啟用的話)。這個選項可能會產生即時延遲顯著的輸出影格,以便提供最高畫質的穩定或其他處理作業。

  • VIDEO_CALL:建議用於長時間執行的相機,如果耗電量為問題,則建議使用。

  • PREVIEW_VIDEO_STILL:建議用於社群媒體應用程式或單一串流用途。這是多功能串流。

  • VENDOR_START:適用於 OEM 定義的用途。

建立 CameraCaptureSession

如要建立相機工作階段,請提供一或多個輸出緩衝區,應用程式可以寫入輸出影格。每個緩衝區都代表一個管線。您必須先完成此操作再開始使用相機,這樣架構才能設定裝置的內部管道,並分配記憶體緩衝區,以便將畫面傳送至所需的輸出目標。

下列程式碼片段展示如何準備一個相機工作階段,其中包含兩個輸出緩衝區,一個屬於 SurfaceView,另一個屬於 ImageReader。將 PREVIEW 串流用途新增至 previewSurface,並將 STILL_CAPTURE 串流用途新增至 imReaderSurface,可讓裝置硬體進一步最佳化這些串流。

Kotlin


// Retrieve the target surfaces, which might be coming from a number of places:
// 1. SurfaceView, if you want to display the image directly to the user
// 2. ImageReader, if you want to read each frame or perform frame-by-frame
// analysis
// 3. OpenGL Texture or TextureView, although discouraged for maintainability
      reasons
// 4. RenderScript.Allocation, if you want to do parallel processing
val surfaceView = findViewById<SurfaceView>(...)
val imageReader = ImageReader.newInstance(...)

// Remember to call this only *after* SurfaceHolder.Callback.surfaceCreated()
val previewSurface = surfaceView.holder.surface
val imReaderSurface = imageReader.surface
val targets = listOf(previewSurface, imReaderSurface)

// Create a capture session using the predefined targets; this also involves
// defining the session state callback to be notified of when the session is
// ready
// Setup Stream Use Case while setting up your Output Configuration.
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
fun configureSession(device: CameraDevice, targets: List<Surface>){
    val configs = mutableListOf<OutputConfiguration>()
    val streamUseCase = CameraMetadata
        .SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL

    targets.forEach {
        val config = OutputConfiguration(it)
        config.streamUseCase = streamUseCase.toLong()
        configs.add(config)
    }
    ...
    device.createCaptureSession(session)
}

Java


// Retrieve the target surfaces, which might be coming from a number of places:
// 1. SurfaceView, if you want to display the image directly to the user
// 2. ImageReader, if you want to read each frame or perform frame-by-frame
      analysis
// 3. RenderScript.Allocation, if you want to do parallel processing
// 4. OpenGL Texture or TextureView, although discouraged for maintainability
      reasons
Surface surfaceView = findViewById<SurfaceView>(...);
ImageReader imageReader = ImageReader.newInstance(...);

// Remember to call this only *after* SurfaceHolder.Callback.surfaceCreated()
Surface previewSurface = surfaceView.getHolder().getSurface();
Surface imageSurface = imageReader.getSurface();
List<Surface> targets = Arrays.asList(previewSurface, imageSurface);

// Create a capture session using the predefined targets; this also involves defining the
// session state callback to be notified of when the session is ready
private void configureSession(CameraDevice device, List<Surface> targets){
    ArrayList<OutputConfiguration> configs= new ArrayList()
    String streamUseCase=  CameraMetadata
        .SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL

    for(Surface s : targets){
        OutputConfiguration config = new OutputConfiguration(s)
        config.setStreamUseCase(String.toLong(streamUseCase))
        configs.add(config)
}

device.createCaptureSession(session)
}

此時,您尚未定義相機的啟用設定。設定工作階段後,您可以建立並分派擷取要求來進行這項工作。

將輸入內容寫入緩衝區時套用的轉換取決於每個目標的類型 (必須是 Surface)。Android 架構知道如何將使用中的原始圖片轉換成適合每個目標的格式。轉換作業由特定 Surface 的像素格式和大小控制。

這個架構會嘗試發揮最佳效能,但部分 Surface 設定組合可能無法正常運作,導致系統無法建立工作階段、分派要求時擲回執行階段錯誤,或是效能降低。該架構可以保證裝置、介面和要求參數的特定組合。createCaptureSession() 的說明文件會提供更多資訊。

單一擷取要求

每個影格使用的設定都會以 CaptureRequest 編碼,然後再傳送至相機。如要建立擷取要求,您可以使用其中一個預先定義的範本,也可以使用 TEMPLATE_MANUAL 進行完整的控制。選擇範本時,您必須提供一或多個輸出緩衝區,以便與要求搭配使用。您只能使用已在要使用的擷取工作階段中定義的緩衝區。

使用建構工具模式擷取要求,可讓開發人員設定多種不同選項,包括自動探索自動對焦鏡頭。設定欄位前,請先呼叫 CameraCharacteristics.getAvailableCaptureRequestKeys(),確認裝置可使用特定選項,並檢查適當的相機特性 (例如可用的自動曝光模式),確認支援所需值。

如要使用專為預覽設計的範本建立 SurfaceView 的擷取要求,且無須進行任何修改,請使用 CameraDevice.TEMPLATE_PREVIEW

Kotlin

val session: CameraCaptureSession = ...  // from CameraCaptureSession.StateCallback
val captureRequest = session.device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
captureRequest.addTarget(previewSurface)

Java

CameraCaptureSession session = ...;  // from CameraCaptureSession.StateCallback
CaptureRequest.Builder captureRequest =
    session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
captureRequest.addTarget(previewSurface);

定義擷取要求後,您現在可以將該要求分派至相機工作階段:

Kotlin

val session: CameraCaptureSession = ...  // from CameraCaptureSession.StateCallback
val captureRequest: CaptureRequest = ...  // from CameraDevice.createCaptureRequest()

// The first null argument corresponds to the capture callback, which you
// provide if you want to retrieve frame metadata or keep track of failed capture
// requests that can indicate dropped frames; the second null argument
// corresponds to the Handler used by the asynchronous callback, which falls
// back to the current thread's looper if null
session.capture(captureRequest.build(), null, null)

Java

CameraCaptureSession session = ...;  // from CameraCaptureSession.StateCallback
CaptureRequest captureRequest = ...;  // from CameraDevice.createCaptureRequest()

// The first null argument corresponds to the capture callback, which you
// provide if you want to retrieve frame metadata or keep track of failed
// capture
// requests that can indicate dropped frames; the second null argument
// corresponds to the Handler used by the asynchronous callback, which falls
// back to the current thread's looper if null
session.capture(captureRequest.build(), null, null);

當輸出影格放入特定緩衝區時,就會觸發擷取回呼。在許多情況下,處理所含影格時就會觸發其他回呼 (例如 ImageReader.OnImageAvailableListener)。這時,您可以從指定的緩衝區中擷取圖片資料。

重複 CaptureRequests

單一相機要求非常簡單,但如要顯示即時預覽或影片,則不實用。在這種情況下,您必須接收連續的影格串流,而非只有單一串流。下列程式碼片段說明如何為工作階段新增重複要求

Kotlin

val session: CameraCaptureSession = ...  // from CameraCaptureSession.StateCallback
val captureRequest: CaptureRequest = ...  // from CameraDevice.createCaptureRequest()

// This keeps sending the capture request as frequently as possible until
// the
// session is torn down or session.stopRepeating() is called
// session.setRepeatingRequest(captureRequest.build(), null, null)

Java

CameraCaptureSession session = ...;  // from CameraCaptureSession.StateCallback
CaptureRequest captureRequest = ...;  // from CameraDevice.createCaptureRequest()

// This keeps sending the capture request as frequently as possible until the
// session is torn down or session.stopRepeating() is called
// session.setRepeatingRequest(captureRequest.build(), null, null);

重複的擷取要求會讓相機裝置使用提供的 CaptureRequest 中的設定持續擷取圖片。Camera2 API 也可以讓使用者傳送重複的 CaptureRequests,藉此從相機擷取影片,如這個 GitHub 上的 Camera2 範例存放區所示。此外,這個程式庫也能使用重複播放爆發的 CaptureRequests 拍攝高速 (慢動作) 影片,如 GitHub 上的 Camera2 慢動作影片範例應用程式所示。

交錯擷取要求

如要在重複擷取要求啟用時傳送第二個擷取要求 (例如顯示觀景窗,讓使用者拍照),您不需要停止持續的重複要求。而是在週期性要求持續執行時,發出非重複的擷取要求。

首次建立工作階段時,所用的任何輸出緩衝區都必須設為相機工作階段的一部分。重複要求的優先順序低於單一影格或爆發要求,因此下列範例可以正常運作:

Kotlin

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

// Create the repeating request and dispatch it
val repeatingRequest = session.device.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW)
repeatingRequest.addTarget(previewSurface)
session.setRepeatingRequest(repeatingRequest.build(), null, null)

// Some time later...

// Create the single request and dispatch it
// NOTE: This can disrupt the ongoing repeating request momentarily
val singleRequest = session.device.createCaptureRequest(
CameraDevice.TEMPLATE_STILL_CAPTURE)
singleRequest.addTarget(imReaderSurface)
session.capture(singleRequest.build(), null, null)

Java

CameraCaptureSession session = ...;  // from CameraCaptureSession.StateCallback

// Create the repeating request and dispatch it
CaptureRequest.Builder repeatingRequest =
session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
repeatingRequest.addTarget(previewSurface);
session.setRepeatingRequest(repeatingRequest.build(), null, null);

// Some time later...

// Create the single request and dispatch it
// NOTE: This can disrupt the ongoing repeating request momentarily
CaptureRequest.Builder singleRequest =
session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
singleRequest.addTarget(imReaderSurface);
session.capture(singleRequest.build(), null, null);

不過,這種做法有一個缺點:您沒有確切知道單一要求何時發生。在下圖中,如果 A 是重複拍攝要求,而 B 是單一影格擷取要求,這就是工作階段處理要求佇列的方式:

圖 2. 插圖:進行中相機工作階段的要求佇列

系統無法保證從 A 發出的最後一個重複要求 (要求 B 啟用之前和下次再次使用 A) 之間的延遲時間,因此可能會發生某些略過的影格。如要避免這個問題,您可以採取下列做法:

  • 從要求 A 新增輸出目標,以要求 B。這樣一來,當 B 的影格準備就緒時,系統就會將該內容複製到 A 的輸出目標中。舉例來說,如果要建立影片快照來維持穩定的影格速率,就必須這麼做。在上述程式碼中,您必須在建構要求之前新增 singleRequest.addTarget(previewSurface)

  • 請搭配使用專為這個特定情境設計的範本組合,例如零延遲延遲。