openCV中可以使用一些函数找到图像轮廓,进而获取目标图像的大小、位置、方向等信息。

下面将针对下图来做图像轮廓的说明:

查找图像轮廓findCounters

1
contours, hierarchy = cv2.findContours( image, mode, method)

式中的返回值为:

  • contours:返回的轮廓。
  • hierarchy:图像的拓扑信息(轮廓层次)。

式中的参数为:

  • image:原始图像。8位单通道图像,所有非零值被处理为1,所有零值保持不变。也就是说灰度图像会被自动处理为二值图像。在实际操作时,可以根据需要,预先使用阈值处理等函数将待查找轮廓的图像处理为二值图像。
  • mode:轮廓检索模式。
  • method:轮廓的近似方法。

函数cv2.findContours()的返回值及参数的含义比较丰富,下面对上述返回值和参数逐一做出说明。

返回值contours

该返回值返回的是一组轮廓信息,类型为list,每个轮廓都是由若干个点所构成的。

  1. coutours[i][j]表示的是第i个轮廓内的第j个点。
  2. len(contours)的结果表示图中找到了多少个轮廓,len(contours[0])的结果表示第1个轮廓中包含了多少个点。
  3. 直接打印contours可以获取某个轮廓中具体点的位置属性。

返回值hierarchy

根据轮廓之间的关系,就能够确定一个轮廓与其他轮廓是如何连接的。比如,确定一个轮廓是某个轮廓的子轮廓,或者是某个轮廓的父轮廓。上述关系被称为层次(组织结构),返回值hierarchy就包含上述层次关系。

contours[i]对应的四个元素可以说明当前轮廓的层次关系

1
[Next, Previous, First_Child, Parent]

式中各元素的含义为:

  • Next:后一个轮廓的索引编号。
  • Previous:前一个轮廓的索引编号。
  • First_Child:第1个子轮廓的索引编号。
  • Parent:父轮廓的索引编号。

如果上述各个参数所对应的关系为空时,也就是没有对应的关系时,则将该参数所对应的值设为“-1”。

需要注意,轮廓的层次结构是由参数mode决定的。也就是说,使用不同的mode,得到轮廓的编号是不一样的,得到的hierarchy也不一样。

参数mode

参数mode决定了轮廓的提取方式,具体有如下4种:

  • cv2.RETR_EXTERNAL:只检测外轮廓。
  • cv2.RETR_LIST:对检测到的轮廓不建立等级关系。
  • cv2.RETR_CCOMP:检索所有轮廓并将它们组织成两级层次结构。上面的一层为外边界,下面的一层为内孔的边界。如果内孔内还有一个连通物体,那么这个物体的边界仍然位于顶层。
  • cv2.RETR_TREE:建立一个等级树结构的轮廓。
1
2
3
contours, hierarchy = cv.findContours(rst, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_NONE)
print(len(contours))
print(hierarchy)

得到输出结果为:

1
2
3
2
[[[ 1 -1 -1 -1]
[-1 0 -1 -1]]]

表示对该图检测外轮廓的时候,只检测到了两个,也就是最外侧的两个长方形,假设此时第一个较大的长方形序号为0,第二个为1。同时打印hierarchy,该结果可这么理解:

  • 输出值“[ 1 -1 -1 -1]”,表示的是第0个轮廓的层次。
    • 它(即第0个轮廓)的后一个轮廓就是第1个轮廓,因此第1个元素的值是“1”。
    • 它的前一个轮廓不存在,因此第2个元素的值是“-1”。
    • 它不存在子轮廓,因此第3个元素的值是“-1”。
    • 它不存在父轮廓,因此第4个元素的值是“-1”。
  • 输出值“[-1 0 -1 -1]”,表示的是第1个轮廓的层次。
    • 它(即第1个轮廓)的后一个轮廓是不存在的,因此第1个元素的值是“-1”。
    • 它的前一个轮廓是第0个轮廓,因此第2个元素的值是“0”。
    • 它不存在子轮廓,因此第3个元素的值是“-1”。
    • 它不存在父轮廓,因此第4个元素的值是“-1”。

两者之间的关系为:

而cv2.RETR_CCOMP和cv2.RETR_TREE可以构建两层轮廓。当有多层轮廓时,应该使用树形结构的轮廓检测。

参数method

参数method决定了如何表达轮廓,可以为如下值:

  • cv2.CHAIN_APPROX_NONE:存储所有的轮廓点,相邻两个点的像素位置差不超过1,即max(abs(x1-x2), abs(y2-y1))=1。
  • cv2.CHAIN_APPROX_SIMPLE:压缩水平方向、垂直方向、对角线方向的元素,只保留该方向的终点坐标。例如,在极端的情况下,一个矩形只需要用4个点来保存轮廓信息。
  • cv2.CHAIN_APPROX_TC89_L1:使用teh-Chinl chain近似算法的一种风格
  • cv2.CHAIN_APPROX_TC89_KCOS:使用teh-Chinl chain近似算法的一种风格。

说白了就是检测长方形的时候,第一种参数画了整个框,第二种就瞄了四个点。

注意事项

  • 待处理的源图像必须是灰度二值图。因此,在通常情况下,都要预先对图像进行阈值分割或者边缘检测处理,得到满意的二值图像后再将其作为参数使用。
  • 在OpenCV中,都是从黑色背景中查找白色对象。因此,对象必须是白色的,背景必须是黑色的。

果然踩坑了踩坑了。难怪刚刚用白底的时候,画出来的len(contours)都是1。

绘制图像轮廓drawContours

函数参数介绍与简单绘制

1
2
3
4
5
6
7
8
9
10
image=cv2.drawContours(
image,
contours,
contourIdx,
color[,
thickness[,
lineType[,
hierarchy[,
maxLevel[,
offset]]]]] )

其中,函数的返回值为image,表示目标图像,即绘制了边缘的原始图像。
该函数有如下参数:

  • image:待绘制轮廓的图像。需要注意,函数cv2.drawContours()会在图像image上直接绘制轮廓。也就是说,在函数执行完以后,image不再是原始图像,而是包含了轮廓的图像。因此,如果图像image还有其他用途的话,则需要预先复制一份,将该副本图像传递给函数cv2.drawContours()使用。(我又踩坑了,这个image必须是原图,如果在转化后的灰度图或者是二值图像上都是没用的。。

  • contours:需要绘制的轮廓。该参数的类型与函数cv2.findContours()的输出contours相同,都是list类型。

  • contourIdx:需要绘制的边缘索引,告诉函数cv2.drawContours()要绘制某一条轮廓还是全部轮廓。如果该参数是一个整数或者为零,则表示绘制对应索引号的轮廓;如果该值为负数(通常为“-1”),则表示绘制全部轮廓。

  • color:绘制的颜色,用BGR格式表示。

  • thickness:可选参数,表示绘制轮廓时所用画笔的粗细。如将该值设置为“-1”,则表示要绘制实心轮廓。

  • lineType:可选参数,表示绘制轮廓时所用的线型。

  • hierarchy:对应函数cv2.findContours()所输出的层次信息。

  • maxLevel:控制所绘制的轮廓层次的深度。如果值为0,表示仅仅绘制第0层的轮廓;如果值为其他的非零正数,表示绘制最高层及以下的相同数量层级的轮廓。

  • offset:偏移参数。该参数使轮廓偏移到不同的位置展示出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
image = cv.imread("img/img2.jpg")
img = cv.cvtColor(image, cv.COLOR_BGR2GRAY) # 先转化为灰度图
t, rst = cv.threshold(img, 100, 255, cv.THRESH_BINARY) # 转化为二值图像

# 现在的返回参数只有两个
contours, hierarchy = cv.findContours(rst, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_NONE)
print(len(contours)) # 2

# 记得,image.copy(),这样才不会更改原图
result = cv.drawContours(image.copy(), contours, -1, (0, 0, 255), thickness=5)

cv.imshow("origin", image)
cv.imshow("result", result)

得到结果如下图所示:

逐一显示所有轮廓

感觉这也是很好玩的操作,我们在黑色背景的基础上,加上检测出来的外框。

所以,如果要绘制图像内的某个具体轮廓,需要将函数cv2.drawContours()的参数contourIdx设置为具体的索引值,通过循环语句逐一绘制轮廓。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
contours, hierarchy = cv.findContours(rst, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_NONE)
length = len(contours)
contoursImg = []

cv.imshow("origin", image)
# 遍历每一个轮廓
for i in range(length):
temp = np.zeros(image.shape, np.uint8) # 设置底边
contoursImg.append(temp)
# 然后给它画上相应的轮廓
contoursImg[i] = cv.drawContours(contoursImg[i], contours, i, (255, 255, 255), 5)

cv.imshow("contours[" + str(i) + "]", contoursImg[i])

# result = cv.drawContours(image.copy(), contours, -1, (0, 0, 255), thickness=5)

取前景对象

将函数cv2.drawContours()的参数thickness的值设置为“-1”,可以绘制前景对象的实心轮廓。将该实心轮廓与原始图像进行“按位与”操作,即可将前景对象从原始图像中提取出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
image = cv.imread("img/rocket.jpg")
img = cv.cvtColor(image, cv.COLOR_BGR2GRAY) # 先转化为灰度图
t, rst = cv.threshold(img, 70, 255, cv.THRESH_BINARY) # 转化为二值图像

# 现在的返回参数只有两个
contours, hierarchy = cv.findContours(rst, cv.RETR_LIST, cv.CHAIN_APPROX_NONE)
length = len(contours)
# thickness = -1时可以得到实心的轮廓
mask = np.zeros(image.shape, np.uint8) # 还是在黑色的背景下把边框给画出来
mask = cv.drawContours(mask, contours, -1, (255, 255, 255), thickness=-1)
loc = cv.bitwise_and(image, mask) #按位与

cv.imshow("result", np.hstack((image, mask, loc)))
cv.waitKey()
cv.destroyAllWindows()

得到最后结果如下,因为阈值的原因,地球外面的那一圈消失了,问题不大。

轮廓面积和周长计算

面积计算

函数cv2.contourArea()用于计算轮廓的面积。

1
retval = cv2.contourArea(contour [, oriented] )

返回值retval是面积值。

式中有两个参数:

  • contour是轮廓。
  • oriented是布尔型值。当它为True 时,返回的值包含正/负号,用来表示轮廓是顺时针还是逆时针的。该参数的默认值是False,表示返回的retval是一个绝对值。

周长计算

函数cv2.arcLength()用于计算轮廓的长度。

1
retval = cv2.arcLength( curve, closed )

式中返回值retval是轮廓的长度(周长)。

上式中有两个参数:

  • curve是轮廓。
  • closed是布尔型值,用来表示轮廓是否是封闭的。该值为True时,表示轮廓是封闭的。
1
2
3
4
5
6
7
8
9
10
11
image = cv.imread("img/img2.jpg")
img = cv.cvtColor(image, cv.COLOR_BGR2GRAY) # 先转化为灰度图
t, rst = cv.threshold(img, 70, 255, cv.THRESH_BINARY) # 转化为二值图像

# 现在的返回参数只有两个
contours, hierarchy = cv.findContours(rst, cv.RETR_LIST, cv.CHAIN_APPROX_NONE)
length = len(contours)

for i in range(length):
# 计算图中所有轮廓的周长和面积
print("Area is: ", cv.contourArea(contours[i]), ", and Length is: ", cv.arcLength(contours[i], True))

运行后可以得到结果:

1
2
3
Area is:  59109.0 , and Length is:  1012.0
Area is: 55870.5 , and Length is: 1122.5554724931717
Area is: 385392.0 , and Length is: 2644.0

轮廓拟合

名词很高级,其实就是画一个接近轮廓的多边形,内接或外接的近似多边形,实例解决一两个例子,然后其他的记录下函数,因为太多了。

矩形包围框

函数boundingRect()能够绘制轮廓的矩形边界

1
retval = cv.boundingRect(array)

注意,它处理的不是图像,而是使用findContours之后找到的轮廓contours。

式中:

  • 返回值retval表示返回的矩形边界的左上角顶点的坐标值及矩形边界的宽度和高度。
  • 参数array是灰度图像或轮廓。

并且它具有四个返回值:

1
x, y, w, h = cv2.boundingRect( array )

这里的4个返回值分别表示:

  • 矩形边界左上角顶点的x坐标。
  • 矩形边界左上角顶点的y坐标。
  • 矩形边界的x方向的长度。
  • 矩形边界的y方向的长度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
image = cv.imread("img/img2.jpg")
img = cv.cvtColor(image, cv.COLOR_BGR2GRAY) # 先转化为灰度图
t, rst = cv.threshold(img, 70, 255, cv.THRESH_BINARY) # 转化为二值图像

# 现在的返回参数只有两个
contours, hierarchy = cv.findContours(rst, cv.RETR_LIST, cv.CHAIN_APPROX_NONE)

# 发现contours[1]是三角形的框
loc1 = cv.drawContours(image.copy(), contours, 1, (0, 0, 255), 5)

x, y, w, h = cv.boundingRect(contours[1])

# 绘制矩形有两种方法
# 方法一
rect_bounding = np.array([[[x, y]], [[x+w, y]], [[x+w, y+h]], [[x, y+h]]])
loc2 = cv.drawContours(image.copy(), [rect_bounding], -1, (0, 0, 255), 5)
# 方法二
loc3 = cv.rectangle(image.copy(), (x, y), (x+w, y+h), (0, 255, 0), 5)

cv.imshow("result", np.hstack((loc1, loc2, loc3)))

cv.waitKey()
cv.destroyAllWindows()

运行后可得到结果:

最小包围圆形

函数cv.minEnclosingCircle()通过迭代算法构造一个对象的面积最小包围图形:

1
center, radius = cv.minEnclosingCircle(points)

式中:

  • 返回值center是最小包围圆形的中心。
  • 返回值radius是最小包围圆形的半径。
  • 参数points是轮廓。
1
2
3
4
5
6
7
8
9
# 找三角形的外接圆
(x, y), radius = cv.minEnclosingCircle(contours[1])

# 使用circle画圆
center = (int(x), int(y))
radius = int(radius)
loc2 = cv.circle(image.copy(), center, radius, (255, 0, 0), 5)

cv.imshow("result", np.hstack((loc1, loc2)))

运行后结果为:

还有其他的我就不示范了,随用随查~