[译] 充分利用多摄像头 API(1)

要了解多摄像头 API,我们必须首先了解逻辑摄像头和物理摄像头之间的区别;这个概念最好用一个例子来说明。例如,我我们可以想像一个有三个后置摄像头而没有前置摄像头的设备。在本例中,三个后置摄像头中的每一个都被认为是一个物理摄像头。然后逻辑摄像头就是两个或更多这些物理摄像头的分组。逻辑摄像头的输出可以是来自其中一个底层物理摄像机的一个流,也可以是同时来自多个底层物理摄像机的融合流;这两种方式都是由相机的 HAL(Hardware Abstraction Layer)来处理的。

许多手机制造商也开发了他们自身的相机应用程序(通常预先安装在他们的设备上)。为了利用所有硬件的功能,他们有时会使用私有或隐藏的 API,或者从驱动程序实现中获得其他应用程序没有特权访问的特殊处理。有些设备甚至通过提供来自不同物理双摄像头的融合流来实现逻辑摄像头的概念,但同样,这只对某些特权应用程序可用。通常,框架只会暴露一个物理摄像头。Android Pie 之前第三方开发者的情况如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

相机功能通常只对特权应用程序可用

从 Android Pie 开始,一些事情发生了变化。首先,在 Android 应用程序中使用 私有 API 不再可行。其次,Android 框架中包含了 多摄像头支持,Android 已经 强烈推荐 手机厂商为面向同一方向的所有物理摄像头提供逻辑摄像头。因此,这是第三方开发人员应该在运行 Android Pie 及以上版本的设备上看到的内容:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

开发人员可完全访问从 Android P 开始的所有摄像头设备

值得注意的是,逻辑摄像头提供的功能完全依赖于相机 HAL 的 OEM 实现。例如,像 Pixel 3 是根据请求的焦距和裁剪区域选择其中一个物理摄像头,用于实现其逻辑相机。

多摄像头 API

新 API 包含了以下新的常量、类和方法:

  • CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
  • CameraCharacteristics.getPhysicalCameraIds()
  • CameraCharacteristics.getAvailablePhysicalCameraRequestKeys()
  • CameraDevice.createCaptureSession(SessionConfiguration config)
  • CameraCharactersitics.LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE
  • OutputConfiguration & SessionConfiguration

由于 Android CDD 的更改,多摄像头 API 也满足了开发人员的某些期望。双摄像头设备在 Android Pie 之前就已经存在,但同时打开多个摄像头需要反复试验;Android 上的多摄像头 API 现在给了我们一组规则,告诉我们什么时候可以打开一对物理摄像头,只要它们是同一逻辑摄像头的一部分。

如上所述,我们可以预期,在大多数情况下,使用 Android Pie 发布的新设备将公开所有物理摄像头(除了更奇特的传感器类型,如红外线),以及更容易使用的逻辑摄像头。此外,非常关键的是,我们可以预期,对于每个保证有效的融合流,属于逻辑摄像头的一个流可以被来自底层物理摄像头的两个流替换。让我们通过一个例子更详细地介绍它。

同时使用多个流

在上一篇博文中,我们详细介绍了在单个摄像头中 同时使用多个流 的规则。同样的规则也适用于多个摄像头,但在 这个文档 中有一个值得注意的补充说明:

对于每个有保证的融合流,逻辑摄像头都支持将一个逻辑 YUV_420_888 或原始流替换为两个相同大小和格式的物理流,每个物理流都来自一个单独的物理摄像头,前提是两个物理摄像头都支持给定的大小和格式。

换句话说,YUV 或 RAW 类型的每个流可以用相同类型和大小的两个流替换。例如,我们可以从单摄像头设备的摄像头视频流开始,配置如下:

  • 流 1:YUV 类型,id = 0 的逻辑摄像机的最大尺寸

然后,一个支持多摄像头的设备将允许我们创建一个会话,用两个物理流替换逻辑 YUV 流:

  • 流 1:YUV 类型,id = 1 的物理摄像头的最大尺寸
  • 流 2:YUV 类型,id = 2 的物理摄像头的最大尺寸

诀窍是,当且仅当这两个摄像头是一个逻辑摄像头分组的一部分时,我们可以用两个等效的流替换 YUV 或原始流 — 即被列在 CameraCharacteristics.getPhysicalCameraIds() 中的。

另一件需要考虑的事情是,框架提供的保证仅仅是同时从多个物理摄像头获取帧的最低要求。我们可以期望在大多数设备中支持额外的流,有时甚至允许我们独立地打开多个物理摄像头设备。不幸的是,由于这不是框架的硬性保证,因此需要我们通过反复试验来执行每个设备的测试和调优。

使用多个物理摄像头创建会话

当我们在一个支持多摄像头的设备中与物理摄像头交互时,我们应该打开一个 CameraDevice(逻辑相机),并在一个会话中与它交互,这个会话必须使用 API CameraDevice.createCaptureSession(SessionConfiguration config) 创建,这个 API 自 SDK 级别 28 起可用。然后,这个 会话参数 将有很多 输出配置,其中每个输出配置将具有一组输出目标,以及(可选的)所需的物理摄像头 ID。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

会话参数和输出配置模型

稍后,当我们分派拍摄请求时,该请求将具有与其关联的输出目标。框架将根据附加到请求的输出目标来决定将请求发送到哪个物理(或逻辑)摄像头。如果输出目标对应于作为 输出配置 的输出目标之一和物理摄像头 ID 一起发送,那么该物理摄像头将接收并处理该请求。

使用一对物理摄像头

面向开发人员的多摄像头 API 中最重要的一个新增功能是识别逻辑摄像头并找到它们背后的物理摄像头。现在我们明白,我们可以同时打开多个物理摄像头(再次,通过打开逻辑摄像头和作为同一会话的一部分),并且有明确的融合流的规则,我们可以定义一个函数来帮助我们识别潜在的可以用来替换一个逻辑摄像机视频流的一对物理摄像头:

/**

  • 帮助类,用于封装逻辑摄像头和两个底层
  • 物理摄像头
    */
    data class DualCamera(val logicalId: String, val physicalId1: String, val physicalId2: String)

fun findDualCameras(manager: CameraManager, facing: Int? = null): Array {
val dualCameras = ArrayList()

// 遍历所有可用的摄像头特征
manager.cameraIdList.map {
Pair(manager.getCameraCharacteristics(it), it)
}.filter {
// 通过摄像头的方向这个请求参数进行过滤
facing == null || it.first.get(CameraCharacteristics.LENS_FACING) == facing
}.filter {
// 逻辑摄像头过滤
it.first.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)!!.contains(
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA)
}.forEach {
// 物理摄像头列表中的所有可能对都是有效结果
// 注意:可能有 N 个物理摄像头作为逻辑摄像头分组的一部分
val physicalCameras = it.first.physicalCameraIds.toTypedArray()
for (idx1 in 0 until physicalCameras.size) {
for (idx2 in (idx1 + 1) until physicalCameras.size) {
dualCameras.add(DualCamera(
it.second, physicalCameras[idx1], physicalCameras[idx2]))
}
}
}

return dualCameras.toTypedArray()
}

物理摄像头的状态处理由逻辑摄像头控制。因此,要打开我们的“双摄像头”,我们只需要打开与我们感兴趣的物理摄像头相对应的逻辑摄像头:

fun openDualCamera(cameraManager: CameraManager,
dualCamera: DualCamera,
executor: Executor = AsyncTask.SERIAL_EXECUTOR,
callback: (CameraDevice) -> Unit) {

cameraManager.openCamera(
dualCamera.logicalId, executor, object : CameraDevice.StateCallback() {
override fun onOpened(device: CameraDevice) = callback(device)
// 为了简便起见,我们省略…
override fun onError(device: CameraDevice, error: Int) = onDisconnected(device)
override fun onDisconnected(device: CameraDevice) = device.close()
})
}

在此之前,除了选择打开哪台摄像头之外,没有什么不同于我们过去打开任何其他摄像头所做的事情。现在是时候使用新的 会话参数 API 创建一个拍摄会话了,这样我们就可以告诉框架将某些目标与特定的物理摄像机 ID 关联起来:

/**

  • 帮助类,封装了定义 3 组输出目标的类型:
    1. 逻辑摄像头
    1. 第一个物理摄像头
    1. 第二个物理摄像头
      */
      typealias DualCameraOutputs =
      Triple<MutableList?, MutableList?, MutableList?>

fun createDualCameraSession(cameraManager: CameraManager,
dualCamera: DualCamera,
targets: DualCameraOutputs,
executor: Executor = AsyncTask.SERIAL_EXECUTOR,
callback: (CameraCaptureSession) -> Unit) {

// 创建三组输出配置:一组用于逻辑摄像头,
// 另一组用于逻辑摄像头。
val outputConfigsLogical = targets.first?.map { OutputConfiguration(it) }
val outputConfigsPhysical1 = targets.second?.map {
OutputConfiguration(it).apply { setPhysicalCameraId(dualCamera.physicalId1) } }
val outputConfigsPhysical2 = targets.third?.map {
OutputConfiguration(it).apply { setPhysicalCameraId(dualCamera.physicalId2) } }

// 将所有输出配置放入单个数组中
val outputConfigsAll = arrayOf(
outputConfigsLogical, outputConfigsPhysical1, outputConfigsPhysical2)
.filterNotNull().flatMap { it }

// 实例化可用于创建会话的会话配置
val sessionConfiguration = SessionConfiguration(SessionConfiguration.SESSION_REGULAR,
outputConfigsAll, executor, object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) = callback(session)
// 省略…
override fun onConfigureFailed(session: CameraCaptureSession) = session.device.close()
})

// 使用前面定义的函数打开逻辑摄像头
openDualCamera(cameraManager, dualCamera, executor = executor) {

// 最后创建会话并通过回调返回
it.createCaptureSession(sessionConfiguration)
}
}

现在,我们可以参考 文档以前的博客文章 来了解支持哪些流的融合。我们只需要记住这些是针对单个逻辑摄像头上的多个流的,并且兼容使用相同的配置的并将其中一个流替换为来自同一逻辑摄像头的两个物理摄像头的两个流。

摄像头会话 就绪后,剩下要做的就是发送我们想要的 拍摄请求。拍摄请求的每个目标将从相关的物理摄像头(如果有的话)接收数据,或者返回到逻辑摄像头。

缩放示例用例

为了将所有这一切与最初讨论的用例之一联系起来,让我们看看如何在我们的相机应用程序中实现一个功能,以便用户能够在不同的物理摄像头之间切换,体验到不同的视野——有效地拍摄不同的“缩放级别”。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

将相机转换为缩放级别用例的示例(来自 Pixel 3 Ad

首先,我们必须选择我们想允许用户在其中进行切换的一对物理摄像机。为了获得最大的效果,我们可以分别搜索提供最小焦距和最大焦距的一对摄像机。通过这种方式,我们选择一种可以在尽可能短的距离上对焦的摄像设备,另一种可以在尽可能远的点上对焦:

fun findShortLongCameraPair(manager: CameraManager, facing: Int? = null): DualCamera? {

return findDualCameras(manager, facing).map {
val characteristics1 = manager.getCameraCharacteristics(it.physicalId1)
val characteristics2 = manager.getCameraCharacteristics(it.physicalId2)

// 查询每个物理摄像头公布的焦距
val focalLengths1 = characteristics1.get(
CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(0F)
val focalLengths2 = characteristics2.get(
CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(0F)

// 计算相机之间最小焦距和最大焦距之间的最大差异
val focalLengthsDiff1 = focalLengths2.max()!! - focalLengths1.min()!!
val focalLengthsDiff2 = focalLengths1.max()!! - focalLengths2.min()!!

// 返回相机 ID 和最小焦距与最大焦距之间的差值
if (focalLengthsDiff1 < focalLengthsDiff2) {
Pair(DualCamera(it.logicalId, it.physicalId1, it.physicalId2), focalLengthsDiff1)
} else {
Pair(DualCamera(it.logicalId, it.physicalId2, it.physicalId1), focalLengthsDiff2)
}

// 只返回差异最大的对,如果没有找到对,则返回 null
}.sortedBy { it.second }.reversed().lastOrNull()?.first
}

一个合理的架构应该是有两个 SurfaceViews,每个流一个,在用户交互时交换,因此在任何给定的时间只有一个是可见的。在下面的代码片段中,我们将演示如何打开逻辑摄像头、配置摄像头输出、创建摄像头会话和启动两个预览流;利用前面定义的功能:

val cameraManager: CameraManager = …

// 从 activity/fragment 中获取两个输出目标

最后

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司21年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

[外链图片转存中…(img-eIwFvsD7-1715544365371)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 14
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在MATLAB中,NURBS(非均匀有理B样条)是一种强大的数学工具,用于表示和处理复杂的曲线和曲面。NURBS在计算机图形学、CAD(计算机辅助设计)、CAM(计算机辅助制造)等领域有着广泛的应用。下面将详细探讨MATLAB中NURBS的绘制方法以及相关知识点。 我们需要理解NURBS的基本概念。NURBS是B样条(B-Spline)的一种扩展,其特殊之处在于引入了权重因子,使得曲线和曲面可以在不均匀的参数空间中进行平滑插值。这种灵活性使得NURBS在处理非均匀数据时尤为有效。 在MATLAB中,可以使用`nurbs`函数创建NURBS对象,它接受控制点、权值、 knot向量等参数。控制点定义了NURBS曲线的基本形状,而knot向量决定了曲线的平滑度和分布。权值则影响曲线通过控制点的方式,大的权值会使曲线更靠近该点。 例如,我们可以使用以下代码创建一个简单的NURBS曲线: ```matlab % 定义控制点 controlPoints = [1 1; 2 2; 3 1; 4 2]; % 定义knot向量 knotVector = [0 0 0 1 1 1]; % 定义权值(默认为1,如果未指定) weights = ones(size(controlPoints,1),1); % 创建NURBS对象 nurbsObj = nurbs(controlPoints, weights, knotVector); ``` 然后,我们可以用`plot`函数来绘制NURBS曲线: ```matlab plot(nurbsObj); grid on; ``` `data_example.mat`可能包含了一个示例的NURBS数据集,其中可能包含了控制点坐标、权值和knot向量。我们可以通过加载这个数据文件来进一步研究NURBS的绘制: ```matlab load('data_example.mat'); % 加载数据 nurbsData = struct2cell(data_example); % 转换为cell数组 % 解析数据 controlPoints = nurbsData{1}; weights = nurbsData{2}; knotVector = nurbsData{3}; % 创建并绘制NURBS曲线 nurbsObj = nurbs(controlPoints, weights, knotVector); plot(nurbsObj); grid on; ``` MATLAB还提供了其他与NURBS相关的函数,如`evalnurbs`用于评估NURBS曲线上的点,`isoparm`用于生成NURBS曲面上的等参线,以及`isocurve`用于在NURBS曲面上提取特定参数值的曲线。这些工具对于分析和操作NURBS对象非常有用。 MATLAB中的NURBS功能允许用户方便地创建、编辑和可视化复杂的曲线和曲面。通过对控制点、knot向量和权值的调整,可以精确地控制NURBS的形状和行为,从而满足各种工程和设计需求。通过深入理解和熟练掌握这些工具,可以在MATLAB环境中实现高效的NURBS建模和分析。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值