CameraX + OpenGL预览的全新版本

前言

一直都想研究一下图像处理,实现一些简单的相机预览及拍照功能。顺便了解一下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时需要利用OpenGLapi创建一个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作为与OpenGLGLSL脚本绑定的逻辑,它的初始化时会执行:顶点坐标纹理坐标的内存空间创建顶点着色器、片元着色器的程序创建,及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]):将参数(包括最新一帧)传递给着色器程序,并通知其绘制。

总结:

  • onSurfaceCreatedSurfaceTexture的创建以及与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. 预览分辨率偏低

Screenshot_2021-11-27-23-34-45-19_00ee5bc901951a8110e840ce0a9eee63.jpg

预览画面旋转

画面旋转可以通过改变纹理坐标的角度出发实现,简单的粗暴的做法是通过调整纹理坐标的顺序实现。但比较通用的做法则是通过获取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;
}
复制代码

旋转后的效果:

Screenshot_2021-11-27-23-51-59-11_00ee5bc901951a8110e840ce0a9eee63.jpg

预览分辨率调整

官方文档对于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:清晰度可能从截图上看不算明显

Screenshot_2021-11-28-00-02-13-88_00ee5bc901951a8110e840ce0a9eee63.jpg

尾巴

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

最后

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

参考文章

官方文档

CameraX入门(拍照、存储展示、切换前后摄像头、手电筒、闪光灯、手势伸缩、双击放大缩小)

CameraX和OpenGL的融合(cameraX预览数据openGL渲染)

抖音分屏效果:CameraX与OpenGL的结合

OpenGL 入门到放弃3-- 用openGL展示相机预览

Guess you like

Origin juejin.im/post/7035293015757307918