图像基本操作

读取图像

1
retval =cv2.imread (filename[,flags])

可以使用相对路径,如:

也可以使用绝对路径。

显示图像

在显示图像中,还涉及其他函数

以下的很多例子里都会见到这几个函数

保存图像

图像处理基础

基本图像

二值图

仅包含黑色和白色,用01进行表示。

灰度图

可以在读取时将参数设为0,即为灰度图

​ 通常, 计算机会将灰度处理为256个灰度级, 用数值区间[0,255]来表示。 其中, 数值”255”表示纯白色, 数值”0”表示纯黑色, 其余的数值表示从纯白到纯黑之间不同级别的灰度。用于表示256个灰度级的数值0~255, 正好可以用一个字节(8位二进制值)来表示。 所以灰度图一定是单通道的

彩图(以RGB色彩空间为例)

读取时参数为1,或者省略

​ 在 RGB 色彩空间中, 存在 R (red, 红色)通道、 G (green, 绿色)通道和 B (blue, 蓝色)通
道, 共三个通道。 每个色彩通道值的范围都在[0,255]之间 , 我们用这三个色彩通道的组合表示颜色。
以比较通俗的方式来解释就是, 有三个油漆桶, 分别装了红色、 绿色、 蓝色的油漆, 我们分别从每个油漆桶中取容星为0~255个单位的不等星的油漆, 将三种油漆混合就可以调配出一种新的颜色。三种油漆经过不同的组合, 共可以调配出所有常见的256x256x256=16 777 216种颜色。

​ 通常用一个三维数组来表示一幅RGB色彩空间的彩色图像。一般情况下 , 在RGB色彩空间中, 图像通道的顺序是R➔G➔B, 即第1个通道是R通道 , 第2个通道是G通道, 第3个通道是B通道。 需要特别注意的是, 在OpenCV中, 通道的顺序是B➔G➔R

像素处理

​ 需要说明的是 , 在OpenCV中, 最小的数据类型是无符号的8位数。 因此 , 在OpenCV中实际上并没有二值图像这种数据类型, 二值图像经常是通过处理得到的 , 然后使用0表示黑色, 使用255表示白色。

​ 可以将二值图像理解为特殊的灰度图像, 这里仅以灰度图像为例讨论像素的读取和修改。 通过2.1节的分析可知, 可以将图像理解为一个矩阵,在面向Python的OpenCV(Open CV for Python)中,图像就是 Numpy 库中的数组。 一个 OpenCV 灰度图像是一个二维数组, 可以使用表达式访间其中的像素值。 例如, 可以使用image[0,0]访间图像image第0行第0列位置上的像素点。 第0行第0列位于图像的左上角 , 其中第1个索引表示第0行, 第2个索引表示第0列。

​ 同样地,彩图就是三维数组,分别是BGR三个二维数组。(RGB色彩空间)。

这部分建议多学numpy。

通道操作(基于RGB)

通道拆分

通过索引拆分(numpy)

如下语句分别从中提取了B通道、 G通道、 R通道。

1
2
3
b=img[: ,: ,O]
g=img[: ,: ,1]
r=img[: ,: ,2]

通过函数拆分

函数cv2.split()能够拆分图像的通道。 例如, 可以使用如下语句拆分彩色BGR图像img, 得到B通道图像b、 G通道图像g和R通道图像r。

b,g,r=cv2.split (img)

上述语句与如下语句是等价的:

1
2
3
b = cv2.split (a) [0]
g = cv2.split (a) [1]
r = cv2.split (a) [2]

通道合并

通道合并是通道拆分的逆过程, 通过合并通道可以将三个通道的灰度图像构成一幅彩色图像。 函数cv2.merge()可以实现图像通道的合并, 例如有B通道图像b、 G通道图像g和R通道图像r, 使用函数cv2.merge()可以将这三个通道合并为一幅BGR的三通道彩色图像。 其实现的语句为:

1
bgr=cv2.merge ([b,g,r]) 

如:

1
2
3
4
5
6
7
if __name__ == '__main__':
src = cv2.imread("image\\0.png")
b, g, r = cv2.split(src)
bgr = cv2.merge([b, g, r])
cv2.imshow("dst",bgr)
cv2.waitKey()
cv2.destroyAllWindows()

图像属性

在图像处理过程中, 经常需要获取图像的属性, 例如图像的大小。

  • shape: 如果是彩色图像, 则返回包含行数 列数 通道数的数组;如果是二值图像或者灰度图像, 则仅返回行数和列数。 通过该属性的返回值是否包含通道数, 可以判断一幅图像是灰度图像(或二值图像)还是彩色图像。

  • size:返回图像的像素数目。 其值为行x列x通道数”, 灰度图像或者二值图像的通道数为1。

图像运算

加法

加号运算符

add()函数

加权和

按位逻辑

按位与

任何数与0都是0,与255则不变,都是二进制的按位与

按位或与按位非、按位异或

与按位与类似。

按位或:dst = cv2.bitwise_or(src[,mask])

按位非:dst = cv2.bitwise_not(src[,mask])

按位异或:dst = cv2.bitwise_xor(src[,mask])

掩模

可以理解为结果在返回前与掩模进行了按位与。

图像与数值的运算

位平面分解与其应用

例子:

应用:加解密与数字水印

加解密是使用密钥图像对原图进行位运算,数字水印是在最低位平面嵌入信息,因为这样对原图影响最小。

这里仅介绍了原始载体图像为灰度图像的情况,在实际中可以根据需要在多个通道内嵌入相同的水印(提高鲁棒性,即使部分水印丢失,也能提取出完整水印信息),或在各个不同的通道内嵌入不同的水印(提高嵌入容量)。在彩色图像的多个通道内嵌入水印的方法,与在灰度图像内嵌入水印的方法相同。

色彩空间

RGB是常见的色彩表示方法,但对于人眼来讲,很难直观地将一种色彩的RGB区分开来,并且在计算机处理色彩时,也有很多其他的颜色表现形式,这就形成了色彩空间。

色彩空间举例

1)RGB颜色空间

RGB(红绿蓝)是依据人眼识别的颜色定义出的空间,可表示大部分颜色。但在科学研究一般不采用RGB颜色空间,因为它的细节难以进行数字化的调整。它将色调,亮度,饱和度三个量放在一起表示,很难分开。它是最通用的面向硬件的彩色模型。该模型用于彩色监视器和一大类彩色视频摄像。

rgb_color_space

2)CMY/CMYK颜色空间

CMY是工业印刷采用的颜色空间。它与RGB对应。简单的类比RGB来源于是物体发光,而CMY是依据反射光得到的。具体应用如打印机:一般采用四色墨盒,即CMY加黑色墨盒

3)HSV/HSB颜色空间

HSV颜色空间是为了更好的数字化处理颜色而提出来的。有许多种HSX颜色空间,其中的X可能是V,也可能是I,依据具体使用而X含义不同。H是色调,S是饱和度,I是强度。HSB(Hue, Saturation, Brightness)颜色模型,这个颜色模型和HSL颜色模型同样都是用户台式机图形程序的颜色表示, 用六角形锥体表示自己的颜色模型。

hsv_color_space

4)HSI/HSL颜色空间

HSI颜色空间是为了更好的数字化处理颜色而提出来的。有许多种HSX颜色空间,其中的X可能是V,也可能是I,依据具体使用而X含义不同。H是色调,S是饱和度,I是强度。HSL(Hue, Saturation, Lightness)颜色模型,这个颜色模型都是用户台式机图形程序的颜色表示, 用六角形锥体表示自己的颜色模型。

hsi_color_space

5)Lab颜色空间

Lab颜色模型是由CIE(国际照明委员会)制定的一种色彩模式。自然界中任何一点色都可以在Lab空间中表达出来,它的色彩空间比RGB空间还要大。另外,这种模式是以数字化方式来描述人的视觉感应, 与设备无关,所以它弥补了RGB和CMYK模式必须依赖于设备色彩特性的不足。由于Lab的色彩空间要比RGB模式和CMYK模式的色彩空间大。这就意味着RGB以及CMYK所能描述的色彩信息在Lab空间中都能 得以影射。Lab颜色模型取坐标Lab,其中L亮度;a的正数代表红色,负端代表绿色;b的正数代表黄色, 负端代表兰色(a,b)有L=116f(y)-16, a=500[f(x/0.982)-f(y)], b=200[f(y)-f(z/1.183 )];其中: f(x)=7.787x+0.138, x<0.008856; f(x)=(x)1/3,x>0.008856

lab_color_space

6)YUV/YCbCr颜色空间

YUV是通过亮度-色差来描述颜色的颜色空间。

亮度信号经常被称作Y,色度信号是由两个互相独立的信号组成。视颜色系统和格式不同,两种色度信号经常被称作UV或PbPr或CbCr。这些都是由不同的编码格式所产生的,但是实际上,他们的概念基本相同。在DVD中,色度信号被存储成Cb和Cr(C代表颜色,b代表蓝色,r代表红色)。

YCbCr:

色彩空间转换

等等

alpha通道

在RGB色彩空间三个通道的基础上 ,还可以加上一个A通道,也叫alpha通道,表示透明度。这种4 个通道的色彩空间被称为RGBA色彩空间,PNG图像是一种典型的4通道图像。alpha 通道的赋值范围是[0,1], 或者[0,255], 表示从透明到不透明。

几何变换

阈值处理

阈值处理是指剔除图像内像素值高于一定值或者低于一定值的像素点。

阈值化处理

如:

1
2
3
4
5
6
7
8
9
if __name__ == '__main__':
src = cv2.imread("image\\0.png",-1)

cv2.imshow("src",src)
ret,dst = cv2.threshold(src,127,255,cv2.THRESH_BINARY)
cv2.imshow("dst",dst)

cv2.waitKey()
cv2.destroyAllWindows()

或者,修改相应参数

1
ret,dst = cv2.threshold(src,70,150,cv2.THRESH_BINARY)

反二值化:

1
ret,dst = cv2.threshold(src,127,255,cv2.THRESH_BINARY_INV)

自适应阈值处理

对于色彩均衡的图像,直接使用一个阈值就能完成对图像的阈值化处理。 但是,有时图像的色彩是不均衡的,此时如果只使用一个阈值,就无法得到清晰有效的阈值分割结果图像。

在进行阈值处理时,自适应阈值处理的方式通过计算每个像素点周围临近区域的加权平均值获得阈值,并使用该闯值对当前像素点进行处理。与普通的阈值处理方法相比,自适应阈值处理能够更好地处理明暗差异较大的图像

处理的图像必须是八位单通道图像 !!

1
2
3
4
5
6
7
if __name__ == '__main__':
src = cv2.imread("image\\0.png",0)
dst = cv2.adaptiveThreshold(src, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 3, 3)
cv2.imshow("dst", dst)

cv2.waitKey()
cv2.destroyAllWindows()

最优阈值函数

实际处理的图像往往是很复杂的, 不太可能像上述img那样 , 一眼就观察出最合适的阈值。 如果一个个去尝试, 工作量无疑是巨大的。Otsu方法能够根据当前图像给出最佳的类间分割阈值。 简而言之, Otsu方法会遍历所有可能阈值, 从而找到最佳的阈值。

在 OpenCV 中, 通过在函数cv2.threshold ()中对参数type 的类型多传递 一个参数 “cv2.THRESH_ OTSU” , 即可实现Otsu方式的阈值分割。需要说明的是, 在使用Otsu方法时, 要把阈值设为0。此时的函数cv2.threshold()会自动寻找最优阈值, 并将该阈值返回

处理的图像必须是八位单通道图像 !!

  • 设定的阈值为0。
  • 返回值t是Otsu方法计算得到并使用的最优阈值。
  • 需要注意,如果采用普通的阈值分割,返回的闯值就是设定的阈值。
1
2
3
4
5
6
7
if __name__ == '__main__':
src = cv2.imread("image\\0.png",0)
t,dst = cv2.threshold(src, 0,255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
cv2.imshow("dst", dst)

cv2.waitKey()
cv2.destroyAllWindows()

图像平滑处理(滤波)

如果针对图像内的每一个像素点都进行上述平滑处理, 就能够对整幅图像完成平滑处理, 有效地去除图像内的噪声信息。图像平滑处理的基本原理是, 将噪声所在像素点的像素值处理为其周围临近像素点的值的近似值

均值滤波

原理

均值滤波是指用当前像素点周围N·N个像素值的均值来代替当前像素值。在进行均值滤波时,首先要考虑需要对周围多少个像素点取平均值。通常情况下,我们会以当前像素点为中心, 对行数和列数相等的一块区域内的所有像素点的像素值求平均。针对边缘像素点, 可以只取图像内存在的周围邻域点的像素值均值。

5×55\times5均值滤波为例,相当于:

函数

卷积核小,滤波效果差,卷积核大,图片失真严重(类似高斯模糊),类似于近视的感觉

1
2
3
4
5
6
7
8
if __name__ == '__main__':
src = cv2.imread("image\\0.png",0)
cv2.imshow("src", src)
dst=cv2.blur(src,(10,10))
cv2.imshow("dst", dst)

cv2.waitKey()
cv2.destroyAllWindows()

方框滤波

原理

方框滤波中,可以自由选择是否对均值滤波的结果进行归一化, 即可以自由选择滤波结果是邻域像素值之和的平均值,还是邻域像素值之和。

函数

当 normalize=0时, 因为不进行归一化处理, 因此滤波得到的值很可能超过当前像素值范围的最大值, 从而被截断为最大值。 这样, 就会得到一幅纯白色的图像。

高斯滤波

原理

在进行均值滤波和方框滤波时,其邻域内每个像素的权重是相等的。在高斯滤波中,会将中心点的权重值加大, 远离中心点的权重值减小,在此基础上计算邻域内各个像素值不同权重的和

例如:

函数

滤波核必须是奇数!!

在该函数中, sigmaYborderType是可选参数。 sigmaX是必选参数, 但是可以将该参数设置为0, 让函数自己去计算sigmaX的具体值。
官方文档建议显式地指定ksizesigmaXsigmaY三个参数的值, 以避免将来函数修改后可能造成的语法错误。 当然, 在实际处理中, 可以显式指定sigmaXsigmaY为默认值0。因此, 函数cv2.GaussianBlur()的常用形式为:

1
dst = cv2.GaussianBlur (src,ksize,0,0) 

效果略好于方框滤波

1
2
3
4
5
6
7
8
if __name__ == '__main__':
src = cv2.imread("image\\0.png")
cv2.imshow("src", src)
dst = cv2.GaussianBlur(src, (9, 9), 0, 0)
cv2.imshow("dst", dst)

cv2.waitKey()
cv2.destroyAllWindows()

中值滤波

原理

中值滤波会取当前像素点及其周围临近像素点(一共有奇数个像素点)的像素值,将这些像素值排序,然后将位于中间位置的像素值作为当前像素点的像素值。(取中位数)

函数

双边滤波

原理

前述滤波方式基本都只考虑了空间的权重信息, 这种情况计算起来比较方便, 但是在边缘信息的处理上存在较大的问题。在均值滤波、 方框滤波、 高斯滤波中, 都会计算边缘上各个像素点的加权平均值, 从而模糊边缘息。

双边滤波在计算某一个像素点的新值时, 不仅考虑距离信息(距离越远,权重越小),还考虑色彩信息(色彩差别越大,权重越小)。双边滤波综合考虑距离和色彩的权重结果,既能够有效地去除噪声,又能够较好地保护边缘信息。

在双边滤波中,当处在边缘时,与当前点色彩相近的像素点(颜色距离很近)会被给予较大的权重值;而与当前色彩差别较大的像素点(颜色距离很远)会被给予较小的权重值(极端情况下权重可能为0, 直接忽略该点),这样就保护了边缘信息。

函数

双边滤波对边缘的保护很好,但对噪声的处理不是很好。

2D卷积

形态学操作

这一部分是以后很多操作的基础,以后很多操作或多或少都跟形态学有关系。

腐蚀

原理

腐蚀能够将图像的边界点消除,使图像沿着边界向内收缩,也可以将小于指定结构体元素的部分去除。腐蚀用来“收缩“或者“细化”二值图像中的前景,借此实现去除噪声、元素分割等功能。在腐蚀过程中,通常使用一个结构元来逐个像素地扫描要被腐蚀的图像,并根据结构元和被腐蚀图像的关系来确定腐蚀结果。

需要注意的是,腐蚀操作等形态学操作是逐个像素地来决定值的,每次判定的点都是与结构元中心点所对应的点。图8-3中的两幅图像表示结构元与前景色的两种不同关系。根据这两种不同的关系来决定,腐蚀结果图像中的结构元中心点所对应位置像素点的像素值。

函数

例如:

1
2
3
4
5
6
7
8
if __name__ == '__main__':
src = cv2.imread("image\\0.png",-1)
cv2.imshow("src", src)
kernel = np.ones((5,5),np.uint8)
dst = cv2.erode(src, kernel)
cv2.imshow("dst", dst)
cv2.waitKey()
cv2.destroyAllWindows()

膨胀

原理

与腐蚀相反,同样地,处理思路也与其相反。

  • 如果结构元中任意一点处千前景图像中,就将膨胀结果图像中对应像素点处理为前景色。
  • 如果结构元完全处千背景图像外,就将膨胀结果图像中对应像素点处理为背景色。

函数

1
2
3
4
5
6
7
8
if __name__ == '__main__':
src = cv2.imread("image\\0.png",-1)
cv2.imshow("src", src)
kernel = np.ones((5,5),np.uint8)
dst = cv2.dilate(src, kernel)
cv2.imshow("dst", dst)
cv2.waitKey()
cv2.destroyAllWindows()

通用形态学函数

函数

  • 开运算进行的操作是先将图像腐蚀,再对腐蚀的结果进行膨胀。开运算可以用千去噪、计数等。(计数:可以利用开运算将连接在一起的不同区域划分开)

  • 闭运算是先膨胀、 后腐蚀的运算, 它有助于关闭前景物体内部的小孔, 或去除物体上的小黑点,还可以将不同的前景图像进行连接。

  • 形态学梯度运算是用图像的膨胀图像减腐蚀图像的操作, 该操作可以获取原始图像中前景图像的边缘。

  • 礼帽运算是用原始图像减去其开运算图像的操作。 礼帽运算能够获取图像的噪声信息,或者得到比原始图像的边缘更亮的边缘信息。

  • 黑帽运算是用闭运算图像减去原始图像的操作。黑帽运算能够获取图像内部的小孔,或前景色中的小黑点, 或者得到比原始图像的边缘更暗的边缘部分。

核函数

图像梯度

图像梯度计算的是图像变化的速度。对千图像的边缘部分,其灰度值变化较大,梯度值也较大;相反,对于图像中比较平滑的部分, 其灰度值变化较小,相应的梯度值也较小。一般情况下,图像梯度计算的是图像的边缘信息

原书对此做了很详细的讲解,可以直接参考。

以下参考

Roberts 算子

Roberts 算子,又称罗伯茨算子,是一种最简单的算子,是一种利用局部差分算子寻找边缘的算子。他采用对角线方向相邻两象素之差近似梯度幅值检测边缘。检测垂直边缘的效果好于斜向边缘,定位精度高,对噪声敏感,无法抑制噪声的影响

1963年, Roberts 提出了这种寻找边缘的算子。 Roberts 边缘算子是一个 2x2 的模版,采用的是对角方向相邻的两个像素之差。

Roberts 算子的模板分为水平方向和垂直方向,如下所示,从其模板可以看出, Roberts 算子能较好的增强正负 45 度的图像边缘

dx=[1001](2)dx= \left[\begin{matrix} -1 & 0 \\ 0 & 1 \\ \end{matrix} \right] \tag{2}

dy=[0110](2)dy = \left[\begin{matrix} 0 & -1 \\ 1 & 0 \\ \end{matrix} \right] \tag{2}

Roberts 算子在水平方向和垂直方向的计算公式如下:

dx(i,j)=f(i+1,j+1)f(i,j)d_x(i,j)=f(i+1,j+1)-f(i,j)

dy(i,j)=f(i,j+1)f(i+1,j)d_y(i,j)=f(i,j+1)-f(i+1,j)

Roberts 算子像素的最终计算公式如下:

S=dx(i,j)2+dy(i,j)2S=\sqrt{d_x(i,j)^2+d_y(i,j)^2}{}

实现 Roberts 算子,我们主要通过 OpenCV 中的 filter2D() 这个函数,这个函数的主要功能是通过卷积核实现对图像的卷积运算:

1
def filter2D(src, ddepth, kernel, dst=None, anchor=None, delta=None, borderType=None)
  • src: 输入图像
  • ddepth: 目标图像所需的深度
  • kernel: 卷积核

接下来开始写代码,首先是图像的读取,并把这个图像转化成灰度图像,这个没啥好说的:

1
2
3
4
5
6
# 读取图像
img = cv.imread('maliao.jpg', cv.COLOR_BGR2GRAY)
rgb_img = cv.cvtColor(img, cv.COLOR_BGR2RGB)

# 灰度化处理图像
grayImage = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

然后是使用 Numpy 构建卷积核,并对灰度图像在 x 和 y 的方向上做一次卷积运算:

1
2
3
4
5
6
# Roberts 算子
kernelx = np.array([[-1, 0], [0, 1]], dtype=int)
kernely = np.array([[0, -1], [1, 0]], dtype=int)

x = cv.filter2D(grayImage, cv.CV_16S, kernelx)
y = cv.filter2D(grayImage, cv.CV_16S, kernely)

注意:在进行了 Roberts 算子处理之后,还需要调用convertScaleAbs()函数计算绝对值,并将图像转换为8位图进行显示,然后才能进行图像融合:

1
2
3
4
# 转 uint8 ,图像融合
absX = cv.convertScaleAbs(x)
absY = cv.convertScaleAbs(y)
Roberts = cv.addWeighted(absX, 0.5, absY, 0.5, 0)

最后是通过 pyplot 将图像显示出来:

1
2
3
4
5
6
7
8
9
# 显示图形
titles = ['原始图像', 'Roberts算子']
images = [rgb_img, Roberts]

for i in range(2):
plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()

最终结果如下:

Prewitt 算子

Prewitt 算子是一种一阶微分算子的边缘检测,利用像素点上下、左右邻点的灰度差,在边缘处达到极值检测边缘,去掉部分伪边缘,对噪声具有平滑作用。

由于 Prewitt 算子采用 3 * 3 模板对区域内的像素值进行计算,而 Robert 算子的模板为 2 * 2 ,故 Prewitt 算子的边缘检测结果在水平方向和垂直方向均比 Robert 算子更加明显。Prewitt算子适合用来识别噪声较多、灰度渐变的图像

Prewitt 算子的模版如下:

dx=[101101101](3)dx = \left[\begin{matrix}1 & 0 & -1 \\1 & 0 & -1 \\ 1 & 0 & -1\end{matrix}\right] \tag{3}

dy=[111000111](3)dy = \left[\begin{matrix}-1 & -1 & -1 \\0 & 0 & 0 \\ 1 & 1 & 1\end{matrix}\right] \tag{3}

在代码实现上, Prewitt 算子的实现过程与 Roberts 算子比较相似,我就不多介绍,直接贴代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt

# 读取图像
img = cv.imread('maliao.jpg', cv.COLOR_BGR2GRAY)
rgb_img = cv.cvtColor(img, cv.COLOR_BGR2RGB)

# 灰度化处理图像
grayImage = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# Prewitt 算子
kernelx = np.array([[1,1,1],[0,0,0],[-1,-1,-1]],dtype=int)
kernely = np.array([[-1,0,1],[-1,0,1],[-1,0,1]],dtype=int)

x = cv.filter2D(grayImage, cv.CV_16S, kernelx)
y = cv.filter2D(grayImage, cv.CV_16S, kernely)

# 转 uint8 ,图像融合
absX = cv.convertScaleAbs(x)
absY = cv.convertScaleAbs(y)
Prewitt = cv.addWeighted(absX, 0.5, absY, 0.5, 0)

# 用来正常显示中文标签
plt.rcParams['font.sans-serif'] = ['SimHei']

# 显示图形
titles = ['原始图像', 'Prewitt 算子']
images = [rgb_img, Prewitt]

for i in range(2):
plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()

从结果上来看, Prewitt 算子图像锐化提取的边缘轮廓,其效果图的边缘检测结果比 Robert 算子更加明显。

Sobel 算子

Sobel 算子的中文名称是索贝尔算子,是一种用于边缘检测的离散微分算子,它结合了高斯平滑和微分求导。

Sobel 算子在 Prewitt 算子的基础上增加了权重的概念,认为相邻点的距离远近对当前像素点的影响是不同的,距离越近的像素点对应当前像素的影响越大,从而实现图像锐化并突出边缘轮廓

算法模版如下:

dx=[101202101](3)dx = \left[\begin{matrix}1 & 0 & -1 \\2 & 0 & -2 \\ 1 & 0 & -1\end{matrix}\right] \tag{3}

dy=[121000121](3)dy = \left[\begin{matrix}-1 & -2 & -1 \\0 & 0 & 0 \\ 1 & 2 & 1\end{matrix}\right] \tag{3}

Sobel 算子根据像素点上下、左右邻点灰度加权差,在边缘处达到极值这一现象检测边缘。对噪声具有平滑作用,提供较为精确的边缘方向信息。因为 Sobel 算子结合了高斯平滑和微分求导(分化),因此结果会具有更多的抗噪性,当对精度要求不是很高时, Sobel 算子是一种较为常用的边缘检测方法。

Sobel 算子近似梯度的大小的计算公式如下:

G=dx2+dy2G=\sqrt{d_x^2+d_y^2}{}

梯度方向的计算公式如下:

θ=tan1(dydx)\theta=\tan^{-1}{(\dfrac{dy}{dx})}

如果以上的角度 θ 等于零,即代表图像该处拥有纵向边缘,左方较右方暗。

在 Python 中,为我们提供了 Sobel() 函数进行运算,整体处理过程和前面的类似,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import cv2 as cv
import matplotlib.pyplot as plt

# 读取图像
img = cv.imread('maliao.jpg', cv.COLOR_BGR2GRAY)
rgb_img = cv.cvtColor(img, cv.COLOR_BGR2RGB)

# 灰度化处理图像
grayImage = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# Sobel 算子
x = cv.Sobel(grayImage, cv.CV_16S, 1, 0)
y = cv.Sobel(grayImage, cv.CV_16S, 0, 1)

# 转 uint8 ,图像融合
absX = cv.convertScaleAbs(x)
absY = cv.convertScaleAbs(y)
Sobel = cv.addWeighted(absX, 0.5, absY, 0.5, 0)

# 用来正常显示中文标签
plt.rcParams['font.sans-serif'] = ['SimHei']

# 显示图形
titles = ['原始图像', 'Sobel 算子']
images = [rgb_img, Sobel]

for i in range(2):
plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()

Laplacian 算子

拉普拉斯( Laplacian )算子是 n 维欧几里德空间中的一个二阶微分算子,常用于图像增强领域和边缘提取。

Laplacian 算子的核心思想:判断图像中心像素灰度值与它周围其他像素的灰度值,如果中心像素的灰度更高,则提升中心像素的灰度;反之降低中心像素的灰度,从而实现图像锐化操作

在实现过程中, Laplacian 算子通过对邻域中心像素的四方向或八方向求梯度,再将梯度相加起来判断中心像素灰度与邻域内其他像素灰度的关系,最后通过梯度运算的结果对像素灰度进行调整。

Laplacian 算子分为四邻域和八邻域,四邻域是对邻域中心像素的四方向求梯度,八邻域是对八方向求梯度。

四邻域模板如下:

H=[010141010](3)H = \left[\begin{matrix}0 & -1 & 0 \\-1 & 4 & -1 \\ 0 & -1 & 0\end{matrix}\right] \tag{3}

八邻域模板如下:

H=[111141111](3)H = \left[\begin{matrix}-1 & -1 & -1 \\-1 & 4 & -1 \\ -1 & -1 & -1\end{matrix}\right] \tag{3}

通过模板可以发现,当邻域内像素灰度相同时,模板的卷积运算结果为0;当中心像素灰度高于邻域内其他像素的平均灰度时,模板的卷积运算结果为正数;当中心像素的灰度低于邻域内其他像素的平均灰度时,模板的卷积为负数。对卷积运算的结果用适当的衰弱因子处理并加在原中心像素上,就可以实现图像的锐化处理。

在 OpenCV 中, Laplacian 算子被封装在 Laplacian() 函数中,其主要是利用Sobel算子的运算,通过加上 Sobel 算子运算出的图像 x 方向和 y 方向上的导数,得到输入图像的图像锐化结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import cv2 as cv
import matplotlib.pyplot as plt

# 读取图像
img = cv.imread('maliao.jpg', cv.COLOR_BGR2GRAY)
rgb_img = cv.cvtColor(img, cv.COLOR_BGR2RGB)

# 灰度化处理图像
grayImage = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# Laplacian
dst = cv.Laplacian(grayImage, cv.CV_16S, ksize = 3)
Laplacian = cv.convertScaleAbs(dst)

# 用来正常显示中文标签
plt.rcParams['font.sans-serif'] = ['SimHei']

# 显示图形
titles = ['原始图像', 'Laplacian 算子']
images = [rgb_img, Laplacian]

for i in range(2):
plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()

最后

边缘检测算法主要是基于图像强度的一阶和二阶导数,但导数通常对噪声很敏感,因此需要采用滤波器来过滤噪声,并调用图像增强或阈值化算法进行处理,最后再进行边缘检测

最后我先使用高斯滤波去噪之后,再进行边缘检测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt

# 读取图像
img = cv.imread('maliao.jpg')
rgb_img = cv.cvtColor(img, cv.COLOR_BGR2RGB)

# 灰度化处理图像
gray_image = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# 高斯滤波
gaussian_blur = cv.GaussianBlur(gray_image, (3, 3), 0)

# Roberts 算子
kernelx = np.array([[-1, 0], [0, 1]], dtype = int)
kernely = np.array([[0, -1], [1, 0]], dtype = int)
x = cv.filter2D(gaussian_blur, cv.CV_16S, kernelx)
y = cv.filter2D(gaussian_blur, cv.CV_16S, kernely)
absX = cv.convertScaleAbs(x)
absY = cv.convertScaleAbs(y)
Roberts = cv.addWeighted(absX, 0.5, absY, 0.5, 0)

# Prewitt 算子
kernelx = np.array([[1, 1, 1], [0, 0, 0], [-1, -1, -1]], dtype=int)
kernely = np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]], dtype=int)
x = cv.filter2D(gaussian_blur, cv.CV_16S, kernelx)
y = cv.filter2D(gaussian_blur, cv.CV_16S, kernely)
absX = cv.convertScaleAbs(x)
absY = cv.convertScaleAbs(y)
Prewitt = cv.addWeighted(absX, 0.5, absY, 0.5, 0)

# Sobel 算子
x = cv.Sobel(gaussian_blur, cv.CV_16S, 1, 0)
y = cv.Sobel(gaussian_blur, cv.CV_16S, 0, 1)
absX = cv.convertScaleAbs(x)
absY = cv.convertScaleAbs(y)
Sobel = cv.addWeighted(absX, 0.5, absY, 0.5, 0)

# 拉普拉斯算法
dst = cv.Laplacian(gaussian_blur, cv.CV_16S, ksize = 3)
Laplacian = cv.convertScaleAbs(dst)

# 展示图像
titles = ['Source Image', 'Gaussian Image', 'Roberts Image',
'Prewitt Image','Sobel Image', 'Laplacian Image']
images = [rgb_img, gaussian_blur, Roberts, Prewitt, Sobel, Laplacian]
for i in np.arange(6):
plt.subplot(2, 3, i+1), plt.imshow(images[i], 'gray')
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()

canny边缘检测

非极大抑制

双阈值确定边缘

函数

以上几步操作都封装好了,可以直接调用。

样例:threshold两个值较小时精度较高

1
2
3
4
5
6
7
8
9
if __name__ == '__main__':
src = cv2.imread("image\\0.png", -1)
cv2.imshow("src", src)
dst1 = cv2.Canny(src,128,255)
cv2.imshow("dst1", dst1)
dst = cv2.Canny(src, 32, 128)
cv2.imshow("dst2", dst)
cv2.waitKey()
cv2.destroyAllWindows()

图像金字塔

图像金字塔是图像多尺度表达的一种,是一种以多分辨率来解释图像的有效但概念简单的结构。一幅图像的金字塔是一系列以金字塔形状排列的分辨率逐步降低,且来源于同一张原始图的图像集合。其通过梯次向下采样获得,直到达到某个终止条件才停止采样。我们将一层一层的图像比喻成金字塔,层级越高,则图像越小,分辨率越低。

高斯金字塔

高斯金字塔是由底部的最大分辨率图像逐次向下采样得到的一系列图像。最下面的图像分辨率最高,越往上图像分辨率越低。

这个过程实际上就是一个重复高斯平滑并重新对图像采样的过程

  1. 对于原始图像先进行一次高斯平滑处理,使用高斯核(5 * 5)进行一次卷积处理。下面是 5 * 5 的高斯核。
  1. 接下来是对图像进行采样,这一步会去除图像中的偶数行和奇数列,从而得到一张图像。
  2. 再然后是重复上面两步,直到得到最终的目标图像为止。

从上面的步骤可以看出,再每次循环中,得到的结果图像只有原图像的 1/4 大小(横纵向均做隔行采样)。

注意:向下采样会逐渐丢失图像信息,属于非线性的处理,此过程不可逆,属于有损处理。

高斯金字塔向上采样:

  1. 将图像在每个方向扩大为原来的两倍,新增的行和列以 0 填充。
  2. 使用高斯核(5 * 5)对得到的图像进行一次高斯平滑处理,获得 「新增像素」的近似值。

注意:此过程与向下采样的过程一样,属于非线性处理,无法逆转,属于有损处理。

此过程得到的图像为放大后的图像,与原图相比会比较模糊,因为在缩放的过程中丢失了一些图像信息,如果想在缩小和放大整个过程中减少信息的丢失。

如果在缩放过程中想要减少图像信息的丢失,这就引出了第二个图像金字塔 —— 「拉普拉斯金字塔」 。

拉普拉斯金字塔

拉普拉斯金字塔可以认为是残差金字塔,用来存储下采样后图片与原始图片的差异。

上面我们介绍了基于高斯金字塔,一个原始图像 Gi ,先进行向下采样得到 G(i-1) ,再对 G(i-1) 进行向上采样得到 Up(Down(Gi)) ,最终得到的 Up(Down(Gi)) 与原始的 Gi 是存在差异的。

这是因为向下采样丢失的信息并不能由向上采样来进行恢复,高斯金字塔是一种有损的采样方式。

如果我们想要完全恢复原始图像,那么我们在进行采样的时候就需要保留差异信息。

这就是拉普拉斯金字塔的核心思想,每次向下采样后,将再次向上采样,得到向上采样的 Up(Down(Gi)) 后,记录 Up(Down(Gi))Gi 的差异信息

函数

OpenCV 为向上采样和向下采样提供了两个函数:pyrDown()pyrUp()

pyrDown() 的原函数如下:

1
def pyrDown(src, dst=None, dstsize=None, borderType=None)
  • src: 表示输入图像。
  • dst: 表示输出图像,它与src类型、大小相同。
  • dstsize: 表示降采样之后的目标图像的大小。
  • borderType: 表示表示图像边界的处理方式。

注意:dstsize 参数是有默认值的,调用函数的时候不指定第三个参数,那么这个值是按照 Size((src.cols+1)/2, (src.rows+1)/2) 计算的。而且不管如何指定这个参数,一定必须保证满足以下关系式:|dstsize.width * 2 - src.cols| ≤ 2; |dstsize.height * 2 - src.rows| ≤ 2。也就是说降采样的意思其实是把图像的尺寸缩减一半,行和列同时缩减一半。

pyrUp() 的原函数如下:

1
def pyrUp(src, dst=None, dstsize=None, borderType=None)
  • src: 表示输入图像。
  • dst: 表示输出图像,它与src类型、大小相同。
  • dstsize: 表示降采样之后的目标图像的大小。
  • borderType: 表示表示图像边界的处理方式。

参数释义和上面的 pyrDown() 保持一致。

下面是高斯金字塔和拉普拉斯金字塔的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import cv2 as cv

#高斯金字塔
def gaussian_pyramid(image):
level = 3#设置金字塔的层数为3
temp = image.copy() #拷贝图像
gaussian_images = [] #建立一个空列表
for i in range(level):
dst = cv.pyrDown(temp) #先对图像进行高斯平滑,然后再进行降采样(将图像尺寸行和列方向缩减一半)
gaussian_images.append(dst) #在列表末尾添加新的对象
cv.imshow("gaussian"+str(i), dst)
temp = dst.copy()
return gaussian_images


#拉普拉斯金字塔
def laplacian_pyramid(image):
gaussian_images = gaussian_pyramid(image) #做拉普拉斯金字塔必须用到高斯金字塔的结果
level = len(gaussian_images)
for i in range(level-1, -1, -1):
if (i-1) < 0:
expand = cv.pyrUp(gaussian_images[i], dstsize = image.shape[:2])
laplacian = cv.subtract(image, expand)
# 展示差值图像
cv.imshow("laplacian_down_"+str(i), laplacian)
else:
expand = cv.pyrUp(gaussian_images[i], dstsize = gaussian_images[i-1].shape[:2])
laplacian = cv.subtract(gaussian_images[i-1], expand)
# 展示差值图像
cv.imshow("laplacian_down_"+str(i), laplacian)


src = cv.imread('maliao.jpg')
print(src.shape)
# 先将图像转化成正方形,否则会报错
input_image = cv.resize(src, (560, 560))
# 设置为 WINDOW_NORMAL 可以任意缩放
cv.namedWindow('input_image', cv.WINDOW_AUTOSIZE)
cv.imshow('input_image', src)
laplacian_pyramid(src)
cv.waitKey(0)
cv.destroyAllWindows()

图像轮廓

边缘检测虽然能够检测出边缘,但边缘是不连续的,检测到的边缘并不是一个整体。图像轮廓是指将边缘连接起来形成的一个整体, 用千后续的计算
OpenCV提供了查找图像轮廓的函数cv2.findContours(),该函数能够查找图像内的轮廓信息,而函数cv2.drawContours() 能够将轮廓绘制出来。
图像轮廓是图像中非常重要的一个特征信息,通过对图像轮廓的操作,我们能够获取目标图像的大小 、位置 、方向等信息。

查找与绘制

一个轮廓对应着一系列的点,这些点以某种方式表示图像中的一条曲线。在 OpenCV 中,函数cv2.findContours()用于查找图像的轮廓,并能够根据参数返回特定表示方式的轮廓(曲线)。函数cv2.drawContours()能够将查找到的轮廓绘制到图像上,该函数可以根据参数在图像上绘制不同样式(实心/空心点, 以及线条的不同粗细、颜色等)的轮廓,可以绘制全部轮廓也可以仅绘制指定的轮廓。

查找轮廓

在 OpenCV 中,轮廓提取函数 findContours() 实现的是 1985 年由一名叫做 Satoshi Suzuki 的人发表的一篇论文中的算法,如下:

Satoshi Suzuki and others. Topological structural analysis of digitized binary images by border following. Computer Vision, Graphics, and Image Processing, 30(1):32–46, 1985.

对原理感兴趣的同学可以去搜搜看,不是很难理解。

先看一个示例代码:

1
2
3
4
5
6
7
8
9
10
import cv2 as cv

img = cv.imread("black.png")
gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 降噪
ret, thresh = cv.threshold(gray_img, 127, 255, 0)
# 寻找轮廓
contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)

print(len(contours[0]))

查找轮廓使用的函数为 findContours() ,它的原型函数如下:

1
contours, hierarchy=cv2.findContours(image, mode, method[, contours[, hierarchy[, offset ]]])  
  • contours:返回的图像轮廓,是个List,contours[i]是第i个轮廓,contours[i][j]是第i个轮廓的第j个点
  • hierarchy:图像的拓扑信息(图像层次)
  • image:源图像。
  • mode:表示轮廓检索模式。
1
2
3
4
cv2.RETR_EXTERNAL 表示只检测外轮廓。
cv2.RETR_LIST 检测的轮廓不建立等级关系。
cv2.RETR_CCOMP 建立两个等级的轮廓,上面的一层为外边界,里面的一层为内孔的边界信息。如果内孔内还有一个连通物体,这个物体的边界也在顶层。
cv2.RETR_TREE 建立一个等级树结构的轮廓。
  • method:表示轮廓近似方法。
1
2
cv2.CHAIN_APPROX_NONE 存储所有的轮廓点。
cv2.CHAIN_APPROX_SIMPLE 压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的终点坐标,例如一个矩形轮廓只需4个点来保存轮廓信息。

这里可以使用 print(len(contours[0])) 函数将包含的点的数量打印出来,比如在上面的示例中,使用参数 cv2.CHAIN_APPROX_NONE 轮廓点有 1382 个,而使用参数 cv2.CHAIN_APPROX_SIMPLE 则轮廓点只有 4 个。

绘制轮廓

绘制轮廓使用到的 OpenCV 为我们提供的 drawContours() 这个函数,下面是它的三个简单的例子:

1
2
3
4
5
6
7
# To draw all the contours in an image:
cv2.drawContours(img, contours, -1, (0,255,0), 3)
# To draw an individual contour, say 4th contour:
cv2.drawContours(img, contours, 3, (0,255,0), 3)
# But most of the time, below method will be useful:
cnt = contours[4]
cv2.drawContours(img, [cnt], 0, (0,255,0), 3)

drawContours() 函数中有五个参数:

  • 第一个参数是源图像。
  • 第二个参数是应该包含轮廓的列表。
  • 第三个参数是列表索引,用来选择要绘制的轮廓,为-1时表示绘制所有轮廓。
  • 第四个参数是轮廓颜色。
  • 第五个参数是轮廓线的宽度,为 -1 时表示填充。

我们接着前面的示例把使用 findContours() 找出来的轮廓绘制出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import cv2 as cv

img = cv.imread("black.png")
gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
cv.imshow("img", img)
# 降噪
ret, thresh = cv.threshold(gray_img, 127, 255, 0)
# 寻找轮廓
contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)

print(len(contours[0]))

# 绘制绿色轮廓
cv.drawContours(img, contours, -1, (0,255,0), 3)

cv.imshow("draw", img)

cv.waitKey(0)
cv.destroyAllWindows()

使用:提取前景

1
2
3
4
5
6
7
8
9
10
11
12
13
if __name__ == '__main__':
src = cv2.imread("image\\0.png")
cv2.imshow("src", src)
grey = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) # 灰度图
ret, binary = cv2.threshold(grey, 127, 255, cv2.THRESH_BINARY) # 二值化
counters, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) # 获取轮廓
mask = np.zeros(src.shape, np.uint8) # 按照原图大小创建掩模
mask = cv2.drawContours(mask, counters, -1, (255, 255, 255), -1) # 给掩模赋值为前景图的实心轮廓
cv2.imshow("mask", mask)
loc = cv2.bitwise_and(src, mask)
cv2.imshow("result", loc)
cv2.waitKey()
cv2.destroyAllWindows()

以上的代码只针对样例图像,并不普适

矩特征

矩的计算:moments()

moments() 函数会将计算得到的矩以字典形式返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import cv2 as cv

img = cv.imread("number.png")

gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 降噪
ret, thresh = cv.threshold(gray_img, 127, 255, 0)
# 寻找轮廓
contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)

cnt = contours[0]
# 获取图像矩
M = cv.moments(cnt)
print(M)

# 质心
cx = int(M['m10'] / M['m00'])
cy = int(M['m01'] / M['m00'])

print(f'质心为:[{cx}, {cy}]')

这时,我们取得了这个图像的矩,矩 M 中包含了很多轮廓的特征信息,除了示例中展示的质心的计算,还有如 M[‘m00’] 表示轮廓面积。

​ 在位置发生变化时,虽然轮廓的面积、周长等特征不变,但是更高阶的特征会随着位置的变化而发生变化。在很多情况下,我们希望比较不同位置的两个对象的一致性。解决这一问题的方法是引入中心矩。中心矩通过城去均值而获取平移不变性,因而能够比较不同位置的两个对象是否一致。很明显,中心矩具有的平移不变性,使它能够忽略两个对象的位置关系,帮助我们比较不同位置上两个对象的一致性

​ 除了考虑平移不变性外,我们还会考虑经过缩放后大小不一致的对象的一致性。也就是说,我们希望图像在缩放前后能够拥有一个稳定的特征值。也就是说 ,让图像在缩放前后具有同样的特征值。显然,中心矩不具有这个属性。例如,两个形状一致、大小不一的对象,其中心矩是有差异的。

归一化中心矩通过除以物体总尺寸而获得缩放不变性。它通过上述计算提取对象的归一化中心矩属性值,该属性值不仅具有平移不变性, 还具有缩放不变性。在OpenCV中, 函数cv2.moments()会同时计算上述空间矩、中心矩和归一化中心矩。

Hu矩

​ Hu矩是归一化中心矩的线性组合。Hu矩在图像旋转、缩放 、平移等操作后,仍能保持矩的不变性,所以经常会使用Hu距来识别图像的特征。
​ 在OpenCV中,使用函数cv2.HuMoments()可以得到Hu距。该函数使用cv2.moments()函 数的返回值作为参数,返回7个Hu矩值。

​ 函数cv2.HuMoments()的语法格式为:
hu=cv2.HuMoments (m)
​ 式中返回值hu, 表示返回的Hu矩值;参数m, 是由函数cv2.moments()计算得到矩特征值。

最后一个参数直接置0即可。

轮廓拟合

正矩形和最小矩形

轮廓外接矩形分为正矩形和最小矩形。使用 cv2.boundingRect(cnt) 来获取轮廓的外接正矩形,它不考虑物体的旋转,所以该矩形的面积一般不会最小;使用 cv.minAreaRect(cnt) 可以获取轮廓的外接最小矩形。

两者的区别如上图,绿线代表的是外接正矩形,红线代表的是外接最小矩形,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import cv2 as cv
import numpy as np

img = cv.imread("number.png")

gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 降噪
ret, thresh = cv.threshold(gray_img, 127, 255, 0)
# 寻找轮廓
contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)

cnt = contours[0]

# 外接正矩形
x, y, w, h = cv.boundingRect(cnt)
cv.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)

# 外接最小矩形
min_rect = cv.minAreaRect(cnt)
print(min_rect)

box = cv.boxPoints(min_rect)
box = np.int0(box)
cv.drawContours(img, [box], 0, (0, 0, 255), 2)

cv.imshow("draw", img)

cv.waitKey(0)
cv.destroyAllWindows()

boundingRect(cnt) 函数的返回值包含四个值,矩形框左上角的坐标 (x, y) 、宽度 w 和高度 h 。

minAreaRect(cnt) 函数的返回值中还包含旋转信息,返回值信息为包括中心点坐标 (x,y),宽高 (w, h) 和旋转角度。

参数cnt可以是灰度图或者轮廓。

最小包围圆

center,radius = cv2.minEnclosingCircle(points)

  • center是最小包围圆的中心
  • radius是半径
  • points是轮廓

最优拟合椭圆

retval = cv2.fitEllipse(points)

众所周知每个椭圆都有相应的外接矩形,返回值retval包含外接矩形的质心、宽、高、旋转角度等参数信息。

最优拟合直线

最小外包三角形

retval,triangle = cv2.minEnclosingTriangle(points)

  • retval:最小外包三角形的面积
  • triangle:最小外包三角形的三个顶点的集合

轮廓近似(逼近多边形)

根据我们指定的精度,它可以将轮廓形状近似为顶点数量较少的其他形状。它是由 Douglas-Peucker 算法实现的。

OpenCV 提供的函数是 approxPolyDP(cnt, epsilon, close)

  • 第一个参数是轮廓。

  • 第二个参数 epsilon 用于轮廓近似的精度,表示原始轮廓与其近似轮廓的最大距离,值越小,近似轮廓越拟合原轮廓。

  • 第三个参数指定近似轮廓是否是闭合的,布尔类型。

具体用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import cv2 as cv

img = cv.imread("number.png")

gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 降噪
ret, thresh = cv.threshold(gray_img, 127, 255, 0)
# 寻找轮廓
contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)

cnt = contours[0]

# 计算 epsilon ,按照周长百分比进行计算,分别取周长 1% 和 10%
epsilon_1 = 0.1 * cv.arcLength(cnt, True)
epsilon_2 = 0.01 * cv.arcLength(cnt, True)

# 进行多边形逼近
approx_1 = cv.approxPolyDP(cnt, epsilon_1, True)
approx_2 = cv.approxPolyDP(cnt, epsilon_2, True)

# 画出多边形
image_1 = cv.cvtColor(gray_img, cv.COLOR_GRAY2BGR)
image_2 = cv.cvtColor(gray_img, cv.COLOR_GRAY2BGR)

cv.polylines(image_1, [approx_1], True, (0, 0, 255), 2)
cv.polylines(image_2, [approx_2], True, (0, 0, 255), 2)
#这里的绘制函数会在后面讲到
#可以用drawContours()代替

cv.imshow("image_1", image_1)
cv.imshow("image_2", image_2)
cv.waitKey(0)
cv.destroyAllWindows()

第一张图是 epsilon 为原始轮廓周长的 10% 时的近似轮廓,第二张图中绿线就是 epsilon 为原始轮廓周长的 1% 时的近似轮廓。

现在使用的图像都是仅有一个轮廓的图像,处理的轮廓都是contours[0]。如果处理的原图像中有多个轮廓,则需要注意控制轮廓的索引,即 contours[i]中的 i 值,使其指向特定的轮廓

凸包

凸包外观看起来与轮廓逼近相似,只不过它是物体最外层的「凸」多边形。

如下图,红色的部分为手掌的凸包,双箭头部分表示凸缺陷(Convexity Defects),凸缺陷常用来进行手势识别等。

函数:

hull = cv2.convexHull(points[,clockwise[,returnPoints]])

  • clockwise:布尔型值。 该值为True时,凸包角点将按顺时针方向排列;该值为False时, 则以逆时针方向排列凸包角点。
  • returnPoints:布尔型值。 默认值是True, 函数返回凸包角点的x/y轴坐标;当为 False时函数返回轮廓中凸包角点的索引。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import cv2 as cv

img = cv.imread("number.png")
gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 降噪
ret, thresh = cv.threshold(gray_img, 127, 255, 0)
# 寻找轮廓
contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
cnt = contours[0]
# 绘制轮廓
image = cv.cvtColor(gray_img, cv.COLOR_GRAY2BGR)
cv.drawContours(image, contours, -1, (0, 0 , 255), 2)

# 寻找凸包,得到凸包的角点
hull = cv.convexHull(cnt)

# 绘制凸包
cv.polylines(image, [hull], True, (0, 255, 0), 2)

cv.imshow("image", image)
cv.waitKey(0)
cv.destroyAllWindows()

还有一个函数,是可以用来判断图形是否凸形的:

1
print(cv.isContourConvex(hull)) # True

它的返回值是 True 或者 False 。

凸缺陷

convexityDefects = cv2.convexityDefects(contour,convexhull)

形状场景算法比较轮廓

作为归一化中心矩的替代算法。

计算形状场景距离

​ OpenCV提供了使用“距离”作为形状比较的度量标准。 这是因为形状之间的差异值和距离有相似之处, 比如二者都只能是零或者正数, 又比如当两个形状一模一样时距离值和差值都等于零。

​ OpenCV提供了函数cv2.createShapeContextDistanceExtractor (), 用于计算形状场景距离。其使用的”形状上下文算法”在计算距离时, 在每个点上附加一个“形状上下文“描述符, 让每个点都能够捕获剩余点相对于它的分布特征, 从而提供全局鉴别特征。

​ 有关该函数的更多理论知识, 可以参考学者 Belongie 等人 2002 年在IEEE Transactions on Pattern Analysis & Machine Intelligence上发表的论文Shape Matching and Object Recognition Using Shape Contexts。

函数cv2.createShapeContextDistanceExtractor()的语法格式为:

1
2
3
4
5
6
7
8
retval = cv2.createShapeContextDistanceExtractor ( 
[,nAngularBins[,
nRadialBins[,
innerRadius[,
outerRadius[,
iterations[,
comparer[,
transf armer]]]]]]])

式中的返回值为retval, 返回结果是ShapeDistanceExtractor类型的变量
该结果可以通过函数cv2.ShapeDistanceExtractor.computeDistance()计算两个不同形状之间的距离。 此函数的语法格式为:

d = cv2.ShapeDistanceExtractor.computeDistance (contour1,contour2) 式中, coutour1和coutour2是不同的轮廓。
函数cv2.createShapeContextDistanceExtractor()的参数都是可选参数:

  • nAngularBins: 为形状匹配中使用的形状上下文描述符建立的角容器的数星。

  • nRadialBins: 为形状匹配中使用的形状上下文描述符建立的径向容器的数星。

  • innerRadius:形状上下文描述符的内半径。

  • outerRadius:形状上下文描述符的外半径。

  • iterations: 迭代次数。

  • comparer:直方图代价提取算子。 该函数使用了直方图代价提取仿函数, 可以直接采用直方图代价提取仿函数的算子作为参数。

  • transformer: 形状变换参数。

计算Hausdorff距离

直方图处理

参考

图像灰度直方图是什么鬼?直方图是都是由横纵坐标组成的,而图像直方图的横坐标 X 轴上表示的是像素点的灰度值(不总是从 0 到 255 的范围),在纵坐标 Y 轴上表示的相应像素数。所以,直方图是可以对整幅图的灰度分布进行整体了解的图示,通过直方图我们可以对图像的对比度、亮度和灰度分布等有一个直观了解。从统计的角度讲,直方图是图像内灰度值的统计特性与图像灰度值之间的函数。

上面这张图来自官方网站,在这张图中,我们可以得到如下信息:

  • 左侧区域显示图像中较暗像素的数量(左侧的灰度级更趋近于 0 )。
  • 右侧区域则显示明亮像素的数量(右侧的灰度级更趋近于 255)。
  • 暗区域多于亮区域,而中间调的数量(中间值的像素值,例如127附近)则非常少。

直方图的绘制

我们需要了解一些与直方图有关的术语。

BINS:如果我们不需要分别找到所有像素值的像素数,而是找到像素值间隔中的像素数怎么办? 例如,您需要找到介于0到15之间的像素数,然后找到16到31之间,…,240到255之间的像素数。只需要16个值即可表示直方图。这就是在OpenCV教程中有关直方图的示例中显示的内容。

因此,我们要做的就是将整个直方图分成16个子部分,每个子部分的值就是其中所有像素数的总和。 每个子部分都称为“ BIN”。在第一种情况下,bin的数量为256个(每个像素一个),而在第二种情况下,bin的数量仅为16个。BINS由OpenCV文档中的histSize术语表示。

DIMS:这是我们为其收集数据的参数的数量。在这种情况下,我们仅收集关于强度值的一件事的数据。所以这里是1。

RANGE:这是您要测量的强度值的范围。通常,它是[0,256],即所有强度值。

使用 Matplotlib 绘图

Matplotlib 带有一个强大的直方图绘图功能:matplotlib.pyplot.hist() ,这个方法可以直接找到直方图进行绘制。

在看示例代码之前,有两个参数需要先介绍下:

  • 数据源:数据源必须是一维数组,通常需要通过函数 ravel() 拉直图像,而函数 ravel() 的作用是将多维数组降为一维数组。
  • 像素级:一般是 256 ,表示 [0, 255] 。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
import cv2 as cv
import matplotlib.pyplot as plt

img = cv.imread("maliao.jpg")

cv.imshow("img", img)
cv.waitKey(0)
cv.destroyAllWindows()

plt.hist(img.ravel(), 256, [0, 256])
plt.show()

输出结果:

当然,我们除了可以绘制灰度直方图以外,还可以绘制出 r,g,b 不同通道的直方图,可以看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import cv2 as cv
import matplotlib.pyplot as plt

img = cv.imread("tiankong.jpg")
color = ('b', 'g', 'r')

cv.imshow("img", img)
cv.waitKey(0)
cv.destroyAllWindows()

for i, col in enumerate(color):
histr = cv.calcHist([img], [i], None, [256], [0, 256])
plt.plot(histr, color = col)
plt.xlim([0, 256])
plt.show()

使用opencv绘制

直方图均衡化

一副效果好的图像通常在直方图上的分布比较均匀,直方图均衡化就是用来改善图像的全局亮度和对比度。

函数resultImg= cv.equalizeHist(src),src必须是单通道图像,所以:

  • 灰度图均衡,直接使用 equalizeHist() 函数。
  • 彩色图均衡,分别在不同的通道均衡后合并。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import cv2 as cv
import numpy as np

if __name__ == '__main__':
img = cv.imread("image\\0.png")
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# 灰度图均衡化
equ = cv.equalizeHist(gray)
# 竖直拼接原图和均衡图
result1 = np.vstack((gray, equ))
cv.imshow("grey",result1)

# 彩色图像均衡化,需要分解通道 对每一个通道均衡化
(b, g, r) = cv.split(img)
bH = cv.equalizeHist(b)
gH = cv.equalizeHist(g)
rH = cv.equalizeHist(r)
# 合并每一个通道
equ2 = cv.merge((bH, gH, rH))
# 竖直拼接原图和均衡图
result2 = np.vstack((img, equ2))
cv.imshow("rgb",result2)

cv.waitKey()
cv.destroyAllWindows()

原理

直方图均衡化的算法主要包括两个步骤:

(1) 计算累计直方图。
(2) 对累计直方图进行区间转换

在此基础上, 再利用人眼视觉达到直方图均衡化的目的。

自适应直方图均衡化

是指在每一个小区域内(默认 8×8 )进行直方图均衡化。当然,如果有噪点的话,噪点会被放大,需要对小区域内的对比度进行了限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import cv2 as cv
import numpy as np

if __name__ == '__main__':
img = cv.imread("image\\0.png",0)

# 全局直方图均衡
equ = cv.equalizeHist(img)

# 自适应直方图均衡
clahe = cv.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
cl1 = clahe.apply(img)

# 水平拼接三张图像
result = np.vstack((img, equ, cl1))
cv.imshow("result",result)

cv.waitKey()
cv.destroyAllWindows()

二维直方图

对于一维直方图,我们从BGR转换为灰度,绘制的是灰度的比例关系(占比关系),对于二维直方图,我们需要将图像从BGR转换为HSV。我们计算并绘制的一维直方图,之所以称为一维,是因为我们仅考虑一个特征,即像素的灰度强度值。 但是在二维直方图中,我们要考虑两个特征。 通常,它用于颜色直方图,其中两个特征是每个像素的色相和饱和度值

对于二维直方图,其参数将进行如下修改:

  • channel = [0,1],因为我们需要同时处理H和S平面。
  • bins = [180,256],对于H平面为180,对于S平面为256。
  • range = [0,180,0,256] ,色相值介于0和180之间,饱和度介于0和256之间。

也就是:

1
2
3
4
5
import numpy as np
import cv2 as cv
img = cv.imread('home.jpg')
hsv = cv.cvtColor(img,cv.COLOR_BGR2HSV)
hist = cv.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])

X轴显示S值,Y轴显示色相。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import cv2 as cv
import matplotlib.pyplot as plt

if __name__ == '__main__':
img = cv.imread("image\\2.png")

hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV)
hist = cv.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
plt.imshow(hist, interpolation='nearest')
plt.show()

cv.waitKey()
cv.destroyAllWindows()

直方图反投影

理论

这是由Michael J. SwainDana H. Ballard在他们的论文《通过颜色直方图索引》中提出的。

用简单的话说是什么意思它用于图像分割或在图像中查找感兴趣的对象。简而言之,它创建的图像大小与输入图像相同(但只有一个通道),其中每个像素对应于该像素属于我们物体的概率。用更简单的话来说,与其余部分相比,输出图像将在可能有对象的区域具有更多的白色值。好吧,这是一个直观的解释。(我无法使其更简单)。直方图反投影与camshift算法等配合使用。

我们该怎么做呢?我们创建一个图像的直方图,其中包含我们感兴趣的对象(在我们的示例中是背景,离开播放器等)。对象应尽可能填充图像以获得更好的效果。而且颜色直方图比灰度直方图更可取,因为对象的颜色对比灰度强度是定义对象的好方法。然后,我们将该直方图“反投影”到需要找到对象的测试图像上,换句话说,我们计算出属于背景的每个像素的概率并将其显示出来。在适当的阈值下产生的输出使我们仅获得背景。

Numpy中的算法

  1. 首先,我们需要计算我们要查找的对象(使其为“ M”)和要搜索的图像(使其为“ I”)的颜色直方图。
1
2
3
4
5
6
7
8
9
10
11
12
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
#roi是我们需要找到的对象或对象区域
roi = cv.imread('rose_red.png')
hsv = cv.cvtColor(roi,cv.COLOR_BGR2HSV)
#目标是我们搜索的图像
target = cv.imread('rose.png')
hsvt = cv.cvtColor(target,cv.COLOR_BGR2HSV)
# 使用calcHist查找直方图。也可以使用np.histogram2d完成
M = cv.calcHist([hsv],[0, 1], None, [180, 256], [0, 180, 0, 256] )
I = cv.calcHist([hsvt],[0, 1], None, [180, 256], [0, 180, 0, 256] )
  1. 求出比值R=MIR = \dfrac{M}{I}。然后反向投影R,即使用R作为调色板,并以每个像素作为其对应的目标概率创建一个新图像。即B(x,y) = R[h(x,y),s(x,y)] 其中h是色调,s是像素在(x,y)的饱和度。之后,应用条件B(x,y)=min[B(x,y),1]B(x,y)=min[B(x,y),1]
1
2
3
4
h,s,v = cv.split(hsvt)
B = R[h.ravel(),s.ravel()]
B = np.minimum(B,1)
B = B.reshape(hsvt.shape[:2])
  1. 现在对圆盘应用卷积,B=DBB=D*B,其中D是圆盘内核。
1
2
3
4
disc = cv.getStructuringElement(cv.MORPH_ELLIPSE,(5,5))
cv.filter2D(B,-1,disc,B)
B = np.uint8(B)
cv.normalize(B,B,0,255,cv.NORM_MINMAX)
  1. 现在最大强度的位置给了我们物体的位置。如果我们期望图像中有一个区域,则对合适的值进行阈值处理将获得不错的结果。
1
ret,thresh = cv.threshold(B,50,255,0) 

比如,我通过一个天空的图像片段查找图像中所有的天空:(图中红框是我提供的ROI)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import numpy as np
import cv2 as cv

if __name__ == '__main__':
#查找样例
roi = cv.imread("image\\1-1.png")
hsv = cv.cvtColor(roi, cv.COLOR_BGR2HSV)

#全图
target = cv.imread("image\\1.png")
hsvt = cv.cvtColor(target, cv.COLOR_BGR2HSV)
M = cv.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
I = cv.calcHist([hsvt], [0, 1], None, [180, 256], [0, 180, 0, 256])
R = M/I

h, s, v = cv.split(hsvt)
B = R[h.ravel(), s.ravel()]
B = np.minimum(B, 1)
B = B.reshape(hsvt.shape[:2])

disc = cv.getStructuringElement(cv.MORPH_ELLIPSE, (5, 5))
cv.filter2D(B, -1, disc, B)
B = np.uint8(B)
cv.normalize(B, B, 0, 255, cv.NORM_MINMAX)

#阈值处理
ret, thresh = cv.threshold(B, 50, 255, 0)

#构造掩模
thresh = cv.merge((thresh, thresh, thresh))
res = cv.bitwise_and(target, thresh)
res = np.vstack((target, thresh, res))
cv.imshow("result",res)

cv.waitKey()
cv.destroyAllWindows()

OpenCV的反投影

OpenCV提供了一个内建的函数cv.calcBackProject()。它的参数几乎与cv.calchist()函数相同。它的一个参数是直方图,也就是物体的直方图,我们必须找到它。另外,在传递给backproject函数之前,应该对对象直方图进行归一化。它返回概率图像。然后我们用圆盘内核对图像进行卷积并应用阈值。下面是我的代码和结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import numpy as np
import cv2 as cv

if __name__ == '__main__':
roi = cv.imread("image\\1-1.png")
hsv = cv.cvtColor(roi, cv.COLOR_BGR2HSV)

target = cv.imread("image\\1.png")
hsvt = cv.cvtColor(target, cv.COLOR_BGR2HSV)
# 计算对象的直方图
roihist = cv.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
# 直方图归一化并利用反传算法
cv.normalize(roihist, roihist, 0, 255, cv.NORM_MINMAX)
dst = cv.calcBackProject([hsvt], [0, 1], roihist, [0, 180, 0, 256], 1)
# 用圆盘进行卷积
disc = cv.getStructuringElement(cv.MORPH_ELLIPSE, (5, 5))
cv.filter2D(dst, -1, disc, dst)
# 应用阈值作与操作
ret, thresh = cv.threshold(dst, 50, 255, 0)
thresh = cv.merge((thresh, thresh, thresh))
res = cv.bitwise_and(target, thresh)
res = np.vstack((target, thresh, res))
cv.imshow("result",res)

cv.waitKey()
cv.destroyAllWindows()

补充:pyplot.subplot()

模块 matplotlib.pyplot 提供了函数matplotlib.pyplot.subplot()用来向当前窗口内添加一个子窗口对象。 该函数的语法格式为:

matplotlib. pyplot.subplot (nrows,ncols,index)

式中:

  • nrows为行数。
  • ncols为列数。
  • index为窗口序号。

例如,subplot(2,3,4)表示在当前的两行三列的窗口的第4个位置上,添加1个子窗口

傅里叶变换

原理

​ 图像处理一般分为空间域处理和频率域处理。

空间域处理是直接对图像内的像素进行处理。 空间域处理主要划分为灰度变换和空间滤波两种形式。 灰度变换是对图像内的单个像素进行处理, 比如调节对比度和处理闯值等。空间滤波涉及图像质星的改变, 例如图像平滑处理。 空间域处理的计算简单方便,运算速度更快。
频率域处理是先将图像变换到频率域, 然后在频率域对图像进行处理, 最后再通过反变换将图像从频率域变换到空间域。 傅里叶变换是应用最广泛的一种频域变换, 它能够将图像从空间域变换到频率域, 而逆傅里叶变换能够将频率域信息变换到空间域内。 傅里叶变换在图像处理领域内有着非常重要的作用。

​ 在图像处理过程中,傅里叶变换就是将图像分解为正弦分量和余弦分量两部分,即将图像从空间域转换到频率域(以下简称频域)。数字图像经过傅里叶变换后,得到的频域值是复数。因此,显示傅里叶变换的结果需要使用实数图像 (real image) 加虚数图像 (complex image)或者幅度图像(magnitude image) 加相位图像 (phase image) 的形式。

​ 因为幅度图像包含了原图像中我们所需要的大部分信息,所以在图像处理过程中,通常仅使用幅度图像。当然,如果希望先在频域内对图像进行处理,再通过逆傅里叶变换得到修改后的空域图像,就必须同时保留幅度图像和相位图像。

​ 对图像进行傅里叶变换后,我们会得到图像中的低频和高频信息。低频信息对应图像内变化缓慢的灰度分量。 高频信息对应图像内变化越来越快的灰度分量,是由灰度的尖锐过渡造成的。例如,在一幅大草原的图像中有一头狮子,低频信息就对应着广袤的颜色趋于一致的草原等细节信息,而高频信息则对应着狮子的轮廓等各种边缘及噪声信息。

​ 对于不同频段的信息,滤波器能够允许一定频率的分量通过或者拒绝其通过,按照其作用方式可以划分为低通滤波器和高通滤波器。

  • 允许低频信号通过的滤波器称为低通滤波器。低通滤波器使高频信号衰减而对低频信号放行,会使图像变模糊。

  • 允许高频信号通过的滤波器称为高通滤波器。高通滤波器使低频信号衰减而让高频信号通过,将增强图像中尖锐的细节, 但是会导致图像的对比度降低。

傅里叶变换可以将图像的高频信号和低频信号分离。那么就可以对傅里叶变换得到的高频信号和低频信号分别进行处理,例如高通滤波或者低通滤波。在对图像的高频或低频信号进行处理后,再进行逆傅里叶变换返回空域,就完成了对图像的频域处理。通过对图像的频域处理,可以实现图像增强、图像去噪、边缘检测、特征提取、压缩和加密等操作。

PS.什么是时域(空间域),什么是频域

从我们出生,我们看到的世界都以时间贯穿,股票的走势、人的身高、汽车的轨迹都会随着时间发生改变。这种以时间作为参照来观察动态世界的方法我们称其为时域分析。而我们也想当然的认为,世间万物都在随着时间不停的改变,并且永远不会静止下来。但如果我告诉你,用另一种方法来观察世界的话,你会发现世界是永恒不变的,你会不会觉得我疯了?我没有疯,这个静止的世界就叫做频域

先举一个公式上并非很恰当,但意义上再贴切不过的例子:

在你的理解中,一段音乐是什么呢?是其在时域内的表现

这是我们对音乐最普遍的理解,一个随着时间变化的震动。但我相信对于乐器小能手们来说,音乐更直观的理解是这样的:是其在频域内的表现

在时域,我们观察到钢琴的琴弦一会上一会下的摆动,就如同一支股票的走势;而在频域,只有那一个永恒的音符。

从信号的角度来讲,时间方向是时域,因为信号是随时间变化的。但从图像的角度讲,这个方向应该称作空间的方向,也就是空间域,因为图像在空间的不同位置的表示是不一样的。

But what does frequency spectrum means in case of images?

The “mathematical equations” are important, so don’t skip them entirely. But the 2d FFT has an intuitive interpretation, too. For illustration, I’ve calculated the inverse FFT of a few sample images:

“数学方程式”很重要,所以不要完全跳过它们。但是2d FFT 也有一个直观的解释。为了说明,我已经计算了一些样本图像的反 FFT:

As you can see, only one pixel is set in the frequency domain. The result in the image domain (I’ve only displayed the real part) is a “rotated cosine pattern” (the imaginary part would be the corresponding sine).

If I set a different pixel in the frequency domain (at the left border):

正如你所看到的,只有一个像素是设置在频率域。图像域中的结果(我只显示了实部)是一个“旋转余弦图案”(虚部将是对应的正弦)。

如果我在频率域(左边框)设置不同的像素:

I get a different 2d frequency pattern.

If I set more than one pixel in the frequency domain:

我得到了一个不同的二维频率模式。

如果我在频率域设置多于一个像素:

you get the sum of two cosines.

So like a 1d wave, that can be represented as a sum of sines and cosines, any 2d image can be represented (loosely speaking) as a sum of “rotated sines and cosines”, as shown above.

when we take fft of a image in opencv, we get weird picture. What does this image denote?

It denotes the amplitudes and frequencies of the sines/cosines that, when added up, will give you the original image.

And what is its application?

There are really too many to name them all. Correlation and convolution can be calculated very efficiently using an FFT, but that’s more of an optimization, you don’t “look” at the FFT result for that. It’s used for image compression, because the high frequency components are usually just noise.

你会得到两个余弦的和。

就像一维波,可以表示为正弦和余弦的和,任何二维图像都可以表示为“旋转正弦和余弦”的和,如上所示。

当我们在 opencv 中对一幅图像进行傅立叶变换时,我们得到了一幅奇怪的图像,这幅图像代表了什么?

它表示正弦/余弦的振幅和频率,加起来就是原始图像。

它的应用是什么?

实在是太多了,无法一一列举。使用 FFT,相关性和卷积可以非常有效地计算,但这是一个更优化,FFT的结果并不以此为目的。因为高频部分通常只是噪音,所以它被用于图像压缩。

Numpy中的傅里叶变换

首先,我们将看到如何使用Numpy查找傅立叶变换。Numpy具有FFT软件包来执行此操作。np.fft.fft2()为我们提供了频率转换,它将是一个复杂的数组。它的第一个参数是输入图像,即灰度图像。第二个参数是可选的,它决定输出数组的大小。如果它大于输入图像的大小,则在计算FFT之前用零填充输入图像。如果小于输入图像,将裁切输入图像。如果未传递任何参数,则输出数组的大小将与输入的大小相同。

现在,一旦获得结果,零频率分量(DC分量)将位于左上角。如果要使其居中,则需要在两个方向上将结果都移动N2\dfrac{N}{2}。只需通过函数np.fft.fftshift()即可完成。(它更容易分析)。找到频率变换后,就可以找到幅度谱。

那么,对于这张图,越靠近中心,频率越低

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

if __name__ == '__main__':
img = cv.imread("image\\1.png",0)

f = np.fft.fft2(img)
fshift = np.fft.fftshift(f)
#对图像进行傅里叶变换后,得到的是一个复数数组。为了显示为图像,需要将它们的值调整到[0,255]的灰度空间内
magnitude_spectrum = 20 * np.log(np.abs(fshift))
plt.subplot(121), plt.imshow(img, cmap='gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(magnitude_spectrum, cmap='gray')
plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
plt.show()

cv.waitKey()
cv.destroyAllWindows()

高通滤波

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

if __name__ == '__main__':
img = cv.imread("image\\1.png", 0)

f = np.fft.fft2(img)
fshift = np.fft.fftshift(f)
rows, cols = img.shape
crow, ccol = int(rows / 2), int(cols / 2)
# 中间30*30的像素置为0
fshift[crow - 30:crow + 30, ccol - 30:ccol + 30] = 0
ishift = np.fft.ifft2(fshift)
iimg = np.abs(ishift)

plt.subplot(121), plt.imshow(img, cmap='gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(iimg, cmap='gray')
plt.title('iimg'), plt.xticks([]), plt.yticks([])
plt.show()

cv.waitKey()
cv.destroyAllWindows()

但这样的高通滤波在图像中会显示出一些波纹状结构,称为振铃效应。这是由我们用于遮罩的矩形窗口引起的。此掩码转换为正弦形状,从而导致此问题。因此,矩形窗口不用于过滤。更好的选择是高斯窗口。

OpenCV中的傅里叶变换

OpenCV为此提供了cv.dft()和cv.idft()函数。它返回与前一个相同的结果,但是有两个通道。第一个通道是结果的实部,第二个通道是结果的虚部。输入图像首先应转换为np.float32。我们来看看怎么做。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

if __name__ == '__main__':
img = cv.imread("image\\1.png", 0)

dft = cv.dft(np.float32(img), flags=cv.DFT_COMPLEX_OUTPUT)
dft_shift = np.fft.fftshift(dft)
magnitude_spectrum = 20 * np.log(cv.magnitude(dft_shift[:, :, 0], dft_shift[:, :, 1]))
plt.subplot(121), plt.imshow(img, cmap='gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(magnitude_spectrum, cmap='gray')
plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
plt.show()

cv.waitKey()
cv.destroyAllWindows()

低通滤波

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

if __name__ == '__main__':
img = cv.imread("image\\1.png", 0)

dft = cv.dft(np.float32(img), flags=cv.DFT_COMPLEX_OUTPUT)
dft_shift = np.fft.fftshift(dft)
magnitude_spectrum = 20 * np.log(cv.magnitude(dft_shift[:, :, 0], dft_shift[:, :, 1]))

rows, cols = img.shape
crow, ccol = int(rows / 2), int(cols / 2)
# 首先创建一个掩码,中心正方形为1,其余全为零
mask = np.zeros((rows, cols, 2), np.uint8)
mask[crow - 30:crow + 30, ccol - 30:ccol + 30] = 1
# 应用掩码和逆DFT
fshift = dft_shift * mask
f_ishift = np.fft.ifftshift(fshift)
img_back = cv.idft(f_ishift)
img_back = cv.magnitude(img_back[:, :, 0], img_back[:, :, 1])
plt.subplot(121), plt.imshow(img, cmap='gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(img_back, cmap='gray')
plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
plt.show()

cv.waitKey()
cv.destroyAllWindows()

踩坑

滤波时,crow, ccol = int(rows / 2), int(cols / 2)而不能用crow, ccol = rows / 2, cols / 2,后者会报错slice indices must be integers or None or have an __index__ method,因为除法返回的是浮点值,不能直接用于二维数组索引。

或者将/改为//

DFT的性能优化

对于某些数组尺寸,DFT的计算性能较好。当数组大小为2的幂时,速度最快。对于大小为2、3和5的乘积的数组,也可以非常有效地进行处理。因此,如果您担心代码的性能,可以在找到DFT之前将数组的大小修改为任何最佳大小(通过填充零)。对于OpenCV,您必须手动填充零。但是对于Numpy,您指定FFT计算的新大小,它将自动为您填充零。

那么如何找到最优的大小呢?OpenCV为此提供了一个函数,cv.getOptimalDFTSize()。它同时适用于cv.dft()和np.fft.fft2()。让我们使用IPython魔术命令timeit来检查它们的性能。

1
2
3
4
5
6
7
8
In [16]: img = cv.imread('messi5.jpg',0)
In [17]: rows,cols = img.shape
In [18]: print("{} {}".format(rows,cols))
342 548
In [19]: nrows = cv.getOptimalDFTSize(rows)
In [20]: ncols = cv.getOptimalDFTSize(cols)
In [21]: print("{} {}".format(nrows,ncols))
360 576

参见,将大小(342,548)修改为(360,576)。现在让我们用零填充(对于OpenCV),并找到其DFT计算性能。您可以通过创建一个新的零数组并将数据复制到其中来完成此操作,或者使用cv.copyMakeBorder()。

1
2
nimg = np.zeros((nrows,ncols))
nimg[:rows,:cols] = img

或者:

1
2
3
4
right = ncols - cols
bottom = nrows - rows
bordertype = cv.BORDER_CONSTANT #只是为了避免PDF文件中的行中断
nimg = cv.copyMakeBorder(img,0,bottom,0,right,bordertype, value = 0)

现在,我们计算Numpy函数的DFT性能比较:

1
2
3
4
In [22]: %timeit fft1 = np.fft.fft2(img)
10 loops, best of 3: 40.9 ms per loop
In [23]: %timeit fft2 = np.fft.fft2(img,[nrows,ncols])
100 loops, best of 3: 10.4 ms per loop

它显示了4倍的加速。现在,我们将尝试使用OpenCV函数。

1
2
3
4
In [24]: %timeit dft1= cv.dft(np.float32(img),flags=cv.DFT_COMPLEX_OUTPUT)
100 loops, best of 3: 13.5 ms per loop
In [27]: %timeit dft2= cv.dft(np.float32(nimg),flags=cv.DFT_COMPLEX_OUTPUT)
100 loops, best of 3: 3.11 ms per loop

它还显示了4倍的加速。您还可以看到OpenCV函数比Numpy函数快3倍左右。也可以对逆FFT进行测试,这留给您练习。

模板匹配

模板匹配是一种用于在较大图像中搜索和查找模板图像位置的方法。为此,OpenCV带有一个函数cv.matchTemplate()。 它只是将模板图像滑动到输入图像上(就像在2D卷积中一样),然后在模板图像下比较模板和输入图像的拼图。 OpenCV中实现了几种比较方法。(您可以检查文档以了解更多详细信息)。它返回一个灰度图像,其中每个像素表示该像素的邻域与模板匹配的程度

注意 如果使用cv.TM_SQDIFF作为比较方法,则最小值提供最佳匹配。

函数

在OpenCV内, 模板匹配是使用函数cv2.matchTemplate()实现的。 该函数的语法格式为:result=cv2.matchTemplate (image,templ,method[,mask])
其中:

  • image为原始图像, 必须是8位或者32位的浮点型图像。
  • templ为模板图像。 它的尺寸必须小于或等于原始图像, 并且与原始图像具有同样的类型
  • method为匹配方法。 该参数通过 TemplateMatchModes实现, 有6种可能的值。
  • mask 为模板图像掩模。它必须和模板图像 tempi 具有相同的类型和大小。通常情况下该值使用默认值即可。当前,该参数仅支持TM_SQDIFF和TM_CCORR_NORMED两个值。

如果输入图像的大小为(WxH),而模板图像的大小为(wxh)则输出图像的大小将为(W - w + 1,H - h + 1)

这是因为,在进行模板匹配时, 模板在原始图像内遍历。 在水平方向上:

  • 遍历的起始坐标是原始图像左数第1个像素值(序号从1开始)。

  • 最后一次比较是当模板图像位于原始图像的最右侧时, 此时其左上角像素点所在的位置是Ww+1W-w+1

因此, 返回值result在水平方向上的大小是Ww+1W-w+1(水平方向上的比较次数)。竖直方向同理。


得到结果后,可以使用cv.minMaxLoc()函数查找最大/最小值在哪。将其作为矩形的左上角,并以(w,h)作为矩形的宽度和高度。该矩形是您模板的区域。

这里需要注意的是 , 函数cv2.matchTemplate()通过参数method来决定使用不同的查找方法。对千不同的查找方法, 返回值result具有不同的含义。 例如:

  • method的值为cv2.TM_SQDIFFcv2.TM_SQDIFF_NORMED时 , result值为0表示匹配度最小,值越大 , 表示匹配度越差。

  • method 的值为 cv2.TM_CCORRcv2.TM_CCORR_NORMEDcv2.TM_CCOEFFcv2.TM_CCOEFF _NORMED时, result的值越小表示匹配度越差, 值越大表示匹配度越好。

从上述分析可以看出 , 查找方法不同, 结果的判定方式也不同。 在查找最佳匹配时 , 首先要确定使用的是何种method, 然后再确定到底是查找最大值, 还是查找最小值

查找最值(极值)与最值所在的位置, 可以使用cv2.minMaxLoc() 函数实现。 该函数语法格式如下:

minVal,maxVal,minLoc,maxLoc=cv2.minMaxLoc (src[,mask])

其中:

  • src单通道数组
  • minVal为返回的最小值, 如果没有最小值, 则可以是NULL(空值)。
  • maxVal为返回的最大值, 如果没有最小值, 则可以是NULL。
  • minLoc为最大值的位置, 如果没有最大值, 则可以是NULL。
  • maxLoc为最大值的位置, 如果没有最大值, 则可以是NULL。

单模板匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

if __name__ == '__main__':
img = cv.imread("image\\0.png", 0)
img2 = img.copy()
target = cv.imread('image\\target-0.png', 0)
w, h = target.shape[::-1]
# 列表中所有的6种比较方法
methods = ['cv.TM_CCOEFF', 'cv.TM_CCOEFF_NORMED', 'cv.TM_CCORR',
'cv.TM_CCORR_NORMED', 'cv.TM_SQDIFF', 'cv.TM_SQDIFF_NORMED']
l = 1
for meth in methods:
img = img2.copy()
method = eval(meth)
# 应用模板匹配
res = cv.matchTemplate(img, target, method)
min_val, max_val, min_loc, max_loc = cv.minMaxLoc(res)
# 如果方法是TM_SQDIFF或TM_SQDIFF_NORMED,则取最小值
if method in [cv.TM_SQDIFF, cv.TM_SQDIFF_NORMED]:
top_left = min_loc
else:
top_left = max_loc
bottom_right = (top_left[0] + w, top_left[1] + h)
cv.rectangle(img, top_left, bottom_right, 0, 2)
plt.subplot(6, 2, l), plt.imshow(res, cmap='gray')
plt.title('Matching Result'), plt.xticks([]), plt.yticks([])
plt.subplot(6, 2, l + 1), plt.imshow(img, cmap='gray')
plt.title(meth), plt.xticks([]), plt.yticks([])
l = l + 2

plt.show()
cv.waitKey()
cv.destroyAllWindows()

但显然,相关系数匹配在这张图片中出了问题,而且其他图片也有类似的问题

多模板匹配

在前面的例子中,我们搜索的子图在整个输入图像内仅出现了一次。但是,有些情况下,要搜索的模板图像很可能在输入图像内出现了多次,这时就需要找出多个匹配结果。而函数cv2.minMaxLoc()仅仅能够找出最值,无法给出所有匹配区域的位置信息。所以,要想匹配多个结果,使用函数cv2.minMaxLoc()是无法实现的需要利用阈值进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

if __name__ == '__main__':
img_rgb = cv.imread('image\\4.png')
img_gray = cv.cvtColor(img_rgb, cv.COLOR_BGR2GRAY)
target = cv.imread('image\\target-4.png', 0)
w, h = target.shape[::-1]
res = cv.matchTemplate(img_gray, target, cv.TM_CCOEFF_NORMED)

#这里的阈值是自己定的
threshold = 0.8
loc = np.where(res >= threshold)
#函数zip()用可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。
for pt in zip(*loc[::-1]):
cv.rectangle(img_rgb, pt, (pt[0] + w, pt[1] + h), (0, 0, 255), 2)
cv.imwrite('res.png', img_rgb)

cv.waitKey()
cv.destroyAllWindows()

注意使用函数numpy.where()在函数cv2.matchTemplate()的输出值中查找指定值, 得到的形式"(行号, 列号)"的位置索引。但是,函数cv2.rectangle()中用于指定顶点的参数所使用的是形式为"(列号, 行号)"的位置索引。所以,在使用函数cv2.rectangle()绘制矩形前 , 要先将函数numpy.where()得到的位置索引做“行列互换"。可以使用如下语句实现loc内行列位置的互换:

loc[::-1],结果如下:

注意到, 本来在函数cv2.rectangle()中设置的边界宽度为2, 但实际上标记出来的宽度远远大于2。这是因为在当前的区域内, 存在多个大于当前指定阈值0.8的情况,所以将它们都做了标记。这样,多个宽度为2的矩形就合在了一起,显得边界比较粗。可以尝试修改阈值,调整宽度,观察不同的演示效果。

特征匹配

本部分可以认为是上一部分地进阶。

​ 大多数人都会玩拼图游戏。你会得到很多小图像,需要正确组装它们以形成大的真实图像。问题是,你怎么做?将相同的理论投影到计算机程序上,以便计算机可以玩拼图游戏呢?如果计算机可以玩拼图游戏,为什么我们不能给计算机提供很多自然风光的真实图像,并告诉计算机将所有这些图像拼接成一个大图像呢?如果计算机可以将多个自然图像缝合在一起,那么如何给建筑物或任何结构提供大量图片并告诉计算机从中创建3D模型呢?

​ 好了,问题和想象力还在继续。但这全都取决于最基本的问题:你如何玩拼图游戏?你如何将许多被扰的图像片段排列成一个大的单张图像?你如何将许多自然图像拼接到一张图像上?

​ 答案是,我们正在寻找独特的,易于跟踪和比较的特定模板或特定特征。如果我们对这种特征进行定义,可能会发现很难用语言来表达它,但是我们知道它们是什么。如果有人要求你指出一项可以在多张图像中进行比较的良好特征,则可以指出其中一项。这就是为什么即使是小孩也可以玩这些游戏的原因。我们在图像中搜索这些特征,找到它们,在其他图像中寻找相同的特征并将它们对齐。仅此而已。(在拼图游戏中,我们更多地研究了不同图像的连续性)。所有这些属性都是我们固有的。

​ 因此,我们的一个基本问题扩展到更多,但变得更加具体。这些特征是什么?(答案对于计算机也应该是可以理解的。)

​ 很难说人类如何发现这些特征。这已经在我们的大脑中进行了编码。但是,如果我们深入研究某些图片并搜索不同的模板,我们会发现一些有趣的东西。例如,看以下的图片:

​ 图像非常简单。在图像的顶部,给出了六个小图像块。你的问题是在原始图像中找到这些补丁的确切位置。你可以找到多少正确的结果?

​ A和B是平坦的表面,它们散布在很多区域上。很难找到这些补丁的确切位置。

​ C和D更简单。它们是建筑物的边缘。你可以找到一个大概的位置,但是准确的位置仍然很困难。这是因为沿着边缘的每个地方的图案都是相同的。但是,在边缘,情况有所不同。因此,与平坦区域相比,边缘是更好的特征,但不够好(在拼图游戏中比较边缘的连续性很好)。

​ 最后,E和F是建筑物的某些角落。而且很容易找到它们。因为在拐角处,无论将此修补程序移动到何处,它的外观都将有所不同。因此,它们可以被视为很好的特征。因此,现在我们进入更简单(且被广泛使用的图像)以更好地理解。

​ 就像上面一样,蓝色补丁是平坦区域,很难找到和跟踪。无论你将蓝色补丁移到何处,它看起来都一样。黑色补丁有一个边缘。如果你沿垂直方向(即沿渐变)移动它,则它会发生变化。沿着边缘(平行于边缘)移动,看起来相同。对于红色补丁,这是一个角落。无论你将补丁移动到何处,它看起来都不同,这意味着它是唯一的。因此,基本上,拐点被认为是图像中的良好特征。(不仅是角落,在某些情况下,斑点也被认为是不错的功能)。

​ 因此,现在我们回答了我们的问题,“这些特征是什么?”。但是出现了下一个问题。我们如何找到它们?还是我们如何找到角落?我们以一种直观的方式回答了这一问题,即寻找图像中在其周围所有区域中移动(少量)变化最大的区域。在接下来的章节中,这将被投影到计算机语言中。因此,找到这些图像特征称为特征检测。

​ 我们在图像中找到了特征。找到它之后,你应该能够在其他图像中找到相同的图像。怎么做?我们围绕该特征采取一个区域,我们用自己的语言解释它,例如“上部是蓝天,下部是建筑物的区域,在建筑物上有玻璃等”,而你在另一个建筑物中搜索相同的区域图片。基本上,你是在描述特征。同样,计算机还应该描述特征周围的区域,以便可以在其他图像中找到它。所谓的描述称为特征描述。获得特征及其描述后,你可以在所有图像中找到相同的功能并将它们对齐,缝合在一起或进行所需的操作。

​ 因此,在此模块中,我们正在寻找OpenCV中的不同算法来查找功能,对其进行描述,进行匹配等。

哈里斯角检测

原理

[论文原文](https://zwn2001.github.io/2022/02/26/A Combined Corner and Edge Detector.pdf)

在上一部分中,我们看到角是图像中各个方向上强度变化很大的区域。Chris Harris和Mike Stephens在1988年的论文《组合式拐角和边缘检测器》中做了一次尝试找到这些拐角的尝试,所以现在将其称为哈里斯拐角检测器。他把这个简单的想法变成了数学形式。它基本上找到了(uv)(u,v)在所有方向上位移的强度差异。表示如下:

可以用如下图来表示:

函数

cv.cornerHarris(img,blockSize,ksize,k)

其参数为:

  • img 输入图像,应为灰度和float32类型
  • blockSize 是拐角检测考虑的邻域大小
  • ksize 使用的Sobel导数的光圈参数。
  • k 等式中的哈里斯检测器自由参数。
  • dst 返回值,灰度图像

corners = cv2.cornerSubPix(gray, np.float32(centroids), (5, 5), (-1, -1), criteria)
具有亚像素精度的角点:有时可能需要以最大的精度找到角点。OpenCV附带了一个函数cv2.cornerSubPix(),它可以进一步细化以亚像素精度检测到的角点。

样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import numpy as np
import cv2 as cv

if __name__ == '__main__':
img = cv.imread('image\\0.jpg')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 寻找哈里斯角
gray = np.float32(gray)
dst = cv.cornerHarris(gray, 2, 3, 0.04)
dst = cv.dilate(dst, None)#膨胀
ret, dst = cv.threshold(dst, 0.01 * dst.max(), 255, 0)
cv.imshow("dst", dst)
dst = np.uint8(dst)
# 寻找质心
ret1, labels, stats, centroids = cv.connectedComponentsWithStats(dst)
# 定义停止和完善拐角的条件
criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 100, 0.001)
corners = cv.cornerSubPix(gray, np.float32(centroids), (5, 5), (-1, -1), criteria)
# 绘制角点和细化的亚像素点
res = np.hstack((centroids, corners))
res = np.int0(res)
# Harris角点用红色像素标记,精细角点用绿色像素标记
img[res[:, 1], res[:, 0]] = [0, 0, 255]
img[res[:, 3], res[:, 2]] = [0, 255, 0]

gray = cv.cvtColor(gray, cv.COLOR_GRAY2BGR)
gray[res[:, 1], res[:, 0]] = [0, 0, 255]
gray[res[:, 3], res[:, 2]] = [0, 255, 0]
cv.imshow("gray", gray)
cv.imshow('cornerSubPix res', img)


cv.waitKey(0)
cv.destroyAllWindows()

Shi-tomas拐角检测器和益于跟踪的特征

原理

在上一章中,我们看到了Harris Corner Detector。1994年下半年,J。Shi和C. Tomasi在他们的论文《有益于跟踪的特征》中做了一个小修改,与Harris Harris Detector相比,显示了更好的结果。哈里斯角落探测器的计分功能由下式给出:

R=λ1λ2k(λ1+λ2)2R=λ_1λ_2−k(λ_1+λ_2)^2

取而代之的是,史托马西提出:

R=min(λ1,λ2)R=min(λ_1,λ_2)

如果大于阈值,则将其视为拐角。如果像在Harris Corner Detector中那样在λ1λ2λ_1−λ_2空间中绘制它,则会得到如下图像:

从图中可以看到,只有当λ1λ_1λ2λ_2大于最小值λminλ_{min}时,才将其视为拐角(绿色区域)。

样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

if __name__ == '__main__':
img = cv.imread('image\\5.png')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
corners = cv.goodFeaturesToTrack(gray, 25, 0.01, 10)
corners = np.int0(corners)
for i in corners:
x, y = i.ravel()
cv.circle(img, (x, y), 3, 255, -1)
plt.imshow(img), plt.show()

cv.waitKey(0)
cv.destroyAllWindows()

此功能更适合跟踪。我们将看到使用它的时机。

SIFT尺度不变特征变换

[论文原文](https://zwn2001.github.io/2022/02/26/Distinctive Image Features from Scale-Invariant Keypoints.pdf)

在前两章中,我们看到了一些像Harris这样的拐角检测器。它们是旋转不变的,这意味着即使图像旋转了,我们也可以找到相同的角。很明显,因为转角在旋转的图像中也仍然是转角。但是缩放呢?如果缩放图像,则拐角可能不是角。例如,检查下面的简单图像。在同一窗口中放大小窗口中小图像中的拐角时,该角是平坦的。因此,Harris拐角不是尺度不变的。

因此,在2004年,不列颠哥伦比亚大学的D.Lowe在他的论文《尺度不变关键点中的独特图像特征》中提出了一种新算法,即尺度不变特征变换(SIFT),该算法提取关键点并计算其描述算符。SIFT算法主要包括四个步骤。 我们将一一看到它们。

霍夫变换

霍夫变换是一种在图像中寻找直线、圆形以及其他简单形状的方法。霍夫变换采用类似于投票的方式来获取当前图像内的形状集合,该变换由 Paul Hough(霍夫)于1962年首次提出。最初的霍夫变换只能用于检测直线,经过发展后,霍夫变换不仅能够识别直线,还能识别其他简单的图形结构,常见的有圆、椭圆等。
本章主要介绍霍夫直线变换和霍夫圆变换。霍夫直线变换用来在图像内寻找直线,霍夫圆变换用来在图像内寻找圆。在 OpenCV 中,前者可以用函数cv2.HoughLines()和函数cv2.HoughLinesP()实现, 后者可以用函数cv2.HoughCircles()实现。

霍夫线变换

原理

一条直线可由两个点A=(x1,y1)A=(x_1,y_1)B=(x2,y2)B=(x_2,y_2)确定(笛卡尔坐标)

另一方面,y=kx+qy = kx + q,也可以写成关于(k,q)的函数表达式(霍夫空间):

对应的变换可以通过图形直观表示:

变换后的空间成为霍夫空间。即:笛卡尔坐标系中一条直线,对应霍夫空间的一个点

反过来同样成立(霍夫空间的一条直线,对应笛卡尔坐标系的一个点):

再来看看A、B两个点,对应霍夫空间的情形:

一步步来,再看一下三个点共线的情况:


根本原理:

可以看出如果笛卡尔坐标系的点共线,这些点在霍夫空间对应的直线交于一点:这也是必然,共线只有一种取值可能。那么,对于笛卡尔空间中的一条直线,直线上所有点在霍夫空间内对应的直线一定交于一点。霍夫变换的投票策略就类似于阈值处理,在某点相交的直线超过一定数量,则认为笛卡尔空间中存在这样的一条直线。


如果不止一条直线呢?再看看多个点的情况(有两条直线):

其实(3,2)与(4,1)也可以组成直线,只不过它有两个点确定,而图中A、B两点是由三条直线汇成,这也是霍夫变换的后处理的基本方式选择由尽可能多直线汇成的点

看看,霍夫空间:选择由三条交汇直线确定的点(中间图),对应的笛卡尔坐标系的直线(右图)。

到这里问题似乎解决了,已经完成了霍夫变换的求解,但是如果像下图这种情况呢?

k=∞是不方便表示的,而且q怎么取值呢,这样不是办法。因此考虑将笛卡尔坐标系换为:极坐标表示

上图右边公式竟然是错的,极坐标公式如下,自己看的时候要修正下:

[公式]

在极坐标系下,其实是一样的:极坐标的点→霍夫空间的直线,只不过霍夫空间不再是(k,q)(k,q)的参数,而是(ρ,θ)(\rho,\theta)的参数,给出对比图:

如果线在原点下方通过,则它将具有正的ρ且角度小于180。如果线在原点上方,则将角度取为小于180,而不是大于180的角度。ρ取负值。任何垂直线将具有0度,水平线将具有90度

现在,让我们看一下霍夫变换如何处理线条。任何一条线都可以用(ρ,θ)这两个术语表示。因此,首先创建2D数组或累加器(以保存两个参数的值),并将其初始设置为0。让行表示ρ,列表示θ。阵列的大小取决于所需的精度。假设您希望角度的精度为1度,则需要180列。对于ρ,最大距离可能是图像的对角线长度。因此,以一个像素精度为准,行数可以是图像的对角线长度。

考虑一个100x100的图像,中间有一条水平线。取直线的第一点。您知道它的(x,y)值。现在在线性方程式中,将值θ= 0,1,2,… 180放进去,然后检查得到ρ。对于每对(ρ,θ),在累加器中对应的(ρ,θ)单元格将值增加1。所以现在在累加器中,单元格(50,90)= 1以及其他一些单元格。

现在,对行的第二个点。执行与上述相同的操作。递增(ρ,θ)对应的单元格中的值。这次,单元格(50,90)=2。实际上,您正在对(ρ,θ)值进行投票。您对线路上的每个点都继续执行此过程。在每个点上,单元格(50,90)都会增加或投票,而其他单元格可能会或可能不会投票。这样一来,最后,单元格(50,90)的投票数将最高。因此,如果您在累加器中搜索最大票数,则将获得(50,90)值,该值表示该图像中的一条线与原点的距离为50,角度为90度。在下面的动画中很好地显示了该图片(图片提供:Amos Storkey)

除此以外,也同样可以使用阈值对票数进行处理。

也就是说我们遍历θ的值,从0-180°,并且同时代入(x,y)的值,求得对应的ρ。

找到0-180°中,哪个度数下的ρ值相同的数量最多。这反向说明了,在一个ρ和θ组成的函数中,符合的点数最多。

函数与示例

函数cv2.HoughLines() 的语法格式为:
lines=cv2.HoughLines (image,rho,theta,threshold)
式中:

  • image是输入图像 , 即源图像, 必须是8位的单通道二值图像。 如果是其他类型的图像, 在进行霍夫变换之前, 需要将其修改为指定格式。

    • 输入图像应该是二进制图像,因此在应用霍夫变换之前,建议应用阈值二值化或使用Canny边缘检测
  • rho为以像素为单位的距离ρ的精度,一般为1。

  • theta为角度θ的精度。 一般情况下, 使用的精度是np.pi/180 , 表示要搜索所有可能的角度。

  • threshold是阈值。 该值越小, 判定出的直线就越多。 通过上一节的分析可知,识别直线时,要判定有多少个点位千该直线上。在判定直线是否存在时,对直线所穿过的点的数星进行评估,如果直线所穿过的点的数量小于阈值,则认为这些点恰好(偶然)在算法上构成直线, 但是在源图像中该直线并不存在;如果大于阈值,则认为直线存在。所以,如果阈值较小,就会得到较多的直线;阈值较大,就会得到较少的直线。

  • 返回值lines中的每个元素都是一对浮点数,表示检测到的直线的参数,即(ρ,θ),numpy.ndarray类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import numpy as np
import cv2 as cv

if __name__ == '__main__':
img = cv.imread('image\\hflines.png')
img2 = img.copy()
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
edges = cv.Canny(gray, 50, 150, apertureSize=3)
lines = cv.HoughLines(edges, 1, np.pi / 180, 200)
for line in lines:
rho, theta = line[0]
a = np.cos(theta)
b = np.sin(theta)
x0 = a * rho
y0 = b * rho
x1 = int(x0 + 1000 * (-b))
y1 = int(y0 + 1000 * (a))
x2 = int(x0 - 1000 * (-b))
y2 = int(y0 - 1000 * (a))
cv.line(img, (x1, y1), (x2, y2), (0, 0, 255), 2)
res = np.vstack((img2, img))
cv.imshow("res",res)

cv.waitKey()
cv.destroyAllWindows()

较粗的直线是因为有多条直线靠近在一起 , 即检测出了重复的结果。 在一些情况下, 使用霍夫变换可能将图像中有限个点碰巧对齐的非直线关系检测为直线, 而导致误检测, 尤其是一些复杂背景的图像, 误检测会很明显。 此图中该间题虽然并不是特别明显, 但是如果将阈值 threshold的值设置得稍小些, 仍然会出现较多重复的检测结果。

概率霍夫变换

概率霍夫变换对基本霍夫变换算法进行了一些修正,是霍夫变换算法的优化。它没有考虑所有的点。相反,它只需要一个足以进行线检测的随机点子集即可。

为了更好地判断直线(线段),概率霍夫变换算法还对选取直线的方法作了两点改进:

  • 所接受直线的最小长度。如果有超过阈值个数的像素点构成了一条直线,但是这条直线很短,那么就不会接受该直线作为判断结果,而认为这条直线仅仅是图像中的若干个像素点恰好随机构成了一种算法上的直线关系而已,实际上原图中并不存在这条直线。

  • 接受直线时允许的最大像素点间距。如果有超过阈值个数的像素点构成了一条直线,但是这组像素点之间的距离都很远,就不会接受该直线作为判断结果,而认为这条直线仅仅是图像中的若干个像素点恰好随机构成了一种算法上的直线关系而已,实际上原始图像中并不存在这条直线。

概率霍夫变换是我们看到的霍夫变换的优化。它没有考虑所有要点。取而代之的是,它仅采用随机的点子集,足以进行线检测。只是我们必须降低阈值

函数与示例

lines = cv2.HoughLinesP(image, rho, theta, threshold, minLineLength,maxLineGap)

  • image是输入图像,即源图像,必须为8位的单通道二值图像。对千其他类型的图像,在进行霍夫变换之前,需要将其修改为这个指定的格式。

  • rho为以像素为单位的距离ρ的精度,一般为1。

  • theta为角度θ的精度。 一般情况下, 使用的精度是np.pi/180 , 表示要搜索所有可能的角度。

  • threshold是阈值。 该值越小, 判定出的直线就越多

  • minLineLength用来控制接受直线的最小长度,默认为0。

  • maxLineGap用来控制接受共线线段之间的最小间隔,即一条线中两点的最大间隔,默认为0。

  • 返回值lines中的每个元素都是一对浮点数,表示检测到的直线的参数,即(ρ,θ),numpy.ndarray类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import numpy as np
import cv2 as cv

if __name__ == '__main__':
img = cv.imread('image\\hflines.png')
img2 = img.copy()
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
edges = cv.Canny(gray, 50, 150, apertureSize=3)
lines = cv.HoughLinesP(edges, 1, np.pi / 180, 100, minLineLength=100, maxLineGap=10)
for line in lines:
x1, y1, x2, y2 = line[0]
cv.line(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
res = np.vstack((img2, img))
cv.imshow("res", res)

cv.waitKey()
cv.destroyAllWindows()

细心观察可以发现,这里画出来的是线段。

霍夫圆变换

圆在数学上表示为(xxcenter)2+(yycenter)2=r2(x−x_{center})^2+(y−y_{center})^2=r^2,其中(xcenter,ycenter)(x_{center},y_{center})是圆的中心,rr是圆的半径。从等式中,我们可以看到我们有3个参数,因此我们需要3D累加器进行霍夫变换,这将非常低效。因此,OpenCV使用更加技巧性的方法,即使用边缘的梯度信息的Hough梯度方法

这种方法采用的策略是两轮筛选。 第1轮筛选找出可能存在圆的位置(圆心);第2轮再根据第1轮的结果筛选出半径大小

与用来决定是否接受直线的两个参数”接受直线的最小长度 (minLineLength) ”和“接受直线时允许的最大像素点间距 (MaxLineGap) ”类似, 霍夫圆变换也有几个用于决定是否接受圆的参数:圆心间的最小距离、 圆的最小半径、 圆的最大半径。

函数

在OpenCV中 , 实现霍夫圆变换的是函数cv2.HoughCircles ()该函数将Canny边缘检测和霍夫变换结合。 其语法格式为:

circles=cv2.HoughCircles (image, method, dp, minDist, param1, param2, minRadius, maxRadius)

  • image:输入图像, 即源图像, 类型为8位的单通道灰度图像

  • method: 检测方法。 截止到OpenCV 4.0.0-pre版本 , HOUGH_GRADIENT是唯一可用的参数值。 该参数代表的是霍夫圆检测中两轮检测所使用的方法。

  • dp累计器分辨率, 它是一个分割比率, 用来指定图像分辨率与圆心累加器分辨率的比例。 例如, 如果dp=1, 则输入图像和累加器具有相同的分辨率。

  • minDist圆心间的最小间距。 该值被作为阈值使用, 如果存在圆心间距离小于该值的多个圆,则仅有一个会被检测出来。 因此, 如果该值太小, 则会有多个临近的圆被检测出来;如果该值太大, 则可能会在检测时漏掉一些圆。

  • param1: 该参数是缺省的 , 在缺省时默认值为100。 它对应的是Canny边缘检测器的高阈值(低阈值是高阈值的二分之一)。

  • param2: 圆心位置必须收到的投票数。 只有在第1轮筛选过程中,投票数超过该值的圆,才有资格进入第2轮的筛选。 因此,该值越大,检测到的圆越少;该值越小,检测到的圆越多。这个参数是缺省的,在缺省时具有默认值100。

  • minRadius圆半径的最小值,小于该值的圆不会被检测出来。 该参数是缺省的,在缺省时具有默认值0,此时该参数不起作用。

  • maxRadius圆半径的最大值,大于该值的圆不会被检测出来。该参数是缺省的,在缺省时具有默认值0,此时该参数不起作用。

  • circles: 返回值,由圆心坐标和半径构成的numpy.ndarray

​ 需要特别注意,在调用函数 cv2.HoughLinesCircles()之前,要对源图像进行平滑操作,以减少图像中的噪声,避免发生误判。

样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import numpy as np
import cv2 as cv

if __name__ == '__main__':
img = cv.imread('image\\6.png', 0)
img = cv.medianBlur(img, 5)#中值滤波
cimg = cv.cvtColor(img, cv.COLOR_GRAY2BGR)
circles = cv.HoughCircles(img, cv.HOUGH_GRADIENT, 1, 20,
param1=50, param2=50, minRadius=0, maxRadius=0)
circles = np.uint16(np.around(circles))
for i in circles[0, :]:
# 绘制外圆
cv.circle(cimg, (i[0], i[1]), i[2], (0, 255, 0), 2)
# 绘制圆心
cv.circle(cimg, (i[0], i[1]), 2, (0, 0, 255), 3)
img = cv.cvtColor(img, cv.COLOR_GRAY2BGR)
res = np.hstack((img, cimg))
cv.imshow('detected circles', res)
cv.imwrite('res.png', res)
cv.waitKey(0)
cv.destroyAllWindows()

注:不同图像间的检测需要调参,尤其是parm2,第二张图要调成32

图像分割与提取

分水岭算法

原理

​ 任何灰度图像都可以看作是一个地形表面,其中高强度表示山峰,低强度表示山谷。你开始用不同颜色的水(标签)填充每个孤立的山谷(局部最小值)。随着水位的上升,根据附近的山峰(坡度),来自不同山谷的水明显会开始合并,颜色也不同。为了避免这种情况,你要在水融合的地方建造屏障。你继续填满水,建造障碍,直到所有的山峰都在水下。然后你创建的屏障将返回你的分割结果。这就是Watershed背后的“思想”。

​ 但是这种方法会由于图像中的噪声或其他不规则性而产生过度分割的结果。因此OpenCV实现了一个基于标记的分水岭算法,你可以指定哪些是要合并的山谷点,哪些不是。这是一个交互式的图像分割。我们所做的是给我们知道的对象赋予不同的标签。用一种颜色(或强度)标记我们确定为前景或对象的区域,用另一种颜色标记我们确定为背景或非对象的区域,最后用0标记我们不确定的区域。这是我们的标记。然后应用分水岭算法。然后我们的标记将使用我们给出的标签进行更新,对象的边界值将为-1

使用

直接使用分水岭算法有时效果并不好,这是图像中的噪声、边界相交、模糊等原因导致的。

我们先从寻找硬币的近似估计开始。因此,我们可以使用Otsu二值化。

现在我们需要去除图像中的任何白点噪声。为此,我们可以使用形态学扩张。要去除对象中的任何小孔,我们可以使用形态学侵蚀(开运算)。对于前景区域,我们可以使用距离变换函数找到。因此,现在我们可以确定,靠近对象中心的区域是前景,而离对象中心很远的区域是背景。我们不确定的唯一区域是硬币的边界区域

因此,我们需要提取我们可确定为硬币的区域。侵蚀会去除边界像素。因此,无论剩余多少,我们都可以肯定它是硬币。如果物体彼此不接触,那将起作用。但是,由于它们彼此接触,因此另一个好选择是找到距离变换并应用适当的阈值。接下来,我们需要找到我们确定它们不是硬币的区域。为此,我们扩张了结果。膨胀将对象边界增加到背景。这样,由于边界区域已删除,因此我们可以确保结果中背景中的任何区域实际上都是背景。参见下图。

左侧为前景,右侧是确定的背景

剩下的区域是我们不知道的区域,无论是硬币还是背景。分水岭算法应该找到它。这些区域通常位于前景和背景相遇(甚至两个不同的硬币相遇)的硬币边界附近。我们称之为边界。可以通过从确定的背景区域中减去确定的前景区域来获得。

边界

查看结果。在阈值图像中,我们得到了一些硬币区域,我们确定它们是硬币,并且现在已分离它们。(在某些情况下,你可能只对前景分割感兴趣,而不对分离相互接触的对象感兴趣。在那种情况下,你无需使用距离变换,只需侵蚀就足够了。侵蚀只是提取确定前景区域的另一种方法。)

现在我们可以确定哪些是硬币的区域,哪些是背景。因此,我们创建了标记(它的大小与原始图像的大小相同,但具有int32数据类型),并标记其中的区域。我们肯定知道的区域(无论是前景还是背景)都标有任何正整数,但是带有不同的整数,而我们不确定的区域则保留为零。为此,我们使用cv.connectedComponents()。它用0标记图像的背景,然后其他对象用从1开始的整数标记。

但是我们知道,如果背景标记为0,则分水岭会将其视为未知区域。所以我们想用不同的整数来标记它。相反,我们将未知定义的未知区域标记为0

现在我们的标记已准备就绪。现在是最后一步的时候了,使用分水岭算法。然后标记图像将被修改。边界区域将标记为-1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

if __name__ == '__main__':
img = cv.imread('image\\0.jpg')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)
# 噪声去除
kernel = np.ones((3, 3), np.uint8)
#这里进行了迭代,其实不迭代也还好
opening = cv.morphologyEx(thresh, cv.MORPH_OPEN, kernel, iterations=2)
# 确定背景区域
sure_bg = cv.dilate(opening, kernel, iterations=3)
# 寻找前景区域
dist_transform = cv.distanceTransform(opening, cv.DIST_L2, 5)
ret2, sure_fg = cv.threshold(dist_transform, 0.7 * dist_transform.max(), 255, cv.THRESH_BINARY)
# 找到未知区域
sure_fg = np.uint8(sure_fg)
unknown = cv.subtract(sure_bg, sure_fg)
# 类别标记
ret3, markers = cv.connectedComponents(sure_fg)

plt.subplot(131)
plt.imshow(markers)
plt.title('mark'), plt.xticks([]), plt.yticks([])

# 为所有的标记加1,保证背景是1而不是0
markers = markers + 1
# 现在让所有的未知区域为0
markers[unknown == 255] = 0

plt.subplot(132)
plt.imshow(markers)
plt.title('mark unknown'), plt.xticks([]), plt.yticks([])

markers = cv.watershed(img, markers)
img[markers == -1] = [255, 0, 0]
plt.subplot(133)
plt.imshow(img)
plt.title('watershed result'), plt.xticks([]), plt.yticks([])
plt.show()

cv.waitKey(0)
cv.destroyAllWindows()

距离变换函数distanceTransform()

当图像内的各个子图没有连接时, 可以直接使用形态学的腐蚀操作确定前景对象, 但是如果图像内的子图连接在一起时,就很难确定前景对象了。此时,借助于距离变换函数cv2.distanceTransform()可以方便地将前景对象提取出来。

距离变换函数cv2.distanceTransform()计算二值图像内任意点到最近背景点的距离。一般情况下,该函数计算的是图像内非零值像素点到最近的零值像素点的距离, 即计算二值图像中所有像素点距离其最近的值为0的像素点的距离。 当然, 如果像素点本身的值为0, 则这个距离也为0。

距离变换函数cv2.distanceTransform()的计算结果反映了各个像素与背景(值为0的像素点)的距离关系。 通常情况下:

  • 如果前景对象的中心(质心)距离值为0的像素点距离较远, 会得到一个较大的值。

  • 如果前景对象的边缘距离值为0的像素点较近, 会得到一个较小的值。
    如果对上述计算结果进行阈值化, 就可以得到图像内子图的中心、 骨架等信息。 距离变换函数cv2.distanceTransform()可以用于计算对象的中心,还能细化轮廓、获取图像前景等,有多种功能。

    • 如:

      1
      2
      dist_transform = cv.distanceTransform(img, cv.DIST_L2, 5)
      ret2, sure_fg = cv.threshold(dist_transform, 0.7 * dist_transform.max(), 255, cv.THRESH_BINARY)

距离变换函数cv2.distanceTransform()的语法格式为:
dst=cv2.distanceTransform(src,distanceType,maskSize[,dstType]])

式中:

  • src是8位单通道的二值图像。
  • distanceType为距离类型参数,其具体值和含义如表所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import numpy as np
import cv2 as cv

if __name__ == '__main__':
img = cv.imread('image\\0.jpg')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)
kernel = np.ones((3, 3), np.uint8)
opening = cv.morphologyEx(thresh, cv.MORPH_OPEN, kernel, iterations=2)
sure_bg = cv.dilate(opening, kernel, iterations=3)
dist_transform = cv.distanceTransform(opening, cv.DIST_L2, 5)
res = np.hstack((gray, dist_transform))
cv.imwrite('res.png', res)

cv.waitKey(0)
cv.destroyAllWindows()

函数connectedComponents()

明确了确定前景后,就可以对确定前景图像进行标注了。在OpenCV中,可以使用函数cv2.connectedComponents()进行标注。该函数会将背景标注为0, 将其他的对象使用从1开始的正整数标注。

函数cv2.connectedComponents()的语法格式为:

retval,labels=cv2.connectedComponents (image)

式中:

  • image为8位单通道的待标注图像。
  • retval为返回的标注的数量。
  • labels为标注的结果图像。

交互式前景提取

GrabCut算法的具体实施过程

  • 将前景所在的大致位置使用矩形框标注出来。值得注意的是,此时矩形框框出的仅仅是前景的大致位置,其中既包含前景又包含背景,所以该区域实际上是未确定区域。但是,该区域以外的区域被认为是 "确定背景”

  • 根据矩形框外部的 "确定背景“ 数据来区分矩形框区域内的前景和背景。

  • 用高斯混合模型 (Gaussians Mixture Model,GMM) 对前景和背景建模。GMM会根据用户的输入学习并创建新的像素分布,对未分类的像素(可能是背景也可能是前景),根据其与已知分类像素(前景和背景)的关系进行分类。

  • 根据像素分布情况生成一幅图,图中的节点就是各个像素点。除了像素点之外,还有两个节点:前景节点和背景节点所有的前景像素都和前景节点相连,所有的背景像素都和背景节点相连。每个像素连接到前景节点或背景节点的边的权重由像素是前景或背景的概率来决定。

  • 图中的每个像素除了与前景节点或背景节点相连外,彼此之间还存在着连接。两个像素连接的边的权重值由它们的相似性决定,两个像素的颜色越接近,边的权重值越大

  • 完成节点连接后,需要解决的问题变成了一幅连通的图。在该图上根据各自边的权重关系进行切割,将不同的点划分为前景节点和背景节点

  • 不断重复上述过程, 直至分类收敛为止。

函数

在OpenCV中, 实现交互式前景提取的函数是cv2.grabCut ()

1
mask,bgdModel,fgdModel=cv2.grabCut(img,mask,rect,bgdModel,fgdModel,iterCount[,mode ])
  • img为输入图像,要求是8位3通道的。

  • mask为掩模图像,要求是8位单通道的。该参数用于确定前景区域、背景区域和不确定区域可以设置为4种形式

    • cv2.GC_BGD:表示确定背景, 也可以用数值0表示。
    • cv2.GC_FGD: 表示确定前景, 也可以用数值1表示。
    • cv2.GC_PR_BGD: 表示可能的背景, 也可以用数值2表示。
    • cv2.GC_PR_FGD: 表示可能的前景, 也可以用数值3表示。

在最后使用模板提取前景时,会将参数值0和2合并为背景(均当作0处理),将参数值1和3合并为前景(均当作1处理)。在通常情况下,我们可以使用白色笔刷和黑色笔刷在掩模图像上做标记,再通过转换将其中的白色像素设置为0,黑色像素设置为1。

  • rect 指包含前景对象的区域,该区域外的部分被认为是“确定背景”。因此,在选取时务必确保让前景包含在rect指定的范围内;否则,rect外的前景部分是不会被提取出来的。只有当参数mode的值被设置为矩形模式cv2.GC_INIT_ WITH_RECT时 ,参数rect才有意义。其格式为(x,y,w,h),分别表示区域左上角像素的x轴和y轴坐标以及区域的宽度和高度。如果前景位于右下方,又不想判断原始图像的大小,对于w 和h可以直接用一个很大的值。使用掩模模式时,将该值设置为none即可。

  • bgdModel为算法内部使用的数组,只需要创建大小为(1,65)的numpy.float64数组。

  • fgdModel为算法内部使用的数组, 只需要创建大小为(1,65)的numpy.float64数组。

  • iterCount表示迭代的次数。

  • mode表示迭代模式。

参考

《OpenCV轻松入门面向Python》

OpenCV官方文档

python图像处理

傅里叶变换

频域与时域

什么是图片的频域

霍夫线变换