Google Jetpack 新组件 CameraX 介绍与实践

近期,Google 的 Jetpack 组件又出了新的库:CameraX 。

顾名思义:CameraX 就是用来进行 Camera 开发的官方库了,而且后续会有 Google 进行维护和升级。这对于广大 Camera 开发工程师和即将成为 Camera 的程序员来说,真是个好消息~~~

CameraX 介绍

官方有给出一个示例的工程,我 fork 了之后,加入使用 OpenGL 黑白滤镜渲染的操作,具体地址如下:

https://github.com/glumes/camera

官方并没有提到 CameraX 库具体如何进行 OpenGL 线程渲染的, 继续往下看,你会找到答案的~~~

关于 CameraX 更多的介绍,建议看看 Google I/O 大会上的视频记录,比看文档能了解更多内容~~~

https://www.youtube.com/watch?v=kuv8uK-5CLY

在视频中提到,目前有很多应用都开始接入了 CameraX,比如 Camera360、Tik Tok 等。

image

简述 Camera 开发

关于 Camera 的开发,之前也有写过相关的文章🤔

Android 相机开发中的尺寸和方向问题

Android Camera 模型及 API 接口演变

对于一个简单能用的 Camera 应用(Demo 级别)来说,关注两个方面就好了:预览和拍摄。

而预览和拍摄的图像都受到分辨率、方向的影响。Camera 最必备的功能就是能针对预览和拍摄提供两套分辨率,因此就得区分场景去设置。

对于拍摄还好说一点,要获得最好的图像质量,就选择同比例中分辨率最大的吧。

而预览的图像最终要呈现到 Android 的 Surface 上,因此选择分辨率的时候要考虑 Surface 的宽高比例,不要出现比例不匹配导致图像拉伸的现象。

另外,如果要做美颜、滤镜类的应用,就要把 Camera 预览的图像放到 OpenGL 渲染的线程上去,然后由 OpenGL 去做图像相关的操作,也就没 Camera 什么事了。等到拍摄图片时,可以由 OpenGL 去获取图像内容,也可以由 Camera 获得图像内容,然后经过 OpenGL 做离屏处理~~~

至于 Camera 开发的其他功能,比如对焦、曝光、白平衡、HDR 等操作,不一定所有的 Camera 都能够支持,而且也可以在上面的基础上当做 Camera 的一个 feature 去拓展开发,并不算难事,这也是一个 Camera 开发工程师进阶所要掌握的内容~~

CameraX 开发实践

CameraX 目前的版本是 1.0.0-alpha01 ,在使用时要添加如下的依赖:

    // CameraX
    def camerax_version = "1.0.0-alpha01"
    implementation "androidx.camera:camera-core:${camerax_version}"
    implementation "androidx.camera:camera-camera2:${camerax_version}"

CameraX 向后兼容到 Android 5.0(API Level 21),并且它是基于 Camera 2.0 的 API 进行封装的,解决了市面上绝大部分手机的兼容性问题~~~

相比 Camera 2.0 复杂的调用流程,CameraX 就简化很多,只关心我们需要的内容就好了,不像前者得自己维护 CameraSession 会话等状态,并且 CameraX 和 Jetpack 主打的 Lifecycle 绑定在一起了,什么时候该打开相机,什么时候该释放相机,都交给 Lifecycle 生命周期去管理吧

上手 CameraX 主要关注三个方面:

  • 图像预览(Image Preview)
  • 图像分析(Image analysis)
  • 图像拍摄(Image capture)

预览

不管是 预览 还是 图像分析、图像拍摄,CameraX 都是通过一个建造者模式来构建参数 Config 类,再由 Config 类创建预览、分析器、拍摄的类,并在绑定生命周期时将它们传过去。

// // Apply declared configs to CameraX using the same lifecycle owner
CameraX.bindToLifecycle(
               lifecycleOwner: this, preview, imageCapture, imageAnalyzer)

既可以绑定 Activity 的 Lifecycle,也可以绑定 Fragment 的。

当需要解除绑定时:

// Unbinds all use cases from the lifecycle and removes them from CameraX.
 CameraX.unbindAll()

关于预览的参数配置,如果你有看过之前的文章:Android 相机开发中的尺寸和方向问题 想必就会很了解了。

提供我们的目标参数,由 CameraX 去判断当前 Camera 是否支持,并选择最符合的。

fun buildPreviewUseCase(): Preview {
    val previewConfig = PreviewConfig.Builder()
        // 宽高比
        .setTargetAspectRatio(aspectRatio)
        // 旋转
        .setTargetRotation(rotation)
        // 分辨率
        .setTargetResolution(resolution)
        // 前后摄像头
        .setLensFacing(lensFacing)
        .build()
    
    // 创建 Preview 对象
    val preview = Preview(previewConfig)
    // 设置监听
    preview.setOnPreviewOutputUpdateListener { previewOutput ->
        // PreviewOutput 会返回一个 SurfaceTexture
        cameraTextureView.surfaceTexture = previewOutput.surfaceTexture
    }

    return preview
}

通过建造者模式创建 Preview 对象,并且一定要给 Preview 对象设置 OnPreviewOutputUpdateListener 接口回调。

相机预览的图像流是通过 SurfaceTexture 来返回的,而在项目例子中,是通过把 TextureView 的 SurfaceTexture 替换成 CameraX 返回的 SurfaceTexture,这样实现了 TextureView 控件显示 Camera 预览内容。

另外,还需要考虑到设备的选择方向,当设备横屏变为竖屏了,TextureView 也要相应的做旋转。

preview.setOnPreviewOutputUpdateListener { previewOutput ->
    cameraTextureView.surfaceTexture = previewOutput.surfaceTexture

    // Compute the center of preview (TextureView)
    val centerX = cameraTextureView.width.toFloat() / 2
    val centerY = cameraTextureView.height.toFloat() / 2

    // Correct preview output to account for display rotation
    val rotationDegrees = when (cameraTextureView.display.rotation) {
        Surface.ROTATION_0 -> 0
        Surface.ROTATION_90 -> 90
        Surface.ROTATION_180 -> 180
        Surface.ROTATION_270 -> 270
        else -> return@setOnPreviewOutputUpdateListener
    }

    val matrix = Matrix()
    matrix.postRotate(-rotationDegrees.toFloat(), centerX, centerY)

    // Finally, apply transformations to TextureView
    cameraTextureView.setTransform(matrix)
}

TextureView 旋转的设置同样在 OnPreviewOutputUpdateListener 接口中去完成。

图像分析

bindToLifecycle 方法中,imageAnalyzer 参数并不是必需的。

ImageAnalysis 可以帮助我们做一些图像质量的分析,需要我们去实现 ImageAnalysis.Analyzer 接口的 analyze 方法。

fun buildImageAnalysisUseCase(): ImageAnalysis {
    // 分析器配置 Config 的建造者
    val analysisConfig = ImageAnalysisConfig.Builder()
        // 宽高比例
        .setTargetAspectRatio(aspectRatio)
        // 旋转
        .setTargetRotation(rotation)
        // 分辨率
        .setTargetResolution(resolution)
        // 图像渲染模式
        .setImageReaderMode(readerMode)
        // 图像队列深度
        .setImageQueueDepth(queueDepth)
        // 设置回调的线程
        .setCallbackHandler(handler)
        .build()
    
    // 创建分析器 ImageAnalysis 对象
    val analysis = ImageAnalysis(analysisConfig)
    
    // setAnalyzer 传入实现了 analyze 接口的类
    analysis.setAnalyzer { image, rotationDegrees ->
        // 可以得到的一些图像信息,参见 ImageProxy 类相关方法
        val rect = image.cropRect
        val format = image.format
        val width = image.width
        val height = image.height
        val planes = image.planes
    }

    return analysis
}

在图像分析器的相关配置中,有个 ImageReaderModeImageQueueDepth 的设置。

ImageQueueDepth 会指定相机管线中图像的个数,提高 ImageQueueDepth 的数量会对相机的性能和内存的使用造成影响

其中,ImageReaderMode 有两种模式:

  • ACQUIRE_LATEST_IMAGE
    • 该模式下,获得图像队列中最新的图片,并且会清空队列已有的旧的图像。
  • ACQUIRE_NEXT_IMAGE
    • 该模式下,获得下一张图像。

在图像分析的 analyze 方法中,能通过 ImageProxy 类拿到一些图像信息,并基于这些信息做分析。

拍摄

拍摄同样有一个 Config 参数构建者类,而且设定的参数和预览相差不大,也是图像宽高比例、旋转方向、分辨率,除此之外还有闪光灯等配置项。

fun buildImageCaptureUseCase(): ImageCapture {
    val captureConfig = ImageCaptureConfig.Builder()
        .setTargetAspectRatio(aspectRatio)
        .setTargetRotation(rotation)
        .setTargetResolution(resolution)
        .setFlashMode(flashMode)
        // 拍摄模式
        .setCaptureMode(captureMode)
        .build()
    
    // 创建 ImageCapture 对象
    val capture = ImageCapture(captureConfig)
    cameraCaptureImageButton.setOnClickListener {
        // Create temporary file
        val fileName = System.currentTimeMillis().toString()
        val fileFormat = ".jpg"
        val imageFile = createTempFile(fileName, fileFormat)
        
        // Store captured image in the temporary file
        capture.takePicture(imageFile, object : ImageCapture.OnImageSavedListener {
            override fun onImageSaved(file: File) {
                // You may display the image for example using its path file.absolutePath
            }

            override fun onError(useCaseError: ImageCapture.UseCaseError, message: String, cause: Throwable?) {
                // Display error message
            }
        })
    }

    return capture
}

在图像拍摄的相关配置中,也有个 CaptureMode 的设置。

它有两种选项:

  • MIN_LATENCY
    • 该模式下,拍摄速度会相对快一点,但图像质量会打折扣
  • MAX_QUALITY
    • 该模式下,拍摄速度会慢一点,但图像质量好

OpenGL 渲染

以上是关于 CameraX 的简单应用方面的内容,更关心的是如何用 CameraX 去做 OpenGL 渲染实现美颜。滤镜等效果。

还记得在图像预览 Preview 的 setOnPreviewOutputUpdateListener 方法中,会返回一个 SurfaceTexture ,相机的图像流就是通过它返回的。

那么要实现 OpenGL 线程的渲染,首先就要基于 EGL 去创建 OpenGL 绘制环境,然后利用 SurfaceTexture 的 attachToGLContext 方法,将 SurfaceTexture 添加到 OpenGL 线程去。

attachToGLContext 的参数是一个纹理 ID ,这个纹理就必须是 OES 类型的纹理。

然后再把这纹理 ID 绘制到 OpenGL 对应的 Surface 上,这可以看成是两个不同的线程在允许,一个 Camera 预览线程,一个 OpenGL 绘制线程。

如果你不是很理解的话,建议还是看看上面提供的代码地址:

https://github.com/glumes/camera

也可以关注我的微信公众号 【纸上浅谈】,里面有一些关于 OpenGL 学习和实践的文章~~~

CameraX 的拓展

如果你看了 Google I/O 大会的视频,那肯定了解 CameraX 的拓展属性。

在视频中提到 Google 也正在和华为、三星、LG、摩托摩拉等厂商进行合作,为了获得厂商系统相机的一些能力,比如 HDR 等。

不过考虑到目前的形势,可能和华为的合作难以继续下去了吧...

但还是期待 CameraX 能给带来更多的新特性吧~~~

参考

  1. https://www.youtube.com/watch?v=kuv8uK-5CLY
  2. https://proandroiddev.com/android-camerax-preview-analyze-capture-1b3f403a9395

觉得文章不错,欢迎关注微信公众号:【纸上浅谈】,获得最新文章推送~~~

扫码关注
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容