美文网首页
音视频开发之旅(39)- 高斯模糊实现与优化

音视频开发之旅(39)- 高斯模糊实现与优化

作者: yabin小站 | 来源:发表于2021-03-25 09:19 被阅读0次

    目录

    1. 高斯模糊的原理
    2. GPUImage模糊的实现分析
    3. 高斯模糊优化
    4. 资料
    5. 收获

    我们在平时的开发中模糊是非常常用的技能,在android中有java的开源方案,也有RenderScript方案,今天我们来学习实践通过OpenGL如何实现高斯模糊。
    在工作中用到的高斯模糊,也只是做到基本的简单实用,为什么能实现以及是否可以性能优化点提升速度降低内存,之前都欠考虑。
    通过这篇我们来学习高斯模糊的原理、实现以及优化,我们的旅程开启。

    一、高斯模糊的原理

    这一小节会涉及到一些数学中基本概念,正态分布、高斯函数、卷积、模糊半径等,通过下面的学习实践我们对其进行回顾学习。

    "模糊",可以理解成每一个像素都取周边像素的平均值,模糊分类有很多种,我们来看下均值模糊和高斯模糊

    均值模糊是每个像素的值都取周边元素的平均值,并且周边没有点不管距离当前点的距离远近,权重相同

    图片截图来自:GAMES101-现代计算机图形学入门-闫令琪

    均值模糊可以实现模糊效果,但是如果模糊后的效果看起来和原图效果更相近,就要考虑权重的问题,即距离越近的点权重越大,距离越远的点权重越小。

    正态分布是一种权重分配模式,越接近中心,取值越大,越远离中心,取值越小。


    图片来自:高斯模糊的算法

    图片是二维的,对应的是二维正态分布,正态分布的密度函数叫做"高斯函数"(Gaussian function)


    图片来自:Android图像处理 - 高斯模糊的原理及实现 ,函数中的σ是x的方差

    有了高斯函数,我们就可以计算每个点的权重。
    假设模糊半径是1,构建一个3x3的矩阵,假设高斯函数的σ为1.5,根据xy的坐标值计算每一个点的权重值,然后所有点权重值相加应该为1,所以对上述计算后的值进行归一化处理。

    有了归一化的权重矩阵,把其作为卷积核,与原有图片进行卷积运算,得出模糊后的值。

    image

    高斯模糊 是一个低通滤波,过滤掉高频信号,剩下低频信号,图像内容的边界去掉 ,实现blur

    二、GPUImage高斯模糊的实现分析

    了解了高斯模糊的原理,这一小节我们看下如何实现高斯模糊,GPUImage是一个非常强大和丰富的OpenGL图像处理开源库,其中带了部分滤镜的实现 ,对应的高斯模糊滤镜 为GPUImageGaussianBlurFilter,我们分析下它是如何实现的。

    //顶点着色器
    
    attribute vec4 position;
    attribute vec4 inputTextureCoordinate;
    
    const int GAUSSIAN_SAMPLES = 9;
    
    uniform float texelWidthOffset;
    uniform float texelHeightOffset;
    
    varying vec2 textureCoordinate;
    varying vec2 blurCoordinates[GAUSSIAN_SAMPLES];
    
    void main()
    {
        gl_Position = position;
        textureCoordinate = inputTextureCoordinate.xy;
        
        // Calculate the positions for the blur
        int multiplier = 0;
        vec2 blurStep;
       vec2 singleStepOffset = vec2(texelHeightOffset, texelWidthOffset);
        
        for (int i = 0; i < GAUSSIAN_SAMPLES; i++)
       {
            multiplier = (i - ((GAUSSIAN_SAMPLES - 1) / 2));
           // Blur in x (horizontal)
           blurStep = float(multiplier) * singleStepOffset;
            blurCoordinates[i] = inputTextureCoordinate.xy + blurStep;
        }
    }
    
    //片源着色器
    
    uniform sampler2D inputImageTexture;
    
    const lowp int GAUSSIAN_SAMPLES = 9;
    
    varying highp vec2 textureCoordinate;
    varying highp vec2 blurCoordinates[GAUSSIAN_SAMPLES];
    
    void main()
    {
        lowp vec3 sum = vec3(0.0);
       lowp vec4 fragColor=texture2D(inputImageTexture,textureCoordinate);
        
        sum += texture2D(inputImageTexture, blurCoordinates[0]).rgb * 0.05;
        sum += texture2D(inputImageTexture, blurCoordinates[1]).rgb * 0.09;
        sum += texture2D(inputImageTexture, blurCoordinates[2]).rgb * 0.12;
        sum += texture2D(inputImageTexture, blurCoordinates[3]).rgb * 0.15;
        sum += texture2D(inputImageTexture, blurCoordinates[4]).rgb * 0.18;
        sum += texture2D(inputImageTexture, blurCoordinates[5]).rgb * 0.15;
        sum += texture2D(inputImageTexture, blurCoordinates[6]).rgb * 0.12;
        sum += texture2D(inputImageTexture, blurCoordinates[7]).rgb * 0.09;
        sum += texture2D(inputImageTexture, blurCoordinates[8]).rgb * 0.05;
    
        gl_FragColor = vec4(sum,fragColor.a);
    }
    

    通过着色器代码我们看到GAUSSIAN_SAMPLES = 9;左右个4个采样,加中心点1个采样点,即 2x4+1=9,是一个9x9的矩阵。
    blurCoordinates存储计算后的纹理的坐标值。然后在片源着色器中进行卷积运算。

    GPUImage采用了分别对X轴和Y轴的高斯模糊,这样降低了算法的复杂度。

    高斯滤波器的卷积核是二维的(mn),则算法复杂度为O(mnMN),复杂度较高,算法复杂度变为O(2mM*N)

    Render如下

    public class GPUImageRender implements GLSurfaceView.Renderer {
    
        private Context context;
        private int inputTextureId;
        private GPUImageGaussianBlurFilter blurFilter;
        private  FloatBuffer glCubeBuffer;
        private  FloatBuffer glTextureBuffer;
    
        public static final float CUBE[] = {
                -1.0f, -1.0f,
                1.0f, -1.0f,
                -1.0f, 1.0f,
                1.0f, 1.0f,
        };
    
        public static final float TEXTURE_NO_ROTATION[] = {
                0.0f, 1.0f,
                1.0f, 1.0f,
                0.0f, 0.0f,
                1.0f, 0.0f,
        };
    
    
        public GPUImageRender(Context context) {
            this.context = context;
        }
    
        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
            String vertexStr = ShaderHelper.loadAsset(context.getResources(), "blur_vertex_gpuimage.glsl");
            String fragStr = ShaderHelper.loadAsset(context.getResources(), "blur_frag_gpuimage.glsl");
    
            blurFilter = new GPUImageGaussianBlurFilter(vertexStr,fragStr);
            blurFilter.ifNeedInit();
    
            inputTextureId = TextureHelper.loadTexture(context, R.drawable.bg);
    
            glCubeBuffer = ByteBuffer.allocateDirect(CUBE.length * 4)
                    .order(ByteOrder.nativeOrder())
                    .asFloatBuffer();
            glCubeBuffer.put(CUBE).position(0);
    
            glTextureBuffer = ByteBuffer.allocateDirect(TEXTURE_NO_ROTATION.length * 4)
                    .order(ByteOrder.nativeOrder())
                    .asFloatBuffer();
            glTextureBuffer.put(TEXTURE_NO_ROTATION).position(0);
        }
    
        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            GLES20.glViewport(0, 0, width, height);
            blurFilter.onOutputSizeChanged(width,height);
    
        }
    
        @Override
        public void onDrawFrame(GL10 gl) {
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
            GLES20.glClearColor(0f,0f,0f,1f);
            blurFilter.onDraw(inputTextureId,glCubeBuffer,glTextureBuffer);
        }
    }
    
    
    public class GPUImageTwoPassFilter extends GPUImageFilterGroup {
        public GPUImageTwoPassFilter(String firstVertexShader, String firstFragmentShader,
                                     String secondVertexShader, String secondFragmentShader) {
            super(null);
            addFilter(new GPUImageFilter(firstVertexShader, firstFragmentShader));
            addFilter(new GPUImageFilter(secondVertexShader, secondFragmentShader));
        }
    }
    
        public GPUImageGaussianBlurFilter(float blurSize,String vertexStr,String fragStr) {
    
            super(vertexStr, fragStr, vertexStr, fragStr);
            this.blurSize = blurSize;
        }
    

    完整代码已上传至github https://github.com/ayyb1988/mediajourney

    其中用到上一篇谈到的FBO技术

    //com.av.mediajourney.opengl.gpuimage.GPUImageFilterGroup#onDraw
    
    public void onDraw(final int textureId, final FloatBuffer cubeBuffer,
                           final FloatBuffer textureBuffer) {
            runPendingOnDrawTasks();
            if (!isInitialized() || frameBuffers == null || frameBufferTextures == null) {
                return;
            }
            if (mergedFilters != null) {
                int size = mergedFilters.size();
                int previousTextureId = textureId;
                for (int i = 0; i < size; i++) {
                    GPUImageFilter filter = mergedFilters.get(i);
                    boolean isNotLast = i < size - 1;
                    //如果不是最后一个,则采用FBO方式,进行离屏渲染;否则不挂载到FBO,直接渲染到屏幕
                    if (isNotLast) {
                        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffers[i]);
                        GLES20.glClearColor(0, 0, 0, 0);
                    }
    
                    //第一个filter,采用输入的纹理id、顶点buffer、纹理buffer
                    if (i == 0) {
                        filter.onDraw(previousTextureId, cubeBuffer, textureBuffer);
                    } else if (i == size - 1) {
                        filter.onDraw(previousTextureId, glCubeBuffer, (size % 2 == 0) ? glTextureFlipBuffer : glTextureBuffer);
                    } else {
                        filter.onDraw(previousTextureId, glCubeBuffer, glTextureBuffer);
                    }
    
                    //如果不是最后一个filter,则解绑FBO,并且把当前的输出作为下一个filter的纹理输入
                    if (isNotLast) {
                        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
                        previousTextureId = frameBufferTextures[i];
                    }
                }
            }
        }
    

    详细代码请查看 github https://github.com/ayyb1988/mediajourney

    高斯模糊后的效果如下:


    三、高斯模糊优化

    在保证模糊效果的前提下,怎么样可以提升模糊的效率,即减少耗时,直接的影响因素就是运算量的大小,可以从下面几个方向进行优化:

    1. 减少偏移大小(模糊半径)
    2. 优化算法实现
    3. 先缩放图片,再进行高斯模糊,减少需要处理的数据量
    4. 了解GPU运行方式,减少分支语句,使用opengl3.0等

    ** 减少偏移大小(模糊半径)和优化算法实现见glsl

    //顶点着色器
    attribute vec4 position;
    attribute vec4 inputTextureCoordinate;
    
    //const int GAUSSIAN_SAMPLES = 9;
    
    //优化点:高斯算子的左右偏移,对应的高斯算子为(SHIFT_SIZE*2+1)
    const int SHIFT_SIZE =2;
    
    uniform float texelWidthOffset;
    uniform float texelHeightOffset;
    
    varying vec2 textureCoordinate;
    varying vec4 blurCoordinates[SHIFT_SIZE];
    
    void main()
    {
        gl_Position = position;
        textureCoordinate = inputTextureCoordinate.xy;
    
        //偏移步距
        vec2 singleStepOffset = vec2(texelHeightOffset, texelWidthOffset);
    
    
        //  int multiplier = 0;
        //  vec2 blurStep;
        //
        //  for (int i = 0; i < GAUSSIAN_SAMPLES; i++)
        //  {
        //      multiplier = (i - ((GAUSSIAN_SAMPLES - 1) / 2));
        //      // Blur in x (horizontal)
        //      blurStep = float(multiplier) * singleStepOffset;
        //      blurCoordinates[i] = inputTextureCoordinate.xy + blurStep;
        //  }
    
        // 优化点:减少循环运算次数
        for (int i=0; i< SHIFT_SIZE; i++){
            blurCoordinates[i] = vec4(textureCoordinate.xy - float(i+1)*singleStepOffset,
            textureCoordinate.xy + float(i+1)*singleStepOffset);
        }
    
    
    }
    
    //片源着色器
    uniform sampler2D inputImageTexture;
    
    //const int GAUSSIAN_SAMPLES = 9;
    
    //优化点:高斯算子的左右偏移,对应的高斯算子为(SHIFT_SIZE*2+1)
    const int SHIFT_SIZE =2;
    
    varying highp vec2 textureCoordinate;
    varying vec4 blurCoordinates[SHIFT_SIZE];
    
    void main()
    {
        /*
       lowp vec3 sum = vec3(0.0);
      lowp vec4 fragColor=texture2D(inputImageTexture,textureCoordinate);
       mediump vec3 sum = fragColor.rgb*0.18;
    
       sum += texture2D(inputImageTexture, blurCoordinates[0]).rgb * 0.05;
       sum += texture2D(inputImageTexture, blurCoordinates[1]).rgb * 0.09;
       sum += texture2D(inputImageTexture, blurCoordinates[2]).rgb * 0.12;
       sum += texture2D(inputImageTexture, blurCoordinates[3]).rgb * 0.15;
    
       sum += texture2D(inputImageTexture, blurCoordinates[4]).rgb * 0.18;
    
       sum += texture2D(inputImageTexture, blurCoordinates[5]).rgb * 0.15;
       sum += texture2D(inputImageTexture, blurCoordinates[6]).rgb * 0.12;
       sum += texture2D(inputImageTexture, blurCoordinates[7]).rgb * 0.09;
       sum += texture2D(inputImageTexture, blurCoordinates[8]).rgb * 0.05;
    
       gl_FragColor = vec4(sum,fragColor.a);*/
        
        
        // 计算当前坐标的颜色值
        vec4 currentColor = texture2D(inputTexture, textureCoordinate);
        mediump vec3 sum = currentColor.rgb;
        // 计算偏移坐标的颜色值总和
        for (int i = 0; i < SHIFT_SIZE; i++) {
            sum += texture2D(inputTexture, blurShiftCoordinates[i].xy).rgb;
            sum += texture2D(inputTexture, blurShiftCoordinates[i].zw).rgb;
        }
        // 求出平均值
        gl_FragColor = vec4(sum * 1.0 / float(2 * SHIFT_SIZE + 1), currentColor.a);
       
    }
    

    ** 先缩放图片,再进行高斯模糊,减少需要处理的数据量**

        private static Bitmap getBitmap(Context context, int resourceId) {
                final BitmapFactory.Options options = new BitmapFactory.Options();
                options.inScaled = false;
        
                // Read in the resource
                Bitmap bitmap = BitmapFactory.decodeResource(
                    context.getResources(), resourceId, options);
        
                //优化点:对原图进行缩放,1/16的数据量 ,缩放大小根据具体场景而定
                bitmap = Bitmap.createScaledBitmap(bitmap,
                        bitmap.getWidth() / 4,
                        bitmap.getHeight() / 4,
                        true);
        
                return bitmap;
            }
    

    详细代码请查看 github https://github.com/ayyb1988/mediajourney

    四、资料

    1. B站视频-GAMES101-现代计算机图形学入门-巨老-闫令琪
    2. 你需要知道的数学知识——卷积
    3. 高斯模糊的算法
    4. 数字图像处理---高斯模糊详解
    5. 对Photoshop高斯模糊滤镜的算法总结
    6. 强烈推荐-Android图像处理 - 高斯模糊的原理及实现
    7. OpenGLES滤镜开发汇总 —— 高斯模糊实现以及优化_
    8. OpenGL shader性能优化策略(一):减少分支语句

    五、收获

    通过本篇的学习实践

    1. 了解了高斯模糊的原理
    2. 分析GPUImage高斯模糊的实现流程
    3. 高斯模糊的优化方向与实现

    感谢你的阅读
    下一篇我们学习实践贝塞尔曲线/面,欢迎关注公众号“音视频开发之旅”,一起学习成长。
    欢迎交流

    相关文章

      网友评论

          本文标题:音视频开发之旅(39)- 高斯模糊实现与优化

          本文链接:https://www.haomeiwen.com/subject/cyklhltx.html