Canny算法中sobel算子padding的方式对极大值抑制的影响
在使用sobel算子计算梯度时,需要注意padding的方式,一般采用复制像素的padding方式,不能使用补零与反射的padding方式。 应为会影响后续极大值抑制时,边缘像素的去留。
完整程序:https://github.com/qyzhizi/canny-algorithm.git
假设原图是:
这是一张非常小的图片,像素是大小(12,13),方便查看中间的计算过程。
原图放大后:
处理程序:python conv2d-canny.py -v -i data/test-small.png -gk 0 -L 10 -H 50
参数解释:
-v
: 打开verbose开关,显示中间过程的图片
-i data/test-small.png
: 待处理的图片
-gk 0
: 图片预处理的高斯核大小为0,意味着不使用高斯模糊
-L 10 -H 50
: 极大值抑制后,阈值处理,低阈值为10,高阈值为50,低于10的像素置为0,中间像素[10, 50]
置为50,高于50的像素置为255。
结果:
梯度幅值图:
极大值抑制后的结果,开起来还不错:
查看sobel 算子计算梯度幅值的程序,可以看到这么一行im = F.pad(im, (1, 1, 1, 1), mode='replicate')
, 这就是复制模式(replicate)的padding方式,在这个基础上修改 sobel 算子 padding 的方式,如果不进行复制模式(replicate)的padding会怎么样?
修改前的程序:
def functional_conv2d_horizontal(im, verbose=False):
"""使用F.Conv2d进行边缘检测, 检测竖直方向的轮廓, 水平梯度,右方向为正方向
Args:
im (tensor): 输入的tensor图像
Returns:
tensor: 输出的tensor图像
"""
sobel_kernel = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype='float32')
sobel_kernel = sobel_kernel.reshape((1, 1, 3, 3))
weight = torch.from_numpy(sobel_kernel)
im = F.pad(im, (1, 1, 1, 1), mode='replicate')
edge_detect = F.conv2d(im, weight, stride=1, padding=0)
if verbose:
plt.imshow(edge_detect.squeeze().detach().numpy(), cmap='gray')
plt.title("horizontal")
plt.show()
return edge_detect
def functional_conv2d_vertical(im, verbose=False):
"""使用F.Conv2d进行边缘检测, 检测水平方向的轮廓, 垂直梯度,向上为正方向
Args:
im (tensor): 输入的tensor图像
Returns:
tensor: 输出的tensor图像
"""
sobel_kernel = np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]], dtype='float32')
sobel_kernel = sobel_kernel.reshape((1, 1, 3, 3))
weight = torch.from_numpy(sobel_kernel)
im = F.pad(im, (1, 1, 1, 1), mode='replicate')
edge_detect = F.conv2d(im, weight, stride=1, padding=0)
if verbose:
plt.imshow(edge_detect.squeeze().detach().numpy() , cmap='gray')
plt.title("vertical")
plt.show()
return edge_detect
修改后的程序, 将这一行im = F.pad(im, (1, 1, 1, 1), mode='replicate')
注释掉:
def functional_conv2d_horizontal(im, verbose=False):
"""使用F.Conv2d进行边缘检测, 检测竖直方向的轮廓, 水平梯度,右方向为正方向
Args:
im (tensor): 输入的tensor图像
Returns:
tensor: 输出的tensor图像
"""
sobel_kernel = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype='float32')
sobel_kernel = sobel_kernel.reshape((1, 1, 3, 3))
weight = torch.from_numpy(sobel_kernel)
# im = F.pad(im, (1, 1, 1, 1), mode='replicate')
edge_detect = F.conv2d(im, weight, stride=1, padding=0)
if verbose:
plt.imshow(edge_detect.squeeze().detach().numpy(), cmap='gray')
plt.title("horizontal")
plt.show()
return edge_detect
def functional_conv2d_vertical(im, verbose=False):
"""使用F.Conv2d进行边缘检测, 检测水平方向的轮廓, 垂直梯度,向上为正方向
Args:
im (tensor): 输入的tensor图像
Returns:
tensor: 输出的tensor图像
"""
sobel_kernel = np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]], dtype='float32')
sobel_kernel = sobel_kernel.reshape((1, 1, 3, 3))
weight = torch.from_numpy(sobel_kernel)
# im = F.pad(im, (1, 1, 1, 1), mode='replicate')
edge_detect = F.conv2d(im, weight, stride=1, padding=0)
if verbose:
plt.imshow(edge_detect.squeeze().detach().numpy() , cmap='gray')
plt.title("vertical")
plt.show()
return edge_detect
执行程序:python conv2d-canny.py -v -i data/test-small.png -gk 0 -L 10 -H 50
梯度幅值图,可看到,长宽比之前少了2个像素,这是因为没有padding的缘故。
极大值抑制后的结果,可以看到靠近边缘的梯度最亮的像素被保留下来了,但是如何你看之前的梯度幅值图,它的边缘一定程度上维持了梯度的变化趋势,这是由于复制模式padding的结果。如果没有复制模式的padding,边缘像素在极大值抑制时可能会有问题,就像下面这样 :
极大值抑制算法:
进行极大值抑制时会对周边先 padding 一圈0,然后对某个像素进行极大值抑制时,然后判断当前像素的方向,方向分别用0, 1, 2, 3
表示,代表着0度、45度、90度,135度
。这里的梯度幅值图的大部分像素都是135度
的方向,所以会比较该方向上的3个像素,如果当前像素的坐标是(0,0)
,它的梯度方向假设是135度,那么另两个像素的坐标就是(-1,1)(1,-1)
,注意即使另两个像素不是135度,依然选这两个坐标,另两个像素是不是135度不重要,至于为什么,我也不知道如何解释。这种情况下,遍历每个像素就得到了极大值抑制的结果。
下面是一个例子: 白色框中的是没有sobel算子没有padding的计算结果,那么在计算极大值抑制时,白色框外会先padding一圈0,这里没有画出来。 蓝色框中的是sobel算子正常padding的计算结果,那么在计算极大值抑制时,蓝色框外会先padding一圈0,如果黄色框所示。
白色框中每个梯度幅值的方向:
tensor([[[[2, 2, 2, 2, 2, 2, 2, 1, 0, 3, 3],
[0, 2, 1, 3, 1, 0, 0, 3, 3, 3, 3],
[2, 0, 0, 0, 0, 2, 3, 3, 3, 3, 3],
[1, 3, 2, 1, 1, 3, 3, 3, 3, 3, 3],
[0, 0, 3, 2, 3, 3, 3, 3, 3, 3, 3],
[2, 1, 0, 3, 3, 3, 3, 3, 3, 3, 1],
[1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 0],
[1, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2],
[3, 3, 3, 3, 3, 3, 3, 0, 3, 3, 2],
[3, 3, 3, 3, 3, 3, 0, 0, 0, 1, 2]]]])
蓝色框中每个梯度幅值的方向:
tensor([[[[2, 2, 2, 2, 2, 0, 0, 1, 1, 2, 3, 3, 2],
[2, 2, 2, 2, 2, 2, 2, 2, 1, 0, 3, 3, 2],
[2, 0, 2, 1, 3, 1, 0, 0, 3, 3, 3, 3, 2],
[2, 2, 0, 0, 0, 0, 2, 3, 3, 3, 3, 3, 2],
[2, 1, 3, 2, 1, 1, 3, 3, 3, 3, 3, 3, 3],
[3, 0, 0, 3, 2, 3, 3, 3, 3, 3, 3, 3, 2],
[2, 2, 1, 0, 3, 3, 3, 3, 3, 3, 3, 1, 0],
[2, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 0, 1],
[1, 1, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2],
[2, 3, 3, 3, 3, 3, 3, 3, 0, 3, 3, 2, 2],
[0, 3, 3, 3, 3, 3, 3, 0, 0, 0, 1, 2, 3],
[0, 0, 0, 3, 3, 3, 1, 0, 0, 0, 2, 2, 2]]]])
根据极大值算法可以获得对应的极大值抑制后的图像。通过梯度幅值方向
与梯度幅值图
可以验证之前的极大值抑制的结果。
sobel 算了除了复制(replicate)模式的padding,还有反射,补零的方式,但是这两种方式在sobel算子中都不能使用。否者破坏梯度图边缘的一致性,这样 后续的极大值抑制会得到错误的边缘。