Recently I’ve had need to develop an algorithm which can find the boundary of a 1-bit per pixel monochrome blob. (In my case, each pixel had a color quality which could be converted to a single-bit ‘true’/’false’ test, rather than a literal monochrome image, but essentially the problem set is the same.)

In this post I intend to describe the algorithm I used.

Given a 2D monochrome image with pixels aligned in an X/Y grid:

We’d like to find the “boundary” of this blob. For now I’ll ignore the problem of what the “boundary” is, but suffice to say that with the boundary we can discover which pixels are inside the blob which border the exterior of the blob, which pixels on the outside border the pixels inside the blob, or to find other attributes of the blob such as a rectangle which bounds the blob.

If we treat each pixel as a discrete 1×1 square rather than a dimensionless point, then I intend to define the boundary as the set of (x,y) coordinates which define the borders around each of those squares, with the upper-left corner of the square representing the coordinate of the point:

So, in this case, I intend to define the boundary as the set of (x,y) coordinates which define the border between the black and the white pixels:

We do this by walking the edge of the blob through a set of very simple rules which effectively walk the boundary of the blob in a clock-wise fashion. This ‘blob walking’ only requires local knowledge at the point we are currently examining.

Because we are walking the blob in a clock-wise fashion, it is easy to find the first point in a blob we are interested in walking: through iteratively searching all of the pixels from upper left to lower right:

(Algorithm 1: Find first point) -- Given: maxx the maximum width of the pixel image maxy the maximum height of the pixel image Returns the found pixel for an eligable blob or 'PixelNotFound'. for (int x = 0; x < maxx; ++x) { for (int y = 0; y < maxy; ++y) { if (IsPixelSet(x,y)) { return Pixel(x,y); } } } return PixelNotFound;

Once we have found our elegable pixel, we can start walking counter clockwise, tracking each of the coordinates we have found as we walk around the perimeter of the blob, traversing either horizontally or vertically.

Given the way we’ve encountered our first pixel in the algorithm above, the pixels around the immediate location at (x,y) looks like the following:

That’s because the way we iterated through, the first pixel we encountered at (x,y) implies that (x-1,y), (x,y-1) and (x-1,y-1) must be clear. Also, if we are to progress in a clock-wise fashion, clearly we should move our current location from (x,y) to (x+1,y):

The design of our algorithm proceeds in a similar fashion: by examining each of the 16 possible pixel configurations we can find in the surrounding 4 pixels, and tracking the one of 4 possible incoming paths we take, we can construct a matrix of directions in which we should take to continue progressing in a clockwise fashion around our eligible blob. And in all but two configuration cases, there was only one possible incoming path we could have taken to get to the center point, since as we presume we are following the edge of the blob we could not have entered between two black pixels or between two white pixels. Some combinations are also illegal since we presume we are walking around the blob in a clock-wise fashion rather than in a counter-clockwise fashion. (This means that we should be, when standing at the point location, pixels should be on the right and never on the left. There is a proof for this which I will not sketch here.)

The 16 possible configurations and the outgoing paths we can take are illustrated below:

Along the top of this table shows the four possible incoming directions: from the left, from the top, from the right and from the bottom. Each of the 16 possible pixel combinations are shown from top to bottom, and blanks indicate where an incoming path was illegal–either because it comes between two blacks or two whites, or because the path would have placed the black pixel on the left of the incoming line rather than on the right.

Note that with only two exceptions each possible combination of pixels produces only one valid outgoing path. Those two exceptions we arbitrarily pick an outgoing of two possible paths which excludes the diagonal pixel; we could have easily gone the other way and included the diagonal, but this may have had the property of including blobs with holes. (If this is acceptable or not depends on how you are using the algorithm.)

This indicates that we could easily construct a switch statement, converting each possible row into an integer from 0 to 15:

Algorithm 2: converting to a pixel value. -- Given: IsPixel(x,y) returns true if the pixel is set and false if it is not set or if the pixel is out of the range from (0,maxx), (0,maxy) Return an integer value from 0 to 15 indicating the pixel combination int GetPixelState(int x,y int y) { int ret = 0; if IsPixel(x-1,y-1) ret |= 1; if IsPixel(x,y-1) ret |= 2; if IsPixel(x-1,y) ret |= 4; if IsPixel(x,y) ret |= 8; return ret;

}

We now can build our switch statement:

Algorithm 3: Getting the next pixel location -- Given: the algorithm above to get the current pixel state, the current (x,y) location, the incoming directiondir, one of LEFT, UP, RIGHT, DOWN Returns the outgoing direction LEFT, UP, RIGHT, DOWN or ILLEGAL if the state was illegal. Note: we don't test the incoming path when there was only one choice. We could, by adding some complexity to this algorithm, for testing purposes. The values below are obtained from examining the table above. int state = GetPixelState(x,y); switch (state) { case 0: return ILLEGAL; case 1: return LEFT; case 2: return UP; case 3: return LEFT; case 4: return DOWN; case 5: return DOWN; case 6: { if (dir == RIGHT) return DOWN; if (dir == LEFT) return UP; return ILLEGAL; } case 7: return DOWN; case 8: return RIGHT; case 9: { if (dir == DOWN) return LEFT; if (dir == UP) return RIGHT; return ILLEGAL; } case 10: return UP; case 11: return LEFT; case 12: return RIGHT; case 13: return RIGHT; case 14: return UP; case 15: return ILLEGAL; }

From all of this we can now easily construct an algorithm which traces the outline of a blob. First, we use Algorithm 1 to find an eligible point. We then set ‘dir’ to UP, since the pixel state we discovered was pixel state 8, and the only legal direction had we been tracing around the blob was UP.

We store away the starting position (x,y), and then we iterate through algorithm 3, getting new directions and updating (x’,y’), traversing (x+1,y) for RIGHT, (x,y+1) for UP, (x-1,y) for LEFT and (x,y-1) for DOWN, until we arrive at our starting point.

As we traverse the parameter, we could either build an array of found (x,y) values, or we could do something else–such as maintaining a bounding rectangle, bumping the boundaries as we find (x,y) values that are outside the rectangle’s boundaries.