OpenGLES在Android上除了可以用來(lái)做游戲朽褪、處理圖片也可以用來(lái)處理視頻圖像诬留、做相機(jī)預(yù)覽美顏等等。本篇博客將介紹利用OpenGLES做相機(jī)預(yù)覽的基本實(shí)現(xiàn)汇荐。
預(yù)覽方案
前面我們有介紹過(guò)利用OpenGLES顯示圖片處理圖片日熬。視頻每一幀其實(shí)也是一張圖片棍厌,Camera預(yù)覽時(shí),每一幀自然也是一幅圖片竖席,我們可以把每張圖片按照時(shí)間順序顯示出來(lái)耘纱,就完成了Camera預(yù)覽的實(shí)現(xiàn)。
那么問(wèn)題來(lái)了毕荐,在前面我們都是直接傳入一個(gè)Bitmap束析,難道我們要把Camera每幀的數(shù)據(jù)轉(zhuǎn)為Bitmap再作為紋理傳入OpenGLES程序繪制出來(lái)么?
顯示一張圖片东跪,我們使用的是諸如GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0)
;這樣的代碼,前面竟然不是GLES20開(kāi)頭的鹰溜,這顯然是“不科學(xué)”的虽填。OpenGLES提供的綁定紋理貼圖的方法為GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,0,GLES20.GL_RGBA,width,height,0,GLES20.GL_RGBA,GLES20.GL_UNSIGNED_BYTE,buffer)
;用這個(gè)方法可以直接傳入Buffer數(shù)據(jù)。
那么問(wèn)題又來(lái)了曹动,雖然OpenGLES給我們提供的入口是傳入Buffer斋日,然而,它卻限制了Buffer的格式為單一通道墓陈,或者是RGBA恶守、RGB等格式,而Camera的幀數(shù)據(jù)卻只能為NV21或者YV21的贡必,不在OpenGLES貼圖數(shù)據(jù)格式的支持范圍兔港。雖然我們可以將NV21、YV12的數(shù)據(jù)轉(zhuǎn)換成RGBA的仔拟,但是對(duì)于預(yù)覽來(lái)說(shuō)衫樊,這個(gè)時(shí)間消耗實(shí)在是太大了。
我們可以直接將NV21的數(shù)據(jù)按照YUV三個(gè)分量直接拆分作為三張紋理傳入利花,然后利用GPU來(lái)將它們轉(zhuǎn)換為RGB顯示出來(lái)科侈,這樣時(shí)間消耗會(huì)小的多。然而這種方式也不是最簡(jiǎn)單的炒事,我們還有更簡(jiǎn)單的方式去實(shí)現(xiàn)臀栈。
Android的Camera及Camera2都允許使用SurfaceTexture作為預(yù)覽載體,但是它們所使用的SurfaceTexture傳入的OpenGL texture object name必須為GLES11Ext.GL_TEXTURE_EXTERNAL_OES
挠乳。這種方式权薯,實(shí)際上就是兩個(gè)OpenGL Thread共享一個(gè)Texture姑躲,不再需要數(shù)據(jù)導(dǎo)入導(dǎo)出,從Camera采集的數(shù)據(jù)直接在GPU中完成轉(zhuǎn)換和渲染崭闲。
Camera預(yù)覽實(shí)現(xiàn)
在之前的博客Android Camera的使用(一)中以SurfaceHolder做預(yù)覽來(lái)介紹了Camera的使用肋联。利用OpenGLES和之前使用步驟大同小異,使用時(shí)刁俭,我們不在使用camera.setPreviewDisplay(SurfaceHolder)
來(lái)設(shè)置預(yù)覽載體橄仍,轉(zhuǎn)而使用camera.setPreviewTexture(SurfaceTexture)
來(lái)使用SurfaceTexture來(lái)設(shè)置。我們也不在需要使用camera.setDisplayOrientation(int)
來(lái)設(shè)置正確的預(yù)覽方向牍戚,直接通過(guò)計(jì)算OpenGLES 程序的頂點(diǎn)坐標(biāo)或者紋理坐標(biāo)侮繁,或者直接計(jì)算出合適的變換矩陣,能夠?qū)㈩A(yù)覽的大小和方向一步到位的設(shè)置好如孝。
紋理的創(chuàng)建
相機(jī)預(yù)覽使用EXTERNAL_OES紋理宪哩,創(chuàng)建方式與2D紋理創(chuàng)建基本相同:
private int createTextureID(){
int[] texture = new int[1];
GLES20.glGenTextures(1, texture, 0);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture[0]);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_MIN_FILTER,GL10.GL_LINEAR);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);
return texture[0];
}
由于我們創(chuàng)建的是擴(kuò)展紋理,所以綁定的時(shí)候我們也需要綁定到擴(kuò)展紋理上才可以正常使用第晰,
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,texture[0])
锁孟。
修改著色器
預(yù)覽相機(jī)的著色器,頂點(diǎn)著色器不變茁瘦,需要修改片元著色器品抽,不再用sampler2D采樣,需要使勇samplerExternalOES 紋理采樣器甜熔,并且要在頭部增加使用擴(kuò)展紋理的聲明#extension GL_OES_EGL_image_external : require圆恤。
#extension GL_OES_EGL_image_external : require
precision mediump float;
varying vec2 textureCoordinate;
uniform samplerExternalOES vTexture;
void main() {
gl_FragColor = texture2D( vTexture, textureCoordinate );
}
計(jì)算變換矩陣
依照之前繪制圖片的經(jīng)驗(yàn)+利用SurfaceHolder做相機(jī)預(yù)覽的經(jīng)驗(yàn),應(yīng)該很容易就可以看到預(yù)覽效果了腔稀。這時(shí)候預(yù)覽的方向和大小比例一般不是我們所期望的樣子盆昙。頂點(diǎn)坐標(biāo)我們以(-1.0,-1.0)-(1.0焊虏,1.0)的范圍淡喜,紋理坐標(biāo)以(0.0,0.0)-(1.0诵闭,1.0)的范圍拆火,且上下左右各個(gè)角一一對(duì)應(yīng)。這時(shí)候涂圆,我們需要得到一個(gè)合適的變換矩陣们镜,傳入頂點(diǎn)著色器,以得到我們期望的預(yù)覽效果润歉。
根據(jù)大多數(shù)的Android手機(jī)模狭,前攝像頭預(yù)覽數(shù)據(jù)旋轉(zhuǎn)了90度,并且左右鏡像了踩衩,后攝像頭旋轉(zhuǎn)了270度嚼鹉。我們需要將其旋轉(zhuǎn)回來(lái)贩汉。另外,相機(jī)數(shù)據(jù)的高寬比和預(yù)覽區(qū)域的高寬比也許并不相等锚赤,所以我們還需要進(jìn)行適當(dāng)?shù)牟眉簦?/p>
//通過(guò)傳入圖片寬高和預(yù)覽寬高匹舞,計(jì)算變換矩陣,得到的變換矩陣是預(yù)覽類似ImageView的centerCrop效果
public static float[] getShowMatrix(int imgWidth,int imgHeight,int viewWidth,int viewHeight){
float[] projection=new float[16];
float[] camera=new float[16];
float[] matrix=new float[16];
float sWhView=(float)viewWidth/viewHeight;
float sWhImg=(float)imgWidth/imgHeight;
if(sWhImg>sWhView){
Matrix.orthoM(projection,0,-sWhView/sWhImg,sWhView/sWhImg,-1,1,1,3);
}else{
Matrix.orthoM(projection,0,-1,1,-sWhImg/sWhView,sWhImg/sWhView,1,3);
}
Matrix.setLookAtM(camera,0,0,0,1,0,0,0,0,1,0);
Matrix.multiplyMM(matrix,0,projection,0,camera,0);
return matrix;
}
//旋轉(zhuǎn)
public static float[] rotate(float[] m,float angle){
Matrix.rotateM(m,0,angle,0,0,1);
return m;
}
//鏡像
public static float[] flip(float[] m,boolean x,boolean y){
if(x||y){
Matrix.scaleM(m,0,x?-1:1,y?-1:1,1);
}
return m;
}
利用上面三個(gè)方法线脚,針對(duì)前后攝像頭赐稽,我們可以計(jì)算我們需要的變換矩陣:
float[] matrix=new float[16];
Gl2Utils.getShowMatrix(matrix,this.dataWidth,this.dataHeight,this.width,this.height);
if(cameraId==1){
Gl2Utils.flip(matrix,true,false);
Gl2Utils.rotate(matrix,90);
}else{
Gl2Utils.rotate(matrix,270);
}
mOesFilter.setMatrix(matrix);
渲染
然后我們可以像渲染圖片將攝像頭每一幀都渲染出來(lái)了。和圖片不同的是浑侥,圖片數(shù)據(jù)是相同的姊舵,而攝像頭數(shù)據(jù)是變換的,所以每當(dāng)攝像頭有新的數(shù)據(jù)來(lái)時(shí)寓落,我們需要通過(guò)surfaceTexture.updateTexImage()更新預(yù)覽上的圖像括丁。怎樣知道攝像頭有新的數(shù)據(jù)到來(lái)呢?答案是SurfaceTexture的OnFrameAvailableListener監(jiān)聽(tīng)伶选。通常updateTexImage不應(yīng)該在OnFrameAvailableLister的回調(diào)方法中直接調(diào)用史飞,而應(yīng)該在onDrawFrame中執(zhí)行。而調(diào)用requestRender仰税,可以觸發(fā)onDrawFrame构资。即通常我們的寫法應(yīng)該為:
surfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
...
requestRender();
...
}
});
其他
雖然預(yù)覽效果OK了,但是我們花費(fèi)如此大的精力肖卧,僅僅只是顯示出攝像頭的數(shù)據(jù)那多不值得蚯窥。在之前處理圖片的方式掸鹅,我們同樣可以用在處理攝像頭數(shù)據(jù)上塞帐,黑白、冷暖色調(diào)巍沙、放大鏡效果葵姥,等等。我們所用的美顏相機(jī)句携,美白榔幸、大眼、瘦臉等等效果基本都是基于GPU實(shí)現(xiàn)的矮嫉,大眼和瘦臉等和人臉特征相關(guān)的還需要用到人臉特征點(diǎn)檢測(cè)削咆。