toxic-johann / toxic-johann.github.io

my blog
6 stars 0 forks source link

【2016-03-27】尝试一下素描化 #26

Open toxic-johann opened 7 years ago

toxic-johann commented 7 years ago

先上一发效果图

如果你只是为了看图的话,直接向下拉就好。

没错,还是继续讨论ascii化图片的问题。上周我尝试了通过调整对比度来让图片更加易看。但是我们可以看到,效果还不能达到我们的理想状态。我们尝试制造出来的ascii化代码图,应该是只关注于边框,而尽可能忽略大片颜色的细微变化。这让我想起了以前绘画中的描边技术,当然再进一步就是素描。

那么我们知道在photoshop中进行素描化是十分简单的。总结起来就是以下几步:

  1. 灰度化
  2. 反色(反相)
  3. 高斯模糊
  4. 颜色减淡

这个听起来还是比较简单的。不过为什么要这么做呢。所以我们先来分析一下这么做的理由。

灰度化还是很容理解的。图片转化为素描必然是只有黑白的。那么灰度化是很正常的。

然后反色目测是用来叠加的,因为我们会用到颜色减淡这个方法。

不过如果我们理解不了颜色减淡叠加,那么我们就不能理解为什么要用高斯模糊和进行反色了。

所以我们先看颜色减淡叠加的公式。设基色为a,叠加色为b,结果为c

c = min(a + a*b/(255-b),255)

根据公式我们可以推测到。

分析了下情况,我发现这个对于我没什么卵用。。。那么我就先去掉高斯模糊,单纯考虑反相之后进行颜色减淡处理会得到什么。

设基色为a,则叠加色为255-a。则有如下推导

c = min(a + a*b/(255-b),255)
  = min(a + a*(255-a)/(255-255+a),255)
  = min(a + 255-a,255)
  = 255

也就是说,与反相后的图片叠加,得出来只是一片白色。不过这个貌似刚好得出来255。有点玄机。所以这个高斯处理就很重要了。

这个时候,我们假设高斯处理带来的变化是d(delta)。即有如下推导

设a为基色,则叠加色为b=a+d。

c = min(a + a*b/(255-b),255)
  = min(a + a*(255-a-d)/(255-255+a+d),255)
  = min(a + a*(255-a-d)/(a+d),255)
  = min(b-d + (b-d)*(255-b)/b,255)
  = min(b-d + (255*b-255*d-b*b+b*d)/b,255)
  = min(b - d + 255 - 255*d/b - b + d,255)
  = min(255-255*(d/b),255) 

这时候我们就可以很明显看出来了。假如d存在且大于0,则得出来的颜色偏向黑色,否则为白色。

这个结果很重要,那就是以为这,我们可以通过delta来留下我们所需要的颜色。即,边框。

这个时候我们就可以来研究下高斯模糊了。

这个解释我就不详细说了,因为真的挺长。所以我就简单解释为,高斯模糊就是把周边数据与自身作了均值处理。

这个对于我们获取边框还是挺有用的。上图。我们引入一个简单的方块,他的底色是白色。

高斯模糊前样例

灰度后

模糊后

我们可以推测得出,处于大色块中间的像素点,因为他附近的色块与其相同,所以他的颜色没有变化。

而处于色块交界处的点,由于两边的像素点不一样,所以造成了差异。因此可以算出delta。

这样子在我们进行色块叠加的时候,这种颜色因此留了下来,也就是我们需要的边框了。而其他大色块,则成为了留白。

素描化的色块

所以这就是素描滤镜的原理。

灰度化上次已经说过了,这次就不说了。

反色

直接获得像素点,然后将用255减去色值即可。

颜色减淡

按照公式代入就好

return Math.min((each+(each*b[index])/(255-b[index])),255);

高斯模糊

有兴趣的去看看样例,这里我就直接说鸟。

首先,我们要决定一下究竟用两次一维高斯模糊还是一次二维高斯模糊好,三次均值的快速高斯模糊这里我暂且不讨论。有兴趣可以看看。效果也是一样的。

设循环一次图像为n(像素点个数),循环高斯模糊半径为r。则有

两次一维高斯,循环次数
2*n*r
一次二维高斯,循环次数
n*r*r

显然,我们两次一维高斯模糊更加划算。

首先计算出高斯矩阵

this._getOneGaussianMatrix = (radius,sigma)=>{
    let gaussMatrix = [];
    let gaussSum = 0;
    // 计算矩阵计算系数
    let a = 1/(Math.sqrt(2*Math.PI)*sigma);
    let b = -1/(2*sigma*sigma);

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

    // 归一化,确保高斯矩阵的最终的和值在0/1之间

    gaussMatrix = gaussMatrix.map(each=>{
        return each/gaussSum;
    });

    return {
        matrix:gaussMatrix,
        sum:gaussSum
    };
},

然后用两次一维高斯模糊处理imageData

this._oneGaussianOp = (imageData,width,height,radius,sigma,alpha)=>{
    let self = this;
    let gauss = self._getOneGaussianMatrix(radius,sigma);
    let length = imageData.length;
    imageData = this._testArrayMap(imageData);
    // x方向进行高斯运算
    let ximage= imageData.map((each,index)=>{
        // 获取各个位置
        let pos = index%4;
        let hei = ~~(index/4/width);
        let wid = ~~(index/4%width);
        // 不处理透明度
        if(!alpha && index%4 == 3){
            return each;
        }
        let sum=0;
        for(let r=-radius;r<=radius;r++){
            let data  = imageData[((width+wid+r)%width+width*(hei))*4+pos];
            let gdata = gauss.matrix[r+radius]*data; 
            sum = sum+gdata;
        }
        return sum;
    });
    // y方向进行高斯运算,在X处理后
    let yimage= ximage.map((each,index)=>{
        let pos = index%4;
        let hei = ~~(index/4/width);
        let wid = ~~(index/4%width);
        // 不处理透明度
        if(!alpha && index%4 == 3){
            return each;
        }
        let sum=0;
        for(let r=-radius;r<=radius;r++){
            let data  = ximage[((height+hei+r)%height*width+wid)*4+pos];
            let gdata = gauss.matrix[r+radius]*data; 
            sum = sum+gdata;
        }
        return sum;
    });

    return yimage;
},

这里主要是定位每个位置要注意下。

然后我们就完成了高斯模糊处理。

这个时候我们就可以完成素描滤镜了。

Uint8ClampedArray本质上是一个用object封装成的array。所以在某些浏览器上(没错说的就是iPhone上面的safari),像map、reduce、from这种方法,很可能没有。这时候我们要注意做特性检测,然后转化成Array进行处理。

this._testArrayMap = (arr)=>{
    if(!arr.map){
        return Array.prototype.slice.call(arr);
    }
    return arr;
},
this._testArrayReduce = (arr)=>{
    if(!arr.reduce){
        return Array.prototype.slice.call(arr);
    }
    return arr;
}
this._generateUint8ClampedArray = arr=>{
    if(this._isUint8ClampedArray(arr)){
        return arr;
    }
    if(!Array.isArray(arr)){
        throw new Error("could only generate Uint8ClampedArray from an array");
        return;
    }
    // 特性判断
    if(!Uint8ClampedArray.from){
        return new Uint8ClampedArray(arr);
    }
    return Uint8ClampedArray.from(arr);

}

貌似安卓的微信浏览器里用的不是Uint8ClampedArray,是CanvasPixelArray对象。然后因为我这个只是自己玩玩。。所以我就没有管。所以部分安卓可能不能用微信访问体验地址。可以用chrome或者其他浏览器试试。

canvas绘制和数据处理的时候貌似占用了整个进程,因此我的处理提示并没有打出来。这个以后我应该会尝试用Web Workers去进行解决。但是这次没有时间暂且不用。

二次一维高斯模糊做出来的所花的时间对于一个800*500的图像大概200ms+。但是我这里整个图片处理,因为要经历很多其他步骤,因此有较多循环。所以我可以看到一个像素化处理大概要用1200ms左右,在电脑上,手机上由于本身处理不大好,而且数组不断改变,所花时间就更长了。大概会有几十秒的延迟。

这个主要是第一我有多次循环,例如取得反色图像之类的这种其实我是可以合在一个循环中做完的。

不过由于只是造轮子练手,而且时间也不算充足。这次我就先不继续优化了。所以大家觉得慢不要打我。

体验网址还是老地方。我调了一下适配,手机上应该会比以前好按点。

见的多了

谈笑风生

一颗赛艇

当然模特还是用回原来的好

滋不滋磁

搞个大新闻

批判一番