前言
这是视频编辑器系列的第二篇文章,在上篇文章中,我们讲解了利用OpenGl和SurfaceView进行视频预览,MediaCodec和MeidaMuxer进行视频录制和断点续录。而这篇主要会讲解一下如何在预览和录制视频的时候,利用OpenGL加上水印和美白磨皮的效果。如今的各种拍照、录制视频类的APP,如果没有美白磨皮的功能,那基本上是没有市场的了。然后,因为最近太忙,导致已经过去这么久才更新第二篇。。。捂脸。。。
本系列的文章,计划包括以下几部分:
1、android视频编辑器之视频录制、断点续录、对焦等
2、android视频编辑器之录制过程中加水印和加美白效果
3、android视频编辑器之本地视频加美白效果和加视频水印
4、android视频编辑器之
通过OpenGL给视频加各类滤镜
5、android视频编辑器之音频编解码、mono转stereo、音频混音、音频音量调节
6、android视频编辑器之通过OpenGL做本地视频拼接
7、android视频编辑器之音视频裁剪、增加背景音乐等
照例,贴出一些借鉴和参考的知识点的链接,非常感谢各位大佬的分享。
Android + JNI +Opengl 开发自己的美图秀秀(我们项目关于美白和滤镜部分,基本上都是参考这个作者的开源项目,非常值得学习一下)
图像处理-美白
给视频加水印
我们看到大大小小的各类视频类app,产生于他们平台的视频在外面分享或者流传的时候,都会加上自己平台的logo,这个就是常说的视频水印。那我们就来尝试给我们自己录制的水平加上水印效果。当然网上很多方案都是通过FFmpeg来在Android平台给视频加水印,但是我们已经说过了这个系列不会涉及到FFmpeg的使用,我们的实现方案还是通过OpenGL来实现给视频加水印的。
通过opengl给视频加水印的原理就是利用OpenGL的混合功能,将视频的画面和水印图片进行混合,生成新的纹理。也就是说还是会处理每一帧的数据,将每一帧的画面都加上水印的图片。
我们会编写一个WaterMarkFilter,来完成加水印的功能。
首先, 新建WaterMarkFilter类
public class WaterMarkFilter extends NoFilter{
}
然后在构造函数中创建一个NoFilter
mFilter=new NoFilter(mRes){
@Override
protected void onClear() {
}
};
重写onCreate函数,创建我们的内部mFilter
@Override
protected void onCreate() {
super.onCreate();
mFilter.create();
createTexture();
}
在onSizeChanged方法中,设置mFilter的size
@Override
protected void onSizeChanged(int width, int height) {
this.width=width;
this.height=height;
mFilter.setSize(width,height);
}
重写draw函数,开启OpenGl的混合功能
@Override
public void draw() {
super.draw();
GLES20.glViewport(x,y,w == 0 ? mBitmap.getWidth():w,h==0?mBitmap.getHeight():h);
GLES20.glDisable(GLES20.GL_DEPTH_TEST);
GLES20.glEnable(GLES20.GL_BLEND);
GLES20.glBlendFunc(GLES20.GL_SRC_COLOR, GLES20.GL_DST_ALPHA);
mFilter.draw();
GLES20.glDisable(GLES20.GL_BLEND);
GLES20.glViewport(0,0,width,height);
}
这个mBitmap 就是我们传入的水印图片的bitmap,我们提供设置水印图片的方法
public void setWaterMark(Bitmap bitmap){
if(this.mBitmap!=null){
this.mBitmap.recycle();
}
this.mBitmap=bitmap;
}
然后draw里面的x,y,w,h四个值,就是我们水印图片处于画面中的位置以及大小
public void setPosition(int x,int y,int width,int height){
this.x=x;
this.y=y;
this.w=width;
this.h=height;
}
然后这个给将原始画面和水印图片进行混合的代码就完成了
但是好像是不是还差了很关键的一步?就是如何将bitmap绘制到纹理上呢?其实这部分是我们上面忽略掉的createTexture中实现的,也就是说创建Texture的具体代码如下
private void createTexture() {
if(mBitmap!=null){
//生成纹理
GLES20.glGenTextures(1,textures,0);
//生成纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,textures[0]);
//设置缩小过滤为使用纹理中坐标最接近的一个像素的颜色作为需要绘制的像素颜色
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
//设置放大过滤为使用纹理中坐标最接近的若干个颜色,通过加权平均算法得到需要绘制的像素颜色
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
//设置环绕方向S,截取纹理坐标到[1/2n,1-1/2n]。将导致永远不会与border融合
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
//设置环绕方向T,截取纹理坐标到[1/2n,1-1/2n]。将导致永远不会与border融合
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0);
//对画面进行矩阵旋转
MatrixUtils.flip(mFilter.getMatrix(),false,true);
mFilter.setTextureId(textures[0]);
}
}
其中非常关键的一行就是,GLUtils.textImage2D(GLES20.GL_TEXTURE_2D,0,mBitmap,0) 。
(PS.因为上篇文章已经说了,不会涉及到太多OpenGL的用法介绍,大部分会以注释的方式出现,所以关于OpenGL有什么不明白的,请多参考其他人分享的相关文章)
GroupFilter类
绘制水印的filter已经写完了,然后我们需要一个GroupGilter,目的是如果有多个Filter需要绘制,那么该类会依次进行绘制,然后提供绘制完成的纹理,而且还拥有两个Texture,一个作为输入,一个作为输出, 然后一直循环。大致代码如下,首先创建一个Filter的队列,mFilterQueue和一个用于循环绘制的List,mFilterQueue用于保持我们添加进去的Filter,比如绘制水印的Filter,而mFilters,就用于循环绘制Filter。
private Queue mFilterQueue;
private List mFilters;
在构造函数中进行初始化
public GroupFilter(Resources res) {
super(res);
mFilters=new ArrayList<>();
mFilterQueue=new ConcurrentLinkedQueue<>();
}
提供添加Filter的方法
public void addFilter(final AFilter filter){
mFilterQueue.add(filter);
}
然后写一个updateFilter,用于将Filter,从Queue中取出,加入到List中
private void updateFilter(){
AFilter f;
while ((f=mFilterQueue.poll())!=null){
f.create();
f.setSize(width,height);
mFilters.add(f);
size++;
}
}
然后创建离屏的buffer以及输入和输出的Texture
private int fTextureSize = 2;
private int[] fFrame = new int[1];
private int[] fRender = new int[1];
private int[] fTexture = new int[fTextureSize];
//创建FrameBuffer
private boolean createFrameBuffer() {
GLES20.glGenFramebuffers(1, fFrame, 0);
GLES20.glGenRenderbuffers(1, fRender, 0);
genTextures();
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fFrame[0]);
GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, fRender[0]);
GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, width,
height);
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
GLES20.GL_TEXTURE_2D, fTexture[0], 0);
GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT,
GLES20.GL_RENDERBUFFER, fRender[0]);
// int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER);
// if(status==GLES20.GL_FRAMEBUFFER_COMPLETE){
// return true;
// }
unBindFrame();
return false;
}
//生成Textures
private void genTextures() {
GLES20.glGenTextures(fTextureSize, fTexture, 0);
for (int i = 0; i < fTextureSize; i++) {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, fTexture[i]);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height,
0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
}
}
然后就是draw的代码,主要是从List中,取出Filter,并且进行绘制
public void draw(){
updateFilter();
textureIndex=0;
GLES20.glViewport(0,0,width,height);
for (AFilter filter:mFilters){
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fFrame[0]);
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
GLES20.GL_TEXTURE_2D, fTexture[textureIndex%2], 0);
GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT,
GLES20.GL_RENDERBUFFER, fRender[0]);
if(textureIndex==0){
filter.setTextureId(getTextureId());
}else{
filter.setTextureId(fTexture[(textureIndex-1)%2]);
}
filter.draw();
unBindFrame();
textureIndex++;
}
}
GroupFilter的核心代码就是上面这些,然后我们需要将水印的Filter 在我们上文中的CameraDrawer中进行创建和添加,然后在合适的位置进行绘制。
在CameraDrawer代码中,基于上文中的代码,创建一个GroupFilter的实例,
/**绘制水印的filter组*/
private final GroupFilter mBeFilter;
在构造函数中进行水印的Filter的创建和添加
mBeFilter = new GroupFilter(resources);
WaterMarkFilter waterMarkFilter = new WaterMarkFilter(resources);
waterMarkFilter.setWaterMark(BitmapFactory.decodeResource(resources,R.mipmap.watermark));
waterMarkFilter.setPosition(30,50,0,0);
mBeFilter.addFilter(waterMarkFilter);
然后在onSurfaceCreated方法中进行mBeFilter的初始化
mBeFilter.create();
在onSurfaceChanged方法中,设置GroupFilter的size
drawFilter.setSize(mPreviewWidth,mPreviewHeight);
然后我们需要对核心的onDrawFrame方法进行改造,我们现在的绘制流程是这样的,首先使用drawFilter将摄像头的画面,绘制到离屏的fFrame和fTexture中
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fFrame[0]);
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
GLES20.GL_TEXTURE_2D, fTexture[0], 0);
GLES20.glViewport(0,0,mPreviewWidth,mPreviewHeight);
drawFilter.draw();
//解绑
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,0);
下一步,我们需要调用GroupFilter,将绘制的画面输入,然后和水印进行混合,然后再输出
mBeFilter.setTextureId(fTexture[0]);
mBeFilter.draw();
下一步就是,从GroupFilter取出绘制完成的texture,我们需要将这个texture分别给到两个第一,第一个就是屏幕上显示,第二个就是给到Encoder,进行后台的视频编码。
/**绘制显示的filter*/
GLES20.glViewport(0,0,width,height);
showFilter.setTextureId(mBeFilter.getOutputTexture());
showFilter.draw();
/**将加过水印的texture,给到encoder进行后台的编码*/
if (videoEncoder != null && recordingEnabled && recordingStatus == RECORDING_ON){
videoEncoder.setTextureId(mBeFilter.getOutputTexture());
videoEncoder.frameAvailable(mSurfaceTextrue);
}
通过上面的流程,我们就完成了视频加水印在屏幕上显示以及编码到视频中。
预览和录制加上美白效果
在上面,我们完成了预览和录制加水印效果,接下来,我们要加上美白的效果。通过opengl给视频加磨皮美白效果可以通过上面的其他人的文章进行了解。有详细的原理解释。这里就不讲一些基础的原理了。首先我们要对每一帧的画面进行美白处理,需要通过shader文件编写详细的处理规则,在R.raw.beauty文件里面。
而不同的美白级别就是通过改变shader文件里面一个参数的大小来实现。所以我们的MagicBeautyFilter文件,主要就是加载beauty这个文件,以及提供修改该文件中影响美白效果的参数的方法。主要的代码如下
在构造函数中加载shader文件 该文件继承自GPUImageFilter。(关于GPUImageFilter,是MagicCamera这个项目添加滤镜的一个滤镜基类,后面讲加滤镜的文章我们再详细的解释它,这里暂时不做讲解)
public MagicBeautyFilter(){
super(NO_FILTER_VERTEX_SHADER ,
OpenGlUtils.readShaderFromRawResource(R.raw.beauty));
}
这里的R.raw.beauty文件,就是美白算法的实现shader。
而GPUImageFilter的构造函数里面主要是如下代码
public GPUImageFilter(final String vertexShader, final String fragmentShader) {
mRunOnDraw = new LinkedList<>();
mVertexShader = vertexShader;
mFragmentShader = fragmentShader;
mGLCubeBuffer = ByteBuffer.allocateDirect(TextureRotationUtil.CUBE.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
mGLCubeBuffer.put(TextureRotationUtil.CUBE).position(0);
mGLTextureBuffer = ByteBuffer.allocateDirect(TextureRotationUtil.TEXTURE_NO_ROTATION.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
mGLTextureBuffer.put(TextureRotationUtil.getRotation(Rotation.NORMAL, false, true)).position(0);
}
包括了shader的设置以及一些buffer的初始化
然后在MagicBeautyFilter的onInit方法里面,找到我们要修改的字段的位置,主要是修改美白效果的params和size设置的singleStepOffset,这两个值的具体作用可以在shader文件中查看。
protected void onInit() {
super.onInit();
mSingleStepOffsetLocation = GLES20.glGetUniformLocation(getProgram(), "singleStepOffset");
mParamsLocation = GLES20.glGetUniformLocation(getProgram(), "params");
setBeautyLevel(3);//beauty Level
}
然后就是对外提供设置美白的等级的方法
public void setBeautyLevel(int level){
mLevel=level;
switch (level) {
case 1:
setFloat(mParamsLocation, 1.0f);
break;
case 2:
setFloat(mParamsLocation, 0.8f);
break;
case 3:
setFloat(mParamsLocation,0.6f);
break;
case 4:
setFloat(mParamsLocation, 0.4f);
break;
case 5:
setFloat(mParamsLocation,0.33f);
break;
default:
break;
}
}
而这个setFloat方法,就是修改shader文件里面的params的值 setFloat的代码主要如下
protected void setFloat(final int location, final float floatValue) {
runOnDraw(new Runnable() {
@Override
public void run() {
GLES20.glUniform1f(location, floatValue);
}
});
}
到这里美白的filter就算写完了。接下来我们就要在CamreaDrawer类中添加上美白filter的使用了。
首先在构造函数中进行初始化
mBeautyFilter = new MagicBeautyFilter();
然后在onSurfaceCreated中进行初始化
mBeautyFilter.init();
在onSurfaceChanged中进行size的设置
mBeautyFilter.onDisplaySizeChanged(mPreviewWidth,mPreviewHeight);
mBeautyFilter.onInputSizeChanged(mPreviewWidth,mPreviewHeight);
然后在onDrawFrame方法中,如果当前是需要美白的话,就对数据进行美白绘制
if (mBeautyFilter != null && mBeautyFilter.getBeautyLevel() != 0){
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());
}
基本过程就是,如果是要进行美白的话,就将纹理传递到美白的filter中,而美白的filter的onDrawFrame函数主要做了什么?这就涉及到了以后我们会说的给视频加滤镜之类的操作,我们在后面的文章中进行详细的解释。这里暂时就不深入了。需要知道的是,经过上述代码的操作,就成功的给视频加上了美白效果。
然后在CameraDrawer类中,对上层提供修改美白效果等级的接口
/**提供修改美白等级的接口*/
public void changeBeautyLevel(int level){
mBeautyFilter.setBeautyLevel(level);
}
public int getBeautyLevel(){
return mBeautyFilter.getBeautyLevel();
}
到这里,我们的美白fitler以及将其在核心的绘制类中使用,已经全部写完了,现在要做的就是,给项目上层ui,加上一些控制美白效果的代码。就不仔细说了 。
结语
加水印图片和美白效果的实现已经完了,如果要换成其他的美白算法的实现,替换掉美白filter里面的那个shader文件就行。虽然本篇文章的大致内容已经讲解的差不多了。但是其实还是有很多细节的地方需要多多注意,比如如果我们添加的不是图片水印而是文字水印,应该怎么做?美白算法在shader里面具体是如何实现的?我们这样依次添加水印和美白效果,是否会影响效率?等等。还是值得深入去学习和思考的。
预告:本篇将的是在预览和录制的时候加上水印和美白效果,那么如何给本地已存在的视频加上水印和美白效果呢?下篇文章,我们就将实现给本地视频添加水印和美白效果。
因为个人水平有限,难免有错误和不足之处,还望大家能包涵和提醒。谢谢啦!!!
其他
项目的github地址
VideoEditor-For-Android