Android OpenGL开发实践 - GLSurfaceView对摄像头数据的再处理

8,577 阅读27分钟
原文链接: mp.weixin.qq.com

随着移动网络的快速发展,移动端网络速度慢和花费较高的瓶颈逐渐消失,直播和视频随着网络的发展快速兴起。在直播和视频和风口之下,如何获取移动端摄像头数据、如何对摄像头数据进行再处理以及如何保存处理后的数据成为移动端视频开发者的必修课。本文首先对GLSurfaceView相关知识进行讲解,然后介绍Android系统如何获取摄像头数据并利用GLSurfaceView渲染到屏幕上,在此基础上以一个黑白滤镜为例介绍拿到摄像头数据后如何对数据进行再处理,并利用GLSurfaceView展示给用户。

GLSurfaceView简介

OpenGL ES是OpenGL的一个子集,它针对 移动端或嵌入式系统做了部分精简,而Android系统中集成了OpenGL ES,方便我们通过其接口充分使用GPU的计算和渲染能力。

GLSurfaceView是管理OpenGL surface的一个特殊的View,它可以帮助我们把OpenGL的surface渲染到Android的View上,并且封装了很多创建OpenGL环境所需要的配置,使我们能够更方便地使用OpenGL。其实使用GLSurfaceView非常简单,只要实现GLSurfaceView.Renderer接口就好了,然后通过 GLSurfaceView.setRenderer(GLSurfaceView.Render renderer)方法把实现的接口传到GLSurfaceView即可。我们来看一个最简单的实现:

运行OpenGL程序需要创建OpenGL Context,即EGL Context,而GLSurfaceView的伟大之处就在于它为我们创建了一个OpenGL的渲染线程,此线程中已经包含了OpenGL 运行所需的上下文环境,GLSurfaceView.Renderer的三个回调方法就运行在OpenGL环境中,省去了复杂和冗长的OpenGL上下文环境的创建过程。下面我们来看看这三个回调:

public void onSurfaceCreated(GL10 glUnused, EGLConfig config)此方法会在Surface第一次创建或重建时调用。在GLSurfaceView attatch到父View的后,此方法会被调用。从这个回调方法名我们可以大概了解这个方法的用处,即在OpenGL surface被创建时的回调。

public void onSurfaceChanged(GL10 glUnused, int width, int height) 此方法在Surface大小变化时调用,例如横屏转为竖屏、GLSurfaceView大小变化等。在Surface第一次创建时也会调用。

public void onDrawFrame(GL10 glUnused) 此方法在渲染一帧图像时调用。在任意时间调用GLSurfaceView的 requestRender()方法后,GLSurfaceView会优先执行已保存在GL线程队列中的Runnable,然后调用此onDrawFrame(GL10 glUnused)方法渲染图像。GL线程队列中的所有Runnable和 onDrawFrame方法的调用都执行在GL线程中。

另外,对于上面接口的调用时机,其实有两种方式可以触发onDrawFrame的调用。GLSurfaceView有接口 GLSurfaceView.setRenderMode(int renderMode)可以设置是连续渲染还是按需渲染。两种模式分别对应下面两个变量:

GLSurfaceView.RENDERMODE_WHEN_DIRTYGLSufaceView.RENDERMODE_CONTINUOUSLY

按需渲染就是前面提到的,在用户调用GLSurfaceView.requestRender()方法时才会调用 onDrawFrame刷新渲染;连续渲染则不依赖于用户调用,GL线程会每隔一段时间自动刷新渲染。连续渲染消耗GPU资源更多,对本文将要讨论的对摄像头数据的再处理,只需要在摄像头数据回调时再刷新渲染即可,所以本文中都将渲染模式设置为按需渲染。

总结一下,GLSurfaceView主要包括以下能力:

  1. 提供一个OpenGL的渲染线程,以防止渲染阻塞主线程。

  2. 提供连续渲染或按需渲染能力。

  3. 封装EGL相关资源和创建和释放,极大地简化了OpenGL与窗口系统接口的使用方式。

获取摄像头数据

获取摄像头数据有一般有两种方式,一种是为相机设置预览的SurfaceTexture,通过回调获得当前可用的摄像头纹理,另一种是为相机设置Camera.PreviewCallback回调,通过回调拿到YUV数据。后一种情况下得到YUV数据格式默认为NV21,也可以通过 parameter.setPreviewFormat(ImageFormat format)来指定YUV数据格式。一般来说,NV21和YV12两种格式是所有Android机型都支持的,其他格式可能在不同机型上有兼容性问题。YUV数据格式不是本文关注的重点,在此不对其格式及兼容性作详细说明。

要对摄像头数据做再处理,首先要拿到摄像头数据。我们先来看看打开相机的最简单逻辑:

设置相机参数并打开相机的主要步骤有以下几点:

  1. 首先需要选择打开哪个摄像头。目前市面上的手机一般有前后两个摄像头,我们首先要确认打开哪个摄像头、找到相应的摄像头id,然后才能调用Camera.open(int cameraId)打开指定的摄像头。选取摄像头的代码如下所示:CameraInfo中包含两个const值: CAMERA_FACING_FRONTCAMERA_FACING_BACK,分别标识前置和后置摄像头摄像头。本文中我们选择使用前置摄像头。

  2. 调用Camera.open(int cameraId)打开前面选择的前置摄像头。

  3. 选取相机预览分辨率。调用相机参数的cp.getSuppoortedPreviewSizes()方法获取摄像头支持的预览分辨率列表,然后从中选取一个适合的大小,调用 cp.setPreviewSize(size.width, size.height)设置相机预览分辨率参数。

  4. 调用mCamera.setParameters(cp)应用前面设置好的相机参数。

做过Android Camera开发的人都知道,一般来说,相机的预览(preview)数据流是要输出到一个可见的SurfaceView上的,然后通过Camera.PreviewCallback的public void onPreviewFrame(byte[] data, Camera camera)方法来获得图像帧数据的拷贝。这就存在一些问题,比如希望对每一帧图像数据进行一些处理后再显示到屏幕上,在Android3.0之前是没有办法做到的。或者说非要做的话也需要用一些小技巧,比如用其他控件把SurfaceView给挡住,但是这个显示原始相机图像流的SurfaceView其实是永远存在的,也就是说被挡住的SurfaceView依然在接收从相机传过来的图像数据,而且一直按照一定帧率去刷新是要消耗CPU的。如果一些参数设置的不恰当,后面隐藏的SurfaceView还有可能会露出来。另外从Camera.PreviewCallback拿到的数据如果需要处理也需要用OpenCV等库在CPU上处理,对每一帧都需要处理的实时相机流数据是很消耗CPU资源的,因此这些小技巧并不是好办法。SurfaceTexture是从Android3.0(API 11)加入的一个新类。这个类跟SurfaceView很像,可以从相机预览或者视频解码里面获取图像流。和SurfaceView不同的是,SurfaceTexture在接收图像流之后,不需要显示出来。这样就好办多了,我们可以用SurfaceTexture接收来自相机的图像数据流,然后从SurfaceTexture中取得图像帧的拷贝进行处理,处理完毕后再送给一个SurfaceView用于显示即可。

一般来说,在CPU上处理图片是比较慢的,现在使用最广泛的图片处理库OpenCV,即使在底层做了编译优化,要做到实时处理720P的图像数据还是吃不消,这时候就要发挥GPU的强大能力了。图像数据无非是一个个的像素点,对图像数据的处理无非是对每个像素点进行计算后重新赋值,一般来说对每个像素点的计算都比较独立,计算也相对简单。CPU虽然计算能力强大,但是并行处理能力有限,对一张720P的图片,一共包含720*1280=921600个像素,要进行这么多次运算,CPU也要望洋兴叹了。GPU与CPU相比最大的优势就是并行处理能力,一般移动端的GPU也要包含数千个处理单元,这些处理单元虽然计算能力比不上CPU,但是却可以同时处理几千个像素点。像素点数据的计算相对简单,而且可以同时处理几千个像素点,图像数据用GPU来做计算就非常适合了。而怎么使用GPU呢?这就要介绍到目前使用最广泛的2D、3D矢量图形沉浸API:OpenGL了。

OpenGL是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API)。这个接口由近350个不同的函数调用组成,用来从简单的图形比特绘制复杂的三维景象。Android系统自带了OpenGL的嵌入式版本:OpenGL ES,相比完整的OpenGL版本接口要少了一些接口,但对一般移动端处理的需求来说足够了。熟悉OpenGL的编程规范,需要学习的东西很多,本文只讲解如何搭建OpenGL渲染相机数据流的过程,以及举例用一个简单的OpenGL的shader程序对相机数据做处理,就不详细讲解OpenGL的编程规范了,有兴趣的同学们可以自己上网搜搜相关教程。OpenGL是实时对摄像头数据做处理的核心,希望以后做这方面工作的同学确实需要好好了解和学习。

言归正传,继续我们的教程。打开摄像头以后,我们需要为相机设置一个预览的SurfaceTexture接收来自相机的图像数据流。SurfaceTexture和OpenGL ES一起使用可以创造出无限可能,下面我们先来看看如何创建一个OpenGL纹理并把它绑定到一个SurfaceTexture,然后将该SurfaceTexture设置为相机预览数据接收器:

经过以上打开相机和设置预览两步,相机就可以正常工作了,相机会源源不断地把摄像头帧数据更新到SurfaceTexture上,即更新到对应的OpenGL纹理上。但是此时我们并不知道相机数据帧何时会更新到SurfaceTexture,也没有在GLSurfaceView的OnDrawFrame方法中将更新后的纹理渲染到屏幕,所以并不能在屏幕上看到预览画面。下面我们先来看看相机如何通知SurfaceTexture其预览数据已更新。

设置SurfaceTexture回调,通知摄像头预览数据已更新

SurfaceTexture有一个很重要的回调:OnFrameAvailableListener。通过名字也可以看出该回调的调用时机,当相机有新的预览帧数据时,此回调会被调用。所以我们为前面的SurfaceTexture设置一个回调,来通知我们相机预览数据已更新:

SurfaceTexture的updateTexImage方法会更新接收到的预览数据到其绑定的OpenGL纹理中。该纹理会默认绑定到OpenGL Context的GL_TEXTURE_EXTERNAL_OES纹理目标对象中。GL_TEXTURE_EXTERNAL_OES是OpenGL中一个特殊的纹理目标对象,与GL_TEXTURE_2D是同级的,有兴趣的同学可以网上搜教程深入了解一下。调用此方法后,我们前面创建的OpenGL纹理中就有了最新的相机预览数据了。要注意的是,此方法只能在生成该纹理的OpenGL线程中调用,所以这个地方通过GLSurfaceView的queueEvent方法将该调用放入GL线程队列中执行。

SurfaceTexture的getTransformMatrix方法可以获取到图像数据流的坐标变换矩阵。一般情况下,相机流数据方向并不是用户正常拿手机的竖屏方向,且前后摄像头数据还存在镜像的问题。如何对摄像头数据进行旋转或镜像得到旋转正确的数据呢?getTransformMatrix获取到的变换矩阵可以帮助我们完成这个看起来很复杂的任务。其实我们不用关心这个矩阵的值到底是什么,只需要在OpenGL 着色器处理顶点数据时直接将其传入作为纹理坐标变换矩阵即可。终于到了我们图像处理的核心:OpenGL着色器程序了。在介绍处理相机流数据的OpenGL着色器之前,我们先来简单了解一下OpenGL的渲染管线,下面这张图是渲染管线每个阶段的抽象显示,蓝色部分是可编程部分,我们可以在这几个部分自己编写着色器程序控制渲染。

我们简单介绍一下这几个阶段。

图形渲染管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是进行坐标变换,同时顶点着色器允许我们对顶点属性进行一些基本处理。

图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并所有的点装配成指定图元的形状:点、线、三角形。

图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。本文中没有对图元做变换,故没有用到几何着色器。

几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。

片段着色器处理完后,最终的对象将会被传到最后一个阶段,我们叫做Alpha测试和混合(Blending)阶段。这个阶段检测片段的对应的深度(和模板(Stencil))值,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。此阶段涉及到深度和模板缓冲区以及OpenGL颜色混合,细说起来又可以写一篇文章了。本文中因为只对相机流的2D图像做全屏处理,片段着色器颜色采用完全替换的方式,不使用深度和模板缓冲区及OpenGL颜色混合模式,在此就不详细讨论该阶段的处理了。

在上图显示的三个可编程阶段中,我们对相机流数据的处理用到了顶点着色器(Vertex Shader)和片段着色器(Fragment Shader),下面我们就来重点看看如何编写顶点着色器和片段着色器,以相机纹理和变换矩阵作为输入,把相机流数据渲染在GSurfaceView上。

编写及初始化OpenGL着色器程序

着色器程序语法与C语言很像,顶点着色器和片段着色器都包含一个main函数,main函数外定义了三种不同类型的变量:uniform、attribute和varying。uniform变量是外部程序传递给着色器的变量,类似C语言的const变量,在OpenGL着色器程序的一次渲染过程中保持不变;attribute变量只在顶点着色器中使用,一般用来表示一些顶点的数据,如顶点坐标,法线,纹理坐标,顶点颜色等;varying变量是顶点着色器和片段着色器之前传递数据用的,它作为顶点着色器的输出,经过图元装配和栅格化后,作为片段着色器的输入。着色器中也内置了一些变量和函数,本文中介绍两个最最常用的内置变量:

  1. gl_Position:顶点着色器中必须对其赋值,其输入序列作为图元装配过程的组成点、线或三角形的坐标序列。

  2. gl_FragColor:片段着色器中必须对其赋值,作为像素点的输出值。

要了解OpenGL着色器语言的使用,本文中的内容只是冰山一角,希望从事OpenGL开发的同学需要花大量时间去深入学习,本文中只对相机流数据用到的着色器程序进行简单介绍。下面我们就来看看相机数据流处理的顶点着色器和片段着色器程序:

顶点着色器主要对顶点坐标进行变换,在相机预览的例子中,我们引入了两个变换矩阵:uMVPMatrixuTexMatrix。其中uMVPMatrix是投影矩阵,主要进行3D及NDC坐标变换,本文中对全屏相机流数据做处理,传入全屏坐标,且不进行变换,故 uMVPMatrix传入单位矩阵即可;uTexMatrix是纹理变换矩阵,前文中我们拿到了摄像头纹理的变换矩阵 mTransformMatrix这时候就派上用场了,uTexMatrix变量传入 mTransformMatrix,相机纹理坐标经过其变换后即可得到旋转正向的坐标序列。

片段着色器对目标点进行颜色赋值。我们在前面拿到了摄像头纹理mPreviewTextureId[0],需要注意的是,在Android中Camera产生的预览纹理是以一种特殊的格式传送的,因此片段着色器里的纹理类型并不是普通的sampler2D,而是samplerExternalOES, 在着色器的头部也必须声明OES 的扩展。除此之外,external OES的纹理和Sampler2D在使用时没有差别。

有了顶点着色器和片段着色器程序,我们怎么把它们加在OpenGL渲染管线中运行起来呢?OpenGL着色器程序和普通程序的运行准备过程差不多,也需要通过编译和链接后才可使用。下面就是编译shader和链接program的代码:

经过以上步骤,我们处理相机流数据的顶点着色器和片段着色器程序就准备好了,最后得到的program就是一个OpenGL ES程序对象,我们可以调用glUseProgram函数,用刚创建的程序对象作为它的参数,以激活这个程序对象:

GLES20.glUseProgram(program);

在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)了。下面还有一个很重要的问题:我们怎么把前面得到的相机纹理和纹理坐标变换矩阵传递给OpenGL ES程序呢?下面我们就来看看如何在OpenGL ES程序中传递各种不同类型的参数。

为着色器程序传递参数

前面提到,着色器中有三种类型的参数:uniform、attribute和varying。varying参数是顶点着色器和片段着色器之前传递参数用的,对外部程序来可见,所以外部程序能传入着色器的参数只有uniform和attribute类型。

不管是uniform还是attribute参数,都需要先拿到其对应的句柄才能进行传参操作。这两种类型参数获取句柄的方法略有不同,以获取上文中attribute类型参数aPosition和uniform类型参数uTexMatrix为例,获取句柄方法分别如下:

attribute类型参数都需要用glGetAttribLocation获取句柄,而uniform参数则是用glGetUniformLocation获取句柄。

获取到句柄后,接下来就是把真正的参数值传进句柄了。我们先来看看两个attribute参数:aPosition和aTextureCoord的传值:

此处涉及到两个OpenGL ES相关的函数调用:

glEnableVertexAttribArray调用后允许顶点着色器读取句柄对应的GPU数据。默认情况下,出于性能考虑,所有顶点着色器的attribute变量都是关闭的,意味着数据在着色器端是不可见的,哪怕数据已经上传到GPU.由glEnableVertexAttribArray启用指定属性,才可在顶点着色器中访问逐顶点的attribute数据。glVertexAttribPointer或VBO只是建立CPU和GPU之间的逻辑连接,从而实现了CPU数据上传至GPU。但是,数据在GPU端是否可见,即着色器能否读取到数据,由是否启用了对应的属性决定,这就是glEnableVertexAttribArray的功能,允许顶点着色器读取GPU数据。

glVertexAttribPointer函数的参数非常多:第一个参数指定句柄;第二个参数指定顶点属性的大小,每个坐标点包含x和y两个float值;第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec都是由浮点数值组成的);第四个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间,这里我们把它设置为GL_FALSE;第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔,由于下个组位置数据在2个GLfloat之后,我们把步长设置为2 sizeof(GLfloat);最后一个参数就是数据buffer。

uniform参数的传递相对简单,对于uMVPMatrix和uTexMatrix参数,获取到句柄后直接用下面的方法即可传入:

OpenGL ES有很多glUniformX的API,就是不同类型的uniform参数的传递方法。samplerExternalOES纹理或sampler2D纹理的传递方法稍微复杂一点:

纹理参数传递时,需要先绑定某个纹理单元,将纹理输入绑定到纹理单元的目标对象上,然后调用glUniform1i设置其参数为该纹理单元。

至此,我们的着色器程序已准备好,所有参数也已设置完毕。万事俱备,只欠东风,下面我们来看看最后一步:将相机流数据渲染到屏幕上。

渲染帧数据

前面步骤都完成后,调用OpenGL ES的渲染指令倒是比较简单了,只有两行代码:

前面提到,OpenGL ES的基本图元有点、线和面(三角形),我们在glDrawArrays调用中传入的第一个参数就是指定基本图元以何种方式组装。组装方式有很多种,枚举值如下:

GL_POINTS 画离散的点GL_LINES 画线(每两个点连成一条线)GL_LINE_STRIP 画线(所有点相互相连,首尾不相连)GL_LINE_LOOP 画线(所有点相互相连,首尾相连)GL_TRIANGLES 填充三角形(将每三个点围成的三角形进行填充,相邻的点之间不填充)GL_TRIANGLE_STRIP 填充三角形(将每三个点围成的三角形进行填充,相邻的点之间填充)GL_TRIANGLE_FAN 填充三角形(以第一个点为顶点,之后每两个点合起来围成的三角形进行填充,相邻的点之间填充)

本文是以两个三角形组成一个矩形的方式把相机纹理渲染到屏幕上的,在这里我们用了GL_TRIANGLE_FAN图元组装方式。

其他组装方式本文不详细介绍,有兴趣的同学可以自己深入了解一下。

经过以上步骤,我们应该可以在屏幕GLSurfaceView区域内看到相机预览数据了,赞!

对摄像头数据的再处理

前面我们已经拿到了摄像头纹理并显示在屏幕上,但我们显示到屏幕上的是摄像头原始数据纹理,中间没有做任何其他处理。如果我们想将摄像头原始纹理做一些处理,比如把彩色图变成黑白图像,然后再显示到屏幕上,应该怎么做呢?其实和我们前面将相机纹理渲染到屏幕的过程是一样的!还记得我们前面的片段着色器吗?我们直接调用gl_FragColor = texture2D(sTexture, vTextureCoord);将目标颜色赋值为输入纹理颜色,所以我们在屏幕上看到的是原图。下面我们来看一个新的片段着色器,它用一个简单的公式对当前像素点的rgb值进行加权,然后将rgb值都设置为此加权值形成灰度图的效果:

对摄像头数据的再处理过程,其实可以看做两个着色器程序串行执行的过程。我们在前面处理摄像头纹理的着色器渲染完成后,暂时保存输出纹理,然后再用上面灰度图的着色器程序将此输出纹理作为输入,再渲染到屏幕上,即可在屏幕上看到对原始彩色纹理处理后生成灰度图纹理的效果,这其实就是我们对摄像头数据的再处理步骤。初始化片段着色器并传参的步骤前面已经详细介绍,对上面的片段着色器再做一遍即可。

这里需要注意的是,暂存第一个着色器的输出纹理需要用到OpenGL的另一个概念:Frame Buffer。 在OpenGL渲染管线中,几何数据和纹理经过多次转化和多次测试,最后以二维像素的形式显示在屏幕上。OpenGL管线的最终渲染目的地被称作帧缓存(framebuffer)。一般情况下,帧缓存完全由window系统生成和管理,由OpenGL使用。这个默认的帧缓存被称作“window系统生成”(window-system-provided)的帧缓存。在OpenGL扩展中,GL_EXT_framebuffer_object提供了一种创建额外的不能显示的帧缓存对象的接口。为了和默认的“window系统生成”的帧缓存区别,这种帧缓冲成为应用程序帧缓存(application-createdframebuffer)。通过使用帧缓存对象(FBO),OpenGL可以将显示输出到引用程序帧缓存对象,而不是传统的“window系统生成”帧缓存。而且,它完全受OpenGL控制。

在一个帧缓存对象中有多个颜色关联点(GL_COLOR_ATTACHMENT0,…,GL_COLOR_ATTACHMENTn),一个深度关联点(GL_DEPTH_ATTACHMENT),和一个模板关联点(GL_STENCIL_ATTACHMENT)。我们可以把纹理图像(Texture Images)或渲染缓存图像(RenderBuffer Images)绑定到这些关联点上。它们之间的关系如下图所示:

GLSurfaceView的onDrawFrame回调中,默认是绑定了window系统生成的FBO的,这个FBO对应屏幕显示,即0号FBO。只要我们中间不切换FBO,所有的glDrawArray或glDrawElements指令调用都是将目标渲染到这个0号FBO的。而对我们对摄像头数据进行处理后再显示到屏幕的需求来说,我们不能将两个着色器程序都直接渲染到屏幕,第一个着色器程序渲染的结果需要输出到一个中间FBO上,然后再切回屏幕对应的0号FBO渲染第二个着色器程序。下面我们来看看如何生成一个中间FBO并绑定到一个纹理图像,这样第一个着色器程序的输出并不直接渲染到屏幕,而是渲染到此FBO绑定的纹理上,然后此纹理再作为灰度图着色器程序的输入,最终渲染到屏幕FBO上。

前面提到FBO可以绑定到纹理对象或者RenderBuffer对象,RenderBuffer是以内部格式存储的经过渲染优化的对象,它的渲染速度更快,缺点是无法对渲染进果进行重采样。如果不需要对FBO的输出再做下一步采样处理,就可以用RenderBuffer。在我们的例子中,因为我们要暂存相机流处理着色器的渲染结果,并作为灰度黑着色器程序的输入,即要对此输出结果进行采样,所以我们必须要用FBO绑定纹理对象的方式。生成FBO并将其绑定到一个纹理的代码如下:

经过上面的代码后,着色器程序的渲染输出都会定位到新生成的FBO上。接下来我们调用相机流处理着色器的渲染流程,渲染完成后,我们再调用GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);即可切回到屏幕对应的0号FBO,然后再以 texture[0]作为灰度图着色器的纹理输入,并调用其渲染流程,我们就可以在屏幕上看到相机流的灰图度效果了。

获取摄像头数据的补充

本文中我们获取摄像头数据是采用SurfaceTexture绑定纹理,相机流数据直接更新到OES纹理上的方式。文章一开始我们提到,获取相机预览数据还有另一种方式,通过为相机设置Camera.PreviewCallback回调拿到YUV格式数据,这种情况下得到YUV数据格式默认为NV21,也可以通过 parameter.setPreviewFormat(ImageFormat format)来指定YUV数据格式。

对图像数据的处理,为了达到实时性的要求,一般情况下还是需要用OpenGL在GPU上完成。所以在拿到相机YUV数据以后,我们需要把YUV数据转换成GPU可用的普通RGBA纹理才方便对数据进行再处理。从相机拿到的YUV数据格式是NV21或NV12,这种格式下,Y数据在一个平面(planar)上,UV数据在一个平面上。这种格式的YUV字节流转换成RGBA纹理一般有两种方式:

  1. UV所在的一个平面拆成U和V数据分别在一个平面上,然后将Y、U、V三个平面作为三个GL_LUMINANCE的纹理作为输入,然后用YUV到RGB的转换矩阵在着色器程序中实现。

  2. 将YUV数据转换成类似RGBA的每个像素点包含YUVA格式的字节流,然后用YUV到RGB的转换矩阵在着色器程序中实现。

两种方式都需要先在CPU上对相机YUV格式字节流做一些预处理,然后上载到GPU上用着色器程序完成转换。这个过程涉及的预处理和着色器程序可以单独再拿一篇文章来写,篇幅有限,本文中就不详细介绍了。

总结

至此,我们经过了选取并打开摄像头、设置相机预览SurfaceTexture、获取相机流数据纹理、使用着色器渲染纹理到屏幕、切换FrameBuffer等等过程,中间很多内容因为篇幅原因没有详细介绍,有兴趣的同学可以自行翻查资料学习。OpenGL在安卓端的应用非常广泛,在移动端直播和视频app中,获取摄像头数据并进行再处理是非常常见的场景,需要充分了解摄像头数据的获取方式、OpenGL的相关知识以及在Android端的使用方式,尤其OpenGL的编程方式与面向方法的编程方式不同,需要了解其渲染管线、shader的参与时机和用法、FrameBuffer相关的知识,才能在现实应用中充分发挥GPU的强大能力,希望本文能对有相关开发需求的同学提供帮助。


参考文献

OpenGL渲染管线OES纹理扩展glEnableVertexAttribArray的作用基本图形定义OpenGL帧缓存YUV与RGB格式转换


作者简介:kevinxing(邢雪源),天天P图Android工程师