1. ROI切割
ROI:Region of Interest,翻译过来就是感兴趣的区域。什么意思呢?比如对于一个人的照片,假如我们要检测眼睛,因为眼睛肯定在脸上,所以我们感兴趣的只有脸这部分,其他都不care,所以可以单独把脸截取出来,这样就可以大大节省计算量,提高运行速度。
还记得Numpy这个库吗?我们在使用OpenCV进行读取图像时,图像数据会被存储为Numpy数组,这也意味着我们可以使用Numpy数组的一些操作来对图像数据进行处理,比如切片。而本实验的原理也是基于Numpy数组的切片操作来完成的,因此在对应的组件中就需要填我们要切割的ROI区域的坐标来完成ROI切割操作。
注意:在OpenCV中,坐标的x轴的正方向是水平向右,y轴的正方向是垂直向下,与数学上的二维坐标并不相同。
在计算机视觉中,当我们使用OpenCV读取RGB三通道图像时,它会被转换成一个三维的Numpy数组。这个数组里的每个元素值都表示图像的一个像素值。这个三维数组的第一个维度(即轴0)通常代表图像的高度,第二个维度(即轴1)代表图像的宽度,而第三个维度(即轴2)代表图像的三个颜色通道(B、G、R,OpenCV读取到的图像以BGR的方式存储)所对应的像素值。
因此,我们可以通过指定切片的范围来选择特定的高度和宽度区域。这样,我们就能够获取这个区域内的所有像素值,即得到了这个区域的图像块,通过Numpy的切片操作,我们就完成了ROI切割的操作。这种提取ROI的方法允许我们仅获取感兴趣区域内的像素,而忽略其他不相关的部分,从而大大减少数据处理和存储的负担。
示例:
import cv2 as cv
img = cv.imread('./images/cat1.png')
print(img.shape)
x_min, x_max = 150, 270
y_min, y_max = 150, 290
img_r = cv.rectangle(img, (x_min, y_min), (x_max, y_max), (0, 0, 255), 2)
cv.imshow('img', img)
cv.imshow('img_r', img_r)
# 切片,进行roi切割
roi_img = img[y_min:y_max, x_min:x_max]
cv.imshow('roi_img', roi_img)
cv.waitKey(0)
cv.destroyAllWindows()
2. 图像添加水印
本实验中添加水印的概念其实可以理解为将一张图片中的某个物体或者图案提取出来,然后叠加到另一张图片上。具体的操作思想是通过将原始图片转换成灰度图,并进行二值化处理,去除背景部分,得到一个类似掩膜的图像。然后将这个二值化图像与另一张图片中要添加水印的区域进行“与”运算,使得目标物体的形状出现在要添加水印的区域。最后,将得到的目标物体图像与要添加水印的区域进行相加,就完成了添加水印的操作。这样可以实现将一个图像中的某个物体或图案叠加到另一个图像上,从而实现添加水印的效果。就本实验而言,会用到两个新的组件,一个是模板输入,一个是图像融合。
2.1 模板输入
其实,该组件起到的就是图片输入的功能,只不过使用模板输入所输入的图片其实是作为要添加的水印,有了水印的彩色图之后,我们需要用它来制作一个掩模,这就用到了灰度化和二值化,即先灰度化后二值化,这就得到了带有水印图案的掩模。
2.2 与运算
有了模板的掩膜之后(也就是二值化图),我们就可以在要添加水印的图像中,根据掩膜的大小切割出一个ROI区域,也就是我们要添加水印的区域,之后让其与模板的掩膜进行与运算,我们知道,与运算的过程中,只要有黑色像素,那么得到的结果图中的对应位置也会是黑色像素。由于模板的掩膜中目标物体的像素值为黑色,所以经过与运算后,就会在ROI区域中得到模板图的形状。
2.3 图像融合(图像位与操作)
将模板的形状添加到水印区域之后,就可以将该图像与原始的模板图进行图像融合。该组件的目的就是将图像对应的数组中的对应元素进行相加(一定要注意这里的两个数组是规格相同的,也就是说要么都是灰度图,要么都是彩图),其过程如下图所示。
因此就可以让原始的模板图与添加模板图形状的ROI区域进行图像融合,得到添加水印的ROI区域。
示例:
import cv2 as cv
# 读取图像
img_1 = cv.imread('./images/bg.png')
img_2 = cv.imread('./images/logo.png')
# 获取图像大小
r1, c1, ch1 = img_1.shape
r2, c2, ch2 = img_2.shape
# 在背景中切割出和img_2相同大小的区域,也就是我们感兴趣的区域
roi = img_1[:r2, :c2, :ch2]
cv.imshow('roi', roi)
# 转logo的灰度图
gary = cv.cvtColor(img_2, cv.COLOR_BGR2GRAY)
# 得到logo的掩膜,得到的是黑化的logo(白底黑字)
_, ma1 = cv.threshold(gary, 170, 255, cv.THRESH_BINARY)
cv.imshow('ma1', ma1)
# roi和ma1与运算
fg1 = cv.bitwise_and(roi, roi, mask=ma1)
cv.imshow('fg1', fg1)
# 得到白化的logo(黑底白字)
_, ma2 = cv.threshold(gary, 170, 255, cv.THRESH_BINARY_INV)
cv.imshow('ma2', ma2)
# 原logo和白化的logo进行与运算:得到红色的logo
fg2 = cv.bitwise_and(img_2, img_2, mask=ma2)
cv.imshow('fg2', fg2)
# 饱和运算
img_roi = cv.add(fg1, fg2)
cv.imshow('img_roi', img_roi)
# 贴水印
roi[:] = cv.add(fg1, fg2)
cv.imshow('img_1', img_1)
cv.imshow('gary', gary)
cv.waitKey(30000)
cv.destroyAllWindows()
3. 图像噪点消除
噪声:指图像中的一些干扰因素,通常是由图像采集设备、传输信道等因素造成的,表现为图像中随机的亮度,也可以理解为有那么一些点的像素值与周围的像素值格格不入。常见的噪声类型包括高斯噪声和椒盐噪声。高斯噪声是一种分布符合正态分布的噪声,会使图像变得模糊或有噪点。椒盐噪声则是一些黑白色的像素值分布在原图像中。
滤波器:也可以叫做卷积核,与自适应二值化中的核一样,本身是一个小的区域,有着特定的核值,并且工作原理也是在原图上进行滑动并计算中心像素点的像素值。滤波器可分为线性滤波和非线性滤波,线性滤波对邻域中的像素进行线性运算,如在核的范围内进行加权求和,常见的线性滤波器有均值滤波、高斯滤波等。非线性滤波则是利用原始图像与模板之间的一种逻辑关系得到结果,常见的非线性滤波器中有中值滤波器、双边滤波器等。
滤波与模糊联系与区别:
- 它们都属于卷积,不同滤波方法之间只是卷积核不同(对线性滤波而言)
- 低通滤波器是模糊,高通滤波器是锐化
- 低通滤波器就是允许低频信号通过,在图像中边缘和噪点都相当于高频部分,所以低通滤波器用于去除噪点、平滑和模糊图像。高通滤波器则反之,用来增强图像边缘,进行锐化处理。
注意:椒盐噪声可以理解为斑点,随机出现在图像中的黑点或白点;高斯噪声可以理解为拍摄图片时由于光照等原因造成的噪声。
本实验中共提供了五种滤波的方式,下面进行一一介绍。
3.1 均值滤波
均值滤波是一种最简单的滤波处理,它取的是卷积核区域内元素的均值,如3×3的卷积核:
在滤波算法组件中,当参数filtering_method选为均值滤波,参数component_param为ksize,代表卷积核的大小,eg:ksize=3,则代表使用3×3的卷积核。
比如有一张4*4的图片,现在使用一个3\*3的卷积核进行均值滤波时,其过程如下所示:
对于边界的像素点,则会进行边界填充,以确保卷积核的中心能够对准边界的像素点进行滤波操作。在OpenCV中,默认的是使用BORDER_REFLECT_101的方式进行填充,下面的滤波方法中除了中值滤波使用的是BORDER_REPLICATE进行填充之外,其他默认也是使用这个方式进行填充,因此下面就不再赘述。通过卷积核在原图上从左上角滑动计算到右下角,从而得到新的4*4的图像的像素值。
示例:
import cv2 as cv
img = cv.imread('./images/lvbo2.png')
img_2 = cv.imread('./images/lvbo3.png')
cv.imshow('img', img)
# 均值滤波
img_blur = cv.blur(img, (3, 3))
cv.imshow('img_blur', img_blur)
cv.waitKey(10 * 1000)
cv.destroyAllWindows()
3.2 方框滤波
方框滤波跟均值滤波很像,如3×3的滤波核如下:
在滤波算法组件中,当参数filtering_method选为方框滤波时,参数component_param为ksize,ddepth,normalize。下面讲解这3个参数的含义:
ksize:代表卷积核的大小,eg:ksize=3,则代表使用3×3的卷积核。
ddepth:输出图像的深度,-1代表使用原图像的深度。
图像深度是指在数字图像处理和计算机视觉领域中,每个像素点所使用的位数(bit depth),也就是用来表示图像中每一个像素点的颜色信息所需的二进制位数。图像深度决定了图像能够表达的颜色数量或灰度级。
normalize:当normalize为True的时候,方框滤波就是均值滤波,上式中的a就等于1/9;normalize为False的时候,a=1,相当于求区域内的像素和。
其滤波的过程与均值滤波一模一样,都采用卷积核从图像左上角开始,逐个计算对应位置的像素值,并从左至右、从上至下滑动卷积核,直至到达图像右下角,唯一的区别就是核值可能会不同。
示例:
import cv2 as cv
img = cv.imread('./images/lvbo2.png')
img_2 = cv.imread('./images/lvbo3.png')
cv.imshow('img', img)
# 方框滤波:normalize=TRUE 表示均值滤波;normalize=False 表示中心点像素值之和,核值为1
img_box_1 = cv.boxFilter(img, -1, (3, 3), normalize=True) # 均值滤波
img_box_2 = cv.boxFilter(img, -1, (3, 3), normalize=False) # 区域像素之和
cv.imshow('img_box_1', img_box_1)
cv.imshow('img_box_2', img_box_2)
cv.waitKey(10 * 1000)
cv.destroyAllWindows()
3.3 高斯滤波
高斯滤波是一种常用的图像处理技术,主要用于平滑图像、去除噪声。它通过使用高斯函数(正态分布)作为卷积核来对图像进行模糊处理。
前面两种滤波方式,卷积核内的每个值都一样,也就是说图像区域中每个像素的权重也就一样。高斯滤波的卷积核权重并不相同:中间像素点权重最高,越远离中心的像素权重越小。还记得我们在自适应二值化里是怎么生成高斯核的吗?这里跟自适应二值化里生成高斯核的步骤是一样的,都是以核的中心位置为坐标原点,然后计算周围点的坐标,然后带入下面的高斯公式中。
其中,x和 y 是相对于中心点的坐标偏移量,σ 是标准差,控制着高斯函数的宽度和高度。较大的 σ 值会导致更广泛的平滑效果。
卷积核通常是一个方形矩阵,其元素值根据高斯函数计算得出,并且这些值加起来等于1,近似于正态分布,以确保输出图像的亮度保持不变。
其中的值也是与自适应二值化里的一样,当时会取固定的系数,当kernel大于7并且没有设置时,会使用固定的公式进行计算σ的值:
我们还是以3*3的卷积核为例,其核值如下所示:
得到了卷积核的核值之后,其滤波过程与上面两种滤波方式的滤波过程一样,都是用卷积核从图像左上角开始,逐个计算对应位置的像素值,并从左至右、从上至下滑动卷积核,直至到达图像右下角,唯一的区别就是核值不同。
在滤波算法组件中,当参数filtering_method选为高斯滤波,参数component_param为ksize,sigmaX。下面讲解这2个参数的含义:
ksize:代表卷积核的大小,eg:ksize=3,则代表使用3×3的卷积核。
sigmaX:就是高斯函数里的值,σx值越大,模糊效果越明显。高斯滤波相比均值滤波效率要慢,但可以有效消除高斯噪声,能保留更多的图像细节,所以经常被称为最有用的滤波器。均值滤波与高斯滤波的对比结果如下(均值滤波丢失的细节更多):
示例:
import cv2 as cv
img = cv.imread('./images/lvbo2.png')
img_2 = cv.imread('./images/lvbo3.png')
cv.imshow('img', img)
# 高斯滤波
img_gauss = cv.GaussianBlur(img, (3, 3), 1)
cv.imshow('img_gauss', img_gauss)
cv.waitKey(10 * 1000)
cv.destroyAllWindows()
3.4 中值滤波
中值又叫中位数,是所有数排序后取中间的值。中值滤波没有核值,而是在原图中从左上角开始,将卷积核区域内的像素值进行排序,并选取中值作为卷积核的中点的像素值,其过程如下所示:
中值滤波就是用区域内的中值来代替本像素值,所以那种孤立的斑点,如0或255很容易消除掉,适用于去除椒盐噪声和斑点噪声。中值是一种非线性操作,效率相比前面几种线性滤波要慢。
比如下面这张斑点噪声图,用中值滤波显然更好:
在滤波算法组件中,当参数filtering_method选为中值滤波,参数component_param为ksize,代表卷积核的大小,eg:ksize=3,则代表使用3×3的卷积核。
示例:
import cv2 as cv
img = cv.imread('./images/lvbo2.png')
img_2 = cv.imread('./images/lvbo3.png')
cv.imshow('img', img)
# 中值滤波
img_m = cv.medianBlur(img_2, 3)
cv.imshow('img_m', img_m)
cv.waitKey(10 * 1000)
cv.destroyAllWindows()
3.5 双边滤波
模糊操作基本都会损失掉图像细节信息,尤其前面介绍的线性滤波器,图像的边缘信息很难保留下来。然而,边缘(edge)信息是图像中很重要的一个特征,所以这才有了双边滤波。
可以看到,双边滤波明显保留了更多边缘信息,下面来介绍一下双边滤波。
双边滤波的基本思路是同时考虑将要被滤波的像素点的空域信息(周围像素点的位置的权重)和值域信息(周围像素点的像素值的权重)。为什么要添加值域信息呢?是因为假设图像在空间中是缓慢变化的话,那么临近的像素点会更相近,但是这个假设在图像的边缘处会不成立,因为图像的边缘处的像素点必不会相近。因此在边缘处如果只是使用空域信息来进行滤波的话,得到的结果必然是边缘被模糊了,这样我们就丢掉了边缘信息,因此添加了值域信息。
双边滤波采用了两个高斯滤波的结合,一个负责计算空间邻近度的权值(也就是空域信息),也就是上面的高斯滤波器,另一个负责计算像素值相似度的权值(也就是值域信息),也是一个高斯滤波器。其公式如下所示:
其中,
S(i,j):指以(i,j)为中心的邻域的范围
f(k,l):输入的点的像素值,也就是在原始图像中位置 (k,l)的像素值。
w(i,j,k,l):这是权重函数,它决定了位置 (k,l)的像素值f(k,l)对位置(i,j)的贡献程度。
g(i,j):表示中心点(i,j)的像素值,也就是经过双边滤波处理后,在位置(i,j)的像素值。
分子:∑(k,l)∈S(i,j)f(k,l)ω(i,j,k,l)
这是对邻域内所有像素值 f(k,l)与其对应的权重 ω(i,j,k,l)的乘积求和。这一步计算了加权后的像素值总和。
分母:∑(k,l)∈S(i,j)ω(i,j,k,l)
这是对邻域内所有像素的权重 ω(i,j,k,l) 求和。这一步计算了权重的总和。
计算过程:
确定邻域:选择一个以 (i,j)为中心的邻域 S(i,j)。
计算权重:对于邻域内的每个像素 (k,l),计算其权重 ω(i,j,k,l)。
加权求和:将邻域内每个像素值 f(k,l) 与其权重 ω(i,j,k,l)相乘,并对所有乘积求和。
归一化:将加权求和的结果除以权重总和,得到最终的像素值 g(i,j)。
上述公式我们进行转化,假设公式中\omega(i,j,k,l)为m,则有
设,则有
此时可以看到,这与上面的滤波中计算过程已经一模一样了,就代表了第一个点的权重。接下来我们看看
是怎么来的,令
而
可以看到,对于来说,这就是普通的高斯滤波函数,其带入的坐标是坐标值,
是程序输入值,该函数是在空间临近度上计算的。而
是计算像素值相似度,也是高斯函数带入坐标值,然后得到对应点的像素值,进行两个点像素值插值的绝对值的平方。也就是说,双边滤波的核值不再是一个固定值,而是随着滤波的过程在不断发生变化的。
在本实验中的滤波算法组件中,当参数filtering_method选为双边滤波,参数component_param为ksize,d,sigmaColor,sigmaSpace,下面讲解这4个参数的含义:
ksize:卷积核的大小
d:过滤时周围每个像素领域的直径,这里已经设置了核大小。d=9===>9x9
sigmaColor:在color space(色彩空间)中过滤sigma。参数越大,那些颜色足够相近的的颜色的影响越大。较大的
sigmaColor
值意味着更大的颜色差异将被允许参与到加权平均中,从而使得颜色相近但不完全相同的像素也能够相互影响。通常情况下,这个值越大,图像看起来越平滑,但同时可能会导致颜色过渡更加模糊。sigmaSpace:在coordinate space(坐标空间)中过滤sigma。这个参数是坐标空间中的标准差,决定了像素位置对滤波结果的影响程度。它定义了在图像的空间域中,一个像素可以影响周围像素的最大距离。换句话说,它控制着滤波器作用的范围大小。较大的
sigmaSpace
值意味着更远的像素也会参与到当前像素的计算中,这可能导致更广泛的平滑效果,但也可能引入更多的模糊。
2个sigma参数:
简单一点可以令2个sigma的值相等;
如果他们很小(小于10),那么滤波器几乎没有什么效果;
如果他们很大(大于150),那么滤波器的效果会很强,使图像显得非常卡通化。
关于参数d:
过大的滤波器(d>5)执行效率低。
对于实时应用,建议取d=5;
对于需要过滤严重噪声的离线应用,可取d=9;
示例:
import cv2 as cv
img = cv.imread('./images/lvbo2.png')
img_2 = cv.imread('./images/lvbo3.png')
cv.imshow('img', img)
# 双边滤波
img_b = cv.bilateralFilter(img, 9, 75, 75)
cv.imshow('img_b', img_b)
cv.waitKey(10 * 1000)
cv.destroyAllWindows()
注意:
- 在不知道用什么滤波器好的时候,优先高斯滤波,然后均值滤波。
- 斑点和椒盐噪声优先使用中值滤波。
- 要去除噪点的同时尽可能保留更多的边缘信息,使用双边滤波。
- 线性滤波方式:均值滤波、方框滤波、高斯滤波(速度相对快)。
- 非线性滤波方式:中值滤波、双边滤波(速度相对慢)。
4. 图像梯度处理
如果你还记得高数中用一阶导数来求极值的话,就很容易理解了:把图片想象成连续函数,因为边缘部分的像素值是与旁边像素明显有区别的,所以对图片局部求极值,就可以得到整幅图片的边缘信息了。不过图片是二维的离散函数,导数就变成了差分,这个差分就称为图像的梯度。
4.1 垂直边缘提取
滤波是应用卷积来实现的,卷积的关键就是卷积核,我们来考察下面这个卷积核:
这个核是用来提取图片中的垂直边缘的,怎么做到的呢?看下图:
当前列左右两侧的元素进行差分,由于边缘的值明显小于(或大于)周边像素,所以边缘的差分结果会明显不同,这样就提取出了垂直边缘。
同理,把上面那个矩阵转置一下,就是提取水平边缘。这种差分操作就称为图像的梯度计算:
cv2.filter2D(src, ddepth, kernel)
filter2D函数是用于对图像进行二维卷积(滤波)操作。它允许自定义卷积核(kernel)来实现各种图像处理效果,如平滑、锐化、边缘检测等
src
: 输入图像,一般为numpy
数组。ddepth
: 输出图像的深度,可以是负值(表示与原图相同)、正值或其他特定值(常用-1 表示输出与输入具有相同的深度)。kernel
: 卷积核,一个二维数组(通常为奇数大小的方形矩阵),用于计算每个像素周围邻域的加权和。先用数组模拟一下
模拟数组示例:
import cv2 as cv
import numpy as np
# 模拟一张图像,灰度图
img = np.array([[100, 102, 109, 110, 98, 20, 19, 18, 21, 22],
[109, 101, 98, 108, 102, 20, 21, 19, 20, 21],
[109, 102, 105, 108, 98, 20, 22, 19, 19, 18],
[109, 98, 102, 108, 102, 20, 23, 19, 20, 22],
[109, 102, 105, 108, 98, 20, 22, 19, 20, 18],
[100, 102, 108, 110, 98, 20, 19, 18, 21, 22],
[109, 101, 98, 108, 102, 20, 22, 19, 20, 21],
[109, 102, 108, 108, 98, 20, 22, 19, 19, 18],
], dtype=np.float32)
kernel = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])
dst = cv.filter2D(img, -1, kernel=kernel)
print(dst)
图像示例:
import cv2 as cv
import numpy as np
img = cv.imread('images/shudu.png')
kernel_1 = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])
kernel_2 = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]).T
dst_1 = cv.filter2D(img, -1, kernel=kernel_1)
dst_2 = cv.filter2D(img, -1, kernel=kernel_2)
cv.imshow('img', img)
cv.imshow('dst_1', dst_1)
cv.imshow('dst_2', dst_2)
cv.waitKey(0)
cv.destroyAllWindows()
4.2 Sobel算子
上面的两个卷积核都叫做Sobel算子,只是方向不同,它先在垂直方向计算梯度:
再在水平方向计算梯度:
最后求出总梯度:
在梯度处理方式这个组件中,当参数filter_method选择Sobel时,其他参数的含义如下所述:
sobel_image = cv2.Sobel(src, ddepth, dx, dy, ksize)
src:这是输入图像,通常应该是一个灰度图像(单通道图像),因为 Sobel 算子是基于像素亮度梯度计算的。在彩色图像的情况下,通常需要先将其转换为灰度图像。
ddepth:这个参数代表输出图像的深度,即输出图像的数据类型。在 OpenCV 中,-1 表示输出图像的深度与输入图像相同。
dx,dy:当组合为dx=1,dy=0时求x方向的一阶导数,在这里,设置为1意味着我们想要计算图像在水平方向(x轴)的梯度。当组合为 dx=0,dy=1时求y方向的一阶导数(如果同时为1,通常得不到想要的结果,想两个方向都处理的比较好 学习使用后面的算子)
ksize:Sobel算子的大小,可选择3、5、7,默认为3。
很多人疑问,Sobel算子的卷积核这几个值是怎么来的呢?事实上,并没有规定,你可以用你自己的。比如,最初只利用邻域间的原始差值来检测边缘的Prewitt算子:
还有比Sobel更好用的Scharr算子,大家可以了解下:
示例:
import cv2 as cv
img = cv.imread('images/shudu.png', cv.COLOR_BGR2GRAY)
# sobel算子
dst1 = cv.Sobel(img, -1, 1, 0, ksize=3) # x方向进行差分,水平边缘
cv.imshow('dst1', dst1)
dst2 = cv.Sobel(img, -1, 0, 1, ksize=3) # y方向差分,竖直边缘
cv.imshow('dst2', dst2)
dst3 = cv.Sobel(img, -1, 1, 1, ksize=3)
cv.imshow('dst3', dst3)
cv.waitKey(3000)
cv.destroyAllWindows()
4.3 Laplacian算子
高数中用一阶导数求极值,在这些极值的地方,二阶导数为0,所以也可以通过求二阶导计算梯度:
一维的一阶和二阶差分公式分别为:
提取前面的系数,那么一维的Laplacian滤波核是:
而对于二维函数f(x,y),两个方向的二阶差分分别是:
合在一起就是:
同样提取前面的系数,那么二维的Laplacian滤波核就是:
这就是Laplacian算子的图像卷积模板,有些资料中在此基础上考虑斜对角情况,将卷积核拓展为:
cv2.Laplacian(src, ddepth)
src:这是输入图像
ddepth:这个参数代表输出图像的深度,即输出图像的数据类型。在 OpenCV 中,-1 表示输出图像的深度与输入图像相同。
Laplacian算子是二阶边缘检测的典型代表,一/二阶边缘检测各有优缺点,大家可自行了解。
示例:
import cv2 as cv
img = cv.imread('images/shudu.png', cv.COLOR_BGR2GRAY)
# 拉普拉索算子
dst_l = cv.Laplacian(img, -1)
cv.imshow('dst_l', dst_l)
cv.waitKey(3000)
cv.destroyAllWindows()
5. 图像边缘检测
5.1 计算图像的梯度与方向
这里使用了sobel算子来计算图像的梯度值,在上一章节中,我们了解到sobel算子其实就是一个核值固定的卷积核,如下所示:
首先使用sobel算子计算中心像素点的两个方向上的梯度和
,然后就能够得到其具体的梯度值:
也可以使用来代替。在OpenCV中,默认使用
来计算梯度值。
然后我们根据如下公式可以得到一个角度值:
这个角度值其实是当前边缘的梯度的方向。通过这个公式我们就可以计算出图片中所有的像素点的梯度值与梯度方向,然后根据梯度方向获取边缘的方向。
a). 并且如果梯度方向不是0°、45°、90°、135°这种特定角度,那么就要用到插值算法来计算当前像素点在其方向上进行插值的结果了,然后进行比较并判断是否保留该像素点。这里使用的是单线性插值,通过A1和A2两个像素点获得与
处的插值,然后与中心点C进行比较(非极大值抑制)。具体的插值算法请参考图像旋转实验。
b). 得到的值之后,就可以对边缘方向进行分类,为了简化计算过程,一般将其归为四个方向:水平方向、垂直方向、45°方向、135°方向。并且:
当值为-22.5°~22.5°,或-157.5°~157.5°,则认为边缘为水平边缘;
当法线方向为22.5°~67.5°,或-112.5°~-157.5°,则认为边缘为45°边缘;
当法线方向为67.5°~112.5°,或-67.5°~-112.5°,则认为边缘为垂直边缘;
当法线方向为112.5°~157.5°,或-22.5°~-67.5°,则认为边缘为135°边缘;
5.2 非极大值抑制
得到每个边缘的方向之后,其实把它们连起来边缘检测就算完了,但是为什么还有这一步与下一步呢?是因为经过第二步得到的边缘不经过处理是没办法使用的,因为高斯滤波的原因,边缘会变得模糊,导致经过第二步后得到的边缘像素点非常多,因此我们需要对其进行一些过滤操作,而非极大值抑制就是一个很好的方法,它会对得到的边缘像素进行一个排除,使边缘尽可能细一点。
在该步骤中,我们需要检查每个像素点的梯度方向上的相邻像素,并保留梯度值最大的像素,将其他像素抑制为零。假设当前像素点为(x,y),其梯度方向是0°,梯度值为G(x,y),那么我们就需要比较G(x,y)与两个相邻像素的梯度值:G(x-1,y)和G(x+1,y)。如果G(x,y)是三个值里面最大的,就保留该像素值,否则将其抑制为零。
总结:非极大值抑制的目的是在已经计算出图像梯度幅度图像的基础上,进一步细化边缘位置,减少假响应并确保边缘轮廓的一致性和单像素宽度。
工作原理:
在Canny算法中,首先通过高斯滤波和平滑图像,然后计算每个像素点的梯度幅值和方向。
对于每一个像素点,假设已知其梯度幅值(通常通过Sobel或其他导数算子计算得到)以及梯度的方向(通常是精确到某个离散的角度集合)。
非极大值抑制会沿着梯度方向检查像素点的梯度幅值是否是其邻域内(包括梯度方向指向的临近像素点)的最大值。
如果像素点的梯度幅值不是其梯度方向上局部极大值,则认为这个点不是边缘点,并将其梯度幅值置零或者忽略它。
这样做可以去除那些位于边缘两侧但由于噪声或者其他原因导致的不准确的梯度响应,从而保证最终得到的边缘只出现在梯度方向的极大值处,形成连续的、单像素宽的边缘。
5.3 双阈值筛选
经过非极大值抑制之后,我们还需要设置阈值来进行筛选,当阈值设的太低,就会出现假边缘,而阈值设的太高,一些较弱的边缘就会被丢掉,因此使用了双阈值来进行筛选,推荐高低阈值的比例为2:1到3:1之间,其原理如下图所示:
当某一像素位置的幅值超过最高阈值时,该像素必是边缘像素;当幅值低于最低像素时,该像素必不是边缘像素;幅值处于最高像素与最低像素之间时,如果它能连接到一个高于阈值的边缘时,则被认为是边缘像素,否则就不会被认为是边缘。也就是说,上图中的A和C是边缘,B不是边缘。因为C虽然不超过最高阈值,但其与A相连,所以C就是边缘。
总结:双阈值检测的目的是基于非极大值抑制后的梯度幅值图像,通过设定高低两个阈值来区分强边和弱边,并有效连接这些边缘点构成完整的边缘线。
工作原理:
设定两个阈值,一般称为高阈值(
highThreshold
)和低阈值(lowThreshold
)。如果一个像素点的梯度幅值大于等于高阈值,则标记为强边缘像素;
若梯度幅值介于高阈值和低阈值之间,则标记为潜在边缘像素;
若梯度幅值低于低阈值,则认为是非边缘像素,不予考虑。
接下来,采用某种形式的连通性分析,例如Hysteresis(滞后效应),只有当一个弱边缘像素与一个强边缘像素相邻时,才保留这个弱边缘像素作为最终的边缘点。否则,弱边缘像素会被丢弃。
5.4 API和使用
edges = cv2.Canny(image, threshold1, threshold2)
image
:输入的灰度/二值化图像数据。threshold1
:低阈值,用于决定可能的边缘点。threshold2
:高阈值,用于决定强边缘点。
示例:
import cv2 as cv
face = cv.imread('images/face.png')
# cv2.Canny(img,threshold_low,threshold_high)
dst = cv.Canny(face, 30, 100)
cv.imshow('face', face)
cv.imshow('dst', dst)
cv.waitKey(0)
cv.destroyAllWindows()