OpenCV与图像处理学习九——连通区域分析算法(含代码)
一、连通区域概要
连通区域(Connected Component)一般是指图像中具有相同像素值且位置相邻的前景像素点组成的图像区域,连通区域分析是指将图像中的各个连通区域找出并标记。连通区域分析是一种在CV和图像分析处理的众多应用领域中较为常用和基本的方法。
例如: OCR识别中字符分割提取(车牌识别、文本识别、字幕识别等)、视觉跟踪中的运动前景
目标分割与提取(行人入侵检测、遗留物体检测、基于视觉的车辆检测与跟踪等)、医学图像处
理(感兴趣目标区域提取)等。
在需要将前景目标提取出来以便后续进行处理的应用场景中都能够用到连通区域分析
方法,通常连通区域分析处理的对象是一张二值化后的图像。
在图像中,最小的单位是像素,每个像素周围有邻接像素,常见的邻接关系有2种: 4邻接与8邻接,如下图所示:
如果A与B连通, B与C连通,则A与C连通,在视觉上看来,彼此连通的点形成了一个区域,而不连通的点形成了不同的区域。这样的一个所有的点彼此连通点构成的集合,我们称为一个连通区域。
我们来看一下下面这个二值化的图:
对于每一个前景像素,只要它的邻域中有像素也是前景,那么它们就属于一个连通区域,在这张图中,如果使用四邻域的规则,那么将可以分成三个连通区域,而使用八邻域的规则,则可以分成两个连通区域。
二、Two-Pass算法
基于上述概念,我们再来学习连通区域分析算法中常用的Two-Pass算法 (两遍扫描法),正如其名,指的就是通过扫描两遍图像,将图像中存在的所有连通域找出并标记。
第一次扫描:
- 从左上角开始遍历像素点,找到第一个像素为255(因为是二值图,只有0和255)的点,令该像素的label=1;
- 当该像素的左邻像素和上邻像素为无效值时,给该像素置一个新的label值, label ++,记录集合;
- 当该像素的左邻像素或者上邻像素有一个为有效值时,将有效值像素的label赋给该像素的label值;
- 当该像素的左邻像素和上邻像素都为有效值时,选取其中较小的label值赋给该像素的label值。
若一张二值图如下所示(蓝点为前景点):
则第一遍扫描之后记录的label如下所示:
通过领域像素得到label或成为领域像素得到label依据的像素点都将记录为一个集合,只有毫无联系的才会记录为不同的集合,如上图所示,前景点被分为了两个集合(橙色标记)。
第二次扫描:
- 对每个点的label进行更新,更新为其对于其集合中最小的label
则就可以得到两个连通区域:
三、代码实现
import cv2
import numpy as np
# 4邻域的连通域和 8邻域的连通域
# [row, col]
NEIGHBOR_HOODS_4 = True
OFFSETS_4 = [[0, -1], [-1, 0], [0, 0], [1, 0], [0, 1]]
NEIGHBOR_HOODS_8 = False
OFFSETS_8 = [[-1, -1], [0, -1], [1, -1],
[-1, 0], [0, 0], [1, 0],
[-1, 1], [0, 1], [1, 1]]
#第二遍扫描
def reorganize(binary_img: np.array):
index_map = []
points = []
index = -1
rows, cols = binary_img.shape
for row in range(rows):
for col in range(cols):
var = binary_img[row][col]
if var < 0.5:
continue
if var in index_map:
index = index_map.index(var)
num = index + 1
else:
index = len(index_map)
num = index + 1
index_map.append(var)
points.append([])
binary_img[row][col] = num
points[index].append([row, col])
#print(binary_img)
#print(points)
return binary_img, points
#四领域或八领域判断
def neighbor_value(binary_img: np.array, offsets, reverse=False):
rows, cols = binary_img.shape
label_idx = 0
rows_ = [0, rows, 1] if reverse == False else [rows-1, -1, -1]
cols_ = [0, cols, 1] if reverse == False else [cols-1, -1, -1]
for row in range(rows_[0], rows_[1], rows_[2]):
for col in range(cols_[0], cols_[1], cols_[2]):
label = 256
if binary_img[row][col] < 0.5:
continue
for offset in offsets:
neighbor_row = min(max(0, row+offset[0]), rows-1)
neighbor_col = min(max(0, col+offset[1]), cols-1)
neighbor_val = binary_img[neighbor_row, neighbor_col]
if neighbor_val < 0.5:
continue
label = neighbor_val if neighbor_val < label else label
if label == 255:
label_idx += 1
label = label_idx
binary_img[row][col] = label
print('第一遍扫描:',binary_img)
print('开始第二遍...')
return binary_img
# binary_img: bg-0, object-255; int
#第一遍扫描
def Two_Pass(binary_img: np.array, neighbor_hoods):
if neighbor_hoods == NEIGHBOR_HOODS_4:
offsets = OFFSETS_4
elif neighbor_hoods == NEIGHBOR_HOODS_8:
offsets = OFFSETS_8
else:
raise ValueError
binary_img = neighbor_value(binary_img, offsets, False)
return binary_img
if __name__ == "__main__":
#创建四行七列的矩阵
binary_img = np.zeros((4, 7), dtype=np.int16)
#指定点设置为255
index = [[0, 2], [0, 5],
[1, 0], [1, 1], [1, 2], [1, 4], [1, 5], [1, 6],
[2, 2], [2, 5],
[3, 1], [3, 2], [3, 4],[3,5], [3, 6]]
for i in index:
binary_img[i[0], i[1]] = np.int16(255)
print("原始二值图像")
print(binary_img)
#print("Two_Pass")
#调用Two Pass算法,计算第一遍扫面的结果
binary_img = Two_Pass(binary_img, NEIGHBOR_HOODS_4)
#print(binary_img)
#计算第一遍扫面的结果
binary_img, points = reorganize(binary_img)
print(binary_img)
#print(points)
结果如下所示:
原始二值图像
[[ 0 0 255 0 0 255 0]
[255 255 255 0 255 255 255]
[ 0 0 255 0 0 255 0]
[ 0 255 255 0 255 255 255]]
第一遍扫描: [[0 0 1 0 0 2 0]
[3 3 1 0 4 2 2]
[0 0 1 0 0 2 0]
[0 5 1 0 6 2 2]]
开始第二遍...
[[0 0 1 0 0 2 0]
[3 3 1 0 4 2 2]
[0 0 1 0 0 2 0]
[0 5 1 0 6 2 2]]