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

目录

  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. 高斯模糊的优化方向与实现

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容