本文共 11280 字,大约阅读时间需要 37 分钟。
自从扁平化流行起来之后,高斯模糊效果渐渐变成了视觉很喜欢用的一种表现形式,我们的视觉小姐姐也特别喜欢。为了满足她,踩了无穷无尽的坑之后,最后只能掏出Canvas来了。
没有什么视觉需求是Canvas解决不了的,如果有,再盖一层Canvas —— 奈帆斯基
对算法部分无爱的弟兄们直接跳过本节也没关系的。
模糊的效果相信大家都不陌生,实际上就是一种加权平均算法。
而 高斯模糊( Gaussian Blur ) 就是以高斯分布作为权重的平均算法。高斯分布长下面这个样子。[ 一维高斯分布 ]
图片有x,y两个维度,所以在平均的时候应该使用二维高斯分布
[ 二维高斯分布 ]
图片Img
, 模糊半径radius
radius
计算出 高斯矩阵 gaussMatrix
避免重复计算遍历每一个像素
gaussMatrix
内的加权均值观察系统的高斯模糊效果,边界总是半透明的。推测是在边界处增加 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。
这样就能对我们的几个常见使用场景进行优化例如一张900x600的图片,需要输出一张300x200@2x
可以将图片先缩放到300x200再计算模糊
例如一张900x600的图片,需要模糊半径150,需要输出一张300x200@2x的图
这样的图可以说是细节全失,通常视觉只Care成图的大概色彩范围,我们可以用一些粗暴的方法。
说白了优化手段就是一招缩小射线,我们抽象一个参数: 缩小倍率 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 }为例:
对比首先要明确的是,在缩小情况下两种算法并不等价。小图放大的模糊效果取决于浏览器本身的算法实现。最终视觉上效果差别不显著,完全可以使用。
考虑到来自服务端的图片可能有各种神奇的尺寸,而通常输出是一个确定的尺寸。
在这样的情况下,缩小比例会产生一些冗余,所以更适合另一个【锁定输出宽高的实现】。
/** * @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对连续执行的代码有静态优化,所以文中所列时间大家不要较真,看个数量级就好 ╮(╯▽╰)╭
Uint8ClampedArray
Cross-Origin in <cavnas>
/** * @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... 啊不,是我亲手模糊的图片。
╮(╯▽╰)╭
转载地址:http://pplvo.baihongyu.com/