前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >OpenGL 之 GPUImage 源码分析

OpenGL 之 GPUImage 源码分析

作者头像
音视频开发进阶
发布2019-07-25 15:54:31
9440
发布2019-07-25 15:54:31
举报

GPUImage 是 iOS 上一个基于 OpenGL 进行图像处理的开源框架,后来有人借鉴它的想法实现了一个 Android 版本的 GPUImage ,本文也主要对 Android 版本的 GPUImage 进行分析。

概要

在 GPUImage 中既有对图像进行处理的,也有对相机内容进行处理的,这里主要以相机处理为例进行分析。

大致会分为三个部分:

  • 相机数据的采集
  • OpenGL 对图像的处理与显示
  • 相机的拍摄

相机数据采集

相机数据采集实际上就是把相机的图像数据转换成 OpenGL 中的纹理。

在相机的业务开发中,会给相机设置 PreviewCallback 回调方法,只要相机处于预览阶段,这个回调就会被重复调用,返回当前预览帧的内容。

代码语言:javascript
复制
1    camera.setPreviewCallback(GPUImageRenderer.this);
2    camera.startPreview();

默认情况下,相机返回的数据是 NV21 格式,也就是 YCbCr_420_SP 格式,而 OpenGL 使用的纹理是 RGB 格式,所以在每一次的回调方法中需要将 YUV 格式的数据转换成 RGB 格式数据。

代码语言:javascript
复制
1GPUImageNativeLibrary.YUVtoRBGA(data, previewSize.width, previewSize.height,
2                             mGLRgbBuffer.array());

有了图像的 RGB 数据,就可以使用 glGenTextures 生成纹理,并用 glTexImage2D 方法将图像数据作为纹理。

另外,如果纹理已经生成了,当再有图像数据过来时,只需要更新数据就好了,无需重复创建纹理。

代码语言:javascript
复制
 1    // 根据图像数据加载纹理
 2    public static int loadTexture(final IntBuffer data, final Size size, final int usedTexId) {
 3        int textures[] = new int[1];
 4        if (usedTexId == NO_TEXTURE) {
 5            GLES20.glGenTextures(1, textures, 0);
 6            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
 7            // 省略部分代码
 8            GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, size.width, size.height,
 9                    0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, data);
10        } else {
11            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, usedTexId);
12            // 更新纹理数据就好,无需重复创建纹理
13            GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, size.width,
14                    size.height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, data);
15            textures[0] = usedTexId;
16        }
17        return textures[0];
18    }

通过在 PreviewCallback 回调方法中的操作,就完成了将图像数据转换为 OpenGL 的纹理。

接下来就是如何将纹理数据进行处理,并且显示到屏幕上。

在相机数据采集中,还有一些小的细节问题,比如相机前置与后置摄像头的左右镜像翻转问题。

对于前置摄像头,再把传感器内容作为纹理显示时,前置摄像头要做一个左右的翻转处理,因为我们看到的是一个镜像内容,符合正常的自拍流程。

在 GPUImage 的 TextureRotationUtil 类中有定义了纹理坐标,这些纹理坐标系的原点不是位于左下角进行定义的,而是位于左上角。

如果以左下角为纹理坐标系的坐标原点,那么除了要将纹理坐标向右顺时针旋转 90° 之外,还需要进行上下翻转才行,至于为什么要向右顺时针旋转 90° ,参考这篇文章,投稿在郭霖的公众号上:

Android相机开发详解

当我们把纹理坐标以左上角为原点,并相对于顶点坐标顺时针旋转 90 ° 之后,才能够正常的显示图像:

代码语言:javascript
复制
 1    // 顶点坐标
 2    static final float CUBE[] = {
 3            -1.0f, -1.0f,
 4            1.0f, -1.0f,
 5            -1.0f, 1.0f,
 6            1.0f, 1.0f,
 7    };
 8    // 以左上角为原点,并相对于顶点坐标顺时针旋转 90° 后的纹理坐标
 9    public static final float TEXTURE_ROTATED_90[] = {
10            1.0f, 1.0f,
11            1.0f, 0.0f,
12            0.0f, 1.0f,
13            0.0f, 0.0f,
14    };

图像处理与显示

在有了纹理之后,需要明确的是,这个纹理就是相机采集到的图像内容,我们要将纹理绘制到屏幕上,实际上是绘制一个矩形,然后纹理是贴在这个矩形上的。

所以,这里可以回顾一下 OpenGL 是如何绘制矩形的,并且将纹理贴到矩形上:

OpenGL 学习系列---纹理

在 GPUImage 中,GPUImageFilter 类就完成了上述的操作,它是 OpenGL 中所有滤镜的基类。

解析 GPUImageFilter 的代码实现:

在 GPUImageFilter 的构造方法中会确定好需要使用的顶点着色器和片段着色器脚本内容。

init 方法中会调用 onInit 方法和 onInitialized 方法。

  • onInit 方法会创建 OpenGL 中的 Program,并且会绑定到着色器脚本中声明的 attributeuniform 变量字段。
  • onInitialized 方法会给一些 uniform 字段变量赋值,在 GPUImageFilter 类中还对不同类型的变量赋值进行了对应的方法,比如对 float 变量:
代码语言:javascript
复制
1    protected void setFloat(final int location, final float floatValue) {
2        runOnDraw(new Runnable() {
3            @Override
4            public void run() {
5                GLES20.glUniform1f(location, floatValue);
6            }
7        });
8    }

在 onDraw 方法中就是执行具体的绘制了,在绘制的时候会执行 runPendingOnDrawTasks 方法,这是因为我们在 init 方法去中给着色器语言中的变量赋值,并没有立即生效,而是添加到了一个链表中,所以需要把链表中的任务执行完了才接着执行绘制。

代码语言:javascript
复制
 1    public void onDraw(final int textureId, final FloatBuffer cubeBuffer,
 2                       final FloatBuffer textureBuffer) {
 3        GLES20.glUseProgram(mGLProgId);
 4        // 执行赋值的任务
 5        runPendingOnDrawTasks();
 6        // 顶点和纹理坐标数据
 7        GLES20.glVertexAttribPointer(mGLAttribPosition, 2, GLES20.GL_FLOAT, false, 0, cubeBuffer);
 8        GLES20.glEnableVertexAttribArray(mGLAttribPosition);
 9        GLES20.glVertexAttribPointer(mGLAttribTextureCoordinate, 2, GLES20.GL_FLOAT, false, 0, textureBuffer);
10        GLES20.glEnableVertexAttribArray(mGLAttribTextureCoordinate);
11        // 在绘制前的最后一波操作
12        onDrawArraysPre();
13        // 最终绘制
14        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
15    }

在绘制时,还需要给顶点坐标赋值,给纹理坐标赋值,GPUImageFilter 并没有去管理顶点坐标和纹理坐标,而是通过传递参数的形式,这样就不用去处理在前置摄像头与后置前摄像头、手机竖立放置与横屏放置时的关系了。

在执行具体的 glDrawArrays 方法之前,还提供了一个 onDrawArraysPre 方法,在这个方法里面还可以执行绘制前的最后一波操作,在某些滤镜的实现中有用到了。

最后才是 glDrawArrays 方法去完成绘制。

当我们不需要 GPUImageFilter 进行绘制时,需要将它销毁掉,在 destroy 方法去进行销毁,并且提供 onDestory 方法去为某些滤镜提供自定义的销毁。

代码语言:javascript
复制
1    public final void destroy() {
2        mIsInitialized = false;
3        GLES20.glDeleteProgram(mGLProgId);
4        onDestroy();
5    }
6
7    public void onDestroy() {
8    }

在 GPUImageFilter 方法中定义了片段着色器脚本,这个脚本是将图像内容原样贴到了矩形上,并没有做特殊的图像处理操作。

而其他滤镜中,更改了着色器脚本,也就会对图像进行其他的处理,在整个 GPUImage 项目中,最精华的也就是那些着色器脚本内容了,如何通过着色器去做图像处理又是一门高深的学问了~~~

解析 GPUImageFilterGroup 的代码实现

当想要对图像进行多次处理时,就得考虑使用 GPUImageFilterGroup 了。

GPUImageFilterGroup 继承自 GPUImageFilter, 顾名思义就是一系列 GPUImageFilter 滤镜的组合,可以把它类比为 ViewGroup ,ViewGroup 即可以包含 View ,也可以包含 ViewGroup ,同样 GPUImageFilterGroup 即可以包含 GPUImageFilter,也可以包含 GPUImageFilterGroup。

在用 GPUImageFilterGroup 进行绘制时,需要把所有的滤镜内容都进行一遍绘制,而对于 GPUImageFilterGroup 包含 GPUImageFilterGroup 的情况,就需要把子 GPUImageFilterGroup 内的滤镜内容拆分出来,最终是用 mMergedFilters 变量表示所有非 GPUImageFilterGroup 类型的 GPUImageFilter 。

代码语言:javascript
复制
 1    // 拿到所有非 GPUImageFilterGroup 的 GPUImageFilter
 2    public void updateMergedFilters() {
 3        List<GPUImageFilter> filters;
 4        for (GPUImageFilter filter : mFilters) {
 5            // 如果滤镜是 GPUImageFilterGroup 类型,就把它拆分了
 6            if (filter instanceof GPUImageFilterGroup) {
 7                // 递归调用 updateMergedFilters 方法去拆分
 8                ((GPUImageFilterGroup) filter).updateMergedFilters();
 9                // 拿到所有非 GPUImageFilterGroup 的 GPUImageFilter
10                filters = ((GPUImageFilterGroup) filter).getMergedFilters();
11                if (filters == null || filters.isEmpty())
12                    continue;
13                // 把 GPUImageFilter 添加到 mMergedFilters 中
14                mMergedFilters.addAll(filters);
15                continue;
16            }
17            // 如果是非 GPUImageFilterGroup 直接添加了
18            mMergedFilters.add(filter);
19        }
20    }

在 GPUImageFilterGroup 执行具体的绘制之前,还创建了和滤镜数量一样多的 FrameBuffer 帧缓冲和 Texture 纹理。

代码语言:javascript
复制
 1        // 遍历时,选择 mMergedFilters 的长度,因为 mMergedFilters 里面才是保存的所有的 滤镜的长度。
 2        if (mMergedFilters != null && mMergedFilters.size() > 0) {
 3            size = mMergedFilters.size();
 4            // FrameBuffer 帧缓冲数量
 5            mFrameBuffers = new int[size - 1];
 6            // 纹理数量
 7            mFrameBufferTextures = new int[size - 1];
 8
 9            for (int i = 0; i < size - 1; i++) {
10                // 生成 FrameBuffer 帧缓冲
11                GLES20.glGenFramebuffers(1, mFrameBuffers, i);
12                // 生成纹理
13                GLES20.glGenTextures(1, mFrameBufferTextures, i);
14                GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mFrameBufferTextures[i]);
15                GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0,
16                        GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
17                // 省略部分代码
18                GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[i]);
19                // 纹理绑定到帧缓冲上
20                GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
21                        GLES20.GL_TEXTURE_2D, mFrameBufferTextures[i], 0);
22                // 省略部分代码
23            }
24        }

如果对 FrameBuffer 的使用不熟悉的话,请参考这篇文章:

OpenGL 之 帧缓冲 使用实践

代码语言:javascript
复制
 1        if (mMergedFilters != null) {
 2            int size = mMergedFilters.size();
 3            // 相机原始图像转换的纹理 ID
 4            int previousTexture = textureId;
 5            for (int i = 0; i < size; i++) {
 6                GPUImageFilter filter = mMergedFilters.get(i);
 7                boolean isNotLast = i < size - 1;
 8                // 如果不是最后一个滤镜,绘制到 FrameBuffer 上,如果是最后一个,就绘制到了屏幕上
 9                if (isNotLast) {
10                    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[i]);
11                    GLES20.glClearColor(0, 0, 0, 0);
12                }
13                // 滤镜绘制代码
14                if (i == 0) {
15                    // 第一个滤镜绘制使用相机的原始图像纹理 ID 和参数传递过来的顶点以及纹理坐标
16                    filter.onDraw(previousTexture, cubeBuffer, textureBuffer);
17                } else if (i == size - 1) {
18                    // 
19                    filter.onDraw(previousTexture, mGLCubeBuffer, (size % 2 == 0) ? mGLTextureFlipBuffer : mGLTextureBuffer);
20                } else {
21                    // 中间的滤镜绘制在之前纹理基础上继续绘制,使用 mGLTextureBuffer 纹理坐标
22                    filter.onDraw(previousTexture, mGLCubeBuffer, mGLTextureBuffer);
23                }
24
25                if (isNotLast) {
26                    // 绑定到屏幕上
27                    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
28                    previousTexture = mFrameBufferTextures[i];
29                }
30            }
31        }

在执行具体的绘制时,只要不是最后一个滤镜,那么就会先绑定到 FrameBuffer 上,然后在 FrameBuffer 上进行绘制,这时绘制是绘制到了 FrameBuffer 绑定的纹理上,绘制结束后再接着解绑,绑定到屏幕上。

如果是最后一个滤镜,那么就不用绑定到 FrameBuffer 上了,直接绘制到屏幕上即可。

在这里有个细节,就是如下的代码:

代码语言:javascript
复制
1    filter.onDraw(previousTexture, mGLCubeBuffer, (size % 2 == 0) ? mGLTextureFlipBuffer : mGLTextureBuffer);

如果是最后一个滤镜,并且滤镜个数为偶数,则使用 mGLTextureFlipBuffer 的纹理坐标,否则使用 mGLTextureBuffer 的纹理坐标。

代码语言:javascript
复制
1// 对应的纹理坐标为 TEXTURE_NO_ROTATION
2 mGLTextureBuffer.put(TEXTURE_NO_ROTATION).position(0);
3
4// 对应的纹理坐标为 TEXTURE_NO_ROTATION,并且 true 的参数表示进行垂直上下翻转
5float[] flipTexture = TextureRotationUtil.getRotation(Rotation.NORMAL, false, true);
6mGLTextureFlipBuffer.put(flipTexture).position(0);

在第一个滤镜绘制时,使用的是参数传递过来的顶点坐标和纹理坐标,中间部分的滤镜使用的是 mGLTextureBuffer 纹理坐标,它对应的纹理坐标数组为 TEXTURE_NO_ROTATION

在前面讲到过,GPUImage 的纹理坐标原点是位于左上角的,所以使用 TEXTURE_NO_ROTATION 的纹理坐标实质上是将图像进行了上下翻转,两次调用TEXTURE_NO_ROTATION纹理坐标时,又将图像复原了,这也就可以解释为什么滤镜个数为偶数时,需要使用 mGLTextureFlipBuffer 纹理坐标将图像再进行一次翻转,而 mGLTextureBuffer 纹理坐标不需要了。

当明白了 GPUImageFilter 和 GPUImageFilterGroup 的实现之后,再去看具体的 Renderer 的代码就明了多了。

onSurfaceCreatedonSurfaceChanged 方法中分别对滤镜进行初始化以及设定宽、高,在 onDrawFrame 方法中调用具体的绘制。

当切换滤镜时,先将上一个滤镜销毁掉,然后初始化新的滤镜并设定宽、高。

代码语言:javascript
复制
1                final GPUImageFilter oldFilter = mFilter;
2                mFilter = filter;
3                if (oldFilter != null) {
4                    oldFilter.destroy();
5                }
6                mFilter.init();
7                GLES20.glUseProgram(mFilter.getProgram());
8                mFilter.onOutputSizeChanged(mOutputWidth, mOutputHeight);

图像拍摄保存处理

在 GPUImage 中相机的拍摄是调用 Camera 的 takePicture 方法,在该方法中返回相机采集的原始图像数据,然后再对该数据进行一遍滤镜处理后并保存。

调用的最后都是通过 glReadPixels 方法将处理后的图像读取出来,并保存为 Bitmap 。

代码语言:javascript
复制
 1    private void convertToBitmap() {
 2        int[] iat = new int[mWidth * mHeight];
 3        IntBuffer ib = IntBuffer.allocate(mWidth * mHeight);
 4        mGL.glReadPixels(0, 0, mWidth, mHeight, GL_RGBA, GL_UNSIGNED_BYTE, ib);
 5        int[] ia = ib.array();
 6        // glReadPixels 读取的内容是上下翻转的,要处理一下
 7        for (int i = 0; i < mHeight; i++) {
 8            for (int j = 0; j < mWidth; j++) {
 9                iat[(mHeight - i - 1) * mWidth + j] = ia[i * mWidth + j];
10            }
11        }
12        mBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
13        mBitmap.copyPixelsFromBuffer(IntBuffer.wrap(iat));
14    }

小结

对 GPUImage 的分析以及滤镜架构的设计大致就是这样了,这些都还不是它的精华啦,重要的还是它的那些着色器脚本,从那些着色器脚本中学会如果通过 GLSL 去实现图像处理算法。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-09-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 纸上浅谈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概要
    • 相机数据采集
    • 图像处理与显示
      • 解析 GPUImageFilter 的代码实现:
        • 解析 GPUImageFilterGroup 的代码实现
        • 图像拍摄保存处理
        • 小结
        相关产品与服务
        图像处理
        图像处理基于腾讯云深度学习等人工智能技术,提供综合性的图像优化处理服务,包括图像质量评估、图像清晰度增强、图像智能裁剪等。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档