【问题标题】:Removing blank space around a circle shaped mask删除圆形蒙版周围的空白区域
【发布时间】:2020-10-19 02:54:57
【问题描述】:

我有一个圆形蒙版的图像,它本质上是黑色图像中的彩色圆圈。

我想删除蒙版周围的所有空白区域,使图像的边界与圆圈对齐:

我编写了一个脚本来执行此操作,方法是搜索每一列和每一行,直到出现一个值大于 0 的像素。从左到右、从右到左、从上到下、从下到上搜索得到蒙版边界,允许我裁剪原始图像。代码如下:

ROWS, COLS, _ = img.shape

BORDER_RIGHT = (0,0)
BORDER_LEFT = (0,0)

right_found = False
left_found = False 

# find borders of blank space for removal.
# left and right border
print('Searching for Right and Left corners')
for col in tqdm(range(COLS), position=0, leave=True):
    for row in range(ROWS):
        if left_found and right_found:
            break
        
        # searching from left to right 
        if not left_found and N.sum(img[row][col]) > 0:
            BORDER_LEFT = (row, col)
            left_found = True
        
        # searching from right to left 
        if not right_found and N.sum(img[row][-col]) > 0:
            BORDER_RIGHT = (row, img.shape[1] + (-col))
            right_found = True

BORDER_TOP = (0,0)
BORDER_BOTTOM = (0,0)

top_found = False
bottom_found = False             

# top and bottom borders 
print('Searching for Top and Bottom corners')
for row in tqdm(range(ROWS), position=0, leave=True):
    for col in range(COLS):
        if top_found and bottom_found:
            break
        
        # searching top to bottom 
        if not top_found and N.sum(img[row][col]) > 0:
            BORDER_TOP = (row, col)
            top_found = True
        
        # searching bottom to top
        if not bottom_found and N.sum(img[-row][col]) > 0:
            BORDER_BOTTOM = (img.shape[0] + (-row), col)
            bottom_found = True

# crop left and right borders 
new_img = img[:,BORDER_LEFT[1]: BORDER_RIGHT[1] ,:]

# crop top and bottom borders 
new_img = new_img[BORDER_TOP[0] : BORDER_BOTTOM[0],:,:]

我想知道是否有更有效的方法来做到这一点。对于较大的图像,这可能会非常耗时,尤其是在蒙版相对于原始图像形状相对较小的情况下。谢谢!

【问题讨论】:

    标签: python opencv image-processing


    【解决方案1】:

    假设图像中只有这个对象,有两种方法可以做到这一点:

    1. 您可以对图像进行阈值处理,然后使用numpy.where 查找所有非零位置,然后在numpy.where 出来的相应行和列位置上使用numpy.minnumpy.max 给您边界矩形。
    2. cv2.findContours阈值后可以先找到物体的轮廓点。这应该会产生一个轮廓,因此一旦有了这些点,就可以通过cv2.boundingRect 来返回矩形的左上角,然后是其范围的宽度和高度。

    如果只有一个对象,第一种方法将有效并且有效。如果有多个对象,则第二个将起作用,但是您必须知道感兴趣的对象在哪个轮廓中,然后您只需索引到cv2.findContours 的输出并将其通过cv2.boundingRect 进行管道传输以获取矩形尺寸感兴趣的对象。

    但是,要点是,这两种方法中的任何一种都比您提出的手动循环每一行和每一列并计算总和的方法高效得多。


    预处理

    这组步骤对这两种方法都是通用的。总之,我们读入图像,然后将其转换为灰度然后阈值。我无权访问您的原始图像,因此我从 Stack Overflow 中读取并裁剪它以使轴不显示。这也适用于第二种方法。

    这是我拍摄快照的重建图像。

    首先,我将直接从互联网上读取图像,并导入完成工作所需的相关包:

    import skimage.io as io
    import numpy as np
    import cv2
    
    img = io.imread('https://i.stack.imgur.com/dj1a8.png')
    

    谢天谢地,Scikit image 有一个直接从 Internet 读取图像的方法:skimage.io.imread

    之后,我要将图像转换为灰度,然后对其进行阈值处理:

    img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
    im = img_gray > 40
    

    我使用 OpenCV 的 cv2.cvtColor 将图像从彩色转换为灰度。之后,我对图像进行阈值处理,以便将任何高于 40 的强度设置为 True,其他所有设置为 False。我通过反复试验选择了阈值 40,直到我得到一个看起来是圆形的面具。看看我们得到的这张图片:


    方法#1

    如上所示,在阈值图像上使用numpy.where,然后使用numpy.minnumpy.max 找到合适的左上角和右下角并裁剪图像:

    (r, c) = np.where(im == 1)
    min_row, min_col = np.min(r), np.min(c)
    max_row, max_col = np.max(r), np.max(c)
    im_crop = img[min_row:max_row+1, min_col:max_col+1]
    

    numpy.where 对于二维数组将返回非零的行和列位置的元组。如果我们找到最小的行和列位置,则对应于边界矩形的左上角。类似地,最大行和列位置对应于边界矩形的右下角。好的是 numpy.minnumpy.max 以矢量化方式工作,这意味着它在一次扫描中对整个 NumPy 数组进行操作。上面使用了这个逻辑,然后我们对原始彩色图像进行索引,以裁剪出包含感兴趣对象的行和列的范围。 im_crop 包含该结果。请注意,当我们索引时,最大行和列需要添加 1,因为带有结束索引的切片是排他的,因此添加 1 可以确保我们包含矩形右下角的像素位置。

    因此我们得到:

    方法#2

    我们将使用cv2.findContours 来查找图像中所有对象的所有轮廓点。因为只有一个对象,所以应该只有一个轮廓,所以我们使用这个轮廓来管道到cv2.boundingRect 以找到对象边界矩形的左上角,结合它的宽度和高度来裁剪图像。

    cnt, _ = cv2.findContours(im.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    x, y, w, h = cv2.boundingRect(cnt[0])
    im_crop = img[y:y+h, x:x+w]
    

    请注意,我们必须将阈值图像转换为无符号 8 位整数,因为这是函数所期望的类型。此外,我们使用cv2.RETR_EXTERNAL,因为我们只想检索我们在图像中看到的任何对象的外周坐标。我们还使用cv2.CHAIN_APPROX_NONE 返回对象上每个可能的轮廓点。 cnt 是在图像中找到的轮廓列表。这个列表的大小应该只有 1,所以我们直接对它进行索引并将其通过管道传输到 cv2.boundingRect。然后我们使用矩形的左上角,结合它的宽度和高度来裁剪对象。

    因此我们得到:


    完整代码

    这是从头到尾的完整代码清单。我在下面留下了 cmets 来描述方法 #1 和 #2 是什么。目前,方法 #2 已被注释掉,但您可以通过简单地注释和取消注释相关代码来决定要使用哪个。

    import skimage.io as io
    import cv2
    import numpy as np
    
    img = io.imread('https://i.stack.imgur.com/dj1a8.png')
    
    img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
    im = img_gray > 40
    
    # Method #1
    (r, c) = np.where(im == 1)
    min_row, min_col = np.min(r), np.min(c)
    max_row, max_col = np.max(r), np.max(c)
    im_crop = img[min_row:max_row+1, min_col:max_col+1]
    
    # Method #2
    #cnt, _ = cv2.findContours(im.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    #x, y, w, h = cv2.boundingRect(cnt[0])
    #im_crop = img[y:y+h, x:x+w]
    

    【讨论】: