上一篇中(http://www.jianshu.com/p/113e4eedb752),我们已经了解了视频录制的大概流程,以及部分关键代码,在这一篇,我给大家介绍借助OpenGL来对视频图像进行处理的实现,并附上源码。

CPU、GPU、OpenGL

简单说明一下为什么要使用OpenGL。

CPU

图像数据是一个个的像素点,对图像数据的处理无非是对每个像素点进行计算后重新赋值,一般来说对每个像素点的计算都比较独立,计算也相对简单。CPU虽然计算能力强大,但是并行处理能力有限,对一张720P的图片,一共包含720*1280=921600个像素,要进行这么多次运算,CPU也要望洋兴叹了。

GPU

GPU与CPU相比最大的优势就是并行处理能力,一般移动端的GPU也要包含数千个处理单元,这些处理单元虽然计算能力比不上CPU,但是却可以同时处理几千个像素点。像素点数据的计算相对简单,而且可以同时处理几千个像素点,图像数据用GPU来做计算就非常适合了。

目前使用最广泛的2D、3D矢量图形沉浸API:OpenGL

OpenGL

用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API)。这个接口由近350个不同的函数调用组成,用来从简单的图形比特绘制复杂的三维景象。

Android系统自带了OpenGL的嵌入式版本:OpenGL ES

OpenGL 中使用shader程序对相机数据进行处理。

视频帧数据处理

如果只是简单录制原始画面的视频,那根本不需要这个环节,不也不需要用到OpenGL,而在现在多数的应用中,有涉及到视频的功能,基本都会做一些特效处理,如美颜、水印、贴纸,甚至加上人脸检测并带上面具等,这一下无疑会比原生视频对用户更有吸引力。

对图像处理首先要拿到元数据,处理完再交给编码器和屏幕显示,而在处理的过程中不应该没处理一个子环节以更新显示一次的。所以我们需要一个不可见的区域,用来接收相机数据,然后处理数据,在这里我们称它为 离屏渲染。

这里用一张图来描述这个过程:

4f63b2436401

image.png

接收相机数据

打开摄像头以后,我们需要为相机设置一个预览的SurfaceTexture接收来自相机的图像数据流。

1、创建SurfaceTexture

2、surfaceTexture绑定纹理OpenGL纹理

3、mCamera.setPreviewTexture(surfaceTexture);

至此,相机开启后数据就会更新到SurfaceTexture,通过surfaceTexture的回调OnFrameAvailableListener可以知道有新数据,然后调用surfaceTexture.updateTexImage把数据更新到绑定的OpenGL纹理中,即上图中的FrameBuffer1/FrameBufferTexture1。

如果不需要其他处理,则输出数据为FrameBuffer1/FrameBufferTexture1。

添加效果

这里以贴一张图为例子说明。

贴图:将2D图形上指定的矩形区域 绘制到 平面的指定区域上

4f63b2436401

引用微信文章的一张图

1、获取图片数据,渲染到2D纹理中

public static void createFrameBuff(int[] frameBuffer, int[] frameBufferTex, int width, int height) {

GLES20.glGenFramebuffers(1, frameBuffer, 0);

GLES20.glGenTextures(1, frameBufferTex, 0);

GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, frameBufferTex[0]);

GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);

GLESTools.checkGlError("createCamFrameBuff");

GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,

GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);

GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,

GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);

GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,

GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);

GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,

GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);

GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffer[0]);

GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, frameBufferTex[0], 0);

GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);

GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);

GLESTools.checkGlError("createCamFrameBuff");

}

2、映射绘制区域

这里有两种方法,

1)在绘制区域限制顶点范围,如:

{0, 0,

1, 0,

1, 1,

0, 0,

1, 1,

1, 0}

表示的是右上角的1/4区域,可以在可视区域限制任意大小范围。

这种情况下,图片和绘制区域就可以一一对应。

2)绘制区域设定为整个显示区域,不用对顶点做限制。

这种情况下,绘制时就要对叠加部分做判断,对于有旋转时,这种情况下可计算的范围也比较大。

3、旋转

这里讲解上面第二种情况的旋转。

4f63b2436401

旋转

如上图所示,未旋转时绘制区域为实线框,此时和绘制图形角度一样,可以看成同一二位坐标系上用平移计算坐标。当旋转θ角度时,绘制区域变成虚线框,此时与绘制图形在坐标上的存在角度差,需要做旋转变换。

P0 表示源图片上的点,P1 表示目标平面上的点,Rect表示目标平面上要显示图片的区域

当旋转θ角度时,

1)在平面上取Rect的中心点center

2)P1以center为中心旋转 -θ 角度,得到平面上的点P2,则P2可对应到源图片上可视区域的点P0

3)绘制时,将P0的点的颜色绘制到平面上P1点的位置上

旋转计算公式:

α = -θ

P2.x = (P1.x - center.x)*cos(α) - (P1.y - center.y)*sin(α) + center.x ;

P2.y = (P1.x - center.x)*sin(α) + (P1.y - center.y)*cos(α) + center.y ;

如图:

4f63b2436401

逆向寻找映射点

如果角度是动态变化的,为了防止对于旋转角度太敏感,可以对角度在一个小区间内只处理一种角度,这个要依时机情况而定。

对于旋转角度的实时计算,只要在一个坐标系中两个点的坐标即可算出:

deltaX:两个点的X坐标相减

deltaY:两个点的Y坐标相减

public static double calcAngle(double deltaX, double deltaY) {

if (deltaY== 0) {

return 0;

}

double tan = Math.atan(Math.abs((deltaY) / (deltaX)));

if (deltaX> 0 && deltaY> 0)//第一象限

{

return -tan;

}

else if (deltaX> 0 && deltaY< 0)//第二象限

{

return tan;

}

else if (deltaX< 0 && deltaY> 0)//第三象限

{

return tan - Math.PI;

}

else

{

return Math.PI - tan;

}

}

来看一下shader语言处理代码

precision highp float;

varying highp vec2 vCamTextureCoord;

uniform sampler2D uCamTexture;

uniform sampler2D uImageTexture;

uniform vec4 imageRect;

uniform float imageAngel;

vec2 rotate(vec2 p0, vec2 center, float angel)

{

float x2 = (p0.x - center.x)*cos(angel) - (p0.y - center.y)*sin(angel) + center.x ;

float y2 = (p0.x - center.x)*sin(angel) + (p0.y - center.y)*cos(angel) + center.y ;

return vec2(x2, y2);

}

void main(){

lowp vec4 c1 = texture2D(uCamTexture, vCamTextureCoord);

lowp vec2 vCamTextureCoord2 = vec2(vCamTextureCoord.x,1.0-vCamTextureCoord.y);

vec2 point = vCamTextureCoord2;

if(imageAngel != 0.0)

{

vec2 center = vec2((imageRect.r+imageRect.b)/2.0, (imageRect.g+imageRect.a)/2.0);

vec2 p2 = rotate(vCamTextureCoord2, center, -imageAngel);

point = p2;

}

if(point.x>imageRect.r && point.ximageRect.g && point.y

{

vec2 imagexy = vec2((point.x-imageRect.r)/(imageRect.b-imageRect.r),(point.y-imageRect.g)/(imageRect.a-imageRect.g));

lowp vec4 c2 = texture2D(uImageTexture, imagexy);

lowp vec4 outputColor = c2+c1*c1.a*(1.0-c2.a);

outputColor.a = 1.0;

gl_FragColor = outputColor;

}else{

gl_FragColor = c1;

}

}

来一张实图:

4f63b2436401

image.png

把处理后的数据输出到屏幕和编码器

预览时可以使用TextureView或GLSurfaceView,其中GLSurfaceView是包含了GL处理线程,而TextureView则需要自己创建。在这里我们以TextureView来介绍。

TextureView对应一个SurfaceTexture,使用SurfaceTexture创建了WindowSurface:

EGL14.eglCreateWindowSurface(EGLDisplay dpy,

EGLConfig config,

Object win, //SurfaceTexture作为此参数的值

int[] attrib_list,

int offset

)

所以通过将上述处理后的纹理渲染到TextureView的SurfaceTexture,然后调用

EGL14.eglSwapBuffers

更新屏幕上显示的画面。

对于编码器,同样的道理,MediaCodec有一个Surface,同样的对应:

EGL14.eglCreateWindowSurface(EGLDisplay dpy,

EGLConfig config,

Object win, //Surface作为此参数的值

int[] attrib_list,

int offset

)

同样,渲染,然后更新数据。

扩展:人脸检测

现在很多视频应用或者视频功能都有人脸检测+人脸动态贴纸,大多数是使用第三方的SDK,一般都需要MONEY,这是老板就会问了:能不能自己做,效果还要和他们一样?

OMG,你和我想的竟然是一样的,但是理想总是丰满的。试想一下,如果这个很容易实现的话,就会有很多的开放SDK,但实际没有。不过没关系,检测的SDK还是有的,只是功能差了点,比如讯飞的SDK,免费的,但只能识别人脸上的21个关键点,人家SenseTime可以做到106个点,听说最近又做到更多的关键点,这么NB,连鱼尾纹都能出现了,只是人家收钱的。

上面老李的图片上就是讯飞识别出俩的21个点 。

拿到关键点后,就可以自己计算要绘制的区域、旋转角度等,然后贴图就是了。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐