前端图片处理 - 高斯模糊

前言

高斯模糊在图片处理中是比较常见的一种处理,其视觉效果就像是经过一个半透明屏幕在观察图像。这篇文章会基于webgl展示一种通用的实现,以及针对大图的优化。

基于二维正态分布的实现

高斯模糊使用正态分布来计算当前像素周围和自身的权重,并将这些权重和对应的rgb值相乘,加总再除以总权重。因为是取像素周围的点,当前点为中点(即 0,0的点),所以使用二维正态分布来计算权重。二维正态分布如下图所示:

png

其中x,y分别是目标像素与中心像素水平和垂直方向的差值,σ是正态分布的标准偏差,一般为模糊直径的1/3,模糊直径越大,则画面越模糊。那根据模糊直径计算当前像素的rgba值,在glsl中的实现如下图所示:

// diameter   模糊直径      
// sampler    图片纹理
// width      图片宽度
// height     图片长度
vec4 blur(int diameter,sampler2D sampler,float width,float height){
        
    const float PI = 3.14159265;
    // 最大模糊直径
    const int maxBlur = 100;
    // 保证模糊直径为奇数
    if(mod(float(diameter), 2.0) == 0.0){
        diameter++;
    }
        
    if(diameter > maxBlur){
        diameter = maxBlur;
    }
    
    // 中心点
    int center = (diameter - 1) / 2;
    // σ的平方
    float sita = pow(float(diameter) / 6.0, 2.0);
    float sum = 0.0;
    vec4 sumVec4 = vec4(0.0);

    for(int i = 0; i < maxBlur; i++) if(i<diameter){
        for(int j = 0; j < maxBlur; j++) if(j<diameter){
            // 遍历周围像素点
            float x = float(i-center);
            float y = float(j-center);
            
            // 计算权重
            float weight = 0.5 / PI / sita * exp(-(pow(x, 2.0) + pow(y, 2.0)) / sita / 2.0);
            // 总权重
            sum += weight;
            // 获取像素点
            vec4 v = texture2D(sampler, vec2( texCoord.x + x/width, texCoord.y + y/height ));
            sumVec4 += v * weight;
        }
    }
    // 加总取平均
    return vec4(sumVec4.r/sum, sumVec4.g/sum, sumVec4.b/sum, sumVec4.a/sum);
} 

·
基于这个函数,实现的效果可以看这里以及相应代码。可以看到,模糊程度是随着模糊长度的加大而增大。

基于一维正态分布的优化实现

通过上面的demo可以发现,模糊直径大于50,会有明显的卡顿,问题在于texture2D这个函数在每个像素上被执行了diameter*diameter次,假如模糊直径是20,那texture2D的被执行次数是400次。对于大图(比如3840 * 2160),texture2D的执行次数不宜超过100次,否则会有明显的卡顿,在性能较低的电脑上,会直接卡死。在实际的应用中,需要对上述的模糊算法进行优化。

一维正态分布如下图所示:
png--1-

优化的核心是使用两次一维正态分布(横向和纵向)来替代一次二维正态分布,需要注意的是,第二次的处理需要在第一次处理的基础上执行,texture的执行次数从原来的diameter*diameter次变为diameter+diameter次,且不影响模糊的效果。这需要借助webgl中的FrameBuffer来实现。

第一次处理的glsl如下所示

// diameter   模糊直径      
// sampler    图片纹理
// width      图片宽度
// height     图片长度 
vec4 blur(int diameter,sampler2D sampler,float width,float height){
            const float PI = 3.14159265;
            const int maxBlur = 41;

            if(mod(float(diameter), 2.0) == 0.0){
                diameter++;
            }
            if(diameter > maxBlur){
                diameter = maxBlur;
            }
            int center = (diameter - 1) / 2;

            float sita = pow(float(diameter) / 6.0, 2.0);
            float radio = sqrt(0.5 / PI / sita);
            float sum = 0.0;
            vec4 sumVec4 = vec4(0.0);

            for(int i = 0; i < maxBlur; i++) if(i<center + 1){

                float weight =  radio * exp(-pow(float(i), 2.0) / sita / 2.0);

                float ii = float(i);

                if(i == 0){
                    // 取中心点
                    vec4 color = texture2D(sampler, texCoord);
                    sumVec4 += color * weight;
                    sum += weight;
                }else{
                    // 左边
                    vec4 left = texture2D(sampler, vec2( texCoord.x - ii/width, texCoord.y));
                    // 右边 
                    vec4 right = texture2D(sampler, vec2( texCoord.x + ii/width, texCoord.y));
                    sumVec4 += left * weight;
                    sumVec4 += right * weight;
                    sum += 2.0 * weight;
                }

            }
            return vec4(sumVec4.r/sum, sumVec4.g/sum, sumVec4.b/sum, sumVec4.a/sum);
        }

第二次处理的glsl如下所示

// diameter   模糊直径      
// sampler    图片纹理
// width      图片宽度
// height     图片长度 
vec4 blur(int diameter,sampler2D sampler,float width,float height){
            const float PI = 3.14159265;
            const int maxBlur = 41;

            if(mod(float(diameter), 2.0) == 0.0){
                diameter++;
            }
            if(diameter > maxBlur){
                diameter = maxBlur;
            }
            int center = (diameter - 1) / 2;

            float sita = pow(float(diameter) / 6.0, 2.0);
            float radio = sqrt(0.5 / PI / sita);
            float sum = 0.0;
            vec4 sumVec4 = vec4(0.0);

            for(int i = 0; i < maxBlur; i++) if(i<center + 1){

                float weight =  radio * exp(-pow(float(i), 2.0) / sita / 2.0);

                float ii = float(i);

                if(i == 0){
                    // 取中心点
                    vec4 color = texture2D(sampler, texCoord);
                    sumVec4 += color * weight;
                    sum += weight;
                }else{
                    // 取上
                    vec4 left = texture2D(sampler, vec2( texCoord.x, texCoord.y - ii/height));
                    // 取下
                    vec4 right = texture2D(sampler, vec2( texCoord.x, texCoord.y + ii/height));
                    sumVec4 += left * weight;
                    sumVec4 += right * weight;
                    sum += 2.0 * weight;
                }

            }
            return vec4(sumVec4.r/sum, sumVec4.g/sum, sumVec4.b/sum, sumVec4.a/sum);
        }

第一次处理需要使用framebuffer应用到一个texture,再在这个texture上应用第二次处理。具体代码可以看这里实现的效果可以看这里。可以看到,在优化之后,模糊半径到100以上也不会有卡顿。

后记

在实际的项目中,可以使用glfx来封装自定义的算法。它同时自带了很多图像处理的效果,基于framebuffer的链式处理也封装的很好,不用像使用原生那么麻烦。

延伸阅读:

高斯模糊简介
高斯模糊浅析
webgl
正态分布
framebuffer机制

知识共享许可协议
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

housheng

Read more posts by this author.

comments powered by Disqus