首页 > 技术文章 > 区域填充算法

iamfatotaku 2020-03-15 12:36 原文

一、区域填充概念

区域:指已经表示成点阵形式的填充图形,是象素的集合

区域填充:将区域内的一点(常称种子点)赋予给定颜色,然后将这种颜色扩展到整个区域内的过程。

区域填充算法要求区域是连通的,因为只有在连通区域中,才可能将种子点的颜色扩展到区域内的其它点。

1、区域有两种表示形式

img

  1. 内点表示
    • 枚举出区域内部的所有象素
    • 内部所有象素着同一个颜色
    • 边界像素着与内部象素不同的颜色。
  2. 边界表示
    • 枚举出区域外部的所有象素
    • 边界上的所有象素着同一个颜色
    • 内部像素着与边界象素不同的颜色。

2、区域连通

img

  1. 四向连通区域:从区域上一点出发可通过上、下、左、右四个方向移动的组合,在不越出区域的前提下,到达区域内的任意象素
  2. 八向连通区域:从区域上一点出发可通过上、下、左、右、左上、右上、左下、右下八个方向移动的组合,在不越出区域的前提下,到达区域内的任意象素
  3. 四连通与八连通区域的区别
    • 连通性:四连通可以看作八连通的自己,但是对边界有要求

二、简单种子填充算法

1、基本思想

给定区域G一种子点(x, y),首先判断该点是否是区域内的一点,如果是,则将该点填充为新的颜色,然后将该点周围的四个点(四连通)或八个点(八连通)作为新的种子点进行同样的处理,通过这种扩散完成对整个区域的填充。

这里给出一个四连通的种子填充算法(区域填充递归算法),使用【栈结构】来实现
原理算法原理如下:种子像素入栈,当【栈非空】时重复如下三步:

img

2、算法代码

这里给出八连通的种子填充算法的代码:

void flood_fill_8(int[] pixels, int x, int y, int old_color, int new_color) {
	if (x < w && x > 0 && y < h && y > 0) {
        // 如果是旧的颜色而且还没有给他填充过
		if (pixels[y * w + x] == old_color) {
            // 填充为新的颜色
			pixels[y * w + x]== new_color);
            // 递归
			flood_fill_8(pixels, x, y + 1, old_color, new_color);
			flood_fill_8(pixels, x, y - 1, old_color, new_color);
			flood_fill_8(pixels, x - 1, y, old_color, new_color);
			flood_fill_8(pixels, x + 1, y, old_color, new_color);
			flood_fill_8(pixels, x + 1, y + 1, old_color, new_color);
			flood_fill_8(pixels, x + 1, y - 1, old_color, new_color);
			flood_fill_8(pixels, x - 1, y + 1, old_color, new_color);
			flood_fill_8(pixels, x - 1, y - 1, old_color, new_color);
		}
	}
}

3、OpenCV实现

import cv2


def seed_fill(img):
    ret, img = cv2.threshold(img, 128, 255, cv2.THRESH_BINARY_INV)
    label = 100
    stack_list = []
    h, w = img.shape
    for i in range(1, h-1, 1):
        for j in range(1, w-1, 1):
            if (img[i][j] == 255):
                img[i][j] = label
                stack_list.append((i, j))
                while len(stack_list) != 0:
                    cur_i = stack_list[-1][0]
                    cur_j = stack_list[-1][1]
                    img[cur_i][cur_j] = label
                    stack_list.remove(stack_list[-1])
                    # 四邻域,可改为八邻域
                    if (img[cur_i-1][cur_j] == 255):
                        stack_list.append((cur_i-1, cur_j))
                    if (img[cur_i][cur_j-1] == 255):
                        stack_list.append((cur_i, cur_j-1))
                    if (img[cur_i+1][cur_j] == 255):
                        stack_list.append((cur_i+1, cur_j))
                    if (img[cur_i][cur_j+1] == 255):
                        stack_list.append((cur_i, cur_j+1))
    cv2.imwrite('./result.jpg', img)
    cv2.imshow('img', img)
    cv2.waitKey()


if __name__ == '__main__':
    img = cv2.imread('./test.jpeg', 0)
    seed_fill(img)

4、简单种子填充算法的优点和缺点

优点:

  1. 该算法也可以填充有孔区域

缺点:

  1. 有些像素会多次入栈,降低算法效率,栈结构占空间
  2. 递归执行,算法简单,但效率不高,区域内每一像素都要进/出栈,费时费内存
  3. 改进算法,减少递归次数,提高效率

三、扫描线种子填充算法

  • 目标:减少递归层次
  • 适用于边界表示的4连通区间

1、基本思想

任意不间断区间中只取一个种子像素(不间断区间指在一条扫描线上一组相邻元素),填充当前扫描线上的该段区间;然后确定与这一区段相邻的上下两条扫描线上位于区域内的区段,并依次把它们保存起来,反复进行这个过程,直到所保存的各个区段都填充完毕。

2、算法步骤

  1. 初始化:将算法设置的堆栈置为空。将给定的种子点\((x, y)\)压入堆栈
  2. 出栈:如果堆栈为空,算法结束;否则取栈顶元素\((x, y)\)作为种子点
  3. 区段填充:从种子点\((x, y)\)开始,沿纵坐标为y的当前扫描线向左右两个方向逐个像素用新的颜色值进行填充,直到边界为止即象素颜色等于边界色。设区间两边界的横坐标分别为xleftxright
  4. 在与当前扫描线相邻的上下两条扫描线上,以区间[xleft, xright]为搜索范围,求出需要填充的各小区间,把各小区间中最右边的点并作为种子点压入堆栈,转到步骤2。

3、算法的关键原则

img

  1. 搜索原则:

    从前一个填充的区间(边界之间的范围xleft, xright)作为后一条扫描线种子点寻找的范围。

  2. 填充原则:

    从种子点往左,右填,填到边界

4、实例

上述算法的描述过于抽象,直接看演示

img

5、算法

Stack stack = new Stack(); //堆栈 pixel_stack初始化
Stack.push(point);       //(x,y)是给定的种子像素
while (!stack.empty()) {
	p = (Point)(stack.pop()); //出栈,从堆栈中取一像素作种子像素
	x = p.x;
	y = p.y;
	savex = x; //保存种子点的横坐标x的值
	while (pixels[y * w + x] != boundary_color) {
		pixels[y * w + x] = new_color;
		x++;
	}             //从种子像素开始向右填充到边界
	xright = x–1; //保存线段的右端点
	x = savex–1;  //设定种子点往左填充的起点
	while (pixels[y * w + x] != boundary_color) {
		pixels[y * w + x] = new_color;
		x = x–1;
	}
	//从种子像素开始向左填充到边界,以上两步完成区间填充。
	xleft = x + 1;     //保存线段的左端点,加1是因为前面 循环时多减一次
	        x = xleft;  //起点是上次的左端点
	y = y + 1;          //开始处理上一条扫描线
	while (x <= xright) { //在上一条扫描线上检查是否需要填充
		span_need_fill = false; //先设定为不需要填充
		while (pixels[y * w + x] == old_color && x <= xright) {
			//待填充的线段
			span_need_fill = true; //发现有旧象素,需要填充
			x = x + 1;
		} //待填充的线段处理完,即遇到边界色,!=old_color跳出

		if (span_need_fill) { //如果区间需要填充,则将其右端点作为种子点压进堆栈
			p = new Point(x - 1, y);
			stack.push(p); //进栈
			span_need_fill = false;
		}
		//继续向右检查以防有遗漏
		while (pixels[y * w + x] != old_color && x <= xright)
			x = x + 1;
	} //在上一条扫描线上检查完
	x=xleft;
	y = y–2; //形成下一条扫描线的y值
	//在下一条扫描线上从左向右检查位于区间[xleft,xright]上的像素,其方法与在上一条扫描线上检查的情况完全一样,见书。
} //出栈完

6、OpenCV实现

# coding:utf8
import cv2
import numpy as np

frame = cv2.cvtColor(cv2.imread(
    'fr_seg_open22_result.pgm'), cv2.COLOR_BGR2GRAY)
ret, frame = cv2.threshold(frame, 127, 255, cv2.THRESH_BINARY)
contour_index = 1


def scan_line_seed(i, j):
    stack = []
    return_value = 0
    if frame[i, j] == 255:
        stack.append((i, j))
        while len(stack) != 0:
            seed = stack.pop()
            x, y = seed
            while frame[x, y] == 255:
                frame[x, y] = contour_index * 10
                x += 1
            x_right = x-1
            x, y = seed
            x -= 1
            while frame[x, y] == 255:
                frame[x, y] = contour_index * 10
                x -= 1
            x_left = x + 1
            # 如果左右端点为空则加入种子点
            if frame[x_left, y - 1] == 255:
                stack.append((x_left, y-1))
            if frame[x_left, y + 1] == 255:
                stack.append((x_left, y+1))
            if frame[x_right, y - 1] == 255:
                stack.append((x_right, y-1))
            if frame[x_right, y + 1] == 255:
                stack.append((x_right, y+1))
            for x in range(x_left, x_right+1):
                # 上左
                if frame[x, y-1] == 255 and frame[x-1, y-1] != 255:
                    stack.append((x, y-1))
                # 下左
                if frame[x, y+1] == 255 and frame[x-1, y+1] != 255:
                    stack.append((x, y+1))
                # 上右
                if frame[x, y-1] != 255 and frame[x-1, y-1] == 255:
                    stack.append((x-1, y-1))
                # 下右
                if frame[x, y+1] != 255 and frame[x-1, y+1] == 255:
                    stack.append((x-1, y+1))

        return_value = 1
    return return_value


for x in range(frame.shape[0]):
    for y in range(frame.shape[1]):
        if scan_line_seed(x, y) == 1:
            contour_index = contour_index + 1
            print contour_index
cv2.namedWindow('frame', cv2.WINDOW_NORMAL)
cv2.imshow('frame', frame)
cv2.imwrite('numbered_intel.pgm', frame)
cv2.waitKey(0)

推荐阅读