Android視頻編輯器(二)預覽、錄製視頻加上水印和美白磨皮效果

前言

     這是視頻編輯器系列的第二篇文章,在上篇文章中,我們講解了利用OpenGl和SurfaceView進行視頻預覽,MediaCodec和MeidaMuxer進行視頻錄製和斷點續錄。而這篇主要會講解一下如何在預覽和錄製視頻的時候,利用OpenGL加上水印和美白磨皮的效果。如今的各種拍照、錄製視頻類的APP,如果沒有美白磨皮的功能,那基本上是沒有市場的了。然後,因爲最近太忙,導致已經過去這麼久才更新第二篇。。。捂臉。。。
     本系列的文章,計劃包括以下幾部分:
      2、android視頻編輯器之錄製過程中加水印和加美白效果
      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<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];
     
    //創建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);
    }
      通過上面的流程,我們就完成了視頻加水印在屏幕上顯示以及編碼到視頻中。
  <Image_1>


預覽和錄製加上美白效果

       在上面,我們完成了預覽和錄製加水印效果,接下來,我們要加上美白的效果。通過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地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章