Android视频编辑器(四)通过OpenGL给视频增加不同滤镜效果
来源:互联网 发布:优化布林线 编辑:程序博客网 时间:2024/05/18 00:57
前言
在上面的几篇文章中,我们实现了录制视频、通过opengl在录制过程中和给本地视频添加水印和美颜效果,还没看过的童鞋,可以看该系列的前面三篇文章。而这篇博客,我们来实现给视频添加各种各样的滤镜。现如今给视频加各种不同的滤镜对各视频类app来说,已经是标配功能。而添加各类滤镜可以使我们拍摄的视频更美观漂亮。所以这篇博客,我们就要来实现给视频添加上除美颜之外的其他滤镜。
本系列的文章包括如下:
1、android视频编辑器之视频录制、断点续录、对焦等
2、android视频编辑器之录制过程中加水印和美白效果
3、android视频编辑器之本地视频加美白效果和加视频水印
4、android视频编辑器之通过OpenGL给视频增加不同的滤镜效果
5、android视频编辑器之音频编解码、mono转stereo、音频混音、音频音量调节
6、android视频编辑器之通过OpenGL做不同视频的拼接
7、android视频编辑器之音视频裁剪、增加背景音乐等
ps: 本项目的大部分滤镜来自于MagicCamera这个开源项目,非常感谢原作者的分享,有兴趣的朋友可以去看看。
用新滤镜替换美颜滤镜
在以前的博客中,我们实现了一个MagicBeautyFilter,来对视频进行美颜处理,那从原理上来说,只要我们更换掉这个MagicBeautyFilter,采用其他滤镜的Filter,即可实现添加完全不同的一种滤镜。
如此的话,那我们先来简单来更换一个怀旧风的滤镜。
首先,新建一个MagicAntiqueFilter类,继承GPUImageFilter,在MagicAntiqueFilter的构造函数中,传入怀旧风的fragmentShader。
public MagicAntiqueFilter(){ super(NO_FILTER_VERTEX_SHADER, OpenGlUtils.readShaderFromRawResource(R.raw.antique)); }在OpenGL的shader文件中,每个滤镜都需要两个shader文件,其中vertexShader是控制每个像素点的位置的,而fragmentShader就是控制每个像素点的颜色的,而我们这里传入的R.raw.antique文件就是怀旧风的颜色shader文件。
而fragmentShader最重要的是,在代码的最后,给gl_FragColor赋值,从而决定像素点的颜色,也就是说每个fragmentShader文件的最后一行代码,肯定是类似如下的赋值代码:
gl_FragColor = vec4(textureColor.r, textureColor.g, textureColor.b, 1.0);至于具体的滤镜实现逻辑,有兴趣的朋友,就可以深入了解下OpenGL的shader文件编写规则,这里就不做深入了,不然每个滤镜的实现逻辑可能都能写一篇博客了。滤镜的实现原理可以见 滤镜制作的基本方法(一)
实现了MagicAntiqueFilter类之后,我们简单的把原来的MagicBeautyFilter替换为新的滤镜。在CameraDrawer类中进行替换,然后加上一些控制逻辑
// mBeautyFilter = new MagicBeautyFilter();mBeautyFilter = new MagicAntiqueFilter();
if (mBeautyFilter != null && isOpen){ EasyGlUtils.bindFrameTexture(fFrame[0],fTexture[0]); GLES20.glViewport(0,0,mPreviewWidth,mPreviewHeight); mBeautyFilter.onDrawFrame(mBeFilter.getOutputTexture()); EasyGlUtils.unBindFrameBuffer(); mProcessFilter.setTextureId(fTexture[0]); }else { mProcessFilter.setTextureId(mBeFilter.getOutputTexture()); }
private boolean isOpen = true; public void changeFilter(){ isOpen = !isOpen; }这个changeFiler方法就是暴露给上层的控制api。从而决定是否加该滤镜。
然后具体的效果就如下
难道这样就完了?我们又不讲解OpenGL滤镜的具体实现,上面这样就可以实现给视频加各种其他的滤镜,那这篇文章岂不是看起来不就虎头蛇尾的。。。必不可能让本篇博客如此虎头蛇尾,那下面我们做什么呢?既然如此的话,我们下面就来实现一个看起来很酷炫的功能,在传统的app更换滤镜的时候,都是选择相应的图标,然后进行滤镜的更换,那我们必不可能如此low,所以呢,我们来实现vue的滤镜更换方式,通过在屏幕上左右滑动,从而实现更换滤镜。
也就是如下效果
ps:不要在意右下角的那个奇葩水印,这是视频本身的原因。图片来自于vue这个应用选取本地视频时候的手机截图。
左边就是有滤镜,而右边就是没有滤镜的效果。这样可以让用户明显的看出不同的滤镜有什么效果,方便进行对比。
实现左右滑动切换滤镜
那么我们来分析一下,如何才能实现这样的效果呢?
首先,我们肯定需要1个以上的滤镜,才能进行切换,所以先将来自于MagicCamera的各种滤镜放进项目,如下是他们的Filter类和shader文件。所以我们去MagicCamera中借用了一些滤镜过来。
然后,我们这个就比以前的要复杂一些了,我们先需要保存三个滤镜,一个是curFilter,一个左边的滤镜leftFilter,以及一个右边的滤镜rightFilter。然后监听界面的滑动事件,如果是向左滑,那么在绘制的时候,左边部分我们加上curFilter,右边部分加上rightFilter,同理向右滑,我们右边部分加上curFilter,而左边部分加上leftFilter。这里有一个核心的问题,如何给同一帧添加两个不同的滤镜,其实在OpenGL中,绘制的时候,除了可以通过GLES20.glViewport方法设置画面的大小,还有一个GLES20.glScissor函数,可以对画面进行裁剪绘制,也就是通过裁剪测试可以是渲染的时候用来限制绘制区域,可以在屏幕(帧缓冲)上指定一个矩形区域,不在此矩形区域内的片元将被丢弃,只有在矩形区域内的片元才有机会最终进入帧缓冲。
开启裁剪测试功能
GLES20.glEnable(GLES20.GL_SCISSOR_TEST);设置要裁剪的区域
GLES20.glScissor(0,0,offset,height);关闭裁剪测试功能
GLES20.glDisable(GLES20.GL_SCISSOR_TEST);通过裁剪功能,我们就可以把同一个画面帧的数据,通过进行裁剪,来加上不同的滤镜,进行绘制。最终实现我们想要的效果。
既然分析完毕了,那我们就来一步步实现我们的代码
功能实现
首先,新建一个SlideGpufilterGroup类,该类就是我们来实现这项功能的核心类
首先,新建一个SlideGpufilterGroup类,该类就是我们来实现这项功能的核心类
public class SlideGpufilterGroup { }然后,我们需要一个数组来标记我们所有的滤镜
private MagicFilterType[] types = new MagicFilterType[]{ MagicFilterType.NONE, MagicFilterType.WARM, MagicFilterType.ANTIQUE, MagicFilterType.INKWELL, MagicFilterType.BRANNAN, MagicFilterType.N1977, MagicFilterType.FREUD, MagicFilterType.HEFE, MagicFilterType.HUDSON, MagicFilterType.NASHVILLE, MagicFilterType.COOL };这里的MagicFilterType,就是我们定义的用来表示滤镜的枚举。
然后定义三个滤镜,分别表示当前的滤镜,左边一个的滤镜,和右边一个的滤镜,对外提供他们的初始化和设置size。
private GPUImageFilter curFilter; private GPUImageFilter leftFilter; private GPUImageFilter rightFilter; public void init() { curFilter.init(); leftFilter.init(); rightFilter.init(); } private void onFilterSizeChanged(int width, int height) { curFilter.onInputSizeChanged(width, height); leftFilter.onInputSizeChanged(width, height); rightFilter.onInputSizeChanged(width, height); curFilter.onDisplaySizeChanged(width, height); leftFilter.onDisplaySizeChanged(width, height); rightFilter.onDisplaySizeChanged(width, height); }然后定义绘制的画面的宽高,由接口从外部进行设置
private int width, height; public void onSizeChanged(int width, int height) { this.width = width; this.height = height; GLES20.glGenFramebuffers(1, fFrame, 0); EasyGlUtils.genTexturesWithParameter(1, fTexture, 0, GLES20.GL_RGBA, width, height); onFilterSizeChanged(width, height); }然后再定义一个帧缓冲区和一个纹理,用于绘制图像
private int[] fFrame = new int[1]; private int[] fTexture = new int[1];并且初始化一个Scroller,来完成滑动事件的响应,Scroller的使用可以见这篇文章Android Scroller完全解析。
private Scroller scroller;
scroller = new Scroller(MyApplication.getContext());初始化一个当前的filter的index,也就是标记位,标识当前是哪一个filter。
private int curIndex = 0;然后,在构造函数里面进行一些初始化
public SlideGpufilterGroup() { initFilter(); scroller = new Scroller(MyApplication.getContext()); }initFilter方法其实就是当前三种filter的初始化
public void initFilter() { curFilter = getFilter(getCurIndex()); leftFilter = getFilter(getLeftIndex()); rightFilter = getFilter(getRightIndex()); }getFilter其实就是根据filter的index进行filter对象的初始化
public GPUImageFilter getFilter(int index) { GPUImageFilter filter = MagicFilterFactory.initFilters(types[index]); if (filter == null) { filter = new GPUImageFilter(); } return filter; }而getCurIndex、getLeftIndex、getRightIndex这三个方法,就是分别获取到当前filter的index和左边、右边的filter的index。
private int getCurIndex() { return curIndex; } private int getLeftIndex() { int leftIndex = curIndex - 1; if (leftIndex < 0) { leftIndex = types.length - 1; } return leftIndex; } private int getRightIndex() { int rightIndex = curIndex + 1; if (rightIndex >= types.length) { rightIndex = 0; } return rightIndex; }当前的filter和左边、右边的filter我们都有了,现在还有两个点,一个就是怎么样具体去绘制当前数据帧,另一个就是怎么进行filter的切换。
首先,我们来看看详细的绘制规则,首先对外提供一个onDrawFrame方法
public void onDrawFrame(int textureId) { EasyGlUtils.bindFrameTexture(fFrame[0], fTexture[0]); if (direction == 0 && offset == 0) { curFilter.onDrawFrame(textureId); } else if (direction == 1) { onDrawSlideLeft(textureId); } else if (direction == -1) { onDrawSlideRight(textureId); } EasyGlUtils.unBindFrameBuffer(); }该方法,接受一个纹理id,就是我们当前要绘制的纹理。然后将帧缓冲和纹理进行绑定,下面就是一个if else的判断,这里direction其实就是我们自己定义的,用来表示当前是什么动作的一个int。
int direction;//0为静止,-1为向左滑,1为向右滑用这样一个int,就可以区分当前的滑动是什么样的状态,如果为0表示没有滑动,那么我们只用绘制curFilter就行了。而上面我们已经进行了curFilter的初始化,它也继承自GPUImageFilter。我们只用调用onDrawFragme把当前的纹理id传入进去就行了。
而这个offset,其实就是我们记录的当前的滑动量,如果offset其实=0的,那表示没有偏移量,那肯定也只用绘制当前这个filter就行了。
如果,当前的状态是向右滑动,也就是direction = 1,我们就调用onDrawSlideLeft函数。
private void onDrawSlideLeft(int textureId) { if (locked && scroller.computeScrollOffset()) { offset = scroller.getCurrX(); drawSlideLeft(textureId); } else { drawSlideLeft(textureId); if (locked) { if (needSwitch) { reCreateRightFilter(); if (mListener != null) { mListener.onFilterChange(types[curIndex]); } } offset = 0; direction = 0; locked = false; } } }我们先来看看scroller的computeScrollOffset方法 官方注释是
/** * Call this when you want to know the new location. If it returns true, * the animation is not yet finished. */ public boolean computeScrollOffset() {其实就是如果还在滑动的话,就返回true。
而这个locked,其实就是我们定义的一个用来控制流程的boolean值,赋值情况,后面的代码会详细讲解。
我们先来继续看这个if逻辑,如果是true的话,就拿到当前的offset偏移量,然后调用drawSlideLeft方法。
private void drawSlideLeft(int textureId) { GLES20.glViewport(0, 0, width, height); GLES20.glEnable(GLES20.GL_SCISSOR_TEST); GLES20.glScissor(0, 0, offset, height); leftFilter.onDrawFrame(textureId); GLES20.glDisable(GLES20.GL_SCISSOR_TEST); GLES20.glViewport(0, 0, width, height); GLES20.glEnable(GLES20.GL_SCISSOR_TEST); GLES20.glScissor(offset, 0, width - offset, height); curFilter.onDrawFrame(textureId); GLES20.glDisable(GLES20.GL_SCISSOR_TEST); }这个方法,就是绘制左滑情况的具体绘制,首先设置画面大小,然后开启了裁剪功能,然后用leftFilter绘制左侧的画面,用curFilter绘制右侧的画面,两者合起来就是一个完整的画面。offset就是用来确定左边和右边分别应该绘制多宽的值。同样我们也会有一个相同原理,类似代码的drawSlideRight方法
private void drawSlideRight(int textureId) { GLES20.glViewport(0, 0, width, height); GLES20.glEnable(GLES20.GL_SCISSOR_TEST); GLES20.glScissor(0, 0, width - offset, height); curFilter.onDrawFrame(textureId); GLES20.glDisable(GLES20.GL_SCISSOR_TEST); GLES20.glViewport(0, 0, width, height); GLES20.glEnable(GLES20.GL_SCISSOR_TEST); GLES20.glScissor(width - offset, 0, offset, height); rightFilter.onDrawFrame(textureId); GLES20.glDisable(GLES20.GL_SCISSOR_TEST); }然后继续看if判断的false情况,如果是false的话,说明滑动动画还没停下来,如果locked是true的话,就判断是否需要切换滤镜,needSwitch在什么情况下为true呢,其实他的判断是通过判断滑动事件,如果当前的offset超过了屏幕宽度的1/3,具体的地方我们接下来会说到。就是在滑动动画停止的时候,如果需要切换filter,我们就调用了reCreateRightFilter()方法,并且返回了一个监听回调。而这个reCreateRightFilter方法具体代码如下。
private void reCreateRightFilter() { decreaseCurIndex(); rightFilter.destroy(); rightFilter = curFilter; curFilter = leftFilter; leftFilter = getFilter(getLeftIndex()); leftFilter.init(); leftFilter.onDisplaySizeChanged(width, height); leftFilter.onInputSizeChanged(width, height); needSwitch = false; }这些代码里面的decreaseCurIndex方法,其实就是移动了curIndex,这个值,因为要向左一个切换滤镜,那么curIndex需要-1.
private void decreaseCurIndex() { curIndex--; if (curIndex < 0) { curIndex = types.length - 1; } }然后就销毁rightFilter。如果把当前的curFilter赋值给rightFilter,当前的curFilter设置为之前的leftFilter,然后再通过getFilter向左一个获取到新的leftFilter。如此就完成了一次滤镜的切换。那么同样的如果需要往右切换filter的话,那肯定有一个类似的方法。
private void reCreateLeftFilter() { increaseCurIndex(); leftFilter.destroy(); leftFilter = curFilter; curFilter = rightFilter; rightFilter = getFilter(getRightIndex()); rightFilter.init(); rightFilter.onDisplaySizeChanged(width, height); rightFilter.onInputSizeChanged(width, height); needSwitch = false; }这就是核心的onDrawFrame函数,然后在解绑掉帧缓冲和纹理。
EasyGlUtils.unBindFrameBuffer();这个类大部分的代码就是这样,最后剩下最重要的函数就是对外提供的onTouchEvent函数
public void onTouchEvent(MotionEvent event) { if (locked) { return; } switch (event.getAction()) { case MotionEvent.ACTION_DOWN: downX = (int) event.getX(); break; case MotionEvent.ACTION_MOVE: if (downX == -1) { return; } int curX = (int) event.getX(); if (curX > downX) { direction = 1; } else { direction = -1; } offset = Math.abs(curX - downX); break; case MotionEvent.ACTION_UP: if (downX == -1) { return; } if (offset == 0) { return; } locked = true; downX = -1; if (offset > Constants.screenWidth / 3) { scroller.startScroll(offset, 0, Constants.screenWidth - offset, 0, 100 * (1 - offset / Constants.screenWidth)); needSwitch = true; } else { scroller.startScroll(offset, 0, -offset, 0, 100 * (offset / Constants.screenWidth)); needSwitch = false; } break; } }这个函数,基本上就是在down事件的时候,获取到X值,然后在move事件的时候,就根据当前的X和down时候的X值做对比的时候,判断是像左滑还是右滑。然后在up事件的时候,将偏移量offset和屏幕宽度进行比较,判断是否需要切换滤镜。
所有的逻辑,基本上就是这样,让我们来看他一下最后的实现效果
结语
到这里的话呢,本篇文章就已经要结束了,我们这篇博客主要就是实现了给视频更换不同的滤镜效果,并且实现了一种很方便的切换滤镜的方式。至于本地视频添加切换不同滤镜效果,原理是相同的,这里就不多讲了,相关代码我都已经上传到了我的github上面。请大家多多star。Thank you。
然后,按照预定的计划,我们下篇文章就会说到android平台音频的硬解码,单声道(mono)转立体声(stero),两个音频的混音,音频原始音量调节这四个方面的知识。基本上也就涵盖了android平台的视频编辑器关于音频的大部分操作,当然有朋友可能会说到变声,这个的话,如果后面有时间,我再写篇博客专门介绍和实现以下android平台的音频的变声。
因为个人水平有限,难免有错误和不足之处,还望大家能包涵和提醒。谢谢啦!!!
其他
项目的github地址,麻烦顺手给个star,谢谢啦~
VideoEditor-For-Android
阅读全文
0 0
- Android视频编辑器(四)通过OpenGL给视频增加不同滤镜效果
- openGL ES进阶教程(四)用openGL ES+MediaPlayer 渲染播放视频+滤镜效果
- camera2 opengl实现滤镜效果录制视频 四 录像
- Android视频编辑器(三)给本地视频加水印和美颜滤镜
- Android视频编辑器(一)通过OpenGL预览、录制视频以及断点续录等
- camera2 opengl实现滤镜效果录制视频 目录
- camera2 opengl实现滤镜效果录制视频 一 相机预览
- camera2 opengl实现滤镜效果录制视频 三 录音
- FFmpeg中的滤镜(四):视频滤镜 -- subtitles
- camera2 opengl实现滤镜效果录制视频 五 音视频合并
- camera2 opengl实现滤镜效果录制视频 二 双SurfaceView渲染
- camera2 opengl实现滤镜效果录制视频 六 摄像头方向(完)
- Android视频编辑器(二)预览、录制视频加上水印和美白磨皮效果
- 视频滤镜
- cpuimage给视频添加滤镜资料之一
- OpenGL ES总结(四)OpenGL 渲染视频画面
- OpenGL ES总结(四)OpenGL 渲染视频画面
- android OpenGL 显示视频
- SSL2810 2017年10月30日提高组T2 数论(math)
- MapReduce MapTask任务数量,切片大小笔记
- Leetcode 645 FindErrorNum
- Mycat安装部署简单使用
- Okhttp带证书封装
- Android视频编辑器(四)通过OpenGL给视频增加不同滤镜效果
- 【bzoj3712】[PA2014]Fiolki
- IDEA(jetbrain通用)优雅级使用教程
- jsp基础知识总结
- POJ2407 Relatives
- TCP和UDP的区别
- Spark架构原理
- Kubernetes微服务架构应用实践
- VS 常用快捷键