注意:本页介绍的是 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 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);
如果您正确配置目标 surface,此代码将仅生成达到由 StreamComfigurationMap.GetOutputMinFrameDuration(int, Size)
和 StreamComfigurationMap.GetOutputStallDuration(int, Size)
确定的最低 FPS 的数据流。
实际性能因设备而异,但 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 |
预览和应用内处理。 | ||
PRIV |
PREVIEW |
YUV |
PREVIEW |
JPEG |
MAXIMUM |
静态拍摄加应用内处理。 |
LEGACY
是可能的最低硬件级别。此表显示,每台支持 Camera2(API 级别 21 及更高级别)的设备使用正确的配置,以及没有过多的开销限制性能(例如内存、CPU 或散热限制)的情况下,可以使用最多三个同时输出串流。
您的应用还需要配置目标输出缓冲区。例如,如需以硬件级别为 LEGACY
的设备为目标,您可以设置两个目标输出 surface,一个使用 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
类似的方法,该方法设置绝对尺寸,考虑宽高比和可用空间,同时在触发 activity 更改时自动调整。
使用所需格式设置 ImageReader
中的另一个 Surface 会更加容易,因为没有需要等待的回调:
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
硬件级别的设备中的其中一个输出目标 Surface 添加条件分支并使用 RECORD
大小,甚至可以将其增加到 MAXIMUM
大小(对于具有 FULL
硬件级别的设备)。