Skip to content

P.250 的图 12.5 和 P.252 代码中的 Sobel 算子和 Prewitt 算子的Gx和Gy矩阵写反了. #24

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
FreeBlues opened this issue Aug 1, 2016 · 14 comments
Labels

Comments

@FreeBlues
Copy link

FreeBlues commented Aug 1, 2016

P.250 的图 12.5 和 P.252 代码中的 Sobel 算子和 Prewitt 算子的Gx和Gy矩阵写反了.

Sobel 算子的 Gx 是用来计算 x 轴向(横向)的梯度, Gy 是用来计算 y 轴向(纵向)的梯度, 因此, Gx 是用像素点右侧相邻位置(右上,右中,右下)的像素点的亮度值(或高度值/灰度值), 分别乘以权重 1,2,1 后求和, 减去像素点左侧相邻位置(左上,左中,左下)的对应数值, 所以, Gx 矩阵应该是:

-1  0  1      
-2  0  2    
-1  0  1   

Gy 类似, 应为:

-1  -2  -1    
0    0   0    
1    2   1    

因为你的示例代码最终使用的是 1 - abs(edgeX)- abs(edgeY), 所以就算把梯度矩阵写反了也不影响, 如果你单独使用它们就会发现问题了, 比如用它们来生成法线贴图.

wiki上相关的条目:
Sobel算子
Prewitt算子

@candycat1992
Copy link
Owner

你好,我明白你的意思,我写的时候也考虑了一些时间要写成什么 样。书里的确有些事情没有交代,我这里再具体说一下。

实际上卷积操作有一个翻转核的操作,我在书里写到过:

qq 20160801175709

这意味着,你看到的卷积核需要翻转一下后再和原图像像素相乘相加。但我的代码实现为了简单和明了并没有进行这一步。

而边缘检测里面的Gx和Gy从直观意义上讲其实是为了分别检测水平方向和垂直方向上的边缘线,也就是说是检测横线和纵线。你所看到的wiki上的Sobel算子之所以是和书里面反的,是因为它的卷积操作是严格按照定义来,即翻转后再权重相加,这样一来,其实和书里面的本质是一样的:书里直接把核翻转过来而在进行“卷积”操作的时候就不会再翻转核了。得到的效果就是,Gx会检测出来水平线:

qq 20160801180213

而Gy会检测出来垂直线:

qq 20160801180304

不知道你是否看明白了。总结来说就是,标准的卷积操作定义是:

  • 翻转卷积核
  • 把卷积核中心放到当前处理的像素上
  • 进行乘法和求和

书里面由于没有进行第一步操作,所以为了达到和标准卷积操作一样的效果就提前在定义卷积核操作的时候把核翻转了,造成和wiki里面的定义不同,但它们的效果其实是一样的。如果按你说的核来进行同样的计算,那么Gx将会检测的是垂直线,这是不对的。

我的确应该在书里更明确地解释一下。感谢反馈 :)

@FreeBlues
Copy link
Author

谢谢你的详细解释.

不过对于卷积核的翻转, 感觉你的理解有误, 我了解到的卷积核的翻转是沿对角线翻转, 也就是翻转 180 度,

比如我们以 wiki 中列出的 Gx 为例, 它原本是这样的形式:

-1  0  1      
-2  0  2    
-1  0  1

沿对角线翻转后会变成这样:

1  0  -1      
2  0  -2    
1  0  -1

相当于在 x 轴向上改变了一下时序.

如果按照你所描述的翻转, 那是 90 度的翻转, 会直接把 x 轴向翻转为 y 轴向.

也就是说 wiki 中提到的 GxGy 矩阵, 就算翻转也不可能翻转为P.250 图12.5 的那两个矩阵.

所以我有些奇怪为什么你上面的例子中的两张图在用了错误的矩阵后居然能输出正确的结果....

@candycat1992
Copy link
Owner

你的对角线翻转不对哦,我研一上图像处理课的时候也困惑了很久,不明白为什么要这样多此一举所以印象非常深刻。

这个翻转类似于矩阵转置,横行会变成纵列,纵列会变成横行。你那个不是沿着对角线翻转,而是沿着中心垂直对称轴翻转,正确翻转可参考矩阵转置。

-1  0  1      
-2  0  2    
-1  0  1

翻转过后是

-1  -2  -1      
0  0  0    
1  2  1

@FreeBlues
Copy link
Author

FreeBlues commented Aug 2, 2016

这里不能跟转置矩阵搞混淆, 沿对角线翻转要做两次, 第一次是沿左下角到右上角的对角线, 第二次是沿左上角到右下角的对角线.

具体来说是这样:

原矩阵

a b c
d e f
g h i

先沿左下角到右上角的对角线翻转, 结果为:

i f c
h e b
g d a

再沿左上角到右下角的对角线翻转, 最终结果为:

i h g
f e d
c b a

而且看看在 wiki 中对翻转的描述:

convolution is the process of flipping both the rows and columns of the kernel and then multiplying locationally similar entries and summing.

也是把卷积核的行列同时翻转, 我们可以先翻转行:

g h i
d e f
a b c

再翻转列:

i h g
f e d
c b a

最终结果一样.

wiki 上贴出的计算示意图也表示是这种翻转:

这里是用矩阵的对应元素相乘再相加, 要跟矩阵相乘区分开.

我写过一个用 Sobel 算子 根据纹理贴图生成法线图的着色器, 就是按照这种梯度矩阵来算的,

用代码生成的法线图:

如果按照你的矩阵来计算, 得到的法线图是这样的:

用图像处理软件生成的法线图:

着色器代码如下:

-- 用 sobel 算子生成法线图    generate normal map with sobel operator
genNormal1 = {
vs = [[
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

varying vec2 vTexCoord;
varying vec4 vColor;
varying vec4 vPosition;

uniform mat4 modelViewProjection;

void main()
{
vColor = color;
vTexCoord = texCoord;
vPosition = position;
gl_Position = modelViewProjection * position;
}
]],
fs = [[
precision highp float;

varying vec2 vTexCoord;
varying vec4 vColor;
varying vec4 vPosition;

// 纹理贴图
uniform sampler2D tex;
uniform sampler2D texture;

//图像横向长度-宽度, 图像纵向长度-高度
uniform float w;
uniform float h;

float clamp1(float, float);
float intensity(vec4);

float clamp1(float pX, float pMax) {
    if (pX > pMax) 
        return pMax;
    else if (pX < 0.0)
        return 0.0;
    else
        return pX;   
}

float intensity(vec4 col) {
    // 计算像素点的灰度值
    return 0.3*col.x + 0.59*col.y + 0.11*col.z;
}

void main() {
// 横向步长-每像素点宽度,纵向步长-每像素点高度
float ws = 1.0/w ;
float hs = 1.0/h ;
float c[10];
vec2 p = vTexCoord;
lowp vec4 col = texture2D( texture, p );

// sobel operator
// position.      Gx.            Gy
// 1 2 3     |-1. 0. 1.|   |-1. -2. -1.|
// 4 5 6     |-2. 0. 2.|   | 0.  0.  0.|
// 7 8 9     |-1. 0. 1.|   | 1.  2.  1.|
// 右上角,右,右下角
c[3] = intensity(texture2D( texture, vec2(clamp(p.x+ws,0.,w), clamp(p.y+hs,0.,h) )));
c[6] = intensity(texture2D( texture, vec2(clamp1(p.x+ws,w), clamp1(p.y,h))));
c[9] = intensity(texture2D( texture, vec2(clamp1(p.x+ws,w), clamp1(p.y-hs,h))));

// 上, 下
c[2] = intensity(texture2D( texture, vec2(clamp1(p.x,w), clamp1(p.y+hs,h))));
c[8] = intensity(texture2D( texture, vec2(clamp1(p.x,w), clamp1(p.y-hs,h))));

// 左上角, 左, 左下角
c[1] = intensity(texture2D( texture, vec2(clamp1(p.x-ws,w), clamp1(p.y+hs,h))));
c[4] = intensity(texture2D( texture, vec2(clamp1(p.x-ws,w), clamp1(p.y,h)))); 
c[7] = intensity(texture2D( texture, vec2(clamp1(p.x-ws,w), clamp1(p.y-hs,h))));

// 先进行 sobel 滤波, 再把范围从 [-1,1] 调整到 [0,1]
// 注意: 比较方向要跟坐标轴方向一致, 横向从左到右, 纵向从下到上
float dx = (c[3]+2.*c[6]+c[9]-(c[1]+2.*c[4]+c[7]) + 1.0) / 2.0;
float dy = (c[7]+2.*c[8]+c[9]-(c[1]+2.*c[2]+c[3]) + 1.0) / 2.0;
float dz = (1.0 + 1.0) / 2.0;

gl_FragColor = vec4(vec3(dx,dy,dz), col.a);

}
]]
}

@candycat1992
Copy link
Owner

我明白你的 意思了。我又查了些资料,我的理解的确是有问题的……

Gx并不是检测水平方向的边缘线,而是水平方向上的梯度变化,结果是检测出来的是垂直方向上的边缘线,所以应该是

-1  0  1      
-2  0  2    
-1  0  1

感谢指正 :)

至于你说计算法线的时候的问题,你是说按书里的做法,xy值就反了是吧。这个问题是另外一个问题了,就是说从高度图生成法线的时候,xy分量分别指的是什么。我也做了实验,靠三种方法生成法线:

  1. 在Unity里选择Create from Grayscale生成法线
  2. 在PS使用NV的工具生成法线
  3. 在shader中使用ddx和ddy来直接计算梯度值:
float height = tex2D(_MainTex, i.uv);
edgeX = ddy(height);
edgeY = ddx(height);
fixed3 norm = fixed3(edgeX, edgeY, 1.0);

qq 20160802134238
qq 20160802134250
qq 20160802134225

结果证明这三个得到的结果是类似的,也就是说法线输出的x分量对应了y方向的梯度变化,y分量对应了x方向上的梯度变化,是反的。书上的Gx和Gy的确写反了,但算法线的时候应该是刚好对才对。。。

你可以贴上你的灰度图和法线生成工具,我再测一下。

@FreeBlues
Copy link
Author

是我没说清楚, 我举那个生成法线图的例子是想说明: 如果把 GxGy 搞反的话生成的法线图是错误的--本来应该显示紫色的地方却显示成了蓝绿色, 反之亦然,

以你上面的三个图来说, 第三张图是错误的, 因为一般 (x,y,z) 对应于法线图的 (r,g,b) , 也就是说 x 对应 红色, y 对应绿色, z 对应蓝色, 所以生成的法线图底色是蓝色, 左右的凹凸是紫色, 上下的凹凸是蓝绿色. 虽然第三张图跟前两张图看起来很类似, 但是在用它做法线贴图(或者叫法线映射)时会产生错误的立体效果.

我使用的原图是这个:

然后用代码提取每个像素的灰度值, 用灰度值来模拟高度值.

图像处理软件用的是 CrazyBump.

BTW, 我是看了第四章的电子版后觉得其中对几种空间的变换解释得很清楚才买的书, 主要是想参考学习一下你的各种 shader 的实现. 发现这本书写得很不错, 很多细节都讲解得比较清楚.

@candycat1992
Copy link
Owner

明白啦!我又试了下,PS里之所以是那样是因为xy取反了,Invert回来就一样了。

嗯呢,的确是我的问题,感谢!!!

总结一下,Gx和Gy分别是检测x方向和y方向的梯度值,所以书里那样写是有错误的。我会更新到勘误列表中。

@candycat1992
Copy link
Owner

已更新,填补了知识漏洞…

感谢支持呀~

@FreeBlues
Copy link
Author

感觉经过这么一番交流后我对图像卷积的理解也加深了. :)

@CaigerDeng
Copy link

我来补个问题,为什么要用1减去水平方向和竖直方向的梯度绝对值呢?

@candycat1992
Copy link
Owner

没什么特别意义,只是因为混合因子edge在lerp里面的位置:

fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv), edge);

也就是说,如果是纯边界的地方,edge是0。而梯度可能为负也可能为正,绝对值表示了它跟邻域之间值的差距大小,所以绝对值越大表示差异越大,越是边界,那么1-abs就是我们需要的edge值。

@mingingzi
Copy link

所以书里的结果虽然对,但严格来说还要再进行一步kernel翻转?
以及我不太明白为什么kernel定义的时候要先定义这个kernel,再翻转它,而不直接把翻转后的结果定义为kernel呢?

谢谢!

@candycat1992
Copy link
Owner

@mingingzi 这个就是卷积核的定义问题了,有很多资料可以参考,比如:
https://www.zhihu.com/question/20500497

@JackKa325
Copy link

image
你好,如果按照上面的说法,卷积核的翻转的话,那这里GX和GY的数组是不是错的?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants