1. 程式人生 > >OpenGL ES (17): 使用GLSurfaceView預覽Camera,並拍照

OpenGL ES (17): 使用GLSurfaceView預覽Camera,並拍照

1.許可權

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- Tell the system this app requires OpenGL ES 2.0. -->
<uses-feature
    android:glEsVersion="0x00020000"
android:required="true" />

2.思路-流程

預覽Camera我們以前是使用SurfaceView得到SurfaceHolder , 然後myCamera.setPreviewDisplay(SurfaceHolder),詳細例子見文章

但是我們這次使用的是OpenGLES中的 GLSurfaceView 來預覽Camera,說這個之前。我們先來說一說 紋理貼圖吧!

之前紋理貼圖的思路是什麼?

  1. 得到GLSurfaceView表面的紋理id
  2. 把圖片生成在對應的紋理id上面,並返回這個紋理id。
  3. 這樣圖片就和這個紋理id綁定了,但是還不知道紋理是如何繪製出來的。
  4. 所以需要傳入紋理座標,跟頂點座標一一對應。
  5. 然後繪製,就形成了紋理貼圖。

這次我們講的GLSurfaceView預覽Camera其實跟紋理貼圖的思路差不多。可以理解成在 GLSurfaceView上畫一個全屏大小的矩形,

把圖片換成了相機Camera資料,把Camera資料 紋理貼圖到 這個畫的矩形上面,就形成了預覽,是不是瞬間秒懂!

只不過還多了一個東西叫 SurfaceTexture ,這個東西是什麼?

我把它理解為 紋理層,相當於 view表面的一層衣服,它與GLSurfaceView的表面紋理id 掛鉤。

然後執行Camera.setPreviewTexture(SurfaceTexture)。相當於之前的 Camera.setPrieviewHolder(surfaceHolder)。

然後執行camera.startPrieView()就可以開始預覽了。沒結束,還要看下面的。

這個紋理層還需要設定一個監聽SurfaceTexture.setOnFrameAvailableListener(this);監聽相機傳來的新的流幀,一旦紋理層有新的資料,就要通知GLSurfaceView進行重繪。GLSurfaceView使用髒模式 setRenderMode(RENDERMODE_WHEN_DIRTY) 。

@Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        //回撥介面,用於通知新的流幀可用
        //紋理層有新資料,就通知view繪製
        this.requestRender();
    }

這樣下來,使用GLSurfaceView預覽相機Camera是不是變的很簡單!

使用相機拍照還是跟之前一樣!設定3個引數,第3個非常重要。

mCamera.takePicture(mShutterCallback, null, mJpegPictureCallback);

 ShutterCallback mShutterCallback = new ShutterCallback()
        //如果這個引數設定為Null,將沒有卡擦的聲音
    {
        public void onShutter() {
        }
    };

    PictureCallback mJpegPictureCallback = new PictureCallback()
            //對jpeg影象資料的回撥,最重要的一個回撥
    {
        public void onPictureTaken(byte[] data, Camera camera) {
            Bitmap b = null;
            if(null != data){
                b = BitmapFactory.decodeByteArray(data, 0, data.length);//data是位元組資料,將其解析成點陣圖
                mCamera.stopPreview();
                isPreviewing = false;
            }
            //儲存圖片到sdcard
            if(null != b)
            {
                //圖片這裡要旋轉下,相機拍出來的照片是倒著的
                Bitmap rotaBitmap = getRotateBitmap(b, 90.0f);
                saveBitmap(rotaBitmap);
            }
            //再次進入預覽
            mCamera.startPreview();
            isPreviewing = true;
        }

    };

3.具體程式碼

  • MainActivity.java 
  • CameraGLSurfaceView.java    (實現了GLSurfaceView介面,Renderer介面,SurfaceTexture.OnFrameAvailableListener紋理層監聽介面)
  • DirectDrawer.java     (一旦需要繪製,在GLSurfaceView中的onDrawFrame()方法中呼叫DirectDrawer.draw()進行繪製 )
  • CameraInterface.java     (相機Camera封裝類,包含開啟、預覽、關閉)

MainActivity.java

public class MainActivity extends Activity{
    private static final String TAG = "TAG";
    CameraGLSurfaceView glSurfaceView = null;
    ImageButton shutterBtn;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        glSurfaceView = (CameraGLSurfaceView)findViewById(R.id.camera_textureview);
        shutterBtn = (ImageButton)findViewById(R.id.btn_shutter);
        initViewParams();
        //拍照
        shutterBtn.setOnClickListener(new BtnListeners());
    }

    private void initViewParams(){
        LayoutParams params = glSurfaceView.getLayoutParams();
        Point p = getScreenMetrics(this);
        params.width = p.x; //view寬
        params.height = p.y; //view高
        //設定GLSurfaceView的寬和高
        glSurfaceView.setLayoutParams(params);
        //設定ImageButton的大小
        LayoutParams p2 = shutterBtn.getLayoutParams();
        p2.width = 100;
        p2.height = 100;
        shutterBtn.setLayoutParams(p2);
    }

    private Point getScreenMetrics(Context context){
        DisplayMetrics dm =context.getResources().getDisplayMetrics();
        int w_screen = dm.widthPixels;
        int h_screen = dm.heightPixels;
        return new Point(w_screen, h_screen);
    }

    //拍照
    private class BtnListeners implements OnClickListener{
        @Override
        public void onClick(View v) {
            switch(v.getId()){
                case R.id.btn_shutter:
                    CameraInterface.getInstance().doTakePicture();
                    break;
                default:break;
            }
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
       //更改檢視在樹中的z順序,因此它位於其他同級檢視之上。
        glSurfaceView.bringToFront();
    }

    @Override
    protected void onPause() {
        super.onPause();
        glSurfaceView.onPause();
    }

}

CameraGLSurfaceView.java

public class CameraGLSurfaceView extends GLSurfaceView implements Renderer, SurfaceTexture.OnFrameAvailableListener {
    private static final String TAG = "TAG";
    Context mContext;
    //以OpenGL ES紋理的形式從影象流中捕獲幀,我把叫做紋理層
    SurfaceTexture mSurface;
    //使用的紋理id
    int mTextureID = -1;
    DirectDrawer mDirectDrawer;
    public CameraGLSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        setEGLContextClientVersion(2);
        setRenderer(this);
        //根據紋理層的監聽,有資料就繪製
        setRenderMode(RENDERMODE_WHEN_DIRTY);
    }
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        //得到view表面的紋理id
        mTextureID = createTextureID();
        //使用這個紋理id得到紋理層SurfaceTexture
        mSurface = new SurfaceTexture(mTextureID);
        //監聽紋理層
        mSurface.setOnFrameAvailableListener(this);
        mDirectDrawer = new DirectDrawer(mTextureID);
        //開啟相機,並未預覽
        CameraInterface.getInstance().doOpenCamera();
    }
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES20.glViewport(0, 0, width, height);
        //如果還未預覽,就開始預覽
        if(!CameraInterface.getInstance().isPreviewing()){
            CameraInterface.getInstance().doStartPreview(mSurface);
        }
    }
    @Override
    public void onDrawFrame(GL10 gl) {
        GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
        //從影象流中將紋理影象更新為最近的幀
        mSurface.updateTexImage();
        mDirectDrawer.draw();
    }

    @Override
    public void onPause() {
        super.onPause();
        CameraInterface.getInstance().doStopCamera();
    }

    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];
    }

    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        //回撥介面,用於通知新的流幀可用。
        Log.i(TAG, "onFrameAvailable...");
        //紋理層有新資料,就通知view繪製
        this.requestRender();
    }

}

CameraInterface.java

public class CameraInterface {
    private Camera mCamera;
    private Camera.Parameters mParams;
    private boolean isPreviewing = false;
    private static CameraInterface mCameraInterface;

    private CameraInterface(){
    }
    public static synchronized CameraInterface getInstance(){
        if(mCameraInterface == null){
            mCameraInterface = new CameraInterface();
        }
        return mCameraInterface;
    }
    //開啟相機
    public void doOpenCamera(){
        if(mCamera == null){
            mCamera = Camera.open();
        }else{
            doStopCamera();
        }
    }
    /*使用TextureView預覽Camera*/
    public void doStartPreview(SurfaceTexture surface){
        if(isPreviewing){
            mCamera.stopPreview();
            return;
        }
        if(mCamera != null){
            try {
                //將相機畫面預覽到紋理層上,紋理層有資料了,再通知view繪製,此時未開始預覽
                mCamera.setPreviewTexture(surface);
            } catch (IOException e) {
                e.printStackTrace();
            }
            //真正開啟預覽,Camera.startPrieView()
            initCamera();
        }
    }

    /**
     * 停止預覽,釋放Camera
     */
    public void doStopCamera(){
        if(null != mCamera)
        {
            mCamera.setPreviewCallback(null);
            mCamera.stopPreview();
            isPreviewing = false;
            mCamera.release();
            mCamera = null;
        }
    }
    /**
     * 拍照
     */
    public void doTakePicture(){
        if(isPreviewing && (mCamera != null)){
            mCamera.takePicture(mShutterCallback, null, mJpegPictureCallback);
        }
    }

    public boolean isPreviewing(){
        return isPreviewing;
    }

    private void initCamera(){
        if(mCamera != null){
            mParams = mCamera.getParameters();
            mParams.setPictureFormat(PixelFormat.JPEG);//設定拍照後儲存的圖片格式
            mCamera.setDisplayOrientation(90);
            //設定攝像頭為持續自動聚焦模式
            mParams.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
            mCamera.setParameters(mParams);
            mCamera.startPreview();//開啟預覽
            //設定預覽標誌位
            isPreviewing = true;
        }
    }

    ShutterCallback mShutterCallback = new ShutterCallback()
        //如果這個引數設定為Null,將沒有卡擦的聲音
    {
        public void onShutter() {
        }
    };

    PictureCallback mJpegPictureCallback = new PictureCallback()
            //對jpeg影象資料的回撥,最重要的一個回撥
    {
        public void onPictureTaken(byte[] data, Camera camera) {
            Bitmap b = null;
            if(null != data){
                b = BitmapFactory.decodeByteArray(data, 0, data.length);//data是位元組資料,將其解析成點陣圖
                mCamera.stopPreview();
                isPreviewing = false;
            }
            //儲存圖片到sdcard
            if(null != b)
            {
                //圖片這裡要旋轉下,相機拍出來的照片是倒著的
                Bitmap rotaBitmap = getRotateBitmap(b, 90.0f);
                saveBitmap(rotaBitmap);
            }
            //再次進入預覽
            mCamera.startPreview();
            isPreviewing = true;
        }

    };

    //旋轉圖片
    private Bitmap getRotateBitmap(Bitmap b, float rotateDegree){
        Matrix matrix = new Matrix();
        matrix.postRotate((float)rotateDegree);
        return Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), matrix, false);
    }
    private static String initPath(){
        String storagePath = Environment.getExternalStorageDirectory().getAbsolutePath()+"/" + "PlayCamera";
        File f = new File(storagePath);
        if(!f.exists()){
            f.mkdir();
        }
        return storagePath;
    }
    private void saveBitmap(Bitmap b){
        String path = initPath();
        String jpegName = path + "/" + System.currentTimeMillis() +".jpg";
        try {
            BufferedOutputStream bos = new BufferedOutputStream( new FileOutputStream(jpegName));
            b.compress(Bitmap.CompressFormat.JPEG, 100, bos);
            bos.flush();
            bos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


}

最後我們來看 DirectDrawer.java中的程式碼:

頂點座標陣列為:

static float squareCoords[] = {
        -1.0f,  1.0f,
        -1.0f, -1.0f,
        1.0f, -1.0f,
        1.0f,  1.0f,
};

但是紋理座標陣列為:

static float textureVertices[] = {
        0.0f, 1.0f,  
        1.0f, 1.0f,
        1.0f, 0.0f,
        0.0f, 0.0f,
};

為什麼不是:

static float textureVertices[] = {
        0.0f, 0.0f,
        0.0f, 1.0f,
        1.0f, 1.0f,
        1.0f, 0.0f,
};

我們看下面一張圖解釋:

因為攝像頭Camera自己旋轉了的原因,實際上對應頂點(-1,1)的位置在(0,1)處,而不在(0,0)處,所以。。。

我們的繪製順序是012023.

具體程式碼如下:

public class DirectDrawer {
    private final String vertexShaderCode =
            "attribute vec4 vPosition;" +
                    "attribute vec2 inputTextureCoordinate;" +
                    "varying vec2 textureCoordinate;" +
                    "void main()" +
                    "{"+
                    "gl_Position = vPosition;"+
                    "textureCoordinate = inputTextureCoordinate;" +
                    "}";

    private final String fragmentShaderCode =
            "#extension GL_OES_EGL_image_external : require\n"+
                    "precision mediump float;" +
                    "varying vec2 textureCoordinate;\n" +
                    "uniform samplerExternalOES s_texture;\n" +
                    "void main() {" +
                    "  gl_FragColor = texture2D( s_texture, textureCoordinate );\n" +
                    "}";

    private FloatBuffer vertexBuffer, textureVerticesBuffer;
    private ShortBuffer drawListBuffer;
    private final int mProgram;
    private int mPositionHandle;
    private int mTextureCoordHandle;

    private short drawOrder[] = { 0, 1, 2, 0, 2, 3 };

    private static final int COORDS_PER_VERTEX = 2;

    private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex

    static float squareCoords[] = {
            -1.0f,  1.0f,
            -1.0f, -1.0f,
            1.0f, -1.0f,
            1.0f,  1.0f,
    };

    static float textureVertices[] = {
            0.0f, 1.0f,
            1.0f, 1.0f,
            1.0f, 0.0f,
            0.0f, 0.0f,
    };

    private int texture;

    public DirectDrawer(int texture)
    {
        this.texture = texture;
        //頂點座標
        ByteBuffer bb = ByteBuffer.allocateDirect(squareCoords.length * 4);
        bb.order(ByteOrder.nativeOrder());
        vertexBuffer = bb.asFloatBuffer();
        vertexBuffer.put(squareCoords);
        vertexBuffer.position(0);
        //頂點繪製順序
        ByteBuffer dlb = ByteBuffer.allocateDirect(drawOrder.length * 2);
        dlb.order(ByteOrder.nativeOrder());
        drawListBuffer = dlb.asShortBuffer();
        drawListBuffer.put(drawOrder);
        drawListBuffer.position(0);
        //紋理座標
        ByteBuffer bb2 = ByteBuffer.allocateDirect(textureVertices.length * 4);
        bb2.order(ByteOrder.nativeOrder());
        textureVerticesBuffer = bb2.asFloatBuffer();
        textureVerticesBuffer.put(textureVertices);
        textureVerticesBuffer.position(0);
        //編譯著色器
        int vertexShader    = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
        int fragmentShader  = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);
        mProgram = GLES20.glCreateProgram();
        GLES20.glAttachShader(mProgram, vertexShader);
        GLES20.glAttachShader(mProgram, fragmentShader);
        GLES20.glLinkProgram(mProgram);
    }

    public void draw()
    {
        GLES20.glUseProgram(mProgram);
        //使用紋理
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture);
        //頂點位置
        mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
        GLES20.glEnableVertexAttribArray(mPositionHandle);
        GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, vertexBuffer);
        //紋理座標
        mTextureCoordHandle = GLES20.glGetAttribLocation(mProgram, "inputTextureCoordinate");
        GLES20.glEnableVertexAttribArray(mTextureCoordHandle);
        GLES20.glVertexAttribPointer(mTextureCoordHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, textureVerticesBuffer);
        //繪製
        GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer);
        //結束
        GLES20.glDisableVertexAttribArray(mPositionHandle);
        GLES20.glDisableVertexAttribArray(mTextureCoordHandle);
    }

    //編譯著色器
    private  int loadShader(int type, String shaderCode){
        int shader = GLES20.glCreateShader(type);
        GLES20.glShaderSource(shader, shaderCode);
        GLES20.glCompileShader(shader);
        return shader;
    }

}

最終效果如圖,拍照照片在sd卡的PlayCamera資料夾中: