目录
前言:
图像的边缘是指图像中灰度值急剧变化的位置,边缘检测的目的是为了绘制边缘线条。边缘检测的目的是为了绘制出边缘线条。边缘通常是不连续的,不能表示整体。
图像的轮廓是指将边缘连接起来形成的整体。这次主要学习边缘检测、图像轮廓和霍夫变换。
1、边缘检测
边缘检测结果通常为黑白图像,图像中的白色线条表示边缘。常见的边缘检测算法有Laplacian边缘检测、Sobel边缘检测和Canny边缘检测。
1.1 Laplacian边缘检测
使用图像矩阵与拉普拉斯核进行卷积运算,其本质是计算图像中任意一点与其在水平方向和垂直方向上4个相邻点的平均值的差值。
dst=cv2.Laplacian(src,depth,ksize,scale,delta,borderType)
depth表示目标图像深度。后四个为可选参数,ksize为用于计算二阶导数滤波器的系数,必须为正数且奇数;scale为可选比例因子;delta为添加到边缘检测结果中的可选增量值;最后为边界类型。
img=cv2.imread('dog2.png')
cv2.imshow('img',img)
img2=cv2.Laplacian(img,cv2.CV_8U)
cv2.imshow('img2',img2)
cv2.waitKey(0)
1.2 Sobel边缘检测
将高斯滤波和微分方程结合起来执行图像卷积运算,其结果有一定抗噪性。
dst=cv2.Sobel(src,depth,dx,dy,ksize,scale,delta,borderType)
dx为导数x的阶数,dy为导数y的阶数。后四个为可选参数,ksize是扩展的Sobel内核的大小,必须是1,3,5或7;scale为计算导数的可选比例因子,其他与拉普拉斯一样。
img=cv2.imread('dog2.png')
cv2.imshow('img',img)
img2=cv2.Sobel(img,cv2.CV_8U,0,1) #表示只计算垂直方向的导数
cv2.imshow('img2',img2)
cv2.waitKey(0)
1.3 Canny边缘检测
Laplacian和Sobel边缘检测都通过卷积计算边缘,算法比较简单,因此结果噪声较多或者损失过多的边缘信息。Canny的算法则更加复杂:首先使用高斯滤波去除图像噪声;然后使用Sobel核进行滤波,计算梯度;在边缘使用非最大值抑制;对检测出的边缘进行双阈值以去除假阳性;最后分析边缘之间的连接性,保留真正的边缘,消除不明显的边缘。
dst=cv2.Canny(src,threshold1,threshold2,apertureSize,L2gradient)
threshold1是第1阈值,threshold2是第2个阈值。后两个为可选参数,前者为计算梯度时使用的Sobel核大小,后者为标志。
img=cv2.imread('dog2.png')
cv2.imshow('img',img)
img2=cv2.Canny(img,150,200)
cv2.imshow('img2',img2)
cv2.waitKey(0)
2、图像轮廓
图像轮廓是指由位于边缘、连续的、具有相同颜色和强度的点构成的曲线,可用于形状分析、对象检测和识别等。
2.1 查找轮廓
contours,hierarchy=cv2.findContours(image,mode,method,offset)
contours是返回的轮廓,hierarchy是返回的轮廓的层次结构;mode是轮廓检索模式,method是轮廓的近似方法。offset是可选参数,为每个轮廓点移动的可选偏移量。
img=cv2.imread('tuxing.png')
cv2.imshow('img',img)
gray=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) #转换为灰度图
ret,thresh=cv2.threshold(gray,125,255,cv2.THRESH_BINARY) #为了提高准确率,二值化阈值处理
b,c,h=cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) #查找轮廓,这里接受参数是三个因为两个报错了
print('轮廓:',c)
print('轮廓类型:',type(c))
print('轮廓个数:',len(c))
print('层次:',h)
print('层次类型:',type(h))
for i in range(3):
img3=np.zeros(img.shape,np.uint8)+255 #创建一幅与原图等大小的白色图像
cv2.polylines(img3,[c[i]],True,(255,0,0),2) #依次绘制轮廓
cv2.imshow('img%s'%i,img3)
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.findContours()返回的是一个list对象,保存了轮廓数组。轮廓数组的每个元素都是一个表示轮廓的array对象;返回的轮廓层次是一个numpy.ndarray对象。
轮廓层次:返回的轮廓层次是一个numpy.ndarray对象。根据轮廓的嵌套关系,可将轮廓之间的层次关系分为父级和子级,外部轮廓为父级,内部是子级。numpy.ndarray对象的每个元素关系格式:【下一个轮廓 前一个轮廓 第一个子级轮廓 父级轮廓】,如【-1 0 2 -1】中-1表示不存咋对应轮廓,前一个轮廓在轮廓数组中的序号为0,第一个子级轮廓序号为2.
2.2 绘制轮廓
绘制轮廓用cv2.drawContours()函数:
image=cv2.drawContours(image,contours,contoursIdx,color,thickness,linetype,hierarchy,maxLevel,offset)
image是在其中绘制轮廓的图像,如先定义的一块白色图像;contours为返回的轮廓数组,contoursIdx是要绘制的轮廓的索引,大于或等于0时绘制对应序号的轮廓,负数时(通常为-1)表示绘制所有的轮廓);color为颜色。后面的都是可选参数,hierarchy是轮廓层次,maxLevel是可绘制的最大轮廓层次深度,offset是绘制轮廓的偏移位置。例如:
img3=cv2.drawContours(img3,c,-1,(0,0,255),2) #c上面接受了返回的轮廓信息
2.3 轮廓特征
①轮廓的矩
轮廓的矩包含了轮廓的各种几何特征,如面积、位置、角度、形状等。函数是cv2.moments():
ret=cv2.moments(array,binaryImage)
ret是返回的轮廓矩,是一个字典对象。大多数矩的含义比较抽象,但其中的零阶矩(m00)表示轮廓的面积;array为轮廓数组;binaryImage为True时,会将array对象的所有非0值设置为1。
img=cv2.imread('tuxing.png')
cv2.imshow('img',img)
gray=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) #转换为灰度图
ret,thresh=cv2.threshold(gray,125,255,cv2.THRESH_BINARY) #为了提高准确率,二值化阈值处理
b,c,h=cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) #查找轮廓,这里接受参数是三个因为两个报错了
for i in range(3):
m=cv2.moments(c[i])
print('轮廓%s的矩'%i,m)
print('轮廓%s的面积:'%i,m['m00']) #零阶矩表示面积
cv2.waitKey(0)
cv2.destroyAllWindows()
②轮廓的面积
ret=cv2.contourArea(contour,oriented)
ret为返回的面积,直接是一个数值;orient为可选参数,为True时,返回值的正与负表示轮廓时顺时针还是逆时针;为False(默认值)时,函数返回值为绝对值。
③轮廓的长度
ret=cv2.ardLength(contour,closed)
ret是返回值,直接是一个长度值;closed时布尔值,为True时表示轮廓是封闭的。
④轮廓的近似多边形
ret=cv2.approxPolyDP(contours,epsilon,closed)
ret是返回的多边形轮廓数组;是精度,表示多边形接近轮廓的最大距离;close是布尔值,True为封闭。
app=cv2.approxPolyDP(c[0],0.1,True) #c[0]表示上面得到的轮廓0
img=cv2.drawContours(img,[app],-1,(255,0,0),2) #绘制多边形轮廓
⑤轮廓的凸包、直边界矩形、旋转矩形、最小外包圆、拟合椭圆、拟合直线、最小外包三角形等,这里不具体介绍,需要时可具体查找。
3、霍夫变换
霍夫变换用于在图像中查找直线和圆等形状。
3.1 霍夫直线变换
cv2.HoughLines()函数利用霍夫变换算法检测图像中的直线:
lines=cv2.HoughLines(image,rho,theta,threshold)
lines是返回的直线,rho是距离的精度(以像素为单位)通常是1;theta是角度的精度,通常使用Π/180°,表示搜索所有可能的角度;threhold未阈值,值越小检测出的直线越多。
一般是在边缘检测如Canny后再检测直线,注意返回的是直线的数组信息,还要通过line函数绘制:
img=cv2.imread('xiangqi.png')
cv2.imshow('img',img)
gray=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) #转换为灰度图
edges=cv2.Canny(gray,50,150,apertureSize=3) #边缘检测
lines=cv2.HoughLines(edges,1,np.pi/180,150) #霍夫直线变换
img3=img.copy()
for line in lines: #一种固定用法。lines是一系列数组,line是一个数组(一个直线的信息)。逐条绘制
rho,theta=line[0]
a=np.cos(theta)
b=np.sin(theta)
x0,y0=a*rho,b*rho
pt1=(int(x0 + 1000 * (-b)), int(y0 + 1000 * (a))) #计算直线端点
pt2=(int(x0 - 1000 * (-b)), int(y0 - 1000 * (a))) #计算直线端点
cv2.line(img3,pt1,pt2,(0,0,255),1) #绘制
cv2.imshow('HoughLines',img3)
cv2.waitKey(0)
cv2.destroyAllWindows()
另一种cv2.HoughLinesP()函数利用概率霍夫变换算法来检测图像中的直线:
lines=cv2.HoughLinesP(image,rho,theta,threshold,minLineLength,maxLineGap)
后两个是可选参数,前者是可接受的直线的最小长度,默认值0;后者是共线线段之间的最大间隔,默认为0。
img=cv2.imread('xiangqi.png')
cv2.imshow('img',img)
gray=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) #转换为灰度图
lines=cv2.HoughLinesP(edges,1,np.pi/180,1,100,10) #霍夫直线变换
img3=img.copy()
for line in lines: #一种固定用法。lines是一系列数组,line是一个数组(一个直线的信息)。逐条绘制
x1,y1,x2,y2=line[0]
cv2.line(img3,(x1,y1),(x2,y2),(0,0,255),2) #绘制
cv2.imshow('HoughLines',img3)
cv2.waitKey(0)
cv2.destroyAllWindows()
3.2 霍夫圆变换
cv2.HoughCircles()函数利用霍夫变换查找图像中的圆:
circles=cv2.HoughCircles(image,method,dp,minDist,param1,param2,minRadius,maxRadius)
method是查找方法,可设置为cv2.HOUGH_GRADIET和cv2.HOUGH_GRADIET_ALT;dp是累加器分辨率,与图像分辨率成反比,如dp=1时累加器与输入图像的分辨率相同,dp=2时累加器的宽度和高度是输入图像的一半;minDist是圆心间的最小距离。
后四个是可选参数,param1是对应Canny边缘检测的高阈值(低阈值时高阈值的一半),默认100;param2时圆心位置必须达到的投票数,值越大,检测出的圆越少,默认100。minRadius是最小圆半径,默认为0;最后是最大圆半径,默认为0:
img=cv2.imread('xiangqi.png')
cv2.imshow('img',img)
gray=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) #转换为灰度图
edges=cv2.Canny(gray,50,150,apertureSize=3) #边缘检测
circles=cv2.HoughCircles(edges,cv2.HOUGH_GRADIENT,1,50,param2=30,minRadius=10,maxRadius=40) #霍夫直线变换
circles=np.uint16(np.around(circles))
img3=img.copy()
for i in circles[0,:]: #一种固定用法。lines是一系列数组,line是一个数组(一个直线的信息)。逐条绘制
cv2.circle(img3,(i[0],i[1]),i[2],(0,255,0),2) #绘制圆
cv2.circle(img3, (i[0],i[1]),2,(255,0,0),3) # 绘制圆心
cv2.imshow('HoughCircles',img3)
cv2.waitKey(0)
cv2.destroyAllWindows()
总结:
由于是初学者可能很多地方没有总结完全或者有误,后续深入学习后会不断回来该删,也欢迎各位朋友指正!下次学习直方图!