Android OpenGL基础(五、相机预览及滤镜)

2,367 阅读8分钟

一、相机简介

  在Android OpenGL基础(三、绘制Bitmap纹理)一文中,我们简单介绍了如何绘制如何把一张图片贴到四边形上。本文介绍如何用GLSurfaceView来实现预览相机。与单张图片纹理不同的地方在于,相机是一个内容不断变化的纹理。
  首先,先简单介绍相机的几个常用方法:

1.1 声明相机权限

  如果APP需要使用相机,则需要在manifest.xml中声明:

<uses-permission android:name="android.permission.CAMERA" />

1.2 检查相机权限

  Android权限类型有两种:

  • 安装时权限:例如普通权限或签名权限,系统会在安装您的应用时自动为其授予相应权限。
  • 运行时权限:在 Android 6.0(API 级别 23)或更高版本的设备上,必须请求权限。   检查当前是否获得相机权限的方法如下:
// 方法1:在Activity中调用Activity提供的API
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    this.checkSelfPermission(Manifest.permission.CAMERA)
}
// 方法2:androidx提供的API
ContextCompat.checkSelfPermission(this,
    Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED

1.3 请求相机权限

  请求相机权限的方法如下:

// 方法1::在Activity中调用Activity提供的API
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    val permissions = arrayOf(Manifest.permission.CAMERA)
    this.requestPermissions(permissions, PERMISSION_REQUEST_CODE)
}
// 方法2:androidx提供的API
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA),1)

1.4 常用方法

  下面简单列举Camera几个常用的方法:

public class Camera1Utils {
    private Camera camera;
    /**
    * 打开相机
    **/
    public void openCamera() {
        // 打开相机
        camera = Camera.open();
        Camera.Parameters parameters = camera.getParameters();
        // 自动对焦
        parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
        camera.setParameters(parameters);
        // 开始相机预览
        camera.startPreview();
    }

    public void stopCamera() {
        if (camera != null) {
            camera.stopPreview();
            camera.release();
        }
    }
    
    /**
    * 用SurfaceHolder承接相机预览数据
    **/
    public void setPreviewDisplay(SurfaceHolder surfaceHolder) {
        try {
            // 把相机预览数据传给SurfaceHolder
            camera.setPreviewDisplay(surfaceHolder);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    /**
    * 用SurfaceTexture承接相机预览数据
    **/
    public void setPreviewTexture(SurfaceTexture surfaceTexture){
        try {
            // 把相机预览数据传给SurfaceTexture
            camera.setPreviewTexture(surfaceTexture);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

二、相机预览

  为了可以在屏幕上看到相机预览画面,在打开相机后,需要把相机数据传递给一个View进行显示。常用的方式是用SurfaceView来显示相机实时画面。

2.1 SurfaceView

  在SurfaceView创建成功后,可以将相机数据传递给SurfaceView的SurfaceHolder,来在SurfaceView中显示相机画面。

class CameraPreview(context: Context,=private val mCamera: Camera
) : SurfaceView(context), SurfaceHolder.Callback {
    override fun surfaceCreated(holder: SurfaceHolder) {
        // Surface创建成功后,把相机数据传递给SurfaceView的SurfaceHolder
        mCamera.apply {
            try {
                setPreviewDisplay(holder)
                startPreview()
            } catch (e: IOException) {
                Log.d(TAG, "Error setting camera preview: ${e.message}")
            }
        }
    }

2.2 GLSurfaceView

  GLSurfaceView类提供了帮助管理 EGL 上下文、在线程间通信以及与 activity 生命周期交互的辅助程序类。GLSurfaceView本身无法和相机数据直接关联起来,需要通过SurfaceTexture。在打开相机后,可以把相机数据传递给SurfaceTexture,在SurfaceTexture中将相机纹理绘制到GLSurfaceView中。本文主要介绍这种方式,在第三节详细介绍。

三、OpenGL实现相机预览

  用OpenGL实现相机预览,下面分为SurfaceTexture、GLSurfaceView、GLSurfaceView.Render、绘制相机纹理几部分来介绍。

3.1 SurfaceTexture

  SurfaceTexture用于在相机启动后,承接相机预览数据,常用方法如下:

public class SurfaceTexture {
    /**
     * 注册OnFrameAvailableListener回调;
     * 当SurfaceTexture有新的数据可用时会回调OnFrameAvailableListener的onFrameAvailable方法
     */
    public void setOnFrameAvailableListener(SurfaceTexture.OnFrameAvailableListener listener) {
        setOnFrameAvailableListener(listener, null);
    }

    /**
     * Update the texture image to the most recent frame from the image stream.
     * 把SurfaceTexture中的数据更新为最新一次的数据
     */
    public void updateTexImage() {
        nativeUpdateTexImage();
    }
}

3.2 GLSurfaceView

  在GLSurfaceView中,在SurfaceTexture中有新的数据(onFrameAvailable)时,调用自身的requestRender(),即可触发自身的重新渲染(onDrawFrame()方法):

class MyGLSurfaceView(context: Context?, attrs: AttributeSet?) : GLSurfaceView(context, attrs),
    SurfaceTexture.OnFrameAvailableListener {
    private val renderer: MyGLRenderer

    init {
        setEGLContextClientVersion(2)
        renderer = MyGLRenderer(this)
        setRenderer(renderer)
        renderMode = RENDERMODE_WHEN_DIRTY
    }

    override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
        // renderMode设置为RENDERMODE_WHEN_DIRTY;
        // 在相机把新的数据传给SurfaceTexture时会回调onFrameAvailable()方法
        // 在onFrameAvailable()方法里调用requestRender()触发渲染更新Surface
        requestRender()
    }
}

3.3 GLSurfaceView.Renderer

  GLSurfaceView.Renderer的工作比较简单,在onSurfaceCreated后启动相机,并把相机预览数据传递给SurfaceTexture,将SurfaceTexture的listener设置为GLSurfaceView,绘制的主要工作在我们自定义的CameraDrawer类中:

class MyGLRenderer(private val frameAvailableListener: SurfaceTexture.OnFrameAvailableListener)
    : GLSurfaceView.Renderer {

    private lateinit var cameraDrawer: CameraDrawer
    private val cameraManager: Camera1Utils = Camera1Utils()

    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
        cameraDrawer = CameraDrawer()
        cameraDrawer.getSurfaceTexture().setOnFrameAvailableListener(frameAvailableListener)
        cameraManager.openCamera()
        cameraManager.setPreviewTexture(cameraDrawer.getSurfaceTexture())
    }

    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        GLES20.glViewport(0, 0, width, height)
    }

    override fun onDrawFrame(gl: GL10?) {
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
        cameraDrawer.getSurfaceTexture().updateTexImage()
        cameraDrawer.draw()
    }
}

3.4 绘制相机纹理

  下面看我们自定义的CameraDrawer类是如何完成相机预览画面的绘制的。

3.4.1 创建纹理

  创建纹理仍然使用Android OpenGL基础(三、绘制Bitmap纹理)中,1.2小节提供的OpenGLUtils工具类,不同的是相机的纹理类型是GLES11Ext.GL_TEXTURE_EXTERNAL_OES:

val texture = OpenGLUtils.createTextures(
    GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 1,
    GLES20.GL_NEAREST, GLES20.GL_LINEAR,
    GLES20.GL_CLAMP_TO_EDGE, GLES20.GL_CLAMP_TO_EDGE
)

3.4.2 相机纹理GLSL

  相机纹理的GLSL代码与Android OpenGL基础(三、绘制Bitmap纹理)中2D图片纹理类似,不同点在于相机纹理需要声明uniform samplerExternalOES s_texture:

    /**
     * 顶点着色器代码
     */
    private val vertexShaderCode = """
        attribute vec4 vPosition;
        attribute vec2 inputTextureCoordinate;
        varying vec2 textureCoordinate;
        void main(){
        gl_Position = vPosition;
        textureCoordinate = inputTextureCoordinate;}      
    """

    /**
     * 片段着色器代码
     */
    private val fragmentShaderCode = """
        #extension GL_OES_EGL_image_external : require
        precision mediump float;varying vec2 textureCoordinate;
        uniform samplerExternalOES s_texture;
        void main() {  gl_FragColor = texture2D( s_texture, textureCoordinate );
        }
        """

3.4.3 相机纹理顶点坐标

  OpenGL预览相机画面其实就是将相机纹理绘制到一个四边形上,与Android OpenGL基础(三、绘制Bitmap纹理)中绘制2D图片纹理不同的地方在于,相机数据的起点是手机横屏时的左上角为(0,0)点,所以如果想要让相机的画面符合我们想要的竖屏预览,需要将顶点对应的纹理坐标设置为:

    // 四边形顶点的坐标
    private var squareCoords = floatArrayOf(
        -1f, 1f, 0.0f,      // top left
        -1f, -1f, 0.0f,      // bottom left
        1f, -1f, 0.0f,      // bottom right
        1f, 1f, 0.0f       // top right
    )

    // 顶点所对应的纹理坐标
    private var textureVertices = floatArrayOf(
        0f, 1f,      // top left
        1f, 1f,      // bottom left
        1f, 0f,       // bottom right
        0f, 0f     // top right
    )

3.4.4 总结

  在修改了以上代码后,实际的绘制方法draw()与Android OpenGL基础(三、绘制Bitmap纹理)中绘制2D纹理完全一致。完整代码如下:

class CameraDrawer {

    /**
     * 顶点着色器代码
     */
    private val vertexShaderCode = """
        attribute vec4 vPosition;
        attribute vec2 inputTextureCoordinate;
        varying vec2 textureCoordinate;
        void main(){
        gl_Position = vPosition;
        textureCoordinate = inputTextureCoordinate;}      
    """

    /**
     * 片段着色器代码
     */
    private val fragmentShaderCode = """
        #extension GL_OES_EGL_image_external : require
        precision mediump float;varying vec2 textureCoordinate;
        uniform samplerExternalOES s_texture;
        void main() {  gl_FragColor = texture2D( s_texture, textureCoordinate );
        }
        """

    /**
     * 着色器程序ID引用
     */
    private var mProgram = 0

    /**
     * 相机预览SurfaceTexture
     */
    private var cameraSurfaceTexture: SurfaceTexture

    // 四边形顶点的坐标
    private var squareCoords = floatArrayOf(
        -1f, 1f, 0.0f,      // top left
        -1f, -1f, 0.0f,      // bottom left
        1f, -1f, 0.0f,      // bottom right
        1f, 1f, 0.0f       // top right
    )

    // 顶点所对应的纹理坐标
    private var textureVertices = floatArrayOf(
        0f, 1f,      // top left
        1f, 1f,      // bottom left
        1f, 0f,       // bottom right
        0f, 0f     // top right
    )

    // 四个顶点的缓冲数组
    private val vertexBuffer: FloatBuffer =
        ByteBuffer.allocateDirect(squareCoords.size * 4).order(ByteOrder.nativeOrder())
            .asFloatBuffer().apply {
                put(squareCoords)
                position(0)
            }

    // 四个顶点的绘制顺序数组
    private val drawOrder = shortArrayOf(0, 1, 2, 0, 2, 3)

    // 四个顶点绘制顺序数组的缓冲数组
    private val drawListBuffer: ShortBuffer =
        ByteBuffer.allocateDirect(drawOrder.size * 2).order(ByteOrder.nativeOrder())
            .asShortBuffer().apply {
                put(drawOrder)
                position(0)
            }

    // 四个顶点的纹理坐标缓冲数组
    private val textureVerticesBuffer: FloatBuffer =
        ByteBuffer.allocateDirect(textureVertices.size * 4).order(ByteOrder.nativeOrder())
            .asFloatBuffer().apply {
                put(textureVertices)
                position(0)
            }

    private var textureID = 0

    // 每个顶点的坐标数
    private val COORDS_PER_VERTEX = 3

    // 每个纹理顶点的坐标数
    private val COORDS_PER_TEXTURE_VERTEX = 2
    private val vertexStride: Int = COORDS_PER_VERTEX * 4
    private val textVertexStride: Int = COORDS_PER_TEXTURE_VERTEX * 4

    init {
        // 编译顶点着色器和片段着色器
        val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
        val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
        // glCreateProgram函数创建一个着色器程序,并返回新创建程序对象的ID引用
        mProgram = GLES20.glCreateProgram().also {
            // 把顶点着色器添加到程序对象
            GLES20.glAttachShader(it, vertexShader)
            // 把片段着色器添加到程序对象
            GLES20.glAttachShader(it, fragmentShader)
            // 连接并创建一个可执行的OpenGL ES程序对象
            GLES20.glLinkProgram(it)
        }
        val texture = OpenGLUtils.createTextures(
            GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 1,
            GLES20.GL_NEAREST, GLES20.GL_LINEAR,
            GLES20.GL_CLAMP_TO_EDGE, GLES20.GL_CLAMP_TO_EDGE
        )
        textureID = texture[0]
        cameraSurfaceTexture = SurfaceTexture(textureID)
    }

    fun getSurfaceTexture(): SurfaceTexture {
        return cameraSurfaceTexture
    }

    fun draw() {
        // 激活着色器程序 Add program to OpenGL ES environment
        GLES20.glUseProgram(mProgram)
        // 获取顶点着色器中的vPosition变量(因为之前已经编译过着色器代码,所以可以从着色器程序中获取);用唯一ID表示
        val position = GLES20.glGetAttribLocation(mProgram, "vPosition")
        // 允许操作顶点对象position
        GLES20.glEnableVertexAttribArray(position)
        // 将顶点数据传递给position指向的vPosition变量;将顶点属性与顶点缓冲对象关联
        GLES20.glVertexAttribPointer(
            position, COORDS_PER_VERTEX, GLES20.GL_FLOAT,
            false, vertexStride, vertexBuffer
        )
        // 激活textureID对应的纹理单元
        GLES20.glActiveTexture(textureID)
        // 绑定纹理
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureID)
        // 获取顶点着色器中的inputTextureCoordinate变量(纹理坐标);用唯一ID表示
        val textureCoordinate = GLES20.glGetAttribLocation(mProgram, "inputTextureCoordinate")
        // 允许操作纹理坐标inputTextureCoordinate变量
        GLES20.glEnableVertexAttribArray(textureCoordinate)
        // 将纹理坐标数据传递给inputTextureCoordinate变量
        GLES20.glVertexAttribPointer(
            textureCoordinate, COORDS_PER_TEXTURE_VERTEX, GLES20.GL_FLOAT,
            false, textVertexStride, textureVerticesBuffer
        )
        // 按drawListBuffer中指定的顺序绘制四边形
        GLES20.glDrawElements(
            GLES20.GL_TRIANGLE_STRIP, drawOrder.size,
            GLES20.GL_UNSIGNED_SHORT, drawListBuffer
        )
        // 操作完后,取消允许操作顶点对象position
        GLES20.glDisableVertexAttribArray(position)
        GLES20.glDisableVertexAttribArray(textureCoordinate)
    }

    private fun loadShader(type: Int, shaderCode: String): Int {
        // glCreateShader函数创建一个顶点着色器或者片段着色器,并返回新创建着色器的ID引用
        val shader = GLES20.glCreateShader(type)
        // 把着色器和代码关联,然后编译着色器
        GLES20.glShaderSource(shader, shaderCode)
        GLES20.glCompileShader(shader)
        return shader
    }
}

四、相机滤镜

  与Android OpenGL基础(四、图片后处理)中介绍的图片后处理类似,在相机的预览中,可以通过修改片段着色的代码实现相机滤镜。

4.1 灰度滤镜

/**
 * 片段着色器代码
 */
private val fragmentShaderCode = """
    #extension GL_OES_EGL_image_external : require
    precision mediump float;varying vec2 textureCoordinate;
    uniform samplerExternalOES s_texture;
    void main() {
    gl_FragColor = texture2D( s_texture, textureCoordinate );
    float average = 0.2126 * gl_FragColor.r + 0.7152 * gl_FragColor.g + 0.0722 * gl_FragColor.b;
    gl_FragColor = vec4(average, average, average, 1.0);
    }
    """

4.2 边缘检测滤镜

/**
 * 片段着色器代码
 */
private val fragmentShaderCode = """
    #extension GL_OES_EGL_image_external : require
    precision mediump float;varying vec2 textureCoordinate;
    uniform samplerExternalOES s_texture;
    const float offset = 1.0f / 300.0f;
    void main() {
        vec2 offsets[9];
        offsets[0] = vec2(-offset,  offset); // 左上
        offsets[1] = vec2( 0.0f,    offset); // 正上
        offsets[2] = vec2( offset,  offset); // 右上
        offsets[3] = vec2(-offset,  0.0f);   // 左
        offsets[4] = vec2( 0.0f,    0.0f);   // 中
        offsets[5] = vec2( offset,  0.0f);   // 右
        offsets[6] = vec2(-offset, -offset); // 左下
        offsets[7] = vec2( 0.0f,   -offset); // 正下
        offsets[8] = vec2( offset, -offset);  // 右下
        // 核函数
        float kernel[9];
        kernel[0] = 1.0f;
        kernel[1] = 1.0f;
        kernel[2] = 1.0f;
        kernel[3] = 1.0f;
        kernel[4] = -8.0f;
        kernel[5] = 1.0f;
        kernel[6] = 1.0f;
        kernel[7] = 1.0f;
        kernel[8] = 1.0f;
        // 计算采样值
        vec3 sampleTex[9];
        for(int i = 0; i < 9; i++)
        {
            sampleTex[i] = vec3(texture2D(s_texture, textureCoordinate.xy + offsets[i]));
        }r
        vec3 col = vec3(0.0);
        for(int i = 0; i < 9; i++)
            col += sampleTex[i] * kernel[i];

        gl_FragColor = vec4(col, 1.0);
    }
    """

  其他类型的滤镜也是类似地修改片段着色器代码即可,下面给出了相机原画面以及几种其他滤镜的效果: image.png

The End

欢迎关注我,一起解锁更多技能:BC的掘金主页~💐 BC的CSDN主页~💐💐 个人信息汇总.png

Android OpenGL开发者文档:developer.android.com/guide/topic…
opengl学习资料:learnopengl-cn.github.io/
相机开发文档:developer.android.com/guide/topic…
surfaceview开发者文档:source.android.com/devices/gra…
滤镜开源库GPUImage:github.com/cats-oss/an…
Android OpenGL基础(一、绘制三角形四边形):juejin.cn/post/707675…
Android OpenGL基础(二、坐标系统):juejin.cn/post/707713…
Android OpenGL基础(三、绘制Bitmap纹理):juejin.cn/post/707967…
Android OpenGL基础(四、图片后处理):juejin.cn/post/708073…
Android OpenGL基础专栏:juejin.cn/column/7076…