OpenCV算法学习笔记之边缘检测(一)
in OpenCV with 0 comment

OpenCV算法学习笔记之边缘检测(一)

in OpenCV with 0 comment

图像的边缘指图像灰度值发生剧烈变化的位置。通过保留图像边缘可以大大减小图像的数据量而又可以尽可能的保留图像内容。边缘检测通过检查每个像素的邻域并对其灰度变化进行量化的,这种灰度变化的量化相当于微积分里连续函数中方向导数或者离散数列的差分。边缘检测大多数是通过基于方向导数掩码(梯度方向导数)求卷积的方法,比较常见的卷积算子有Roberts算子、Prewitt算子、Sobel算子、Scharr算子等,之后我们会介绍常用的边缘检测算法Canny算法与拉普拉斯变换。

Roberts边缘检测

原理

Roberts边缘检测是图像矩阵与以下两个卷积核:

$$ Roberts_{135} = \left( \begin{matrix}1&0 \\ 0&- 1 \end{matrix} \right),Roberts_{45} = \left( \begin{matrix}0&1 \\ - 1&0 \end{matrix} \right) $$

分别做卷积,注意是和这两个卷积核逆时针旋转180°后再进行卷积运算。$Roberts_{135}$的锚点是在旋转后的第0行第0列,而$Roberts_{45}$的锚点是在旋转后的第0行第1列。

与Roberts核进行卷积,本质上是两个对角方向上的差分:与$Robert_{135}$卷积后的结果取绝对值,反映的是45°方向上的灰度变化率;与$Roberts_{45}$卷积核卷积后的结果取绝对值,反映的是135°方向上的灰度变化率。也可以对这两个算子进行改造:

$$ Roberts_x = \left( \begin{matrix}1&- 1 \end{matrix} \right),Roberts_y = \left( \begin{matrix}1 \\ - 1 \end{matrix} \right) $$

来得到垂直方向和水平方向上的边缘,锚点都在旋转后的第0行第0列的位置。

假设图像与$n$个卷积核进行卷积运算,记$cov_1,cov_2,\dots , cov_n$为图像与这些卷积核卷积后的结果,通常有以下几种方式衡量最后输出的边缘强度:

  1. 取对应位置绝对值的和:$\sum^n_{i=1}| cov_i |$;
  2. 取对应位置的平方和的开方:$\sqrt{\sum^n_{i=1}cov_i^2}$;
  3. 取对应位置绝对值的最大值:$max\{|cov_1|, |cov_2|, \dots , |cov_n|\}$;
  4. 插值法:$\sum^n_{i=1}a_i|cov_i|$,其中$a_i \geq 0$,且$\sum^n_{i=1}a_i=1$;

取绝对值最大值的方式对边缘的走向比较敏感,取平方和的方式效果一般是最好的,但同时会更耗时。

Python实现

利用函数convolve2d实现图像与两个Roberts算子的卷积,因为这两个核的高宽均为偶数,这种情况下,该函数默认的锚点位置在最右下角,而Roberts算子的锚点位置一个是在$(0, 0)$,一个是在$(0, 1)$位置,所以需要先计算full卷积,再根据锚点位置截取得到same卷积。

roberts函数返回的是图像与卷积核卷积后的结果,对其取绝对值衡量后就得到了图像的边缘强度,如果需要进行边缘强度的灰度值显示,还需要对其进行类型转换。如果输入的是8位图,因此和Roberts算子卷积后的结果不会大于255,所以直接转换为np.uint8即可而不用进行截断。

def roberts(img, _boundary='full', _fill_vaule=0):
    """
    
    :param img: 输入图像
    :param _boundary: 卷积方式
    :param _fill_value: 边界填充值
    :return: 图像分别与两个卷积核卷积后的结果
    """
    # 图像的高宽
    height, width = img.shape[0:2]
    # 卷积核的尺寸
    height2, width2 = 2, 2
    # 卷积核1以及锚点的位置
    roberts1 = np.array([[1, 0], [0, -1]], np.float32)
    kr1, kc1 = 0, 0
    # 计算full卷积
    img_cov1 = signal.convolve2d(img, roberts1, mode='full', boundary=_boundary, fillvalue=_fill_value)
    # 根据锚点位置截取full卷积得到same卷积
    img_conv1 = img_conv1[height2-kr1-1:height+height2-kr1-1, width2-kc1-1:width+width2-kc1-1]
    # 卷积核2
    roberts2 = np.array([[0, 1], [-1, 0]], np.float32)
    # 先计算full卷积
    img_conv2 = signal.convolve2d(img, roberts2, mode='full', boundary=_boundary, fillvalue=_fill_value)
    # 锚点的位置
    kr2, kc2 = 0, 1
    # 根据锚点位置截取full卷积得到same卷积
    img_conv2 = img_conv2[height2-kr2-1:height+height2-kr2-1, width2-kc2-1:width1+width2-kc2-1]
    return (img_conv1, img_conv2)


if __name__ = "__main__":
    src = cv2.imread("./test.png")
    # 显示原图
    cv2.show("src", src)
    # 卷积,注意边界扩充一般用symm
    src_conv1, src_conv2 = roberts(src, "symm")
    # 45°方向上的边缘强度的灰度级显示
    src_conv1 = np.abs(src_conv1)
    edge_45 = src_conv1.astype(np.uint8)
    cv2.imshow("edge45", edge_45)
    # 135°方向上的边缘强度的灰度级显示
    src_conv2 = np.abs(src_conv2)
    edge_135 = src_conv2.astype(np.uint8)
    cv2.imshow("edge135", edge_135)
    # 用平方和的开方来衡量最后输出的边缘
    edge = np.sqrt(np.power(src_conv1, 2.0) + np.power(src_conv2, 2.0))
    edge = np.round(edge)
    edge[edge > 255] = 255
    edge = edge.astype(np.uint8)
    
    cv2.imshow("edge", edge)
    cv2.waitkey()

Prewitt边缘检测

原理

Prewitt算子由以下两个卷积核组成:

$$ prewitt_x = \left( \begin{matrix} 1&0&-1 \\ 1&0&-1 \\ 1&0&-1 \end{matrix} \right),prewitt_y = \left( \begin{matrix} 1&1&1 \\ 0&0&0 \\ -1&-1&-1 \end{matrix} \right) $$

它们的锚点都在中心即$(1, 1)$的位置(从0开始数)。图像与$prewitt_x$算子卷积后结果可以反映垂直方向上的边缘;与$prewitt_y$算子卷积后结果可以反映水平方向上的边缘。而且这两个算子都是可分离的:

$$ prewitt_x = \left( \begin{matrix} 1 \\ 1 \\ 1 \end{matrix} \right) \bigstar \left( \begin{matrix} 1&0&-1\end{matrix} \right),prewitt_y = \left( \begin{matrix} 1&1&1 \end{matrix} \right) \bigstar\left( \begin{matrix} 1 \\ 0 \\ -1 \end{matrix} \right) $$

对于每个分离的卷积核,锚点也都是在中间的位置。可以看出$prewitt_x$算子实际上先对图像进行垂直方向上的非归一化的均值平滑,然后进行水平方向上的差分;$prewitt_y$算子是先对图像进行水平方向上的非归一化的均值平滑,然后进行垂直方向上的差分。

由于对图像进行了平滑处理,所以对噪声较多的图像的处理Prewitt算子的效果比Roberts效果要好。Prewitt算子还有两种变形:

$$ prewitt_{135} = \left( \begin{matrix} 1&1&0 \\ 1&0&-1 \\ 0&-1&-1 \end{matrix} \right),prewitt_{45} = \left( \begin{matrix} 0&1&1 \\ -1&0&1 \\ -1&-1&0 \end{matrix} \right) $$

反映的是图像45°和135°方向上的边缘,但它们并不是可分离的。

Python实现

由于Prewitt算子可分离,所以在代码实现中,利用卷积运算的结合律先进行水平方向上的平滑,在进行垂直方向上的差分(或者先进行垂直方向上的平滑,在进行水平方向上的差分)。

def prewitt(img, _boundary='symm'):
    """
    
    :param img: 输入图像
    :param _boundary: 边界扩充方式
    :return 与水平prewitt算子卷积后的结果和与垂直prewitt算子卷积后的结果组成的元组
    """
    # prewitt_x是可分离的,故分两次小卷积运算
    # 1. 垂直方向上的均值平滑
    ones_y = np.array([[1], [1], [1]], np.float32)
    img_conv_pre_x = signal.convolve2d(img, ones_y, mode='same', boundary=_boundary)
    # 2. 水平方向上的差分
    diff_x = np.arrray([[1, 0, -1]], np.float32)
    img_conv_pre_x = signal.convolve2d(img_conv_pre_x, diff_x, mode='same', boundary=_boundary)
    
    # prewitt_y是可分离的,故分两次小卷积运算
    # 1. 水平方向上的均值平滑
    ones_x = np.array([[1, 1, 1]], np.float32)
    img_conv_pre_y = signal.convolve2d(img, ones_x, mode='same', boundary=_boundary)
    # 2. 垂直方向上的差分
    diff_y = np.arrray([[1], [0], [-1]], np.float32)
    img_conv_pre_y = signal.convolve2d(img_conv_pre_y, diff_y, mode='same', boundary=_boundary)
    
    return (img_conv_pre_x, img_conv_pre_y)

与Roberts边缘检测不同,Prewitt边缘检测中图像与算子的卷积结果取绝对值是有可能大于255的,所以在进行灰度值显示时要对大于255的数据进行截断处理

差分方向(梯度方向)与得到的边缘方向是垂直的,比如水平差分方向的卷积反映的是垂直方向上的边缘。

Sobel边缘检测

原理

在图像的平滑处理中,高斯平滑的效果往往比均值平滑要好,因此把Prewitt算子的非归一化的均值卷积核替换成非归一化的高斯卷积核,就可以得到3阶的Sobel算子:

$$ sobel_x = \left( \begin{matrix} 1 \\ 2 \\ 1 \end{matrix} \right) \bigstar \left( \begin{matrix} 1&0&-1\end{matrix} \right) = \left( \begin{matrix} 1&0&-1 \\ 2&0&-2 \\ 1&0&-1 \end{matrix} \right) $$

$$ sobel_y = \left( \begin{matrix} 1&2&1 \end{matrix} \right) \bigstar\left( \begin{matrix} 1 \\ 0 \\ -1 \end{matrix} \right) = \left( \begin{matrix} 1&2&1 \\ 0&0&0 \\ -1&-2&-1 \end{matrix} \right) $$

可以利用二项式展开式的系数构建窗口更大的Sobel算子,窗口大小为奇数。

构建高阶Sobel算子

Sobel算子是在一个坐标轴方向上进行非归一化的高斯平滑,在另一个坐标轴方向上进行差分处理。$n\times n$的Sobel算子是由平滑算子和差分算子进行full卷积而得到的,其中$n$为奇数。对于窗口为$n$的非归一化的高斯算子等于$n-1$阶的二项式展开式的系数;窗口为$n$的差分算子是在$n-2$阶二项式展开式的系数两侧补零,然后向后差分得到的。如构建5阶非归一化的高斯平滑算子,取二项式的指数为4,展开式的系数为:

展开式系数

构建5阶差分算子,首先计算$n-2$阶二项式展开式系数:

三阶二项式展开系数

然后两侧补零并后向差分:

补零以及差分

得到5阶差分算子:

差分结果

Python实现

定义函数pascal_smooth返回$n$阶的非归一化高斯平滑算子,也就是$n-1$阶的二项式展开式的系数,其中对于阶乘的实现,利用Python的函数包math中的factorial,参数n为奇数。

def pascal_smooth(n):
    """ 高斯平滑算子 """
    smooth = np.zeros([1, n], np.float32)
    for i in range(n):
        smooth[0][i] = math.factorial(n-1)/(math.factorial(n-1-i))
    return smooth


def pascal_diff(n):
    """ 差分算子 """
    diff = np.zeros([1, n], np.float32)
    pascal_smooth_previous = pascal_smooth(n-1)
    for i in range(n):
        if i == 0:
            # 恒等于1
            diff[0][i] = pascal_smooth_previous[0][i]
        elif i == n-1:
            diff[0][i] = -pascal_smooth_previous[0][i-1]
        else:
            diff[0][i] = pascal_smooth_previous[0][i] - pascal_smooth_previous[0][i-1]
    return diff

直接将高斯算子与差分算子进行full卷积就可以得到Sobel算子,不过在真正计算时,并不需要这一步。利用Sobel算子的分离性可以分别与高斯算子和差分算子进行卷积即可得到Sobel结果。

def sobel(img, n):
    """
    
    :param img: 输入图像
    :param n: Sobel算子阶数
    :return 分别与水平Sobel算子卷积的结果和与垂直Sobel算子卷积的结果组成的元组
    """
    rows, cols = image.shape
    # 得到平滑算子
    smooth_kernel = pascal_smooth(n)
    # 得到差分算子
    diff_kernel = pascal_diff(n)
    # 与水平方向上的Sobel算子的卷积
    # 先进行垂直方向上的平滑
    img_sobel_x = signal.convolve2d(img, smooth_kernel.transpose(), mode='same')
    # 再进行水平方向上的差分
    img_sobel_x = signal.convolve2d(img_sobel_x, diff_kernel, mode='same')
    # 与垂直方向上的Sobel算子的卷积
    # 先进行水平方向上的平滑
    img_sobel_y = signal.convolve2d(img, smooth_kernel, mode='same')
    # 再进行垂直方向上的差分
    img_sobel_y = signal.convolve2d(img_sobel_y, diff_kernel.trangpose(), mode='same')
    
    return (img_sobel_x, img_sobel_y)

Sobel算子处理后的结果进行反色后会呈现铅笔素描的效果。

OpenCV提供函数void Sobel(InputArray src, OutputArray dst, int ddept, int dx, int dy, int ksize=3, double scale=1, double delta=0, int borderType=BORDER_DEFAULT)实现了Sobel边缘检测,参数解释如下表所示:

参数解释
src输入矩阵
dst输出矩阵
ddept输出矩阵的数据类型
dx当dx != 0时,src与差分方向为水平方向上的Sobel核卷积
dy当dx = 0,dy != 0时,src与差分方向为垂直方向上的Sobel核卷积
ksizesobel核的尺寸,值为1,3,5,7
scale比例系数
delta平移系数
borderType边界扩充类型

对于参数ddepth,它的设置与函数filter2D类似,事实上,Sobel函数的卷积步骤就是由filter2D实现的。对于参数ksize,当它等于1时,代表Sobel核没有平滑算子,只有差分算子,即如果设置参数dx=1,dy=0,那么src只与$1\times 3$的水平方向上的差分算子$\left( \begin{matrix}1&0&- 1 \end{matrix} \right)$卷积,没有平滑算子。

Scharr算子

原理

Scharr边缘检测算子与Prewitt边缘检测算子和3阶的Sobel算子类似,由以下两个卷积核组成:

$$ scharr_x = \left( \begin{matrix} 3&0&-3 \\ 10&0&-10 \\ 3&0&-3 \end{matrix} \right), \ \ \ scharr_y = \left( \begin{matrix} 3&10&3 \\ 0&0&0 \\ -3&-10&-3 \end{matrix} \right) $$

其中锚点都是在中心位置。这两个卷积核都是不可分离的,与水平方向上的$scharr_x$卷积的结果反映垂直方向上的边缘强度,与垂直方向上的$scharr_y$卷积的结果反映水平方向上的边缘强度。同样,Scharr算子也可以扩展到其他方向,如:

$$ scharr_{45} = \left( \begin{matrix} 0&3&10 \\ -3&0&3 \\ -10&-3&0 \end{matrix} \right), \ \ \ scharr_{135} = \left( \begin{matrix} 10&3&0 \\ 3&0&-3 \\ 0&-3&-10 \end{matrix} \right) $$

分别反应的是135°和45°方向上的边缘。

C++实现

void scharr(InputArray src, OutputArray dst, int ddepth, int x, int y=0, int borderType=BORDER_DEFAULT) {
    CV_Assert(!(x == 0 && y == 0));
    Mat scharr_x = (Mat_<float>(3, 3) << 3, 0, -3, 10, 0, -10, 3, 0, -3);
    Mat scharr_y = (Mat_<float>(3, 3) << 3, 10, 3, 0, 0, 0, -3, -10, -3);
    // 当x不等于0时,src和scharr_x卷积
    if (x != 0 && y == 0) {
        conv2D(src, shcarr_x, dst, ddepth, Point(-1, -1), borderType);
    }
    // 当y不等于0时,src和scharr_y卷积
    if (x == 0 && y != 0) {
        conv2D(src, shcarr_y, dst, ddepth, Point(-1, -1), borderType);
    }
}

与Prewitt边缘检测相比,因为Scharr卷积核中系数的增大,所以灰度变化较为敏感,即使灰度变化较小的区域也能得到较强的边缘强度。

OpenCV提供函数void Scharr(InputArray src, OutputArray dst, int ddepth, int dx, int dy, double scale=1, double delta=0, int borderType=BORDER_DEFAULT),参数解释和上面的一样。

Kirsch算子和Robinson算子

原理

Kirsch算子

Kirsch算子由以下8个卷积核组成:

$$ k_1 = \left( \begin{matrix} 5&5&5 \\ -3&0&-3 \\ -3&-3&-3 \end{matrix} \right), k_2 = \left( \begin{matrix} -3&-3&-3 \\ -3&0&-3 \\ 5&5&5 \end{matrix} \right),k_3=\left( \begin{matrix} -3&5&5 \\ 5&0&-3 \\ 5&5&-3 \end{matrix} \right),k_4=\left( \begin{matrix} -3&-3&-3 \\ 5&0&-3 \\ 5&5&-3 \end{matrix} \right) \\ k_5 = \left( \begin{matrix} -3&-3&5 \\ -3&0&5 \\ -3&-3&5 \end{matrix} \right),\ \ \ \ \ k_6=\left( \begin{matrix} 5&-3&-3 \\ 5&0&-3 \\ 5&-3&-3 \end{matrix} \right),\ k_7=\left( \begin{matrix} -3&-3&-3 \\ -3&0&5 \\ -3&5&5 \end{matrix} \right),\ k_8=\left( \begin{matrix} 5&5&-3 \\ 5&0&-3 \\ -3&-3&-3 \end{matrix} \right) $$

图像与每一个卷积核进行卷积,然后取绝对值作为对应方向上的边缘强度的量化。对8个卷积结果取绝对值,然后取最大值作为最后输出的边缘强度。

Robinson算子

与Kirsch算子类似,Robinson算子也是由8个卷积核组成:

$$ r_1 = \left( \begin{matrix} 1&1&1 \\ 1&-2&1 \\ -1&-1&-1 \end{matrix} \right), r_2 = \left( \begin{matrix} 1&1&1 \\ -1&-2&1 \\ -1&-1&1 \end{matrix} \right),r_3=\left( \begin{matrix} -1&1&1 \\ -1&-2&1 \\ -1&1&1 \end{matrix} \right),r_4=\left( \begin{matrix} -1&-1&1 \\ -1&-2&1 \\ 1&1&1 \end{matrix} \right) \\ r_5 = \left( \begin{matrix} -1&-1&-1 \\ 1&-2&1 \\ 1&1&1 \end{matrix} \right),\ \ \ \ \ k_6=\left( \begin{matrix} 1&-1&-1 \\ 1&-2&-1 \\ 1&1&1 \end{matrix} \right),\ r_7=\left( \begin{matrix} 1&1&-1 \\ 1&-2&-1 \\ 1&1&-1 \end{matrix} \right),\ r_8=\left( \begin{matrix} 1&1&1 \\ 1&-2&-1 \\ 1&-1&-1 \end{matrix} \right) $$

其检测过程和Kirsch是一样的。

由于卷积核的卷积运算以及绝对值的求取都比较简单,所以这里就不给出具体的代码实现了。

因此Kirsch算子使用了8个方向上的卷积核,所以其检测的边缘比标准的Prewitt算子和Sobel算子检测到的边缘会显得更加丰富。

参考

《OpenCV算法精解——基于Python和C++》(张平)第八章

Responses