H5中基于Canvas实现的高斯模糊

简介: 自从扁平化流行起来之后,高斯模糊效果渐渐变成了视觉很喜欢用的一种表现形式,我们的视觉小姐姐也特别喜欢。为了满足她,踩了无穷无尽的坑之后,最后只能掏出Canvas来了。 没有什么视觉需求是Canvas解决不了的,如果有,再盖一层Canvas —— 奈帆斯基 ##解决痛点 1. CSS模糊 和 大面积transform 混用时,会导致的性能问题 ( 卡 ) 2. CSS模糊 在图

自从扁平化流行起来之后,高斯模糊效果渐渐变成了视觉很喜欢用的一种表现形式,我们的视觉小姐姐也特别喜欢。为了满足她,踩了无穷无尽的坑之后,最后只能掏出Canvas来了。

没有什么视觉需求是Canvas解决不了的,如果有,再盖一层Canvas —— 奈帆斯基

解决痛点

  1. CSS模糊 和 大面积transform 混用时,会导致的性能问题 ( 卡 )
  2. CSS模糊 在图片边界的表现不够优秀
  3. iOS下高像素的高斯模糊会出现 奇怪的现象 ( 突然颜色大变 )
  4. 一套解决方案,不再需要 svg+多种css兼容 判环境应用

理论原理

对算法部分无爱的弟兄们直接跳过本节也没关系的。

理论

模糊的效果相信大家都不陌生,实际上就是一种加权平均算法。
高斯模糊( Gaussian Blur ) 就是以高斯分布作为权重的平均算法。高斯分布长下面这个样子。

[ 一维高斯分布 ]

图片有x,y两个维度,所以在平均的时候应该使用二维高斯分布

[ 二维高斯分布 ]

基本算法

  1. 输入 图片Img , 模糊半径radius
  2. radius 计算出 高斯矩阵 gaussMatrix 避免重复计算
  3. 遍历每一个像素

    • 提取当前像素[x,y]{r,g,b,a}
    • 求范围 [ x ± radius , y ± radius ] 内的 {r,g,b,a} 各自在 gaussMatrix 内的加权均值
  4. 输出

边界的处理

观察系统的高斯模糊效果,边界总是半透明的。推测是在边界处增加 alpha=0 的点补齐计算。

[ css模糊 - 红色部分为初始边界 ]

嗯,这个效果也算 痛点 之一吧。我的解决方案是:仅计算存在的点的权重

算法实现

const gaussBlur = function (imgData, radius) {

    radius *= 3; //不知为什么,我的模糊半径是 css中 filter:bulr 值的三倍时效果才一致。

    //Copy图片内容
    const pixes = new Uint8ClampedArray(imgData.data);
    const width = imgData.width;
    const height = imgData.height;

    let gaussSum = 0,
        x, y,
        r, g, b, a, i;

    //模糊半径取整
    radius = Math.floor(radius);
    //sigma越小中心点权重越高, sigma越大越接近平均模糊
    const sigma = radius / 3;
    //两个分布无相关性, 为了各方向上权重分布一致
    const Ror = 0;

    const L = radius * 2 + 1;  //矩阵宽度

    const Ror2 = Ror * Ror;
    const s2 = sigma * sigma;
    const c1 = 1 / (  2 * Math.PI * s2 * Math.sqrt(1 - Ror * Ror));
    const c2 = -1 / (2 * (1 - Ror2));

    //定义高斯矩阵 , 存储在一维数组中
    const gaussMatrix = [];

    //根据 xy 计算 index
    gaussMatrix.getIndex = (x, y)=> {
        return (x + radius) + (y + radius) * L;
    }
    //根据 xy 获取权重
    gaussMatrix.getWeight = (x, y)=> {
        return gaussMatrix[gaussMatrix.getIndex(x, y)];
    }
    //根据 index 获取 x 偏移
    gaussMatrix.getX = (index)=> {
        return index % L - radius;
    }
    //根据 index 获取 y 偏移
    gaussMatrix.getY = (index)=> {
        return Math.floor(index / L) - radius;
    }

    //覆写forEach , 方便遍历
    gaussMatrix.forEach = (f)=> {
        gaussMatrix.map((w, i)=> {
            f(w, gaussMatrix.getX(i), gaussMatrix.getY(i))
        })
    }

    //生成高斯矩阵
    for (y = -radius; y <= radius; y++) {
        for (x = -radius; x <= radius; x++) {
            let i = gaussMatrix.getIndex(x, y);
            g = c1 * Math.exp(c2 * (x * x + 2 * Ror * x * y + y * y) / s2);
            gaussMatrix[i] = g;
        }
    }

    //快捷获取像素点数据
    const getPixel = (x, y)=> {
        if (x < 0 || x >= width || y < 0 || y >= height) {
            return null;
        }
        let p = (x + y * width) * 4;
        return pixes.subarray(p, p + 4);
    }

    //遍历图像上的每个点
    i = 0;
    for (y = 0; y < height; y++) {
        for (x = 0; x < width; x++) {

            //重置 r g b a Sum
            r = g = b = a = 0;
            gaussSum = 0;

            //遍历模糊半径内的其他点
            gaussMatrix.forEach((w, dx, dy)=> {
                let p = getPixel(x + dx, y + dy);
                if (!p)return;

                //求加权和
                r += p[0] * w;
                g += p[1] * w;
                b += p[2] * w;
                a += p[3] * w;
                gaussSum += w;
            });

            //写回imgData
            imgData.data.set([r, g, b, a].map(v=>v / gaussSum), i);

            //遍历下一个点
            i += 4;
        }
    }

    return imgData;
};

写完了实现的我,迫不及待的试了试

[ 效果拔群! 无与伦比! 掌声呢?!!! ]

一般来说写到这里,就算功成名就了,不过我瞥了一眼控制台...

足足算了21秒,这可是我心爱的 MacPro,我要报警了!

优化算法

目前的算法,复杂度大约是 w h (2r)^2

之后我去搜了搜 大神代码,发现他们是先进行一轮X轴方向模糊,再进行一轮Y轴方向模糊,复杂度只有 2 w h * 2r , 一下少了好多运算量。

我们也来试试。

[ 效果立竿见影 ]

以我的数学水平,并不能证明两者是等效的,但是从视觉上来看是一致的,为什么可以这样优化,期望大神赐教。

使用优化

从算法上可以看出来,运算量由三个方面来决定:图片宽w、高h,模糊半径r。
这样就能对我们的几个常见使用场景进行优化

1. 大尺寸图片

例如一张900x600的图片,需要输出一张300x200@2x

可以将图片先缩放到300x200再计算模糊

2. 大半径模糊

例如一张900x600的图片,需要模糊半径150,需要输出一张300x200@2x的图

这样的图可以说是细节全失,通常视觉只Care成图的大概色彩范围,我们可以用一些粗暴的方法。

  1. 等比例计算,把图片变成 6x4 r=1
  2. 计算模糊,输出 6x4 的图片
  3. 使用css拉伸到 300x200

实现

说白了优化手段就是一招缩小射线,我们抽象一个参数: 缩小倍率 shrink

/**
 * @public
 * 暴露的异步模糊方法
 * ---------------------
 * @param URL       图片地址,需要跨域支持
 * @param r         模糊半径 {Int}
 * @param shrink    缩小比率 {Number}
 * @return {Promise}
 */
export const blur = (URL, r, shrink = 1)=> {
    return new Promise((resolve, reject)=> {

        const IMG = new Image();
        IMG.crossOrigin = '*'; //需要图片跨域支持

        IMG.onload = function () {
            const Canvas = document.createElement('CANVAS'); //大量使用可考虑只创建一次

            let w = IMG.width, h = IMG.height;

            //缩小比例不为1时 , 重新计算宽高比
            if (shrink !== 1) {
                w = Math.ceil(w / shrink);
                h = Math.ceil(h / shrink);
                r = Math.ceil(r / shrink);
            }

            //因为懒, 就全Try了, 实际上只 Try跨域错误 即可
            try {
                //设置Canvas宽高,获取上下文
                Canvas.width = w;
                Canvas.height = h;
                let ctx = Canvas.getContext('2d');

                ctx.drawImage(IMG, 0, 0, w, h);

                //提取图片信息
                let d = ctx.getImageData(0, 0, w, h);

                //进行高斯模糊
                let gd = gaussBlur(d, r, 0);

                //绘制模糊图像
                ctx.putImageData(gd, 0, 0);

                resolve(Canvas.toDataURL());
            } catch (e) {
                reject(e);
            }
        };
        IMG.src = URL;
    })
};

以一张 640x426 的图片,输出{ 300x200,r=10 }为例:
对比

  1. 原尺寸模糊
  2. 缩小到1/10进行模糊


首先要明确的是,在缩小情况下两种算法并不等价。小图放大的模糊效果取决于浏览器本身的算法实现。最终视觉上效果差别不显著,完全可以使用

面对形形色色的尺寸

考虑到来自服务端的图片可能有各种神奇的尺寸,而通常输出是一个确定的尺寸。

在这样的情况下,缩小比例会产生一些冗余,所以更适合另一个【锁定输出宽高的实现】。

/**
 * @public
 * 暴露的异步模糊方法
 * ---------------------
 * @param URL       图片地址,需要跨域支持
 * @param r         模糊半径 {Int}
 * @param w         输出宽度 {Number}
 * @param h         输出高度 {Number}
 * @return {Promise}
 */
export const blurWH = (URL, r, w ,h)=> {
    return new Promise((resolve, reject)=> {

        const IMG = new Image();
        IMG.crossOrigin = '*'; //需要图片跨域支持

        IMG.onload = function () {
            const Canvas = document.createElement('CANVAS'); //大量使用可考虑只创建一次

            //锁定输出宽高之后, 就不需要Care 原图有多宽多高了
            //let w = IMG.width, h = IMG.height;

            //因为懒, 就全Try了, 实际上只 Try跨域错误 即可
            try {
                //设置Canvas宽高,获取上下文
                Canvas.width = w;
                Canvas.height = h;
                let ctx = Canvas.getContext('2d');

                ctx.drawImage(IMG, 0, 0, w, h);

                //提取图片信息
                let d = ctx.getImageData(0, 0, w, h);

                //进行高斯模糊
                let gd = gaussBlur(d, r, 0);

                //绘制模糊图像
                ctx.putImageData(gd, 0, 0);

                resolve(Canvas.toDataURL());
            } catch (e) {
                reject(e);
            }
        };
        IMG.src = URL;
    })
};

总结

V8对连续执行的代码有静态优化,所以文中所列时间大家不要较真,看个数量级就好 ╮(╯▽╰)╭

兼容性

  1. Uint8ClampedArray Can I use?

    • Android 4+
    • iOS safari 7.1+
  2. Cross-Origin in <cavnas> Can I use?

    • Android 4.4 +
    • iOS safari 7.1+

完整实现


/**
 * @fileOverview
 * 高斯模糊
 * @author iNahoo
 * @since 2017/5/8.
 */
"use strict";

const gaussBlur = function (imgData, radius) {

    radius *= 3;    //不知为什么,我的模糊半径是 css中 filter:bulr 值的三倍时效果才一致。

    //Copy图片内容
    let pixes = new Uint8ClampedArray(imgData.data);
    const width = imgData.width;
    const height = imgData.height;
    let gaussMatrix = [],
        gaussSum,
        x, y,
        r, g, b, a,
        i, j, k,
        w;

    radius = Math.floor(radius);
    const sigma = radius / 3;

    a = 1 / (Math.sqrt(2 * Math.PI) * sigma);
    b = -1 / (2 * sigma * sigma);

    //生成高斯矩阵
    for (i = -radius; i <= radius; i++) {
        gaussMatrix.push(a * Math.exp(b * i * i));
    }

    //x 方向一维高斯运算
    for (y = 0; y < height; y++) {
        for (x = 0; x < width; x++) {
            r = g = b = a = gaussSum = 0;
            for (j = -radius; j <= radius; j++) {
                k = x + j;
                if (k >= 0 && k < width) {
                    i = (y * width + k) * 4;
                    w = gaussMatrix[j + radius];

                    r += pixes[i] * w;
                    g += pixes[i + 1] * w;
                    b += pixes[i + 2] * w;
                    a += pixes[i + 3] * w;

                    gaussSum += w;
                }
            }

            i = (y * width + x) * 4;
            //计算加权均值
            imgData.data.set([r, g, b, a].map(v=>v / gaussSum), i);
        }
    }

    pixes.set(imgData.data);
    
    //y 方向一维高斯运算
    for (x = 0; x < width; x++) {
        for (y = 0; y < height; y++) {
            r = g = b = a = gaussSum = 0;
            for (j = -radius; j <= radius; j++) {
                k = y + j;

                if (k >= 0 && k < height) {
                    i = (k * width + x) * 4;
                    w = gaussMatrix[j + radius];

                    r += pixes[i] * w;
                    g += pixes[i + 1] * w;
                    b += pixes[i + 2] * w;
                    a += pixes[i + 3] * w;

                    gaussSum += w;
                }
            }
            i = (y * width + x) * 4;
            imgData.data.set([r, g, b, a].map(v=>v / gaussSum), i);
        }
    }

    return imgData;
};

/**
 * @public
 * 暴露的异步模糊方法
 * ---------------------
 * @param URL       图片地址,需要跨域支持
 * @param r         模糊半径 {Int}
 * @param shrink    缩小比率 {Number}
 * @return {Promise}
 */
export const blur = (URL, r, shrink = 1)=> {
    return new Promise((resolve, reject)=> {

        const IMG = new Image();
        IMG.crossOrigin = '*'; //需要图片跨域支持

        IMG.onload = function () {
            const Canvas = document.createElement('CANVAS'); //大量使用可考虑只创建一次

            let w = IMG.width, h = IMG.height;

            //缩小比例不为1时 , 重新计算宽高比
            if (shrink !== 1) {
                w = Math.ceil(w / shrink);
                h = Math.ceil(h / shrink);
                r = Math.ceil(r / shrink);
            }

            //因为懒, 就全Try了, 实际上只 Try跨域错误 即可
            try {
                //设置Canvas宽高,获取上下文
                Canvas.width = w;
                Canvas.height = h;
                let ctx = Canvas.getContext('2d');

                ctx.drawImage(IMG, 0, 0, w, h);

                //提取图片信息
                let d = ctx.getImageData(0, 0, w, h);

                //进行高斯模糊
                let gd = gaussBlur(d, r, 0);

                //绘制模糊图像
                ctx.putImageData(gd, 0, 0);

                resolve(Canvas.toDataURL());
            } catch (e) {
                reject(e);
            }
        };
        IMG.src = URL;
    })
};

/**
 * @public
 * 暴露的异步模糊方法
 * ---------------------
 * @param URL       图片地址,需要跨域支持
 * @param r         模糊半径 {Int}
 * @param w         输出宽度 {Number}
 * @param h         输出高度 {Number}
 * @return {Promise}
 */
export const blurWH = (URL, r, w, h)=> {
    return new Promise((resolve, reject)=> {

        const IMG = new Image();
        IMG.crossOrigin = '*'; //需要图片跨域支持

        IMG.onload = function () {
            const Canvas = document.createElement('CANVAS'); //大量使用可考虑只创建一次

            //锁定输出宽高之后, 就不需要Care 原图有多宽多高了
            //let w = IMG.width, h = IMG.height;

            //因为懒, 就全Try了, 实际上只 Try跨域错误 即可
            try {
                //设置Canvas宽高,获取上下文
                Canvas.width = w;
                Canvas.height = h;
                let ctx = Canvas.getContext('2d');

                ctx.drawImage(IMG, 0, 0, w, h);

                //提取图片信息
                let d = ctx.getImageData(0, 0, w, h);

                //进行高斯模糊
                let gd = gaussBlur(d, r, 0);

                //绘制模糊图像
                ctx.putImageData(gd, 0, 0);

                resolve(Canvas.toDataURL());
            } catch (e) {
                reject(e);
            }
        };
        IMG.src = URL;
    })
};

广告

知道我为什么不放Demo嘛?

我大A工作室开发的 《淘票票专业版》 已经上线啦!

想看demo的欢迎下载APP,浏览各个影片详情的时候,顺便瞅一眼头部的海报背景,那我是逝去的头f... 啊不,是我亲手模糊的图片。

╮(╯▽╰)╭

参考资料

目录
相关文章
|
5月前
|
前端开发
canvas图像阴影处理
canvas图像阴影处理
|
5月前
|
算法 计算机视觉
图像处理之快速均值模糊(Box Blur)
图像处理之快速均值模糊(Box Blur)
31 0
|
5月前
|
算法 C语言 计算机视觉
图像处理之图像快速插值放缩算法
图像处理之图像快速插值放缩算法
34 0
|
6月前
|
存储 前端开发
canvas详解05-变形
canvas详解05-变形
71 2
|
小程序
小程序实现全屏幕高斯模糊背景图
小程序实现全屏幕高斯模糊背景图
191 0
|
存储 API 图形学
OpenCV_11 轮廓检测:图像的轮廓+绘制轮廓+轮廓近似+边界矩形+椭圆拟合+直线拟合
轮廓检测指检测图像中的对象边界,更偏向于关注上层语义对象。如OpenCV中的findContours()函数, 它会得到每一个轮廓并以点向量方式存储,除此也得到一个图像的拓扑信息,即一个轮廓的后一个轮廓、前一个轮廓等的索引编号。
1599 0
|
资源调度 计算机视觉
CV10 图像模糊(均值、高斯、中值、双边滤波)
当我们只想得到感兴趣的物体时,通过图像模糊,可以将那些尺寸和亮度较小的物体过滤掉,较大的物体则易于检测。除了降低噪声,这就是图像平滑(模糊)的另一个重要应用:减少噪点,突出ROI,以便目标提取。
333 0
|
并行计算 iOS开发 MacOS
Metal每日分享,高斯双边模糊滤镜效果
Metal每日分享,高斯双边模糊滤镜效果
Metal每日分享,高斯双边模糊滤镜效果
|
并行计算 iOS开发 计算机视觉
Metal每日分享,均值模糊滤镜效果
Metal每日分享,均值模糊滤镜效果
Metal每日分享,均值模糊滤镜效果
|
前端开发 算法 图形学
webgl实现径向模糊
webgl实现径向模糊
webgl实现径向模糊