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

此系列的其他文章:
OpenCV算法学习笔记之初识OpenCV
OpenCV算法学习笔记之几何变换
OpenCV算法学习笔记之对比度增强
OpenCV算法学习笔记之平滑算法
OpenCV算法学习笔记之阈值分割
OpenCV算法学习笔记之形态学处理
OpenCV算法学习笔记之边缘处理(二)
OpenCV算法学习笔记之形状检测

更多文章可以访问我的博客Aengus | Blog

图像的边缘指图像灰度值发生剧烈变化的位置。通过保留图像边缘可以大大减小图像的数据量而又可以尽可能的保留图像内容。边缘检测通过检查每个像素的邻域并对其灰度变化进行量化的,这种灰度变化的量化相当于微积分里连续函数中方向导数或者离散数列的差分。边缘检测大多数是通过基于方向导数掩码(梯度方向导数)求卷积的方法,比较常见的卷积算子有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核卷积
ksize sobel核的尺寸,值为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++》(张平)第八章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,968评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,682评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,254评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,074评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,964评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,055评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,484评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,170评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,433评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,512评论 2 308
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,296评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,184评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,545评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,150评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,437评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,630评论 2 335

推荐阅读更多精彩内容