边缘检测的理解

边缘是什么

边缘可以定义为颜色剧变的区域。想象下面这样一幅图。

在中间的黑白交界处就是颜色剧变的地方,假如取其中一个像素观察,会发现它左边的像素值(255)比右边的像素值(0)大,左边差值的绝对值是255,可以理解为变化的大小或强度,变化的方向是沿着 x 方向的。这样有大小有方向的东西跟大学时学的梯度很像,因此最基本的边缘检测可以理解为求图像上每一点的梯度。

梯度的计算

我们把梯度分解为 x 方向的梯度 G_{x} 和 y 方向的梯度 G_{y} 。合并的梯度大小和方向就可以分别用以下的公式表示:
梯度大小:
|G| = \sqrt{ G_{x}^{2} + G_{y}^{2} }
梯度方向:
\theta = arctan( G_{y} / G_{x} )

某个像素点的 x 方向的梯度的计算可以通过这个像素点左右两边的像素值的差值的绝对值计算出来,而 y 方向的梯度可以通过该像素点上下两边的像素值的差值的绝对值计算。

根据计算方法的不同衍生了不同的边缘检测算法,其中一种就是用索贝尔(Sobel)算子与图像进行卷积。Sobel 算子是两个 3x3 的矩阵,分别负责计算 x 方向和 y 方向的梯度。

其中计算 x 方向梯度的算子是:
\begin{matrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{matrix}

计算 y 方向梯度的算子是:
\begin{matrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \end{matrix}

用这两个算子分别和图像做卷积就可以得到 x 方向和 y 方向的梯度。

图像卷积

假设我们用下面这样的卷积核:

\begin{matrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{matrix}
对下面这样的图像进行卷积:

在结果矩阵中,卷积核中间的位置对应到结果矩阵上就等于 100x(-1) + 50x0 + 255x1 + 10x(-2) + 20x0 + 120x2 + 25x(-1) + 30x0 + 150x1 = 700 。

然后我们把卷积核往右移一格,再重复这个过程,直到遍历完整幅图。

这时候算出来的值是:285
得到的结果矩阵:

但是最左边一列和最上边一行的值我们算不出来,因为卷积核是 3x3 的,而图像外面没有值,我通常的处理方式是:如果是在算一幅比较大的图,就直接把边界值填充为 0 ,而如果是在做数值计算或者图像很小,去掉会很影响精度的情况下,就把图像往外扩充一个像素,并且把值填充为 0 ,这在卷积神经网络中也叫 padding 。

代码实现

def filter2D(img, kernel):
    img_h, img_w = img.shape[:2]
    k_size = kernel.shape[0]
    pad = (k_size - 1) // 2

    result = np.zeros((img_h, img_w), dtype=np.float32)

    for i in range(pad, img_h-pad):
        for j in range(pad, img_w-pad):
            result[i,j] = np.sum(img[i-pad:i+pad+1, j-pad:j+pad+1] * kernel)

![lena.jpg](https://upload-images.jianshu.io/upload_images/9826651-692cb361f3aefdad.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    return result

这里对边界的处理是直接填充为 0,注意 img[i-pad:i+pad+1, j-pad:j+pad+1] * kernel 这里,乘号两边是相同大小的矩阵,运算的结果是两个等大的矩阵对应位置的元素相乘。

在卷积完以后,要表示梯度大小,还要对卷积结果取绝对值,如果要用于显示,还要把所有值限制在 0 - 255 之间。

# x方向梯度
    dx = np.abs(filter2D(img, dx_kernel))
    dx[dx > 255] = 255
    dx[dx < 0] = 0
    # y方向梯度
    dy = np.abs(filter2D(img, dy_kernel))
    dy[dy > 255] = 255
    dy[dy < 0] = 0

我们要处理的图片是这一张,原图有露点所以手动打了码。

lena.jpg
lena.jpg

显示的效果如下:

x方向梯度
x方向梯度
y方向梯度
y方向梯度

 

合并的梯度
合并的梯度

合并的梯度按公式做是这样的:

# 合并梯度
grad = np.sqrt(dx**2 + dy**2)
grad[grad > 255] = 255

但在OpenCV实现的 Sobel 中其实是用两个方向梯度的平均,这样可以节省耗时的操作,而且不用再对超出范围的值做处理。

# 合并梯度
grad = dx / 2 + dy / 2

显示效果如下:

opencv中的合并梯度
opencv中的合并梯度

上面的代码展示的是我们怎么手工实现 Sobel,但 OpenCV 有自带的实现方式。

def opencv_sobel(img):
    x = cv2.Sobel(img, cv2.CV_16S, 1, 0)
    y = cv2.Sobel(img, cv2.CV_16S, 0, 1)
     
    absX = cv2.convertScaleAbs(x)   # 转回uint8
    absY = cv2.convertScaleAbs(y)
     
    dst = cv2.addWeighted(absX,0.5,absY,0.5,0)

    return dst

为什么边缘重要

边缘其实是很多其他更高层的特征如 HOG、SIFT 等图像特征的 building block 。我们要表达一个物体的轮廓时也是首先描绘边缘,就像画画时要先画线稿。一个更有力的科学证据就是 Hubel & Wiesel 在 1958 年对猫的视觉皮层做的实验(为什么科学家都喜欢虐猫?),他们把两个电极插进猫脑子里,探测初级视觉皮层的信号,并且发现猫只有在看到移动的边缘的时候初级视觉皮层才有信号,并由此逐步推导出视觉皮层对图像的理解是逐级往后传递的,越深的层级表达的信息越复杂,如初级只能理解边缘、到了后面就能理解基本形状、直到我们的人脸、汽车等。


本文参考链接:https://www.jianshu.com/p/7206c91a99a0

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值