第五节、轮廓检测、直线和圆、多边形检测
一、轮廓检测
在计算机视觉中,轮廓检测是另一个比较重要的任务,不单是用来检测图像或者视频帧中物体的轮廓,而且还有其他操作与轮廓检测相关。这些操作中,计算多边形边界,形状逼近和计算机感 兴趣区域。
这是与图像数据交互时的简单操作,因为numpy
中的矩阵中的矩形区域可以使用数组切片(slice
)定义。在介绍物体检测(包括人脸)和物体跟踪的概念时会大量使用这种技术。
1.1 图像阈值操作
为了从一幅图像中提取我们需要的部分,应该用图像中的每一个像素点的灰度值与选取的阈值进行比较,并作出相应的判断(阈值的选取依赖于具体的问题,物体在不同的图像中可能会有不同的灰度值)。
opencv
提供了threshold()
函数对图像的阈值进行处理,图像阈值操作函数原型:
cv2.threshold(src,thresh,maxval,type[,dst])
threshold()
共支持五中类型的阈值化方式,分别是:
- 二进制阈值化;
- 反二进制阈值化;
- 截断阈值化;
- 阈值化为0和反阈值化为0。
函数返回阈值操作后的图像。函数参数如下:
src
: 输入图像,图像必须为单通道8位或32位浮点型图像;thresh
: 设定的阈值;maxval
::使用cv2.THRESH_BINARY
和cv2.THRESH_BINARY_INV
类型的最大值;type
: 阈值化类型,可以通过ThresholdTypes
查看,下面给出opencv
中五种阈值化类型及其对应公式:
dst
: 输出图像,与输入图像尺寸和类型相同;
1.2 寻找图像轮廓
opencv
中提供findContours()
函数来寻找图像中物体的轮廓,并结合drawContours()
函数将找到的轮廓绘制出。寻找图像轮廓函数原型:
cv2.findContours(image,mode,method[,contours,hierarchy[,offset]])
这个函数会修改输入图像,因此建议使用原始图像的一份拷贝(比如说img.copy()
作为输入图像)。函数返回三个值:返回修改后的图像,图像的轮廓以及它们的层次。
函数参数:
(1) image
:输入图像,函数接受的参数是二值图,即黑白的(不是灰度图),我们同样可以使用cv2.compare
,cv2.inRange
,cv2.threshold
,cv2.adaptiveThreshold
,cv2.Canny
等函数来创建二值图像,如果第二个参数为cv2.RETR_CCOMP
或cv2.RETR_FLOODFILL
,输入图像可以是32-bit
整型图像(cv2.CV_32SC1
);
(2) mode
:轮廓检索模式,如下:
其中:
RETR_EXTERNAL
:表示只检测最外层轮廓,这对消除包含在其他轮廓中的轮廓很有用(比如在大多数情况下,不需要检测一个目标包含在另一个与之相同的目标里面),对所有轮廓设置hierarchy[i][2]=hierarchy[i][3]=-1
;RETR_LIST
:提取所有轮廓,检测的轮廓不建立等级关系;RETR_CCOMP
:提取所有轮廓,并将轮廓组织成双层结构(two-level hierarchy
),顶层为连通域的外围边界,次层位内层边界;RETR_TREE
:提取所有轮廓并重新建立网状轮廓结构;RETR_FLOODFILL
:官网没有介绍,应该是洪水填充法;
(3) method
:轮廓近似方法,如下:
其中:
CHAIN_APPROX_NONE
:获取每个轮廓的每个像素,相邻的两个点的像素位置差不超过1;CHAIN_APPROX_SIMPLE
:压缩水平方向,垂直方向,对角线方向的元素,值保留该方向的重点坐标,如果一个矩形轮廓只需4个点来保存轮廓信息;CHAIN_APPROX_TC89_L1
和CHAIN_APPROX_TC89_KCOS
使用Teh-Chinl
链逼近算法中的一种;contours
:检测到的轮廓(list
),每个轮廓都是一个ndarray
,每个ndarray
是一个轮廓上点的集合。一个轮廓并不是存储轮廓上所有的点,而是只存储可以用直线描述轮廓的点,比如一个正方形,只需要四个顶点就能描述轮廓了;hierarchy
:函数返回一个可选的hierarchy
结果,这是一个ndarray
,形状为[1,轮廓个数,4],其中hierarchy[0]
元素的个数和轮廓个数相同。每个轮廓contours[0][i]
对应4个hierarchy
元素hierarchy[0][i][0]~hierarchy[0][i][3]
,分别表示后一个轮廓,前一个轮廓,父轮廓,内嵌轮廓的索引,如果没有对应项,则相应的hierarchy[0][i]
设置为负数;
(4) offset
:轮廓点可选偏移量,有默认值。
1.3 轮廓绘制
轮廓绘制函数原型:
cv2.drawContours(image,contours,contourIdx,color[,thickness[,lineType[,hierarchy[,maxLevel[,offset]]]]])
该函数返回绘制有轮廓的图像。
image
:输入/输出图像,指明在哪个图像上绘制轮廓。并且该函数会修改源图像image
;contours
:使用findContours
检测到的轮廓数据,传入一个list
。contourIdx
:绘制轮廓的索引变量(表示绘制第几个轮廓),如果为负值则绘制所有输入轮廓;color
:轮廓颜色;thickness
:绘制轮廓所用线条粗细度,如果值为负值,则在轮廓内部绘制;lineTpye
:线条类型,有默认值LINE_8,有如下可选类型;hierarchy
:可选层次结构信息;maxLevel
:用于绘制轮廓的最大等级;offset
:可选轮廓便宜参数,用制定偏移量offset=(dx, dy)
给出绘制轮廓的偏移量;
1.4 示例程序
轮廓检测示例:
'''
轮廓检测
'''
#加载图像img
img = cv2.imread('./image/img6.jpg',cv2.IMREAD_COLOR)
cv2.imshow('img',img)
#转换为灰色gray_img
gray_img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
cv2.imshow('gray_img',gray_img)
#对图像二值化处理 输入图像必须为单通道8位或32位浮点型
ret,thresh = cv2.threshold(gray_img,127,255,0)
cv2.imshow('thresh',thresh)
#寻找图像轮廓 返回修改后的图像 图像的轮廓 以及它们的层次
image,contours,hierarchy = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
cv2.imshow('image',image)
print('contours[0]:',contours[0])
print('len(contours)',len(contours))
print('hierarchy,shape',hierarchy.shape)
print('hierarchy[0]:',hierarchy[0])
#在原图img上绘制轮廓contours
img = cv2.drawContours(img,contours,-1,(0,255,0),2)
cv2.imshow('contours',img)
cv2.waitKey()
cv2.destroyAllWindows()
二、查找最小闭圆轮廓案例
找到一个正方形轮廓很简单,要找到到不规则的,歪斜的以及旋转的形状,可以用OpenCV
的cv2.findContours()
函数,它能得到最好的结果,下面来看一副图:
现实的应用会对目标的边界框,最小矩形面积,最小闭圆特别感兴趣,将cv2.findContours()
函数和少量的OpenCV
的功能相结合就非常容易实现这些功能:
使用boundingRect()
函数计算包围轮廓的矩形框,使用minEnclosingCircle()
函数计算包围轮廓的最小圆包围。
2.1 计算矩形边界框
cv2.boundingRect(img)
函数 计算并返回点集最外面的矩形边界,参数一般传入一个轮廓,contours[0]
;
x,y,w,h = cv2.boundingRect(c)
函数返回四个值,分别是x,y,w,h
。x,y
是矩阵左上点的左边,w,h
是矩阵的宽和高。
然后画出这个矩形(在原图img
上绘制):这个操作非常简单,它将轮廓信息转换为(x,y)
坐标,并加上矩形的高度和宽度。
cv2.rectangle(img,(x,y),(x+w,y+h),(0,255,0),2)
下面来将如何找到一个旋转的矩阵和圆形轮廓。 首先加载图片,然后在源图像的灰度图像上面执行一个二值化操作。这样之后,可在这个灰度图像上执行所有计算轮廓的操作,但在源图像上可利用色彩信息来画这些轮廓。
2.2 计算最小矩形边界框
计算包含出包围目标的最小矩形区域(旋转矩形):
#找到最小区域
rect = cv2.minAreaRect(c)
#计算最小矩形的坐标
box = cv2.boxPoints(rect)
#坐标转换为整数
box = np.int0(box)
这里用到一个非常有趣的机制:OpenCV
没有函数能直接从轮廓信息中计算出最小矩形顶点的坐标。所以需要计算最小矩形区域,然后计算这个矩形的顶点。注意计算出来的顶点左边是浮点型,但是所得像素的坐标值是整数,所以需要做一个转换。
函数cv2.minAreaRect()
返回一个tuple
:(最小外接矩形的中心(x,y)
,(宽度,高度),旋转角度)。
但是要绘制这个矩形,我们需要矩形的4个顶点坐标box
, 通过函数cv2.cv.BoxPoints()
获得,box
:[ [x0,y0], [x1,y1], [x2,y2], [x3,y3] ]
;
最小外接矩形的4个顶点顺序、中心坐标、宽度、高度、旋转角度(是度数形式,不是弧度数)的对应关系如下:
注意:旋转角度θ
是水平轴(x
轴)逆时针旋转,与碰到的矩形的第一条边的夹角。并且这个边的边长是width
,另一条边边长是height
。也就是说,在这里,width
与height
不是按照长短来定义的。
在opencv
中,坐标系原点在左上角,相对于x轴,逆时针旋转角度为负,顺时针旋转角度为正。在这里,θ∈(-90度,0]
。
然后画出这个矩形(在原图img
上绘制):
cv2.drawContours(img,[box],0,(255,0,0),3)
首先,该函数与所有绘图函数一样,它会修改源,其次该函数的第二个参数接收一个保存着轮廓的数组,从而可以在一次操作中绘制一系列的轮廓。因此如果只有一组点来表示多边形轮廓,可以把这组点放到一个list
中,就像前面例子里处理方框(box
)那样。这个函数第三个参数是绘制的轮廓数组的索引,-1表示绘制所有的轮廓,否则只绘制轮廓数组里指定的轮廓。
大多数绘图函数把绘图的颜色和线宽放在最后两个参数里。
2.3 计算最小闭圆
cv2.minEnclosingCircle
( points )函数 利用迭代算法,对给定的二维点集寻找计算可包围点集的最小圆形;
其中参数有:
points
: 输入的二维点集,一般传入一个轮廓contours[0]
;
函数会返回一个元组,第一个元素为圆心的坐标组成的元素,第二个元素为圆的半径值。把这些值转换为整数后就能很容易地绘制出圆来。
代码如下:
#计算闭圆中心店和和半径
(x,y),radius = cv2.minEnclosingCircle(c)
#转换为整型
center = (int(x),int(y))
radius = int(radius)
#绘制闭圆(在原图img上绘制)
img = cv2.circle(img,center,radius,(0,255,0),2)
2.4 完整代码
完整代码如下:
'''
边框 最小矩形区域和最小闭圆的轮廓
'''
img = cv2.pyrDown(cv2.imread('./image/img16.jpg',cv2.IMREAD_UNCHANGED))
#转换为灰色gray_img
gray_img = cv2.cvtColor(img.copy(),cv2.COLOR_BGR2GRAY)
#对图像二值化处理 输入图像必须为单通道8位或32位浮点型
ret,thresh = cv2.threshold(gray_img,127,255,cv2.THRESH_BINARY)
#寻找最外面的图像轮廓 返回修改后的图像 图像的轮廓 以及它们的层次
image,contours,hierarchy = cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
print(type(contours))
print(type(contours[0]))
print(len(contours))
#遍历每一个轮廓
for c in contours:
#找到边界框的坐标
x,y,w,h = cv2.boundingRect(c)
#在img图像上 绘制矩形 线条颜色为green 线宽为2
cv2.rectangle(img,(x,y),(x+w,y+h),(0,255,0),2)
#找到最小区域
rect = cv2.minAreaRect(c)
#计算最小矩形的坐标
box = cv2.boxPoints(rect)
#坐标转换为整数
box = np.int0(box)
#绘制轮廓 最小矩形 blue
cv2.drawContours(img,[box],0,(255,0,0),3)
#计算闭圆中心店和和半径
(x,y),radius = cv2.minEnclosingCircle(c)
#转换为整型
center = (int(x),int(y))
radius = int(radius)
#绘制闭圆
img = cv2.circle(img,center,radius,(0,255,0),2)
cv2.drawContours(img,contours,-1,(0,0,255),2)
cv2.imshow('contours',img)
运行后的结果:
三、凸轮廓与Douglas-Peucker
算法
大多数处理轮廓的时候,图的形状(包括凸形状)都是变化多样的。凸形状内部的任意两点的连线都在该形状内部。
3.1 cv2.approxPloyDP
cv2.approxPloyDP
函数,它用来计算近似的多边形框。该函数有三个参数:
- 第一个参数为轮廓;
- 第二个参数为
ε
值,它表示源轮廓与近似多边形的最大差值(这个值越小,近似多边形与源轮廓越接近); - 第三个参数为布尔标记,它表示这个多边形是否闭合;
ε
值对获取有用的轮廓非常重要,所以需要理解它表示什么意思。ε
是为所得到的近似多边形周长与源轮廓周长之间的最大差值,这个值越小,近似多边形与源轮廓就越相似。
为什么有了一个精确表示的轮廓却还需要得到一个近似多边形呢?这是因为一个多边形由一组直线构成,能够在一个区域里定义多边形,以便于之后进行操作与处理,这在许多计算机视觉任务中非常重要。
3.2 cv2.arcLength
在了解了ε
值是什么之后,需要得到轮廓的周长信息来作为参考值。这可以通过cv2.arcLength
函数来完成:
#arcLength获取轮廓的周长
epsilon = 0.01*cv2.arcLength(cnt,True)
#计算矩形的多边形框
approx = cv2.approxPolyDP(cnt,epsilon,True)
3.3 cv2.convexHull
可以通过OpenCV
来有效地计算一个近似多边形。为了计算凸形状,需要利用cv2.convexHull
来处理获取的轮廓信息。
#从轮廓信息中计算得到凸形状
hull = cv2.convexHull(cnt)
3.4 测试
为了理解源轮廓、近似多边形和凸包的不同之处,可以把他们放在一副图片中进行观察:
img = cv2.imread('./image/img18.jpg',cv2.IMREAD_COLOR)
img = cv2.resize(img,None,fx=0.6,fy=0.6,interpolation=cv2.INTER_CUBIC)
#创建一个空白图像,用来绘制轮廓
canvas = np.zeros(img.shape,np.uint8)
#转换为灰色gray_img
gray_img = cv2.cvtColor(img.copy(),cv2.COLOR_BGR2GRAY)
#进行均值滤波,去除一些噪声
kernel = np.ones((3,3),np.float32)/9
gray_img = cv2.filter2D(gray_img,-1,kernel)
#cv2.imshow('gray_img',gray_img)
#对图像二值化处理 输入图像必须为单通道8位或32位浮点型 像素>125 设置为0(黑) 否则设置为255(白)
ret,thresh = cv2.threshold(gray_img,125,255,cv2.THRESH_BINARY_INV)
#cv2.imshow('thresh',thresh)
#寻找图像轮廓 返回修改后的图像 图像的轮廓 以及它们的层次
image,contours,hierarchy = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
#获取最大的一个轮廓
cnt = contours[0]
max_area = cv2.contourArea(cnt)
#对每一个轮廓进行遍历
for cont in contours:
if cv2.contourArea(cont) > max_area:
cnt = cont
max_area = cv2.contourArea(cont)
print('max_area',max_area)
'''计算最大轮廓的多边形框'''
#arcLength获取轮廓的周长
epsilon = 0.01*cv2.arcLength(cnt,True)
#计算矩形的多边形框
approx = cv2.approxPolyDP(cnt,epsilon,True)
#从轮廓信息中计算得到凸形状
hull = cv2.convexHull(cnt)
print('contours',len(contours),type(contours))
print('cnt.shape',cnt.shape,type(cnt))
print('approx.shape',approx.shape,type(approx))
print('hull.shape',hull.shape,type(hull))
#在源图像中绘制所有轮廓 传入的死一个list
cv2.drawContours(img,contours,-1,(0,255,0),2) #GREEN 绘制所有的轮廓
cv2.drawContours(canvas,[cnt],-1,(0,255,0),2) #GREEN 绘制最大的轮廓
cv2.drawContours(canvas,[approx],-1,(0,0,255),2) #RED 绘制最大轮廓对应的多边形框
cv2.drawContours(canvas,[hull],-1,(255,0,0),2) #BLUE 绘制最大轮廓对应的凸包
cv2.imshow('img',img)
cv2.imshow('ALL',canvas)
cv2.waitKey()
cv2.destroyAllWindows()
如上图所示,凸包是由蓝色表示,然后里面是近似多边形,使用红色表示,在两者之间的是源图片中一个最大的轮廓,它主要由弧线构成。
四、直线和圆检测
检测边缘和轮廓不仅重要,还经常用到,它们也是构成其他复杂操作的基础。直线和形状检查与边缘和轮廓检测有密切的关系。
Hough
变换是直线和形状检测背后的理论基础,它由Richard Duda
和Peter Hart
发明,他们是对Paul Hough
在20世纪60年代早期所做工作的扩展。
Hough
变换的原理实际上是将直线坐标问题转换为参数平面问题,具体实现原理可以参考:《史上最详细的Hough
直线检测》。
4.1 直线检测
首先介绍直线检测,这可通过HoughLines
和HoughLinesP
函数来完成,它们仅有的差别是:
- 第一个函数使用标准的
Hough
变换; - 第二个函数使用概率
Hough
变换(因此名称里有一个P
)。
HoughLinesP
函数之所以称为概率版本的Hough
变换是因为它只通过分析点的子集并估计这些点都属于一条直线的概率,这是标准Hough
变换的优化版本。该函数的计算代价会少一些,执行会变得更快。
4.1.1 cv2.HoughLinesP
HoughLinesP
函数会接收一个由Candy
边缘检测滤波器处理过的单通道二值图像。 不一定需要Candy
滤波器,但是一个经过去噪并且只有边缘的图像当做Hough
变换的输入会很不错 ,因此使用Candy
滤波器是一个普遍的惯例。
HoughLinesP
函数参数如下:
- 需要处理的图像,需要是灰度图;
- 线段的几何表示
rho
和theta
,一般分别取1和np.pi/180
; - 阈值。低于该阈值的直线会被忽略。
Hough
变换可以理解为投票箱和投票数之间的关系,每一个投票箱代表一个直线,投票数达到阈值的直线会被保留,其他的会被删除; - 最小直线长度;
- 最大线段间隙;
该函数返回一个numpy.array
类型,形状为[num,1,4]
,每一行对应一条直线,每条直线形状为(1,4)
,这4个数值表示起始点和终止点坐标。
4.1.2 示例程序
下面是一个例子:
img = cv2.imread('./image/img19.jpg')
#转换为灰度图片
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
#中值滤波
gray = cv2.medianBlur(gray,ksize=3)
#边缘检测
edges = cv2.Canny(gray,50,100)
minLineLength = 200
maxLineGap = 5
#直线检测
lines = cv2.HoughLinesP(edges,1,np.pi/180,100, minLineLength,maxLineGap)
print('len(lines)',len(lines),type(lines))
print('lines[0].shape',lines[0].shape)
for i in range(len(lines)):
for x1,y1,x2,y2 in lines[i]:
cv2.line(img, (x1,y1), (x2,y2),(i*20,100+i*20,255),2)
cv2. imshow("edges", edges)
cv2. imshow("lines", img)
cv2.waitKey()
cv2.destroyAllWindows()
除了HoughLinesP
函数调用是这段代码的关键点以外,设置最小直线长度(更短的直线会被消除)和最大线段间隙也很重要,一条线段长度大于这个值会被视为两条分开的线段。
4.2 圆检测
OpenCV
的HoughCircles
函数可用来检测圆,其主要是利用霍尔变换在图像中寻找圆。我们知道,一个圆形的表达式为:
一个圆环的确定需要三个参数,那么霍尔变换的累加器必须是三维的,但是这样的计算效率很低,而OpenCV
采用了霍夫梯度的方法,这里利用了边界的梯度信息。
首先对图像进行Candy
边缘检测,对边缘中的每一个非0点,通过sobel
算子进行计算局部梯度。那么计算得到的梯度方向,实际上就是圆切线的法线。三条法线即可确定一个圆心,同理在累加器中对圆心通过的法线进行累加,就得到可圆环的判定。
4.2.1 cv2.HoughCircles
函数原型:
cv2.HoughCircles(img,method,dp,minDist,circles,param1,param2,minRadius,maxRadius)
函数的参数如下:
-
img
为输入图像,需要是灰度图; -
method
为检测方法,常用cv2.HOUGH_GRADIENT
; -
dp
为检测内侧圆心的累加器图像的分辨率于输入图像之比的倒数;- 如
dp=1
,累加器和输入图像具有相同的分辨率; - 如果
dp=2
,累加器便有输入图像一半那么大的宽度和高度;
- 如
-
minDist
表示两个圆之间圆心的最小距离。 -
param1
有默认值100
,它是method
设置的检测方法对应的参数,对当前唯一的方法霍夫梯度法cv2.HOUGH_GRADIENT
,它表示传递给Candy
边缘检测算子的高阈值,而低阈值为高阈值的一半; -
param2
有默认值100
,它是method
设置的检测方法对应的参数,对当前唯一的方法霍夫梯度法cv2.HOUGH_GRADIENT
,它表示在检测阶段圆心的累加器阈值,它越小,就越可以检测到更多根本不存在的圆,而它越大的话,能通过检测的圆就更接近完美的圆形了; -
minRadius
有默认值0,圆半径的最小值; -
maxRadius
有默认值0,圆半径的最大值;
4.2.2 示例程序
下面是一个例子:
img = cv2.imread('./image/img20.jpg')
#缩小
img = cv2.resize(img,None,fx=0.5,fy=0.5,interpolation=cv2.INTER_CUBIC)
#转换为灰度图片
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
#中值滤波
gray = cv2.medianBlur(gray,ksize=3)
#圆检测
circles = cv2.HoughCircles(gray,cv2.HOUGH_GRADIENT,1,120,param1=100,param2=30,minRadius=0,maxRadius=0)
print('circles',type(circles),circles.shape) #circles <class 'numpy.ndarray'> (1, 3, 3)
circles = np.uint16(np.around(circles))
for i in circles[0,:]:
#绘制圆 (i[0],i[1])为圆心,i[2]为半径
cv2.circle(img,(i[0],i[1]),i[2],(0,255,0),2)
#绘制圆心
cv2.circle(img,(i[0],i[1]),2,(255,0,0),3)
cv2.imshow('circles',img)
cv2.waitKey()
cv2.destroyAllWindows()
4.3 检测其他形状
Hough
变换能检测的形状仅限于圆,但是前面曾提到过检测任何形状的方法,特别是用approxPloyDP
函数来检测。该函数提供多边形的近似,所以如果你的图像有多边形,再结合cv2.findContous
函数和cv2.approxPloyDP
函数,就可以相当准确的检测出来。
五、代码下载
参考文章:
[1] python-opencv2
利用cv2.findContours()
函数来查找检测物体的轮廓
[2] Python
下opencv
使用笔记(十一)(详解hough
变换检测直线与圆)