效果对比
我这里用的是一般的 LIC(因为我觉得这个好看一点 >_-) ,作者使用的是特殊的 LIC(卷积核权重由 dw 和 w 计算得出)
dw 和曲线上采样的坐标 (p.x、p.y) 和特征向量(t.x 、t.y)的关系如下图所示
引言
上文提到过改进的广义 Kuwahara 算法,但是它也存在缺陷——它无法捕获方向特征并会产生聚集的瑕疵。进而作者提出了各向异性的 Kuwahara (Anistropic Kuwahara),该新算法可以依据输入图像的局部结构来进行自适应地滤波——在均匀区域,滤波器的形状应该是一个圆,而在各向异性区域,滤波器应该是一个长轴与图像特征主方向对齐的椭圆。
各向异性的 Kuwahara 滤波器使用椭圆形定义的加权函数,椭圆的形状基于局部方向和各向异性来得到。滤波器响应被定义为局部平均值的加权和,其中对那些低标准差的平均值赋予了更高的权重。
如图,作者认为该算法能够较好的解决瑕疵聚集的问题,而且还产生像绘画一样的有方向性的图像特征。
算法流程
算法的主要流程是:我们从计算结构张量开始,然后用高斯滤波器平滑它。然后从平滑过的结构张量的特征值和特征向量出发,推导出各向异性的测量和局部方向。最后,执行真正的滤波操作。
计算局部方向并测量各项异性
局部方向和各向异性的估计是基于结构张量的特征值和特征向量。我们直接从输入的 RGB 值计算结构张量。设 f 为输入图像,设 Sobel 滤波器的横向和纵向卷积遮罩算子为
,
然后依据下式计算 f 的偏导 ( 表示卷积操作)
,
然后 f 的结构张量 就可以表示为 (其中 表示点积操作)
结构张量的特征值对应于 f 的最小和最大变化率的平方,特征向量对应于各自的方向。我们通过选择对应于最小变化率的特征向量,来得到一个向量场。如下图 (b) 所示,该向量场是不连续的。为了使向量场平滑,我们需要对结构张量进行了平滑处理。运用了高斯平滑之后,结果如图 (c) 所示。平滑结构张量是对张量的一个线性操作,但对特征向量的影响是高度非线性的,在几何上与主成分分析(PCA)相对应。在我们的例子中,我们使用标准差σ = 2.0 的高斯滤波器。注意我们没有对张量进行标准化。因此,在平滑过程中,梯度幅值较大的边缘对应的结构张量的权值更大。因此,边缘的方向信息被分布到边缘的邻域中,如图 (d) 所示。
结构张量的特征值是非负实数,由式 计算得到。
在最小变化率的方向上的特征向量则由式 计算得到。
接着我们就可以定义局部方向 。
为了测量各向异性的程度(度量),我们使用式 ,其中各向异性 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 的轴向椭圆
通过将 x, y 旋转角度 ,我们可得旋转后椭圆的式子
这是一个有两个变量的二次多项式,通过重新展开和合并项,它可以用标准化的形式重写为
其中
水平极值位于 y 方向的偏导数消失(=0)的地方
然后我们把这个 y 替换之前 P(x, y) 式中的 y,得到
因此,椭圆的横向极值可以计算出来
同理,我们得到纵向极值
假设期望的滤波半径 r > 0 。令 表示局部方向并令 A 表示前一节中定义的各向异性。为了根据各向异性的量来调整离心率(eccentricity),我们设置了
和
其中参数 是一个调节参数,当 时,主轴 a 和副轴 b 将收敛到 1 。而我们在所有例子中都使用 ,这样能产生最大的离心率(4)。由 a,、b 和 定义的椭圆,其主轴将对齐到局部图像方向。它在各向异性区域中具有较高的偏心率,在各向同性区域中则呈圆形。
现在令
然后使用 将椭圆上的点映射到半径为 0.5 的圆盘上。如下所示。
因此,椭圆上带权的函数可以表示为
滤波最后的输出和 广义的 Kuwahara 的输出所采用的公式是一致的:
各向异性 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 那样通过采样权重函数 来构造纹理贴图。由于椭圆是关于原点对称的,所以当 N=8 时,k= 0, 1, 2, 3 (即 )
(完)