OpenCV --- 图像预处理(六)
OpenCV — 图像预处理(六)
文章目录
- OpenCV --- 图像预处理(六)
- 十四,图像边缘检测
- 14.1 高斯滤波
- 14.2 计算图像的梯度与方向
- 14.3 非极大值抑制
- 14.4 双阈值筛选
- 14.5 API和使用
- 十五,绘制图像轮廓
- 15.1 什么是轮廓
- 15.2 寻找轮廓
- 15.2.1 mode参数
- 15.2.2 method参数
- 15.3 绘制轮廓
- 十六,凸包特征检测
- 16.1 获取凸包点
- 16.2 绘制凸包
十四,图像边缘检测
14.1 高斯滤波
边缘检测本身属于锐化操作,对噪点比较敏感,所以需要进行平滑处理。这里使用的是一个5*5的高斯核对图像进行消除噪声。上一个实验中已经介绍了高斯滤波的具体过程,这里就不再过多叙述,只展示一下用到的5*5的高斯核
14.2 计算图像的梯度与方向
这里使用了sobel算子来计算图像的梯度值,在上一章节中,我们了解到sobel算子其实就是一个核值固定的卷积核,如下所示:
s o b e l ( 水平方向 ) = [ − 1 0 1 − 2 0 2 − 1 0 1 ] sobel(水平方向)=\left[\begin{array}{c c c}{{-1}}&{{0}}&{{1}}\\ {{-2}}&{{0}}&{{2}}\\ {{-1}}&{{0}}&{{1}}\end{array}\right] sobel(水平方向)= −1−2−1000121
s o b e l ( 垂直方向 ) = [ − 1 − 2 − 1 0 0 0 1 2 1 ] sobel(垂直方向)=\left[\begin{array}{c c c}{{-1}}&{{-2}}&{{-1}}\\ {{0}}&{{0}}&{{0}}\\ {{1}}&{{2}}&{{1}}\end{array}\right] sobel(垂直方向)= −101−202−101
首先使用sobel算子计算中心像素点的两个方向上的梯度 G x G_{x} Gx和 G y G_{y} Gy,然后就能够得到其具体的梯度值:
G = G x 2 + G y 2 G={\sqrt{G_{x}{}^{2}+G_{y}{}^{2}}} G=Gx2+Gy2
也可以使用 G = ∣ G x + G y ∣ G=|G_{x}+G_{y}| G=∣Gx+Gy∣来代替。在OpenCV中,默认使用 G = ∣ G x + G y ∣ G=|G_{x}+G_{y}| G=∣Gx+Gy∣来计算梯度值。
然后我们根据如下公式可以得到一个角度值
G y G x = tan ( θ ) {\frac{G_{\mathrm{y}}}{G_{x}}}=\tan\,(\theta) GxGy=tan(θ)
θ = arctan ( G y G x ) \theta=\arctan\,({\frac{G_{\mathrm{y}}}{G_{x}}}) θ=arctan(GxGy)
这个角度值其实是当前边缘的梯度的方向。通过这个公式我们就可以计算出图片中所有的像素点的梯度值与梯度方向,然后根据梯度方向获取边缘的方向。
a). 并且如果梯度方向不是0°、45°、90°、135°这种特定角度,那么就要用到插值算法来计算当前像素点在其方向上进行插值的结果了,然后进行比较并判断是否保留该像素点。这里使用的是单线性插值,通过A1和A2两个像素点获得dTmp1与dTmp2处的插值,然后与中心点C进行比较(非极大值抑制)。具体的插值算法请参考图像旋转实验。
b). 得到 θ \theta θ的值之后,就可以对边缘方向进行分类,为了简化计算过程,一般将其归为四个方向:水平方向、垂直方向、45°方向、135°方向。并且:
当 θ \theta θ值为-22.5°~22.5°,或-157.5°~157.5°,则认为边缘为水平边缘;
当法线方向为22.5°~67.5°,或-112.5°~-157.5°,则认为边缘为45°边缘;
当法线方向为67.5°~112.5°,或-67.5°~-112.5°,则认为边缘为垂直边缘;
当法线方向为112.5°~157.5°,或-22.5°~-67.5°,则认为边缘为135°边缘;
14.3 非极大值抑制
得到每个边缘的方向之后,其实把它们连起来边缘检测就算完了,但是为什么还有这一步与下一步呢?是因为经过第二步得到的边缘不经过处理是没办法使用的,因为高斯滤波的原因,边缘会变得模糊,导致经过第二步后得到的边缘像素点非常多,因此我们需要对其进行一些过滤操作,而非极大值抑制就是一个很好的方法,它会对得到的边缘像素进行一个排除,使边缘尽可能细一点。
在该步骤中,我们需要检查每个像素点的梯度方向上的相邻像素,并保留梯度值最大的像素,将其他像素抑制为零。假设当前像素点为(x,y),其梯度方向是0°,梯度值为G(x,y),那么我们就需要比较G(x,y)与两个相邻像素的梯度值:G(x-1,y)和G(x+1,y)。如果G(x,y)是三个值里面最大的,就保留该像素值,否则将其抑制为零。
14.4 双阈值筛选
经过非极大值抑制之后,我们还需要设置阈值来进行筛选,当阈值设的太低,就会出现假边缘,而阈值设的太高,一些较弱的边缘就会被丢掉,因此使用了双阈值来进行筛选,推荐高低阈值的比例为2:1到3:1之间,其原理如下图所示:
当某一像素位置的幅值超过最高阈值时,该像素必是边缘像素;当幅值低于最低像素时,该像素必不是边缘像素;幅值处于最高像素与最低像素之间时,如果它能连接到一个高于阈值的边缘时,则被认为是边缘像素,否则就不会被认为是边缘。也就是说,上图中的A和C是边缘,B不是边缘。因为C虽然不超过最高阈值,但其与A相连,所以C就是边缘。
14.5 API和使用
edges = cv2.Canny(image, threshold1, threshold2),即使读到的是彩色图也可以进行处理。
-
image
:输入的灰度/二值化图像数据。 -
threshold1
:低阈值,用于决定可能的边缘点。 -
threshold2
:高阈值,用于决定强边缘点。
#图像边缘检测
img = cv.imread('txycl/images/shudu.png')
ret,imgbinary = cv.threshold(img,127,255,cv.THRESH_BINARY)
#canny
img_canny = cv.Canny(img,100,200)
cv.imshow('img_canny',img_canny)
cv.imshow('imgbinary',imgbinary)
cv.waitKey(0)
cv.destroyAllWindows()
十五,绘制图像轮廓
15.1 什么是轮廓
轮廓是一系列相连的点组成的曲线,代表了物体的基本外形。相对于边缘,轮廓是连续的,边缘不一定连续
轮廓的作用:
- 形状分析
- 目标识别
- 图像分割
15.2 寻找轮廓
寻找轮廓需要将图像做一个二值化处理,并且根据图像的不同选择不同的二值化方法来将图像中要绘制轮廓的部分置为白色,其余部分置为黑色。也就是说,我们需要对原始的图像进行灰度化、二值化的处理,令目标区域显示为白色,其他区域显示为黑色,之后,对图像中的像素进行遍历,当一个白色像素相邻(上下左右及两条对角线)位置有黑色像素存在或者一个黑色像素相邻(上下左右及两条对角线)位置有白色像素存在时,那么该像素点就会被认定为边界像素点,轮廓就是有无数个这样的边界点组成的
contours,hierarchy = cv2.findContours(image,mode,method)
-
contours:表示获取到的轮廓点的列表。检测到有多少个轮廓,该列表就有多少子列表,每一个子列表都代表了一个轮廓中所有点的坐标。
-
hierarchy:表示轮廓之间的关系。对于第i条轮廓, h i e r a r c h y [ i ] [ 0 ] hierarchy[i][0] hierarchy[i][0], h i e r a r c h y [ i ] [ 1 ] hierarchy[i][1] hierarchy[i][1] , h i e r a r c h y [ i ] [ 2 ] hierarchy[i][2] hierarchy[i][2] ,$ hierarchy[i][3]$分别表示其后一条轮廓、前一条轮廓、(同层次的第一个)子轮廓、父轮廓的索引(如果没有相应的轮廓,则对应位置为-1)。该参数的使用情况会比较少。
-
image:表示输入的二值化图像。
-
mode:表示轮廓的检索模式。
-
method:轮廓的表示方法。
15.2.1 mode参数
轮廓查找方式。返回不同的层级关系。
mode参数共有四个选项分别为:RETR_LIST,RETR_EXTERNAL,RETR_CCOMP,RETR_TREE。
- RETR_EXTERNAL
表示只查找最外层的轮廓。并且在hierarchy里的轮廓关系中,每一个轮廓只有前一条轮廓与后一条轮廓的索引,而没有父轮廓与子轮廓的索引。
2.3.4.会查找所有轮廓,但会有层级关系。
- RETR_LIST
表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,每一个轮廓只有前一条轮廓与后一条轮廓的索引,而没有父轮廓与子轮廓的索引。
RETR_CCOMP
表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,轮廓会按照成对的方式显示。
在 RETR_CCOMP
模式下,轮廓被分为两个层级:
- 层级 0:所有外部轮廓(最外层的边界)。
- 层级 1:所有内部轮廓(孔洞或嵌套的区域)。
RETR_TREE
表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,轮廓会按照树的方式显示,其中最外层的轮廓作为树根,其子轮廓是一个个的树枝。
15.2.2 method参数
轮廓存储方法。轮廓近似方法。决定如何简化轮廓点的数量。就是找到轮廓后怎么去存储这些点。
method参数有三个选项:CHAIN_APPROX_NONE、CHAIN_APPROX_SIMPLE、CHAIN_APPROX_TC89_L1。
-
CHAIN_APPROX_NONE
表示将所有的轮廓点都进行存储 -
CHAIN_APPROX_SIMPLE
表示只存储有用的点,比如直线只存储起点和终点,四边形只存储四个顶点,默认使用这个方法;
对于mode和method这两个参数来说,一般使用RETR_EXTERNAL和CHAIN_APPROX_SIMPLE这两个选项。
15.3 绘制轮廓
轮廓找出来后,其实返回的是一个轮廓点坐标的列表,因此我们需要根据这些坐标将轮廓画出来,因此就用到了绘制轮廓的方法。
cv2.drawContours(image, contours, contourIdx, color, thickness)
- image:原始图像,一般为单通道或三通道的 numpy 数组。
- contours:包含多个轮廓的列表,每个轮廓本身也是一个由点坐标构成的二维数组(numpy数组)。
- contourIdx:要绘制的轮廓索引。如果设为
-1
,则会绘制所有轮廓。根据索引找到轮廓点绘制出来。默认是-1。 - color:绘制轮廓的颜色,可以是 BGR 值或者是灰度值(对于灰度图像)。
- thickness:轮廓线的宽度,如果是正数,则画实线;如果是负数,则填充轮廓内的区域。
import cv2 as cv
import numpy as np#读图,转灰度
number = cv.imread('txycl/images/num.png',cv.IMREAD_GRAYSCALE)#二值化
ret,binary = cv.threshold(number,127,255,cv.THRESH_OTSU+cv.THRESH_BINARY_INV)#查找轮廓
contours,h = cv.findContours(binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)#绘制轮廓
cv.drawContours(binary,contours,-1,(0,0,255),2)#显示图像
cv.imshow('number',number)
cv.imshow('binary',binary)
cv.waitKey(0)
cv.destroyAllWindows()
print(contours)
十六,凸包特征检测
在进行凸包特征检测之前,首先要了解什么是凸包。通俗的讲,凸包其实就是将一张图片中物体的最外层的点连接起来构成的凸多边形,它能包含物体中所有的内容。
一般来说,凸包都是伴随着某类点集存在的,也被称为某个点集的凸包。
对于一个点集来说,如果该点集存在凸包,那么这个点集里面的所有点要么在凸包上,要么在凸包内。
凸包检测常用在物体识别、手势识别、边界检测等领域。
- 穷举法
将集中的点进行两两配对,并进行连线,对于每条直线,检查其余所有的点是否处于该直线的同一侧,如果是,那么说明构成该直线的两个点就是凸包点,其余的线依次进行计算,从而获取所有的凸包点。
用向量的思想,点都是有坐标的,连起来就可以构成一个向量。再以其中一个点,连接另一个点,构成另一个向量,让两个向量做外积,就是叉积。也就是 s t d = ∣ 向量 a ∣ ∗ ∣ 向量 b ∣ ∗ s i n ( θ ) std=|向量a|*|向量b|*sin(\theta) std=∣向量a∣∗∣向量b∣∗sin(θ),能控制 s t d std std的正负的只能是 θ \theta θ,如果计算出来的 s t d std std的正负都相同,说明这些点都在这条直线的同一侧,那么这两个点就是凸包的边界点。然后换两个点,就是说换一条直线,换一个向量,继续进行检测,直到找到凸包的所有的边界点。
缺点:时间复杂度高,不断使用for循环,耗时
- QuickHull法
将所有点放在二维坐标系中,找到横坐标最小和最大的两个点 P 1 P_1 P1和 P 2 P_2 P2并连线。此时整个点集被分为两部分,直线上为上包,直线下为下包。
以上保暖为例,找到上包中的点距离该直线最远的点 P 3 P_3 P3,连线并寻找直线 P 1 P 3 P1P3 P1P3左侧的点 P 2 P 3 P2P3 P2P3右侧的点,然后重复本步骤,直到找不到为止。对下包也是这样操作
16.1 获取凸包点
cv2.convexHull(points)
points
:输入参数,图像的轮廓
16.2 绘制凸包
cv2.polylines(image, pts, isClosed, color, thickness=1)
image
:要绘制线条的目标图像,它应该是一个OpenCV格式的二维图像数组(如numpy数组)。pts
:一个二维 numpy 数组,每个元素是一维数组,代表一个多边形的一系列顶点坐标。isClosed
:布尔值,表示是否闭合多边形,如果为 True,会在最后一个顶点和第一个顶点间自动添加一条线段,形成封闭的多边形。color
:线条颜色,可以是一个三元组或四元组,分别对应BGR或BGRA通道的颜色值,或者是灰度图像的一个整数值。thickness
(可选):线条宽度,默认值为1。
#绘制轮廓img = cv.imread('txycl/images/tu.png')
#灰度图
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
#二值化处理,阈值法 因为目标是白色我们需要白色
ret, binary = cv.threshold(gray, 200, 255, cv.THRESH_BINARY)
# cv.imwrite("./002.png",binary)
#查找轮廓
contours, h = cv.findContours(binary, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
# cv.drawContours(img, contours,-1,(0,0,255),2)
#查找凸包
hull = cv. convexHull(contours[0])
for i in range(len(contours)):hull=cv.convexHull(contours[i])
#绘制凸包
cv.polylines(img, [hull], True, (0, 0, 255), 3)
print(hull)
cv.imshow('1', img)
cv.imshow('2', binary)
cv.waitKey(0)