josealeixo.pc
josealeixo.pc

Reputation: 391

How to get a floodfilled area and its borders in an image with Python OpenCV?

I have an image such as this one, which is only black and white:

Image of a Maze

I would like to obtain only the flooded area of the image with the border using cv2.floodfill, like so (pardon my Paint skills):

enter image description here

Here's my current code:

    # Copy the image.
    im_floodfill = cv2.resize(actual_map_image, (500, 500)).copy()

    # Floodfill from point (X, Y)
    cv2.floodFill(im_floodfill, None, (X, Y), (255, 255, 255))

    # Display images.
    cv2.imshow("Floodfilled Image", im_floodfill)
    cv2.waitKey(0)

The output I get is equal to the original image. How can I get only the flooded area with borders?

EDIT: I want to floodfill from any white point inside the "arena", like the red dot (X,Y) in the image. I wish to have only the outer border of the small circles inside the arena and the inner border of the outside walls.

EDIT2: I'm halfway there with this:

# Resize for test purposes 
    actual_map_image = cv2.resize(actual_map_image, (1000, 1000))
    actual_map_image = cv2.cvtColor(actual_map_image, cv2.COLOR_BGR2GRAY)

    h, w = actual_map_image.shape[:2]
    flood_mask = np.zeros((h+2, w+2), dtype=np.uint8)
    connectivity = 8
    flood_fill_flags = (connectivity | cv2.FLOODFILL_FIXED_RANGE | cv2.FLOODFILL_MASK_ONLY | 255 << 8) 

    # Copy the image.
    im_floodfill = actual_map_image.copy()

    # Floodfill from point inside arena, not inside a black dot
    cv2.floodFill(im_floodfill, flood_mask, (h/2 + 20, w/2 + 20), 255, None, None, flood_fill_flags)

    borders = []
    for i in range(len(actual_map_image)):
        borders.append([B-A for A,B in zip(actual_map_image[i], flood_mask[i])])

    borders = np.asarray(borders)
    borders = cv2.bitwise_not(borders)

    # Display images.
    cv2.imshow("Original Image", cv2.resize(actual_map_image, (500, 500)))
    cv2.imshow("Floodfilled Image", cv2.resize(flood_mask, (500, 500)))
    cv2.imshow("Borders", cv2.resize(borders, (500, 500)))

    cv2.waitKey(0)

I get this:

enter image description here

However, I feel like this is the wrong way of getting the borders, and they are incomplete.

Upvotes: 3

Views: 4357

Answers (3)

Mark Setchell
Mark Setchell

Reputation: 207455

I think the easiest, and fastest, way to do this is to flood-fill the arena with mid-grey. Then extract just the grey pixels and find their edges. That looks like this, but bear in mind more than half the lines are comments and debug statements :-)

#!/usr/bin/env python3

import cv2

# Load image as greyscale to use 1/3 of the memory and processing time
im = cv2.imread('arena.png', cv2.IMREAD_GRAYSCALE)

# Floodfill arena area with value 128, i.e. mid-grey
floodval = 128
cv2.floodFill(im, None, (150,370), floodval)
# DEBUG cv2.imwrite('result-1.png', im)

# Extract filled area alone
arena = ((im==floodval) * 255).astype(np.uint8)
# DEBUG cv2.imwrite('result-2.png', arena)

# Find edges and save
edges = cv2.Canny(arena,100,200)
# DEBUG cv2.imwrite('result-3.png',edges)

Here are the 3 steps of debug output showing you the sequence of processing:

result-1.png looks like this:

enter image description here

result-2.png looks like this:

enter image description here

result-3.png looks like this:

enter image description here


By the way, you don't have to write any Python code to do this, as you can just do it in the Terminal with ImageMagick which is included in most Linux distros and is available for macOS and Windows. The method used here corresponds exactly to the method I used in Python above:

magick arena.png -colorspace gray               \
   -fill gray -draw "color 370,150 floodfill"   \
   -fill white +opaque gray -canny 0x1+10%+30% result.png

Upvotes: 5

josealeixo.pc
josealeixo.pc

Reputation: 391

I had to create my own Flood Fill implementation to get what I wanted. I based myself on this one.

def fill(data, start_coords, fill_value, border_value, connectivity=8):
    """
    Flood fill algorithm

    Parameters
    ----------
    data : (M, N) ndarray of uint8 type
        Image with flood to be filled. Modified inplace.
    start_coords : tuple
        Length-2 tuple of ints defining (row, col) start coordinates.
    fill_value : int
        Value the flooded area will take after the fill.
    border_value: int
        Value of the color to paint the borders of the filled area with.
    connectivity: 4 or 8
        Connectivity which we use for the flood fill algorithm (4-way or 8-way).

    Returns
    -------
    filled_data: ndarray
        The data with the filled area.
    borders: ndarray
        The borders of the filled area painted with border_value color.
    """
    assert connectivity in [4,8]

    filled_data = data.copy()

    xsize, ysize = filled_data.shape
    orig_value = filled_data[start_coords[0], start_coords[1]]

    stack = set(((start_coords[0], start_coords[1]),))
    if fill_value == orig_value:
        raise ValueError("Filling region with same value already present is unsupported. Did you already fill this region?")

    border_points = []

    while stack:
        x, y = stack.pop()

        if filled_data[x, y] == orig_value:
            filled_data[x, y] = fill_value
            if x > 0:
                stack.add((x - 1, y))
            if x < (xsize - 1):
                stack.add((x + 1, y))
            if y > 0:
                stack.add((x, y - 1))
            if y < (ysize - 1):
                stack.add((x, y + 1))

            if connectivity == 8:
                if x > 0 and y > 0:
                    stack.add((x - 1, y - 1))
                if x > 0 and y < (ysize - 1):
                    stack.add((x - 1, y + 1))
                if x < (xsize - 1) and y > 0:
                    stack.add((x + 1, y - 1))
                if x < (xsize - 1) and y < (ysize - 1):
                    stack.add((x + 1, y + 1))
        else:
            if filled_data[x, y] != fill_value:
                border_points.append([x,y])

    # Fill all image with white
    borders = filled_data.copy()
    borders.fill(255)

    # Paint borders
    for x,y in border_points:
        borders[x, y] = border_value

    return filled_data, borders

The only thing I did was adding the else condition. If the point does not have a value equal to orig_value or fill_value, then it is a border, so I append it to a list that contains the points of all borders. Then I only paint the borders.

I was able to get the following images with this code:

    # Resize for test purposes 
    actual_map_image = cv2.resize(actual_map_image, (500, 500))
    actual_map_image = cv2.cvtColor(actual_map_image, cv2.COLOR_BGR2GRAY)

    h, w = actual_map_image.shape[:2]

    filled_data, borders = fill(actual_map_image, [h/2 + 20, w/2 + 20], 127, 0, connectivity=8)

    cv2.imshow("Original Image", actual_map_image)
    cv2.imshow("Filled Image", filled_data)
    cv2.imshow("Borders", borders)

enter image description here

The one on the right was what I was aiming for. Thank you all!

Upvotes: 0

shortcipher3
shortcipher3

Reputation: 1380

How about dilating and xor

kernel = np.ones((3,3), np.uint8)
dilated = cv2.dilate(actual_map_image, kernel, iterations = 1)
borders = cv2.bitwise_xor(dilated, actual_map_image)

That will give you only the borders, I'm not clear if you want the circle borders only or also the interior borders, you should be able to remove borders you don't want based on size.


You can remove the exterior border with a size threshold, define a function like this:

def size_threshold(bw, minimum, maximum):
    retval, labels, stats, centroids = cv.connectedComponentsWithStats(bw)
    for val in np.where((stats[:, 4] < minimum) + (stats[:, 4] > maximum))[0]:
      labels[labels==val] = 0
    return (labels > 0).astype(np.uint8) * 255

result = size_threshold(borders, 0, 500)

Replace 500 with the a number larger than borders you want to keep and smaller than the border you want to lose.

Upvotes: 1

Related Questions