【问题标题】:How to get a floodfilled area and its borders in an image with Python OpenCV?如何使用 Python OpenCV 在图像中获得泛滥区域及其边界?
【发布时间】:2020-01-06 01:58:30
【问题描述】:

我有一张像这样的图片,只有黑白的:

我只想使用cv2.floodfill 获取图像带边框的被淹没区域,就像这样(请原谅我的绘画技巧):

这是我当前的代码:

    # 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)

我得到的输出等于原始图像。我怎样才能只获得带边界的洪水区域?

编辑:我想从“竞技场”内的任何 white 点进行填充,例如图像中的红点 (X,Y)。我希望只有竞技场内小圆圈的外边界和外墙的内边界。

EDIT2:我已经完成了一半:

# 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)

我明白了:

但是,我觉得这是获取边界的错误方式,并且它们是不完整的。

【问题讨论】:

  • 我不明白你正在填充什么区域并要提取。坐标 260,260 在白色的右下角区域。你是充满白色的洪水。所以我不期望有任何改变。请澄清。
  • 您想要删除这些点还是只保留这些点?
  • @fmw42。我在代码中调整了图像的大小,260,260 旨在成为中心圆附近的一个白点。我已经用我想开始的一个点的例子更新了图像。我只想提取填充区域,而不是填充填充的图像。
  • @MH304,我只想保留点的边框。

标签: python opencv


【解决方案1】:

我认为最简单、最快的方法是用中灰色填充竞技场。然后只提取灰色像素并找到它们的边缘。看起来像这样,但请记住,超过一半的行是 cmets 和调试语句 :-)

#!/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)

以下是调试输出的 3 个步骤,显示处理顺序:

result-1.png 看起来像这样:

result-2.png 看起来像这样:

result-3.png 看起来像这样:


顺便说一句,您不必编写任何 Python 代码来执行此操作,因为您可以使用大多数 Linux 发行版中包含的 ImageMagick 在终端中执行此操作,并且可用于macOS 和 Windows。这里使用的方法和我上面Python中使用的方法一模一样:

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

【讨论】:

  • 您好!谢谢您的回答。效果很好!感谢您花时间下载和编辑图像。我将其标记为正确答案,因为它使用 OpenCV,并且比我的答案更简单。
  • 酷 - 祝你的项目好运!请记住,问题(和答案)是免费的,所以如果您遇到困难,请回来询问更多问题:-)
【解决方案2】:

膨胀和异或怎么样

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

这只会给你边框,我不清楚你是只想要圆形边框还是内部边框,你应该能够根据大小删除你不想要的边框。


你可以用大小阈值去掉外边框,定义这样的函数:

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)

将 500 替换为大于您想要保留的边框且小于您想要丢失的边框的数字。

【讨论】:

  • 嗨!我运行了您的代码(必须修复“dilation”和“dilared”),虽然它有边框 (imgur.com/a/tDbDaw1),但它给了我 all 边框。我只想要图像“竞技场”内的那些。
  • 根据答案中指示的大小删除您不想要的边框,我会更新删除
  • 您好!你的修复工作。我给你+1,因为你的最终结果是我想要的,但方法不同,我相信它不适用于我需要处理的其他图像。我在下面回答了我的解决方案。谢谢!
【解决方案3】:

我必须创建自己的 Flood Fill 实现来获得我想要的。我基于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

我唯一做的就是添加else 条件。如果该点的值不等于orig_valuefill_value,那么它就是一个边框,所以我将它附加到一个包含所有边框点的列表中。然后我只画边框。

我能够使用此代码获得以下图像:

    # 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)

右边是我的目标。谢谢大家!

【讨论】:

    猜你喜欢
    • 2015-11-21
    • 2021-10-09
    • 2014-04-26
    • 2016-06-23
    • 2015-08-15
    • 2012-02-23
    • 2019-01-31
    • 2021-09-27
    • 2020-09-01
    相关资源
    最近更新 更多