Shader特效——“Anistropic Kuwahara” 的实现 【GLSL】

效果对比

原图
效果图
向量场可视化的效果

我这里用的是一般的 LIC(因为我觉得这个好看一点 >_-) ,作者使用的是特殊的 LIC(卷积核权重由 dw 和 w 计算得出)

dw 和曲线上采样的坐标 (p.x、p.y) 和特征向量(t.x 、t.y)的关系如下图所示

引言

上文提到过改进的广义 Kuwahara 算法,但是它也存在缺陷——它无法捕获方向特征并会产生聚集的瑕疵。进而作者提出了各向异性的 Kuwahara (Anistropic Kuwahara),该新算法可以依据输入图像的局部结构来进行自适应地滤波——在均匀区域,滤波器的形状应该是一个圆,而在各向异性区域,滤波器应该是一个长轴与图像特征主方向对齐的椭圆。

(a) 平坦区域 (b) 转角 (c) 边缘

各向异性的 Kuwahara 滤波器使用椭圆形定义的加权函数,椭圆的形状基于局部方向和各向异性来得到。滤波器响应被定义为局部平均值的加权和,其中对那些低标准差的平均值赋予了更高的权重。

各向异性的 Kuwahara 的输出图细节

 如图,作者认为该算法能够较好的解决瑕疵聚集的问题,而且还产生像绘画一样的有方向性的图像特征。

 算法流程

 算法的主要流程是:我们从计算结构张量开始,然后用高斯滤波器平滑它。然后从平滑过的结构张量的特征值和特征向量出发,推导出各向异性的测量和局部方向。最后,执行真正的滤波操作。

各向异性 Kuwahara 的算法图解

 计算局部方向并测量各项异性

局部方向和各向异性的估计是基于结构张量的特征值和特征向量。我们直接从输入的 RGB 值计算结构张量。设 f 为输入图像,设 Sobel 滤波器的横向和纵向卷积遮罩算子为

                                                       S_{x}=\frac{1}{4}\begin{pmatrix} +1 & 0 & -1 \\ +2 & 0 & -2 \\ +1 & 0 & -1 \end{pmatrix} , S_{y}=\frac{1}{4}\begin{pmatrix} +1 & +2 & +1 \\ 0 & 0 & 0 \\ -1 & -2 & -1 \end{pmatrix}

然后依据下式计算 f 的偏导 (\ast 表示卷积操作)

                                                      f_{x}=S_{x}\ast f ,f_{y}=S_{y}\ast f 

然后 f 的结构张量 (g_{i, j}) 就可以表示为 (其中 \cdot 表示点积操作)

                                                     (g_{i, j}) = \begin{pmatrix} f_{x}\cdot f_{x} & f_{x}\cdot f_{y} \\ f_{x}\cdot f_{y} & f_{y}\cdot f_{y} \end{pmatrix} =: \begin{pmatrix} E & F\\ F & G \end{pmatrix}

 结构张量的特征值对应于 f 的最小和最大变化率的平方,特征向量对应于各自的方向。我们通过选择对应于最小变化率的特征向量,来得到一个向量场。如下图 (b) 所示,该向量场是不连续的。为了使向量场平滑,我们需要对结构张量进行了平滑处理。运用了高斯平滑之后,结果如图 (c) 所示。平滑结构张量是对张量的一个线性操作,但对特征向量的影响是高度非线性的,在几何上与主成分分析(PCA)相对应。在我们的例子中,我们使用标准差σ = 2.0 的高斯滤波器。注意我们没有对张量进行标准化。因此,在平滑过程中,梯度幅值较大的边缘对应的结构张量的权值更大。因此,边缘的方向信息被分布到边缘的邻域中,如图 (d) 所示。

(a) 原始图像 (b) 结构张量的特征向量 (c) 光滑结构张量的特征向量 (d)利用线积分卷积 ( LIC) 可视化的平滑结构张量的特征向量 (e) 各向异性(蓝色=低,红色=高)

结构张量的特征值是非负实数,由式  \lambda _{1,2}=\frac{E+G\pm \sqrt{(E-G)^{2}+4F^{2})}}{2} 计算得到。

在最小变化率的方向上的特征向量则由式    t=\begin{pmatrix} \lambda _{1}-E\\ -F \end{pmatrix}  计算得到。

接着我们就可以定义局部方向 \varphi =arg(t)

为了测量各向异性的程度(度量),我们使用式 A = \frac{\lambda _{1}-\lambda _{2}}{\lambda _{1}+\lambda _{2}} ,其中各向异性 A 的取值范围是 [0, 1],0 表示各向同性, 1 表示完全各向异性的区域,如图 (e) 所示。

计算结构张量的代码:

precision mediump float;

#iChannel0 "file://./beard.jpg"

void main (void)
{
    vec2 src_size = iResolution.xy;
    vec2 uv = gl_FragCoord.xy / src_size;
    vec2 d = 1.0 / src_size;
    // Sx
    vec3 u = (
                 -1.0 * texture2D(iChannel0, uv + vec2(-d.x, -d.y)).xyz +
                 -2.0 * texture2D(iChannel0, uv + vec2(-d.x,  0.0)).xyz +
                 -1.0 * texture2D(iChannel0, uv + vec2(-d.x,  d.y)).xyz +
                 +1.0 * texture2D(iChannel0, uv + vec2( d.x, -d.y)).xyz +
                 +2.0 * texture2D(iChannel0, uv + vec2( d.x,  0.0)).xyz +
                 +1.0 * texture2D(iChannel0, uv + vec2( d.x,  d.y)).xyz
             ) / 4.0;
    // Sy
    vec3 v = (
                 -1.0 * texture2D(iChannel0, uv + vec2(-d.x, -d.y)).xyz +
                 -2.0 * texture2D(iChannel0, uv + vec2( 0.0, -d.y)).xyz +
                 -1.0 * texture2D(iChannel0, uv + vec2( d.x, -d.y)).xyz +
                 +1.0 * texture2D(iChannel0, uv + vec2(-d.x,  d.y)).xyz +
                 +2.0 * texture2D(iChannel0, uv + vec2( 0.0,  d.y)).xyz +
                 +1.0 * texture2D(iChannel0, uv + vec2( d.x,  d.y)).xyz
             ) / 4.0;

    gl_FragColor = vec4(dot(u, u), dot(v, v), dot(u, v), 1.0);
}

计算局部方向和各项异性(向量场)的代码如下: 

precision mediump float;

#iChannel0 "file://./XDOG/Gaussian_K-frag.glsl"

void main (void) {
    vec2 uv = gl_FragCoord.xy / iResolution.xy;
    // uv = vec2(uv.x, 1.-uv.y);
    vec3 g = texture2D(iChannel0, uv).xyz;
    
    float lambda1 = 0.5 * (g.y + g.x + sqrt(g.y*g.y - 2.0*g.x*g.y + g.x*g.x + 4.0*g.z*g.z));
    float lambda2 = 0.5 * (g.y + g.x - sqrt(g.y*g.y - 2.0*g.x*g.y + g.x*g.x + 4.0*g.z*g.z));
    vec2 v = vec2(lambda1 - g.x, -g.z);
    vec2 t;

    if (length(v) > 0.0) {
        t = normalize(v);    
    } else {
        t = vec2(0.0, 1.0);
    }
    float phi = atan(t.y, t.x);
    float A = (lambda1 + lambda2 > 0.0)?(lambda1 - lambda2) / (lambda1 + lambda2) : 0.0;
    gl_FragColor = vec4(t, phi, A);
}

滤波操作

其实这里的思路还是和之前的 Kuwahara 一样,只不过需要重新计算权重函数而已。

我们从计算椭圆的边界矩形开始。定义一个长轴为 a,短轴为 b 的轴向椭圆 

                                                           \frac{x^{2}}{a^{2}}+\frac{y^{2}}{b^{2}}=1

 通过将 x, y 旋转角度 \varphi ,我们可得旋转后椭圆的式子 

                                                          \frac{(xcos\varphi -ysin\varphi )^{2}}{a^{2}}+\frac{(xsin\varphi +ycos\varphi )^{2}}{b^{2}}=1

 这是一个有两个变量的二次多项式,通过重新展开和合并项,它可以用标准化的形式重写为

                                                        P(x,y)=Ax^{2}+By^{2}+Cx+Dy+Exy+F=0

 其中 

                                                          A = a^{2}sin^{2}\varphi +b^{2}cos^{2}\varphi

                                                          B = a^{2}cos^{2}\varphi +b^{2}sin^{2}\varphi

                                                          C = 0 

                                                          D = 0

                                                          E = 2(a^{2}-b^{2}) sin\varphi cos\varphi

                                                          F = -a^{2}b^{2}

水平极值位于 y 方向的偏导数消失(=0)的地方

                                                         \frac{\partial P}{\partial y} = 2By+Ex = 0 \Leftrightarrow y=\frac{-Ex}{2B}

 然后我们把这个 y 替换之前 P(x, y) 式中的 y,得到 

                                                                         (A-\frac{E^{2}}{4B})x^{2}+F=0

因此,椭圆的横向极值可以计算出来

                                                           x=\pm \sqrt{\frac{F}{\frac{E^{2}}{4B}-A}}=\pm \sqrt{a^{2}cos^{2}\varphi +b^{2}sin^{2}\varphi }

同理,我们得到纵向极值

                                                                          y=\pm \sqrt{a^{2}sin^{2}\varphi +b^{2}cos^{2}\varphi }

 假设期望的滤波半径 r > 0 。令 \varphi 表示局部方向并令 A 表示前一节中定义的各向异性。为了根据各向异性的量来调整离心率(eccentricity),我们设置了 

                                                           a=\frac{\alpha +A}{\alpha }r   和  b = \frac{\alpha }{\alpha + A} r

其中参数 \alpha>0 是一个调节参数,当 \alpha \rightarrow \infty 时,主轴 a 和副轴 b 将收敛到 1 。而我们在所有例子中都使用 \alpha =1,这样能产生最大的离心率(4)。由 a,、b 和 \varphi 定义的椭圆,其主轴将对齐到局部图像方向。它在各向异性区域中具有较高的偏心率,在各向同性区域中则呈圆形。

现在令

                                                                       S=\begin{pmatrix} \frac{1}{2a} & 0 \\ 0 & \frac{1}{2b} \end {pmatrix}

然后使用   SR_{\varphi }  将椭圆上的点映射到半径为 0.5 的圆盘上。如下所示。

映射 SR_ϕ 定义了一个线性坐标变换,对定义的椭圆(长轴 a, 短轴 b 和角 ϕ 圆盘半径为 0.5)进行映射。

 因此,椭圆上带权的函数可以表示为     w_{k}(x,y)=K_{0}\left ( \begin{pmatrix} 0.5\\ 0.5 \end{pmatrix} + R_{-2\pi k/N}SR_{\varphi}(x,y))\right )

滤波最后的输出和 广义的 Kuwahara 的输出所采用的公式是一致的:

                                                                            F(x_{0}, y_{0}) := \frac{\sum _{k}\alpha _{k}m_{k}}{\sum _{k}\alpha _{k}}

 各向异性 Kuwahara 滤波器的实现与广义 Kuwahara 滤波器的实现非常相似。因此,以下只列出各向异性的 Kuwahara 滤波器的方差计算。

实现各向异性的 Kuwahara 的方差计算部分代码如下:

vec4 t = texture2D (tfm, uv );
float a = radius * clamp (( alpha + t.w) / alpha, 0.1, 2.0);
float b = radius * clamp (alpha / (alpha + t.w), 0.1, 2.0);
float cos_phi = cos (t.z);
float sin_phi = sin (t.z);
mat2 R = mat2(cos_phi, -sin_phi, sin_phi, cos_phi ); // 旋转矩阵
mat2 S = mat2 (0.5 / a, 0.0, 0.0, 0.5 / b);          // 由各向异性计算出的变换矩阵
mat2 SR = S * R;
// 横向和纵向的极值
int max_x = int (sqrt (a *a *cos_phi * cos_phi +
                       b *b *sin_phi *sin_phi ));
int max_y = int (sqrt (a *a *sin_phi * sin_phi +
                       b *b *cos_phi *cos_phi ));
for (int j = -max_y ; j <= max_y ; ++ j)
{
    for (int i = -max_x ; i <= max_x ; ++ i)
    {
        vec2 v = SR * vec2(i, j);
        if (dot (v, v) <= 0.25)
        {
            vec3 c = texture2D (src, uv + vec2(i, j) / src_size ). rgb ;
            // 一次处理一个扇区
            for (int k = 0; k < N; ++ k)
            {
                // 注意这里纹理坐标是 v,采样扇区权重函数
                float w = texture2D (K0, vec2 (0.5, 0.5) + v).x;
                m[k] += vec4 (c * w, w);
                s[k] += c * c * w;
                v *= X;
            }
        }
    }
}

以及给出了 N = 8 时的优化方差计算代码:

{
    vec3 c = texture2D (src, uv). rgb ;
    float w = texture2D (K0123, vec2 (0.5, 0.5)). x;
    for (int k = 0; k < N; ++ k)
    {
        m[k] += vec4 (c * w, w);
        s[k] += c * c * w;
    }
}
for (int j = 0; j <= max_y ; ++ j)
{
    for (int i = -max_x ; i <= max_x ; ++ i)
    {
        if (( j != 0) || (i > 0))
        {
            vec2 v = SR * vec2(i, j);
            if (dot (v, v) <= 0.25)
            {
                vec3 c0 = texture2D (src, uv + vec2 (i, j) / src_size ). rgb ;
                vec3 c1 = texture2D (src, uv - vec2 (i, j) / src_size ). rgb ;
                vec3 cc0 = c0 * c0 ;
                vec3 cc1 = c1 * c1 ;
                // 一次取出四个扇区
                vec4 w0123 = texture2D (K0123, vec2 (0.5, 0.5) + v);
                for (int k = 0; k < 4; ++ k)
                {
                    m[k] += vec4(c0 * w0123 [k], w0123 [k]);
                    s[k] += cc0 * w0123 [k];
                    m[k + 4] += vec4(c1 * w0123 [k], w0123 [k]);
                    s[k + 4] += cc1 * w0123 [k];
                }
                // 对称的另外四个扇区
                vec4 w4567 = texture2D (K0123, vec2 (0.5, 0.5) - v);
                for (int k = 0; k < 4; ++ k)
                {
                    m[k + 4] += vec4(c0 * w4567 [k], w4567 [k]);
                    s[k + 4] += cc0 * w4567 [k];
                    m[k] += vec4(c1 * w4567 [k], w4567 [k]);
                    s[k] += cc1 * w4567 [k];
                }
            }
        }
    }
}

为了能够减少查找纹理的个数,我们把四张纹理合成到一张纹理的 RGBA 四个通道内。然后像 广义 Kuwahara 那样通过采样权重函数 w_{0}, w_{1}, w_{2}, w_{3} 来构造纹理贴图。由于椭圆是关于原点对称的,所以当 N=8 时,k= 0, 1, 2, 3 (即  w_{k+4}(x,y)=w_k(-x, -y) )

分离出来的四个扇区

 (完)

发布了233 篇原创文章 · 获赞 221 · 访问量 106万+

猜你喜欢

转载自blog.csdn.net/panda1234lee/article/details/103663204
今日推荐