注意:本頁面所述是指 Camera2 套件。除非應用程式需要 Camera2 的特定低階功能,否則建議使用 CameraX。CameraX 和 Camera2 均支援 Android 5.0 (API 級別 21) 以上版本。
相機應用程式可以同時使用多個影格串流。在某些情況下,不同的串流甚至需要不同的影格解析度或像素格式。常見的用途包括:
- 錄影:一個串流用於預覽,另一個串流則編碼並儲存至檔案。
- 條碼掃描:一個串流用於預覽,另一個用於條碼偵測。
- 計算攝影:一個串流用於預覽,另一個用於臉部或場景偵測。
處理影格時,會產生不小的效能成本,在進行平行串流或管道處理時,成本會倍增。
CPU、GPU 和 DSP 等資源或許能利用架構的重新處理功能,但記憶體等資源會線性成長。
每個要求有多個目標
多個攝影機串流影像可以合併為單一 CameraCaptureRequest。以下程式碼片段說明如何設定相機工作階段,其中一個串流用於相機預覽,另一個串流用於圖片處理:
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 basic 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 basic 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),請使用下列程式碼:
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();
輸出大小
所有可用的輸出大小都會列在 StreamConfigurationMap.getOutputSizes() 中,但只有兩個與相容性有關:PREVIEW 和 MAXIMUM。大小是上限。如果大小為 PREVIEW 的項目有效,則任何小於 PREVIEW 的項目也有效。MAXIMUM 也是如此。CameraDevice 的說明文件會解釋這些大小。
可用的輸出大小取決於所選格式。在指定 CameraCharacteristics 和格式的情況下,您可以查詢可用的輸出大小,如下所示:
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);
在相機預覽和錄製用途中,請使用目標類別判斷支援的大小。格式會由相機架構本身處理:
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);
如要取得 MAXIMUM 大小,請依區域排序輸出大小,並傳回最大值:
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 是指符合裝置螢幕解析度或 1080p (1920x1080) 的最佳尺寸,以較小者為準。顯示比例可能與螢幕的顯示比例不完全相符,因此您可能需要對串流套用上下黑邊或裁剪,才能以全螢幕模式顯示。如要取得正確的預覽畫面大小,請比較可用的輸出大小和螢幕大小,同時考量螢幕可能旋轉。
下列程式碼定義了輔助類別 SmartSize,可讓大小比較更輕鬆:
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;
}
查看支援的硬體層級
如要判斷執行階段可用的功能,請使用 CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL 檢查支援的硬體層級。
使用 CameraCharacteristics 物件時,您可以透過單一陳述式擷取硬體層級:
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);
整合所有元素
您可以根據輸出類型、輸出大小和硬體層級,判斷哪些串流組合有效。下圖是硬體層級為 LEGACY 的 CameraDevice 支援的設定。
| 目標 1 | 目標 2 | 目標 3 | 用途範例 | |||
|---|---|---|---|---|---|---|
| 類型 | 大小上限 | 類型 | 大小上限 | 類型 | 大小上限 | |
PRIV |
MAXIMUM |
簡單預覽、GPU 影片處理,或無預覽錄影。 | ||||
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 是最低的硬體層級。這張表格顯示,只要設定正確,且沒有過多限制效能的負擔 (例如記憶體、CPU 或散熱限制),每部支援 Camera2 (API 級別 21 以上) 的裝置最多可同時輸出三個串流。
應用程式也需要設定指定輸出緩衝區。舉例來說,如要指定硬體層級為 LEGACY 的裝置,您可以設定兩個目標輸出介面,一個使用 ImageFormat.PRIVATE,另一個使用 ImageFormat.YUV_420_888。使用 PREVIEW 大小時,系統支援這個組合。使用本文稍早定義的函式,取得相機 ID 所需的預覽大小,需要下列程式碼:
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);
您必須使用提供的回呼,等待 SurfaceView 準備就緒:
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
}
...
});
您可以呼叫 SurfaceHolder.setFixedSize(),強制 SurfaceView 符合相機輸出大小,也可以採用類似 GitHub 上相機範例通用模組中 AutoFitSurfaceView 的方法,設定絕對大小,同時考量顯示比例和可用空間,並在觸發活動變更時自動調整。
使用所選格式從 ImageReader 設定其他介面會比較容易,因為不需要等待回呼:
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);
使用 ImageReader 等封鎖目標緩衝區時,請在使用後捨棄影格:
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 硬體層級指定最低公分母裝置。您可以新增條件式分支,並在硬體層級為 LIMITED 的裝置上,將 RECORD 大小用於其中一個輸出目標介面,甚至在硬體層級為 FULL 的裝置上,將大小增加至 MAXIMUM。