opencv 轮廓处理相关问题

在 opencv 上使用 cv::findContours 查找轮廓,经常遇到轮廓层次、点序等问题,这里对相关问题做一个统一总结

轮廓的层次关系

cv: : findContours 返回轮廓层次关系是 [1, N, 4],其中 N 是轮廓数量,4 是表示层次关系的四元组:[下一个,上一个,First_Child,父],元组内存储的是轮廓 Contours 的序号

  • RETR_EXTERNAL: 只寻找最高层级的轮廓
  • RETR_LIST: 最简单的一种寻找方式,它不建立轮廓间的子属关系,也就是所有轮廓都属于同一层级, hierarchy 中的后两个值 [First Child, Parent] 都为 - 1
  • RETR_CCOMP: 它把所有的轮廓只分为 2 个层级,不是外层的就是里层的
  • RETR_TREE: 完整建立轮廓的层级从属关系
1
2
3
4
5
6
7
8
9
10
11
# 生成模拟图
img1 = np.zeros((300, 450,3), 'uint8')
img1 = cv2.line(img1,[30,10],[100,10],(0,100,255),4)
img1 = cv2.line(img1,[120,15],[430,15],(0,100,255),4)
img1 = cv2.rectangle(img1, [20,60 ,400 ,220],(0,100,255),2)
img1 = cv2.rectangle(img1, [50 ,100, 340, 150],(0,100,255),2)
img1 = cv2.rectangle(img1, [90 ,130 ,140 ,100],(0,100,255),-1)
img1 = cv2.rectangle(img1, [260, 155, 100, 50],(0,100,255),-1)
img1 = cv2.rectangle(img1, [105 ,150 ,100 ,20],0,-1);
img1 = cv2.rectangle(img1, [105 ,190 ,100 ,30],0,-1);
show_images([img1])

png

1
2
3
# 查找轮廓
contours,hierarchy=cv2.findContours(img1[...,-1],cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
contours[0].shape,contours[1].shape,len(contours),hierarchy.shape
((8, 1, 2), (8, 1, 2), 10, (1, 10, 4))

hierarchy 表示返回轮廓层次关系是 [1, 10, 4],其中 10 是轮廓数量,4 是表示层次关系的四元组:[下一个,上一个,First_Child,父]

1
2
3
4
5
6
7
8
9
10
11
# 渲染函数
import copy

def render_image(contours):
img2=copy.deepcopy(img1)
for idx,contour in enumerate(contours):
if idx==1 or idx==3:
cv2.putText(img2,str(idx),(contour[0,0,0],contour[0,0,1]+15),cv2.FONT_HERSHEY_SCRIPT_SIMPLEX, 0.5, (255, 0, 0),2)
else:
cv2.putText(img2,str(idx),(contour[0,0,0],contour[0,0,1]),cv2.FONT_HERSHEY_SCRIPT_SIMPLEX, 0.5, (255, 0, 0),2)
show_images([img2])

RETR_EXTERNAL

1
2
contours,hierarchy=cv2.findContours(img1[...,-1],cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
render_image(contours)

png

RETR_LIST

1
2
contours,hierarchy=cv2.findContours(img1[...,-1],cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)
render_image(contours)

png

RETR_CCOMP

1
2
3
contours,hierarchy=cv2.findContours(img1[...,-1],cv2.RETR_CCOMP,cv2.CHAIN_APPROX_SIMPLE)
render_image(contours)
hierarchy

png

RETR_TREE

1
2
3
contours,hierarchy=cv2.findContours(img1[...,-1],cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
render_image(contours)
hierarchy

png

轮廓点点序

contours 表示轮廓的点,对于每个轮廓来说,其点是按什么顺序返回呢?

首先通过以下代码生成模拟图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import math
from PIL import Image, ImageDraw
from PIL import ImagePath
import numpy as np
import cv2
import copy

# 由点生成模拟图片
side = 8
xy = [
tuple([
132.6923076923077,
72.11538461538458
]),
tuple([
69.23076923076923,
245.19230769230768
]),
tuple([
207.69230769230768,
133.65384615384613
]),
tuple([
50.9230769230769229,
133.65384615384613
]),
tuple([
200.03846153846155,
248.07692307692304
])
]

image = ImagePath.Path(xy).getbbox()
size = list(map(int, map(math.ceil, (300,300))))
img = Image.new("RGB", size, "#000000")
img1 = ImageDraw.Draw(img)
img1.polygon(xy, fill ="#eeeeff", outline ="blue")

np_img = np.array(img, dtype=np.uint8)
np_img=cv2.cvtColor(np_img,cv2.COLOR_RGB2GRAY)
show_images([np_img])

opencv轮廓处理相关问题-20250201201205-1

然后使用 cv2.findContours 找到轮廓点,接着使用绘制函数逐渐将点绘制出来,每绘制一个点,输出一次图片,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 使用findContours计算边缘点
contours,hierarchys = cv2.findContours(np_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) #原始mask,检测轮廓及层次关系
print(len(contours),len(contours[0]))

# 取第一组点
contour=np.array(contours[0])
contour=contour.reshape((-1,contour.shape[1],contour.shape[-1])) #去掉矩阵中间维度1
contour=cv2.approxPolyDP(contour,2,True) #使用opencv求得近似多边形的点集合
approx_contours=np.reshape(contour,(contour.shape[0],contour.shape[2]))

# 依次绘制findContours返回出点
img=np.zeros((300,300,3),np.uint8)
LABEL_COLORMAP = imgviz.label_colormap(value=200)
imgs=[]
points_colors=[]
for i,point in enumerate(approx_contours):
x,y=point
getcolor=tuple(LABEL_COLORMAP[(i+1)%len(LABEL_COLORMAP)].tolist())
cv2.circle(img,(x,y),10,getcolor,-1)
points_colors.append(getcolor)
imgs.append(copy.deepcopy(img))

# 查看图片->逆时针or顺时针
show_images([np.array(points_colors).reshape(1,len(points_colors),3).astype(np.uint8)])
show_images(imgs,2)

opencv轮廓处理相关问题-20250201201206

总结:findContours 以最左边的点为起点,并逆时针返回边缘点

极少像素的边缘点

对于找到边缘为极少数的情况,cv2.findContours 返回轮廓点是怎么样的呢?本节进行研究

1
2
3
4
5
6
7
8
# 定义cv2.findContours查找边缘函数,并绘制结果图
import matplotlib.pyplot as plt
def check_findContours(binary):
contours,hierarchy=cv2.findContours(binary,
cv2.RETR_CCOMP,cv2.CHAIN_APPROX_SIMPLE)
print('原始非0位置:',np.where(binary>0),'\nfindContours查找位置:',np.array(contours).tolist())
cv2.drawContours(binary,contours,-1,(9),1)
show_images([binary],set_locator=True,is_grid=True)

单个点

1
2
3
4
5
# 生成图像
import numpy as np
binary1=np.zeros((10,10),np.uint8)
binary1[5,5]=9
check_findContours(binary1)
原始非0位置: (array([5]), array([5])) 
findContours查找位置: [[[[5, 5]]]]

png

两个同向点

1
2
3
binary2=np.zeros((10,10),np.uint8)
binary2[5:7,6]=9
check_findContours(binary2)
原始非0位置: (array([5, 6]), array([6, 6])) 
findContours查找位置: [[[[6, 5]], [[6, 6]]]]

png

两个对角点

1
2
3
4
binary3=np.zeros((10,10),np.uint8)
binary3[5,5]=9
binary3[6,6]=9
check_findContours(binary3)
原始非0位置: (array([5, 6]), array([5, 6])) 
findContours查找位置: [[[[5, 5]], [[6, 6]]]]

png

三个点

1
2
3
4
binary4=np.zeros((10,10),np.uint8)
binary4[5:7,6]=9
binary4[6,7]=9
check_findContours(binary4)
原始非0位置: (array([5, 6, 6]), array([6, 6, 7])) 
findContours查找位置: [[[[6, 5]], [[6, 6]], [[7, 6]]]]

png

四个点

1
2
3
binary5=np.zeros((10,10),np.uint8)
binary5[5:7,5:7]=9
check_findContours(binary5)
原始非0位置: (array([5, 5, 6, 6]), array([5, 6, 5, 6])) 
findContours查找位置: [[[[5, 5]], [[5, 6]], [[6, 6]], [[6, 5]]]]

png

结论:cv2.findContours 返回属于目标内部最边缘的点,如果只有一个点,返回点本身