Flood Fill Algorithm

Sample Problem: Connected Fields

Farmer John's fields are broken into fields, with paths between some of them. Unfortunately, some fields are not reachable from other fields via the paths.

Define a superfield is a collection of fields that are all reachable from each other. Calculate the number of superfields.

The Abstraction

Given: a undirected graph

The component of a graph is a maximal-sized (though not necessarily maximum) subgraph which is connected.

Calculate the component of the graph.
 


This graph has three components: {1,4,8}, {2,5,6,7,9}, and {3}.

 

The Algorithm: Flood Fill

Flood fill can be performed three basic ways: depth-first, breadth-first, and breadth-first scanning. The basic idea is to find some node which has not been assigned to a component and to calculate the component which contains. The question is how to calculate the component.

In the depth-first formulation, the algorithm looks at each step through all of the neighbors of the current node, and, for those that have not been assigned to a component yet, assigns them to this component and recurses on them.

In the breadth-first formulation, instead of recursing on the newly assigned nodes, they are added to a queue.

In the breadth-first scanning formulation, every node has two values: component and visited. When calculating the component, the algorithm goes through all of the nodes that have been assigned to that component but not visited yet, and assigns their neighbors to the current component.

The depth-first formulation is the easiest to code and debug, but can require a stack as big as the original graph. For explicit graphs, this is not so bad, but for implicit graphs, such as the problem presented has, the numbers of nodes can be very large.

The breadth-formulation does a little better, as the queue is much more efficient than the run-time stack is, but can still run into the same problem. Both the depth-first and breadth-first formulations run in N + M time, where N is the number of vertices and M is the number of edges.

The breadth-first scanning formulation, however, requires very little extra space. In fact, being a little tricky, it requires no extra space. However, it is slower, requiring up to N*N + M time, where N is the number of vertices in the graph.

Pseudocode for Breadth-First Scanning

This code uses a trick to not use extra space, marking nodes to be visited as in component -2 and actually assigning them to the current component when they are actually visited.

component(i) denotes the
component that node is in
 function flood_fill(new_component) 

 do
   num_visited 0
   for all nodes i
     if component(i) -2
       num_visited num_visited 1
       component(i) new_component
       for all neighbors of node i
         if component(j) nil
10           component(j) -2
11 until num_visited 

12 function find_components 

13  num_components 0
14  for all nodes i
15    component(node i) nil
16  for all nodes i
17    if component(node i) is nil
18      num_components =
                 num_components 1
19      component(i) -2
20      flood_fill(component
                        num_components)

Running time of this algorithm is O(N 2), where N is the numbers of nodes. Every edge is traversed twice (once for each end-point), and each node is only marked once.

Execution Example

Consider the graph from above.


 

The algorithm starts with all nodes assigned to no component.

Going through the nodes in order first node not assigned to any component yet is vertex 1. Start a new component (component 1) for that node, and set the component of node 1 to -2 (any nodes not shown are unassigned).

Node Component
1 -2

Now, in the flood_fill code, the first time through the do loop, it finds the node 1 is assigned to component -2. Thus, it reassigns it to component 1, signifying that it has been visited, and then assigns its neighbors (node 4) to component -2.

Node Component
1 1
4 -2

As the loop through all the nodes continues, it finds that node 4 is also assigned to component -2, and processes it appropriately as well.

Node Component
1 1
4 1
8 -2

Node 8 is the next to be processed.

Node Component
1 1
4 1
8 1

Now, the for loop continues, and finds no more nodes that have not been assigned yet. Since the until clause is not satisfied ( num_visited = 3), it tries again. This time, no nodes are found, so the function exits and component 1 is complete.

The search for unassigned nodes continues, finding node 2. A new component (component 2) is allocated, node 2 is marked as in component -2, and flood_fill is called.

Node Component
1 1
2 -2
4 1
8 1

Node 2 is found as marked in component -2, and is processed.

Node Component
1 1
2 2
4 1
7 -2
8 1
9 -2

Next, node 7 is processed.

Node Component
1 1
2 2
4 1
5 -2
7 2
8 1
9 -2

Then node 9 is processed.

Node Component
1 1
2 2
4 1
5 -2
7 2
8 1
9 2

The terminating condition does not hold ( num_visited = 3), so the search through for nodes assigned to component -2 starts again. Node 5 is the first one found.

Node Component
1 1
2 2
4 1
5 2
6 -2
7 2
8 1
9 2

Node 6 is the next node found to be in component -2.

Node Component
1 1
2 2
4 1
5 2
6 2
7 2
8 1
9 2

No more nodes are found assigned to component -2, but the terminating condition does not hold, so one more pass through the nodes is performed, finding no nodes assigned to component -2. Thus, the search for unassigned nodes continue from node 2, finding node 3 unassigned.

Node Component
1 1
2 2
3 -2
4 1
5 2
6 2
7 2
8 1
9 2

Node 3 is processed.

Node Component
1 1
2 2
3 3
4 1
5 2
6 2
7 2
8 1
9 2

From here, the algorithm eventually terminates, as there are no more nodes assigned to component -2 and no unassigned nodes. The three components of the graph have been determined, along with the component to which each node belongs.

**********************************************************************************

+ From wikipedia

Flood fill, also called seed fill, is an algorithm that determines the area connected to a given node in a multi-dimensional array. It is used in the "bucket" fill tool of paint programs to determine which parts of a bitmap to fill with color.

+ the algorithms

The flood fill algorithm takes three parameters: a start node, a target color, and a replacement color. The algorithm looks for all nodes in the array which are connected to the start node by a path of the target color, and changes them to the replacement color. There are many ways in which the flood-fill algorithm can be structured, but they all make use of a queue or stack data structure, explicitly or implicitly.

从上面的算法介绍可知,凡是会搜索的同学就能很轻易地掌握floodfill。因为floodfill算法从大体而言可以细分为两种算法思想,一种是DFS,一种是BFS。以下讲介绍两大种常用的算法,并简单分析其中用到的dfs和bfs。

1. per-pixel fill (点点填充)

 

                                             

recursive flood-fill with 4 directions       recursive flood-fill with 8 directions

这两个有一个小区别,就是8方向的算法是在4方向的算法的基础上添加了四个方向(左上、左下、右上、右下),因此造成的结果是8方向的算法有可能会“leak through sloped edges of 1 pixel thick”。至于其它方面则是完全一样,下面所有的算法都是基于4方向的。

dfs的搜索思想(递归)写的代码:

Flood-fill (node, target-color, replacement-color):
1. If the color of node is not equal to target-color, return.
2. Set the color of node to replacement-color.
3. Perform Flood-fill (one step to the west of node, target-color, replacement-color).
    Perform Flood-fill (one step to the east of node, target-color, replacement-color).
    Perform Flood-fill (one step to the north of node, target-color, replacement-color).
    Perform Flood-fill (one step to the south of node, target-color, replacement-color).
4. Return.

bfs的搜索思想(队列)写的代码:

Flood-fill (node, target-color, replacement-color):
1. Set Q to the empty queue.
2. If the color of node is not equal to target-color, return.
3. Add node to the end of Q.
4. While "Q" is not empty:
5.     Set "n" equal to the first element of "Q"
6.     If the color of n is equal to target-color, set the color of n to replacement-color.
7.     Remove first element from "Q"
8.     If the color of the node to the west of n is target-color, set the color of that node to replacement-color, add that node to the end of Q.
9.     If the color of the node to the east of n is target-color, set the color of that node to replacement-color, add that node to the end of Q.
10.    If the color of the node to the north of n is target-color, set the color of that node to replacement-color, add that node to the end of Q.
11.    If the color of the node to the south of n is target-color, set the color of that node to replacement-color, add that node to the end of Q.
12. Return.

以上两种小算法的问题在于,如果填充的面积较大的话,程序很容易爆掉。究其原因,就是搜索的深度过大或队列的长度不够造成的。因此为了减少搜索深度或进队列的元素个数,可以用线方式代替点方式。而这样做还有一个好处就是填充速度的提高。

2. scanline fill (扫描线填充)

 

//stack friendly and fast floodfill algorithm(递归深搜的写法)

void floodFillScanline(int x, int y, int newColor, int oldColor)
{
    if(oldColor == newColor) return;
    if(screenBuffer[x][y] != oldColor) return;
     
    int y1;
   
    //draw current scanline from start position to the top
    y1 = y;
    while(y1 < h && screenBuffer[x][y1] == oldColor)
    {
        screenBuffer[x][y1] = newColor;
        y1++;
    }   
   
    //draw current scanline from start position to the bottom
    y1 = y - 1;
    while(y1 >= 0 && screenBuffer[x][y1] == oldColor)
    {
        screenBuffer[x][y1] = newColor;
        y1--;
    }
   
    //test for new scanlines to the left
    y1 = y;
    while(y1 < h && screenBuffer[x][y1] == newColor)
    {
        if(x > 0 && screenBuffer[x - 1][y1] == oldColor)
        {
            floodFillScanline(x - 1, y1, newColor, oldColor);
        }
        y1++;
    }
    y1 = y - 1;
    while(y1 >= 0 && screenBuffer[x][y1] == newColor)
    {
        if(x > 0 && screenBuffer[x - 1][y1] == oldColor)
        {
            floodFillScanline(x - 1, y1, newColor, oldColor);
        }
        y1--;
    }
   
    //test for new scanlines to the right
    y1 = y;
    while(y1 < h && screenBuffer[x][y1] == newColor)
    {
        if(x < w - 1 && screenBuffer[x + 1][y1] == oldColor)
        {          
            floodFillScanline(x + 1, y1, newColor, oldColor);
        }
        y1++;
    }
    y1 = y - 1;
    while(y1 >= 0 && screenBuffer[x][y1] == newColor)
    {
        if(x < w - 1 && screenBuffer[x + 1][y1] == oldColor)
        {
            floodFillScanline(x + 1, y1, newColor, oldColor);
        }
        y1--;
    }
}

//The scanline floodfill algorithm using our own stack routines, faster(广搜队列的写法)

void floodFillScanlineStack(int x, int y, int newColor, int oldColor)
{
    if(oldColor == newColor) return;
    emptyStack();
   
    int y1;
    bool spanLeft, spanRight;
   
    if(!push(x, y)) return;
   
    while(pop(x, y))
    {   
        y1 = y;
        while(y1 >= 0 && screenBuffer[x][y1] == oldColor) y1--;
        y1++;
        spanLeft = spanRight = 0;
        while(y1 < h && screenBuffer[x][y1] == oldColor )
        {
            screenBuffer[x][y1] = newColor;
            if(!spanLeft && x > 0 && screenBuffer[x - 1][y1] == oldColor)
            {
                if(!push(x - 1, y1)) return;
                spanLeft = 1;
            }
            else if(spanLeft && x > 0 && screenBuffer[x - 1][y1] != oldColor)
            {
                spanLeft = 0;
            }//写这一部分是防止因为同一列上有断开的段而造成的可能的“没填充”
            if(!spanRight && x < w - 1 && screenBuffer[x + 1][y1] == oldColor)
            {
                if(!push(x + 1, y1)) return;
                spanRight = 1;
            }
            else if(spanRight && x < w - 1 && screenBuffer[x + 1][y1] != oldColor)
            {
                spanRight = 0;
            } //写这一部分是防止因为同一列上有断开的段而造成的可能的“没填充”
            y1++;
        }
    }
}

以上两个小算法填充的方向都是纵向填充,你也可以修改成横向填充。

 

Posted on 2009-04-17 08:43  leivo  阅读(1653)  评论(0编辑  收藏  举报