Android Camera API/Camera2 API 相機預覽及濾鏡、貼紙等處理


http://www.mamicode.com/info-detail-1723122.html

Android Lollipop 增加了Camera2 API,並將原來的Camera API標記為廢棄了。相對原來的Camera API來說,Camera2是重新定義的相機 API,也重構了相機 API 的架構。初看之下,可能會感覺Camera2使用起來比Camera要復雜,然而使用過后,你也許就會喜歡上使用Camera2了。無論是Camera還是Camera2,當相機遇到OpenGL就比較好玩了。

問題及思路

Camera的預覽比較常見的是使用SurfaceHolder來預覽,Camera2比較常見的是使用TextureView來預覽。如果利用SurfaceHolder作為Camera2的預覽,利用TextureView作為Camera的預覽怎么做呢?實現起來可能也很簡單,如果預覽之前,要做美顏、磨皮或者加水印等等處理呢?實現后又如何保證使用Camera還是Camera2 API,使用SurfaceHolder還是TextureView預覽,或者是直接編碼不預覽都可以迅速的更改呢? 
本篇博客示例將數據、處理過程、預覽界面分離開,使得無論使用Camera還是Camera2,只需關注相機自身。無論最終期望顯示在哪里,都只需提供一個顯示的載體。詳細點說來就是:

  1. 以一個SurfaceTexture作為接收相機預覽數據的載體,這個SurfaceTexture就是處理器的輸入。
  2. SurfaceView、TextureView或者是Surface,提供SurfaceTexture或者Surface給處理器作為輸出,來接收處理結果。
  3. 重點就是處理器了。處理器利用GLSurfaceView提供的GL環境,以相機數據作為輸入,進行處理,處理的結果渲染到視圖提供的輸出點上,而不是GLSurfaceView內部的Surface上。當然,也可以不用GLSurfaceView,自己利用EGL來實現GL環境,問題也不大。具體實現就參照GLSurfaceView的源碼來了。

處理效果

既然是用OpenGL來處理,索性利用OpenGL在圖像上加兩個其他圖片,類似水印、貼紙的效果。隨便兩幅圖貼上去,也不管好看不好看了,重點是功能。依次為先貼紙然后灰色濾鏡,先灰色濾鏡然后貼紙,只有貼紙。 
技術分享技術分享技術分享

具體實現

根據上述思路來,主要涉及到以下問題:

  1. 使用GLSurfaceView創建GL環境,但是要讓這個環境為離屏渲染服務,而不是直接渲染到GLSurfaceView的Surface上。在這其中還涉及到其他的一些問題,具體的問題,在下面再說。
  2. OpenGL 的使用。相關文章
  3. 務必使相機、處理過程、顯示視圖分離。以便能夠自由的替換數據源、顯示視圖,只需要關注處理過程。

GLSurfaceView的利用

通常我們在Android中使用OpenGL環境,只需要在GLSurfaceView的Renderer接口中,調用GL函數就好了。這是因為GLSurfaceView在內部幫我們創建了GL環境,如果我們要拋開GLSurfaceView的話,只需要根據GLSurfaceView創建GL環境的過程在,做相同實現就可了,也就是EGL的使用。也就是說,OpenGL是離不開EGL的。EGL的使用步驟參考。 
首先,我們使用GLSurfaceView,是希望利用它的GL環境,而不是它的視圖,所以,我們需要改變它的渲染位置為我們期望的位置:

//這句是必要的,避免GLSurfaceView自帶的Surface影響渲染 getHolder().addCallback(null); //指定外部傳入的surface為渲染的window surface setEGLWindowSurfaceFactory(new GLSurfaceView.EGLWindowSurfaceFactory() { @Override public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, Object window) { //這里的surface由外部傳入,可以為Surface、SurfaceTexture或者SurfaceHolder return egl.eglCreateWindowSurface(display,config,surface,null); } @Override public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) { egl.eglDestroySurface(display, surface); } });

另外,GLSurfaceView的GL環境是受View的狀態影響的,比如View的可見與否,創建和銷毀,等等。我們需要盡可能的讓GL環境變得可控。因此,GLSurfaceView有兩個方法一頂要暴露出來:

public void attachedToWindow(){ super.onAttachedToWindow(); } public void detachedFromWindow(){ super.onDetachedFromWindow(); }

這里就又有問題了,因為GLSurfaceView的onAttachedToWindow和onDetachedFromWindow是需要保證它有parent的。所以,在這里必須給GLSurfaceView一個父布局

//自定義的GLSurfaceView mGLView=new GLView(mContext); //避免GLView的attachToWindow和detachFromWindow崩潰 new ViewGroup(mContext) { @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { } }.addView(mGLView);

另外,GLSurfaceView的其他設置:

setEGLContextClientVersion(2); setRenderer(TextureController.this); setRenderMode(RENDERMODE_WHEN_DIRTY); setPreserveEGLContextOnPause(true);

這樣,我們就可以愉快的使用GLSurfaceView來提供GL環境,給指定的Surface或者SurfaceTexture渲染圖像了。

數據的接收

我們針對的是相機的處理,相機的圖像數據和視頻的圖像數據,在Android中都可以直接利用SurfaceTexture來接收,所以我們可以提供一個SurfaceTexture給相機,然后將SurfaceTexture的數據拿出來,調整好方向,作為原始數據。具體處理和相機普通的預覽類似,不同的是,我們是不希望直接顯示到屏幕上的,而且在后續我們還會對這個圖像做其他處理。所以我們時將相機的當前幀數據渲染到一個2d的texture上,作為后續處理過程的輸入。所以在渲染時,需要綁定FrameBuffer。

@Override public void draw() { boolean a=GLES20.glIsEnabled(GLES20.GL_DEPTH_TEST); if(a){ GLES20.glDisable(GLES20.GL_DEPTH_TEST); } if(mSurfaceTexture!=null){ mSurfaceTexture.updateTexImage(); mSurfaceTexture.getTransformMatrix(mCoordOM); mFilter.setCoordMatrix(mCoordOM); } EasyGlUtils.bindFrameTexture(fFrame[0],fTexture[0]); GLES20.glViewport(0,0,width,height); mFilter.setTextureId(mCameraTexture[0]); mFilter.draw(); Log.e("wuwang","textureFilter draw"); EasyGlUtils.unBindFrameBuffer(); if(a){ GLES20.glEnable(GLES20.GL_DEPTH_TEST); } }

上面所使用的mFilter就是用來渲染相機數據的Filter,該Filter所起的作用就是將相機數據的方向調整正確。然后通過綁定FrameBuffer並制定接受渲染的Texture,就可以將相機數據以一個正確的方向渲染到這個指定的Texture上了。 
mFilter的頂點着色器為:

attribute vec4 vPosition; attribute vec2 vCoord; uniform mat4 vMatrix; uniform mat4 vCoordMatrix; varying vec2 textureCoordinate; void main(){ gl_Position = vMatrix*vPosition; textureCoordinate = (vCoordMatrix*vec4(vCoord,0,1)).xy; }

其片元着色器為:

#extension GL_OES_EGL_image_external : require precision mediump float; varying vec2 textureCoordinate; uniform samplerExternalOES vTexture; void main() { gl_FragColor = texture2D( vTexture, textureCoordinate ); }

渲染相機數據到指定窗口上

在之前利用OpenGLES預覽Camera的博客中,我們是直接將相機的數據“draw”到屏幕上了。在上面的處理中,我們在繪制之前調用了EasyGlUtils.bindFrameTexture(fFrame[0],fTexture[0]),這個方法是讓我們后續的渲染,渲染到fTexture[0]這個紋理上。具體可以參考前面的博客FBO離屏渲染。 
所以,通過上面的方式接收數據,然后利用自定義的GLSurfaceView指定渲染窗口並執行渲染后,我們依舊無法看到相機的預覽效果。為了將相機的數據渲染到屏幕上,我們需要將fTexture[0]的內容再渲染到制定的窗口上。這個渲染比之前的接收相機數據,渲染到fTexture[0]上更為簡單:

AFilter filter=new NoFilter(getResource()); ... void onDrawFrame(GL10 gl){ GLES20.glViewPort(0,0,width,height) filter.setMatrix(matrix); filter.setTexture(fTexture[0]); filter.draw(); } ...

NoFilter的頂點着色器為:

attribute vec4 vPosition; attribute vec2 vCoord; uniform mat4 vMatrix; varying vec2 textureCoordinate; void main(){ gl_Position = vMatrix*vPosition; textureCoordinate = vCoord; }

片元着色器為:

precision mediump float; varying vec2 textureCoordinate; uniform sampler2D vTexture; void main() { gl_FragColor = texture2D( vTexture, textureCoordinate ); }

沒錯,就是顯示超簡單的渲染一張圖片的着色器,看過前面的博客的應該見過這段着色器代碼。

增加濾鏡、貼紙效果

如果僅僅是預覽相機,我們這種做法簡直就是多次一句,直接指定渲染窗口,渲染出來就萬事大吉了。但是僅僅是這樣的話,就太沒意思了。而現在要做的就是相機有意思的起點了。很多有意思的相機應用,都可以通過這樣的方式去實現,比如我們常見美妝、美顏、色彩處理(濾鏡),甚至瘦臉、大眼,或者其他的讓臉變胖的,以及一些給相機中的人帶眼鏡、帽子、發箍(這些一般需要做人臉識別特征點定位)等等等等。 
通過上面的接收數據,和渲染相機數據到指定的窗口上,我們是已經可以看到渲染的結果了的。 
然后我們要在渲染到指定窗口前,增加其他的Filter,為了保證易用性,我們增加一個GroupFilter,讓其他的Filter,直接加入到GroupFilter中來完成處理。

public class GroupFilter extends AFilter{ private Queue<AFilter> mFilterQueue; private List<AFilter> mFilters; private int width=0, height=0; private int size=0; public GroupFilter(Resources res) { super(res); mFilters=new ArrayList<>(); mFilterQueue=new ConcurrentLinkedQueue<>(); } @Override protected void initBuffer() { } public void addFilter(final AFilter filter){ //繪制到frameBuffer上和繪制到屏幕上的紋理坐標是不一樣的 //Android屏幕相對GL世界的紋理Y軸翻轉 MatrixUtils.flip(filter.getMatrix(),false,true); mFilterQueue.add(filter); } public boolean removeFilter(AFilter filter){ boolean b=mFilters.remove(filter); if(b){ size--; } return b; } public AFilter removeFilter(int index){ AFilter f=mFilters.remove(index); if(f!=null){ size--; } return f; } public void clearAll(){ mFilterQueue.clear(); mFilters.clear(); size=0; } public void draw(){ updateFilter(); textureIndex=0; if(size>0){ 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]); GLES20.glViewport(0,0,width,height); if(textureIndex==0){ filter.setTextureId(getTextureId()); }else{ filter.setTextureId(fTexture[(textureIndex-1)%2]); } filter.draw(); unBindFrame(); textureIndex++; } } } private void updateFilter(){ AFilter f; while ((f=mFilterQueue.poll())!=null){ f.create(); f.setSize(width,height); mFilters.add(f); size++; } } @Override public int getOutputTexture(){ return size==0?getTextureId():fTexture[(textureIndex-1)%2];}@OverrideprotectedvoidonCreate(){}@OverrideprotectedvoidonSizeChanged(int width,int height){this.width=width;this.height=height; updateFilter(); createFrameBuffer();}//創建離屏bufferprivateint fTextureSize =2;privateint[] fFrame =newint[1];privateint[] fRender =newint[1];privateint[] fTexture =newint[fTextureSize];privateint textureIndex=0;//創建FrameBufferprivatebooleancreateFrameBuffer(){ 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();returnfalse;}//生成TexturesprivatevoidgenTextures(){ 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);}}//取消綁定TextureprivatevoidunBindFrame(){ GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER,0); GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,0);}privatevoiddeleteFrameBuffer(){ GLES20.glDeleteRenderbuffers(1, fRender,0); GLES20.glDeleteFramebuffers(1, fFrame,0); GLES20.glDeleteTextures(1, fTexture,0);}}

將這個FilterGroup加入到已有的流程中,只需要將保持的相機數據的Texture作為FilterGroup的輸入,然后將FilterGroup的輸出作為渲染到指定窗口的Filter的輸入即可:

 @Override public void onDrawFrame(GL10 gl) { if(isParamSet.get()){ mCameraFilter.draw(); mGroupFilter.setTextureId(mCameraFilter.getOutputTexture()); mGroupFilter.draw(); GLES20.glViewport(0,0,mWindowSize.x,mWindowSize.y); mShowFilter.setMatrix(SM); mShowFilter.setTextureId(mGroupFilter.getOutputTexture()); mShowFilter.draw(); if(mRenderer!=null){ mRenderer.onDrawFrame(gl); } callbackIfNeeded(); } }

然后將需要增加的其他的Filter,依次增加到GroupFilter中即可:

WaterMarkFilter filter=new WaterMarkFilter(getResources()); filter.setWaterMark(BitmapFactory.decodeResource(getResources(),R.mipmap.logo)); filter.setPosition(300,50,300,150); mController.addFilter(filter); //mController.addFilter(new GrayFilter(getResources())); mController.setFrameCallback(720, 1280, Camera2Activity.this);

其他

如果之前沒有接觸過OpenGL,這篇博客看下來可能也是雲里霧里。主要是因為篇幅有限,加上之前的博客分享的也是從零開始學習OpenGLES的內容,所以在這篇博客中沒有贅述,如有問題,可在評論區留言,或給我發郵件,共同探討。另外,分享一下我們公司的項目——AiyaEffectSDK,可以快速實現各種好玩的美顏、貼紙效果,歡迎Star和Fork。

源碼

所有的代碼全部在一個項目中,托管在Github上——Android OpenGLES 2.0系列博客的Demo


歡迎轉載,轉載請保留文章出處。湖廣午王的博客[http://blog.csdn.net/junzia/article/details/61207844]


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM