今天,我们将要去看一下怎么使用安卓创意集成的 Camera2 API 去得到一个集成相机。具体来说,我们将尽最大努力去用 SurfaceView 展示我们输入源的一张图片,和同时建立 ImageReader 处理来自相同来源的单独的帧。
正如你所知,在Android平台上构建东西时,需要考虑很多未知因素。有这么多不同的设备类型,而且无法预测该设备的服务和硬件会是什么样子。这就是为什么我们需要与API接口,为我们解决这些未知值,我们需要安全地处理代码中的所有事件。Camera2让这一切变得简单。因此,当您在某个设备上运行应用程序时,您可能想询问的第一件事是,我可以使用哪种设备?该设备是否同时具有正面和背面摄像头?它支持哪些决议?我们使用CameraManager回答这些问题。
顺便说一下,我已经开始了一个新的、空的项目,我将从那里开始在MainActivity中构建。使用此API不需要任何外部依赖项。
实际上,让我们回想一下。首先我们需要请求许可才能使用相机。这里有一个帮助您完成此操作的助手:
/** 请求相机权限助手 */
object CameraPermissionHelper {
private const val CAMERA_PERMISSION_CODE = 0
private const val CAMERA_PERMISSION = Manifest.permission.CAMERA
/** 请检查我们是否拥有此应用程序所需的权限。 */
fun hasCameraPermission(activity: Activity): Boolean {
return ContextCompat.checkSelfPermission(activity, CAMERA_PERMISSION) == PackageManager.PERMISSION_GRANTED
}
/** 请检查我们是否拥有此应用程序所需的权限,如果没有,请求他们。 */
fun requestCameraPermission(activity: Activity) {
ActivityCompat.requestPermissions(
activity, arrayOf(CAMERA_PERMISSION), CAMERA_PERMISSION_CODE)
}
/** 检查是否需要显示此许可的理由。 */
fun shouldShowRequestPermissionRationale(activity: Activity): Boolean {
return ActivityCompat.shouldShowRequestPermissionRationale(activity, CAMERA_PERMISSION)
}
/** 启动应用程序设置以授予权限 */
fun launchPermissionSettings(activity: Activity) {
val intent = Intent()
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
intent.data = Uri.fromParts("package", activity.packageName, null)
activity.startActivity(intent)
}
}
实现这个回调
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
if (!CameraPermissionHelper.hasCameraPermission(this)) {
Toast.makeText(this, "Camera permission is needed to run this application", Toast.LENGTH_LONG)
.show()
if (!CameraPermissionHelper.shouldShowRequestPermissionRationale(this)) {
// 检查“不再询问”时权限被拒绝。
CameraPermissionHelper.launchPermissionSettings(this)
}
finish()
}
recreate()
}
确保你加入了这一行在 Manifest 文件中
然后调用
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (!CameraPermissionHelper.hasCameraPermission(this)) {
CameraPermissionHelper.requestCameraPermission(this)
return
}
}
private fun startCameraSession() {
val cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
if (cameraManager.cameraIdList.isEmpty()) {
// 没有相机
return
}
val firstCamera = cameraIdList[0]
cameraManager.openCamera(firstCamera, object: CameraDevice.StateCallback() {
override fun onDisconnected(p0: CameraDevice) { }
override fun onError(p0: CameraDevice, p1: Int) { }
override fun onOpened(cameraDevice: CameraDevice) {
// 使用相机
val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraDevice.id)
cameraCharacteristics[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]?.let { streamConfigurationMap ->
streamConfigurationMap.getOutputSizes(ImageFormat.YUV_420_888)?.let { yuvSizes ->
val previewSize = yuvSizes.last()
}
}
}
}, Handler { true })
}
在这里,我们使用 manager 抓取列表中的第一个相机。然后我们调用 CameraManager.openCamera(),传递 id 以访问该相机设备。一旦成功打开,我们将调用CameraManager.getCameraCharacteristics以查询该相机设备的详细信息。如果您想更详细地了解要使用哪台相机,可以在打开相机之前调用 getCameraCharacteristics 来检查它是否具有所需的属性。
在本例中,我检索相机的 StreamConfigurationMap ,其中包含有关设备支持的输出格式的所有信息。接下来,我获取YUV_420_888格式(如果存在),因为这是我的项目所需要的。您可以根据需要使用其他格式。我对此不太了解。然后,我从该列表中获取最后一个大小,这将是本示例所需的最低分辨率大小。
下一步,我们需要做一些新人可能不会想到的至关重要的事情。我们需要检查Android设备的方向和摄像头输出的数据的方向是否互换!设备可能是纵向的,但相机仍然根据相机传感器输出横向的图像。Camera2示例项目提供了一种方法来确定是否存在这种情况:
private fun areDimensionsSwapped(displayRotation: Int, cameraCharacteristics: CameraCharacteristics): Boolean {
var swappedDimensions = false
when (displayRotation) {
Surface.ROTATION_0, Surface.ROTATION_180 -> {
if (cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) == 90 || cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) == 270) {
swappedDimensions = true
}
}
Surface.ROTATION_90, Surface.ROTATION_270 -> {
if (cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) == 0 || cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) == 180) {
swappedDimensions = true
}
}
else -> {
// 显示旋转无效
}
}
return swappedDimensions
}
然后返回 startCameraSession:
val previewSize = yuvSizes.last()
// 账户.
val displayRotation = windowManager.defaultDisplay.rotation
val swappedDimensions = areDimensionsSwapped(displayRotation, cameraCharacteristics)
// 如果需要交换长和宽
val rotatedPreviewWidth = if (swappedDimensions) previewSize.height else previewSize.width
val rotatedPreviewHeight = if (swappedDimensions) previewSize.width else previewSize.height
此时,我们准备好与 SurfaceView 集成。要做到这一点其实很简单。首先,我们用 SurfaceView 替换自动生成的“Hello World!”文本视图
我们已经有了相机设备特性给出的输出大小,所以我们只需将 holder 上的预览固定大小设置为输出大小:
surfaceView.holder.setFixedSize(rotatedPreviewWidth, rotatedPreviewHeight)
现在我们只需要连接输入源,即我们的相机设备和 surface。我们使用CameraCaptureSession.CaptureCallback()执行此操作:
val previewSurface = surfaceView.holder.surface
val captureCallback = object : CameraCaptureSession.StateCallback()
{
override fun onConfigureFailed(session: CameraCaptureSession) {}
override fun onConfigured(session: CameraCaptureSession) {
// 会话已配置
val previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
.apply {
addTarget(previewSurface)
}
session.setRepeatingRequest(
previewRequestBuilder.build(),
object: CameraCaptureSession.CaptureCallback() {},
Handler { true }
)
}
}
cameraDevice.createCaptureSession(mutableListOf(previewSurface), captureCallback, Handler { true })
如果你试着运行这个,一切都会好起来的。但是你不会看到任何预览出现…这是因为我们还没有调用startCameraSession()!事实上,它并没有那么简单,因为SurfaceView必须异步准备自己,即使在“活动”是交互式的之后,它也可能没有准备好。
所以我们需要一个回调:
val surfaceReadyCallback = object: SurfaceHolder.Callback {
override fun surfaceChanged(p0: SurfaceHolder?, p1: Int, p2: Int, p3: Int) { }
override fun surfaceDestroyed(p0: SurfaceHolder?) { }
override fun surfaceCreated(p0: SurfaceHolder?) {
startCameraSession()
}
}
回到onCreate(),访问Kotlin启用的同一合成视图引用:
surfaceView.holder.addCallback(surfaceReadyCallback)
现在,当你运行时,你应该会看到一个漂亮的小预览…
让我们快速完成向应用程序添加另一个输出源的脚手架:ImageReader。Camera2最酷的地方是它可以将任意数量的输入连接到各种输出。因此,我们可以显示预览并处理来自以下来源的图像:
//账户
surfaceView.holder.setFixedSize(rotatedPreviewWidth, rotatedPreviewHeight)
// 配置图像读取器
val imageReader = ImageReader.newInstance(rotatedPreviewWidth, rotatedPreviewHeight,
ImageFormat.YUV_420_888, 2)
imageReader.setOnImageAvailableListener({
// 做一些事
}, Handler { true })
我们配置 ImageReader 和 surface view 用相同的图像格式
ImageReader将其数据渲染到Surface,我们可以直接访问该Surface:
// cont.
val previewSurface = surfaceView.holder.surface
val recordingSurface = imageReader.surface
将其添加到我们的回调中。通过这样做,我们确保对于进入预览的每个图像,也会将其发送到 ImageReader:
// cont.
val previewRequestBuilder = camera.createCaptureRequest(TEMPLATE_PREVIEW).apply {
addTarget(previewSurface)
addTarget(recordingSurface)
}
并将其添加到我们session中的surfaces列表中:
// cont.
cameraDevice.createCaptureSession(mutableListOf(previewSurface, recordingSurface), captureCallback, Handler { true })
生成的图像将是一个包含输入图像缓冲区的对象,如果您愿意,可以使用层和所有您需要的东西来分解和重新组合图像的每一位。
就是这样!我希望这简化了您理解此API的过程。请务必查看主要来源,尤其是样本项目,以获得进一步的澄清。还有一个关于我使用上述代码的应用程序的快速简介:
KanjiReader是一个图像处理应用程序,可以让您即时翻译日语汉字,并查看其含义和读数的读数。
实现效果:
Android: https://play.google.com/store/apps/details?id=tylerwalker.io.kanjireader
iOS: Kanji-Reader on the App Store
文章来源