Android OpenGL基础相机预览及滤镜

Posted 小陈乱敲代码

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android OpenGL基础相机预览及滤镜相关的知识,希望对你有一定的参考价值。

一、相机简介

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);
    
    """ 

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

最后

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长。而不成体系的学习效果低效漫长且无助。时间久了,付出巨大的时间成本和努力,没有看到应有的效果,会气馁是再正常不过的。

所以学习一定要找到最适合自己的方式,有一个思路方法,不然不止浪费时间,更可能把未来发展都一起耽误了。

如果你是卡在缺少学习资源的瓶颈上,那么刚刚好我能帮到你。以上知识笔记全部免费分享,**如有需要获取知识笔记的朋友,可以点击下方二维码费领取。

以上是关于Android OpenGL基础相机预览及滤镜的主要内容,如果未能解决你的问题,请参考以下文章

Android Camera API/Camera2 API 相机预览及滤镜贴纸等处理

Android Camera API/Camera2 API 相机预览及滤镜贴纸等处理

android 采集摄像头预览帧,使用opencv和MediaCodec直接录制水印滤镜视频

android 采集摄像头预览帧,使用opencv和MediaCodec直接录制水印滤镜视频

使用 OpenGL 2.0 API 在 GLSurfaceView 上将 Android 相机预览旋转 90 度

Android openGl开发详解-相机预览