OpenCV( Open source Computer Vision)是一个开源程序库, 包含了500多个用于图像和视频分析的优化算法。该程序库建立于1999年, 目前在计算机视觉领域的研发人员社区中非常流行, 被用作主要开发工具。OpenCV最初由英特尔公司的Gary Bradski带领一个小组开发, 其目的是推动计算机视觉的研究, 促进基于大量视觉处理、CPU密集型应用程序的开发。
1. 基础知识
1.1 入门
pip install opencv-python opencv-contrib-python
import cv2
import numpy as np
# imread和imdecode的最大区别就在于, imread在读取路径含有中文名的图片时会报错, 而imdecode可以正常读取。
origin_image = cv2.imread("E:/xx.jpg")
origin_image = cv2.imdecode(np.fromfile("E:/xx.jpg", dtype=np.uint8))
# from buffer bytes
origin_image = cv2.cvtColor(np.asarray(images[0]), cv2.IMREAD_COLOR)
1.2 Numpy库
使用面向Python的OpenCV(OpenCV for Python)必须熟练掌握Numpy库, 尤其是Numpy.array库, Numpy.array 库是Python处理图像的基础。
像素是图像构成的基本单位, 像素处理是图像处理的基本操作, 可以通过位置索引的形式对图像内的元素进行访问、处理。
可以将图像理解为一个矩阵, 在面向Python的OpenCV(OpenCV for Python)中, 图像就是Numpy库中的数组。一个OpenCV灰度图像是一个二维数组, 可以使用表达式访问其中的像素值。
numpy.array 提供了item()
和itemset()
函数来访问和修改像素值, 而且这两个函数都是经过优化处理的, 能够更大幅度地提高处理效率。
- item(行,列)
- item(行,列,通道): 访问 RGB 模式图像
- itemset(索引值,新值)
1.3 色彩
神经生理学实验发现, 在视网膜上存在三种不同的颜色感受器, 能够感受三种不同的颜色: 红色、绿色和蓝色, 即三基色。自然界中常见的各种色光都可以通过将三基色按照一定的比例混合构成。除此以外, 从光学角度出发, 可以将颜色解析为主波长、纯度、明度等。从心理学和视觉角度出发, 可以将颜色解析为色调、饱和度、亮度等。通常, 我们将上述采用不同的方式表述颜色的模式称为色彩空间, 或者颜色空间、颜色模式等。
需要说明的是, 在OpenCV中, 最小的数据类型是无符号的8位数。因此, 在OpenCV中实际上并没有二值图像这种数据类型, 二值图像经常是通过处理得到的, 然后使用0表示黑色, 使用 255 表示白色。
RGB 模式的彩色图像在读入OpenCV 内进行处理时, 会按照行方向依次读取该RGB图像的B通道、G通道、R通道的像素点, 并将像素点以行为单位存储在 ndarray 的列中。
1.4 感兴趣区域( ROI)
在图像处理过程中, 我们可能会对图像的某一个特定区域感兴趣, 该区域被称为感兴趣区域(Region of Interest, ROI)。在设定感兴趣区域 ROI 后, 就可以对该区域进行整体操作。
注意ROI的坐标系[Y上:Y下, X上:X下]
, 先Y轴再X轴, 如: image[200:400, 600:800]
1.5 通道操作
在 RGB 图像中, 图像是由R通道、G通道、B通道三个通道构成的。需要注意的是, 在OpenCV中, 通道是按照B通道→G通道→R通道
的顺序存储的。在图像处理过程中, 可以根据需要对通道进行拆分和合并。
b = img[:, :, 0]
g = img[:, :, 1]
r = img[:, :, 2]
# 函数 cv2.split()能够拆分图像的通道
b, g, r = cv2.split(img)
通道合并是通道拆分的逆过程, 通过合并通道可以将三个通道的灰度图像构成一幅彩色图像。函数cv2.merge()
可以实现图像通道的合并, 例如有 B通道图像b、G通道图像g和R通道图像r, 使用函数cv2.merge()
可以将这三个通道合并为一幅 BGR 的三通道彩色图像。
bgr=cv2.merge([b,g,r])
1.6 图像属性
在图像处理过程中, 经常需要获取图像的属性, 例如图像的大小、类型等。这里介绍几个常用的属性:
- shape: 如果是彩色图像, 则返回包含行数、列数、通道数的数组; 如果是二值图像或者灰度图像, 则仅返回行数和列数。通过该属性的返回值是否包含通道数, 可以判断一幅图像是灰度图像(或二值图像)还是彩色图像。
- size: 返回图像的像素数目。其值为"行×列×通道数", 灰度图像或者二值图像的通道数为 1。
- dtype: 返回图像的数据类型。
2. 图像运算
2.1 加法运算
可以通过加号运算符"+“对图像进行加法运算, 也可以通过cv2.add()
函数对图像进行加法运算。两种不同的加法运算方式, 对超过255 的数值的处理方式是不一样的。
- 加号运算符”+"
\[𝑎 + 𝑏 = \begin{cases} a + b, & \text a + b \leq 255 \\ mod(a + b, 255), & \text a + b \gt 255 \end{cases} \]
当然, 上述公式也可以简化为𝑎 + 𝑏 = mod(𝑎 + 𝑏, 256), 在运算时无论相加的和是否大于255, 都对数值 256 取模。通过将数组的数值类型定义为 dtype=np.uint8, 可以保证数组值的范围在[0,255]之间。
- cv2.add()
\[𝑎 + 𝑏 = \begin{cases} a + b, & \text a + b \leq 255 \\ 255, & \text a + b \gt 255 \end{cases} \]
使用函数cv2.add()对像素值a和像素值b进行求和运算时, 会得到像素值对应图像的饱和值(最大值)。
2.2 加权和
所谓图像加权和, 就是在计算两幅图像的像素值之和时, 将每幅图像的权重考虑进来, 可以用公式表示为:
\[dst = saturate(src1 × \alpha + src2 × \beta + \gamma) \]
式中, saturate()表示取饱和值(最大值)。图像进行加权和计算时, 要求 src1 和 src2 必须大小、类型相同, 但是对具体是什么类型和通道没有特殊限制。
OpenCV 中提供了函数 cv2.addWeighted(), 用来实现图像的加权和(混合、融合), 该函数的语法格式为:
dst=cv2.addWeighted(src1, alpha, src2, beta, gamma)
2.3 按位逻辑运算
逻辑运算是一种非常重要的运算方式, 图像处理过程中经常要按照位进行逻辑运算, OpenCV 中的按位逻辑运算, 简称位运算。
- cv2.bitwise_and(): 按位与, Mask
- cv2.bitwise_or(): 按位或
- cv2.bitwise_xor(): 按位异或
- cv2.bitwise_not(): 按位取反
2.4 掩模
OpenCV 中的很多函数都会指定一个掩模, 也被称为掩码, 例如:
计算结果=cv2.add(img1, img2, 掩模)
当使用掩模参数时, 操作只会在掩模值为非空的像素点上执行, 并将其他像素点的值置为0。在计算时, 掩
码为 1 的部分对应"img1+img2", 其他部分的像素值均为"0"。
2.5 图像与数值的运算
在上述加法运算和按位运算中, 参与运算的两个算子(参数)既可以是两幅图像, 也可以是一幅图像与一个数值。
如果想增加图像的整体亮度, 可以将每一个像素值都加上一个特定值。在具体实现时, 可以给图像加上一个统一像素值的图像, 也可以给图像加上一个固定值。
2.6 位平面分解
将灰度图像中处于同一比特位上的二进制像素值进行组合, 得到一幅二进制值图像, 该图像被称为灰度图像的一个位平面, 这个过程被称为位平面分解。
2.7 图像加密和解密
通过对原始图像与密钥图像进行按位异或, 可以实现加密; 将加密后的图像与密钥图像再次进行按位异或, 可以实现解密。
2.8 数字水印
最低有效位(Least Significant Bit, LSB)指的是一个二进制数中的第0位(即最低位)。最低有效位信息隐藏指的是, 将一个需要隐藏的二值图像信息嵌入载体图像的最低有效位, 即将载体图像的最低有效位层替换为当前需要隐藏的二值图像, 从而实现将二值图像隐藏的目的。由于二值图像处于载体图像的最低有效位上, 所以对于载体图像的影响非常不明显, 其具有较高的隐蔽性。
3. 色彩空间类型转换
在OpenCV中有超过150中进行颜色空间转换的方法。但是你以后就会发现我们经常用到的也就两种: BGR↔Gray和BGR↔HSV。
import cv2
flags=[i for i in dir(cv2) if i.startswith('COLOR_')]
print(flags)
RGB图像是一种比较常见的色彩空间类型, 除此以外还有一些其他的色彩空间, 比较常见的包括GRAY色彩空间(灰度图像)、XYZ色彩空间、YCrCb色彩空间、HSV色彩空间、HLS色彩空间、CIELab色彩空间、CIELuv色彩空间、Bayer色彩空间等。每个色彩空间都有自己擅长的处理问题的领域, 因此, 为了更方便地处理某个具体问题, 就要用到色彩空间类型转换。
在使用OpenCV处理图像时, 可能会在RGB色彩空间和HSV色彩空间之间进行转换。在进行图像的特征提取、距离计算时, 往往先将图像从RGB色彩空间处理为灰度色彩空间。在一些应用中, 可能需要将色彩空间的图像转换为二值图像。
3.1 色彩空间基础
3.1.1 GRAY
GRAY(灰度图像)通常指8位灰度图, 其具有256个灰度级, 像素值的范围是[0,255]。
当图像由RGB色彩空间转换为GRAY色彩空间时, 其处理方式如下:
Gray = 0.299 · 𝑅 + 0.587 · 𝐺 + 0.114 · 𝐵
当图像由GRAY色彩空间转换为RGB色彩空间时, 最终所有通道的值都将是相同的, 其处理方式如下:
𝑅 = Gray
𝐺 = Gray
B = Gray
3.1.2 XYZ色彩空间
XYZ色彩空间是由CIE(International Commission on Illumination)定义的, 是一种更便于计算的色彩空间, 它可以与RGB色彩空间相互转换。
3.1.3 YCrCb色彩空间
人眼视觉系统(HVS, Human Visual System)对颜色的敏感度要低于对亮度的敏感度。在传统的RGB色彩空间内, RGB三原色具有相同的重要性, 但是忽略了亮度信息。
在 YCrCb 色彩空间中, Y代表光源的亮度, 色度信息保存在Cr和Cb中, 其中: Cr表示红色分量信息, Cb表示蓝色分量信息。
从RGB色彩空间到YCrCb色彩空间的转换公式为:
\[𝑌 = 0.299 · 𝑅 + 0.587 · 𝐺 + 0.114 · 𝐵 \\ Cr = (𝑅 − 𝑌) × 0.713 + delta \\ Cb = (𝐵 − 𝑌) × 0.564 + delta \]
式中delta的值为:
\[delta \begin{cases} 128, & \text 8 位图像 \\32768, & \text 16 位图像 \\ 0.5, & \text 单精度图像\end{cases} \]
从YCrCb色彩空间到RGB色彩空间的转换公式为:
\[𝐵 = 𝑌 + 1.773 · (Cb − delta) \\ 𝑅 = 𝑌 + 1.403 · (Cr − delta) \\ 𝐺 = 𝑌 − 0.714 · (Cr − delta) − 0.344 · (Cb − delta) \]
3.1.4 HSV色彩空间
RGB 是从硬件的角度提出的颜色模型, 在与人眼匹配的过程中可能存在一定的差异, HSV色彩空间是一种面向视觉感知的颜色模型dHSV 色彩空间从心理学和视觉的角度出发, 指出人眼的色彩知觉主要包含三要素: 色调(Hue, 也称为色相)、饱和度(Saturation)、亮度(Value), 色调指光的颜色, 饱和度是指色彩的深浅程度, 亮度指人眼感受到的光的明暗程度。
在具体实现上, 我们将物理空间的颜色分布在圆周上, 不同的角度代表不同的颜色。因此, 通过调整色调值就能选取不同的颜色, 色调的取值区间为[0, 360]。色调取不同值时, 所代表的颜色如表 4-1 所示, 两个角度之间的角度对应两个颜色之间的过渡色。
色调值(度) | 颜色 |
---|---|
0 | 红色 |
60 | 黄色 |
120 | 绿色 |
180 | 青色 |
240 | 蓝色 |
300 | 品红色 |
# 从BGR色彩得到对应的HSV的H值
green=np.uint8([[[0,255,0]]])
hsv_green=cv2.cvtColor(green,cv2.COLOR_BGR2HSV)
print(hsv_green)
饱和度为一比例值, 范围是[0, 1], 具体为所选颜色的纯度值和该颜色最大纯度值之间的比值。饱和度的值为0 时, 只有灰度。亮度表示色彩的明亮程度, 取值范围也是[0, 1]。
在 HSV 色彩模型中, 取色变得更加直观。例如, 取值"色调=0, 饱和度=1, 亮度=1", 则当前色彩为深红色, 而且颜色较亮; 取值"色调=120, 饱和度=0.3, 亮度=0.4", 则当前色彩为浅绿色, 而且颜色较暗。
所有这些转换都被封装在OpenCV的cv2.cvtColor()
函数内。通常情况下, 我们都是直接调用该函数来完成色彩空间转换的, 而不用考虑函数的内部实现细节。
3.1.5 HLS色彩空间
HLS色彩空间包含的三要素是色调H(Hue)、光亮度/明度L(Lightness)、饱和度S(Saturation)。与HSV色彩空间类似, 只是HLS色彩空间用"光亮度/明度L(lightness)“替换了"亮度(Value)"。
3.1.6 CIELab*色彩空间
CIELab*色彩空间是均匀色彩空间模型, 它是面向视觉感知的颜色模型。从视觉感知均匀的角度来讲, 人所感知到的两种颜色的区别程度, 应该与这两种颜色在色彩空间中的距离成正比。
3.1.7 CIELuv*色彩空间
CIELuv色彩空间同 CIELab色彩空间一样, 都是均匀的颜色模型dCIELuv*色彩空间与设备无关, 适用于显示器显示和根据加色原理进行组合的场合, 该模型中比较强调对红色的表示, 即对红色的变化比较敏感, 但对蓝色的变化不太敏感。
3.1.8 Bayer色彩空间
Bayer色彩空间(Bayer模型)被广泛地应用在CCD和CMOS相机中。它能够从单平面R、G、B交错表内获取彩色图像。
3.2 类型转换
在OpenCV内, 使用cv2.cvtColor()函数实现色彩空间的变换。该函数能够实现多个色彩空间之间的转换。其语法格式为:
dst = cv2.cvtColor(src, code [, dstCn])
需要注意, BGR色彩空间与传统的RGB色彩空间不同。对于一个标准的24位位图, BGR色彩空间中第1个8位(第1个字节)存储的是蓝色组成信息(Bluecomponent), 第2个8位(第2个字节)存储的是绿色组成信息(Greencomponent), 第3个8位(第3个字节)存储的是红色组成信息(Redcomponent)。同样, 其第4个、第5个、第6个字节分别存储蓝色、绿色、红色组成信息, 以此类推。
在 HSV 或 HLS 色彩空间中, 色调值通常在[0,360)范围内, 在 8 位图中转换到上述色彩空间后, 色调值要除以 2, 让其值范围变为[0,180), 以满足存储范围, 即让值的分布位于8 位图能够表示的范围[0,255]内。
3.3 HSV色彩空间讨论
RGB色彩空间是一种被广泛接受的色彩空间, 但是该色彩空间过于抽象, 我们不能够直接通过其值感知具体的色彩。我们更习惯使用直观的方式来感知颜色, HSV色彩空间提供了这样的方式。通过HSV色彩空间, 我们能够更加方便地通过色调、饱和度和亮度来感知颜色。
- H(Hue, 即色相): 色调H的取值范围是[0,360]
- S(Saturation, 即饱和度): 饱和度值的范围是[0,1]
- V(Value, 即色调、纯度): 亮度的范围与饱和度的范围一致, 都是[0,1]
当S=1、V=1时, H所代表的任何颜色被称为纯色;
当S=0时, 即饱和度为0, 颜色最浅, 最浅被描述为灰色(灰色也有亮度, 黑色和白色也属于灰色), 灰色的亮度由V决定, 此时H无意义;
当V=0时, 颜色最暗, 最暗被描述为黑色, 因此此时H(无论什么颜色最暗都为黑色)和S(无论什么深浅的颜色最暗都为黑色)均无意义。
需要注意, 在从RGB/BGR色彩空间转换到HSV色彩空间时, OpenCV为了满足8位图的要求, 对HSV空间的值进行了映射处理。
在HSV色彩空间中, H通道(饱和度Hue通道)对应不同的颜色。或者换个角度理解, 颜色的差异主要体现在H通道值的不同上。所以, 通过对H通道值进行筛选, 便能够筛选出特定的颜色。
OpenCV中通过函数cv2.inRange()来判断图像内像素点的像素值是否在指定的范围内, 其语法格式为:
dst = cv2.inRange(src, lowerb, upperb)
在OpenCV中的HSV模式内, 蓝色在H通道内的值是120。在提取蓝色时, 通常将"蓝色值120"附近的一个区间的值作为提取范围。该区间的半径通常为10左右, 例如通常提取[120−10,120+10]范围内的值来指定蓝色。
3.4 alpha 通道
在RGB色彩空间三个通道的基础上, 还可以加上一个A通道, 也叫alpha通道, 表示透明度。这种4个通道的色彩空间被称为RGBA色彩空间, PNG图像是一种典型的4通道图像。alpha通道的赋值范围是[0, 1], 或者[0, 255], 表示从透明到不透明。
4. 几何变换
几何变换是指将一幅图像映射到另外一幅图像内的操作OpenCV提供了多个与映射有关的函数, 这些函数使用起来方便灵活, 能够高效地完成图像的映射。
根据OpenCV函数的不同, 本章将映射关系划分为缩放、翻转、仿射变换、透视、重映射等。
4.1 缩放
使用函数cv2.resize()实现对图像的缩放, 具体形式为: cv2.resize(src, dsize[, fx[, fy[, interpolation]]])
在使用cv2.resize()函数时, 要额外注意参数dsize(顺序是w、h)的属性顺序问题, 它与shape的顺序(h、w、…)不一致!
4.2 翻转
在OpenCV中, 图像的翻转采用函数cv2.flip()实现, 该函数能够实现图像在水平方向翻转、垂直方向翻转、两个方向同时翻转, 其语法结构为:
dst = cv2.flip(src, flipCode)
**flipCode参数值: **
- 0只能是0绕着x轴翻转
- 正数: 1、2、3等任意正数绕着y轴翻转
- 负数: −1、−2、−3等任意负数围绕x轴、y轴同时翻转
4.3 仿射变换
仿射变换是指图像可以通过一系列的几何变换来实现平移、旋转等多种操作。该变换能够保持图像的平直性和平行性。平直性是指图像经过仿射变换后, 直线仍然是直线; 平行性是指图像在完成仿射变换后, 平行线仍然是平行线。
OpenCV中的仿射函数为cv2.warpAffine()
, 其通过一个变换矩阵(映射矩阵)M实现变换, 具体为:
\[dst(𝑥,𝑦)=src(𝑀11𝑥+𝑀12𝑦+𝑀13,𝑀21𝑥+𝑀22𝑦+𝑀23) \]
dst = cv2.warpAffine(src, M, dsize[, flags[, borderMode[, borderValue]]])
- 平移: 就是将对象换一个位置。如果要沿(x,y)方向移动, 移动的距离是(tx,ty), 可以以下面的方式构建移动矩阵:
\[M = \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \end{bmatrix} \]
- 旋转: 在使用函数cv2.warpAffine()对图像进行旋转时, 可以通过函数cv2.getRotationMatrix2D()获取转换矩阵。
- 仿射变换: 对于更复杂仿射变换, OpenCV提供了函数cv2.getAffineTransform()来生成仿射函数cv2.warpAffine()所使用的转换矩阵M。语法格式为:
retval=cv2.getAffineTransform(src, dst)
4.4 透视
对于视角变换, 我们需要一个3x3变换矩阵。在变换前后直线还是直线。要构建这个变换矩阵, 你需要在输入图像上找4个点, 以及他们在输出图像上对应的位置。这四个点中的任意三个都不能共线。这个变换矩阵可以有函数cv2.getPerspectiveTransform()
构建。然后把这个矩阵传给函数cv2.warpPerspective。
透视变换则可以将矩形映射为任意四边形。透视变换通过函数cv2.warpPerspective()实现, 语法是: dst = cv2.warpPerspective(src, M, dsize[, flags[, borderMode[, borderValue]]])
4.5 重映射
把一幅图像内的像素点放置到另外一幅图像内的指定位置, 这个过程称为重映射。OpenCV提供了多种重映射方式, 但是我们有时会希望使用自定义的方式来完成重映射。
OpenCV内的重映射函数cv2.remap()提供了更方便、更自由的映射方式, 其语法格式如下:
dst = cv2.remap(src, map1, map2, interpolation[, borderMode[, borderValue]])
重映射通过修改像素点的位置得到一幅新图像。在构建新图像时, 需要确定新图像中每个像素点在原始图像中的位置。因此, 映射函数的作用是查找新图像像素在原始图像内的位置。
5. 阈值处理
阈值处理是指剔除图像内像素值高于一定值或者低于一定值的像素点。
OpenCV提供了函数cv2.threshold()和函数cv2.adaptiveThreshold(), 用于实现阈值处理。
5.1 threshold 函数
# 第一个参数就是原图像, 原图像应该是灰度图
retval, dst = cv2.threshold(src, thresh, maxval, type)
5.2 自适应阈值处理
对于色彩均衡的图像, 直接使用一个阈值就能完成对图像的阈值化处理。但是, 有时图像的色彩是不均衡的, 此时如果只使用一个阈值, 就无法得到清晰有效的阈值分割结果图像。
在进行阈值处理时, 自适应阈值处理的方式通过计算每个像素点周围临近区域的加权平均值获得阈值, 并使用该阈值对当前像素点进行处理。与普通的阈值处理方法相比, 自适应阈值处理能够更好地处理明暗差异较大的图像。
需要注意的是, 该图像必须是8位单通道的图像。
OpenCV提供了函数cv2.adaptiveThreshold()来实现自适应阈值处理, 语法格式为: dst = cv2.adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C)
对比普通的阈值处理与自适应阈值处理可以发现, 自适应阈值处理保留了更多的细节信息。在一些极端情况下, 普通的阈值处理会丢失大量的信息, 而自适应阈值处理可以得到效果更好的二值图像。
5.3 Otsu 处理
在使用函数cv2.threshold()进行阈值处理时, 需要自定义一个阈值, 并以此阈值作为图像阈值处理的依据。通常情况下处理的图像都是色彩均衡的, 这时直接将阈值设为127是比较合适的。
Otsu方法能够根据当前图像给出最佳的类间分割阈值。简而言之, Otsu方法会遍历所有可能阈值, 从而找到最佳的阈值。在使用Otsu方法时, 要把阈值设为0, 该图像必须是8位单通道的图像。
dst = cv2.cvtColor(origin_image, cv2.COLOR_RGB2GRAY)
t, rst = cv2.threshold(dst, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
print(t)
6. 图像平滑处理
在尽量保留图像原有信息的情况下, 过滤掉图像内部的噪声, 这一过程称为对图像的平滑处理, 所得的图像称为平滑图像。
图像平滑处理的基本原理是, 将噪声所在像素点的像素值处理为其周围临近像素点的值的近似值。
图像平滑处理对应的是英文Smoothing Images。图像平滑处理通常伴随图像模糊操作, 因此图像平滑处理有时也被称为图像模糊处理, 图像模糊处理对应的英文是Blurring Images。
6.1 均值滤波
均值滤波是指用当前像素点周围N*N个像素值的均值来代替当前像素值。使用该方法遍历处理图像内的每一个像素点, 即可完成整幅图像的均值滤波。由一个归一化卷积框完成的。他只是用卷积框覆盖区域所有像素的平均值来代替中心元素。可以使用函数cv2.blur()和cv2.boxFilter()来完这个任务。
在OpenCV中, 实现均值滤波的函数是cv2.blur()
, 其语法格式为: dst = cv2.blur(src, ksize, anchor, borderType)
图像深度应该是CV_8U、CV_16U、CV_16S、CV_32F或者CV_64F中的一种。
滤波核大小是指在均值处理过程中, 其邻域图像的高度和宽度。例如, 其值可以为(5, 5), 表示以 5×5 大小的邻域均值作为图像均值滤波处理的结果。
6.2 方框滤波
OpenCV还提供了方框滤波方式, 与均值滤波的不同在于, 方框滤波不会计算像素均值。在方框滤波中, 可以自由选择是否对均值滤波的结果进行归一化, 即可以自由选择滤波结果是邻域像素值之和的平均值, 还是邻域像素值之和。
在OpenCV中, 实现方框滤波的函数是cv2.boxFilter()
, 其语法格式为:
dst = cv2.boxFilter(src, ddepth, ksize, anchor, normalize, borderType)
滤波核大小是指在滤波处理过程中所选择的邻域图像的高度和宽度。
6.3 高斯滤波
在进行均值滤波和方框滤波时, 其邻域内每个像素的权重是相等的。在高斯滤波中, 会将中心点的权重值加大, 远离中心点的权重值减小, 在此基础上计算邻域内各个像素值不同权重的和。
在高斯滤波中, 卷积核中的值不再都是1。高斯核的宽和高必须是奇数。如果两个标准差都是0, 那么函数会根据核函数的大小自己计算。高斯滤波可以有效的从图像中去除高斯噪音。
在OpenCV中, 实现高斯滤波的函数是cv2.GaussianBlur(), 该函数的语法格式是:
dst = cv2.GaussianBlur(src, ksize, sigmaX, sigmaY, borderType)
6.4 中值滤波
中值滤波不再采用加权求均值的方式计算滤波结果。它用邻域内所有像素值的中间值来替代当前像素点的像素值。
中值滤波会取当前像素点及其周围临近像素点(一共有奇数个像素点)的像素值, 将这些像素值排序, 然后将位于中间位置的像素值作为当前像素点的像素值。
这个滤波器经常用来去除椒盐噪声。
在OpenCV中, 实现中值滤波的函数是cv2.medianBlur()
, 其语法格式如下:
dst = cv2.medianBlur(src, ksize)
6.5 双边滤波
双边滤波是综合考虑空间信息和色彩信息的滤波方式, 在滤波过程中能够有效地保护图像内的边缘信息。
双边滤波在计算某一个像素点的新值时, 不仅考虑距离信息(距离越远, 权重越小), 还考虑色彩信息(色彩差别越大, 权重越小)。双边滤波综合考虑距离和色彩的权重结果, 既能够有效地去除噪声, 又能够较好地保护边缘信息。
在OpenCV中, 实现双边滤波的函数是cv2.bilateralFilter(), 该函数的语法是:
dst = cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace, borderType)
双边滤波的优势体现在对于边缘信息的处理上。
6.6 2D 卷积(自定义滤波)
在OpenCV中, 允许用户自定义卷积核实现卷积操作, 使用自定义卷积核实现卷积操作的。函数是cv2.filter2D(), 其语法格式为: dst = cv2.filter2D(src, ddepth, kernel, anchor, delta, borderType)
7. 形态学
形态学, 即数学形态学(Mathematical Morphology), 是图像处理过程中一个非常重要的研究方向。形态学主要从图像内提取分量信息, 该分量信息通常对于表达和描绘图像的形状具有重要意义, 通常是图像理解时所使用的最本质的形状特征。形态学处理在视觉检测、文字识别、医学图像处理、图像压缩编码等领域都有非常重要的应用。
形态学操作主要包含: 腐蚀、膨胀、开运算、闭运算、形态学梯度(Morphological Gradient)运算、顶帽运算(礼帽运算)、黑帽运算等操作。
7.1 腐蚀
腐蚀是最基本的形态学操作之一, 它能够将图像的边界点消除, 使图像沿着边界向内收缩, 也可以将小于指定结构体元素的部分去除。
腐蚀用来"收缩"或者"细化"二值图像中的前景, 借此实现去除噪声、元素分割等功能。
需要注意的是, 腐蚀操作等形态学操作是逐个像素地来决定值的, 每次判定的点都是与结构元中心点所对应的点。
在OpenCV中, 使用函数cv2.erode()
实现腐蚀操作, 其语法格式为:
dst = cv2.erode(src, kernel[, anchor[, iterations[, borderType[, borderValue]]]])
kernel代表腐蚀操作时所采用的结构类型。它可以自定义生成, 也可以通过函数cv2.getStructuringElement()
生成。
kernel = np.ones((5,1),np.uint8)
dst = cv2.erode(thumbnail_image, kernel)
cv2.imshow("dst", dst)
cv2.waitKey()
cv2.destroyAllWindows()
7.2 膨胀
膨胀操作是形态学中另外一种基本的操作。膨胀操作和腐蚀操作的作用是相反的, 膨胀操作能对图像的边界进行扩张。膨胀操作将与当前对象(前景)接触到的背景点合并到当前对象内, 从而实现将图像的边界点向外扩张。如果图像内两个对象的距离较近, 那么在膨胀的过程中, 两个对象可能会连通在一起。膨胀操作对填补图像分割后图像内所存在的空白相当有帮助。
在OpenCV内, 采用函数cv2.dilate()
实现对图像的膨胀操作, 该函数的语法结构为:
dst = cv2.dilate(src, kernel[, anchor[, iterations[, borderType[, borderValue]]]])
kernel = np.ones((7, 1), np.uint8)
dst = cv2.dilate(thumbnail_image, kernel)
cv2.imshow("dst", dst)
cv2.waitKey()
cv2.destroyAllWindows()
将腐蚀和膨胀操作进行组合, 就可以实现开运算、闭运算(关运算)、形态学梯度(Morphological Gradient)运算、礼帽运算(顶帽运算)、黑帽运算、击中击不中等多种不同形式的运算。
OpenCV提供了函数cv2.morphologyEx()
来实现上述形态学运算, 其语法结构如下:
dst = cv2.morphologyEx(src, op, kernel[, anchor[, iterations[, borderType[, borderValue]]]]])
7.3 开运算
开运算是先腐蚀、后膨胀的运算, 可以用于去噪、计数等: opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
7.4 闭运算
闭运算是先膨胀、后腐蚀的运算, 有助于关闭前景物体内部的小孔, 或去除物体上的小黑点, 还可以将不同的前景图像进行连接: closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
7.5 形态学梯度运算
形态学梯度运算是用图像的膨胀图像减腐蚀图像的操作, 该操作可以获取原始图像中前景图像的边缘。
result = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)
7.6 顶帽运算(礼帽运算)
礼帽运算是用原始图像减去其开运算图像的操作。礼帽运算能够获取图像的噪声信息, 或者得到比原始图像的边缘更亮的边缘信息。
result = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)
7.7 黑帽运算
黑帽运算是用闭运算图像减去原始图像的操作。黑帽运算能够获取图像内部的小孔, 或前景色中的小黑点, 或者得到比原始图像的边缘更暗的边缘部分。
其语法结构如下:
result = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)
7.8 核函数
在进行形态学操作时, 必须使用一个特定的核(结构元)。该核可以自定义生成, 也可以通过函数cv2.getStructuringElement()构造。函数cv2.getStructuringElement()能够构造并返回一
个用于形态学处理所使用的结构元素。该函数的语法格式为:
retval = cv2.getStructuringElement(shape, ksize[, anchor])
8. 图像梯度
图像梯度计算的是图像变化的速度。对于图像的边缘部分, 其灰度值变化较大, 梯度值也较大; 相反, 对于图像中比较平滑的部分, 其灰度值变化较小, 相应的梯度值也较小。一般情况下, 图像梯度计算的是图像的边缘信息。
严格来讲, 图像梯度计算需要求导数, 但是图像梯度一般通过计算像素值的差来得到梯度的近似值(近似导数值)。
8.1 Sobel理论基础
Sobel算子是一种离散的微分算子, 该算子结合了高斯平滑和微分求导运算。该算子利用局部差分寻找边缘, 计算所得的是一个梯度的近似值。
滤波器通常是指由一幅图像根据像素点(x, y)临近的区域计算得到另外一幅新图像的算法。因此, 滤波器是由邻域及预定义的操作构成的。滤波器规定了滤波时所采用的形状以及该区域内像素值的组成规律。滤波器也被称为"掩模”、“核”、“模板”、“窗口”、“算子"等。一般信号领域将其称为"滤波器”, 数学领域将其称为"核"。线性滤波器", 也就是说, 滤波的目标像素点的值等于原始像素值及其周围像素值的加权和。这种基于线性核的滤波, 就是我们所熟悉的卷积。
计算水平方向偏导数的近似值
将Sobel算子与原始图像src进行卷积计算, 可以计算水平方向上的像素值变化情况。例如, 当Sobel算子的大小为 3×3 时, 水平方向偏导数Gx的计算方式为:
\[G_x = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & -1 \\ -1 & 0 & 1\end{bmatrix} \cdot src \]
计算垂直方向偏导数的近似值
将Sobel算子与原始图像src进行卷积计算, 可以计算垂直方向上的变化情况。例如, 当Sobel算子的大小为3×3时, 垂直方向偏导数Gy的计算方式为:
\[G_y = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1\end{bmatrix} \cdot src \]
在OpenCV内, 使用函数cv2.Sobel()
实现Sobel算子运算, 其语法形式为:
dst = cv2.Sobel(src, ddepth, dx, dy[,ksize[, scale[, delta[, borderType]]]])
在函数cv2.Sobel()的语法中规定, 可以将函数cv2.Sobel()内ddepth参数的值设置为-1, 让处理结果与原始图像保持一致。但是, 如果直接将参数ddepth的值设置为-1, 在计算时得到的结果可能是错误的。所以, 通常要将函数cv2.Sobel()内参数ddepth的值设置为"cv2.CV_64F"。
在OpenCV中, 使用函数cv2.convertScaleAbs()
对参数取绝对值, 语法格式为: dst = cv2.convertScaleAbs(src [, alpha[, beta]])
如果ksize=-1, 会使用3x3的Scharr滤波器, 它的的效果要比3x3的Sobel滤波器好(而且速度相同, 所以在使用3x3滤波器时应该尽量使用Scharr滤波器)。
8.2 Scharr算子
OpenCV提供了Scharr算子, 该算子具有和Sobel算子同样的速度, 且精度更高。可以将Scharr算子看作对Sobel算子的改进, 其核通常为:
\[G_x \begin{bmatrix} -3 & 0 & 3 \\-10 & 0 & 10 \\-3 & 0 & 3 \end{bmatrix} \]
\[G_y \begin{bmatrix} -3 & -10 & -3 \\0 & 0 & 10 \\3 & 10 & 3 \end{bmatrix} \]
OpenCV提供了函数cv2.Scharr()
来计算Scharr算子, 其语法格式如下:
dst = cv2.Scharr(src, ddepth, dx, dy[, scale[, delta[, borderType]]])
函数cv2.Scharr()
和函数cv2.Sobel()
的使用方式基本一致。
Sobel算子和Scharr算子的比较
Sobel算子的缺点是, 当其核结构较小时, 精确度不高, 而Scharr算子具有更高的精度。
8.3 Laplacian 算子
Laplacian(拉普拉斯)算子是一种二阶导数算子, 其具有旋转不变性, 可以满足不同方向的图像边缘锐化(边缘检测)的要求。通常情况下, 其算子的系数之和需要为零。
Laplacian算子类似二阶Sobel导数, 需要计算两个方向的梯度值。
在OpenCV内使用函数cv2.Laplacian()实现Laplacian算子的计算, 该函数的语法格式为:
dst = cv2.Laplacian(src, ddepth[, ksize[, scale[, delta[, borderType]]]])
拉普拉斯滤波器使用的卷积核:
\[kernel = \begin{bmatrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \end{bmatrix} \]
9. Canny 边缘检测
Canny边缘检测是一种使用多级边缘检测算法检测边缘的方法。1986年, JohnF.Canny发表了著名的论文A Computational Approach to Edge Detection, 在该论文中详述了如何进行边缘检测。
OpenCV提供了函数cv2.Canny()实现Canny边缘检测, 其语法形式如下:
edges = cv.Canny(image, threshold1, threshold2[, apertureSize[, L2gradient]])
- threshold1表示处理过程中的第一个阈值。
- threshold2表示处理过程中的第二个阈值。
- apertureSize表示Sobel算子的孔径大小。
- L2gradient为计算图像梯度幅度(gradient magnitude)的标识。其默认值为False。如果为True, 则使用更精确的L2范数进行计算(即两个方向的导数的平方和再开方), 否则使用L1范数(直接将两个方向导数的绝对值相加)。
\[norm= \begin{cases} |d𝐼/dx|+|d𝐼/dy|, & \text L2gradient = False \\ \sqrt{(d𝐼/dx)^2 + (d𝐼/dy)^2}, & \text L2gradient = True\end{cases} \]
Canny 边缘检测分为如下几个步骤。
9.1 去噪。噪声会影响边缘检测的准确性, 因此首先要将噪声过滤掉。
滤波的目的是平滑一些纹理较弱的非边缘区域, 以便得到更准确的边缘。在实际处理过程中, 通常采用高斯滤波去除图像中的噪声。
滤波器的大小也是可变的, 高斯核的大小对于边缘检测的效果具有很重要的作用。滤波器的核越大, 边缘信息对于噪声的敏感度就越低。不过, 核越大, 边缘检测的定位错误也会随之增加。通常来说, 一个 5×5 的核能够满足大多数的情况。
9.2 计算梯度的幅度与方向。
边缘检测算子返回水平方向的Gx和垂直方向的Gy。梯度的幅度𝐺和方向𝛩(用角度值表示)为:
\[G = \sqrt{G_x^2 + G_y^2} \\ \Theta= atan2(G_y,G_x) \]
式中, atan2(•)表示具有两个参数的arctan函数。
9.3 非极大值抑制, 即适当地让边缘"变瘦"。
在获得了梯度的幅度和方向后, 遍历图像中的像素点, 去除所有非边缘的点。在具体实现时, 逐一遍历像素点, 判断当前像素点是否是周围像素点中具有相同梯度方向的最大值, 并根据判断结果决定是否抑制该点。通过以上描述可知, 该步骤是边缘细化的过程。针对每一个像素点:
- 如果该点是正/负梯度方向上的局部最大值, 则保留该点。
- 如果不是, 则抑制该点(归零)。
9.4 确定边缘。使用双阈值算法确定最终的边缘信息。
完成上述步骤后, 图像内的强边缘已经在当前获取的边缘图像内。但是, 一些虚边缘可能也在边缘图像内。这些虚边缘可能是真实图像产生的, 也可能是由于噪声所产生的。对于后者, 必须将其剔除。
设置两个阈值, 其中一个为高阈值maxVal, 另一个为低阈值minVal。根据当前边缘像素的梯度值(指的是梯度幅度, 下同)与这两个阈值之间的关系, 判断边缘的属性。具体步骤为:
- 如果当前边缘像素的梯度值大于或等于maxVal, 则将当前边缘像素标记为强边缘。
- 如果当前边缘像素的梯度值介于maxVal与minVal之间, 则将当前边缘像素标记为虚边缘(需要保留)。
- 如果当前边缘像素的梯度值小于或等于minVal, 则抑制当前边缘像素。
在上述过程中, 我们得到了虚边缘, 需要对其做进一步处理。一般通过判断虚边缘与强边缘是否连接, 来确定虚边缘到底属于哪种情况。通常情况下, 如果一个虚边缘:
- 与强边缘连接, 则将该边缘处理为边缘。
- 与强边缘无连接, 则该边缘为弱边缘, 将其抑制。
10. 图像金字塔
图像金字塔是由一幅图像的多个不同分辨率的子图所构成的图像集合。该组图像是由单个图像通过不断地降采样所产生的, 最小的图像可能仅仅有一个像素点。
通常情况下, 图像金字塔的底部是待处理的高分辨率图像(原始图像), 而顶部则为其低分辨率的近似图像。向金字塔的顶部移动时, 图像的尺寸和分辨率都不断地降低。通常情况下, 每向上移动一级, 图像的宽和高都降低为原来的二分之一。
最简单的图像金字塔可以通过不断地删除图像的偶数行和偶数列得到。例如, 有一幅图像, 其大小是M*N
, 删除其偶数行和偶数列后得到一幅(M/2)*(N/2)
大小的图像。经过上述处理后, 图像大小变为原来的四分之一, 不断地重复该过程, 就可以得到该图像的图像金字塔。这被称为Octave。
也可以先对原始图像滤波, 得到原始图像的近似图像, 然后将近似图像的偶数行和偶数列删除以获取向下采样的结果。有多种滤波器可以选择。例如:
- 邻域滤波器: 采用邻域平均技术求原始图像的近似图像。该滤波器能够产生平均金字塔。
- 高斯滤波器: 采用高斯滤波器对原始图像进行滤波, 得到高斯金字塔。这是OpenCV函数cv2.pyrDown()所采用的方式。
10.1 pyrDown 函数及使用
OpenCV提供了函数cv2.pyrDown()
, 用于实现图像高斯金字塔操作中的向下采样, 其语法形式为:
dst = cv2.pyrDown(src[, dstsize[, borderType]])
10.2 pyrUp 函数及使用
在OpenCV中, 使用函数cv2.pyrUp()
实现图像金字塔操作中的向上采样, 其语法形式如下:
dst = cv2.pyrUp(src[, dstsize[, borderType]])
10.3 拉普拉斯金字塔
拉普拉斯金字塔的定义形式为:
\(L_i = G_i - pyrUp(G_i + 1)\)
拉普拉斯金字塔的作用在于, 能够恢复高分辨率的图像。
图像金字塔的一个应用是图像融合。例如, 在图像缝合中, 你需要将两幅图叠在一起, 但是由于连接区域图像像素的不连续性, 整幅图的效果看起来会很差。这时图像金字塔就可以排上用场了, 他可以帮你实现无缝连接。
11. 图像轮廓
边缘检测虽然能够检测出边缘, 但边缘是不连续的, 检测到的边缘并不是一个整体。图像轮廓是指将边缘连接起来形成的一个整体, 用于后续的计算。图像轮廓是图像中非常重要的一个特征信息, 通过对图像轮廓的操作, 我们能够获取目标图像的大小、位置、方向等信息。
OpenCV提供了查找图像轮廓的函数cv2.findContours()
, 该函数能够查找图像内的轮廓信息, 而函数cv2.drawContours()
能够将轮廓绘制出来。
从OpenCV 3.2开始, findContours()
不再修改源图像。
11.1 查找并绘制轮廓
函数cv2.findContours()的语法格式为:
# OpenCV4.x
contours, hierarchy = cv2.findContours(image, mode, method)
- contours: 返回的轮廓。
contours的type属性是list类型, list的每个元素都是图像的一个轮廓, 用Numpy中的ndarray结构表示。每一个轮廓都是由若干个像素点构成的, 点的个数不固定, 具体个数取决于轮廓的形状。 - hierarchy: 图像的拓扑信息(轮廓层次)。
图像内的轮廓可能位于不同的位置。比如, 一个轮廓在另一个轮廓的内部。在这种情况下, 我们将外部的轮廓称为父轮廓, 内部的轮廓称为子轮廓。按照上述关系分类, 一幅图像中所有轮廓之间就建立了父子关系。根据轮廓之间的关系, 就能够确定一个轮廓与其他轮廓是如何连接的。
每个轮廓contours[i]对应4个元素来说明当前轮廓的层次关系。其形式为:[Next, Previous, First_Child, Parent]
轮廓的层次结构是由参数mode决定的。也就是说, 使用不同的mode, 得到轮廓的编号是不一样的, 得到的hierarchy也不一样。
输出值"[1 -1 -1 -1]", 表示的是第0个轮廓的层次。它的前一个轮廓不存在, 因此第2个元素的值是"-1"。它不存在子轮廓, 因此第3个元素的值是"-1"。它不存在父轮廓, 因此第4个元素的值是"-1"。
-
mode: 轮廓检索模式
- cv2.RETR_EXTERNAL: 只会返回最外边的的轮廓, 所有的子轮廓都会被忽略掉。
- cv2.RETR_CCOMP: 返回所有的轮廓并将轮廓分为两级组织结构。
- cv2.RETR_LIST: 只是提取所有的轮廓, 而不去创建任何父子关系。
- cv2.RETR_TREE: 会返回所有轮廓, 并且创建一个完整的组织结构列表。
-
method: 轮廓的近似方法
- cv2.CHAIN_APPROX_NONE: 所有的边界点都会被存储。
- cv2.CHAIN_APPROX_SIMPLE: 将轮廓上的冗余点都去掉, 压缩轮廓, 从而节省内存开支。
在使用函数cv2.findContours()查找图像轮廓时, 需要注意以下问题:
- 待处理的源图像必须是灰度二值图。因此, 在通常情况下, 都要预先对图像进行阈值分割或者边缘检测处理, 得到满意的二值图像后再将其作为参数使用。
- 在 OpenCV中, 都是从黑色背景中查找白色对象。因此, 对象必须是白色的, 背景必须是黑色的。
- 在 OpenCV 4.x中, 函数cv2.findContours()仅有两个返回值。
在 OpenCV中, 可以使用函数cv2.drawContours()绘制图像轮廓。该函数的语法格式是:
image = cv2.drawContours(image,contours,contourIdx,color[,thickness[,lineType[,hierarchy[,maxLevel[,offset]]]]])
11.2 矩特征
比较两个轮廓最简单的方法是比较二者的轮廓矩。轮廓矩代表了一个轮廓、一幅图像、一组点集的全局特征。矩信息包含了对应对象不同类型的几何特征, 例如大小、位置、角度、形状等。矩特征被广泛地应用在模式识别、图像识别等方面。
OpenCV提供了函数cv2.moments()来获取图像的moments特征。通常情况下, 我们将使用函数cv2.moments()获取的轮廓特征称为"轮廓矩"。轮廓矩描述了一个轮廓的重要特征, 使用轮廓矩可以方便地比较两个轮廓。
retval = cv2.moments(array[, binaryImage])
该函数的返回值 retval 是矩特征, 主要包括:
-
空间矩: 它们是图像像素值与位置的乘积的和。空间矩的定义如下:
- 零阶矩(M00): 表示图像的面积(或像素的总和)。
- 一阶矩(M10, M01): 分别表示图像在x和y方向上的加权和。
- 二阶矩(M20, M11, M02): 表示图像在x和y方向上的加权平方和。
-
中心矩: 是相对于图像质心的矩, 它们不受图像平移的影响, 定义如下:
\[\mu_{ij} = \sum x \sum y(x-\overline{x})^i(y-\overline{y})^jI(x, y) \]
其中, \(\overline{x}\) 和 \(\overline{y}\) 是图像的质心坐标, 计算公式为:
\[\overline{x} = \frac{M_{10}}{M_{00}}, \overline{y} = \frac{M_{01}}{M_{00}} \]
- 归一化中心矩:是对中心矩进行归一化处理,使得它们不受图像尺度的影响。
很明显, 如果两个轮廓的矩一致, 那么这两个轮廓就是一致的。虽然大多数矩都是通过数学公式计算得到的抽象特征, 但是零阶矩"m00"的含义比较直观, 它表示一个轮廓的面积。
函数cv2.contourArea()用于计算轮廓的面积, 语法格式为: retval =cv2.contourArea(contour [, oriented]))
- oriented是布尔型值。当它为True时, 返回的值包含正/负号, 用来表示轮廓是顺时针还是逆时针的。该参数的默认值是False, 表示返回的retval是一个绝对值。
函数cv2.arcLength()用于计算轮廓的长度, 语法格式为: retval = cv2.arcLength(curve, closed)
- closed是布尔型值, 用来表示轮廓是否是封闭的。该值为True时, 表示轮廓是封闭的。
11.3 Hu矩
Hu矩是归一化中心矩的线性组合。Hu矩在图像旋转、缩放、平移等操作后, 仍能保持矩的不变性, 所以经常会使用Hu距来识别图像的特征。在OpenCV中, 使用函数cv2.HuMoments()可以得到Hu距。该函数使用cv2.moments()函数的返回值作为参数, 返回7个Hu矩值。
hu = cv2.HuMoments(m)
- 参数 m, 是由函数 cv2.moments()计算得到矩特征值。
Hu矩是归一化中心矩的线性组合, 每一个矩都是通过归一化中心矩的组合运算得到的。我们可以通过Hu矩来判断两个对象的一致性。为了更直观方便地比较Hu矩值, OpenCV提供了函数cv2.matchShapes(), 对两个对象的Hu矩进行比较。
retval = cv2.matchShapes(contour1, contour2, method, parameter)
11.4 轮廓拟合
在计算轮廓时, 可能并不需要实际的轮廓, 而仅需要一个接近于轮廓的近似多边形。OpenCV 提供了多种计算轮廓近似多边形的方法。
- 函数cv2.boundingRect()能够绘制轮廓的矩形边界。该函数的语法格式为:
retval = cv2.boundingRect(array)
# x,y,w,h = cv2.boundingRect(array)
-
返回值retval表示返回的矩形边界的左上角顶点的坐标值及矩形边界的宽度和高度。
-
参数array是灰度图像或轮廓。
-
函数cv2.minAreaRect()能够绘制轮廓的最小包围矩形框:
retval =cv2.minAreaRect(points)
返回值retval的结构是(最小外接矩形的中心(x,y),(宽度,高度),旋转角度)
, 不符合函数cv2.drawContours()的参数结构要求。因此, 必须将其转换为符合要求的结构, 才能使用。函数cv2.boxPoints()能够将上述返回值retval转换为符合要求的结构。函数cv2.boxPoints()的语法格式是:points = cv2.boxPoints(box)
-
函数cv2.minEnclosingCircle()通过迭代算法构造一个对象的面积最小包围圆形:
center, radius = cv2.minEnclosingCircle(points)
-
在OpenCV中, 函数cv2.fitEllipse()可以用来构造最优拟合椭圆:
retval = cv2.fitEllipse(points)
函数返回的是拟合椭圆的外接矩形, retval包含外接矩形的质心、宽、高、旋转角度等参数信息, 这些信息正好与椭圆的中心点、轴长度、旋转角度等信息吻合。
注意: 形状必须至少包含5个点。 -
在OpenCV中, 函数cv2.fitLine()用来构造最优拟合直线:
line = cv2.fitLine(points, distType, param, reps, aeps)
- distType: 距离类型。拟合直线时, 要使输入点到拟合直线的距离之和最小
-
在OpenCV中, 函数cv2.minEnclosingTriangle()用来构造最小外包三角形:
retval, triangle = cv2.minEnclosingTriangle(points)
-
函数cv2.approxPolyDP()用来构造指定精度的逼近多边形曲线:
approxCurve = cv2.approxPolyDP(curve, epsilon, closed)
- 采用的是Douglas-Peucker算法(DP算法)
11.5 凸包
逼近多边形是轮廓的高度近似, 但是有时候, 我们希望使用一个多边形的凸包来简化它。凸包跟逼近多边形很像, 只不过它是物体最外层的"凸"多边形。凸包指的是完全包含原有轮廓, 并且仅由轮廓上的点所构成的多边形。凸包的每一处都是凸的, 即在凸包内连接任意两点的直线都在凸包的内部。在凸包内, 任意连续三个点的内角小于180°。
-
OpenCV提供函数cv2.convexHull()用于获取轮廓的凸包:
hull = cv2.convexHull(points[, clockwise[, returnPoints]])
-
凸包与轮廓之间的部分, 称为凸缺陷。在OpenCV中使用函数cv2.convexityDefects()获取凸缺陷:
convexityDefects = cv2.convexityDefects(contour, convexhull)
-
在OpenCV中, 可以用函数cv2.isContourConvex()来判断轮廓是否是凸形的, 其语法格式为:
retval = cv2.isContourConvex(contour)
-
在OpenCV中, 函数cv2.pointPolygonTest()被用来计算点到多边形(轮廓)的最短距离(也就是垂线距离), 这个计算过程又称点和多边形的关系测试:
retval = cv2.pointPolygonTest(contour, pt, measureDist)
使用函数cv2.pointPolygonTest()判断点与轮廓的关系时, 需要将参数measureDist的值设置为False。
11.6 利用形状场景算法比较轮廓
用矩比较形状是一种非常有效的方法, 不过现在有了更有效的方法。从OpenCV 3开始, 有了专有模块shape, 该模块中的形状场景算法能够更高效地比较形状。
OpenCV提供了使用"距离"作为形状比较的度量标准。这是因为形状之间的差异值和距离有相似之处, 比如二者都只能是零或者正数, 又比如当两个形状一模一样时距离值和差值都等于零。
OpenCV 提供了函数 cv2.createShapeContextDistanceExtractor(), 用于计算形状场景距离。其使用的"形状上下文算法"在计算距离时, 在每个点上附加一个"形状上下文"描述符, 让每个点都能够捕获剩余点相对于它的分布特征, 从而提供全局鉴别特征。
函数 cv2.createShapeContextDistanceExtractor()的语法格式为:
retval = cv2.createShapeContextDistanceExtractor(
[, nAngularBins[,
nRadialBins[,
innerRadius[,
outerRadius[,
iterations[,
comparer[,
transformer]]]]]]]
)
该结果可以通过函数cv2.ShapeDistanceExtractor.computeDistance()计算两个不同形状之间的距离: retval = cv2.ShapeDistanceExtractor.computeDistance(contour1, contour2)
11.6.1 计算Hausdorff距离
Hausdorff距离的计算方法是:
- 针对图像A内的每一个点, 寻找其距离图像B的最短距离, 将这个最短距离作为Hausdorff直接距离D1。
- 针对图像B内的每一个点, 寻找其距离图像A的最短距离, 将这个最短距离作为Hausdorff直接距离D2。
- 将上述D1、D2中的较大者作为Hausdorff距离。
通常情况下, Hausdorff距离H(·)是根据对象A和对象B之间的Hausdorff直接距离h(·)来定义的, 用数学公式表式如下:
\(H(A, B) = max(h(A, B), h(B, A))\)
其中:
\(h(A, B) = max_{a \in A} \quad min_{b \in B} || a-b ||\)
OpenCV提供了函数cv2.createHausdorffDistanceExtractor()来计算Hausdorff距离: retval = cv2.createHausdorffDistanceExtractor([, distanceFlag [, rankProp]])
11.7 轮廓的特征值
轮廓自身的一些属性特征及轮廓所包围对象的特征对于描述图像具有重要意义。
- 宽高比:可以使用宽高比(AspectRation)来描述轮廓, 例如矩形轮廓的宽高比为:
宽高比=宽度(Width)/高度(Height)
- 可以使用轮廓面积与矩形边界(矩形包围框、矩形轮廓)面积之比Extend来描述图像及其轮廓特征。
\[Extend = \frac{轮廓面积(对象面积)}{矩形边界面积} \]
- 可以使用轮廓面积与凸包面积之比Solidity来衡量图像、轮廓及凸包的特征。其计算方法为:
\[Solidity = \frac{轮廓面积(对象面积)}{凸包面积} \]
- 可以用等效直径来衡量轮廓的特征值, 该值是与轮廓面积相等的圆形的直径。其计算公式为:
\[等效直径 = \sqrt{\frac{4 × 轮廓面积}{\pi}} \]
-
在OpenCV中, 函数cv2.fitEllipse()可以用来构造最优拟合椭圆, 还可以在返回值内分别返回椭圆的中心点、轴长、旋转角度等信息。使用这种形式, 能够更直观地获取椭圆的方向等信息。
-
使用Numpy函数获取轮廓像素点
- numpy.nonzero()函数能够找出数组内非零元素的位置, 但是其返回值是将行、列分别显示的。使用numpy.transpose()函数处理上述返回值, 则得到这些点的(x, y)形式的坐标
-
使用OpenCV函数获取轮廓点
- OpenCV提供了函数cv2.findNonZero()用于查找非零元素的索引:
idx = cv2.findNonZero(src)
- OpenCV提供了函数cv2.findNonZero()用于查找非零元素的索引:
-
OpenCV提供了函数cv2.minMaxLoc(), 用于在指定的对象内查找最大值、最小值及其位置:
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(imgray,mask = mask)
- OpenCV提供了函数cv2.mean(), 用于计算一个对象的平均颜色或平均灰度为:
mean_val = cv2.mean(im,mask = mask)
- 获取某个对象内的极值点, 例如最左端、最右端、最上端、最下端的四个点。 OpenCV 提供了相应的函数来找出这些点, 通常的语法格式是:
leftmost = tuple(cnt[cnt[:,:,0].argmin()][0])
rightmost = tuple(cnt[cnt[:,:,0].argmax()][0])
topmost = tuple(cnt[cnt[:,:,1].argmin()][0])
bottommost = tuple(cnt[cnt[:,:,1].argmax()][0])
12. 直方图处理
直方图是图像处理过程中的一种非常重要的分析工具。直方图从图像内部灰度级的角度对图像进行表述, 包含十分丰富而重要的信息。从直方图的角度对图像进行处理, 可以达到增强图像显示效果的目的。
从统计的角度讲, 直方图是图像内灰度值的统计特性与图像灰度值之间的函数, 直方图统计图像内各个灰度级出现的次数。从直方图的图形上观察, 横坐标是图像中各像素点的灰度级, 纵坐标是具有该灰度级(像素值)的像素个数。
图像直方图的x轴区间一般是[0,255], 对应的是8位位图的256个灰度级; y轴对应的是具有相应灰度级的像素点的个数。有时为了便于表示, 也会采用归一化直方图。在归一化直方图中, x轴仍然表示灰度级; y轴不再表示灰度级出现的次数, 而是灰度级出现的频率。
直方图均衡化经常用来使所有的图片具有相同的亮度条件的参考工具。这在很多情况下都很有用。例如, 脸部识别, 在训练分类器前, 训练集的所有图片都要先进行直方图均衡化从而使它们达到相同的亮度条件。
在OpenCV的官网上, 特别提出了要注意三个概念: DIMS、BINS、RANGE。
- DIMS: 表示在绘制直方图时, 收集的参数的数量。一般情况下, 直方图中收集的数据只有一种, 就是灰度级。因此, 该值为1。
- RANGE: 表示要统计的灰度级范围, 一般为[0,255]。0对应的是黑色, 255对应的是白色。
- BINS: 参数子集的数目。在处理数据的过程中, 有时需要将众多的数据划分为若干个组, 再进行分析。
12.1 绘制直方图
Python的模块matplotlib.pyplot中的hist()函数能够方便地绘制直方图, 我们通常采用该函数直接绘制直方图。除此以外, OpenCV中的cv2.calcHist()函数能够计算统计直方图, 还可以在此基础上绘制图像的直方图。
-
使用Numpy绘制直方图
模块matplotlib.pyplot提供了一个类似于MATLAB绘图方式的框架, 可以使用其中的matplotlib.pyplot.hist()函数(以下简称为hist()函数)来绘制直方图。此函数的作用是根据数据源和灰度级分组绘制直方图。其基本语法格式为:matplotlib.pyplot.hist(X, BINS)
- X: 数据源, 必须是一维的。图像通常是二维的, 需要使用ravel()函数将图像处理为一维数据源以后, 再作为参数使用。
- BINS: BINS 的具体值, 表示灰度级的分组情况。
-
使用OpenCV绘制直方图
OpenCV提供了函数cv2.calcHist()用来计算图像的统计直方图, 该函数能统计各个灰度级的像素点个数。利用matplotlib.pyplot模块中的plot()函数, 可以将函数cv2.calcHist()的统计结果绘制成直方图:hist = cv2.calcHist(images, channels, mask, histSize, ranges, accumulate)
12.2 直方图均衡化
如果一幅图像拥有全部可能的灰度级, 并且像素值的灰度均匀分布, 那么这幅图像就具有高对比度和多变的灰度色调, 灰度级丰富且覆盖范围较大。在外观上, 这样的图像具有更丰富的色彩, 不会过暗或过亮。
直方图均衡化的主要目的是将原始图像的灰度级均匀地映射到整个灰度级范围内, 得到一个灰度级分布均匀的图像。这种均衡化, 既实现了灰度值统计上的概率均衡, 也实现了人类视觉系统(Human Visual System, HVS)上的视觉均衡。
直方图均衡化的算法主要包括两个步骤:
-
计算累计直方图。
-
对累计直方图进行区间转换
-
在原有范围内实现均衡化
在原有范围内实现直方图均衡化时, 用当前灰度级的累计概率乘以当前灰度级的最大值7, 得到新的灰度级, 并作为均衡化的结果。 -
在更广泛的范围内实现均衡化
在更广泛的范围内实现直方图均衡化时, 用当前灰度级的累计概率乘以更广泛范围灰度级的最大值, 得到新的灰度级, 并作为均衡化的结果。
OpenCV使用函数cv2.equalizeHist()实现直方图均衡化: dst = cv2.equalizeHist(src)
12.2.1 CLAHE 自适应的直方图均衡化
整幅图像会被分成很多小块, 这些小块被称为"tiles"(在OpenCV中tiles的大小默认是8x8), 然后再对每一个小块分别进行直方图均衡化(跟前面类似)。
对于每个小块来说, 如果直方图中的bin超过对比度的上限的话, 就把其中的像素点均匀分散到其他bins中, 然后在进行直方图均衡化。最后, 为了去除每一个小块之间"人造的"(由于算法造成)边界, 再使用双线性差值, 对小块进行缝合。
import numpy as np
import cv2
img = cv2.imread('tsukuba_l.png',0)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
cl1 = clahe.apply(img)
12.3 2D 直方图
使用函数cv2.calcHist()来计算直方图既简单又方便。如果要绘制颜色直方图的话, 我们首先需要将图像的颜色空间从BGR转换到 HSV。(记住, 计算一维直方图, 要从BGR转换到HSV)。计算2D直方图, 函数的参数要做如下修改:
- channels=[0, 1]: 因为我们需要同时处理H和S两个通道。
- bins=[180, 256]: H通道为180, S通道为256。
- range=[0, 180, 0, 256]: H的取值范围在0到180, S的取值范围在0到256。
12.4 直方图反向投影
直方图反向投影是由Michael J. Swain和Dana H. Ballard在他们的文章"Indexing via color histograms"中提出。
它可以用来做图像分割, 或者在图像中找寻我们感兴趣的部分。简单来说, 它会输出与输入图像(待搜索)同样大小的图像, 其中的每一个像素值代表了输入图像上对应点属于目标对象的概率。直方图投影经常与camshift算法等一起使用。
OpenCV提供的函数cv2.calcBackProject()可以用来做直方图反向投影。它的参数与函数cv2.calcHist()的参数基本相同。
13. 傅里叶变换
图像处理一般分为空间域处理和频率域处理。
空间域处理是直接对图像内的像素进行处理。空间域处理主要划分为灰度变换和空间滤波两种形式。
频率域处理是先将图像变换到频率域, 然后在频率域对图像进行处理, 最后再通过反变换将图像从频率域变换到空间域。傅里叶变换是应用最广泛的一种频域变换, 它能够将图像从空间域变换到频率域, 而逆傅里叶变换能够将频率域信息变换到空间域内。傅里叶变换在图像处理领域内有着非常重要的作用。
法国数学家傅里叶指出, 任何周期函数都可以表示为不同频率的正弦函数和的形式。
在图像处理过程中, 傅里叶变换就是将图像分解为正弦分量和余弦分量两部分, 即将图像从空间域转换到频率域(以下简称频域)。数字图像经过傅里叶变换后, 得到的频域值是复数。因此, 显示傅里叶变换的结果需要使用实数图像(real image)加虚数图像(complex image), 或者幅度图像(magnitude image)加相位图像(phase image)的形式。
因为幅度图像包含了原图像中我们所需要的大部分信息, 所以在图像处理过程中, 通常仅使用幅度图像。当然, 如果希望先在频域内对图像进行处理, 再通过逆傅里叶变换得到修改后的空域图像, 就必须同时保留幅度图像和相位图像。
对图像进行傅里叶变换后, 我们会得到图像中的低频和高频信息。低频信息对应图像内变化缓慢的灰度分量。高频信息对应图像内变化越来越快的灰度分量, 是由灰度的尖锐过渡造成的。
傅里叶变换的目的, 就是为了将图像从空域转换到频域, 并在频域内实现对图像内特定对象的处理, 然后再对经过处理的频域图像进行逆傅里叶变换得到空域图像。傅里叶变换在图像处理领域发挥着非常关键的作用, 可以实现图像增强、图像去噪、边缘检测、特征提取、图像压缩和加密等。
13.1 Numpy实现傅里叶变换
Numpy模块提供了傅里叶变换功能, Numpy模块中的fft2()函数可以实现图像的傅里叶变换, 它的语法格式是: 返回值 = numpy.fft.fft2(原始图像)
注意的是, 参数"原始图像"的类型是灰度图像, 函数的返回值是一个复数数组(complex ndarray)。
经过该函数的处理, 就能得到图像的频谱信息。此时, 图像频谱中的零频率分量位于频谱图像的左上角, 为了便于观察, 通常会使用numpy.fft.fftshift()函数将零频率成分移动到频域图像的中心位置: 返回值=numpy.fft.fftshift(原始频谱)
对图像进行傅里叶变换后, 得到的是一个复数数组。为了显示为图像, 需要将它们的值调整到[0, 255]的灰度空间内: 像素新值=20*np.log(np.abs(频谱值))
逆傅里叶变换过程中, 需要先使用numpy.fft.ifftshift()
函数将零频率分量移到原来的位置, 再进行逆傅里叶变换: 调整后的频谱 = numpy.fft.ifftshift(原始频谱)
numpy.fft.ifft2()函数可以实现逆傅里叶变换, 返回空域复数数组。它是numpy.fft.fft2()的逆函数: 返回值=numpy.fft.ifft2(频域数据)
滤波器能够允许一定频率的分量通过或者拒绝其通过, 按照其作用方式可以划分为低通滤波器和高通滤波器。
- 允许低频信号通过的滤波器称为低通滤波器。低通滤波器使高频信号衰减而对低频信号放行, 会使图像变模糊。
- 允许高频信号通过的滤波器称为高通滤波器。高通滤波器使低频信号衰减而让高频信号通过, 将增强图像中尖锐的细节, 但是会导致图像的对比度降低。
13.2 OpenCV 实现傅里叶变换
OpenCV提供了函数cv2.dft()和cv2.idft()来实现傅里叶变换和逆傅里叶变换。
函数cv2.dft()的语法格式为: 返回结果=cv2.dft(原始图像, 转换标识)
- “转换标识"的值通常为"cv2.DFT_COMPLEX_OUTPUT”, 用来输出一个复数阵列.
- 函数cv2.dft()返回的结果与使用Numpy进行傅里叶变换得到的结果是一致的, 但是它返回的值是双通道的, 第1个通道是结果的实数部分, 第2个通道是结果的虚数部分。
此时, 零频率分量并不在中心位置, 为了处理方便需要将其移至中心位置, 可以用函数numpy.fft.fftshift()实现。
经过上述处理后, 频谱图像还只是一个由实部和虚部构成的值。要将其显示出来, 还要做进一步的处理才行。函数 cv2.magnitude()可以计算频谱信息的幅度: 返回值=cv2.magnitude(参数 1, 参数 2)
得到频谱信息的幅度后, 通常还要对幅度值做进一步的转换, 以便将频谱信息以图像的形式展示出来。简单来说, 就是需要将幅度值映射到灰度图像的灰度空间[0, 255]内, 使其以灰度图像的形式显示出来:
result = 20*np.log(cv2.magnitude(实部,虚部))
在OpenCV中, 使用函数cv2.idft()实现逆傅里叶变换, 该函数是傅里叶变换函数cv2.dft()的逆函数: 返回结果=cv2.idft(原始数据)
14. 模板匹配
模板匹配是指在当前图像A内寻找与图像B最相似的部分, 一般将图像A称为输入图像, 将图像B称为模板图像。
在OpenCV内, 模板匹配是使用函数cv2.matchTemplate()实现的为: result = cv2.matchTemplate(image, templ, method[, mask ])
- templ为模板图像。它的尺寸必须小于或等于原始图像, 并且与原始图像具有同样的类型。
- 返回值result是由每个位置的比较结果组合所构成的一个结果集, 类型是单通道32位浮点型。如果输入图像(原始图像)尺寸是WH, 模板的尺寸是wh, 则返回值的大小为(W-w+1)*(H-h+1)。
在查找最佳匹配时, 首先要确定使用的是何种method, 然后再确定到底是查找最大值, 还是查找最小值。
查找最值(极值)与最值所在的位置, 可以使用cv2.minMaxLoc()函数实现: minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(src [, mask])
有些情况下, 要搜索的模板图像很可能在输入图像内出现了多次, 这时就需要找出多个匹配结果。
函数where()能够获取模板匹配位置的集合。对于不同的输入, 其返回的值是不同的。
15. 霍夫变换
霍夫变换是一种在图像中寻找直线、圆形以及其他简单形状的方法。霍夫变换采用类似于投票的方式来获取当前图像内的形状集合, 该变换由Paul Hough(霍夫)于1962年首次提出。最初的霍夫变换只能用于检测直线, 经过发展后, 霍夫变换不仅能够识别直线, 还能识别其他简单的图形结构, 常见的有圆、椭圆等。
15.1 霍夫直线变换
OpenCV提供了函数cv2.HoughLines()和函数cv2.HoughLinesP()用来实现霍夫直线变换。
与笛卡儿坐标系对应, 我们构造一个霍夫坐标系(对应于霍夫空间)。在霍夫坐标系中, 横坐标采用笛卡儿坐标系中直线的斜率k, 纵坐标使用笛卡儿坐标系中直线的截距b。可以这样理解, 霍夫空间内的一个点(𝑘0, 𝑏0), 映射到笛卡儿空间, 就是一条直线𝑦 = 𝑘0𝑥 + 𝑏0
。
OpenCV提供了函数cv2.HoughLines()用来实现霍夫直线变换, 该函数要求所操作的源图像是一个二值图像, 所以在进行霍夫变换之前要先将源图像进行二值化, 或者进行Canny边缘检测。
lines=cv2.HoughLines(image, rho, theta, threshold)
- rho为以像素为单位的距离r的精度。一般情况下, 使用的精度是1。
- theta为角度𝜃的精度。一般情况下, 使用的精度是π/180, 表示要搜索所有可能的角度。
- threshold是阈值。该值越小, 判定出的直线就越多。
- 返回值lines中的每个元素都是一对浮点数, 表示检测到的直线的参数, 即(r,θ), 是numpy.ndarray类型。
使用函数cv2.HoughLines()检测到的是图像中的直线而不是线段, 因此检测到的直线是没有端点的。所以, 我们在进行霍夫直线变换时所绘制的直线都是穿过整幅图像的。
在一些情况下, 使用霍夫变换可能将图像中有限个点碰巧对齐的非直线关系检测为直线, 而导致误检测, 尤其是一些复杂背景的图像, 误检测会很明显。
概率霍夫变换对基本霍夫变换算法进行了一些修正, 是霍夫变换算法的优化。它没有考虑所有的点。相反, 它只需要一个足以进行线检测的随机点子集即可。
在 OpenCV 中, 函数 cv2.HoughLinesP()实现了概率霍夫变换。其语法格式为:
lines = cv2.HoughLinesP(image, rho, theta, threshold, minLineLength, maxLineGap)
15.1 霍夫圆变换
只要是能够用一个参数方程表示的对象, 都适合用霍夫变换来检测。
用霍夫圆变换来检测图像中的圆, 与使用霍夫直线变换检测直线的原理类似。在霍夫圆变换中, 需要考虑圆半径和圆心(x坐标、y坐标)共3个参数。在OpenCV中, 采用的策略是两轮筛选。第1轮筛选找出可能存在圆的位置(圆心); 第2轮再根据第1轮的结果筛选出半径大小。
在OpenCV中, 实现霍夫圆变换的是函数cv2.HoughCircles(), 该函数将Canny边缘检测和霍夫变换结合。其语法格式为:
circles = cv2.HoughCircles(image,method,dp,minDist,param1,param2,minRadius,maxRadius)
16. 图像分割与提取
16.1 分水岭算法
图像分割是图像处理过程中一种非常重要的操作。分水岭算法将图像形象地比喻为地理学上的地形表面, 实现图像分割, 该算法非常有效。
冈萨雷斯在《数字图像处理》一书中, 对分水岭算法进行了细致的分析与介绍。
由于噪声等因素的影响, 采用上述基础分水岭算法经常会得到过度分割的结果。过度分割会将图像划分为一个个稠密的独立小块, 让分割失去了意义。
为了改善图像分割效果, 人们提出了基于掩模的改进的分水岭算法。改进的分水岭算法允许用户将他认为是同一个分割区域的部分标注出来(被标注的部分就称为掩模)。这样, 分水岭算法在处理时, 就会将标注的部分处理为同一个分割区域。
在OpenCV中, 可以使用函数cv2.watershed()实现分水岭算法。在具体的实现过程中, 还需要借助于形态学函数、距离变换函数cv2.distanceTransform()、cv2.connectedComponents()来完成图像分割。
当图像内的各个子图没有连接时, 可以直接使用形态学的腐蚀操作确定前景对象, 但是如果图像内的子图连接在一起时, 就很难确定前景对象了。此时, 借助于距离变换函数cv2.distanceTransform()可以方便地将前景对象提取出来。
明确了确定前景后, 就可以对确定前景图像进行标注了。在OpenCV中, 可以使用函数cv2.connectedComponents()进行标注。该函数会将背景标注为0, 将其他的对象使用从1开始的正整数标注: retval, labels = cv2.connectedComponents(image)
在OpenCV中, 实现分水岭算法的函数语法格式为: markers = cv2.watershed(image, markers)
16.2 交互式前景提取
经典的前景提取技术主要使用纹理(颜色)信息, 如魔术棒工具, 或根据边缘(对比度)信息, 如智能剪刀等完成。
在开始提取前景时, 先用一个矩形框指定前景区域所在的大致位置范围, 然后不断迭代地分割, 直到达到最好的效果。
OpenCV的官网上有更详细的资料http://www.cs.ru.ac.za/research/g02m1682/。在OpenCV中, 实现交互式前景提取的函数语法格式为:
mask, bgdModel, fgdModel = cv2.grabCut(img, mask, rect, bgdModel, fgdModel, iterCount[, mode])
17. 视频处理
视频信号(以下简称为视频)是非常重要的视觉信息来源, 它是视觉处理过程中经常要处理的一类信号。实际上, 视频是由一系列图像构成的, 这一系列图像被称为帧, 帧是以固定的时间间隔从视频中获取的。获取(播放)帧的速度称为帧速率, 其单位通常使用"帧/秒"表示, 代表在1秒内所出现的帧数, 对应的英文是FPS(Frames Per Second)。如果从视频中提取出独立的帧, 就可以使用图像处理的方法对其进行处理, 达到处理视频的目的。
OpenCV提供了cv2.VideoCapture类和cv2.VideoWriter类来支持各种类型的视频文件。在不同的操作系统中, 它们支持的文件类型可能有所不同, 但是在各种操作系统中均支持AVI格式的视频文件。
17.1 VideoCapture
OpenCV提供了cv2.VideoCapture类来处理视频。cv2.VideoCapture类处理视频的方式非常简单、快捷, 而且它既能处理视频文件又能处理摄像头信息: 捕获对象=cv2.VideoCapture("摄像头 ID 号")
- 一般情况下, 使用cv2.VideoCapture()函数即可完成摄像头的初始化。
- 为了防止初始化发生错误, 可以使用函数来检查初始化是否成功:
retval = cv2.VideoCapture.isOpened()
- 如果摄像头初始化失败, 可以使用函数打开摄像头:
retval = cv2.VideoCapture.open(index)
- 摄像头初始化成功后, 就可以从摄像头中捕获帧信息了。捕获帧所使用的是函数:
retval, image=cv2.VideoCapture.read()
- 在不需要摄像头时, 要关闭摄像头。关闭摄像头使用的是函数:
None = cv2.VideoCapture.release()
- 获取cv2.VideoCapture 类对象的属性:
retval = cv2.VideoCapture.get(propId)
- 设置cv2.VideoCapture类对象的属性:
retval = cv2.VideoCapture.set(propId, value)
如果需要同步一组或一个多头(multihead)摄像头(例如立体摄像头或Kinect)的视频数据时, 该函数就无法胜任了。可以把函数cv2.VideoCapture.read()理解为是由函数cv2.VideoCapture.grab()和函数cv2.VideoCapture.retrieve()组成的。函数cv2.VideoCapture.grab()用来指向下一帧, 函数cv2.VideoCapture.retrieve()用来解码并返回一帧。因此, 可以使用函数cv2.VideoCapture.grab()和函数cv2.VideoCapture.retrieve()获取多个摄像头的数据。
- retval= cv2.VideoCapture.grab()
- retval, image = cv2.VideoCapture.retrieve()
播放视频文件时, 需要将函数cv2.VideoCapture()的参数值设置为视频文件的名称。在播放视频时, 可以通过设置函数cv2.waitKey()中的参数值, 来设置播放视频时每一帧的持续(停留)时间。如果函数cv2.waitKey()中的参数值:
- 较小, 则说明每一帧停留的时间较短, 视频播放速度会较快。
- 较大, 则说明每一帧停留的时间较长, 视频播放速度会较慢。
该参数的单位是ms, 通常情况下, 将这个参数的值设置为25即可。
17.2 VideoWriter 类
OpenCV中的cv2.VideoWriter类可以将图片序列保存成视频文件, 也可以修改视频的各种属性, 还可以完成对视频类型的转换。
- 构造函数:
<VideoWriter object> = cv2.VideoWriter(filename, fourcc, fps, frameSize[, isColor])
- fourcc表示视频编/解码类型(格式)查询 http://www.fourcc.org
- 指定视频编码格式:
fourcc = cv2.VideoWriter_fourcc(*'XVID')
- write函数:
None = cv2.VideoWriter.write(image)
- 在调用该函数时, 直接将要写入的视频帧传入该函数即可。
- 释放:
None = cv2.VideoWriter.release()
18. 绘图及交互
在处理图像时, 可能需要与当前正在处理的图像进行交互。 OpenCV提供了鼠标事件, 使用户可以通过鼠标与图像交互。
OpenCV还提供了滚动条用于实现交互功能。用户可以拖动滚动条在某一个范围内设置特定的值, 并将该值应用于后续的图像处理中。而且, 如果设置为二值形式, 滚动条还可以作为开关选择器使用。
- 绘制直线的函数: cv2.line()
- 绘制矩形的函数: cv2.rectangle()
- 绘制圆的函数: cv2.circle()
- 绘制椭圆的函数: cv2.ellipse()
- 绘制多边形的函数: cv2.polylines()
- 在图像内添加文字的函数: cv2.putText()
当用户触发鼠标事件时, 我们希望对该事件做出响应。例如, 用户单击鼠标, 我们就画一个圆。通常的做法是, 创建一个OnMouseAction()响应函数, 将要实现的操作写在该响应函数内。响应函数是按照固定的格式创建的, 其格式为: def OnMouseAction(event,x,y,flags,param)
定义响应函数以后, 要将该函数与一个特定的窗口建立联系(绑定), 让该窗口内的鼠标触发事件时, 能够找到该响应函数并执行。要将函数与窗口绑定, 可以通过函数实现: cv2.setMouseCallback(winname, onMouse)
滚动条(Trackbar)在OpenCV中是非常方便的交互工具, 它依附于特定的窗口而存在。通过调节滚动条能够设置、获取指定范围内的特定值。在OpenCV中, 定义滚动条其语法格式为: cv2.createTrackbar(trackbarname, winname, value, count, onChange)
19. OpenCV 机器学习
19.1 K近邻算法
机器学习算法是从数据中产生模型, 也就是进行学习的算法(下文也简称为算法)。K近邻算法是最简单的机器学习算法之一, 主要用于将对象划分到已知类中, 在生活中被广泛使用。
K近邻算法的本质是将指定对象根据已知特征值分类。为了确定分类, 需要定义特征。
K近邻算法在获取各个样本的特征值之后, 计算待识别样本的特征值与各个已知分类的样本特征值之间的距离, 然后找出k个最邻近的样本, 根据k个最邻近样本中占比最高的样本所属的分类, 来确定待识别样本的分类。
在计算与特征值的距离时要充分考虑不同参数之间的权值。通常情况下, 由于各个参数的量纲不一致等原因, 需要对参数进行处理, 让所有参数具有相等的权值。一般情况下, 对参数进行归一化处理即可。做归一化时, 通常使用特征值除以所有特征值中的最大值(或者最大值与最小值的差)。
OpenCV提供了函数cv2.KNearest()用来实现K近邻算法, 在OpenCV中可以直接调用该函数。
19.2 支持向量机
支持向量机(Support Vector Machine, SVM)是一种二分类模型, 目标是寻找一个标准(称为超平面)对样本数据进行分割, 分割的原则是确保分类最优化(类别之间的间隔最大)。当数据集较小时, 使用支持向量机进行分类非常有效。支持向量机是最好的现成分类器之一, 这里所谓的"现成"是指分类器不加修改即可直接使用。
在对原始数据分类的过程中, 可能无法使用线性方法实现分割。支持向量机在分类时, 把无法线性分割的数据映射到高维空间, 然后在高维空间找到分类最优的线性分类器。
Python提供了不同的实现支持向量机的库(例如sk-learn库、LIBSVM库等), OpenCV也提供了对支持向量机的支持, 对于上述库, 基本都可以直接使用, 无须深入了解支持向量机的原理。
在使用支持向量机模块时, 需要先使用函数生成用于后续训练的空分类器模型: svm = cv2.ml.SVM_create( )
获取了空分类器svm后, 针对该模型使用函数对训练数据进行训练: 训练结果 = svm.train(训练数据,训练数据排列格式,训练数据的标签)
19.3 K均值聚类
当我们要预测的是一个离散值时, 做的工作就是"分类"。
机器学习模型还可以将训练集中的数据划分为若干个组, 每个组被称为一个"簇(cluster)"。这些自动形成的簇, 可能对应着不同的潜在概念。这种学习方式被称为"聚类(clusting)", 它的重要特点是在学习过程中不需要用标签对训练样本进行标注。也就是说, 学习过程能够根据现有训练集自动完成分类(聚类)。
根据训练数据是否有标签, 我们可以将学习划分为监督学习和无监督学习。前面介绍的K近邻、支持向量机都是监督学习, 提供有标签的数据给算法学习, 然后对数据分类。而聚类是无监督学习, 事先并不知道分类标签是什么, 直接对数据分类。
K均值聚类是一种将输入数据划分为k个簇的简单的聚类算法, 该算法不断提取当前分类的中心点(也称为质心或重心), 并最终在分类稳定时完成聚类。从本质上说, K均值聚类是一种迭代算法。
K均值聚类算法的基本步骤如下:
- 随机选取k个点作为分类的中心点。
- 将每个数据点放到距离它最近的中心点所在的类中。
- 重新计算各个分类的数据点的平均值, 将该平均值作为新的分类中心点。
- 重复步骤2和步骤3, 直到分类稳定。
OpenCV提供了函数来实现K均值聚类: retval, bestLabels, centers=cv2.kmeans(data, K, bestLabels, criteria, attempts, flags)
20. 其他
20.1 OpenCV贡献库
该扩展库的名称为 opencv_contrib, 主要由社区开发和维护, 其包含的视觉应用比OpenCV主库更全面。OpenCV 贡献库中包含非 OpenCV许可的部分, 并且包含受专利保护的算法。因此, 在使用该模块前需要特别注意。
OpenCV 贡献库(opencv-contrib-python)中包含了非常多的扩展模块, 举例如下:
- bioinspired: 生物视觉模块。
- datasets: 数据集读取模块。
- dnn: 深度神经网络模块。
- face: 人脸识别模块。
- matlab: MATLAB 接口模块。
- stereo: 双目立体匹配模块。
- text: 视觉文本匹配模块。
- tracking: 基于视觉的目标跟踪模块。
- ximgpro: 图像处理扩展模块。
- xobjdetect: 增强 2D 目标检测模块。
- xphoto: 计算摄影扩展模块。
20.2 问题列表
20.2.1 文件读取
在显示图像时, 初学者最经常遇到的一个错误是"error: (-215:Assertion failed) size.width>0&& size.height>0 in function ‘cv::imshow’", 说明当前要显示的图像是空的(None), 这通常是由于在读取文件时没有找到图像文件造成的。
参考:
论文: A Computational Approach to Edge Detection
论文: Shape Matching and Object RecognitionUsing Shape Contexts
论文: GrabCut: Interactive Foreground Extraction Using Iterated Graph Cuts
论文: Rapid Object Detection Using A Boosted Cascade Of Simple Features 和 Robust Real-time Face Detection