opencv02-图像预处理(终章)
前言:
上节我们学习了图像轮廓特征查找,查找图像外接矩形、最小外接矩形以及最小外接圆,并分别通过绘制直线和绘制轮廓的API实现;然后学习了直方图均衡化,知道如何绘制直方图(直方图的特征以及横坐标)以及直方图均衡化的作用(增加对比度),了解了传统的自适应直方图均衡化不能解决的问题:忽略了图像的局部特征和全局对比度的差异,因此引入对比度受限的自适应直方图均衡化:在每个小区域内进行直方图均衡化,并对该区域内的对比度进行限制,这样就减少了丢失细节信息的情况;最后学习了模板匹配的知识,对于模板匹配的方法有所了解,如平方差匹配、归一化平方差匹配、相关匹配、归一化相关匹配、相关系数匹配以及归一化相关系数匹配,然后通过绘制矩形将匹配成功的区域表示出来。
本节是opencv图像预处理的最后一节,包括三个知识点:霍夫变换、图像亮度变换、形态学变换。
每一节都会导入模块cv2:import cv2 as cv 。并且在必要时导入numpy模块:import numpy as np(需要用到numpy库中的一些方法和对象,如np.int/np.cos等)
一、霍夫变换
1、霍夫变换的基本概念
霍夫变换是一种图像处理的技术,主要用于检测图像中的直线、圆等几何形状(下面将学习检测图像中的直线和圆的方法)。
基本思想是将图像空间中的点映射到参数空间中,再通过参数空间中寻找累计最大值取实现对特定形状的检测。
注意:霍夫变换中图像参数必须是二值化的,因此要进行灰度化和二值化处理(将目标图案二值化为白色区域、其他区域为黑色)。并且霍夫变换是在边缘点的基础上进行的,因此在进行霍夫变换之前一定不要忘记进行边缘检测,再运用边缘检测后的数组作为参数。
霍夫变换提取直线和提取圆如下图所示:
2、霍夫直线变换
2.1需要的API说明
1.灰度化与二值化
gray=cv.cvtColor(img,cv.COLOR_BGR2GRAY)
thresh,binary=cv.threshold(gray,thresh,maxval,二值化方法)
2.Canny边缘检测(基于二值化后的图像数据)
binary=cv.Canny(binary,threshold1,threshold2)
3.霍夫直线变换
lines=cv.HoughLines(image,rho,theta,threshold)
- image:输入的二值化图像(白色代表边缘点、黑色代表背景)
- lines返回值: 函数返回一个三维数组,每一行代表一个二维数组,即一条直线在霍夫空间中的参数(rho,theta)
- rho:r的精度,以像素为单位,表示霍夫空间中每一步的距离增量, 值越大,考虑越多的线。
- theta:角度θ的精度,通常以弧度为单位,表示霍夫空间中每一步的角度增量。值越小,考虑越多的线。
- threshold:累加数阈值,只有累积投票数超过这个阈值的候选直线才会被返回。
2.2基本原理
对于一条在坐标系上的直线(非垂直x轴的直线),都可以用y=kx+b来表示,x,y分别表示横纵坐标,k表示斜率、b表示截距,并且k和b都是一个固定的参数。我们将等式进行变换,就可以得到霍夫空间的等式: b=-kx+y。
此时我们以b和k为新的横纵坐标(自变量和因变量),则x和y为固定的参数。如下图所示:
从上图就可以看出,左侧xy坐标轴上,y=kx+q是一条斜率存在且不为零的直线,映射到右侧kq坐标轴上得到一个点(k,q),这个点就是y=kx+q中的两个固定参数k和q。右侧即为霍夫空间(参数空间)。所以说直角坐标系下的一条直线对应了霍夫空间中的一个点。相应的霍夫空间中的一条直线q=-xk+y也对应了直角坐标系中的一个点(x,y)。如下图所示:
那么对于一个二值化后的图像,其中的每一个目标像素点(白色)都对应了霍夫空间上的一条直线。如果霍夫空间上的两条直线相交于一点时,就代表直角坐标系中对应的某两个点(在一条直线上),而当霍夫空间中有很多条直线相交于一点时,就代表直角坐标系中对应了很多个点(在一条直线上),因此我们可以通过检测霍夫空间中有最多直线相交于一点的交点来找到直角坐标系中对应的直线y=kx+q。
前面提到为什么垂直于x轴的直线不能于霍夫空间进行相互转换?这是因为对于x=1这种垂直于x轴的直线y是无限大的,这意味着它的斜率是不存在的,无法映射到霍夫空间去,因此就无法使用上面的方法进行检测了,所以为了解决这个问题,我们将直角坐标系转换为极坐标系,这样就包容了x=1的情况,然后再通过极坐标系于霍夫空间进行相互转化。如下图所示:
在极坐标系下是一样的,极坐标中的点对于霍夫空间中的线,霍夫空间中的点对应极坐标中的直线。并且此时的霍夫空间不再是以k为横轴、b为纵轴,而是以为θ横轴、ρ(上图中的r)为纵轴。上面的公式中,x、y是直线上某点的横纵坐标(直角坐标系下的横纵坐标),和是极坐标下的坐标,因此我们只要知道某点的x和y的坐标,就可以得到一个关于θ-ρ的表达式,如下图所示:
根据上图,霍夫空间在极坐标系下,一点可以产生一条三角函数曲线,而多条这样的曲线可能会相交于同一点。因此,我们可以通过设定一个阈值,来检测霍夫空间中的三角函数曲线相交的次数。如果一个交点的三角函数曲线相交次数超过阈值,那么这个交点所代表的直线就可能是我们寻找的目标直线。
2.3代码实现与效果呈现
代码实现:
import cv2 as cvimport numpy as np#读图img=cv.imread(\"../images/huofu.png\")#灰度化gray=cv.cvtColor(img,cv.COLOR_BGR2GRAY)#二值化(目标团直线和圆为白色,底为黑色,用OTSU和阈值法相结合)thresh,binary=cv.threshold(gray,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)#由于霍夫变换是基于边缘检测后的,所以在进行霍夫变换之前要传入边缘检测后的图像数据binary=cv.Canny(binary,50,150)#进行霍夫直线变换lines=cv.HoughLines(binary,0.8,np.pi/180,70)#遍历lines(通过绘制直线表示霍夫直线变换的结果)for line in lines: rho,theta=line[0] cos_theta=np.cos(theta) sin_theta=np.sin(theta) x1,x2=0,img.shape[1]#从图像最左侧x=0到最右侧x=shape[1](w) y1,y2=int((rho-x1*cos_theta)/sin_theta),int((rho-x2*cos_theta)/sin_theta) #绘制直线 cv.line(img,(x1,y1),(x2,y2),(0,255,0),1)#显示效果cv.imshow(\"img\",img)cv.waitKey(0)cv.destroyAllWindows()
效果呈现:
(左图为原图,右图为霍夫直线变换后的图像)
3、统计概率霍夫直线变换
3.1需要的API说明
lines=cv.HoughLinesP(image,rho,theta,threshold,lines=None,minLineLength=0,maxLineLength=0)
-
image:输入图像,通常为二值图像,其中白点表示边缘点,黑点为背景。
-
rho:极径分辨率,以像素为单位,表示极坐标系中的距离分辨率。
-
theta:极角分辨率,以弧度为单位,表示极坐标系中角度的分辨率。
-
threshold:阈值,用于过滤掉弱检测结果,只有累计投票数超过这个阈值的直线才会被返回。
-
lines(可选):一个可初始化的输出数组,用于存储检测到的直线参数。
-
minLineLength(可选):最短长度阈值,比这个长度短的线会被排除。
-
maxLineLength(可选):同一直线两点之间的最大距离。当霍夫变换检测到一系列接近直角的线段时,这些线段可能是同一直线的不同部分。
maxLineGap
参数指定了在考虑这些线段属于同一直线时,它们之间最大可接受的像素间隔。 -
返回值lines:cv.HoughLinesP函数返回一个三维数组,每一行是一个包含4个元素的二维数组,分别表示每条直线的起始点和结束点在图像中的坐标(x1, y1, x2, y2)。
前面的霍夫直线变换又称为标准霍夫变换,它会计算图像中的每一个点,计算量比较大,另外它得到的是一整条线(r和θ),并不知道原图中的直线端点,如下图所示:
所以提出了统计概率霍夫直线变换,这是一种改进的霍夫变换,它在获取到直线之后,会检测原图中在该直线上的点,并获取到直线两侧的端点坐标(这样获取的直线就不像上图那样溢出了)。然后通过两个端点的坐标来计算该直线的长度,再通过直线的长度与最短长度阈值的比较来决定该直线要不要保留。
3.2代码实现与效果呈现
代码实现:
import cv2 as cvimport numpy as np#读图img=cv.imread(\"../images/huofu.png\")#灰度化gray=cv.cvtColor(img,cv.COLOR_BGR2GRAY)#二值化(目标团直线和圆为白色,底为黑色,用OTSU和阈值法相结合)thresh,binary=cv.threshold(gray,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)#由于霍夫变换是基于边缘检测后的,所以在进行霍夫变换之前要传入边缘检测后的图像数据edges=cv.Canny(binary,50,150)#进行霍夫直线变换lines=cv.HoughLinesP(edges,0.8,np.pi/180,90,minLineLength=5,maxLineGap=10)#遍历lines(通过绘制直线表示霍夫直线变换的结果)for line in lines: x1,y1,x2,y2=line[0] #绘制直线 cv.line(img,(x1,y1),(x2,y2),(0,255,0),1)#显示效果cv.imshow(\"img\",img)cv.waitKey(0)cv.destroyAllWindows()
效果呈现:
(左图为原图,右图为统计概率霍夫直线变换后的图像)
4、霍夫圆变换
4.1需要的API说明
circles=cv.HoughCircles(image,method,dp,minDist,param1,param2)
-
image:输入图像,通常是灰度图像。
-
method:使用的霍夫变换方法:霍夫梯度法,可以是 cv
.
HOUGH_GRADIENT,这是唯一在OpenCV中用于圆检测的方法。 -
dp:累加器分辨率与输入图像分辨率之间的降采样比率,用于加速运算但不影响准确性。设置为1表示霍夫梯度法中累加器图像的分辨率与原图一致
-
minDist:检测到的圆心之间的最小允许距离,以像素为单位。在霍夫变换检测圆的过程中,可能会检测到许多潜在的圆心。minDist参数就是为了过滤掉过于接近的圆检测结果,避免检测结果过于密集。当你设置一个较小的 minDist值时,算法会尝试找出尽可能多的圆,即使是彼此靠得很近的圆也可能都被检测出来。相反,当你设置一个较大的 minDist值时,算法会倾向于只检测那些彼此间存在一定距离的独立的圆。
-
param1和 param2:这两个参数是在使用 cv
.
HOUGH_GRADIENT方法时的特定参数,分别为:-
param1(可选):阈值1,决定边缘强度的阈值。
-
param2:阈值2,控制圆心识别的精确度。较大的该值会使得检测更严格的圆。
param2
通常被称为圆心累积概率的阈值。在使用霍夫梯度方法时,param2
设置的是累加器阈值,它决定了哪些候选圆点集合被认为是有效的圆。较高的param2
值意味着对圆的检测更严格,只有在累加器中积累了足够高的响应值才认为是真实的圆;较低的param2
值则会降低检测的门槛,可能会检测到更多潜在的圆,但也可能包含更多的误检结果。
-
-
返回值:cv
.
HoughCircles返回一个三维numpy数组,每一行的元素包含了所有满足条件的圆的参数(x,y,r)。
霍夫圆变换跟直线变换类似,它可以从图像中找出潜在的圆形结构,并返回它们的中心坐标和半径,只不过线用(r,θ)表示,圆用(x,y,r)表示。
4.2代码实现与效果呈现
代码实现:
import cv2 as cvimport numpy as np#读图img = cv.imread(\'../images/huofu.png\')#灰度化gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)print( gray.shape)#这里可以不进行二值化处理,因为二值化处理会增加噪点,导致霍夫圆变换得到的圆与原图形差异较大。因此这里我只是把代码放在此处,而注释掉不运行。#_,binary=cv.threshold(gray,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)#霍夫变换都是基于边缘检测进行的,所以这里也要进行边缘检测(直接传入灰度图)edges=cv.Canny(gray,50,150)#霍夫圆变换circles=cv.HoughCircles(edges,cv.HOUGH_GRADIENT,1,20,param2=30)#遍历circlesfor circle in circles: x,y,r=circle[0] #类型转换(防止溢出) x,y,r=np.int_(x),np.int_(y),np.int_(r) #绘制圆形 cv.circle(img,(x,y),r,(0,255,0),1)#显示效果cv.imshow(\"img\",img)cv.waitKey(0)cv.destroyAllWindows()
效果呈现:
二、图像亮度变换
因为图像是由很多个像素组成的,所以在调整图像亮度时,本质还是图像像素值的变换。
亮度和对比度的区别也是很重要的。对比度的调整的方法就有我们之前学习的直方图均衡化。调整对比度的本质就是图像暗处像素强度变低,图像亮处像素强度变高,从而拉大中间某个区域范围的显示精度。
如下图,上面两幅是亮度不同的对比图,下面两幅是对比度不同的对比图。
1、线性变换
与前面学习的颜色的加权加法cv.addWeighted(src1,alpha,src2,beta,gamma)API相同,可以对图像的像素值进行加权平均,进而改变图像的整体亮度。亮度增益可以通过向每个像素值添加一个正值来实现。
cv.addWeighted(src1,alpha,src2,beta,gamma)
-
src1:第一张输入图像,它将被赋予权重
alpha
。 -
alpha:第一个输入图像的权重。
-
src2:第二张输入图像,它将被赋予权重
beta
。 -
beta:第二个输入图像的权重。
-
gamma:一个标量,将被添加到权重求和的结果上,可用于调整总体亮度。
计算公式为:dst=src1*alpha+src2*beta+gamma
代码实现:
import cv2 as cvimport numpy as np#读图cat=cv.imread(\"../images/cat1.png\")#构造一个全黑背景图像,大小和原图像相同shape=cat.shapeblack=np.zeros(shape,dtype=np.uint8)#调整亮度(线性变换:加权平均)dst=cv.addWeighted(cat,1,black,0,100)#显示效果cv.imshow(\"cat\",cat)cv.imshow(\"dst\",dst)cv.waitKey(0)cv.destroyAllWindows()
效果呈现:
(左图为原图,右图为调整了亮度的图像,可以看出,右图调整亮度后明显变亮了很多,如果输入的gamma参数为负,则是调暗)
2、直接像素值修改
2.1需要的API说明
numpy.clip(a, a_min, a_max)
-
a:输入数组。
-
a_min:指定的最小值,数组中所有小于 a_min的元素将被替换为 a_min。
-
a_max:指定的最大值,数组中所有大于 a_max的元素将被替换为 a_max。
直接像素值修改的本质就是在需要增加或减少固定的亮度值,可以直接遍历图像像素并对每个像素值进行加减操作。
2.2代码实现与效果呈现
这里我们需要创建一个窗口,用来实现滑条(手动实现亮度调整)
#创建窗口,实现滑条(用于亮度调整)window_name=\"tracker\"cv.namedWindow(window_name)
代码完整实现:
import cv2 as cvimport numpy as np#创建窗口,实现滑条(用于亮度调整)window_name=\"tracker\"cv.namedWindow(window_name)cat=cv.imread(\"../images/cat1.png\")#定义亮度调整的方法def adjust(x):#传入的只能是整数,但是要表示滑条可以在-255到255之间调整,所以我们设定传入的数值在0-510之间,p设为x-255即可 p=x-255 #像素值调整 dst=np.uint8(np.clip(np.int_(cat)+p,0,255))#转换类型为整型 cv.imshow(\"dst\",dst) print(p)#主函数if __name__ == \"__main__\": #创建滑条 cv.createTrackbar(\"light\",window_name,0,510,adjust) cv.waitKey(0) cv.destroyAllWindows()
效果呈现:
三、形态学变换
形态学变换是一种基于形状的简单变换,它的处理对象通常是二值化图像。形态学变换有两个输入,一个输出:输入为原图像(二值化图像)、核(结构化元素),输出为形态学变换后的图像。其基本操作有腐蚀和膨胀,这两种操作是相反的,即较亮的像素会被腐蚀和膨胀。下面我们来说一下核、腐蚀与膨胀的概念。
1、核
自适应二值化中,我们已经接触过核了,还记得吗?就是那个在原图中不断滑动计算的3*3的小区域,那其实就是一个3*3的核。
核(kernel)其实就是一个小区域,通常为3*3、5*5、7*7大小,有着其自己的结构,比如矩形结构、椭圆结构、十字形结构,如下图所示。通过不同的结构可以对不同特征的图像进行形态学操作的处理。
2、腐蚀
腐蚀操作就是使用核在原图(二值化图)上进行从左到右、从上到下的滑动(也就是从图像的左上角开始,滑动到图像的右下角)。在滑动过程中,令核值为1的区域与被核覆盖的对应区域进行相乘,得到其最小值,该最小值就是卷积核覆盖区域的中心像素点的新像素值,接着继续滑动。由于操作图像为二值图,所以不是黑就是白,这就意味着,在被核值为1覆盖的区域内,只要有黑色(像素值为0),那么该区域的中心像素点必定为黑色(0)。这样做的结果就是会将二值化图像中的白色部分尽可能的压缩,如下图所示,该图经过腐蚀之后,“变瘦”了。
在腐蚀操作的详细流程中,遍历图像的过程如下:
-
初始化:
-
设置一个起始位置(通常从图像的左上角开始)。
-
准备好结构元素(structuring element),它是一个小的矩阵,大小通常是奇数,并且有一个明确的中心点。
-
-
逐像素处理: 对于输入图像中的每一个像素,执行以下步骤:
a. 定位: 将结构元素移动到当前待处理像素的位置,使得结构元素的中心与该像素对齐。
b. 区域覆盖: 结构元素会覆盖图像上的一个局部邻域,这个邻域由结构元素的尺寸决定。
c. 条件检查: 检查结构元素覆盖区域内所有图像像素的颜色。对于二值图像来说,就是看这些像素是否都是白色(前景像素)。如果所有被结构元素覆盖的像素均为白色,则继续下一个步骤;否则,跳过此步骤,将中心像素视为背景像素。
d. 侵蚀决策: 如果结构元素覆盖的所有像素都是白色,则原图像中的中心像素保持不变(在输出图像中仍为白色);否则,将中心像素变为黑色(在输出图像中变为背景色)。
-
迭代移动: 结构元素沿着图像从左到右、从上到下逐行逐列地移动,重复上述过程,直到整个图像都被结构元素遍历过。
-
循环处理: 如果指定了多个迭代次数,那么在整个图像完成一次遍历后,再次从头开始进行同样的遍历和侵蚀决策,直到达到指定的迭代次数。
通过这样的遍历方式,腐蚀操作能够逐步收缩目标物体边界,消除孤立的噪声像素以及细化连续的前景区域。
总结:二值图腐蚀后白色像素(非0)变少了。
3、膨胀
膨胀与腐蚀刚好相反,膨胀操作就是使用核在原图(二值化图)上进行从左到右、从上到下的滑动(也就是从图像的左上角开始,滑动到图像的右下角),在滑动过程中,令核值为1的区域与被核覆盖的对应区域进行相乘,得到其最大值,该最大值就是核覆盖区域的中心像素点的新像素值,接着继续滑动。由于操作图像为二值图,所以不是黑就是白,这就意味着,在卷积核覆盖的区域内,只要有白色(像素值为255),那么该区域的中心像素点必定为白色(255)。这样做的结果就是会将二值化图像中的白色部分尽可能的扩张,如下图所示,该图经过膨胀之后,“变胖”了。
在膨胀操作的详细流程中,遍历图像的过程如下:
-
初始化:
-
设置一个起始位置(通常从图像的左上角开始)。
-
准备好结构元素(structuring element),它是一个小的矩阵,大小通常是奇数,并且有一个明确的中心点。
-
-
逐像素处理: 对于输入图像中的每一个像素,执行以下步骤:
a. 定位: 将结构元素移动到当前待处理像素的位置,使得结构元素的中心与该像素对齐。
b. 区域覆盖: 结构元素会覆盖图像上的一个局部邻域,这个邻域由结构元素的尺寸决定。
c. 条件检查: 检查结构元素覆盖区域内是否存在白色(前景)像素。对于二值图像来说,如果有任何一个被结构元素覆盖的像素是白色的,则继续下一步;否则,将中心像素保持原样(黑色或非目标物体像素不变)。
d. 膨胀决策: 如果在结构元素覆盖的范围内找到了至少一个白色像素,则无论原中心像素是什么颜色,都将输出图像中的该中心像素设置为白色(前景色)。这表示即使原中心像素可能是背景像素,但只要其周围有白色像素存在,就认为该位置也应属于前景区域。
e. 更新输出: 根据上述判断结果更新输出图像对应位置的像素值。
-
迭代移动: 结构元素沿着图像从左到右、从上到下逐行逐列地移动,重复上述过程,直到整个图像都被结构元素遍历过。
-
循环处理: 如果指定了多个迭代次数,那么在整个图像完成一次遍历后,再次从头开始进行同样的遍历和膨胀决策,直到达到指定的迭代次数。
通过这样的遍历方式,膨胀操作能够逐步扩大目标物体边界,连接断裂的前景部分,并填充内部空洞,使得物体轮廓更加明显且连续。
注意: 此案例代表灰度图
总结:二值图膨胀后白色像素变多了。
4、开运算
开运算是先腐蚀后膨胀,其作用是:分离物体,消除小区域。特点:消除噪点,去除小的干扰块,而不影响原来的图像。
5、闭运算
闭运算与开运算相反,是先膨胀后腐蚀,作用是消除/“闭合”物体里面的孔洞,特点:可以填充闭合区域,不应先原来的图像。
6、礼帽运算
原图像与“开运算“的结果图之差,因为开运算带来的结果是放大了裂缝或者局部低亮度的区域,因此,从原图中减去开运算后的图,得到的效果图突出了比原图轮廓周围的区域更明亮的区域,且这一操作和选择的核的大小相关。
礼帽运算用来分离比邻近点亮一些的斑块。当一幅图像具有大幅的背景的时候,而微小物品比较有规律的情况下,可以使用礼帽运算进行背景提取。
7、黑帽运算
黑帽运算为”闭运算“的结果图与原图像之差。
黑帽运算后的效果图突出了比原图轮廓周围的区域更暗的区域,且这一操作和选择的核的大小相关。
黑帽运算用来分离比邻近点暗一些的斑块。
8、形态学梯度
形态学梯度是一个基于结构元素的图像处理方法,它通过比较原图像与膨胀图和腐蚀图之间的差异来突出图像边缘特征。具体来说,对于图像中的每个像素点,其形态学梯度值是该像素点在膨胀后的图像值与其在腐蚀后的图像值之差。这样得到的结果通常能够强化图像的边缘信息,并且对噪声有一定的抑制作用。
opencv的知识点就告一段落啦,完结撒花*★,°*:.☆( ̄▽ ̄)/$:*.°★* 。