概述
本轮廓提取算法来自opencv,findContours()。
根据论文:
Satoshi Suzuki and others. Topological structural analysis of digitized binary images by border following. Computer Vision, Graphics, and Image Processing, 30(1):32–46, 1985.
轮廓的边界分为外边界和孔边界,这个暂时没有理清,看其他人的说法实现起来怪怪的。
此外轮廓还分为父轮廓和子轮廓,父轮廓包裹子轮廓,但似乎不要求父轮廓是一个完整的环
原理步骤
初始化
- 假设输入图像为 f ( i , j ) f(i,j) f(i,j), i i i为行, j j j为列。
- 边界序号为NBD,初始化为1,上一个边界序号为LNBD,同样初始化为1
- 每次到达新的一行,需要将LNBD置1
- 严格来说,需要先将图像向外扩展1个像素,填充为非轮廓的值,否则应当从点(1,1)开始(初始点为(0,0)),但是这样会导致最边缘的轮廓线没有识别
- 计算过程中会将图中的值逐渐替换为轮廓标记
开始遍历全图
1.搜索轮廓起点
当
f
(
i
,
j
)
=
0
f(i,j)=0
f(i,j)=0,无操作
当
f
(
i
,
j
)
=
1
f(i,j)=1
f(i,j)=1,分为两种情况:
-
若 f ( i , j ) = 1 f(i,j)=1 f(i,j)=1且 f ( i , j − 1 ) = 0 f(i,j-1)=0 f(i,j−1)=0,则 i , j i,j i,j是外边界开始点,NBD+=1, ( i 2 , j 2 ) = ( i , j − 1 ) (i2,j2) = (i,j-1) (i2,j2)=(i,j−1)
-
若 f ( i , j ) > = 1 f(i,j)>=1 f(i,j)>=1且 f ( i , j + 1 ) = 0 f(i,j+1)=0 f(i,j+1)=0,则 i , j i,j i,j是孔边界开始点,NBD+=1, ( i 2 , j 2 ) = ( i , j + 1 ) (i2,j2) = (i,j+1) (i2,j2)=(i,j+1),且如果 f ( i , j ) > 1 f(i,j)>1 f(i,j)>1,说明是查找过的点,则 L N B D = f ( i , j ) LNBD = f(i,j) LNBD=f(i,j)
-
根据下表判断当前边界的父边界:
实际上就是上一个边界类型和当前边界类型不同,则父是上一个,反之若类型相同,则父与上一个相同。注意,子边界只记录第一个子边界。
关于轮廓(边界)的层级:hierarchy轮廓层级详解
OPENCV中将轮廓信息记录为一串数组,由四个数描述,这四个数分别代表后一个轮廓的序号、前一个轮廓的序号、子轮廓的序号、父轮廓的序号,第一层轮廓固定为[-1,-1,1,-1]
-
其它情况,到第4步
2.顺时针检查轮廓
以 ( i , j ) (i,j) (i,j)为中心, ( i 2 , j 2 ) (i2,j2) (i2,j2)为起点,按顺时针方向查找 ( i , j ) (i,j) (i,j)的领域内是否存在非0像素点,若找到,则记为 ( i 1 , j 1 ) (i1,j1) (i1,j1)。否则令 f ( i , j ) = − N B D f(i,j)=-NBD f(i,j)=−NBD,转到第4步
( i 2 , j 2 ) = ( i 1 , j 1 ) , ( i 3 , j 3 ) = ( i , j ) (i2,j2)=(i1,j1),(i3,j3)=(i,j) (i2,j2)=(i1,j1),(i3,j3)=(i,j)
先计算顺时针可能是为了防止触发后面步骤中
3.逆时针跟踪轮廓
以
(
i
3
,
j
3
)
(i3,j3)
(i3,j3)为中心,逆时针方向,
(
i
2
,
j
2
)
(i2,j2)
(i2,j2)的下一个点为起点,查找
(
i
3
,
j
3
)
(i3,j3)
(i3,j3)邻域内的非0像素点,记为
(
i
4
,
j
4
)
(i4,j4)
(i4,j4)
- 如果检查过 ( i 3 , j 3 + 1 ) (i3,j3+1) (i3,j3+1),且该点是0,则 f ( i 3 , j 3 ) = − N B D f(i3,j3) = -NBD f(i3,j3)=−NBD
- 如果 ( i 3 , j 3 + 1 ) (i3,j3+1) (i3,j3+1)不是已经检查过的0像素点,则 f ( i 3 , j 3 ) = N B D f(i3,j3) = NBD f(i3,j3)=NBD
- 其它情况不改变 f ( i 3 , j 3 ) f(i3,j3) f(i3,j3)
此时,如果
(
i
4
,
j
4
)
=
(
i
,
j
)
(i4,j4)=(i,j)
(i4,j4)=(i,j)且
(
i
3
,
j
3
)
=
(
i
1
,
j
1
)
(i3,j3)=(i1,j1)
(i3,j3)=(i1,j1),说明回到起点,转到第4步。否则令
(
i
2
,
j
2
)
=
(
i
3
,
j
3
)
,
(
i
3
,
j
3
)
=
(
i
4
,
j
4
)
(i2,j2)=(i3,j3),(i3,j3)=(i4,j4)
(i2,j2)=(i3,j3),(i3,j3)=(i4,j4),回到第3步开始循环
4.其它
如果 f ( i , j ) ! = 1 f(i,j)!=1 f(i,j)!=1,则 L N B D = ∣ f ( i , j ) ∣ LNBD=|f(i,j)| LNBD=∣f(i,j)∣,然后继续遍历图像像素
注:以上步骤主要来自这篇文章:https://zhuanlan.zhihu.com/p/107257870
Python实现
下面代码可以运行,就是可能有缩进的问题
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread('xxxxx')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
gray = (gray/255).astype(np.int)
plt.figure(3)
plt.imshow(gray,cmap=plt.cm.gray)
#graycp = gray.copy()
NBD = 1
sursize = 8 #8连通
hierarchy = [[-1,-1,-1,-1],[-1,-1,1,-1]] #第0个随便填了全-1
dir_8 = [[0,-1],[-1,-1],[-1,0],[-1,1],[0,1],[1,1],[1,0],[1,-1],[0,-1],[-1,-1],[-1,0],[-1,1],[0,1],[1,1],[1,0],[1,-1]]#8连通域的8个方向,重复一次
for i in range(1,gray.shape[0] - 1):
LNBD = 1
for j in range(1,gray.shape[1] - 1):
if gray[i,j] == 0:# 0不在考虑范围内
continue
is_outline = 0
#第一步------------------------------------
if gray[i,j] == 1 and gray[i,j-1] == 0:
NBD = NBD + 1
[i2,j2] = dir_8[0]
dir2 = 0
is_outline = 1
#与上一个边界不同,那么父轮廓就是上一个边界
if LNBD < 0:
hierarchy = hierarchy + [[NBD + 1,NBD - 1, -1, -LNBD]]#后一个,前一个,子轮廓,父轮廓,外/孔,关于后一个和前一个,目前还不知道具体指什么
if hierarchy[-LNBD][2] == -1:
hierarchy[-LNBD][2] = NBD
else:
hierarchy = hierarchy + [[NBD + 1,NBD - 1, -1, hierarchy[LNBD][3]]]
elif gray[i,j]>=1 and gray[i,j+1] == 0:
NBD=NBD+1
[i2,j2] = dir_8[4]
dir2 = 4
is_outline = 2
if LNBD > 0:
hierarchy = hierarchy + [[NBD + 1,NBD - 1, -1, LNBD]]
if hierarchy[LNBD][2] == -1:
hierarchy[LNBD][2] = NBD
else:
hierarchy = hierarchy + [[NBD + 1,NBD - 1, -1, hierarchy[-LNBD][3]]]
if gray[i,j] > 1:
LNBD = gray[i,j]
if is_outline > 0:
print('newOutline',[i,j])
[i1,j1] = [i,j]
[i3,j3] = [i,j]
[i4,j4] = [i,j]
dir2 = dir2
get = 0
breakwhile = 0
#第二步 ------------------------------------------------------
for surround_i in range(1,8):
if gray[dir_8[dir2 + surround_i][0] + i,dir_8[dir2 + surround_i][1] + j] != 0:
[i1,j1] = [dir_8[dir2 + surround_i][0] + i,dir_8[dir2 + surround_i][1] + j]
[i2,j2] = [i1,j1]
[i3,j3] = [i,j]
get = 1
dir2 = dir2 + surround_i
if dir2 < 7: #将下标往后推一个周期,为了做逆时针查找
dir2 = dir2 + 8
break
#第三步 ---------------------------------------------------
if get == 0: #没找到
gray[i,j] = -NBD
break
else:
while 1:
neg = 0
for surround_i2 in range(-1,-8,-1):
#防止越界(严格说应该把图像边界向外扩展1个像素)
if dir_8[dir2 + surround_i2][0] + i3 < 0 or dir_8[dir2 + surround_i2][0] + i3 >= gray.shape[0] or dir_8[dir2 + surround_i2][1] + j3 < 0 or dir_8[dir2 + surround_i2][1] + j3 >= gray.shape[1]:
breakwhile = 1
continue
#检查[i,j+1]是否是已检查过的0值点
if dir_8[dir2 + surround_i2]] == [0,1] and gray[dir_8[dir2 + surround_i2][0] + i3,dir_8[dir2 + surround_i2][1] + j3] == 0:
neg = 1
#查找[i3,j3]邻域,以i2,j2为起点
if gray[dir_8[dir2 + surround_i2][0] + i3,dir_8[dir2 + surround_i2][1] + j3] != 0:
[i4,j4] = [dir_8[dir2 + surround_i2][0] + i3,dir_8[dir2 + surround_i2][1] + j3]
break
print("first point",[i,j],"find",[i4,j4])
#接触到边界
if breakwhile == 1:
break
if neg == 1:
gray[i3,j3] = -NBD
elif gray[i3,j3] == 1:
gray[i3,j3] = NBD
if [i4,j4] == [i,j] and [i3,j3] == [i1,j1]:#回到了起点
break
else:
dir2 = dir2 + surround_i2 + (int)(sursize/2) #保存[i3,j3]指向[i2,j2]的方向,也就是直接把找到的方向翻过来就可以
if dir2 > 15: #保证dir2不越界
dir2 = dir2 - 8
if dir2 < 7:
dir2 = dir2 + 8
[i2,j2]=[i3,j3]
[i3,j3]=[i4,j4]#继续找
if gray[i,j] != 1:
LNBD = gray[i,j] #考虑到父轮廓的判别,我没有取绝对值,但总觉得怪怪的,这里应该有坑,或者有别的方法
plt.figure(4)
plt.imshow(gray,cmap=plt.cm.gray)
效果
原图
赋值结果
14里面为啥有个15?感觉代码还是有问题
结果
目前代码的父子轮廓关系还比较混乱,暂时没有想到好的修复方向