CameraX新版(1.2.0-alpha02) + OpenGL实现相机预览

Refrence

https://juejin.cn/post/7035293015757307918


CameraX比较新的版本中,Preview.setOnPreviewOutputUpdateListener(...)已经不可用了

前言

一直都想研究一下图像处理,实现一些简单的相机预览及拍照功能。顺便了解一下CameraX的使用。找了一些有关CameraX + OpenGL实现相机预览的文章,但可能是由于CameraXapi变化的缘故,代码无法正常运行,所以参考了这些资料结合自己的研究,写一篇关于CameraX + OpenGL实现相机预览的文章作为记录和分享

本文代码所使用的CameraX版本为1.0.0-rc03,测试设备为OPPO R15。

CameraX的使用

有关CameraX的简单使用介绍,可以参考官方文档CameraX入门。里面会有比较详细的介绍。由于CameraX配置使用并不复杂,本文对于此部分只是简单贴代码,为后续的对于OpenGL的扩展作铺垫

配置一个预览分辨率为640 * 480,后置摄像头的相机预览:

// MainActivity.kt
private fun setUpCamera() {
    val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
    cameraProviderFuture.addListener({
        val cameraProvider = cameraProviderFuture.get()

        val preview: Preview = Preview.Builder()
            .setTargetResolution(Size(480, 640))
            .setTargetRotation(this.display.rotation)
            .build()

        // 拍照时使用
        val imageCapture = ImageCapture.Builder()
            .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
            .setTargetRotation(this.display.rotation)
            .build()

        preview.setSurfaceProvider(previewView)

        cameraProvider.unbindAll()

        val camera = cameraProvider.bindToLifecycle(
                        this as LifecycleOwner,
                        CameraSelector.DEFAULT_BACK_CAMERA,
                        imageCapture,
                        preview)

        // 控制闪光灯、切换摄像头等。。。
        val cameraInfo = camera.cameraInfo
        val cameraControl = camera.cameraControl
    }, ContextCompat.getMainExecutor(this))
}

预览画面与视图的绑定设置发生在:

preview.setSurfaceProvider(previewView)

其中previewViewJetpack提供的,即原生帮开发者做了相机预览的封装兼容

<androidx.camera.view.PreviewView
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

当然,这里我们使用SurfaceView/TextureView自己实现也是可以的,只是需要自己实现Preview.SurfaceProvider接口进行绑定

回归到本文的主题:使用OpenGL实现相机预览,也就是利用OpenGL将预览帧渲染出来。那么OpenGL结合CameraX的话,解决问题的关键就是利用上述的绑定关系自行实现Preview.SurfaceProvider接口来解决了。

OpenGL各生命周期内需要干什么?

OpenGL初始化

init {
    setEGLContextClientVersion(2)
    setRenderer(cameraRender)
    
    renderMode = RENDERMODE_WHEN_DIRTY
}

先来看看OpenGL的各生命周期,即在渲染器接口GLSurfaceView.Renderer各个回调内,会干什么

public interface Renderer {
    void onSurfaceCreated(GL10 gl, EGLConfig config);

    void onSurfaceChanged(GL10 gl, int width, int height);

    void onDrawFrame(GL10 gl);
}

onSurfaceCreated

由于需要使用OpenGL绘制预览帧,所以在onSurfaceCreated时需要利用OpenGL的api创建一个SurfaceTexture,后面会将这个SurfaceTexture绘制出来。

override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
    gl?.let {
        it.glGenTextures(textures.size, textures, 0)
        surfaceTexture = SurfaceTexture(textures[0])
        screenFilter = ScreenFilter(context)
    }
}
  • textures[0]作为为OpenGL Texture的唯一标识
  • 创建一个SurfaceTexture
  • ScreenFilter作为与OpenGL的GLSL脚本绑定的逻辑,它的初始化时会执行:顶点坐标、纹理坐标的内存空间创建,顶点着色器、片元着色器的程序创建,及GLSL内部的变量映射创建
// ScreenFilter.kt
init {
    vertexBuffer = ByteBuffer.allocateDirect(4 * 4 * 2)
        .order(ByteOrder.nativeOrder())
        .asFloatBuffer()
    vertexBuffer.clear()
    vertexBuffer.put(VERTEX)

    textureBuffer = ByteBuffer.allocateDirect(4 * 2 * 4)
        .order(ByteOrder.nativeOrder())
        .asFloatBuffer()
    textureBuffer.clear()
    textureBuffer.put(TEXTURE)

    val vertexShader = OpenGLUtils.readRawTextFile(context, R.raw.camera_vert)
    val textureShader = OpenGLUtils.readRawTextFile(context, R.raw.camera_frag)

    program = OpenGLUtils.loadProgram(vertexShader, textureShader)

    vPosition = GLES20.glGetAttribLocation(program, "vPosition")
    vCoord = GLES20.glGetAttribLocation(program, "vCoord")
    vTexture = GLES20.glGetUniformLocation(program, "vTexture")
    vMatrix = GLES20.glGetUniformLocation(program, "vMatrix")
}

onSurfaceChanged

onSurfaceChanged时,由于宽高的确定,可以开始之前提到的相机预览,以及设置OpenGL的视窗大小。

override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
    setUpCamera()
    gl?.glViewport(0, 0, width, height)
}

onDrawFrame

onDrawFrame,顾名思义就是得将当前代表的最新一帧预览帧利用OpenGL绘制出来

override fun onDrawFrame(gl: GL10?) {
    val surfaceTexture = this.surfaceTexture
    if (gl == null || surfaceTexture == null) return
    gl.glClearColor(0f, 0f, 0f, 0f)
    gl.glClear(GLES20.GL_COLOR_BUFFER_BIT)
    surfaceTexture.updateTexImage()
    
    screenFilter?.onDrawFrame(textures[0])
}

// ScreenFilter.kt
fun onDrawFrame(textureId: Int): Int {
    // 使用着色器程序
    GLES20.glUseProgram(program)
    // 给着色器程序中传值
    // 给顶点坐标数据传值
    vertexBuffer.position(0)
    GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer)
    // 激活
    GLES20.glEnableVertexAttribArray(vPosition)
    // 给纹理坐标数据传值
    textureBuffer.position(0)
    GLES20.glVertexAttribPointer(vCoord, 2, GLES20.GL_FLOAT, false, 0, textureBuffer)
    GLES20.glEnableVertexAttribArray(vCoord)

    // 给片元着色器中的 采样器绑定
    // 激活图层
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
    // 图像数据
    GLES20.glBindTexture(GLES11Ext.GL_SAMPLER_EXTERNAL_OES, textureId)
    // 传递参数
    GLES20.glUniform1i(vTexture, 0)

    //参数传递完毕,通知 opengl开始画画
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)

    // 解绑
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
    return textureId
}
  • surfaceTexture.updateTexImage():更新到图像流中的最新一帧。
  • screenFilter?.onDrawFrame(textures[0]):将参数(包括最新一帧)传递给着色器程序,并通知其绘制。

总结:

  • onSurfaceCreated:SurfaceTexture的创建以及与OpenGL环境的绑定,OpenGL的着色器程序初始化
  • onSurfaceChanged:相机初始化(ps:这个时机也可以提前),设置OpenGL的视窗大小(ps:宽高也可先保存,后续在绘制时设置视窗大小)
  • onDrawFrame:刷新SurfaceTexture,并使用OpenGL绘制出来。

SurfaceTexture与Preview绑定

回到实现Preview.SurfaceProvider接口,绑定相机预览输出的事情。接下来要做的事情,就是将onSurfaceCreated时创建的SurfaceTexturePreview绑定,绑定后的SurfaceTexture能表示实时的预览帧。ps:该情况与使用TextureView作为相机预览的情况类似。

实现Preview.SurfaceProvider,并重写onSurfaceRequested方法:

override fun onSurfaceRequested(request: SurfaceRequest) {
    surfaceTexture?.setOnFrameAvailableListener(this)
    val surface = Surface(surfaceTexture)
    request.provideSurface(surface, executor) {
        surface.release()
        surfaceTexture?.release()
    }
}

其中,surfaceTexture即为onSurfaceCreated时创建的SurfaceTexture

至此,相机输出的预览画面,经过CameraXPreviewSurfaceTexture进行了绑定,预览帧的更新会在SurfaceTexture中得到体现。

此处还会设置SurfaceTexture.OnFrameAvailableListener,作为在新的预览帧刷新时,及时进行OpenGL的绘制。

override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
    cameraView.requestRender()
}

着色器

顶点着色器

//  顶点坐标
attribute vec4 vPosition;
//  纹理坐标
attribute vec4 vCoord;
//  传给片元着色器的像素点
varying vec2 aCoord;

void main() {
    gl_Position = vPosition;
    aCoord = vCoord.xy;
}

片元着色器

#extension GL_OES_EGL_image_external : require
//SurfaceTexture比较特殊
//float数据是什么精度的
precision mediump float;

//采样点的坐标
varying vec2 aCoord;

//采样器
uniform samplerExternalOES vTexture;

void main() {
    // 变量 接收像素值
    // texture2D:采样器 采集 aCoord的像素
    // 赋值给gl_FragColor
    /// 正常预览
    gl_FragColor = texture2D(vTexture, aCoord);
}

至此,整个预览就完成了,效果为下图。两个比较明显的问题:

  1. 预览画面旋转问题,摄像头默认为横屏状态下的输出,预览画面没有旋转
  2. 预览分辨率偏低

预览画面旋转

画面旋转可以通过改变纹理坐标的角度出发实现,简单的粗暴的做法是通过调整纹理坐标的顺序实现。但比较通用的做法则是通过获取SurfaceTexture的变换矩阵

onDrawFrame时,可以在updateTexImage后获取变换矩阵。之后在顶点着色器中,将变换矩阵与纹理坐标相乘,再将结果提供给片元着色器使用。

片元着色器:

surfaceTexture.updateTexImage()
surfaceTexture.getTransformMatrix(textureMatrix)
screenFilter?.setTransformMatrix(textureMatrix)

// ScreenFilter#onDrawFrame
GLES20.glUniformMatrix4fv(vMatrix, 1, false, mtx, 0)

顶点着色器:

//  顶点坐标
attribute vec4 vPosition;
//  纹理坐标
attribute vec4 vCoord;

uniform mat4 vMatrix;
//  传给片元着色器的像素点
varying vec2 aCoord;

void main() {
    gl_Position = vPosition;
    aCoord = (vMatrix * vec4(vCoord.x, vCoord.y, 1.0, 1.0)).xy;
}

预览分辨率调整

官方文档对于Preview.SurfaceProvider介绍中,示例代码有提到结合OpenGL的使用,其中有修改SurfaceTexture的操作。笔者从AndroidCode Search中查找到了一个关于CameraX结合OpenGL预览的示例代码。里面有提到需要调用SurfaceTexture#setDefaultBufferSize,设置尺寸。于是就有:

override fun onSurfaceRequested(request: SurfaceRequest) {
    // request.resolution可以为刚开始设置Preview的预览分辨率,640 * 480
    val resetTexture = resetPreviewTexture(request.resolution) ?: return
    val surface = Surface(resetTexture)
    request.provideSurface(surface, executor) {
        surface.release()
        surfaceTexture?.release()
    }
}

@WorkerThread
private fun resetPreviewTexture(size: Size): SurfaceTexture? {
    return this.surfaceTexture?.let { surfaceTexture ->
        surfaceTexture.setOnFrameAvailableListener(this)
        surfaceTexture.setDefaultBufferSize(size.width, size.height)
        surfaceTexture
    }
}

最终效果(ps:清晰度可能从截图上看不算明显)

尾巴

还有一个值得注意的点是:由于预览分辨率定了4:3,所以SurfaceView的宽高比也应该是4:3。可以通过onMeasure强制设置,也可通过xml布局设置。不然就可能会出现画面被拉伸的感觉…

最后

本文总结了如果利用CameraX结合OpenGL进行相机预览,当然使用Camera/Camera2api也是可行的,重点还是预览画面的获取与绑定。最后附上demo:xcyoung/opengl-camera

参考文章

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值