android gpu 视频编码,Android Mp4视频录制(OpenGL实现篇,附DEMO)
上一篇中(http://www.jianshu.com/p/113e4eedb752),我们已经了解了视频录制的大概流程,以及部分关键代码,在这一篇,我给大家介绍借助OpenGL来对视频图像进行处理的实现,并附上源码。CPU、GPU、OpenGL简单说明一下为什么要使用OpenGL。CPU图像数据是一个个的像素点,对图像数据的处理无非是对每个像素点进行计算后重新赋值,一般来说对每个像素点的计算都比
上一篇中(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,而在现在多数的应用中,有涉及到视频的功能,基本都会做一些特效处理,如美颜、水印、贴纸,甚至加上人脸检测并带上面具等,这一下无疑会比原生视频对用户更有吸引力。
对图像处理首先要拿到元数据,处理完再交给编码器和屏幕显示,而在处理的过程中不应该没处理一个子环节以更新显示一次的。所以我们需要一个不可见的区域,用来接收相机数据,然后处理数据,在这里我们称它为 离屏渲染。
这里用一张图来描述这个过程:
image.png
接收相机数据
打开摄像头以后,我们需要为相机设置一个预览的SurfaceTexture接收来自相机的图像数据流。
1、创建SurfaceTexture
2、surfaceTexture绑定纹理OpenGL纹理
3、mCamera.setPreviewTexture(surfaceTexture);
至此,相机开启后数据就会更新到SurfaceTexture,通过surfaceTexture的回调OnFrameAvailableListener可以知道有新数据,然后调用surfaceTexture.updateTexImage把数据更新到绑定的OpenGL纹理中,即上图中的FrameBuffer1/FrameBufferTexture1。
如果不需要其他处理,则输出数据为FrameBuffer1/FrameBufferTexture1。
添加效果
这里以贴一张图为例子说明。
贴图:将2D图形上指定的矩形区域 绘制到 平面的指定区域上
引用微信文章的一张图
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、旋转
这里讲解上面第二种情况的旋转。
旋转
如上图所示,未旋转时绘制区域为实线框,此时和绘制图形角度一样,可以看成同一二位坐标系上用平移计算坐标。当旋转θ角度时,绘制区域变成虚线框,此时与绘制图形在坐标上的存在角度差,需要做旋转变换。
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 ;
如图:
逆向寻找映射点
如果角度是动态变化的,为了防止对于旋转角度太敏感,可以对角度在一个小区间内只处理一种角度,这个要依时机情况而定。
对于旋转角度的实时计算,只要在一个坐标系中两个点的坐标即可算出:
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;
}
}
来一张实图:
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个点 。
拿到关键点后,就可以自己计算要绘制的区域、旋转角度等,然后贴图就是了。
更多推荐
所有评论(0)