OpenCV Findcontours( ) 函数原理出自于该论文的算法:Topological Structural Analysis of Digitized Binary Images by Border Following
文章传送门:http://pdf-s3.xuebalib.com:1262/1ftg5E69C3uX.pdf
最近读了这篇论文并尝试复现,并填了论文里面没提到的一个小坑,整理了一下算法论文和思路,并附上python代码,如果有错误希望各位大佬批评指正(目前只做了Algorithm1,Algorithm2寻找最外围轮廓没写)
一些重要定义图一 边界关系示例
1,轮廓点(border point):如果一个像素1在4-或者8-邻域找到一个像素为0的点,为一个轮廓点,如上图的B1,B2,B3,B4,其中阴影部分为1,白色部分为0
2,连通区域的环绕(surroundness among connected components),对于两个相邻的连通区域S1和S2,如果对于S1上任意一个点的4个方向,都能达到S2,那么S2b环绕S1
3,关于外轮廓(outer border)和孔轮廓(hole border),外轮廓是像素为1连通域内像素为0连通域环绕的轮廓点(如图一B4),孔轮廓是像素为0的连通区域被像素为1的连通区域环绕的轮廓点(如图一 B2)。
4,父轮廓(parent border),定义了层级关系,假设有像素为1的连通区域S1和像素为0的连通区域S2,并且S2环绕S1
(1)外轮廓S1的父轮廓为环绕S2的值为1的像素,如B3的父轮廓为B2
(2)如果S2是背景,父轮廓为frame 如B4父轮廓为frame
轮廓扫描
开始点(starting point):文章中扫描的方式为从左到右,从上到下的顺序,当找到是边界起始点的时候,根据下图判断轮廓类型。如图二,满足(a),(b)两种条件的分别为外轮廓和孔轮廓起始点图二 开始点
找到起始点后,根据上一个轮廓的编号(LNBD)判断父轮廓。看table1,如果当前的轮廓与LNBD代表的轮廓是同一个类型的轮廓,则当前轮廓的父轮廓是LNBD代表的轮廓的父轮廓。
最后进行border following找到该轮廓的所有点,参考APPENDIX1:
定义输入图片
,初始化NBD为1,LNBD为1.并且每一行扫描开始,LNBD重设为1
(1)情况一:如果
并且
,则(i,j)是外轮廓的起始点,NBD+1,
.
情况二:如果
并且
, 则(i,j)是孔轮廓的起始点,NBD+1,
.
其他情况跳到(4)
(2)基于轮廓种类决定父轮廓
(3.1)从
开始,以
为中心顺时针找到一个非零点为
,如果没有吧-NBD赋值给
,跳到步骤(4)
(3.2)
,
.
(3.3)从
开始,以
为中心逆时针找到一个非零点为
(3.4)根据
,即当前扫描到的pixel,改变
的值,如果
,则
, 如果
(可能为正或者负数)并且
,则
, 其他情况不改变值
(3.5)如果
代表回到了原点,跳到(4)。否则,
,
.
(4)如果
那么
, 从(i,j+1)开始继续扫描直到最右下角的像素
整个算法通俗来说就是不断更新当前点(i3,j3),然后绕着该点逆时针旋转找下一点并且不断更新像素值的过程,下面以文章中给的例子讲解
从图三看,第一次扫描到(a)中的打圈的1,根据(3.4),改变像素为2,然后逆时针寻找,发现到了左边边缘的2根据(3.4)应该是-2。这样下去结果不对啊!
后来想了一段时间,这里对像素左边和右边同时为0的情况,应该做特殊处理。因为轮廓是逆时针寻找,那么可以通过寻找的方位判断该赋值NBD还是-NBD,如果是从上往下扫的,则为NBD,如果是从下往上扫描的,则赋值-NBD。(具体实现可以参考代码)
修正后最后结果和文章一致了!有兴趣的朋友可以看下代码~
结果图,第一个index为轮廓编号,1为frame边缘,接着是son子轮廓,parent父轮廓,start_point轮廓开始的index,contour_type轮廓类型是否为孔结果图
# -*- coding: utf-8 -*-
"""Created on Wed May 27 15:01:45 2020@author: 73766"""
import matplotlib.pyplot as plt
import numpy as np
#import cv2
#class Contour:
# def __init__(self,parent,cur_num,contour_type):
# self.parent = parent
# self.contour_num = cur_num
# self.contour_type = contour_type #Hole/Outer
class FindContours:
def __init__(self):
self.grid = np.array([[1,1,1,1,1,1,1,0,0],
[1,0,0,1,0,0,1,0,1],
[1,0,0,1,0,0,1,0,0],
[1,1,1,1,1,1,1,0,0]])
self.reset()
def reset(self):
self.grid = np.pad(self.grid, ((1, 1), (1, 1)), 'constant', constant_values=0)
self.LNBD = 1
self.NBD = 1
self.Disp_with_number = True
self.MAX_BODER_NUMBER = self.grid.shape[0]*self.grid.shape[1]
self.contours_dict = {}
self.contours_dict[1] = self.Contour(-1,"Hole")
def Contour(self,parent,contour_type,start_point = [-1,-1]):
contour = {"parent":parent,
"contour_type":contour_type,
"son":[],
"start_point":start_point}#Hole/Outer
return contour
def load_map_from_array(self,grid):
self.grid = grid.copy().astype("int32")
self.reset()
def trans_number_to_char(self,num):
if self.Disp_with_number:
return str(num)
if num >1:
return chr(63 + num)
if num <0:
return chr(95 - num)
else:
return str(num)
'''display gridd '''
def disp_grid(self):
for i in range(self.grid.shape[0]):
num = '\033[0;37m' + '['
print(num,end = ' ')
for j in range(self.grid.shape[1]):
if self.grid[i][j] == 0:
num = '\033[0;37m' + self.trans_number_to_char(self.grid[i][j])
print(num,end = ' ')
else:
num = '\033[1;31m' + self.trans_number_to_char(self.grid[i][j])
print(num,end = ' ')
num = '\033[0;37m' + ']'
print(num)
print("\033[0;37m")
def find_neighbor(self,center,start,clock_wise = 1):
weight = -1
if clock_wise == 1:
weight = 1
#direction = np.array([[1,0],[0,-1],[0,-1],[-1,0],[-1,0],[0,1],[0,1]])
neighbors = np.array([[0,0],[0,1],[0,2],[1,2],[2,2],[2,1],[2,0],[1,0]])
indexs = np.array([[0,1,2],
[7,9,3],
[6,5,4]])
#print(center,start)
start_ind = indexs[start[0] - center[0]+1][start[1] - center[1]+1]
# print(start_ind)
for i in range(1,len(neighbors)+1):
cur_ind = (start_ind + i*weight+8)%8
#print(cur_ind)
x = neighbors[cur_ind][0] + center[0] - 1
y = neighbors[cur_ind][1] + center[1] - 1
# grid[x][y] = a
# a+=1
if self.grid[x][y] != 0:
return [x,y]
return [-1,-1]
def board_follow(self,center_p,start_p,mode):
ij = center_p
ij2 = start_p
ij1 = self.find_neighbor(ij,ij2,1)
x = ij1[0]
y = ij1[1]
if ij1 == [-1,-1]:
self.grid[ij[0]][ij[1]] = -self.NBD
return
ij2 = ij1
ij3 = ij
for k in range(self.MAX_BODER_NUMBER):
#step 3.3
ij4 = self.find_neighbor(ij3,ij2,0)
x = ij3[0]
y = ij3[1]
if ij4[0] - ij2[0] <=0:
weight = -1
else:
weight = 1
if self.grid[x][y] < 0:
self.grid[x][y] = self.grid[x][y]
elif self.grid[x][y-1] == 0 and self.grid[x][y+1] ==0:
self.grid[x][y] = self.NBD*weight
elif self.grid[x][y+1]== 0:
self.grid[x][y] = -self.NBD
elif self.grid[x][y]== 1 and self.grid[x][y+1] != 0:
self.grid[x][y] = self.NBD
else:
self.grid[x][y] = self.grid[x][y]
if ij4 == ij and ij3 ==ij1:
return
ij2 = ij3
ij3 = ij4
def raster_scan(self):
#self.disp_grid()
for i in range(self.grid.shape[0]):
self.LNBD = 1
for j in range(self.grid.shape[1]):
if abs(self.grid[i][j]) > 1:
self.LNBD = abs(self.grid[i][j])
if self.grid[i][j] >= 1:
if self.grid[i][j] == 1 and self.grid[i][j-1] == 0:
self.NBD += 1
self.board_follow([i,j],[i,j-1],1)
border_type = "Outer"
elif self.grid[i][j] > 1 and self.grid[i][j+1] == 0:
border_type = "Hole"
#print(i,j)
self.NBD += 1
self.board_follow([i,j],[i,j+1],1)
#self.contours_dict[self.NBD] = self.Contour(self.LNBD,border_type)
#self.disp_grid()
else:
continue
parent = self.LNBD
if self.contours_dict[self.LNBD]["contour_type"] == border_type:
parent = self.contours_dict[self.LNBD]["parent"]
self.contours_dict[self.NBD] = self.Contour(parent,border_type,[i-1,j-1])
self.contours_dict[parent]["son"].append(self.NBD)
#print("NBD",self.NBD,"LNBD",self.LNBD)
self.grid = self.grid[1:-1,1:-1]
def main():
fc = FindContours()
fc.raster_scan()
fc.disp_grid()
print(fc.contours_dict)
grid1 = np.array([[0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,1,0,0,0,0,0,0,0,0,0],
[0,0,1,1,1,1,1,1,1,0,0,0,0],
[0,0,1,0,0,1,0,0,0,1,1,0,0],
[0,0,1,0,0,1,0,0,1,0,0,0,0],
[0,0,1,0,0,1,0,0,1,0,0,0,0],
[0,0,1,1,1,1,1,1,1,0,0,0,0],
[0,0,0,1,0,0,1,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0],])
fc.load_map_from_array(grid1)
fc.raster_scan()
fc.disp_grid()
print(fc.contours_dict)
#
# img1 = cv2.imread("D:\\datas\\luoxuan1.png")
# img = np.mean(np.float32(img1), axis=2)
# img[img<130] = 0
# img[img>0] = 1
# img = 1-img
#
# fc.load_map_from_array(img)
# fc.raster_scan()
# ret =abs(fc.grid)
# ret[ret<2] = 0
# ret[ret>0] = 1
# plt.figure()
# plt.imshow(img,"gray") # 显示图片
# plt.axis('off') # 不显示坐标轴
# plt.show()
# plt.figure()
# plt.imshow(ret,"gray") # 显示图片
# plt.axis('off') # 不显示坐标轴
# plt.show()
if __name__ == "__main__":
main()
欢迎大家关注我的专栏,也欢迎大家投稿,我们可以一起研究openCV的原理,分享知识共同进步!OpenCV算法原理理解和numpy实现zhuanlan.zhihu.com