前言

     这是视频编辑器系列的第二篇文章,在上篇文章中,我们讲解了利用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){@Overrideprotected void onClear() {}};
重写onCreate函数,创建我们的内部mFilter 
   @Overrideprotected void onCreate() {super.onCreate();mFilter.create();createTexture();}
在onSizeChanged方法中,设置mFilter的size    
   @Overrideprotected void onSizeChanged(int width, int height) {this.width=width;this.height=height;mFilter.setSize(width,height);}
     重写draw函数,开启OpenGl的混合功能 
  @Overridepublic 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<AFilter> mFilterQueue;private List<AFilter> 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];
    //创建FrameBufferprivate 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;}//生成Texturesprivate 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() {@Overridepublic 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

Android视频编辑器(二)预览、录制视频加上水印和美白磨皮效果相关推荐

  1. Android 使用CameraX实现预览/拍照/录制视频/图片分析/对焦/缩放/切换摄像头等操作

    1. CameraX架构 看官方文档 CameraX架构 有如下这一段话 使用CameraX,借助名为"用例"的抽象概念与设备的相机进行交互. 预览 : 接受用于显示预览的Surf ...

  2. Android OpenGL使用GLSurfaceView预览视频

    Android OpenGL使用GLSurfaceView预览视频 第一章 相关知识介绍 在介绍具体的功能之前,先对一些主要的类和方法进行一些介绍,这样可以更好的理解整个程序 1.1 GLSurfac ...

  3. android 与后台实时视频,Android实时监控项目第四篇:后台线程发送预览帧视频数据...

    还记得上篇提到的setPreviewCallback(Camera.PreviewCallback cb)函数吗?我们在开始预览帧视频之前,调用的它,这里要注意其内部的Camera.PreviewCa ...

  4. 直播视频跨浏览器预览方案(ffmpeg+VideoJS+H5)

    直播视频跨浏览器预览方案(ffmpeg+VideoJS+H5)附源码 1.概述 2.前言 3.思路 4.详解 4.1.ffmpeg 4.1.1.配置方法 4.1.2.验证方法 4.1.3.启动方法 4 ...

  5. 视频软件Vegas预览画面很卡怎么解决?

    做视频的小伙伴都知道,剪视频的时候最烦躁的就是卡顿,不能编辑,不能预览.最近很多同学就反映在使用Vegas的时候,预览窗口播放非常卡顿,有时候根本预览不了,这该如何解决呢? 方法一: 点击左侧轨道头中 ...

  6. Atitit.web预览播放视频的总结

    Atitit.web预览播放视频的总结 1. 浏览器类型的兼容性(chrome,ff,ie) 1 2. 操作系统的兼容性 1 3. 视频格式的内部视频格式跟播放器插件的兼容性.. 2 4. 指定播放器 ...

  7. windows 电脑图片/视频不展示预览图

    文章目录 windows 电脑图片/视频不展示预览图 描述 可能原因及解决办法 1. Windows的缩略图预览功能被禁用 2. 缩略图预览功能卡住 3. 重新安装Windows Media Play ...

  8. vue 视频长传与预览

    vue <el-col :span="11"><el-form-itemlabel="快学视频"prop="videoUrl&quo ...

  9. H5 Vue 视频 video 支持预览图 poster

    最近项目上要求文章中插入视频,并且视频需要支持预览图,给用户更好的视觉效果.本来以为加上poster就够了,但是没想到 ios 微信内置浏览器会有如下这种效果,很影响视觉体验. 优化步骤: 1.上传视 ...

最新文章

  1. linxu passwd 给linux用户设置密码 命令
  2. poi 合并单元格_POI数据获取脚本分享
  3. Python进阶11-标准库介绍02
  4. 产品经理面试中那些不忍直视的奇葩题目,面试官你真是够了!
  5. 文件操作-小文件复制
  6. noip2011day1题解
  7. 联想MWC大秀另一面AI实力,BAT为此转型以求
  8. Atitit 时间的展示格式与存储格式 目录 1.1. 赛事时间的格式起源 1 1.1.1. 六十[编辑] 1 1.2. 1h 12m 23s 模式 (可读性最好 2 1.3. 日常模式 1:45:
  9. Atitit 并发技术的选项 attilax总结 艾龙 著 1. 三大并发模型 1 2. 从可读性考虑 优先使用 并行工作者 多线程模式,不要使用异步流水线模式 2 2.1. 多线程模式方便全局
  10. 中华石杉Java面试突击第一季笔记一(消息队列)
  11. 文件打不开只读或服务器未响应,Mac的Word经常未响应怎么办
  12. OSG智能指针---Referenced类
  13. 【机器学习】机器学习模型迭代方法(Python)
  14. JPA映射数据库mysql表名,字段名大小写转化,下划线分割.
  15. 深层揭露百度缘何被黑
  16. 利用大数据挖掘创新市场监管新方式
  17. 计算机组成原理 程序计数器和寄存器的长度
  18. 8个快速提高用户忠诚度的套路,运营人必备
  19. 乐理基础知识-5.和弦
  20. 利用开源软件架设中小型私有云存储系统【简要选型】

热门文章

  1. 记一次被虐的很惨的面试
  2. CDH集群中HDFS单点故障解决方案:HA模式(High Availability)
  3. 0基础学MySQL数据库—从小白到大牛(20)大小写规范、sql_mode的合理设置
  4. 不一样的“中国速度”,数据可视化交通运输大屏,带你见证中国高铁
  5. 迎国庆,2021新款苹果 iPad,包邮送一台!
  6. 2022年煤矿探放水题库及模拟考试
  7. php c端,蛋白测序(N端,C端测序)
  8. NeurIPS 2020 | 基于协同集成与分发的协同显著性目标检测网络
  9. 一键安装微信已完成,编号10,欢迎品尝
  10. 360查出 HEUR/Malware.QVMxx.Gen 病毒含义